commit 7e8cddf2085575519ecc16eed6b3710f61acc96f Author: BlubbFish Date: Mon Aug 3 22:31:27 2026 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..728f711 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.vs +ImageSharp/bin +ImageSharp/obj +ImageSharp.Drawing/bin +ImageSharp.Drawing/obj +PolygonClipper/bin +PolygonClipper/obj +SixLabors.Fonts/bin +SixLabors.Fonts/obj \ No newline at end of file diff --git a/DebugGuard.cs b/DebugGuard.cs new file mode 100644 index 0000000..dfd4b5a --- /dev/null +++ b/DebugGuard.cs @@ -0,0 +1,281 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace SixLabors { + /// + /// Provides methods to protect against invalid parameters for a DEBUG build. + /// + [DebuggerStepThrough] +#pragma warning disable RCS1043 // Remove 'partial' modifier from type with a single part. + internal static partial class DebugGuard +#pragma warning restore RCS1043 // Remove 'partial' modifier from type with a single part. + { + /// + /// Ensures that the value is not null. + /// + /// The target object, which cannot be null. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// is null. + [Conditional("DEBUG")] + public static void NotNull([NotNull] TValue? value, [CallerArgumentExpression("value")] string? parameterName = null) + where TValue : class => + ArgumentNullException.ThrowIfNull(value, parameterName); + + /// + /// Ensures that the target value is not null, empty, or whitespace. + /// + /// The target string, which should be checked against being null or empty. + /// Name of the parameter. + /// is null. + /// is empty or contains only blanks. + [Conditional("DEBUG")] + public static void NotNullOrWhiteSpace([NotNull] string? value, [CallerArgumentExpression("value")] string? paramName = null) + { + ArgumentNullException.ThrowIfNull(value); + + if (string.IsNullOrWhiteSpace(value)) + { + ThrowArgumentException("Must not be empty or whitespace.", paramName!); + } + } + + /// + /// Ensures that the specified value is less than a maximum value. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// + /// is greater than the maximum value. + /// + [Conditional("DEBUG")] + public static void MustBeLessThan(TValue value, TValue max, string parameterName) + where TValue : IComparable + { + if (value.CompareTo(max) >= 0) + { + ThrowArgumentOutOfRangeException(parameterName, $"Value {value} must be less than {max}."); + } + } + + /// + /// Verifies that the specified value is less than or equal to a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// + /// is greater than the maximum value. + /// + [Conditional("DEBUG")] + public static void MustBeLessThanOrEqualTo(TValue value, TValue max, string parameterName) + where TValue : IComparable + { + if (value.CompareTo(max) > 0) + { + ThrowArgumentOutOfRangeException(parameterName, $"Value {value} must be less than or equal to {max}."); + } + } + + /// + /// Verifies that the specified value is greater than a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// + /// is less than the minimum value. + /// + [Conditional("DEBUG")] + public static void MustBeGreaterThan(TValue value, TValue min, string parameterName) + where TValue : IComparable + { + if (value.CompareTo(min) <= 0) + { + ThrowArgumentOutOfRangeException( + parameterName, + $"Value {value} must be greater than {min}."); + } + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// + /// is less than the minimum value. + /// + [Conditional("DEBUG")] + public static void MustBeGreaterThanOrEqualTo(TValue value, TValue min, string parameterName) + where TValue : IComparable + { + if (value.CompareTo(min) < 0) + { + ThrowArgumentOutOfRangeException(parameterName, $"Value {value} must be greater than or equal to {min}."); + } + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value and less than + /// or equal to a maximum value and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// + /// is less than the minimum value of greater than the maximum value. + /// + [Conditional("DEBUG")] + public static void MustBeBetweenOrEqualTo(TValue value, TValue min, TValue max, string parameterName) + where TValue : IComparable + { + if (value.CompareTo(min) < 0 || value.CompareTo(max) > 0) + { + ThrowArgumentOutOfRangeException( + parameterName, + $"Value {value} must be greater than or equal to {min} and less than or equal to {max}."); + } + } + + /// + /// Verifies, that the method parameter with specified target value is true + /// and throws an exception if it is found to be so. + /// + /// The target value, which cannot be false. + /// The name of the parameter that is to be checked. + /// The error message, if any to add to the exception. + /// + /// is false. + /// + [Conditional("DEBUG")] + public static void IsTrue(bool target, string parameterName, string message) + { + if (!target) + { + ThrowArgumentException(message, parameterName); + } + } + + /// + /// Verifies, that the method parameter with specified target value is false + /// and throws an exception if it is found to be so. + /// + /// The target value, which cannot be true. + /// The name of the parameter that is to be checked. + /// The error message, if any to add to the exception. + /// + /// is true. + /// + [Conditional("DEBUG")] + public static void IsFalse(bool target, string parameterName, string message) + { + if (target) + { + ThrowArgumentException(message, parameterName); + } + } + + /// + /// Verifies, that the `source` span has the length of 'minLength', or longer. + /// + /// The element type of the spans. + /// The source span. + /// The minimum length. + /// The name of the parameter that is to be checked. + /// + /// has less than items. + /// + [Conditional("DEBUG")] + public static void MustBeSizedAtLeast(ReadOnlySpan source, int minLength, string parameterName) + { + if (source.Length < minLength) + { + ThrowArgumentException($"Span-s must be at least of length {minLength}!", parameterName); + } + } + + /// + /// Verifies, that the `source` span has the length of 'minLength', or longer. + /// + /// The element type of the spans. + /// The target span. + /// The minimum length. + /// The name of the parameter that is to be checked. + /// + /// has less than items. + /// + [Conditional("DEBUG")] + public static void MustBeSizedAtLeast(Span source, int minLength, string parameterName) + { + if (source.Length < minLength) + { + ThrowArgumentException($"The size must be at least {minLength}.", parameterName); + } + } + + /// + /// Verifies that the 'destination' span is not shorter than 'source'. + /// + /// The source element type. + /// The destination element type. + /// The source span. + /// The destination span. + /// The name of the argument for 'destination'. + [Conditional("DEBUG")] + public static void DestinationShouldNotBeTooShort( + ReadOnlySpan source, + Span destination, + string destinationParamName) + { + if (destination.Length < source.Length) + { + ThrowArgumentException($"Destination span is too short!", destinationParamName); + } + } + + /// + /// Verifies that the 'destination' span is not shorter than 'source'. + /// + /// The source element type. + /// The destination element type. + /// The source span. + /// The destination span. + /// The name of the argument for 'destination'. + [Conditional("DEBUG")] + public static void DestinationShouldNotBeTooShort( + Span source, + Span destination, + string destinationParamName) + { + if (destination.Length < source.Length) + { + ThrowArgumentException($"Destination span is too short!", destinationParamName); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentException(string message, string parameterName) => + throw new ArgumentException(message, parameterName); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentOutOfRangeException(string parameterName, string message) => + throw new ArgumentOutOfRangeException(parameterName, message); + } +} diff --git a/Guard.Numeric.cs b/Guard.Numeric.cs new file mode 100644 index 0000000..a9053a7 --- /dev/null +++ b/Guard.Numeric.cs @@ -0,0 +1,1272 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors { + /// + /// Provides methods to protect against invalid parameters. + /// + internal static partial class Guard + { + /// + /// Ensures that the specified value is less than a maximum value. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThan(byte value, byte max, string parameterName) + { + if (value < max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThan(value, max, parameterName); + } + + /// + /// Verifies that the specified value is less than or equal to a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThanOrEqualTo(byte value, byte max, string parameterName) + { + if (value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThanOrEqualTo(value, max, parameterName); + } + + /// + /// Verifies that the specified value is greater than a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThan(byte value, byte min, string parameterName) + { + if (value > min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThan(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThanOrEqualTo(byte value, byte min, string parameterName) + { + if (value >= min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThanOrEqualTo(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value and less than + /// or equal to a maximum value and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value of greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeBetweenOrEqualTo(byte value, byte min, byte max, string parameterName) + { + if (value >= min && value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeBetweenOrEqualTo(value, min, max, parameterName); + } + + /// + /// Ensures that the specified value is less than a maximum value. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThan(sbyte value, sbyte max, string parameterName) + { + if (value < max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThan(value, max, parameterName); + } + + /// + /// Verifies that the specified value is less than or equal to a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThanOrEqualTo(sbyte value, sbyte max, string parameterName) + { + if (value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThanOrEqualTo(value, max, parameterName); + } + + /// + /// Verifies that the specified value is greater than a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThan(sbyte value, sbyte min, string parameterName) + { + if (value > min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThan(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThanOrEqualTo(sbyte value, sbyte min, string parameterName) + { + if (value >= min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThanOrEqualTo(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value and less than + /// or equal to a maximum value and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value of greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeBetweenOrEqualTo(sbyte value, sbyte min, sbyte max, string parameterName) + { + if (value >= min && value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeBetweenOrEqualTo(value, min, max, parameterName); + } + + /// + /// Ensures that the specified value is less than a maximum value. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThan(short value, short max, string parameterName) + { + if (value < max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThan(value, max, parameterName); + } + + /// + /// Verifies that the specified value is less than or equal to a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThanOrEqualTo(short value, short max, string parameterName) + { + if (value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThanOrEqualTo(value, max, parameterName); + } + + /// + /// Verifies that the specified value is greater than a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThan(short value, short min, string parameterName) + { + if (value > min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThan(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThanOrEqualTo(short value, short min, string parameterName) + { + if (value >= min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThanOrEqualTo(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value and less than + /// or equal to a maximum value and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value of greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeBetweenOrEqualTo(short value, short min, short max, string parameterName) + { + if (value >= min && value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeBetweenOrEqualTo(value, min, max, parameterName); + } + + /// + /// Ensures that the specified value is less than a maximum value. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThan(ushort value, ushort max, string parameterName) + { + if (value < max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThan(value, max, parameterName); + } + + /// + /// Verifies that the specified value is less than or equal to a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThanOrEqualTo(ushort value, ushort max, string parameterName) + { + if (value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThanOrEqualTo(value, max, parameterName); + } + + /// + /// Verifies that the specified value is greater than a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThan(ushort value, ushort min, string parameterName) + { + if (value > min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThan(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThanOrEqualTo(ushort value, ushort min, string parameterName) + { + if (value >= min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThanOrEqualTo(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value and less than + /// or equal to a maximum value and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value of greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeBetweenOrEqualTo(ushort value, ushort min, ushort max, string parameterName) + { + if (value >= min && value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeBetweenOrEqualTo(value, min, max, parameterName); + } + + /// + /// Ensures that the specified value is less than a maximum value. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThan(char value, char max, string parameterName) + { + if (value < max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThan(value, max, parameterName); + } + + /// + /// Verifies that the specified value is less than or equal to a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThanOrEqualTo(char value, char max, string parameterName) + { + if (value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThanOrEqualTo(value, max, parameterName); + } + + /// + /// Verifies that the specified value is greater than a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThan(char value, char min, string parameterName) + { + if (value > min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThan(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThanOrEqualTo(char value, char min, string parameterName) + { + if (value >= min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThanOrEqualTo(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value and less than + /// or equal to a maximum value and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value of greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeBetweenOrEqualTo(char value, char min, char max, string parameterName) + { + if (value >= min && value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeBetweenOrEqualTo(value, min, max, parameterName); + } + + /// + /// Ensures that the specified value is less than a maximum value. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThan(int value, int max, string parameterName) + { + if (value < max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThan(value, max, parameterName); + } + + /// + /// Verifies that the specified value is less than or equal to a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThanOrEqualTo(int value, int max, string parameterName) + { + if (value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThanOrEqualTo(value, max, parameterName); + } + + /// + /// Verifies that the specified value is greater than a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThan(int value, int min, string parameterName) + { + if (value > min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThan(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThanOrEqualTo(int value, int min, string parameterName) + { + if (value >= min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThanOrEqualTo(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value and less than + /// or equal to a maximum value and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value of greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeBetweenOrEqualTo(int value, int min, int max, string parameterName) + { + if (value >= min && value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeBetweenOrEqualTo(value, min, max, parameterName); + } + + /// + /// Ensures that the specified value is less than a maximum value. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThan(uint value, uint max, string parameterName) + { + if (value < max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThan(value, max, parameterName); + } + + /// + /// Verifies that the specified value is less than or equal to a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThanOrEqualTo(uint value, uint max, string parameterName) + { + if (value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThanOrEqualTo(value, max, parameterName); + } + + /// + /// Verifies that the specified value is greater than a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThan(uint value, uint min, string parameterName) + { + if (value > min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThan(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThanOrEqualTo(uint value, uint min, string parameterName) + { + if (value >= min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThanOrEqualTo(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value and less than + /// or equal to a maximum value and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value of greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeBetweenOrEqualTo(uint value, uint min, uint max, string parameterName) + { + if (value >= min && value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeBetweenOrEqualTo(value, min, max, parameterName); + } + + /// + /// Ensures that the specified value is less than a maximum value. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThan(float value, float max, string parameterName) + { + if (value < max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThan(value, max, parameterName); + } + + /// + /// Verifies that the specified value is less than or equal to a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThanOrEqualTo(float value, float max, string parameterName) + { + if (value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThanOrEqualTo(value, max, parameterName); + } + + /// + /// Verifies that the specified value is greater than a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThan(float value, float min, string parameterName) + { + if (value > min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThan(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThanOrEqualTo(float value, float min, string parameterName) + { + if (value >= min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThanOrEqualTo(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value and less than + /// or equal to a maximum value and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value of greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeBetweenOrEqualTo(float value, float min, float max, string parameterName) + { + if (value >= min && value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeBetweenOrEqualTo(value, min, max, parameterName); + } + + /// + /// Ensures that the specified value is less than a maximum value. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThan(long value, long max, string parameterName) + { + if (value < max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThan(value, max, parameterName); + } + + /// + /// Verifies that the specified value is less than or equal to a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThanOrEqualTo(long value, long max, string parameterName) + { + if (value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThanOrEqualTo(value, max, parameterName); + } + + /// + /// Verifies that the specified value is greater than a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThan(long value, long min, string parameterName) + { + if (value > min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThan(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThanOrEqualTo(long value, long min, string parameterName) + { + if (value >= min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThanOrEqualTo(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value and less than + /// or equal to a maximum value and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value of greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeBetweenOrEqualTo(long value, long min, long max, string parameterName) + { + if (value >= min && value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeBetweenOrEqualTo(value, min, max, parameterName); + } + + /// + /// Ensures that the specified value is less than a maximum value. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThan(ulong value, ulong max, string parameterName) + { + if (value < max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThan(value, max, parameterName); + } + + /// + /// Verifies that the specified value is less than or equal to a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThanOrEqualTo(ulong value, ulong max, string parameterName) + { + if (value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThanOrEqualTo(value, max, parameterName); + } + + /// + /// Verifies that the specified value is greater than a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThan(ulong value, ulong min, string parameterName) + { + if (value > min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThan(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThanOrEqualTo(ulong value, ulong min, string parameterName) + { + if (value >= min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThanOrEqualTo(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value and less than + /// or equal to a maximum value and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value of greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeBetweenOrEqualTo(ulong value, ulong min, ulong max, string parameterName) + { + if (value >= min && value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeBetweenOrEqualTo(value, min, max, parameterName); + } + + /// + /// Ensures that the specified value is less than a maximum value. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThan(double value, double max, string parameterName) + { + if (value < max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThan(value, max, parameterName); + } + + /// + /// Verifies that the specified value is less than or equal to a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThanOrEqualTo(double value, double max, string parameterName) + { + if (value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThanOrEqualTo(value, max, parameterName); + } + + /// + /// Verifies that the specified value is greater than a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThan(double value, double min, string parameterName) + { + if (value > min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThan(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThanOrEqualTo(double value, double min, string parameterName) + { + if (value >= min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThanOrEqualTo(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value and less than + /// or equal to a maximum value and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value of greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeBetweenOrEqualTo(double value, double min, double max, string parameterName) + { + if (value >= min && value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeBetweenOrEqualTo(value, min, max, parameterName); + } + + /// + /// Ensures that the specified value is less than a maximum value. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThan(decimal value, decimal max, string parameterName) + { + if (value < max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThan(value, max, parameterName); + } + + /// + /// Verifies that the specified value is less than or equal to a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThanOrEqualTo(decimal value, decimal max, string parameterName) + { + if (value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThanOrEqualTo(value, max, parameterName); + } + + /// + /// Verifies that the specified value is greater than a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThan(decimal value, decimal min, string parameterName) + { + if (value > min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThan(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThanOrEqualTo(decimal value, decimal min, string parameterName) + { + if (value >= min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThanOrEqualTo(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value and less than + /// or equal to a maximum value and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value of greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeBetweenOrEqualTo(decimal value, decimal min, decimal max, string parameterName) + { + if (value >= min && value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeBetweenOrEqualTo(value, min, max, parameterName); + } + } +} diff --git a/Guard.Numeric.tt.bak b/Guard.Numeric.tt.bak new file mode 100644 index 0000000..ba6b168 --- /dev/null +++ b/Guard.Numeric.tt.bak @@ -0,0 +1,132 @@ +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ output extension=".cs" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors + +/// +/// Provides methods to protect against invalid parameters. +/// +internal static partial class Guard +{ +<# +var types = new[] { "byte", "sbyte", "short", "ushort", "char", "int", "uint", "float", "long", "ulong", "double", "decimal" }; + +for (var i = 0; i < types.Length; i++) +{ +if (i > 0) WriteLine(""); + +var T = types[i]; +#> + /// + /// Ensures that the specified value is less than a maximum value. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThan(<#=T#> value, <#=T#> max, string parameterName) + { + if (value < max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThan(value, max, parameterName); + } + + /// + /// Verifies that the specified value is less than or equal to a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThanOrEqualTo(<#=T#> value, <#=T#> max, string parameterName) + { + if (value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThanOrEqualTo(value, max, parameterName); + } + + /// + /// Verifies that the specified value is greater than a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThan(<#=T#> value, <#=T#> min, string parameterName) + { + if (value > min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThan(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThanOrEqualTo(<#=T#> value, <#=T#> min, string parameterName) + { + if (value >= min) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThanOrEqualTo(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value and less than + /// or equal to a maximum value and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// + /// is less than the minimum value of greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeBetweenOrEqualTo(<#=T#> value, <#=T#> min, <#=T#> max, string parameterName) + { + if (value >= min && value <= max) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeBetweenOrEqualTo(value, min, max, parameterName); + } +<# +} +#> +} diff --git a/Guard.cs b/Guard.cs new file mode 100644 index 0000000..894bc5f --- /dev/null +++ b/Guard.cs @@ -0,0 +1,289 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace SixLabors { + /// + /// Provides methods to protect against invalid parameters. + /// + [DebuggerStepThrough] + internal static partial class Guard + { + /// + /// Ensures that the value is not null. + /// + /// The target object, which cannot be null. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NotNull([NotNull]TValue? value, [CallerArgumentExpression("value")] string? parameterName = null) + where TValue : class => + ArgumentNullException.ThrowIfNull(value, parameterName); + + /// + /// Ensures that the target value is not null, empty, or whitespace. + /// + /// The target string, which should be checked against being null or empty. + /// Name of the parameter. + /// is null. + /// is empty or contains only blanks. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NotNullOrWhiteSpace([NotNull]string? value, string parameterName) + { + if (!string.IsNullOrWhiteSpace(value)) + { + return; + } + + ThrowHelper.ThrowArgumentExceptionForNotNullOrWhitespace(value, parameterName); + } + + /// + /// Ensures that the specified value is less than a maximum value. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThan(TValue value, TValue max, string parameterName) + where TValue : IComparable + { + if (value.CompareTo(max) < 0) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThan(value, max, parameterName); + } + + /// + /// Verifies that the specified value is less than or equal to a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// + /// is greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeLessThanOrEqualTo(TValue value, TValue max, string parameterName) + where TValue : IComparable + { + if (value.CompareTo(max) <= 0) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThanOrEqualTo(value, max, parameterName); + } + + /// + /// Verifies that the specified value is greater than a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThan(TValue value, TValue min, string parameterName) + where TValue : IComparable + { + if (value.CompareTo(min) > 0) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThan(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// + /// is less than the minimum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeGreaterThanOrEqualTo(TValue value, TValue min, string parameterName) + where TValue : IComparable + { + if (value.CompareTo(min) >= 0) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThanOrEqualTo(value, min, parameterName); + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value and less than + /// or equal to a maximum value and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// + /// is less than the minimum value of greater than the maximum value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeBetweenOrEqualTo(TValue value, TValue min, TValue max, string parameterName) + where TValue : IComparable + { + if (value.CompareTo(min) >= 0 && value.CompareTo(max) <= 0) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeBetweenOrEqualTo(value, min, max, parameterName); + } + + /// + /// Verifies, that the method parameter with specified target value is true + /// and throws an exception if it is found to be so. + /// + /// The target value, which cannot be false. + /// The name of the parameter that is to be checked. + /// The error message, if any to add to the exception. + /// + /// is false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void IsTrue(bool target, string parameterName, string message) + { + if (target) + { + return; + } + + ThrowHelper.ThrowArgumentException(message, parameterName); + } + + /// + /// Verifies, that the method parameter with specified target value is false + /// and throws an exception if it is found to be so. + /// + /// The target value, which cannot be true. + /// The name of the parameter that is to be checked. + /// The error message, if any to add to the exception. + /// + /// is true. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void IsFalse(bool target, string parameterName, string message) + { + if (!target) + { + return; + } + + ThrowHelper.ThrowArgumentException(message, parameterName); + } + + /// + /// Verifies, that the `source` span has the length of 'minLength', or longer. + /// + /// The element type of the spans. + /// The source span. + /// The minimum length. + /// The name of the parameter that is to be checked. + /// + /// has less than items. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeSizedAtLeast(ReadOnlySpan source, int minLength, string parameterName) + { + if (source.Length >= minLength) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeSizedAtLeast(minLength, parameterName); + } + + /// + /// Verifies, that the `source` span has the length of 'minLength', or longer. + /// + /// The element type of the spans. + /// The target span. + /// The minimum length. + /// The name of the parameter that is to be checked. + /// + /// has less than items. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MustBeSizedAtLeast(Span source, int minLength, string parameterName) + { + if (source.Length >= minLength) + { + return; + } + + ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeSizedAtLeast(minLength, parameterName); + } + + /// + /// Verifies that the 'destination' span is not shorter than 'source'. + /// + /// The source element type. + /// The destination element type. + /// The source span. + /// The destination span. + /// The name of the argument for 'destination'. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void DestinationShouldNotBeTooShort( + ReadOnlySpan source, + Span destination, + string destinationParamName) + { + if (destination.Length >= source.Length) + { + return; + } + + ThrowHelper.ThrowArgumentException("Destination span is too short!", destinationParamName); + } + + /// + /// Verifies that the 'destination' span is not shorter than 'source'. + /// + /// The source element type. + /// The destination element type. + /// The source span. + /// The destination span. + /// The name of the argument for 'destination'. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void DestinationShouldNotBeTooShort( + Span source, + Span destination, + string destinationParamName) + { + if (destination.Length >= source.Length) + { + return; + } + + ThrowHelper.ThrowArgumentException("Destination span is too short!", destinationParamName); + } + } +} diff --git a/ImageSharp.Drawing/ArcLineSegment.cs b/ImageSharp.Drawing/ArcLineSegment.cs new file mode 100644 index 0000000..b255021 --- /dev/null +++ b/ImageSharp.Drawing/ArcLineSegment.cs @@ -0,0 +1,373 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Represents a line segment that contains radii and angles that will be rendered as a elliptical arc. + /// + public class ArcLineSegment : ILineSegment + { + private const float ZeroTolerance = 1e-05F; + private readonly PointF[] linePoints; + + /// + /// Initializes a new instance of the class. + /// + /// The absolute coordinates of the current point on the path. + /// The absolute coordinates of the final point of the arc. + /// The radii of the ellipse (also known as its semi-major and semi-minor axes). + /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. + /// + /// The large arc flag, and is if an arc spanning less than or equal to 180 degrees + /// is chosen, or if an arc spanning greater than 180 degrees is chosen. + /// + /// + /// The sweep flag, and is if the line joining center to arc sweeps through decreasing + /// angles, or if it sweeps through increasing angles. + /// + public ArcLineSegment(PointF from, PointF to, SizeF radius, float rotation, bool largeArc, bool sweep) + { + rotation = GeometryUtilities.DegreeToRadian(rotation); + bool ellipse = largeArc && ((Vector2)to - (Vector2)from).LengthSquared() < ZeroTolerance && radius.Width > 0 && radius.Height > 0; + if (ellipse) + { + // The circle always has a start angle of 0 which is positioned at 3 o'clock. + // This means the centre point is to the left of the start position. + Vector2 center = (Vector2)from - new Vector2(radius.Width, 0); + this.linePoints = EllipticArcToBezierCurve(from, center, radius, rotation, 0, sweep ? 2 * MathF.PI : -2 * MathF.PI); + } + else + { + this.linePoints = EllipticArcFromEndParams(from, to, radius, rotation, largeArc, sweep); + } + + this.Bounds = CalculateBounds(this.linePoints); + } + + /// + /// Initializes a new instance of the class. + /// + /// The coordinates of the center of the ellipse. + /// The radii of the ellipse (also known as its semi-major and semi-minor axes). + /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. + /// + /// The start angle of the elliptical arc prior to the stretch and rotate operations. + /// (0 is at the 3 o'clock position of the arc's circle). + /// + /// The angle between and the end of the arc. + public ArcLineSegment(PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle) + { + rotation = GeometryUtilities.DegreeToRadian(rotation); + startAngle = GeometryUtilities.DegreeToRadian(Clamp(startAngle, -360F, 360F)); + sweepAngle = GeometryUtilities.DegreeToRadian(Clamp(sweepAngle, -360F, 360F)); + + Vector2 from = EllipticArcPoint(center, radius, rotation, startAngle); + Vector2 to = EllipticArcPoint(center, radius, rotation, startAngle + sweepAngle); + + bool largeArc = Math.Abs(sweepAngle) > MathF.PI; + bool sweep = sweepAngle > 0; + bool ellipse = largeArc && (to - from).LengthSquared() < ZeroTolerance && radius.Width > 0 && radius.Height > 0; + + if (ellipse) + { + this.linePoints = EllipticArcToBezierCurve(from, center, radius, rotation, startAngle, sweepAngle); + } + else + { + this.linePoints = EllipticArcFromEndParams(from, to, radius, rotation, largeArc, sweep); + } + + this.Bounds = CalculateBounds(this.linePoints); + } + + private ArcLineSegment(PointF[] linePoints) + { + this.linePoints = linePoints; + this.Bounds = CalculateBounds(linePoints); + } + + /// + public PointF StartPoint => this.linePoints[0]; + + /// + public PointF EndPoint => this.linePoints[^1]; + + /// + public RectangleF Bounds { get; } + + /// + public int LinearVertexCount(Vector2 scale) => this.linePoints.Length; + + /// + public void CopyTo(Span destination, bool skipFirstPoint, Vector2 scale) + { + int startIndex = skipFirstPoint ? 1 : 0; + ReadOnlySpan source = this.linePoints.AsSpan(startIndex); + + if (scale == Vector2.One) + { + source.CopyTo(destination); + return; + } + + for (int i = 0; i < source.Length; i++) + { + destination[i] = new PointF(source[i].X * scale.X, source[i].Y * scale.Y); + } + } + + /// + /// Transforms the current using specified matrix. + /// + /// The transformation matrix. + /// An with the matrix applied to it. + public ILineSegment Transform(Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + return this; + } + + PointF[] transformedPoints = new PointF[this.linePoints.Length]; + for (int i = 0; i < this.linePoints.Length; i++) + { + transformedPoints[i] = PointF.Transform(this.linePoints[i], matrix); + } + + return new ArcLineSegment(transformedPoints); + } + + /// + ILineSegment ILineSegment.Transform(Matrix4x4 matrix) => this.Transform(matrix); + + /// + /// Computes the bounds for the retained linearized arc points. + /// + private static RectangleF CalculateBounds(ReadOnlySpan points) + { + float minX = float.MaxValue; + float minY = float.MaxValue; + float maxX = float.MinValue; + float maxY = float.MinValue; + + for (int i = 0; i < points.Length; i++) + { + PointF point = points[i]; + minX = MathF.Min(minX, point.X); + minY = MathF.Min(minY, point.Y); + maxX = MathF.Max(maxX, point.X); + maxY = MathF.Max(maxY, point.Y); + } + + return RectangleF.FromLTRB(minX, minY, maxX, maxY); + } + + private static PointF[] EllipticArcFromEndParams( + PointF from, + PointF to, + SizeF radius, + float rotation, + bool largeArc, + bool sweep) + { + Vector2 absRadius = Vector2.Abs(radius); + + if (EllipticArcOutOfRange(from, to, radius)) + { + return [from, to]; + } + + EndpointToCenterArcParams(from, to, ref absRadius, rotation, largeArc, sweep, out Vector2 center, out Vector2 angles); + return EllipticArcToBezierCurve(from, center, absRadius, rotation, angles.X, angles.Y); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool EllipticArcOutOfRange(Vector2 from, Vector2 to, Vector2 radius) + { + // F.6.2 Out-of-range parameters + radius = Vector2.Abs(radius); + float len = (to - from).LengthSquared(); + if (len < ZeroTolerance) + { + return true; + } + + if (radius.X < ZeroTolerance || radius.Y < ZeroTolerance) + { + return true; + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector2 EllipticArcDerivative(Vector2 r, float xAngle, float t) + => new( + (-r.X * MathF.Cos(xAngle) * MathF.Sin(t)) - (r.Y * MathF.Sin(xAngle) * MathF.Cos(t)), + (-r.X * MathF.Sin(xAngle) * MathF.Sin(t)) + (r.Y * MathF.Cos(xAngle) * MathF.Cos(t))); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector2 EllipticArcPoint(Vector2 c, Vector2 r, float xAngle, float t) + => new( + c.X + (r.X * MathF.Cos(xAngle) * MathF.Cos(t)) - (r.Y * MathF.Sin(xAngle) * MathF.Sin(t)), + c.Y + (r.X * MathF.Sin(xAngle) * MathF.Cos(t)) + (r.Y * MathF.Cos(xAngle) * MathF.Sin(t))); + + private static PointF[] EllipticArcToBezierCurve(Vector2 from, Vector2 center, Vector2 radius, float xAngle, float startAngle, float sweepAngle) + { + float s = startAngle; + float e = s + sweepAngle; + bool neg = e < s; + float sign = neg ? -1 : 1; + float remain = Math.Abs(e - s); + int curveCount = Math.Max((int)MathF.Ceiling(remain / (MathF.PI / 4F)), 1); + + // Arc flattening retains the final point array, so use the builder to avoid the + // intermediate collection and copy a list would generate. + FlattenedPointBuilder points = new(curveCount * 4); + + Vector2 prev = EllipticArcPoint(center, radius, xAngle, s); + + while (remain > ZeroTolerance) + { + float step = (float)Math.Min(remain, Math.PI / 4); + float signStep = step * sign; + + Vector2 p1 = prev; + Vector2 p2 = EllipticArcPoint(center, radius, xAngle, s + signStep); + + float alphaT = (float)Math.Tan(signStep / 2); + float alpha = (float)(Math.Sin(signStep) * (Math.Sqrt(4 + (3 * alphaT * alphaT)) - 1) / 3); + Vector2 q1 = p1 + (alpha * EllipticArcDerivative(radius, xAngle, s)); + Vector2 q2 = p2 - (alpha * EllipticArcDerivative(radius, xAngle, s + signStep)); + + CubicBezierLineSegment bezier = new(from, q1, q2, p2); + int bezierCount = bezier.LinearVertexCount(Vector2.One); + Span destination = points.GetAppendSpan(bezierCount); + bezier.CopyTo(destination, skipFirstPoint: false, Vector2.One); + points.Advance(bezierCount); + + from = p2; + + s += signStep; + remain -= step; + prev = p2; + } + + return points.Detach(); + } + + private static void EndpointToCenterArcParams( + Vector2 p1, + Vector2 p2, + ref Vector2 r, + float xRotation, + bool flagA, + bool flagS, + out Vector2 center, + out Vector2 angles) + { + double rX = Math.Abs(r.X); + double rY = Math.Abs(r.Y); + + // (F.6.5.1) + double dx2 = (p1.X - p2.X) / 2.0; + double dy2 = (p1.Y - p2.Y) / 2.0; + double x1p = (Math.Cos(xRotation) * dx2) + (Math.Sin(xRotation) * dy2); + double y1p = (-Math.Sin(xRotation) * dx2) + (Math.Cos(xRotation) * dy2); + + // (F.6.5.2) + double rxs = rX * rX; + double rys = rY * rY; + double x1ps = x1p * x1p; + double y1ps = y1p * y1p; + + // check if the radius is too small `pq < 0`, when `dq > rxs * rys` (see below) + // cr is the ratio (dq : rxs * rys) + double cr = (x1ps / rxs) + (y1ps / rys); + if (cr > 1) + { + // scale up rX,rY equally so cr == 1 + double s = Math.Sqrt(cr); + rX = s * rX; + rY = s * rY; + rxs = rX * rX; + rys = rY * rY; + } + + double dq = (rxs * y1ps) + (rys * x1ps); + double pq = ((rxs * rys) - dq) / dq; + double q = Math.Sqrt(Math.Max(0, pq)); // Use Max to account for float precision + if (flagA == flagS) + { + q = -q; + } + + double cxp = q * rX * y1p / rY; + double cyp = -q * rY * x1p / rX; + + // (F.6.5.3) + double cx = (Math.Cos(xRotation) * cxp) - (Math.Sin(xRotation) * cyp) + ((p1.X + p2.X) / 2); + double cy = (Math.Sin(xRotation) * cxp) + (Math.Cos(xRotation) * cyp) + ((p1.Y + p2.Y) / 2); + + // (F.6.5.5) + double theta = SvgAngle(1, 0, (x1p - cxp) / rX, (y1p - cyp) / rY); + + // (F.6.5.6) + double delta = SvgAngle((x1p - cxp) / rX, (y1p - cyp) / rY, (-x1p - cxp) / rX, (-y1p - cyp) / rY); + delta %= Math.PI * 2; + + if (!flagS && delta > 0) + { + delta -= 2 * Math.PI; + } + + if (flagS && delta < 0) + { + delta += 2 * Math.PI; + } + + r = new Vector2((float)rX, (float)rY); + center = new Vector2((float)cx, (float)cy); + angles = new Vector2((float)theta, (float)delta); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float Clamp(float val, float min, float max) + { + if (val < min) + { + return min; + } + else if (val > max) + { + return max; + } + else + { + return val; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float SvgAngle(double ux, double uy, double vx, double vy) + { + Vector2 u = new((float)ux, (float)uy); + Vector2 v = new((float)vx, (float)vy); + + // (F.6.5.4) + float dot = Vector2.Dot(u, v); + float len = u.Length() * v.Length(); + float ang = (float)Math.Acos(Clamp(dot / len, -1, 1)); // floating point precision, slightly over values appear + if (((u.X * v.Y) - (u.Y * v.X)) < 0) + { + ang = -ang; + } + + return ang; + } + } +} diff --git a/ImageSharp.Drawing/BooleanOperation.cs b/ImageSharp.Drawing/BooleanOperation.cs new file mode 100644 index 0000000..762282b --- /dev/null +++ b/ImageSharp.Drawing/BooleanOperation.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing { + /// + public enum BooleanOperation + { + /// + Intersection = 0, + + /// + Union = 1, + + /// + Difference = 2, + + /// + Xor = 3 + } +} diff --git a/ImageSharp.Drawing/ClipPathExtensions.cs b/ImageSharp.Drawing/ClipPathExtensions.cs new file mode 100644 index 0000000..e0199d5 --- /dev/null +++ b/ImageSharp.Drawing/ClipPathExtensions.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.PolygonGeometry; +using SixLabors.ImageSharp.Drawing.Processing; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Provides extension methods to that allow the clipping of shapes. + /// + public static class ClipPathExtensions + { + private static readonly ShapeOptions DefaultOptions = new(); + + /// + /// Clips the specified subject path with the provided clipping paths. + /// + /// The subject path. + /// The clipping paths. + /// The clipped . + public static IPath Clip(this IPath subjectPath, params IPath[] clipPaths) + => subjectPath.Clip(DefaultOptions, clipPaths); + + /// + /// Clips the specified subject path with the provided clipping paths. + /// + /// The subject path. + /// The shape options. + /// The clipping paths. + /// The clipped . + public static IPath Clip( + this IPath subjectPath, + ShapeOptions options, + params IPath[] clipPaths) + => ClippedShapeGenerator.GenerateClippedShapes(options.BooleanOperation, subjectPath, clipPaths); + + /// + /// Clips the specified subject path with the provided clipping paths. + /// + /// The subject path. + /// The clipping paths. + /// The clipped . + public static IPath Clip(this IPath subjectPath, IEnumerable clipPaths) + => subjectPath.Clip(DefaultOptions, clipPaths); + + /// + /// Clips the specified subject path with the provided clipping paths. + /// + /// The subject path. + /// The shape options. + /// The clipping paths. + /// The clipped . + public static IPath Clip( + this IPath subjectPath, + ShapeOptions options, + IEnumerable clipPaths) + => ClippedShapeGenerator.GenerateClippedShapes(options.BooleanOperation, subjectPath, clipPaths); + } +} diff --git a/ImageSharp.Drawing/ComplexPolygon.cs b/ImageSharp.Drawing/ComplexPolygon.cs new file mode 100644 index 0000000..60dde89 --- /dev/null +++ b/ImageSharp.Drawing/ComplexPolygon.cs @@ -0,0 +1,303 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Represents a complex polygon made up of one or more shapes overlayed on each other, + /// where overlaps causes holes. + /// + /// + public sealed class ComplexPolygon : IPath, IPathInternals, IInternalPathOwner + { + private readonly IPath[] paths; + private List? internalPaths; + private float length; + private RectangleF? bounds; + private IPath? closedPath; + private LinearGeometryCache geometryCache; + + /// + /// Initializes a new instance of the class. + /// + /// The contour path. + /// The hole path. + public ComplexPolygon(PointF[] contour, PointF[] hole) + : this(new Path(new LinearLineSegment(contour)), new Path(new LinearLineSegment(hole))) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The paths. + public ComplexPolygon(IEnumerable paths) + : this([.. paths]) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The paths. + public ComplexPolygon(params IPath[] paths) + { + Guard.NotNull(paths, nameof(paths)); + + this.paths = paths; + + if (paths.Length == 0) + { + this.bounds = RectangleF.Empty; + } + + this.PathType = PathTypes.Mixed; + } + + /// + public PathTypes PathType { get; } + + /// + /// Gets the collection of paths that make up this shape. + /// + public IEnumerable Paths => this.paths; + + /// + public RectangleF Bounds => this.bounds ??= this.CalcBounds(); + + /// + public IPath Transform(Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + // No transform to apply skip it + return this; + } + + IPath[] shapes = new IPath[this.paths.Length]; + + for (int i = 0; i < shapes.Length; i++) + { + shapes[i] = this.paths[i].Transform(matrix); + } + + return new ComplexPolygon(shapes); + } + + /// + public IEnumerable Flatten() + { + List paths = new(this.paths.Length); + foreach (IPath path in this.Paths) + { + paths.AddRange(path.Flatten()); + } + + return paths; + } + + /// + public LinearGeometry ToLinearGeometry(Vector2 scale) + => this.geometryCache.TryGet(scale, out LinearGeometry? hit) + ? hit + : this.geometryCache.Store(scale, this.BuildLinearGeometry(scale)); + + private LinearGeometry BuildLinearGeometry(Vector2 scale) + { + int pointCount = 0; + int contourCount = 0; + int segmentCount = 0; + int nonHorizontalSegmentCountPixelBoundary = 0; + int nonHorizontalSegmentCountPixelCenter = 0; + + bool hasBounds = false; + float minX = float.MaxValue; + float minY = float.MaxValue; + float maxX = float.MinValue; + float maxY = float.MinValue; + + foreach (IPath path in this.paths) + { + LinearGeometry geometry = path.ToLinearGeometry(scale); + + if (geometry.Info.PointCount == 0) + { + continue; + } + + RectangleF childBounds = geometry.Info.Bounds; + minX = MathF.Min(minX, childBounds.Left); + minY = MathF.Min(minY, childBounds.Top); + maxX = MathF.Max(maxX, childBounds.Right); + maxY = MathF.Max(maxY, childBounds.Bottom); + hasBounds = true; + + pointCount += geometry.Info.PointCount; + contourCount += geometry.Info.ContourCount; + segmentCount += geometry.Info.SegmentCount; + nonHorizontalSegmentCountPixelBoundary += geometry.Info.NonHorizontalSegmentCountPixelBoundary; + nonHorizontalSegmentCountPixelCenter += geometry.Info.NonHorizontalSegmentCountPixelCenter; + } + + PointF[] points = new PointF[pointCount]; + LinearContour[] contours = new LinearContour[contourCount]; + int pointStart = 0; + int contourStart = 0; + int segmentStart = 0; + + foreach (IPath path in this.paths) + { + LinearGeometry geometry = path.ToLinearGeometry(scale); + if (geometry.Info.PointCount == 0) + { + continue; + } + + for (int i = 0; i < geometry.Points.Count; i++) + { + points[pointStart + i] = geometry.Points[i]; + } + + for (int i = 0; i < geometry.Contours.Count; i++) + { + LinearContour contour = geometry.Contours[i]; + contours[contourStart + i] = new LinearContour + { + PointStart = pointStart + contour.PointStart, + PointCount = contour.PointCount, + SegmentStart = segmentStart + contour.SegmentStart, + SegmentCount = contour.SegmentCount, + IsClosed = contour.IsClosed + }; + } + + pointStart += geometry.Info.PointCount; + contourStart += geometry.Info.ContourCount; + segmentStart += geometry.Info.SegmentCount; + } + + RectangleF bounds = hasBounds ? RectangleF.FromLTRB(minX, minY, maxX, maxY) : RectangleF.Empty; + + return new LinearGeometry( + new LinearGeometryInfo + { + Bounds = bounds, + ContourCount = contours.Length, + PointCount = points.Length, + SegmentCount = segmentCount, + NonHorizontalSegmentCountPixelBoundary = nonHorizontalSegmentCountPixelBoundary, + NonHorizontalSegmentCountPixelCenter = nonHorizontalSegmentCountPixelCenter + }, + contours, + points); + } + + /// + public IPath AsClosedPath() + { + if (this.PathType == PathTypes.Closed) + { + return this; + } + + if (this.closedPath is not null) + { + return this.closedPath; + } + + IPath[] paths = new IPath[this.paths.Length]; + for (int i = 0; i < this.paths.Length; i++) + { + paths[i] = this.paths[i].AsClosedPath(); + } + + this.closedPath = new ComplexPolygon(paths); + return this.closedPath; + } + + /// + SegmentInfo IPathInternals.PointAlongPath(float distance) + { + this.EnsureInternalPaths(); + + distance %= this.length; + foreach (InternalPath p in this.internalPaths) + { + if (p.Length >= distance) + { + return p.PointAlongPath(distance); + } + + // Reduce it before trying the next path + distance -= p.Length; + } + + ThrowOutOfRange(); + return default; + } + + /// + IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() + { + this.EnsureInternalPaths(); + return this.internalPaths; + } + + [MemberNotNull(nameof(internalPaths))] + private void EnsureInternalPaths() + { + if (this.internalPaths is not null) + { + return; + } + + this.InitInternalPaths(); + } + + /// + /// Initializes and . + /// + [MemberNotNull(nameof(internalPaths))] + private void InitInternalPaths() + { + this.internalPaths = new List(this.paths.Length); + this.length = 0; + + foreach (IPath p in this.paths) + { + foreach (ISimplePath s in p.Flatten()) + { + InternalPath ip = new(s.Points, s.IsClosed); + this.length += ip.Length; + this.internalPaths.Add(ip); + } + } + } + + private RectangleF CalcBounds() + { + float minX = float.MaxValue; + float maxX = float.MinValue; + float minY = float.MaxValue; + float maxY = float.MinValue; + + foreach (IPath p in this.paths) + { + RectangleF pBounds = p.Bounds; + + minX = MathF.Min(minX, pBounds.Left); + maxX = MathF.Max(maxX, pBounds.Right); + minY = MathF.Min(minY, pBounds.Top); + maxY = MathF.Max(maxY, pBounds.Bottom); + } + + return new RectangleF(minX, minY, maxX - minX, maxY - minY); + } + + private static InvalidOperationException ThrowOutOfRange() => new("Should not be possible to reach this line"); + } +} diff --git a/ImageSharp.Drawing/CubicBezierLineSegment.cs b/ImageSharp.Drawing/CubicBezierLineSegment.cs new file mode 100644 index 0000000..3137843 --- /dev/null +++ b/ImageSharp.Drawing/CubicBezierLineSegment.cs @@ -0,0 +1,263 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Threading; +using SixLabors.ImageSharp.Drawing.Helpers; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Represents a line segment that contains a lists of control points that will be rendered as a cubic bezier curve + /// + /// + public sealed class CubicBezierLineSegment : ILineSegment + { + // Code for this taken from + private const float MinimumSqrDistance = 1.75f; + private const float DivisionThreshold = -.9995f; + + private readonly PointF[] controlPoints; + private FlattenedCache? flattenedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The points. + public CubicBezierLineSegment(PointF[] points) + { + Guard.NotNull(points, nameof(points)); + Guard.MustBeGreaterThanOrEqualTo(points.Length, 4, nameof(points)); + Guard.IsTrue((points.Length - 1) % 3 == 0, nameof(points), "points must be a multiple of 3 plus 1 long."); + this.controlPoints = points; + } + + /// + /// Initializes a new instance of the class. + /// + /// The start. + /// The control point1. + /// The control point2. + /// The end. + /// The additional points. + public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF controlPoint2, PointF end, params PointF[] additionalPoints) + : this(new[] { start, controlPoint1, controlPoint2, end }.Concat(additionalPoints)) + { + } + + /// + public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF controlPoint2, PointF end) + : this([start, controlPoint1, controlPoint2, end]) + { + } + + /// + /// Gets the control points. + /// + public IReadOnlyList ControlPoints => this.controlPoints; + + /// + public PointF StartPoint => this.controlPoints[0]; + + /// + public PointF EndPoint => this.controlPoints[^1]; + + /// + public RectangleF Bounds => CalculateBounds(this.GetFlattenedPoints(Vector2.One)); + + /// + public int LinearVertexCount(Vector2 scale) => this.GetFlattenedPoints(scale).Length; + + /// + public void CopyTo(Span destination, bool skipFirstPoint, Vector2 scale) + { + PointF[] flattened = this.GetFlattenedPoints(scale); + int startIndex = skipFirstPoint ? 1 : 0; + flattened.AsSpan(startIndex).CopyTo(destination); + } + + /// + /// Returns the flattened point run for this curve under , computing it on first + /// request and reusing the cached result for subsequent calls at the same scale. + /// + /// + /// Publication uses so a concurrent reader either observes + /// or a fully-constructed entry. + /// + private PointF[] GetFlattenedPoints(Vector2 scale) + { + FlattenedCache? hit = Volatile.Read(ref this.flattenedCache); + if (hit is not null && hit.Scale == scale) + { + return hit.Points; + } + + PointF[] baked = FlattenCurve(this.controlPoints, scale); + Volatile.Write(ref this.flattenedCache, new FlattenedCache(scale, baked)); + return baked; + } + + /// + /// Gets the control points of this curve. + /// + /// The control points of this curve. + public ReadOnlyMemory GetControlPoints() => this.controlPoints; + + /// + /// Transforms this line segment using the specified matrix. + /// + /// The matrix. + /// A line segment with the matrix applied to it. + public CubicBezierLineSegment Transform(Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + // no transform to apply skip it + return this; + } + + PointF[] transformedPoints = new PointF[this.controlPoints.Length]; + + for (int i = 0; i < this.controlPoints.Length; i++) + { + transformedPoints[i] = PointF.Transform(this.controlPoints[i], matrix); + } + + return new CubicBezierLineSegment(transformedPoints); + } + + /// + ILineSegment ILineSegment.Transform(Matrix4x4 matrix) => this.Transform(matrix); + + /// + /// Flattens every cubic in under the supplied device-space + /// into a single contiguous point run. Subdivision density is evaluated + /// against the scaled control points so the polyline adapts to rendering scale. + /// + private static PointF[] FlattenCurve(PointF[] controlPoints, Vector2 scale) + { + int curveCount = (controlPoints.Length - 1) / 3; + + // Flattened points are cached as a retained array, so use the builder to avoid + // the intermediate collection and copy a list would generate. + FlattenedPointBuilder output = new(curveCount * 4); + + for (int curveIndex = 0; curveIndex < curveCount; curveIndex++) + { + int nodeIndex = curveIndex * 3; + Vector2 p0 = new(controlPoints[nodeIndex].X * scale.X, controlPoints[nodeIndex].Y * scale.Y); + Vector2 p1 = new(controlPoints[nodeIndex + 1].X * scale.X, controlPoints[nodeIndex + 1].Y * scale.Y); + Vector2 p2 = new(controlPoints[nodeIndex + 2].X * scale.X, controlPoints[nodeIndex + 2].Y * scale.Y); + Vector2 p3 = new(controlPoints[nodeIndex + 3].X * scale.X, controlPoints[nodeIndex + 3].Y * scale.Y); + + if (curveIndex == 0) + { + output.Add((PointF)p0); + } + + SubdivideAndAppend(0F, 1F, p0, p1, p2, p3, ref output, 0); + output.Add((PointF)p3); + } + + return output.Detach(); + } + + /// + /// Recursively subdivides the scaled cubic segment, appending midpoints in left-to-right order. + /// + private static void SubdivideAndAppend( + float t0, + float t1, + Vector2 p0, + Vector2 p1, + Vector2 p2, + Vector2 p3, + ref FlattenedPointBuilder output, + int depth) + { + if (depth > 999) + { + return; + } + + Vector2 left = CalculateBezierPoint(t0, p0, p1, p2, p3); + Vector2 right = CalculateBezierPoint(t1, p0, p1, p2, p3); + + if ((left - right).LengthSquared() < MinimumSqrDistance) + { + return; + } + + float midT = (t0 + t1) / 2; + Vector2 mid = CalculateBezierPoint(midT, p0, p1, p2, p3); + + Vector2 leftDirection = Vector2.Normalize(left - mid); + Vector2 rightDirection = Vector2.Normalize(right - mid); + + if (Vector2.Dot(leftDirection, rightDirection) > DivisionThreshold || Math.Abs(midT - 0.5f) < 0.0001f) + { + SubdivideAndAppend(t0, midT, p0, p1, p2, p3, ref output, depth + 1); + output.Add((PointF)mid); + SubdivideAndAppend(midT, t1, p0, p1, p2, p3, ref output, depth + 1); + } + } + + /// + /// Calculates the bezier point along the line. + /// + /// The position within the line. + /// The p 0. + /// The p 1. + /// The p 2. + /// The p 3. + /// + /// The . + /// + private static Vector2 CalculateBezierPoint(float t, Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) + { + float u = 1 - t; + float tt = t * t; + float uu = u * u; + float uuu = uu * u; + float ttt = tt * t; + + Vector2 p = uuu * p0; // first term + + p += 3 * uu * t * p1; // second term + p += 3 * u * tt * p2; // third term + p += ttt * p3; // fourth term + + return p; + } + + /// + /// Computes the bounds for the cached linearized bezier points. + /// + private static RectangleF CalculateBounds(ReadOnlySpan points) + { + float minX = float.MaxValue; + float minY = float.MaxValue; + float maxX = float.MinValue; + float maxY = float.MinValue; + + for (int i = 0; i < points.Length; i++) + { + PointF point = points[i]; + minX = MathF.Min(minX, point.X); + minY = MathF.Min(minY, point.Y); + maxX = MathF.Max(maxX, point.X); + maxY = MathF.Max(maxY, point.Y); + } + + return RectangleF.FromLTRB(minX, minY, maxX, maxY); + } + + private sealed class FlattenedCache(Vector2 scale, PointF[] points) + { + public Vector2 Scale { get; } = scale; + + public PointF[] Points { get; } = points; + } + } +} diff --git a/ImageSharp.Drawing/EllipsePolygon.cs b/ImageSharp.Drawing/EllipsePolygon.cs new file mode 100644 index 0000000..b94ce76 --- /dev/null +++ b/ImageSharp.Drawing/EllipsePolygon.cs @@ -0,0 +1,125 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// An elliptical shape made up of a single path made up of one of more s. + /// + public sealed class EllipsePolygon : Polygon, IPathInternals + { + /// + /// Initializes a new instance of the class. + /// + /// The location the center of the ellipse will be placed. + /// The width/height of the final ellipse. + public EllipsePolygon(PointF location, SizeF size) + : base(CreateSegment(location, size)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The location the center of the circle will be placed. + /// The radius final circle. + public EllipsePolygon(PointF location, float radius) + : this(location, new SizeF(radius * 2, radius * 2)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The x-coordinate of the center of the ellipse. + /// The y-coordinate of the center of the ellipse. + /// The width the ellipse should have. + /// The height the ellipse should have. + public EllipsePolygon(float x, float y, float width, float height) + : this(new PointF(x, y), new SizeF(width, height)) + { + } + + private EllipsePolygon(ILineSegment[] segments) + : base(segments, true) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The x-coordinate of the center of the circle. + /// The y-coordinate of the center of the circle. + /// The radius final circle. + public EllipsePolygon(float x, float y, float radius) + : this(new PointF(x, y), new SizeF(radius * 2, radius * 2)) + { + } + + /// + public override IPath Transform(Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + return this; + } + + ILineSegment[] segments = new ILineSegment[this.LineSegments.Count]; + + for (int i = 0; i < segments.Length; i++) + { + segments[i] = this.LineSegments[i].Transform(matrix); + } + + return new EllipsePolygon(segments); + } + + /// + // TODO switch this out to a calculated algorithm + SegmentInfo IPathInternals.PointAlongPath(float distance) + => this.InnerPath.PointAlongPath(distance); + + private static CubicBezierLineSegment CreateSegment(Vector2 location, SizeF size) + { + Guard.MustBeGreaterThan(size.Width, 0, "width"); + Guard.MustBeGreaterThan(size.Height, 0, "height"); + + const float kappa = 0.5522848f; + + Vector2 sizeVector = size; + sizeVector /= 2; + + Vector2 rootLocation = location - sizeVector; + + Vector2 pointO = sizeVector * kappa; + Vector2 pointE = location + sizeVector; + Vector2 pointM = location; + Vector2 pointMminusO = pointM - pointO; + Vector2 pointMplusO = pointM + pointO; + + PointF[] points = + [ + new Vector2(rootLocation.X, pointM.Y), + + new Vector2(rootLocation.X, pointMminusO.Y), + new Vector2(pointMminusO.X, rootLocation.Y), + new Vector2(pointM.X, rootLocation.Y), + + new Vector2(pointMplusO.X, rootLocation.Y), + new Vector2(pointE.X, pointMminusO.Y), + new Vector2(pointE.X, pointM.Y), + + new Vector2(pointE.X, pointMplusO.Y), + new Vector2(pointMplusO.X, pointE.Y), + new Vector2(pointM.X, pointE.Y), + + new Vector2(pointMminusO.X, pointE.Y), + new Vector2(rootLocation.X, pointMplusO.Y), + new Vector2(rootLocation.X, pointM.Y) + ]; + + return new CubicBezierLineSegment(points); + } + } +} diff --git a/ImageSharp.Drawing/EmptyPath.cs b/ImageSharp.Drawing/EmptyPath.cs new file mode 100644 index 0000000..96f4149 --- /dev/null +++ b/ImageSharp.Drawing/EmptyPath.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// A path that is always empty. + /// + public sealed class EmptyPath : IPath + { + private static readonly LinearGeometry EmptyGeometry = new( + new LinearGeometryInfo + { + Bounds = RectangleF.Empty, + ContourCount = 0, + PointCount = 0, + SegmentCount = 0, + NonHorizontalSegmentCountPixelBoundary = 0, + NonHorizontalSegmentCountPixelCenter = 0 + }, + [], + []); + + private EmptyPath(PathTypes pathType) => this.PathType = pathType; + + /// + /// Gets the closed path instance of the empty path + /// + public static EmptyPath ClosedPath { get; } = new(PathTypes.Closed); + + /// + /// Gets the open path instance of the empty path + /// + public static EmptyPath OpenPath { get; } = new(PathTypes.Open); + + /// + public PathTypes PathType { get; } + + /// + public RectangleF Bounds => RectangleF.Empty; + + /// + public IPath AsClosedPath() => ClosedPath; + + /// + public IEnumerable Flatten() => []; + + /// + public LinearGeometry ToLinearGeometry(Vector2 scale) => EmptyGeometry; + + /// + public IPath Transform(Matrix4x4 matrix) => this; + } +} diff --git a/ImageSharp.Drawing/FlattenedPointBuilder.cs b/ImageSharp.Drawing/FlattenedPointBuilder.cs new file mode 100644 index 0000000..e0b481a --- /dev/null +++ b/ImageSharp.Drawing/FlattenedPointBuilder.cs @@ -0,0 +1,84 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Builds the retained array used by flattened segment caches without an intermediate collection copy. + /// + /// + /// Segment flatteners ultimately need to return a tightly-sized array that can be cached by the segment instance. + /// This builder owns that array while points are appended. + /// + internal struct FlattenedPointBuilder + { + private PointF[] points; + private int count; + + /// + /// Initializes a new instance of the struct. + /// + /// The estimated number of points that will be appended. + public FlattenedPointBuilder(int capacity) + { + this.points = new PointF[Math.Max(capacity, 4)]; + this.count = 0; + } + + /// + /// Appends one point to the retained point array. + /// + /// The point to append. + public void Add(PointF point) + { + this.EnsureCapacity(this.count + 1); + this.points[this.count++] = point; + } + + /// + /// Reserves a writable append window for callers that populate multiple points directly. + /// + /// The number of points to reserve. + /// A span covering the reserved append window. + public Span GetAppendSpan(int length) + { + this.EnsureCapacity(this.count + length); + return this.points.AsSpan(this.count, length); + } + + /// + /// Commits points previously written through . + /// + /// The number of points written to the reserved append window. + public void Advance(int length) => this.count += length; + + /// + /// Returns the owned point array. + /// + /// The tightly-sized retained point array. + public PointF[] Detach() + { + if (this.count != this.points.Length) + { + Array.Resize(ref this.points, this.count); + } + + return this.points; + } + + /// + /// Ensures the owned array can store the requested total point count. + /// + /// The total number of points that must fit. + private void EnsureCapacity(int capacity) + { + if (capacity <= this.points.Length) + { + return; + } + + Array.Resize(ref this.points, Math.Max(capacity, this.points.Length * 2)); + } + } +} diff --git a/ImageSharp.Drawing/Helpers/ArrayExtensions.cs b/ImageSharp.Drawing/Helpers/ArrayExtensions.cs new file mode 100644 index 0000000..df765d0 --- /dev/null +++ b/ImageSharp.Drawing/Helpers/ArrayExtensions.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Drawing.Helpers { + /// + /// Extension methods for arrays. + /// + internal static class ArrayExtensions + { + /// + /// Concatenates two arrays into one. + /// + /// The element type. + /// The first source array. + /// The second source array. + /// + /// A new array containing the elements of both source arrays, or + /// when is empty. + /// + public static T[] Concat(this T[] source1, T[] source2) + { + if (source2 is null || source2.Length == 0) + { + return source1; + } + + T[] target = new T[source1.Length + source2.Length]; + source1.AsSpan().CopyTo(target); + source2.AsSpan().CopyTo(target.AsSpan(source1.Length)); + + return target; + } + } +} diff --git a/ImageSharp.Drawing/Helpers/MatrixUtilities.cs b/ImageSharp.Drawing/Helpers/MatrixUtilities.cs new file mode 100644 index 0000000..6dc025a --- /dev/null +++ b/ImageSharp.Drawing/Helpers/MatrixUtilities.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Drawing.Helpers { + /// + /// Provides helper methods for extracting properties from transformation matrices. + /// + internal static class MatrixUtilities + { + /// + /// Extracts the average 2D scale factor from a . + /// This is the mean of the X and Y axis scale magnitudes, suitable for + /// uniformly scaling radii under non-uniform or projective transforms. + /// + /// The transformation matrix. + /// The average scale factor. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float GetAverageScale(in Matrix4x4 matrix) + { + float sx = MathF.Sqrt((matrix.M11 * matrix.M11) + (matrix.M12 * matrix.M12)); + float sy = MathF.Sqrt((matrix.M21 * matrix.M21) + (matrix.M22 * matrix.M22)); + return (sx + sy) * 0.5f; + } + } +} diff --git a/ImageSharp.Drawing/Helpers/PolygonUtilities.cs b/ImageSharp.Drawing/Helpers/PolygonUtilities.cs new file mode 100644 index 0000000..92c5def --- /dev/null +++ b/ImageSharp.Drawing/Helpers/PolygonUtilities.cs @@ -0,0 +1,126 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Drawing; + +namespace SixLabors.ImageSharp.Drawing.Helpers { + /// + /// Provides low-level geometry helpers for polygon winding and segment intersection. + /// + /// + /// Polygon methods expect a closed ring where the first point is repeated as the last point. + /// Orientation signs are defined using world-space math conventions (Y points up): + /// positive signed area is counter-clockwise and negative signed area is clockwise. + /// In screen space (Y points down), the visual winding appears inverted. + /// + internal static class PolygonUtilities + { + // Epsilon used for floating-point tolerance. Values within +-Eps are treated as zero. + // This reduces instability when segments are nearly parallel or endpoints are close. + private const float Eps = 1e-3f; + private const float MinusEps = -Eps; + private const float OnePlusEps = 1 + Eps; + + /// + /// Ensures that a closed polygon ring matches the expected orientation. + /// + /// Polygon ring to normalize in place. + /// + /// Expected orientation sign: + /// positive for counter-clockwise in world space, negative for clockwise in world space. + /// + /// + /// The ring is reversed only when its orientation sign disagrees with + /// . Degenerate rings (zero area) are not changed. + /// + public static void EnsureOrientation(Span polygon, int expectedOrientation) + { + if (GetPolygonOrientation(polygon) * expectedOrientation < 0) + { + polygon.Reverse(); + } + } + + /// + /// Returns the orientation sign of a closed polygon ring using the shoelace sum. + /// + /// Closed polygon ring. + /// + /// -1 for clockwise, 1 for counter-clockwise, or 0 for degenerate (zero-area) input. + /// + private static int GetPolygonOrientation(ReadOnlySpan polygon) + { + float sum = 0f; + for (int i = 0; i < polygon.Length - 1; ++i) + { + PointF current = polygon[i]; + PointF next = polygon[i + 1]; + sum += (current.X * next.Y) - (next.X * current.Y); + } + + // A tolerant compare could be used here, but edge scanning does not special-case + // zero-area or near-zero-area input, so we keep this strict sign check. + return Math.Sign(sum); + } + + /// + /// Tests whether two line segments intersect, excluding collinear overlap cases. + /// + /// Start point of segment A. + /// End point of segment A. + /// Start point of segment B. + /// End point of segment B. + /// + /// Receives the intersection point when an intersection is found. + /// If no intersection is detected, the value is not modified. + /// + /// + /// when the segments intersect within their extents + /// (including endpoints); otherwise . + /// + /// + /// This solves the two segment equations in parametric form and accepts values in [0, 1] + /// with an epsilon margin for floating-point tolerance. + /// Parallel and collinear pairs are rejected early (cross product ~= 0). + /// + public static bool LineSegmentToLineSegmentIgnoreCollinear( + Vector2 a0, + Vector2 a1, + Vector2 b0, + Vector2 b1, + ref Vector2 intersectionPoint) + { + // Direction vectors of the segments. + float dax = a1.X - a0.X; + float day = a1.Y - a0.Y; + float dbx = b1.X - b0.X; + float dby = b1.Y - b0.Y; + + // Cross product of the direction vectors. Near zero means parallel/collinear. + float crossD = (-dbx * day) + (dax * dby); + + // Reject parallel and collinear lines. Collinear overlap is intentionally not handled. + if (crossD is > MinusEps and < Eps) + { + return false; + } + + // Solve for parameters s and t where: + // a0 + t * (a1 - a0) = b0 + s * (b1 - b0) + float s = ((-day * (a0.X - b0.X)) + (dax * (a0.Y - b0.Y))) / crossD; + float t = ((dbx * (a0.Y - b0.Y)) - (dby * (a0.X - b0.X))) / crossD; + + // If both parameters are within [0,1] (with tolerance), the segments intersect. + if (s > MinusEps && s < OnePlusEps && t > MinusEps && t < OnePlusEps) + { + intersectionPoint.X = a0.X + (t * dax); + intersectionPoint.Y = a0.Y + (t * day); + return true; + } + + return false; + } + } +} diff --git a/ImageSharp.Drawing/IInternalPathOwner.cs b/ImageSharp.Drawing/IInternalPathOwner.cs new file mode 100644 index 0000000..8331a6a --- /dev/null +++ b/ImageSharp.Drawing/IInternalPathOwner.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// An internal interface for shapes which are backed by + /// so we can have a fast path tessellating them. + /// + internal interface IInternalPathOwner + { + /// + /// Returns the rings as a readonly collection of elements. + /// + /// The . + public IReadOnlyList GetRingsAsInternalPath(); + } +} diff --git a/ImageSharp.Drawing/ILineSegment.cs b/ImageSharp.Drawing/ILineSegment.cs new file mode 100644 index 0000000..a8ad84f --- /dev/null +++ b/ImageSharp.Drawing/ILineSegment.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Represents a simple path segment + /// + public interface ILineSegment + { + /// + /// Gets the start point. + /// + public PointF StartPoint { get; } + + /// + /// Gets the end point. + /// + /// + /// The end point. + /// + public PointF EndPoint { get; } + + /// + /// Gets the bounds of the linearized segment output. + /// + public RectangleF Bounds { get; } + + /// + /// Returns the number of linear vertices emitted by this segment when flattened under the supplied + /// device-space . + /// + /// The X/Y scale at which curves are flattened. Pass for local-space counts. + /// The number of linear vertices this segment emits. + public int LinearVertexCount(Vector2 scale); + + /// + /// Writes the segment's linearized points to , baked at the supplied + /// device-space . + /// + /// The destination point span. + /// Whether to skip the first emitted point. + /// The X/Y scale at which curves are flattened. Pass for local-space output. + public void CopyTo(Span destination, bool skipFirstPoint, Vector2 scale); + + /// + /// Transforms the current LineSegment using specified matrix. + /// + /// The matrix. + /// A line segment with the matrix applied to it. + public ILineSegment Transform(Matrix4x4 matrix); + } +} diff --git a/ImageSharp.Drawing/IPath.cs b/ImageSharp.Drawing/IPath.cs new file mode 100644 index 0000000..49aaeec --- /dev/null +++ b/ImageSharp.Drawing/IPath.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Represents a logic path that can be drawn. + /// + public interface IPath + { + /// + /// Gets a value indicating whether this instance is closed, open or a composite path with a mixture of open and closed figures. + /// + public PathTypes PathType { get; } + + /// + /// Gets the bounds enclosing the path. + /// + public RectangleF Bounds { get; } + + /// + /// Converts the into a simple linear path. + /// + /// Returns the current as simple linear path. + public IEnumerable Flatten(); + + /// + /// Converts this path into a retained , flattening curves at the precision of + /// the supplied device-space . + /// + /// The X/Y scale at which curves are flattened. + /// The retained linear geometry. + public LinearGeometry ToLinearGeometry(Vector2 scale); + + /// + /// Transforms the path using the specified matrix. + /// + /// The matrix. + /// A new path with the matrix applied to it. + public IPath Transform(Matrix4x4 matrix); + + /// + /// Returns this path with all figures closed. + /// + /// A new close . + public IPath AsClosedPath(); + } +} diff --git a/ImageSharp.Drawing/IPathCollection.cs b/ImageSharp.Drawing/IPathCollection.cs new file mode 100644 index 0000000..c45caca --- /dev/null +++ b/ImageSharp.Drawing/IPathCollection.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Represents a logic path that can be drawn + /// + public interface IPathCollection : IEnumerable + { + /// + /// Gets the bounds enclosing the path + /// + public RectangleF Bounds { get; } + + /// + /// Transforms the path using the specified matrix. + /// + /// The matrix. + /// A new path collection with the matrix applied to it. + public IPathCollection Transform(Matrix4x4 matrix); + } +} diff --git a/ImageSharp.Drawing/IPathInternals.cs b/ImageSharp.Drawing/IPathInternals.cs new file mode 100644 index 0000000..f571ecc --- /dev/null +++ b/ImageSharp.Drawing/IPathInternals.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing { + /// + /// An interface for internal operations we don't want to expose on . + /// + internal interface IPathInternals : IPath + { + /// + /// Returns information about a point at a given distance along a path. + /// + /// The distance along the path to return details for. + /// + /// The segment information. + /// + SegmentInfo PointAlongPath(float distance); + } +} diff --git a/ImageSharp.Drawing/ISimplePath.cs b/ImageSharp.Drawing/ISimplePath.cs new file mode 100644 index 0000000..aec0497 --- /dev/null +++ b/ImageSharp.Drawing/ISimplePath.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Represents a simple (non-composite) path defined by a series of points. + /// + public interface ISimplePath + { + /// + /// Gets a value indicating whether this instance is a closed path. + /// + public bool IsClosed { get; } + + /// + /// Gets the points that make this up as a simple linear path. + /// + public ReadOnlyMemory Points { get; } + } +} diff --git a/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/ImageSharp.Drawing/ImageSharp.Drawing.csproj new file mode 100644 index 0000000..0ce9fc3 --- /dev/null +++ b/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -0,0 +1,37 @@ + + + + net10.0 + SixLabors.ImageSharp.Drawing + SixLabors.ImageSharp.Drawing + SixLabors.ImageSharp.Drawing + SixLabors.ImageSharp.Drawing + sixlabors.imagesharp.drawing.128.png + LICENSE + https://github.com/SixLabors/ImageSharp.Drawing/ + $(RepositoryUrl) + ImageSharp Drawing Graphics Shapes Paths Text Fonts Vector Raster + Drawing extensions for ImageSharp with support for shapes, paths, text, and image rendering. + Debug;Release + true + + + + + + enable + Nullable + + + + + + + + + + + + + + diff --git a/ImageSharp.Drawing/InternalPath.cs b/ImageSharp.Drawing/InternalPath.cs new file mode 100644 index 0000000..eda5884 --- /dev/null +++ b/ImageSharp.Drawing/InternalPath.cs @@ -0,0 +1,397 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Internal logic for integrating linear paths. + /// + internal class InternalPath + { + /// + /// The epsilon for float comparison + /// + private const float Epsilon = 0.003f; + private const float Epsilon2 = 0.2f; + + /// + /// The points. + /// + private readonly PointData[] points; + + /// + /// Materialized points projected from . + /// + private PointF[]? materializedPoints; + + /// + /// The closed path. + /// + private readonly bool closedPath; + + /// + /// Initializes a new instance of the class. + /// + /// The segments. + /// if set to true [is closed path]. + /// Whether to remove close and collinear vertices + internal InternalPath(IReadOnlyList segments, bool isClosedPath, bool removeCloseAndCollinear = true) + : this(Simplify(segments, isClosedPath, removeCloseAndCollinear), isClosedPath) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The points. + /// if set to true [is closed path]. + internal InternalPath(ReadOnlyMemory points, bool isClosedPath) + : this(Simplify(points.Span, isClosedPath, true), isClosedPath) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The points. + /// if set to true [is closed path]. + private InternalPath(PointData[] points, bool isClosedPath) + { + this.points = points; + this.closedPath = isClosedPath; + + if (this.points.Length > 0) + { + float minX, minY, maxX, maxY, length; + length = 0; + minX = minY = float.MaxValue; + maxX = maxY = float.MinValue; + + foreach (PointData point in this.points) + { + length += point.Length; + minX = Math.Min(point.Point.X, minX); + minY = Math.Min(point.Point.Y, minY); + maxX = Math.Max(point.Point.X, maxX); + maxY = Math.Max(point.Point.Y, maxY); + } + + this.Bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY); + this.Length = length; + } + else + { + this.Bounds = RectangleF.Empty; + this.Length = 0; + } + } + + /// + /// Gets the bounds. + /// + /// + /// The bounds. + /// + public RectangleF Bounds { get; } + + /// + /// Gets the length. + /// + /// + /// The length. + /// + public float Length { get; } + + /// + /// Gets the length. + /// + public int PointCount => this.points.Length; + + /// + /// Gets the points. + /// + /// The + internal ReadOnlyMemory Points() => this.materializedPoints ??= this.CreatePoints(); + + /// + /// Calculates the point a certain distance a path. + /// + /// The distance along the path to find details of. + /// + /// Returns details about a point along a path. + /// + /// Thrown if no points found. + internal SegmentInfo PointAlongPath(float distanceAlongPath) + { + int pointCount = this.PointCount; + if (this.closedPath) + { + // Move the distance back to the beginning since this is a closed polygon. + distanceAlongPath %= this.Length; + pointCount--; + } + + for (int i = 0; i < pointCount; i++) + { + int next = WrapArrayIndex(i + 1, this.PointCount); + if (distanceAlongPath < this.points[next].Length) + { + float t = distanceAlongPath / this.points[next].Length; + Vector2 point = Vector2.Lerp(this.points[i].Point, this.points[next].Point, t); + Vector2 diff = this.points[i].Point - this.points[next].Point; + + return new SegmentInfo + { + Point = point, + Angle = (float)(Math.Atan2(diff.Y, diff.X) % (Math.PI * 2)) + }; + } + + distanceAlongPath -= this.points[next].Length; + } + + // Closed paths will never reach this point. + // For open paths we're going to create a new virtual point that extends past the path. + // The position and angle for that point are calculated based upon the last two points. + PointF a = this.points[Math.Max(this.points.Length - 2, 0)].Point; + PointF b = this.points[^1].Point; + Vector2 delta = a - b; + float angle = (float)(Math.Atan2(delta.Y, delta.X) % (Math.PI * 2)); + + Matrix4x4 transform = Matrix4x4.CreateRotationZ(angle - MathF.PI) * Matrix4x4.CreateTranslation(b.X, b.Y, 0); + + return new SegmentInfo + { + Point = PointF.Transform(new PointF(distanceAlongPath, 0), transform), + Angle = angle + }; + } + + // Modulo is a very slow operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int WrapArrayIndex(int i, int arrayLength) => i < arrayLength ? i : i - arrayLength; + + private PointF[] CreatePoints() + { + PointF[] result = new PointF[this.points.Length]; + for (int i = 0; i < result.Length; i++) + { + result[i] = this.points[i].Point; + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static PointOrientation CalculateOrientation(Vector2 p, Vector2 q, Vector2 r) + { + // See http://www.geeksforgeeks.org/orientation-3-ordered-points/ + // for details of below formula. + Vector2 qp = q - p; + Vector2 rq = r - q; + float val = (qp.Y * rq.X) - (qp.X * rq.Y); + + if (val is > -Epsilon and < Epsilon) + { + return PointOrientation.Collinear; // colinear + } + + return (val > 0) ? PointOrientation.Clockwise : PointOrientation.Counterclockwise; // clock or counterclock wise + } + + /// + /// Simplifies the collection of segments. + /// + /// The segments. + /// Weather the path is closed or open. + /// Whether to remove close and collinear vertices + /// + /// The . + /// + private static PointData[] Simplify(IReadOnlyList segments, bool isClosed, bool removeCloseAndCollinear) + { + // Pre-compute capacity from identity-transform vertex counts to avoid List resizing. + int totalPoints = 0; + for (int s = 0; s < segments.Count; s++) + { + totalPoints += segments[s].LinearVertexCount(Vector2.One); + } + + List simplified = new(totalPoints); + + // Track indices where collinear direction reversals represent user-intended + // geometry: interior points of multi-point linear segments, and junction + // points between two linear segments (e.g. PathBuilder LineTo → LineTo). + // Reversals at all other indices (flattened curves, curve junctions) are + // artifacts and should be removed normally. + HashSet? linearReversalIndices = null; + ILineSegment? prevSeg = null; + + foreach (ILineSegment seg in segments) + { + int start = simplified.Count; + int segmentCount = seg.LinearVertexCount(Vector2.One); + CollectionsMarshal.SetCount(simplified, start + segmentCount); + Span destination = CollectionsMarshal.AsSpan(simplified).Slice(start, segmentCount); + seg.CopyTo(destination, skipFirstPoint: false, Vector2.One); + + if (seg is LinearLineSegment) + { + // Interior points of a multi-point linear segment (e.g. DrawLine with 3+ points). + if (segmentCount > 2) + { + linearReversalIndices ??= []; + for (int i = start + 1; i < start + segmentCount - 1; i++) + { + _ = linearReversalIndices.Add(i); + } + } + + // Junction between two linear segments (e.g. PathBuilder LineTo → LineTo). + if (prevSeg is LinearLineSegment && start > 0) + { + linearReversalIndices ??= []; + _ = linearReversalIndices.Add(start); + } + } + + prevSeg = seg; + } + + return Simplify(CollectionsMarshal.AsSpan(simplified), isClosed, removeCloseAndCollinear, linearReversalIndices); + } + + private static PointData[] Simplify(ReadOnlySpan points, bool isClosed, bool removeCloseAndCollinear, HashSet? linearReversalIndices = null) + { + int polyCorners = points.Length; + if (polyCorners == 0) + { + return []; + } + + List results = new(polyCorners); + Vector2 lastPoint = points[0]; + + if (!isClosed) + { + results.Add(new PointData + { + Point = points[0], + Orientation = PointOrientation.Collinear, + Length = 0 + }); + } + else + { + int prev = polyCorners; + do + { + prev--; + if (prev == 0) + { + // All points are common, shouldn't match anything + results.Add( + new PointData + { + Point = points[0], + Orientation = PointOrientation.Collinear, + Length = 0, + }); + + return [.. results]; + } + } + while (removeCloseAndCollinear && Equivalent(points[0], points[prev], Epsilon2)); // skip points too close together + + polyCorners = prev + 1; + lastPoint = points[prev]; + + results.Add( + new PointData + { + Point = points[0], + Orientation = CalculateOrientation(lastPoint, points[0], points[1]), + Length = Vector2.Distance(lastPoint, points[0]), + }); + + lastPoint = points[0]; + } + + for (int i = 1; i < polyCorners; i++) + { + int next = WrapArrayIndex(i + 1, polyCorners); + PointOrientation or = CalculateOrientation(lastPoint, points[i], points[next]); + if (removeCloseAndCollinear && or == PointOrientation.Collinear && next != 0) + { + // Preserve collinear points that represent a direction reversal (U-turn) + // within a single segment. E.g. (10,10)→(90,10)→(20,10): the middle point + // is collinear but the stroker needs to see the reversal. + // Don't preserve reversals at segment boundaries — these arise from joining + // different path segments (e.g. arc-to-arc) and are not user-intended. + bool preserve = false; + if (linearReversalIndices == null || linearReversalIndices.Contains(i)) + { + Vector2 incoming = (Vector2)points[i] - lastPoint; + Vector2 outgoing = (Vector2)points[next] - (Vector2)points[i]; + float inLen = incoming.Length(); + float outLen = outgoing.Length(); + preserve = inLen > Epsilon && outLen > Epsilon && Vector2.Dot(incoming, outgoing) < 0; + } + + if (!preserve) + { + continue; + } + } + + results.Add( + new PointData + { + Point = points[i], + Orientation = or, + Length = Vector2.Distance(lastPoint, points[i]), + }); + lastPoint = points[i]; + } + + if (isClosed && removeCloseAndCollinear) + { + // walk back removing collinear points + while (results.Count > 2 && results[^1].Orientation == PointOrientation.Collinear) + { + results.RemoveAt(results.Count - 1); + } + } + + return [.. results]; + } + + /// + /// Determines whether two points are within the specified coordinate threshold of one another. + /// + /// The first point. + /// The second point. + /// The per-axis distance threshold. + /// + /// when both coordinates are within ; otherwise, . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool Equivalent(PointF source1, PointF source2, float threshold) + { + Vector2 abs = Vector2.Abs(source1 - source2); + return abs.X < threshold && abs.Y < threshold; + } + + private struct PointData + { + public PointF Point; + public PointOrientation Orientation; + public float Length; + } + } +} diff --git a/ImageSharp.Drawing/IntersectionRule.cs b/ImageSharp.Drawing/IntersectionRule.cs new file mode 100644 index 0000000..aea334b --- /dev/null +++ b/ImageSharp.Drawing/IntersectionRule.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Provides options for calculating intersection points. + /// + public enum IntersectionRule + { + /// + /// Only odd numbered sub-regions are filled. + /// + EvenOdd = 0, + + /// + /// Only non-zero sub-regions are filled. + /// + NonZero = 1 + } +} diff --git a/ImageSharp.Drawing/LineCap.cs b/ImageSharp.Drawing/LineCap.cs new file mode 100644 index 0000000..f7e999d --- /dev/null +++ b/ImageSharp.Drawing/LineCap.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing { + /// + public enum LineCap + { + /// + Butt, + + /// + Square, + + /// + Round + } +} diff --git a/ImageSharp.Drawing/LineJoin.cs b/ImageSharp.Drawing/LineJoin.cs new file mode 100644 index 0000000..914d4d4 --- /dev/null +++ b/ImageSharp.Drawing/LineJoin.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing { + /// + public enum LineJoin + { + /// + Miter = 0, + + /// + MiterRevert = 1, + + /// + Round = 2, + + /// + Bevel = 3, + + /// + MiterRound = 4 + } +} diff --git a/ImageSharp.Drawing/LinearContour.cs b/ImageSharp.Drawing/LinearContour.cs new file mode 100644 index 0000000..0a3e6ac --- /dev/null +++ b/ImageSharp.Drawing/LinearContour.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Describes a single contour within a . + /// + /// + /// A contour identifies a contiguous point run in and the corresponding range in + /// the derived segment stream exposed by . + /// + public readonly struct LinearContour + { + /// + /// Gets the zero-based index of the first point belonging to this contour in . + /// + public required int PointStart { get; init; } + + /// + /// Gets the number of stored points belonging to this contour. + /// + public required int PointCount { get; init; } + + /// + /// Gets the zero-based index of the first derived segment belonging to this contour. + /// + public required int SegmentStart { get; init; } + + /// + /// Gets the number of derived segments belonging to this contour. + /// + public required int SegmentCount { get; init; } + + /// + /// Gets a value indicating whether the contour is closed. + /// + /// + /// When , the final derived segment for the contour joins the last stored point back to + /// the first stored point. Closed contours do not duplicate the first point at the end of their stored point run. + /// + public required bool IsClosed { get; init; } + } +} diff --git a/ImageSharp.Drawing/LinearGeometry.cs b/ImageSharp.Drawing/LinearGeometry.cs new file mode 100644 index 0000000..3e88ea1 --- /dev/null +++ b/ImageSharp.Drawing/LinearGeometry.cs @@ -0,0 +1,181 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Represents retained linearized geometry that can be consumed directly by drawing backends. + /// + /// + /// + /// A instance stores contour-local point data plus the metadata required to + /// interpret those points as a sequence of final linear segments. + /// + /// + /// Closed contours do not duplicate their first point at the end of the stored point run. Closure is represented + /// by , and the closing segment is derived by . + /// + /// + /// The retained storage model is: + /// + /// + /// stores the concatenated point data for every contour. + /// maps each contour to its point run and derived segment range. + /// exposes geometry-wide metadata such as bounds and total segment count. + /// + /// + public sealed class LinearGeometry + { + private readonly LinearContour[] contours; + private readonly PointF[] points; + + /// + /// Initializes a new instance of the class. + /// + /// The geometry metadata. + /// The contour metadata. + /// The point storage. + public LinearGeometry(LinearGeometryInfo info, IReadOnlyList contours, IReadOnlyList points) + { + Guard.NotNull(contours, nameof(contours)); + Guard.NotNull(points, nameof(points)); + + this.Info = info; + this.contours = contours as LinearContour[] ?? [.. contours]; + this.points = points as PointF[] ?? [.. points]; + this.Contours = this.contours; + this.Points = this.points; + } + + /// + /// Gets geometry-wide metadata for this retained result. + /// + public LinearGeometryInfo Info { get; } + + /// + /// Gets the contour metadata describing how is partitioned. + /// + /// + /// Each entry defines one contour's point run and the corresponding segment range in the derived segment stream. + /// + public IReadOnlyList Contours { get; } + + /// + /// Gets the retained point storage for all contours in this geometry. + /// + /// + /// Points are stored per contour in contour order. A closed contour does not repeat its first point at the end + /// of its stored point run. + /// + public IReadOnlyList Points { get; } + + internal ReadOnlySpan GetContours() => this.contours; + + internal ReadOnlySpan GetContourPoints(in LinearContour contour) + => this.points.AsSpan(contour.PointStart, contour.PointCount); + + /// + /// Creates retained geometry for one open polyline, baked under the supplied device-space . + /// + /// The polyline points. + /// The X/Y scale at which the polyline is baked. + /// The retained open polyline geometry. + public static LinearGeometry CreateOpenPolyline(PointF[] points, Vector2 scale) + { + Guard.NotNull(points, nameof(points)); + Guard.MustBeGreaterThanOrEqualTo(points.Length, 2, nameof(points)); + + PointF[] retained; + if (scale == Vector2.One) + { + retained = points; + } + else + { + retained = new PointF[points.Length]; + for (int i = 0; i < points.Length; i++) + { + retained[i] = new PointF(points[i].X * scale.X, points[i].Y * scale.Y); + } + } + + RectangleF bounds = GetPointBounds(retained); + int segmentCount = retained.Length - 1; + int nonHorizontalBoundary = 0; + int nonHorizontalCenter = 0; + for (int i = 0; i < segmentCount; i++) + { + PointF start = retained[i]; + PointF end = retained[i + 1]; + if ((int)MathF.Floor(start.Y) != (int)MathF.Floor(end.Y)) + { + nonHorizontalBoundary++; + } + + if ((int)MathF.Floor(start.Y + 0.5F) != (int)MathF.Floor(end.Y + 0.5F)) + { + nonHorizontalCenter++; + } + } + + return new LinearGeometry( + new LinearGeometryInfo + { + Bounds = bounds, + ContourCount = 1, + PointCount = retained.Length, + SegmentCount = segmentCount, + NonHorizontalSegmentCountPixelBoundary = nonHorizontalBoundary, + NonHorizontalSegmentCountPixelCenter = nonHorizontalCenter + }, + [new LinearContour + { + PointStart = 0, + PointCount = retained.Length, + SegmentStart = 0, + SegmentCount = segmentCount, + IsClosed = false + } + ], + retained); + } + + /// + /// Creates retained geometry for one open polyline. + /// + /// The polyline points. + /// The retained open polyline geometry. + public static LinearGeometry CreateOpenPolyline(PointF[] points) + => CreateOpenPolyline(points, Vector2.One); + + /// + /// Gets an enumerator for the derived linear segments represented by and . + /// + /// + /// A zero-allocation enumerator that yields the final linear segments in contour order. + /// + public SegmentEnumerator GetSegments() => new(this); + + private static RectangleF GetPointBounds(PointF[] points) + { + float minX = points[0].X; + float minY = points[0].Y; + float maxX = minX; + float maxY = minY; + + for (int i = 1; i < points.Length; i++) + { + PointF point = points[i]; + minX = MathF.Min(minX, point.X); + minY = MathF.Min(minY, point.Y); + maxX = MathF.Max(maxX, point.X); + maxY = MathF.Max(maxY, point.Y); + } + + return RectangleF.FromLTRB(minX, minY, maxX, maxY); + } + } +} diff --git a/ImageSharp.Drawing/LinearGeometryCache.cs b/ImageSharp.Drawing/LinearGeometryCache.cs new file mode 100644 index 0000000..5a74ef1 --- /dev/null +++ b/ImageSharp.Drawing/LinearGeometryCache.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Threading; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Single-entry memoization slot for a scale-baked derived from an . + /// Entries are keyed on the X/Y scale so text and panning workloads — which re-render the same shapes at a fixed + /// zoom level while their rotation/translation/perspective drift — hit a stable cached bake. + /// + /// + /// Safe for concurrent readers and writers. Publication uses so a reader + /// either observes or a fully-constructed entry. + /// + internal struct LinearGeometryCache + { + private Entry? entry; + + public bool TryGet(Vector2 scale, [NotNullWhen(true)] out LinearGeometry? value) + { + Entry? hit = Volatile.Read(ref this.entry); + if (hit is not null && hit.Scale == scale) + { + value = hit.Value; + return true; + } + + value = null; + return false; + } + + public LinearGeometry Store(Vector2 scale, LinearGeometry value) + { + Volatile.Write(ref this.entry, new Entry(scale, value)); + return value; + } + + private sealed class Entry(Vector2 scale, LinearGeometry value) + { + public Vector2 Scale { get; } = scale; + + public LinearGeometry Value { get; } = value; + } + } +} diff --git a/ImageSharp.Drawing/LinearGeometryInfo.cs b/ImageSharp.Drawing/LinearGeometryInfo.cs new file mode 100644 index 0000000..2a55649 --- /dev/null +++ b/ImageSharp.Drawing/LinearGeometryInfo.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Describes geometry-wide metadata for a instance. + /// + /// + /// This metadata is computed eagerly during lowering so backends do not need to enumerate the geometry again to + /// discover basic information such as total segment count or bounds. + /// + public readonly struct LinearGeometryInfo + { + /// + /// Gets the bounds of all points stored in the containing . + /// + public required RectangleF Bounds { get; init; } + + /// + /// Gets the total number of contours in the containing . + /// + public required int ContourCount { get; init; } + + /// + /// Gets the total number of stored points across all contours. + /// + public required int PointCount { get; init; } + + /// + /// Gets the total number of derived linear segments across all contours. + /// + public required int SegmentCount { get; init; } + + /// + /// Gets the number of derived segments that remain non-horizontal when sampled on pixel boundaries. + /// + /// + /// A segment contributes to this count when its start and end sample into different rows under + /// pixel-boundary sampling. + /// + public required int NonHorizontalSegmentCountPixelBoundary { get; init; } + + /// + /// Gets the number of derived segments that remain non-horizontal when sampled at pixel centers. + /// + /// + /// A segment contributes to this count when its start and end sample into different rows after the + /// half-pixel center-sampling offset is applied. + /// + public required int NonHorizontalSegmentCountPixelCenter { get; init; } + } +} diff --git a/ImageSharp.Drawing/LinearLineSegment.cs b/ImageSharp.Drawing/LinearLineSegment.cs new file mode 100644 index 0000000..58b259a --- /dev/null +++ b/ImageSharp.Drawing/LinearLineSegment.cs @@ -0,0 +1,144 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Helpers; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Represents a series of control points that will be joined by straight lines + /// + /// + public sealed class LinearLineSegment : ILineSegment + { + /// + /// The collection of points. + /// + private readonly PointF[] points; + + /// + /// Initializes a new instance of the class. + /// + /// The start. + /// The end. + public LinearLineSegment(PointF start, PointF end) + : this([start, end]) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The point1. + /// The point2. + /// Additional points + public LinearLineSegment(PointF point1, PointF point2, params PointF[] additionalPoints) + : this(new[] { point1, point2 }.Concat(additionalPoints)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The points. + public LinearLineSegment(PointF[] points) + { + Guard.NotNull(points, nameof(points)); + Guard.MustBeGreaterThanOrEqualTo(points.Length, 2, nameof(points)); + this.points = points; + this.Bounds = CalculateBounds(points); + } + + /// + /// Gets the start point. + /// + public PointF StartPoint => this.points[0]; + + /// + /// Gets the end point. + /// + /// + /// The end point. + /// + public PointF EndPoint => this.points[^1]; + + /// + public RectangleF Bounds { get; } + + /// + public int LinearVertexCount(Vector2 scale) => this.points.Length; + + /// + public void CopyTo(Span destination, bool skipFirstPoint, Vector2 scale) + { + int startIndex = skipFirstPoint ? 1 : 0; + ReadOnlySpan source = this.points.AsSpan(startIndex); + + if (scale == Vector2.One) + { + source.CopyTo(destination); + return; + } + + for (int i = 0; i < source.Length; i++) + { + destination[i] = new PointF(source[i].X * scale.X, source[i].Y * scale.Y); + } + } + + /// + /// Transforms the current LineSegment using specified matrix. + /// + /// The matrix. + /// + /// A line segment with the matrix applied to it. + /// + public LinearLineSegment Transform(Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + // no transform to apply skip it + return this; + } + + PointF[] transformedPoints = new PointF[this.points.Length]; + + for (int i = 0; i < this.points.Length; i++) + { + transformedPoints[i] = PointF.Transform(this.points[i], matrix); + } + + return new LinearLineSegment(transformedPoints); + } + + /// + /// Transforms the current LineSegment using specified matrix. + /// + /// The matrix. + /// A line segment with the matrix applied to it. + ILineSegment ILineSegment.Transform(Matrix4x4 matrix) => this.Transform(matrix); + + /// + /// Computes the bounds for the retained linear point run. + /// + private static RectangleF CalculateBounds(ReadOnlySpan points) + { + float minX = float.MaxValue; + float minY = float.MaxValue; + float maxX = float.MinValue; + float maxY = float.MinValue; + + for (int i = 0; i < points.Length; i++) + { + PointF point = points[i]; + minX = MathF.Min(minX, point.X); + minY = MathF.Min(minY, point.Y); + maxX = MathF.Max(maxX, point.X); + maxY = MathF.Max(maxY, point.Y); + } + + return RectangleF.FromLTRB(minX, minY, maxX, maxY); + } + } +} diff --git a/ImageSharp.Drawing/LinearSegment.cs b/ImageSharp.Drawing/LinearSegment.cs new file mode 100644 index 0000000..fe87b68 --- /dev/null +++ b/ImageSharp.Drawing/LinearSegment.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Represents one derived linear segment within a . + /// + /// + /// Instances are produced by and contain the per-segment values required by current + /// backend scene-building code without forcing each backend to recompute them from the endpoints on every iteration. + /// + public readonly struct LinearSegment + { + /// + /// Gets the segment start point. + /// + public required PointF Start { get; init; } + + /// + /// Gets the segment end point. + /// + public required PointF End { get; init; } + + /// + /// Gets the smaller of . and .. + /// + public required float MinY { get; init; } + + /// + /// Gets the larger of . and .. + /// + public required float MaxY { get; init; } + + /// + /// Gets a value indicating whether the segment is horizontal. + /// + /// + /// A segment is horizontal when . equals + /// .. + /// + public required bool IsHorizontal { get; init; } + } +} diff --git a/ImageSharp.Drawing/OutlinePathExtensions.cs b/ImageSharp.Drawing/OutlinePathExtensions.cs new file mode 100644 index 0000000..b0438d5 --- /dev/null +++ b/ImageSharp.Drawing/OutlinePathExtensions.cs @@ -0,0 +1,119 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.PolygonGeometry; +using SixLabors.ImageSharp.Drawing.Processing; +using System; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Extensions to that allow the generation of outlines. + /// + public static class OutlinePathExtensions + { + private static readonly StrokeOptions DefaultOptions = new(); + + /// + /// Generates an outline of the path. + /// + /// The path to outline + /// The outline width. + /// A new representing the outline. + public static IPath GenerateOutline(this IPath path, float width) + => GenerateOutline(path, width, DefaultOptions); + + /// + /// Generates an outline of the path. + /// + /// The path to outline + /// The outline width. + /// The stroke geometry options. + /// A new representing the outline. + public static IPath GenerateOutline(this IPath path, float width, StrokeOptions strokeOptions) + { + if (width <= 0) + { + return Path.Empty; + } + + return StrokedShapeGenerator.GenerateStrokedShapes(path, width, strokeOptions); + } + + /// + /// Generates an outline of the path with alternating on and off segments based on the pattern. + /// + /// The path to outline + /// The outline width. + /// The pattern made of multiples of the width. + /// A new representing the outline. + public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern) + => path.GenerateOutline(width, pattern, false); + + /// + /// Generates an outline of the path with alternating on and off segments based on the pattern. + /// + /// The path to outline + /// The outline width. + /// The pattern made of multiples of the width. + /// The stroke geometry options. + /// A new representing the outline. + public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, StrokeOptions strokeOptions) + => GenerateOutline(path, width, pattern, false, strokeOptions); + + /// + /// Generates an outline of the path with alternating on and off segments based on the pattern. + /// + /// The path to outline + /// The outline width. + /// The pattern made of multiples of the width. + /// Whether the first item in the pattern is on or off. + /// A new representing the outline. + public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool startOff) + => GenerateOutline(path, width, pattern, startOff, DefaultOptions); + + /// + /// Generates an outline of the path with alternating on and off segments based on the pattern. + /// + /// The path to outline + /// The outline width. + /// The pattern made of multiples of the width. + /// Whether the first item in the pattern is on or off. + /// The stroke geometry options. + /// A new representing the outline. + public static IPath GenerateOutline( + this IPath path, + float width, + ReadOnlySpan pattern, + bool startOff, + StrokeOptions strokeOptions) + { + if (width <= 0) + { + return Path.Empty; + } + + if (pattern.Length < 2) + { + return path.GenerateOutline(width, strokeOptions); + } + + IPath dashed = path.GenerateDashes(width, pattern, startOff); + + // GenerateDashes returns the original path when the pattern is degenerate + // or when segmentation would exceed safety limits; stroke it as solid. + if (ReferenceEquals(dashed, path)) + { + return path.GenerateOutline(width, strokeOptions); + } + + if (dashed == Path.Empty) + { + return Path.Empty; + } + + // Each dash segment is an open sub-path; stroke expansion and boolean merge + // are handled by the generator. + return StrokedShapeGenerator.GenerateStrokedShapes(dashed, width, strokeOptions); + } + } +} diff --git a/ImageSharp.Drawing/Path.cs b/ImageSharp.Drawing/Path.cs new file mode 100644 index 0000000..0ad9015 --- /dev/null +++ b/ImageSharp.Drawing/Path.cs @@ -0,0 +1,718 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// A aggregate of s making a single logical path. + /// + /// + public class Path : IPath, ISimplePath, IPathInternals, IInternalPathOwner + { + private readonly ILineSegment[] lineSegments; + private InternalPath? innerPath; + private IReadOnlyList? internalPathRings; + private IPath? closedPath; + private LinearGeometryCache geometryCache; + private RectangleF? bounds; + + /// + /// Initializes a new instance of the class. + /// + /// The collection of points; processed as a series of linear line segments. + public Path(PointF[] points) + : this(new LinearLineSegment(points)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The segments. + public Path(IEnumerable segments) + : this(GetSegmentArray(segments)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The path. + public Path(Path path) + : this(path.LineSegments) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The segments. + public Path(params ILineSegment[] segments) + { + Guard.NotNull(segments, nameof(segments)); + this.lineSegments = segments; + } + + /// + /// Gets the default empty path. + /// + public static IPath Empty { get; } = EmptyPath.OpenPath; + + /// + bool ISimplePath.IsClosed => this.IsClosed; + + /// + public virtual bool IsClosed => false; + + /// + public ReadOnlyMemory Points => this.InnerPath.Points(); + + /// + public RectangleF Bounds => this.bounds ??= this.CalculateBounds(); + + /// + public PathTypes PathType => this.IsClosed ? PathTypes.Closed : PathTypes.Open; + + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + internal int MaxIntersections => this.InnerPath.PointCount; + + /// + /// Gets readonly collection of line segments. + /// + public IReadOnlyList LineSegments => this.lineSegments; + + /// + /// Gets or sets a value indicating whether close or collinear vertices should be removed. TEST ONLY! + /// + internal bool RemoveCloseAndCollinearPoints { get; set; } = true; + + private protected InternalPath InnerPath => + this.innerPath ??= new InternalPath(this.lineSegments, this.IsClosed, this.RemoveCloseAndCollinearPoints); + + /// + public virtual IPath Transform(Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + return this; + } + + ILineSegment[] segments = new ILineSegment[this.lineSegments.Length]; + + for (int i = 0; i < segments.Length; i++) + { + segments[i] = this.lineSegments[i].Transform(matrix); + } + + return new Path(segments); + } + + /// + public IPath AsClosedPath() + { + if (this.IsClosed) + { + return this; + } + + return this.closedPath ??= new Polygon(this.LineSegments); + } + + /// + public IEnumerable Flatten() + { + yield return this; + } + + /// + public virtual LinearGeometry ToLinearGeometry(Vector2 scale) + => this.geometryCache.TryGet(scale, out LinearGeometry? hit) + ? hit + : this.geometryCache.Store(scale, this.BuildLinearGeometry(scale)); + + private LinearGeometry BuildLinearGeometry(Vector2 scale) + { + if (this.lineSegments.Length == 0) + { + return new LinearGeometry( + new LinearGeometryInfo + { + Bounds = RectangleF.Empty, + ContourCount = 0, + PointCount = 0, + SegmentCount = 0, + NonHorizontalSegmentCountPixelBoundary = 0, + NonHorizontalSegmentCountPixelCenter = 0 + }, + [], + []); + } + + PointF? lastEndPoint = null; + int pointCount = 0; + + for (int i = 0; i < this.lineSegments.Length; i++) + { + ILineSegment segment = this.lineSegments[i]; + bool skipFirstPoint = lastEndPoint?.Equals(segment.StartPoint) == true; + pointCount += segment.LinearVertexCount(scale) - (skipFirstPoint ? 1 : 0); + lastEndPoint = segment.EndPoint; + } + + PointF[] points = new PointF[pointCount]; + LinearContour[] contours = pointCount == 0 ? [] : new LinearContour[1]; + + bool hasBounds = false; + float minX = float.MaxValue; + float minY = float.MaxValue; + float maxX = float.MinValue; + float maxY = float.MinValue; + int nonHorizontalSegmentCountPixelBoundary = 0; + int nonHorizontalSegmentCountPixelCenter = 0; + int pointIndex = 0; + lastEndPoint = null; + + for (int i = 0; i < this.lineSegments.Length; i++) + { + ILineSegment segment = this.lineSegments[i]; + bool skipFirstPoint = lastEndPoint?.Equals(segment.StartPoint) == true; + int contributionCount = segment.LinearVertexCount(scale) - (skipFirstPoint ? 1 : 0); + Span destination = points.AsSpan(pointIndex, contributionCount); + + segment.CopyTo(destination, skipFirstPoint, scale); + lastEndPoint = segment.EndPoint; + + for (int p = 0; p < destination.Length; p++) + { + PointF point = destination[p]; + minX = MathF.Min(minX, point.X); + minY = MathF.Min(minY, point.Y); + maxX = MathF.Max(maxX, point.X); + maxY = MathF.Max(maxY, point.Y); + hasBounds = true; + } + + pointIndex += contributionCount; + } + + int segmentCount = pointCount == 0 ? 0 : this.IsClosed ? pointCount : pointCount - 1; + CountNonHorizontalSegments(points, pointCount, this.IsClosed, ref nonHorizontalSegmentCountPixelBoundary, ref nonHorizontalSegmentCountPixelCenter); + + if (pointCount > 0) + { + contours[0] = new LinearContour + { + PointStart = 0, + PointCount = pointCount, + SegmentStart = 0, + SegmentCount = segmentCount, + IsClosed = this.IsClosed + }; + } + + RectangleF bounds = hasBounds ? RectangleF.FromLTRB(minX, minY, maxX, maxY) : RectangleF.Empty; + + return new LinearGeometry( + new LinearGeometryInfo + { + Bounds = bounds, + ContourCount = contours.Length, + PointCount = points.Length, + SegmentCount = segmentCount, + NonHorizontalSegmentCountPixelBoundary = nonHorizontalSegmentCountPixelBoundary, + NonHorizontalSegmentCountPixelCenter = nonHorizontalSegmentCountPixelCenter + }, + contours, + points); + } + + /// + SegmentInfo IPathInternals.PointAlongPath(float distance) + => this.InnerPath.PointAlongPath(distance); + + /// + IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() + => this.internalPathRings ??= [this.InnerPath]; + + /// + /// Computes path bounds directly from segment bounds without materializing . + /// + private RectangleF CalculateBounds() + { + if (this.lineSegments.Length == 0) + { + return RectangleF.Empty; + } + + RectangleF bounds = this.lineSegments[0].Bounds; + + for (int i = 1; i < this.lineSegments.Length; i++) + { + bounds = RectangleF.Union(bounds, this.lineSegments[i].Bounds); + } + + return bounds; + } + + /// + /// Materializes the segment sequence into the retained array used by the path. + /// + /// The segment sequence to materialize. + /// The retained segment array. + private static ILineSegment[] GetSegmentArray(IEnumerable segments) + { + Guard.NotNull(segments, nameof(segments)); + return segments as ILineSegment[] ?? [.. segments]; + } + + /// + /// Counts how many derived segments survive as non-horizontal raster work for each sampling origin. + /// + /// The retained contour point run. + /// The number of retained points in the contour. + /// Whether the contour closes back to its first point. + /// The accumulated pixel-boundary count to update. + /// The accumulated pixel-center count to update. + private static void CountNonHorizontalSegments( + ReadOnlySpan points, + int pointCount, + bool isClosed, + ref int nonHorizontalSegmentCountPixelBoundary, + ref int nonHorizontalSegmentCountPixelCenter) + { + if (pointCount <= 1) + { + return; + } + + int segmentCount = isClosed ? pointCount : pointCount - 1; + for (int i = 0; i < segmentCount; i++) + { + PointF start = points[i]; + PointF end = points[(i + 1) == pointCount ? 0 : i + 1]; + if (ToFixedBoundary(start.Y) != ToFixedBoundary(end.Y)) + { + nonHorizontalSegmentCountPixelBoundary++; + } + + if (ToFixedCenter(start.Y) != ToFixedCenter(end.Y)) + { + nonHorizontalSegmentCountPixelCenter++; + } + } + } + + /// + /// Converts a coordinate to the fixed-point row space used by boundary-sampled raster work. + /// + /// The coordinate to convert. + /// The rounded 24.8 fixed-point value. + private static int ToFixedBoundary(float value) => (int)MathF.Round(value * 256F); + + /// + /// Converts a coordinate to the fixed-point row space used by center-sampled raster work. + /// + /// The coordinate to convert. + /// The rounded 24.8 fixed-point value after the half-pixel sampling offset is applied. + private static int ToFixedCenter(float value) => (int)MathF.Round((value + 0.5F) * 256F); + + /// + /// Converts an SVG path string into an . + /// + /// The string containing the SVG path data. + /// + /// When this method returns, contains the logic path converted from the given SVG path string; otherwise, . + /// This parameter is passed uninitialized. + /// + /// if the input value can be parsed and converted; otherwise, . + public static bool TryParseSvgPath(string svgPath, [NotNullWhen(true)] out IPath? value) + => TryParseSvgPath(svgPath.AsSpan(), out value); + + /// + /// Converts an SVG path string into an . + /// + /// The string containing the SVG path data. + /// + /// When this method returns, contains the logic path converted from the given SVG path string; otherwise, . + /// This parameter is passed uninitialized. + /// + /// if the input value can be parsed and converted; otherwise, . + public static bool TryParseSvgPath(ReadOnlySpan svgPath, [NotNullWhen(true)] out IPath? value) + { + value = null; + + PathBuilder builder = new(); + + PointF first = PointF.Empty; + PointF c = PointF.Empty; + PointF lastc = PointF.Empty; + PointF point1; + PointF point2; + PointF point3; + + char op = '\0'; + char previousOp = '\0'; + bool relative = false; + while (true) + { + svgPath = svgPath.TrimStart(); + if (svgPath.Length == 0) + { + break; + } + + char ch = svgPath[0]; + if (char.IsDigit(ch) || ch == '-' || ch == '+' || ch == '.') + { + // SVG allows repeated operand groups to reuse the previous command. + // A leading number is only valid once a drawable command is active. + if (op is '\0' or 'Z') + { + return false; + } + } + else if (IsSeparator(ch)) + { + svgPath = TrimSeparator(svgPath); + } + else + { + op = ch; + relative = false; + if (char.IsLower(op)) + { + op = char.ToUpper(op, CultureInfo.InvariantCulture); + relative = true; + } + + svgPath = TrimSeparator(svgPath[1..]); + } + + // Read every operand for the command before appending geometry. That keeps + // malformed or truncated data from leaking a partially parsed segment into the path. + switch (op) + { + case 'M': + if (!TryFindPoint(ref svgPath, relative, c, out point1)) + { + return false; + } + + _ = builder.MoveTo(point1); + previousOp = '\0'; + + // Extra coordinate pairs after a move command are implicit line commands. + op = 'L'; + c = point1; + break; + case 'L': + if (!TryFindPoint(ref svgPath, relative, c, out point1)) + { + return false; + } + + _ = builder.LineTo(point1); + c = point1; + break; + case 'H': + if (!TryFindScaler(ref svgPath, out float x)) + { + return false; + } + + if (relative) + { + x += c.X; + } + + if (!float.IsFinite(x)) + { + return false; + } + + _ = builder.LineTo(x, c.Y); + c.X = x; + break; + case 'V': + if (!TryFindScaler(ref svgPath, out float y)) + { + return false; + } + + if (relative) + { + y += c.Y; + } + + if (!float.IsFinite(y)) + { + return false; + } + + _ = builder.LineTo(c.X, y); + c.Y = y; + break; + case 'C': + if (!TryFindPoint(ref svgPath, relative, c, out point1) + || !TryFindPoint(ref svgPath, relative, c, out point2) + || !TryFindPoint(ref svgPath, relative, c, out point3)) + { + return false; + } + + _ = builder.CubicBezierTo(point1, point2, point3); + lastc = point2; + c = point3; + break; + case 'S': + if (!TryFindPoint(ref svgPath, relative, c, out point2) + || !TryFindPoint(ref svgPath, relative, c, out point3)) + { + return false; + } + + point1 = c; + if (previousOp is 'C' or 'S') + { + // Smooth cubic curves mirror the previous cubic control point. + // Without a preceding cubic command, the current point is the control point. + point1.X -= lastc.X - c.X; + point1.Y -= lastc.Y - c.Y; + } + + _ = builder.CubicBezierTo(point1, point2, point3); + lastc = point2; + c = point3; + break; + case 'Q': // Quadratic Bezier Curve + if (!TryFindPoint(ref svgPath, relative, c, out point1) + || !TryFindPoint(ref svgPath, relative, c, out point2)) + { + return false; + } + + _ = builder.QuadraticBezierTo(point1, point2); + lastc = point1; + c = point2; + break; + case 'T': + if (!TryFindPoint(ref svgPath, relative, c, out point2)) + { + return false; + } + + point1 = c; + if (previousOp is 'Q' or 'T') + { + // Smooth quadratic curves mirror the previous quadratic control point. + // Without a preceding quadratic command, the current point is the control point. + point1.X -= lastc.X - c.X; + point1.Y -= lastc.Y - c.Y; + } + + _ = builder.QuadraticBezierTo(point1, point2); + lastc = point1; + c = point2; + break; + case 'A': + // Arc flags are single SVG grammar tokens, not numbers. Reading them as + // scalars would accept malformed flag/end-point boundaries such as "04445". + if (!TryFindScaler(ref svgPath, out float radiiX) + || !TryTrimSeparator(ref svgPath) + || !TryFindScaler(ref svgPath, out float radiiY) + || !TryTrimSeparator(ref svgPath) + || !TryFindScaler(ref svgPath, out float angle) + || !TryTrimSeparator(ref svgPath) + || !TryFindFlag(ref svgPath, out bool largeArc) + || !TryTrimSeparator(ref svgPath) + || !TryFindFlag(ref svgPath, out bool sweep) + || !TryFindPoint(ref svgPath, relative, c, out PointF point)) + { + return false; + } + + _ = builder.ArcTo(radiiX, radiiY, angle, largeArc, sweep, point); + c = point; + break; + case 'Z': + _ = builder.CloseFigure(); + c = first; + break; + case '~': + if (!TryFindPoint(ref svgPath, relative, c, out point1) + || !TryFindPoint(ref svgPath, relative, c, out point2)) + { + return false; + } + + _ = builder.MoveTo(point1).LineTo(point2); + break; + default: + return false; + } + + if (previousOp == 0) + { + first = c; + } + + previousOp = op; + } + + value = builder.Build(); + return true; + } + + private static bool TryFindFlag(ref ReadOnlySpan str, out bool value) + { + str = TrimSeparator(str); + + // https://www.w3.org/TR/SVG11/paths.html#PathDataBNF + // flag: "0" | "1" + // Adjacent flags are valid, so this consumes exactly one character. + if (str.Length == 0 || (str[0] is not '0' and not '1')) + { + value = default; + return false; + } + + value = str[0] == '1'; + str = str[1..]; + return true; + } + + private static bool TryTrimSeparator(ref ReadOnlySpan str) + { + // SVG separators are optional in places where the next token can be + // recognized unambiguously. Keep this chainable with the operand readers. + ReadOnlySpan result = TrimSeparator(str); + if (str[^result.Length..].StartsWith(result)) + { + str = result; + return true; + } + + return false; + } + + private static bool TryFindScaler(ref ReadOnlySpan str, out float value) + { + ReadOnlySpan source = TrimSeparator(str); + if (TryReadScalar(source, out value, out int length)) + { + str = source[length..]; + return true; + } + + value = default; + return false; + } + + private static bool TryFindPoint(ref ReadOnlySpan str, bool relative, PointF current, out PointF value) + { + if (TryFindScaler(ref str, out float x) && TryFindScaler(ref str, out float y)) + { + // Relative operands can overflow after adding the current point even when + // each parsed scalar is finite, so validate the absolute result as well. + if (relative) + { + x += current.X; + y += current.Y; + } + + if (!float.IsFinite(x) || !float.IsFinite(y)) + { + value = default; + return false; + } + + value = new PointF(x, y); + return true; + } + + value = default; + return false; + } + + private static bool TryReadScalar(ReadOnlySpan str, out float scaler, out int length) + { + // SVG path numbers can be tightly packed: "10-20" is two numbers, as is + // "0.5.6". Stop at the first character that belongs to the next token. + bool hasDot = false; + for (int i = 0; i < str.Length; i++) + { + char ch = str[i]; + + if (IsSeparator(ch)) + { + length = i; + return TryParseFloat(str[..length], out scaler); + } + + if (ch == '.') + { + if (hasDot) + { + // Second decimal point starts a new number. + length = i; + return TryParseFloat(str[..length], out scaler); + } + + hasDot = true; + } + else if ((ch is '-' or '+') && i > 0) + { + // A sign character mid-number starts a new number, + // unless it follows an exponent indicator. + char prev = str[i - 1]; + if (prev is not 'e' and not 'E') + { + length = i; + return TryParseFloat(str[..length], out scaler); + } + } + else if (char.IsLetter(ch)) + { + // Hit a command letter; end this number. + length = i; + return TryParseFloat(str[..length], out scaler); + } + } + + length = str.Length; + return TryParseFloat(str, out scaler); + } + + private static bool IsSeparator(char ch) + => char.IsWhiteSpace(ch) || ch == ','; + + private static ReadOnlySpan TrimSeparator(ReadOnlySpan data) + { + if (data.Length == 0) + { + return data; + } + + int idx = 0; + for (; idx < data.Length; idx++) + { + if (!IsSeparator(data[idx])) + { + break; + } + } + + return data[idx..]; + } + + private static bool TryParseFloat(ReadOnlySpan str, out float value) + => float.TryParse(str, CultureInfo.InvariantCulture, out value) && float.IsFinite(value); + } +} diff --git a/ImageSharp.Drawing/PathBuilder.cs b/ImageSharp.Drawing/PathBuilder.cs new file mode 100644 index 0000000..ce82bce --- /dev/null +++ b/ImageSharp.Drawing/PathBuilder.cs @@ -0,0 +1,787 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Allow you to derivatively build shapes and paths. + /// + public class PathBuilder + { + private readonly List
figures = []; + private readonly Matrix4x4 defaultTransform; + private Figure currentFigure; + private Matrix4x4 currentTransform; + private Matrix4x4 setTransform; + private Vector2 currentPoint; + + /// + /// Initializes a new instance of the class. + /// + public PathBuilder() + : this(Matrix4x4.Identity) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The default transform. + public PathBuilder(Matrix4x4 defaultTransform) + { + this.defaultTransform = defaultTransform; + this.Clear(); + _ = this.ResetTransform(); + } + + /// + /// Gets the current transformation matrix. + /// + /// + /// Returns a copy of the matrix. Because is a value type, + /// modifications to the returned value do not affect the internal state. To change the transform, + /// call . + /// + /// The current transformation matrix. + public Matrix4x4 Transform => this.currentTransform; + + /// + /// Sets the translation to be applied to all items to follow being applied to the . + /// + /// The transform. + /// The . + public PathBuilder SetTransform(Matrix4x4 transform) + { + this.setTransform = transform; + this.currentTransform = this.setTransform * this.defaultTransform; + return this; + } + + /// + /// Sets the origin all subsequent point should be relative to. + /// + /// The origin. + /// The . + public PathBuilder SetOrigin(PointF origin) + { + // The new origin should be transformed based on the default transform + this.setTransform.Translation = new Vector3(origin.X, origin.Y, 0); + this.currentTransform = this.setTransform * this.defaultTransform; + + return this; + } + + /// + /// Resets the transform to the default. + /// + /// The . + public PathBuilder ResetTransform() + { + this.setTransform = Matrix4x4.Identity; + this.currentTransform = this.setTransform * this.defaultTransform; + + return this; + } + + /// + /// Resets the origin to the default. + /// + /// The . + public PathBuilder ResetOrigin() + { + this.setTransform.Translation = Vector3.Zero; + this.currentTransform = this.setTransform * this.defaultTransform; + + return this; + } + + /// + /// Moves to current point to the supplied vector. + /// + /// The point. + /// The . + public PathBuilder MoveTo(PointF point) + { + _ = this.StartFigure(); + this.currentPoint = PointF.Transform(point, this.currentTransform); + return this; + } + + /// + /// Moves to current point to the supplied vector. + /// + /// The x-coordinate. + /// The y-coordinate. + /// The + public PathBuilder MoveTo(float x, float y) + => this.MoveTo(new PointF(x, y)); + + /// + /// Draws the line connecting the current the current point to the new point. + /// + /// The point. + /// The . + public PathBuilder LineTo(PointF point) + => this.AddLine(this.currentPoint, point); + + /// + /// Draws the line connecting the current the current point to the new point. + /// + /// The x. + /// The y. + /// The + public PathBuilder LineTo(float x, float y) + => this.LineTo(new PointF(x, y)); + + /// + /// Adds the line connecting the current point to the new point. + /// + /// The start. + /// The end. + /// The . + public PathBuilder AddLine(PointF start, PointF end) + => this.AddSegment(new LinearLineSegment(start, end)); + + /// + /// Adds the line connecting the current point to the new point. + /// + /// The x1. + /// The y1. + /// The x2. + /// The y2. + /// The . + public PathBuilder AddLine(float x1, float y1, float x2, float y2) + => this.AddLine(new PointF(x1, y1), new PointF(x2, y2)); + + /// + /// Adds a series of line segments connecting the current point to the new points. + /// + /// The points. + /// The . + public PathBuilder AddLines(IEnumerable points) + { + Guard.NotNull(points, nameof(points)); + return this.AddLines([.. points]); + } + + /// + /// Adds a series of line segments connecting the current point to the new points. + /// + /// The points. + /// The . + public PathBuilder AddLines(params PointF[] points) + { + Guard.NotNull(points, nameof(points)); + return this.AddSegment(new LinearLineSegment(points)); + } + + /// + /// Adds the segment. + /// + /// The segment. + /// The . + public PathBuilder AddSegment(ILineSegment segment) + { + Guard.NotNull(segment, nameof(segment)); + + segment = segment.Transform(this.currentTransform); + this.currentFigure.AddSegment(segment); + this.currentPoint = segment.EndPoint; + return this; + } + + /// + /// Draws a quadratic bezier from the current point to the + /// + /// The second control point. + /// The point. + /// The . + public PathBuilder QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) + => this.AddQuadraticBezier(this.currentPoint, secondControlPoint, point); + + /// + /// Draws a quadratic bezier from the current point to the + /// + /// The second control point. + /// The third control point. + /// The point. + /// The . + public PathBuilder CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) + => this.AddCubicBezier(this.currentPoint, secondControlPoint, thirdControlPoint, point); + + /// + /// Adds a quadratic bezier curve to the current figure joining the point to the . + /// + /// The start point. + /// The control point1. + /// The end point. + /// The . + public PathBuilder AddQuadraticBezier(PointF startPoint, PointF controlPoint, PointF endPoint) + { + Vector2 startPointVector = startPoint; + Vector2 controlPointVector = controlPoint; + Vector2 endPointVector = endPoint; + + Vector2 c1 = ((controlPointVector - startPointVector) * 2 / 3) + startPointVector; + Vector2 c2 = ((controlPointVector - endPointVector) * 2 / 3) + endPointVector; + + return this.AddCubicBezier(startPointVector, c1, c2, endPoint); + } + + /// + /// Adds a cubic bezier curve to the current figure joining the point to the . + /// + /// The start point. + /// The control point1. + /// The control point2. + /// The end point. + /// The . + public PathBuilder AddCubicBezier(PointF startPoint, PointF controlPoint1, PointF controlPoint2, PointF endPoint) + => this.AddSegment(new CubicBezierLineSegment(startPoint, controlPoint1, controlPoint2, endPoint)); + + /// + /// + /// Adds an elliptical arc to the current figure. The arc curves from the last point to , + /// choosing one of four possible routes: clockwise or counterclockwise, and smaller or larger. + /// + /// + /// The arc sweep is always less than 360 degrees. The method appends a line + /// to the last point if either radii are zero, or if last point is equal to . + /// In addition the method scales the radii to fit last point and if both + /// are greater than zero but too small to describe an arc. + /// + /// + /// The x-radius of the ellipsis. + /// The y-radius of the ellipsis. + /// The rotation along the X-axis; measured in degrees clockwise. + /// + /// The large arc flag, and is if an arc spanning less than or equal to 180 degrees + /// is chosen, or if an arc spanning greater than 180 degrees is chosen. + /// + /// + /// The sweep flag, and is if the line joining center to arc sweeps through decreasing + /// angles, or if it sweeps through increasing angles. + /// + /// The end point of the arc. + /// The . + public PathBuilder ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, PointF point) + => this.AddArc(this.currentPoint, radiusX, radiusY, rotation, largeArc, sweep, point); + + /// + /// + /// Adds an elliptical arc to the current figure. The arc curves from the to , + /// choosing one of four possible routes: clockwise or counterclockwise, and smaller or larger. + /// + /// + /// The arc sweep is always less than 360 degrees. The method appends a line + /// to the last point if either radii are zero, or if last point is equal to . + /// In addition the method scales the radii to fit last point and if both + /// are greater than zero but too small to describe an arc. + /// + /// + /// The start point of the arc. + /// The x-radius of the ellipsis. + /// The y-radius of the ellipsis. + /// The rotation along the X-axis; measured in degrees clockwise. + /// + /// The large arc flag, and is if an arc spanning less than or equal to 180 degrees + /// is chosen, or if an arc spanning greater than 180 degrees is chosen. + /// + /// + /// The sweep flag, and is if the line joining center to arc sweeps through decreasing + /// angles, or if it sweeps through increasing angles. + /// + /// The end point of the arc. + /// The . + public PathBuilder AddArc(PointF startPoint, float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, PointF endPoint) + => this.AddSegment(new ArcLineSegment(startPoint, endPoint, new SizeF(radiusX, radiusY), rotation, largeArc, sweep)); + + /// + /// Adds an elliptical arc to the current figure. + /// + /// A that represents the rectangular bounds of the ellipse from which the arc is taken. + /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. + /// + /// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle). + /// + /// The angle between and the end of the arc. + /// The . + public PathBuilder AddArc(RectangleF rectangle, float rotation, float startAngle, float sweepAngle) + => this.AddArc((rectangle.Right + rectangle.Left) / 2, (rectangle.Bottom + rectangle.Top) / 2, rectangle.Width / 2, rectangle.Height / 2, rotation, startAngle, sweepAngle); + + /// + /// Adds an elliptical arc to the current figure. + /// + /// A that represents the rectangular bounds of the ellipse from which the arc is taken. + /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. + /// + /// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle). + /// + /// The angle between and the end of the arc. + /// The . + public PathBuilder AddArc(Rectangle rectangle, int rotation, int startAngle, int sweepAngle) + => this.AddArc((RectangleF)rectangle, rotation, startAngle, sweepAngle); + + /// + /// Adds an elliptical arc to the current figure. + /// + /// The center of the ellipse from which the arc is taken. + /// The x-radius of the ellipsis. + /// The y-radius of the ellipsis. + /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. + /// + /// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle). + /// + /// The angle between and the end of the arc. + /// The . + public PathBuilder AddArc(PointF center, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle) + => this.AddArc(center.X, center.Y, radiusX, radiusY, rotation, startAngle, sweepAngle); + + /// + /// Adds an elliptical arc to the current figure. + /// + /// The center of the ellipse from which the arc is taken. + /// The x-radius of the ellipsis. + /// The y-radius of the ellipsis. + /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. + /// + /// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle). + /// + /// The angle between and the end of the arc. + /// The . + public PathBuilder AddArc(Point center, int radiusX, int radiusY, int rotation, int startAngle, int sweepAngle) + => this.AddArc((PointF)center, radiusX, radiusY, rotation, startAngle, sweepAngle); + + /// + /// Adds an elliptical arc to the current figure. + /// + /// The x-coordinate of the center point of the ellipse from which the arc is taken. + /// The y-coordinate of the center point of the ellipse from which the arc is taken. + /// The x-radius of the ellipsis. + /// The y-radius of the ellipsis. + /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. + /// + /// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle). + /// + /// The angle between and the end of the arc. + /// The . + public PathBuilder AddArc(int x, int y, int radiusX, int radiusY, int rotation, int startAngle, int sweepAngle) + => this.AddSegment(new ArcLineSegment(new PointF(x, y), new SizeF(radiusX, radiusY), rotation, startAngle, sweepAngle)); + + /// + /// Adds an elliptical arc to the current figure. + /// + /// The x-coordinate of the center point of the ellipse from which the arc is taken. + /// The y-coordinate of the center point of the ellipse from which the arc is taken. + /// The x-radius of the ellipsis. + /// The y-radius of the ellipsis. + /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. + /// + /// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle). + /// + /// The angle between and the end of the arc. + /// The . + public PathBuilder AddArc(float x, float y, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle) + => this.AddSegment(new ArcLineSegment(new PointF(x, y), new SizeF(radiusX, radiusY), rotation, startAngle, sweepAngle)); + + /// + /// Adds a pie sector to the current path as a closed figure. + /// + /// The center point of the pie sector. + /// The x and y radii of the pie ellipse. + /// The ellipse rotation in degrees. + /// The pie start angle in degrees. + /// The pie sweep angle in degrees. + /// The . + public PathBuilder AddPie(PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle) + { + _ = this.StartFigure(); + + foreach (ILineSegment segment in new PiePolygon(center, radius, rotation, startAngle, sweepAngle).LineSegments) + { + _ = this.AddSegment(segment); + } + + return this.CloseFigure(); + } + + /// + /// Adds a pie sector to the current path as a closed figure. + /// + /// The center point of the pie sector. + /// The x and y radii of the pie ellipse. + /// The pie start angle in degrees. + /// The pie sweep angle in degrees. + /// The . + public PathBuilder AddPie(PointF center, SizeF radius, float startAngle, float sweepAngle) + => this.AddPie(center, radius, 0F, startAngle, sweepAngle); + + /// + /// Adds a pie sector to the current path as a closed figure. + /// + /// The x-coordinate of the pie center. + /// The y-coordinate of the pie center. + /// The x-radius of the pie ellipse. + /// The y-radius of the pie ellipse. + /// The ellipse rotation in degrees. + /// The pie start angle in degrees. + /// The pie sweep angle in degrees. + /// The . + public PathBuilder AddPie(float x, float y, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle) + => this.AddPie(new PointF(x, y), new SizeF(radiusX, radiusY), rotation, startAngle, sweepAngle); + + /// + /// Adds a pie sector to the current path as a closed figure. + /// + /// The x-coordinate of the pie center. + /// The y-coordinate of the pie center. + /// The x-radius of the pie ellipse. + /// The y-radius of the pie ellipse. + /// The pie start angle in degrees. + /// The pie sweep angle in degrees. + /// The . + public PathBuilder AddPie(float x, float y, float radiusX, float radiusY, float startAngle, float sweepAngle) + => this.AddPie(x, y, radiusX, radiusY, 0F, startAngle, sweepAngle); + + /// + /// Adds a rectangle to the current path as a closed figure. + /// + /// The rectangle bounds. + /// The . + public PathBuilder AddRectangle(RectangleF rectangle) + => this.AddRectangle(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + + /// + /// Adds a rectangle to the current path as a closed figure. + /// + /// The rectangle bounds. + /// The . + public PathBuilder AddRectangle(Rectangle rectangle) + => this.AddRectangle((RectangleF)rectangle); + + /// + /// Adds a rectangle to the current path as a closed figure. + /// + /// The x-coordinate of the rectangle. + /// The y-coordinate of the rectangle. + /// The rectangle width. + /// The rectangle height. + /// The . + public PathBuilder AddRectangle(float x, float y, float width, float height) + => this.AddPolygon( + new PointF(x, y), + new PointF(x + width, y), + new PointF(x + width, y + height), + new PointF(x, y + height)); + + /// + /// Adds a rounded rectangle to the current path as a closed figure. + /// + /// The rectangle bounds. + /// The x and y radius of each corner. + /// The . + public PathBuilder AddRoundedRectangle(RectangleF rectangle, float radius) + => this.AddRoundedRectangle(rectangle, new SizeF(radius, radius)); + + /// + /// Adds a rounded rectangle to the current path as a closed figure. + /// + /// The rectangle bounds. + /// The x and y radii of each corner. + /// The . + public PathBuilder AddRoundedRectangle(RectangleF rectangle, SizeF radius) + { + _ = this.StartFigure(); + + foreach (ILineSegment segment in new RoundedRectanglePolygon(rectangle, radius).LineSegments) + { + _ = this.AddSegment(segment); + } + + return this.CloseFigure(); + } + + /// + /// Adds a rounded rectangle to the current path as a closed figure. + /// + /// The rectangle bounds. + /// The x and y radius of each corner. + /// The . + public PathBuilder AddRoundedRectangle(Rectangle rectangle, float radius) + => this.AddRoundedRectangle((RectangleF)rectangle, radius); + + /// + /// Adds a rounded rectangle to the current path as a closed figure. + /// + /// The rectangle bounds. + /// The x and y radii of each corner. + /// The . + public PathBuilder AddRoundedRectangle(Rectangle rectangle, SizeF radius) + => this.AddRoundedRectangle((RectangleF)rectangle, radius); + + /// + /// Adds a rounded rectangle to the current path as a closed figure. + /// + /// The x-coordinate of the rectangle. + /// The y-coordinate of the rectangle. + /// The rectangle width. + /// The rectangle height. + /// The x and y radius of each corner. + /// The . + public PathBuilder AddRoundedRectangle(float x, float y, float width, float height, float radius) + => this.AddRoundedRectangle(new RectangleF(x, y, width, height), radius); + + /// + /// Adds a rounded rectangle to the current path as a closed figure. + /// + /// The x-coordinate of the rectangle. + /// The y-coordinate of the rectangle. + /// The rectangle width. + /// The rectangle height. + /// The x and y radii of each corner. + /// The . + public PathBuilder AddRoundedRectangle(float x, float y, float width, float height, SizeF radius) + => this.AddRoundedRectangle(new RectangleF(x, y, width, height), radius); + + /// + /// Adds a polygon to the current path as a closed figure. + /// + /// The polygon vertices. + /// The . + public PathBuilder AddPolygon(IEnumerable points) + { + Guard.NotNull(points, nameof(points)); + return this.AddPolygon([.. points]); + } + + /// + /// Adds a polygon to the current path as a closed figure. + /// + /// The polygon vertices. + /// The . + public PathBuilder AddPolygon(params PointF[] points) + { + Guard.NotNull(points, nameof(points)); + + _ = this.StartFigure(); + _ = this.AddSegment(new LinearLineSegment(points)); + return this.CloseFigure(); + } + + /// + /// Adds a regular polygon to the current path as a closed figure. + /// + /// The center point of the polygon. + /// The number of polygon vertices. + /// The polygon radius. + /// The . + public PathBuilder AddRegularPolygon(PointF center, int vertices, float radius) + => this.AddRegularPolygon(center, vertices, radius, 0F); + + /// + /// Adds a regular polygon to the current path as a closed figure. + /// + /// The center point of the polygon. + /// The number of polygon vertices. + /// The polygon radius. + /// The polygon rotation angle in degrees. + /// The . + public PathBuilder AddRegularPolygon(PointF center, int vertices, float radius, float angle) + { + _ = this.StartFigure(); + + foreach (ILineSegment segment in new RegularPolygon(center, vertices, radius, angle).LineSegments) + { + _ = this.AddSegment(segment); + } + + return this.CloseFigure(); + } + + /// + /// Adds a regular polygon to the current path as a closed figure. + /// + /// The x-coordinate of the polygon center. + /// The y-coordinate of the polygon center. + /// The number of polygon vertices. + /// The polygon radius. + /// The . + public PathBuilder AddRegularPolygon(float x, float y, int vertices, float radius) + => this.AddRegularPolygon(new PointF(x, y), vertices, radius); + + /// + /// Adds a regular polygon to the current path as a closed figure. + /// + /// The x-coordinate of the polygon center. + /// The y-coordinate of the polygon center. + /// The number of polygon vertices. + /// The polygon radius. + /// The polygon rotation angle in degrees. + /// The . + public PathBuilder AddRegularPolygon(float x, float y, int vertices, float radius, float angle) + => this.AddRegularPolygon(new PointF(x, y), vertices, radius, angle); + + /// + /// Adds a star to the current path as a closed figure. + /// + /// The center point of the star. + /// The number of star prongs. + /// The inner star radius. + /// The outer star radius. + /// The . + public PathBuilder AddStar(PointF center, int prongs, float innerRadii, float outerRadii) + => this.AddStar(center, prongs, innerRadii, outerRadii, 0F); + + /// + /// Adds a star to the current path as a closed figure. + /// + /// The center point of the star. + /// The number of star prongs. + /// The inner star radius. + /// The outer star radius. + /// The star rotation angle in degrees. + /// The . + public PathBuilder AddStar(PointF center, int prongs, float innerRadii, float outerRadii, float angle) + { + _ = this.StartFigure(); + + foreach (ILineSegment segment in new StarPolygon(center, prongs, innerRadii, outerRadii, angle).LineSegments) + { + _ = this.AddSegment(segment); + } + + return this.CloseFigure(); + } + + /// + /// Adds a star to the current path as a closed figure. + /// + /// The x-coordinate of the star center. + /// The y-coordinate of the star center. + /// The number of star prongs. + /// The inner star radius. + /// The outer star radius. + /// The . + public PathBuilder AddStar(float x, float y, int prongs, float innerRadii, float outerRadii) + => this.AddStar(new PointF(x, y), prongs, innerRadii, outerRadii); + + /// + /// Adds a star to the current path as a closed figure. + /// + /// The x-coordinate of the star center. + /// The y-coordinate of the star center. + /// The number of star prongs. + /// The inner star radius. + /// The outer star radius. + /// The star rotation angle in degrees. + /// The . + public PathBuilder AddStar(float x, float y, int prongs, float innerRadii, float outerRadii, float angle) + => this.AddStar(new PointF(x, y), prongs, innerRadii, outerRadii, angle); + + /// + /// Starts a new figure but leaves the previous one open. + /// + /// The . + public PathBuilder StartFigure() + { + if (!this.currentFigure.IsEmpty) + { + this.currentFigure = new Figure(); + this.figures.Add(this.currentFigure); + } + else + { + this.currentFigure.IsClosed = false; + } + + return this; + } + + /// + /// Closes the current figure. + /// + /// The . + public PathBuilder CloseFigure() + { + this.currentFigure.IsClosed = true; + _ = this.StartFigure(); + + return this; + } + + /// + /// Closes the current figure. + /// + /// The . + public PathBuilder CloseAllFigures() + { + foreach (Figure f in this.figures) + { + f.IsClosed = true; + } + + _ = this.CloseFigure(); + + return this; + } + + /// + /// Builds a complex polygon from the current working set of working operations. + /// + /// The current set of operations as a complex polygon + public IPath Build() + { + IPath[] paths = [.. this.figures.Where(x => !x.IsEmpty).Select(x => x.Build())]; + if (paths.Length == 1) + { + return paths[0]; + } + + return new ComplexPolygon(paths); + } + + /// + /// Resets this instance, clearing any drawn paths and resetting any transforms. + /// + /// The . + public PathBuilder Reset() + { + this.Clear(); + _ = this.ResetTransform(); + this.currentPoint = default; + + return this; + } + + /// + /// Clears all drawn paths, Leaving any applied transforms. + /// + [MemberNotNull(nameof(currentFigure))] + public void Clear() + { + this.currentFigure = new Figure(); + this.figures.Clear(); + this.figures.Add(this.currentFigure); + } + + private class Figure + { + private readonly List segments = []; + + public bool IsClosed { get; set; } + + public bool IsEmpty => this.segments.Count == 0; + + public void AddSegment(ILineSegment segment) => this.segments.Add(segment); + + public IPath Build() + => this.IsClosed + ? new Polygon([.. this.segments], true) + : new Path(this.segments.ToArray()); + } + } +} diff --git a/ImageSharp.Drawing/PathCollection.cs b/ImageSharp.Drawing/PathCollection.cs new file mode 100644 index 0000000..c34e79f --- /dev/null +++ b/ImageSharp.Drawing/PathCollection.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// A aggregate of s to apply common operations to them. + /// + /// + public class PathCollection : IPathCollection + { + private readonly IPath[] paths; + private RectangleF? bounds; + + /// + /// Initializes a new instance of the class. + /// + /// The collection of paths + public PathCollection(IEnumerable paths) + : this(GetPathArray(paths)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The collection of paths + public PathCollection(params IPath[] paths) + { + Guard.NotNull(paths, nameof(paths)); + this.paths = paths; + + if (paths.Length == 0) + { + this.bounds = new RectangleF(0, 0, 0, 0); + } + } + + /// + public RectangleF Bounds => this.bounds ??= this.CalcBounds(); + + private RectangleF CalcBounds() + { + float minX, minY, maxX, maxY; + minX = minY = float.MaxValue; + maxX = maxY = float.MinValue; + + foreach (IPath path in this.paths) + { + RectangleF bounds = path.Bounds; + minX = Math.Min(bounds.Left, minX); + minY = Math.Min(bounds.Top, minY); + maxX = Math.Max(bounds.Right, maxX); + maxY = Math.Max(bounds.Bottom, maxY); + } + + return new RectangleF(minX, minY, maxX - minX, maxY - minY); + } + + /// + public IEnumerator GetEnumerator() => ((IEnumerable)this.paths).GetEnumerator(); + + /// + public IPathCollection Transform(Matrix4x4 matrix) + { + IPath[] result = new IPath[this.paths.Length]; + + for (int i = 0; i < this.paths.Length && i < result.Length; i++) + { + result[i] = this.paths[i].Transform(matrix); + } + + return new PathCollection(result); + } + + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this.paths).GetEnumerator(); + + private static IPath[] GetPathArray(IEnumerable paths) + { + Guard.NotNull(paths, nameof(paths)); + return paths as IPath[] ?? [.. paths]; + } + } +} diff --git a/ImageSharp.Drawing/PathExtensions.Internal.cs b/ImageSharp.Drawing/PathExtensions.Internal.cs new file mode 100644 index 0000000..503a4b0 --- /dev/null +++ b/ImageSharp.Drawing/PathExtensions.Internal.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Convenience methods that can be applied to shapes and paths. + /// + public static partial class PathExtensions + { + /// + /// Create a path with the segment order reversed. + /// + /// The path to reverse. + /// The reversed . + internal static IPath Reverse(this IPath path) + { + // TODO. Make this a void. We can reverse the segments in place and then reverse the points in place as well. + IEnumerable segments = path.Flatten().Select(static p => new LinearLineSegment(ReversePoints(p.Points.Span))); + bool closed = false; + if (path is ISimplePath sp) + { + closed = sp.IsClosed; + } + + return closed ? new Polygon(segments) : new Path(segments); + } + + private static PointF[] ReversePoints(ReadOnlySpan points) + { + PointF[] reversed = new PointF[points.Length]; + for (int i = 0; i < reversed.Length; i++) + { + reversed[i] = points[points.Length - 1 - i]; + } + + return reversed; + } + } +} diff --git a/ImageSharp.Drawing/PathExtensions.cs b/ImageSharp.Drawing/PathExtensions.cs new file mode 100644 index 0000000..db8fc30 --- /dev/null +++ b/ImageSharp.Drawing/PathExtensions.cs @@ -0,0 +1,219 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Convenience methods that can be applied to shapes and paths. + /// + public static partial class PathExtensions + { + /// + /// Creates a path rotated by the specified radians around its center. + /// + /// The path to rotate. + /// The radians to rotate the path. + /// A with a rotate transform applied. + public static IPathCollection Rotate(this IPathCollection path, float radians) + => path.Transform(new Matrix4x4(Matrix3x2.CreateRotation(radians, RectangleF.Center(path.Bounds)))); + + /// + /// Creates a path rotated by the specified degrees around its center. + /// + /// The path to rotate. + /// The degree to rotate the path. + /// A with a rotate transform applied. + public static IPathCollection RotateDegree(this IPathCollection shape, float degree) + => shape.Rotate(GeometryUtilities.DegreeToRadian(degree)); + + /// + /// Creates a path translated by the supplied position + /// + /// The path to translate. + /// The translation position. + /// A with a translate transform applied. + public static IPathCollection Translate(this IPathCollection path, PointF position) + => path.Transform(Matrix4x4.CreateTranslation(position.X, position.Y, 0)); + + /// + /// Creates a path translated by the supplied position + /// + /// The path to translate. + /// The amount to translate along the X axis. + /// The amount to translate along the Y axis. + /// A with a translate transform applied. + public static IPathCollection Translate(this IPathCollection path, float x, float y) + => path.Translate(new PointF(x, y)); + + /// + /// Creates a path translated by the supplied position + /// + /// The path to translate. + /// The amount to scale along the X axis. + /// The amount to scale along the Y axis. + /// A with a translate transform applied. + public static IPathCollection Scale(this IPathCollection path, float scaleX, float scaleY) + => path.Transform(Matrix4x4.CreateScale(scaleX, scaleY, 1, new Vector3(RectangleF.Center(path.Bounds), 0))); + + /// + /// Creates a path translated by the supplied position + /// + /// The path to translate. + /// The amount to scale along both the x and y axis. + /// A with a translate transform applied. + public static IPathCollection Scale(this IPathCollection path, float scale) + => path.Transform(Matrix4x4.CreateScale(scale, scale, 1, new Vector3(RectangleF.Center(path.Bounds), 0))); + + /// + /// Creates a path rotated by the specified radians around its center. + /// + /// The path to rotate. + /// The radians to rotate the path. + /// A with a rotate transform applied. + public static IPath Rotate(this IPath path, float radians) + => path.Transform(new Matrix4x4(Matrix3x2.CreateRotation(radians, RectangleF.Center(path.Bounds)))); + + /// + /// Creates a path rotated by the specified degrees around its center. + /// + /// The path to rotate. + /// The degree to rotate the path. + /// A with a rotate transform applied. + public static IPath RotateDegree(this IPath shape, float degree) + => shape.Rotate(GeometryUtilities.DegreeToRadian(degree)); + + /// + /// Creates a path translated by the supplied position + /// + /// The path to translate. + /// The translation position. + /// A with a translate transform applied. + public static IPath Translate(this IPath path, PointF position) + => path.Transform(Matrix4x4.CreateTranslation(position.X, position.Y, 0)); + + /// + /// Creates a path translated by the supplied position + /// + /// The path to translate. + /// The amount to translate along the X axis. + /// The amount to translate along the Y axis. + /// A with a translate transform applied. + public static IPath Translate(this IPath path, float x, float y) + => path.Translate(new Vector2(x, y)); + + /// + /// Creates a path translated by the supplied position + /// + /// The path to translate. + /// The amount to scale along the X axis. + /// The amount to scale along the Y axis. + /// A with a translate transform applied. + public static IPath Scale(this IPath path, float scaleX, float scaleY) + => path.Transform(Matrix4x4.CreateScale(scaleX, scaleY, 1, new Vector3(RectangleF.Center(path.Bounds), 0))); + + /// + /// Creates a path translated by the supplied position + /// + /// The path to translate. + /// The amount to scale along both the x and y axis. + /// A with a translate transform applied. + public static IPath Scale(this IPath path, float scale) + => path.Transform(Matrix4x4.CreateScale(scale, scale, 1, new Vector3(RectangleF.Center(path.Bounds), 0))); + + /// + /// Calculates the approximate length of the path as though each segment were unrolled into a line. + /// + /// The path to compute the length for. + /// + /// The representing the unrolled length. + /// For closed paths, the length includes an implicit closing segment. + /// + public static float ComputeLength(this IPath path) + { + float dist = 0; + foreach (ISimplePath s in path.Flatten()) + { + ReadOnlySpan points = s.Points.Span; + if (points.Length < 2) + { + // Only a single point + continue; + } + + for (int i = 1; i < points.Length; i++) + { + dist += Vector2.Distance(points[i - 1], points[i]); + } + + if (s.IsClosed) + { + dist += Vector2.Distance(points[0], points[^1]); + } + } + + return dist; + } + + /// + /// Calculates the total area of all paths in the specified collection. + /// + /// A collection of paths for which to compute the combined area. Cannot be null. + /// + /// The total area, in square units, enclosed by all paths in the collection. + /// + public static float ComputeArea(this IPathCollection paths) + { + float area = 0; + foreach (IPath path in paths) + { + area += path.ComputeArea(); + } + + return area; + } + + /// + /// Calculates the total area enclosed by the specified path. + /// + /// + /// This method sums the areas of all subpaths within the path. Subpaths with fewer than three + /// points are ignored, as they do not form a closed region. The result is always non-negative, regardless of the + /// winding direction of the subpaths. + /// + /// + /// The path for which to compute the enclosed area. Must contain at least one subpath with three or more points to + /// contribute to the area calculation. + /// + /// + /// The total area, in square units, enclosed by all subpaths of the path. Returns 0 if the path does not contain + /// any subpaths with at least three points. + /// + public static float ComputeArea(this IPath path) + { + float area = 0; + foreach (ISimplePath s in path.Flatten()) + { + ReadOnlySpan points = s.Points.Span; + if (points.Length < 3) + { + // Not enough points to form an area + continue; + } + + float subArea = 0; + for (int i = 0; i < points.Length; i++) + { + PointF p1 = points[i]; + PointF p2 = points[(i + 1) % points.Length]; + subArea += (p1.X * p2.Y) - (p2.X * p1.Y); + } + + area += MathF.Abs(subArea) * .5F; + } + + return area; + } + } +} diff --git a/ImageSharp.Drawing/PathTypes.cs b/ImageSharp.Drawing/PathTypes.cs new file mode 100644 index 0000000..e76805b --- /dev/null +++ b/ImageSharp.Drawing/PathTypes.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Describes the different type of paths. + /// + public enum PathTypes + { + /// + /// Denotes a path containing a single simple open path + /// + Open, + + /// + /// Denotes a path describing a single simple closed shape + /// + Closed, + + /// + /// Denotes a path containing one or more child paths that could be open or closed. + /// + Mixed + } +} diff --git a/ImageSharp.Drawing/PiePolygon.cs b/ImageSharp.Drawing/PiePolygon.cs new file mode 100644 index 0000000..d44541e --- /dev/null +++ b/ImageSharp.Drawing/PiePolygon.cs @@ -0,0 +1,120 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// A pie sector polygon defined by a center point, radii, rotation, and arc sweep. + /// + public sealed class PiePolygon : Polygon + { + /// + /// Initializes a new instance of the class. + /// + /// The center point of the pie sector. + /// The x and y radii of the pie ellipse. + /// The ellipse rotation in degrees. + /// The pie start angle in degrees. + /// The pie sweep angle in degrees. + public PiePolygon(PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle) + : base(CreateSegments(center, radius, rotation, startAngle, sweepAngle)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The center point of the pie sector. + /// The x and y radii of the pie ellipse. + /// The pie start angle in degrees. + /// The pie sweep angle in degrees. + public PiePolygon(PointF center, SizeF radius, float startAngle, float sweepAngle) + : this(center, radius, 0F, startAngle, sweepAngle) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The x-coordinate of the pie center. + /// The y-coordinate of the pie center. + /// The x-radius of the pie ellipse. + /// The y-radius of the pie ellipse. + /// The ellipse rotation in degrees. + /// The pie start angle in degrees. + /// The pie sweep angle in degrees. + public PiePolygon(float x, float y, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle) + : this(new PointF(x, y), new SizeF(radiusX, radiusY), rotation, startAngle, sweepAngle) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The x-coordinate of the pie center. + /// The y-coordinate of the pie center. + /// The x-radius of the pie ellipse. + /// The y-radius of the pie ellipse. + /// The pie start angle in degrees. + /// The pie sweep angle in degrees. + public PiePolygon(float x, float y, float radiusX, float radiusY, float startAngle, float sweepAngle) + : this(x, y, radiusX, radiusY, 0F, startAngle, sweepAngle) + { + } + + private PiePolygon(ILineSegment[] segments) + : base(segments, true) + { + } + + /// + public override IPath Transform(Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + return this; + } + + ILineSegment[] segments = new ILineSegment[this.LineSegments.Count]; + + for (int i = 0; i < segments.Length; i++) + { + segments[i] = this.LineSegments[i].Transform(matrix); + } + + return new PiePolygon(segments); + } + + private static ILineSegment[] CreateSegments(PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle) + { + Guard.MustBeGreaterThan(radius.Width, 0, "radiusX"); + Guard.MustBeGreaterThan(radius.Height, 0, "radiusY"); + + PointF arcStart = GetArcPoint(center, radius, rotation, startAngle); + ArcLineSegment arc = new(center, radius, rotation, startAngle, sweepAngle); + + return + [ + new LinearLineSegment(center, arcStart), + arc, + new LinearLineSegment(arc.EndPoint, center) + ]; + } + + private static PointF GetArcPoint(PointF center, SizeF radius, float rotation, float angle) + { + float rotationRadians = rotation * (MathF.PI / 180F); + float angleRadians = angle * (MathF.PI / 180F); + float cosRotation = MathF.Cos(rotationRadians); + float sinRotation = MathF.Sin(rotationRadians); + float cosAngle = MathF.Cos(angleRadians); + float sinAngle = MathF.Sin(angleRadians); + + return new PointF( + center.X + (radius.Width * cosRotation * cosAngle) - (radius.Height * sinRotation * sinAngle), + center.Y + (radius.Width * sinRotation * cosAngle) + (radius.Height * cosRotation * sinAngle)); + } + } +} diff --git a/ImageSharp.Drawing/PointOrientation.cs b/ImageSharp.Drawing/PointOrientation.cs new file mode 100644 index 0000000..8e06817 --- /dev/null +++ b/ImageSharp.Drawing/PointOrientation.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Represents the orientation of a point from a line. + /// + internal enum PointOrientation + { + /// + /// The point is collinear. + /// + Collinear = 0, + + /// + /// The point is clockwise. + /// + Clockwise = 1, + + /// + /// The point is counter-clockwise. + /// + Counterclockwise = 2 + } +} diff --git a/ImageSharp.Drawing/Polygon.cs b/ImageSharp.Drawing/Polygon.cs new file mode 100644 index 0000000..fbc9e1a --- /dev/null +++ b/ImageSharp.Drawing/Polygon.cs @@ -0,0 +1,98 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// A shape made up of a single closed path made up of one of more s + /// + public class Polygon : Path + { + /// + /// Initializes a new instance of the class. + /// + /// The collection of points; processed as a series of linear line segments. + public Polygon(PointF[] points) + : this(new LinearLineSegment(points)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The segments. + public Polygon(params ILineSegment[] segments) + : base(segments) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The segments. + public Polygon(IEnumerable segments) + : base(segments) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The segment. + public Polygon(ILineSegment segment) + : base(segment) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The path. + internal Polygon(Path path) + : base(path) + { + } + + /// + /// Initializes a new instance of the class using the specified line segments. + /// + /// + /// If owned is set to , modifications to the segments array after construction may affect + /// the Polygon instance. If owned is , the segments are copied to ensure the Polygon is not affected by + /// external changes. + /// + /// An array of line segments that define the edges of the polygon. The order of segments determines the shape of + /// the polygon. + /// + /// to indicate that the Polygon instance takes ownership of the segments array; + /// to create a copy of the array. + /// + internal Polygon(ILineSegment[] segments, bool owned) + : base(owned ? segments : [.. segments]) + { + } + + /// + public override bool IsClosed => true; + + /// + public override IPath Transform(Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + return this; + } + + ILineSegment[] segments = new ILineSegment[this.LineSegments.Count]; + + for (int i = 0; i < segments.Length; i++) + { + segments[i] = this.LineSegments[i].Transform(matrix); + } + + return new Polygon(segments, true); + } + } +} diff --git a/ImageSharp.Drawing/PolygonGeometry/ClippedShapeGenerator.cs b/ImageSharp.Drawing/PolygonGeometry/ClippedShapeGenerator.cs new file mode 100644 index 0000000..f4cf22a --- /dev/null +++ b/ImageSharp.Drawing/PolygonGeometry/ClippedShapeGenerator.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.PolygonClipper; +using System.Collections.Generic; +using PCPolygon = SixLabors.PolygonClipper.Polygon; +using PolygonClipperAction = SixLabors.PolygonClipper.PolygonClipper; + +namespace SixLabors.ImageSharp.Drawing.PolygonGeometry { + /// + /// Generates clipped shapes from one or more input paths using polygon boolean operations. + /// + /// + /// This class provides a high-level wrapper around the low-level . + /// It accumulates subject and clip polygons, applies the specified , + /// and converts the resulting polygon contours back into instances suitable + /// for rendering or further processing. + /// + internal static class ClippedShapeGenerator + { + /// + /// Generates the final clipped shapes from the previously provided subject and clip paths. + /// + /// + /// The boolean operation to perform, such as , + /// , or . + /// + /// The subject path. + /// The clipping paths. + /// + /// The representing the result of the boolean operation. + /// + public static ComplexPolygon GenerateClippedShapes( + BooleanOperation operation, + IPath subject, + IEnumerable clip) + { + Guard.NotNull(subject); + Guard.NotNull(clip); + + PCPolygon s = PolygonClipperFactory.FromSimpleClosedPaths(subject.Flatten()); + PCPolygon c = PolygonClipperFactory.FromClosedPaths(clip); + + PCPolygon result = operation switch + { + BooleanOperation.Xor => PolygonClipperAction.Xor(s, c), + BooleanOperation.Difference => PolygonClipperAction.Difference(s, c), + BooleanOperation.Union => PolygonClipperAction.Union(s, c), + _ => PolygonClipperAction.Intersection(s, c), + }; + + IPath[] shapes = new IPath[result.Count]; + + int index = 0; + for (int i = 0; i < result.Count; i++) + { + shapes[index++] = new Polygon(CreateContourPoints(result, i)); + } + + return new(shapes); + } + + /// + /// Converts a PolygonClipper contour to ImageSharp points and normalizes winding for parent/child rings. + /// + /// The polygon containing the contour hierarchy. + /// The contour index to convert. + /// The converted point array. + private static PointF[] CreateContourPoints(PCPolygon polygon, int contourIndex) + { + Contour contour = polygon[contourIndex]; + PointF[] points = new PointF[contour.Count]; + bool reverse = ShouldReverseForNonZeroWinding(polygon, contourIndex); + + if (!reverse) + { + for (int i = 0; i < contour.Count; i++) + { + Vertex vertex = contour[i]; + points[i] = new PointF((float)vertex.X, (float)vertex.Y); + } + + return points; + } + + for (int sourceIndex = contour.Count - 1, targetIndex = 0; sourceIndex >= 0; sourceIndex--, targetIndex++) + { + Vertex vertex = contour[sourceIndex]; + points[targetIndex] = new PointF((float)vertex.X, (float)vertex.Y); + } + + return points; + } + + /// + /// Ensures child contours (holes/islands) use opposite winding to their direct parent. + /// This keeps clipped output deterministic when consumed with the NonZero fill rule. + /// + /// The polygon containing contour hierarchy information. + /// The contour index to inspect. + /// when the contour should be reversed. + private static bool ShouldReverseForNonZeroWinding(PCPolygon polygon, int contourIndex) + { + Contour contour = polygon[contourIndex]; + if (contour.ParentIndex is not int parentIndex || (uint)parentIndex >= (uint)polygon.Count) + { + return false; + } + + Contour parentContour = polygon[parentIndex]; + return contour.IsCounterClockwise() == parentContour.IsCounterClockwise(); + } + } +} diff --git a/ImageSharp.Drawing/PolygonGeometry/PolygonClipperFactory.cs b/ImageSharp.Drawing/PolygonGeometry/PolygonClipperFactory.cs new file mode 100644 index 0000000..0989e08 --- /dev/null +++ b/ImageSharp.Drawing/PolygonGeometry/PolygonClipperFactory.cs @@ -0,0 +1,81 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.PolygonClipper; +using System; +using System.Collections.Generic; +using PCPolygon = SixLabors.PolygonClipper.Polygon; + +namespace SixLabors.ImageSharp.Drawing.PolygonGeometry { + /// + /// Builders for from ImageSharp paths. + /// Converts ImageSharp paths to the format required by PolygonClipper. + /// + /// + /// PolygonClipper computes parent-child relationships, depth, and orientation during its + /// sweep line algorithm, so we only need to provide contours with vertices. + /// + internal static class PolygonClipperFactory + { + /// + /// Creates a polygon from multiple paths. + /// + /// The paths to convert. + /// A containing all flattened paths as contours. + public static PCPolygon FromClosedPaths(IEnumerable paths) + { + PCPolygon polygon = []; + + foreach (IPath path in paths) + { + polygon = FromSimpleClosedPaths(path.Flatten(), polygon); + } + + return polygon; + } + + /// + /// Converts closed simple paths to PolygonClipper contours. + /// + /// Closed simple paths. + /// Optional existing polygon to populate. + /// The constructed . + /// + /// This method simply converts ImageSharp paths to PolygonClipper contours by copying vertices. + /// PolygonClipper's sweep line algorithm will determine parent-child relationships, depth, + /// and proper orientation during clipping operations. We only need to ensure paths are + /// closed and have sufficient vertices. + /// + public static PCPolygon FromSimpleClosedPaths(IEnumerable paths, PCPolygon? polygon = null) + { + polygon ??= []; + + foreach (ISimplePath p in paths) + { + if (!p.IsClosed) + { + continue; + } + + ReadOnlySpan points = p.Points.Span; + if (points.Length < 3) + { + continue; + } + + Contour contour = []; + + // Copy all vertices + for (int i = 0; i < points.Length; i++) + { + contour.Add(new Vertex(points[i].X, points[i].Y)); + } + + // Add the contour - PolygonClipper will determine parent/depth/orientation during sweep + polygon.Add(contour); + } + + return polygon; + } + } +} diff --git a/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs b/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs new file mode 100644 index 0000000..3841b79 --- /dev/null +++ b/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs @@ -0,0 +1,106 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.PolygonClipper; +using System; +using PCPolygon = SixLabors.PolygonClipper.Polygon; +using StrokeOptions = SixLabors.ImageSharp.Drawing.Processing.StrokeOptions; + +namespace SixLabors.ImageSharp.Drawing.PolygonGeometry { + /// + /// Generates stroked and merged shapes using polygon stroking and boolean clipping. + /// + internal static class StrokedShapeGenerator + { + /// + /// Strokes a path and returns a merged outline from its flattened segments. + /// + /// The source path. It is flattened using the current flattening settings. + /// The stroke width in the caller's coordinate space. + /// The stroke geometry options. + /// + /// A representing the stroked outline after boolean merge. + /// + public static ComplexPolygon GenerateStrokedShapes(IPath path, float width, StrokeOptions options) + { + // 1) Stroke the input path as open or closed. + PCPolygon rings = []; + + foreach (ISimplePath sp in path.Flatten()) + { + ReadOnlySpan span = sp.Points.Span; + + if (span.Length < 2) + { + continue; + } + + Contour ring = new(span.Length); + for (int i = 0; i < span.Length; i++) + { + PointF p = span[i]; + ring.Add(new Vertex(p.X, p.Y)); + } + + if (sp.IsClosed) + { + ring.Add(ring[0]); + } + + rings.Add(ring); + } + + int count = rings.Count; + if (count == 0) + { + return new([]); + } + + PCPolygon result = PolygonStroker.Stroke(rings, width, CreateStrokeOptions(options)); + + IPath[] shapes = new IPath[result.Count]; + int index = 0; + for (int i = 0; i < result.Count; i++) + { + Contour contour = result[i]; + PointF[] points = new PointF[contour.Count]; + + for (int j = 0; j < contour.Count; j++) + { + Vertex vertex = contour[j]; + points[j] = new PointF((float)vertex.X, (float)vertex.Y); + } + + shapes[index++] = new Polygon(points); + } + + return new(shapes); + } + + private static PolygonClipper.StrokeOptions CreateStrokeOptions(StrokeOptions options) + { + PolygonClipper.StrokeOptions o = new() + { + ArcDetailScale = options.ArcDetailScale, + MiterLimit = options.MiterLimit, + LineJoin = options.LineJoin switch + { + LineJoin.MiterRound => PolygonClipper.LineJoin.MiterRound, + LineJoin.Bevel => PolygonClipper.LineJoin.Bevel, + LineJoin.Round => PolygonClipper.LineJoin.Round, + LineJoin.MiterRevert => PolygonClipper.LineJoin.MiterRevert, + _ => PolygonClipper.LineJoin.Miter, + }, + + LineCap = options.LineCap switch + { + LineCap.Round => PolygonClipper.LineCap.Round, + LineCap.Square => PolygonClipper.LineCap.Square, + _ => PolygonClipper.LineCap.Butt, + } + }; + + return o; + } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/ApplyBarrier.cs b/ImageSharp.Drawing/Processing/Backends/ApplyBarrier.cs new file mode 100644 index 0000000..18627d0 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/ApplyBarrier.cs @@ -0,0 +1,176 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// Processor barrier recorded in a drawing backend timeline. + /// + internal sealed class ApplyBarrier + { + /// + /// Initializes a new instance of the class. + /// + /// The closed path defining the processed region. + /// The drawing options captured when the barrier was recorded. + /// The active clip paths captured when the barrier was recorded. + /// The canvas-local bounds captured when the barrier was recorded. + /// The absolute target bounds captured when the barrier was recorded. + /// The absolute destination offset captured when the barrier was recorded. + /// Indicates whether the barrier was recorded inside a layer. + /// The processor operation to run against the replay-time snapshot. + internal ApplyBarrier( + IPath path, + DrawingOptions options, + IReadOnlyList clipPaths, + Rectangle canvasBounds, + Rectangle targetBounds, + Point destinationOffset, + bool isInsideLayer, + Action operation) + { + this.Path = path; + this.Options = options; + this.ClipPaths = clipPaths; + this.CanvasBounds = canvasBounds; + this.TargetBounds = targetBounds; + this.DestinationOffset = destinationOffset; + this.IsInsideLayer = isInsideLayer; + this.Operation = operation; + } + + /// + /// Gets the closed path defining the processed region. + /// + public IPath Path { get; } + + /// + /// Gets the drawing options captured when the barrier was recorded. + /// + public DrawingOptions Options { get; } + + /// + /// Gets the active clip paths captured when the barrier was recorded. + /// + public IReadOnlyList ClipPaths { get; } + + /// + /// Gets the canvas-local bounds captured when the barrier was recorded. + /// + public Rectangle CanvasBounds { get; } + + /// + /// Gets the absolute target bounds captured when the barrier was recorded. + /// + public Rectangle TargetBounds { get; } + + /// + /// Gets the absolute destination offset captured when the barrier was recorded. + /// + public Point DestinationOffset { get; } + + /// + /// Gets a value indicating whether the barrier was recorded inside a layer. + /// + public bool IsInsideLayer { get; } + + /// + /// Gets the processor operation to run against the replay-time snapshot. + /// + public Action Operation { get; } + + /// + /// Creates the transient image-brush draw command that writes this barrier's processed snapshot back to the target. + /// + /// The pixel format. + /// The active processing configuration. + /// The backend used to read the replay-time target pixels. + /// The target frame. + /// The image resource that must stay alive while the returned command batch is rendered. + /// The transient write-back command batch, or when the barrier has no target coverage. + public DrawingCommandBatch? CreateWriteBackBatch( + Configuration configuration, + IDrawingBackend backend, + ICanvasFrame target, + out IDisposable? ownedResource) + where TPixel : unmanaged, IPixel + { + RectangleF rawBounds = RectangleF.Transform(this.Path.Bounds, this.Options.Transform); + Rectangle sourceRect = ToConservativeBounds(rawBounds); + sourceRect = Rectangle.Intersect(this.CanvasBounds, sourceRect); + + if (sourceRect.Width <= 0 || sourceRect.Height <= 0) + { + ownedResource = null; + return null; + } + + Image sourceImage = new(configuration, sourceRect.Width, sourceRect.Height); + try + { + backend.ReadRegion( + configuration, + target, + sourceRect, + sourceImage.Frames.RootFrame.PixelBuffer.GetRegion()); + + sourceImage.Mutate(this.Operation); + + Point brushOffset = new( + sourceRect.X - (int)MathF.Floor(rawBounds.Left), + sourceRect.Y - (int)MathF.Floor(rawBounds.Top)); + + ImageBrush brush = new(sourceImage, sourceImage.Bounds, brushOffset); + GraphicsOptions graphicsOptions = this.Options.GraphicsOptions; + RasterizationMode rasterizationMode = graphicsOptions.Antialias + ? RasterizationMode.Antialiased + : RasterizationMode.Aliased; + + RectangleF pathBounds = this.Path.Bounds; + Rectangle interest = Rectangle.FromLTRB( + (int)MathF.Floor(pathBounds.Left), + (int)MathF.Floor(pathBounds.Top), + (int)MathF.Ceiling(pathBounds.Right), + (int)MathF.Ceiling(pathBounds.Bottom)); + + RasterizerOptions rasterizerOptions = new( + interest, + this.Options.ShapeOptions.IntersectionRule, + rasterizationMode, + RasterizerSamplingOrigin.PixelBoundary, + graphicsOptions.AntialiasThreshold); + + CompositionCommand command = CompositionCommand.Create( + this.Path, + brush, + this.Options, + in rasterizerOptions, + this.TargetBounds, + this.DestinationOffset, + this.ClipPaths, + this.IsInsideLayer); + + ownedResource = sourceImage; + CompositionSceneCommand[] commands = [new PathCompositionSceneCommand(command)]; + return new DrawingCommandBatch(commands, hasLayers: false); + } + catch + { + sourceImage.Dispose(); + throw; + } + } + + private static Rectangle ToConservativeBounds(RectangleF bounds) + => Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/CanvasRegionFrame{TPixel}.cs b/ImageSharp.Drawing/Processing/Backends/CanvasRegionFrame{TPixel}.cs new file mode 100644 index 0000000..e3c957f --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/CanvasRegionFrame{TPixel}.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// Frame adapter that exposes a clipped subregion of another frame. + /// + /// The pixel format. + internal sealed class CanvasRegionFrame : ICanvasFrame + where TPixel : unmanaged, IPixel + { + private readonly ICanvasFrame parent; + private readonly Rectangle region; + + /// + /// Initializes a new instance of the class. + /// + /// The parent frame that owns the target pixels. + /// The child region in parent-local coordinates. + public CanvasRegionFrame(ICanvasFrame parent, Rectangle region) + { + Guard.NotNull(parent, nameof(parent)); + Guard.MustBeGreaterThanOrEqualTo(region.Width, 0, nameof(region)); + Guard.MustBeGreaterThanOrEqualTo(region.Height, 0, nameof(region)); + + this.parent = parent; + this.region = region; + } + + /// + public Rectangle Bounds => new( + this.parent.Bounds.X + this.region.X, + this.parent.Bounds.Y + this.region.Y, + this.region.Width, + this.region.Height); + + /// + public bool TryGetCpuRegion(out Buffer2DRegion region) + { + if (!this.parent.TryGetCpuRegion(out Buffer2DRegion parentRegion)) + { + region = default; + return false; + } + + region = parentRegion.GetSubRegion(this.region); + return true; + } + + /// + public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface) + => this.parent.TryGetNativeSurface(out surface); + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs b/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs new file mode 100644 index 0000000..efb1865 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs @@ -0,0 +1,215 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// Identifies the flush-time role carried by a . + /// + public enum CompositionCommandKind : byte + { + /// + /// A fill-path command. + /// + FillLayer = 0, + + /// + /// Starts an isolated compositing layer. + /// + BeginLayer = 1, + + /// + /// Ends the most recently opened layer. + /// + EndLayer = 2 + } + + /// + /// One normalized fill-path or layer-based composition command queued for backend execution. + /// + /// + /// This type carries fill-path commands plus inline layer boundaries. + /// + public readonly struct CompositionCommand + { + private readonly IPath? sourcePath; + private readonly Brush? brush; + private readonly DrawingOptions? drawingOptions; + private readonly GraphicsOptions? layerGraphicsOptions; + private readonly IReadOnlyList? clipPaths; + + private CompositionCommand( + CompositionCommandKind kind, + IPath? sourcePath, + Brush? brush, + DrawingOptions? drawingOptions, + GraphicsOptions? layerGraphicsOptions, + in RasterizerOptions rasterizerOptions, + Rectangle targetBounds, + Rectangle layerBounds, + Point destinationOffset, + IReadOnlyList? clipPaths, + bool isInsideLayer) + { + this.Kind = kind; + this.sourcePath = sourcePath; + this.brush = brush; + this.drawingOptions = drawingOptions; + this.layerGraphicsOptions = layerGraphicsOptions; + this.RasterizerOptions = rasterizerOptions; + this.TargetBounds = targetBounds; + this.LayerBounds = layerBounds; + this.DestinationOffset = destinationOffset; + this.clipPaths = clipPaths; + this.IsInsideLayer = isInsideLayer; + } + + /// + /// Gets the command kind. + /// + public CompositionCommandKind Kind { get; } + + /// + /// Gets the absolute bounds of the logical target for this command. + /// + public Rectangle TargetBounds { get; } + + /// + /// Gets the absolute bounds of the layer opened by this command. + /// + /// + /// Only meaningful for and + /// . + /// + public Rectangle LayerBounds { get; } + + /// + /// Gets the brush used during composition. + /// + public Brush Brush => this.brush ?? throw new InvalidOperationException("Layer commands do not carry a brush."); + + /// + /// Gets the drawing options carried by the command. + /// + public DrawingOptions DrawingOptions => this.drawingOptions ?? throw new InvalidOperationException("Layer commands do not carry drawing options."); + + /// + /// Gets graphics options used for composition or layer compositing. + /// + public GraphicsOptions GraphicsOptions => this.drawingOptions?.GraphicsOptions ?? this.layerGraphicsOptions!; + + /// + /// Gets rasterizer options used to generate coverage. + /// + public RasterizerOptions RasterizerOptions { get; } + + /// + /// Gets the absolute destination offset where the local coverage should be composited. + /// + public Point DestinationOffset { get; } + + /// + /// Gets the source path carried by the command. + /// + public IPath SourcePath => this.sourcePath ?? throw new InvalidOperationException("Layer commands do not carry path geometry."); + + /// + /// Gets the command transform. + /// + public Matrix4x4 Transform => this.drawingOptions?.Transform ?? Matrix4x4.Identity; + + /// + /// Gets the clip paths carried by the command. + /// + public IReadOnlyList? ClipPaths => this.clipPaths; + + /// + /// Gets the shape options carried by the command. + /// + public ShapeOptions ShapeOptions => this.drawingOptions?.ShapeOptions ?? throw new InvalidOperationException("Layer commands do not carry shape options."); + + /// + /// Gets a value indicating whether the command was recorded inside a layer. + /// + public bool IsInsideLayer { get; } + + /// + /// Creates a fill-path composition command. + /// + /// Path in target-local coordinates. + /// Brush used during composition. + /// Drawing options (graphics, shape, transform) used during composition. + /// Rasterizer options used to generate coverage. + /// The absolute bounds of the logical target for this command. + /// Absolute destination offset where coverage is composited. + /// Optional clip paths supplied with the command. + /// True if the command was recorded inside a layer. + /// The composition command. + public static CompositionCommand Create( + IPath path, + Brush brush, + DrawingOptions drawingOptions, + in RasterizerOptions rasterizerOptions, + Rectangle targetBounds, + Point destinationOffset, + IReadOnlyList? clipPaths, + bool isInsideLayer) + => new( + CompositionCommandKind.FillLayer, + path, + brush, + drawingOptions, + null, + in rasterizerOptions, + targetBounds, + default, + destinationOffset, + clipPaths, + isInsideLayer); + + /// + /// Creates a begin-layer composition command. is false on the + /// BeginLayer marker itself; the flag is only meaningful for fills/strokes that follow it. + /// + /// The absolute bounds of the layer. + /// The compositing options used when the layer closes. + /// The begin-layer command. + public static CompositionCommand CreateBeginLayer(Rectangle layerBounds, GraphicsOptions graphicsOptions) + => new( + CompositionCommandKind.BeginLayer, + null, + null, + null, + graphicsOptions, + default, + layerBounds, + layerBounds, + default, + null, + false); + + /// + /// Creates an end-layer composition command. is false on the + /// EndLayer marker itself; the flag is only meaningful for fills/strokes that preceded it. + /// + /// The absolute bounds of the layer being closed. + /// The compositing options used by the layer. + /// The end-layer command. + public static CompositionCommand CreateEndLayer(Rectangle layerBounds, GraphicsOptions graphicsOptions) + => new( + CompositionCommandKind.EndLayer, + null, + null, + null, + graphicsOptions, + default, + layerBounds, + layerBounds, + default, + null, + false); + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/CompositionSceneCommandBase.cs b/ImageSharp.Drawing/Processing/Backends/CompositionSceneCommandBase.cs new file mode 100644 index 0000000..efe8db1 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/CompositionSceneCommandBase.cs @@ -0,0 +1,132 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#pragma warning disable SA1649 // Scene command types are grouped together in one file. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// Visitor contract for one flush-scoped composition scene command. + /// + public interface ICompositionSceneCommandVisitor + { + /// + /// Visits one fill-path or layer-based composition command. + /// + /// The command being visited. + public void Visit(PathCompositionSceneCommand command); + + /// + /// Visits one stroked path command. + /// + /// The command being visited. + public void Visit(StrokePathCompositionSceneCommand command); + + /// + /// Visits one explicit stroked line-segment command. + /// + /// The command being visited. + public void Visit(LineSegmentCompositionSceneCommand command); + + /// + /// Visits one explicit stroked polyline command. + /// + /// The command being visited. + public void Visit(PolylineCompositionSceneCommand command); + } + + /// + /// Base type for one draw-order command in a flush-scoped scene stream. + /// + public abstract class CompositionSceneCommand + { + /// + /// Dispatches the command to a visitor without a per-item kind switch at the call site. + /// + /// The visitor receiving the command. + public abstract void Accept(ICompositionSceneCommandVisitor visitor); + } + + /// + /// Scene command wrapper for fill-path and layer-based composition commands. + /// + public sealed class PathCompositionSceneCommand : CompositionSceneCommand + { + /// + /// Initializes a new instance of the class. + /// + /// The wrapped composition command. + public PathCompositionSceneCommand(in CompositionCommand command) + => this.Command = command; + + /// + /// Gets the wrapped composition command. + /// + public CompositionCommand Command { get; internal set; } + + /// + public override void Accept(ICompositionSceneCommandVisitor visitor) => visitor.Visit(this); + } + + /// + /// Scene command wrapper for stroked path commands. + /// + public sealed class StrokePathCompositionSceneCommand : CompositionSceneCommand + { + /// + /// Initializes a new instance of the class. + /// + /// The wrapped stroke path command. + public StrokePathCompositionSceneCommand(in StrokePathCommand command) + => this.Command = command; + + /// + /// Gets the wrapped stroke path command. + /// + public StrokePathCommand Command { get; internal set; } + + /// + public override void Accept(ICompositionSceneCommandVisitor visitor) => visitor.Visit(this); + } + + /// + /// Scene command wrapper for explicit stroked line-segment commands. + /// + public sealed class LineSegmentCompositionSceneCommand : CompositionSceneCommand + { + /// + /// Initializes a new instance of the class. + /// + /// The wrapped stroke line-segment command. + public LineSegmentCompositionSceneCommand(in StrokeLineSegmentCommand command) + => this.Command = command; + + /// + /// Gets the wrapped stroke line-segment command. + /// + public StrokeLineSegmentCommand Command { get; } + + /// + public override void Accept(ICompositionSceneCommandVisitor visitor) => visitor.Visit(this); + } + + /// + /// Scene command wrapper for explicit stroked polyline commands. + /// + public sealed class PolylineCompositionSceneCommand : CompositionSceneCommand + { + /// + /// Initializes a new instance of the class. + /// + /// The wrapped stroke polyline command. + public PolylineCompositionSceneCommand(in StrokePolylineCommand command) + => this.Command = command; + + /// + /// Gets the wrapped stroke polyline command. + /// + public StrokePolylineCommand Command { get; } + + /// + public override void Accept(ICompositionSceneCommandVisitor visitor) => visitor.Visit(this); + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/DEFAULT_DRAWING_BACKEND.md b/ImageSharp.Drawing/Processing/Backends/DEFAULT_DRAWING_BACKEND.md new file mode 100644 index 0000000..8352a19 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/DEFAULT_DRAWING_BACKEND.md @@ -0,0 +1,373 @@ +# DefaultDrawingBackend + +`DefaultDrawingBackend` is the CPU execution backend for ImageSharp.Drawing. It creates retained CPU scenes from prepared drawing command batches, executes those scenes with reusable scratch, and writes the result into a CPU destination buffer. + +This document explains the backend as a system rather than as a list of methods. The goal is to help a newcomer understand: + +- where the CPU backend fits in the canvas/backend selection model +- what problem the CPU backend is solving +- why the backend is organized around a retained row-oriented execution plan +- what `FlushScene` means in this architecture +- how rasterization, brush application, and layer composition fit together + +## Where The CPU Backend Fits + +`DefaultDrawingBackend` is the standard CPU execution path behind `DrawingCanvas`. + +The canvas architecture reaches this backend in two common ways: + +- ordinary typed canvas construction resolves `IDrawingBackend` from `Configuration` +- specialized infrastructure can construct a canvas with an explicit backend instance + +The CPU path usually uses the first route. The WebGPU helpers use the second route when they need a canvas that targets a native surface through `WebGPUDrawingBackend`. + +That means the CPU backend is one backend implementation within the shared canvas architecture, not a separate public drawing model. It executes against any frame that exposes a writable CPU region, whether that frame is pure memory or a hybrid frame that also carries a native surface. + +## The Main Problem + +By the time work reaches `DefaultDrawingBackend`, the public drawing API has already been normalized into prepared commands. That is helpful, but it does not make CPU execution trivial. + +The backend still has to solve a hard scheduling problem. + +It needs to answer questions such as: + +- which destination rows each command touches +- how to preserve draw order while running work in parallel +- how to avoid re-deriving geometry information in the hot loop +- where temporary memory should live and when it should be reused + +If the CPU backend executed commands directly from the incoming scene, each worker would repeatedly rediscover which rows matter, which parts of the geometry matter in those rows, and how much scratch is needed. That would push expensive planning work into the hottest part of the pipeline. + +So the backend takes a different approach: + +it turns the whole command batch into a row-oriented execution plan first, then executes that plan. + +That decision explains most of the backend architecture. + +## The Core Idea + +The CPU backend is a flush executor, not a command-at-a-time painter. + +Its central idea is: + +> convert a command batch into row-local raster work once, then execute rows directly with reusable worker-local scratch + +That is why the backend is built around `FlushScene`. + +`FlushScene` is a retained execution plan. In non-retained rendering it is short-lived and disposed after one replay entry; in retained rendering it can live with the returned `DefaultDrawingBackendScene`. Its job is to take a prepared command stream and reorganize it into a form that is cheap for the row executor to consume. + +If that idea is clear, most of the important types fall into place. + +## The Most Important Terms + +### Backend + +`DefaultDrawingBackend` is the top-level CPU executor. It owns backend policy and orchestration: + +- acquiring a writable CPU destination +- creating the retained execution plan +- executing that plan +- handling CPU layer composition + +It does not own every detail of geometry planning or scan conversion. + +It also does not own backend selection. By the time `CreateScene(...)` or `RenderScene(...)` is called, the typed canvas implementation has already chosen the backend instance that will receive the prepared work. + +### Scene + +In the canvas architecture, the backend receives a `DrawingCommandBatch`. That batch already contains prepared commands and explicit layer boundaries for one contiguous command range. + +For the CPU backend, that incoming batch is the starting point, not the final execution form. + +### Flush Scene + +`FlushScene` is the most important supporting type in the CPU backend. + +In this codebase, `FlushScene` means: + +"the retained, row-oriented execution plan for one CPU command batch" + +It owns the retained information needed to make execution cheap: + +- the visible prepared commands +- retained rasterizable geometry +- row membership +- row-local execution items +- scratch size requirements for the flush + +### Rasterizer + +`DefaultRasterizer` is the geometry-to-coverage engine. + +It is responsible for: + +- fixed-point scan conversion +- fill-rule handling +- coverage accumulation +- emitting row coverage spans + +It is not responsible for deciding which commands should run in which rows, and it does not write final pixels directly. + +### Brush Renderer + +`BrushRenderer` is the coverage-to-color engine for one prepared drawing command. + +It receives: + +- a destination row slice +- coverage data +- destination position +- reusable workspace + +and updates pixels accordingly. + +The important separation is: + +- the rasterizer decides coverage +- the brush renderer decides color +- the backend executor binds the two together + +### Worker State + +`WorkerState` is the reusable per-worker execution state. + +It owns worker-local scratch such as: + +- raster scratch +- brush workspace +- the coverage row handler state + +This is how the backend avoids allocating fresh buffers for every row item during the hot parallel pass. + +## The Big Picture Flow + +The easiest way to understand the backend is to follow one command batch from scene creation to execution. + +```mermaid +flowchart TD + A[DrawingCanvas disposal replay] --> B[DefaultDrawingBackend.CreateScene] + B --> C[FlushScene.Create] + C --> D[Prepare visible items] + D --> E[Build row-local execution plan] + E --> F[DefaultDrawingBackend.RenderScene] + F --> G[Acquire CPU destination] + G --> H[Execute rows in parallel] + H --> I[DefaultRasterizer emits coverage] + I --> J[BrushRenderer shades pixels] + J --> K[Destination frame updated] +``` + +There are three major stages in that flow: + +1. build the retained execution plan +2. establish the destination frame +3. execute rows using that plan + +## What `DefaultDrawingBackend` Owns + +`DefaultDrawingBackend` is intentionally smaller than its supporting types. It owns orchestration, not every low-level detail. + +Its responsibilities are: + +- create a `FlushScene` +- acquire a writable CPU region from the target frame +- execute that scene +- provide CPU layer composition services +- manage frame usage for CPU-backed targets + +The expensive work is delegated: + +- `FlushScene` owns retained row planning +- `DefaultRasterizer` owns scan conversion +- `BrushRenderer` owns brush-specific shading + +That split keeps each type focused on one class of problem. + +The canvas layer above that split is also important: + +- `DrawingCanvas` records public drawing intent +- `DrawingCanvasBatcher` prepares commands and constructs `DrawingCommandBatch` values +- `DefaultDrawingBackend` executes the retained scene on a CPU destination + +## Building The Flush Scene + +`FlushScene.Create(...)` turns the prepared command stream into an execution plan in several phases. Each phase changes the data into a form that is cheaper for the next phase to consume. + +```mermaid +flowchart LR + A[Prepared commands] --> B[Filter and compact visible work] + B --> C[Create retained raster geometry] + C --> D[Build row membership] + D --> E[Build row-local execution items] + E --> F[FlushScene] +``` + +### 1. Filter and compact visible work + +The scene builder begins from the incoming command stream and keeps only the work that is visible and relevant to the flush. The later phases should not pay repeatedly for invisible commands through sparse scans or conditional branching. + +### 2. Create retained raster geometry + +For each visible item, the builder decomposes the command's drawing matrix into an X/Y scale and the rotation-shear-translation-perspective residual, asks the path for its scale-baked `LinearGeometry` via `ToLinearGeometry(Vector2 scale)`, and hands both the geometry and the residual to `DefaultRasterizer` to create the retained rasterizable payload. Curve subdivision therefore happens once per (path, scale) pair — cached on the `IPath` — and any per-frame rotation or translation rides into the rasterizer as the residual without forcing the path to re-flatten. + +This step matters because it moves expensive geometry preparation out of the hot row loop and out of every frame of workloads like text or panning that drift only in their residual. + +### 3. Build row membership + +Once retained geometry exists, the scene builder determines which scene rows each item touches. That produces row-local membership information while preserving original submission order within every row. + +That detail is critical. Parallel execution is allowed, but draw order must remain deterministic within each row. + +### 4. Build row-local execution items + +The scene then materializes the payload that the row executor will visit. Each row item points into flush-owned retained storage and carries just enough metadata to reconstruct a cheap `RasterizableBand` view when execution reaches that row. + +At that point the scene is execution-ready. + +## Why The Backend Is Row-First + +The CPU backend executes rows, not commands. + +This is one of the most important architectural choices in the whole path. + +Why it helps: + +- each worker naturally touches localized destination memory +- scratch can be reused across many row items +- draw order is straightforward inside a row +- geometry planning stays out of the hottest loop + +A row-first executor fits the actual shape of CPU rendering much better than a command-first executor would. + +## The Execution Pass + +When `FlushScene.Execute(...)` runs, the backend prepares brush renderers and then executes scene rows in parallel. + +```mermaid +sequenceDiagram + participant Exec as FlushScene.Execute + participant Worker as WorkerState + participant Raster as DefaultRasterizer + participant Brush as BrushRenderer + + Exec->>Brush: create one renderer per visible item + Exec->>Worker: start parallel row pass + Worker->>Exec: enumerate row items in order + Worker->>Raster: ExecuteRasterizableBand(...) + Raster-->>Exec: coverage rows + Exec->>Brush: Apply(...) +``` + +There are two important ownership patterns in that pass: + +- renderers are created once per visible item before the hot row loop +- scratch and workspace are reused per worker during the row loop + +That is one of the backend's main performance properties. + +## How Rasterization and Shading Stay Separate + +The rasterizer and the backend solve different problems. + +`DefaultRasterizer` is responsible for geometry and coverage. + +`DefaultDrawingBackend` and `FlushScene` are responsible for: + +- which items execute +- when they execute +- where their coverage belongs in the destination +- which brush renderer should consume that coverage + +That separation is intentional. It lets the rasterizer stay geometry-focused while the backend handles composition and destination layout. + +## Coverage Routing + +The rasterizer does not write destination pixels directly. Instead it emits row coverage through a handler supplied by the backend. + +The backend-side row handler: + +- receives emitted coverage +- maps band-local coordinates back into destination coordinates +- slices the correct destination row +- invokes the correct `BrushRenderer` + +```mermaid +flowchart LR + A[Rasterizer coverage row] --> B[Row handler] + B --> C[Map to destination slice] + C --> D[BrushRenderer.Apply] + D --> E[Pixels updated] +``` + +This is why the brush renderer can stay target-unbound. It receives the destination row slice and coverage data at execution time rather than owning the destination frame itself. + +## Layer Composition + +CPU layer composition is a separate concern from path rasterization. + +`ComposeLayer()` composites one CPU frame into another using `PixelBlender`. That path exists because compositing an already-rasterized layer is a different problem from scanning geometry into coverage. + +Keeping those paths separate makes the backend easier to reason about. + +## Frame And Memory Lifetime + +The backend aligns ownership with the actual execution lifetime. + +### Flush-owned + +Owned by `FlushScene`: + +- visible item arrays +- row structures +- retained raster data +- start-cover storage + +Disposed when the flush ends. + +### Worker-owned + +Owned by `WorkerState` during execution: + +- raster scratch +- brush workspace + +Disposed when the worker completes. + +### Item-owned + +Created once per visible item during execution: + +- `BrushRenderer` + +Retained for the duration of the row pass and then released with the flush-owned scene item state. + +That ownership model keeps allocation and disposal aligned with real work lifetime. + +## Reading Guide + +If you are new to this backend, read the code in this order: + +1. `DrawingCanvas.cs` +2. `DrawingCanvas{TPixel}.cs` +3. `DrawingCanvasBatcher{TPixel}.cs` +4. `DefaultDrawingBackend.cs` +5. `FlushScene.cs` +6. `FlushScene.RetainedTypes.cs` +7. `DefaultDrawingBackend.Helpers.cs` +8. `DefaultRasterizer.cs` + +That order mirrors the runtime flow: + +canvas and backend selection -> backend orchestration -> retained row planning -> row execution structures -> worker helpers -> scan conversion + +## The Mental Model To Keep + +The easiest way to keep this backend straight is to remember that it is not a command-at-a-time painter. It is a flush executor that converts visible commands into row-local retained raster work and then executes that work with reusable scratch. + +If that model is clear, the major types fall into place: + +- `DrawingCanvas` records intent, and the typed implementation selects the backend +- `DefaultDrawingBackend` orchestrates +- `FlushScene` plans +- `DefaultRasterizer` converts geometry to coverage +- `BrushRenderer` converts coverage to color diff --git a/ImageSharp.Drawing/Processing/Backends/DEFAULT_RASTERIZER.md b/ImageSharp.Drawing/Processing/Backends/DEFAULT_RASTERIZER.md new file mode 100644 index 0000000..7682a84 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/DEFAULT_RASTERIZER.md @@ -0,0 +1,458 @@ +# DefaultRasterizer + +`DefaultRasterizer` is the CPU polygon scanner used by the retained fill path in ImageSharp.Drawing. Its job is narrow but central: take already-prepared geometry, convert that geometry into fixed-point edge contributions, and emit coverage rows that the CPU backend can turn into pixels. + +This rasterizer is based on ideas and implementation techniques from the Blaze project: + +- https://github.com/aurimasg/blaze + +This document explains the rasterizer as a newcomer needs to understand it: + +- where the rasterizer fits relative to `DrawingCanvas` and `DefaultDrawingBackend` +- what problem the rasterizer is solving inside the CPU backend +- why the rasterizer is split into retained geometry building and band execution +- what retained geometry, bands, and coverage mean in this architecture +- how scan conversion stays separate from brush shading and frame ownership + +## Where The Rasterizer Fits + +`DefaultRasterizer` sits below `DrawingCanvas`, the typed canvas implementation, and `DefaultDrawingBackend`. + +The canvas records commands, the batcher prepares them into `DrawingCommandBatch` ranges, and `DefaultDrawingBackend` chooses the row-oriented execution plan for each retained CPU scene. `DefaultRasterizer` then handles the narrower geometry-to-coverage problem inside that CPU execution path. + +That means the rasterizer does not select the backend, own the destination frame, or interpret the public drawing API directly. It receives already-prepared geometry through the CPU backend pipeline, and the backend later routes its coverage into whichever frame exposes the CPU region for the flush. + +## The Main Problem + +The CPU backend does not want to rediscover shape geometry every time it touches a destination row. + +If row execution had to start from raw prepared paths every time, the backend would repeatedly need to: + +- walk contours +- split segments against row-band boundaries +- compute left-of-band winding influence +- rebuild scan-conversion state for the same shape over and over + +That would push expensive geometry work into the hottest part of CPU rendering. + +So the rasterizer solves a different problem: + +it builds retained rasterizable geometry once, then executes compact band-local scanning work many times, cheaply. + +That two-phase design is the core idea behind `DefaultRasterizer`. + +## The Core Idea + +The rasterizer is a retained fixed-point polygon scanner. + +Its central idea is: + +> build band-local retained line data once, then execute fixed-point scan conversion from that retained data + +This is why the rasterizer has two very different modes of work: + +1. retained geometry building +2. band execution + +The first phase is a preparation phase. The second is the hot execution phase. + +If that distinction is clear, the code becomes much easier to follow. + +## The Most Important Terms + +### Rasterizer + +`DefaultRasterizer` is the geometry-to-coverage engine. + +It is responsible for: + +- converting prepared geometry into retained scan-conversion data +- rasterizing retained band data with fixed-point arithmetic +- emitting coverage rows + +It is not responsible for: + +- brush color generation +- destination frame ownership +- layer composition +- deciding which scene items should execute + +Those problems belong to the CPU backend and `FlushScene`. + +### Retained Geometry + +Retained geometry is the rasterizer's prepared execution payload. + +In this codebase, retained geometry means: + +"the fixed-point, band-local line data and start-cover seeds needed to rasterize one prepared shape later without revisiting its original contour data" + +That retained form is stored in `RasterizableGeometry`. + +### Band + +A band is one small vertical slice of a shape's retained geometry. + +The rasterizer does not keep one giant scene-wide edge table. It stores data in row bands so execution can stay local and bounded. + +### Rasterizable Geometry + +`RasterizableGeometry` is the retained representation of one prepared shape. + +It stores: + +- clipped local bounds +- band-local metadata +- retained line arrays +- optional start-cover seeds for bands that need carry-in winding + +This is the retained object that the CPU backend keeps in `FlushScene`. + +### Rasterizable Band + +A `RasterizableBand` is the execution-time view over one retained band of one retained shape. + +It is the immediate input to `ExecuteRasterizableBand(...)`. + +### Context + +`DefaultRasterizer.Context` is the mutable fixed-point scanning state used during band execution. + +It is a `ref struct` because it is tied directly to worker-owned scratch spans and should not escape the execution scope. + +### Coverage + +Coverage is the rasterizer's output. + +The rasterizer does not decide final pixel colors. It decides how much geometric coverage each pixel receives. The backend later passes that coverage to a `BrushRenderer`, which decides how the destination pixels should be shaded. + +## Pipeline Placement + +The rasterizer sits in the middle of the CPU backend pipeline. + +Upstream: + +- `CompositionCommand` preparation produces prepared geometry +- the typed canvas implementation and `DrawingCanvasBatcher` have already selected and called the CPU backend +- `FlushScene` decides which items are visible and when they execute + +Downstream: + +- the rasterizer emits row coverage +- `DefaultDrawingBackend` routes that coverage into `BrushRenderer.Apply(...)` + +```mermaid +flowchart TD + A[Prepared geometry] --> B[DefaultRasterizer.CreateRasterizableGeometry] + B --> C[RasterizableGeometry] + C --> D[Build RasterizableBand view] + D --> E[ExecuteRasterizableBand] + E --> F[Coverage rows] + F --> G[Brush renderer] +``` + +That placement is important. The rasterizer is neither the public drawing model nor the final shading model. It is the geometry-to-coverage step between them. + +## Why The Rasterizer Has Two Phases + +The rasterizer separates: + +1. building retained geometry +2. executing retained geometry + +### Phase 1: retained geometry building + +`CreateRasterizableGeometry(...)` converts prepared geometry into a retained representation that is cheap to execute later. + +This phase: + +- walks prepared contours +- converts coordinates into fixed-point +- clips or splits segments as needed for band boundaries +- records visible line pieces into retained line storage +- records left-of-band winding influence into start-cover tables + +The output is `RasterizableGeometry`. + +### Phase 2: band execution + +`ExecuteRasterizableBand(...)` is the hot execution entry point. + +It does not revisit the original contour data. It receives a `RasterizableBand` view over retained data and performs the minimum work needed to emit coverage rows for that band. + +```mermaid +sequenceDiagram + participant Exec as ExecuteRasterizableBand + participant Ctx as Context + participant Emit as Coverage Row Handler + + Exec->>Ctx: Reconfigure(...) + Exec->>Ctx: SeedStartCovers(...) + Exec->>Ctx: Rasterize retained lines + Exec->>Ctx: EmitCoverageRows(...) + Ctx-->>Emit: coverage rows + Exec->>Ctx: ResetTouchedRows() +``` + +That separation is one of the key reasons the retained fill path performs well. Expensive geometry work happens once; execution consumes compact band-local data. + +## Fixed-Point Precision + +The rasterizer works in 24.8 fixed-point coordinates. + +That means: + +- `1` pixel = `256` fixed-point units +- `FixedShift = 8` +- `FixedOne = 256` + +This gives the scanner subpixel precision while keeping the hot execution path integer-based. Geometry may begin as floating-point path data, but once a retained line reaches the scan-conversion core it is treated as fixed-point state. + +Coverage is converted back into normalized `float` values only at the emission boundary. + +## Why Bands Exist + +The rasterizer does not retain one monolithic edge table. It retains geometry in vertical row bands. + +That matters because it keeps execution local and bounded. + +When a segment crosses multiple bands, the linearizer splits it so each band receives only the portion it must scan. If a segment influences winding inside the visible band from the left side, that influence is folded into a start-cover seed rather than keeping an invisible off-screen line around forever. + +This gives the backend several important properties: + +- execution only touches the band it is currently composing +- left-of-band winding can be precomputed +- scratch requirements stay bounded +- row-oriented execution consumes compact band-local payloads + +```mermaid +flowchart TD + A[Contour segment] --> B{Touches one band?} + B -- Yes --> C[Store visible line in that band] + B -- No --> D[Split across band boundaries] + D --> E[Store band-local visible pieces] + D --> F[Accumulate start-cover seeds where needed] +``` + +## Retained Geometry: What Gets Stored + +`RasterizableGeometry` stores the retained data needed to rasterize a prepared shape later. + +That includes: + +- the local bounds of the prepared shape +- band count and band-local metadata +- retained line arrays for each band +- optional start-cover arrays for bands that need carry-in winding + +The retained line arrays use specialized storage formats such as: + +- `LineArrayX16Y16` +- `LineArrayX32Y16` + +These are storage-oriented types. They exist to retain compact fixed-point line segments so execution does not need to revisit contour data. + +## The Linearizer + +The linearizer is the retained-geometry builder. It is generic over line-array storage, but the conceptual work is the same across variants. + +Its responsibilities are: + +- traverse prepared contours +- apply the residual transform per-point as contours are read +- clip work to retained bounds +- convert coordinates into fixed-point +- decide whether a segment is contained or must be split +- store visible line pieces +- accumulate start covers for left-of-band influence + +For a newcomer, the most important thing to understand is that the linearizer is not the hot coverage emitter. It is the preparation step that turns arbitrary contour geometry into a stable retained scanning payload. + +### Residual transform application + +The prepared `LinearGeometry` passed to `CreateRasterizableGeometry(...)` carries scale-baked points — the effective X/Y scale of the drawing matrix has already been absorbed into the flattened contour, so curve subdivision happens at device-scale precision. The remaining rotation, shear, translation, and perspective is handed to the rasterizer as a separate `Matrix4x4 residual`, which the linearizer applies per-point where the contour is read: at segment emission time in `ProcessContained` / `ProcessUncontained` for fills, and at bounds / closure / contour-segment construction sites in the stroke linearizer. + +This split keeps the scale-baked geometry cacheable across frames (text and panning workloads reuse the same bake at a fixed zoom) while letting per-frame rotation or translation ride through the rasterizer without re-subdividing curves. + +### Contained lines + +A contained line is one whose fixed-point endpoints already fit the assumptions of the current retained band representation. Those lines can be pushed directly into retained storage after the required fixed-point and band-boundary handling. + +### Split lines + +When a line crosses band boundaries, the linearizer splits it so each band receives only the contribution it needs to scan. + +### Start-cover seeding + +When a line contributes winding inside the visible band but lies partially to the left of the visible X range, the retained geometry stores that influence in a start-cover array instead of retaining an off-screen line. + +This is one of the most important ideas in the retained design: + +- visible geometry becomes retained lines +- invisible left-of-band winding becomes retained start-cover seeds + +## The Execution Context + +`DefaultRasterizer.Context` is the mutable fixed-point scanning state used during band execution. + +It owns per-band mutable state such as: + +- `bitVectors` +- `coverArea` +- `startCover` +- `rowMinTouchedColumn` +- `rowMaxTouchedColumn` +- `rowHasBits` +- `rowTouched` +- `touchedRows` + +This state is reused across bands by reconfiguration, not by reallocation. + +```mermaid +flowchart LR + A[WorkerScratch] --> B[Context] + B --> C[Rasterize retained lines] + C --> D[Mutate coverArea and bit vectors] + D --> E[Emit coverage rows] + E --> F[Reset touched rows] +``` + +The `Context` bridges retained geometry and emitted coverage. + +## How Coverage Accumulation Works + +The rasterizer uses the classic area-and-cover formulation. + +When a fixed-point line is rasterized, it is broken into cell contributions. Those contributions eventually reach `AddCell(...)`, which updates: + +- delta cover +- delta area + +Rows also track sparse touched-column information through bit vectors, so the emitter can avoid scanning the full width of empty rows. + +```mermaid +flowchart TD + A[Rasterize fixed-point line] --> B[Decompose into touched cells] + B --> C["AddCell(row, column, deltaCover, deltaArea)"] + C --> D[Update coverArea] + C --> E[Mark bitVectors] + C --> F[Track touched rows and bounds] + C --> G{column < 0?} + G -- Yes --> H[Fold into startCover] + G -- No --> I[Keep visible cell contribution] +``` + +This is why the rasterizer can honor fill rules later. It accumulates signed contributions first and applies the fill rule during coverage emission. + +## Coverage Emission + +`EmitCoverageRows(...)` converts the accumulated fixed-point state into row spans. + +For each touched row, the emitter: + +1. starts from the seeded `startCover` +2. walks the row's touched columns using the bit vectors +3. updates the running cover from `deltaCover` +4. combines running cover and `deltaArea` into signed area +5. converts signed area into normalized coverage using the selected fill rule +6. coalesces equal-coverage spans +7. writes only non-zero spans into the reusable scanline buffer +8. invokes the row callback + +```mermaid +flowchart LR + A[Touched row] --> B[Walk set bits] + B --> C[Reconstruct cover and area] + C --> D[Apply fill rule] + D --> E[Coalesce equal coverage] + E --> F[Write compact scanline spans] + F --> G[Invoke row handler] +``` + +The rasterizer therefore emits only rows that actually received contributions and only the non-zero spans within those rows. + +## Fill Rules + +The rasterizer supports both `NonZero` and `EvenOdd`. + +### NonZero + +The accumulated signed area is treated as winding magnitude. Coverage is the clamped absolute value of that area. + +### EvenOdd + +The accumulated area is wrapped into the even-odd domain before coverage is produced. This gives parity-based behavior without changing the earlier scan-conversion logic. + +The fill rule is therefore an emission-time decision, not a geometry-preprocessing decision. + +## Antialiased And Aliased Modes + +The rasterizer can emit either continuous or thresholded coverage. + +- `Antialiased` mode keeps the continuous coverage produced by the area-and-cover math +- `Aliased` mode thresholds that continuous coverage using `AntialiasThreshold` + +The scan-conversion core stays the same in both modes. Only the final conversion from area to emitted coverage changes. + +## Why Self-Intersections Work + +The rasterizer can handle self-intersections because it does not require geometric boolean normalization before rasterization. It accumulates signed contributions and then applies the selected fill rule during emission. + +That means overlapping or self-crossing contours are resolved by: + +- area-and-cover integration +- winding or parity mapping + +instead of by an earlier polygon-boolean pass. + +## How The Rasterizer Stays Separate From The Backend + +The rasterizer and the backend solve different problems. + +The rasterizer decides: + +- how geometry contributes coverage +- which rows and columns within a band are touched +- how much coverage each emitted span has + +The backend decides: + +- which scene items execute +- which retained band is being scanned +- which destination slice receives the coverage +- which brush renderer consumes the emitted spans + +That separation is one of the main architectural advantages of the current CPU path. + +## Reading Guide + +If you are new to this part of the library, read the rasterizer in this order: + +1. `DrawingCanvas.cs` +2. `DrawingCanvas{TPixel}.cs` +3. `DrawingCanvasBatcher{TPixel}.cs` +4. `DefaultDrawingBackend.cs` +5. `FlushScene.cs` +6. `CreateRasterizableGeometry(...)` in `DefaultRasterizer.cs` +7. `Linearizer` and the concrete linearizers in `DefaultRasterizer.Linearizer.cs` +8. retained line types in `DefaultRasterizer.RetainedTypes.cs` +9. `ExecuteRasterizableBand(...)` in `DefaultRasterizer.cs` +10. `Context` in `DefaultRasterizer.cs` + +That order mirrors the data lifecycle: + +canvas intent -> prepared geometry -> retained storage -> band execution -> coverage emission + +## The Mental Model To Keep + +The easiest way to reason about `DefaultRasterizer` is this: + +it is a retained fixed-point polygon scanner that transforms prepared geometry into compact band-local line payloads, then turns those payloads into row coverage spans. + +If that model stays clear, the rest of the code becomes easier to read: + +- the canvas and backend docs explain how execution reaches the CPU path +- the linearizer explains where retained line data comes from +- `RasterizableGeometry` explains what is stored +- the `Context` explains how retained data becomes coverage +- the backend explains how coverage becomes pixels diff --git a/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.Helpers.cs b/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.Helpers.cs new file mode 100644 index 0000000..64b67ab --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.Helpers.cs @@ -0,0 +1,202 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// CPU backend that executes path coverage rasterization and brush composition directly against a CPU region. + /// + public sealed partial class DefaultDrawingBackend + { + /// + /// Adapts rasterizer coverage callbacks into brush application against the active band target. + /// + /// The pixel format. + private readonly struct FillCoverageRowHandler : IRasterizerCoverageRowHandler + where TPixel : unmanaged, IPixel + { + private readonly BrushRenderer renderer; + private readonly BandTarget target; + private readonly BrushWorkspace brushWorkspace; + + /// + /// Initializes a new instance of the struct. + /// + /// The brush renderer that will consume emitted coverage spans. + /// The active band target being rendered. + /// The worker-local brush workspace. + public FillCoverageRowHandler( + BrushRenderer renderer, + BandTarget target, + BrushWorkspace brushWorkspace) + { + this.renderer = renderer; + this.target = target; + this.brushWorkspace = brushWorkspace; + } + + /// + /// Applies one emitted coverage span to the active destination band. + /// + /// The absolute destination row. + /// The absolute start column of the coverage span. + /// The emitted coverage values. + public void Handle(int y, int startX, Span coverage) + { + int localY = y - this.target.AbsoluteTop; + if ((uint)localY >= (uint)this.target.Region.Height) + { + return; + } + + int clipStartX = Math.Max(startX, this.target.AbsoluteLeft); + int clipEndX = Math.Min(startX + coverage.Length, this.target.AbsoluteLeft + this.target.Region.Width); + if (clipEndX <= clipStartX) + { + return; + } + + // The rasterizer emits absolute coordinates; clip them once here so the brush + // renderer can operate against a tight destination span with no extra bounds work. + int coverageOffset = clipStartX - startX; + int clippedLength = clipEndX - clipStartX; + Span destinationRow = this.target.Region + .DangerousGetRowSpan(localY) + .Slice(clipStartX - this.target.AbsoluteLeft, clippedLength); + this.renderer.Apply(destinationRow, coverage.Slice(coverageOffset, clippedLength), clipStartX, y, this.brushWorkspace); + } + } + + /// + /// Represents one active composition target for a retained row. + /// + /// The pixel format. + private sealed class BandTarget : IDisposable + where TPixel : unmanaged, IPixel + { + private readonly Buffer2D? owner; + + /// + /// Initializes a new instance of the class over an existing region. + /// + /// The destination region. + /// The absolute X origin of the region. + /// The absolute Y origin of the region. + /// The graphics options used when this target is later composited. + public BandTarget(Buffer2DRegion region, int absoluteLeft, int absoluteTop, GraphicsOptions? graphicsOptions) + { + this.Region = region; + this.AbsoluteLeft = absoluteLeft; + this.AbsoluteTop = absoluteTop; + this.GraphicsOptions = graphicsOptions; + } + + /// + /// Initializes a new instance of the class over an owned temporary buffer. + /// + /// The owned buffer backing the target. + /// The absolute bounds represented by the target. + /// The graphics options used when this target is later composited. + public BandTarget(Buffer2D owner, Rectangle bounds, GraphicsOptions? graphicsOptions) + { + this.owner = owner; + this.Region = owner.GetRegion(); + this.AbsoluteLeft = bounds.X; + this.AbsoluteTop = bounds.Y; + this.GraphicsOptions = graphicsOptions; + } + + /// + /// Gets the writable pixel region for the target. + /// + public Buffer2DRegion Region { get; } + + /// + /// Gets the absolute X origin of . + /// + public int AbsoluteLeft { get; } + + /// + /// Gets the absolute Y origin of . + /// + public int AbsoluteTop { get; } + + /// + /// Gets the graphics options associated with the target when it is used as a layer. + /// + public GraphicsOptions? GraphicsOptions { get; } + + /// + /// Releases the owned temporary buffer when the target represents a layer. + /// + public void Dispose() => this.owner?.Dispose(); + } + + /// + /// Holds the reusable worker-local scratch used while executing retained scene rows. + /// + /// The pixel format. + private sealed class WorkerState : IDisposable + where TPixel : unmanaged, IPixel + { + private readonly MemoryAllocator allocator; + private DefaultRasterizer.WorkerScratch? scratch; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator used for scratch growth. + /// The destination width used to size the brush workspace. + /// The maximum retained layer depth required by the scene. + public WorkerState( + MemoryAllocator allocator, + int destinationWidth, + int layerDepth) + { + this.allocator = allocator; + this.BrushWorkspace = new BrushWorkspace(allocator, destinationWidth); + this.TargetStack = new BandTarget[layerDepth]; + } + + /// + /// Gets the reusable brush workspace for the worker. + /// + public BrushWorkspace BrushWorkspace { get; } + + /// + /// Gets the reusable composition target stack for the worker. + /// + public BandTarget[] TargetStack { get; } + + /// + /// Returns a reusable raster scratch instance sized for the requested width. + /// + /// The minimum scanline width required by the current row. + /// A scratch instance that can execute the row. + public DefaultRasterizer.WorkerScratch GetOrCreateScratch(int requiredWidth) + { + DefaultRasterizer.WorkerScratch? current = this.scratch; + if (current is not null && current.CanReuse(requiredWidth)) + { + return current; + } + + current?.Dispose(); + this.scratch = DefaultRasterizer.CreateWorkerScratch(this.allocator, requiredWidth); + return this.scratch; + } + + /// + /// Releases the worker-local scratch and brush workspace. + /// + public void Dispose() + { + this.scratch?.Dispose(); + this.BrushWorkspace.Dispose(); + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs new file mode 100644 index 0000000..7b096a1 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -0,0 +1,483 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// CPU backend that executes path coverage rasterization and brush composition directly against a CPU region. + /// + public sealed partial class DefaultDrawingBackend : IDrawingBackend + { + /// + /// Gets the default backend instance. + /// + public static DefaultDrawingBackend Instance { get; } = new(); + + /// + public DrawingBackendScene CreateScene( + Configuration configuration, + Rectangle targetBounds, + DrawingCommandBatch commandBatch, + IReadOnlyList? ownedResources = null) + { + FlushScene scene = FlushScene.Create( + commandBatch, + targetBounds, + configuration.MemoryAllocator, + configuration.MaxDegreeOfParallelism); + + return new DefaultDrawingBackendScene(scene, targetBounds, ownedResources); + } + + /// + public void RenderScene( + Configuration configuration, + ICanvasFrame target, + DrawingBackendScene scene) + where TPixel : unmanaged, IPixel + { + if (scene is not DefaultDrawingBackendScene cpuScene) + { + throw new InvalidOperationException("The retained scene is not a CPU drawing backend scene."); + } + + if (!target.TryGetCpuRegion(out Buffer2DRegion destinationFrame)) + { + throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible frame targets."); + } + + if (target.Bounds != cpuScene.Bounds) + { + throw new InvalidOperationException("The target bounds do not match the retained CPU scene bounds."); + } + + if (cpuScene.Scene is FlushScene flushScene && flushScene.RowCount != 0) + { + ExecuteScene(configuration, destinationFrame, flushScene); + } + } + + /// + /// Executes one retained flush scene against a CPU destination frame. + /// + /// The pixel format. + /// The active processing configuration. + /// The destination CPU region. + /// The retained scene to execute. + private static void ExecuteScene( + Configuration configuration, + Buffer2DRegion destinationFrame, + FlushScene scene) + where TPixel : unmanaged, IPixel + { + // Warm the cached renderers before the row loop so the hot execution path only + // performs retained-scene work and brush application. + if (scene.FillItemCount > 0) + { + for (int i = 0; i < scene.FillItems.Length; i++) + { + if (scene.FillItems[i] is FlushScene.FillSceneItem item) + { + _ = item.GetRenderer(configuration, destinationFrame.Width); + } + } + } + + if (scene.StrokeItemCount > 0) + { + for (int i = 0; i < scene.StrokeItems.Length; i++) + { + if (scene.StrokeItems[i] is FlushScene.StrokeSceneItem item) + { + _ = item.GetRenderer(configuration, destinationFrame.Width); + } + } + } + + int requestedParallelism = configuration.MaxDegreeOfParallelism; + _ = Parallel.For( + fromInclusive: 0, + toExclusive: scene.RowCount, + parallelOptions: ParallelExecutionHelper.CreateParallelOptions(requestedParallelism, scene.RowCount), + localInit: () => new WorkerState(configuration.MemoryAllocator, destinationFrame.Width, scene.MaxLayerDepth + 1), + body: (rowIndex, _, state) => + { + ExecuteSceneRow( + configuration, + destinationFrame, + scene, + scene.Rows[rowIndex], + state); + + return state; + }, + localFinally: static state => state.Dispose()); + } + + /// + /// Executes one retained scene row against the destination band it overlaps. + /// + /// The pixel format. + /// The active processing configuration. + /// The destination CPU region. + /// The retained flush scene. + /// The retained scene row to execute. + /// The worker-local scratch and compositing state. + private static void ExecuteSceneRow( + Configuration configuration, + Buffer2DRegion destinationFrame, + FlushScene scene, + in FlushScene.SceneRow row, + WorkerState state) + where TPixel : unmanaged, IPixel + { + int bandTop = row.RowBandIndex * DefaultRasterizer.DefaultTileHeight; + int localBandTop = bandTop - destinationFrame.Bounds.Y; + int bandHeight = Math.Min(DefaultRasterizer.DefaultTileHeight, destinationFrame.Height - localBandTop); + if (bandHeight <= 0) + { + return; + } + + Buffer2DRegion destinationBand = destinationFrame.GetSubRegion(0, localBandTop, destinationFrame.Width, bandHeight); + BandTarget[] targetStack = state.TargetStack; + int targetCount = 1; + targetStack[0] = new BandTarget(destinationBand, destinationFrame.Bounds.X, bandTop, null); + int scratchWidth = GetRowScratchWidth(scene, row, destinationFrame.Width); + DefaultRasterizer.WorkerScratch scratch = state.GetOrCreateScratch(scratchWidth); + + try + { + for (FlushScene.SceneOperationBlock? block = row.FirstBlock; block is not null; block = block.Next) + { + foreach (FlushScene.SceneOperation operation in block.Items) + { + // Each retained row contains a compact mix of layer control operations and + // draw operations in original command order, so the executor can replay the + // row without re-walking the full scene description. + switch (operation.Kind) + { + case FlushScene.SceneOperationKind.BeginLayer: + GraphicsOptions? layerOptions = scene.LayerOptions[operation.ItemIndex]; + + targetStack[targetCount++] = + new BandTarget( + configuration.MemoryAllocator.Allocate2D(operation.LayerBounds.Width, operation.LayerBounds.Height, AllocationOptions.Clean), + operation.LayerBounds, + layerOptions); + break; + + case FlushScene.SceneOperationKind.EndLayer: + BandTarget source = targetStack[--targetCount]; + BandTarget destination = targetStack[targetCount - 1]; + CompositeLayerBand(configuration, source, destination, state.BrushWorkspace); + source.Dispose(); + break; + + case FlushScene.SceneOperationKind.FillItem: + BandTarget target = targetStack[targetCount - 1]; + FlushScene.FillSceneItem sceneItem = scene.FillItems[operation.ItemIndex]!; + ExecuteFillOperation( + sceneItem.GetRenderer(configuration, destinationFrame.Width), + new DefaultRasterizer.RasterizableItem(sceneItem.Rasterizable, operation.LocalRowIndex), + target, + scratch, + state); + break; + + case FlushScene.SceneOperationKind.StrokeItem: + BandTarget strokeTarget = targetStack[targetCount - 1]; + FlushScene.StrokeSceneItem strokeSceneItem = scene.StrokeItems[operation.ItemIndex]!; + ExecuteStrokeOperation( + strokeSceneItem.GetRenderer(configuration, destinationFrame.Width), + new DefaultRasterizer.StrokeRasterizableItem(strokeSceneItem.Rasterizable, operation.LocalRowIndex), + strokeTarget, + scratch, + state); + break; + } + } + } + } + finally + { + for (int i = 1; i < targetCount; i++) + { + targetStack[i].Dispose(); + targetStack[i] = null!; + } + + targetStack[0] = null!; + } + } + + /// + /// Computes the minimum reusable scratch width needed to execute one retained scene row. + /// + /// The retained flush scene. + /// The retained scene row. + /// The baseline width taken from the destination band. + /// The scratch width required by the row. + private static int GetRowScratchWidth( + FlushScene scene, + in FlushScene.SceneRow row, + int minimumWidth) + { + int width = minimumWidth; + for (FlushScene.SceneOperationBlock? block = row.FirstBlock; block is not null; block = block.Next) + { + foreach (FlushScene.SceneOperation operation in block.Items) + { + if (operation.Kind is FlushScene.SceneOperationKind.BeginLayer or FlushScene.SceneOperationKind.EndLayer) + { + continue; + } + + int itemWidth = operation.Kind == FlushScene.SceneOperationKind.FillItem + ? scene.FillItems[operation.ItemIndex]!.Rasterizable.Width + : scene.StrokeItems[operation.ItemIndex]!.Rasterizable.Width; + if (itemWidth > width) + { + width = itemWidth; + } + } + } + + return width; + } + + /// + /// Executes one retained fill operation through the rasterizer and brush renderer. + /// + /// The pixel format. + /// The memoized brush renderer for the scene item. + /// The retained rasterizable row item to execute. + /// The active composition target for the row. + /// The worker-local raster scratch. + /// The worker-local execution state. + private static void ExecuteFillOperation( + BrushRenderer renderer, + DefaultRasterizer.RasterizableItem item, + BandTarget target, + DefaultRasterizer.WorkerScratch scratch, + WorkerState state) + where TPixel : unmanaged, IPixel + { + DefaultRasterizer.RasterizableBandInfo bandInfo = item.Rasterizable.GetBandInfo(item.LocalRowIndex); + DefaultRasterizer.Context context = scratch.CreateContext( + bandInfo.IntersectionRule, + bandInfo.RasterizationMode, + bandInfo.AntialiasThreshold); + FillCoverageRowHandler rowHandler = new(renderer, target, state.BrushWorkspace); + DefaultRasterizer.ExecuteRasterizableItem( + ref context, + in item, + in bandInfo, + scratch.Scanline, + ref rowHandler); + } + + /// + /// Executes one retained stroke operation through the rasterizer and brush renderer. + /// + /// The pixel format. + /// The memoized brush renderer for the scene item. + /// The retained stroke rasterizable row item to execute. + /// The active composition target for the row. + /// The worker-local raster scratch. + /// The worker-local execution state. + private static void ExecuteStrokeOperation( + BrushRenderer renderer, + DefaultRasterizer.StrokeRasterizableItem item, + BandTarget target, + DefaultRasterizer.WorkerScratch scratch, + WorkerState state) + where TPixel : unmanaged, IPixel + { + DefaultRasterizer.RasterizableBandInfo bandInfo = item.Rasterizable.GetBandInfo(item.LocalRowIndex); + DefaultRasterizer.Context context = scratch.CreateContext( + bandInfo.IntersectionRule, + bandInfo.RasterizationMode, + bandInfo.AntialiasThreshold); + FillCoverageRowHandler rowHandler = new(renderer, target, state.BrushWorkspace); + Span strokeBandCoverage = item.Rasterizable.RequiresBandCoverage ? scratch.StrokeBandCoverage : []; + DefaultRasterizer.ExecuteStrokeRasterizableItem( + ref context, + in item, + in bandInfo, + scratch.Scanline, + strokeBandCoverage, + ref rowHandler); + } + + /// + /// Composites one temporary layer band back into its destination band. + /// + /// The pixel format. + /// The active processing configuration. + /// The source layer band. + /// The destination band to blend into. + /// The worker-local amount buffer workspace. + private static void CompositeLayerBand( + Configuration configuration, + BandTarget source, + BandTarget destination, + BrushWorkspace brushWorkspace) + where TPixel : unmanaged, IPixel + { + int width = source.Region.Width; + if (width == 0 || source.Region.Height == 0) + { + return; + } + + Rectangle overlap = Rectangle.Intersect( + new Rectangle(source.AbsoluteLeft, source.AbsoluteTop, source.Region.Width, source.Region.Height), + new Rectangle(destination.AbsoluteLeft, destination.AbsoluteTop, destination.Region.Width, destination.Region.Height)); + + if (overlap.Width <= 0 || overlap.Height <= 0) + { + return; + } + + if (source.GraphicsOptions is not GraphicsOptions graphicsOptions) + { + return; + } + + PixelBlender blender = PixelOperations.Instance.GetPixelBlender(graphicsOptions); + Span amounts = brushWorkspace.GetAmounts(overlap.Width); + amounts[..overlap.Width].Fill(graphicsOptions.BlendPercentage); + + int sourceOffsetX = overlap.X - source.AbsoluteLeft; + int sourceOffsetY = overlap.Y - source.AbsoluteTop; + int destinationOffsetX = overlap.X - destination.AbsoluteLeft; + int destinationOffsetY = overlap.Y - destination.AbsoluteTop; + + // Blend the overlapping rows only; the retained scene has already clipped the layer + // bounds so there is no need for extra per-pixel bounds logic here. + for (int y = 0; y < overlap.Height; y++) + { + Span sourceRow = source.Region.DangerousGetRowSpan(sourceOffsetY + y).Slice(sourceOffsetX, overlap.Width); + Span destinationRow = destination.Region.DangerousGetRowSpan(destinationOffsetY + y).Slice(destinationOffsetX, overlap.Width); + blender.Blend( + configuration, + destinationRow, + destinationRow, + sourceRow, + amounts[..overlap.Width], + brushWorkspace.GetBlendScratch(overlap.Width, 3)); + } + } + + /// + /// Composites one CPU-backed frame onto another using the supplied graphics options. + /// + /// The pixel format. + /// The active processing configuration. + /// The source frame. + /// The destination frame. + /// The destination offset relative to . + /// The graphics options controlling composition. + public static void ComposeLayer( + Configuration configuration, + ICanvasFrame source, + ICanvasFrame destination, + Point destinationOffset, + GraphicsOptions options) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + + if (!source.TryGetCpuRegion(out Buffer2DRegion sourceRegion)) + { + throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible source frames."); + } + + if (!destination.TryGetCpuRegion(out Buffer2DRegion destinationRegion)) + { + throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible destination frames."); + } + + PixelBlender blender = PixelOperations.Instance.GetPixelBlender(options); + float blendPercentage = options.BlendPercentage; + + int srcWidth = sourceRegion.Width; + int srcHeight = sourceRegion.Height; + int dstWidth = destinationRegion.Width; + int dstHeight = destinationRegion.Height; + + // Clamp the compositing region to both source and destination bounds. + int startX = Math.Max(0, -destinationOffset.X); + int startY = Math.Max(0, -destinationOffset.Y); + int endX = Math.Min(srcWidth, dstWidth - destinationOffset.X); + int endY = Math.Min(srcHeight, dstHeight - destinationOffset.Y); + + if (endX <= startX || endY <= startY) + { + return; + } + + int width = endX - startX; + + // Allocate a reusable per-row amount buffer from the memory pool. + using IMemoryOwner amountsOwner = configuration.MemoryAllocator.Allocate(width); + Span amounts = amountsOwner.Memory.Span; + amounts.Fill(blendPercentage); + + for (int y = startY; y < endY; y++) + { + Span srcRow = sourceRegion.DangerousGetRowSpan(y).Slice(startX, width); + int dstX = destinationOffset.X + startX; + int dstY = destinationOffset.Y + y; + Span dstRow = destinationRegion.DangerousGetRowSpan(dstY).Slice(dstX, width); + + blender.Blend(configuration, dstRow, dstRow, srcRow, amounts); + } + } + + /// + public void ReadRegion( + Configuration configuration, + ICanvasFrame target, + Rectangle sourceRectangle, + Buffer2DRegion destination) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(destination.Buffer, nameof(destination)); + + // CPU backend readback is available only when the target exposes CPU pixels. + if (!target.TryGetCpuRegion(out Buffer2DRegion sourceRegion)) + { + throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible frame targets for readback."); + } + + // Clamp the request to the target region to avoid out-of-range row slicing. + Rectangle clipped = Rectangle.Intersect( + new Rectangle(0, 0, sourceRegion.Width, sourceRegion.Height), + sourceRectangle); + + if (clipped.Width <= 0 || clipped.Height <= 0) + { + throw new ArgumentException("The requested readback rectangle does not intersect the target bounds.", nameof(sourceRectangle)); + } + + int copyWidth = Math.Min(clipped.Width, destination.Width); + int copyHeight = Math.Min(clipped.Height, destination.Height); + + for (int y = 0; y < copyHeight; y++) + { + sourceRegion.DangerousGetRowSpan(clipped.Y + y) + .Slice(clipped.X, copyWidth) + .CopyTo(destination.DangerousGetRowSpan(y)); + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackendScene.cs b/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackendScene.cs new file mode 100644 index 0000000..00b895d --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackendScene.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// Retained scene created by the CPU drawing backend. + /// + public sealed class DefaultDrawingBackendScene : DrawingBackendScene + { + /// + /// Initializes a new instance of the class. + /// + /// The retained CPU flush scene. + /// The target bounds used to create the scene. + /// Resources that must stay alive for the retained scene. + internal DefaultDrawingBackendScene( + FlushScene scene, + Rectangle bounds, + IReadOnlyList? ownedResources) + : base(bounds, ownedResources) + => this.Scene = scene; + + /// + /// Gets the retained CPU flush scene when this is a leaf scene. + /// + internal FlushScene? Scene { get; } + + /// + protected override void DisposeCore() + => this.Scene?.Dispose(); + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.Outputs.cs b/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.Outputs.cs new file mode 100644 index 0000000..0387906 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.Outputs.cs @@ -0,0 +1,147 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + internal static partial class DefaultRasterizer + { + /// + /// Contract implemented by retained line-block payloads. + /// + /// The concrete retained line-block type. + internal interface ILineBlock + where TSelf : class, ILineBlock + { + /// + /// Gets the number of lines stored in a full block. + /// + public static abstract int LineCount { get; } + + /// + /// Gets the next block in the retained chain. + /// + public TSelf? Next { get; } + + /// + /// Rasterizes the leading lines from this block. + /// + /// The number of leading lines to rasterize from this block. + /// The mutable scan-conversion context to write into. + public void Rasterize(int count, ref Context context); + } + + /// + /// Retained tile-space bounds for one linearized geometry payload. + /// + internal readonly struct TileBounds + { + /// + /// Initializes a new instance of the struct. + /// + /// The tile-space left coordinate. + /// The tile-space top coordinate. + /// The tile-space column count. + /// The tile-space row count. + public TileBounds(int x, int y, int columnCount, int rowCount) + { + this.X = x; + this.Y = y; + this.ColumnCount = columnCount; + this.RowCount = rowCount; + } + + /// + /// Gets the tile-space left coordinate. + /// + public int X { get; } + + /// + /// Gets the tile-space top coordinate. + /// + public int Y { get; } + + /// + /// Gets the tile-space column count. + /// + public int ColumnCount { get; } + + /// + /// Gets the tile-space row count. + /// + public int RowCount { get; } + } + + /// + /// Holds the finalized retained raster payload for one line-block encoding. + /// + /// The concrete retained line-block type. + internal sealed class LinearizedRasterData + where TLineBlock : class, ILineBlock + { + /// + /// Initializes a new instance of the class. + /// + /// The source linear geometry. + /// The retained tile-space bounds. + /// The retained line-block chain for each row band. + /// The valid line count in each row's front block. + /// The retained start-cover seeds for each row band. + public LinearizedRasterData( + LinearGeometry geometry, + TileBounds bounds, + TLineBlock?[] lines, + int[] firstBlockLineCounts, + IMemoryOwner?[] startCoverTable) + { + this.Geometry = geometry; + this.Bounds = bounds; + this.Lines = lines; + this.FirstBlockLineCounts = firstBlockLineCounts; + this.StartCoverTable = startCoverTable; + } + + /// + /// Gets the source linear geometry. + /// + public LinearGeometry Geometry { get; } + + /// + /// Gets the retained tile-space bounds. + /// + public TileBounds Bounds { get; } + + /// + /// Gets the retained line-block chain for each row band. + /// + public TLineBlock?[] Lines { get; } + + /// + /// Gets the valid front-block line count for each row band. + /// + public int[] FirstBlockLineCounts { get; } + + /// + /// Gets the retained start-cover seeds for each row band. + /// + public IMemoryOwner?[] StartCoverTable { get; } + + /// + /// Iterates the retained line blocks for one row band. + /// + /// The row band index to iterate. + /// The mutable scan-conversion context. + public void Iterate(int rowIndex, ref Context context) + { + int count = this.FirstBlockLineCounts[rowIndex]; + TLineBlock? lineBlock = this.Lines[rowIndex]; + while (lineBlock is not null) + { + lineBlock.Rasterize(count, ref context); + lineBlock = lineBlock.Next; + count = TLineBlock.LineCount; + } + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.cs b/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.cs new file mode 100644 index 0000000..00009dc --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.cs @@ -0,0 +1,920 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + internal static partial class DefaultRasterizer + { + /// + /// Base class that lowers translated geometry into retained per-row line storage. + /// + /// The mutable per-row line collector type. + private abstract class Linearizer + where TL : class + { + private bool hasAnyCoverage; + + protected Linearizer( + LinearGeometry geometry, + Matrix4x4 residual, + int translateX, + int translateY, + int minX, + int minY, + int width, + int height, + int firstBandIndex, + int rowBandCount, + float samplingOffsetX, + float samplingOffsetY, + MemoryAllocator allocator) + { + this.Geometry = geometry; + this.Residual = residual; + this.HasResidual = !residual.IsIdentity; + this.TranslateX = translateX; + this.TranslateY = translateY; + this.MinX = minX; + this.MinY = minY; + this.Width = width; + this.Height = height; + this.FirstBandIndex = firstBandIndex; + this.RowBandCount = rowBandCount; + this.SamplingOffsetX = samplingOffsetX; + this.SamplingOffsetY = samplingOffsetY; + this.Allocator = allocator; + this.BandTopStart = (firstBandIndex * PreferredRowHeight) - minY; + this.FirstBlockLineCounts = new int[rowBandCount]; + this.LineCounts = new int[rowBandCount]; + this.StartCoverTable = new IMemoryOwner?[rowBandCount]; + this.LineArrays = new TL?[rowBandCount]; + } + + /// + /// Gets the source geometry being lowered. + /// + protected LinearGeometry Geometry { get; } + + /// + /// Gets the residual transform applied to each source point during emission. + /// + protected Matrix4x4 Residual { get; } + + /// + /// Gets a value indicating whether is non-identity. + /// + protected bool HasResidual { get; } + + /// + /// Gets the translated X offset applied to the geometry. + /// + protected int TranslateX { get; } + + /// + /// Gets the translated Y offset applied to the geometry. + /// + protected int TranslateY { get; } + + /// + /// Gets the minimum destination X bound after clipping. + /// + protected int MinX { get; } + + /// + /// Gets the minimum destination Y bound after clipping. + /// + protected int MinY { get; } + + /// + /// Gets the visible destination width in pixels. + /// + protected int Width { get; } + + /// + /// Gets the visible destination height in pixels. + /// + protected int Height { get; } + + /// + /// Gets the first retained row-band index touched by the geometry. + /// + protected int FirstBandIndex { get; } + + /// + /// Gets the number of retained row bands owned by the geometry. + /// + protected int RowBandCount { get; } + + /// + /// Gets the horizontal sampling offset applied before fixed-point conversion. + /// + protected float SamplingOffsetX { get; } + + /// + /// Gets the vertical sampling offset applied before fixed-point conversion. + /// + protected float SamplingOffsetY { get; } + + /// + /// Gets the allocator used for retained start-cover storage. + /// + protected MemoryAllocator Allocator { get; } + + /// + /// Gets the top offset, in whole pixels, of the first retained row band. + /// + protected int BandTopStart { get; } + + /// + /// Gets the mutable per-row line collectors used during lowering. + /// + protected TL?[] LineArrays { get; } + + /// + /// Gets the valid front-block line count for each retained row band. + /// + protected int[] FirstBlockLineCounts { get; } + + /// + /// Gets the total retained line count for each row band. + /// + protected int[] LineCounts { get; } + + /// + /// Gets the retained start-cover storage for each row band. + /// + protected IMemoryOwner?[] StartCoverTable { get; } + + /// + /// Gets a value indicating whether any retained payload was produced. + /// + protected ref bool HasAnyCoverage => ref this.hasAnyCoverage; + + /// + /// Executes the linearization pass and finalizes the retained row payloads. + /// + /// when any retained coverage was produced; otherwise . + protected virtual bool ProcessCore() + { + RectangleF translatedBounds = this.HasResidual + ? RectangleF.Transform(this.Geometry.Info.Bounds, this.Residual) + : this.Geometry.Info.Bounds; + translatedBounds.Offset(this.TranslateX + this.SamplingOffsetX - this.MinX, this.TranslateY + this.SamplingOffsetY - this.MinY); + + bool contains = + translatedBounds.Left >= 0F && + translatedBounds.Top >= 0F && + translatedBounds.Right <= this.Width && + translatedBounds.Bottom <= this.Height; + + // Contained geometry can skip clipping and go straight to the fixed-point band splitter. + if (contains) + { + this.ProcessContained(); + } + else + { + // Geometry that touches the interest edges needs clipping so start covers and line + // segments still match the destination bounds seen by the rasterizer. + this.ProcessUncontained(); + } + + if (!this.hasAnyCoverage) + { + return false; + } + + this.FinalizeLines(); + return true; + } + + /// + /// Linearizes geometry that is fully contained inside the destination interest. + /// + protected void ProcessContained() + { + SegmentEnumerator enumerator = this.Geometry.GetSegments(); + Matrix4x4 residual = this.Residual; + bool hasResidual = this.HasResidual; + while (enumerator.MoveNext()) + { + LinearSegment segment = enumerator.Current; + PointF p0 = segment.Start; + PointF p1 = segment.End; + if (hasResidual) + { + p0 = PointF.Transform(p0, residual); + p1 = PointF.Transform(p1, residual); + } + + this.AddContainedLineF24Dot8( + FloatToFixed24Dot8(((p0.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX), + FloatToFixed24Dot8(((p0.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY), + FloatToFixed24Dot8(((p1.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX), + FloatToFixed24Dot8(((p1.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY)); + } + } + + /// + /// Linearizes geometry that intersects the destination interest bounds and requires clipping. + /// + protected void ProcessUncontained() + { + SegmentEnumerator enumerator = this.Geometry.GetSegments(); + Matrix4x4 residual = this.Residual; + bool hasResidual = this.HasResidual; + while (enumerator.MoveNext()) + { + LinearSegment segment = enumerator.Current; + PointF p0 = segment.Start; + PointF p1 = segment.End; + if (hasResidual) + { + p0 = PointF.Transform(p0, residual); + p1 = PointF.Transform(p1, residual); + } + + this.AddUncontainedLine( + ((p0.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX, + ((p0.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY, + ((p1.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX, + ((p1.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY); + } + } + + /// + /// Clips one geometry line against the destination interest and adds the retained result. + /// + /// The starting X coordinate in translated float space. + /// The starting Y coordinate in translated float space. + /// The ending X coordinate in translated float space. + /// The ending Y coordinate in translated float space. + protected void AddUncontainedLine(float x0, float y0, float x1, float y1) + { + if (y0 == y1) + { + return; + } + + if (y0 <= 0F && y1 <= 0F) + { + return; + } + + if (y0 >= this.Height && y1 >= this.Height) + { + return; + } + + if (x0 >= this.Width && x1 >= this.Width) + { + return; + } + + if (x0 == x1) + { + int x0c = Math.Clamp(FloatToFixed24Dot8(x0), 0, this.Width * FixedOne); + int p0y = Math.Clamp(FloatToFixed24Dot8(y0), 0, this.Height * FixedOne); + int p1y = Math.Clamp(FloatToFixed24Dot8(y1), 0, this.Height * FixedOne); + + if (x0c == 0) + { + // Segments clipped fully to the left edge do not produce a visible line, but they + // still change winding for rows they cross. Retain that effect as start covers. + this.UpdateStartCoversClipped(p0y, p1y); + this.hasAnyCoverage = true; + } + else + { + this.AddContainedLineF24Dot8(x0c, p0y, x0c, p1y); + } + + return; + } + + double deltayV = Math.Abs(y1 - y0); + double deltaxV = x1 - x0; + double rx0 = x0; + double ry0 = y0; + double rx1 = x1; + double ry1 = y1; + + if (y1 > y0) + { + if (y0 < 0F) + { + double t = -y0 / deltayV; + rx0 = x0 + (deltaxV * t); + ry0 = 0D; + } + + if (y1 > this.Height) + { + double t = (this.Height - y0) / deltayV; + rx1 = x0 + (deltaxV * t); + ry1 = this.Height; + } + } + else + { + if (y0 > this.Height) + { + double t = (y0 - this.Height) / deltayV; + rx0 = x0 + (deltaxV * t); + ry0 = this.Height; + } + + if (y1 < 0F) + { + double t = y0 / deltayV; + rx1 = x0 + (deltaxV * t); + ry1 = 0D; + } + } + + if (rx0 >= this.Width && rx1 >= this.Width) + { + return; + } + + if (rx0 > 0D && rx1 > 0D && rx0 < this.Width && rx1 < this.Width) + { + this.AddContainedLineF24Dot8( + Math.Clamp(FloatToFixed24Dot8((float)rx0), 0, this.Width * FixedOne), + Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne), + Math.Clamp(FloatToFixed24Dot8((float)rx1), 0, this.Width * FixedOne), + Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne)); + return; + } + + if (rx0 <= 0D && rx1 <= 0D) + { + // A segment that stays left of the visible band contributes winding only. + this.UpdateStartCoversClipped( + Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne), + Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne)); + this.hasAnyCoverage = true; + return; + } + + double deltayH = ry1 - ry0; + double deltaxH = Math.Abs(rx1 - rx0); + + if (rx1 > rx0) + { + double bx1 = rx1; + double by1 = ry1; + + if (rx1 > this.Width) + { + double t = (this.Width - rx0) / deltaxH; + by1 = ry0 + (deltayH * t); + bx1 = this.Width; + } + + if (rx0 < 0D) + { + double t = -rx0 / deltaxH; + int a = Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne); + int by = Math.Clamp(FloatToFixed24Dot8((float)(ry0 + (deltayH * t))), 0, this.Height * FixedOne); + int cx = Math.Clamp(FloatToFixed24Dot8((float)bx1), 0, this.Width * FixedOne); + int cy = Math.Clamp(FloatToFixed24Dot8((float)by1), 0, this.Height * FixedOne); + + this.UpdateStartCoversClipped(a, by); + this.hasAnyCoverage = true; + + // The visible portion begins exactly at x == 0 after the left-edge clip. + this.AddContainedLineF24Dot8(0, by, cx, cy); + } + else + { + this.AddContainedLineF24Dot8( + Math.Clamp(FloatToFixed24Dot8((float)rx0), 0, this.Width * FixedOne), + Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne), + Math.Clamp(FloatToFixed24Dot8((float)bx1), 0, this.Width * FixedOne), + Math.Clamp(FloatToFixed24Dot8((float)by1), 0, this.Height * FixedOne)); + } + } + else + { + double bx0 = rx0; + double by0 = ry0; + + if (rx0 > this.Width) + { + double t = (rx0 - this.Width) / deltaxH; + by0 = ry0 + (deltayH * t); + bx0 = this.Width; + } + + if (rx1 < 0D) + { + double t = rx0 / deltaxH; + int ax = Math.Clamp(FloatToFixed24Dot8((float)bx0), 0, this.Width * FixedOne); + int ay = Math.Clamp(FloatToFixed24Dot8((float)by0), 0, this.Height * FixedOne); + int by = Math.Clamp(FloatToFixed24Dot8((float)(ry0 + (deltayH * t))), 0, this.Height * FixedOne); + int c = Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne); + + // The right-to-left case mirrors the left-edge handling above: emit the + // visible portion first, then retain the winding-only tail as start covers. + this.AddContainedLineF24Dot8(ax, ay, 0, by); + this.UpdateStartCoversClipped(by, c); + this.hasAnyCoverage = true; + } + else + { + this.AddContainedLineF24Dot8( + Math.Clamp(FloatToFixed24Dot8((float)bx0), 0, this.Width * FixedOne), + Math.Clamp(FloatToFixed24Dot8((float)by0), 0, this.Height * FixedOne), + Math.Clamp(FloatToFixed24Dot8((float)rx1), 0, this.Width * FixedOne), + Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne)); + } + } + } + + /// + /// Adds one fully-contained line segment in 24.8 fixed-point coordinates. + /// + /// The starting X coordinate. + /// The starting Y coordinate. + /// The ending X coordinate. + /// The ending Y coordinate. + protected void AddContainedLineF24Dot8(int x0, int y0, int x1, int y1) + { + if (y0 == y1) + { + return; + } + + if (x0 == x1) + { + if (y0 < y1) + { + this.VerticalDown(x0, y0, y1); + } + else + { + this.VerticalUp(x0, y0, y1); + } + + return; + } + + int dx = Math.Abs(x1 - x0); + int dy = Math.Abs(y1 - y0); + if (dx > MaximumDelta || dy > MaximumDelta) + { + int mx = (x0 + x1) >> 1; + int my = (y0 + y1) >> 1; + this.AddContainedLineF24Dot8(x0, y0, mx, my); + this.AddContainedLineF24Dot8(mx, my, x1, y1); + return; + } + + int rowIndex0; + int rowIndex1; + int bandTopStart = this.BandTopStart * FixedOne; + int bandHeight = PreferredRowHeight * FixedOne; + if (y0 < y1) + { + rowIndex0 = (y0 - bandTopStart) / bandHeight; + rowIndex1 = ((y1 - 1) - bandTopStart) / bandHeight; + } + else + { + rowIndex0 = ((y0 - 1) - bandTopStart) / bandHeight; + rowIndex1 = (y1 - bandTopStart) / bandHeight; + } + + if ((uint)rowIndex0 >= (uint)this.RowBandCount || (uint)rowIndex1 >= (uint)this.RowBandCount) + { + return; + } + + if (rowIndex0 == rowIndex1) + { + int rowTop = bandTopStart + (rowIndex0 * bandHeight); + this.AppendLine(rowIndex0, x0, y0 - rowTop, x1, y1 - rowTop); + this.LineCounts[rowIndex0]++; + this.hasAnyCoverage = true; + return; + } + + this.SplitAcrossBands(x0, y0, x1, y1); + } + + /// + /// Creates the mutable line collector used for one row band. + /// + /// The mutable line collector. + protected abstract TL CreateLineArray(); + + /// + /// Appends one line segment into the retained row-band collector. + /// + /// The local row-band index. + /// The starting X coordinate relative to the row band. + /// The starting Y coordinate relative to the row band. + /// The ending X coordinate relative to the row band. + /// The ending Y coordinate relative to the row band. + protected abstract void AppendLine(int rowIndex, int x0, int y0, int x1, int y1); + + /// + /// Finalizes the mutable collectors into the retained line-block representation. + /// + protected abstract void FinalizeLines(); + + /// + /// Gets the mutable line collector for a row band, creating it on first use. + /// + /// The local row-band index. + /// The mutable line collector. + protected TL GetOrCreateLineArray(int rowIndex) + { + TL? lineArray = this.LineArrays[rowIndex]; + if (lineArray is not null) + { + return lineArray; + } + + lineArray = this.CreateLineArray(); + this.LineArrays[rowIndex] = lineArray; + return lineArray; + } + + /// + /// Adds a downward vertical segment by delegating to the shared band-splitting path. + /// + /// The fixed-point X coordinate. + /// The starting fixed-point Y coordinate. + /// The ending fixed-point Y coordinate. + private void VerticalDown(int x, int y0, int y1) => this.SplitAcrossBands(x, y0, x, y1); + + /// + /// Adds an upward vertical segment by delegating to the shared band-splitting path. + /// + /// The fixed-point X coordinate. + /// The starting fixed-point Y coordinate. + /// The ending fixed-point Y coordinate. + private void VerticalUp(int x, int y0, int y1) => this.SplitAcrossBands(x, y0, x, y1); + + /// + /// Splits a contained line segment at row-band boundaries and appends each retained piece. + /// + /// The starting X coordinate. + /// The starting Y coordinate. + /// The ending X coordinate. + /// The ending Y coordinate. + private void SplitAcrossBands(int x0, int y0, int x1, int y1) + { + int dy = y1 - y0; + int dx = x1 - x0; + int bandTopStart = this.BandTopStart * FixedOne; + int bandHeight = PreferredRowHeight * FixedOne; + int startBand = dy > 0 ? (y0 - bandTopStart) / bandHeight : ((y0 - 1) - bandTopStart) / bandHeight; + int endBand = dy > 0 ? ((y1 - 1) - bandTopStart) / bandHeight : (y1 - bandTopStart) / bandHeight; + int step = dy > 0 ? 1 : -1; + int currentBand = startBand; + int currentX = x0; + int currentY = y0; + + while (currentBand != endBand) + { + int bandBoundaryY = dy > 0 ? bandTopStart + ((currentBand + 1) * bandHeight) : bandTopStart + (currentBand * bandHeight); + int deltaY = bandBoundaryY - currentY; + int nextX = currentX + (int)(((long)dx * deltaY) / dy); + int rowTop = bandTopStart + (currentBand * bandHeight); + + // Each retained segment is stored in the local coordinate space of its owning band. + this.AppendLine(currentBand, currentX, currentY - rowTop, nextX, bandBoundaryY - rowTop); + this.LineCounts[currentBand]++; + this.hasAnyCoverage = true; + currentX = nextX; + currentY = bandBoundaryY; + currentBand += step; + + if ((uint)currentBand >= (uint)this.RowBandCount) + { + return; + } + } + + int finalRowTop = bandTopStart + (endBand * bandHeight); + this.AppendLine(endBand, currentX, currentY - finalRowTop, x1, y1 - finalRowTop); + this.LineCounts[endBand]++; + this.hasAnyCoverage = true; + } + + /// + /// Updates retained start-cover rows for a line that has been clipped against the visible band. + /// + /// The clipped starting Y coordinate. + /// The clipped ending Y coordinate. + private void UpdateStartCoversClipped(int y0, int y1) + { + if (y0 == y1) + { + return; + } + + if (y0 < y1) + { + int bandTopStart = this.BandTopStart * FixedOne; + int bandHeight = PreferredRowHeight * FixedOne; + int rowIndex0 = (y0 - bandTopStart) / bandHeight; + int rowIndex1 = ((y1 - 1) - bandTopStart) / bandHeight; + rowIndex0 = Math.Clamp(rowIndex0, 0, this.RowBandCount - 1); + rowIndex1 = Math.Clamp(rowIndex1, 0, this.RowBandCount - 1); + int fy0 = y0 - (bandTopStart + (rowIndex0 * bandHeight)); + int fy1 = y1 - (bandTopStart + (rowIndex1 * bandHeight)); + this.UpdateStartCovers(rowIndex0, fy0, rowIndex0 == rowIndex1 ? fy1 : bandHeight); + for (int i = rowIndex0 + 1; i < rowIndex1; i++) + { + // Full interior bands receive a constant winding contribution. + this.FillStartCovers(i, -FixedOne); + } + + if (rowIndex0 != rowIndex1) + { + this.UpdateStartCovers(rowIndex1, 0, fy1); + } + } + else + { + int bandTopStart = this.BandTopStart * FixedOne; + int bandHeight = PreferredRowHeight * FixedOne; + int rowIndex0 = ((y0 - 1) - bandTopStart) / bandHeight; + int rowIndex1 = (y1 - bandTopStart) / bandHeight; + rowIndex0 = Math.Clamp(rowIndex0, 0, this.RowBandCount - 1); + rowIndex1 = Math.Clamp(rowIndex1, 0, this.RowBandCount - 1); + int fy0 = y0 - (bandTopStart + (rowIndex0 * bandHeight)); + int fy1 = y1 - (bandTopStart + (rowIndex1 * bandHeight)); + this.UpdateStartCovers(rowIndex0, fy0, rowIndex0 == rowIndex1 ? fy1 : 0); + for (int i = rowIndex0 - 1; i > rowIndex1; i--) + { + // Full interior bands receive a constant winding contribution. + this.FillStartCovers(i, FixedOne); + } + + if (rowIndex0 != rowIndex1) + { + this.UpdateStartCovers(rowIndex1, bandHeight, fy1); + } + } + } + + /// + /// Fills an entire retained start-cover row with a constant winding value. + /// + /// The local row-band index. + /// The constant winding value to add. + private void FillStartCovers(int localBandIndex, int value) + { + IMemoryOwner? owner = this.StartCoverTable[localBandIndex]; + if (owner is null) + { + owner = this.Allocator.Allocate(PreferredRowHeight, AllocationOptions.Clean); + this.StartCoverTable[localBandIndex] = owner; + owner.Memory.Span[..PreferredRowHeight].Fill(value); + return; + } + + Span covers = owner.Memory.Span[..PreferredRowHeight]; + for (int i = 0; i < PreferredRowHeight; i++) + { + covers[i] += value; + } + } + + /// + /// Updates a retained start-cover row for one clipped vertical interval. + /// + /// The local row-band index. + /// The starting Y coordinate relative to the row band. + /// The ending Y coordinate relative to the row band. + private void UpdateStartCovers(int localBandIndex, int y0, int y1) + { + IMemoryOwner? owner = this.StartCoverTable[localBandIndex]; + if (owner is null) + { + owner = this.Allocator.Allocate(PreferredRowHeight, AllocationOptions.Clean); + this.StartCoverTable[localBandIndex] = owner; + } + + Span covers = owner.Memory.Span[..PreferredRowHeight]; + if (y0 < y1) + { + UpdateCoverTableDown(covers, y0, y1); + } + else + { + UpdateCoverTableUp(covers, y0, y1); + } + } + + /// + /// Applies a downward winding contribution to one retained start-cover table. + /// + /// The retained start-cover rows. + /// The starting Y coordinate relative to the row band. + /// The ending Y coordinate relative to the row band. + private static void UpdateCoverTableDown(Span covers, int y0, int y1) + { + int rowIndex0 = y0 >> FixedShift; + int rowIndex1 = (y1 - 1) >> FixedShift; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + + if (rowIndex0 == rowIndex1) + { + covers[rowIndex0] -= fy1 - fy0; + return; + } + + covers[rowIndex0] -= FixedOne - fy0; + for (int i = rowIndex0 + 1; i < rowIndex1; i++) + { + covers[i] -= FixedOne; + } + + covers[rowIndex1] -= fy1; + } + + /// + /// Applies an upward winding contribution to one retained start-cover table. + /// + /// The retained start-cover rows. + /// The starting Y coordinate relative to the row band. + /// The ending Y coordinate relative to the row band. + private static void UpdateCoverTableUp(Span covers, int y0, int y1) + { + int rowIndex0 = (y0 - 1) >> FixedShift; + int rowIndex1 = y1 >> FixedShift; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + + if (rowIndex0 == rowIndex1) + { + covers[rowIndex0] += fy0 - fy1; + return; + } + + covers[rowIndex0] += fy0; + for (int i = rowIndex0 - 1; i > rowIndex1; i--) + { + covers[i] += FixedOne; + } + + covers[rowIndex1] += FixedOne - fy1; + } + } + + /// + /// Linearizer that finalizes retained lines into the 32-bit-X encoding. + /// + private sealed class LinearizerX32Y16 : Linearizer + { + /// + /// Initializes a new instance of the class. + /// + public LinearizerX32Y16( + LinearGeometry geometry, + Matrix4x4 residual, + int translateX, + int translateY, + int minX, + int minY, + int width, + int height, + int firstBandIndex, + int rowBandCount, + float samplingOffsetX, + float samplingOffsetY, + MemoryAllocator allocator) + : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator) + => this.FinalLines = new LineArrayX32Y16Block?[rowBandCount]; + + /// + /// Gets the finalized retained line blocks for each row band. + /// + public LineArrayX32Y16Block?[] FinalLines { get; } + + /// + protected override LineArrayX32Y16 CreateLineArray() => new(); + + /// + protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1) + => this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1); + + /// + protected override void FinalizeLines() + { + for (int i = 0; i < this.RowBandCount; i++) + { + LineArrayX32Y16? lineArray = this.LineArrays[i]; + this.FinalLines[i] = lineArray?.GetFrontBlock(); + this.FirstBlockLineCounts[i] = lineArray?.GetFrontBlockLineCount() ?? 0; + } + } + + /// + /// Executes the 32-bit-X linearization pass and returns the retained result. + /// + /// The finalized retained raster data. + /// when retained coverage was produced; otherwise . + internal bool TryProcess(out LinearizedRasterData result) + { + if (!this.ProcessCore()) + { + result = null!; + return false; + } + + result = new LinearizedRasterData( + this.Geometry, + new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount), + this.FinalLines, + this.FirstBlockLineCounts, + this.StartCoverTable); + + return true; + } + } + + /// + /// Linearizer that finalizes retained lines into the packed 16-bit-X encoding. + /// + private sealed class LinearizerX16Y16 : Linearizer + { + /// + /// Initializes a new instance of the class. + /// + public LinearizerX16Y16( + LinearGeometry geometry, + Matrix4x4 residual, + int translateX, + int translateY, + int minX, + int minY, + int width, + int height, + int firstBandIndex, + int rowBandCount, + float samplingOffsetX, + float samplingOffsetY, + MemoryAllocator allocator) + : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator) + => this.FinalLines = new LineArrayX16Y16Block?[rowBandCount]; + + /// + /// Gets the finalized retained line blocks for each row band. + /// + public LineArrayX16Y16Block?[] FinalLines { get; } + + /// + protected override LineArrayX16Y16 CreateLineArray() => new(); + + /// + protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1) + => this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1); + + /// + protected override void FinalizeLines() + { + for (int i = 0; i < this.RowBandCount; i++) + { + LineArrayX16Y16? lineArray = this.LineArrays[i]; + this.FinalLines[i] = lineArray?.GetFrontBlock(); + this.FirstBlockLineCounts[i] = lineArray?.GetFrontBlockLineCount() ?? 0; + } + } + + /// + /// Executes the 16-bit-X linearization pass and returns the retained result. + /// + /// The finalized retained raster data. + /// when retained coverage was produced; otherwise . + internal bool TryProcess(out LinearizedRasterData result) + { + if (!this.ProcessCore()) + { + result = null!; + return false; + } + + result = new LinearizedRasterData( + this.Geometry, + new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount), + this.FinalLines, + this.FirstBlockLineCounts, + this.StartCoverTable); + + return true; + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RasterizableGeometry.cs b/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RasterizableGeometry.cs new file mode 100644 index 0000000..540b387 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RasterizableGeometry.cs @@ -0,0 +1,174 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + internal static partial class DefaultRasterizer + { + /// + /// Flush-scoped retained row-local raster payload for one prepared fill geometry. + /// + internal sealed class RasterizableGeometry : IDisposable + { + private readonly RasterizableBandInfo[] bandInfos; + private readonly LineArrayX16Y16Block?[]? linesX16; + private readonly LineArrayX32Y16Block?[]? linesX32; + private readonly int[] firstBlockLineCounts; + private readonly IMemoryOwner?[] startCoverTable; + + /// + /// Initializes a new instance of the class. + /// + /// The first absolute row-band index touched by the geometry. + /// The number of retained local row bands owned by the geometry. + /// The geometry-local visible band width in pixels. + /// The bit-vector width in machine words required by the geometry. + /// The scanner cover/area stride required by the geometry. + /// The retained row-band height in pixels. + /// Indicates whether the geometry uses the narrow X16Y16 line encoding. + /// The retained metadata for each local row band. + /// The retained narrow line chains for each local row band. + /// The retained wide line chains for each local row band. + /// The valid line count in each front retained block. + /// The retained start-cover table for each local row band. + public RasterizableGeometry( + int firstRowBandIndex, + int rowBandCount, + int width, + int wordsPerRow, + int coverStride, + int bandHeight, + bool isX16, + RasterizableBandInfo[] bandInfos, + LineArrayX16Y16Block?[]? linesX16, + LineArrayX32Y16Block?[]? linesX32, + int[] firstBlockLineCounts, + IMemoryOwner?[] startCoverTable) + { + this.FirstRowBandIndex = firstRowBandIndex; + this.RowBandCount = rowBandCount; + this.Width = width; + this.WordsPerRow = wordsPerRow; + this.CoverStride = coverStride; + this.BandHeight = bandHeight; + this.IsX16 = isX16; + this.bandInfos = bandInfos; + this.linesX16 = linesX16; + this.linesX32 = linesX32; + this.firstBlockLineCounts = firstBlockLineCounts; + this.startCoverTable = startCoverTable; + } + + /// + /// Gets the first absolute row-band index touched by this geometry. + /// + public int FirstRowBandIndex { get; } + + /// + /// Gets the number of retained local row bands owned by this geometry. + /// + public int RowBandCount { get; } + + /// + /// Gets the geometry-local visible band width in pixels. + /// + public int Width { get; } + + /// + /// Gets the bit-vector width in machine words required by this geometry. + /// + public int WordsPerRow { get; } + + /// + /// Gets the scanner cover/area stride required by this geometry. + /// + public int CoverStride { get; } + + /// + /// Gets the retained row-band height in pixels. + /// + public int BandHeight { get; } + + /// + /// Gets a value indicating whether this geometry uses Blaze's narrow X16Y16 line arrays. + /// + public bool IsX16 { get; } + + /// + /// Returns when the given local row band has retained coverage payload. + /// + /// The local row band index. + /// when the row band has retained coverage; otherwise . + public bool HasCoverage(int localRowIndex) => this.bandInfos[localRowIndex].HasCoverage; + + /// + /// Gets the retained narrow line block chain for one local row. + /// + /// The local row band index. + /// The retained narrow line chain for the row. + public LineArrayX16Y16Block? GetLinesX16ForRow(int localRowIndex) => this.linesX16![localRowIndex]; + + /// + /// Gets the retained wide line block chain for one local row. + /// + /// The local row band index. + /// The retained wide line chain for the row. + public LineArrayX32Y16Block? GetLinesX32ForRow(int localRowIndex) => this.linesX32![localRowIndex]; + + /// + /// Gets the number of valid lines in the first retained block for a local row. + /// + /// The local row band index. + /// The valid line count in the front retained block. + public int GetFirstBlockLineCountForRow(int localRowIndex) => this.firstBlockLineCounts[localRowIndex]; + + /// + /// Gets the retained start-cover table entry for a local row, if one exists. + /// + /// The local row band index. + /// The retained start-cover span for the row. + public ReadOnlySpan GetCoversForRow(int localRowIndex) + { + IMemoryOwner? covers = this.startCoverTable[localRowIndex]; + return covers is null ? ReadOnlySpan.Empty : covers.Memory.Span[..this.BandHeight]; + } + + /// + /// Gets the retained start-cover row payload without further interpretation, matching Blaze naming. + /// + /// The local row band index. + /// The retained start-cover span for the row. + public ReadOnlySpan GetActualCoversForRow(int localRowIndex) => this.GetCoversForRow(localRowIndex); + + /// + /// Gets retained metadata for one local row band. + /// + /// The local row band index. + /// The retained band metadata. + public RasterizableBandInfo GetBandInfo(int localRowIndex) => this.bandInfos[localRowIndex]; + + /// + /// Releases the retained line blocks and start-cover storage. + /// + public void Dispose() + { + if (this.linesX16 is not null) + { + Array.Clear(this.linesX16); + } + + if (this.linesX32 is not null) + { + Array.Clear(this.linesX32); + } + + for (int i = 0; i < this.startCoverTable.Length; i++) + { + this.startCoverTable[i]?.Dispose(); + } + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RetainedTypes.cs b/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RetainedTypes.cs new file mode 100644 index 0000000..2e2c572 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RetainedTypes.cs @@ -0,0 +1,536 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + internal static partial class DefaultRasterizer + { + /// + /// References one retained rasterizable geometry row inside a prepared scene item. + /// + internal readonly struct RasterizableItem + { + /// + /// Initializes a new instance of the struct. + /// + /// The retained rasterizable geometry. + /// The local row index within . + public RasterizableItem(RasterizableGeometry rasterizable, int localRowIndex) + { + this.Rasterizable = rasterizable; + this.LocalRowIndex = localRowIndex; + } + + /// + /// Gets the retained rasterizable geometry. + /// + public RasterizableGeometry Rasterizable { get; } + + /// + /// Gets the local row index within . + /// + public int LocalRowIndex { get; } + + /// + /// Gets the number of lines stored in the first retained block for this row. + /// + /// The number of valid lines in the leading block. + public int GetFirstBlockLineCount() => this.Rasterizable.GetFirstBlockLineCountForRow(this.LocalRowIndex); + + /// + /// Gets the 16-bit X retained line block for the row when the geometry uses the compact encoding. + /// + /// The retained block chain, or when the row uses the 32-bit encoding. + public LineArrayX16Y16Block? GetLineArrayX16() => this.Rasterizable.GetLinesX16ForRow(this.LocalRowIndex); + + /// + /// Gets the 32-bit X retained line block for the row when the geometry uses the wide encoding. + /// + /// The retained block chain, or when the row uses the 16-bit encoding. + public LineArrayX32Y16Block? GetLineArrayX32() => this.Rasterizable.GetLinesX32ForRow(this.LocalRowIndex); + + /// + /// Gets the retained start-cover seeds for the row. + /// + /// The retained start-cover span. + public ReadOnlySpan GetActualCovers() => this.Rasterizable.GetActualCoversForRow(this.LocalRowIndex); + } + + /// + /// References one retained stroke row inside a prepared scene item. + /// + internal readonly struct StrokeRasterizableItem + { + /// + /// Initializes a new instance of the struct. + /// + /// The retained stroke rasterizable geometry. + /// The local row index within . + public StrokeRasterizableItem(StrokeRasterizableGeometry rasterizable, int localRowIndex) + { + this.Rasterizable = rasterizable; + this.LocalRowIndex = localRowIndex; + } + + /// + /// Gets the retained stroke rasterizable geometry. + /// + public StrokeRasterizableGeometry Rasterizable { get; } + + /// + /// Gets the local row index within . + /// + public int LocalRowIndex { get; } + } + + /// + /// Metadata that describes one prepared rasterizable band. + /// + internal readonly struct RasterizableBandInfo + { + /// + /// Initializes a new instance of the struct. + /// + /// The number of retained visible lines in the band. + /// The band height in pixels. + /// The visible band width in pixels. + /// The bit-vector width in machine words. + /// The scanner cover/area stride. + /// The absolute destination X coordinate of the band's left column. + /// The absolute destination Y coordinate of the band's top row. + /// The fill rule used when resolving accumulated winding. + /// The rasterization mode used by the band. + /// The aliased threshold used when the band runs in aliased mode. + /// Indicates whether the band has non-zero start-cover seeds. + public RasterizableBandInfo( + int lineCount, + int bandHeight, + int width, + int wordsPerRow, + int coverStride, + int destinationLeft, + int destinationTop, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + float antialiasThreshold, + bool hasStartCovers) + { + this.LineCount = lineCount; + this.BandHeight = bandHeight; + this.Width = width; + this.WordsPerRow = wordsPerRow; + this.CoverStride = coverStride; + this.DestinationLeft = destinationLeft; + this.DestinationTop = destinationTop; + this.IntersectionRule = intersectionRule; + this.RasterizationMode = rasterizationMode; + this.AntialiasThreshold = antialiasThreshold; + this.HasStartCovers = hasStartCovers; + } + + /// + /// Gets the number of visible raster lines stored for the band. + /// + public int LineCount { get; } + + /// + /// Gets the band height in pixels. + /// + public int BandHeight { get; } + + /// + /// Gets the visible band width in pixels. + /// + public int Width { get; } + + /// + /// Gets the bit-vector width in machine words. + /// + public int WordsPerRow { get; } + + /// + /// Gets the scanner cover/area stride. + /// + public int CoverStride { get; } + + /// + /// Gets the absolute destination X coordinate of the band's left column. + /// + public int DestinationLeft { get; } + + /// + /// Gets the absolute destination Y coordinate of the band's top row. + /// + public int DestinationTop { get; } + + /// + /// Gets the fill rule used when resolving accumulated winding. + /// + public IntersectionRule IntersectionRule { get; } + + /// + /// Gets the coverage mode used by the band. + /// + public RasterizationMode RasterizationMode { get; } + + /// + /// Gets the aliased threshold used when the band runs in aliased mode. + /// + public float AntialiasThreshold { get; } + + /// + /// Gets a value indicating whether the band has non-zero start-cover seeds. + /// + public bool HasStartCovers { get; } + + /// + /// Gets a value indicating whether the band would emit any coverage. + /// + public bool HasCoverage => this.LineCount > 0 || this.HasStartCovers; + } + + /// + /// Collects retained line segments whose X coordinates require 32-bit storage. + /// + internal sealed class LineArrayX32Y16 + { + private LineArrayX32Y16Block? current; + private int count = LineArrayX32Y16Block.LineCount; + + /// + /// Gets the front block in the retained line chain. + /// + /// The front retained block, or when no lines were appended. + public LineArrayX32Y16Block? GetFrontBlock() => this.current; + + /// + /// Gets the number of valid lines in the front retained block. + /// + /// The number of valid front-block lines. + public int GetFrontBlockLineCount() => this.current is null ? 0 : this.count; + + /// + /// Appends one retained line to the chain. + /// + /// The starting X coordinate in 24.8 fixed-point. + /// The starting Y coordinate in 24.8 fixed-point. + /// The ending X coordinate in 24.8 fixed-point. + /// The ending Y coordinate in 24.8 fixed-point. + public void AppendLine(int x0, int y0, int x1, int y1) + { + if (y0 == y1) + { + return; + } + + int packedY0Y1 = Pack(y0, y1); + LineArrayX32Y16Block? block = this.current; + int currentCount = this.count; + if (currentCount < LineArrayX32Y16Block.LineCount) + { + block!.Set(currentCount, packedY0Y1, x0, x1); + this.count = currentCount + 1; + } + else + { + LineArrayX32Y16Block next = new(block); + next.Set(0, packedY0Y1, x0, x1); + this.current = next; + this.count = 1; + } + } + + /// + /// Packs two signed 16-bit fixed-point values into one 32-bit integer. + /// + /// The low 16-bit value. + /// The high 16-bit value. + /// The packed value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Pack(int lo, int hi) => (lo & 0xFFFF) | (hi << 16); + } + + /// + /// Represents one retained 32-bit-X line block. + /// + internal sealed class LineArrayX32Y16Block : ILineBlock + { + private const int BlockLineCount = 32; + private PackedLineX32Y16Buffer lines; + + /// + /// Initializes a new instance of the class. + /// + /// The next block in the retained chain. + public LineArrayX32Y16Block(LineArrayX32Y16Block? next) => this.Next = next; + + /// + public static int LineCount => BlockLineCount; + + /// + public LineArrayX32Y16Block? Next { get; } + + /// + /// Stores one retained line into the block. + /// + /// The block-local line index. + /// The packed 16-bit Y endpoints. + /// The starting X coordinate in 24.8 fixed-point. + /// The ending X coordinate in 24.8 fixed-point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(int index, int packedY0Y1, int x0, int x1) + { + ref PackedLineX32Y16 line = ref this.lines[index]; + line.PackedY0Y1 = packedY0Y1; + line.X0 = x0; + line.X1 = x1; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Rasterize(int count, ref Context context) + { + for (int i = 0; i < count; i++) + { + PackedLineX32Y16 line = this.lines[i]; + context.RasterizeLineSegment(line.X0, UnpackLo(line.PackedY0Y1), line.X1, UnpackHi(line.PackedY0Y1)); + } + } + + /// + /// Iterates the retained block chain and rasterizes each block in sequence. + /// + /// The number of valid lines stored in the front block. + /// The mutable scan-conversion context. + public void Iterate(int firstBlockLineCount, ref Context context) + { + int count = firstBlockLineCount; + LineArrayX32Y16Block? lineBlock = this; + while (lineBlock is not null) + { + lineBlock.Rasterize(count, ref context); + lineBlock = lineBlock.Next; + count = LineCount; + } + } + + /// + /// Unpacks the low signed 16-bit value from a packed endpoint pair. + /// + /// The packed endpoint pair. + /// The unpacked low value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int UnpackLo(int packed) => (short)(packed & 0xFFFF); + + /// + /// Unpacks the high signed 16-bit value from a packed endpoint pair. + /// + /// The packed endpoint pair. + /// The unpacked high value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int UnpackHi(int packed) => packed >> 16; + + /// + /// Holds one retained 32-bit-X line record in block-local storage. + /// + private struct PackedLineX32Y16 + { + /// + /// Gets or sets the packed Y endpoints. + /// + public int PackedY0Y1; + + /// + /// Gets or sets the starting X coordinate. + /// + public int X0; + + /// + /// Gets or sets the ending X coordinate. + /// + public int X1; + } + + /// + /// Holds the fixed-capacity retained line payload inline with the block object. + /// + [InlineArray(BlockLineCount)] + private struct PackedLineX32Y16Buffer + { + private PackedLineX32Y16 element0; + } + } + + /// + /// Collects retained line segments whose X coordinates fit in packed 16-bit storage. + /// + internal sealed class LineArrayX16Y16 + { + private LineArrayX16Y16Block? current; + private int count = LineArrayX16Y16Block.LineCount; + + /// + /// Gets the front block in the retained line chain. + /// + /// The front retained block, or when no lines were appended. + public LineArrayX16Y16Block? GetFrontBlock() => this.current; + + /// + /// Gets the number of valid lines in the front retained block. + /// + /// The number of valid front-block lines. + public int GetFrontBlockLineCount() => this.current is null ? 0 : this.count; + + /// + /// Appends one retained line to the chain. + /// + /// The starting X coordinate in 24.8 fixed-point. + /// The starting Y coordinate in 24.8 fixed-point. + /// The ending X coordinate in 24.8 fixed-point. + /// The ending Y coordinate in 24.8 fixed-point. + public void AppendLine(int x0, int y0, int x1, int y1) + { + if (y0 == y1) + { + return; + } + + int packedY0Y1 = Pack(y0, y1); + int packedX0X1 = Pack(x0, x1); + LineArrayX16Y16Block? block = this.current; + int currentCount = this.count; + if (currentCount < LineArrayX16Y16Block.LineCount) + { + block!.Set(currentCount, packedY0Y1, packedX0X1); + this.count = currentCount + 1; + } + else + { + LineArrayX16Y16Block next = new(block); + next.Set(0, packedY0Y1, packedX0X1); + this.current = next; + this.count = 1; + } + } + + /// + /// Packs two signed 16-bit fixed-point values into one 32-bit integer. + /// + /// The low 16-bit value. + /// The high 16-bit value. + /// The packed value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Pack(int lo, int hi) => (lo & 0xFFFF) | (hi << 16); + } + + /// + /// Represents one retained 16-bit-X line block. + /// + internal sealed class LineArrayX16Y16Block : ILineBlock + { + private const int BlockLineCount = 32; + private PackedLineX16Y16Buffer lines; + + /// + /// Initializes a new instance of the class. + /// + /// The next block in the retained chain. + public LineArrayX16Y16Block(LineArrayX16Y16Block? next) => this.Next = next; + + /// + public static int LineCount => BlockLineCount; + + /// + public LineArrayX16Y16Block? Next { get; } + + /// + /// Stores one retained line into the block. + /// + /// The block-local line index. + /// The packed 16-bit Y endpoints. + /// The packed 16-bit X endpoints. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(int index, int packedY0Y1, int packedX0X1) + { + ref PackedLineX16Y16 line = ref this.lines[index]; + line.PackedY0Y1 = packedY0Y1; + line.PackedX0X1 = packedX0X1; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Rasterize(int count, ref Context context) + { + for (int i = 0; i < count; i++) + { + PackedLineX16Y16 line = this.lines[i]; + context.RasterizeLineSegment( + UnpackLo(line.PackedX0X1), + UnpackLo(line.PackedY0Y1), + UnpackHi(line.PackedX0X1), + UnpackHi(line.PackedY0Y1)); + } + } + + /// + /// Iterates the retained block chain and rasterizes each block in sequence. + /// + /// The number of valid lines stored in the front block. + /// The mutable scan-conversion context. + public void Iterate(int firstBlockLineCount, ref Context context) + { + int count = firstBlockLineCount; + LineArrayX16Y16Block? lineBlock = this; + while (lineBlock is not null) + { + lineBlock.Rasterize(count, ref context); + lineBlock = lineBlock.Next; + count = LineCount; + } + } + + /// + /// Unpacks the low signed 16-bit value from a packed endpoint pair. + /// + /// The packed endpoint pair. + /// The unpacked low value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int UnpackLo(int packed) => (short)(packed & 0xFFFF); + + /// + /// Unpacks the high signed 16-bit value from a packed endpoint pair. + /// + /// The packed endpoint pair. + /// The unpacked high value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int UnpackHi(int packed) => packed >> 16; + + /// + /// Holds one retained 16-bit-X line record in block-local storage. + /// + private struct PackedLineX16Y16 + { + /// + /// Gets or sets the packed Y endpoints. + /// + public int PackedY0Y1; + + /// + /// Gets or sets the packed X endpoints. + /// + public int PackedX0X1; + } + + /// + /// Holds the fixed-capacity retained line payload inline with the block object. + /// + [InlineArray(BlockLineCount)] + private struct PackedLineX16Y16Buffer + { + private PackedLineX16Y16 element0; + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Stroke.cs b/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Stroke.cs new file mode 100644 index 0000000..6bd682d --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Stroke.cs @@ -0,0 +1,1579 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { +#pragma warning disable SA1201 // Elements should appear in the correct order + + internal static partial class DefaultRasterizer + { + private const float StrokeDirectionEpsilon = 1e-6F; + private const float StrokeParallelEpsilon = 1e-5F; + private const int DirectStrokeVerticalSampleCount = 4; + + /// + /// Creates retained row-local raster payload for one stroked centerline geometry. + /// + /// The source stroke centerline geometry. + /// The residual transform applied to each source point during emission. + /// The stroke metadata. + /// The destination-space X translation applied at composition time. + /// The destination-space Y translation applied at composition time. + /// The rasterizer options used to generate coverage. + /// The isotropic scale factor applied to the stroke width so expansion runs in device-space pixels. + /// The allocator used for retained raster storage. + /// The retained rasterizable geometry for the stroke, or when the stroke produces no coverage. + internal static StrokeRasterizableGeometry? CreatePathStrokeRasterizableGeometry( + LinearGeometry geometry, + Matrix4x4 residual, + Pen pen, + int translateX, + int translateY, + in RasterizerOptions options, + float widthScale, + MemoryAllocator allocator) + { + if (pen.StrokeWidth <= 0F) + { + return null; + } + + return CreateRetainedStrokeRasterizableGeometry( + geometry, + residual, + new StrokeStyle(pen, widthScale), + translateX, + translateY, + in options, + allocator); + } + + /// + /// Creates retained row-local raster payload for one stroked two-point line segment. + /// + /// The retained stroke start point. + /// The retained stroke end point. + /// The stroke metadata. + /// The destination-space X translation applied at composition time. + /// The destination-space Y translation applied at composition time. + /// The rasterizer options used to generate coverage. + /// The isotropic scale factor applied to the stroke width so expansion runs in device-space pixels. + /// The allocator used for retained raster storage. + /// The retained rasterizable geometry for the stroke, or when the stroke produces no coverage. + internal static StrokeRasterizableGeometry? CreateLineSegmentStrokeRasterizableGeometry( + PointF start, + PointF end, + Pen pen, + int translateX, + int translateY, + in RasterizerOptions options, + float widthScale, + MemoryAllocator allocator) + { + if (pen.StrokeWidth <= 0F) + { + return null; + } + + float samplingOffsetX = 0.5F; + float samplingOffsetY = 0.5F; + + StrokeStyle strokeStyle = new(pen, widthScale); + + RectangleF bounds = RectangleF.FromLTRB( + MathF.Min(start.X, end.X), + MathF.Min(start.Y, end.Y), + MathF.Max(start.X, end.X), + MathF.Max(start.Y, end.Y)); + + RectangleF translatedBounds = InflateStrokeBounds(bounds, strokeStyle); + + translatedBounds.Offset(translateX + samplingOffsetX, translateY + samplingOffsetY); + + Rectangle geometryBounds = Rectangle.FromLTRB( + (int)MathF.Floor(translatedBounds.Left), + (int)MathF.Floor(translatedBounds.Top), + (int)MathF.Ceiling(translatedBounds.Right) + 1, + (int)MathF.Ceiling(translatedBounds.Bottom)); + + Rectangle clippedBounds = Rectangle.Intersect(geometryBounds, options.Interest); + if (clippedBounds.Width <= 0 || clippedBounds.Height <= 0) + { + return null; + } + + int width = clippedBounds.Width; + int firstRowBandIndex = clippedBounds.Top / PreferredRowHeight; + int lastRowBandIndex = (clippedBounds.Bottom - 1) / PreferredRowHeight; + int rowBandCount = lastRowBandIndex - firstRowBandIndex + 1; + int wordsPerRow = BitVectorsForMaxBitCount(width); + int coverStride = checked(width << 1); + + if (wordsPerRow <= 0 || coverStride <= 0) + { + ThrowInterestBoundsTooLarge(); + } + + RasterizableBandInfo[] bandInfos = new RasterizableBandInfo[rowBandCount]; + int estimatedLineCount = EstimateStrokeBandLineCount(start, end); + for (int i = 0; i < rowBandCount; i++) + { + int bandTop = (firstRowBandIndex + i) * PreferredRowHeight; + bandInfos[i] = new RasterizableBandInfo( + estimatedLineCount, + PreferredRowHeight, + width, + wordsPerRow, + coverStride, + clippedBounds.Left, + bandTop, + options.IntersectionRule, + options.RasterizationMode, + options.AntialiasThreshold, + hasStartCovers: false); + } + + return new StrokeRasterizableGeometry( + firstRowBandIndex, + rowBandCount, + width, + wordsPerRow, + coverStride, + PreferredRowHeight, + bandInfos, + new LineSegmentStrokeRasterData( + start, + end, + strokeStyle, + translateX, + translateY, + firstRowBandIndex, + rowBandCount, + samplingOffsetX, + samplingOffsetY)); + } + + /// + /// Expands one stroked centerline geometry once into retained per-band line storage. + /// + /// The retained stroke centerline geometry. + /// The residual transform applied to each source point during emission. + /// The stroke style. + /// The destination-space X translation applied at composition time. + /// The destination-space Y translation applied at composition time. + /// The rasterizer options used for the retained bands. + /// The allocator used for retained raster storage. + /// The retained stroke rasterizable geometry, or when the stroke produces no coverage. + private static StrokeRasterizableGeometry? CreateRetainedStrokeRasterizableGeometry( + LinearGeometry geometry, + Matrix4x4 residual, + in StrokeStyle stroke, + int translateX, + int translateY, + in RasterizerOptions options, + MemoryAllocator allocator) + { + if (geometry.Info.PointCount == 0) + { + return null; + } + + float samplingOffsetX = 0.5F; + float samplingOffsetY = 0.5F; + + RectangleF sourceBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual); + RectangleF translatedBounds = InflateStrokeBounds(sourceBounds, stroke); + translatedBounds.Offset(translateX + samplingOffsetX, translateY + samplingOffsetY); + + Rectangle geometryBounds = Rectangle.FromLTRB( + (int)MathF.Floor(translatedBounds.Left), + (int)MathF.Floor(translatedBounds.Top), + (int)MathF.Ceiling(translatedBounds.Right) + 1, + (int)MathF.Ceiling(translatedBounds.Bottom) + 1); + + Rectangle clippedBounds = Rectangle.Intersect(geometryBounds, options.Interest); + if (clippedBounds.Width <= 0 || clippedBounds.Height <= 0) + { + return null; + } + + int width = clippedBounds.Width; + int height = clippedBounds.Height; + int firstRowBandIndex = clippedBounds.Top / PreferredRowHeight; + int lastRowBandIndex = (clippedBounds.Bottom - 1) / PreferredRowHeight; + int rowBandCount = lastRowBandIndex - firstRowBandIndex + 1; + int wordsPerRow = BitVectorsForMaxBitCount(width); + int coverStride = checked(width << 1); + + if (wordsPerRow <= 0 || coverStride <= 0) + { + ThrowInterestBoundsTooLarge(); + } + + if (width < 128) + { + StrokeLinearizerX16Y16 linearizer = new( + geometry, + residual, + stroke, + translateX, + translateY, + clippedBounds.Left, + clippedBounds.Top, + width, + height, + firstRowBandIndex, + rowBandCount, + samplingOffsetX, + samplingOffsetY, + allocator); + + if (!linearizer.TryProcess(out LinearizedRasterData result)) + { + return null; + } + + return CreateRetainedStrokeRasterizableGeometry( + firstRowBandIndex, + rowBandCount, + width, + wordsPerRow, + coverStride, + clippedBounds.Left, + options, + result); + } + + StrokeLinearizerX32Y16 wideLinearizer = new( + geometry, + residual, + stroke, + translateX, + translateY, + clippedBounds.Left, + clippedBounds.Top, + width, + height, + firstRowBandIndex, + rowBandCount, + samplingOffsetX, + samplingOffsetY, + allocator); + + if (!wideLinearizer.TryProcess(out LinearizedRasterData wideResult)) + { + return null; + } + + return CreateRetainedStrokeRasterizableGeometry( + firstRowBandIndex, + rowBandCount, + width, + wordsPerRow, + coverStride, + clippedBounds.Left, + options, + wideResult); + } + + /// + /// Wraps finalized retained stroke line storage in the normal stroke rasterizable payload. + /// + private static StrokeRasterizableGeometry CreateRetainedStrokeRasterizableGeometry( + int firstRowBandIndex, + int rowBandCount, + int width, + int wordsPerRow, + int coverStride, + int destinationLeft, + in RasterizerOptions options, + LinearizedRasterData result) + { + RasterizableBandInfo[] bandInfos = new RasterizableBandInfo[rowBandCount]; + for (int i = 0; i < rowBandCount; i++) + { + int bandTop = (firstRowBandIndex + i) * PreferredRowHeight; + bool hasStartCovers = result.StartCoverTable[i] is not null; + bandInfos[i] = new RasterizableBandInfo( + CountLines(result.Lines[i], result.FirstBlockLineCounts[i]), + PreferredRowHeight, + width, + wordsPerRow, + coverStride, + destinationLeft, + bandTop, + options.IntersectionRule, + options.RasterizationMode, + options.AntialiasThreshold, + hasStartCovers); + } + + RasterizableGeometry retained = new( + firstRowBandIndex, + rowBandCount, + width, + wordsPerRow, + coverStride, + PreferredRowHeight, + isX16: true, + bandInfos, + result.Lines, + null, + result.FirstBlockLineCounts, + result.StartCoverTable); + + return new StrokeRasterizableGeometry( + retained.FirstRowBandIndex, + retained.RowBandCount, + retained.Width, + retained.WordsPerRow, + retained.CoverStride, + retained.BandHeight, + bandInfos, + new RetainedStrokeRasterData(retained), + retained); + } + + /// + /// Wraps finalized retained wide stroke line storage in the normal stroke rasterizable payload. + /// + private static StrokeRasterizableGeometry CreateRetainedStrokeRasterizableGeometry( + int firstRowBandIndex, + int rowBandCount, + int width, + int wordsPerRow, + int coverStride, + int destinationLeft, + in RasterizerOptions options, + LinearizedRasterData result) + { + RasterizableBandInfo[] bandInfos = new RasterizableBandInfo[rowBandCount]; + for (int i = 0; i < rowBandCount; i++) + { + int bandTop = (firstRowBandIndex + i) * PreferredRowHeight; + bool hasStartCovers = result.StartCoverTable[i] is not null; + bandInfos[i] = new RasterizableBandInfo( + CountLines(result.Lines[i], result.FirstBlockLineCounts[i]), + PreferredRowHeight, + width, + wordsPerRow, + coverStride, + destinationLeft, + bandTop, + options.IntersectionRule, + options.RasterizationMode, + options.AntialiasThreshold, + hasStartCovers); + } + + RasterizableGeometry retained = new( + firstRowBandIndex, + rowBandCount, + width, + wordsPerRow, + coverStride, + PreferredRowHeight, + isX16: false, + bandInfos, + null, + result.Lines, + result.FirstBlockLineCounts, + result.StartCoverTable); + + return new StrokeRasterizableGeometry( + retained.FirstRowBandIndex, + retained.RowBandCount, + retained.Width, + retained.WordsPerRow, + retained.CoverStride, + retained.BandHeight, + bandInfos, + new RetainedStrokeRasterData(retained), + retained); + } + + /// + /// Returns the conservative retained line count used for one two-point stroke segment. + /// + /// The stroke start point. + /// The stroke end point. + /// The estimated retained line count for the stroke. + private static int EstimateStrokeBandLineCount(PointF start, PointF end) + { + float samplingOffset = 0.5F; + int segmentCount = (int)MathF.Floor(start.Y + samplingOffset) != (int)MathF.Floor(end.Y + samplingOffset) ? 1 : 0; + return Math.Max(segmentCount * 4, 1); + } + + /// + /// Inflates centerline bounds conservatively for the current stroke style. + /// + /// The centerline bounds. + /// The stroke style used for inflation. + /// The inflated stroke bounds. + private static RectangleF InflateStrokeBounds(RectangleF bounds, in StrokeStyle stroke) + { + float joinInflate = stroke.LineJoin switch + { + LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound + => stroke.HalfWidth * (float)Math.Max(stroke.MiterLimit, 1D), + _ => stroke.HalfWidth + }; + + float capInflate = stroke.LineCap == LineCap.Square + ? stroke.HalfWidth * MathF.Sqrt(2F) + : stroke.HalfWidth; + + float inflate = MathF.Max(joinInflate, capInflate); + + bounds.Inflate(new SizeF(inflate, inflate)); + return bounds; + } + + /// + /// Initializes a new instance of the class. + /// + internal abstract class StrokeRasterData + { + /// + /// Initializes a new instance of the class. + /// + /// The stroke style. + /// The destination-space X translation applied at composition time. + /// The destination-space Y translation applied at composition time. + /// The first retained row-band index touched by the stroke. + /// The number of retained row bands touched by the stroke. + /// The horizontal sampling offset. + /// The vertical sampling offset. + protected StrokeRasterData( + StrokeStyle stroke, + int translateX, + int translateY, + int firstBandIndex, + int rowBandCount, + float samplingOffsetX, + float samplingOffsetY) + { + this.Stroke = stroke; + this.TranslateX = translateX; + this.TranslateY = translateY; + this.FirstBandIndex = firstBandIndex; + this.RowBandCount = rowBandCount; + this.SamplingOffsetX = samplingOffsetX; + this.SamplingOffsetY = samplingOffsetY; + } + + public StrokeStyle Stroke { get; } + + /// + /// Gets the destination-space X translation applied at composition time. + /// + public int TranslateX { get; } + + /// + /// Gets the destination-space Y translation applied at composition time. + /// + public int TranslateY { get; } + + /// + /// Gets the first retained row-band index touched by this stroke. + /// + public int FirstBandIndex { get; } + + /// + /// Gets the number of retained row bands touched by this stroke. + /// + public int RowBandCount { get; } + + /// + /// Gets the horizontal sampling offset applied during rasterization. + /// + public float SamplingOffsetX { get; } + + /// + /// Gets the vertical sampling offset applied during rasterization. + /// + public float SamplingOffsetY { get; } + + public virtual bool RequiresBandCoverage => false; + + /// + /// Rasterizes one retained row band using the derived stroke payload. + /// + /// The coverage row handler type. + /// The mutable scan-conversion context. + /// The retained band metadata. + /// The reusable scanline scratch buffer. + /// The reusable per-band stroke coverage scratch buffer. + /// The coverage row handler that receives emitted runs. + public abstract void ExecuteBand( + ref Context context, + in RasterizableBandInfo bandInfo, + Span scanline, + Span strokeBandCoverage, + ref TRowHandler rowHandler) + where TRowHandler : struct, IRasterizerCoverageRowHandler; + } + + /// + /// Retained stroke source data for one explicit two-point line segment. + /// + internal sealed class LineSegmentStrokeRasterData : StrokeRasterData + { + /// + /// Initializes a new instance of the class. + /// + /// The retained line start point. + /// The retained line end point. + /// The stroke style. + /// The destination-space X translation applied at composition time. + /// The destination-space Y translation applied at composition time. + /// The first retained row-band index touched by the stroke. + /// The number of retained row bands touched by the stroke. + /// The horizontal sampling offset. + /// The vertical sampling offset. + public LineSegmentStrokeRasterData( + PointF start, + PointF end, + StrokeStyle stroke, + int translateX, + int translateY, + int firstBandIndex, + int rowBandCount, + float samplingOffsetX, + float samplingOffsetY) + : base(stroke, translateX, translateY, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY) + { + this.Start = start; + this.End = end; + } + + /// + /// Gets the retained line start point. + /// + public PointF Start { get; } + + /// + /// Gets the retained line end point. + /// + public PointF End { get; } + + /// + /// The coverage row handler type. + /// The mutable scan-conversion context. + /// The retained band metadata. + /// The reusable scanline scratch buffer. + /// The reusable per-band stroke coverage scratch buffer. + /// The coverage row handler that receives emitted runs. + public override void ExecuteBand( + ref Context context, + in RasterizableBandInfo bandInfo, + Span scanline, + Span strokeBandCoverage, + ref TRowHandler rowHandler) + => DirectLineSegmentBandRasterizer.Rasterize( + this.Start, + this.End, + this.Stroke, + this.TranslateX, + this.TranslateY, + this.SamplingOffsetX, + this.SamplingOffsetY, + in bandInfo, + scanline, + ref rowHandler); + } + + /// + /// Retained stroke source data backed by one-time outline linearization. + /// + internal sealed class RetainedStrokeRasterData : StrokeRasterData + { + /// + /// Initializes a new instance of the class. + /// + /// The retained fill-style raster payload replayed for the stroke. + public RetainedStrokeRasterData(RasterizableGeometry outline) + : base(default, 0, 0, outline.FirstRowBandIndex, outline.RowBandCount, 0F, 0F) + => this.Outline = outline; + + /// + /// Gets the retained fill-style raster payload for the stroked outline. + /// + public RasterizableGeometry Outline { get; } + + /// + /// The coverage row handler type. + /// The mutable scan-conversion context. + /// The retained band metadata. + /// The reusable scanline scratch buffer. + /// The reusable per-band stroke coverage scratch buffer. + /// The coverage row handler that receives emitted runs. + public override void ExecuteBand( + ref Context context, + in RasterizableBandInfo bandInfo, + Span scanline, + Span strokeBandCoverage, + ref TRowHandler rowHandler) + { + int localRowIndex = (bandInfo.DestinationTop / PreferredRowHeight) - this.FirstBandIndex; + context.SeedStartCovers(this.Outline.GetActualCoversForRow(localRowIndex)); + + if (this.Outline.IsX16) + { + LineArrayX16Y16Block? lines = this.Outline.GetLinesX16ForRow(localRowIndex); + lines?.Iterate(this.Outline.GetFirstBlockLineCountForRow(localRowIndex), ref context); + } + else + { + LineArrayX32Y16Block? lines = this.Outline.GetLinesX32ForRow(localRowIndex); + lines?.Iterate(this.Outline.GetFirstBlockLineCountForRow(localRowIndex), ref context); + } + + context.EmitCoverageRows(bandInfo.DestinationTop, bandInfo.DestinationLeft, scanline, ref rowHandler); + context.ResetTouchedRows(); + } + } + + /// + /// Flush-scoped retained row-local raster payload for one stroked centerline geometry. + /// + internal sealed class StrokeRasterizableGeometry : IDisposable + { + private readonly RasterizableBandInfo[] bandInfos; + private readonly StrokeRasterData strokeData; + private readonly IDisposable? ownedDisposable; + + /// + /// Initializes a new instance of the class. + /// + /// The first absolute row-band index touched by the stroke. + /// The number of retained local row bands owned by the stroke. + /// The stroke-local visible band width in pixels. + /// The bit-vector width in machine words required by the stroke. + /// The scanner cover/area stride required by the stroke. + /// The retained row-band height in pixels. + /// The retained metadata for each local row band. + /// The retained stroke source data consumed during execution. + /// Optional retained storage owned by this stroke rasterizable. + public StrokeRasterizableGeometry( + int firstRowBandIndex, + int rowBandCount, + int width, + int wordsPerRow, + int coverStride, + int bandHeight, + RasterizableBandInfo[] bandInfos, + StrokeRasterData strokeData, + IDisposable? ownedDisposable = null) + { + this.FirstRowBandIndex = firstRowBandIndex; + this.RowBandCount = rowBandCount; + this.Width = width; + this.WordsPerRow = wordsPerRow; + this.CoverStride = coverStride; + this.BandHeight = bandHeight; + this.bandInfos = bandInfos; + this.strokeData = strokeData; + this.ownedDisposable = ownedDisposable; + } + + /// + /// Gets the first absolute row-band index touched by this stroke. + /// + public int FirstRowBandIndex { get; } + + /// + /// Gets the number of retained local row bands owned by this stroke. + /// + public int RowBandCount { get; } + + /// + /// Gets the stroke-local visible band width in pixels. + /// + public int Width { get; } + + /// + /// Gets the bit-vector width in machine words required by this stroke. + /// + public int WordsPerRow { get; } + + /// + /// Gets the scanner cover/area stride required by this stroke. + /// + public int CoverStride { get; } + + /// + /// Gets the retained row-band height in pixels. + /// + public int BandHeight { get; } + + public bool RequiresBandCoverage => this.strokeData.RequiresBandCoverage; + + /// + /// Returns when the given local row band has retained coverage payload. + /// + /// The local row band index. + /// when the row band has retained coverage; otherwise . + public bool HasCoverage(int localRowIndex) => this.bandInfos[localRowIndex].HasCoverage; + + /// + /// Gets retained metadata for one local row band. + /// + /// The local row band index. + /// The retained band metadata. + public RasterizableBandInfo GetBandInfo(int localRowIndex) => this.bandInfos[localRowIndex]; + + /// + /// Rasterizes one retained row band directly from the stroke centerline data. + /// + /// The mutable scan-conversion context. + /// The retained band metadata. + /// The reusable scanline scratch buffer. + /// The reusable per-band stroke coverage scratch buffer. + /// The coverage handler that consumes emitted spans. + /// The row handler type. + public void ExecuteBand( + ref Context context, + in RasterizableBandInfo bandInfo, + Span scanline, + Span strokeBandCoverage, + ref TRowHandler rowHandler) + where TRowHandler : struct, IRasterizerCoverageRowHandler + => this.strokeData.ExecuteBand(ref context, in bandInfo, scanline, strokeBandCoverage, ref rowHandler); + + /// + /// Releases any retained disposable storage owned by this stroke rasterizable. + /// + public void Dispose() => this.ownedDisposable?.Dispose(); + } + + /// + /// Direct execution-time rasterizer for one stroked explicit line segment. + /// + private readonly struct DirectLineSegmentBandRasterizer + { + private readonly Vector2 start; + private readonly Vector2 end; + private readonly Vector2 translation; + private readonly StrokeStyle stroke; + private readonly int width; + private readonly int height; + private readonly int destinationLeft; + private readonly int destinationTop; + private readonly RasterizationMode rasterizationMode; + private readonly float antialiasThreshold; + + /// + /// Initializes a new instance of the struct. + /// + /// The retained stroke start point. + /// The retained stroke end point. + /// The stroke style. + /// The destination-space X translation applied at composition time. + /// The destination-space Y translation applied at composition time. + /// The horizontal sampling offset. + /// The vertical sampling offset. + /// The retained band metadata. + private DirectLineSegmentBandRasterizer( + PointF start, + PointF end, + StrokeStyle stroke, + int translateX, + int translateY, + float samplingOffsetX, + float samplingOffsetY, + in RasterizableBandInfo bandInfo) + { + this.translation = new( + (translateX - bandInfo.DestinationLeft) + samplingOffsetX, + (translateY - bandInfo.DestinationTop) + samplingOffsetY); + + this.start = start; + this.end = end; + this.stroke = stroke; + this.width = bandInfo.Width; + this.height = bandInfo.BandHeight; + this.destinationLeft = bandInfo.DestinationLeft; + this.destinationTop = bandInfo.DestinationTop; + this.rasterizationMode = bandInfo.RasterizationMode; + this.antialiasThreshold = bandInfo.AntialiasThreshold; + } + + /// + /// Rasterizes one explicit line segment directly into the supplied row handler. + /// + /// The coverage row handler type. + /// The retained stroke start point. + /// The retained stroke end point. + /// The stroke style. + /// The destination-space X translation applied at composition time. + /// The destination-space Y translation applied at composition time. + /// The horizontal sampling offset. + /// The vertical sampling offset. + /// The retained band metadata. + /// The reusable scanline scratch buffer. + /// The coverage row handler that receives emitted runs. + public static void Rasterize( + PointF start, + PointF end, + StrokeStyle stroke, + int translateX, + int translateY, + float samplingOffsetX, + float samplingOffsetY, + in RasterizableBandInfo bandInfo, + Span scanline, + ref TRowHandler rowHandler) + where TRowHandler : struct, IRasterizerCoverageRowHandler + => new DirectLineSegmentBandRasterizer( + start, + end, + stroke, + translateX, + translateY, + samplingOffsetX, + samplingOffsetY, + in bandInfo).Rasterize(scanline, ref rowHandler); + + /// + /// Rasterizes the stored segment across the active band, falling back to a point footprint for degenerate input. + /// + /// The coverage row handler type. + /// The reusable scanline scratch buffer. + /// The coverage row handler that receives emitted runs. + private void Rasterize(Span scanline, ref TRowHandler rowHandler) + where TRowHandler : struct, IRasterizerCoverageRowHandler + { + if (this.stroke.Width <= 0F || this.width <= 0 || this.height <= 0) + { + return; + } + + Vector2 translatedStart = this.start + this.translation; + Vector2 translatedEnd = this.end + this.translation; + if (!TryGetDirection(translatedStart, translatedEnd, out Vector2 tangent, out _)) + { + this.RasterizePointLike(translatedStart, scanline, ref rowHandler); + return; + } + + float halfWidth = this.stroke.HalfWidth; + Vector2 normal = GetStrokeOffsetNormal(tangent) * halfWidth; + Vector2 extension = this.stroke.LineCap == LineCap.Square ? tangent * halfWidth : Vector2.Zero; + Vector2 p0 = translatedStart + normal - extension; + Vector2 p1 = translatedEnd + normal + extension; + Vector2 p2 = translatedEnd - normal + extension; + Vector2 p3 = translatedStart - normal - extension; + + for (int row = 0; row < this.height; row++) + { + this.EmitLineCoverageRow(row, p0, p1, p2, p3, scanline, ref rowHandler); + } + } + + /// + /// Rasterizes a degenerate segment as a point-like cap footprint. + /// + /// The coverage row handler type. + /// The band-local center point. + /// The reusable scanline scratch buffer. + /// The coverage row handler that receives emitted runs. + private void RasterizePointLike(Vector2 center, Span scanline, ref TRowHandler rowHandler) + where TRowHandler : struct, IRasterizerCoverageRowHandler + { + for (int row = 0; row < this.height; row++) + { + this.EmitPointCoverageRow(row, center, scanline, ref rowHandler); + } + } + + /// + /// Computes and emits one raster row for the stroked line body and any cap overlap. + /// + /// The coverage row handler type. + /// The band-local row index. + /// The first quad corner. + /// The second quad corner. + /// The third quad corner. + /// The fourth quad corner. + /// The reusable scanline scratch buffer. + /// The coverage row handler that receives emitted runs. + private void EmitLineCoverageRow( + int row, + Vector2 p0, + Vector2 p1, + Vector2 p2, + Vector2 p3, + Span scanline, + ref TRowHandler rowHandler) + where TRowHandler : struct, IRasterizerCoverageRowHandler + { + float globalLeft = float.PositiveInfinity; + float globalRight = float.NegativeInfinity; + int sampleCount = 0; + + for (int sampleIndex = 0; sampleIndex < DirectStrokeVerticalSampleCount; sampleIndex++) + { + // First pass finds the tight horizontal span touched by any vertical sample so the + // accumulation pass only clears and updates the columns that can actually contribute. + float sampleY = row + ((sampleIndex + 0.5F) / DirectStrokeVerticalSampleCount); + bool hasInterval = false; + + if (!TryGetQuadrilateralIntervalAtY(p0, p1, p2, p3, sampleY, out float left, out float right)) + { + if (this.stroke.LineCap == LineCap.Round) + { + bool hasRoundInterval = false; + if (TryGetCircleIntervalAtY(this.start, this.stroke.HalfWidth, sampleY, out float startLeft, out float startRight)) + { + hasRoundInterval = true; + left = startLeft; + right = startRight; + } + + if (TryGetCircleIntervalAtY(this.end, this.stroke.HalfWidth, sampleY, out float endLeft, out float endRight)) + { + if (!hasRoundInterval) + { + hasRoundInterval = true; + left = endLeft; + right = endRight; + } + else + { + left = MathF.Min(left, endLeft); + right = MathF.Max(right, endRight); + } + } + + if (!hasRoundInterval) + { + continue; + } + + hasInterval = true; + } + else + { + continue; + } + } + else if (this.stroke.LineCap == LineCap.Round) + { + if (TryGetCircleIntervalAtY(this.start, this.stroke.HalfWidth, sampleY, out float startLeft, out float startRight)) + { + left = MathF.Min(left, startLeft); + right = MathF.Max(right, startRight); + } + + if (TryGetCircleIntervalAtY(this.end, this.stroke.HalfWidth, sampleY, out float endLeft, out float endRight)) + { + left = MathF.Min(left, endLeft); + right = MathF.Max(right, endRight); + } + + hasInterval = true; + } + else + { + hasInterval = true; + } + + if (!hasInterval) + { + continue; + } + + globalLeft = MathF.Min(globalLeft, left); + globalRight = MathF.Max(globalRight, right); + sampleCount++; + } + + if (sampleCount == 0) + { + return; + } + + int startColumn = Math.Max(0, (int)MathF.Floor(globalLeft)); + int endColumn = Math.Min(this.width, (int)MathF.Ceiling(globalRight)); + if (endColumn <= startColumn) + { + return; + } + + Span rowCoverage = scanline[startColumn..endColumn]; + rowCoverage.Clear(); + + float sampleWeight = 1F / DirectStrokeVerticalSampleCount; + for (int sampleIndex = 0; sampleIndex < DirectStrokeVerticalSampleCount; sampleIndex++) + { + // Second pass accumulates weighted horizontal coverage for each vertical supersample. + float sampleY = row + ((sampleIndex + 0.5F) / DirectStrokeVerticalSampleCount); + bool hasInterval; + + if (!TryGetQuadrilateralIntervalAtY(p0, p1, p2, p3, sampleY, out float left, out float right)) + { + if (this.stroke.LineCap == LineCap.Round) + { + bool hasRoundInterval = false; + left = default; + right = default; + if (TryGetCircleIntervalAtY(this.start, this.stroke.HalfWidth, sampleY, out float startLeft, out float startRight)) + { + hasRoundInterval = true; + left = startLeft; + right = startRight; + } + + if (TryGetCircleIntervalAtY(this.end, this.stroke.HalfWidth, sampleY, out float endLeft, out float endRight)) + { + if (!hasRoundInterval) + { + hasRoundInterval = true; + left = endLeft; + right = endRight; + } + else + { + left = MathF.Min(left, endLeft); + right = MathF.Max(right, endRight); + } + } + + hasInterval = hasRoundInterval; + } + else + { + hasInterval = false; + } + } + else + { + if (this.stroke.LineCap == LineCap.Round) + { + if (TryGetCircleIntervalAtY(this.start, this.stroke.HalfWidth, sampleY, out float startLeft, out float startRight)) + { + left = MathF.Min(left, startLeft); + right = MathF.Max(right, startRight); + } + + if (TryGetCircleIntervalAtY(this.end, this.stroke.HalfWidth, sampleY, out float endLeft, out float endRight)) + { + left = MathF.Min(left, endLeft); + right = MathF.Max(right, endRight); + } + } + + hasInterval = true; + } + + if (hasInterval) + { + AccumulateIntervalCoverage(rowCoverage, startColumn, left, right, sampleWeight); + } + } + + this.FinalizeCoverageRow(row, startColumn, rowCoverage, ref rowHandler); + } + + /// + /// Computes and emits one raster row for a point-like stroke footprint. + /// + /// The coverage row handler type. + /// The band-local row index. + /// The band-local center point. + /// The reusable scanline scratch buffer. + /// The coverage row handler that receives emitted runs. + private void EmitPointCoverageRow( + int row, + Vector2 center, + Span scanline, + ref TRowHandler rowHandler) + where TRowHandler : struct, IRasterizerCoverageRowHandler + { + float globalLeft = float.PositiveInfinity; + float globalRight = float.NegativeInfinity; + int sampleCount = 0; + + for (int sampleIndex = 0; sampleIndex < DirectStrokeVerticalSampleCount; sampleIndex++) + { + float sampleY = row + ((sampleIndex + 0.5F) / DirectStrokeVerticalSampleCount); + bool hasInterval = this.stroke.LineCap == LineCap.Round + ? TryGetCircleIntervalAtY(center, this.stroke.HalfWidth, sampleY, out float left, out float right) + : TryGetAxisAlignedIntervalAtY( + center.Y - this.stroke.HalfWidth, + center.Y + this.stroke.HalfWidth, + center.X - this.stroke.HalfWidth, + center.X + this.stroke.HalfWidth, + sampleY, + out left, + out right); + + if (!hasInterval) + { + continue; + } + + globalLeft = MathF.Min(globalLeft, left); + globalRight = MathF.Max(globalRight, right); + sampleCount++; + } + + if (sampleCount == 0) + { + return; + } + + int startColumn = Math.Max(0, (int)MathF.Floor(globalLeft)); + int endColumn = Math.Min(this.width, (int)MathF.Ceiling(globalRight)); + if (endColumn <= startColumn) + { + return; + } + + Span rowCoverage = scanline[startColumn..endColumn]; + rowCoverage.Clear(); + + float sampleWeight = 1F / DirectStrokeVerticalSampleCount; + for (int sampleIndex = 0; sampleIndex < DirectStrokeVerticalSampleCount; sampleIndex++) + { + float sampleY = row + ((sampleIndex + 0.5F) / DirectStrokeVerticalSampleCount); + bool hasInterval = this.stroke.LineCap == LineCap.Round + ? TryGetCircleIntervalAtY(center, this.stroke.HalfWidth, sampleY, out float left, out float right) + : TryGetAxisAlignedIntervalAtY( + center.Y - this.stroke.HalfWidth, + center.Y + this.stroke.HalfWidth, + center.X - this.stroke.HalfWidth, + center.X + this.stroke.HalfWidth, + sampleY, + out left, + out right); + + if (hasInterval) + { + AccumulateIntervalCoverage(rowCoverage, startColumn, left, right, sampleWeight); + } + } + + this.FinalizeCoverageRow(row, startColumn, rowCoverage, ref rowHandler); + } + + /// + /// Applies the selected rasterization mode and emits the non-zero runs for one row. + /// + /// The coverage row handler type. + /// The band-local row index. + /// The first covered column in the current scanline slice. + /// The accumulated row coverage slice. + /// The coverage row handler that receives emitted runs. + private void FinalizeCoverageRow( + int row, + int startColumn, + Span rowCoverage, + ref TRowHandler rowHandler) + where TRowHandler : struct, IRasterizerCoverageRowHandler + { + if (this.rasterizationMode == RasterizationMode.Aliased) + { + for (int i = 0; i < rowCoverage.Length; i++) + { + rowCoverage[i] = rowCoverage[i] >= this.antialiasThreshold ? 1F : 0F; + } + } + + EmitCoverageRuns(rowCoverage, startColumn, this.destinationLeft, this.destinationTop + row, ref rowHandler); + } + + /// + /// Accumulates one horizontal sample interval into per-pixel row coverage. + /// + /// The per-pixel row coverage buffer. + /// The destination column corresponding to index 0 in . + /// The left edge of the sample interval. + /// The right edge of the sample interval. + /// The contribution weight of the current vertical sample. + private static void AccumulateIntervalCoverage( + Span rowCoverage, + int baseColumn, + float left, + float right, + float sampleWeight) + { + int bandLeft = baseColumn; + int bandRight = baseColumn + rowCoverage.Length; + float clampedLeft = MathF.Max(left, bandLeft); + float clampedRight = MathF.Min(right, bandRight); + if (clampedRight <= clampedLeft) + { + return; + } + + int startPixel = (int)MathF.Floor(clampedLeft); + int endPixel = (int)MathF.Ceiling(clampedRight); + if (endPixel <= startPixel) + { + return; + } + + if (endPixel == startPixel + 1) + { + rowCoverage[startPixel - baseColumn] += (clampedRight - clampedLeft) * sampleWeight; + return; + } + + rowCoverage[startPixel - baseColumn] += ((startPixel + 1) - clampedLeft) * sampleWeight; + for (int x = startPixel + 1; x < endPixel - 1; x++) + { + rowCoverage[x - baseColumn] += sampleWeight; + } + + rowCoverage[(endPixel - 1) - baseColumn] += (clampedRight - (endPixel - 1)) * sampleWeight; + } + + /// + /// Emits contiguous non-zero coverage runs for one raster row. + /// + /// The coverage row handler type. + /// The per-pixel row coverage buffer. + /// The destination column corresponding to index 0 in . + /// The destination-space band left edge. + /// The destination-space row. + /// The coverage row handler that receives emitted runs. + private static void EmitCoverageRuns( + Span rowCoverage, + int startColumn, + int destinationLeft, + int destinationY, + ref TRowHandler rowHandler) + where TRowHandler : struct, IRasterizerCoverageRowHandler + { + int runStart = -1; + for (int i = 0; i < rowCoverage.Length; i++) + { + if (rowCoverage[i] > 0F) + { + runStart = runStart < 0 ? i : runStart; + continue; + } + + if (runStart >= 0) + { + rowHandler.Handle( + destinationY, + destinationLeft + startColumn + runStart, + rowCoverage[runStart..i]); + runStart = -1; + } + } + + if (runStart >= 0) + { + rowHandler.Handle( + destinationY, + destinationLeft + startColumn + runStart, + rowCoverage[runStart..]); + } + } + + /// + /// Intersects a horizontal sample line with an axis-aligned rectangle. + /// + /// The rectangle top edge. + /// The rectangle bottom edge. + /// The rectangle left edge. + /// The rectangle right edge. + /// The sample row in band-local coordinates. + /// Receives the left intersection bound. + /// Receives the right intersection bound. + /// when the sample intersects the rectangle. + private static bool TryGetAxisAlignedIntervalAtY( + float top, + float bottom, + float left, + float right, + float sampleY, + out float intervalLeft, + out float intervalRight) + { + if (sampleY < top || sampleY > bottom) + { + intervalLeft = default; + intervalRight = default; + return false; + } + + intervalLeft = left; + intervalRight = right; + return intervalRight > intervalLeft; + } + + /// + /// Intersects a horizontal sample line with a circle. + /// + /// The circle center. + /// The circle radius. + /// The sample row in band-local coordinates. + /// Receives the left intersection bound. + /// Receives the right intersection bound. + /// when the sample intersects the circle. + private static bool TryGetCircleIntervalAtY( + Vector2 center, + float radius, + float sampleY, + out float intervalLeft, + out float intervalRight) + { + float dy = sampleY - center.Y; + float radiusSquared = radius * radius; + float dySquared = dy * dy; + if (dySquared > radiusSquared) + { + intervalLeft = default; + intervalRight = default; + return false; + } + + float dx = MathF.Sqrt(MathF.Max(0F, radiusSquared - dySquared)); + intervalLeft = center.X - dx; + intervalRight = center.X + dx; + return intervalRight > intervalLeft; + } + + /// + /// Intersects a horizontal sample line with a convex quadrilateral. + /// + /// The first quadrilateral vertex. + /// The second quadrilateral vertex. + /// The third quadrilateral vertex. + /// The fourth quadrilateral vertex. + /// The sample row in band-local coordinates. + /// Receives the left intersection bound. + /// Receives the right intersection bound. + /// when the sample intersects the quadrilateral. + private static bool TryGetQuadrilateralIntervalAtY( + Vector2 p0, + Vector2 p1, + Vector2 p2, + Vector2 p3, + float sampleY, + out float intervalLeft, + out float intervalRight) + { + intervalLeft = float.PositiveInfinity; + intervalRight = float.NegativeInfinity; + bool hasIntersection = false; + AppendEdgeInterval(p0, p1, sampleY, ref hasIntersection, ref intervalLeft, ref intervalRight); + AppendEdgeInterval(p1, p2, sampleY, ref hasIntersection, ref intervalLeft, ref intervalRight); + AppendEdgeInterval(p2, p3, sampleY, ref hasIntersection, ref intervalLeft, ref intervalRight); + AppendEdgeInterval(p3, p0, sampleY, ref hasIntersection, ref intervalLeft, ref intervalRight); + return hasIntersection && intervalRight > intervalLeft; + } + + /// + /// Expands the current sample interval bounds with one polygon edge intersection. + /// + /// The edge start point. + /// The edge end point. + /// The sample row in band-local coordinates. + /// Tracks whether any edge has intersected the sample row yet. + /// The running left intersection bound. + /// The running right intersection bound. + private static void AppendEdgeInterval( + Vector2 start, + Vector2 end, + float sampleY, + ref bool hasIntersection, + ref float intervalLeft, + ref float intervalRight) + { + float minY = MathF.Min(start.Y, end.Y); + float maxY = MathF.Max(start.Y, end.Y); + if (sampleY < minY || sampleY > maxY) + { + return; + } + + if (MathF.Abs(end.Y - start.Y) <= StrokeDirectionEpsilon) + { + intervalLeft = MathF.Min(intervalLeft, MathF.Min(start.X, end.X)); + intervalRight = MathF.Max(intervalRight, MathF.Max(start.X, end.X)); + hasIntersection = true; + return; + } + + float t = (sampleY - start.Y) / (end.Y - start.Y); + float x = start.X + ((end.X - start.X) * t); + intervalLeft = MathF.Min(intervalLeft, x); + intervalRight = MathF.Max(intervalRight, x); + hasIntersection = true; + } + } + + /// + /// Returns the tessellation segment count used for one round join or cap arc. + /// + /// The arc radius. + /// The arc sweep angle in radians. + /// The tessellation detail scale. + /// The number of intermediate tessellation points. + private static int GetArcSubdivisionCount(float radius, double angle, double arcDetailScale) + { + double safeRadius = Math.Max(radius, StrokeDirectionEpsilon); + double safeScale = Math.Max(arcDetailScale, 0.01D); + double ratio = safeRadius / (safeRadius + (0.125D / safeScale)); + ratio = Math.Clamp(ratio, -1D, 1D); + double theta = Math.Acos(ratio) * 2D; + return theta <= 0D + ? 0 + : Math.Max(0, (int)(angle / theta)); + } + + /// + /// Returns the stroke offset unit normal for a normalized tangent. + /// + /// The normalized tangent. + /// The stroke offset unit normal. + private static Vector2 GetStrokeOffsetNormal(Vector2 tangent) => new(tangent.Y, -tangent.X); + + /// + /// Attempts to normalize the direction from to . + /// + /// The segment start point. + /// The segment end point. + /// Receives the normalized direction. + /// Receives the segment length. + /// when the segment has non-zero length. + private static bool TryGetDirection(PointF start, PointF end, out Vector2 direction, out float length) + { + Vector2 delta = end - start; + float lengthSquared = delta.LengthSquared(); + if (lengthSquared <= StrokeDirectionEpsilon * StrokeDirectionEpsilon) + { + direction = default; + length = 0F; + return false; + } + + length = MathF.Sqrt(lengthSquared); + direction = delta / length; + return true; + } + + /// + /// Attempts to intersect the two infinite offset support lines used by a join. + /// + /// The join point. + /// The offset vector on the previous segment. + /// The normalized tangent of the previous segment. + /// The offset vector on the next segment. + /// The normalized tangent of the next segment. + /// Receives the line intersection when one exists. + /// when the offset lines intersect. + private static bool TryIntersectOffsetLines( + Vector2 point, + Vector2 previousOffset, + Vector2 previousTangent, + Vector2 nextOffset, + Vector2 nextTangent, + out Vector2 intersection) + { + Vector2 a = point + previousOffset; + Vector2 b = point + nextOffset; + float denominator = Cross(previousTangent, nextTangent); + if (MathF.Abs(denominator) <= StrokeParallelEpsilon) + { + intersection = default; + return false; + } + + float t = Cross(b - a, nextTangent) / denominator; + intersection = a + (previousTangent * t); + return true; + } + + /// + /// Returns the 2D cross product scalar of the supplied vectors. + /// + /// The left operand. + /// The right operand. + /// The 2D cross product scalar. + private static float Cross(Vector2 left, Vector2 right) => (left.X * right.Y) - (left.Y * right.X); + + /// + /// Normalizes an angle into the inclusive-exclusive range [0, 2Ï€). + /// + /// The angle to normalize. + /// The normalized angle. + private static double NormalizePositiveAngle(double angle) + { + double fullTurn = Math.PI * 2D; + while (angle < 0D) + { + angle += fullTurn; + } + + while (angle >= fullTurn) + { + angle -= fullTurn; + } + + return angle; + } + + /// + /// Holds the stroke style values consumed by the CPU direct-stroke rasterizer. + /// + internal readonly struct StrokeStyle + { + /// + /// Initializes a new instance of the struct. + /// + /// The source pen. + /// The isotropic scale factor applied to the stroke width so the expansion happens in device-space pixels. + public StrokeStyle(Pen pen, float widthScale) + { + this.Width = pen.StrokeWidth * widthScale; + this.LineCap = pen.StrokeOptions.LineCap; + this.LineJoin = pen.StrokeOptions.LineJoin; + this.MiterLimit = pen.StrokeOptions.MiterLimit; + this.ArcDetailScale = pen.StrokeOptions.ArcDetailScale; + } + + /// + /// Gets the stroke width in device-space pixels. + /// + public float Width { get; } + + /// + /// Gets half the stroke width in device-space pixels. + /// + public float HalfWidth => this.Width * 0.5F; + + /// + /// Gets the cap style applied to open contour endpoints. + /// + public LineCap LineCap { get; } + + /// + /// Gets the outer join style applied to contour corners. + /// + public LineJoin LineJoin { get; } + + /// + /// Gets the outer miter limit expressed in stroke-width units. + /// + public double MiterLimit { get; } + + /// + /// Gets the round join/cap tessellation detail scale. + /// + public double ArcDetailScale { get; } + } + } + +#pragma warning restore SA1201 // Elements should appear in the correct order +} diff --git a/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.StrokeLinearizer.cs b/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.StrokeLinearizer.cs new file mode 100644 index 0000000..cb13e87 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.StrokeLinearizer.cs @@ -0,0 +1,1158 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + internal static partial class DefaultRasterizer + { + /// + /// Base retained stroke linearizer that expands stroked centerlines once into row-local line storage. + /// + /// The mutable per-row retained line collector type. + private abstract class StrokeLinearizer : Linearizer + where TL : class + { + private const float StrokeMicroSegmentEpsilon = 1F / 64F; + + private readonly StrokeStyle stroke; + + /// + /// Initializes a new instance of the class. + /// + /// The stroked centerline geometry. + /// The residual transform applied to each source point during emission. + /// The stroke style. + /// The destination-space X translation applied at composition time. + /// The destination-space Y translation applied at composition time. + /// The minimum destination X bound after clipping. + /// The minimum destination Y bound after clipping. + /// The visible destination width in pixels. + /// The visible destination height in pixels. + /// The first retained row-band index. + /// The retained row-band count. + /// The horizontal sampling offset. + /// The vertical sampling offset. + /// The allocator used for retained start-cover storage. + protected StrokeLinearizer( + LinearGeometry geometry, + Matrix4x4 residual, + StrokeStyle stroke, + int translateX, + int translateY, + int minX, + int minY, + int width, + int height, + int firstBandIndex, + int rowBandCount, + float samplingOffsetX, + float samplingOffsetY, + MemoryAllocator allocator) + : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator) + => this.stroke = stroke; + + private enum ContourInterest + { + Outside, + Clipped, + Contained + } + + /// + protected override bool ProcessCore() + { + ReadOnlySpan contours = this.Geometry.GetContours(); + for (int contourIndex = 0; contourIndex < contours.Length; contourIndex++) + { + LinearContour contour = contours[contourIndex]; + if (contour.PointCount == 0) + { + continue; + } + + ReadOnlySpan contourPoints = this.Geometry.GetContourPoints(contour); + ContourInterest contourInterest = this.GetContourInterest(contourPoints); + if (contourInterest == ContourInterest.Outside) + { + continue; + } + + bool isClosed = this.IsContourClosedForEmission(contourPoints, contour.IsClosed); + + this.ProcessContour(contourPoints, isClosed, contourInterest == ContourInterest.Contained); + } + + if (!this.HasAnyCoverage) + { + return false; + } + + this.FinalizeLines(); + return true; + } + + /// + /// Classifies one stroked contour against the interest bounds. + /// + /// The contour points. + /// The contour's relationship to the interest bounds. + private ContourInterest GetContourInterest(ReadOnlySpan contourPoints) + { + RectangleF translatedBounds = InflateStrokeBounds(this.GetPointBounds(contourPoints), this.stroke); + translatedBounds.Offset(this.TranslateX + this.SamplingOffsetX - this.MinX, this.TranslateY + this.SamplingOffsetY - this.MinY); + + if (translatedBounds.Right <= 0F || + translatedBounds.Bottom <= 0F || + translatedBounds.Left >= this.Width || + translatedBounds.Top >= this.Height) + { + return ContourInterest.Outside; + } + + if (translatedBounds.Left >= 0F && + translatedBounds.Top >= 0F && + translatedBounds.Right <= this.Width && + translatedBounds.Bottom <= this.Height) + { + return ContourInterest.Contained; + } + + return ContourInterest.Clipped; + } + + /// + /// Returns whether a contour should be treated as closed when emitting stroke geometry. + /// + /// The contour points. + /// Indicates whether the contour is explicitly closed. + /// when the contour should be stroked as closed; otherwise . + private bool IsContourClosedForEmission(ReadOnlySpan contourPoints, bool isDeclaredClosed) + { + if (contourPoints.Length < 3) + { + return false; + } + + PointF first = this.TransformPoint(contourPoints[0]); + PointF last = this.TransformPoint(contourPoints[^1]); + + if (isDeclaredClosed || first == last) + { + return true; + } + + Vector2 delta = first - last; + float closeThreshold = MathF.Max(this.stroke.Width, 1E-3F); + return delta.LengthSquared() <= closeThreshold * closeThreshold; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private PointF TransformPoint(PointF point) + => this.HasResidual ? PointF.Transform(point, this.Residual) : point; + + /// + /// Processes one centerline contour. + /// + /// The contiguous contour points. + /// Indicates whether the contour is closed. + /// Indicates whether the stroked contour is fully contained within the interest. + private void ProcessContour(ReadOnlySpan contourPoints, bool isClosed, bool contained) + { + using IMemoryOwner rentedSegmentsOwner = this.Allocator.Allocate(contourPoints.Length); + Span rentedSegments = rentedSegmentsOwner.Memory.Span; + int segmentCount = this.BuildContourSegments( + contourPoints, + isClosed, + rentedSegments, + out int distinctPointCount, + out PointF pointLike); + + if (segmentCount == 0) + { + this.EmitPointStrokeContour(pointLike, contained); + return; + } + + if (segmentCount == 1 || distinctPointCount == 2) + { + StrokeContourSegment segment = rentedSegments[0]; + this.EmitOpenSegmentStrokeContour(segment.Start, segment.End, contained); + return; + } + + if (isClosed) + { + this.EmitClosedStrokeContour(rentedSegments[..segmentCount], contained); + return; + } + + this.EmitOpenStrokeContour(rentedSegments[..segmentCount], contained); + } + + /// + /// Builds one contour-local stroke segment array while collapsing immediate duplicate points. + /// + /// The contiguous contour points. + /// Indicates whether the contour is closed. + /// The destination segment buffer. + /// Receives the number of distinct contour points. + /// Receives the fallback point for degenerate contours. + /// The number of emitted contour segments. + private int BuildContourSegments( + ReadOnlySpan contourPoints, + bool isClosed, + Span segments, + out int distinctPointCount, + out PointF pointLike) + { + pointLike = default; + distinctPointCount = 0; + if (contourPoints.IsEmpty) + { + return 0; + } + + Matrix4x4 residual = this.Residual; + bool hasResidual = this.HasResidual; + PointF firstPoint = hasResidual ? PointF.Transform(contourPoints[0], residual) : contourPoints[0]; + PointF previousPoint = firstPoint; + pointLike = firstPoint; + distinctPointCount = 1; + int segmentCount = 0; + + for (int i = 1; i < contourPoints.Length; i++) + { + PointF point = hasResidual ? PointF.Transform(contourPoints[i], residual) : contourPoints[i]; + if (point == previousPoint) + { + continue; + } + + if (TryCreateStrokeContourSegment(previousPoint, point, out StrokeContourSegment segment)) + { + distinctPointCount++; + segments[segmentCount++] = segment; + previousPoint = point; + } + + pointLike = point; + } + + if (isClosed && + distinctPointCount > 1 && + previousPoint == firstPoint) + { + distinctPointCount--; + } + + if (isClosed && + segmentCount > 1 && + previousPoint != firstPoint && + TryCreateStrokeContourSegment(previousPoint, firstPoint, out StrokeContourSegment closingSegment)) + { + segments[segmentCount++] = closingSegment; + } + + return segmentCount; + } + + /// + /// Creates one contour-local stroke segment descriptor. + /// + /// The segment start point. + /// The segment end point. + /// Receives the segment descriptor. + /// when a non-degenerate segment exists. + private static bool TryCreateStrokeContourSegment(PointF start, PointF end, out StrokeContourSegment segment) + { + if (Vector2.DistanceSquared(start, end) <= StrokeMicroSegmentEpsilon * StrokeMicroSegmentEpsilon) + { + segment = default; + return false; + } + + if (!TryGetDirection(start, end, out Vector2 tangent, out float length)) + { + segment = default; + return false; + } + + segment = new StrokeContourSegment(start, end, tangent, length); + return true; + } + + /// + /// Gets the point bounds for one contour. + /// + /// The contiguous contour points. + /// The contour point bounds. + private RectangleF GetPointBounds(ReadOnlySpan contourPoints) + { + Matrix4x4 residual = this.Residual; + bool hasResidual = this.HasResidual; + PointF first = hasResidual ? PointF.Transform(contourPoints[0], residual) : contourPoints[0]; + float minX = first.X; + float minY = first.Y; + float maxX = minX; + float maxY = minY; + + for (int i = 1; i < contourPoints.Length; i++) + { + PointF point = hasResidual ? PointF.Transform(contourPoints[i], residual) : contourPoints[i]; + minX = MathF.Min(minX, point.X); + minY = MathF.Min(minY, point.Y); + maxX = MathF.Max(maxX, point.X); + maxY = MathF.Max(maxY, point.Y); + } + + return RectangleF.FromLTRB(minX, minY, maxX, maxY); + } + + /// + /// Emits one stroked open segment. + /// + /// The segment start point. + /// The segment end point. + /// Indicates whether the segment is fully contained within the interest. + private void EmitOpenSegmentStrokeContour(PointF start, PointF end, bool contained) + { + if (!TryGetDirection(start, end, out Vector2 tangent, out _)) + { + this.EmitPointStrokeContour(start, contained); + return; + } + + float halfWidth = this.stroke.HalfWidth; + Vector2 normal = GetStrokeOffsetNormal(tangent) * halfWidth; + Vector2 extension = this.stroke.LineCap == LineCap.Square ? tangent * halfWidth : Vector2.Zero; + Vector2 startVector = start; + Vector2 endVector = end; + PointF p0 = startVector + normal - extension; + PointF p1 = endVector + normal + extension; + PointF p2 = endVector - normal + extension; + PointF p3 = startVector - normal - extension; + + this.EmitLine(p0, p1, contained); + + if (this.stroke.LineCap == LineCap.Round) + { + this.EmitDirectedArcContour(endVector, normal, -normal, contained); + } + else + { + this.EmitLine(p1, p2, contained); + } + + this.EmitLine(p2, p3, contained); + + if (this.stroke.LineCap == LineCap.Round) + { + this.EmitDirectedArcContour(startVector, -normal, normal, contained); + } + else + { + this.EmitLine(p3, p0, contained); + } + } + + /// + /// Emits one stroked open multi-segment contour from precomputed contour-local segments. + /// + /// The precomputed contour-local segments. + /// Indicates whether the contour is fully contained within the interest. + private void EmitOpenStrokeContour(ReadOnlySpan segments, bool contained) + { + StrokeContourSegment startSegment = segments[0]; + StrokeContourSegment endSegment = segments[^1]; + float halfWidth = this.stroke.HalfWidth; + Vector2 startNormal = startSegment.Normal * halfWidth; + Vector2 endNormal = endSegment.Normal * halfWidth; + Vector2 startExtension = this.stroke.LineCap == LineCap.Square ? startSegment.Tangent * halfWidth : Vector2.Zero; + Vector2 endExtension = this.stroke.LineCap == LineCap.Square ? endSegment.Tangent * halfWidth : Vector2.Zero; + Vector2 startPoint = startSegment.Start; + Vector2 endPoint = endSegment.End; + ContourState strokeContour = default; + this.AppendContourPoint(ref strokeContour, startPoint + startNormal - startExtension, contained); + + for (int i = 1; i < segments.Length; i++) + { + StrokeContourSegment previousSegment = segments[i - 1]; + StrokeContourSegment nextSegment = segments[i]; + + // Forward traversal: (v0, v1, v2) = (prev.Start, shared_vertex, next.End). + this.AppendSideJoinContour( + ref strokeContour, + previousSegment.Start, + previousSegment.End, + nextSegment.End, + previousSegment.Length, + nextSegment.Length, + contained); + } + + this.AppendContourPoint(ref strokeContour, endPoint + endNormal + endExtension, contained); + + if (this.stroke.LineCap == LineCap.Round) + { + this.AppendDirectedArcContour(ref strokeContour, endPoint, endNormal, -endNormal, contained); + } + else + { + this.AppendContourPoint(ref strokeContour, endPoint - endNormal + endExtension, contained); + } + + for (int i = segments.Length - 1; i >= 1; i--) + { + StrokeContourSegment previousSegment = segments[i]; + StrokeContourSegment nextSegment = segments[i - 1]; + + // Reverse traversal: vertex order reversed so PolygonStroker's Outline2 + // state machine lines up with the port below. + this.AppendSideJoinContour( + ref strokeContour, + previousSegment.End, + previousSegment.Start, + nextSegment.Start, + previousSegment.Length, + nextSegment.Length, + contained); + } + + this.AppendContourPoint(ref strokeContour, startPoint - startNormal - startExtension, contained); + + if (this.stroke.LineCap == LineCap.Round) + { + this.AppendDirectedArcContour(ref strokeContour, startPoint, -startNormal, startNormal, contained); + } + + this.CloseContour(ref strokeContour, contained); + } + + /// + /// Emits the two stroked contours for a closed contour from precomputed contour-local segments. + /// + /// The precomputed contour-local segments. + /// Indicates whether the contour is fully contained within the interest. + private void EmitClosedStrokeContour(ReadOnlySpan segments, bool contained) + { + ContourState leftContour = default; + for (int i = 0; i < segments.Length; i++) + { + StrokeContourSegment previousSegment = i == 0 ? segments[^1] : segments[i - 1]; + StrokeContourSegment nextSegment = segments[i]; + + this.AppendSideJoinContour( + ref leftContour, + previousSegment.Start, + nextSegment.Start, + nextSegment.End, + previousSegment.Length, + nextSegment.Length, + contained); + } + + this.CloseContour(ref leftContour, contained); + + ContourState reversedContour = default; + for (int i = segments.Length - 1; i >= 0; i--) + { + StrokeContourSegment previousSegment = segments[i]; + StrokeContourSegment nextSegment = i == 0 ? segments[^1] : segments[i - 1]; + + this.AppendSideJoinContour( + ref reversedContour, + previousSegment.End, + previousSegment.Start, + nextSegment.Start, + previousSegment.Length, + nextSegment.Length, + contained); + } + + this.CloseContour(ref reversedContour, contained); + } + + /// + /// Emits a point-like stroke as a cap contour. + /// + /// The point-like stroke location. + /// Indicates whether the contour is fully contained within the interest. + private void EmitPointStrokeContour(PointF point, bool contained) + { + Vector2 center = point; + float halfWidth = this.stroke.HalfWidth; + if (this.stroke.LineCap == LineCap.Round) + { + Vector2 startOffset = new(halfWidth, 0F); + this.EmitDirectedArcContour(center, startOffset, -startOffset, contained); + this.EmitDirectedArcContour(center, -startOffset, startOffset, contained); + return; + } + + PointF p0 = center + new Vector2(-halfWidth, -halfWidth); + PointF p1 = center + new Vector2(halfWidth, -halfWidth); + PointF p2 = center + new Vector2(halfWidth, halfWidth); + PointF p3 = center + new Vector2(-halfWidth, halfWidth); + this.EmitLine(p0, p1, contained); + this.EmitLine(p1, p2, contained); + this.EmitLine(p2, p3, contained); + this.EmitLine(p3, p0, contained); + } + + /// + /// Emits one round cap or join arc directly into the retained line storage. + /// + /// The arc center. + /// The start offset from the center. + /// The end offset from the center. + /// Indicates whether the arc is fully contained within the interest. + private void EmitDirectedArcContour( + Vector2 center, + Vector2 fromOffset, + Vector2 toOffset, + bool contained) + { + if (fromOffset == Vector2.Zero || toOffset == Vector2.Zero) + { + this.EmitLine(center + fromOffset, center + toOffset, contained); + return; + } + + float radius = fromOffset.Length(); + if (radius <= StrokeDirectionEpsilon) + { + this.EmitLine(center + fromOffset, center + toOffset, contained); + return; + } + + double startAngle = Math.Atan2(fromOffset.Y, fromOffset.X); + double endAngle = Math.Atan2(toOffset.Y, toOffset.X); + double sweep = NormalizePositiveAngle(endAngle - startAngle); + int subdivisionCount = GetArcSubdivisionCount(radius, sweep, this.stroke.ArcDetailScale); + double step = sweep / (subdivisionCount + 1); + + PointF previousPoint = center + fromOffset; + for (int i = 1; i <= subdivisionCount; i++) + { + float angle = (float)(startAngle + (step * i)); + PointF point = center + new Vector2(MathF.Cos(angle) * radius, MathF.Sin(angle) * radius); + this.EmitLine(previousPoint, point, contained); + previousPoint = point; + } + + this.EmitLine(previousPoint, center + toOffset, contained); + } + + /// + /// Appends a contour arc directly to the active stroke contour. + /// + /// The active contour state. + /// The arc center. + /// The start offset from the center. + /// The end offset from the center. + /// Indicates whether the arc is fully contained within the interest. + private void AppendDirectedArcContour( + ref ContourState contour, + Vector2 center, + Vector2 fromOffset, + Vector2 toOffset, + bool contained) + { + if (fromOffset == Vector2.Zero || toOffset == Vector2.Zero) + { + this.AppendContourPoint(ref contour, center + toOffset, contained); + return; + } + + float radius = fromOffset.Length(); + if (radius <= StrokeDirectionEpsilon) + { + this.AppendContourPoint(ref contour, center + toOffset, contained); + return; + } + + double startAngle = Math.Atan2(fromOffset.Y, fromOffset.X); + double endAngle = Math.Atan2(toOffset.Y, toOffset.X); + double sweep = NormalizePositiveAngle(endAngle - startAngle); + int subdivisionCount = GetArcSubdivisionCount(radius, sweep, this.stroke.ArcDetailScale); + double step = sweep / (subdivisionCount + 1); + + for (int i = 1; i <= subdivisionCount; i++) + { + float angle = (float)(startAngle + (step * i)); + this.AppendContourPoint( + ref contour, + center + new Vector2(MathF.Cos(angle) * radius, MathF.Sin(angle) * radius), + contained); + } + + this.AppendContourPoint(ref contour, center + toOffset, contained); + } + + /// + /// Appends one side join point sequence directly to the active stroke contour. + /// + /// + /// Direct port of PolygonStroker.CalcJoin so the rasterizer emits the same + /// join geometry as the reference CPU stroker. Each side of the outline calls this + /// once per source vertex; the reverse side reverses the vertex order exactly + /// like PolygonStroker's Outline2 state. + /// + /// The active contour state. + /// Previous source vertex in the emission's traversal order. + /// Current source vertex (the corner). + /// Next source vertex in the emission's traversal order. + /// Length of segment v0-v1. + /// Length of segment v1-v2. + /// Indicates whether the join is fully contained within the interest. + private void AppendSideJoinContour( + ref ContourState contour, + Vector2 v0, + Vector2 v1, + Vector2 v2, + float len1, + float len2, + bool contained) + { + float eps = StrokeDirectionEpsilon; + float halfWidth = this.stroke.HalfWidth; + float widthAbs = halfWidth; + float strokeWidth = halfWidth; + + if (len1 < eps || len2 < eps) + { + // Degenerate neighborhood: fall back to best available segment direction. + float l1 = len1 >= eps ? len1 : len2; + float l2 = len2 >= eps ? len2 : len1; + float invL1 = strokeWidth / l1; + float invL2 = strokeWidth / l2; + + Vector2 seg1 = v1 - v0; + Vector2 seg2 = v2 - v1; + + float offX1 = seg1.Y * invL1; + float offY1 = seg1.X * invL1; + float offX2 = seg2.Y * invL2; + float offY2 = seg2.X * invL2; + + this.AppendContourPoint(ref contour, new Vector2(v1.X + offX1, v1.Y - offY1), contained); + this.AppendContourPoint(ref contour, new Vector2(v1.X + offX2, v1.Y - offY2), contained); + return; + } + + Vector2 segForward = v1 - v0; + Vector2 segNext = v2 - v1; + float invLen1 = strokeWidth / len1; + float invLen2 = strokeWidth / len2; + float dx1 = segForward.Y * invLen1; + float dy1 = segForward.X * invLen1; + float dx2 = segNext.Y * invLen2; + float dy2 = segNext.X * invLen2; + + float cp = Cross(segNext, segForward); + + if (MathF.Abs(cp) > float.Epsilon && cp > 0F) + { + float limit = MathF.Min(len1, len2) / widthAbs; + if (limit < 1.01F) + { + limit = 1.01F; + } + + this.CalcMiter(ref contour, v0, v1, v2, dx1, dy1, dx2, dy2, LineJoin.MiterRevert, limit, 0F, contained); + return; + } + + // Outer corner. + Vector2 averageOffset = new Vector2(dx1 + dx2, dy1 + dy2) * 0.5F; + float bevelDistance = averageOffset.Length(); + + float widthEps = widthAbs / 1024F; + if ((this.stroke.LineJoin is LineJoin.Round or LineJoin.Bevel) && + ((float)this.stroke.ArcDetailScale * (widthAbs - bevelDistance)) < widthEps) + { + Vector2 outerOffset1 = new(dx1, -dy1); + Vector2 outerOffset2 = new(dx2, -dy2); + if (TryCalcIntersection(v0 + outerOffset1, v1 + outerOffset1, v1 + outerOffset2, v2 + outerOffset2, out Vector2 intersection)) + { + this.AppendContourPoint(ref contour, intersection, contained); + } + else + { + this.AppendContourPoint(ref contour, new Vector2(v1.X + dx1, v1.Y - dy1), contained); + } + + return; + } + + switch (this.stroke.LineJoin) + { + case LineJoin.Miter: + case LineJoin.MiterRevert: + case LineJoin.MiterRound: + this.CalcMiter(ref contour, v0, v1, v2, dx1, dy1, dx2, dy2, this.stroke.LineJoin, (float)this.stroke.MiterLimit, bevelDistance, contained); + break; + + case LineJoin.Round: + this.CalcArc(ref contour, v1.X, v1.Y, dx1, -dy1, dx2, -dy2, contained); + break; + + default: + this.AppendContourPoint(ref contour, new Vector2(v1.X + dx1, v1.Y - dy1), contained); + this.AppendContourPoint(ref contour, new Vector2(v1.X + dx2, v1.Y - dy2), contained); + break; + } + } + + /// + /// Direct port of PolygonStroker.CalcMiter. Emits the miter apex (or the + /// configured overflow fallback) at the join vertex. + /// + private void CalcMiter( + ref ContourState contour, + Vector2 v0, + Vector2 v1, + Vector2 v2, + float dx1, + float dy1, + float dx2, + float dy2, + LineJoin lineJoin, + float miterLimit, + float bevelDistance, + bool contained) + { + Vector2 p0 = v0; + Vector2 p1 = v1; + Vector2 p2 = v2; + Vector2 offset1 = new(dx1, -dy1); + Vector2 offset2 = new(dx2, -dy2); + + float xi = v1.X; + float yi = v1.Y; + float intersectionDistance = 1F; + float limit = this.stroke.HalfWidth * miterLimit; + bool miterLimitExceeded = true; + bool intersectionFailed = true; + + if (TryCalcIntersection(p0 + offset1, p1 + offset1, p1 + offset2, p2 + offset2, out Vector2 intersection)) + { + xi = intersection.X; + yi = intersection.Y; + intersectionDistance = Vector2.Distance(p1, intersection); + if (intersectionDistance <= limit) + { + this.AppendContourPoint(ref contour, intersection, contained); + miterLimitExceeded = false; + } + + intersectionFailed = false; + } + else + { + // Parallel/near-parallel fallback: probe a candidate offset point. + Vector2 probe = new(v1.X + dx1, v1.Y - dy1); + if ((CrossProduct(v0, v1, probe) < 0F) == (CrossProduct(v1, v2, probe) < 0F)) + { + this.AppendContourPoint(ref contour, probe, contained); + miterLimitExceeded = false; + } + } + + if (!miterLimitExceeded) + { + return; + } + + switch (lineJoin) + { + case LineJoin.MiterRevert: + this.AppendContourPoint(ref contour, new Vector2(v1.X + dx1, v1.Y - dy1), contained); + this.AppendContourPoint(ref contour, new Vector2(v1.X + dx2, v1.Y - dy2), contained); + break; + + case LineJoin.MiterRound: + this.CalcArc(ref contour, v1.X, v1.Y, dx1, -dy1, dx2, -dy2, contained); + break; + + default: + if (intersectionFailed) + { + // No reliable apex: project a clipped bevel using local tangent/perpendicular vectors. + this.AppendContourPoint( + ref contour, + new Vector2(v1.X + dx1 + (dy1 * miterLimit), v1.Y - dy1 + (dx1 * miterLimit)), + contained); + this.AppendContourPoint( + ref contour, + new Vector2(v1.X + dx2 - (dy2 * miterLimit), v1.Y - dy2 - (dx2 * miterLimit)), + contained); + } + else + { + float x1 = v1.X + dx1; + float y1 = v1.Y - dy1; + float x2 = v1.X + dx2; + float y2 = v1.Y - dy2; + float ratio = (limit - bevelDistance) / (intersectionDistance - bevelDistance); + this.AppendContourPoint(ref contour, new Vector2(x1 + ((xi - x1) * ratio), y1 + ((yi - y1) * ratio)), contained); + this.AppendContourPoint(ref contour, new Vector2(x2 + ((xi - x2) * ratio), y2 + ((yi - y2) * ratio)), contained); + } + + break; + } + } + + /// + /// Direct port of PolygonStroker.CalcArc. Emits intermediate arc vertices + /// around a join center between two offset vectors. + /// + private void CalcArc( + ref ContourState contour, + float x, + float y, + float dx1, + float dy1, + float dx2, + float dy2, + bool contained) + { + float strokeWidth = this.stroke.HalfWidth; + double a1 = Math.Atan2(dy1, dx1); + double a2 = Math.Atan2(dy2, dx2); + + double widthAbs = strokeWidth; + double da = Math.Acos(widthAbs / (widthAbs + (0.125D / this.stroke.ArcDetailScale))) * 2D; + this.AppendContourPoint(ref contour, new Vector2(x + dx1, y + dy1), contained); + + if (a1 > a2) + { + a2 += Math.PI * 2D; + } + + int n = (int)((a2 - a1) / da); + da = (a2 - a1) / (n + 1); + a1 += da; + for (int i = 0; i < n; i++) + { + this.AppendContourPoint( + ref contour, + new Vector2((float)(x + (Math.Cos(a1) * strokeWidth)), (float)(y + (Math.Sin(a1) * strokeWidth))), + contained); + a1 += da; + } + + this.AppendContourPoint(ref contour, new Vector2(x + dx2, y + dy2), contained); + } + + /// + /// Signed area of triangle (a, b, point), matching PolygonStroker.CrossProduct. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float CrossProduct(Vector2 a, Vector2 b, Vector2 point) + => ((point.X - b.X) * (b.Y - a.Y)) - ((point.Y - b.Y) * (b.X - a.X)); + + /// + /// Intersects two infinite lines defined by point pairs (a, b) and (c, d), + /// matching PolygonStroker.TryCalcIntersection. + /// + private static bool TryCalcIntersection(Vector2 a, Vector2 b, Vector2 c, Vector2 d, out Vector2 intersection) + { + const float eps = 1e-7F; + Vector2 ab = b - a; + Vector2 cd = d - c; + float denominator = Cross(ab, cd); + if (MathF.Abs(denominator) < eps) + { + intersection = default; + return false; + } + + float t = Cross(c - a, cd) / denominator; + intersection = a + (ab * t); + return true; + } + + /// + /// Appends one point to the active contour. + /// + /// The active contour state. + /// The point to append. + /// Indicates whether the contour is fully contained within the interest. + private void AppendContourPoint(ref ContourState state, PointF point, bool contained) + { + if (!state.HasPoint) + { + state.HasPoint = true; + state.FirstPoint = point; + state.PreviousPoint = point; + return; + } + + if (state.PreviousPoint == point) + { + return; + } + + this.EmitLine(state.PreviousPoint, point, contained); + state.PreviousPoint = point; + } + + /// + /// Closes the active contour. + /// + /// The active contour state. + /// Indicates whether the contour is fully contained within the interest. + private void CloseContour(ref ContourState state, bool contained) + { + if (!state.HasPoint || state.PreviousPoint == state.FirstPoint) + { + return; + } + + this.EmitLine(state.PreviousPoint, state.FirstPoint, contained); + state.PreviousPoint = state.FirstPoint; + } + + /// + /// Emits one stroked boundary edge into retained line storage. + /// + /// The edge start point. + /// The edge end point. + /// Indicates whether the edge is fully contained within the interest. + private void EmitLine(PointF start, PointF end, bool contained) + { + if (contained) + { + this.AddContainedLineF24Dot8( + FloatToFixed24Dot8(((start.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX), + FloatToFixed24Dot8(((start.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY), + FloatToFixed24Dot8(((end.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX), + FloatToFixed24Dot8(((end.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY)); + return; + } + + this.AddUncontainedLine( + ((start.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX, + ((start.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY, + ((end.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX, + ((end.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY); + } + + /// + /// Returns the stroke offset normal matching PolygonStroker's dx/dy convention. + /// + /// The normalized segment tangent. + /// The stroke-side offset normal. + private static Vector2 GetStrokeOffsetNormal(Vector2 tangent) => new(tangent.Y, -tangent.X); + + private readonly struct StrokeContourSegment + { + public StrokeContourSegment(PointF start, PointF end, Vector2 tangent, float length) + { + this.Start = start; + this.End = end; + this.Tangent = tangent; + this.Normal = GetStrokeOffsetNormal(tangent); + this.Length = length; + } + + public PointF Start { get; } + + public PointF End { get; } + + public Vector2 Tangent { get; } + + public Vector2 Normal { get; } + + public float Length { get; } + } + + private struct ContourState + { + public bool HasPoint; + public PointF FirstPoint; + public PointF PreviousPoint; + } + } + + /// + /// Stroke linearizer that finalizes retained lines into the 32-bit-X encoding. + /// + private sealed class StrokeLinearizerX32Y16 : StrokeLinearizer + { + /// + /// Initializes a new instance of the class. + /// + /// The stroked centerline geometry. + /// The residual transform applied to each source point during emission. + /// The stroke style. + /// The destination-space X translation applied at composition time. + /// The destination-space Y translation applied at composition time. + /// The minimum destination X bound after clipping. + /// The minimum destination Y bound after clipping. + /// The visible destination width in pixels. + /// The visible destination height in pixels. + /// The first retained row-band index. + /// The retained row-band count. + /// The horizontal sampling offset. + /// The vertical sampling offset. + /// The allocator used for retained start-cover storage. + public StrokeLinearizerX32Y16( + LinearGeometry geometry, + Matrix4x4 residual, + StrokeStyle stroke, + int translateX, + int translateY, + int minX, + int minY, + int width, + int height, + int firstBandIndex, + int rowBandCount, + float samplingOffsetX, + float samplingOffsetY, + MemoryAllocator allocator) + : base(geometry, residual, stroke, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator) + => this.FinalLines = new LineArrayX32Y16Block?[rowBandCount]; + + /// + /// Gets the finalized retained line blocks for each row band. + /// + public LineArrayX32Y16Block?[] FinalLines { get; } + + /// + protected override LineArrayX32Y16 CreateLineArray() => new(); + + /// + protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1) + => this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1); + + /// + protected override void FinalizeLines() + { + for (int i = 0; i < this.RowBandCount; i++) + { + LineArrayX32Y16? lineArray = this.LineArrays[i]; + this.FinalLines[i] = lineArray?.GetFrontBlock(); + this.FirstBlockLineCounts[i] = lineArray?.GetFrontBlockLineCount() ?? 0; + } + } + + /// + /// Executes the retained stroke linearization pass and returns the finalized payload. + /// + /// The finalized retained raster data. + /// when retained coverage was produced; otherwise . + internal bool TryProcess(out LinearizedRasterData result) + { + if (!this.ProcessCore()) + { + result = null!; + return false; + } + + result = new LinearizedRasterData( + this.Geometry, + new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount), + this.FinalLines, + this.FirstBlockLineCounts, + this.StartCoverTable); + + return true; + } + } + + /// + /// Stroke linearizer that finalizes retained lines into the packed 16-bit-X encoding. + /// + private sealed class StrokeLinearizerX16Y16 : StrokeLinearizer + { + /// + /// Initializes a new instance of the class. + /// + /// The stroked centerline geometry. + /// The residual transform applied to each source point during emission. + /// The stroke style. + /// The destination-space X translation applied at composition time. + /// The destination-space Y translation applied at composition time. + /// The minimum destination X bound after clipping. + /// The minimum destination Y bound after clipping. + /// The visible destination width in pixels. + /// The visible destination height in pixels. + /// The first retained row-band index. + /// The retained row-band count. + /// The horizontal sampling offset. + /// The vertical sampling offset. + /// The allocator used for retained start-cover storage. + public StrokeLinearizerX16Y16( + LinearGeometry geometry, + Matrix4x4 residual, + StrokeStyle stroke, + int translateX, + int translateY, + int minX, + int minY, + int width, + int height, + int firstBandIndex, + int rowBandCount, + float samplingOffsetX, + float samplingOffsetY, + MemoryAllocator allocator) + : base(geometry, residual, stroke, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator) + => this.FinalLines = new LineArrayX16Y16Block?[rowBandCount]; + + /// + /// Gets the finalized retained line blocks for each row band. + /// + public LineArrayX16Y16Block?[] FinalLines { get; } + + /// + protected override LineArrayX16Y16 CreateLineArray() => new(); + + /// + protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1) + => this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1); + + /// + protected override void FinalizeLines() + { + for (int i = 0; i < this.RowBandCount; i++) + { + LineArrayX16Y16? lineArray = this.LineArrays[i]; + this.FinalLines[i] = lineArray?.GetFrontBlock(); + this.FirstBlockLineCounts[i] = lineArray?.GetFrontBlockLineCount() ?? 0; + } + } + + /// + /// Executes the retained stroke linearization pass and returns the finalized payload. + /// + /// The finalized retained raster data. + /// when retained coverage was produced; otherwise . + internal bool TryProcess(out LinearizedRasterData result) + { + if (!this.ProcessCore()) + { + result = null!; + return false; + } + + result = new LinearizedRasterData( + this.Geometry, + new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount), + this.FinalLines, + this.FirstBlockLineCounts, + this.StartCoverTable); + + return true; + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs b/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs new file mode 100644 index 0000000..1235686 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs @@ -0,0 +1,1766 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// Fixed-point rasterizer that converts retained fill geometry into per-row coverage. + /// + /// + /// The rasterizer works in scene-aligned row bands. Each retained band stores compact line blocks + /// plus optional start-cover seeds, and execution replays that retained payload directly against + /// worker-local scratch without rebuilding geometry on every row. + /// + internal static partial class DefaultRasterizer + { + // Tile height used by the parallel row-tiling pipeline. + internal const int DefaultTileHeight = 16; + + private const int FixedShift = 8; + private const int FixedOne = 1 << FixedShift; + private const int MaximumDelta = 2048 << FixedShift; + private static readonly int WordBitCount = nint.Size * 8; + private const int AreaToCoverageShift = 9; + private const int CoverageStepCount = 256; + private const int EvenOddMask = (CoverageStepCount * 2) - 1; + private const int EvenOddPeriod = CoverageStepCount * 2; + private const float CoverageScale = 1F / CoverageStepCount; + + /// + /// Gets the preferred scene row height used by the CPU rasterizer. + /// + internal static int PreferredRowHeight => DefaultTileHeight; + + /// + /// Executes one retained rasterizable row item against a reusable scanner context. + /// + internal static void ExecuteRasterizableItem( + ref Context context, + in RasterizableItem item, + in RasterizableBandInfo bandInfo, + Span scanline, + ref TRowHandler rowHandler) + where TRowHandler : struct, IRasterizerCoverageRowHandler + { + context.Reconfigure( + bandInfo.Width, + bandInfo.WordsPerRow, + bandInfo.CoverStride, + bandInfo.BandHeight, + bandInfo.IntersectionRule, + bandInfo.RasterizationMode, + bandInfo.AntialiasThreshold); + + context.SeedStartCovers(item.GetActualCovers()); + if (item.Rasterizable.IsX16) + { + LineArrayX16Y16Block? lines = item.GetLineArrayX16(); + lines?.Iterate(item.GetFirstBlockLineCount(), ref context); + } + else + { + LineArrayX32Y16Block? lines = item.GetLineArrayX32(); + lines?.Iterate(item.GetFirstBlockLineCount(), ref context); + } + + context.EmitCoverageRows(bandInfo.DestinationTop, bandInfo.DestinationLeft, scanline, ref rowHandler); + context.ResetTouchedRows(); + } + + /// + /// Executes one retained stroke row item against a reusable scanner context. + /// + internal static void ExecuteStrokeRasterizableItem( + ref Context context, + in StrokeRasterizableItem item, + in RasterizableBandInfo bandInfo, + Span scanline, + Span strokeBandCoverage, + ref TRowHandler rowHandler) + where TRowHandler : struct, IRasterizerCoverageRowHandler + { + context.Reconfigure( + bandInfo.Width, + bandInfo.WordsPerRow, + bandInfo.CoverStride, + bandInfo.BandHeight, + bandInfo.IntersectionRule, + bandInfo.RasterizationMode, + bandInfo.AntialiasThreshold); + + item.Rasterizable.ExecuteBand(ref context, in bandInfo, scanline, strokeBandCoverage, ref rowHandler); + } + + /// + /// Converts bit count to the number of machine words needed to hold the bitset row. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int BitVectorsForMaxBitCount(int maxBitCount) => (maxBitCount + WordBitCount - 1) / WordBitCount; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static WorkerScratch CreateWorkerScratch(MemoryAllocator allocator, int width) + => WorkerScratch.Create(allocator, BitVectorsForMaxBitCount(width), checked(width << 1), width, PreferredRowHeight); + + /// + /// Converts a float coordinate to signed 24.8 fixed-point. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FloatToFixed24Dot8(float value) => (int)MathF.Round(value * FixedOne); + + /// + /// Returns one when a fixed-point value lies exactly on a cell boundary at or below zero. + /// This is used to keep edge ownership consistent for vertical lines. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FindAdjustment(int value) + { + int lte0 = ~((value - 1) >> 31) & 1; + int divisibleBy256 = (((value & (FixedOne - 1)) - 1) >> 31) & 1; + return lte0 & divisibleBy256; + } + + /// + /// Machine-word trailing zero count used for sparse bitset iteration. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int TrailingZeroCount(nuint value) + => nint.Size == sizeof(ulong) + ? BitOperations.TrailingZeroCount((ulong)value) + : BitOperations.TrailingZeroCount((uint)value); + + /// + /// Throws when the requested raster interest exceeds the scanner's indexing limits. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowInterestBoundsTooLarge() + => throw new ImageProcessingException("The rasterizer interest bounds are too large for DefaultRasterizer buffers."); + + /// + /// Creates retained row-local raster payload for one lowered geometry. + /// + internal static RasterizableGeometry? CreateRasterizableGeometry( + LinearGeometry geometry, + Matrix4x4 residual, + int translateX, + int translateY, + in RasterizerOptions options, + MemoryAllocator allocator) + { + float samplingOffsetX = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; + float samplingOffsetY = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; + + RectangleF translatedBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual); + translatedBounds.Offset(translateX + samplingOffsetX, translateY + samplingOffsetY); + + // The retained clipper ignores segments at the maximum X edge, + // so extend the right bound by one pixel to keep closing vertical edges available. + Rectangle geometryBounds = Rectangle.FromLTRB( + (int)MathF.Floor(translatedBounds.Left), + (int)MathF.Floor(translatedBounds.Top), + (int)MathF.Ceiling(translatedBounds.Right) + 1, + (int)MathF.Ceiling(translatedBounds.Bottom)); + + Rectangle clippedBounds = Rectangle.Intersect(geometryBounds, options.Interest); + if (clippedBounds.Width <= 0 || clippedBounds.Height <= 0) + { + return null; + } + + int width = clippedBounds.Width; + int height = clippedBounds.Height; + int firstRowBandIndex = clippedBounds.Top / PreferredRowHeight; + int lastRowBandIndex = (clippedBounds.Bottom - 1) / PreferredRowHeight; + int rowBandCount = lastRowBandIndex - firstRowBandIndex + 1; + int wordsPerRow = BitVectorsForMaxBitCount(width); + int coverStride = checked(width << 1); + + if (wordsPerRow <= 0 || coverStride <= 0) + { + ThrowInterestBoundsTooLarge(); + } + + if (width < 128) + { + LinearizerX16Y16 linearizer = new( + geometry, + residual, + translateX, + translateY, + clippedBounds.Left, + clippedBounds.Top, + width, + height, + firstRowBandIndex, + rowBandCount, + samplingOffsetX, + samplingOffsetY, + allocator); + + if (!linearizer.TryProcess(out LinearizedRasterData result)) + { + return null; + } + + RasterizableBandInfo[] bandInfos = new RasterizableBandInfo[rowBandCount]; + for (int i = 0; i < rowBandCount; i++) + { + int bandTop = (firstRowBandIndex + i) * PreferredRowHeight; + bool hasStartCovers = result.StartCoverTable[i] is not null; + bandInfos[i] = new RasterizableBandInfo( + CountLines(result.Lines[i], result.FirstBlockLineCounts[i]), + PreferredRowHeight, + width, + wordsPerRow, + coverStride, + clippedBounds.Left, + bandTop, + options.IntersectionRule, + options.RasterizationMode, + options.AntialiasThreshold, + hasStartCovers); + } + + return new RasterizableGeometry( + firstRowBandIndex, + rowBandCount, + width, + wordsPerRow, + coverStride, + PreferredRowHeight, + isX16: true, + bandInfos, + result.Lines, + null, + result.FirstBlockLineCounts, + result.StartCoverTable); + } + else + { + LinearizerX32Y16 linearizer = new( + geometry, + residual, + translateX, + translateY, + clippedBounds.Left, + clippedBounds.Top, + width, + height, + firstRowBandIndex, + rowBandCount, + samplingOffsetX, + samplingOffsetY, + allocator); + + if (!linearizer.TryProcess(out LinearizedRasterData result)) + { + return null; + } + + RasterizableBandInfo[] bandInfos = new RasterizableBandInfo[rowBandCount]; + for (int i = 0; i < rowBandCount; i++) + { + int bandTop = (firstRowBandIndex + i) * PreferredRowHeight; + bool hasStartCovers = result.StartCoverTable[i] is not null; + bandInfos[i] = new RasterizableBandInfo( + CountLines(result.Lines[i], result.FirstBlockLineCounts[i]), + PreferredRowHeight, + width, + wordsPerRow, + coverStride, + clippedBounds.Left, + bandTop, + options.IntersectionRule, + options.RasterizationMode, + options.AntialiasThreshold, + hasStartCovers); + } + + return new RasterizableGeometry( + firstRowBandIndex, + rowBandCount, + width, + wordsPerRow, + coverStride, + PreferredRowHeight, + isX16: false, + bandInfos, + null, + result.Lines, + result.FirstBlockLineCounts, + result.StartCoverTable); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CountLines(TLineBlock? firstLineBlock, int firstBlockLineCount) + where TLineBlock : class, ILineBlock + { + if (firstLineBlock is null) + { + return 0; + } + + int count = firstBlockLineCount; + TLineBlock? block = firstLineBlock.Next; + while (block is not null) + { + count += TLineBlock.LineCount; + block = block.Next; + } + + return count; + } + + /// + /// Band/tile-local scanner context that owns mutable coverage accumulation state. + /// + /// + /// Instances are intentionally stack-bound to keep hot-path data in spans and avoid heap churn. + /// + internal ref struct Context + { + private readonly Span bitVectors; + private readonly Span coverArea; + private readonly Span startCover; + private readonly Span rowMinTouchedColumn; + private readonly Span rowMaxTouchedColumn; + private readonly Span rowHasBits; + private readonly Span rowTouched; + private readonly Span touchedRows; + private int width; + private int height; + private int wordsPerRow; + private int coverStride; + private IntersectionRule intersectionRule; + private RasterizationMode rasterizationMode; + private float antialiasThreshold; + private int touchedRowCount; + + /// + /// Initializes a new instance of the struct. + /// + /// Scratch bit vectors that record which cells in each row received edge contributions. + /// Scratch cell table that accumulates signed cover/area values for the current band. + /// Scratch per-row start-cover values carried into coverage emission. + /// Scratch per-row minimum touched column bounds. + /// Scratch per-row maximum touched column bounds. + /// Scratch flags indicating whether a row has any bit-vector backed cell data. + /// Scratch flags indicating whether a row has received any contribution in the current band. + /// Scratch list of rows touched in the current band so emission can skip untouched rows. + /// The fill rule used when converting accumulated winding/coverage into final alpha. + /// The rasterization mode that controls how antialiasing thresholds are interpreted. + /// The threshold used when antialiasing is conditionally reduced or disabled. + public Context( + Span bitVectors, + Span coverArea, + Span startCover, + Span rowMinTouchedColumn, + Span rowMaxTouchedColumn, + Span rowHasBits, + Span rowTouched, + Span touchedRows, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + float antialiasThreshold) + { + this.bitVectors = bitVectors; + this.coverArea = coverArea; + this.startCover = startCover; + this.rowMinTouchedColumn = rowMinTouchedColumn; + this.rowMaxTouchedColumn = rowMaxTouchedColumn; + this.rowHasBits = rowHasBits; + this.rowTouched = rowTouched; + this.touchedRows = touchedRows; + this.width = 0; + this.height = 0; + this.wordsPerRow = 0; + this.coverStride = 0; + this.intersectionRule = intersectionRule; + this.rasterizationMode = rasterizationMode; + this.antialiasThreshold = antialiasThreshold; + this.touchedRowCount = 0; + } + + /// + /// Reconfigures this reusable context for a specific destination band without reallocating its scratch storage. + /// + /// The width, in pixels, of the current destination band. + /// The number of machine words used to represent one row of bit-vector coverage. + /// The stride, in cells, between rows in the cover/area table. + /// The height, in pixels, of the current destination band. + /// The fill rule used when converting accumulated winding/coverage into final alpha. + /// The rasterization mode that controls how antialiasing thresholds are interpreted. + /// The threshold used when antialiasing is conditionally reduced or disabled. + public void Reconfigure( + int width, + int wordsPerRow, + int coverStride, + int height, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + float antialiasThreshold) + { + this.width = width; + this.height = height; + this.wordsPerRow = wordsPerRow; + this.coverStride = coverStride; + this.intersectionRule = intersectionRule; + this.rasterizationMode = rasterizationMode; + this.antialiasThreshold = antialiasThreshold; + } + + /// + /// Seeds the current band with carry-over start-cover values produced while linearizing retained geometry. + /// + /// The per-row start-cover contributions for the destination band being rasterized. + public void SeedStartCovers(ReadOnlySpan startCovers) + { + int count = Math.Min(this.height, startCovers.Length); + for (int i = 0; i < count; i++) + { + int cover = startCovers[i]; + if (cover == 0) + { + continue; + } + + this.startCover[i] += cover; + this.MarkRowTouched(i); + } + } + + /// + /// Applies one clipped left-of-band winding interval directly to the current start-cover rows. + /// + /// The starting Y coordinate in 24.8 fixed-point band-local space. + /// The ending Y coordinate in 24.8 fixed-point band-local space. + public void AddClippedStartCover(int y0, int y1) + { + if (y0 == y1) + { + return; + } + + if (y0 < y1) + { + int rowIndex0 = y0 >> FixedShift; + int rowIndex1 = (y1 - 1) >> FixedShift; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + + if (rowIndex0 == rowIndex1) + { + this.AddStartCoverCell(rowIndex0, -(fy1 - fy0)); + return; + } + + this.AddStartCoverCell(rowIndex0, -(FixedOne - fy0)); + for (int row = rowIndex0 + 1; row < rowIndex1; row++) + { + this.AddStartCoverCell(row, -FixedOne); + } + + this.AddStartCoverCell(rowIndex1, -fy1); + return; + } + + int upRowIndex0 = (y0 - 1) >> FixedShift; + int upRowIndex1 = y1 >> FixedShift; + int upFy0 = y0 - (upRowIndex0 << FixedShift); + int upFy1 = y1 - (upRowIndex1 << FixedShift); + + if (upRowIndex0 == upRowIndex1) + { + this.AddStartCoverCell(upRowIndex0, upFy0 - upFy1); + return; + } + + this.AddStartCoverCell(upRowIndex0, upFy0); + for (int row = upRowIndex0 - 1; row > upRowIndex1; row--) + { + this.AddStartCoverCell(row, FixedOne); + } + + this.AddStartCoverCell(upRowIndex1, FixedOne - upFy1); + } + + /// + /// Rasterizes a single retained line segment into the current band scratch tables. + /// + /// The starting X coordinate in 24.8 fixed-point destination space. + /// The starting Y coordinate in 24.8 fixed-point destination space. + /// The ending X coordinate in 24.8 fixed-point destination space. + /// The ending Y coordinate in 24.8 fixed-point destination space. + public void RasterizeLineSegment(int x0, int y0, int x1, int y1) + => this.RasterizeLine(x0, y0, x1, y1); + + /// + /// Converts accumulated cover/area tables into non-zero coverage span callbacks. + /// + /// Absolute destination Y corresponding to row zero in this context. + /// Absolute destination X corresponding to column zero in this context. + /// Reusable scanline scratch buffer used to materialize emitted spans. + /// Coverage callback invoked for each emitted non-zero span. + public readonly void EmitCoverageRows( + int destinationTop, + int destinationLeft, + Span scanline, + ref TRowHandler rowHandler) + where TRowHandler : struct, IRasterizerCoverageRowHandler + { + // Iterate only rows that actually received coverage contributions. + // MarkRowTouched is called from AddCell for all contributions, including + // column-less startCover accumulations, so touchedRows is complete. + for (int i = 0; i < this.touchedRowCount; i++) + { + int row = this.touchedRows[i]; + int rowCover = this.startCover[row]; + bool rowHasBits = this.rowHasBits[row] != 0; + + if (!rowHasBits) + { + // No touched cells in this row, but carry cover from x < 0 can still + // produce a full-width constant span. + float coverage = this.AreaToCoverage(rowCover << AreaToCoverageShift); + if (coverage > 0F) + { + scanline[..this.width].Fill(coverage); + rowHandler.Handle(destinationTop + row, destinationLeft, scanline[..this.width]); + } + + continue; + } + + int minTouchedColumn = this.rowMinTouchedColumn[row]; + int maxTouchedColumn = this.rowMaxTouchedColumn[row]; + ReadOnlySpan rowBitVectors = this.bitVectors.Slice(row * this.wordsPerRow, this.wordsPerRow); + this.EmitRowCoverage( + rowBitVectors, + row, + rowCover, + minTouchedColumn, + maxTouchedColumn, + destinationLeft, + destinationTop + row, + scanline, + ref rowHandler); + } + } + + /// + /// Clears only rows touched during the previous rasterization pass. + /// + /// + /// This sparse reset strategy avoids clearing full scratch buffers when geometry is sparse. + /// + public void ResetTouchedRows() + { + // Reset only rows that received contributions in this band. This avoids clearing + // full temporary buffers when geometry is sparse relative to the interest bounds. + for (int i = 0; i < this.touchedRowCount; i++) + { + int row = this.touchedRows[i]; + this.startCover[row] = 0; + this.rowTouched[row] = 0; + + if (this.rowHasBits[row] == 0) + { + continue; + } + + this.rowHasBits[row] = 0; + + // Clear only touched bitset words for this row. + int minWord = this.rowMinTouchedColumn[row] / WordBitCount; + int maxWord = this.rowMaxTouchedColumn[row] / WordBitCount; + int wordCount = (maxWord - minWord) + 1; + this.bitVectors.Slice((row * this.wordsPerRow) + minWord, wordCount).Clear(); + } + + this.touchedRowCount = 0; + } + + /// + /// Emits one row by iterating touched columns and coalescing equal-coverage spans. + /// + /// Bitset words indicating touched columns in this row. + /// Row index inside the context. + /// Initial carry cover value from x less than zero contributions. + /// Minimum touched column index in this row. + /// Maximum touched column index in this row. + /// Absolute destination X corresponding to column zero in this context. + /// Absolute destination y for this row. + /// Reusable scanline coverage buffer used for per-span materialization. + /// Coverage callback invoked for each emitted non-zero span. + private readonly void EmitRowCoverage( + ReadOnlySpan rowBitVectors, + int row, + int cover, + int minTouchedColumn, + int maxTouchedColumn, + int destinationLeft, + int destinationY, + Span scanline, + ref TRowHandler rowHandler) + where TRowHandler : struct, IRasterizerCoverageRowHandler + { + int rowOffset = row * this.coverStride; + int spanStart = 0; + int spanEnd = 0; + float spanCoverage = 0F; + int runStart = -1; + int runEnd = -1; + int minWord = minTouchedColumn / WordBitCount; + int maxWord = maxTouchedColumn / WordBitCount; + + for (int wordIndex = minWord; wordIndex <= maxWord; wordIndex++) + { + // Iterate touched columns sparsely by scanning set bits only. + nuint bitset = rowBitVectors[wordIndex]; + while (bitset != 0) + { + int localBitIndex = TrailingZeroCount(bitset); + bitset &= bitset - 1; + + int x = (wordIndex * WordBitCount) + localBitIndex; + if ((uint)x >= (uint)this.width) + { + continue; + } + + int tableIndex = rowOffset + (x << 1); + + // Area uses current cover before adding this cell's delta. This matches + // scan-conversion math where area integrates the edge state at cell entry. + int area = this.coverArea[tableIndex + 1] + (cover << AreaToCoverageShift); + float coverage = this.AreaToCoverage(area); + + if (spanEnd == x) + { + if (coverage <= 0F) + { + // Zero coverage is a hard break. Everything buffered so far belongs + // to the contiguous non-zero region immediately before x, and the + // current pixel is outside that region. Flush now so a later non-zero + // span cannot be merged across this hole into the same row callback. + BufferSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + FlushBufferedRun(ref rowHandler, destinationY, destinationLeft, scanline, ref runStart, ref runEnd); + spanStart = x + 1; + spanEnd = spanStart; + spanCoverage = 0F; + } + else if (coverage == spanCoverage) + { + spanEnd = x + 1; + } + else + { + BufferSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + } + else + { + // We jumped over untouched columns. If cover != 0 the gap has a constant + // non-zero coverage and must be emitted as its own run. + if (cover == 0) + { + // A zero-coverage gap is the same kind of hard break as a zero + // coverage cell above: the buffered run must end before the gap so + // the next visible span starts a new contiguous non-zero interval. + BufferSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + FlushBufferedRun(ref rowHandler, destinationY, destinationLeft, scanline, ref runStart, ref runEnd); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + else + { + float gapCoverage = this.AreaToCoverage(cover << AreaToCoverageShift); + if (gapCoverage <= 0F) + { + // Even-odd can map non-zero winding to zero coverage. + // Treat this as a hard run break so we don't bridge across a + // zero-alpha hole and emit one callback for what is really two + // separate visible regions. + BufferSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + FlushBufferedRun(ref rowHandler, destinationY, destinationLeft, scanline, ref runStart, ref runEnd); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + else if (spanCoverage == gapCoverage) + { + if (coverage == gapCoverage) + { + spanEnd = x + 1; + } + else + { + BufferSpan(scanline, spanStart, x, spanCoverage, ref runStart, ref runEnd); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + } + else + { + BufferSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + BufferSpan(scanline, spanEnd, x, gapCoverage, ref runStart, ref runEnd); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + } + } + + cover += this.coverArea[tableIndex]; + } + } + + BufferSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + + if (cover != 0 && spanEnd < this.width) + { + BufferSpan(scanline, spanEnd, this.width, this.AreaToCoverage(cover << AreaToCoverageShift), ref runStart, ref runEnd); + } + + // At this point the buffered run, if any, represents one contiguous destination-space + // interval whose pixels all have non-zero coverage. Emitting that interval in one + // callback preserves the exact per-pixel coverage values already written into the + // scratch scanline while avoiding a stream of tiny span callbacks. + FlushBufferedRun(ref rowHandler, destinationY, destinationLeft, scanline, ref runStart, ref runEnd); + } + + /// + /// Converts accumulated signed area to normalized coverage under the selected fill rule. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly float AreaToCoverage(int area) + { + int signedArea = area >> AreaToCoverageShift; + int absoluteArea = signedArea < 0 ? -signedArea : signedArea; + float coverage; + + if (this.intersectionRule == IntersectionRule.NonZero) + { + // Non-zero winding clamps absolute winding accumulation to [0, 1]. + if (absoluteArea >= CoverageStepCount) + { + coverage = 1F; + } + else + { + coverage = absoluteArea * CoverageScale; + } + } + else + { + // Even-odd wraps every 2*CoverageStepCount and mirrors second half. + int wrapped = absoluteArea & EvenOddMask; + if (wrapped > CoverageStepCount) + { + wrapped = EvenOddPeriod - wrapped; + } + + coverage = wrapped >= CoverageStepCount ? 1F : wrapped * CoverageScale; + } + + if (this.rasterizationMode == RasterizationMode.Aliased) + { + // Aliased mode quantizes final coverage to hard 0/1 per pixel + // using the configurable threshold from GraphicsOptions.AntialiasThreshold. + return coverage >= this.antialiasThreshold ? 1F : 0F; + } + + return coverage; + } + + /// + /// Buffers one non-zero span into the current contiguous row run. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void BufferSpan( + Span scanline, + int start, + int end, + float coverage, + ref int runStart, + ref int runEnd) + { + if (coverage <= 0F || end <= start) + { + return; + } + + if (runStart < 0) + { + runStart = start; + runEnd = end; + } + else if (end > runEnd) + { + runEnd = end; + } + + // All spans in one buffered run are contiguous in destination space. That lets us + // pack them into one scratch slice, keep their exact per-pixel coverage values, and + // later hand the whole visible interval to the renderer in a single callback. + scanline[(start - runStart)..(end - runStart)].Fill(coverage); + } + + /// + /// Emits the currently buffered contiguous run, if any. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void FlushBufferedRun( + ref TRowHandler rowHandler, + int destinationY, + int destinationLeft, + Span scanline, + ref int runStart, + ref int runEnd) + where TRowHandler : struct, IRasterizerCoverageRowHandler + { + if (runStart < 0) + { + return; + } + + rowHandler.Handle(destinationY, destinationLeft + runStart, scanline[..(runEnd - runStart)]); + runStart = -1; + runEnd = -1; + } + + /// + /// Sets a row/column bit and reports whether it was newly set. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly bool ConditionalSetBit(int row, int column, out bool rowHadBits) + { + int bitIndex = row * this.wordsPerRow; + int wordIndex = bitIndex + (column / WordBitCount); + nuint mask = (nuint)1 << (column % WordBitCount); + ref nuint word = ref this.bitVectors[wordIndex]; + bool newlySet = (word & mask) == 0; + word |= mask; + + // Single read of rowHasBits serves both the conditional store + // and the caller's min/max column tracking. + rowHadBits = this.rowHasBits[row] != 0; + if (!rowHadBits) + { + this.rowHasBits[row] = 1; + } + + return newlySet; + } + + /// + /// Adds one cell contribution into cover/area accumulators. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddCell(int row, int column, int delta, int area) + { + if ((uint)row >= (uint)this.height) + { + return; + } + + this.MarkRowTouched(row); + + if (column < 0) + { + // Contributions left of x=0 accumulate into the row carry. + this.startCover[row] += delta; + return; + } + + if ((uint)column >= (uint)this.width) + { + return; + } + + int index = (row * this.coverStride) + (column << 1); + if (this.ConditionalSetBit(row, column, out bool rowHadBits)) + { + // First write wins initialization path avoids reading old values. + this.coverArea[index] = delta; + this.coverArea[index + 1] = area; + } + else + { + // Multiple edges can hit the same cell; accumulate signed values. + this.coverArea[index] += delta; + this.coverArea[index + 1] += area; + } + + if (!rowHadBits) + { + this.rowMinTouchedColumn[row] = column; + this.rowMaxTouchedColumn[row] = column; + } + else + { + if (column < this.rowMinTouchedColumn[row]) + { + this.rowMinTouchedColumn[row] = column; + } + + if (column > this.rowMaxTouchedColumn[row]) + { + this.rowMaxTouchedColumn[row] = column; + } + } + } + + /// + /// Adds one start-cover delta for a touched row. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddStartCoverCell(int row, int delta) + { + if (delta == 0 || (uint)row >= (uint)this.height) + { + return; + } + + this.MarkRowTouched(row); + this.startCover[row] += delta; + } + + /// + /// Marks a row as touched once so sparse reset can clear it later. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void MarkRowTouched(int row) + { + if (this.rowTouched[row] != 0) + { + return; + } + + this.rowTouched[row] = 1; + this.touchedRows[this.touchedRowCount++] = row; + } + + /// + /// Emits one vertical cell contribution. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CellVertical(int px, int py, int x, int y0, int y1) + { + int delta = y0 - y1; + int area = delta * ((FixedOne * 2) - x - x); + this.AddCell(py, px, delta, area); + } + + /// + /// Emits one general cell contribution. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Cell(int row, int px, int x0, int y0, int x1, int y1) + { + int delta = y0 - y1; + int area = delta * ((FixedOne * 2) - x0 - x1); + this.AddCell(row, px, delta, area); + } + + /// + /// Rasterizes a downward vertical edge segment. + /// + private void VerticalDown(int columnIndex, int y0, int y1, int x) + { + int rowIndex0 = y0 >> FixedShift; + int rowIndex1 = (y1 - 1) >> FixedShift; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int fx = x - (columnIndex << FixedShift); + + if (rowIndex0 == rowIndex1) + { + // Entire segment stays within one row. + this.CellVertical(columnIndex, rowIndex0, fx, fy0, fy1); + return; + } + + // First partial row, full middle rows, last partial row. + this.CellVertical(columnIndex, rowIndex0, fx, fy0, FixedOne); + + for (int row = rowIndex0 + 1; row < rowIndex1; row++) + { + this.CellVertical(columnIndex, row, fx, 0, FixedOne); + } + + this.CellVertical(columnIndex, rowIndex1, fx, 0, fy1); + } + + /// + /// Rasterizes an upward vertical edge segment. + /// + private void VerticalUp(int columnIndex, int y0, int y1, int x) + { + int rowIndex0 = (y0 - 1) >> FixedShift; + int rowIndex1 = y1 >> FixedShift; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int fx = x - (columnIndex << FixedShift); + + if (rowIndex0 == rowIndex1) + { + // Entire segment stays within one row. + this.CellVertical(columnIndex, rowIndex0, fx, fy0, fy1); + return; + } + + // First partial row, full middle rows, last partial row (upward direction). + this.CellVertical(columnIndex, rowIndex0, fx, fy0, 0); + + for (int row = rowIndex0 - 1; row > rowIndex1; row--) + { + this.CellVertical(columnIndex, row, fx, FixedOne, 0); + } + + this.CellVertical(columnIndex, rowIndex1, fx, FixedOne, fy1); + } + + // The following row/line helpers are directional variants of the same fixed-point edge + // walker. They are intentionally split to minimize branch costs in hot loops. + + /// + /// Rasterizes a downward, left-to-right segment within a single row. + /// + private void RowDownR(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + int columnIndex0 = p0x >> FixedShift; + int columnIndex1 = (p1x - 1) >> FixedShift; + int fx0 = p0x - (columnIndex0 << FixedShift); + int fx1 = p1x - (columnIndex1 << FixedShift); + + if (columnIndex0 == columnIndex1) + { + this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); + return; + } + + int dx = p1x - p0x; + int dy = p1y - p0y; + int pp = (FixedOne - fx0) * dy; + int cy = p0y + (pp / dx); + + this.Cell(rowIndex, columnIndex0, fx0, p0y, FixedOne, cy); + + int idx = columnIndex0 + 1; + + if (idx != columnIndex1) + { + int mod = (pp % dx) - dx; + int p = FixedOne * dy; + int lift = p / dx; + int rem = p % dx; + + for (; idx != columnIndex1; idx++) + { + int delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dx; + delta++; + } + + int ny = cy + delta; + this.Cell(rowIndex, idx, 0, cy, FixedOne, ny); + cy = ny; + } + } + + this.Cell(rowIndex, columnIndex1, 0, cy, fx1, p1y); + } + + /// + /// RowDownR variant that handles perfectly vertical edge ownership consistently. + /// + private void RowDownR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + if (p0x < p1x) + { + this.RowDownR(rowIndex, p0x, p0y, p1x, p1y); + } + else + { + int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; + int x = p0x - (columnIndex << FixedShift); + this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); + } + } + + /// + /// Rasterizes an upward, left-to-right segment within a single row. + /// + private void RowUpR(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + int columnIndex0 = p0x >> FixedShift; + int columnIndex1 = (p1x - 1) >> FixedShift; + int fx0 = p0x - (columnIndex0 << FixedShift); + int fx1 = p1x - (columnIndex1 << FixedShift); + + if (columnIndex0 == columnIndex1) + { + this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); + return; + } + + int dx = p1x - p0x; + int dy = p0y - p1y; + int pp = (FixedOne - fx0) * dy; + int cy = p0y - (pp / dx); + + this.Cell(rowIndex, columnIndex0, fx0, p0y, FixedOne, cy); + + int idx = columnIndex0 + 1; + + if (idx != columnIndex1) + { + int mod = (pp % dx) - dx; + int p = FixedOne * dy; + int lift = p / dx; + int rem = p % dx; + + for (; idx != columnIndex1; idx++) + { + int delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dx; + delta++; + } + + int ny = cy - delta; + this.Cell(rowIndex, idx, 0, cy, FixedOne, ny); + cy = ny; + } + } + + this.Cell(rowIndex, columnIndex1, 0, cy, fx1, p1y); + } + + /// + /// RowUpR variant that handles perfectly vertical edge ownership consistently. + /// + private void RowUpR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + if (p0x < p1x) + { + this.RowUpR(rowIndex, p0x, p0y, p1x, p1y); + } + else + { + int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; + int x = p0x - (columnIndex << FixedShift); + this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); + } + } + + /// + /// Rasterizes a downward, right-to-left segment within a single row. + /// + private void RowDownL(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + int columnIndex0 = (p0x - 1) >> FixedShift; + int columnIndex1 = p1x >> FixedShift; + int fx0 = p0x - (columnIndex0 << FixedShift); + int fx1 = p1x - (columnIndex1 << FixedShift); + + if (columnIndex0 == columnIndex1) + { + this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); + return; + } + + int dx = p0x - p1x; + int dy = p1y - p0y; + int pp = fx0 * dy; + int cy = p0y + (pp / dx); + + this.Cell(rowIndex, columnIndex0, fx0, p0y, 0, cy); + + int idx = columnIndex0 - 1; + + if (idx != columnIndex1) + { + int mod = (pp % dx) - dx; + int p = FixedOne * dy; + int lift = p / dx; + int rem = p % dx; + + for (; idx != columnIndex1; idx--) + { + int delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dx; + delta++; + } + + int ny = cy + delta; + this.Cell(rowIndex, idx, FixedOne, cy, 0, ny); + cy = ny; + } + } + + this.Cell(rowIndex, columnIndex1, FixedOne, cy, fx1, p1y); + } + + /// + /// RowDownL variant that handles perfectly vertical edge ownership consistently. + /// + private void RowDownL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + if (p0x > p1x) + { + this.RowDownL(rowIndex, p0x, p0y, p1x, p1y); + } + else + { + int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; + int x = p0x - (columnIndex << FixedShift); + this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); + } + } + + /// + /// Rasterizes an upward, right-to-left segment within a single row. + /// + private void RowUpL(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + int columnIndex0 = (p0x - 1) >> FixedShift; + int columnIndex1 = p1x >> FixedShift; + int fx0 = p0x - (columnIndex0 << FixedShift); + int fx1 = p1x - (columnIndex1 << FixedShift); + + if (columnIndex0 == columnIndex1) + { + this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); + return; + } + + int dx = p0x - p1x; + int dy = p0y - p1y; + int pp = fx0 * dy; + int cy = p0y - (pp / dx); + + this.Cell(rowIndex, columnIndex0, fx0, p0y, 0, cy); + + int idx = columnIndex0 - 1; + + if (idx != columnIndex1) + { + int mod = (pp % dx) - dx; + int p = FixedOne * dy; + int lift = p / dx; + int rem = p % dx; + + for (; idx != columnIndex1; idx--) + { + int delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dx; + delta++; + } + + int ny = cy - delta; + this.Cell(rowIndex, idx, FixedOne, cy, 0, ny); + cy = ny; + } + } + + this.Cell(rowIndex, columnIndex1, FixedOne, cy, fx1, p1y); + } + + /// + /// RowUpL variant that handles perfectly vertical edge ownership consistently. + /// + private void RowUpL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + if (p0x > p1x) + { + this.RowUpL(rowIndex, p0x, p0y, p1x, p1y); + } + else + { + int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; + int x = p0x - (columnIndex << FixedShift); + this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); + } + } + + /// + /// Rasterizes a downward, left-to-right segment spanning multiple rows. + /// + private void LineDownR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) + { + int dx = x1 - x0; + int dy = y1 - y0; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + + // p/delta/mod/rem implement an integer DDA that advances x at row boundaries + // without per-row floating-point math. + int p = (FixedOne - fy0) * dx; + int delta = p / dy; + int cx = x0 + delta; + + this.RowDownR_V(rowIndex0, x0, fy0, cx, FixedOne); + + int row = rowIndex0 + 1; + + if (row != rowIndex1) + { + int mod = (p % dy) - dy; + p = FixedOne * dx; + int lift = p / dy; + int rem = p % dy; + + for (; row != rowIndex1; row++) + { + delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dy; + delta++; + } + + int nx = cx + delta; + this.RowDownR_V(row, cx, 0, nx, FixedOne); + cx = nx; + } + } + + this.RowDownR_V(rowIndex1, cx, 0, x1, fy1); + } + + /// + /// Rasterizes an upward, left-to-right segment spanning multiple rows. + /// + private void LineUpR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) + { + int dx = x1 - x0; + int dy = y0 - y1; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + + // Upward version of the same integer DDA stepping as LineDownR. + int p = fy0 * dx; + int delta = p / dy; + int cx = x0 + delta; + + this.RowUpR_V(rowIndex0, x0, fy0, cx, 0); + + int row = rowIndex0 - 1; + if (row != rowIndex1) + { + int mod = (p % dy) - dy; + p = FixedOne * dx; + int lift = p / dy; + int rem = p % dy; + + for (; row != rowIndex1; row--) + { + delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dy; + delta++; + } + + int nx = cx + delta; + this.RowUpR_V(row, cx, FixedOne, nx, 0); + cx = nx; + } + } + + this.RowUpR_V(rowIndex1, cx, FixedOne, x1, fy1); + } + + /// + /// Rasterizes a downward, right-to-left segment spanning multiple rows. + /// + private void LineDownL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) + { + int dx = x0 - x1; + int dy = y1 - y0; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + + // Right-to-left variant of the integer DDA. + int p = (FixedOne - fy0) * dx; + int delta = p / dy; + int cx = x0 - delta; + + this.RowDownL_V(rowIndex0, x0, fy0, cx, FixedOne); + + int row = rowIndex0 + 1; + if (row != rowIndex1) + { + int mod = (p % dy) - dy; + p = FixedOne * dx; + int lift = p / dy; + int rem = p % dy; + + for (; row != rowIndex1; row++) + { + delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dy; + delta++; + } + + int nx = cx - delta; + this.RowDownL_V(row, cx, 0, nx, FixedOne); + cx = nx; + } + } + + this.RowDownL_V(rowIndex1, cx, 0, x1, fy1); + } + + /// + /// Rasterizes an upward, right-to-left segment spanning multiple rows. + /// + private void LineUpL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) + { + int dx = x0 - x1; + int dy = y0 - y1; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + + // Upward + right-to-left variant of the integer DDA. + int p = fy0 * dx; + int delta = p / dy; + int cx = x0 - delta; + + this.RowUpL_V(rowIndex0, x0, fy0, cx, 0); + + int row = rowIndex0 - 1; + if (row != rowIndex1) + { + int mod = (p % dy) - dy; + p = FixedOne * dx; + int lift = p / dy; + int rem = p % dy; + + for (; row != rowIndex1; row--) + { + delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dy; + delta++; + } + + int nx = cx - delta; + this.RowUpL_V(row, cx, FixedOne, nx, 0); + cx = nx; + } + } + + this.RowUpL_V(rowIndex1, cx, FixedOne, x1, fy1); + } + + /// + /// Dispatches a clipped edge to the correct directional fixed-point walker. + /// + private void RasterizeLine(int x0, int y0, int x1, int y1) + { + if (x0 == x1) + { + // Vertical edges need ownership adjustment to avoid double counting at cell seams. + int columnIndex = (x0 - FindAdjustment(x0)) >> FixedShift; + if (y0 < y1) + { + this.VerticalDown(columnIndex, y0, y1, x0); + } + else + { + this.VerticalUp(columnIndex, y0, y1, x0); + } + + return; + } + + if (y0 < y1) + { + // Downward edges use inclusive top/exclusive bottom row mapping. + int rowIndex0 = y0 >> FixedShift; + int rowIndex1 = (y1 - 1) >> FixedShift; + + if (rowIndex0 == rowIndex1) + { + int rowBase = rowIndex0 << FixedShift; + int localY0 = y0 - rowBase; + int localY1 = y1 - rowBase; + if (x0 < x1) + { + this.RowDownR(rowIndex0, x0, localY0, x1, localY1); + } + else + { + this.RowDownL(rowIndex0, x0, localY0, x1, localY1); + } + } + else if (x0 < x1) + { + this.LineDownR(rowIndex0, rowIndex1, x0, y0, x1, y1); + } + else + { + this.LineDownL(rowIndex0, rowIndex1, x0, y0, x1, y1); + } + + return; + } + + // Upward edges mirror the mapping to preserve winding consistency. + int upRowIndex0 = (y0 - 1) >> FixedShift; + int upRowIndex1 = y1 >> FixedShift; + + if (upRowIndex0 == upRowIndex1) + { + int rowBase = upRowIndex0 << FixedShift; + int localY0 = y0 - rowBase; + int localY1 = y1 - rowBase; + if (x0 < x1) + { + this.RowUpR(upRowIndex0, x0, localY0, x1, localY1); + } + else + { + this.RowUpL(upRowIndex0, x0, localY0, x1, localY1); + } + } + else if (x0 < x1) + { + this.LineUpR(upRowIndex0, upRowIndex1, x0, y0, x1, y1); + } + else + { + this.LineUpL(upRowIndex0, upRowIndex1, x0, y0, x1, y1); + } + } + } + + /// + /// Immutable scanner-local edge record (16 bytes). + /// + /// + /// All coordinates are stored as signed 24.8 fixed-point integers for predictable hot-path + /// access without per-read unpacking. Row bounds are computed inline from Y coordinates + /// where needed. + /// + internal readonly struct EdgeData + { + /// + /// Gets edge start X in scanner-local coordinates (24.8 fixed-point). + /// + public readonly int X0; + + /// + /// Gets edge start Y in scanner-local coordinates (24.8 fixed-point). + /// + public readonly int Y0; + + /// + /// Gets edge end X in scanner-local coordinates (24.8 fixed-point). + /// + public readonly int X1; + + /// + /// Gets edge end Y in scanner-local coordinates (24.8 fixed-point). + /// + public readonly int Y1; + + /// + /// Initializes a new instance of the struct. + /// + public EdgeData(int x0, int y0, int x1, int y1) + { + this.X0 = x0; + this.Y0 = y0; + this.X1 = x1; + this.Y1 = y1; + } + } + + /// + /// Reusable per-worker scratch buffers used by raster band execution. + /// + internal sealed class WorkerScratch : IDisposable + { + private readonly int wordsPerRow; + private readonly int coverStride; + private readonly int width; + private readonly int tileCapacity; + private readonly MemoryAllocator allocator; + private readonly IMemoryOwner bitVectorsOwner; + private readonly IMemoryOwner coverAreaOwner; + private readonly IMemoryOwner startCoverOwner; + private readonly IMemoryOwner rowMinTouchedColumnOwner; + private readonly IMemoryOwner rowMaxTouchedColumnOwner; + private readonly IMemoryOwner rowHasBitsOwner; + private readonly IMemoryOwner rowTouchedOwner; + private readonly IMemoryOwner touchedRowsOwner; + private readonly IMemoryOwner scanlineOwner; + private IMemoryOwner? strokeBandCoverageOwner; + + private WorkerScratch( + MemoryAllocator allocator, + int wordsPerRow, + int coverStride, + int width, + int tileCapacity, + IMemoryOwner bitVectorsOwner, + IMemoryOwner coverAreaOwner, + IMemoryOwner startCoverOwner, + IMemoryOwner rowMinTouchedColumnOwner, + IMemoryOwner rowMaxTouchedColumnOwner, + IMemoryOwner rowHasBitsOwner, + IMemoryOwner rowTouchedOwner, + IMemoryOwner touchedRowsOwner, + IMemoryOwner scanlineOwner) + { + this.allocator = allocator; + this.wordsPerRow = wordsPerRow; + this.coverStride = coverStride; + this.width = width; + this.tileCapacity = tileCapacity; + this.bitVectorsOwner = bitVectorsOwner; + this.coverAreaOwner = coverAreaOwner; + this.startCoverOwner = startCoverOwner; + this.rowMinTouchedColumnOwner = rowMinTouchedColumnOwner; + this.rowMaxTouchedColumnOwner = rowMaxTouchedColumnOwner; + this.rowHasBitsOwner = rowHasBitsOwner; + this.rowTouchedOwner = rowTouchedOwner; + this.touchedRowsOwner = touchedRowsOwner; + this.scanlineOwner = scanlineOwner; + } + + /// + /// Gets reusable scanline scratch for this worker. + /// + public Span Scanline => this.scanlineOwner.Memory.Span; + + /// + /// Gets reusable per-band stroke coverage scratch for this worker. + /// + public Span StrokeBandCoverage + => (this.strokeBandCoverageOwner ??= + this.allocator.Allocate(checked(this.width * this.tileCapacity * DirectStrokeVerticalSampleCount))) + .Memory.Span; + + /// + /// Returns when this scratch has compatible dimensions and sufficient + /// capacity for the requested parameters, making it safe to reuse without reallocation. + /// + internal bool CanReuse(int requiredWordsPerRow, int requiredCoverStride, int requiredWidth, int minCapacity) + => this.wordsPerRow >= requiredWordsPerRow + && this.coverStride >= requiredCoverStride + && this.width >= requiredWidth + && this.tileCapacity >= minCapacity; + + /// + /// Returns when this scratch can be reused for the default band configuration + /// at the requested width. + /// + internal bool CanReuse(int requiredWidth) + => this.CanReuse(BitVectorsForMaxBitCount(requiredWidth), checked(requiredWidth << 1), requiredWidth, PreferredRowHeight); + + /// + /// Allocates worker-local scratch sized for the configured tile/band capacity. + /// + public static WorkerScratch Create(MemoryAllocator allocator, int wordsPerRow, int coverStride, int width, int tileCapacity) + { + int bitVectorCapacity = checked(wordsPerRow * tileCapacity); + int coverAreaCapacity = checked(coverStride * tileCapacity); + IMemoryOwner bitVectorsOwner = allocator.Allocate(bitVectorCapacity, AllocationOptions.Clean); + IMemoryOwner coverAreaOwner = allocator.Allocate(coverAreaCapacity); + IMemoryOwner startCoverOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); + IMemoryOwner rowMinTouchedColumnOwner = allocator.Allocate(tileCapacity); + IMemoryOwner rowMaxTouchedColumnOwner = allocator.Allocate(tileCapacity); + IMemoryOwner rowHasBitsOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); + IMemoryOwner rowTouchedOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); + IMemoryOwner touchedRowsOwner = allocator.Allocate(tileCapacity); + IMemoryOwner scanlineOwner = allocator.Allocate(width); + + return new WorkerScratch( + allocator, + wordsPerRow, + coverStride, + width, + tileCapacity, + bitVectorsOwner, + coverAreaOwner, + startCoverOwner, + rowMinTouchedColumnOwner, + rowMaxTouchedColumnOwner, + rowHasBitsOwner, + rowTouchedOwner, + touchedRowsOwner, + scanlineOwner); + } + + /// + /// Creates a context view over a compatible prefix of this scratch for the requested geometry width. + /// + public Context CreateContext( + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + float antialiasThreshold) + => new( + this.bitVectorsOwner.Memory.Span, + this.coverAreaOwner.Memory.Span, + this.startCoverOwner.Memory.Span, + this.rowMinTouchedColumnOwner.Memory.Span, + this.rowMaxTouchedColumnOwner.Memory.Span, + this.rowHasBitsOwner.Memory.Span, + this.rowTouchedOwner.Memory.Span, + this.touchedRowsOwner.Memory.Span, + intersectionRule, + rasterizationMode, + antialiasThreshold); + + /// + /// Releases worker-local scratch buffers back to the allocator. + /// + public void Dispose() + { + this.bitVectorsOwner.Dispose(); + this.coverAreaOwner.Dispose(); + this.startCoverOwner.Dispose(); + this.rowMinTouchedColumnOwner.Dispose(); + this.rowMaxTouchedColumnOwner.Dispose(); + this.rowHasBitsOwner.Dispose(); + this.rowTouchedOwner.Dispose(); + this.touchedRowsOwner.Dispose(); + this.scanlineOwner.Dispose(); + this.strokeBandCoverageOwner?.Dispose(); + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/DrawingBackendScene.cs b/ImageSharp.Drawing/Processing/Backends/DrawingBackendScene.cs new file mode 100644 index 0000000..d0d6455 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/DrawingBackendScene.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// Base type for retained drawing backend scenes. + /// + public abstract class DrawingBackendScene : IDisposable + { + private readonly IReadOnlyList? ownedResources; + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The target bounds used to create the scene. + /// Resources that must stay alive for the retained scene. + protected DrawingBackendScene( + Rectangle bounds, + IReadOnlyList? ownedResources) + { + this.Bounds = bounds; + this.ownedResources = ownedResources; + } + + /// + /// Gets the target bounds used to create the scene. + /// + public Rectangle Bounds { get; } + + /// + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.DisposeCore(); + this.DisposeOwnedResources(); + this.isDisposed = true; + GC.SuppressFinalize(this); + } + + /// + /// Disposes backend-specific resources retained by this scene. + /// + protected virtual void DisposeCore() + { + } + + /// + /// Disposes resources retained for image-brush commands in this scene. + /// + private void DisposeOwnedResources() + { + if (this.ownedResources is null) + { + return; + } + + for (int i = 0; i < this.ownedResources.Count; i++) + { + this.ownedResources[i].Dispose(); + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/DrawingCommandBatch.cs b/ImageSharp.Drawing/Processing/Backends/DrawingCommandBatch.cs new file mode 100644 index 0000000..29d8e08 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/DrawingCommandBatch.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// One prepared draw-order command batch consumed by a drawing backend. + /// + public readonly struct DrawingCommandBatch + { + /// + /// Initializes a new instance of the struct. + /// + /// The draw-order scene commands. + /// Indicates whether the command stream contains layer boundaries. + public DrawingCommandBatch( + IReadOnlyList commands, + bool hasLayers) + { + this.Commands = commands; + this.HasLayers = hasLayers; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The backing command buffer. + /// The number of commands in the prepared batch. + /// Indicates whether the command stream contains layer boundaries. + internal DrawingCommandBatch( + CompositionSceneCommand[] commands, + int commandCount, + bool hasLayers) + : this(new ArraySegment(commands, 0, commandCount), hasLayers) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The backing command buffer. + /// The first command index. + /// The number of commands in the prepared batch. + /// Indicates whether the command stream contains layer boundaries. + internal DrawingCommandBatch( + CompositionSceneCommand[] commands, + int startIndex, + int commandCount, + bool hasLayers) + : this(new ArraySegment(commands, startIndex, commandCount), hasLayers) + { + } + + /// + /// Gets the draw-order scene commands. + /// + public IReadOnlyList Commands { get; } + + /// + /// Gets the total number of draw-order commands in the scene. + /// + public int CommandCount => this.Commands.Count; + + /// + /// Gets a value indicating whether this scene contains inline layer commands. + /// + public bool HasLayers { get; } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/FlushScene.RetainedTypes.cs b/ImageSharp.Drawing/Processing/Backends/FlushScene.RetainedTypes.cs new file mode 100644 index 0000000..17b9343 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/FlushScene.RetainedTypes.cs @@ -0,0 +1,475 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// Represents a flush-ready CPU scene built from retained row-local raster payload. + /// + internal sealed partial class FlushScene + { + /// + /// Identifies the retained row operation carried by a . + /// + internal enum SceneOperationKind : byte + { + /// + /// A retained fill item. + /// + FillItem = 0, + + /// + /// A retained stroke item. + /// + StrokeItem = 1, + + /// + /// Starts an isolated compositing layer. + /// + BeginLayer = 2, + + /// + /// Ends the most recently opened layer. + /// + EndLayer = 3 + } + + /// + /// Holds one retained row operation. + /// + internal readonly struct SceneOperation + { + /// + /// Initializes a new instance of the struct for a draw item. + /// + /// The retained draw operation kind. + /// The retained scene item index. + /// The retained rasterizable row index. + public SceneOperation(SceneOperationKind kind, int itemIndex, int localRowIndex) + { + this.Kind = kind; + this.ItemIndex = itemIndex; + this.LocalRowIndex = localRowIndex; + this.LayerBounds = default; + } + + /// + /// Initializes a new instance of the struct for a layer control operation. + /// + /// The layer operation kind. + /// The retained row-local layer bounds. + /// The retained layer-options index for begin-layer operations. + public SceneOperation(CompositionCommandKind kind, Rectangle layerBounds, int itemIndex) + { + this.Kind = kind == CompositionCommandKind.BeginLayer ? SceneOperationKind.BeginLayer : SceneOperationKind.EndLayer; + this.ItemIndex = itemIndex; + this.LocalRowIndex = -1; + this.LayerBounds = layerBounds; + } + + /// + /// Gets the operation kind. + /// + public SceneOperationKind Kind { get; } + + /// + /// Gets the retained scene item index for fill operations. + /// + public int ItemIndex { get; } + + /// + /// Gets the retained rasterizable row index for fill operations. + /// + public int LocalRowIndex { get; } + + /// + /// Gets the retained row-local layer bounds for layer operations. + /// + public Rectangle LayerBounds { get; } + } + + /// + /// Holds one retained scene row. + /// + internal readonly struct SceneRow : IDisposable + { + private readonly SceneOperationBlock? firstBlock; + private readonly SceneOperationBlock? lastBlock; + private readonly int rowBandIndex; + private readonly int count; + + /// + /// Initializes a new instance of the struct. + /// + /// The first retained row-item block. + /// The last retained row-item block. + /// The absolute row-band index represented by the row. + /// The number of retained operations in the row. + public SceneRow(SceneOperationBlock? firstBlock, SceneOperationBlock? lastBlock, int rowBandIndex, int count) + { + this.firstBlock = firstBlock; + this.lastBlock = lastBlock; + this.rowBandIndex = rowBandIndex; + this.count = count; + } + + /// + /// Gets the absolute row-band index represented by this scene row. + /// + public int RowBandIndex => this.rowBandIndex; + + /// + /// Gets the number of row items in this scene row. + /// + public int Count => this.count; + + /// + /// Gets the first retained row-item block. + /// + public SceneOperationBlock? FirstBlock => this.firstBlock; + + /// + /// Gets the last retained row-item block. + /// + public SceneOperationBlock? LastBlock => this.lastBlock; + + /// + /// Releases the row storage. + /// + public void Dispose() + { + SceneOperationBlock? block = this.firstBlock; + while (block is not null) + { + SceneOperationBlock? next = block.Next; + block.Dispose(); + block = next; + } + } + } + + /// + /// Appends row items directly into allocator-backed row storage. + /// + private struct RowBuilder : IDisposable + { + private readonly MemoryAllocator allocator; + private SceneOperationBlock? firstBlock; + private SceneOperationBlock? lastBlock; + private int count; + + /// + /// Initializes a new instance of the struct. + /// + /// The allocator used for row-block storage. + public RowBuilder(MemoryAllocator allocator) + { + this.allocator = allocator; + this.firstBlock = null; + this.lastBlock = null; + this.count = 0; + } + + /// + /// Gets a value indicating whether the builder has been initialized. + /// + public readonly bool IsInitialized => this.allocator is not null; + + /// + /// Gets the number of operations appended to this builder. + /// + public readonly int Count => this.count; + + /// + /// Appends a row item. + /// + /// The retained operation to append. + public void Append(SceneOperation operation) + { + if (this.lastBlock is null) + { + SceneOperationBlock block = new(this.allocator); + block.Append(operation); + this.firstBlock = block; + this.lastBlock = block; + this.count++; + return; + } + + SceneOperationBlock current = this.lastBlock; + if (current.Count < SceneOperationBlock.ItemsPerBlock) + { + current.Append(operation); + this.count++; + return; + } + + // Once a row block fills, link a fresh fixed-capacity block instead of reallocating + // and copying existing operations. This keeps the retained row builder append-only. + SceneOperationBlock next = new(this.allocator); + next.Append(operation); + current.Next = next; + next.Previous = current; + this.lastBlock = next; + this.count++; + } + + /// + /// Appends the retained blocks owned by to + /// without copying individual operations. + /// + /// The builder receiving the appended blocks. + /// The builder supplying the appended blocks. + public static void AppendBuilder(ref RowBuilder destination, ref RowBuilder source) + { + if (source.firstBlock is null) + { + return; + } + + if (destination.firstBlock is null) + { + destination = source; + source = default; + return; + } + + destination.lastBlock!.Next = source.firstBlock; + source.firstBlock.Previous = destination.lastBlock; + destination.lastBlock = source.lastBlock; + destination.count += source.count; + source = default; + } + + /// + /// Finalizes the builder into retained scene storage. + /// + /// The absolute row-band index represented by the row. + /// The finalized retained row. + public readonly SceneRow Finalize(int rowBandIndex) => new(this.firstBlock, this.lastBlock, rowBandIndex, this.count); + + /// + /// Disposes unfinalized storage. + /// + public readonly void Dispose() + { + SceneOperationBlock? block = this.firstBlock; + while (block is not null) + { + SceneOperationBlock? next = block.Next; + block.Dispose(); + block = next; + } + } + } + + /// + /// Represents one fixed-capacity row-item block. + /// + /// + /// This mirrors Blaze's RowItemList<T>::Block shape: append into the current block, + /// allocate a fresh block only when that block fills, and never reallocate or copy existing blocks. + /// + internal sealed class SceneOperationBlock : IDisposable + { + private readonly IMemoryOwner owner; + + /// + /// Initializes a new instance of the class. + /// + /// The allocator used for block storage. + public SceneOperationBlock(MemoryAllocator allocator) + => this.owner = allocator.Allocate(ItemsPerBlock); + + /// + /// Gets the fixed item capacity per block. + /// + public static int ItemsPerBlock => 32; + + /// + /// Gets or sets the previous block in the row list. + /// + public SceneOperationBlock? Previous { get; set; } + + /// + /// Gets or sets the next block in the row list. + /// + public SceneOperationBlock? Next { get; set; } + + /// + /// Gets the number of items written into this block. + /// + public int Count { get; private set; } + + /// + /// Gets the items written into this block. + /// + public Span Items => this.owner.Memory.Span[..this.Count]; + + /// + /// Appends an item into this block. + /// + /// The retained operation to append. + public void Append(SceneOperation operation) => this.owner.Memory.Span[this.Count++] = operation; + + /// + /// Releases the block storage. + /// + public void Dispose() => this.owner.Dispose(); + } + + /// + /// Holds one retained fill scene item. + /// + internal sealed class FillSceneItem : IDisposable + { + private object? renderer; + + /// + /// Initializes a new instance of the class. + /// + /// The brush used by the fill item. + /// The graphics options used by the fill item. + /// The brush bounds used for applicator creation. + /// The retained rasterizable geometry. + public FillSceneItem( + Brush brush, + GraphicsOptions graphicsOptions, + Rectangle brushBounds, + DefaultRasterizer.RasterizableGeometry rasterizable) + { + this.Brush = brush; + this.GraphicsOptions = graphicsOptions; + this.BrushBounds = brushBounds; + this.Rasterizable = rasterizable; + } + + /// + /// Gets the brush used by the fill item. + /// + public Brush Brush { get; } + + /// + /// Gets the graphics options used by the fill item. + /// + public GraphicsOptions GraphicsOptions { get; } + + /// + /// Gets the brush bounds used for applicator creation. + /// + public Rectangle BrushBounds { get; } + + /// + /// Gets the retained rasterizable geometry. + /// + public DefaultRasterizer.RasterizableGeometry Rasterizable { get; } + + /// + /// Gets the memoized renderer for this scene item, creating it on first use. + /// + /// The pixel format. + /// The active processing configuration. + /// The destination canvas width. + /// The memoized renderer for the scene item. + public BrushRenderer GetRenderer(Configuration configuration, int canvasWidth) + where TPixel : unmanaged, IPixel + { + if (this.renderer is BrushRenderer typed) + { + return typed; + } + + typed = this.Brush.CreateRenderer( + configuration, + this.GraphicsOptions, + canvasWidth, + this.BrushBounds); + + this.renderer = typed; + return typed; + } + + /// + public void Dispose() => this.Rasterizable.Dispose(); + } + + /// + /// Holds one retained stroke scene item. + /// + internal sealed class StrokeSceneItem : IDisposable + { + private object? renderer; + + /// + /// Initializes a new instance of the class. + /// + /// The prepared brush for the stroke item. + /// The graphics options for the stroke item. + /// The prepared brush bounds. + /// The retained stroke rasterizable geometry. + public StrokeSceneItem( + Brush brush, + GraphicsOptions graphicsOptions, + Rectangle brushBounds, + DefaultRasterizer.StrokeRasterizableGeometry rasterizable) + { + this.Brush = brush; + this.GraphicsOptions = graphicsOptions; + this.BrushBounds = brushBounds; + this.Rasterizable = rasterizable; + } + + /// + /// Gets the prepared brush for the stroke item. + /// + public Brush Brush { get; } + + /// + /// Gets the graphics options for the stroke item. + /// + public GraphicsOptions GraphicsOptions { get; } + + /// + /// Gets the prepared brush bounds for the stroke item. + /// + public Rectangle BrushBounds { get; } + + /// + /// Gets the retained stroke rasterizable geometry. + /// + public DefaultRasterizer.StrokeRasterizableGeometry Rasterizable { get; } + + /// + /// Gets the memoized renderer for this scene item, creating it on first use. + /// + /// The pixel format. + /// The active processing configuration. + /// The destination canvas width. + /// The memoized renderer for the scene item. + public BrushRenderer GetRenderer(Configuration configuration, int canvasWidth) + where TPixel : unmanaged, IPixel + { + if (this.renderer is BrushRenderer typed) + { + return typed; + } + + typed = this.Brush.CreateRenderer( + configuration, + this.GraphicsOptions, + canvasWidth, + this.BrushBounds); + + this.renderer = typed; + return typed; + } + + /// + public void Dispose() => this.Rasterizable.Dispose(); + } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/FlushScene.cs b/ImageSharp.Drawing/Processing/Backends/FlushScene.cs new file mode 100644 index 0000000..794d005 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/FlushScene.cs @@ -0,0 +1,1168 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// Represents a flush-ready CPU scene built from retained row-local raster payload. + /// + internal sealed partial class FlushScene : IDisposable + { + private static readonly FlushScene EmptyScene = new( + fillItemCount: 0, + strokeItemCount: 0, + rowCount: 0, + rowItemCount: 0, + totalEdgeCount: 0, + singleBandItemCount: 0, + smallEdgeItemCount: 0, + maxLayerDepth: 0, + fillItems: [], + strokeItems: [], + layerOptions: [], + rows: []); + + /// + /// Initializes a new instance of the class. + /// + private FlushScene( + int fillItemCount, + int strokeItemCount, + int rowCount, + int rowItemCount, + long totalEdgeCount, + int singleBandItemCount, + int smallEdgeItemCount, + int maxLayerDepth, + FillSceneItem?[] fillItems, + StrokeSceneItem?[] strokeItems, + GraphicsOptions?[] layerOptions, + SceneRow[] rows) + { + this.FillItemCount = fillItemCount; + this.StrokeItemCount = strokeItemCount; + this.RowCount = rowCount; + this.RowItemCount = rowItemCount; + this.TotalEdgeCount = totalEdgeCount; + this.SingleBandItemCount = singleBandItemCount; + this.SmallEdgeItemCount = smallEdgeItemCount; + this.MaxLayerDepth = maxLayerDepth; + this.FillItems = fillItems; + this.StrokeItems = strokeItems; + this.LayerOptions = layerOptions; + this.Rows = rows; + } + + /// + /// Gets the number of visible draw items retained by the scene. + /// + public int ItemCount => this.FillItemCount + this.StrokeItemCount; + + /// + /// Gets the number of visible fill items retained by the scene. + /// + public int FillItemCount { get; } + + /// + /// Gets the number of visible stroke items retained by the scene. + /// + public int StrokeItemCount { get; } + + /// + /// Gets the retained visible scene items. + /// + internal FillSceneItem?[] FillItems { get; } + + /// + /// Gets the retained visible stroke scene items. + /// + internal StrokeSceneItem?[] StrokeItems { get; } + + /// + /// Gets the retained layer options indexed by begin-layer command index. + /// + internal GraphicsOptions?[] LayerOptions { get; } + + /// + /// Gets the number of scene rows containing executable work. + /// + public int RowCount { get; } + + /// + /// Gets the retained row lists. + /// + internal SceneRow[] Rows { get; } + + /// + /// Gets the total number of row items retained by the scene. + /// + public int RowItemCount { get; } + + /// + /// Gets the total number of encoded raster edges retained by the scene. + /// + public long TotalEdgeCount { get; } + + /// + /// Gets the number of items that occupy a single row band. + /// + public int SingleBandItemCount { get; } + + /// + /// Gets the number of items whose retained edge count is small. + /// + public int SmallEdgeItemCount { get; } + + /// + /// Gets the maximum retained layer nesting depth in this scene. + /// + public int MaxLayerDepth { get; } + + /// + /// Creates a new scene by scheduling visible draw operations directly over retained rasterizable geometry. + /// + /// The prepared composition scene. + /// The destination bounds of the flush. + /// The allocator used for retained row storage. + /// + /// The maximum degree of parallelism to use when building the scene, or -1 to pass + /// through the runtime's unlimited sentinel for . + /// + /// A flush-ready scene. + public static FlushScene Create( + DrawingCommandBatch scene, + in Rectangle targetBounds, + MemoryAllocator allocator, + int maxDegreeOfParallelism) + { + int commandCount = scene.CommandCount; + + if (commandCount == 0) + { + return Empty(); + } + + IReadOnlyList commands = scene.Commands; + int firstTargetRowBandIndex = targetBounds.Top / DefaultRasterizer.DefaultTileHeight; + int lastTargetRowBandIndex = (targetBounds.Bottom - 1) / DefaultRasterizer.DefaultTileHeight; + int targetRowCount = (lastTargetRowBandIndex - firstTargetRowBandIndex) + 1; + Rectangle targetRectangle = targetBounds; + + if (targetRowCount <= 0) + { + return Empty(); + } + + FillSceneItem?[] fillItems = new FillSceneItem?[commandCount]; + StrokeSceneItem?[] strokeItems = new StrokeSceneItem?[commandCount]; + GraphicsOptions?[] layerOptions = new GraphicsOptions?[commandCount]; + int partitionCount = ParallelExecutionHelper.GetPartitionCount(maxDegreeOfParallelism, commandCount, targetRowCount); + PartitionState[] partitions = new PartitionState[partitionCount]; + + _ = Parallel.For( + 0, + partitionCount, + ParallelExecutionHelper.CreateParallelOptions(maxDegreeOfParallelism, partitionCount), + partitionIndex => + { + // Integer division splits the commands into contiguous half-open ranges, + // keeping the partitions balanced while assigning each command exactly once. + int commandStart = (partitionIndex * commandCount) / partitionCount; + int commandEnd = ((partitionIndex + 1) * commandCount) / partitionCount; + + partitions[partitionIndex] = ProcessPartition( + commands, + commandStart, + commandEnd, + targetRectangle, + firstTargetRowBandIndex, + targetRowCount, + allocator, + fillItems, + strokeItems, + layerOptions); + }); + + RowBuilder[] rowBuilders = new RowBuilder[targetRowCount]; + int fillItemCount = 0; + int strokeItemCount = 0; + long totalEdgeCount = 0; + int singleBandItemCount = 0; + int smallEdgeItemCount = 0; + int currentLayerDepth = 0; + int maxLayerDepth = 0; + + for (int i = 0; i < partitionCount; i++) + { + PartitionState partition = partitions[i]; + fillItemCount += partition.FillItemCount; + strokeItemCount += partition.StrokeItemCount; + totalEdgeCount += partition.TotalEdgeCount; + singleBandItemCount += partition.SingleBandItemCount; + smallEdgeItemCount += partition.SmallEdgeItemCount; + maxLayerDepth = Math.Max(maxLayerDepth, currentLayerDepth + partition.MaxLayerDepth); + currentLayerDepth += partition.LayerDepthDelta; + + for (int rowSlot = 0; rowSlot < targetRowCount; rowSlot++) + { + RowBuilder.AppendBuilder(ref rowBuilders[rowSlot], ref partition.RowBuilders[rowSlot]); + } + } + + int rowCount = 0; + int rowItemCount = 0; + for (int i = 0; i < rowBuilders.Length; i++) + { + if (!rowBuilders[i].IsInitialized) + { + continue; + } + + rowCount++; + rowItemCount += rowBuilders[i].Count; + } + + if ((fillItemCount + strokeItemCount) == 0 || rowItemCount == 0) + { + DisposeRows(rowBuilders); + return Empty(); + } + + SceneRow[] sceneRows = FinalizeRows(rowBuilders, firstTargetRowBandIndex, rowCount); + return new FlushScene( + fillItemCount, + strokeItemCount, + rowCount, + rowItemCount, + totalEdgeCount, + singleBandItemCount, + smallEdgeItemCount, + maxLayerDepth, + fillItems, + strokeItems, + layerOptions, + sceneRows); + } + + /// + /// Releases retained scene storage. + /// + public void Dispose() + { + for (int i = 0; i < this.Rows.Length; i++) + { + this.Rows[i].Dispose(); + } + + for (int i = 0; i < this.FillItems.Length; i++) + { + this.FillItems[i]?.Dispose(); + } + + for (int i = 0; i < this.StrokeItems.Length; i++) + { + this.StrokeItems[i]?.Dispose(); + } + } + + /// + /// Creates an empty scene instance. + /// + private static FlushScene Empty() => EmptyScene; + + /// + /// Identifies whether a path-backed command contributes executable retained raster work to the scene. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSceneDrawable(in CompositionCommand command) + => command.Kind == CompositionCommandKind.FillLayer; + + /// + /// Accumulates retained fill statistics used for scene heuristics. + /// + private static void AccumulateFillItemStats( + DefaultRasterizer.RasterizableGeometry rasterizable, + ref long totalEdgeCount, + ref int smallEdgeItemCount, + ref int singleBandItemCount) + { + for (int localRowIndex = 0; localRowIndex < rasterizable.RowBandCount; localRowIndex++) + { + if (!rasterizable.HasCoverage(localRowIndex)) + { + continue; + } + + DefaultRasterizer.RasterizableBandInfo info = rasterizable.GetBandInfo(localRowIndex); + totalEdgeCount += info.LineCount; + if (info.LineCount <= 8) + { + smallEdgeItemCount++; + } + } + + if (rasterizable.RowBandCount == 1) + { + singleBandItemCount++; + } + } + + /// + /// Accumulates retained stroke statistics used for scene heuristics. + /// + private static void AccumulateStrokeItemStats( + DefaultRasterizer.StrokeRasterizableGeometry rasterizable, + ref long totalEdgeCount, + ref int smallEdgeItemCount, + ref int singleBandItemCount) + { + for (int localRowIndex = 0; localRowIndex < rasterizable.RowBandCount; localRowIndex++) + { + if (!rasterizable.HasCoverage(localRowIndex)) + { + continue; + } + + DefaultRasterizer.RasterizableBandInfo info = rasterizable.GetBandInfo(localRowIndex); + totalEdgeCount += info.LineCount; + if (info.LineCount <= 8) + { + smallEdgeItemCount++; + } + } + + if (rasterizable.RowBandCount == 1) + { + singleBandItemCount++; + } + } + + /// + /// Appends retained fill row operations for one item into the row builders owned by the current partition. + /// + private static void AppendFillRowOperations( + RowBuilder[] rowBuilders, + int rowStart, + int rowEnd, + int firstTargetRowBandIndex, + int itemIndex, + DefaultRasterizer.RasterizableGeometry rasterizable, + MemoryAllocator allocator) + { + int localRowStart = Math.Max(0, rowStart - (rasterizable.FirstRowBandIndex - firstTargetRowBandIndex)); + int localRowEnd = Math.Min(rasterizable.RowBandCount, rowEnd - (rasterizable.FirstRowBandIndex - firstTargetRowBandIndex)); + for (int localRowIndex = localRowStart; localRowIndex < localRowEnd; localRowIndex++) + { + if (!rasterizable.HasCoverage(localRowIndex)) + { + continue; + } + + int rowSlot = (rasterizable.FirstRowBandIndex - firstTargetRowBandIndex) + localRowIndex; + ref RowBuilder builder = ref rowBuilders[rowSlot]; + if (!builder.IsInitialized) + { + builder = new RowBuilder(allocator); + } + + builder.Append(new SceneOperation(SceneOperationKind.FillItem, itemIndex, localRowIndex)); + } + } + + /// + /// Appends retained stroke row operations for one item into the row builders owned by the current partition. + /// + private static void AppendStrokeRowOperations( + RowBuilder[] rowBuilders, + int rowStart, + int rowEnd, + int firstTargetRowBandIndex, + int itemIndex, + DefaultRasterizer.StrokeRasterizableGeometry rasterizable, + MemoryAllocator allocator) + { + int localRowStart = Math.Max(0, rowStart - (rasterizable.FirstRowBandIndex - firstTargetRowBandIndex)); + int localRowEnd = Math.Min(rasterizable.RowBandCount, rowEnd - (rasterizable.FirstRowBandIndex - firstTargetRowBandIndex)); + for (int localRowIndex = localRowStart; localRowIndex < localRowEnd; localRowIndex++) + { + if (!rasterizable.HasCoverage(localRowIndex)) + { + continue; + } + + int rowSlot = (rasterizable.FirstRowBandIndex - firstTargetRowBandIndex) + localRowIndex; + ref RowBuilder builder = ref rowBuilders[rowSlot]; + if (!builder.IsInitialized) + { + builder = new RowBuilder(allocator); + } + + builder.Append(new SceneOperation(SceneOperationKind.StrokeItem, itemIndex, localRowIndex)); + } + } + + /// + /// Computes the row-slot range a fill or stroke command may write to. When the command was + /// recorded inside a SaveLayer the row distribution is confined to the layer's row bands so + /// a command's geometry cannot leak into rows that lie above or below the layer's + /// . Outside any layer (root or region scope) + /// the command is allowed to address every row; constraining row distribution by the + /// region's bounds would change long-standing rendering behaviour for region-only paths. + /// + /// The command's absolute target bounds. + /// The first row-band index covered by the partition. + /// The total number of row slots owned by the partition. + /// True if the command was recorded inside a SaveLayer scope. + /// The first row slot the command may write to. + /// The exclusive end row slot the command may write to. + private static void GetEffectiveRowSlotRange( + Rectangle commandTargetBounds, + int firstTargetRowBandIndex, + int totalRowSlots, + bool isInsideLayer, + out int rowStart, + out int rowEnd) + { + if (!isInsideLayer) + { + rowStart = 0; + rowEnd = totalRowSlots; + return; + } + + int firstRowBand = commandTargetBounds.Top / DefaultRasterizer.DefaultTileHeight; + int lastRowBand = (commandTargetBounds.Bottom - 1) / DefaultRasterizer.DefaultTileHeight; + rowStart = Math.Max(0, firstRowBand - firstTargetRowBandIndex); + rowEnd = Math.Min(totalRowSlots, lastRowBand - firstTargetRowBandIndex + 1); + } + + /// + /// Identifies whether a command contributes retained per-row layer control operations. + /// + private static bool TryGetLayerOperation( + in CompositionCommand command, + in Rectangle targetBounds, + int firstTargetRowBandIndex, + out CompositionCommandKind operationKind, + out Rectangle layerBounds, + out int firstRowSlot, + out int lastRowSlot) + { + operationKind = default; + layerBounds = default; + firstRowSlot = 0; + lastRowSlot = -1; + + switch (command.Kind) + { + case CompositionCommandKind.BeginLayer: + operationKind = CompositionCommandKind.BeginLayer; + break; + + case CompositionCommandKind.EndLayer: + operationKind = CompositionCommandKind.EndLayer; + break; + + default: + return false; + } + + Rectangle bounds = Rectangle.Intersect(command.LayerBounds, targetBounds); + if (bounds.Height <= 0 || bounds.Width <= 0) + { + return false; + } + + layerBounds = bounds; + int firstRowBandIndex = bounds.Top / DefaultRasterizer.DefaultTileHeight; + int lastRowBandIndex = (bounds.Bottom - 1) / DefaultRasterizer.DefaultTileHeight; + firstRowSlot = firstRowBandIndex - firstTargetRowBandIndex; + lastRowSlot = lastRowBandIndex - firstTargetRowBandIndex; + return firstRowSlot <= lastRowSlot; + } + + /// + /// Finalizes row-owned append builders into immutable scene rows. + /// + private static SceneRow[] FinalizeRows(RowBuilder[] builders, int firstTargetRowBandIndex, int rowCount) + { + SceneRow[] rows = new SceneRow[rowCount]; + int writeIndex = 0; + for (int i = 0; i < builders.Length; i++) + { + if (!builders[i].IsInitialized) + { + continue; + } + + rows[writeIndex++] = builders[i].Finalize(firstTargetRowBandIndex + i); + } + + return rows; + } + + /// + /// Disposes partially created row builders. + /// + private static void DisposeRows(RowBuilder[] builders) + { + for (int i = 0; i < builders.Length; i++) + { + builders[i].Dispose(); + } + } + + private static PartitionState ProcessPartition( + IReadOnlyList commands, + int commandStart, + int commandEnd, + in Rectangle targetBounds, + int firstTargetRowBandIndex, + int targetRowCount, + MemoryAllocator allocator, + FillSceneItem?[] fillItems, + StrokeSceneItem?[] strokeItems, + GraphicsOptions?[] layerOptions) + { + RowBuilder[] rowBuilders = new RowBuilder[targetRowCount]; + int fillItemCount = 0; + int strokeItemCount = 0; + long totalEdgeCount = 0; + int singleBandItemCount = 0; + int smallEdgeItemCount = 0; + int currentLayerDepth = 0; + int maxLayerDepth = 0; + + for (int commandIndex = commandStart; commandIndex < commandEnd; commandIndex++) + { + CompositionSceneCommand command = commands[commandIndex]; + if (command is PathCompositionSceneCommand pathCommand) + { + ProcessPathCommand( + pathCommand.Command, + commandIndex, + targetBounds, + firstTargetRowBandIndex, + rowBuilders, + allocator, + fillItems, + strokeItems, + layerOptions, + ref fillItemCount, + ref strokeItemCount, + ref totalEdgeCount, + ref singleBandItemCount, + ref smallEdgeItemCount, + ref currentLayerDepth, + ref maxLayerDepth); + } + else if (command is StrokePathCompositionSceneCommand strokePathCommand) + { + ProcessStrokePathCommand( + strokePathCommand.Command, + commandIndex, + targetRowCount, + firstTargetRowBandIndex, + rowBuilders, + allocator, + strokeItems, + ref strokeItemCount, + ref totalEdgeCount, + ref singleBandItemCount, + ref smallEdgeItemCount); + } + else if (command is LineSegmentCompositionSceneCommand lineSegmentCommand) + { + ProcessLineSegmentCommand( + lineSegmentCommand.Command, + commandIndex, + targetRowCount, + firstTargetRowBandIndex, + rowBuilders, + allocator, + strokeItems, + ref strokeItemCount, + ref totalEdgeCount, + ref singleBandItemCount, + ref smallEdgeItemCount); + } + else + { + ProcessPolylineCommand( + ((PolylineCompositionSceneCommand)command).Command, + commandIndex, + targetRowCount, + firstTargetRowBandIndex, + rowBuilders, + allocator, + strokeItems, + ref strokeItemCount, + ref totalEdgeCount, + ref singleBandItemCount, + ref smallEdgeItemCount); + } + } + + return new PartitionState( + fillItemCount, + strokeItemCount, + totalEdgeCount, + singleBandItemCount, + smallEdgeItemCount, + currentLayerDepth, + maxLayerDepth, + rowBuilders); + } + + private static void ProcessPathCommand( + in CompositionCommand command, + int commandIndex, + in Rectangle targetBounds, + int firstTargetRowBandIndex, + RowBuilder[] rowBuilders, + MemoryAllocator allocator, + FillSceneItem?[] fillItems, + StrokeSceneItem?[] strokeItems, + GraphicsOptions?[] layerOptions, + ref int fillItemCount, + ref int strokeItemCount, + ref long totalEdgeCount, + ref int singleBandItemCount, + ref int smallEdgeItemCount, + ref int currentLayerDepth, + ref int maxLayerDepth) + { + if (TryGetLayerOperation( + command, + targetBounds, + firstTargetRowBandIndex, + out CompositionCommandKind operationKind, + out Rectangle layerBounds, + out int firstRowSlot, + out int lastRowSlot)) + { + if (operationKind == CompositionCommandKind.BeginLayer) + { + currentLayerDepth++; + maxLayerDepth = Math.Max(maxLayerDepth, currentLayerDepth); + } + else + { + currentLayerDepth--; + } + + int layerOptionsIndex = -1; + if (operationKind == CompositionCommandKind.BeginLayer) + { + // BeginLayer carries the compositing options used later by the matching EndLayer. + // Store them at the command index so row operations can keep a compact integer reference. + layerOptions[commandIndex] = command.GraphicsOptions; + layerOptionsIndex = commandIndex; + } + + AppendLayerOperations(rowBuilders, firstRowSlot, lastRowSlot, layerBounds, operationKind, layerOptionsIndex, targetBounds, allocator); + return; + } + + if (!IsSceneDrawable(command)) + { + return; + } + + if (!TryPrepareFillPath(command, allocator, out PreparedFillItem preparedFill) || + preparedFill.Rasterizable.RowBandCount == 0) + { + return; + } + + fillItems[commandIndex] = new FillSceneItem(preparedFill.Brush, preparedFill.GraphicsOptions, preparedFill.BrushBounds, preparedFill.Rasterizable); + fillItemCount++; + AccumulateFillItemStats(preparedFill.Rasterizable, ref totalEdgeCount, ref smallEdgeItemCount, ref singleBandItemCount); + GetEffectiveRowSlotRange(command.TargetBounds, firstTargetRowBandIndex, rowBuilders.Length, command.IsInsideLayer, out int rowStart, out int rowEnd); + AppendFillRowOperations(rowBuilders, rowStart, rowEnd, firstTargetRowBandIndex, commandIndex, preparedFill.Rasterizable, allocator); + } + + private static void ProcessStrokePathCommand( + in StrokePathCommand command, + int commandIndex, + int targetRowCount, + int firstTargetRowBandIndex, + RowBuilder[] rowBuilders, + MemoryAllocator allocator, + StrokeSceneItem?[] strokeItems, + ref int strokeItemCount, + ref long totalEdgeCount, + ref int singleBandItemCount, + ref int smallEdgeItemCount) + { + if (!TryPrepareStrokePath(command, allocator, out PreparedStrokeItem preparedStroke) || + preparedStroke.Rasterizable.RowBandCount == 0) + { + return; + } + + strokeItems[commandIndex] = new StrokeSceneItem(preparedStroke.Brush, preparedStroke.GraphicsOptions, preparedStroke.BrushBounds, preparedStroke.Rasterizable); + strokeItemCount++; + AccumulateStrokeItemStats(preparedStroke.Rasterizable, ref totalEdgeCount, ref smallEdgeItemCount, ref singleBandItemCount); + GetEffectiveRowSlotRange(command.TargetBounds, firstTargetRowBandIndex, targetRowCount, command.IsInsideLayer, out int rowStart, out int rowEnd); + AppendStrokeRowOperations(rowBuilders, rowStart, rowEnd, firstTargetRowBandIndex, commandIndex, preparedStroke.Rasterizable, allocator); + } + + private static void ProcessLineSegmentCommand( + in StrokeLineSegmentCommand command, + int commandIndex, + int targetRowCount, + int firstTargetRowBandIndex, + RowBuilder[] rowBuilders, + MemoryAllocator allocator, + StrokeSceneItem?[] strokeItems, + ref int strokeItemCount, + ref long totalEdgeCount, + ref int singleBandItemCount, + ref int smallEdgeItemCount) + { + if (!TryPrepareLineSegmentStroke(command, allocator, out PreparedStrokeItem preparedStroke) || + preparedStroke.Rasterizable.RowBandCount == 0) + { + return; + } + + strokeItems[commandIndex] = new StrokeSceneItem(preparedStroke.Brush, preparedStroke.GraphicsOptions, preparedStroke.BrushBounds, preparedStroke.Rasterizable); + strokeItemCount++; + AccumulateStrokeItemStats(preparedStroke.Rasterizable, ref totalEdgeCount, ref smallEdgeItemCount, ref singleBandItemCount); + GetEffectiveRowSlotRange(command.TargetBounds, firstTargetRowBandIndex, targetRowCount, command.IsInsideLayer, out int rowStart, out int rowEnd); + AppendStrokeRowOperations(rowBuilders, rowStart, rowEnd, firstTargetRowBandIndex, commandIndex, preparedStroke.Rasterizable, allocator); + } + + private static void ProcessPolylineCommand( + in StrokePolylineCommand command, + int commandIndex, + int targetRowCount, + int firstTargetRowBandIndex, + RowBuilder[] rowBuilders, + MemoryAllocator allocator, + StrokeSceneItem?[] strokeItems, + ref int strokeItemCount, + ref long totalEdgeCount, + ref int singleBandItemCount, + ref int smallEdgeItemCount) + { + if (!TryPreparePolylineStroke(command, allocator, out PreparedStrokeItem preparedStroke) || + preparedStroke.Rasterizable.RowBandCount == 0) + { + return; + } + + strokeItems[commandIndex] = new StrokeSceneItem(preparedStroke.Brush, preparedStroke.GraphicsOptions, preparedStroke.BrushBounds, preparedStroke.Rasterizable); + strokeItemCount++; + AccumulateStrokeItemStats(preparedStroke.Rasterizable, ref totalEdgeCount, ref smallEdgeItemCount, ref singleBandItemCount); + GetEffectiveRowSlotRange(command.TargetBounds, firstTargetRowBandIndex, targetRowCount, command.IsInsideLayer, out int rowStart, out int rowEnd); + AppendStrokeRowOperations(rowBuilders, rowStart, rowEnd, firstTargetRowBandIndex, commandIndex, preparedStroke.Rasterizable, allocator); + } + + private static void AppendLayerOperations( + RowBuilder[] rowBuilders, + int firstRowSlot, + int lastRowSlot, + Rectangle layerBandBounds, + CompositionCommandKind operationKind, + int layerOptionsIndex, + in Rectangle targetBounds, + MemoryAllocator allocator) + { + for (int rowSlot = firstRowSlot; rowSlot <= lastRowSlot; rowSlot++) + { + ref RowBuilder builder = ref rowBuilders[rowSlot]; + if (!builder.IsInitialized) + { + builder = new RowBuilder(allocator); + } + + int rowTop = targetBounds.Top + (rowSlot * DefaultRasterizer.DefaultTileHeight); + Rectangle rowBounds = new(targetBounds.Left, rowTop, targetBounds.Width, DefaultRasterizer.DefaultTileHeight); + Rectangle rowLayerBounds = Rectangle.Intersect(layerBandBounds, rowBounds); + builder.Append(new SceneOperation(operationKind, rowLayerBounds, layerOptionsIndex)); + } + } + + private static bool TryPrepareFillPath( + in CompositionCommand command, + MemoryAllocator allocator, + out PreparedFillItem prepared) + { + IPath path = command.SourcePath; + Matrix4x4 transform = command.Transform; + bool hasTransform = !transform.IsIdentity; + Vector2 scale = ExtractScale(transform); + Matrix4x4 residual = ComputeResidual(scale, transform); + LinearGeometry geometry = path.ToLinearGeometry(scale); + Brush sourceBrush = hasTransform ? command.Brush.Transform(transform) : command.Brush; + RectangleF geometryBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual); + + if (!TryResolveRasterization( + sourceBrush, + geometryBounds, + command.RasterizerOptions, + command.DestinationOffset, + command.TargetBounds, + out Brush brush, + out RasterizerOptions rasterizerOptions, + out Rectangle brushBounds)) + { + prepared = default; + return false; + } + + DefaultRasterizer.RasterizableGeometry? rasterizable = DefaultRasterizer.CreateRasterizableGeometry( + geometry, + residual, + command.DestinationOffset.X, + command.DestinationOffset.Y, + rasterizerOptions, + allocator); + + if (rasterizable is null) + { + prepared = default; + return false; + } + + prepared = new PreparedFillItem(brush, command.GraphicsOptions, brushBounds, rasterizable); + return true; + } + + private static bool TryPrepareStrokePath( + in StrokePathCommand command, + MemoryAllocator allocator, + out PreparedStrokeItem prepared) + { + IPath path = command.SourcePath; + Matrix4x4 transform = command.Transform; + bool hasTransform = !transform.IsIdentity; + Vector2 scale = ExtractScale(transform); + Matrix4x4 residual = ComputeResidual(scale, transform); + LinearGeometry geometry = path.ToLinearGeometry(scale); + float widthScale = GetTransformWidthScale(transform); + RectangleF geometryBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual); + RectangleF strokeBounds = GetStrokeBounds(geometryBounds, command.Pen, widthScale); + Brush sourceBrush = hasTransform ? command.Brush.Transform(transform) : command.Brush; + + if (!TryResolveRasterization( + sourceBrush, + strokeBounds, + command.RasterizerOptions, + command.DestinationOffset, + command.TargetBounds, + out Brush brush, + out RasterizerOptions rasterizerOptions, + out Rectangle brushBounds)) + { + prepared = default; + return false; + } + + DefaultRasterizer.StrokeRasterizableGeometry? rasterizable = DefaultRasterizer.CreatePathStrokeRasterizableGeometry( + geometry, + residual, + command.Pen, + command.DestinationOffset.X, + command.DestinationOffset.Y, + rasterizerOptions, + widthScale, + allocator); + if (rasterizable is null) + { + prepared = default; + return false; + } + + prepared = new PreparedStrokeItem(brush, command.GraphicsOptions, brushBounds, rasterizable); + return true; + } + + private static bool TryPrepareLineSegmentStroke( + in StrokeLineSegmentCommand command, + MemoryAllocator allocator, + out PreparedStrokeItem prepared) + { + Matrix4x4 transform = command.Transform; + bool hasTransform = !transform.IsIdentity; + PointF start = hasTransform ? PointF.Transform(command.SourceStart, transform) : command.SourceStart; + PointF end = hasTransform ? PointF.Transform(command.SourceEnd, transform) : command.SourceEnd; + float widthScale = GetTransformWidthScale(transform); + RectangleF segmentBounds = RectangleF.FromLTRB( + MathF.Min(start.X, end.X), + MathF.Min(start.Y, end.Y), + MathF.Max(start.X, end.X), + MathF.Max(start.Y, end.Y)); + RectangleF bounds = GetStrokeBounds(segmentBounds, command.Pen, widthScale); + Brush sourceBrush = hasTransform ? command.Brush.Transform(transform) : command.Brush; + + if (!TryResolveRasterization( + sourceBrush, + bounds, + command.RasterizerOptions, + command.DestinationOffset, + command.TargetBounds, + out Brush brush, + out RasterizerOptions rasterizerOptions, + out Rectangle brushBounds)) + { + prepared = default; + return false; + } + + DefaultRasterizer.StrokeRasterizableGeometry? rasterizable = DefaultRasterizer.CreateLineSegmentStrokeRasterizableGeometry( + start, + end, + command.Pen, + command.DestinationOffset.X, + command.DestinationOffset.Y, + rasterizerOptions, + widthScale, + allocator); + + if (rasterizable is null) + { + prepared = default; + return false; + } + + prepared = new PreparedStrokeItem(brush, command.GraphicsOptions, brushBounds, rasterizable); + return true; + } + + private static bool TryPreparePolylineStroke( + in StrokePolylineCommand command, + MemoryAllocator allocator, + out PreparedStrokeItem prepared) + { + Matrix4x4 transform = command.Transform; + bool hasTransform = !transform.IsIdentity; + Vector2 scale = ExtractScale(transform); + Matrix4x4 residual = ComputeResidual(scale, transform); + LinearGeometry geometry = LinearGeometry.CreateOpenPolyline(command.SourcePoints, scale); + float widthScale = GetTransformWidthScale(transform); + RectangleF geometryBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual); + RectangleF strokeBounds = GetStrokeBounds(geometryBounds, command.Pen, widthScale); + Brush sourceBrush = hasTransform ? command.Brush.Transform(transform) : command.Brush; + + if (!TryResolveRasterization( + sourceBrush, + strokeBounds, + command.RasterizerOptions, + command.DestinationOffset, + command.TargetBounds, + out Brush brush, + out RasterizerOptions rasterizerOptions, + out Rectangle brushBounds)) + { + prepared = default; + return false; + } + + DefaultRasterizer.StrokeRasterizableGeometry? rasterizable = DefaultRasterizer.CreatePathStrokeRasterizableGeometry( + geometry, + residual, + command.Pen, + command.DestinationOffset.X, + command.DestinationOffset.Y, + rasterizerOptions, + widthScale, + allocator); + + if (rasterizable is null) + { + prepared = default; + return false; + } + + prepared = new PreparedStrokeItem(brush, command.GraphicsOptions, brushBounds, rasterizable); + return true; + } + + private static bool TryResolveRasterization( + Brush brush, + RectangleF bounds, + in RasterizerOptions options, + Point destinationOffset, + in Rectangle targetBounds, + out Brush resolvedBrush, + out RasterizerOptions resolvedOptions, + out Rectangle brushBounds) + { + resolvedBrush = brush; + + if (options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter) + { + bounds = new RectangleF(bounds.X + 0.5F, bounds.Y + 0.5F, bounds.Width, bounds.Height); + } + + Rectangle localInterest = Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right) + 1, + (int)MathF.Ceiling(bounds.Bottom) + 1); + + Rectangle absoluteInterest = new( + localInterest.X + destinationOffset.X, + localInterest.Y + destinationOffset.Y, + localInterest.Width, + localInterest.Height); + + Rectangle clippedDestination = Rectangle.Intersect(targetBounds, absoluteInterest); + if (clippedDestination.Width <= 0 || clippedDestination.Height <= 0) + { + resolvedOptions = default; + brushBounds = default; + return false; + } + + resolvedOptions = new RasterizerOptions( + absoluteInterest, + options.IntersectionRule, + options.RasterizationMode, + options.SamplingOrigin, + options.AntialiasThreshold); + + brushBounds = absoluteInterest; + return true; + } + + private static RectangleF GetStrokeBounds(RectangleF bounds, Pen pen, float widthScale) + { + float halfWidth = pen.StrokeWidth * widthScale * 0.5F; + float joinInflate = pen.StrokeOptions.LineJoin switch + { + LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound => (float)(halfWidth * Math.Max(pen.StrokeOptions.MiterLimit, 1D)), + _ => halfWidth + }; + + float capInflate = pen.StrokeOptions.LineCap == LineCap.Square + ? halfWidth * MathF.Sqrt(2F) + : halfWidth; + + float inflate = MathF.Max(joinInflate, capInflate); + + bounds.Inflate(new SizeF(inflate, inflate)); + return bounds; + } + + /// + /// Returns the isotropic scale factor embedded in a drawing transform so stroke widths match device-space pixels. + /// + /// + /// Uses the square root of the absolute 2D determinant, the SVG-style fallback for non-uniform + /// scale. Reduces to the uniform scale for pure scale/rotate/translate matrices. + /// + private static float GetTransformWidthScale(Matrix4x4 transform) + { + if (transform.IsIdentity) + { + return 1F; + } + + float det = (transform.M11 * transform.M22) - (transform.M12 * transform.M21); + return MathF.Sqrt(MathF.Abs(det)); + } + + private static Vector2 ExtractScale(Matrix4x4 matrix) + => new( + MathF.Sqrt((matrix.M11 * matrix.M11) + (matrix.M12 * matrix.M12)), + MathF.Sqrt((matrix.M21 * matrix.M21) + (matrix.M22 * matrix.M22))); + + private static Matrix4x4 ComputeResidual(Vector2 scale, Matrix4x4 matrix) + => Matrix4x4.CreateScale(1F / scale.X, 1F / scale.Y, 1F) * matrix; + + private readonly struct PreparedFillItem + { + public PreparedFillItem( + Brush brush, + GraphicsOptions graphicsOptions, + Rectangle brushBounds, + DefaultRasterizer.RasterizableGeometry rasterizable) + { + this.Brush = brush; + this.GraphicsOptions = graphicsOptions; + this.BrushBounds = brushBounds; + this.Rasterizable = rasterizable; + } + + public Brush Brush { get; } + + public GraphicsOptions GraphicsOptions { get; } + + public Rectangle BrushBounds { get; } + + public DefaultRasterizer.RasterizableGeometry Rasterizable { get; } + } + + private readonly struct PreparedStrokeItem + { + public PreparedStrokeItem( + Brush brush, + GraphicsOptions graphicsOptions, + Rectangle brushBounds, + DefaultRasterizer.StrokeRasterizableGeometry rasterizable) + { + this.Brush = brush; + this.GraphicsOptions = graphicsOptions; + this.BrushBounds = brushBounds; + this.Rasterizable = rasterizable; + } + + public Brush Brush { get; } + + public GraphicsOptions GraphicsOptions { get; } + + public Rectangle BrushBounds { get; } + + public DefaultRasterizer.StrokeRasterizableGeometry Rasterizable { get; } + } + + private readonly struct PartitionState + { + public PartitionState( + int fillItemCount, + int strokeItemCount, + long totalEdgeCount, + int singleBandItemCount, + int smallEdgeItemCount, + int layerDepthDelta, + int maxLayerDepth, + RowBuilder[] rowBuilders) + { + this.FillItemCount = fillItemCount; + this.StrokeItemCount = strokeItemCount; + this.TotalEdgeCount = totalEdgeCount; + this.SingleBandItemCount = singleBandItemCount; + this.SmallEdgeItemCount = smallEdgeItemCount; + this.LayerDepthDelta = layerDepthDelta; + this.MaxLayerDepth = maxLayerDepth; + this.RowBuilders = rowBuilders; + } + + public int FillItemCount { get; } + + public int StrokeItemCount { get; } + + public long TotalEdgeCount { get; } + + public int SingleBandItemCount { get; } + + public int SmallEdgeItemCount { get; } + + public int LayerDepthDelta { get; } + + public int MaxLayerDepth { get; } + + public RowBuilder[] RowBuilders { get; } + } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/ICanvasFrame{TPixel}.cs b/ImageSharp.Drawing/Processing/Backends/ICanvasFrame{TPixel}.cs new file mode 100644 index 0000000..e488a49 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/ICanvasFrame{TPixel}.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// Per-frame destination for . + /// + /// The pixel format. + public interface ICanvasFrame + where TPixel : unmanaged, IPixel + { + /// + /// Gets the frame bounds in root target coordinates. + /// + public Rectangle Bounds { get; } + + /// + /// Attempts to get a CPU-accessible destination region. + /// + /// The CPU region when available. + /// when a CPU region is available. + public bool TryGetCpuRegion(out Buffer2DRegion region); + + /// + /// Attempts to get an opaque native destination surface. + /// + /// The native surface when available. + /// when a native surface is available. + public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface); + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs new file mode 100644 index 0000000..37f5c14 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// Defines the contract for creating and rendering retained drawing scenes for canvas targets. + /// + public interface IDrawingBackend + { + /// + /// Creates a retained backend scene from a prepared command batch. + /// + /// The active processing configuration. + /// The target bounds used for target-dependent scene data. + /// The scene commands in submission order. + /// The resources that must stay alive for the returned scene. + /// A retained backend scene. + public DrawingBackendScene CreateScene( + Configuration configuration, + Rectangle targetBounds, + DrawingCommandBatch commandBatch, + IReadOnlyList? ownedResources = null); + + /// + /// Renders a retained backend scene into the target. + /// + /// The pixel format. + /// The active processing configuration. + /// The target frame. + /// The retained backend scene to render. + public void RenderScene( + Configuration configuration, + ICanvasFrame target, + DrawingBackendScene scene) + where TPixel : unmanaged, IPixel; + + /// + /// Reads source pixels from the target into the destination region. + /// + /// The pixel format. + /// The active processing configuration. + /// The target frame. + /// The source rectangle in target-local coordinates. + /// The destination region that receives the copied pixels. + public void ReadRegion( + Configuration configuration, + ICanvasFrame target, + Rectangle sourceRectangle, + Buffer2DRegion destination) + where TPixel : unmanaged, IPixel; + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/IRasterizerCoverageRowHandler.cs b/ImageSharp.Drawing/Processing/Backends/IRasterizerCoverageRowHandler.cs new file mode 100644 index 0000000..62fe242 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/IRasterizerCoverageRowHandler.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// Receives one emitted non-zero coverage span from the rasterizer. + /// + internal interface IRasterizerCoverageRowHandler + { + /// + /// Handles one emitted non-zero coverage span. + /// + /// The destination y coordinate. + /// The first x coordinate represented by . + /// Non-zero coverage values starting at . + public void Handle(int y, int startX, Span coverage); + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/MemoryCanvasFrame{TPixel}.cs b/ImageSharp.Drawing/Processing/Backends/MemoryCanvasFrame{TPixel}.cs new file mode 100644 index 0000000..5794380 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/MemoryCanvasFrame{TPixel}.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// Canvas frame backed by a . + /// + /// The pixel format. + public sealed class MemoryCanvasFrame : ICanvasFrame + where TPixel : unmanaged, IPixel + { + private readonly Buffer2DRegion region; + + /// + /// Initializes a new instance of the class. + /// + /// The pixel buffer region backing this frame. + public MemoryCanvasFrame(Buffer2DRegion region) + { + Guard.NotNull(region.Buffer, nameof(region)); + this.region = region; + } + + /// + public Rectangle Bounds => this.region.Bounds; + + /// + public bool TryGetCpuRegion(out Buffer2DRegion region) + { + region = this.region; + return true; + } + + /// + public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface) + { + surface = null; + return false; + } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/NativeCanvasFrame{TPixel}.cs b/ImageSharp.Drawing/Processing/Backends/NativeCanvasFrame{TPixel}.cs new file mode 100644 index 0000000..5ba6dfe --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/NativeCanvasFrame{TPixel}.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// Canvas frame backed by a . + /// + /// The pixel format. + public sealed class NativeCanvasFrame : ICanvasFrame + where TPixel : unmanaged, IPixel + { + private readonly NativeSurface surface; + + /// + /// Initializes a new instance of the class. + /// + /// The frame bounds. + /// The native surface backing this frame. + public NativeCanvasFrame(Rectangle bounds, NativeSurface surface) + { + Guard.NotNull(surface, nameof(surface)); + Guard.MustBeGreaterThan(bounds.Width, 0, nameof(bounds)); + Guard.MustBeGreaterThan(bounds.Height, 0, nameof(bounds)); + + this.Bounds = bounds; + this.surface = surface; + } + + /// + public Rectangle Bounds { get; } + + /// + public bool TryGetCpuRegion(out Buffer2DRegion region) + { + region = default; + return false; + } + + /// + public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface) + { + surface = this.surface; + return true; + } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/NativeSurface.cs b/ImageSharp.Drawing/Processing/Backends/NativeSurface.cs new file mode 100644 index 0000000..62ab3c6 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/NativeSurface.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// Base type for backend-specific native drawing targets. + /// + public abstract class NativeSurface + { + /// + /// Initializes a new instance of the class. + /// + protected NativeSurface() + { + } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/ParallelExecutionHelper.cs b/ImageSharp.Drawing/Processing/Backends/ParallelExecutionHelper.cs new file mode 100644 index 0000000..d6a31bd --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/ParallelExecutionHelper.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Threading.Tasks; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// Centralizes the conversion from configuration parallelism settings to partition counts and + /// instances used by retained-scene CPU execution paths. + /// + internal static class ParallelExecutionHelper + { + /// + /// Computes the number of partitions to schedule for work constrained by a single work-item limit. + /// + /// + /// The configured maximum degree of parallelism. A value of -1 leaves the runtime + /// parallelism cap unbounded, but partition planning remains capped to + /// to avoid excessive fan-out. + /// + /// The total number of work items available for partitioning. + /// The number of partitions to schedule. + public static int GetPartitionCount(int maxDegreeOfParallelism, int workItemCount) + => Math.Min(GetPartitionLimit(maxDegreeOfParallelism), workItemCount); + + /// + /// Computes the number of partitions to schedule for work constrained by two independent limits. + /// + /// + /// The configured maximum degree of parallelism. A value of -1 leaves the runtime + /// parallelism cap unbounded, but partition planning remains capped to + /// to avoid excessive fan-out. + /// + /// The total number of work items available for partitioning. + /// An additional caller-specific upper bound on useful partitions. + /// The number of partitions to schedule. + public static int GetPartitionCount(int maxDegreeOfParallelism, int workItemCount, int secondaryLimit) + => Math.Min(GetPartitionLimit(maxDegreeOfParallelism), Math.Min(workItemCount, secondaryLimit)); + + /// + /// Creates the for a partitioned operation. + /// + /// + /// The configured maximum degree of parallelism. A value of -1 retains the runtime's + /// unbounded sentinel because is always positive; positive + /// values are capped to the smaller of the configured limit and the useful partition count. + /// + /// The computed positive number of useful partitions for the operation. + /// The instance for the operation. + public static ParallelOptions CreateParallelOptions(int maxDegreeOfParallelism, int partitionCount) + => new() { MaxDegreeOfParallelism = Math.Min(maxDegreeOfParallelism, partitionCount) }; + + /// + /// Computes the internal partition-planning cap for the configured parallelism setting. + /// + /// + /// The configured maximum degree of parallelism. A value of -1 keeps the runtime + /// parallelism setting unbounded, but partition planning is capped to + /// . + /// + /// The maximum number of partitions to plan for. + private static int GetPartitionLimit(int maxDegreeOfParallelism) + => maxDegreeOfParallelism == -1 ? Environment.ProcessorCount : maxDegreeOfParallelism; + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs b/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs new file mode 100644 index 0000000..ff020ab --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs @@ -0,0 +1,98 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// Describes whether rasterizers should emit continuous coverage or binary aliased coverage. + /// + public enum RasterizationMode + { + /// + /// Emit continuous coverage in the range [0, 1]. + /// + Antialiased = 0, + + /// + /// Emit binary coverage values (0 or 1). + /// + Aliased = 1 + } + + /// + /// Describes where sample coverage is aligned relative to destination pixels. + /// + public enum RasterizerSamplingOrigin + { + /// + /// Samples are aligned to pixel boundaries. + /// + PixelBoundary = 0, + + /// + /// Samples are aligned to pixel centers. + /// + PixelCenter = 1 + } + + /// + /// Immutable options used by rasterizers when scan-converting vector geometry. + /// + public readonly struct RasterizerOptions + { + /// + /// Initializes a new instance of the struct. + /// + /// Destination bounds to rasterize into. + /// Polygon intersection rule. + /// Rasterization coverage mode. + /// Sampling origin alignment. + /// Coverage threshold for aliased mode (0 to 1). + public RasterizerOptions( + Rectangle interest, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + RasterizerSamplingOrigin samplingOrigin, + float antialiasThreshold) + { + this.Interest = interest; + this.IntersectionRule = intersectionRule; + this.RasterizationMode = rasterizationMode; + this.SamplingOrigin = samplingOrigin; + this.AntialiasThreshold = antialiasThreshold; + } + + /// + /// Gets destination bounds to rasterize into. + /// + public Rectangle Interest { get; } + + /// + /// Gets the polygon intersection rule. + /// + public IntersectionRule IntersectionRule { get; } + + /// + /// Gets the rasterization coverage mode. + /// + public RasterizationMode RasterizationMode { get; } + + /// + /// Gets the sampling origin alignment. + /// + public RasterizerSamplingOrigin SamplingOrigin { get; } + + /// + /// Gets the coverage threshold used when is . + /// Pixels with coverage above this value are rendered as fully opaque; pixels below are discarded. + /// + public float AntialiasThreshold { get; } + + /// + /// Creates a copy of the current options with a different interest rectangle. + /// + /// The replacement interest rectangle. + /// A new value. + public RasterizerOptions WithInterest(Rectangle interest) + => new(interest, this.IntersectionRule, this.RasterizationMode, this.SamplingOrigin, this.AntialiasThreshold); + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/StrokeLineSegmentCommand.cs b/ImageSharp.Drawing/Processing/Backends/StrokeLineSegmentCommand.cs new file mode 100644 index 0000000..cd7630d --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/StrokeLineSegmentCommand.cs @@ -0,0 +1,136 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// One explicit stroked two-point line-segment command queued by the canvas batcher. + /// + public readonly struct StrokeLineSegmentCommand + { + private readonly PointF sourceStart; + private readonly PointF sourceEnd; + private readonly DrawingOptions drawingOptions; + + /// + /// Initializes a new instance of the struct. + /// + /// The source line start point. + /// The source line end point. + /// The brush used to shade the stroke. + /// The drawing options (graphics, shape, transform) used during composition. + /// The rasterizer options used to generate coverage. + /// The absolute bounds of the logical target. + /// The absolute destination offset of the command. + /// The stroke metadata. + /// True if the command was recorded inside a layer. + public StrokeLineSegmentCommand( + PointF sourceStart, + PointF sourceEnd, + Brush brush, + DrawingOptions drawingOptions, + in RasterizerOptions rasterizerOptions, + Rectangle targetBounds, + Point destinationOffset, + Pen pen, + bool isInsideLayer) + { + this.sourceStart = sourceStart; + this.sourceEnd = sourceEnd; + this.drawingOptions = drawingOptions; + this.Brush = brush; + this.RasterizerOptions = rasterizerOptions; + this.TargetBounds = targetBounds; + this.DestinationOffset = destinationOffset; + this.Pen = pen; + this.IsInsideLayer = isInsideLayer; + } + + /// + /// Gets the brush used during composition. + /// + public Brush Brush { get; } + + /// + /// Gets the drawing options carried by the command. + /// + public DrawingOptions DrawingOptions => this.drawingOptions; + + /// + /// Gets the graphics options used during composition. + /// + public GraphicsOptions GraphicsOptions => this.drawingOptions.GraphicsOptions; + + /// + /// Gets the rasterizer options used to generate coverage. + /// + public RasterizerOptions RasterizerOptions { get; } + + /// + /// Gets the absolute bounds of the logical target for this command. + /// + public Rectangle TargetBounds { get; } + + /// + /// Gets the absolute destination offset where the local coverage should be composited. + /// + public Point DestinationOffset { get; } + + /// + /// Gets the stroke metadata for this command. + /// + public Pen Pen { get; } + + /// + /// Gets the source line start point. + /// + public PointF SourceStart => this.sourceStart; + + /// + /// Gets the source line end point. + /// + public PointF SourceEnd => this.sourceEnd; + + /// + /// Gets the command transform. + /// + public Matrix4x4 Transform => this.drawingOptions.Transform; + + /// + /// Gets a value indicating whether the command was recorded inside a layer. + /// + public bool IsInsideLayer { get; } + + /// + /// Computes the conservative stroked bounds of one two-point line segment. + /// + /// The line start point. + /// The line end point. + /// The stroke metadata. + /// The conservative stroked bounds. + public static RectangleF GetConservativeBounds(PointF start, PointF end, Pen pen) + { + float left = MathF.Min(start.X, end.X); + float top = MathF.Min(start.Y, end.Y); + float right = MathF.Max(start.X, end.X); + float bottom = MathF.Max(start.Y, end.Y); + RectangleF bounds = RectangleF.FromLTRB(left, top, right, bottom); + return InflateBounds(bounds, pen); + } + + private static RectangleF InflateBounds(RectangleF bounds, Pen pen) + { + float halfWidth = pen.StrokeWidth * 0.5F; + float inflate = pen.StrokeOptions.LineJoin switch + { + LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound => (float)(halfWidth * Math.Max(pen.StrokeOptions.MiterLimit, 1D)), + _ => halfWidth + }; + + bounds.Inflate(new SizeF(inflate, inflate)); + return bounds; + } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/StrokePathCommand.cs b/ImageSharp.Drawing/Processing/Backends/StrokePathCommand.cs new file mode 100644 index 0000000..11fcf81 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/StrokePathCommand.cs @@ -0,0 +1,111 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// One stroked path command queued by the canvas batcher. + /// + public readonly struct StrokePathCommand + { + private readonly IPath sourcePath; + private readonly DrawingOptions drawingOptions; + private readonly IReadOnlyList? clipPaths; + + /// + /// Initializes a new instance of the struct. + /// + /// The source stroke path. + /// The brush used to shade the stroke. + /// The drawing options (graphics, shape, transform) used during composition. + /// The rasterizer options used to generate coverage. + /// The absolute bounds of the logical target. + /// The absolute destination offset of the command. + /// The stroke metadata. + /// Optional clip paths supplied with the command. + /// True if the command was recorded inside a layer. + public StrokePathCommand( + IPath sourcePath, + Brush brush, + DrawingOptions drawingOptions, + in RasterizerOptions rasterizerOptions, + Rectangle targetBounds, + Point destinationOffset, + Pen pen, + IReadOnlyList? clipPaths, + bool isInsideLayer) + { + this.sourcePath = sourcePath; + this.drawingOptions = drawingOptions; + this.clipPaths = clipPaths; + this.Brush = brush; + this.RasterizerOptions = rasterizerOptions; + this.TargetBounds = targetBounds; + this.DestinationOffset = destinationOffset; + this.Pen = pen; + this.IsInsideLayer = isInsideLayer; + } + + /// + /// Gets the brush used during composition. + /// + public Brush Brush { get; } + + /// + /// Gets the drawing options carried by the command. + /// + public DrawingOptions DrawingOptions => this.drawingOptions; + + /// + /// Gets the graphics options used during composition. + /// + public GraphicsOptions GraphicsOptions => this.drawingOptions.GraphicsOptions; + + /// + /// Gets the rasterizer options used to generate coverage. + /// + public RasterizerOptions RasterizerOptions { get; } + + /// + /// Gets the absolute bounds of the logical target for this command. + /// + public Rectangle TargetBounds { get; } + + /// + /// Gets the absolute destination offset where the local coverage should be composited. + /// + public Point DestinationOffset { get; } + + /// + /// Gets the stroke metadata for this command. + /// + public Pen Pen { get; } + + /// + /// Gets the source stroke path. + /// + public IPath SourcePath => this.sourcePath; + + /// + /// Gets the drawing transform. + /// + public Matrix4x4 Transform => this.drawingOptions.Transform; + + /// + /// Gets the optional clip paths carried by the command. + /// + public IReadOnlyList? ClipPaths => this.clipPaths; + + /// + /// Gets the shape options carried by the command. + /// + public ShapeOptions ShapeOptions => this.drawingOptions.ShapeOptions; + + /// + /// Gets a value indicating whether the command was recorded inside a layer. + /// + public bool IsInsideLayer { get; } + } +} diff --git a/ImageSharp.Drawing/Processing/Backends/StrokePolylineCommand.cs b/ImageSharp.Drawing/Processing/Backends/StrokePolylineCommand.cs new file mode 100644 index 0000000..206fe28 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Backends/StrokePolylineCommand.cs @@ -0,0 +1,148 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends { + /// + /// One explicit stroked open polyline command queued by the canvas batcher. + /// + public readonly struct StrokePolylineCommand + { + private readonly PointF[] sourcePoints; + private readonly DrawingOptions drawingOptions; + + /// + /// Initializes a new instance of the struct. + /// + /// The source polyline points. + /// The brush used to shade the stroke. + /// The drawing options (graphics, shape, transform) used during composition. + /// The rasterizer options used to generate coverage. + /// The absolute bounds of the logical target. + /// The absolute destination offset of the command. + /// The stroke metadata. + /// True if the command was recorded inside a layer. + public StrokePolylineCommand( + PointF[] sourcePoints, + Brush brush, + DrawingOptions drawingOptions, + in RasterizerOptions rasterizerOptions, + Rectangle targetBounds, + Point destinationOffset, + Pen pen, + bool isInsideLayer) + { + ArgumentNullException.ThrowIfNull(sourcePoints); + if (sourcePoints.Length < 2) + { + throw new ArgumentOutOfRangeException(nameof(sourcePoints), "Open stroke polylines require at least two points."); + } + + this.sourcePoints = sourcePoints; + this.drawingOptions = drawingOptions; + this.Brush = brush; + this.RasterizerOptions = rasterizerOptions; + this.TargetBounds = targetBounds; + this.DestinationOffset = destinationOffset; + this.Pen = pen; + this.IsInsideLayer = isInsideLayer; + } + + /// + /// Gets the brush used during composition. + /// + public Brush Brush { get; } + + /// + /// Gets the drawing options carried by the command. + /// + public DrawingOptions DrawingOptions => this.drawingOptions; + + /// + /// Gets the graphics options used during composition. + /// + public GraphicsOptions GraphicsOptions => this.drawingOptions.GraphicsOptions; + + /// + /// Gets the rasterizer options used to generate coverage. + /// + public RasterizerOptions RasterizerOptions { get; } + + /// + /// Gets the absolute bounds of the logical target for this command. + /// + public Rectangle TargetBounds { get; } + + /// + /// Gets the absolute destination offset where the local coverage should be composited. + /// + public Point DestinationOffset { get; } + + /// + /// Gets the stroke metadata for this command. + /// + public Pen Pen { get; } + + /// + /// Gets the source polyline points. + /// + public PointF[] SourcePoints => this.sourcePoints; + + /// + /// Gets the command transform. + /// + public Matrix4x4 Transform => this.drawingOptions.Transform; + + /// + /// Gets a value indicating whether the command was recorded inside a layer. + /// + public bool IsInsideLayer { get; } + + /// + /// Computes the conservative stroked bounds of one open polyline. + /// + /// The polyline points. + /// The stroke metadata. + /// The conservative stroked bounds. + public static RectangleF GetConservativeBounds(PointF[] points, Pen pen) + { + ArgumentNullException.ThrowIfNull(points); + if (points.Length == 0) + { + return RectangleF.Empty; + } + + float minX = points[0].X; + float minY = points[0].Y; + float maxX = minX; + float maxY = minY; + + for (int i = 1; i < points.Length; i++) + { + PointF point = points[i]; + minX = MathF.Min(minX, point.X); + minY = MathF.Min(minY, point.Y); + maxX = MathF.Max(maxX, point.X); + maxY = MathF.Max(maxY, point.Y); + } + + RectangleF bounds = RectangleF.FromLTRB(minX, minY, maxX, maxY); + return InflateBounds(bounds, pen); + } + + private static RectangleF InflateBounds(RectangleF bounds, Pen pen) + { + float halfWidth = pen.StrokeWidth * 0.5F; + float inflate = pen.StrokeOptions.LineJoin switch + { + LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound => (float)(halfWidth * Math.Max(pen.StrokeOptions.MiterLimit, 1D)), + _ => halfWidth + }; + + bounds.Inflate(new SizeF(inflate, inflate)); + return bounds; + } + } +} diff --git a/ImageSharp.Drawing/Processing/Brush.cs b/ImageSharp.Drawing/Processing/Brush.cs new file mode 100644 index 0000000..5de6788 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Brush.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Represents a logical configuration of a brush which can be used to source pixel colors. + /// + /// + /// A brush creates a that performs the logic for retrieving + /// pixel values for specific locations. + /// + public abstract class Brush : IEquatable + { + /// + /// Creates the prepared execution object for this brush. + /// + /// The pixel type. + /// The configuration instance to use when performing operations. + /// The graphic options. + /// The canvas width for the current render pass. + /// The region the brush will be applied to. + /// + /// The for this brush. + /// + /// + /// The when being applied to things like shapes would usually be the + /// bounding box of the shape not necessarily the bounds of the whole image. + /// + public abstract BrushRenderer CreateRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + RectangleF region) + where TPixel : unmanaged, IPixel; + + /// + /// Returns a new brush with its defining geometry transformed by the given matrix. + /// + /// The transformation matrix to apply. + /// A transformed brush, or this if the brush has no spatial parameters. + public virtual Brush Transform(Matrix4x4 matrix) => this; + + /// + public abstract bool Equals(Brush? other); + + /// + public override bool Equals(object? obj) => this.Equals(obj as Brush); + + /// + public abstract override int GetHashCode(); + } +} diff --git a/ImageSharp.Drawing/Processing/BrushRenderer.cs b/ImageSharp.Drawing/Processing/BrushRenderer.cs new file mode 100644 index 0000000..df9de99 --- /dev/null +++ b/ImageSharp.Drawing/Processing/BrushRenderer.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Renders a against individual coverage scanlines. + /// + /// The pixel format. + public abstract class BrushRenderer + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration instance to use when performing operations. + /// The graphics options. + /// The canvas width for the current render pass. + protected BrushRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth) + { + this.Configuration = configuration; + this.Options = options; + this.CanvasWidth = canvasWidth; + this.Blender = PixelOperations.Instance.GetPixelBlender(options); + } + + /// + /// Gets the configuration instance to use when performing operations. + /// + protected Configuration Configuration { get; } + + /// + /// Gets the pixel blender. + /// + internal PixelBlender Blender { get; } + + /// + /// Gets the graphics options. + /// + protected GraphicsOptions Options { get; } + + /// + /// Gets the canvas width for the current render pass. + /// + protected int CanvasWidth { get; } + + /// + /// Applies the opacity weighting for each pixel in a scanline to the target based on the + /// pattern contained in the brush. + /// + /// The destination row slice to shade. + /// The coverage values for the current destination scanline. + /// The x-position in the target pixel space that the start of the scanline data corresponds to. + /// The y-position in the target pixel space that the scanline corresponds to. + /// The worker-local scratch workspace for temporary blending buffers. + public abstract void Apply( + Span destinationRow, + ReadOnlySpan scanline, + int x, + int y, + BrushWorkspace workspace); + } +} diff --git a/ImageSharp.Drawing/Processing/BrushWorkspace.cs b/ImageSharp.Drawing/Processing/BrushWorkspace.cs new file mode 100644 index 0000000..054c285 --- /dev/null +++ b/ImageSharp.Drawing/Processing/BrushWorkspace.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Worker-local scratch workspace used by prepared brushes during row composition. + /// + /// The target pixel format. + public sealed class BrushWorkspace : IDisposable + where TPixel : unmanaged, IPixel + { + private readonly IMemoryOwner amountsOwner; + private readonly IMemoryOwner overlaysOwner; + private readonly IMemoryOwner blendScratchOwner; + + internal BrushWorkspace(MemoryAllocator allocator, int rowWidth) + { + int capacity = Math.Max(1, rowWidth); + this.amountsOwner = allocator.Allocate(capacity); + this.overlaysOwner = allocator.Allocate(capacity); + this.blendScratchOwner = allocator.Allocate(capacity * 3); + } + + /// + /// Gets the shared amount buffer for the requested length. + /// + /// The number of elements required. + /// A slice of the worker-local pooled amount buffer. + public Span GetAmounts(int length) + { + ArgumentOutOfRangeException.ThrowIfNegative(length); + return this.amountsOwner.Memory.Span[..length]; + } + + /// + /// Gets the shared overlay buffer for the requested length. + /// + /// The number of elements required. + /// A slice of the worker-local pooled overlay buffer. + public Span GetOverlays(int length) + { + ArgumentOutOfRangeException.ThrowIfNegative(length); + return this.overlaysOwner.Memory.Span[..length]; + } + + /// + /// Gets the shared vector scratch for the requested row length and vector row count. + /// + /// The number of pixels in the row. + /// The number of temporary vector rows required. + /// A slice of the worker-local pooled vector scratch buffer. + public Span GetBlendScratch(int length, int vectorRows) + { + ArgumentOutOfRangeException.ThrowIfNegative(length); + ArgumentOutOfRangeException.ThrowIfLessThan(vectorRows, 1); + return this.blendScratchOwner.Memory.Span[..(length * vectorRows)]; + } + + /// + public void Dispose() + { + this.amountsOwner.Dispose(); + this.overlaysOwner.Dispose(); + this.blendScratchOwner.Dispose(); + } + } +} diff --git a/ImageSharp.Drawing/Processing/Brushes.Hatch.cs b/ImageSharp.Drawing/Processing/Brushes.Hatch.cs new file mode 100644 index 0000000..f57daca --- /dev/null +++ b/ImageSharp.Drawing/Processing/Brushes.Hatch.cs @@ -0,0 +1,648 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Provides additional hatch pattern brush factories. + /// + public static partial class Brushes + { + // These hatch arrays were derived using the GDI+ pixel extraction technique described at + // https://web.archive.org/web/20221228174326/https://www.codeproject.com/Articles/5350583/Recreating-Gdiplus-hatches-with-SkiaSharp. + private static readonly bool[,] HorizontalPattern = + { + { true, true, true, true, true, true, true, true, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + }; + + private static readonly bool[,] VerticalPattern = + { + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + }; + + private static readonly bool[,] ForwardDiagonalPattern = + { + { true, false, false, false, false, false, false, false, }, + { false, true, false, false, false, false, false, false, }, + { false, false, true, false, false, false, false, false, }, + { false, false, false, true, false, false, false, false, }, + { false, false, false, false, true, false, false, false, }, + { false, false, false, false, false, true, false, false, }, + { false, false, false, false, false, false, true, false, }, + { false, false, false, false, false, false, false, true, }, + }; + + private static readonly bool[,] BackwardDiagonalPattern = + { + { false, false, false, false, false, false, false, true, }, + { false, false, false, false, false, false, true, false, }, + { false, false, false, false, false, true, false, false, }, + { false, false, false, false, true, false, false, false, }, + { false, false, false, true, false, false, false, false, }, + { false, false, true, false, false, false, false, false, }, + { false, true, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + }; + + private static readonly bool[,] CrossPattern = + { + { true, true, true, true, true, true, true, true, }, + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + }; + + private static readonly bool[,] DiagonalCrossPattern = + { + { true, false, false, false, false, false, false, true, }, + { false, true, false, false, false, false, true, false, }, + { false, false, true, false, false, true, false, false, }, + { false, false, false, true, true, false, false, false, }, + { false, false, false, true, true, false, false, false, }, + { false, false, true, false, false, true, false, false, }, + { false, true, false, false, false, false, true, false, }, + { true, false, false, false, false, false, false, true, }, + }; + + private static readonly bool[,] Percent05Pattern = + { + { true, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, true, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + }; + + private static readonly bool[,] Percent10Pattern = + { + { true, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, true, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, true, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + }; + + private static readonly bool[,] Percent20Pattern = + { + { true, false, false, false, true, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, true, false, false, false, true, false, }, + { false, false, false, false, false, false, false, false, }, + { true, false, false, false, true, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, true, false, false, false, true, false, }, + { false, false, false, false, false, false, false, false, }, + }; + + private static readonly bool[,] Percent25Pattern = + { + { true, false, false, false, true, false, false, false, }, + { false, false, true, false, false, false, true, false, }, + { true, false, false, false, true, false, false, false, }, + { false, false, true, false, false, false, true, false, }, + { true, false, false, false, true, false, false, false, }, + { false, false, true, false, false, false, true, false, }, + { true, false, false, false, true, false, false, false, }, + { false, false, true, false, false, false, true, false, }, + }; + + private static readonly bool[,] Percent30Pattern = + { + { true, false, true, false, true, false, true, false, }, + { false, true, false, false, false, true, false, false, }, + { true, false, true, false, true, false, true, false, }, + { false, false, false, true, false, false, false, true, }, + { true, false, true, false, true, false, true, false, }, + { false, true, false, false, false, true, false, false, }, + { true, false, true, false, true, false, true, false, }, + { false, false, false, true, false, false, false, true, }, + }; + + private static readonly bool[,] Percent40Pattern = + { + { true, false, true, false, true, false, true, false, }, + { false, true, false, true, false, true, false, true, }, + { true, false, true, false, true, false, true, false, }, + { false, true, false, true, false, false, false, true, }, + { true, false, true, false, true, false, true, false, }, + { false, true, false, true, false, true, false, true, }, + { true, false, true, false, true, false, true, false, }, + { false, false, false, true, false, true, false, true, }, + }; + + private static readonly bool[,] Percent50Pattern = + { + { true, false, true, false, true, false, true, false, }, + { false, true, false, true, false, true, false, true, }, + { true, false, true, false, true, false, true, false, }, + { false, true, false, true, false, true, false, true, }, + { true, false, true, false, true, false, true, false, }, + { false, true, false, true, false, true, false, true, }, + { true, false, true, false, true, false, true, false, }, + { false, true, false, true, false, true, false, true, }, + }; + + private static readonly bool[,] Percent60Pattern = + { + { true, true, true, false, true, true, true, false, }, + { false, true, false, true, false, true, false, true, }, + { true, false, true, true, true, false, true, true, }, + { false, true, false, true, false, true, false, true, }, + { true, true, true, false, true, true, true, false, }, + { false, true, false, true, false, true, false, true, }, + { true, false, true, true, true, false, true, true, }, + { false, true, false, true, false, true, false, true, }, + }; + + private static readonly bool[,] Percent70Pattern = + { + { false, true, true, true, false, true, true, true, }, + { true, true, false, true, true, true, false, true, }, + { false, true, true, true, false, true, true, true, }, + { true, true, false, true, true, true, false, true, }, + { false, true, true, true, false, true, true, true, }, + { true, true, false, true, true, true, false, true, }, + { false, true, true, true, false, true, true, true, }, + { true, true, false, true, true, true, false, true, }, + }; + + private static readonly bool[,] Percent75Pattern = + { + { false, true, true, true, false, true, true, true, }, + { true, true, true, true, true, true, true, true, }, + { true, true, false, true, true, true, false, true, }, + { true, true, true, true, true, true, true, true, }, + { false, true, true, true, false, true, true, true, }, + { true, true, true, true, true, true, true, true, }, + { true, true, false, true, true, true, false, true, }, + { true, true, true, true, true, true, true, true, }, + }; + + private static readonly bool[,] Percent80Pattern = + { + { true, true, true, false, true, true, true, true, }, + { true, true, true, true, true, true, true, true, }, + { true, true, true, true, true, true, true, false, }, + { true, true, true, true, true, true, true, true, }, + { true, true, true, false, true, true, true, true, }, + { true, true, true, true, true, true, true, true, }, + { true, true, true, true, true, true, true, false, }, + { true, true, true, true, true, true, true, true, }, + }; + + private static readonly bool[,] Percent90Pattern = + { + { true, true, true, true, true, true, true, true, }, + { true, true, true, true, true, true, true, true, }, + { true, true, true, true, true, true, true, true, }, + { true, true, true, true, false, true, true, true, }, + { true, true, true, true, true, true, true, true, }, + { true, true, true, true, true, true, true, true, }, + { true, true, true, true, true, true, true, true, }, + { false, true, true, true, true, true, true, true, }, + }; + + private static readonly bool[,] LightDownwardDiagonalPattern = + { + { true, false, false, false, true, false, false, false, }, + { false, true, false, false, false, true, false, false, }, + { false, false, true, false, false, false, true, false, }, + { false, false, false, true, false, false, false, true, }, + { true, false, false, false, true, false, false, false, }, + { false, true, false, false, false, true, false, false, }, + { false, false, true, false, false, false, true, false, }, + { false, false, false, true, false, false, false, true, }, + }; + + private static readonly bool[,] LightUpwardDiagonalPattern = + { + { false, false, false, true, false, false, false, true, }, + { false, false, true, false, false, false, true, false, }, + { false, true, false, false, false, true, false, false, }, + { true, false, false, false, true, false, false, false, }, + { false, false, false, true, false, false, false, true, }, + { false, false, true, false, false, false, true, false, }, + { false, true, false, false, false, true, false, false, }, + { true, false, false, false, true, false, false, false, }, + }; + + private static readonly bool[,] DarkDownwardDiagonalPattern = + { + { true, true, false, false, true, true, false, false, }, + { false, true, true, false, false, true, true, false, }, + { false, false, true, true, false, false, true, true, }, + { true, false, false, true, true, false, false, true, }, + { true, true, false, false, true, true, false, false, }, + { false, true, true, false, false, true, true, false, }, + { false, false, true, true, false, false, true, true, }, + { true, false, false, true, true, false, false, true, }, + }; + + private static readonly bool[,] DarkUpwardDiagonalPattern = + { + { false, false, true, true, false, false, true, true, }, + { false, true, true, false, false, true, true, false, }, + { true, true, false, false, true, true, false, false, }, + { true, false, false, true, true, false, false, true, }, + { false, false, true, true, false, false, true, true, }, + { false, true, true, false, false, true, true, false, }, + { true, true, false, false, true, true, false, false, }, + { true, false, false, true, true, false, false, true, }, + }; + + private static readonly bool[,] WideDownwardDiagonalPattern = + { + { true, true, false, false, false, false, false, true, }, + { true, true, true, false, false, false, false, false, }, + { false, true, true, true, false, false, false, false, }, + { false, false, true, true, true, false, false, false, }, + { false, false, false, true, true, true, false, false, }, + { false, false, false, false, true, true, true, false, }, + { false, false, false, false, false, true, true, true, }, + { true, false, false, false, false, false, true, true, }, + }; + + private static readonly bool[,] WideUpwardDiagonalPattern = + { + { true, false, false, false, false, false, true, true, }, + { false, false, false, false, false, true, true, true, }, + { false, false, false, false, true, true, true, false, }, + { false, false, false, true, true, true, false, false, }, + { false, false, true, true, true, false, false, false, }, + { false, true, true, true, false, false, false, false, }, + { true, true, true, false, false, false, false, false, }, + { true, true, false, false, false, false, false, true, }, + }; + + private static readonly bool[,] LightVerticalPattern = + { + { true, false, false, false, true, false, false, false, }, + { true, false, false, false, true, false, false, false, }, + { true, false, false, false, true, false, false, false, }, + { true, false, false, false, true, false, false, false, }, + { true, false, false, false, true, false, false, false, }, + { true, false, false, false, true, false, false, false, }, + { true, false, false, false, true, false, false, false, }, + { true, false, false, false, true, false, false, false, }, + }; + + private static readonly bool[,] LightHorizontalPattern = + { + { true, true, true, true, true, true, true, true, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { true, true, true, true, true, true, true, true, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + }; + + private static readonly bool[,] NarrowVerticalPattern = + { + { false, true, false, true, false, true, false, true, }, + { false, true, false, true, false, true, false, true, }, + { false, true, false, true, false, true, false, true, }, + { false, true, false, true, false, true, false, true, }, + { false, true, false, true, false, true, false, true, }, + { false, true, false, true, false, true, false, true, }, + { false, true, false, true, false, true, false, true, }, + { false, true, false, true, false, true, false, true, }, + }; + + private static readonly bool[,] NarrowHorizontalPattern = + { + { true, true, true, true, true, true, true, true, }, + { false, false, false, false, false, false, false, false, }, + { true, true, true, true, true, true, true, true, }, + { false, false, false, false, false, false, false, false, }, + { true, true, true, true, true, true, true, true, }, + { false, false, false, false, false, false, false, false, }, + { true, true, true, true, true, true, true, true, }, + { false, false, false, false, false, false, false, false, }, + }; + + private static readonly bool[,] DarkVerticalPattern = + { + { true, true, false, false, true, true, false, false, }, + { true, true, false, false, true, true, false, false, }, + { true, true, false, false, true, true, false, false, }, + { true, true, false, false, true, true, false, false, }, + { true, true, false, false, true, true, false, false, }, + { true, true, false, false, true, true, false, false, }, + { true, true, false, false, true, true, false, false, }, + { true, true, false, false, true, true, false, false, }, + }; + + private static readonly bool[,] DarkHorizontalPattern = + { + { true, true, true, true, true, true, true, true, }, + { true, true, true, true, true, true, true, true, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { true, true, true, true, true, true, true, true, }, + { true, true, true, true, true, true, true, true, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + }; + + private static readonly bool[,] DashedDownwardDiagonalPattern = + { + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { true, false, false, false, true, false, false, false, }, + { false, true, false, false, false, true, false, false, }, + { false, false, true, false, false, false, true, false, }, + { false, false, false, true, false, false, false, true, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + }; + + private static readonly bool[,] DashedUpwardDiagonalPattern = + { + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, true, false, false, false, true, }, + { false, false, true, false, false, false, true, false, }, + { false, true, false, false, false, true, false, false, }, + { true, false, false, false, true, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + }; + + private static readonly bool[,] DashedHorizontalPattern = + { + { true, true, true, true, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, true, true, true, true, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + }; + + private static readonly bool[,] DashedVerticalPattern = + { + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { false, false, false, false, true, false, false, false, }, + { false, false, false, false, true, false, false, false, }, + { false, false, false, false, true, false, false, false, }, + { false, false, false, false, true, false, false, false, }, + }; + + private static readonly bool[,] SmallConfettiPattern = + { + { true, false, false, false, false, false, false, false, }, + { false, false, false, false, true, false, false, false, }, + { false, true, false, false, false, false, false, false, }, + { false, false, false, false, false, false, true, false, }, + { false, false, false, true, false, false, false, false, }, + { false, false, false, false, false, false, false, true, }, + { false, false, true, false, false, false, false, false, }, + { false, false, false, false, false, true, false, false, }, + }; + + private static readonly bool[,] LargeConfettiPattern = + { + { true, false, true, true, false, false, false, true, }, + { false, false, true, true, false, false, false, false, }, + { false, false, false, false, false, false, true, true, }, + { false, false, false, true, true, false, true, true, }, + { true, true, false, true, true, false, false, false, }, + { true, true, false, false, false, false, false, false, }, + { false, false, false, false, true, true, false, false, }, + { true, false, false, false, true, true, false, true, }, + }; + + private static readonly bool[,] ZigZagPattern = + { + { true, false, false, false, false, false, false, true, }, + { false, true, false, false, false, false, true, false, }, + { false, false, true, false, false, true, false, false, }, + { false, false, false, true, true, false, false, false, }, + { true, false, false, false, false, false, false, true, }, + { false, true, false, false, false, false, true, false, }, + { false, false, true, false, false, true, false, false, }, + { false, false, false, true, true, false, false, false, }, + }; + + private static readonly bool[,] WavePattern = + { + { false, false, false, false, false, false, false, false, }, + { false, false, false, true, true, false, false, false, }, + { false, false, true, false, false, true, false, true, }, + { true, true, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, true, true, false, false, false, }, + { false, false, true, false, false, true, false, true, }, + { true, true, false, false, false, false, false, false, }, + }; + + private static readonly bool[,] DiagonalBrickPattern = + { + { false, false, false, false, false, false, false, true, }, + { false, false, false, false, false, false, true, false, }, + { false, false, false, false, false, true, false, false, }, + { false, false, false, false, true, false, false, false, }, + { false, false, false, true, true, false, false, false, }, + { false, false, true, false, false, true, false, false, }, + { false, true, false, false, false, false, true, false, }, + { true, false, false, false, false, false, false, true, }, + }; + + private static readonly bool[,] HorizontalBrickPattern = + { + { true, true, true, true, true, true, true, true, }, + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { true, true, true, true, true, true, true, true, }, + { false, false, false, false, true, false, false, false, }, + { false, false, false, false, true, false, false, false, }, + { false, false, false, false, true, false, false, false, }, + }; + + private static readonly bool[,] WeavePattern = + { + { true, false, false, false, true, false, false, false, }, + { false, true, false, true, false, true, false, false, }, + { false, false, true, false, false, false, true, false, }, + { false, true, false, false, false, true, false, true, }, + { true, false, false, false, true, false, false, false, }, + { false, false, false, true, false, true, false, false, }, + { false, false, true, false, false, false, true, false, }, + { false, true, false, true, false, false, false, true, }, + }; + + private static readonly bool[,] PlaidPattern = + { + { true, false, true, false, true, false, true, false, }, + { false, true, false, true, false, true, false, true, }, + { true, false, true, false, true, false, true, false, }, + { false, true, false, true, false, true, false, true, }, + { true, true, true, true, false, false, false, false, }, + { true, true, true, true, false, false, false, false, }, + { true, true, true, true, false, false, false, false, }, + { true, true, true, true, false, false, false, false, }, + }; + + private static readonly bool[,] DivotPattern = + { + { false, false, false, false, false, false, false, false, }, + { false, false, false, true, false, false, false, false, }, + { false, false, false, false, true, false, false, false, }, + { false, false, false, true, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, true, }, + { true, false, false, false, false, false, false, false, }, + }; + + private static readonly bool[,] DottedGridPattern = + { + { true, false, true, false, true, false, true, false, }, + { false, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { true, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + }; + + private static readonly bool[,] DottedDiamondPattern = + { + { true, false, false, false, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, true, false, false, false, true, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, false, false, true, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + { false, false, true, false, false, false, true, false, }, + { false, false, false, false, false, false, false, false, }, + }; + + private static readonly bool[,] ShinglePattern = + { + { false, false, false, false, false, false, true, true, }, + { true, false, false, false, false, true, false, false, }, + { false, true, false, false, true, false, false, false, }, + { false, false, true, true, false, false, false, false, }, + { false, false, false, false, true, true, false, false, }, + { false, false, false, false, false, false, true, false, }, + { false, false, false, false, false, false, false, true, }, + { false, false, false, false, false, false, false, true, }, + }; + + private static readonly bool[,] TrellisPattern = + { + { true, true, true, true, true, true, true, true, }, + { false, true, true, false, false, true, true, false, }, + { true, true, true, true, true, true, true, true, }, + { true, false, false, true, true, false, false, true, }, + { true, true, true, true, true, true, true, true, }, + { false, true, true, false, false, true, true, false, }, + { true, true, true, true, true, true, true, true, }, + { true, false, false, true, true, false, false, true, }, + }; + + private static readonly bool[,] SpherePattern = + { + { false, true, true, true, false, true, true, true, }, + { true, false, false, false, true, false, false, true, }, + { true, false, false, false, true, true, true, true, }, + { true, false, false, false, true, true, true, true, }, + { false, true, true, true, false, true, true, true, }, + { true, false, false, true, true, false, false, false, }, + { true, true, true, true, true, false, false, false, }, + { true, true, true, true, true, false, false, false, }, + }; + + private static readonly bool[,] SmallGridPattern = + { + { true, true, true, true, true, true, true, true, }, + { true, false, false, false, true, false, false, false, }, + { true, false, false, false, true, false, false, false, }, + { true, false, false, false, true, false, false, false, }, + { true, true, true, true, true, true, true, true, }, + { true, false, false, false, true, false, false, false, }, + { true, false, false, false, true, false, false, false, }, + { true, false, false, false, true, false, false, false, }, + }; + + private static readonly bool[,] SmallCheckerBoardPattern = + { + { true, false, false, true, true, false, false, true, }, + { false, true, true, false, false, true, true, false, }, + { false, true, true, false, false, true, true, false, }, + { true, false, false, true, true, false, false, true, }, + { true, false, false, true, true, false, false, true, }, + { false, true, true, false, false, true, true, false, }, + { false, true, true, false, false, true, true, false, }, + { true, false, false, true, true, false, false, true, }, + }; + + private static readonly bool[,] LargeCheckerBoardPattern = + { + { true, true, true, true, false, false, false, false, }, + { true, true, true, true, false, false, false, false, }, + { true, true, true, true, false, false, false, false, }, + { true, true, true, true, false, false, false, false, }, + { false, false, false, false, true, true, true, true, }, + { false, false, false, false, true, true, true, true, }, + { false, false, false, false, true, true, true, true, }, + { false, false, false, false, true, true, true, true, }, + }; + + private static readonly bool[,] OutlinedDiamondPattern = + { + { true, false, false, false, false, false, true, false, }, + { false, true, false, false, false, true, false, false, }, + { false, false, true, false, true, false, false, false, }, + { false, false, false, true, false, false, false, false, }, + { false, false, true, false, true, false, false, false, }, + { false, true, false, false, false, true, false, false, }, + { true, false, false, false, false, false, true, false, }, + { false, false, false, false, false, false, false, true, }, + }; + + private static readonly bool[,] SolidDiamondPattern = + { + { false, false, false, true, false, false, false, false, }, + { false, false, true, true, true, false, false, false, }, + { false, true, true, true, true, true, false, false, }, + { true, true, true, true, true, true, true, false, }, + { false, true, true, true, true, true, false, false, }, + { false, false, true, true, true, false, false, false, }, + { false, false, false, true, false, false, false, false, }, + { false, false, false, false, false, false, false, false, }, + }; + } +} diff --git a/ImageSharp.Drawing/Processing/Brushes.cs b/ImageSharp.Drawing/Processing/Brushes.cs new file mode 100644 index 0000000..2022359 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Brushes.cs @@ -0,0 +1,841 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// A collection of methods for creating generic brushes. + /// + public static partial class Brushes + { + /// + /// Creates a brush that paints a solid color. + /// + /// The brush color. + /// A new . + public static SolidBrush Solid(Color color) => new(color); + + /// + /// Creates a brush that paints horizontal line hatching using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush Horizontal(Color foreColor) + => new(foreColor, Color.Transparent, HorizontalPattern); + + /// + /// Creates a brush that paints horizontal line hatching using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Horizontal(Color foreColor, Color backColor) + => new(foreColor, backColor, HorizontalPattern); + + /// + /// Creates a brush that paints horizontal line hatching for the minimum hatch style using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush Min(Color foreColor) + => new(foreColor, Color.Transparent, HorizontalPattern); + + /// + /// Creates a brush that paints horizontal line hatching for the minimum hatch style using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Min(Color foreColor, Color backColor) + => new(foreColor, backColor, HorizontalPattern); + + /// + /// Creates a brush that paints vertical line hatching using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush Vertical(Color foreColor) + => new(foreColor, Color.Transparent, VerticalPattern); + + /// + /// Creates a brush that paints vertical line hatching using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Vertical(Color foreColor, Color backColor) + => new(foreColor, backColor, VerticalPattern); + + /// + /// Creates a brush that paints diagonal line hatching from upper left to lower right using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush ForwardDiagonal(Color foreColor) + => new(foreColor, Color.Transparent, ForwardDiagonalPattern); + + /// + /// Creates a brush that paints diagonal line hatching from upper left to lower right using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush ForwardDiagonal(Color foreColor, Color backColor) + => new(foreColor, backColor, ForwardDiagonalPattern); + + /// + /// Creates a brush that paints diagonal line hatching from upper right to lower left using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush BackwardDiagonal(Color foreColor) + => new(foreColor, Color.Transparent, BackwardDiagonalPattern); + + /// + /// Creates a brush that paints diagonal line hatching from upper right to lower left using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush BackwardDiagonal(Color foreColor, Color backColor) + => new(foreColor, backColor, BackwardDiagonalPattern); + + /// + /// Creates a brush that paints intersecting horizontal and vertical line hatching using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush Cross(Color foreColor) => new(foreColor, Color.Transparent, CrossPattern); + + /// + /// Creates a brush that paints intersecting horizontal and vertical line hatching using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Cross(Color foreColor, Color backColor) => new(foreColor, backColor, CrossPattern); + + /// + /// Creates a brush that paints intersecting forward and backward diagonal line hatching using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush DiagonalCross(Color foreColor) => new(foreColor, Color.Transparent, DiagonalCrossPattern); + + /// + /// Creates a brush that paints intersecting forward and backward diagonal line hatching using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush DiagonalCross(Color foreColor, Color backColor) => new(foreColor, backColor, DiagonalCrossPattern); + + /// + /// Creates a brush that paints a 5-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 5:95. + /// + /// The foreground color. + /// A new . + public static PatternBrush Percent05(Color foreColor) => new(foreColor, Color.Transparent, Percent05Pattern); + + /// + /// Creates a brush that paints a 5-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 5:95. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Percent05(Color foreColor, Color backColor) => new(foreColor, backColor, Percent05Pattern); + + /// + /// Creates a brush that paints a 10-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 10:90. + /// + /// The foreground color. + /// A new . + public static PatternBrush Percent10(Color foreColor) + => new(foreColor, Color.Transparent, Percent10Pattern); + + /// + /// Creates a brush that paints a 10-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 10:90. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Percent10(Color foreColor, Color backColor) + => new(foreColor, backColor, Percent10Pattern); + + /// + /// Creates a brush that paints a 20-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 20:80. + /// + /// The foreground color. + /// A new . + public static PatternBrush Percent20(Color foreColor) + => new(foreColor, Color.Transparent, Percent20Pattern); + + /// + /// Creates a brush that paints a 20-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 20:80. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Percent20(Color foreColor, Color backColor) + => new(foreColor, backColor, Percent20Pattern); + + /// + /// Creates a brush that paints a 25-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 25:75. + /// + /// The foreground color. + /// A new . + public static PatternBrush Percent25(Color foreColor) => new(foreColor, Color.Transparent, Percent25Pattern); + + /// + /// Creates a brush that paints a 25-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 25:75. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Percent25(Color foreColor, Color backColor) => new(foreColor, backColor, Percent25Pattern); + + /// + /// Creates a brush that paints a 30-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 30:70. + /// + /// The foreground color. + /// A new . + public static PatternBrush Percent30(Color foreColor) => new(foreColor, Color.Transparent, Percent30Pattern); + + /// + /// Creates a brush that paints a 30-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 30:70. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Percent30(Color foreColor, Color backColor) => new(foreColor, backColor, Percent30Pattern); + + /// + /// Creates a brush that paints a 40-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 40:60. + /// + /// The foreground color. + /// A new . + public static PatternBrush Percent40(Color foreColor) => new(foreColor, Color.Transparent, Percent40Pattern); + + /// + /// Creates a brush that paints a 40-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 40:60. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Percent40(Color foreColor, Color backColor) => new(foreColor, backColor, Percent40Pattern); + + /// + /// Creates a brush that paints a 50-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 50:50. + /// + /// The foreground color. + /// A new . + public static PatternBrush Percent50(Color foreColor) => new(foreColor, Color.Transparent, Percent50Pattern); + + /// + /// Creates a brush that paints a 50-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 50:50. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Percent50(Color foreColor, Color backColor) => new(foreColor, backColor, Percent50Pattern); + + /// + /// Creates a brush that paints a 60-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 60:40. + /// + /// The foreground color. + /// A new . + public static PatternBrush Percent60(Color foreColor) => new(foreColor, Color.Transparent, Percent60Pattern); + + /// + /// Creates a brush that paints a 60-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 60:40. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Percent60(Color foreColor, Color backColor) => new(foreColor, backColor, Percent60Pattern); + + /// + /// Creates a brush that paints a 70-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 70:30. + /// + /// The foreground color. + /// A new . + public static PatternBrush Percent70(Color foreColor) => new(foreColor, Color.Transparent, Percent70Pattern); + + /// + /// Creates a brush that paints a 70-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 70:30. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Percent70(Color foreColor, Color backColor) => new(foreColor, backColor, Percent70Pattern); + + /// + /// Creates a brush that paints a 75-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 75:25. + /// + /// The foreground color. + /// A new . + public static PatternBrush Percent75(Color foreColor) => new(foreColor, Color.Transparent, Percent75Pattern); + + /// + /// Creates a brush that paints a 75-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 75:25. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Percent75(Color foreColor, Color backColor) => new(foreColor, backColor, Percent75Pattern); + + /// + /// Creates a brush that paints an 80-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 80:20. + /// + /// The foreground color. + /// A new . + public static PatternBrush Percent80(Color foreColor) => new(foreColor, Color.Transparent, Percent80Pattern); + + /// + /// Creates a brush that paints an 80-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 80:20. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Percent80(Color foreColor, Color backColor) => new(foreColor, backColor, Percent80Pattern); + + /// + /// Creates a brush that paints a 90-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 90:10. + /// + /// The foreground color. + /// A new . + public static PatternBrush Percent90(Color foreColor) => new(foreColor, Color.Transparent, Percent90Pattern); + + /// + /// Creates a brush that paints a 90-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 90:10. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Percent90(Color foreColor, Color backColor) => new(foreColor, backColor, Percent90Pattern); + + /// + /// Creates a brush that paints downward diagonal lines spaced more closely than ForwardDiagonal using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush LightDownwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, LightDownwardDiagonalPattern); + + /// + /// Creates a brush that paints downward diagonal lines spaced more closely than ForwardDiagonal using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush LightDownwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, LightDownwardDiagonalPattern); + + /// + /// Creates a brush that paints upward diagonal lines spaced more closely than BackwardDiagonal using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush LightUpwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, LightUpwardDiagonalPattern); + + /// + /// Creates a brush that paints upward diagonal lines spaced more closely than BackwardDiagonal using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush LightUpwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, LightUpwardDiagonalPattern); + + /// + /// Creates a brush that paints thicker downward diagonal lines spaced more closely than ForwardDiagonal using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush DarkDownwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, DarkDownwardDiagonalPattern); + + /// + /// Creates a brush that paints thicker downward diagonal lines spaced more closely than ForwardDiagonal using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush DarkDownwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, DarkDownwardDiagonalPattern); + + /// + /// Creates a brush that paints thicker upward diagonal lines spaced more closely than BackwardDiagonal using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush DarkUpwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, DarkUpwardDiagonalPattern); + + /// + /// Creates a brush that paints thicker upward diagonal lines spaced more closely than BackwardDiagonal using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush DarkUpwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, DarkUpwardDiagonalPattern); + + /// + /// Creates a brush that paints wide downward diagonal lines with ForwardDiagonal spacing using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush WideDownwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, WideDownwardDiagonalPattern); + + /// + /// Creates a brush that paints wide downward diagonal lines with ForwardDiagonal spacing using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush WideDownwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, WideDownwardDiagonalPattern); + + /// + /// Creates a brush that paints wide upward diagonal lines with BackwardDiagonal spacing using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush WideUpwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, WideUpwardDiagonalPattern); + + /// + /// Creates a brush that paints wide upward diagonal lines with BackwardDiagonal spacing using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush WideUpwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, WideUpwardDiagonalPattern); + + /// + /// Creates a brush that paints vertical lines spaced more closely than Vertical using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush LightVertical(Color foreColor) => new(foreColor, Color.Transparent, LightVerticalPattern); + + /// + /// Creates a brush that paints vertical lines spaced more closely than Vertical using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush LightVertical(Color foreColor, Color backColor) => new(foreColor, backColor, LightVerticalPattern); + + /// + /// Creates a brush that paints horizontal lines spaced more closely than Horizontal using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush LightHorizontal(Color foreColor) => new(foreColor, Color.Transparent, LightHorizontalPattern); + + /// + /// Creates a brush that paints horizontal lines spaced more closely than Horizontal using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush LightHorizontal(Color foreColor, Color backColor) => new(foreColor, backColor, LightHorizontalPattern); + + /// + /// Creates a brush that paints narrow vertical lines spaced more closely than LightVertical using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush NarrowVertical(Color foreColor) => new(foreColor, Color.Transparent, NarrowVerticalPattern); + + /// + /// Creates a brush that paints narrow vertical lines spaced more closely than LightVertical using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush NarrowVertical(Color foreColor, Color backColor) => new(foreColor, backColor, NarrowVerticalPattern); + + /// + /// Creates a brush that paints narrow horizontal lines spaced more closely than LightHorizontal using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush NarrowHorizontal(Color foreColor) => new(foreColor, Color.Transparent, NarrowHorizontalPattern); + + /// + /// Creates a brush that paints narrow horizontal lines spaced more closely than LightHorizontal using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush NarrowHorizontal(Color foreColor, Color backColor) => new(foreColor, backColor, NarrowHorizontalPattern); + + /// + /// Creates a brush that paints thicker vertical lines spaced more closely than Vertical using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush DarkVertical(Color foreColor) => new(foreColor, Color.Transparent, DarkVerticalPattern); + + /// + /// Creates a brush that paints thicker vertical lines spaced more closely than Vertical using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush DarkVertical(Color foreColor, Color backColor) => new(foreColor, backColor, DarkVerticalPattern); + + /// + /// Creates a brush that paints thicker horizontal lines spaced more closely than Horizontal using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush DarkHorizontal(Color foreColor) => new(foreColor, Color.Transparent, DarkHorizontalPattern); + + /// + /// Creates a brush that paints thicker horizontal lines spaced more closely than Horizontal using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush DarkHorizontal(Color foreColor, Color backColor) => new(foreColor, backColor, DarkHorizontalPattern); + + /// + /// Creates a brush that paints dashed diagonal lines from upper left to lower right using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush DashedDownwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, DashedDownwardDiagonalPattern); + + /// + /// Creates a brush that paints dashed diagonal lines from upper left to lower right using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush DashedDownwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, DashedDownwardDiagonalPattern); + + /// + /// Creates a brush that paints dashed diagonal lines from upper right to lower left using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush DashedUpwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, DashedUpwardDiagonalPattern); + + /// + /// Creates a brush that paints dashed diagonal lines from upper right to lower left using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush DashedUpwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, DashedUpwardDiagonalPattern); + + /// + /// Creates a brush that paints dashed horizontal lines using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush DashedHorizontal(Color foreColor) => new(foreColor, Color.Transparent, DashedHorizontalPattern); + + /// + /// Creates a brush that paints dashed horizontal lines using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush DashedHorizontal(Color foreColor, Color backColor) => new(foreColor, backColor, DashedHorizontalPattern); + + /// + /// Creates a brush that paints dashed vertical lines using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush DashedVertical(Color foreColor) => new(foreColor, Color.Transparent, DashedVerticalPattern); + + /// + /// Creates a brush that paints dashed vertical lines using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush DashedVertical(Color foreColor, Color backColor) => new(foreColor, backColor, DashedVerticalPattern); + + /// + /// Creates a brush that paints a small confetti-style hatch using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush SmallConfetti(Color foreColor) => new(foreColor, Color.Transparent, SmallConfettiPattern); + + /// + /// Creates a brush that paints a small confetti-style hatch using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush SmallConfetti(Color foreColor, Color backColor) => new(foreColor, backColor, SmallConfettiPattern); + + /// + /// Creates a brush that paints a confetti-style hatch with larger pieces than SmallConfetti using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush LargeConfetti(Color foreColor) => new(foreColor, Color.Transparent, LargeConfettiPattern); + + /// + /// Creates a brush that paints a confetti-style hatch with larger pieces than SmallConfetti using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush LargeConfetti(Color foreColor, Color backColor) => new(foreColor, backColor, LargeConfettiPattern); + + /// + /// Creates a brush that paints horizontal lines formed from zigzags using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush ZigZag(Color foreColor) => new(foreColor, Color.Transparent, ZigZagPattern); + + /// + /// Creates a brush that paints horizontal lines formed from zigzags using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush ZigZag(Color foreColor, Color backColor) => new(foreColor, backColor, ZigZagPattern); + + /// + /// Creates a brush that paints horizontal lines formed from wave shapes using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush Wave(Color foreColor) => new(foreColor, Color.Transparent, WavePattern); + + /// + /// Creates a brush that paints horizontal lines formed from wave shapes using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Wave(Color foreColor, Color backColor) => new(foreColor, backColor, WavePattern); + + /// + /// Creates a brush that paints staggered brick shapes running diagonally upward using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush DiagonalBrick(Color foreColor) => new(foreColor, Color.Transparent, DiagonalBrickPattern); + + /// + /// Creates a brush that paints staggered brick shapes running diagonally upward using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush DiagonalBrick(Color foreColor, Color backColor) => new(foreColor, backColor, DiagonalBrickPattern); + + /// + /// Creates a brush that paints staggered brick shapes arranged horizontally using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush HorizontalBrick(Color foreColor) => new(foreColor, Color.Transparent, HorizontalBrickPattern); + + /// + /// Creates a brush that paints staggered brick shapes arranged horizontally using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush HorizontalBrick(Color foreColor, Color backColor) => new(foreColor, backColor, HorizontalBrickPattern); + + /// + /// Creates a brush that paints a woven-material hatch using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush Weave(Color foreColor) => new(foreColor, Color.Transparent, WeavePattern); + + /// + /// Creates a brush that paints a woven-material hatch using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Weave(Color foreColor, Color backColor) => new(foreColor, backColor, WeavePattern); + + /// + /// Creates a brush that paints a plaid-material hatch using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush Plaid(Color foreColor) => new(foreColor, Color.Transparent, PlaidPattern); + + /// + /// Creates a brush that paints a plaid-material hatch using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Plaid(Color foreColor, Color backColor) => new(foreColor, backColor, PlaidPattern); + + /// + /// Creates a brush that paints a divot-style hatch using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush Divot(Color foreColor) => new(foreColor, Color.Transparent, DivotPattern); + + /// + /// Creates a brush that paints a divot-style hatch using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Divot(Color foreColor, Color backColor) => new(foreColor, backColor, DivotPattern); + + /// + /// Creates a brush that paints intersecting horizontal and vertical dotted lines using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush DottedGrid(Color foreColor) => new(foreColor, Color.Transparent, DottedGridPattern); + + /// + /// Creates a brush that paints intersecting horizontal and vertical dotted lines using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush DottedGrid(Color foreColor, Color backColor) => new(foreColor, backColor, DottedGridPattern); + + /// + /// Creates a brush that paints intersecting forward and backward diagonal dotted lines using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush DottedDiamond(Color foreColor) => new(foreColor, Color.Transparent, DottedDiamondPattern); + + /// + /// Creates a brush that paints intersecting forward and backward diagonal dotted lines using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush DottedDiamond(Color foreColor, Color backColor) => new(foreColor, backColor, DottedDiamondPattern); + + /// + /// Creates a brush that paints layered shingle shapes running diagonally downward using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush Shingle(Color foreColor) => new(foreColor, Color.Transparent, ShinglePattern); + + /// + /// Creates a brush that paints layered shingle shapes running diagonally downward using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Shingle(Color foreColor, Color backColor) => new(foreColor, backColor, ShinglePattern); + + /// + /// Creates a brush that paints a trellis-style hatch using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush Trellis(Color foreColor) => new(foreColor, Color.Transparent, TrellisPattern); + + /// + /// Creates a brush that paints a trellis-style hatch using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Trellis(Color foreColor, Color backColor) => new(foreColor, backColor, TrellisPattern); + + /// + /// Creates a brush that paints adjacent sphere-like shapes using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush Sphere(Color foreColor) => new(foreColor, Color.Transparent, SpherePattern); + + /// + /// Creates a brush that paints adjacent sphere-like shapes using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush Sphere(Color foreColor, Color backColor) => new(foreColor, backColor, SpherePattern); + + /// + /// Creates a brush that paints intersecting horizontal and vertical lines spaced more closely than Cross using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush SmallGrid(Color foreColor) => new(foreColor, Color.Transparent, SmallGridPattern); + + /// + /// Creates a brush that paints intersecting horizontal and vertical lines spaced more closely than Cross using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush SmallGrid(Color foreColor, Color backColor) => new(foreColor, backColor, SmallGridPattern); + + /// + /// Creates a brush that paints a small checkerboard hatch using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush SmallCheckerBoard(Color foreColor) => new(foreColor, Color.Transparent, SmallCheckerBoardPattern); + + /// + /// Creates a brush that paints a small checkerboard hatch using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush SmallCheckerBoard(Color foreColor, Color backColor) => new(foreColor, backColor, SmallCheckerBoardPattern); + + /// + /// Creates a brush that paints a checkerboard hatch with larger squares than SmallCheckerBoard using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush LargeCheckerBoard(Color foreColor) => new(foreColor, Color.Transparent, LargeCheckerBoardPattern); + + /// + /// Creates a brush that paints a checkerboard hatch with larger squares than SmallCheckerBoard using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush LargeCheckerBoard(Color foreColor, Color backColor) => new(foreColor, backColor, LargeCheckerBoardPattern); + + /// + /// Creates a brush that paints outlined diamond shapes formed by crossing diagonal lines using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush OutlinedDiamond(Color foreColor) => new(foreColor, Color.Transparent, OutlinedDiamondPattern); + + /// + /// Creates a brush that paints outlined diamond shapes formed by crossing diagonal lines using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush OutlinedDiamond(Color foreColor, Color backColor) => new(foreColor, backColor, OutlinedDiamondPattern); + + /// + /// Creates a brush that paints a filled diamond checkerboard hatch using the foreground color on a transparent background. + /// + /// The foreground color. + /// A new . + public static PatternBrush SolidDiamond(Color foreColor) => new(foreColor, Color.Transparent, SolidDiamondPattern); + + /// + /// Creates a brush that paints a filled diamond checkerboard hatch using the specified foreground and background colors. + /// + /// The foreground color. + /// The background color. + /// A new . + public static PatternBrush SolidDiamond(Color foreColor, Color backColor) => new(foreColor, backColor, SolidDiamondPattern); + } +} diff --git a/ImageSharp.Drawing/Processing/ColorStop.cs b/ImageSharp.Drawing/Processing/ColorStop.cs new file mode 100644 index 0000000..9341e28 --- /dev/null +++ b/ImageSharp.Drawing/Processing/ColorStop.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// A struct that defines a single color stop. + /// + [DebuggerDisplay("ColorStop({Ratio} -> {Color}")] + public readonly struct ColorStop + { + /// + /// Initializes a new instance of the struct. + /// + /// Where should it be? 0 is at the start, 1 at the end of the Gradient. + /// What color should be used at that point? + public ColorStop(float ratio, in Color color) + { + this.Ratio = ratio; + this.Color = color; + } + + /// + /// Gets the point along the defined gradient axis. + /// + public float Ratio { get; } + + /// + /// Gets the color to be used. + /// + public Color Color { get; } + } +} diff --git a/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md b/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md new file mode 100644 index 0000000..f093378 --- /dev/null +++ b/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md @@ -0,0 +1,392 @@ +# DrawingCanvas + +`DrawingCanvas` is the high-level drawing surface used by ImageSharp.Drawing. It lets the library expose one drawing model while supporting very different execution targets: + +- CPU rasterization into memory +- GPU execution through native surfaces +- backends that prefer their own internal representation, such as vector export + +That unification is the hard part. The public API wants to feel immediate and simple: fill a path, draw text, save state, restore state, draw into a region, maybe draw into a layer. The backends, however, do not all want the same kind of work. A CPU rasterizer wants rows, spans, and direct pixel access. A GPU backend wants compact command data, stable batching, and a single handoff point. A vector exporter would want semantic geometry rather than already-rasterized pixels. + +The architecture around `DrawingCanvas` and its typed implementation exists to absorb that mismatch. + +This document explains that architecture from the outside in. The goal is to help a newcomer understand what problem each piece solves before diving into methods and types. + +## The Main Problem + +If the canvas executed every public call immediately, each backend would have to implement the entire public drawing model directly: + +- save and restore state +- clip stacking +- layers and isolated composition +- text drawing +- image drawing with transforms +- region drawing +- brush and pen handling +- transform handling + +That sounds straightforward until the differences between backends become obvious. + +The CPU backend can cheaply mutate a memory buffer row by row. The GPU backend wants a larger batch of work so it can amortize setup, upload, and dispatch costs. A vector-style backend would ideally preserve geometry and draw intent for as long as possible. If each backend solved all of that from scratch, they would drift apart quickly and correctness bugs would multiply. + +So the architecture chooses a different approach: + +`DrawingCanvas` records drawing intent first, normalizes that intent when replaying or creating a retained scene, and only then hands the work to the backend. + +That one decision explains most of the surrounding design. + +## The Core Idea + +The canvas is a deferred renderer. + +Drawing calls do not rasterize immediately. They create `CompositionCommand` records and queue them into `DrawingCanvasBatcher`. The expensive normalization work happens later, during replay, when the batcher prepares those commands and hands `DrawingCommandBatch` ranges to the backend. + +That gives the architecture three important benefits. + +First, the public API stays backend-agnostic. A fill is a fill, whether the target is CPU memory or a GPU surface. + +Second, the expensive shared command work can happen once, in one shared place. Transform application, stroke expansion, clip application, and dash expansion are not reimplemented independently by every backend. + +Third, the backend receives a much more stable handoff. Instead of reacting to a long stream of public API calls, it receives prepared command batches with consistent semantics. + +## The Most Important Terms + +Before looking at the flow, it helps to define the major terms in the sense used by this codebase. + +### Canvas + +`DrawingCanvas` is the public drawing facade. It owns the current drawing state, accepts commands, and decides when to flush. + +It is not the rasterizer. It is the object that makes the public drawing model coherent. + +Callers usually reach that model through `IImageProcessingContext.Paint(...)`. + +When callers already have an `ImageFrame` or `ImageFrame`, the public `CreateCanvas(...)` frame extensions create a canvas directly over that frame. The caller owns the returned canvas and must dispose it to replay recorded work into the frame. + +`DrawingCanvas` is the typed implementation that carries the target pixel format for brush normalization, +readback, and backend execution. Factory methods return `DrawingCanvas` so CPU and GPU entry points expose the same +canvas-facing API while still constructing the typed implementation internally. + +### Batcher + +`DrawingCanvasBatcher` is the deferred command queue. It stores pending `CompositionCommand` values, records the canvas replay timeline, prepares commands during replay, and creates `DrawingCommandBatch` values for command-range entries. + +It is the bridge between the immediate-looking public API and the deferred backend handoff. + +### Command + +`CompositionCommand` is the recorded unit of drawing intent. In the common case it means "fill this path with this brush under this state". The command stream also carries explicit layer boundaries through `BeginLayer` and `EndLayer`. + +The command remains relatively close to the original user request. It may hold the original path, pen, brush, transform, and clip paths. + +### Preparation + +Preparation is the normalization step that turns recorded intent into backend-ready commands. + +`DrawingCanvasBatcher.PrepareCommands(...)` runs only when needed. It applies command transforms, expands strokes to fill paths, applies clip paths so clipped commands reach the backend as ordinary fills, and expands dashed strokes when a stroke pattern is present. + +Preparation stops at `DrawingCommandBatch`. Backend-specific lowering happens after that, inside `IDrawingBackend.CreateScene(...)`. + +### Command Batch + +`DrawingCommandBatch` is the prepared command range handed to the backend. It contains the command stream for one contiguous range and scene-level facts such as whether that range contains layer boundaries. + +It is the backend handoff boundary. + +### Backend + +`IDrawingBackend` is the execution engine behind the canvas. The important implementations are: + +- `DefaultDrawingBackend` for CPU rendering +- `WebGPUDrawingBackend` for GPU rendering through native surfaces + +The backend creates retained scenes from command batches and renders retained scenes into typed target frames. + +There are two backend-selection paths in the architecture: + +- direct `DrawingCanvas` construction resolves the backend from `Configuration` +- specialized infrastructure can construct a canvas with an explicit backend + +The ordinary CPU entry point is `Paint(...)` on `IImageProcessingContext`, which routes into the typed +implementation internally. Public `ImageFrame` canvas extensions provide the lower-level frame entry point for callers that want to own the canvas lifetime directly. + +That explicit-backend path matters for the WebGPU helpers. `WebGPUWindow`, `WebGPUExternalSurface`, and `WebGPURenderTarget` create canvases that point directly at their owned `WebGPUDrawingBackend` instance instead of storing that backend on the caller's `Configuration`. + +### Frame + +`ICanvasFrame` is the target abstraction that the backend renders into. + +This is one of the terms that can be ambiguous without context, so it is worth being explicit. In this architecture, a canvas frame is not "a UI frame" or "one animation frame". It means "the destination surface for one canvas instance". + +The important properties of a frame are: + +- `Bounds` +- whether it exposes a CPU region through `TryGetCpuRegion(...)` +- whether it exposes a native surface through `TryGetNativeSurface(...)` + +That abstraction lets the same canvas target: + +- pure CPU memory with `MemoryCanvasFrame` +- a native or GPU surface with `NativeCanvasFrame` +- a combined CPU plus native target +- a clipped view over another frame with `CanvasRegionFrame` + +The point is not to hide all differences. The point is to express the minimum target contract the backends need. + +### Layer + +A layer is isolated group rendering. In public API terms, it is created with `SaveLayer(...)` and later closed by `Restore()` or `RestoreTo(...)`. + +In this architecture, a layer is recorded inline in the command stream as: + +- `BeginLayer` +- commands inside the layer +- `EndLayer` + +The backend is responsible for lowering those layer boundaries into the execution model it needs. + +Layer semantics stay in the shared command model so every backend receives the same layer structure at the handoff boundary. + +## The Big Picture Flow + +The easiest way to understand the system is to follow one normal draw call all the way through. + +### Step 1: The canvas records intent + +A public method such as `Fill(...)`, `Draw(...)`, or `DrawText(...)` resolves the active state and creates one or more `CompositionCommand` values. + +At this point the canvas is mostly recording: + +- geometry references +- brushes or pens +- active transform +- clip paths +- graphics options +- target bounds relevant to this command + +The canvas does not try to fully rasterize anything here. + +### Step 2: The batcher owns the pending work + +Commands go into `DrawingCanvasBatcher`. + +The batcher exists so the canvas does not need to talk to the backend for every single API call. It accumulates work until a timeline boundary is reached. + +The replay boundary usually comes from: + +- explicit `Flush()`, which seals the current command range +- `Apply(...)`, which needs read-modify-write behavior +- `RenderScene(...)`, which inserts an existing retained scene into the timeline +- disposal of the owning canvas + +### Step 3: The batcher prepares commands + +When the root canvas is disposed, or when the caller creates a retained scene, the batcher seals any pending commands and prepares the command buffer. This is where the architecture does the heavy shared work that would otherwise be duplicated across backends. + +For a typical path-based command, canvas preparation does the following in concept: + +1. transform the source path into its final geometry space +2. if a pen is present, expand the stroke to fill geometry +3. apply clip paths +4. transform the brush into the same command space +5. leave backend-specific retained geometry construction to `CreateScene(...)` + +This is the architectural center of gravity. It is the shared normalization stage that makes the backends simpler. + +### Step 4: The backend creates and renders scenes + +After preparation, disposal replay walks the canvas timeline in order. Command-range entries become short-lived retained scenes through `backend.CreateScene(...)`, and those scenes are then rendered through `backend.RenderScene(...)`. + +From that point the CPU and GPU paths diverge. + +The CPU backend lowers each command batch into a row-oriented retained representation through `FlushScene` during `CreateScene(...)` and then composites into memory during `RenderScene(...)`. + +The WebGPU backend encodes each command batch into its retained GPU representation during `CreateScene(...)`, then uploads render-scoped resources and dispatches GPU work during `RenderScene(...)`. + +The architecture is successful if both backends can differ dramatically here without needing the public canvas model itself to fork. + +## Why State Is Snapshotted + +Drawing APIs look stateful because they are stateful. The active transform, clips, graphics options, and layer information all affect future commands. + +`DrawingCanvasState` exists so that state changes are cheap to reason about and cheap to attach to commands. + +The state snapshot contains the active options and target information for subsequent commands, including: + +- `Options` +- `ClipPaths` +- `IsLayer` +- layer-related graphics options and bounds +- current target bounds + +The canvas treats this state as immutable snapshots on a stack. `Save()` pushes a copy. `Restore()` pops one. Drawing calls always read the current top-of-stack state. + +That makes save and restore semantics predictable and backend-independent. + +## How Layers Work In This Architecture + +Layer terminology often causes confusion because different systems use it differently. In this codebase, the most useful mental model is: + +"A layer is a nested composition scope recorded inline in the command stream." + +When `SaveLayer(...)` is called, the canvas: + +1. clamps the requested layer bounds to the canvas +2. converts them into absolute target bounds +3. records `BeginLayer` +4. pushes a state snapshot that marks the new layer scope + +The layer bounds are expressed in the active local coordinate system, so the canvas +transform in effect at `SaveLayer(...)` time is applied when resolving the layer's +absolute target bounds. The resolved bounds limit isolation, allocation, and final +composition. They do not shift the canvas coordinate system; draw commands inside a +bounded layer still use the same local coordinates as the parent canvas. + +When the layer is later closed through `Restore()` or `RestoreTo(...)`, the canvas records `EndLayer`. + +The actual isolation is implemented later by the backend. + +On the CPU backend, layer boundaries become temporary backing buffers during scene execution. + +On the WebGPU backend, layer boundaries become explicit staged-scene operations inside the GPU-oriented pipeline. + +The key architectural point is that the public canvas records one shared layer model and lets the backend lower it. + +## Why Frames Exist + +The frame abstraction solves another unification problem. + +The canvas should be able to target a plain in-memory image, but that should not force the GPU backend to pretend everything is CPU memory. Likewise, GPU-native targets should not force the CPU path to know about native surfaces directly. + +`ICanvasFrame` is the contract that keeps those concerns separated. + +In this architecture, a frame means "the destination surface and its capabilities". That is why the interface exposes both: + +- geometric bounds +- optional CPU access +- optional native-surface access + +This lets the same canvas code target different kinds of surfaces without rewriting the command model. + +`CanvasRegionFrame` extends that idea one step further by saying "treat this clipped rectangle inside another frame as the target". That is how region canvases can share the same backend and batcher model while still drawing into a sub-rectangle. + +## What `CreateRegion(...)` Really Means + +`CreateRegion(...)` does not create a new independent rendering universe. It creates a child canvas that views a clipped sub-region of the parent target. + +The child: + +- wraps the parent target in `CanvasRegionFrame` +- keeps using the same backend +- keeps using the same shared batcher +- keeps participating in the same deferred replay model + +The child canvas has local coordinates starting at `(0, 0)`, but its frame bounds resolve to the correct absolute position inside the parent target. + +That distinction matters. It means the region API is a coordinate-system convenience, not a request to fork rendering into a totally separate backend pipeline. + +## Why `DrawImage(...)` Is Special + +Most draw calls record intent and defer the heavy work. + +`DrawImage(...)` is the notable exception. + +Images behave differently from paths because the canvas cannot simply attach a transform and let the backend "figure it out later" in the same way. The code performs eager image work before the final command is queued. + +The rough flow is: + +1. crop and scale the source image if needed +2. if a canvas transform is active, bake that transform into the image pixels +3. align the transformed bitmap to integer canvas bounds +4. create an `ImageBrush` +5. queue the final fill command using that brush + +This design avoids applying the canvas transform twice and keeps the later command model consistent with brush-based filling. + +That is why `DrawImage(...)` should be understood as "prepare an image-backed brush, then queue a normal fill", not as a completely separate rasterization pipeline. + +## What The CPU Backend Receives + +Once a command batch reaches `DefaultDrawingBackend.CreateScene(...)`, the public drawing model is already normalized. + +The CPU backend does not need to understand every public API call individually. It works with: + +- prepared commands +- layer boundaries +- target bounds during `CreateScene(...)` +- the destination frame during `RenderScene(...)` + +It lowers each command batch into a retained row-oriented structure through `FlushScene`. Later, `RenderScene(...)` acquires the CPU destination frame, allocates temporary backing buffers for layers when needed, and composites the final result into the target frame. + +That is the payoff of the architecture: the CPU backend is solving a rendering problem, not a public-API interpretation problem. + +## What The WebGPU Backend Receives + +The WebGPU backend receives the same command batch shape, but it splits retained scene creation from render-scoped GPU work. + +`CreateScene(...)` handles: + +- encoding prepared command data + +`RenderScene(...)` handles: + +- creating render-scoped native resources +- planning dispatches +- executing the GPU pipeline + +It benefits from the same canvas-level decisions: + +- commands are already normalized +- layers already exist as explicit boundaries +- the frame already describes whether a native surface is available + +The WebGPU public helpers reach this point in a target-first way: + +- `WebGPUWindow` acquires a presentable native target per frame +- `WebGPURenderTarget` owns an offscreen native target for GPU drawing and readback +- `WebGPUExternalSurface` attaches WebGPU drawing to a caller-owned native host + +Those helpers all create typed canvas instances with an explicit `WebGPUDrawingBackend`, so GPU execution stays attached to the WebGPU object that owns the native target and backend lifetime while callers work through `DrawingCanvas`. + +The backend is free to choose a very different execution model because the canvas has already solved the shared semantics problem. + +## The Practical Mental Model + +If you are new to this code, the most useful mental model is: + +`DrawingCanvas` is the stateful front end that records drawing intent, `DrawingCanvas` is the typed implementation, `DrawingCanvasBatcher` is the deferred handoff boundary, and the backend creates and renders retained scenes from prepared command batches. + +Everything else serves that flow. + +State snapshots exist so save and restore are precise. + +Commands exist so public API calls can be deferred. + +Preparation exists so backend-agnostic normalization happens once. + +Frames exist so the same canvas can target memory, native surfaces, or sub-regions. + +Layers exist as inline composition scopes in the command stream. + +Once those ideas are clear, the code stops looking like a random collection of types and starts looking like one system with a clear division of responsibility. + +## Reading Guide + +If you want to move from the architecture into the code, this is the best order. + +1. `DrawingCanvas.cs` +2. `DrawingCanvas{TPixel}.cs` +3. `DrawingCanvasFactoryExtensions.cs` and `DrawingCanvas.Shapes.cs` +4. `DrawingCanvasBatcher{TPixel}.cs` +5. `CompositionCommand.cs` +6. `DefaultDrawingBackend.cs` +7. `FlushScene.cs` +8. `WebGPUEnvironment.cs` +9. `WebGPUWindow.cs`, `WebGPUExternalSurface.cs`, and `WebGPURenderTarget.cs` +10. `WebGPUDrawingBackend` and its scene/dispatch types + +That path follows the real runtime flow: + +public API -> recorded command -> prepared command batch -> backend scene creation -> backend scene rendering + +Following the code in that order is much easier than starting from the backend internals first. diff --git a/ImageSharp.Drawing/Processing/DrawingCanvas.Shapes.cs b/ImageSharp.Drawing/Processing/DrawingCanvas.Shapes.cs new file mode 100644 index 0000000..ce0cc0c --- /dev/null +++ b/ImageSharp.Drawing/Processing/DrawingCanvas.Shapes.cs @@ -0,0 +1,225 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Convenience shape helpers that forward to the core primitives. + /// + public abstract partial class DrawingCanvas + { + /// + /// Saves the current drawing state and begins an isolated compositing layer over the whole canvas. + /// + /// The save count after the layer state has been pushed. + public int SaveLayer() + => this.SaveLayer(new GraphicsOptions(), this.Bounds); + + /// + /// Saves the current drawing state and begins an isolated compositing layer over the whole canvas. + /// + /// Graphics options controlling how the layer is composited on restore. + /// The save count after the layer state has been pushed. + public int SaveLayer(GraphicsOptions layerOptions) + => this.SaveLayer(layerOptions, this.Bounds); + + /// + /// Fills the whole canvas using the given brush. + /// + /// Brush used to shade destination pixels. + public void Fill(Brush brush) + { + Rectangle bounds = this.Bounds; + + this.Fill(brush, new RectanglePolygon(bounds)); + } + + /// + /// Fills a local region using the given brush. + /// + /// Brush used to shade destination pixels. + /// Region to fill in local coordinates. + public void Fill(Brush brush, Rectangle region) + => this.Fill(brush, new RectanglePolygon(region)); + + /// + /// Clears the whole canvas using the given brush and clear-style composition options. + /// + /// Brush used to shade destination pixels during clear. + public void Clear(Brush brush) + { + Rectangle bounds = this.Bounds; + + this.Clear(brush, new RectanglePolygon(bounds)); + } + + /// + /// Clears a local region using the given brush and clear-style composition options. + /// + /// Brush used to shade destination pixels during clear. + /// Region to clear in local coordinates. + public void Clear(Brush brush, Rectangle region) + => this.Clear(brush, new RectanglePolygon(region)); + + /// + /// Fills all paths in a collection using the given brush. + /// + /// Brush used to shade covered pixels. + /// Path collection to fill. + public void Fill(Brush brush, IPathCollection paths) + { + Guard.NotNull(paths, nameof(paths)); + + foreach (IPath path in paths) + { + this.Fill(brush, path); + } + } + + /// + /// Fills a path built by the provided builder using the given brush. + /// + /// Brush used to shade covered pixels. + /// The path builder describing the fill region. + public void Fill(Brush brush, PathBuilder pathBuilder) + { + Guard.NotNull(pathBuilder, nameof(pathBuilder)); + + this.Fill(brush, pathBuilder.Build()); + } + + /// + /// Fills an ellipse using the provided brush. + /// + /// Brush used to shade covered pixels. + /// Ellipse center point in local coordinates. + /// Ellipse width and height in local coordinates. + public void FillEllipse(Brush brush, PointF center, SizeF size) + => this.Fill(brush, new EllipsePolygon(center, size)); + + /// + /// Fills the closed arc shape produced by joining the arc endpoints with a straight line. + /// + /// Brush used to shade covered pixels. + /// Arc center point in local coordinates. + /// Arc radii in local coordinates. + /// Ellipse rotation in degrees. + /// Arc start angle in degrees. + /// Arc sweep angle in degrees. + public void FillArc(Brush brush, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle) + => this.Fill(brush, new Path(new ArcLineSegment(center, radius, rotation, startAngle, sweepAngle))); + + /// + /// Fills a pie sector using the provided brush. + /// + /// Brush used to shade covered pixels. + /// The center point of the pie sector in local coordinates. + /// The x and y radii of the pie sector in local coordinates. + /// Ellipse rotation in degrees. + /// The start angle of the pie sector in degrees. + /// The sweep angle of the pie sector in degrees. + public void FillPie(Brush brush, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle) + => this.Fill(brush, new PiePolygon(center, radius, rotation, startAngle, sweepAngle)); + + /// + /// Fills a pie sector using the provided brush. + /// + /// Brush used to shade covered pixels. + /// The center point of the pie sector in local coordinates. + /// The x and y radii of the pie sector in local coordinates. + /// The start angle of the pie sector in degrees. + /// The sweep angle of the pie sector in degrees. + public void FillPie(Brush brush, PointF center, SizeF radius, float startAngle, float sweepAngle) + => this.Fill(brush, new PiePolygon(center, radius, startAngle, sweepAngle)); + + /// + /// Draws an arc outline using the provided pen. + /// + /// Pen used to generate the arc outline. + /// Arc center point in local coordinates. + /// Arc radii in local coordinates. + /// Ellipse rotation in degrees. + /// Arc start angle in degrees. + /// Arc sweep angle in degrees. + public void DrawArc(Pen pen, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle) + => this.Draw(pen, new Path(new ArcLineSegment(center, radius, rotation, startAngle, sweepAngle))); + + /// + /// Draws a cubic bezier outline using the provided pen. + /// + /// Pen used to generate the bezier outline. + /// Bezier control points. + public void DrawBezier(Pen pen, params PointF[] points) + { + Guard.NotNull(points, nameof(points)); + + this.Draw(pen, new Path(new CubicBezierLineSegment(points))); + } + + /// + /// Draws an ellipse outline using the provided pen. + /// + /// Pen used to generate the ellipse outline. + /// Ellipse center point in local coordinates. + /// Ellipse width and height in local coordinates. + public void DrawEllipse(Pen pen, PointF center, SizeF size) + => this.Draw(pen, new EllipsePolygon(center, size)); + + /// + /// Draws a pie sector outline using the provided pen. + /// + /// Pen used to generate the pie outline. + /// The center point of the pie sector in local coordinates. + /// The x and y radii of the pie sector in local coordinates. + /// Ellipse rotation in degrees. + /// The start angle of the pie sector in degrees. + /// The sweep angle of the pie sector in degrees. + public void DrawPie(Pen pen, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle) + => this.Draw(pen, new PiePolygon(center, radius, rotation, startAngle, sweepAngle)); + + /// + /// Draws a pie sector outline using the provided pen. + /// + /// Pen used to generate the pie outline. + /// The center point of the pie sector in local coordinates. + /// The x and y radii of the pie sector in local coordinates. + /// The start angle of the pie sector in degrees. + /// The sweep angle of the pie sector in degrees. + public void DrawPie(Pen pen, PointF center, SizeF radius, float startAngle, float sweepAngle) + => this.Draw(pen, new PiePolygon(center, radius, startAngle, sweepAngle)); + + /// + /// Draws a rectangular outline using the provided pen. + /// + /// Pen used to generate the rectangle outline. + /// Rectangle region to stroke. + public void Draw(Pen pen, Rectangle region) + => this.Draw(pen, new RectanglePolygon(region)); + + /// + /// Draws all paths in a collection using the provided pen. + /// + /// Pen used to generate outlines. + /// Path collection to stroke. + public void Draw(Pen pen, IPathCollection paths) + { + Guard.NotNull(paths, nameof(paths)); + + foreach (IPath path in paths) + { + this.Draw(pen, path); + } + } + + /// + /// Draws a path outline built by the provided builder using the given pen. + /// + /// Pen used to generate the outline fill path. + /// The path builder describing the path to stroke. + public void Draw(Pen pen, PathBuilder pathBuilder) + { + Guard.NotNull(pathBuilder, nameof(pathBuilder)); + + this.Draw(pen, pathBuilder.Build()); + } + } +} diff --git a/ImageSharp.Drawing/Processing/DrawingCanvas.cs b/ImageSharp.Drawing/Processing/DrawingCanvas.cs new file mode 100644 index 0000000..d7df848 --- /dev/null +++ b/ImageSharp.Drawing/Processing/DrawingCanvas.cs @@ -0,0 +1,294 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Text; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Transforms; +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Represents a drawing canvas over a frame target. + /// + public abstract partial class DrawingCanvas : IDisposable + { + /// + /// Gets the local bounds of this canvas. + /// + public abstract Rectangle Bounds { get; } + + /// + /// Gets the number of saved states currently on the canvas stack. + /// + public abstract int SaveCount { get; } + + /// + /// Saves the current drawing state on the state stack. + /// + /// + /// This operation stores the current canvas state by reference. + /// If the same instance is mutated after + /// , those mutations are visible when restoring. + /// + /// The save count after the state has been pushed. + public abstract int Save(); + + /// + /// Saves the current drawing state and replaces the active state with the provided options and clip paths. + /// + /// + /// The provided instance is stored by reference. + /// Mutating it after this call mutates the active/restored state behavior. + /// + /// Drawing options for the new active state. + /// Clip paths for the new active state. + /// The save count after the previous state has been pushed. + public abstract int Save(DrawingOptions options, params IPath[] clipPaths); + + /// + /// Saves the current drawing state and begins an isolated compositing layer + /// bounded to a subregion. Subsequent draw commands are recorded into that isolated + /// logical layer. When closes the layer, it is recorded into the + /// canvas timeline and later composed during using the specified + /// . + /// + /// + /// The layer bounds are expressed in the current local coordinate system and are + /// transformed with the active drawing transform when the layer is created. They + /// limit allocation and compositing only; they do not change the canvas coordinate + /// system used by commands recorded inside the layer. + /// + /// + /// Graphics options controlling how the closed layer is composited against the parent canvas + /// when the canvas timeline is rendered during . + /// + /// + /// The local bounds of the layer. Only this region is allocated and composited. + /// + /// The save count after the layer state has been pushed. + public abstract int SaveLayer(GraphicsOptions layerOptions, Rectangle bounds); + + /// + /// Restores the most recently saved state. + /// + /// + /// If the most recently saved state was created by a SaveLayer overload, + /// the layer is closed in the recorded timeline. Actual composition happens during + /// . + /// + public abstract void Restore(); + + /// + /// Restores to a specific save count. + /// + /// + /// State frames above are discarded, + /// and the last discarded frame becomes the current state. + /// If any discarded state was created by a SaveLayer overload, + /// those layers are closed in the recorded timeline and composed during + /// . + /// + /// The save count to restore to. + public abstract void RestoreTo(int saveCount); + + /// + /// Creates a child canvas over a subregion in local coordinates. + /// + /// The child region in local coordinates. + /// A child canvas with local origin at (0,0). + public abstract DrawingCanvas CreateRegion(Rectangle region); + + /// + /// Clears a path region using the given brush and clear-style composition options. + /// + /// Brush used to shade destination pixels during clear. + /// The path region to clear. + public abstract void Clear(Brush brush, IPath path); + + /// + /// Fills a path in local coordinates using the given brush. + /// + /// Brush used to shade covered pixels. + /// The path to fill. + public abstract void Fill(Brush brush, IPath path); + + /// + /// Applies an image-processing operation to a local region. + /// + /// The local region to process. + /// The image-processing operation to apply to the region. + public abstract void Apply(Rectangle region, Action operation); + + /// + /// Applies an image-processing operation to a region described by a path builder. + /// + /// The path builder describing the region to process. + /// The image-processing operation to apply to the region. + public abstract void Apply(PathBuilder pathBuilder, Action operation); + + /// + /// Applies an image-processing operation to a path region. + /// + /// + /// The operation affects only pixels covered by the supplied path. + /// + /// The path region to process. + /// The image-processing operation to apply to the region. + public abstract void Apply(IPath path, Action operation); + + /// + /// Draws a polyline outline using the provided pen and drawing options. + /// + /// Pen used to generate the line outline. + /// Polyline points. + public abstract void DrawLine(Pen pen, params PointF[] points); + + /// + /// Draws a path outline in local coordinates using the given pen. + /// + /// Pen used to generate the outline fill path. + /// The path to stroke. + public abstract void Draw(Pen pen, IPath path); + + /// + /// Draws text onto this canvas. + /// + /// The text rendering options. + /// The text to draw. + /// Optional brush used to fill glyphs. + /// Optional pen used to outline glyphs. + public abstract void DrawText( + RichTextOptions textOptions, + ReadOnlySpan text, + Brush? brush, + Pen? pen); + + /// + /// Draws text along a path baseline onto this canvas. + /// + /// The text rendering options. + /// The text to draw. + /// The path used as the text baseline in local canvas coordinates. + /// Optional brush used to fill glyphs. + /// Optional pen used to outline glyphs. + public abstract void DrawText( + RichTextOptions textOptions, + ReadOnlySpan text, + IPath path, + Brush? brush, + Pen? pen); + + /// + /// Draws a prepared text block onto this canvas. + /// + /// The prepared text block to draw. + /// The drawing location in local canvas coordinates. + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// Optional brush used to fill glyphs. + /// Optional pen used to outline glyphs. + public abstract void DrawText( + TextBlock textBlock, + PointF location, + float wrappingLength, + Brush? brush, + Pen? pen); + + /// + /// Draws a prepared text block along a path baseline onto this canvas. + /// + /// The prepared text block to draw. + /// The path used as the text baseline in local canvas coordinates. + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// Optional brush used to fill glyphs. + /// Optional pen used to outline glyphs. + public abstract void DrawText( + TextBlock textBlock, + IPath path, + float wrappingLength, + Brush? brush, + Pen? pen); + + /// + /// Draws one prepared line layout onto this canvas. + /// + /// The prepared line layout to draw. + /// The drawing location in local canvas coordinates. + /// Optional brush used to fill glyphs. + /// Optional pen used to outline glyphs. + public abstract void DrawText( + LineLayout lineLayout, + PointF location, + Brush? brush, + Pen? pen); + + /// + /// Draws one prepared line layout along a path baseline onto this canvas. + /// + /// The prepared line layout to draw. + /// The path used as the text baseline in local canvas coordinates. + /// Optional brush used to fill glyphs. + /// Optional pen used to outline glyphs. + public abstract void DrawText( + LineLayout lineLayout, + IPath path, + Brush? brush, + Pen? pen); + + /// + /// Draws layered glyph geometry. + /// + /// Brush used to fill glyph layers. + /// Pen used to outline dominant painted layers. + /// Layered glyph geometry to draw. + public abstract void DrawGlyphs( + Brush brush, + Pen pen, + IEnumerable glyphs); + + /// + /// Measures the full set of layout metrics for the supplied text. + /// + /// The text shaping and layout options. + /// The text to measure. + /// A value containing the metrics for the laid-out text. + public abstract TextMetrics MeasureText(RichTextOptions textOptions, ReadOnlySpan text); + + /// + /// Draws an image source region into a destination rectangle. + /// + /// The source image. + /// The source rectangle within . + /// The destination rectangle in local canvas coordinates. + /// + /// Optional resampler used when scaling or transforming the image. Defaults to . + /// + public abstract void DrawImage( + Image image, + Rectangle sourceRect, + RectangleF destinationRect, + IResampler? sampler = null); + + /// + /// Creates a retained backend scene from the drawing commands currently queued on this canvas. + /// + /// A retained backend scene. + public abstract DrawingBackendScene CreateScene(); + + /// + /// Renders a retained backend scene into this canvas target. + /// + /// The retained backend scene to render. + public abstract void RenderScene(DrawingBackendScene scene); + + /// + /// Seals queued drawing commands into the canvas timeline. + /// + public abstract void Flush(); + + /// + public abstract void Dispose(); + } +} diff --git a/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs b/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs new file mode 100644 index 0000000..28b0575 --- /dev/null +++ b/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs @@ -0,0 +1,485 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Queues normalized composition commands emitted by + /// and prepares them in deterministic draw order. + /// + /// + /// The batcher owns command buffering and replay ordering only; it does not rasterize or composite. + /// Draw commands are stored in the command buffer until a timeline command-range entry references + /// them. Existing retained scenes passed through are stored + /// separately and referenced by timeline entry index. During disposal replay, command ranges are + /// lowered to short-lived backend scenes at the position where the canvas recorded the range. + /// + internal sealed class DrawingCanvasBatcher + where TPixel : unmanaged, IPixel + { + private readonly Configuration configuration; + + // Draw commands stay in this buffer until replay lowers referenced command ranges + // into backend scenes at their recorded timeline position. + private CompositionSceneCommand[] commands; + private int commandCount; + private int sealedCommandCount; + + // Layer metadata is range-sensitive, so sealing advances this alongside command + // sealing instead of letting layer state leak across later command ranges. + private int layerCommandCount; + private int sealedLayerCommandCount; + + // Clip and dash flags gate whole-buffer command preparation; prepared commands + // remain in the same command buffer until replay consumes it. + private bool hasClips; + private bool hasDashes; + + // Timeline entries keep compact indexes into the command, barrier, and retained + // scene buffers while preserving the order recorded by the canvas. + private DrawingCanvasTimelineEntry[] entries; + + // Apply barriers carry replay-time target read/process/write operations. + private ApplyBarrier[] applyBarriers; + private int applyBarrierCount; + + // These are existing retained scenes recorded through RenderScene, not scenes + // produced later from this batcher's own command ranges. + private DrawingBackendScene[] insertedScenes; + private int insertedSceneCount; + + internal DrawingCanvasBatcher(Configuration configuration) + { + this.configuration = configuration; + this.commands = []; + this.entries = []; + this.applyBarriers = []; + this.insertedScenes = []; + } + + /// + /// Gets a value indicating whether there are queued commands or timeline entries. + /// + public bool HasRecordedWork => this.commandCount > 0 || this.TimelineEntryCount > 0; + + /// + /// Gets the number of ordered replay items recorded in the canvas timeline. + /// + /// + /// This is not a draw-command count. A single entry can represent a contiguous command range, + /// an apply barrier, or an inserted retained scene. + /// + public int TimelineEntryCount { get; private set; } + + /// + /// Appends one normalized composition command to the pending queue. + /// + /// The command to queue. + public void AddComposition(in CompositionCommand composition) + { + this.EnsureCommandCapacity(this.commandCount + 1); + this.commands[this.commandCount++] = new PathCompositionSceneCommand(composition); + + if (composition.Kind is not CompositionCommandKind.FillLayer) + { + this.layerCommandCount++; + } + + this.hasClips |= composition.ClipPaths is not null; + } + + /// + /// Appends one stroked path command to the pending queue. + /// + /// The command to queue. + public void AddStrokePath(in StrokePathCommand command) + { + this.EnsureCommandCapacity(this.commandCount + 1); + this.commands[this.commandCount++] = new StrokePathCompositionSceneCommand(command); + this.hasClips |= command.ClipPaths is not null; + this.hasDashes |= command.Pen.StrokePattern.Length >= 2; + } + + /// + /// Appends one explicit stroked line-segment command to the pending queue. + /// + /// The command to queue. + public void AddStrokeLineSegment(in StrokeLineSegmentCommand command) + { + this.EnsureCommandCapacity(this.commandCount + 1); + this.commands[this.commandCount++] = new LineSegmentCompositionSceneCommand(command); + } + + /// + /// Appends one explicit stroked polyline command to the pending queue. + /// + /// The command to queue. + public void AddStrokePolyline(in StrokePolylineCommand command) + { + this.EnsureCommandCapacity(this.commandCount + 1); + this.commands[this.commandCount++] = new PolylineCompositionSceneCommand(command); + } + + /// + /// Seals currently queued commands into the replay timeline. + /// + /// + /// This records a command range only. Backend scenes are created later by the replay path + /// from the referenced command range, so sealing does not render or allocate backend scene state. + /// + public void SealCommands() + { + int count = this.commandCount - this.sealedCommandCount; + if (count == 0) + { + return; + } + + this.EnsureEntryCapacity(this.TimelineEntryCount + 1); + this.entries[this.TimelineEntryCount++] = DrawingCanvasTimelineEntry.CreateCommandRange( + this.sealedCommandCount, + count, + this.layerCommandCount != this.sealedLayerCommandCount); + + this.sealedCommandCount = this.commandCount; + this.sealedLayerCommandCount = this.layerCommandCount; + } + + /// + /// Appends an apply barrier to the replay timeline after sealing queued commands. + /// + /// The apply barrier to append. + internal void AddApplyBarrier(ApplyBarrier barrier) + { + this.SealCommands(); + this.EnsureApplyBarrierCapacity(this.applyBarrierCount + 1); + + int barrierIndex = this.applyBarrierCount; + this.applyBarriers[this.applyBarrierCount++] = barrier; + this.EnsureEntryCapacity(this.TimelineEntryCount + 1); + this.entries[this.TimelineEntryCount++] = DrawingCanvasTimelineEntry.CreateApplyBarrier(barrierIndex); + } + + /// + /// Records an existing retained scene in the replay timeline after sealing queued commands. + /// + /// + /// This stores only scenes passed to . Scenes produced + /// from this canvas's own command ranges are created later by the backend from command batches. + /// + /// The retained scene to render at this point in the timeline. + public void AddScene(DrawingBackendScene scene) + { + this.SealCommands(); + this.EnsureInsertedSceneCapacity(this.insertedSceneCount + 1); + + int sceneIndex = this.insertedSceneCount; + this.insertedScenes[this.insertedSceneCount++] = scene; + this.EnsureEntryCapacity(this.TimelineEntryCount + 1); + this.entries[this.TimelineEntryCount++] = DrawingCanvasTimelineEntry.CreateScene(sceneIndex); + } + + /// + /// Creates a retained backend scene from the recorded timeline. + /// + /// The backend used to create the retained scene. + /// The target bounds used for target-dependent scene creation. + /// The resources that must stay alive for the returned scene. + /// The retained backend scene. + public DrawingBackendScene CreateScene( + IDrawingBackend backend, + Rectangle targetBounds, + IReadOnlyList? ownedResources) + { + if (!this.HasRecordedWork) + { + throw new InvalidOperationException("Cannot create a retained scene from an empty canvas."); + } + + this.SealAndPrepareCommands(); + + return backend.CreateScene( + this.configuration, + targetBounds, + new DrawingCommandBatch(this.commands, this.commandCount, this.layerCommandCount > 0), + ownedResources); + } + + /// + /// Seals any pending commands and prepares queued command data for backend scene creation. + /// + public void SealAndPrepareCommands() + { + this.SealCommands(); + + this.PrepareCommands(); + } + + /// + /// Creates a command batch over one recorded command-range timeline entry. + /// + /// The command-range timeline entry. + /// The command batch. + public DrawingCommandBatch CreateCommandBatch(DrawingCanvasTimelineEntry entry) + => new(this.commands, entry.Index, entry.Count, entry.HasLayers); + + /// + /// Gets one recorded timeline entry. + /// + /// The entry index. + /// The recorded timeline entry. + public DrawingCanvasTimelineEntry GetEntry(int index) + => this.entries[index]; + + /// + /// Gets one recorded apply barrier. + /// + /// The apply-barrier index. + /// The recorded apply barrier. + internal ApplyBarrier GetApplyBarrier(int index) + => this.applyBarriers[index]; + + /// + /// Gets one retained scene reference recorded through . + /// + /// The retained-scene reference index. + /// The retained scene to render at the timeline entry. + public DrawingBackendScene GetInsertedScene(int index) + => this.insertedScenes[index]; + + /// + /// Clears command references after a prepared batch has been consumed. + /// + public void ClearCommandBatch() + { + Array.Clear(this.commands, 0, this.commandCount); + Array.Clear(this.entries, 0, this.TimelineEntryCount); + Array.Clear(this.applyBarriers, 0, this.applyBarrierCount); + Array.Clear(this.insertedScenes, 0, this.insertedSceneCount); + this.commandCount = 0; + this.sealedCommandCount = 0; + this.layerCommandCount = 0; + this.sealedLayerCommandCount = 0; + this.TimelineEntryCount = 0; + this.applyBarrierCount = 0; + this.insertedSceneCount = 0; + this.hasClips = false; + this.hasDashes = false; + } + + /// + /// Ensures that the command buffer can store the requested command count without reallocating. + /// + /// The required command capacity. + private void EnsureCommandCapacity(int requiredCapacity) + { + if (requiredCapacity <= this.commands.Length) + { + return; + } + + int nextCapacity = this.commands.Length == 0 ? 16 : this.commands.Length * 2; + if (nextCapacity < requiredCapacity) + { + nextCapacity = requiredCapacity; + } + + Array.Resize(ref this.commands, nextCapacity); + } + + /// + /// Ensures that the timeline entry buffer can store the requested entry count without reallocating. + /// + /// The required entry capacity. + private void EnsureEntryCapacity(int requiredCapacity) + { + if (requiredCapacity <= this.entries.Length) + { + return; + } + + int nextCapacity = this.entries.Length == 0 ? 4 : this.entries.Length * 2; + if (nextCapacity < requiredCapacity) + { + nextCapacity = requiredCapacity; + } + + Array.Resize(ref this.entries, nextCapacity); + } + + /// + /// Ensures that the apply-barrier buffer can store the requested barrier count without reallocating. + /// + /// The required barrier capacity. + private void EnsureApplyBarrierCapacity(int requiredCapacity) + { + if (requiredCapacity <= this.applyBarriers.Length) + { + return; + } + + int nextCapacity = this.applyBarriers.Length == 0 ? 2 : this.applyBarriers.Length * 2; + if (nextCapacity < requiredCapacity) + { + nextCapacity = requiredCapacity; + } + + Array.Resize(ref this.applyBarriers, nextCapacity); + } + + /// + /// Ensures that the inserted-scene buffer can store the requested scene count without reallocating. + /// + /// The required scene capacity. + private void EnsureInsertedSceneCapacity(int requiredCapacity) + { + if (requiredCapacity <= this.insertedScenes.Length) + { + return; + } + + int nextCapacity = this.insertedScenes.Length == 0 ? 2 : this.insertedScenes.Length * 2; + if (nextCapacity < requiredCapacity) + { + nextCapacity = requiredCapacity; + } + + Array.Resize(ref this.insertedScenes, nextCapacity); + } + + private void PrepareCommands() + { + if (!this.hasClips && !this.hasDashes) + { + return; + } + + // If clipping is present we need to apply that now before handing the command + // to the backend. This avoids complicating the backend with clipping logic + // and allows us to reuse the same optimized backend code for clipped and unclipped paths. + int requestedParallelism = this.configuration.MaxDegreeOfParallelism; + int partitionCount = ParallelExecutionHelper.GetPartitionCount(requestedParallelism, this.commandCount); + + if (partitionCount <= 1) + { + for (int i = 0; i < this.commandCount; i++) + { + PrepareCommand(ref this.commands[i]); + } + + return; + } + + _ = Parallel.For( + 0, + partitionCount, + ParallelExecutionHelper.CreateParallelOptions(requestedParallelism, partitionCount), + partitionIndex => + { + // Integer division splits the commands into contiguous half-open ranges, + // keeping the partitions balanced while assigning each command exactly once. + int commandStart = (partitionIndex * this.commandCount) / partitionCount; + int commandEnd = ((partitionIndex + 1) * this.commandCount) / partitionCount; + + for (int i = commandStart; i < commandEnd; i++) + { + PrepareCommand(ref this.commands[i]); + } + }); + } + + private static void PrepareCommand(ref CompositionSceneCommand command) + { + if (command is PathCompositionSceneCommand pathCommand) + { + CompositionCommand composition = pathCommand.Command; + if (composition.ClipPaths is { Count: > 0 }) + { + IPath path = composition.SourcePath; + DrawingOptions sourceOptions = composition.DrawingOptions; + + if (sourceOptions.Transform != Matrix4x4.Identity) + { + path = path.Transform(sourceOptions.Transform); + } + + path = path.Clip(sourceOptions.ShapeOptions, composition.ClipPaths); + + RasterizerOptions rasterizerOptions = composition.RasterizerOptions; + DrawingOptions preparedOptions = WithIdentityTransform(sourceOptions); + + // Update the command with the clipped path. + pathCommand.Command = CompositionCommand.Create( + path, + composition.Brush.Transform(sourceOptions.Transform), + preparedOptions, + in rasterizerOptions, + composition.TargetBounds, + composition.DestinationOffset, + null, + composition.IsInsideLayer); + } + } + else if (command is StrokePathCompositionSceneCommand strokePathCommand) + { + StrokePathCommand composition = strokePathCommand.Command; + + if (composition.ClipPaths is { Count: > 0 }) + { + IPath path = composition.Pen.GeneratePath(composition.SourcePath); + DrawingOptions sourceOptions = composition.DrawingOptions; + + if (sourceOptions.Transform != Matrix4x4.Identity) + { + path = path.Transform(sourceOptions.Transform); + } + + path = path.Clip(sourceOptions.ShapeOptions, composition.ClipPaths); + + RasterizerOptions rasterizerOptions = composition.RasterizerOptions; + DrawingOptions preparedOptions = WithIdentityTransform(sourceOptions); + + command = new PathCompositionSceneCommand( + CompositionCommand.Create( + path, + composition.Brush.Transform(sourceOptions.Transform), + preparedOptions, + in rasterizerOptions, + composition.TargetBounds, + composition.DestinationOffset, + null, + composition.IsInsideLayer)); + } + else + { + // We need to dash the path here before sending it to the backend. + Pen pen = composition.Pen; + if (pen.StrokePattern.Length >= 2) + { + strokePathCommand.Command = new StrokePathCommand( + composition.SourcePath.GenerateDashes(pen.StrokeWidth, pen.StrokePattern.Span), + composition.Brush, + composition.DrawingOptions, + composition.RasterizerOptions, + composition.TargetBounds, + composition.DestinationOffset, + composition.Pen, + null, + composition.IsInsideLayer); + } + } + } + } + + private static DrawingOptions WithIdentityTransform(DrawingOptions source) + => source.Transform == Matrix4x4.Identity + ? source + : new DrawingOptions(source.GraphicsOptions, source.ShapeOptions, Matrix4x4.Identity); + } +} diff --git a/ImageSharp.Drawing/Processing/DrawingCanvasFactoryExtensions.cs b/ImageSharp.Drawing/Processing/DrawingCanvasFactoryExtensions.cs new file mode 100644 index 0000000..066684e --- /dev/null +++ b/ImageSharp.Drawing/Processing/DrawingCanvasFactoryExtensions.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Extension methods for creating drawing canvas instances over ImageSharp image frames. + /// + public static class DrawingCanvasFactoryExtensions + { + /// + /// Creates a drawing canvas over an existing typed image frame. + /// + /// + /// The caller owns the returned canvas and must dispose it to replay recorded work into the frame. + /// + /// The pixel format. + /// The frame backing the canvas. + /// The configuration to use for this canvas instance. + /// Initial drawing options for this canvas instance. + /// Initial clip paths for this canvas instance. + /// A drawing canvas targeting . + public static DrawingCanvas CreateCanvas( + this ImageFrame frame, + Configuration configuration, + DrawingOptions options, + params IPath[] clipPaths) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(frame, nameof(frame)); + Guard.NotNull(options, nameof(options)); + Guard.NotNull(clipPaths, nameof(clipPaths)); + + return new DrawingCanvas( + configuration, + options, + frame.PixelBuffer.GetRegion(), + clipPaths); + } + + /// + /// Creates a drawing canvas over an existing image frame. + /// + /// + /// The caller owns the returned canvas and must dispose it to replay recorded work into the frame. + /// + /// The frame backing the canvas. + /// The configuration to use for this canvas instance. + /// Initial drawing options for this canvas instance. + /// Initial clip paths for this canvas instance. + /// A drawing canvas targeting . + public static DrawingCanvas CreateCanvas( + this ImageFrame frame, + Configuration configuration, + DrawingOptions options, + params IPath[] clipPaths) + { + Guard.NotNull(frame, nameof(frame)); + Guard.NotNull(options, nameof(options)); + Guard.NotNull(clipPaths, nameof(clipPaths)); + + CanvasFactoryVisitor visitor = new(configuration, options, clipPaths); + frame.AcceptVisitor(visitor); + return visitor.Value!; + } + + private struct CanvasFactoryVisitor : IImageFrameVisitor + { + private readonly Configuration configuration; + private readonly DrawingOptions options; + private readonly IPath[] clipPaths; + + public CanvasFactoryVisitor(Configuration configuration, DrawingOptions options, IPath[] clipPaths) + { + this.configuration = configuration; + this.options = options; + this.clipPaths = clipPaths; + } + + public DrawingCanvas? Value { get; private set; } + + void IImageFrameVisitor.Visit(ImageFrame frame) + => this.Value = frame.CreateCanvas(this.configuration, this.options, this.clipPaths); + } + } +} diff --git a/ImageSharp.Drawing/Processing/DrawingCanvasState.cs b/ImageSharp.Drawing/Processing/DrawingCanvasState.cs new file mode 100644 index 0000000..7c4b9d7 --- /dev/null +++ b/ImageSharp.Drawing/Processing/DrawingCanvasState.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Immutable drawing state snapshot used by . + /// + internal sealed class DrawingCanvasState + { + /// + /// Initializes a new instance of the class. + /// + /// Drawing options for this state. + /// Clip paths for this state. + /// Absolute target bounds used for commands recorded in this state. + /// Absolute destination offset for paths recorded in local canvas coordinates. + public DrawingCanvasState( + DrawingOptions options, + IReadOnlyList clipPaths, + Rectangle targetBounds, + Point destinationOffset) + { + this.Options = options; + this.ClipPaths = clipPaths; + this.TargetBounds = targetBounds; + this.DestinationOffset = destinationOffset; + } + + /// + /// Gets drawing options associated with this state. + /// + /// + /// This is the original reference supplied to the state. + /// It is not deep-cloned. + /// + public DrawingOptions Options { get; } + + /// + /// Gets clip paths associated with this state. + /// + public IReadOnlyList ClipPaths { get; } + + /// + /// Gets the absolute target bounds used for commands recorded in this state. + /// + public Rectangle TargetBounds { get; } + + /// + /// Gets the absolute destination offset for paths recorded in local canvas coordinates. + /// + public Point DestinationOffset { get; } + + /// + /// Gets a value indicating whether this state represents a compositing layer. + /// + public bool IsLayer { get; init; } + + /// + /// Gets the layer compositing options when this state represents a compositing layer. + /// + public GraphicsOptions? LayerOptions { get; init; } + } +} diff --git a/ImageSharp.Drawing/Processing/DrawingCanvasTimelineEntry.cs b/ImageSharp.Drawing/Processing/DrawingCanvasTimelineEntry.cs new file mode 100644 index 0000000..81dc35e --- /dev/null +++ b/ImageSharp.Drawing/Processing/DrawingCanvasTimelineEntry.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Identifies the kind of replay item stored in a drawing canvas timeline. + /// + internal enum DrawingCanvasTimelineEntryKind + { + /// + /// A contiguous range of draw commands. + /// + CommandRange, + + /// + /// An apply barrier. + /// + ApplyBarrier, + + /// + /// An existing retained scene recorded through . + /// + Scene + } + + /// + /// Represents one ordered item in the canvas replay timeline. + /// + /// + /// Command ranges reference contiguous draw commands; they are not backend scene objects yet. + /// Apply barriers and retained scene references point into side buffers by index, keeping this + /// type compact while preserving the exact order in which the canvas recorded replay work. + /// + internal readonly struct DrawingCanvasTimelineEntry + { + private DrawingCanvasTimelineEntry( + DrawingCanvasTimelineEntryKind kind, + int index, + int count, + bool hasLayers) + { + this.Kind = kind; + this.Index = index; + this.Count = count; + this.HasLayers = hasLayers; + } + + /// + /// Gets the kind of replay item represented by this entry. + /// + public DrawingCanvasTimelineEntryKind Kind { get; } + + /// + /// Gets the command start index for command ranges, or the side-buffer index for barriers and scenes. + /// + public int Index { get; } + + /// + /// Gets the number of commands represented by a command-range entry. + /// + public int Count { get; } + + /// + /// Gets a value indicating whether the command range contains layer boundary commands. + /// + public bool HasLayers { get; } + + /// + /// Creates a command-range entry. + /// + /// The first command index. + /// The command count. + /// Indicates whether the command range contains layer boundary commands. + /// The command-range entry. + public static DrawingCanvasTimelineEntry CreateCommandRange(int startIndex, int count, bool hasLayers) + => new(DrawingCanvasTimelineEntryKind.CommandRange, startIndex, count, hasLayers); + + /// + /// Creates an apply-barrier entry. + /// + /// The apply-barrier index. + /// The apply-barrier entry. + public static DrawingCanvasTimelineEntry CreateApplyBarrier(int index) + => new(DrawingCanvasTimelineEntryKind.ApplyBarrier, index, 0, false); + + /// + /// Creates an entry for an existing retained scene recorded through . + /// + /// The retained-scene reference index. + /// The retained-scene entry. + public static DrawingCanvasTimelineEntry CreateScene(int index) + => new(DrawingCanvasTimelineEntryKind.Scene, index, 0, false); + } +} diff --git a/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs new file mode 100644 index 0000000..91133e4 --- /dev/null +++ b/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -0,0 +1,1641 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.Fonts; +using SixLabors.Fonts.Rendering; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Processing.Processors.Text; +using SixLabors.ImageSharp.Drawing.Text; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// A drawing canvas over a frame target. + /// + /// The pixel format. + public sealed class DrawingCanvas : DrawingCanvas + where TPixel : unmanaged, IPixel + { + /// + /// Processing configuration used by operations executed through this canvas. + /// + private readonly Configuration configuration; + + /// + /// Backend responsible for rasterizing and composing draw commands. + /// + private readonly IDrawingBackend backend; + + /// + /// Destination frame receiving rendered output. + /// + private readonly ICanvasFrame targetFrame; + + /// + /// Command batcher used to defer and submit composition commands. + /// + private readonly DrawingCanvasBatcher batcher; + + /// + /// Temporary image resources that must stay alive until queued commands are flushed. + /// + private readonly List> pendingImageResources = []; + + /// + /// Indicates whether this canvas owns final disposal of the shared batcher. + /// + private readonly bool ownsBatcher; + + /// + /// Tracks whether this instance has already been disposed. + /// + private bool isDisposed; + + /// + /// Stack of saved drawing states for Save/Restore operations. + /// + private readonly Stack savedStates = new(); + + // Per-canvas glyph-outline cache: hoists RichTextGlyphRenderer's per-glyph outline cache from + // per-DrawText-call scope up to the whole canvas, so a glyph outline built once is reused by + // every DrawText call on this canvas (across a frame's many text runs) instead of being + // rebuilt for every run on a text-heavy page. + private readonly Dictionary> glyphCache = []; + + /// + /// Initializes a new instance of the class. + /// + /// The active processing configuration. + /// Initial drawing options for this canvas instance. + /// The destination target region. + /// Initial clip paths for this canvas instance. + public DrawingCanvas( + Configuration configuration, + DrawingOptions options, + Buffer2DRegion targetRegion, + params IPath[] clipPaths) + : this(configuration, options, new MemoryCanvasFrame(targetRegion), clipPaths) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The active processing configuration. + /// Initial drawing options for this canvas instance. + /// The destination frame. + /// Initial clip paths for this canvas instance. + public DrawingCanvas( + Configuration configuration, + DrawingOptions options, + ICanvasFrame targetFrame, + params IPath[] clipPaths) + : this(configuration, options, configuration.GetDrawingBackend(), targetFrame, clipPaths) + { + } + + /// + /// Initializes a new instance of the class with an explicit backend and initial state. + /// + /// The active processing configuration. + /// Initial drawing options for this canvas instance. + /// The drawing backend implementation. + /// The destination frame. + /// Initial clip paths for this canvas instance. + public DrawingCanvas( + Configuration configuration, + DrawingOptions options, + IDrawingBackend backend, + ICanvasFrame targetFrame, + params IPath[] clipPaths) + : this( + configuration, + backend, + targetFrame, + new DrawingCanvasBatcher(configuration), + new DrawingCanvasState(options, clipPaths, targetFrame.Bounds, targetFrame.Bounds.Location), + true) + { + } + + /// + /// Initializes a new instance of the class + /// with explicit backend and batcher instances. + /// + /// The active processing configuration. + /// The drawing backend implementation. + /// The destination frame. + /// The command batcher used for deferred composition. + /// The default state used when no scoped state is active. + /// Whether this canvas owns final disposal of the shared batcher. + private DrawingCanvas( + Configuration configuration, + IDrawingBackend backend, + ICanvasFrame targetFrame, + DrawingCanvasBatcher batcher, + DrawingCanvasState defaultState, + bool ownsBatcher) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(backend, nameof(backend)); + Guard.NotNull(targetFrame, nameof(targetFrame)); + Guard.NotNull(batcher, nameof(batcher)); + Guard.NotNull(defaultState, nameof(defaultState)); + + if (!targetFrame.TryGetCpuRegion(out _) && !targetFrame.TryGetNativeSurface(out _)) + { + throw new NotSupportedException("Canvas frame must expose either a CPU region or a native surface."); + } + + this.configuration = configuration; + this.backend = backend; + this.targetFrame = targetFrame; + this.batcher = batcher; + this.ownsBatcher = ownsBatcher; + + // Canvas coordinates are local to the current frame; origin stays at (0,0). + this.Bounds = new Rectangle(0, 0, targetFrame.Bounds.Width, targetFrame.Bounds.Height); + this.savedStates.Push(defaultState); + } + + /// + public override Rectangle Bounds { get; } + + /// + public override int SaveCount => this.savedStates.Count; + + /// + public override int Save() + { + this.EnsureNotDisposed(); + DrawingCanvasState current = this.ResolveState(); + + // Push a non-layer copy of the current state. + // Only states pushed by SaveLayer() should trigger layer compositing on restore. + this.savedStates.Push(new DrawingCanvasState(current.Options, current.ClipPaths, current.TargetBounds, current.DestinationOffset)); + return this.savedStates.Count; + } + + /// + public override int Save(DrawingOptions options, params IPath[] clipPaths) + => this.SaveCore(options, clipPaths); + + private int SaveCore(DrawingOptions options, IReadOnlyList clipPaths) + { + this.EnsureNotDisposed(); + Guard.NotNull(options, nameof(options)); + Guard.NotNull(clipPaths, nameof(clipPaths)); + + _ = this.Save(); + DrawingCanvasState current = this.ResolveState(); + DrawingCanvasState state = new(options, clipPaths, current.TargetBounds, current.DestinationOffset); + _ = this.savedStates.Pop(); + this.savedStates.Push(state); + return this.savedStates.Count; + } + + /// + public override int SaveLayer(GraphicsOptions layerOptions, Rectangle bounds) + { + this.EnsureNotDisposed(); + Guard.NotNull(layerOptions, nameof(layerOptions)); + Guard.MustBeGreaterThan(bounds.Width, 0, nameof(bounds)); + Guard.MustBeGreaterThan(bounds.Height, 0, nameof(bounds)); + + DrawingCanvasState currentState = this.ResolveState(); + Rectangle absoluteLayerBounds = ResolveLayerBounds(currentState, bounds); + + // Keep layer boundaries in the shared command stream so the backend can lower them inline. + this.batcher.AddComposition(CompositionCommand.CreateBeginLayer(absoluteLayerBounds, layerOptions)); + + // A bounded layer clips and allocates the isolated target, but it does not shift the canvas coordinate system. + DrawingCanvasState layerState = new(currentState.Options, currentState.ClipPaths, absoluteLayerBounds, currentState.DestinationOffset) + { + IsLayer = true, + LayerOptions = layerOptions, + }; + + this.savedStates.Push(layerState); + return this.savedStates.Count; + } + + /// + public override void Restore() + { + this.EnsureNotDisposed(); + if (this.savedStates.Count <= 1) + { + return; + } + + DrawingCanvasState popped = this.savedStates.Pop(); + if (popped.IsLayer) + { + this.batcher.AddComposition(CompositionCommand.CreateEndLayer(popped.TargetBounds, popped.LayerOptions!)); + } + } + + /// + public override void RestoreTo(int saveCount) + { + this.EnsureNotDisposed(); + Guard.MustBeBetweenOrEqualTo(saveCount, 1, this.savedStates.Count, nameof(saveCount)); + + this.RestoreToCore(saveCount); + } + + /// + public override DrawingCanvas CreateRegion(Rectangle region) + { + this.EnsureNotDisposed(); + + Rectangle clipped = Rectangle.Intersect(this.Bounds, region); + CanvasRegionFrame childFrame = new(this.targetFrame, clipped); + DrawingCanvasState currentState = this.ResolveState(); + + // Regions share the same batcher and deferred image resources. Only the root canvas owns flushing. + return new DrawingCanvas( + this.configuration, + this.backend, + childFrame, + this.batcher, + new DrawingCanvasState(currentState.Options, currentState.ClipPaths, childFrame.Bounds, childFrame.Bounds.Location) + { + IsLayer = currentState.IsLayer, + LayerOptions = currentState.LayerOptions, + }, + false); + } + + /// + public override void Clear(Brush brush, IPath path) + { + DrawingCanvasState state = this.ResolveState(); + DrawingOptions options = state.Options.CloneForClearOperation(); + this.ExecuteWithTemporaryState(options, state.ClipPaths, () => this.Fill(brush, path)); + } + + /// + public override void Fill(Brush brush, IPath path) + { + this.EnsureNotDisposed(); + Guard.NotNull(path, nameof(path)); + Guard.NotNull(brush, nameof(brush)); + this.EnqueueFillPath(brush, path); + } + + /// + public override void Apply(Rectangle region, Action operation) + => this.Apply(new RectanglePolygon(region), operation); + + /// + public override void Apply(PathBuilder pathBuilder, Action operation) + { + Guard.NotNull(pathBuilder, nameof(pathBuilder)); + this.Apply(pathBuilder.Build(), operation); + } + + /// + public override void Apply(IPath path, Action operation) + { + this.EnsureNotDisposed(); + Guard.NotNull(path, nameof(path)); + Guard.NotNull(operation, nameof(operation)); + + DrawingCanvasState state = this.ResolveState(); + ApplyBarrier barrier = new( + path.AsClosedPath(), + state.Options, + state.ClipPaths, + this.Bounds, + state.TargetBounds, + state.DestinationOffset, + state.IsLayer, + operation); + + this.batcher.AddApplyBarrier(barrier); + } + + /// + /// Draws a two-point line segment using the provided pen and drawing options. + /// + /// Pen used to generate the line outline. + /// Line start point. + /// Line end point. + public void DrawLine(Pen pen, PointF start, PointF end) + { + this.EnsureNotDisposed(); + Guard.NotNull(pen, nameof(pen)); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; + + // Stroke geometry can self-overlap; non-zero winding preserves stroke semantics. + if (effectiveOptions.ShapeOptions.IntersectionRule != IntersectionRule.NonZero) + { + ShapeOptions shapeOptions = effectiveOptions.ShapeOptions.DeepClone(); + shapeOptions.IntersectionRule = IntersectionRule.NonZero; + effectiveOptions = new DrawingOptions(effectiveOptions.GraphicsOptions, shapeOptions, effectiveOptions.Transform); + } + + if (state.ClipPaths.Count > 0 || !pen.StrokePattern.IsEmpty) + { + this.PrepareCompositionCore( + new Path([start, end]), + pen.StrokeFill, + effectiveOptions, + RasterizerSamplingOrigin.PixelCenter, + state.ClipPaths, + pen); + return; + } + + this.PrepareStrokeLineSegmentCompositionCore(start, end, pen.StrokeFill, effectiveOptions, pen); + } + + /// + public override void DrawLine(Pen pen, params PointF[] points) + { + Guard.NotNull(points, nameof(points)); + + if (points.Length == 2) + { + this.DrawLine(pen, points[0], points[1]); + return; + } + + this.EnsureNotDisposed(); + Guard.NotNull(pen, nameof(pen)); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; + + // Stroke geometry can self-overlap; non-zero winding preserves stroke semantics. + if (effectiveOptions.ShapeOptions.IntersectionRule != IntersectionRule.NonZero) + { + ShapeOptions shapeOptions = effectiveOptions.ShapeOptions.DeepClone(); + shapeOptions.IntersectionRule = IntersectionRule.NonZero; + effectiveOptions = new DrawingOptions(effectiveOptions.GraphicsOptions, shapeOptions, effectiveOptions.Transform); + } + + if (state.ClipPaths.Count > 0 || !pen.StrokePattern.IsEmpty) + { + this.PrepareCompositionCore( + new Path(points), + pen.StrokeFill, + effectiveOptions, + RasterizerSamplingOrigin.PixelCenter, + state.ClipPaths, + pen); + return; + } + + this.PrepareStrokePolylineCompositionCore(points, pen.StrokeFill, effectiveOptions, pen); + } + + /// + public override void Draw(Pen pen, IPath path) + { + this.EnsureNotDisposed(); + Guard.NotNull(pen, nameof(pen)); + Guard.NotNull(path, nameof(path)); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; + + // Stroke geometry can self-overlap; non-zero winding preserves stroke semantics. + if (effectiveOptions.ShapeOptions.IntersectionRule != IntersectionRule.NonZero) + { + ShapeOptions shapeOptions = effectiveOptions.ShapeOptions.DeepClone(); + shapeOptions.IntersectionRule = IntersectionRule.NonZero; + effectiveOptions = new DrawingOptions(effectiveOptions.GraphicsOptions, shapeOptions, effectiveOptions.Transform); + } + + this.PrepareCompositionCore( + path, + pen.StrokeFill, + effectiveOptions, + RasterizerSamplingOrigin.PixelCenter, + state.ClipPaths, + pen); + } + + /// + public override void DrawText( + RichTextOptions textOptions, + ReadOnlySpan text, + Brush? brush, + Pen? pen) + => this.DrawTextCore(textOptions, text, path: null, brush, pen); + + /// + public override void DrawText( + RichTextOptions textOptions, + ReadOnlySpan text, + IPath path, + Brush? brush, + Pen? pen) + { + Guard.NotNull(path, nameof(path)); + this.DrawTextCore(textOptions, text, path, brush, pen); + } + + private void DrawTextCore( + RichTextOptions textOptions, + ReadOnlySpan text, + IPath? path, + Brush? brush, + Pen? pen) + { + this.EnsureNotDisposed(); + + if (text.IsEmpty) + { + return; + } + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; + + EnsureTextPaint(brush, pen); + + RichTextOptions configuredOptions = ConfigureTextOptions(textOptions, path, out IPath? configuredPath); + using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, configuredPath, pen, brush, this.glyphCache); + TextRenderer renderer = new(glyphRenderer); + renderer.RenderText(text, configuredOptions); + + this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions, state.ClipPaths); + } + + /// + public override void DrawText( + TextBlock textBlock, + PointF location, + float wrappingLength, + Brush? brush, + Pen? pen) + { + this.EnsureNotDisposed(); + Guard.NotNull(textBlock, nameof(textBlock)); + EnsureTextPaint(brush, pen); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; + + // Prepared text already owns shaping and layout options. The caller-supplied + // location is therefore applied as canvas placement before the active canvas + // transform, instead of mutating text options or rebuilding the block. + DrawingOptions placedOptions = new( + effectiveOptions.GraphicsOptions, + effectiveOptions.ShapeOptions, + Matrix4x4.CreateTranslation(location.X, location.Y, 0) * effectiveOptions.Transform); + + using RichTextGlyphRenderer glyphRenderer = new(placedOptions, path: null, pen, brush, this.glyphCache); + textBlock.RenderTo(glyphRenderer, wrappingLength); + + this.DrawTextOperations(glyphRenderer.DrawingOperations, placedOptions, state.ClipPaths); + } + + /// + public override void DrawText( + TextBlock textBlock, + IPath path, + float wrappingLength, + Brush? brush, + Pen? pen) + { + this.EnsureNotDisposed(); + Guard.NotNull(textBlock, nameof(textBlock)); + Guard.NotNull(path, nameof(path)); + EnsureTextPaint(brush, pen); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; + + using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path, pen, brush, this.glyphCache); + textBlock.RenderTo(glyphRenderer, wrappingLength); + + this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions, state.ClipPaths); + } + + /// + public override void DrawText( + LineLayout lineLayout, + PointF location, + Brush? brush, + Pen? pen) + { + this.EnsureNotDisposed(); + Guard.NotNull(lineLayout, nameof(lineLayout)); + EnsureTextPaint(brush, pen); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; + + // LineLayout represents a single already-broken line. Placement belongs + // to the drawing host, so the line can be reused in arbitrary slots + // without changing the prepared text object. + DrawingOptions placedOptions = new( + effectiveOptions.GraphicsOptions, + effectiveOptions.ShapeOptions, + Matrix4x4.CreateTranslation(location.X, location.Y, 0) * effectiveOptions.Transform); + + using RichTextGlyphRenderer glyphRenderer = new(placedOptions, path: null, pen, brush, this.glyphCache); + lineLayout.RenderTo(glyphRenderer); + + this.DrawTextOperations(glyphRenderer.DrawingOperations, placedOptions, state.ClipPaths); + } + + /// + public override void DrawText( + LineLayout lineLayout, + IPath path, + Brush? brush, + Pen? pen) + { + this.EnsureNotDisposed(); + Guard.NotNull(lineLayout, nameof(lineLayout)); + Guard.NotNull(path, nameof(path)); + EnsureTextPaint(brush, pen); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; + + using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path, pen, brush, this.glyphCache); + lineLayout.RenderTo(glyphRenderer); + + this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions, state.ClipPaths); + } + + /// + public override void DrawGlyphs( + Brush brush, + Pen pen, + IEnumerable glyphs) + { + this.EnsureNotDisposed(); + Guard.NotNull(brush, nameof(brush)); + Guard.NotNull(pen, nameof(pen)); + Guard.NotNull(glyphs, nameof(glyphs)); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions baseOptions = state.Options; + IReadOnlyList clipPaths = state.ClipPaths; + + foreach (GlyphPathCollection glyph in glyphs) + { + if (glyph.LayerCount == 0) + { + continue; + } + + if (glyph.LayerCount == 1) + { + this.Fill(brush, glyph.Paths); + continue; + } + + float glyphArea = glyph.Bounds.Width * glyph.Bounds.Height; + for (int layerIndex = 0; layerIndex < glyph.LayerCount; layerIndex++) + { + GlyphLayerInfo layer = glyph.Layers[layerIndex]; + if (layer.Count == 0) + { + continue; + } + + PathCollection layerPaths = glyph.GetLayerPaths(layerIndex); + DrawingOptions layerOptions = baseOptions.CloneOrReturnForRules( + layer.IntersectionRule, + layer.PixelAlphaCompositionMode, + layer.PixelColorBlendingMode); + + bool shouldFill; + if (layer.Kind is GlyphLayerKind.Decoration or GlyphLayerKind.Glyph) + { + shouldFill = true; + } + else + { + float layerArea = layerPaths.ComputeArea(); + shouldFill = layerArea > 0F && glyphArea > 0F && (layerArea / glyphArea) < 0.50F; + } + + this.ExecuteWithTemporaryState(layerOptions, clipPaths, () => + { + if (shouldFill) + { + this.Fill(brush, layerPaths); + } + else + { + this.Draw(pen, layerPaths); + } + }); + } + } + } + + /// + public override TextMetrics MeasureText(RichTextOptions textOptions, ReadOnlySpan text) + { + this.EnsureNotDisposed(); + return TextMeasurer.Measure(text, textOptions); + } + + /// + public override void DrawImage( + Image image, + Rectangle sourceRect, + RectangleF destinationRect, + IResampler? sampler) + { + this.EnsureNotDisposed(); + Guard.NotNull(image, nameof(image)); + + if (image is Image specificImage) + { + this.DrawImageCore(specificImage, sourceRect, destinationRect, sampler, ownsSourceImage: false); + return; + } + + Image convertedImage = image.CloneAs(); + this.DrawImageCore(convertedImage, sourceRect, destinationRect, sampler, ownsSourceImage: true); + } + + /// + public void DrawImage( + Image image, + Rectangle sourceRect, + RectangleF destinationRect, + IResampler? sampler = null) + { + this.EnsureNotDisposed(); + Guard.NotNull(image, nameof(image)); + this.DrawImageCore(image, sourceRect, destinationRect, sampler, ownsSourceImage: false); + } + + /// + public override DrawingBackendScene CreateScene() + { + this.EnsureNotDisposed(); + + IDisposable[]? ownedResources = this.DetachPendingImageResources(); + + try + { + return this.batcher.CreateScene(this.backend, this.targetFrame.Bounds, ownedResources); + } + catch + { + DisposeOwnedResources(ownedResources); + throw; + } + finally + { + this.batcher.ClearCommandBatch(); + } + } + + /// + public override void RenderScene(DrawingBackendScene scene) + { + this.EnsureNotDisposed(); + Guard.NotNull(scene, nameof(scene)); + this.batcher.AddScene(scene); + } + + private void DrawImageCore( + Image image, + Rectangle sourceRect, + RectangleF destinationRect, + IResampler? sampler, + bool ownsSourceImage) + { + bool disposeSourceImage = ownsSourceImage; + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; + DrawingOptions commandOptions = effectiveOptions; + IReadOnlyList commandClipPaths = state.ClipPaths; + + if (sourceRect.Width <= 0 || + sourceRect.Height <= 0 || + destinationRect.Width <= 0 || + destinationRect.Height <= 0) + { + return; + } + + Rectangle clippedSourceRect = Rectangle.Intersect(sourceRect, image.Bounds); + if (clippedSourceRect.Width <= 0 || clippedSourceRect.Height <= 0) + { + return; + } + + RectangleF clippedDestinationRect = MapSourceClipToDestination(sourceRect, destinationRect, clippedSourceRect); + if (clippedDestinationRect.Width <= 0 || clippedDestinationRect.Height <= 0) + { + return; + } + + Size scaledSize = new( + Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Width)), + Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Height))); + + bool requiresScaling = + clippedSourceRect.Width != scaledSize.Width || + clippedSourceRect.Height != scaledSize.Height; + + Image brushImage = image; + RectangleF brushImageRegion = clippedSourceRect; + RectangleF renderDestinationRect = clippedDestinationRect; + Image? ownedImage = null; + + try + { + // Phase 1: Prepare source pixels (crop/scale) in image-local space. + if (requiresScaling) + { + ownedImage = CreateScaledDrawImage(image, clippedSourceRect, scaledSize, sampler); + brushImage = ownedImage; + brushImageRegion = ownedImage.Bounds; + } + else if (clippedSourceRect != image.Bounds) + { + ownedImage = image.Clone(ctx => ctx.Crop(clippedSourceRect)); + brushImage = ownedImage; + brushImageRegion = ownedImage.Bounds; + } + + // Phase 2: Apply canvas transform to image content when requested. + if (effectiveOptions.Transform != Matrix4x4.Identity) + { + Image transformed = CreateTransformedDrawImage( + brushImage, + clippedDestinationRect, + effectiveOptions.Transform, + sampler, + out renderDestinationRect); + + ownedImage?.Dispose(); + ownedImage = transformed; + brushImage = transformed; + brushImageRegion = transformed.Bounds; + + // The image pixels and destination rect are already in transformed canvas space, + // so the queued fill must not apply the canvas transform a second time. + commandOptions = new DrawingOptions( + effectiveOptions.GraphicsOptions, + effectiveOptions.ShapeOptions, + Matrix4x4.Identity); + commandClipPaths = TransformClipPaths(state.ClipPaths, effectiveOptions.Transform); + } + + if (renderDestinationRect.Width <= 0 || renderDestinationRect.Height <= 0) + { + return; + } + + // Phase 3: Transfer temp-image ownership to deferred batch execution. + if (!ReferenceEquals(brushImage, image)) + { + if (disposeSourceImage) + { + image.Dispose(); + disposeSourceImage = false; + } + + this.pendingImageResources.Add(brushImage); + ownedImage = null; + } + else if (disposeSourceImage) + { + this.pendingImageResources.Add(image); + disposeSourceImage = false; + } + + ImageBrush brush = new(brushImage, brushImageRegion); + IPath destinationPath = new RectanglePolygon( + renderDestinationRect.X, + renderDestinationRect.Y, + renderDestinationRect.Width, + renderDestinationRect.Height); + + this.PrepareCompositionCore( + destinationPath, + brush, + commandOptions, + RasterizerSamplingOrigin.PixelBoundary, + commandClipPaths); + } + finally + { + ownedImage?.Dispose(); + if (disposeSourceImage) + { + image.Dispose(); + } + } + } + + /// + /// Prepares a path fill composition command and enqueues it in the batcher. + /// + /// Path to fill. + /// Brush used for shading. + /// Effective drawing options. + /// Rasterizer sampling origin. + /// Optional clip paths to apply during preparation. + /// Optional pen for stroke commands. + private void PrepareCompositionCore( + IPath path, + Brush brush, + DrawingOptions options, + RasterizerSamplingOrigin samplingOrigin, + IReadOnlyList? clipPaths = null, + Pen? pen = null) + { + brush = this.NormalizeBrush(brush); + + GraphicsOptions graphicsOptions = options.GraphicsOptions; + ShapeOptions shapeOptions = options.ShapeOptions; + RasterizationMode rasterizationMode = graphicsOptions.Antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; + + RectangleF bounds = path.Bounds; + if (samplingOrigin == RasterizerSamplingOrigin.PixelCenter) + { + bounds = new RectangleF(bounds.X + 0.5F, bounds.Y + 0.5F, bounds.Width, bounds.Height); + } + + Rectangle interest = Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + + RasterizerOptions rasterizerOptions = new( + interest, + shapeOptions.IntersectionRule, + rasterizationMode, + samplingOrigin, + graphicsOptions.AntialiasThreshold); + + DrawingCanvasState state = this.ResolveState(); + + // Commands carry their absolute target bounds and destination origin explicitly. + // Bounded layers can clip the target while preserving the active canvas coordinate origin. + if (pen is null) + { + this.batcher.AddComposition( + CompositionCommand.Create( + path, + brush, + options, + in rasterizerOptions, + state.TargetBounds, + state.DestinationOffset, + clipPaths, + state.IsLayer)); + return; + } + + this.batcher.AddStrokePath( + new StrokePathCommand( + path, + brush, + options, + in rasterizerOptions, + state.TargetBounds, + state.DestinationOffset, + pen, + clipPaths, + state.IsLayer)); + } + + /// + /// Enqueues one explicit two-point stroke line-segment command using the current canvas state. + /// + private void PrepareStrokeLineSegmentCompositionCore( + PointF start, + PointF end, + Brush brush, + DrawingOptions options, + Pen pen) + { + brush = this.NormalizeBrush(brush); + + GraphicsOptions graphicsOptions = options.GraphicsOptions; + RasterizationMode rasterizationMode = graphicsOptions.Antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; + RectangleF bounds = StrokeLineSegmentCommand.GetConservativeBounds(start, end, pen); + Rectangle interest = Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right) + 1, + (int)MathF.Ceiling(bounds.Bottom) + 1); + + RasterizerOptions rasterizerOptions = new( + interest, + options.ShapeOptions.IntersectionRule, + rasterizationMode, + RasterizerSamplingOrigin.PixelCenter, + graphicsOptions.AntialiasThreshold); + + DrawingCanvasState state = this.ResolveState(); + this.batcher.AddStrokeLineSegment( + new StrokeLineSegmentCommand( + start, + end, + brush, + options, + in rasterizerOptions, + state.TargetBounds, + state.DestinationOffset, + pen, + state.IsLayer)); + } + + /// + /// Enqueues one explicit stroked open polyline command using the current canvas state. + /// + private void PrepareStrokePolylineCompositionCore( + PointF[] points, + Brush brush, + DrawingOptions options, + Pen pen) + { + brush = this.NormalizeBrush(brush); + + GraphicsOptions graphicsOptions = options.GraphicsOptions; + RasterizationMode rasterizationMode = graphicsOptions.Antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; + RectangleF bounds = StrokePolylineCommand.GetConservativeBounds(points, pen); + Rectangle interest = Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right) + 1, + (int)MathF.Ceiling(bounds.Bottom) + 1); + + RasterizerOptions rasterizerOptions = new( + interest, + options.ShapeOptions.IntersectionRule, + rasterizationMode, + RasterizerSamplingOrigin.PixelCenter, + graphicsOptions.AntialiasThreshold); + + DrawingCanvasState state = this.ResolveState(); + this.batcher.AddStrokePolyline( + new StrokePolylineCommand( + points, + brush, + options, + in rasterizerOptions, + state.TargetBounds, + state.DestinationOffset, + pen, + state.IsLayer)); + } + + /// + /// Normalizes brushes that carry image sources containing the wrong pixel format exactly once. + /// + /// The logical brush supplied by the caller. + /// The brush to queue for this canvas flush. + private Brush NormalizeBrush(Brush brush) + { + if (brush is not ImageBrush imageBrush) + { + return brush; + } + + if (brush is ImageBrush typedBrush) + { + return typedBrush; + } + + // Normalize the source image once so deferred composition does not repeat per-pixel conversions. + Image convertedImage = imageBrush.UntypedImage.CloneAs(); + this.pendingImageResources.Add(convertedImage); + return new ImageBrush(convertedImage, imageBrush.SourceRegion, imageBrush.Offset); + } + + /// + /// Enqueues a fill command for one path using the current canvas state. + /// + /// Brush used for shading. + /// Path to fill. + private void EnqueueFillPath(Brush brush, IPath path) + { + DrawingCanvasState state = this.ResolveState(); + IPath closed = path.AsClosedPath(); + + this.PrepareCompositionCore( + closed, + brush, + state.Options, + RasterizerSamplingOrigin.PixelBoundary, + state.ClipPaths); + } + + /// + /// Converts rendered text operations to composition commands and submits them to the batcher. + /// + /// Text drawing operations produced by glyph layout/rendering. + /// Drawing options applied to each operation. + /// Clip paths resolved from effective canvas state. + private void DrawTextOperations( + List operations, + DrawingOptions drawingOptions, + IReadOnlyList clipPaths) + { + // Build composition commands and enforce render-pass ordering while preserving + // original emission order inside each pass. This preserves overlapping color-font + // layer compositing semantics (for example emoji mouth/teeth layers). + List<(byte RenderPass, int Sequence, CompositionSceneCommand Command)> entries = new(operations.Count); + for (int i = 0; i < operations.Count; i++) + { + DrawingOperation operation = operations[i]; + entries.Add((operation.RenderPass, i, this.CreateTextCompositionCommand(operation, drawingOptions, clipPaths))); + } + + entries.Sort(static (a, b) => + { + int cmp = a.RenderPass.CompareTo(b.RenderPass); + return cmp != 0 ? cmp : a.Sequence.CompareTo(b.Sequence); + }); + + for (int i = 0; i < entries.Count; i++) + { + if (entries[i].Command is PathCompositionSceneCommand pathCommand) + { + this.batcher.AddComposition(pathCommand.Command); + } + else + { + this.batcher.AddStrokePath(((StrokePathCompositionSceneCommand)entries[i].Command).Command); + } + } + } + + /// + /// Resolves the currently active drawing state. + /// + /// The current state. + private DrawingCanvasState ResolveState() => this.savedStates.Peek(); + + /// + /// Ensures text drawing has at least one paint source. + /// + /// Optional fill brush. + /// Optional outline pen. + private static void EnsureTextPaint(Brush? brush, Pen? pen) + { + if (brush is null && pen is null) + { + throw new ArgumentException($"Expected a {nameof(brush)} or {nameof(pen)}. Both were null"); + } + } + + /// + /// Executes an action with a temporary scoped state, restoring the previous scoped state afterwards. + /// + /// Temporary drawing options. + /// Temporary clip paths. + /// Action to execute. + private void ExecuteWithTemporaryState(DrawingOptions options, IReadOnlyList clipPaths, Action action) + { + int saveCount = this.savedStates.Count; + _ = this.SaveCore(options, clipPaths); + try + { + action(); + } + finally + { + this.RestoreTo(saveCount); + } + } + + /// + public override void Flush() + { + this.EnsureNotDisposed(); + this.batcher.SealCommands(); + } + + /// + public override void Dispose() + { + if (this.isDisposed) + { + return; + } + + try + { + // Dispose should finalize the same drawing state transitions as RestoreTo(1), + // otherwise active layers can composite with different options than an explicit restore. + this.RestoreToCore(1); + if (this.ownsBatcher) + { + this.RenderRecordedTimeline(); + } + } + finally + { + if (this.ownsBatcher) + { + this.DisposePendingImageResources(); + } + + // Release the per-canvas glyph-outline cache. + this.glyphCache.Clear(); + + this.isDisposed = true; + } + } + + /// + /// Ensures this instance is not disposed. + /// + private void EnsureNotDisposed() + => ObjectDisposedException.ThrowIf(this.isDisposed, this); + + /// + /// Renders the recorded timeline owned by the root canvas during disposal. + /// + /// + /// Command-range entries are lowered to short-lived backend scenes here. Scene entries + /// reference retained scenes that were recorded earlier through . + /// + private void RenderRecordedTimeline() + { + if (!this.batcher.HasRecordedWork) + { + return; + } + + this.batcher.SealAndPrepareCommands(); + try + { + for (int i = 0; i < this.batcher.TimelineEntryCount; i++) + { + DrawingCanvasTimelineEntry entry = this.batcher.GetEntry(i); + switch (entry.Kind) + { + case DrawingCanvasTimelineEntryKind.CommandRange: + this.RenderCommandBatch(this.batcher.CreateCommandBatch(entry)); + break; + + case DrawingCanvasTimelineEntryKind.ApplyBarrier: + this.RenderApplyBarrier(this.batcher.GetApplyBarrier(entry.Index)); + break; + + case DrawingCanvasTimelineEntryKind.Scene: + this.backend.RenderScene( + this.configuration, + this.targetFrame, + this.batcher.GetInsertedScene(entry.Index)); + + break; + } + } + } + finally + { + this.batcher.ClearCommandBatch(); + } + } + + /// + /// Creates and renders one backend scene for a prepared command batch. + /// + /// The command batch to render. + private void RenderCommandBatch(DrawingCommandBatch commandBatch) + { + using DrawingBackendScene scene = this.backend.CreateScene( + this.configuration, + this.targetFrame.Bounds, + commandBatch); + + this.backend.RenderScene(this.configuration, this.targetFrame, scene); + } + + /// + /// Executes one apply barrier at its replay position. + /// + /// The apply barrier to execute. + private void RenderApplyBarrier(ApplyBarrier barrier) + { + DrawingCommandBatch? maybeCommandBatch = barrier.CreateWriteBackBatch( + this.configuration, + this.backend, + this.targetFrame, + out IDisposable? ownedResource); + + if (maybeCommandBatch is not DrawingCommandBatch commandBatch) + { + return; + } + + try + { + this.RenderCommandBatch(commandBatch); + } + finally + { + ownedResource?.Dispose(); + } + } + + /// + /// Restores the saved-state stack to without public guard checks. + /// Layer states are unwound through the normal compositing path so restore and disposal + /// preserve identical layer semantics. + /// + /// The target stack depth to restore to. + private void RestoreToCore(int saveCount) + { + while (this.savedStates.Count > saveCount) + { + DrawingCanvasState popped = this.savedStates.Pop(); + if (popped.IsLayer) + { + // Restore and Dispose unwind layers through the same command stream path. + this.batcher.AddComposition(CompositionCommand.CreateEndLayer(popped.TargetBounds, popped.LayerOptions!)); + } + } + } + + /// + /// Normalizes text options to avoid applying origin translation twice when path-based text is used. + /// + /// Input text options. + /// Optional path to draw the text along. + /// The path translated into text layout space when needed. + /// Normalized text options for rendering. + private static RichTextOptions ConfigureTextOptions(RichTextOptions options, IPath? path, out IPath? configuredPath) + { + configuredPath = path; + + if (path is not null && options.Origin != Vector2.Zero) + { + // Path-based text uses the path itself as positioning source; fold origin into the path + // to avoid applying both path layout and origin translation. + configuredPath = path.Translate(options.Origin); + return new RichTextOptions(options) + { + Origin = Vector2.Zero + }; + } + + return options; + } + + /// + /// Builds a normalized composition command for a text drawing operation. + /// + /// The source drawing operation. + /// Drawing options applied to the operation. + /// Optional clip paths to apply during preparation. + /// A composition scene command ready for batching. + private CompositionSceneCommand CreateTextCompositionCommand( + DrawingOperation operation, + DrawingOptions drawingOptions, + IReadOnlyList? clipPaths = null) + { + Brush compositeBrush = operation.Kind == DrawingOperationKind.Fill + ? operation.Brush! + : operation.Pen!.StrokeFill; + + GraphicsOptions graphicsOptions = + drawingOptions.GraphicsOptions.CloneOrReturnForRules( + operation.PixelAlphaCompositionMode, + operation.PixelColorBlendingMode); + + RasterizationMode rasterizationMode = graphicsOptions.Antialias + ? RasterizationMode.Antialiased + : RasterizationMode.Aliased; + + ShapeOptions shapeOptions = drawingOptions.ShapeOptions; + + DrawingCanvasState state = this.ResolveState(); + Point destinationOffset = new( + state.DestinationOffset.X + operation.RenderLocation.X, + state.DestinationOffset.Y + operation.RenderLocation.Y); + + Pen? pen = operation.Kind == DrawingOperationKind.Draw ? operation.Pen : null; + + IntersectionRule intersectionRule = pen is not null && operation.IntersectionRule != IntersectionRule.NonZero + ? IntersectionRule.NonZero + : operation.IntersectionRule; + + RasterizerSamplingOrigin samplingOrigin = pen is not null + ? RasterizerSamplingOrigin.PixelCenter + : RasterizerSamplingOrigin.PixelBoundary; + + RasterizerOptions rasterizerOptions = new( + default, + intersectionRule, + rasterizationMode, + samplingOrigin, + graphicsOptions.AntialiasThreshold); + + // Glyph paths arrive pre-laid-out, so the queued command must report identity transform + // and the GraphicsOptions clone produced above. Reuse the caller's instance only when both already match. + DrawingOptions effectiveOptions = ReferenceEquals(graphicsOptions, drawingOptions.GraphicsOptions) + && drawingOptions.Transform == Matrix4x4.Identity + ? drawingOptions + : new DrawingOptions(graphicsOptions, shapeOptions, Matrix4x4.Identity); + + IReadOnlyList? operationClipPaths = clipPaths; + if (clipPaths != null && clipPaths.Count > 0 && (operation.RenderLocation.X != 0 || operation.RenderLocation.Y != 0)) + { + IPath[] translatedClipPaths = new IPath[clipPaths.Count]; + + // Text glyph paths are queued in glyph-local coordinates and placed with RenderLocation, + // so canvas-space clip paths must be moved into that same local space before clipping. + for (int i = 0; i < clipPaths.Count; i++) + { + translatedClipPaths[i] = clipPaths[i].Translate(-operation.RenderLocation); + } + + operationClipPaths = translatedClipPaths; + } + + if (pen is null) + { + return new PathCompositionSceneCommand( + CompositionCommand.Create( + operation.Path, + compositeBrush, + effectiveOptions, + in rasterizerOptions, + state.TargetBounds, + destinationOffset, + operationClipPaths, + state.IsLayer)); + } + + return new StrokePathCompositionSceneCommand( + new StrokePathCommand( + operation.Path, + compositeBrush, + effectiveOptions, + in rasterizerOptions, + state.TargetBounds, + destinationOffset, + pen, + operationClipPaths, + state.IsLayer)); + } + + /// + /// Converts floating bounds to a conservative integer rectangle using floor/ceiling. + /// + /// The floating bounds to convert. + /// A rectangle covering the full floating bounds extent. + private static Rectangle ToConservativeBounds(RectangleF bounds) + => Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + + /// + /// Resolves local layer bounds to absolute target bounds using the active transform. + /// + /// The current drawing state. + /// The layer bounds in local canvas coordinates. + /// The absolute layer bounds clipped to the active target. + private static Rectangle ResolveLayerBounds(DrawingCanvasState state, Rectangle bounds) + { + RectangleF transformedBounds = bounds; + Matrix4x4 transform = state.Options.Transform; + if (!transform.IsIdentity) + { + transformedBounds = RectangleF.Transform(transformedBounds, transform); + } + + Rectangle localLayerBounds = ToConservativeBounds(transformedBounds); + Rectangle absoluteLayerBounds = new( + state.DestinationOffset.X + localLayerBounds.X, + state.DestinationOffset.Y + localLayerBounds.Y, + localLayerBounds.Width, + localLayerBounds.Height); + + return Rectangle.Intersect(state.TargetBounds, absoluteLayerBounds); + } + + /// + /// Creates resize options used for image drawing operations. + /// + /// Requested output size. + /// Optional resampler. Defaults to bicubic. + /// A resize options instance configured for stretch behavior. + private static ResizeOptions CreateDrawImageResizeOptions(Size size, IResampler? sampler) + => new() + { + Size = size, + Mode = ResizeMode.Stretch, + Sampler = sampler ?? KnownResamplers.Bicubic + }; + + /// + /// Creates a scaled image for drawing, optionally cropping to a source region first. + /// + /// The source image. + /// The clipped source rectangle. + /// The target scaled size. + /// Optional resampler used for scaling. + /// A new image containing the scaled pixels. + private static Image CreateScaledDrawImage( + Image image, + Rectangle clippedSourceRect, + Size scaledSize, + IResampler? sampler) + { + ResizeOptions effectiveResizeOptions = CreateDrawImageResizeOptions(scaledSize, sampler); + if (clippedSourceRect == image.Bounds) + { + return image.Clone(ctx => ctx.Resize(effectiveResizeOptions)); + } + + Image result = image.Clone(ctx => ctx.Crop(clippedSourceRect)); + result.Mutate(ctx => ctx.Resize(effectiveResizeOptions)); + return result; + } + + /// + /// Applies a transform to image content and returns the transformed image. + /// + /// The source image. + /// Destination rectangle in canvas coordinates. + /// Canvas transform to apply. + /// Optional resampler used during transform. + /// Receives the transformed destination bounds. + /// A new image containing transformed pixels. + private static Image CreateTransformedDrawImage( + Image image, + RectangleF destinationRect, + Matrix4x4 transform, + IResampler? sampler, + out RectangleF transformedDestinationRect) + { + // Source space: pixel coordinates in the untransformed source image (0..Width, 0..Height). + // Destination space: where that image would land on the canvas without any extra transform. + // This matrix maps source -> destination by scaling to destination size then translating to destination origin. + Matrix4x4 sourceToDestination = Matrix4x4.CreateScale( + destinationRect.Width / image.Width, + destinationRect.Height / image.Height, + 1) + * Matrix4x4.CreateTranslation(destinationRect.X, destinationRect.Y, 0); + + // Apply the canvas transform after source->destination placement: + // source -> destination -> transformed-canvas. + Matrix4x4 sourceToTransformedCanvas = sourceToDestination * transform; + + // Compute the transformed axis-aligned bounds in canvas space. + RectangleF transformedBounds = RectangleF.Transform( + new RectangleF(0, 0, image.Width, image.Height), + sourceToTransformedCanvas); + + // ImageBrush samples against integer pixel locations. Align the baked bitmap to integer + // canvas bounds so the bitmap origin and brush sampling origin agree exactly. + int alignedLeft = (int)MathF.Floor(transformedBounds.Left); + int alignedTop = (int)MathF.Floor(transformedBounds.Top); + int alignedRight = (int)MathF.Ceiling(transformedBounds.Right); + int alignedBottom = (int)MathF.Ceiling(transformedBounds.Bottom); + + transformedDestinationRect = RectangleF.FromLTRB( + alignedLeft, + alignedTop, + alignedRight, + alignedBottom); + + Size targetSize = new( + Math.Max(1, alignedRight - alignedLeft), + Math.Max(1, alignedBottom - alignedTop)); + + // ImageSharp.Transform expects output coordinates relative to the output bitmap origin (0,0). + // Shift transformed-canvas coordinates so the aligned integer canvas bounds become 0,0. + Matrix4x4 sourceToTarget = sourceToTransformedCanvas + * Matrix4x4.CreateTranslation(-alignedLeft, -alignedTop, 0); + + // Resample source pixels into the target bitmap using the computed source->target mapping. + return image.Clone(ctx => ctx.Transform( + image.Bounds, + sourceToTarget, + targetSize, + sampler ?? KnownResamplers.Bicubic)); + } + + /// + /// Maps a clipped source rectangle back to the corresponding destination rectangle. + /// + /// Original source rectangle. + /// Original destination rectangle. + /// Source rectangle clipped to image bounds. + /// The destination rectangle corresponding to the clipped source region. + private static RectangleF MapSourceClipToDestination( + Rectangle sourceRect, + RectangleF destinationRect, + Rectangle clippedSourceRect) + { + float scaleX = destinationRect.Width / sourceRect.Width; + float scaleY = destinationRect.Height / sourceRect.Height; + + float left = destinationRect.Left + ((clippedSourceRect.Left - sourceRect.Left) * scaleX); + float top = destinationRect.Top + ((clippedSourceRect.Top - sourceRect.Top) * scaleY); + float width = clippedSourceRect.Width * scaleX; + float height = clippedSourceRect.Height * scaleY; + + return new RectangleF(left, top, width, height); + } + + /// + /// Transforms clip paths into the same coordinate space as an eagerly-transformed draw-image command. + /// + /// Clip paths from the current canvas state. + /// Canvas transform already applied to the image content. + /// The transformed clip path list. + private static IReadOnlyList TransformClipPaths(IReadOnlyList clipPaths, Matrix4x4 transform) + { + if (clipPaths.Count == 0 || transform.IsIdentity) + { + return clipPaths; + } + + IPath[] transformed = new IPath[clipPaths.Count]; + for (int i = 0; i < transformed.Length; i++) + { + transformed[i] = clipPaths[i].Transform(transform); + } + + return transformed; + } + + /// + /// Disposes image resources retained for deferred draw execution. + /// + private void DisposePendingImageResources() + { + if (this.pendingImageResources.Count == 0) + { + return; + } + + // Release deferred image resources once queued operations have executed. + for (int i = 0; i < this.pendingImageResources.Count; i++) + { + this.pendingImageResources[i].Dispose(); + } + + this.pendingImageResources.Clear(); + } + + /// + /// Transfers pending image resources to a retained scene. + /// + /// The resources that must remain alive for the retained scene, or when none exist. + private IDisposable[]? DetachPendingImageResources() + { + if (this.pendingImageResources.Count == 0) + { + return null; + } + + IDisposable[] resources = new IDisposable[this.pendingImageResources.Count]; + + for (int i = 0; i < this.pendingImageResources.Count; i++) + { + resources[i] = this.pendingImageResources[i]; + } + + this.pendingImageResources.Clear(); + return resources; + } + + /// + /// Disposes resources that failed to transfer to a retained scene. + /// + /// The resources to dispose. + private static void DisposeOwnedResources(IDisposable[]? resources) + { + if (resources is null) + { + return; + } + + for (int i = 0; i < resources.Length; i++) + { + resources[i].Dispose(); + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/DrawingHelpers.cs b/ImageSharp.Drawing/Processing/DrawingHelpers.cs new file mode 100644 index 0000000..1867eb7 --- /dev/null +++ b/ImageSharp.Drawing/Processing/DrawingHelpers.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing { + internal static class DrawingHelpers + { + /// + /// Convert a to a of the given pixel type. + /// + /// The type of pixel format. + /// The color matrix. + public static DenseMatrix ToPixelMatrix(this DenseMatrix colorMatrix) + where TPixel : unmanaged, IPixel + { + DenseMatrix result = new(colorMatrix.Columns, colorMatrix.Rows); + Color.ToPixel(colorMatrix.Span, result.Span); + return result; + } + } +} diff --git a/ImageSharp.Drawing/Processing/DrawingOperation.cs b/ImageSharp.Drawing/Processing/DrawingOperation.cs new file mode 100644 index 0000000..4fcecc7 --- /dev/null +++ b/ImageSharp.Drawing/Processing/DrawingOperation.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing { + internal enum DrawingOperationKind : byte + { + Fill = 0, + Draw = 1 + } + + internal struct DrawingOperation + { + public DrawingOperationKind Kind { get; set; } + + public IPath Path { get; set; } + + public Point RenderLocation { get; set; } + + public IntersectionRule IntersectionRule { get; set; } + + public byte RenderPass { get; set; } + + public Brush? Brush { get; set; } + + public Pen? Pen { get; set; } + + public PixelAlphaCompositionMode PixelAlphaCompositionMode { get; set; } + + public PixelColorBlendingMode PixelColorBlendingMode { get; set; } + } +} diff --git a/ImageSharp.Drawing/Processing/DrawingOptions.cs b/ImageSharp.Drawing/Processing/DrawingOptions.cs new file mode 100644 index 0000000..d0b8dfe --- /dev/null +++ b/ImageSharp.Drawing/Processing/DrawingOptions.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Provides options for influencing drawing operations, combining graphics rendering settings, + /// shape fill-rule behavior, and an optional coordinate transform. + /// + public class DrawingOptions + { + private GraphicsOptions graphicsOptions; + private ShapeOptions shapeOptions; + + /// + /// Initializes a new instance of the class. + /// + public DrawingOptions() + { + this.graphicsOptions = new GraphicsOptions(); + this.shapeOptions = new ShapeOptions(); + this.Transform = Matrix4x4.Identity; + } + + internal DrawingOptions( + GraphicsOptions graphicsOptions, + ShapeOptions shapeOptions, + Matrix4x4 transform) + { + DebugGuard.NotNull(graphicsOptions, nameof(graphicsOptions)); + DebugGuard.NotNull(shapeOptions, nameof(shapeOptions)); + + this.graphicsOptions = graphicsOptions; + this.shapeOptions = shapeOptions; + this.Transform = transform; + } + + /// + /// Gets or sets the graphics rendering options that control antialiasing, blending, alpha composition, + /// and coverage thresholding for the drawing operation. + /// + public GraphicsOptions GraphicsOptions + { + get => this.graphicsOptions; + set + { + Guard.NotNull(value, nameof(this.GraphicsOptions)); + this.graphicsOptions = value; + } + } + + /// + /// Gets or sets the shape options that control fill-rule intersection mode and boolean clipping behavior. + /// + public ShapeOptions ShapeOptions + { + get => this.shapeOptions; + set + { + Guard.NotNull(value, nameof(this.ShapeOptions)); + this.shapeOptions = value; + } + } + + /// + /// Gets or sets the transform matrix applied to vector output before rasterization. + /// For strokes, the pen is expanded in local geometry space and the resulting outline is transformed before rasterization. + /// Defaults to . + /// + public Matrix4x4 Transform { get; set; } + } +} diff --git a/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs b/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs new file mode 100644 index 0000000..9037333 --- /dev/null +++ b/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Adds extensions that help working with . + /// + public static class DrawingOptionsDefaultsExtensions + { + /// + /// Gets the default drawing options against the source image processing context. + /// + /// The image processing context to retrieve defaults from. + /// The globally configured default options. + public static DrawingOptions GetDrawingOptions(this IImageProcessingContext context) + => new(context.GetGraphicsOptions(), new ShapeOptions(), Matrix4x4.Identity); + + /// + /// Clones the path graphic options and applies changes required to force clearing. + /// + /// The drawing options to clone + /// A clone of shapeOptions with ColorBlendingMode, AlphaCompositionMode, and BlendPercentage set + internal static DrawingOptions CloneForClearOperation(this DrawingOptions drawingOptions) + { + GraphicsOptions options = drawingOptions.GraphicsOptions.DeepClone(); + options.ColorBlendingMode = PixelColorBlendingMode.Normal; + options.AlphaCompositionMode = PixelAlphaCompositionMode.Src; + options.BlendPercentage = 1F; + + return new DrawingOptions(options, drawingOptions.ShapeOptions, drawingOptions.Transform); + } + } +} diff --git a/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs b/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs new file mode 100644 index 0000000..48d8c92 --- /dev/null +++ b/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs @@ -0,0 +1,159 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Provides an implementation of a brush for painting elliptical gradients. + /// The ellipse is defined by a center point, a point on the longest axis, and the ratio + /// between the longest and shortest axes. + /// + public sealed class EllipticGradientBrush : GradientBrush + { + /// + /// The center of the elliptical gradient and 0 for the color stops. + /// The end point of the reference axis of the ellipse. + /// + /// The ratio of the axis widths. + /// The second axis is perpendicular to the reference axis and its length is the reference axis length + /// multiplied by this factor. + /// + /// Defines how the colors of the gradients are repeated. + /// The color stops. + public EllipticGradientBrush( + PointF center, + PointF referenceAxisEnd, + float axisRatio, + GradientRepetitionMode repetitionMode, + params ColorStop[] colorStops) + : base(repetitionMode, colorStops) + { + this.Center = center; + this.ReferenceAxisEnd = referenceAxisEnd; + this.AxisRatio = axisRatio; + } + + /// + /// Gets the center of the ellipse. + /// + public PointF Center { get; } + + /// + /// Gets the end point of the reference axis. + /// + public PointF ReferenceAxisEnd { get; } + + /// + /// Gets the ratio of the secondary axis to the primary axis. + /// + public float AxisRatio { get; } + + /// + public override Brush Transform(Matrix4x4 matrix) + { + PointF tc = PointF.Transform(this.Center, matrix); + PointF tRef = PointF.Transform(this.ReferenceAxisEnd, matrix); + + // Compute a point on the perpendicular (secondary) axis and transform it. + float refDx = this.ReferenceAxisEnd.X - this.Center.X; + float refDy = this.ReferenceAxisEnd.Y - this.Center.Y; + float refLen = MathF.Sqrt((refDx * refDx) + (refDy * refDy)); + float secondLen = refLen * this.AxisRatio; + + // Perpendicular direction (rotated 90 degrees). + PointF secondEnd = new( + this.Center.X + (-refDy / refLen * secondLen), + this.Center.Y + (refDx / refLen * secondLen)); + PointF tSec = PointF.Transform(secondEnd, matrix); + + // Derive new ratio from transformed lengths. + float newRefLen = MathF.Sqrt( + ((tRef.X - tc.X) * (tRef.X - tc.X)) + ((tRef.Y - tc.Y) * (tRef.Y - tc.Y))); + float newSecLen = MathF.Sqrt( + ((tSec.X - tc.X) * (tSec.X - tc.X)) + ((tSec.Y - tc.Y) * (tSec.Y - tc.Y))); + float newRatio = newRefLen > 0f ? newSecLen / newRefLen : this.AxisRatio; + + return new EllipticGradientBrush(tc, tRef, newRatio, this.RepetitionMode, this.ColorStopsArray); + } + + /// + public override BrushRenderer CreateRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + RectangleF region) => + new EllipticGradientBrushRenderer( + configuration, + options, + canvasWidth, + this, + this.ColorStopsArray, + this.RepetitionMode); + + /// + private sealed class EllipticGradientBrushRenderer : GradientBrushRenderer + where TPixel : unmanaged, IPixel + { + private readonly PointF center; + + private readonly float cosRotation; + + private readonly float sinRotation; + + private readonly float referenceRadiusSquared; + + private readonly float secondRadiusSquared; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration instance to use when performing operations. + /// The graphics options. + /// The canvas width for the current render pass. + /// The elliptic gradient brush. + /// Definition of colors. + /// Defines how the gradient colors are repeated. + public EllipticGradientBrushRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + EllipticGradientBrush brush, + ColorStop[] colorStops, + GradientRepetitionMode repetitionMode) + : base(configuration, options, canvasWidth, colorStops, repetitionMode) + { + this.center = brush.Center; + + float refDx = brush.ReferenceAxisEnd.X - brush.Center.X; + float refDy = brush.ReferenceAxisEnd.Y - brush.Center.Y; + float rotation = MathF.Atan2(refDy, refDx); + float referenceRadius = MathF.Sqrt((refDx * refDx) + (refDy * refDy)); + float secondRadius = referenceRadius * brush.AxisRatio; + + this.referenceRadiusSquared = referenceRadius * referenceRadius; + this.secondRadiusSquared = secondRadius * secondRadius; + this.sinRotation = MathF.Sin(rotation); + this.cosRotation = MathF.Cos(rotation); + } + + /// + protected override float PositionOnGradient(float x, float y) + { + float x0 = x - this.center.X; + float y0 = y - this.center.Y; + + float xR = (x0 * this.cosRotation) - (y0 * this.sinRotation); + float yR = (x0 * this.sinRotation) + (y0 * this.cosRotation); + + float xSquared = xR * xR; + float ySquared = yR * yR; + + return MathF.Sqrt((xSquared / this.referenceRadiusSquared) + (ySquared / this.secondRadiusSquared)); + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/GradientBrush.cs b/ImageSharp.Drawing/Processing/GradientBrush.cs new file mode 100644 index 0000000..9293408 --- /dev/null +++ b/ImageSharp.Drawing/Processing/GradientBrush.cs @@ -0,0 +1,244 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Base class for Gradient brushes + /// + public abstract class GradientBrush : Brush + { + /// + /// Defines how the colors are repeated beyond the interval [0..1] + /// The gradient colors. + protected GradientBrush(GradientRepetitionMode repetitionMode, params ColorStop[] colorStops) + { + this.RepetitionMode = repetitionMode; + + InsertionSort(colorStops, (a, b) => a.Ratio.CompareTo(b.Ratio)); + this.ColorStopsArray = colorStops; + } + + /// + /// Gets how the colors are repeated beyond the interval [0..1]. + /// + public GradientRepetitionMode RepetitionMode { get; } + + /// + /// Gets the color stops for this gradient. + /// + public ReadOnlySpan ColorStops => this.ColorStopsArray; + + /// + /// Gets the color stops array for use by derived applicators. + /// + protected ColorStop[] ColorStopsArray { get; } + + /// + public override bool Equals(Brush? other) + { + if (other is GradientBrush brush) + { + return this.RepetitionMode == brush.RepetitionMode + && this.ColorStopsArray?.SequenceEqual(brush.ColorStopsArray) == true; + } + + return false; + } + + /// + public override int GetHashCode() + => HashCode.Combine(this.RepetitionMode, this.ColorStopsArray); + + /// + /// Sorts the collection in place using a stable insertion sort. + /// is not stable and can reorder + /// equal-ratio color stops, producing non-deterministic gradient results. + /// + private static void InsertionSort(T[] collection, Comparison comparison) + { + int count = collection.Length; + for (int j = 1; j < count; j++) + { + T key = collection[j]; + + int i = j - 1; + for (; i >= 0 && comparison(collection[i], key) > 0; i--) + { + collection[i + 1] = collection[i]; + } + + collection[i + 1] = key; + } + } + + /// + /// Base class for gradient brush applicators + /// + /// The pixel format. + internal abstract class GradientBrushRenderer : BrushRenderer + where TPixel : unmanaged, IPixel + { + private static readonly TPixel Transparent = Color.Transparent.ToPixel(); + + private readonly ColorStop[] colorStops; + + private readonly GradientRepetitionMode repetitionMode; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration instance to use when performing operations. + /// The graphics options. + /// The canvas width for the current render pass. + /// An array of color stops sorted by their position. + /// Defines if and how the gradient should be repeated. + protected GradientBrushRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + ColorStop[] colorStops, + GradientRepetitionMode repetitionMode) + : base(configuration, options, canvasWidth) + { + this.colorStops = colorStops; + this.repetitionMode = repetitionMode; + } + + internal TPixel this[int x, int y] + { + get + { + float fx = x + 0.5f; + float fy = y + 0.5f; + + float positionOnCompleteGradient = this.PositionOnGradient(fx, fy); + if (float.IsNaN(positionOnCompleteGradient)) + { + return Transparent; + } + + switch (this.repetitionMode) + { + case GradientRepetitionMode.Repeat: + positionOnCompleteGradient %= 1; + break; + case GradientRepetitionMode.Reflect: + positionOnCompleteGradient %= 2; + if (positionOnCompleteGradient > 1) + { + positionOnCompleteGradient = 2 - positionOnCompleteGradient; + } + + break; + case GradientRepetitionMode.DontFill: + if (positionOnCompleteGradient is > 1 or < 0) + { + return Transparent; + } + + break; + case GradientRepetitionMode.None: + default: + // do nothing. The following could be done, but is not necessary: + // onLocalGradient = Math.Min(0, Math.Max(1, onLocalGradient)); + break; + } + + (ColorStop from, ColorStop to) = this.GetGradientSegment(positionOnCompleteGradient); + + if (from.Color.Equals(to.Color)) + { + return from.Color.ToPixel(); + } + + float onLocalGradient = (positionOnCompleteGradient - from.Ratio) / (to.Ratio - from.Ratio); + + // TODO: This should use premultiplied vectors to avoid bad blends e.g. red -> brown <- green. + return Color.FromScaledVector( + Vector4.Lerp( + from.Color.ToScaledVector4(), + to.Color.ToScaledVector4(), + onLocalGradient)).ToPixel(); + } + } + + /// + public override void Apply( + Span destinationRow, + ReadOnlySpan scanline, + int x, + int y, + BrushWorkspace workspace) + { + Span amounts = workspace.GetAmounts(scanline.Length); + Span overlays = workspace.GetOverlays(scanline.Length); + float blendPercentage = this.Options.BlendPercentage; + + // TODO: Remove bounds checks. + if (blendPercentage < 1) + { + for (int i = 0; i < scanline.Length; i++) + { + amounts[i] = scanline[i] * blendPercentage; + overlays[i] = this[x + i, y]; + } + } + else + { + for (int i = 0; i < scanline.Length; i++) + { + amounts[i] = scanline[i]; + overlays[i] = this[x + i, y]; + } + } + + this.Blender.Blend( + this.Configuration, + destinationRow, + destinationRow, + overlays, + amounts, + workspace.GetBlendScratch(scanline.Length, 3)); + } + + /// + /// Calculates the position on the gradient for a given point. + /// This method is abstract as it's content depends on the shape of the gradient. + /// + /// The x-coordinate of the point. + /// The y-coordinate of the point. + /// + /// The position the given point has on the gradient. + /// The position is not bound to the [0..1] interval. + /// Values outside of that interval may be treated differently, + /// e.g. for the enum. + /// + protected abstract float PositionOnGradient(float x, float y); + + private (ColorStop From, ColorStop To) GetGradientSegment(float positionOnCompleteGradient) + { + ColorStop localGradientFrom = this.colorStops[0]; + ColorStop localGradientTo = default; + + foreach (ColorStop colorStop in this.colorStops) + { + localGradientTo = colorStop; + + if (colorStop.Ratio > positionOnCompleteGradient) + { + // we're done here, so break it! + break; + } + + localGradientFrom = localGradientTo; + } + + return (localGradientFrom, localGradientTo); + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/GradientRepetitionMode.cs b/ImageSharp.Drawing/Processing/GradientRepetitionMode.cs new file mode 100644 index 0000000..a3814b9 --- /dev/null +++ b/ImageSharp.Drawing/Processing/GradientRepetitionMode.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Modes to repeat a gradient. + /// + public enum GradientRepetitionMode + { + /// + /// Don't repeat, keep the color of start and end beyond those points stable. + /// + None, + + /// + /// Repeat the gradient. + /// If it's a black-white gradient, with Repeat it will be Black->{gray}->White|Black->{gray}->White|... + /// + Repeat, + + /// + /// Reflect the gradient. + /// Similar to , but each other repetition uses inverse order of s. + /// Used on a Black-White gradient, Reflect leads to Black->{gray}->White->{gray}->White... + /// + Reflect, + + /// + /// With DontFill a gradient does not touch any pixel beyond it's borders. + /// For the this is beyond the orthogonal through start and end, + /// For and it's beyond 1.0. + /// + DontFill + } +} diff --git a/ImageSharp.Drawing/Processing/ImageBrush.cs b/ImageSharp.Drawing/Processing/ImageBrush.cs new file mode 100644 index 0000000..95c4eae --- /dev/null +++ b/ImageSharp.Drawing/Processing/ImageBrush.cs @@ -0,0 +1,268 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Provides an implementation of an image brush for painting images within areas. + /// + /// The pixel format of the source image. + public sealed class ImageBrush : ImageBrush + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The source image to draw. + public ImageBrush(Image image) + : base(image) + => this.SourceImage = image; + + /// + /// Initializes a new instance of the class. + /// + /// The source image to draw. + /// An offset to apply to the image while drawing the texture. + public ImageBrush(Image image, Point offset) + : base(image, offset) + => this.SourceImage = image; + + /// + /// Initializes a new instance of the class. + /// + /// The source image to draw. + /// The region of interest within the source image. + public ImageBrush(Image image, RectangleF region) + : base(image, region) + => this.SourceImage = image; + + /// + /// Initializes a new instance of the class. + /// + /// The source image to draw. + /// The region of interest within the source image. + /// An offset to apply to the image while drawing the texture. + public ImageBrush(Image image, RectangleF region, Point offset) + : base(image, region, offset) + => this.SourceImage = image; + + /// + /// Gets the typed source image used by this brush. + /// + public Image SourceImage { get; } + } + + /// + /// The untyped base class for image brushes, used to support non-generic brush references in drawing contexts. + /// + public abstract class ImageBrush : Brush + { + /// + /// Initializes a new instance of the class. + /// + /// The source image to draw. + protected ImageBrush(Image image) + : this(image, image.Bounds) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The image. + /// + /// An offset to apply the to image image while drawing apply the texture. + /// + protected ImageBrush(Image image, Point offset) + : this(image, image.Bounds, offset) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The image. + /// + /// The region of interest. + /// This overrides any region used to initialize the brush applicator. + /// + protected ImageBrush(Image image, RectangleF region) + : this(image, region, Point.Empty) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The image. + /// + /// The region of interest. + /// This overrides any region used to initialize the brush applicator. + /// + /// + /// An offset to apply the to image image while drawing apply the texture. + /// + protected ImageBrush(Image image, RectangleF region, Point offset) + { + this.UntypedImage = image; + this.SourceRegion = RectangleF.Intersect(image.Bounds, region); + this.Offset = offset; + } + + /// + /// Gets the source image used by this brush. + /// + public Image UntypedImage { get; } + + /// + /// Gets the source region within the image. + /// + public RectangleF SourceRegion { get; } + + /// + /// Gets the offset applied to the brush origin. + /// + public Point Offset { get; } + + /// + public override bool Equals(Brush? other) + { + if (other is ImageBrush ib) + { + return ib.UntypedImage == this.UntypedImage && ib.SourceRegion == this.SourceRegion; + } + + return false; + } + + /// + public override int GetHashCode() => HashCode.Combine(this.UntypedImage, this.SourceRegion); + + /// + public override BrushRenderer CreateRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + RectangleF region) + { + if (this.UntypedImage is Image image) + { + return new ImageBrushRenderer(configuration, options, canvasWidth, image, region, this.SourceRegion, this.Offset); + } + + // This will never be hit as the brush is always normalized by the drawing canvas + // but we do it to satisfy the type system. + ThrowIfInvalidImagePixelFormat(); + return null; + } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowIfInvalidImagePixelFormat() + => throw new UnreachableException("The pixel format of the image is not supported by this brush renderer"); + + /// + /// The image brush applicator. + /// + /// The pixel format. + private sealed class ImageBrushRenderer : BrushRenderer + where TPixel : unmanaged, IPixel + { + private readonly ImageFrame sourceFrame; + + /// + /// The region of the source image we will be using to draw from. + /// + private readonly Rectangle sourceRegion; + + /// + /// The Y offset. + /// + private readonly int offsetY; + + /// + /// The X offset. + /// + private readonly int offsetX; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration instance to use when performing operations. + /// The graphics options. + /// The canvas width for the current render pass. + /// The image. + /// The region of the target image we will be drawing to. + /// The region of the source image we will be using to source pixels to draw from. + /// An offset to apply to the texture while drawing. + public ImageBrushRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + Image image, + RectangleF targetRegion, + RectangleF sourceRegion, + Point offset) + : base(configuration, options, canvasWidth) + { + this.sourceFrame = image.Frames.RootFrame; + this.sourceRegion = Rectangle.Intersect(image.Bounds, (Rectangle)sourceRegion); + + this.offsetY = (int)MathF.Floor(targetRegion.Top) + offset.Y; + this.offsetX = (int)MathF.Floor(targetRegion.Left) + offset.X; + } + + internal TPixel this[int x, int y] + { + get + { + int srcX = ((x - this.offsetX) % this.sourceRegion.Width) + this.sourceRegion.X; + int srcY = ((y - this.offsetY) % this.sourceRegion.Height) + this.sourceRegion.Y; + return this.sourceFrame[srcX, srcY]; + } + } + + /// + public override void Apply( + Span destinationRow, + ReadOnlySpan scanline, + int x, + int y, + BrushWorkspace workspace) + { + Span amountSpan = workspace.GetAmounts(scanline.Length); + Span overlaySpan = workspace.GetOverlays(scanline.Length); + + int offsetX = x - this.offsetX; + int sourceY = ((((y - this.offsetY) % this.sourceRegion.Height) // clamp the number between -height and +height + + this.sourceRegion.Height) % this.sourceRegion.Height) // clamp the number between 0 and +height + + this.sourceRegion.Y; + Span sourceRow = this.sourceFrame.PixelBuffer.DangerousGetRowSpan(sourceY); + + for (int i = 0; i < scanline.Length; i++) + { + amountSpan[i] = scanline[i] * this.Options.BlendPercentage; + + int sourceX = ((((i + offsetX) % this.sourceRegion.Width) // clamp the number between -width and +width + + this.sourceRegion.Width) % this.sourceRegion.Width) // clamp the number between 0 and +width + + this.sourceRegion.X; + + overlaySpan[i] = sourceRow[sourceX]; + } + + this.Blender.Blend( + this.Configuration, + destinationRow, + destinationRow, + overlaySpan, + amountSpan, + workspace.GetBlendScratch(scanline.Length, 3)); + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/LinearGradientBrush.cs b/ImageSharp.Drawing/Processing/LinearGradientBrush.cs new file mode 100644 index 0000000..0205d8e --- /dev/null +++ b/ImageSharp.Drawing/Processing/LinearGradientBrush.cs @@ -0,0 +1,199 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Provides a brush that paints linear gradients within an area. + /// Supports both classic two-point gradients and three-point (rotated) gradients. + /// + public sealed class LinearGradientBrush : GradientBrush + { + /// + /// Initializes a new instance of the class using + /// a start and end point. + /// + /// The start point of the gradient. + /// The end point of the gradient. + /// Defines how the colors are repeated. + /// The ordered color stops of the gradient. + public LinearGradientBrush( + PointF p0, + PointF p1, + GradientRepetitionMode repetitionMode, + params ColorStop[] colorStops) + : base(repetitionMode, colorStops) + { + this.StartPoint = p0; + this.EndPoint = p1; + } + + /// + /// Initializes a new instance of the class using + /// three points to define a rotated gradient axis. + /// + /// The first point (start of the gradient). + /// The second point (gradient vector endpoint). + /// + /// The rotation reference point. This defines the rotation of the gradient axis. + /// + /// Defines how the colors are repeated. + /// The ordered color stops of the gradient. + public LinearGradientBrush( + PointF p0, + PointF p1, + PointF rotationPoint, + GradientRepetitionMode repetitionMode, + params ColorStop[] colorStops) + : base(repetitionMode, colorStops) + { + ResolveAxis(p0, p1, rotationPoint, out PointF start, out PointF end); + this.StartPoint = start; + this.EndPoint = end; + } + + /// + /// Gets the start point of the gradient axis. + /// + public PointF StartPoint { get; } + + /// + /// Gets the end point of the gradient axis. + /// + public PointF EndPoint { get; } + + /// + public override Brush Transform(Matrix4x4 matrix) + => new LinearGradientBrush( + PointF.Transform(this.StartPoint, matrix), + PointF.Transform(this.EndPoint, matrix), + this.RepetitionMode, + this.ColorStopsArray); + + /// + public override bool Equals(Brush? other) + { + if (other is LinearGradientBrush brush) + { + return base.Equals(other) + && this.StartPoint.Equals(brush.StartPoint) + && this.EndPoint.Equals(brush.EndPoint); + } + + return false; + } + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), this.StartPoint, this.EndPoint); + + /// + /// Resolves a three-point gradient axis into a two-point axis by projecting + /// the gradient vector (p0 to p1) onto the perpendicular of the rotation vector (p0 to rotationPoint). + /// This follows the COLRv1 font specification for rotated linear gradients. + /// + /// The gradient start point. + /// The gradient vector endpoint. + /// The rotation reference point. + /// The resolved start point of the gradient axis. + /// The resolved end point of the gradient axis. + private static void ResolveAxis(PointF p0, PointF p1, PointF rotationPoint, out PointF start, out PointF end) + { + // Gradient vector from p0 to p1. + float vx = p1.X - p0.X; + float vy = p1.Y - p0.Y; + + // Rotation vector from p0 to rotation point. + float rx = rotationPoint.X - p0.X; + float ry = rotationPoint.Y - p0.Y; + + // Perpendicular to the rotation vector. + float nx = ry; + float ny = -rx; + + float ndotn = (nx * nx) + (ny * ny); + if (ndotn == 0f) + { + // Degenerate: p0 == rotationPoint, fall back to original axis. + start = p0; + end = p1; + } + else + { + // Project the gradient vector onto the perpendicular direction. + float vdotn = (vx * nx) + (vy * ny); + float scale = vdotn / ndotn; + start = p0; + end = new PointF(p0.X + (scale * nx), p0.Y + (scale * ny)); + } + } + + /// + public override BrushRenderer CreateRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + RectangleF region) + => new LinearGradientBrushRenderer( + configuration, + options, + canvasWidth, + this, + this.ColorStopsArray, + this.RepetitionMode); + + /// + /// Implements the gradient application logic for . + /// + /// The pixel format. + private sealed class LinearGradientBrushRenderer : GradientBrushRenderer + where TPixel : unmanaged, IPixel + { + private readonly PointF start; + private readonly float alongX; + private readonly float alongY; + private readonly float alongsSquared; + + /// + /// Initializes a new instance of the class. + /// + /// The ImageSharp configuration. + /// The graphics options. + /// The canvas width for the current render pass. + /// The linear gradient brush. + /// The gradient color stops. + /// Defines how the gradient repeats. + public LinearGradientBrushRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + LinearGradientBrush brush, + ColorStop[] colorStops, + GradientRepetitionMode repetitionMode) + : base(configuration, options, canvasWidth, colorStops, repetitionMode) + { + this.start = brush.StartPoint; + + this.alongX = brush.EndPoint.X - this.start.X; + this.alongY = brush.EndPoint.Y - this.start.Y; + this.alongsSquared = (this.alongX * this.alongX) + (this.alongY * this.alongY); + } + + /// + protected override float PositionOnGradient(float x, float y) + { + if (this.alongsSquared == 0f) + { + return 1f; + } + + float deltaX = x - this.start.X; + float deltaY = y - this.start.Y; + return ((deltaX * this.alongX) + (deltaY * this.alongY)) / this.alongsSquared; + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/PaintExtensions.cs b/ImageSharp.Drawing/Processing/PaintExtensions.cs new file mode 100644 index 0000000..5b83906 --- /dev/null +++ b/ImageSharp.Drawing/Processing/PaintExtensions.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Represents the per-frame painting callback executed by . + /// + /// The drawing canvas for the current image frame. + public delegate void CanvasAction(DrawingCanvas canvas); + + /// + /// Adds image-processing extensions that paint each frame through . + /// + public static class PaintExtensions + { + /// + /// Paints each image frame using drawing options from the current context. + /// + /// The image processing context to paint. + /// The per-frame painting callback. + /// The so additional processing operations can be chained. + public static IImageProcessingContext Paint( + this IImageProcessingContext source, + CanvasAction action) + => source.Paint(source.GetDrawingOptions(), action); + + /// + /// Paints each image frame using the supplied drawing options. + /// + /// The image processing context to paint. + /// The drawing options applied when creating each frame canvas. + /// The per-frame painting callback. + /// The so additional processing operations can be chained. + public static IImageProcessingContext Paint( + this IImageProcessingContext source, + DrawingOptions options, + CanvasAction action) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(action, nameof(action)); + + return source.ApplyProcessor(new PaintProcessor(options, action)); + } + } +} diff --git a/ImageSharp.Drawing/Processing/PaintProcessor.cs b/ImageSharp.Drawing/Processing/PaintProcessor.cs new file mode 100644 index 0000000..4f0ba31 --- /dev/null +++ b/ImageSharp.Drawing/Processing/PaintProcessor.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Defines the image processor used by + /// to execute a canvas callback for each image frame. + /// + public sealed class PaintProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The drawing options used when creating each frame canvas. + /// The per-frame painting callback. + public PaintProcessor(DrawingOptions options, CanvasAction action) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(action, nameof(action)); + + this.Options = options; + this.Action = action; + } + + /// + /// Gets the drawing options used when creating each frame canvas. + /// + public DrawingOptions Options { get; } + + /// + /// Gets the per-frame painting callback. + /// + internal CanvasAction Action { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor( + Configuration configuration, + Image source, + Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new PaintProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp.Drawing/Processing/PaintProcessor{TPixel}.cs b/ImageSharp.Drawing/Processing/PaintProcessor{TPixel}.cs new file mode 100644 index 0000000..1e77151 --- /dev/null +++ b/ImageSharp.Drawing/Processing/PaintProcessor{TPixel}.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Executes the callback for a specific pixel type by creating a + /// over each frame. + /// + /// The pixel format. + internal sealed class PaintProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly PaintProcessor definition; + private readonly CanvasAction action; + + /// + /// Initializes a new instance of the class. + /// + /// The processing configuration. + /// The non-generic processor definition that owns the drawing options and callback. + /// The source image. + /// The source bounds passed through the processing pipeline. + public PaintProcessor( + Configuration configuration, + PaintProcessor definition, + Image source, + Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.definition = definition; + this.action = definition.Action; + } + + /// + protected override void OnFrameApply(ImageFrame source) + { + using DrawingCanvas canvas = source.CreateCanvas(this.Configuration, this.definition.Options); + this.action(canvas); + } + } +} diff --git a/ImageSharp.Drawing/Processing/PathGradientBrush.cs b/ImageSharp.Drawing/Processing/PathGradientBrush.cs new file mode 100644 index 0000000..7f49ce3 --- /dev/null +++ b/ImageSharp.Drawing/Processing/PathGradientBrush.cs @@ -0,0 +1,413 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Helpers; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Provides an implementation of a brush for painting gradients between multiple color positions in 2D coordinates. + /// + public sealed class PathGradientBrush : Brush + { + private readonly PointF[] points; + private readonly Color[] colors; + private readonly Edge[] edges; + + /// + /// Initializes a new instance of the class. + /// + /// Points that constitute a polygon that represents the gradient area. + /// Array of colors that correspond to each point in the polygon. + public PathGradientBrush(PointF[] points, Color[] colors) + { + Guard.NotNull(points, nameof(points)); + Guard.MustBeGreaterThanOrEqualTo(points.Length, 3, nameof(points)); + Guard.NotNull(colors, nameof(colors)); + Guard.MustBeGreaterThan(colors.Length, 0, nameof(colors)); + + int size = points.Length; + + this.points = [.. points]; + this.colors = [.. colors]; + this.edges = new Edge[this.points.Length]; + + for (int i = 0; i < this.points.Length; i++) + { + this.edges[i] = new Edge(this.points[i % size], this.points[(i + 1) % size], ColorAt(i), ColorAt(i + 1)); + } + + this.CenterColor = CalculateCenterColor(this.colors); + + Color ColorAt(int index) => this.colors[index % this.colors.Length]; + } + + /// + /// Initializes a new instance of the class. + /// + /// Points that constitute a polygon that represents the gradient area. + /// Array of colors that correspond to each point in the polygon. + /// Color at the center of the gradient area to which the other colors converge. + public PathGradientBrush(PointF[] points, Color[] colors, Color centerColor) + : this(points, colors) + { + this.CenterColor = centerColor; + this.HasExplicitCenterColor = true; + } + + /// + /// Gets the polygon points that define the gradient area. + /// + public ReadOnlySpan Points => this.points; + + /// + /// Gets the colors that are mapped to the polygon points. + /// + public ReadOnlySpan Colors => this.colors; + + /// + /// Gets the color at the center of the gradient area. + /// + public Color CenterColor { get; } + + /// + /// Gets a value indicating whether the center color was explicitly supplied. + /// + public bool HasExplicitCenterColor { get; } + + /// + public override Brush Transform(Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + return this; + } + + PointF[] transformedPoints = new PointF[this.points.Length]; + for (int i = 0; i < transformedPoints.Length; i++) + { + transformedPoints[i] = PointF.Transform(this.points[i], matrix); + } + + return this.HasExplicitCenterColor + ? new PathGradientBrush(transformedPoints, this.colors, this.CenterColor) + : new PathGradientBrush(transformedPoints, this.colors); + } + + /// + public override bool Equals(Brush? other) + { + if (other is PathGradientBrush brush) + { + return this.CenterColor.Equals(brush.CenterColor) + && this.HasExplicitCenterColor.Equals(brush.HasExplicitCenterColor) + && this.edges?.SequenceEqual(brush.edges) == true; + } + + return false; + } + + /// + public override int GetHashCode() + => HashCode.Combine(this.edges, this.CenterColor, this.HasExplicitCenterColor); + + /// + public override BrushRenderer CreateRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + RectangleF region) + => new PathGradientBrushRenderer( + configuration, + options, + canvasWidth, + this.edges, + this.CenterColor, + this.HasExplicitCenterColor); + + private static Color CalculateCenterColor(Color[] colors) + { + Guard.NotNull(colors, nameof(colors)); + Guard.MustBeGreaterThan(colors.Length, 0, nameof(colors)); + + return Color.FromScaledVector(colors.Select(c => c.ToScaledVector4()).Aggregate((p1, p2) => p1 + p2) / colors.Length); + } + + private static float DistanceBetween(Vector2 p1, Vector2 p2) => (p2 - p1).Length(); + + private readonly struct Intersection + { + public Intersection(PointF point, float distance) + { + this.Point = point; + this.Distance = distance; + } + + public PointF Point { get; } + + public float Distance { get; } + } + + /// + /// An edge of the polygon that represents the gradient area. + /// + private class Edge : IEquatable + { + private readonly float length; + + public Edge(Vector2 start, Vector2 end, Color startColor, Color endColor) + { + this.Start = start; + this.End = end; + this.StartColor = startColor.ToScaledVector4(); + this.EndColor = endColor.ToScaledVector4(); + + this.length = DistanceBetween(this.End, this.Start); + } + + public Vector2 Start { get; } + + public Vector2 End { get; } + + public Vector4 StartColor { get; } + + public Vector4 EndColor { get; } + + public bool Intersect( + Vector2 start, + Vector2 end, + ref Vector2 ip) => + PolygonUtilities.LineSegmentToLineSegmentIgnoreCollinear(start, end, this.Start, this.End, ref ip); + + public Vector4 ColorAt(float distance) + { + float ratio = this.length > 0 ? distance / this.length : 0; + + return Vector4.Lerp(this.StartColor, this.EndColor, ratio); + } + + public Vector4 ColorAt(PointF point) => this.ColorAt(DistanceBetween(point, this.Start)); + + public bool Equals(Edge? other) + => other != null && + other.Start == this.Start && + other.End == this.End && + other.StartColor.Equals(this.StartColor) && + other.EndColor.Equals(this.EndColor); + + public override bool Equals(object? obj) => this.Equals(obj as Edge); + + public override int GetHashCode() + => HashCode.Combine(this.Start, this.End, this.StartColor, this.EndColor); + } + + /// + /// The path gradient brush applicator. + /// + /// The pixel format. + private sealed class PathGradientBrushRenderer : BrushRenderer + where TPixel : unmanaged, IPixel + { + private readonly Vector2 center; + + private readonly Vector4 centerColor; + + private readonly bool hasSpecialCenterColor; + + private readonly float maxDistance; + + private readonly IList edges; + + private readonly TPixel centerPixel; + + private readonly TPixel transparentPixel; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration instance to use when performing operations. + /// The graphics options. + /// The canvas width for the current render pass. + /// Edges of the polygon. + /// Color at the center of the gradient area to which the other colors converge. + /// Whether the center color is different from a smooth gradient between the edges. + public PathGradientBrushRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + IList edges, + Color centerColor, + bool hasSpecialCenterColor) + : base(configuration, options, canvasWidth) + { + this.edges = edges; + Vector2[] points = [.. edges.Select(s => s.Start)]; + + this.center = points.Aggregate((p1, p2) => p1 + p2) / edges.Count; + this.centerColor = centerColor.ToScaledVector4(); + this.hasSpecialCenterColor = hasSpecialCenterColor; + this.centerPixel = centerColor.ToPixel(); + this.maxDistance = points.Select(p => p - this.center).Max(d => d.Length()); + this.transparentPixel = Color.Transparent.ToPixel(); + } + + internal TPixel this[int x, int y] + { + get + { + // Match other gradient brushes by evaluating at pixel centers. + Vector2 point = new(x + 0.5F, y + 0.5F); + + if (point == this.center) + { + return this.centerPixel; + } + + if (this.edges.Count == 3 && !this.hasSpecialCenterColor) + { + if (!FindPointOnTriangle( + this.edges[0].Start, + this.edges[1].Start, + this.edges[2].Start, + point, + out float u, + out float v)) + { + return this.transparentPixel; + } + + Vector4 pointColor = ((1 - u - v) * this.edges[0].StartColor) + + (u * this.edges[0].EndColor) + + (v * this.edges[2].StartColor); + + return TPixel.FromScaledVector4(pointColor); + } + + Vector2 direction = Vector2.Normalize(point - this.center); + Vector2 end = point + (direction * this.maxDistance); + + (Edge Edge, Vector2 Point)? isc = this.FindIntersection(point, end); + + if (!isc.HasValue) + { + return this.transparentPixel; + } + + Vector2 intersection = isc.Value.Point; + Vector4 edgeColor = isc.Value.Edge.ColorAt(intersection); + + float length = DistanceBetween(intersection, this.center); + float ratio = length > 0 ? DistanceBetween(intersection, point) / length : 0; + + Vector4 color = Vector4.Lerp(edgeColor, this.centerColor, ratio); + + return TPixel.FromScaledVector4(color); + } + } + + /// + public override void Apply( + Span destinationRow, + ReadOnlySpan scanline, + int x, + int y, + BrushWorkspace workspace) + { + Span amounts = workspace.GetAmounts(scanline.Length); + Span overlays = workspace.GetOverlays(scanline.Length); + float blendPercentage = this.Options.BlendPercentage; + + // TODO: Remove bounds checks. + if (blendPercentage < 1) + { + for (int i = 0; i < scanline.Length; i++) + { + amounts[i] = scanline[i] * blendPercentage; + overlays[i] = this[x + i, y]; + } + } + else + { + for (int i = 0; i < scanline.Length; i++) + { + amounts[i] = scanline[i]; + overlays[i] = this[x + i, y]; + } + } + + this.Blender.Blend( + this.Configuration, + destinationRow, + destinationRow, + overlays, + amounts, + workspace.GetBlendScratch(scanline.Length, 3)); + } + + private (Edge Edge, Vector2 Point)? FindIntersection( + PointF start, + PointF end) + { + Vector2 ip = default; + Vector2 closestIntersection = default; + Edge? closestEdge = null; + float minDistance = float.MaxValue; + foreach (Edge edge in this.edges) + { + if (!edge.Intersect(start, end, ref ip)) + { + continue; + } + + float d = Vector2.DistanceSquared(start, ip); + if (d < minDistance) + { + minDistance = d; + closestEdge = edge; + closestIntersection = ip; + } + } + + return closestEdge != null ? (closestEdge, closestIntersection) : null; + } + + private static bool FindPointOnTriangle(Vector2 v1, Vector2 v2, Vector2 v3, Vector2 point, out float u, out float v) + { + Vector2 e1 = v2 - v1; + Vector2 e2 = v3 - v2; + Vector2 e3 = v1 - v3; + + Vector2 pv1 = point - v1; + Vector2 pv2 = point - v2; + Vector2 pv3 = point - v3; + + Vector3 d1 = Vector3.Cross(new Vector3(e1.X, e1.Y, 0), new Vector3(pv1.X, pv1.Y, 0)); + Vector3 d2 = Vector3.Cross(new Vector3(e2.X, e2.Y, 0), new Vector3(pv2.X, pv2.Y, 0)); + Vector3 d3 = Vector3.Cross(new Vector3(e3.X, e3.Y, 0), new Vector3(pv3.X, pv3.Y, 0)); + + if (Math.Sign(Vector3.Dot(d1, d2)) * Math.Sign(Vector3.Dot(d1, d3)) == -1 || Math.Sign(Vector3.Dot(d1, d2)) * Math.Sign(Vector3.Dot(d2, d3)) == -1) + { + u = 0; + v = 0; + return false; + } + + // From Real-Time Collision Detection + // https://gamedev.stackexchange.com/questions/23743/whats-the-most-efficient-way-to-find-barycentric-coordinates + float d00 = Vector2.Dot(e1, e1); + float d01 = -Vector2.Dot(e1, e3); + float d11 = Vector2.Dot(e3, e3); + float d20 = Vector2.Dot(pv1, e1); + float d21 = -Vector2.Dot(pv1, e3); + float denominator = (d00 * d11) - (d01 * d01); + u = ((d11 * d20) - (d01 * d21)) / denominator; + v = ((d00 * d21) - (d01 * d20)) / denominator; + return true; + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/PatternBrush.cs b/ImageSharp.Drawing/Processing/PatternBrush.cs new file mode 100644 index 0000000..ad916cc --- /dev/null +++ b/ImageSharp.Drawing/Processing/PatternBrush.cs @@ -0,0 +1,169 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Provides an implementation of a pattern brush for painting patterns. + /// + /// + /// The patterns that are used to create a custom pattern brush are made up of a repeating matrix of flags, + /// where each flag denotes whether to draw the foreground color or the background color. + /// so to create a new bool[,] with your flags + /// + /// For example if you wanted to create a diagonal line that repeat every 4 pixels you would use a pattern like so + /// 1000 + /// 0100 + /// 0010 + /// 0001 + /// + /// + /// or you want a horizontal stripe which is 3 pixels apart you would use a pattern like + /// 1 + /// 0 + /// 0 + /// + /// + public sealed class PatternBrush : Brush + { + /// + /// Initializes a new instance of the class. + /// + /// Color of the fore. + /// Color of the back. + /// The pattern. + public PatternBrush(Color foreColor, Color backColor, bool[,] pattern) + : this(foreColor, backColor, new DenseMatrix(pattern)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Color of the fore. + /// Color of the back. + /// The pattern. + internal PatternBrush(Color foreColor, Color backColor, in DenseMatrix pattern) + { + this.Pattern = new DenseMatrix(pattern.Columns, pattern.Rows); + for (int i = 0; i < pattern.Data.Length; i++) + { + if (pattern.Data[i]) + { + this.Pattern.Data[i] = foreColor; + } + else + { + this.Pattern.Data[i] = backColor; + } + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The brush. + internal PatternBrush(PatternBrush brush) => this.Pattern = brush.Pattern; + + /// + /// Gets the pattern color matrix. + /// + public DenseMatrix Pattern { get; } + + /// + public override bool Equals(Brush? other) + { + if (other is PatternBrush sb) + { + return sb.Pattern.Equals(this.Pattern); + } + + return false; + } + + /// + public override int GetHashCode() + => this.Pattern.GetHashCode(); + + /// + public override BrushRenderer CreateRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + RectangleF region) + => + new PatternBrushRenderer( + configuration, + options, + canvasWidth, + this.Pattern.ToPixelMatrix()); + + /// + /// The pattern brush applicator. + /// + /// The pixel format. + private sealed class PatternBrushRenderer : BrushRenderer + where TPixel : unmanaged, IPixel + { + private readonly DenseMatrix pattern; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration instance to use when performing operations. + /// The graphics options. + /// The canvas width for the current render pass. + /// The pattern. + public PatternBrushRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + in DenseMatrix pattern) + : base(configuration, options, canvasWidth) + => this.pattern = pattern; + + internal TPixel this[int x, int y] + { + get + { + x %= this.pattern.Columns; + y %= this.pattern.Rows; + + // 2d array index at row/column + return this.pattern[y, x]; + } + } + + /// + public override void Apply( + Span destinationRow, + ReadOnlySpan scanline, + int x, + int y, + BrushWorkspace workspace) + { + int patternY = y % this.pattern.Rows; + Span amounts = workspace.GetAmounts(scanline.Length); + Span overlays = workspace.GetOverlays(scanline.Length); + + for (int i = 0; i < scanline.Length; i++) + { + amounts[i] = Math.Clamp(scanline[i] * this.Options.BlendPercentage, 0, 1F); + + int patternX = (x + i) % this.pattern.Columns; + overlays[i] = this.pattern[patternY, patternX]; + } + + this.Blender.Blend( + this.Configuration, + destinationRow, + destinationRow, + overlays, + amounts, + workspace.GetBlendScratch(scanline.Length, 3)); + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/PatternPen.cs b/ImageSharp.Drawing/Processing/PatternPen.cs new file mode 100644 index 0000000..967ff05 --- /dev/null +++ b/ImageSharp.Drawing/Processing/PatternPen.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Defines a pen that can apply a pattern to a line with a set brush and thickness + /// + /// + /// The pattern will be in to the form of + /// + /// new float[]{ 1f, 2f, 0.5f} + /// + /// this will be converted into a pattern that is 3.5 times longer that the width with 3 sections. + /// + /// Section 1 will be width long (making a square) and will be filled by the brush. + /// Section 2 will be width * 2 long and will be empty. + /// Section 3 will be width/2 long and will be filled. + /// + /// The pattern will immediately repeat without gap. + /// + public class PatternPen : Pen + { + /// + /// Initializes a new instance of the class. + /// + /// The color. + /// The stroke pattern. + public PatternPen(Color color, float[] strokePattern) + : base(new SolidBrush(color), 1, strokePattern) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The color. + /// The stroke width in the path's local coordinate space before any drawing transform is applied. + /// The stroke pattern. + public PatternPen(Color color, float strokeWidth, float[] strokePattern) + : base(new SolidBrush(color), strokeWidth, strokePattern) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The brush used to fill the stroke outline. + /// The stroke width in the path's local coordinate space before any drawing transform is applied. + /// The stroke pattern. + public PatternPen(Brush strokeFill, float strokeWidth, float[] strokePattern) + : base(strokeFill, strokeWidth, strokePattern) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The pen options. + public PatternPen(PenOptions options) + : base(options) + { + } + + /// + public override bool Equals(Pen? other) + { + if (other is PatternPen) + { + return base.Equals(other); + } + + return false; + } + + /// + public override IPath GeneratePath(IPath path, float strokeWidth) + => path.GenerateOutline(strokeWidth, this.StrokePattern.Span, this.StrokeOptions); + } +} diff --git a/ImageSharp.Drawing/Processing/Pen.cs b/ImageSharp.Drawing/Processing/Pen.cs new file mode 100644 index 0000000..e7f7ae4 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Pen.cs @@ -0,0 +1,120 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// The base class for pens that can apply a pattern to a line with a set brush and thickness + /// + /// + /// The pattern will be in to the form of + /// + /// new float[]{ 1f, 2f, 0.5f} + /// + /// this will be converted into a pattern that is 3.5 times longer that the width with 3 sections. + /// + /// Section 1 will be width long (making a square) and will be filled by the brush. + /// Section 2 will be width * 2 long and will be empty. + /// Section 3 will be width/2 long and will be filled. + /// + /// The pattern will immediately repeat without gap. + /// + public abstract class Pen : IEquatable + { + private readonly float[] pattern; + + /// + /// Initializes a new instance of the class. + /// + /// The brush used to fill the stroke outline. + protected Pen(Brush strokeFill) + : this(strokeFill, 1) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The brush used to fill the stroke outline. + /// The stroke width in the path's local coordinate space before any drawing transform is applied. + protected Pen(Brush strokeFill, float strokeWidth) + : this(strokeFill, strokeWidth, Pens.EmptyPattern) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The brush used to fill the stroke outline. + /// The stroke width in the path's local coordinate space before any drawing transform is applied. + /// The stroke pattern. + protected Pen(Brush strokeFill, float strokeWidth, float[] strokePattern) + { + Guard.NotNull(strokeFill, nameof(strokeFill)); + + Guard.MustBeGreaterThan(strokeWidth, 0, nameof(strokeWidth)); + Guard.NotNull(strokePattern, nameof(strokePattern)); + + this.StrokeFill = strokeFill; + this.StrokeWidth = strokeWidth; + this.pattern = strokePattern; + this.StrokeOptions = new StrokeOptions(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The pen options. + protected Pen(PenOptions options) + { + this.StrokeFill = options.StrokeFill; + this.StrokeWidth = options.StrokeWidth; + this.pattern = options.StrokePattern; + this.StrokeOptions = options.StrokeOptions ?? new StrokeOptions(); + } + + /// + public Brush StrokeFill { get; } + + /// + public float StrokeWidth { get; } + + /// + public ReadOnlyMemory StrokePattern => this.pattern; + + /// + public StrokeOptions StrokeOptions { get; } + + /// + /// Applies the styling from the pen to a path and generate a new path with the final vector. + /// + /// The source path + /// The with the pen styling applied. + public IPath GeneratePath(IPath path) + => this.GeneratePath(path, this.StrokeWidth); + + /// + /// Applies the styling from the pen to a path and generate a new path with the final vector. + /// + /// The source path + /// The stroke width in the path's local coordinate space before any drawing transform is applied. + /// The with the pen styling applied. + public abstract IPath GeneratePath(IPath path, float strokeWidth); + + /// + public virtual bool Equals(Pen? other) + => other != null + && this.StrokeWidth == other.StrokeWidth + && this.StrokeFill.Equals(other.StrokeFill) + && this.StrokeOptions.Equals(other.StrokeOptions) + && this.StrokePattern.Span.SequenceEqual(other.StrokePattern.Span); + + /// + public override bool Equals(object? obj) => this.Equals(obj as Pen); + + /// + public override int GetHashCode() + => HashCode.Combine(this.StrokeWidth, this.StrokeFill, this.StrokeOptions, this.pattern); + } +} diff --git a/ImageSharp.Drawing/Processing/PenOptions.cs b/ImageSharp.Drawing/Processing/PenOptions.cs new file mode 100644 index 0000000..6965b98 --- /dev/null +++ b/ImageSharp.Drawing/Processing/PenOptions.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Provides a set of configurations options for pens. + /// + public struct PenOptions + { + /// + /// Initializes a new instance of the struct. + /// + /// The stroke width in the path's local coordinate space before any drawing transform is applied. + public PenOptions(float strokeWidth) + : this(Color.Black, strokeWidth) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The color. + /// The stroke width in the path's local coordinate space before any drawing transform is applied. + public PenOptions(Color color, float strokeWidth) + : this(color, strokeWidth, null) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The color. + /// The stroke width in the path's local coordinate space before any drawing transform is applied. + /// The stroke pattern. + public PenOptions(Color color, float strokeWidth, float[]? strokePattern) + : this(new SolidBrush(color), strokeWidth, strokePattern) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The brush used to fill the stroke outline. + /// The stroke width in the path's local coordinate space before any drawing transform is applied. + /// The stroke pattern. + public PenOptions(Brush strokeFill, float strokeWidth, float[]? strokePattern) + { + Guard.MustBeGreaterThan(strokeWidth, 0, nameof(strokeWidth)); + + this.StrokeFill = strokeFill; + this.StrokeWidth = strokeWidth; + this.StrokePattern = strokePattern ?? Pens.EmptyPattern; + this.StrokeOptions = new StrokeOptions(); + } + + /// + /// Gets the brush used to fill the stroke outline. Defaults to . + /// + public Brush StrokeFill { get; } + + /// + /// Gets the stroke width in the path's local coordinate space before any drawing transform is applied. Defaults to 1. + /// + public float StrokeWidth { get; } + + /// + /// Gets the stroke pattern. + /// + public float[] StrokePattern { get; } + + /// + /// Gets or sets the stroke geometry options used to stroke paths drawn with this pen. + /// + public StrokeOptions? StrokeOptions { get; set; } + } +} diff --git a/ImageSharp.Drawing/Processing/Pens.cs b/ImageSharp.Drawing/Processing/Pens.cs new file mode 100644 index 0000000..77c9c98 --- /dev/null +++ b/ImageSharp.Drawing/Processing/Pens.cs @@ -0,0 +1,110 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Contains a collection of common pen styles. + /// + public static class Pens + { + private static readonly float[] DashDotPattern = [3f, 1f, 1f, 1f]; + private static readonly float[] DashDotDotPattern = [3f, 1f, 1f, 1f, 1f, 1f]; + private static readonly float[] DottedPattern = [1f, 1f]; + private static readonly float[] DashedPattern = [3f, 1f]; + internal static readonly float[] EmptyPattern = []; + + /// + /// Create a solid pen without any drawing patterns + /// + /// The color. + /// The . + public static SolidPen Solid(Color color) => new(color); + + /// + /// Create a solid pen without any drawing patterns + /// + /// The brush. + /// The . + public static SolidPen Solid(Brush brush) => new(brush); + + /// + /// Create a solid pen without any drawing patterns + /// + /// The color. + /// The width. + /// The . + public static SolidPen Solid(Color color, float width) => new(color, width); + + /// + /// Create a solid pen without any drawing patterns + /// + /// The brush. + /// The width. + /// The . + public static SolidPen Solid(Brush brush, float width) => new(brush, width); + + /// + /// Create a pen with a 'Dash' drawing patterns + /// + /// The color. + /// The width. + /// The . + public static PatternPen Dash(Color color, float width) => new(color, width, DashedPattern); + + /// + /// Create a pen with a 'Dash' drawing patterns + /// + /// The brush. + /// The width. + /// The . + public static PatternPen Dash(Brush brush, float width) => new(brush, width, DashedPattern); + + /// + /// Create a pen with a 'Dot' drawing patterns + /// + /// The color. + /// The width. + /// The . + public static PatternPen Dot(Color color, float width) => new(color, width, DottedPattern); + + /// + /// Create a pen with a 'Dot' drawing patterns + /// + /// The brush. + /// The width. + /// The . + public static PatternPen Dot(Brush brush, float width) => new(brush, width, DottedPattern); + + /// + /// Create a pen with a 'Dash Dot' drawing patterns + /// + /// The color. + /// The width. + /// The . + public static PatternPen DashDot(Color color, float width) => new(color, width, DashDotPattern); + + /// + /// Create a pen with a 'Dash Dot' drawing patterns + /// + /// The brush. + /// The width. + /// The . + public static PatternPen DashDot(Brush brush, float width) => new(brush, width, DashDotPattern); + + /// + /// Create a pen with a 'Dash Dot Dot' drawing patterns + /// + /// The color. + /// The width. + /// The . + public static PatternPen DashDotDot(Color color, float width) => new(color, width, DashDotDotPattern); + + /// + /// Create a pen with a 'Dash Dot Dot' drawing patterns + /// + /// The brush. + /// The width. + /// The . + public static PatternPen DashDotDot(Brush brush, float width) => new(brush, width, DashDotDotPattern); + } +} diff --git a/ImageSharp.Drawing/Processing/RadialGradientBrush.cs b/ImageSharp.Drawing/Processing/RadialGradientBrush.cs new file mode 100644 index 0000000..bc30566 --- /dev/null +++ b/ImageSharp.Drawing/Processing/RadialGradientBrush.cs @@ -0,0 +1,455 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Helpers; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// A radial gradient brush defined by either one circle or two circles. + /// When one circle is provided, the gradient parameter is the distance from the center divided by the radius. + /// When two circles are provided, the gradient parameter is computed along the family of circles interpolating + /// between the start and end circles. + /// + public sealed class RadialGradientBrush : GradientBrush + { + /// + /// Initializes a new instance of the class using a single circle. + /// + /// The center of the circular gradient. + /// The radius of the circular gradient. + /// Defines how the colors in the gradient are repeated. + /// The ordered gradient stops. + public RadialGradientBrush( + PointF center, + float radius, + GradientRepetitionMode repetitionMode, + params ColorStop[] colorStops) + : base(repetitionMode, colorStops) + { + this.Center0 = center; + this.Radius0 = radius; + this.Center1 = null; + this.Radius1 = null; + } + + /// + /// Initializes a new instance of the class using two circles. + /// + /// The center of the starting circle. + /// The radius of the starting circle. + /// The center of the ending circle. + /// The radius of the ending circle. + /// Defines how the colors in the gradient are repeated. + /// The ordered gradient stops. + public RadialGradientBrush( + PointF startCenter, + float startRadius, + PointF endCenter, + float endRadius, + GradientRepetitionMode repetitionMode, + params ColorStop[] colorStops) + : base(repetitionMode, colorStops) + { + this.Center0 = startCenter; + this.Radius0 = startRadius; + this.Center1 = endCenter; + this.Radius1 = endRadius; + } + + /// + /// Gets the center of the starting circle. + /// + public PointF Center0 { get; } + + /// + /// Gets the radius of the starting circle. + /// + public float Radius0 { get; } + + /// + /// Gets the center of the ending circle, or for single-circle form. + /// + public PointF? Center1 { get; } + + /// + /// Gets the radius of the ending circle, or for single-circle form. + /// + public float? Radius1 { get; } + + /// + /// Gets a value indicating whether this is a two-circle radial gradient. + /// + public bool IsTwoCircle => this.Center1.HasValue && this.Radius1.HasValue; + + /// + public override Brush Transform(Matrix4x4 matrix) + { + PointF tc0 = PointF.Transform(this.Center0, matrix); + float scale = MatrixUtilities.GetAverageScale(in matrix); + if (this.IsTwoCircle) + { + PointF tc1 = PointF.Transform(this.Center1!.Value, matrix); + return new RadialGradientBrush(tc0, this.Radius0 * scale, tc1, this.Radius1!.Value * scale, this.RepetitionMode, this.ColorStopsArray); + } + + return new RadialGradientBrush(tc0, this.Radius0 * scale, this.RepetitionMode, this.ColorStopsArray); + } + + /// + public override bool Equals(Brush? other) + { + if (other is RadialGradientBrush b) + { + return base.Equals(other) + && this.Center0.Equals(b.Center0) + && this.Radius0.Equals(b.Radius0) + && Nullable.Equals(this.Center1, b.Center1) + && Nullable.Equals(this.Radius1, b.Radius1); + } + + return false; + } + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), this.Center0, this.Radius0, this.Center1, this.Radius1); + + /// + public override BrushRenderer CreateRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + RectangleF region) + => new RadialGradientBrushRenderer( + configuration, + options, + canvasWidth, + this.Center0, + this.Radius0, + this.Center1, + this.Radius1, + this.ColorStopsArray, + this.RepetitionMode); + + /// + /// The radial gradient brush applicator. + /// + private sealed class RadialGradientBrushRenderer : GradientBrushRenderer + where TPixel : unmanaged, IPixel + { + private const float GradientEpsilon = 1F / (1 << 12); + + // Single-circle fields + private readonly bool isTwoCircle; + private readonly float c0x; + private readonly float c0y; + private readonly float r0; + + // Two-circle gradient fields. + // The transform changes coordinates so the gradient can be evaluated + // with simple formulas around a canonical line/circle configuration. + private readonly Matrix3x2 radialTransform; + private readonly float focalX; + private readonly float radius; + private readonly bool isStrip; + private readonly bool isCircular; + private readonly bool isFocalOnCircle; + private readonly bool isSwapped; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration instance to use when performing operations. + /// The graphics options. + /// The canvas width for the current render pass. + /// Center of the starting circle. + /// Radius of the starting circle. + /// Center of the ending circle, or null to use single-circle form. + /// Radius of the ending circle, or null to use single-circle form. + /// Definition of colors. + /// How the colors are repeated beyond the first gradient. + public RadialGradientBrushRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + PointF center0, + float radius0, + PointF? center1, + float? radius1, + ColorStop[] colorStops, + GradientRepetitionMode repetitionMode) + : base(configuration, options, canvasWidth, colorStops, repetitionMode) + { + this.c0x = center0.X; + this.c0y = center0.Y; + this.r0 = radius0; + + this.isTwoCircle = center1.HasValue && radius1.HasValue; + + if (this.isTwoCircle) + { + ConicalGradientParameters parameters = CreateConicalGradientParameters( + center0, + radius0, + center1!.Value, + radius1!.Value); + + this.radialTransform = parameters.Transform; + this.focalX = parameters.FocalX; + this.radius = parameters.Radius; + this.isStrip = parameters.IsStrip; + this.isCircular = parameters.IsCircular; + this.isFocalOnCircle = parameters.IsFocalOnCircle; + this.isSwapped = parameters.IsSwapped; + } + else + { + this.radialTransform = Matrix3x2.Identity; + this.focalX = 0F; + this.radius = 0F; + this.isStrip = false; + this.isCircular = false; + this.isFocalOnCircle = false; + this.isSwapped = false; + } + } + + /// + protected override float PositionOnGradient(float x, float y) + { + if (!this.isTwoCircle) + { + float ux = x - this.c0x, uy = y - this.c0y; + return MathF.Sqrt((ux * ux) + (uy * uy)) / this.r0; + } + + // Move the sample into the canonical coordinate system where the + // end circle lies on the x-axis and the conic can be solved using + // closed-form expressions. + Vector2 local = Vector2.Transform(new Vector2(x, y), this.radialTransform); + float localX = local.X; + float localY = local.Y; + float xx = localX * localX; + float yy = localY * localY; + float t; + + if (this.isStrip) + { + // Strip gradients are bounded by a band around the axis. + // radius stores the squared half-width in normalized space, + // so points outside the band are invalid. + float a = this.radius - yy; + if (a < 0F) + { + return float.NaN; + } + + // Once inside the band, the parameter advances along the axis. + t = MathF.Sqrt(a) + localX; + } + else if (this.isFocalOnCircle) + { + // This degenerate case reduces to a rational expression where + // the focal point sits exactly on the limiting circle. + if (localX == 0F) + { + return float.NaN; + } + + t = (xx + yy) / localX; + if (t < 0F) + { + return float.NaN; + } + } + else if (this.radius > 1F) + { + // Wide cones use a circular norm. The x term shifts the root + // back into the original gradient parameterization. + float radiusReciprocal = this.isCircular ? 0F : 1F / this.radius; + t = MathF.Sqrt(xx + yy) - (localX * radiusReciprocal); + } + else + { + // Narrow cones use a hyperbolic form. Points with x^2 < y^2 + // lie outside the valid branch and must not contribute. + float a = xx - yy; + if (a < 0F) + { + return float.NaN; + } + + // lessScale picks the correct branch of the hyperbola after + // swaps and orientation changes. + float lessScale = (this.isSwapped || (1F - this.focalX) < 0F) ? -1F : 1F; + t = (lessScale * MathF.Sqrt(a)) - (localX / this.radius); + if (t < 0F) + { + return float.NaN; + } + } + + // Convert back from the normalized local solution into the brush's + // gradient parameter, then undo the earlier swap if required. + t = this.focalX + (MathF.Sign(1F - this.focalX) * t); + return this.isSwapped ? 1F - t : t; + } + + private static ConicalGradientParameters CreateConicalGradientParameters( + PointF center0, + float radius0, + PointF center1, + float radius1) + { + PointF p0 = center0; + PointF p1 = center1; + float r0 = radius0; + float r1 = radius1; + + if (MathF.Abs(r0 - r1) <= GradientEpsilon) + { + // When both circles have the same radius, the locus becomes a + // strip: solve along the axis between the centers, with the + // radius contributing only a perpendicular cutoff. + float scaled = r0 / Distance(p0, p1); + return new ConicalGradientParameters( + TwoPointToUnitLine(p0, p1), + 0F, + scaled * scaled, + isStrip: true, + isCircular: false, + isFocalOnCircle: false, + isSwapped: false); + } + + bool isCircular = false; + if (p0 == p1) + { + isCircular = true; + + // Equal centers make the conic circular. Nudge slightly so the + // line construction below stays invertible. + p0 = new PointF(p0.X + GradientEpsilon, p0.Y + GradientEpsilon); + } + + bool isSwapped = false; + if (r1 == 0F) + { + isSwapped = true; + + // Put the zero-radius focus on the start side so the later + // formulas keep one orientation. + (p0, p1) = (p1, p0); + (r0, r1) = (r1, r0); + } + + // focalX describes where the focal point lies along the line from + // the start circle to the end circle. Values outside [0, 1] are + // valid and correspond to cones whose focus lies beyond an endpoint. + float focalX = r0 / (r0 - r1); + PointF cf = new( + ((1F - focalX) * p0.X) + (focalX * p1.X), + ((1F - focalX) * p0.Y) + (focalX * p1.Y)); + + // radius is the end-circle radius expressed in the normalized frame + // built from the focal point and the end center. + float radius = r1 / Distance(cf, p1); + Matrix3x2 userToUnitLine = TwoPointToUnitLine(cf, p1); + Matrix3x2 transform; + bool isFocalOnCircle = false; + + if (MathF.Abs(radius - 1F) <= GradientEpsilon) + { + isFocalOnCircle = true; + + // When the focal point lies on the circle, the quadratic terms + // collapse to a simpler rational form. + float scale = 0.5F * MathF.Abs(1F - focalX); + transform = userToUnitLine * Matrix3x2.CreateScale(scale); + } + else + { + // Otherwise scale the unit-line frame so the gradient can be + // tested with either x^2 + y^2 or x^2 - y^2, depending on + // whether the cone opens wider or narrower than the unit case. + float a = (radius * radius) - 1F; + float scaleRatio = MathF.Abs(1F - focalX) / a; + float scaleX = radius * scaleRatio; + float scaleY = MathF.Sqrt(MathF.Abs(a)) * scaleRatio; + transform = userToUnitLine * Matrix3x2.CreateScale(scaleX, scaleY); + } + + return new ConicalGradientParameters( + transform, + focalX, + radius, + isStrip: false, + isCircular: isCircular, + isFocalOnCircle: isFocalOnCircle, + isSwapped: isSwapped); + } + + private static float Distance(Vector2 p0, Vector2 p1) => Vector2.Distance(p0, p1); + + private static Matrix3x2 TwoPointToUnitLine(PointF p0, PointF p1) + { + // Build a change-of-basis that sends the segment p0->p1 to the + // unit line. That lets the gradient math work in one fixed frame + // instead of re-deriving equations for every brush. + Matrix3x2 source = FromPoly2(p0, p1); + Matrix3x2.Invert(source, out Matrix3x2 inverse); + return inverse * FromPoly2(new PointF(0F, 0F), new PointF(1F, 0F)); + } + + private static Matrix3x2 FromPoly2(PointF p0, PointF p1) + + // This affine frame uses p0 as the origin and p0->p1 as one axis. + // Its inverse is the basis change we need for normalization. + => new( + p1.Y - p0.Y, + p0.X - p1.X, + p1.X - p0.X, + p1.Y - p0.Y, + p0.X, + p0.Y); + + private readonly struct ConicalGradientParameters + { + public ConicalGradientParameters( + Matrix3x2 transform, + float focalX, + float radius, + bool isStrip, + bool isCircular, + bool isFocalOnCircle, + bool isSwapped) + { + this.Transform = transform; + this.FocalX = focalX; + this.Radius = radius; + this.IsStrip = isStrip; + this.IsCircular = isCircular; + this.IsFocalOnCircle = isFocalOnCircle; + this.IsSwapped = isSwapped; + } + + public Matrix3x2 Transform { get; } + + public float FocalX { get; } + + public float Radius { get; } + + public bool IsStrip { get; } + + public bool IsCircular { get; } + + public bool IsFocalOnCircle { get; } + + public bool IsSwapped { get; } + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs b/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs new file mode 100644 index 0000000..e71bc99 --- /dev/null +++ b/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Adds extensions that allow configuring the drawing backend implementation. + /// + public static class RasterizerDefaultsExtensions + { + /// + /// Sets the drawing backend against the source image processing context. + /// + /// The image processing context to store the backend against. + /// The backend to use. + /// The passed in to allow chaining. + internal static IImageProcessingContext SetDrawingBackend(this IImageProcessingContext context, IDrawingBackend backend) + { + Guard.NotNull(backend, nameof(backend)); + context.Properties[typeof(IDrawingBackend)] = backend; + + return context; + } + + /// + /// Sets the default drawing backend against the configuration. + /// + /// The configuration to store the backend against. + /// The backend to use. + public static void SetDrawingBackend(this Configuration configuration, IDrawingBackend backend) + { + Guard.NotNull(backend, nameof(backend)); + configuration.Properties[typeof(IDrawingBackend)] = backend; + } + + /// + /// Gets the drawing backend from the source image processing context. + /// + /// The image processing context to retrieve the backend from. + /// The configured backend. + internal static IDrawingBackend GetDrawingBackend(this IImageProcessingContext context) + { + if (context.Properties.TryGetValue(typeof(IDrawingBackend), out object? backend) && + backend is IDrawingBackend configured) + { + return configured; + } + + return context.Configuration.GetDrawingBackend(); + } + + /// + /// Gets the default drawing backend from the configuration. + /// + /// The configuration to retrieve the backend from. + /// The configured backend. + internal static IDrawingBackend GetDrawingBackend(this Configuration configuration) + { + if (configuration.Properties.TryGetValue(typeof(IDrawingBackend), out object? backend) && + backend is IDrawingBackend configured) + { + return configured; + } + + IDrawingBackend defaultBackend = DefaultDrawingBackend.Instance; + configuration.Properties[typeof(IDrawingBackend)] = defaultBackend; + return defaultBackend; + } + } +} diff --git a/ImageSharp.Drawing/Processing/RecolorBrush.cs b/ImageSharp.Drawing/Processing/RecolorBrush.cs new file mode 100644 index 0000000..9c503ae --- /dev/null +++ b/ImageSharp.Drawing/Processing/RecolorBrush.cs @@ -0,0 +1,145 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Provides an implementation of a brush that can recolor an image + /// + public sealed class RecolorBrush : Brush + { + /// + /// Initializes a new instance of the class. + /// + /// Color of the source. + /// Color of the target. + /// The threshold as a value between 0 and 1. + public RecolorBrush(Color sourceColor, Color targetColor, float threshold) + { + this.SourceColor = sourceColor; + this.Threshold = threshold; + this.TargetColor = targetColor; + } + + /// + /// Gets the threshold. + /// + public float Threshold { get; } + + /// + /// Gets the source color. + /// + public Color SourceColor { get; } + + /// + /// Gets the target color. + /// + public Color TargetColor { get; } + + /// + public override BrushRenderer CreateRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + RectangleF region) + => new RecolorBrushRenderer( + configuration, + options, + canvasWidth, + this.SourceColor.ToPixel(), + this.TargetColor.ToPixel(), + this.Threshold); + + /// + public override bool Equals(Brush? other) + { + if (other is RecolorBrush brush) + { + return this.SourceColor.Equals(brush.SourceColor) + && this.TargetColor.Equals(brush.TargetColor) + && this.Threshold == brush.Threshold; + } + + return false; + } + + /// + public override int GetHashCode() + => HashCode.Combine(this.Threshold, this.SourceColor, this.TargetColor); + + /// + /// The recolor brush applicator. + /// + /// The pixel format. + private sealed class RecolorBrushRenderer : BrushRenderer + where TPixel : unmanaged, IPixel + { + private readonly Vector4 sourceColor; + private readonly float threshold; + private readonly TPixel targetColorPixel; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration instance to use when performing operations. + /// The options + /// The canvas width for the current render pass. + /// Color of the source. + /// Color of the target. + /// The threshold . + public RecolorBrushRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + TPixel sourceColor, + TPixel targetColor, + float threshold) + : base(configuration, options, canvasWidth) + { + this.sourceColor = sourceColor.ToScaledVector4(); + this.targetColorPixel = targetColor; + + // TODO: Review this. We can skip the conversion from/to Vector4. + // Lets hack a min max extremes for a color space by letting the IPackedPixel clamp our values to something in the correct spaces :) + TPixel maxColor = TPixel.FromVector4(new Vector4(float.MaxValue)); + TPixel minColor = TPixel.FromVector4(new Vector4(float.MinValue)); + this.threshold = Vector4.DistanceSquared(maxColor.ToVector4(), minColor.ToVector4()) * threshold; + } + + /// + public override void Apply( + Span destinationRow, + ReadOnlySpan scanline, + int x, + int y, + BrushWorkspace workspace) + { + Span amounts = workspace.GetAmounts(scanline.Length); + Span overlays = workspace.GetOverlays(scanline.Length); + + for (int i = 0; i < scanline.Length; i++) + { + amounts[i] = scanline[i] * this.Options.BlendPercentage; + TPixel result = destinationRow[i]; + Vector4 background = result.ToVector4(); + float distance = Vector4.DistanceSquared(background, this.sourceColor); + overlays[i] = distance <= this.threshold + ? this.Blender.Blend(result, this.targetColorPixel, (this.threshold - distance) / this.threshold) + : result; + } + + this.Blender.Blend( + this.Configuration, + destinationRow, + destinationRow, + overlays, + amounts, + workspace.GetBlendScratch(scanline.Length, 3)); + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.Brushes.cs b/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.Brushes.cs new file mode 100644 index 0000000..b28bcfd --- /dev/null +++ b/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.Brushes.cs @@ -0,0 +1,204 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using SixLabors.Fonts; +using SixLabors.Fonts.Rendering; +using SixLabors.ImageSharp.Drawing.Helpers; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text { + /// + /// Utilities to translate format-agnostic paints (from Fonts) into ImageSharp.Drawing brushes. + /// + internal sealed partial class RichTextGlyphRenderer + { + /// + /// Attempts to create an ImageSharp.Drawing from a . + /// + /// The paint definition coming from the interpreter. + /// A transform to apply to the brush coordinates. + /// The resulting brush, or if the paint is unsupported. + /// if a brush could be created; otherwise, . + public static bool TryCreateBrush([NotNullWhen(true)] Paint? paint, Matrix4x4 transform, [NotNullWhen(true)] out Brush? brush) + { + brush = null; + + if (paint is null) + { + return false; + } + + switch (paint) + { + case SolidPaint sp: + brush = new SolidBrush(ToColor(sp.Color, sp.Opacity)); + return true; + + case LinearGradientPaint lg: + return TryCreateLinearGradientBrush(lg, transform, out brush); + case RadialGradientPaint rg: + return TryCreateRadialGradientBrush(rg, transform, out brush); + case SweepGradientPaint sg: + return TryCreateSweepGradientBrush(sg, transform, out brush); + default: + return false; + } + } + + /// + /// Creates a from a . + /// + /// The linear gradient paint. + /// The transform to apply to the gradient points. + /// The resulting brush. + /// if created; otherwise, . + private static bool TryCreateLinearGradientBrush(LinearGradientPaint paint, Matrix4x4 transform, out Brush? brush) + { + // Map gradient stops (apply paint opacity multiplier to each stop's alpha). + ColorStop[] stops = ToColorStops(paint.Stops, paint.Opacity); + + // Map spread method. + GradientRepetitionMode mode = MapSpread(paint.Spread); + + PointF p0 = paint.P0; + PointF p1 = paint.P1; + PointF? p2 = paint.P2; + + // Apply any transform defined on the paint. + if (!transform.IsIdentity) + { + p0 = PointF.Transform(p0, transform); + p1 = PointF.Transform(p1, transform); + + if (p2.HasValue) + { + p2 = PointF.Transform(p2.Value, transform); + } + } + + if (p2.HasValue) + { + brush = new LinearGradientBrush(p0, p1, p2.Value, mode, stops); + return true; + } + + brush = new LinearGradientBrush(p0, p1, mode, stops); + return true; + } + + /// + /// Creates a from a . + /// + /// The radial gradient paint. + /// The transform to apply to the gradient center point. + /// The resulting brush. + /// if created; otherwise, . + private static bool TryCreateRadialGradientBrush(RadialGradientPaint paint, Matrix4x4 transform, out Brush? brush) + { + // Map gradient stops (apply paint opacity multiplier to each stop's alpha). + ColorStop[] stops = ToColorStops(paint.Stops, paint.Opacity); + + // Map spread method. + GradientRepetitionMode mode = MapSpread(paint.Spread); + + // Apply any transform defined on the paint. + PointF center0 = paint.Center0; + PointF center1 = paint.Center1; + float radius0 = paint.Radius0; + float radius1 = paint.Radius1; + if (!transform.IsIdentity) + { + center0 = PointF.Transform(center0, transform); + center1 = PointF.Transform(center1, transform); + float scale = MatrixUtilities.GetAverageScale(in transform); + radius0 *= scale; + radius1 *= scale; + } + + brush = new RadialGradientBrush(center0, radius0, center1, radius1, mode, stops); + return true; + } + + /// + /// Creates a from a . + /// + /// The sweep gradient paint. + /// The transform to apply to the gradient center point. + /// The resulting brush. + /// if created; otherwise, . + private static bool TryCreateSweepGradientBrush(SweepGradientPaint paint, Matrix4x4 transform, out Brush? brush) + { + // Map gradient stops (apply paint opacity multiplier to each stop's alpha). + ColorStop[] stops = ToColorStops(paint.Stops, paint.Opacity); + + // Map spread method. + GradientRepetitionMode mode = MapSpread(paint.Spread); + + // Apply any transform defined on the paint. + PointF center = paint.Center; + if (!transform.IsIdentity) + { + center = PointF.Transform(center, transform); + } + + brush = new SweepGradientBrush(center, paint.StartAngle, paint.EndAngle, mode, stops); + return true; + } + + /// + /// Maps an to . + /// + /// The spread method. + /// The repetition mode. + private static GradientRepetitionMode MapSpread(SpreadMethod spread) + => spread switch + { + SpreadMethod.Reflect => GradientRepetitionMode.Reflect, + SpreadMethod.Repeat => GradientRepetitionMode.Repeat, + + // Pad extends edge colors, which matches 'None' (not 'DontFill'). + _ => GradientRepetitionMode.None, + }; + + /// + /// Converts gradient stops and applies a paint opacity multiplier. + /// + /// The source stops. + /// The paint opacity in range [0,1]. + /// An array of . + private static ColorStop[] ToColorStops(ReadOnlySpan stops, float paintOpacity) + { + if (stops.Length == 0) + { + return []; + } + + ColorStop[] result = new ColorStop[stops.Length]; + + for (int i = 0; i < stops.Length; i++) + { + GradientStop s = stops[i]; + Color c = ToColor(s.Color, paintOpacity); + result[i] = new ColorStop(s.Offset, c); + } + + return result; + } + + /// + /// Converts a with an additional opacity multiplier to ImageSharp . + /// + /// The glyph color. + /// The opacity multiplier in range [0,1]. + /// The ImageSharp color. + private static Color ToColor(in GlyphColor c, float opacity) + { + float a = Math.Clamp(c.A / 255f * Math.Clamp(opacity, 0f, 1f), 0f, 1f); + byte aa = (byte)MathF.Round(a * 255f); + return Color.FromPixel(new Rgba32(c.R, c.G, c.B, aa)); + } + } +} diff --git a/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs b/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs new file mode 100644 index 0000000..e361357 --- /dev/null +++ b/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs @@ -0,0 +1,970 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.Fonts; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Unicode; +using SixLabors.ImageSharp.Drawing.Text; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text { + /// + /// Allows the rendering of rich text configured via . + /// + internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposable + { + // --- Render-pass ordering constants --- + // Within DrawTextOperations, operations are sorted first by RenderPass so that + // fills paint beneath outlines, and outlines beneath decorations. + private const byte RenderOrderFill = 0; + private const byte RenderOrderOutline = 1; + private const byte RenderOrderDecoration = 2; + + private readonly DrawingOptions drawingOptions; + + /// The default pen supplied by the caller (e.g. from DrawText(..., pen)). + private readonly Pen? defaultPen; + + /// The default brush supplied by the caller (e.g. from DrawText(..., brush)). + private readonly Brush? defaultBrush; + + /// + /// When the text is laid out along a path, this holds the path internals + /// for point-along-path queries. for normal (linear) text. + /// + private readonly IPathInternals? path; + private bool isDisposed; + + // --- Per-glyph mutable state reset in BeginGlyph --- + + /// The (or ) governing the current glyph. + private TextRun? currentTextRun; + + /// Brush resolved from the current , or . + private Brush? currentBrush; + + /// Pen resolved from the current , or . + private Pen? currentPen; + + /// The fill rule for the current color layer (COLR). + private FillRule currentFillRule; + + /// Alpha composition mode active for the current glyph/layer. + private PixelAlphaCompositionMode currentCompositionMode; + + /// Color blending mode active for the current glyph/layer. + private PixelColorBlendingMode currentBlendingMode; + + /// Whether the current glyph uses vertical layout (affects decoration orientation). + private bool currentDecorationIsVertical; + + /// Set to when is called, cleared in . + private bool hasLayer; + + // --- Glyph outline cache --- + // Glyphs that share the same CacheKey (same glyph id, sub-pixel position quantized + // to 1/AccuracyMultiple, pen reference, etc.) reuse the translated IPath from the + // first occurrence. This avoids re-building the full outline for repeated characters. + // + // AccuracyMultiple = 8 means sub-pixel positions are quantized to 1/8 px steps. + // Benchmarked to give <0.2% image difference vs. uncached, with >60% cache hit ratio. + private const float AccuracyMultiple = 8; + + /// Maps cache keys to their list of entries (one per layer). + /// Owned by the enclosing and shared across every DrawText + /// call on that canvas, so glyph outlines persist beyond a single text draw. + private readonly Dictionary> glyphCache; + + /// Read cursor into the cached layer list for layered cache hits. + private int cacheReadIndex; + + /// + /// when the current glyph is a cache miss and its outline + /// must be fully rasterized; on a cache hit (reuse path). + /// + private bool rasterizationRequired; + + /// + /// to disable the glyph cache entirely (e.g. path-based text + /// where every glyph has a unique transform). + /// + private readonly bool noCache; + + /// The cache key computed for the current glyph in . + private CacheKey currentCacheKey; + + /// + /// The transformed (post-) bounding-box location + /// of the current glyph. Stored so can compute + /// for future cache-hit render location estimation. + /// + private PointF currentTransformedBoundsLocation; + + /// + /// Initializes a new instance of the class. + /// + /// Drawing options (transform, graphics options) for the text. + /// Optional path to draw the text along. + /// Default pen for outlined text, or for fill-only. + /// Default brush for filled text, or for outline-only. + /// Caller-owned per-canvas glyph cache shared across renderer + /// instances so glyph outlines persist beyond a single text draw. + public RichTextGlyphRenderer( + DrawingOptions drawingOptions, + IPath? path, + Pen? pen, + Brush? brush, + Dictionary> glyphCache) + : base(drawingOptions.Transform) + { + this.drawingOptions = drawingOptions; + this.defaultPen = pen; + this.defaultBrush = brush; + this.glyphCache = glyphCache; + this.DrawingOperations = []; + this.currentCompositionMode = drawingOptions.GraphicsOptions.AlphaCompositionMode; + this.currentBlendingMode = drawingOptions.GraphicsOptions.ColorBlendingMode; + + if (path is not null) + { + // Path-based text gives each glyph a unique per-position transform, + // so cache hits are vanishingly rare; disable caching entirely. + this.rasterizationRequired = true; + this.noCache = true; + if (path is IPathInternals internals) + { + this.path = internals; + } + else + { + this.path = new ComplexPolygon(path); + } + } + } + + /// + /// Gets the list of instances accumulated during text rendering. + /// After RenderText completes, this list is consumed by + /// to build composition commands. + /// + public List DrawingOperations { get; } + + /// + protected override void BeginText(in FontRectangle bounds) => this.DrawingOperations.Clear(); + + /// + protected override bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) + { + // Resolves the active brush/pen from the text run, computes the cache key, + // and takes one of three paths: + // 1. Non-layered cache hit without decorations: emit cached ops, return false (fast path). + // 2. Layered or decorated cache hit: reuse cached path, return true for EndGlyph/SetDecoration. + // 3. Cache miss: rasterize from scratch. + this.cacheReadIndex = 0; + this.currentDecorationIsVertical = parameters.LayoutMode is GlyphLayoutMode.Vertical or GlyphLayoutMode.VerticalRotated; + this.currentTextRun = parameters.TextRun; + if (parameters.TextRun is RichTextRun drawingRun) + { + this.currentBrush = drawingRun.Brush; + this.currentPen = drawingRun.Pen; + } + else + { + this.currentBrush = null; + this.currentPen = null; + } + + if (!this.noCache) + { + // Transform the font-metric bounds by the drawing transform so that the + // sub-pixel position and size reflect the final screen coordinates. + // Quantize to 1/AccuracyMultiple px steps for cache key comparison. + RectangleF currentBounds = RectangleF.Transform( + new RectangleF(bounds.Location, new SizeF(bounds.Width, bounds.Height)), + this.drawingOptions.Transform); + + this.currentTransformedBoundsLocation = currentBounds.Location; + + PointF currentBoundsDelta = currentBounds.Location - ClampToPixel(currentBounds.Location); + PointF subPixelLocation = new( + MathF.Round(currentBoundsDelta.X * AccuracyMultiple) / AccuracyMultiple, + MathF.Round(currentBoundsDelta.Y * AccuracyMultiple) / AccuracyMultiple); + + SizeF subPixelSize = new( + MathF.Round(currentBounds.Width * AccuracyMultiple) / AccuracyMultiple, + MathF.Round(currentBounds.Height * AccuracyMultiple) / AccuracyMultiple); + + this.currentCacheKey = CacheKey.FromParameters( + parameters, + new RectangleF(subPixelLocation, subPixelSize), + this.currentPen ?? this.defaultPen); + + if (this.glyphCache.TryGetValue(this.currentCacheKey, out List? cachedEntries)) + { + if (cachedEntries.Count > 0 && !cachedEntries[0].IsLayered + && this.EnabledDecorations() == TextDecorations.None) + { + // Non-layered cache hit without decorations: emit operations directly + // and tell the font engine to skip the outline entirely + // (no MoveTo/LineTo/SetDecoration/EndGlyph). + this.EmitCachedGlyphOperations(cachedEntries[0], currentBounds.Location); + return false; + } + + // Layered or decorated cache hit: let the normal flow handle + // per-layer state and decoration callbacks. + this.rasterizationRequired = false; + return true; + } + } + + // Transform the glyph vectors using the original bounds + // The default transform will automatically be applied. + this.TransformGlyph(in bounds); + this.rasterizationRequired = true; + return true; + } + + /// + protected override void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) + { + // Capture the color-layer paint, fill rule, and composite mode. + // Setting hasLayer tells EndGlyph to skip its default single-layer path emission. + this.hasLayer = true; + this.currentFillRule = fillRule; + if (TryCreateBrush(paint, this.Builder.Transform, out Brush? brush)) + { + this.currentBrush = brush; + this.currentCompositionMode = TextUtilities.MapCompositionMode(paint.CompositeMode); + this.currentBlendingMode = TextUtilities.MapBlendingMode(paint.CompositeMode); + } + } + + /// + protected override void EndLayer() + { + // Finalizes a color layer. On a cache miss, translates the built path to local + // coordinates and stores it for future hits. On a cache hit, reads the stored + // path and adjusts the render location using sub-pixel delta compensation. + GlyphRenderData renderData = default; + IPath? fillPath = null; + + // Fix up the text runs colors. + // Only if both brush and pen is null do we fallback to the default value. + if (this.currentBrush == null && this.currentPen == null) + { + this.currentBrush = this.defaultBrush; + this.currentPen = this.defaultPen; + } + + // When rendering layers we only fill them. + // Any drawing of outlines is ignored as that doesn't really make sense. + bool renderFill = this.currentBrush != null; + + // Path has already been added to the collection via the base class. + IPath path = this.CurrentPaths[^1]; + Point renderLocation = ClampToPixel(path.Bounds.Location); + if (this.noCache || this.rasterizationRequired) + { + if (path.Bounds.Equals(RectangleF.Empty)) + { + return; + } + + if (renderFill) + { + renderData.FillPath = path.Translate(-renderLocation); + fillPath = renderData.FillPath; + } + + // Capture the delta between the location and the truncated render location. + // We can use this to offset the render location on the next instance of this glyph. + renderData.LocationDelta = (Vector2)(path.Bounds.Location - renderLocation); + renderData.IsLayered = true; + + if (!this.noCache) + { + this.UpdateCache(renderData); + } + } + else + { + renderData = this.glyphCache[this.currentCacheKey][this.cacheReadIndex++]; + + // Offset the render location by the delta from the cached glyph and this one. + Vector2 previousDelta = renderData.LocationDelta; + Vector2 currentLocation = path.Bounds.Location; + Vector2 currentDelta = path.Bounds.Location - ClampToPixel(path.Bounds.Location); + + if (previousDelta.Y > currentDelta.Y) + { + // Move the location down to match the previous location offset. + currentLocation += new Vector2(0, previousDelta.Y - currentDelta.Y); + } + else if (previousDelta.Y < currentDelta.Y) + { + // Move the location up to match the previous location offset. + currentLocation -= new Vector2(0, currentDelta.Y - previousDelta.Y); + } + else if (previousDelta.X > currentDelta.X) + { + // Move the location right to match the previous location offset. + currentLocation += new Vector2(previousDelta.X - currentDelta.X, 0); + } + else if (previousDelta.X < currentDelta.X) + { + // Move the location left to match the previous location offset. + currentLocation -= new Vector2(currentDelta.X - previousDelta.X, 0); + } + + renderLocation = ClampToPixel(currentLocation); + + if (renderFill && renderData.FillPath is not null) + { + fillPath = renderData.FillPath; + } + } + + if (fillPath is not null) + { + IntersectionRule fillRule = TextUtilities.MapFillRule(this.currentFillRule); + this.DrawingOperations.Add(new DrawingOperation + { + Kind = DrawingOperationKind.Fill, + Path = fillPath, + RenderLocation = renderLocation, + IntersectionRule = fillRule, + Brush = this.currentBrush, + RenderPass = RenderOrderFill, + PixelAlphaCompositionMode = this.currentCompositionMode, + PixelColorBlendingMode = this.currentBlendingMode + }); + } + + this.currentFillRule = FillRule.NonZero; + this.currentCompositionMode = this.drawingOptions.GraphicsOptions.AlphaCompositionMode; + this.currentBlendingMode = this.drawingOptions.GraphicsOptions.ColorBlendingMode; + } + + /// + public override TextDecorations EnabledDecorations() + { + // Returns the union of decorations from TextRun.TextDecorations and any + // decoration pens set on the current RichTextRun. The font engine uses + // this result to decide which SetDecoration calls to emit. + TextRun? run = this.currentTextRun; + TextDecorations decorations = run?.TextDecorations ?? TextDecorations.None; + + if (this.currentTextRun is RichTextRun drawingRun) + { + if (drawingRun.UnderlinePen != null) + { + decorations |= TextDecorations.Underline; + } + + if (drawingRun.StrikeoutPen != null) + { + decorations |= TextDecorations.Strikeout; + } + + if (drawingRun.OverlinePen != null) + { + decorations |= TextDecorations.Overline; + } + } + + return decorations; + } + + /// + public override void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) + { + // Emits a DrawingOperation for a text decoration. Resolves the decoration pen + // from the current RichTextRun, re-scales the base-class path when the pen's + // stroke width differs from the font-metric thickness, and anchors the scaling + // per decoration type (overline to bottom edge, underline to top edge, strikeout to center). + // Decorations are not cached. + if (thickness == 0) + { + return; + } + + Brush? brush = null; + Pen? pen = null; + if (this.currentTextRun is RichTextRun drawingRun) + { + brush = drawingRun.Brush; + + if (textDecorations == TextDecorations.Strikeout) + { + pen = drawingRun.StrikeoutPen ?? pen; + } + else if (textDecorations == TextDecorations.Underline) + { + pen = drawingRun.UnderlinePen ?? pen; + } + else if (textDecorations == TextDecorations.Overline) + { + pen = drawingRun.OverlinePen; + } + } + + // Always respect the pen stroke width if explicitly set. + float originalThickness = thickness; + if (pen is not null) + { + // Clamp the thickness to whole pixels. + thickness = MathF.Max(1F, (float)Math.Round(pen.StrokeWidth)); + } + else + { + // The thickness of the line has already been clamped in the base class. + pen = new SolidPen((brush ?? this.defaultBrush)!, thickness); + } + + // Path has already been added to the collection via the base class. + IPath path = this.CurrentPaths[^1]; + IPath outline = path; + + if (originalThickness != thickness) + { + // Respect edge anchoring per decoration type: + // - Overline: keep the base edge fixed (bottom in horizontal; left in vertical) + // - Underline: keep the top edge fixed (top in horizontal; right in vertical) + // - Strikeout: keep the center fixed (default behavior) + float ratio = thickness / originalThickness; + if (ratio != 1f) + { + Vector2 scale = this.currentDecorationIsVertical + ? new Vector2(ratio, 1f) + : new Vector2(1f, ratio); + + RectangleF b = path.Bounds; + Vector2 center = new(b.Left + (b.Width * 0.5f), b.Top + (b.Height * 0.5f)); + Vector2 anchor = center; + + if (textDecorations == TextDecorations.Overline) + { + anchor = this.currentDecorationIsVertical + ? new Vector2(b.Left, center.Y) // vertical: anchor left edge + : new Vector2(center.X, b.Bottom); // horizontal: anchor bottom edge + } + else if (textDecorations == TextDecorations.Underline) + { + anchor = this.currentDecorationIsVertical + ? new Vector2(b.Right, center.Y) // vertical: anchor right edge + : new Vector2(center.X, b.Top); // horizontal: anchor top edge + } + + // Scale about the chosen anchor so the fixed edge stays in place. + outline = outline.Transform(Matrix4x4.CreateScale(scale.X, scale.Y, 1, new Vector3(anchor, 0))); + } + } + + // Render the path here. Decorations are un-cached. + Point renderLocation = ClampToPixel(outline.Bounds.Location); + IPath decorationPath = outline.Translate(-renderLocation); + Brush decorationBrush = pen.StrokeFill; + this.DrawingOperations.Add(new DrawingOperation + { + Kind = DrawingOperationKind.Fill, + Path = decorationPath, + RenderLocation = renderLocation, + IntersectionRule = IntersectionRule.NonZero, + Brush = decorationBrush, + RenderPass = RenderOrderDecoration + }); + } + + /// + protected override void EndGlyph() + { + // If hasLayer is set, layers were already handled by EndLayer; skip. + // Otherwise, on a cache miss the built path is translated to local coordinates, + // stored for future hits, and emitted as fill and/or outline DrawingOperations. + // On a cache hit the stored path is reused with sub-pixel delta compensation. + if (this.hasLayer) + { + // The layer has already been rendered. + this.hasLayer = false; + return; + } + + GlyphRenderData renderData = default; + IPath? glyphPath = null; + + // Fix up the text runs colors. + // Only if both brush and pen is null do we fallback to the default value. + if (this.currentBrush == null && this.currentPen == null) + { + this.currentBrush = this.defaultBrush; + this.currentPen = this.defaultPen; + } + + bool renderFill = false; + bool renderOutline = false; + + // If we are using the fonts color layers we ignore the request to draw an outline only + // because that won't really work. Instead we force drawing using fill with the requested color. + if (this.currentBrush != null) + { + renderFill = true; + } + + if (this.currentPen != null) + { + renderOutline = true; + } + + // Path has already been added to the collection via the base class. + IPath path = this.CurrentPaths[^1]; + Point renderLocation = ClampToPixel(path.Bounds.Location); + if (this.noCache || this.rasterizationRequired) + { + if (path.Bounds.Equals(RectangleF.Empty)) + { + return; + } + + IPath localPath = path.Translate(-renderLocation); + if (renderFill || renderOutline) + { + renderData.FillPath = localPath; + glyphPath = renderData.FillPath; + } + + // Capture the delta between the location and the truncated render location. + // We can use this to offset the render location on the next instance of this glyph. + renderData.LocationDelta = (Vector2)(path.Bounds.Location - renderLocation); + + // Store the offset between outline bounds and font metric bounds so that + // cache hits in BeginGlyph can accurately estimate the path location. + renderData.BoundsOffset = (Vector2)(path.Bounds.Location - this.currentTransformedBoundsLocation); + + if (!this.noCache) + { + this.UpdateCache(renderData); + } + } + else + { + renderData = this.glyphCache[this.currentCacheKey][this.cacheReadIndex++]; + + // Offset the render location by the delta from the cached glyph and this one. + Vector2 previousDelta = renderData.LocationDelta; + Vector2 currentLocation = path.Bounds.Location; + Vector2 currentDelta = path.Bounds.Location - ClampToPixel(path.Bounds.Location); + + if (previousDelta.Y > currentDelta.Y) + { + // Move the location down to match the previous location offset. + currentLocation += new Vector2(0, previousDelta.Y - currentDelta.Y); + } + else if (previousDelta.Y < currentDelta.Y) + { + // Move the location up to match the previous location offset. + currentLocation -= new Vector2(0, currentDelta.Y - previousDelta.Y); + } + else if (previousDelta.X > currentDelta.X) + { + // Move the location right to match the previous location offset. + currentLocation += new Vector2(previousDelta.X - currentDelta.X, 0); + } + else if (previousDelta.X < currentDelta.X) + { + // Move the location left to match the previous location offset. + currentLocation -= new Vector2(currentDelta.X - previousDelta.X, 0); + } + + renderLocation = ClampToPixel(currentLocation); + + if (renderFill && renderData.FillPath is not null) + { + glyphPath = renderData.FillPath; + } + + if (renderOutline && renderData.FillPath is not null) + { + glyphPath = renderData.FillPath; + } + } + + if (renderFill && glyphPath is not null) + { + IntersectionRule fillRule = TextUtilities.MapFillRule(this.currentFillRule); + this.DrawingOperations.Add(new DrawingOperation + { + Kind = DrawingOperationKind.Fill, + Path = glyphPath, + RenderLocation = renderLocation, + IntersectionRule = fillRule, + Brush = this.currentBrush, + RenderPass = RenderOrderFill, + PixelAlphaCompositionMode = this.currentCompositionMode, + PixelColorBlendingMode = this.currentBlendingMode + }); + } + + if (renderOutline && glyphPath is not null) + { + IntersectionRule outlineRule = TextUtilities.MapFillRule(this.currentFillRule); + this.DrawingOperations.Add(new DrawingOperation + { + Kind = DrawingOperationKind.Draw, + Path = glyphPath, + RenderLocation = renderLocation, + IntersectionRule = outlineRule, + Pen = this.currentPen, + RenderPass = RenderOrderOutline, + PixelAlphaCompositionMode = this.currentCompositionMode, + PixelColorBlendingMode = this.currentBlendingMode + }); + } + } + + /// + /// Emits fill and/or outline s from a cached + /// entry. Called from on a + /// non-layered, decoration-free cache hit when the font engine is told to skip + /// the outline entirely (returns ). + /// + /// The cached render data containing the translated path and location delta. + /// The transformed bounding-box origin for the current glyph instance. + private void EmitCachedGlyphOperations(GlyphRenderData renderData, PointF currentBoundsLocation) + { + // Estimate the outline bounds location using the stored offset between + // the outline bounds and the font metric bounds from the original glyph. + PointF estimatedPathLocation = new( + currentBoundsLocation.X + renderData.BoundsOffset.X, + currentBoundsLocation.Y + renderData.BoundsOffset.Y); + Point renderLocation = ComputeCacheHitRenderLocation(estimatedPathLocation, renderData.LocationDelta); + + // Fix up the text runs colors. + Brush? brush = this.currentBrush; + Pen? pen = this.currentPen; + if (brush == null && pen == null) + { + brush = this.defaultBrush; + pen = this.defaultPen; + } + + IPath? glyphPath = renderData.FillPath; + if (glyphPath is null) + { + return; + } + + if (brush != null) + { + IntersectionRule fillRule = TextUtilities.MapFillRule(this.currentFillRule); + this.DrawingOperations.Add(new DrawingOperation + { + Kind = DrawingOperationKind.Fill, + Path = glyphPath, + RenderLocation = renderLocation, + IntersectionRule = fillRule, + Brush = brush, + RenderPass = RenderOrderFill, + PixelAlphaCompositionMode = this.currentCompositionMode, + PixelColorBlendingMode = this.currentBlendingMode + }); + } + + if (pen != null) + { + IntersectionRule outlineRule = TextUtilities.MapFillRule(this.currentFillRule); + this.DrawingOperations.Add(new DrawingOperation + { + Kind = DrawingOperationKind.Draw, + Path = glyphPath, + RenderLocation = renderLocation, + IntersectionRule = outlineRule, + Pen = pen, + RenderPass = RenderOrderOutline, + PixelAlphaCompositionMode = this.currentCompositionMode, + PixelColorBlendingMode = this.currentBlendingMode + }); + } + } + + /// + /// Computes the pixel-snapped render location for a cache-hit glyph by compensating + /// for the sub-pixel delta difference between the original cached glyph and the + /// current instance. This keeps glyphs visually aligned even when their sub-pixel + /// positions differ slightly. + /// + /// The estimated outline bounds origin for the current glyph. + /// The sub-pixel delta recorded when the path was first cached. + /// A pixel-snapped render location. + private static Point ComputeCacheHitRenderLocation(PointF pathLocation, Vector2 previousDelta) + { + Vector2 currentLocation = (Vector2)pathLocation; + Vector2 currentDelta = currentLocation - (Vector2)ClampToPixel(pathLocation); + + if (previousDelta.Y > currentDelta.Y) + { + currentLocation += new Vector2(0, previousDelta.Y - currentDelta.Y); + } + else if (previousDelta.Y < currentDelta.Y) + { + currentLocation -= new Vector2(0, currentDelta.Y - previousDelta.Y); + } + else if (previousDelta.X > currentDelta.X) + { + currentLocation += new Vector2(previousDelta.X - currentDelta.X, 0); + } + else if (previousDelta.X < currentDelta.X) + { + currentLocation -= new Vector2(currentDelta.X - previousDelta.X, 0); + } + + return ClampToPixel(currentLocation); + } + + /// + /// Stores a entry in the glyph cache under the + /// current key. Creates the cache list on first insertion for a given key. + /// + private void UpdateCache(GlyphRenderData renderData) + { + if (!this.glyphCache.TryGetValue(this.currentCacheKey, out List? _)) + { + this.glyphCache[this.currentCacheKey] = []; + } + + this.glyphCache[this.currentCacheKey].Add(renderData); + } + + /// + public void Dispose() => this.Dispose(true); + + /// + /// Truncates a floating-point position to the nearest whole pixel toward negative infinity. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Point ClampToPixel(PointF point) => Point.Truncate(point); + + /// + /// Applies the path-based transform to the + /// for the current glyph, positioning it along the text path (if any) or + /// leaving the identity transform for linear text. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void TransformGlyph(in FontRectangle bounds) + => this.Builder.SetTransform(this.ComputeTransform(in bounds)); + + /// + /// Computes the combined translation + rotation matrix that places a glyph + /// along the text path. For linear text (no path), returns . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Matrix4x4 ComputeTransform(in FontRectangle bounds) + { + if (this.path is null) + { + return Matrix4x4.Identity; + } + + // Find the point of this intersection along the given path. + // We want to find the point on the path that is closest to the center-bottom side of the glyph. + Vector2 half = new(bounds.Width * .5F, 0); + SegmentInfo pathPoint = this.path.PointAlongPath(bounds.Left + half.X); + + // Now offset to our target point since we're aligning the top-left location of our glyph against the path. + Vector2 translation = (Vector2)pathPoint.Point - bounds.Location - half + new Vector2(0, bounds.Top); + return Matrix4x4.CreateTranslation(translation.X, translation.Y, 0) + * new Matrix4x4(Matrix3x2.CreateRotation(pathPoint.Angle - MathF.PI, (Vector2)pathPoint.Point)); + } + + /// + /// Releases managed resources (glyph cache and drawing operations list). + /// + /// to release managed resources. + private void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + // The glyph cache is owned by the canvas and outlives this renderer. + this.DrawingOperations.Clear(); + } + + this.isDisposed = true; + } + } + + /// + /// Per-layer cached data for a rasterized glyph. Stores the locally-translated + /// path and the sub-pixel deltas needed to reposition the path at a different + /// screen location on a cache hit. + /// + internal struct GlyphRenderData + { + /// + /// The fractional-pixel offset between the path's bounding-box origin + /// and the truncated (pixel-snapped) render location. Used to compensate + /// for sub-pixel position differences between cache hits. + /// + public Vector2 LocationDelta; + + /// + /// The offset between the outline path's bounding-box origin and the + /// font-metric bounds origin. Stored on first rasterization so that + /// can estimate the path location + /// from only the font-metric bounds (which are available without outline data). + /// + public Vector2 BoundsOffset; + + /// + /// The glyph outline path translated to local coordinates (origin at 0,0). + /// Shared across all cache hits for the same . + /// + public IPath? FillPath; + + /// + /// if this entry belongs to a multi-layer (COLR) glyph. + /// Non-layered cache hits with no decorations can skip the outline entirely + /// (return from ); layered hits + /// still need the per-layer BeginLayer/EndLayer callbacks. + /// + public bool IsLayered; + } + + /// + /// Identifies a unique glyph variant for caching purposes. Two glyphs with the same + /// share identical outline geometry and can reuse the same + /// . The key includes the glyph id, font metrics, + /// sub-pixel position (quantized to ), and the pen reference + /// (since stroke width affects the outline path). + /// + internal readonly struct CacheKey : IEquatable + { + /// Gets the font family name. + public string Font { get; init; } + + /// Gets the glyph color variant (normal, COLR, etc.). + public GlyphColor GlyphColor { get; init; } + + /// Gets the glyph type (simple, composite, etc.). + public GlyphType GlyphType { get; init; } + + /// Gets the font style (regular, bold, italic, etc.). + public FontStyle FontStyle { get; init; } + + /// Gets the glyph index within the font. + public ushort GlyphId { get; init; } + + /// Gets the composite glyph parent index (0 for non-composite). + public ushort CompositeGlyphId { get; init; } + + /// Gets the Unicode code point this glyph represents. + public CodePoint CodePoint { get; init; } + + /// Gets the em-size at which the glyph is rendered. + public float PointSize { get; init; } + + /// Gets the DPI used for rendering. + public float Dpi { get; init; } + + /// Gets the layout mode (horizontal, vertical, vertical-rotated). + public GlyphLayoutMode LayoutMode { get; init; } + + /// Gets any text attributes (e.g. superscript/subscript) that affect rendering. + public TextAttributes TextAttributes { get; init; } + + /// Gets text decorations that may influence outline geometry. + public TextDecorations TextDecorations { get; init; } + + /// Gets the quantized sub-pixel bounds used for position-sensitive cache lookup. + public RectangleF Bounds { get; init; } + + /// + /// Gets the pen reference used for outlined text. Compared by reference equality + /// so that different pen instances (even with the same stroke width) produce + /// separate cache entries; this is correct because pen identity affects stroke + /// pattern and dash style. + /// + public Pen? PenReference { get; init; } + + public static bool operator ==(CacheKey left, CacheKey right) => left.Equals(right); + + public static bool operator !=(CacheKey left, CacheKey right) => !(left == right); + + /// + /// Creates a from glyph renderer parameters and quantized bounds. + /// The grapheme index is intentionally excluded because it varies per glyph instance + /// while the outline geometry remains the same for matching glyph+position. + /// + /// The glyph renderer parameters from the font engine. + /// Quantized sub-pixel bounds for position-sensitive lookup. + /// The pen reference for outlined text, or . + /// A new cache key. + public static CacheKey FromParameters( + in GlyphRendererParameters parameters, + RectangleF bounds, + Pen? penReference) + => new() + { + // Do not include the grapheme index as that will + // always vary per glyph instance. + Font = parameters.Font, + GlyphType = parameters.GlyphType, + FontStyle = parameters.FontStyle, + GlyphId = parameters.GlyphId, + CompositeGlyphId = parameters.CompositeGlyphId, + CodePoint = parameters.CodePoint, + PointSize = parameters.PointSize, + Dpi = parameters.Dpi, + LayoutMode = parameters.LayoutMode, + TextAttributes = parameters.TextRun.TextAttributes, + TextDecorations = parameters.TextRun.TextDecorations, + Bounds = bounds, + PenReference = penReference + }; + + public override bool Equals(object? obj) + => obj is CacheKey key && this.Equals(key); + + public bool Equals(CacheKey other) + => this.Font == other.Font && + this.GlyphColor.Equals(other.GlyphColor) && + this.GlyphType == other.GlyphType && + this.FontStyle == other.FontStyle && + this.GlyphId == other.GlyphId && + this.CompositeGlyphId == other.CompositeGlyphId && + this.CodePoint.Equals(other.CodePoint) && + this.PointSize == other.PointSize && + this.Dpi == other.Dpi && + this.LayoutMode == other.LayoutMode && + this.TextAttributes == other.TextAttributes && + this.TextDecorations == other.TextDecorations && + this.Bounds.Equals(other.Bounds) && + ReferenceEquals(this.PenReference, other.PenReference); + + public override int GetHashCode() + { + HashCode hash = default; + hash.Add(this.Font); + hash.Add(this.GlyphColor); + hash.Add(this.GlyphType); + hash.Add(this.FontStyle); + hash.Add(this.GlyphId); + hash.Add(this.CompositeGlyphId); + hash.Add(this.CodePoint); + hash.Add(this.PointSize); + hash.Add(this.Dpi); + hash.Add(this.LayoutMode); + hash.Add(this.TextAttributes); + hash.Add(this.TextDecorations); + hash.Add(this.Bounds); + hash.Add(this.PenReference is null ? 0 : RuntimeHelpers.GetHashCode(this.PenReference)); + return hash.ToHashCode(); + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/RichTextOptions.cs b/ImageSharp.Drawing/Processing/RichTextOptions.cs new file mode 100644 index 0000000..a712c12 --- /dev/null +++ b/ImageSharp.Drawing/Processing/RichTextOptions.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Provides configuration options for rendering and shaping of rich text. + /// + public class RichTextOptions : TextOptions + { + /// + /// Initializes a new instance of the class. + /// + /// The font. + public RichTextOptions(Font font) + : base(font) + => this.TextRuns = []; + + /// + /// Initializes a new instance of the class from properties + /// copied from the given instance. + /// + /// The options whose properties are copied into this instance. + public RichTextOptions(RichTextOptions options) + : base(options) + { + List runs = new(options.TextRuns.Count); + foreach (RichTextRun run in options.TextRuns) + { + runs.Add(new RichTextRun() + { + Brush = run.Brush, + Pen = run.Pen, + StrikeoutPen = run.StrikeoutPen, + UnderlinePen = run.UnderlinePen, + OverlinePen = run.OverlinePen, + Start = run.Start, + End = run.End, + Font = run.Font, + TextAttributes = run.TextAttributes, + TextDecorations = run.TextDecorations, + Placeholder = run.Placeholder + }); + } + + this.TextRuns = runs; + } + + /// + /// Gets or sets an optional collection of text runs to apply to the body of text. + /// + public new IReadOnlyList TextRuns + { + get => (IReadOnlyList)base.TextRuns; + set => base.TextRuns = value; + } + } +} diff --git a/ImageSharp.Drawing/Processing/RichTextRun.cs b/ImageSharp.Drawing/Processing/RichTextRun.cs new file mode 100644 index 0000000..66e0b77 --- /dev/null +++ b/ImageSharp.Drawing/Processing/RichTextRun.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Represents a run of drawable text spanning a series of graphemes within a string. + /// + public class RichTextRun : TextRun + { + /// + /// Gets or sets the brush used for filling this run. + /// + public Brush? Brush { get; set; } + + /// + /// Gets or sets the pen used for outlining this run. + /// + public Pen? Pen { get; set; } + + /// + /// Gets or sets the pen used for drawing strikeout features for this run. + /// + public Pen? StrikeoutPen { get; set; } + + /// + /// Gets or sets the pen used for drawing underline features for this run. + /// + public Pen? UnderlinePen { get; set; } + + /// + /// Gets or sets the pen used for drawing overline features for this run. + /// + public Pen? OverlinePen { get; set; } + } +} diff --git a/ImageSharp.Drawing/Processing/ShapeOptions.cs b/ImageSharp.Drawing/Processing/ShapeOptions.cs new file mode 100644 index 0000000..43fed3b --- /dev/null +++ b/ImageSharp.Drawing/Processing/ShapeOptions.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Provides options for controlling how vector shapes are interpreted during rasterization, + /// including the fill-rule intersection mode and boolean clipping operations. + /// + public class ShapeOptions : IDeepCloneable + { + /// + /// Initializes a new instance of the class. + /// + public ShapeOptions() + { + } + + private ShapeOptions(ShapeOptions source) + { + this.IntersectionRule = source.IntersectionRule; + this.BooleanOperation = source.BooleanOperation; + } + + /// + /// Gets or sets the boolean clipping operation used when a clipping path is applied. + /// Determines how the clip shape interacts with the target region + /// (e.g. subtracts the clip shape). + /// + /// Defaults to . + /// + public BooleanOperation BooleanOperation { get; set; } = BooleanOperation.Difference; + + /// + /// Gets or sets the fill rule that determines how overlapping or nested contours affect coverage. + /// fills any region with a non-zero winding number; + /// alternates fill/hole for each contour crossing. + /// + /// Defaults to . + /// + public IntersectionRule IntersectionRule { get; set; } = IntersectionRule.NonZero; + + /// + public ShapeOptions DeepClone() => new(this); + } +} diff --git a/ImageSharp.Drawing/Processing/SolidBrush.cs b/ImageSharp.Drawing/Processing/SolidBrush.cs new file mode 100644 index 0000000..479ea62 --- /dev/null +++ b/ImageSharp.Drawing/Processing/SolidBrush.cs @@ -0,0 +1,119 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Provides an implementation of a solid brush for painting solid color areas. + /// + public sealed class SolidBrush : Brush + { + /// + /// Initializes a new instance of the class. + /// + /// The color. + public SolidBrush(Color color) => this.Color = color; + + /// + /// Gets the color. + /// + public Color Color { get; } + + /// + public override BrushRenderer CreateRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + RectangleF region) + => new SolidBrushRenderer(configuration, options, canvasWidth, this.Color.ToPixel()); + + /// + public override bool Equals(Brush? other) + { + if (other is SolidBrush sb) + { + return sb.Color.Equals(this.Color); + } + + return false; + } + + /// + public override int GetHashCode() => this.Color.GetHashCode(); + + /// + /// The solid brush applicator. + /// + /// The pixel format. + private sealed class SolidBrushRenderer : BrushRenderer + where TPixel : unmanaged, IPixel + { + private readonly TPixel color; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration instance to use when performing operations. + /// The graphics options. + /// The canvas width for the current render pass. + /// The color. + public SolidBrushRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + TPixel color) + : base(configuration, options, canvasWidth) + => this.color = color; + + /// + public override void Apply( + Span destinationRow, + ReadOnlySpan scanline, + int x, + int y, + BrushWorkspace workspace) + { + // Constrain the spans to each other + if (destinationRow.Length > scanline.Length) + { + destinationRow = destinationRow[..scanline.Length]; + } + else + { + scanline = scanline[..destinationRow.Length]; + } + + Configuration configuration = this.Configuration; + if (this.Options.BlendPercentage == 1F) + { + this.Blender.Blend( + configuration, + destinationRow, + destinationRow, + this.color, + scanline, + workspace.GetBlendScratch(scanline.Length, 2)); + } + else + { + Span amounts = workspace.GetAmounts(scanline.Length); + + for (int i = 0; i < scanline.Length; i++) + { + amounts[i] = scanline[i] * this.Options.BlendPercentage; + } + + this.Blender.Blend( + configuration, + destinationRow, + destinationRow, + this.color, + amounts, + workspace.GetBlendScratch(scanline.Length, 2)); + } + } + } + } +} diff --git a/ImageSharp.Drawing/Processing/SolidPen.cs b/ImageSharp.Drawing/Processing/SolidPen.cs new file mode 100644 index 0000000..0ba794e --- /dev/null +++ b/ImageSharp.Drawing/Processing/SolidPen.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Defines a pen that can apply a pattern to a line with a set brush and thickness. + /// + public class SolidPen : Pen + { + /// + /// Initializes a new instance of the class. + /// + /// The color. + public SolidPen(Color color) + : base(new SolidBrush(color)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The color. + /// The width. + public SolidPen(Color color, float width) + : base(new SolidBrush(color), width) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The brush used to fill the stroke outline. + public SolidPen(Brush strokeFill) + : base(strokeFill) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The brush used to fill the stroke outline. + /// The stroke width in the path's local coordinate space before any drawing transform is applied. + public SolidPen(Brush strokeFill, float strokeWidth) + : base(strokeFill, strokeWidth) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The pen options. + public SolidPen(PenOptions options) + : base(options) + { + } + + /// + public override bool Equals(Pen? other) + { + if (other is SolidPen) + { + return base.Equals(other); + } + + return false; + } + + /// + public override IPath GeneratePath(IPath path, float strokeWidth) + => path.GenerateOutline(strokeWidth, this.StrokeOptions); + } +} diff --git a/ImageSharp.Drawing/Processing/StrokeOptions.cs b/ImageSharp.Drawing/Processing/StrokeOptions.cs new file mode 100644 index 0000000..9cf76f6 --- /dev/null +++ b/ImageSharp.Drawing/Processing/StrokeOptions.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + public sealed class StrokeOptions : IEquatable + { + /// + public double MiterLimit { get; set; } = 4D; + + /// + public double ArcDetailScale { get; set; } = 1D; + + /// + public LineJoin LineJoin { get; set; } = LineJoin.Bevel; + + /// + public LineCap LineCap { get; set; } = LineCap.Butt; + + /// + public override bool Equals(object? obj) => this.Equals(obj as StrokeOptions); + + /// + public bool Equals(StrokeOptions? other) + => other is not null && + this.MiterLimit == other.MiterLimit && + this.ArcDetailScale == other.ArcDetailScale && + this.LineJoin == other.LineJoin && + this.LineCap == other.LineCap; + + /// + public override int GetHashCode() + => HashCode.Combine( + this.MiterLimit, + this.ArcDetailScale, + this.LineJoin, + this.LineCap); + } +} diff --git a/ImageSharp.Drawing/Processing/SweepGradientBrush.cs b/ImageSharp.Drawing/Processing/SweepGradientBrush.cs new file mode 100644 index 0000000..22fadb2 --- /dev/null +++ b/ImageSharp.Drawing/Processing/SweepGradientBrush.cs @@ -0,0 +1,305 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing { + /// + /// Provides an implementation of a brush for painting sweep (conic) gradients within areas. + /// Angles increase counter-clockwise from +X on the design grid. + /// + public sealed class SweepGradientBrush : GradientBrush + { + /// + /// Initializes a new instance of the class. + /// + /// The center point of the sweep gradient in device space. + /// + /// The starting angle, in degrees, measured counter-clockwise from +X on the design grid. + /// This value is stored as provided so the sign and magnitude of the sweep remain intact. + /// + /// + /// The ending angle, in degrees, measured counter-clockwise from +X on the design grid. + /// If equal to , the gradient is treated as a full 360 degree sweep. + /// Otherwise, the signed difference between start and end determines the sweep direction. + /// + /// Defines how the gradient colors are repeated beyond the interval [0..1]. + /// The gradient color stops. Ratios must be in [0..1] and are interpreted along the angular sweep. + public SweepGradientBrush( + PointF center, + float startAngleDegrees, + float endAngleDegrees, + GradientRepetitionMode repetitionMode, + params ColorStop[] colorStops) + : base(repetitionMode, colorStops) + { + this.Center = center; + this.StartAngleDegrees = startAngleDegrees; + this.EndAngleDegrees = endAngleDegrees; + } + + /// + /// Gets the center point of the sweep gradient. + /// + public PointF Center { get; } + + /// + /// Gets the starting angle in degrees. + /// + public float StartAngleDegrees { get; } + + /// + /// Gets the ending angle in degrees. + /// + public float EndAngleDegrees { get; } + + /// + public override Brush Transform(Matrix4x4 matrix) + { + PointF tc = PointF.Transform(this.Center, matrix); + + // Treat the brush as two rays starting at the center: + // one ray for the start angle and one ray for the end angle. + // The important value is the signed angular distance between those rays. + // We keep that sign so a reflected transform can turn a counter-clockwise + // sweep into a clockwise sweep instead of silently "fixing" it. + float sweepDegrees = GetEffectiveSweepDegrees(this.StartAngleDegrees, this.EndAngleDegrees); + float startRad = GeometryUtilities.DegreeToRadian(this.StartAngleDegrees); + float endRad = GeometryUtilities.DegreeToRadian(this.StartAngleDegrees + sweepDegrees); + + // The public API uses the design-grid convention, which is y-up. + // Screen pixels are y-down, so a positive mathematical rotation uses + // `center.Y - sin(theta)` rather than `center.Y + sin(theta)`. + PointF startDir = PointF.Transform(new PointF(this.Center.X + MathF.Cos(startRad), this.Center.Y - MathF.Sin(startRad)), matrix); + PointF endDir = PointF.Transform(new PointF(this.Center.X + MathF.Cos(endRad), this.Center.Y - MathF.Sin(endRad)), matrix); + + // Convert the transformed rays back into brush angles in the same public convention: + // counter-clockwise from +X on the design grid. + float newStart = NormalizeDirectionDegrees(MathF.Atan2(-(startDir.Y - tc.Y), startDir.X - tc.X) * (180f / MathF.PI)); + float newEnd = NormalizeDirectionDegrees(MathF.Atan2(-(endDir.Y - tc.Y), endDir.X - tc.X) * (180f / MathF.PI)); + + // A negative determinant means the transform flips orientation. + // That flips the direction of the sweep, so we use it to decide whether + // the end angle should unwrap forwards or backwards from the new start. + float determinant = (matrix.M11 * matrix.M22) - (matrix.M12 * matrix.M21); + float directionHint = MathF.Sign(sweepDegrees); + if (directionHint == 0F) + { + directionHint = 1F; + } + + if (determinant < 0F) + { + directionHint = -directionHint; + } + + return new SweepGradientBrush( + tc, + newStart, + UnwrapSweepEndDegrees(newStart, newEnd, directionHint, MathF.Abs(sweepDegrees)), + this.RepetitionMode, + this.ColorStopsArray); + } + + /// + public override bool Equals(Brush? other) + { + // Sweep brushes are equal only when they describe the same center, + // the same signed angular interval, and the same inherited stop data. + if (other is SweepGradientBrush brush) + { + return base.Equals(other) + && this.Center.Equals(brush.Center) + && this.StartAngleDegrees.Equals(brush.StartAngleDegrees) + && this.EndAngleDegrees.Equals(brush.EndAngleDegrees); + } + + return false; + } + + /// + public override int GetHashCode() + => HashCode.Combine( + base.GetHashCode(), + this.Center, + this.StartAngleDegrees, + this.EndAngleDegrees); + + /// + /// Converts the stored start/end angles into the signed sweep interval that the brush should render. + /// + /// The starting angle in degrees. + /// The ending angle in degrees. + /// + /// The signed angular interval in degrees. Equal endpoints are treated as a full turn. + /// + // Sweep gradients interpret equal endpoints as "full turn". + // All other cases keep the caller-provided signed angular span. + private static float GetEffectiveSweepDegrees(float startAngleDegrees, float endAngleDegrees) + { + float sweepDegrees = endAngleDegrees - startAngleDegrees; + if (MathF.Abs(sweepDegrees) < 1e-6F) + { + // Equal endpoints mean "full circle", not an empty span. + return 360F; + } + + return sweepDegrees; + } + + /// + /// Normalizes an angle to the canonical [0, 360) direction range. + /// + /// The angle to normalize. + /// The equivalent direction in the canonical degree range. + // Convert any equivalent direction into the canonical [0, 360) representation + // so transformed brushes remain stable when compared or reused. + private static float NormalizeDirectionDegrees(float degrees) + { + float normalized = degrees % 360F; + if (normalized < 0F) + { + normalized += 360F; + } + + return normalized; + } + + /// + /// Reconstructs the signed end angle after independently transforming the start and end rays. + /// + /// The transformed starting angle in normalized degrees. + /// The transformed ending angle in normalized degrees. + /// + /// The expected sweep direction. Positive means unwrap forwards, negative means unwrap backwards. + /// + /// The minimum magnitude the restored interval must preserve. + /// The unwrapped ending angle measured relative to . + // After transforming the start and end rays separately, both directions land in [0, 360). + // This method restores the intended signed sweep by unwrapping the end angle relative to + // the start angle, using the desired direction as the constraint. + private static float UnwrapSweepEndDegrees(float startDegrees, float endDegrees, float directionHint, float minimumMagnitude) + { + float delta = endDegrees - startDegrees; + if (directionHint >= 0F) + { + // Keep the end angle ahead of the start angle for a positive sweep. + while (delta < 0F) + { + delta += 360F; + } + + if (MathF.Abs(delta) < 1e-6F && minimumMagnitude >= 360F - 1e-6F) + { + delta = 360F; + } + } + else + { + // Keep the end angle behind the start angle for a negative sweep. + while (delta > 0F) + { + delta -= 360F; + } + + if (MathF.Abs(delta) < 1e-6F && minimumMagnitude >= 360F - 1e-6F) + { + delta = -360F; + } + } + + return startDegrees + delta; + } + + /// + public override BrushRenderer CreateRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + RectangleF region) => + + // The renderer precomputes the angular interval once and then samples it per pixel. + new SweepGradientBrushRenderer( + configuration, + options, + canvasWidth, + this, + this.ColorStopsArray, + this.RepetitionMode); + + /// + /// The sweep (conic) gradient brush applicator. + /// + /// The pixel format. + private sealed class SweepGradientBrushRenderer : GradientBrushRenderer + where TPixel : unmanaged, IPixel + { + private const float Tau = MathF.Tau; + + private readonly float cx; + + private readonly float cy; + + private readonly float startRad; + + private readonly float endRad; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration instance to use when performing operations. + /// The graphics options. + /// The canvas width for the current render pass. + /// The sweep gradient brush. + /// The gradient color stops (ratios in [0..1]). + /// Defines how gradient colors are repeated outside [0..1]. + public SweepGradientBrushRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth, + SweepGradientBrush brush, + ColorStop[] colorStops, + GradientRepetitionMode repetitionMode) + : base(configuration, options, canvasWidth, colorStops, repetitionMode) + { + this.cx = brush.Center.X; + this.cy = brush.Center.Y; + + // Store the interval as radians once so sampling only needs one subtraction and one divide. + float sweepDegrees = GetEffectiveSweepDegrees(brush.StartAngleDegrees, brush.EndAngleDegrees); + this.startRad = GeometryUtilities.DegreeToRadian(brush.StartAngleDegrees); + this.endRad = GeometryUtilities.DegreeToRadian(brush.StartAngleDegrees + sweepDegrees); + } + + /// + protected override float PositionOnGradient(float x, float y) + { + // Move the sample into center-relative coordinates. + float dx = x - this.cx; + float dy = y - this.cy; + + if (dx == 0f && dy == 0f) + { + // The center has no unique angle, so pick a stable value on the gradient. + return 0f; + } + + // Convert from y-down image space back into the brush's y-up angle convention, + // then normalize to [0, 2π) so subtraction against the stored start angle is stable. + float angle = MathF.Atan2(-dy, dx); + if (angle < 0f) + { + angle += Tau; + } + + // Divide by the signed angular span. + // A positive denominator produces a counter-clockwise sweep and a negative + // denominator produces a clockwise sweep. The base gradient code then applies + // the repetition mode to this unbounded parameter. + return (angle - this.startRad) / (this.endRad - this.startRad); + } + } + } +} diff --git a/ImageSharp.Drawing/RectanglePolygon.cs b/ImageSharp.Drawing/RectanglePolygon.cs new file mode 100644 index 0000000..cab09a7 --- /dev/null +++ b/ImageSharp.Drawing/RectanglePolygon.cs @@ -0,0 +1,278 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// A closed rectangular path defined by four straight edges. + /// + public sealed class RectanglePolygon : IPath, ISimplePath, IPathInternals + { + private readonly Vector2 topLeft; + private readonly Vector2 bottomRight; + private readonly PointF[] points; + private readonly float halfLength; + private readonly float length; + private LinearGeometryCache geometryCache; + + /// + /// Initializes a new instance of the class. + /// + /// The horizontal position of the rectangle. + /// The vertical position of the rectangle. + /// The width of the rectangle. + /// The height of the rectangle. + public RectanglePolygon(float x, float y, float width, float height) + : this(new PointF(x, y), new SizeF(width, height)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The which specifies the rectangle's top-left point in a two-dimensional plane. + /// + /// + /// The which specifies the rectangle's bottom-right point in a two-dimensional plane. + /// + public RectanglePolygon(PointF topLeft, PointF bottomRight) + { + this.Location = topLeft; + this.topLeft = topLeft; + this.bottomRight = bottomRight; + this.Size = new SizeF(bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y); + + this.points = + [ + this.topLeft, + new Vector2(this.bottomRight.X, this.topLeft.Y), + this.bottomRight, + new Vector2(this.topLeft.X, this.bottomRight.Y) + ]; + + this.halfLength = this.Size.Width + this.Size.Height; + this.length = this.halfLength * 2; + this.Bounds = new RectangleF(this.Location, this.Size); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The which specifies the rectangle's point in a two-dimensional plane. + /// + /// + /// The which specifies the rectangle's height and width. + /// + public RectanglePolygon(PointF point, SizeF size) + : this(point, point + size) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The rectangle. + public RectanglePolygon(RectangleF rectangle) + : this(rectangle.Location, rectangle.Location + rectangle.Size) + { + } + + /// + /// Gets the location. + /// + public PointF Location { get; } + + /// + /// Gets the x-coordinate of the left edge. + /// + public float Left => this.X; + + /// + /// Gets the x-coordinate. + /// + public float X => this.topLeft.X; + + /// + /// Gets the x-coordinate of the right edge. + /// + public float Right => this.bottomRight.X; + + /// + /// Gets the y-coordinate of the top edge. + /// + public float Top => this.Y; + + /// + /// Gets the y-coordinate. + /// + public float Y => this.topLeft.Y; + + /// + /// Gets the y-coordinate of the bottom edge. + /// + public float Bottom => this.bottomRight.Y; + + /// + public RectangleF Bounds { get; private set; } + + /// + public bool IsClosed => true; + + /// + public ReadOnlyMemory Points => this.points; + + /// + /// Gets the size. + /// + public SizeF Size { get; } + + /// + /// Gets the width. + /// + public float Width => this.Size.Width; + + /// + /// Gets the height. + /// + public float Height => this.Size.Height; + + /// + public PathTypes PathType => PathTypes.Closed; + + /// + /// Gets the center point. + /// + public PointF Center => (this.topLeft + this.bottomRight) / 2; + + /// + /// Converts a polygon to a rectangle polygon from its bounds. + /// + /// The polygon to convert. + public static explicit operator RectanglePolygon(Polygon polygon) + => new(polygon.Bounds.X, polygon.Bounds.Y, polygon.Bounds.Width, polygon.Bounds.Height); + + /// + public IPath Transform(Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + return this; + } + + // Rectangles may be rotated and skewed which means they will then need representing by a polygon + return new Polygon(new LinearLineSegment(this.points).Transform(matrix)); + } + + /// + SegmentInfo IPathInternals.PointAlongPath(float distance) + { + distance %= this.length; + + if (distance < this.Width) + { + // we are on the top stretch + return new SegmentInfo + { + Point = new Vector2(this.Left + distance, this.Top), + Angle = MathF.PI + }; + } + + distance -= this.Width; + if (distance < this.Height) + { + // down on right + return new SegmentInfo + { + Point = new Vector2(this.Right, this.Top + distance), + Angle = -MathF.PI / 2 + }; + } + + distance -= this.Height; + if (distance < this.Width) + { + // bottom right to left + return new SegmentInfo + { + Point = new Vector2(this.Right - distance, this.Bottom), + Angle = 0 + }; + } + + distance -= this.Width; + return new SegmentInfo + { + Point = new Vector2(this.Left, this.Bottom - distance), + Angle = (float)(Math.PI / 2) + }; + } + + /// + public IEnumerable Flatten() + { + yield return this; + } + + /// + public LinearGeometry ToLinearGeometry(Vector2 scale) + => this.geometryCache.TryGet(scale, out LinearGeometry? hit) + ? hit + : this.geometryCache.Store(scale, this.BuildLinearGeometry(scale)); + + private LinearGeometry BuildLinearGeometry(Vector2 scale) + { + PointF p0 = new(this.points[0].X * scale.X, this.points[0].Y * scale.Y); + PointF p1 = new(this.points[1].X * scale.X, this.points[1].Y * scale.Y); + PointF p2 = new(this.points[2].X * scale.X, this.points[2].Y * scale.Y); + PointF p3 = new(this.points[3].X * scale.X, this.points[3].Y * scale.Y); + + PointF[] points = [p0, p1, p2, p3]; + + float minX = MathF.Min(MathF.Min(p0.X, p1.X), MathF.Min(p2.X, p3.X)); + float minY = MathF.Min(MathF.Min(p0.Y, p1.Y), MathF.Min(p2.Y, p3.Y)); + float maxX = MathF.Max(MathF.Max(p0.X, p1.X), MathF.Max(p2.X, p3.X)); + float maxY = MathF.Max(MathF.Max(p0.Y, p1.Y), MathF.Max(p2.Y, p3.Y)); + + // Any rotation or shear in the transform can turn the axis-aligned edges into slanted ones, + // so count each edge individually rather than assuming the axis-aligned case. + int nonHorizontalSegmentCountPixelBoundary = 0; + int nonHorizontalSegmentCountPixelCenter = 0; + for (int i = 0; i < 4; i++) + { + PointF a = points[i]; + PointF b = points[(i + 1) % 4]; + if (MathF.Floor(a.Y) != MathF.Floor(b.Y)) + { + nonHorizontalSegmentCountPixelBoundary++; + } + + if (MathF.Floor(a.Y + 0.5F) != MathF.Floor(b.Y + 0.5F)) + { + nonHorizontalSegmentCountPixelCenter++; + } + } + + return new LinearGeometry( + new LinearGeometryInfo + { + Bounds = RectangleF.FromLTRB(minX, minY, maxX, maxY), + ContourCount = 1, + PointCount = 4, + SegmentCount = 4, + NonHorizontalSegmentCountPixelBoundary = nonHorizontalSegmentCountPixelBoundary, + NonHorizontalSegmentCountPixelCenter = nonHorizontalSegmentCountPixelCenter + }, + [new LinearContour { PointStart = 0, PointCount = 4, SegmentStart = 0, SegmentCount = 4, IsClosed = true }], + points); + } + + /// + public IPath AsClosedPath() => this; + } +} diff --git a/ImageSharp.Drawing/RegularPolygon.cs b/ImageSharp.Drawing/RegularPolygon.cs new file mode 100644 index 0000000..d2802f5 --- /dev/null +++ b/ImageSharp.Drawing/RegularPolygon.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// A shape made up of a single path made up of one of more s + /// + public class RegularPolygon : Polygon + { + /// + /// Initializes a new instance of the class. + /// + /// The location the center of the polygon will be placed. + /// The number of vertices the should have. + /// The radius of the circle that would touch all vertices. + /// The angle of rotation in degrees. + public RegularPolygon(PointF location, int vertices, float radius, float angle) + : base(CreateSegment(location, radius, vertices, angle)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The location the center of the polygon will be placed. + /// The number of vertices the should have. + /// The radius of the circle that would touch all vertices. + public RegularPolygon(PointF location, int vertices, float radius) + : this(location, vertices, radius, 0) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The x-coordinate of the center of the polygon. + /// The y-coordinate of the center of the polygon. + /// The number of vertices the should have. + /// The radius of the circle that would touch all vertices. + /// The angle of rotation in degrees. + public RegularPolygon(float x, float y, int vertices, float radius, float angle) + : this(new PointF(x, y), vertices, radius, angle) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The x-coordinate of the center of the polygon. + /// The y-coordinate of the center of the polygon. + /// The number of vertices the should have. + /// The radius of the circle that would touch all vertices. + public RegularPolygon(float x, float y, int vertices, float radius) + : this(new PointF(x, y), vertices, radius) + { + } + + private static LinearLineSegment CreateSegment(PointF location, float radius, int vertices, float angle) + { + Guard.MustBeGreaterThan(vertices, 2, nameof(vertices)); + Guard.MustBeGreaterThan(radius, 0, nameof(radius)); + + PointF distanceVector = new(0, radius); + + float anglePerSegments = (float)(2 * Math.PI / vertices); + float current = GeometryUtilities.DegreeToRadian(angle); + PointF[] points = new PointF[vertices]; + for (int i = 0; i < vertices; i++) + { + PointF rotated = PointF.Transform(distanceVector, Matrix4x4.CreateRotationZ(current)); + + points[i] = rotated + location; + + current += anglePerSegments; + } + + return new LinearLineSegment(points); + } + } +} diff --git a/ImageSharp.Drawing/RoundedRectanglePolygon.cs b/ImageSharp.Drawing/RoundedRectanglePolygon.cs new file mode 100644 index 0000000..af9011e --- /dev/null +++ b/ImageSharp.Drawing/RoundedRectanglePolygon.cs @@ -0,0 +1,142 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// A closed rectangular path with rounded corners. + /// + public sealed class RoundedRectanglePolygon : Polygon + { + /// + /// Initializes a new instance of the class. + /// + /// The rectangle bounds. + /// The x and y radius of each corner. + public RoundedRectanglePolygon(RectangleF rectangle, float radius) + : this(rectangle, new SizeF(radius, radius)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The rectangle bounds. + /// The x and y radii of each corner. + public RoundedRectanglePolygon(RectangleF rectangle, SizeF radius) + : base(CreateSegments(rectangle, radius)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The x-coordinate of the rectangle. + /// The y-coordinate of the rectangle. + /// The rectangle width. + /// The rectangle height. + /// The x and y radius of each corner. + public RoundedRectanglePolygon(float x, float y, float width, float height, float radius) + : this(new RectangleF(x, y, width, height), radius) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The x-coordinate of the rectangle. + /// The y-coordinate of the rectangle. + /// The rectangle width. + /// The rectangle height. + /// The x and y radii of each corner. + public RoundedRectanglePolygon(float x, float y, float width, float height, SizeF radius) + : this(new RectangleF(x, y, width, height), radius) + { + } + + private RoundedRectanglePolygon(ILineSegment[] segments) + : base(segments, true) + { + } + + /// + public override IPath Transform(Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + return this; + } + + ILineSegment[] segments = new ILineSegment[this.LineSegments.Count]; + + for (int i = 0; i < segments.Length; i++) + { + segments[i] = this.LineSegments[i].Transform(matrix); + } + + return new RoundedRectanglePolygon(segments); + } + + private static ILineSegment[] CreateSegments(RectangleF rectangle, SizeF radius) + { + float left = MathF.Min(rectangle.Left, rectangle.Right); + float top = MathF.Min(rectangle.Top, rectangle.Bottom); + float right = MathF.Max(rectangle.Left, rectangle.Right); + float bottom = MathF.Max(rectangle.Top, rectangle.Bottom); + float width = right - left; + float height = bottom - top; + + if (width <= 0 || height <= 0) + { + return []; + } + + float radiusX = radius.Width; + float radiusY = radius.Height; + + if (radiusX <= 0 || radiusY <= 0) + { + return + [ + new LinearLineSegment( + new PointF(left, top), + new PointF(right, top), + new PointF(right, bottom), + new PointF(left, bottom)) + ]; + } + + float radiusScale = MathF.Min(width / (radiusX + radiusX), height / (radiusY + radiusY)); + if (radiusScale < 1F) + { + // Preserve the supplied corner shape while shrinking it enough that opposing corners do not overlap. + radiusX *= radiusScale; + radiusY *= radiusScale; + } + + SizeF cornerRadius = new(radiusX, radiusY); + PointF topLeft = new(left + radiusX, top); + PointF topRight = new(right - radiusX, top); + PointF rightTop = new(right, top + radiusY); + PointF rightBottom = new(right, bottom - radiusY); + PointF bottomRight = new(right - radiusX, bottom); + PointF bottomLeft = new(left + radiusX, bottom); + PointF leftBottom = new(left, bottom - radiusY); + PointF leftTop = new(left, top + radiusY); + + return + [ + new LinearLineSegment(topLeft, topRight), + new ArcLineSegment(new PointF(right - radiusX, top + radiusY), cornerRadius, 0F, -90F, 90F), + new LinearLineSegment(rightTop, rightBottom), + new ArcLineSegment(new PointF(right - radiusX, bottom - radiusY), cornerRadius, 0F, 0F, 90F), + new LinearLineSegment(bottomRight, bottomLeft), + new ArcLineSegment(new PointF(left + radiusX, bottom - radiusY), cornerRadius, 0F, 90F, 90F), + new LinearLineSegment(leftBottom, leftTop), + new ArcLineSegment(new PointF(left + radiusX, top + radiusY), cornerRadius, 0F, 180F, 90F) + ]; + } + } +} diff --git a/ImageSharp.Drawing/SegmentEnumerator.cs b/ImageSharp.Drawing/SegmentEnumerator.cs new file mode 100644 index 0000000..d0e0e0c --- /dev/null +++ b/ImageSharp.Drawing/SegmentEnumerator.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Enumerates the derived linear segments in a . + /// + /// + /// The enumerator derives segments from and . + /// Segments are yielded in contour order. Within each contour, adjacent stored points form segments in point order, + /// and a closed contour contributes one additional closing segment from its last stored point back to its first. + /// + public ref struct SegmentEnumerator + { + private readonly LinearGeometry geometry; + private int contourIndex; + private int segmentIndexInContour; + private LinearSegment current; + + internal SegmentEnumerator(LinearGeometry geometry) + { + this.geometry = geometry; + this.contourIndex = 0; + this.segmentIndexInContour = 0; + this.current = default; + } + + /// + /// Gets the current derived linear segment. + /// + public readonly LinearSegment Current => this.current; + + /// + /// Advances to the next derived segment. + /// + /// + /// if a segment was produced; otherwise . + /// + public bool MoveNext() + { + while (this.contourIndex < this.geometry.Contours.Count) + { + LinearContour contour = this.geometry.Contours[this.contourIndex]; + if (this.segmentIndexInContour < contour.SegmentCount) + { + int pointStart = contour.PointStart; + int pointIndex = pointStart + this.segmentIndexInContour; + + PointF start = this.geometry.Points[pointIndex]; + PointF end = this.segmentIndexInContour == contour.PointCount - 1 + ? this.geometry.Points[pointStart] + : this.geometry.Points[pointIndex + 1]; + + this.current = CreateSegment(start, end); + this.segmentIndexInContour++; + return true; + } + + this.contourIndex++; + this.segmentIndexInContour = 0; + } + + return false; + } + + private static LinearSegment CreateSegment(PointF start, PointF end) + => new() + { + Start = start, + End = end, + MinY = MathF.Min(start.Y, end.Y), + MaxY = MathF.Max(start.Y, end.Y), + IsHorizontal = start.Y == end.Y + }; + } +} diff --git a/ImageSharp.Drawing/SegmentInfo.cs b/ImageSharp.Drawing/SegmentInfo.cs new file mode 100644 index 0000000..674dd40 --- /dev/null +++ b/ImageSharp.Drawing/SegmentInfo.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Returns metadata about the point along a path. + /// + public readonly struct SegmentInfo + { + /// + /// Gets the point on the path + /// + public PointF Point { get; init; } + + /// + /// Gets the angle of the segment. Measured in radians. + /// + public float Angle { get; init; } + } +} diff --git a/ImageSharp.Drawing/SplitPathExtensions.cs b/ImageSharp.Drawing/SplitPathExtensions.cs new file mode 100644 index 0000000..f6c091b --- /dev/null +++ b/ImageSharp.Drawing/SplitPathExtensions.cs @@ -0,0 +1,203 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// Extensions to for splitting paths into dash segments + /// without performing stroke expansion. + /// + public static class SplitPathExtensions + { + // Safety limit: if the estimated number of dash segments exceeds this threshold, + // return the original path unsplit to avoid runaway segmentation from very short + // patterns applied to very long paths. + private const int MaxPatternSegments = 10000; + + /// + /// Splits the given path into dash segments based on the provided pattern. + /// Returns a composite path containing only the "on" segments as open sub-paths. + /// + /// The centerline path to split. + /// The stroke width (pattern elements are multiples of this). + /// The dash pattern. Each element is a multiple of . + /// A path containing the "on" dash segments. + public static IPath GenerateDashes(this IPath path, float strokeWidth, ReadOnlySpan pattern) + => path.GenerateDashes(strokeWidth, pattern, startOff: false); + + /// + /// Splits the given path into dash segments based on the provided pattern. + /// Returns a composite path containing only the "on" segments as open sub-paths. + /// + /// The centerline path to split. + /// The stroke width (pattern elements are multiples of this). + /// The dash pattern. Each element is a multiple of . + /// Whether the first item in the pattern is off rather than on. + /// A path containing the "on" dash segments. + public static IPath GenerateDashes(this IPath path, float strokeWidth, ReadOnlySpan pattern, bool startOff) + { + if (pattern.Length < 2) + { + return path; + } + + const float eps = 1e-6f; + + // Compute the absolute pattern length in path units to detect degenerate patterns. + float patternLength = 0f; + for (int i = 0; i < pattern.Length; i++) + { + patternLength += MathF.Abs(pattern[i]) * strokeWidth; + } + + // Fallback to the original path when the dash pattern is too small to be meaningful. + if (patternLength <= eps) + { + return path; + } + + IEnumerable simplePaths = path.Flatten(); + List segments = []; + List buffer = new(64); + + foreach (ISimplePath p in simplePaths) + { + bool online = !startOff; + int patternPos = 0; + float targetLength = pattern[patternPos] * strokeWidth; + + ReadOnlySpan pts = p.Points.Span; + if (pts.Length < 2) + { + continue; + } + + // Number of edges to traverse (closed paths wrap; open paths stop one short). + int edgeCount = p.IsClosed ? pts.Length : pts.Length - 1; + + // Compute total path length to estimate the number of dash segments. + // This avoids runaway segmentation when a very short pattern is applied + // to a very long path. + float totalLength = 0f; + for (int j = 0; j < edgeCount; j++) + { + int nextIndex = p.IsClosed ? (j + 1) % pts.Length : j + 1; + totalLength += Vector2.Distance(pts[j], pts[nextIndex]); + } + + if (totalLength > eps) + { + float estimatedSegments = (totalLength / patternLength) * pattern.Length; + if (estimatedSegments > MaxPatternSegments) + { + return path; + } + } + + int ei = 0; + Vector2 current = pts[0]; + + while (ei < edgeCount) + { + int nextIndex = p.IsClosed ? (ei + 1) % pts.Length : ei + 1; + Vector2 next = pts[nextIndex]; + float segLen = Vector2.Distance(current, next); + + // Skip degenerate zero-length segments. + if (segLen <= eps) + { + current = next; + ei++; + continue; + } + + // Accumulate into the current dash span when the segment is shorter + // than the remaining target length. + if (segLen + eps < targetLength) + { + if (online) + { + buffer.Add(current); + } + + current = next; + ei++; + targetLength -= segLen; + continue; + } + + // Close out a dash span when the segment length matches the target. + if (MathF.Abs(segLen - targetLength) <= eps) + { + if (online) + { + buffer.Add(current); + buffer.Add(next); + FlushBuffer(buffer, segments); + } + + buffer.Clear(); + online = !online; + current = next; + ei++; + patternPos = (patternPos + 1) % pattern.Length; + targetLength = pattern[patternPos] * strokeWidth; + continue; + } + + // Split inside this segment to end the current dash span. + float t = targetLength / segLen; + Vector2 split = current + (t * (next - current)); + + if (online) + { + buffer.Add(current); + buffer.Add(split); + FlushBuffer(buffer, segments); + } + + buffer.Clear(); + online = !online; + current = split; // continue along the same geometric segment + patternPos = (patternPos + 1) % pattern.Length; + targetLength = pattern[patternPos] * strokeWidth; + } + + // Flush the tail of the last dash span, if any. + if (buffer.Count > 0) + { + if (online) + { + buffer.Add(current); + FlushBuffer(buffer, segments); + } + + buffer.Clear(); + } + } + + if (segments.Count == 0) + { + return path; + } + + if (segments.Count == 1) + { + return segments[0]; + } + + return new ComplexPolygon(segments); + } + + private static void FlushBuffer(List buffer, List segments) + { + if (buffer.Count >= 2 && buffer[0] != buffer[^1]) + { + segments.Add(new Path(new LinearLineSegment([.. buffer]))); + } + } + } +} diff --git a/ImageSharp.Drawing/StarPolygon.cs b/ImageSharp.Drawing/StarPolygon.cs new file mode 100644 index 0000000..45ae245 --- /dev/null +++ b/ImageSharp.Drawing/StarPolygon.cs @@ -0,0 +1,100 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing { + /// + /// A star-shaped polygon defined by alternating inner and outer radii. + /// + public sealed class StarPolygon : Polygon + { + /// + /// Initializes a new instance of the class. + /// + /// The center point of the star. + /// The number of star prongs. + /// The inner star radius. + /// The outer star radius. + /// The angle of rotation in degrees. + public StarPolygon(PointF location, int prongs, float innerRadii, float outerRadii, float angle) + : base(CreateSegment(location, innerRadii, outerRadii, prongs, angle)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The center point of the star. + /// The number of star prongs. + /// The inner star radius. + /// The outer star radius. + public StarPolygon(PointF location, int prongs, float innerRadii, float outerRadii) + : this(location, prongs, innerRadii, outerRadii, 0) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The x-coordinate of the star center. + /// The y-coordinate of the star center. + /// The number of star prongs. + /// The inner star radius. + /// The outer star radius. + /// The angle of rotation in degrees. + public StarPolygon(float x, float y, int prongs, float innerRadii, float outerRadii, float angle) + : this(new PointF(x, y), prongs, innerRadii, outerRadii, angle) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The x-coordinate of the star center. + /// The y-coordinate of the star center. + /// The number of star prongs. + /// The inner star radius. + /// The outer star radius. + public StarPolygon(float x, float y, int prongs, float innerRadii, float outerRadii) + : this(new PointF(x, y), prongs, innerRadii, outerRadii) + { + } + + private static LinearLineSegment CreateSegment(Vector2 location, float innerRadii, float outerRadii, int prongs, float angle) + { + Guard.MustBeGreaterThan(prongs, 2, nameof(prongs)); + Guard.MustBeGreaterThan(innerRadii, 0, nameof(innerRadii)); + Guard.MustBeGreaterThan(outerRadii, 0, nameof(outerRadii)); + + Vector2 distanceVectorInner = new(0, innerRadii); + Vector2 distanceVectorOuter = new(0, outerRadii); + + int vertices = prongs * 2; + float anglePerSegments = (float)(2 * Math.PI / vertices); + float current = GeometryUtilities.DegreeToRadian(angle); + PointF[] points = new PointF[vertices]; + Vector2 distance = distanceVectorInner; + for (int i = 0; i < vertices; i++) + { + if (distance == distanceVectorInner) + { + distance = distanceVectorOuter; + } + else + { + distance = distanceVectorInner; + } + + Vector2 rotated = PointF.Transform(distance, Matrix4x4.CreateRotationZ(current)); + + points[i] = rotated + location; + + current += anglePerSegments; + } + + return new LinearLineSegment(points); + } + } +} diff --git a/ImageSharp.Drawing/Text/BaseGlyphBuilder.cs b/ImageSharp.Drawing/Text/BaseGlyphBuilder.cs new file mode 100644 index 0000000..859502f --- /dev/null +++ b/ImageSharp.Drawing/Text/BaseGlyphBuilder.cs @@ -0,0 +1,584 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.Fonts; +using SixLabors.Fonts.Rendering; +using SixLabors.ImageSharp.Drawing.Processing; + +namespace SixLabors.ImageSharp.Drawing.Text { + /// + /// Defines a base rendering surface that Fonts can use to generate shapes. + /// + internal class BaseGlyphBuilder : IGlyphRenderer + { + /// + /// The last point emitted by MoveTo / LineTo / curve commands. + /// Used as the implicit start of the next segment. + /// + private Vector2 currentPoint; + + /// + /// Snapshot of the for the glyph currently + /// being processed. Set at the start of each BeginGlyph call and read by + /// SetDecoration to determine layout orientation. + /// + private GlyphRendererParameters parameters; + + // Tracks whether geometry was emitted inside BeginLayer/EndLayer pairs for this glyph. + // When true, EndGlyph skips its default single-layer path capture because layers + // already contributed their paths individually. + private bool usedLayers; + + // Tracks whether we are currently inside a layer block. + // Guards against unbalanced EndLayer calls. + private bool inLayer; + + // --- Per-GRAPHEME layered capture --- + // A grapheme cluster (e.g. a base glyph + COLR v0 color layers) may span + // multiple BeginGlyph/EndGlyph calls. These fields aggregate all layers + // belonging to the same grapheme into a single GlyphPathCollection. + private GlyphPathCollection.Builder? graphemeBuilder; + private int graphemePathCount; + private int currentGraphemeIndex = -1; + private readonly List currentGlyphs = []; + + // Previous decoration details per decoration type, used to stitch adjacent + // decorations together and eliminate sub-pixel gaps between glyphs. + private TextDecorationDetails? previousUnderlineTextDecoration; + private TextDecorationDetails? previousOverlineTextDecoration; + private TextDecorationDetails? previousStrikeoutTextDecoration; + + // Per-layer (within current grapheme) bookkeeping: + private int layerStartIndex; + private Paint? currentLayerPaint; + private FillRule currentLayerFillRule; + private ClipQuad? currentClipBounds; + + /// + /// Initializes a new instance of the class + /// with an identity transform. + /// + public BaseGlyphBuilder() => this.Builder = new PathBuilder(); + + /// + /// Initializes a new instance of the class + /// with the specified transform applied to all incoming glyph geometry. + /// + /// A matrix transform applied to every point received from the font engine. + public BaseGlyphBuilder(Matrix4x4 transform) => this.Builder = new PathBuilder(transform); + + /// + /// Gets the flattened paths captured for all glyphs/graphemes. + /// + public IPathCollection Paths => new PathCollection(this.CurrentPaths); + + /// + /// Gets the layer-preserving collections captured per grapheme in rendering order. + /// Each entry aggregates all glyph layers that belong to a single grapheme cluster. + /// + public IReadOnlyList Glyphs => this.currentGlyphs; + + /// + /// Gets the used to accumulate outline segments + /// (MoveTo, LineTo, curves) for the current glyph or layer. + /// The builder is cleared between glyphs / layers. + /// + protected PathBuilder Builder { get; } + + /// + /// Gets the running list of all instances produced so far + /// (glyph outlines, layer outlines, and decoration rectangles). Subclasses + /// read from the end of this list (e.g. CurrentPaths[^1]) to obtain + /// the most recently built path. + /// + protected List CurrentPaths { get; } = []; + + /// + /// Called by the font engine after all glyphs in the text block have been rendered. + /// Flushes any in-progress grapheme aggregate and resets per-text-block state. + /// + void IGlyphRenderer.EndText() + { + // Finalize the last grapheme, if any: + if (this.graphemeBuilder is not null && this.graphemePathCount > 0) + { + this.currentGlyphs.Add(this.graphemeBuilder.Build()); + } + + this.graphemeBuilder = null; + this.graphemePathCount = 0; + this.currentGraphemeIndex = -1; + this.previousUnderlineTextDecoration = null; + this.previousOverlineTextDecoration = null; + this.previousStrikeoutTextDecoration = null; + + this.EndText(); + } + + void IGlyphRenderer.BeginText(in FontRectangle bounds) => this.BeginText(bounds); + + /// + /// Called by the font engine before emitting outline data for a single glyph. + /// Manages grapheme-cluster transitions and resets per-glyph state. + /// + /// + /// to have the font engine emit the full outline + /// (MoveTo/LineTo/curves/EndGlyph); to skip it entirely, + /// which is used by caching subclasses when the glyph path is already available. + /// + bool IGlyphRenderer.BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) + { + // If grapheme changed, flush previous aggregate and start a new one: + if (this.graphemeBuilder is not null && this.currentGraphemeIndex != parameters.GraphemeIndex) + { + if (this.graphemePathCount > 0) + { + this.currentGlyphs.Add(this.graphemeBuilder.Build()); + } + + this.graphemeBuilder = null; + this.graphemePathCount = 0; + } + + if (this.graphemeBuilder is null) + { + this.graphemeBuilder = new GlyphPathCollection.Builder(); + this.currentGraphemeIndex = parameters.GraphemeIndex; + this.graphemePathCount = 0; + } + + this.parameters = parameters; + this.Builder.Clear(); + this.usedLayers = false; + this.inLayer = false; + + this.layerStartIndex = this.graphemePathCount; + this.currentLayerPaint = null; + this.currentLayerFillRule = FillRule.NonZero; + this.currentClipBounds = null; + return this.BeginGlyph(in bounds, in parameters); + } + + /// + void IGlyphRenderer.BeginFigure() => this.Builder.StartFigure(); + + /// + void IGlyphRenderer.CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) + { + this.Builder.AddCubicBezier(this.currentPoint, secondControlPoint, thirdControlPoint, point); + this.currentPoint = point; + } + + /// + /// Called by the font engine after the outline for a single glyph has been fully emitted. + /// Builds the accumulated path and registers it as a grapheme layer unless explicit + /// BeginLayer/EndLayer pairs already handled layer registration. + /// + void IGlyphRenderer.EndGlyph() + { + // If the glyph did not open any explicit layer, treat its geometry as a single + // implicit layer so that non-color glyphs still produce a GlyphPathCollection entry. + if (!this.usedLayers) + { + IPath path = this.Builder.Build(); + + this.CurrentPaths.Add(path); + + if (this.graphemeBuilder is not null) + { + this.graphemeBuilder.AddPath(path); + this.graphemeBuilder.AddLayer( + startIndex: this.graphemePathCount, + count: 1, + paint: null, + fillRule: FillRule.NonZero, + bounds: path.Bounds, + kind: GlyphLayerKind.Glyph); + + this.graphemePathCount++; + } + } + + this.EndGlyph(); + this.Builder.Clear(); + this.inLayer = false; + this.usedLayers = false; + this.layerStartIndex = this.graphemePathCount; + } + + /// + void IGlyphRenderer.EndFigure() => this.Builder.CloseFigure(); + + /// + void IGlyphRenderer.LineTo(Vector2 point) + { + this.Builder.AddLine(this.currentPoint, point); + this.currentPoint = point; + } + + /// + void IGlyphRenderer.MoveTo(Vector2 point) + { + this.Builder.StartFigure(); + this.currentPoint = point; + } + + /// + void IGlyphRenderer.ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, Vector2 point) + { + this.Builder.AddArc(this.currentPoint, radiusX, radiusY, rotation, largeArc, sweep, point); + this.currentPoint = point; + } + + /// + void IGlyphRenderer.QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) + { + this.Builder.AddQuadraticBezier(this.currentPoint, secondControlPoint, point); + this.currentPoint = point; + } + + /// + /// Called by the font engine to begin a color layer within a COLR v0/v1 glyph. + /// Each layer receives its own paint, fill rule, and optional clip bounds. + /// + void IGlyphRenderer.BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) + { + this.usedLayers = true; + this.inLayer = true; + this.layerStartIndex = this.graphemePathCount; + this.currentLayerPaint = paint; + this.currentLayerFillRule = fillRule; + this.currentClipBounds = clipBounds; + + this.Builder.Clear(); + this.BeginLayer(paint, fillRule, clipBounds); + } + + /// + /// Called by the font engine to close a color layer opened by BeginLayer. + /// Builds the layer path, applies any clip quad, and registers the result + /// as a painted layer in the current grapheme aggregate. + /// + void IGlyphRenderer.EndLayer() + { + if (!this.inLayer) + { + return; + } + + IPath path = this.Builder.Build(); + + // If the layer defines a clip quad (e.g. from COLR v1), intersect the + // built path with the quad polygon to constrain rendering. + if (this.currentClipBounds is not null) + { + ClipQuad clip = this.currentClipBounds.Value; + PointF[] points = [clip.TopLeft, clip.TopRight, clip.BottomRight, clip.BottomLeft]; + LinearLineSegment segment = new(points); + Polygon polygon = new(segment); + + ShapeOptions options = new() + { + BooleanOperation = BooleanOperation.Intersection, + IntersectionRule = TextUtilities.MapFillRule(this.currentLayerFillRule) + }; + + path = path.Clip(options, polygon); + } + + this.CurrentPaths.Add(path); + + if (this.graphemeBuilder is not null) + { + this.graphemeBuilder.AddPath(path); + this.graphemeBuilder.AddLayer( + startIndex: this.layerStartIndex, + count: 1, + paint: this.currentLayerPaint, + fillRule: this.currentLayerFillRule, + bounds: path.Bounds, + kind: GlyphLayerKind.Painted); + + this.graphemePathCount++; + } + + this.Builder.Clear(); + this.inLayer = false; + this.currentLayerPaint = null; + this.currentLayerFillRule = FillRule.NonZero; + this.currentClipBounds = null; + this.EndLayer(); + } + + /// + /// Called by the font engine to emit a text decoration (underline, strikeout, or overline) + /// for the current glyph. Builds a filled rectangle path from the start/end positions and + /// thickness, then registers it as a layer. + /// Adjacent decorations are stitched together using the previous decoration details to + /// eliminate sub-pixel gaps caused by font metric rounding. + /// + void IGlyphRenderer.SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) + { + if (thickness == 0) + { + return; + } + + // Clamp the thickness to whole pixels. + thickness = MathF.Max(1F, (float)Math.Round(thickness)); + IGlyphRenderer renderer = this; + + bool rotated = this.parameters.LayoutMode is GlyphLayoutMode.Vertical or GlyphLayoutMode.VerticalRotated; + Vector2 pad = rotated ? new Vector2(thickness * .5F, 0) : new Vector2(0, thickness * .5F); + + start = ClampToPixel(start, (int)thickness, rotated); + end = ClampToPixel(end, (int)thickness, rotated); + + // Sometimes the start and end points do not align properly leaving pixel sized gaps + // so we need to adjust them. Use any previous decoration to try and continue the line. + TextDecorationDetails? previous = textDecorations switch + { + TextDecorations.Underline => this.previousUnderlineTextDecoration, + TextDecorations.Overline => this.previousOverlineTextDecoration, + TextDecorations.Strikeout => this.previousStrikeoutTextDecoration, + _ => null + }; + + if (previous != null) + { + float prevThickness = previous.Value.Thickness; + Vector2 prevStart = previous.Value.Start; + Vector2 prevEnd = previous.Value.End; + + // If the previous line is identical to the new one ignore it. + // This can happen when multiple glyph layers are used. + if (prevStart == start && prevEnd == end) + { + return; + } + + // Align the new line with the previous one if they are close enough. + // Use a 2 pixel threshold to account for anti-aliasing gaps. + if (rotated) + { + if (thickness == prevThickness + && prevEnd.Y + 2 >= start.Y + && prevEnd.X == start.X) + { + start = prevEnd; + } + } + else if (thickness == prevThickness + && prevEnd.Y == start.Y + && prevEnd.X + 2 >= start.X) + { + start = prevEnd; + } + } + + TextDecorationDetails current = new() + { + Start = start, + End = end, + Thickness = thickness + }; + + switch (textDecorations) + { + case TextDecorations.Underline: + this.previousUnderlineTextDecoration = current; + break; + case TextDecorations.Strikeout: + this.previousStrikeoutTextDecoration = current; + break; + case TextDecorations.Overline: + this.previousOverlineTextDecoration = current; + break; + } + + Vector2 a = start - pad; + Vector2 b = start + pad; + Vector2 c = end + pad; + Vector2 d = end - pad; + + // Drawing is always centered around the point so we need to offset by half. + Vector2 offset = Vector2.Zero; + if (textDecorations == TextDecorations.Overline) + { + // CSS overline is drawn above the position, so we need to move it up. + offset = rotated ? new Vector2(thickness * .5F, 0) : new Vector2(0, -(thickness * .5F)); + } + else if (textDecorations == TextDecorations.Underline) + { + // CSS underline is drawn below the position, so we need to move it down. + offset = rotated ? new Vector2(-(thickness * .5F), 0) : new Vector2(0, thickness * .5F); + } + + // We clamp the start and end points to the pixel grid to avoid anti-aliasing + // when there is no transform. + renderer.BeginFigure(); + renderer.MoveTo(ClampToPixel(a + offset)); + renderer.LineTo(ClampToPixel(b + offset)); + renderer.LineTo(ClampToPixel(c + offset)); + renderer.LineTo(ClampToPixel(d + offset)); + renderer.EndFigure(); + + IPath path = this.Builder.Build(); + + // If the path is degenerate (e.g. zero width line) we just skip it + // and return. This might happen when clamping moves the points. + if (path.Bounds.IsEmpty) + { + this.Builder.Clear(); + return; + } + + this.CurrentPaths.Add(path); + if (this.graphemeBuilder is not null) + { + // Decorations are emitted as independent paths; each layer must point + // at the path index appended for this specific decoration. + this.graphemeBuilder.AddPath(path); + this.graphemeBuilder.AddLayer( + startIndex: this.graphemePathCount, + count: 1, + paint: this.currentLayerPaint, + fillRule: FillRule.NonZero, + bounds: path.Bounds, + kind: GlyphLayerKind.Decoration); + + this.graphemePathCount++; + } + + this.Builder.Clear(); + this.SetDecoration(textDecorations, start, end, thickness); + } + + /// + protected virtual void BeginText(in FontRectangle bounds) + { + } + + /// + /// Called after base-class bookkeeping in IGlyphRenderer.BeginGlyph. + /// Subclasses override this to apply transforms, consult caches, or opt out of + /// outline emission by returning . + /// + /// The font-metric bounding rectangle of the glyph. + /// Identifies the glyph (id, font, layout mode, text run, etc.). + /// + /// to receive outline data and an EndGlyph call; + /// to skip outline emission for this glyph entirely. + /// + protected virtual bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) + => true; + + /// + /// Called after the base class has built and registered the glyph path. + /// Subclasses override this to emit drawing operations from the captured path. + /// + protected virtual void EndGlyph() + { + } + + /// + /// Called after the base class has flushed all grapheme aggregates. + /// Subclasses override this for any per-text-block finalization. + /// + protected virtual void EndText() + { + } + + /// + /// Called when a COLR color layer begins. Subclasses override this to + /// capture the layer's paint and composite mode. + /// + /// The paint for this color layer, or for the default foreground. + /// The fill rule to use when rasterizing this layer. + /// Optional clip quad constraining the layer region. + protected virtual void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) + { + } + + /// + /// Called when a COLR color layer ends. Subclasses override this to + /// emit the layer as a drawing operation. + /// + protected virtual void EndLayer() + { + } + + /// + /// Returns the set of text decorations enabled for the current glyph. + /// The font engine calls this to decide which SetDecoration callbacks to emit. + /// Subclasses override this to include decorations implied by rich-text pens + /// (e.g. ). + /// + /// A flags enum of the active text decorations. + public virtual TextDecorations EnabledDecorations() + => this.parameters.TextRun.TextDecorations; + + /// + /// Override point for subclasses to emit decoration drawing operations. + /// Called after the base class has built and registered the decoration path + /// in . + /// + /// The type of decoration (underline, strikeout, or overline). + /// The start position of the decoration line. + /// The end position of the decoration line. + /// The thickness of the decoration line in pixels. + public virtual void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) + { + } + + /// + /// Truncates a floating-point position to the nearest whole pixel toward negative infinity. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Point ClampToPixel(PointF point) => Point.Truncate(point); + + /// + /// Snaps a decoration endpoint to the pixel grid, taking stroke thickness and + /// orientation into account. Even-thickness lines snap to whole pixels; odd-thickness + /// lines snap to half pixels so the stroke center lands on a pixel boundary. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static PointF ClampToPixel(PointF point, int thickness, bool rotated) + { + // Even thickness: snap to whole pixels. + if ((thickness & 1) == 0) + { + return Point.Truncate(point); + } + + // Odd thickness: snap to half pixels along the perpendicular axis + // so the 1px-wide center row/column aligns with physical pixels. + if (rotated) + { + return Point.Truncate(point) + new Vector2(.5F, 0); + } + + return Point.Truncate(point) + new Vector2(0, .5F); + } + + /// + /// Records the start, end, and thickness of a previously emitted decoration line + /// so that the next adjacent decoration can be stitched seamlessly. + /// + private struct TextDecorationDetails + { + /// Gets or sets the start position of the decoration. + public Vector2 Start { get; set; } + + /// Gets or sets the end position of the decoration. + public Vector2 End { get; set; } + + /// Gets or sets the decoration thickness in pixels. + public float Thickness { get; internal set; } + } + } +} diff --git a/ImageSharp.Drawing/Text/GlyphBuilder.cs b/ImageSharp.Drawing/Text/GlyphBuilder.cs new file mode 100644 index 0000000..63ec00a --- /dev/null +++ b/ImageSharp.Drawing/Text/GlyphBuilder.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Text { + /// + /// A rendering surface that Fonts can use to generate shapes. + /// Extends by adding a configurable origin offset + /// so that all captured geometry is translated by the specified amount. + /// + internal class GlyphBuilder : BaseGlyphBuilder + { + /// + /// Initializes a new instance of the class. + /// + public GlyphBuilder() + : this(Vector2.Zero) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The origin. + public GlyphBuilder(Vector2 origin) => this.Builder.SetOrigin(origin); + } +} diff --git a/ImageSharp.Drawing/Text/GlyphLayerInfo.cs b/ImageSharp.Drawing/Text/GlyphLayerInfo.cs new file mode 100644 index 0000000..6b8c1eb --- /dev/null +++ b/ImageSharp.Drawing/Text/GlyphLayerInfo.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.Fonts.Rendering; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Text { + /// + /// Describes a single painted layer as a span within the glyph's path list. + /// + public readonly struct GlyphLayerInfo + { + /// + /// Initializes a new instance of the struct. + /// + /// Start index (inclusive) of the layer's paths within the glyph's path list. + /// Number of paths in this layer. + /// The layer paint (null means use renderer default). + /// The fill rule to use for this layer. + /// Axis-aligned bounds of the layer geometry. + /// An optional semantic hint for the layer type. + internal GlyphLayerInfo( + int startIndex, + int count, + Paint? paint, + FillRule fillRule, + RectangleF bounds, + GlyphLayerKind kind) + { + this.StartIndex = startIndex; + this.Count = count; + this.Paint = paint; + this.IntersectionRule = TextUtilities.MapFillRule(fillRule); + + CompositeMode compositeMode = paint?.CompositeMode ?? CompositeMode.SrcOver; + this.PixelAlphaCompositionMode = TextUtilities.MapCompositionMode(compositeMode); + this.PixelColorBlendingMode = TextUtilities.MapBlendingMode(compositeMode); + this.Bounds = bounds; + this.Kind = kind; + } + + private GlyphLayerInfo( + int startIndex, + int count, + Paint? paint, + IntersectionRule intersectionRule, + PixelAlphaCompositionMode compositionMode, + PixelColorBlendingMode colorBlendingMode, + RectangleF bounds, + GlyphLayerKind kind) + { + this.StartIndex = startIndex; + this.Count = count; + this.Paint = paint; + this.IntersectionRule = intersectionRule; + this.PixelAlphaCompositionMode = compositionMode; + this.PixelColorBlendingMode = colorBlendingMode; + this.Bounds = bounds; + this.Kind = kind; + } + + /// + /// Gets the start index (inclusive) of the layer span within the glyph's path list. + /// + public int StartIndex { get; } + + /// + /// Gets the number of paths in this layer. + /// + public int Count { get; } + + /// + /// Gets the paint definition to use for this layer; may be . + /// + public Paint? Paint { get; } + + /// + /// Gets the fill rule for rasterization of this layer. + /// + public IntersectionRule IntersectionRule { get; } + + /// + /// Gets the pixel alpha composition mode to use for this layer. + /// + public PixelAlphaCompositionMode PixelAlphaCompositionMode { get; } + + /// + /// Gets the pixel color blending mode to use for this layer. + /// + public PixelColorBlendingMode PixelColorBlendingMode { get; } + + /// + /// Gets the bounds of the layer geometry (device space). + /// + public RectangleF Bounds { get; } + + /// + /// Gets the semantic kind of the layer (for policy decisions). + /// + public GlyphLayerKind Kind { get; } + + internal static GlyphLayerInfo Transform(in GlyphLayerInfo info, Matrix4x4 matrix) + => new( + info.StartIndex, + info.Count, + info.Paint, + info.IntersectionRule, + info.PixelAlphaCompositionMode, + info.PixelColorBlendingMode, + RectangleF.Transform(info.Bounds, matrix), + info.Kind); + } +} diff --git a/ImageSharp.Drawing/Text/GlyphLayerKind.cs b/ImageSharp.Drawing/Text/GlyphLayerKind.cs new file mode 100644 index 0000000..476af95 --- /dev/null +++ b/ImageSharp.Drawing/Text/GlyphLayerKind.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Text { + /// + /// Optional semantic classification for layers to aid monochrome projection or decoration handling. + /// + public enum GlyphLayerKind + { + /// + /// Regular glyph geometry layer. + /// + Glyph = 0, + + /// + /// Text decoration geometry (underline/overline/strikethrough). + /// + Decoration = 1, + + /// + /// Painted layer (e.g. color emoji glyph). + /// + Painted = 2 + } +} diff --git a/ImageSharp.Drawing/Text/GlyphPathCollection.cs b/ImageSharp.Drawing/Text/GlyphPathCollection.cs new file mode 100644 index 0000000..20c7fc8 --- /dev/null +++ b/ImageSharp.Drawing/Text/GlyphPathCollection.cs @@ -0,0 +1,186 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Numerics; +using SixLabors.Fonts.Rendering; + +namespace SixLabors.ImageSharp.Drawing.Text { + /// + /// A geometry + paint container for a single glyph, preserving painted layer boundaries. + /// + public sealed class GlyphPathCollection + { + private readonly List paths; + private readonly ReadOnlyCollection readOnlyPaths; + private readonly List layers; + private readonly ReadOnlyCollection readOnlyLayers; + + /// + /// Initializes a new instance of the class. + /// + /// All paths emitted for the glyph in z-order. + /// Layer descriptors referring to spans within . + internal GlyphPathCollection(List paths, List layers) + { + Guard.NotNull(paths, nameof(paths)); + Guard.NotNull(layers, nameof(layers)); + + this.paths = paths; + this.layers = layers; + + this.readOnlyPaths = new ReadOnlyCollection(this.paths); + this.readOnlyLayers = new ReadOnlyCollection(this.layers); + this.Paths = new PathCollection(this.paths); + } + + /// + /// Gets the flattened geometry for the glyph (all paths in z-order). + /// This is equivalent to concatenating all layer spans. + /// + public IPathCollection Paths { get; } + + /// + /// Gets a read-only view of all individual paths in z-order. + /// + public IReadOnlyList PathList => this.readOnlyPaths; + + /// + /// Gets a read-only list of layer descriptors preserving paint, fill rule and path spans. + /// + public IReadOnlyList Layers => this.readOnlyLayers; + + /// + /// Gets the number of layers. + /// + public int LayerCount => this.layers.Count; + + /// + /// Gets an axis-aligned bounding box of the entire glyph in device space. + /// + public RectangleF Bounds => this.Paths.Bounds; + + /// + /// Transforms the glyph using the specified matrix. + /// + /// The transform matrix. + /// + /// A new with the matrix applied to it. + /// + public GlyphPathCollection Transform(Matrix4x4 matrix) + { + List transformed = new(this.paths.Count); + + for (int i = 0; i < this.paths.Count; i++) + { + transformed.Add(this.paths[i].Transform(matrix)); + } + + List transformedLayers = new(this.layers.Count); + for (int i = 0; i < this.layers.Count; i++) + { + transformedLayers.Add(GlyphLayerInfo.Transform(this.layers[i], matrix)); + } + + return new GlyphPathCollection(transformed, transformedLayers); + } + + /// + /// Creates a containing only the paths from layers that + /// satisfy . Useful to project to monochrome. + /// + /// A filter deciding whether to keep a layer. + /// A new with the selected paths. + public PathCollection ToPathCollection(Func? predicate = null) + { + List kept = []; + for (int i = 0; i < this.layers.Count; i++) + { + GlyphLayerInfo li = this.layers[i]; + if (predicate?.Invoke(li) == false) + { + continue; + } + + int end = li.StartIndex + li.Count; + for (int p = li.StartIndex; p < end; p++) + { + kept.Add(this.paths[p]); + } + } + + return new PathCollection(kept); + } + + /// + /// Gets a view of a single layer's geometry. + /// + /// The zero-based layer index. + /// A path collection comprising only that layer's span. + public PathCollection GetLayerPaths(int layerIndex) + { + Guard.MustBeLessThan(layerIndex, this.layers.Count, nameof(layerIndex)); + + GlyphLayerInfo li = this.layers[layerIndex]; + List chunk = new(li.Count); + int end = li.StartIndex + li.Count; + for (int p = li.StartIndex; p < end; p++) + { + chunk.Add(this.paths[p]); + } + + return new PathCollection(chunk); + } + + /// + /// Builder used by glyph renderers to populate a . + /// + internal sealed class Builder + { + private readonly List paths = []; + private readonly List layers = []; + + /// + /// Adds a completed path to the collection (current z-order position). + /// + /// The path to add. + public void AddPath(IPath path) => this.paths.Add(path); + + /// + /// Adds a layer descriptor pointing at the most recently added paths. + /// + /// Start index within the path list (inclusive). + /// Number of paths belonging to this layer. + /// The paint for this layer (may be null for default). + /// The fill rule for this layer. + /// Optional cached bounds for this layer. + /// Optional semantic kind (eg. Decoration). + /// + /// Thrown if the specified span is out of range of the current path list. + /// + public void AddLayer( + int startIndex, + int count, + Paint? paint, + FillRule fillRule, + RectangleF bounds, + GlyphLayerKind kind = GlyphLayerKind.Glyph) + { + if (startIndex < 0 || count < 0 || startIndex + count > this.paths.Count) + { + throw new ArgumentOutOfRangeException(nameof(count), "Layer span is out of range of the current path list."); + } + + this.layers.Add(new GlyphLayerInfo(startIndex, count, paint, fillRule, bounds, kind)); + } + + /// + /// Builds the immutable . + /// + /// The collection. + public GlyphPathCollection Build() => new(this.paths, this.layers); + } + } +} diff --git a/ImageSharp.Drawing/Text/PathGlyphBuilder.cs b/ImageSharp.Drawing/Text/PathGlyphBuilder.cs new file mode 100644 index 0000000..fa645c4 --- /dev/null +++ b/ImageSharp.Drawing/Text/PathGlyphBuilder.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.Fonts; +using SixLabors.Fonts.Rendering; + +namespace SixLabors.ImageSharp.Drawing.Text { + /// + /// A rendering surface that Fonts can use to generate shapes by following a path. + /// Each glyph is positioned along the path and rotated to match the path tangent + /// at the glyph's horizontal center. + /// + internal sealed class PathGlyphBuilder : GlyphBuilder + { + /// + /// The path that glyphs are laid out along. Exposed as + /// to access the method for efficient + /// position + tangent queries. + /// + private readonly IPathInternals path; + + /// + /// Initializes a new instance of the class. + /// + /// The path to render the glyphs along. + public PathGlyphBuilder(IPath path) + { + if (path is IPathInternals internals) + { + this.path = internals; + } + else + { + // Wrap in ComplexPolygon to gain IPathInternals. + this.path = new ComplexPolygon(path); + } + } + + /// + protected override bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) + { + // Translate + rotate the glyph to follow the path. Always returns true because + // path-based glyphs are never cached (each has a unique per-position transform). + this.TransformGlyph(in bounds); + return true; + } + + /// + /// Computes the translation + rotation matrix that places a glyph along the path. + /// The glyph's horizontal center is mapped to the path distance, and the glyph + /// is rotated to match the path tangent at that point. + /// + /// The font-metric bounding rectangle of the glyph. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void TransformGlyph(in FontRectangle bounds) + { + // Query the path at the glyph's horizontal center. + Vector2 half = new(bounds.Width * .5F, 0); + SegmentInfo pathPoint = this.path.PointAlongPath(bounds.Left + half.X); + + // Translate so the glyph's top-left aligns with the path point, + // then rotate around the path point to follow the tangent. + Vector2 translation = (Vector2)pathPoint.Point - bounds.Location - half + new Vector2(0, bounds.Top); + Matrix4x4 matrix = Matrix4x4.CreateTranslation(translation.X, translation.Y, 0) * new Matrix4x4(Matrix3x2.CreateRotation(pathPoint.Angle - MathF.PI, (Vector2)pathPoint.Point)); + + this.Builder.SetTransform(matrix); + } + } +} diff --git a/ImageSharp.Drawing/Text/TextBuilder.cs b/ImageSharp.Drawing/Text/TextBuilder.cs new file mode 100644 index 0000000..90d4cb0 --- /dev/null +++ b/ImageSharp.Drawing/Text/TextBuilder.cs @@ -0,0 +1,106 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Numerics; +using SixLabors.Fonts; +using SixLabors.Fonts.Rendering; + +namespace SixLabors.ImageSharp.Drawing.Text { + /// + /// Builds vector shapes from text using the provided layout and rendering options. + /// + public static class TextBuilder + { + /// + /// Generates the combined outline paths for all rendered glyphs in . + /// The result merges per-glyph outlines into a single suitable for filling or stroking as one unit. + /// + /// The text to shape and render. + /// The text rendering and layout options. + /// The combined for the rendered glyphs. + public static IPathCollection GeneratePaths(string text, TextOptions textOptions) + { + GlyphBuilder glyphBuilder = new(); + TextRenderer renderer = new(glyphBuilder); + + renderer.RenderText(text, textOptions); + + return glyphBuilder.Paths; + } + + /// + /// Generates per-glyph path data and metadata for the rendered . + /// Each entry contains the combined outline paths for a glyph and associated metadata that enables intelligent fill or stroke decisions at the glyph level. + /// + /// The text to shape and render. + /// The text rendering and layout options. + /// A read-only list of entries, one for each rendered glyph. + public static IReadOnlyList GenerateGlyphs(string text, TextOptions textOptions) + { + GlyphBuilder glyphBuilder = new(); + TextRenderer renderer = new(glyphBuilder); + + renderer.RenderText(text, textOptions); + + return glyphBuilder.Glyphs; + } + + /// + /// Generates the combined outline paths for all rendered glyphs in , + /// laid out along the supplied baseline. + /// The result merges per-glyph outlines into a single . + /// + /// The text to shape and render. + /// The path that defines the text baseline. + /// The text rendering and layout options. + /// The combined for the rendered glyphs. + public static IPathCollection GeneratePaths(string text, IPath path, TextOptions textOptions) + { + (IPath Path, TextOptions TextOptions) transformed = ConfigureOptions(textOptions, path); + PathGlyphBuilder glyphBuilder = new(transformed.Path); + TextRenderer renderer = new(glyphBuilder); + + renderer.RenderText(text, transformed.TextOptions); + + return glyphBuilder.Paths; + } + + /// + /// Generates per-glyph path data and metadata for the rendered , + /// laid out along the supplied baseline. + /// Each entry contains the combined outline paths for a glyph and associated metadata. + /// + /// The text to shape and render. + /// The path that defines the text baseline. + /// The text rendering and layout options. + /// A read-only list of entries, one for each rendered glyph. + public static IReadOnlyList GenerateGlyphs(string text, IPath path, TextOptions textOptions) + { + (IPath Path, TextOptions TextOptions) transformed = ConfigureOptions(textOptions, path); + PathGlyphBuilder glyphBuilder = new(transformed.Path); + TextRenderer renderer = new(glyphBuilder); + + renderer.RenderText(text, transformed.TextOptions); + + return glyphBuilder.Glyphs; + } + + private static (IPath Path, TextOptions TextOptions) ConfigureOptions(TextOptions options, IPath path) + { + // When a path is specified we should explicitly follow that path + // and not adjust the origin. Any translation should be applied to the path. + if (options.Origin != Vector2.Zero) + { + TextOptions clone = new(options) + { + Origin = Vector2.Zero + }; + + return (path.Translate(options.Origin), clone); + } + + return (path, options); + } + } +} diff --git a/ImageSharp.Drawing/Text/TextUtilities.cs b/ImageSharp.Drawing/Text/TextUtilities.cs new file mode 100644 index 0000000..41fe12c --- /dev/null +++ b/ImageSharp.Drawing/Text/TextUtilities.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Rendering; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Text { + internal static class TextUtilities + { + public static IntersectionRule MapFillRule(FillRule fillRule) + => fillRule switch + { + FillRule.EvenOdd => IntersectionRule.EvenOdd, + FillRule.NonZero => IntersectionRule.NonZero, + _ => IntersectionRule.NonZero, + }; + + public static PixelAlphaCompositionMode MapCompositionMode(CompositeMode mode) + => mode switch + { + CompositeMode.Clear => PixelAlphaCompositionMode.Clear, + CompositeMode.Src => PixelAlphaCompositionMode.Src, + CompositeMode.Dest => PixelAlphaCompositionMode.Dest, + CompositeMode.SrcOver => PixelAlphaCompositionMode.SrcOver, + CompositeMode.DestOver => PixelAlphaCompositionMode.DestOver, + CompositeMode.SrcIn => PixelAlphaCompositionMode.SrcIn, + CompositeMode.DestIn => PixelAlphaCompositionMode.DestIn, + CompositeMode.SrcOut => PixelAlphaCompositionMode.SrcOut, + CompositeMode.DestOut => PixelAlphaCompositionMode.DestOut, + CompositeMode.SrcAtop => PixelAlphaCompositionMode.SrcAtop, + CompositeMode.DestAtop => PixelAlphaCompositionMode.DestAtop, + CompositeMode.Xor => PixelAlphaCompositionMode.Xor, + _ => PixelAlphaCompositionMode.SrcOver, + }; + + public static PixelColorBlendingMode MapBlendingMode(CompositeMode mode) + => mode switch + { + CompositeMode.Plus => PixelColorBlendingMode.Add, + CompositeMode.Screen => PixelColorBlendingMode.Screen, + CompositeMode.Overlay => PixelColorBlendingMode.Overlay, + CompositeMode.Darken => PixelColorBlendingMode.Darken, + CompositeMode.Lighten => PixelColorBlendingMode.Lighten, + CompositeMode.HardLight => PixelColorBlendingMode.HardLight, + CompositeMode.Multiply => PixelColorBlendingMode.Multiply, + + // TODO: We do not support the following separate alpha blending modes: + // - ColorDodge, ColorBurn, SoftLight, Difference, Exclusion + // TODO: We do not support the non-alpha blending modes. + // - Hue, Saturation, Color, Luminosity + _ => PixelColorBlendingMode.Normal + }; + + public static DrawingOptions CloneOrReturnForRules( + this DrawingOptions drawingOptions, + IntersectionRule intersectionRule, + PixelAlphaCompositionMode compositionMode, + PixelColorBlendingMode colorBlendingMode) + { + if (drawingOptions.ShapeOptions.IntersectionRule == intersectionRule && + drawingOptions.GraphicsOptions.AlphaCompositionMode == compositionMode && + drawingOptions.GraphicsOptions.ColorBlendingMode == colorBlendingMode) + { + return drawingOptions; + } + + ShapeOptions shapeOptions = drawingOptions.ShapeOptions.DeepClone(); + shapeOptions.IntersectionRule = intersectionRule; + + GraphicsOptions graphicsOptions = drawingOptions.GraphicsOptions.DeepClone(); + graphicsOptions.AlphaCompositionMode = compositionMode; + graphicsOptions.ColorBlendingMode = colorBlendingMode; + + return new DrawingOptions(graphicsOptions, shapeOptions, drawingOptions.Transform); + } + + public static GraphicsOptions CloneOrReturnForRules( + this GraphicsOptions graphicsOptions, + PixelAlphaCompositionMode compositionMode, + PixelColorBlendingMode colorBlendingMode) + { + if (graphicsOptions.AlphaCompositionMode == compositionMode && + graphicsOptions.ColorBlendingMode == colorBlendingMode) + { + return graphicsOptions; + } + + GraphicsOptions clone = graphicsOptions.DeepClone(); + clone.AlphaCompositionMode = compositionMode; + clone.ColorBlendingMode = colorBlendingMode; + return clone; + } + } +} diff --git a/ImageSharp.sln b/ImageSharp.sln new file mode 100644 index 0000000..4db19b8 --- /dev/null +++ b/ImageSharp.sln @@ -0,0 +1,49 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.5.11716.220 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp", "ImageSharp\ImageSharp.csproj", "{98A4132B-831F-1B25-EE38-C7F74C821418}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing", "ImageSharp.Drawing\ImageSharp.Drawing.csproj", "{5C8FD0CD-B64B-D986-E79F-5D636E896775}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolygonClipper", "PolygonClipper\PolygonClipper.csproj", "{9948E74A-6B90-9064-B740-3D90961926CE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SixLabors.Fonts", "SixLabors.Fonts\SixLabors.Fonts.csproj", "{C14226BF-02E8-4477-F66E-F5CD9400E0D6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {98A4132B-831F-1B25-EE38-C7F74C821418}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98A4132B-831F-1B25-EE38-C7F74C821418}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98A4132B-831F-1B25-EE38-C7F74C821418}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98A4132B-831F-1B25-EE38-C7F74C821418}.Release|Any CPU.Build.0 = Release|Any CPU + {5C8FD0CD-B64B-D986-E79F-5D636E896775}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C8FD0CD-B64B-D986-E79F-5D636E896775}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C8FD0CD-B64B-D986-E79F-5D636E896775}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C8FD0CD-B64B-D986-E79F-5D636E896775}.Release|Any CPU.Build.0 = Release|Any CPU + {9948E74A-6B90-9064-B740-3D90961926CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9948E74A-6B90-9064-B740-3D90961926CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9948E74A-6B90-9064-B740-3D90961926CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9948E74A-6B90-9064-B740-3D90961926CE}.Release|Any CPU.Build.0 = Release|Any CPU + {C14226BF-02E8-4477-F66E-F5CD9400E0D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C14226BF-02E8-4477-F66E-F5CD9400E0D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C14226BF-02E8-4477-F66E-F5CD9400E0D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C14226BF-02E8-4477-F66E-F5CD9400E0D6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D34064B0-6AAD-4904-B943-30C56518B9A3} + EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + SharedInfrastructure.projitems*{5c8fd0cd-b64b-d986-e79f-5d636e896775}*SharedItemsImports = 5 + SharedInfrastructure.projitems*{98a4132b-831f-1b25-ee38-c7f74c821418}*SharedItemsImports = 5 + SharedInfrastructure.projitems*{9948e74a-6b90-9064-b740-3d90961926ce}*SharedItemsImports = 5 + SharedInfrastructure.projitems*{c14226bf-02e8-4477-f66e-f5cd9400e0d6}*SharedItemsImports = 5 + EndGlobalSection +EndGlobal diff --git a/ImageSharp/Advanced/AdvancedImageExtensions.cs b/ImageSharp/Advanced/AdvancedImageExtensions.cs new file mode 100644 index 0000000..132a698 --- /dev/null +++ b/ImageSharp/Advanced/AdvancedImageExtensions.cs @@ -0,0 +1,160 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Advanced { + /// + /// Extension methods over Image{TPixel} + /// + public static class AdvancedImageExtensions + { + /// + /// For a given file path find the best encoder to use via its extension. + /// + /// The source image. + /// The target file path to save the image to. + /// The matching . + /// The file path is null. + /// No encoder available for provided path. + public static IImageEncoder DetectEncoder(this Image source, string filePath) + { + Guard.NotNull(filePath, nameof(filePath)); + + String ext = Path.GetExtension(filePath); + if (!source.Configuration.ImageFormatsManager.TryFindFormatByFileExtension(ext, out IImageFormat? format)) + { + StringBuilder sb = new(); + sb = sb.AppendLine(CultureInfo.InvariantCulture, $"No encoder was found for extension '{ext}'. Registered encoders include:"); + foreach (IImageFormat fmt in source.Configuration.ImageFormats) + { + sb = sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", fmt.Name, string.Join(", ", fmt.FileExtensions), Environment.NewLine); + } + + throw new UnknownImageFormatException(sb.ToString()); + } + + IImageEncoder? encoder = source.Configuration.ImageFormatsManager.GetEncoder(format); + + if (encoder is null) + { + StringBuilder sb = new(); + sb = sb.AppendLine(CultureInfo.InvariantCulture, $"No encoder was found for extension '{ext}' using image format '{format.Name}'. Registered encoders include:"); + foreach (KeyValuePair enc in source.Configuration.ImageFormatsManager.ImageEncoders) + { + sb = sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", enc.Key, enc.Value.GetType().Name, Environment.NewLine); + } + + throw new UnknownImageFormatException(sb.ToString()); + } + + return encoder; + } + + /// + /// Accepts a to implement a double-dispatch pattern in order to + /// apply pixel-specific operations on non-generic instances + /// + /// The source image. + /// The image visitor. + public static void AcceptVisitor(this Image source, IImageVisitor visitor) + => source.Accept(visitor); + + /// + /// Accepts a to implement a double-dispatch pattern in order to + /// apply pixel-specific operations on non-generic instances + /// + /// The source image. + /// The image visitor. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + public static Task AcceptVisitorAsync(this Image source, IImageVisitorAsync visitor, CancellationToken cancellationToken = default) + => source.AcceptAsync(visitor, cancellationToken); + + /// + /// Accepts a to implement a double-dispatch pattern in order to + /// apply pixel-specific operations on non-generic instances + /// + /// The source image frame. + /// The image visitor. + public static void AcceptVisitor(this ImageFrame source, IImageFrameVisitor visitor) + => source.Accept(visitor); + + /// + /// Gets the representation of the pixels as a containing the backing pixel data of the image + /// stored in row major order, as a list of contiguous blocks in the source image's pixel format. + /// + /// The source image. + /// The type of the pixel. + /// The . + /// + /// Certain Image Processors may invalidate the returned and all it's buffers, + /// therefore it's not recommended to mutate the image while holding a reference to it's . + /// + /// Thrown when the in . + public static IMemoryGroup GetPixelMemoryGroup(this ImageFrame source) + where TPixel : unmanaged, IPixel + => source?.PixelBuffer.FastMemoryGroup.View ?? throw new ArgumentNullException(nameof(source)); + + /// + /// Gets the representation of the pixels as a containing the backing pixel data of the image + /// stored in row major order, as a list of contiguous blocks in the source image's pixel format. + /// + /// The source image. + /// The type of the pixel. + /// The . + /// + /// Certain Image Processors may invalidate the returned and all it's buffers, + /// therefore it's not recommended to mutate the image while holding a reference to it's . + /// + /// Thrown when the in . + public static IMemoryGroup GetPixelMemoryGroup(this Image source) + where TPixel : unmanaged, IPixel + => source?.Frames.RootFrame.GetPixelMemoryGroup() ?? throw new ArgumentNullException(nameof(source)); + + /// + /// Gets the representation of the pixels as a of contiguous memory + /// at row beginning from the first pixel on that row. + /// + /// The type of the pixel. + /// The source. + /// The row. + /// The + public static Memory DangerousGetPixelRowMemory(this ImageFrame source, int rowIndex) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(source, nameof(source)); + Guard.MustBeGreaterThanOrEqualTo(rowIndex, 0, nameof(rowIndex)); + Guard.MustBeLessThan(rowIndex, source.Height, nameof(rowIndex)); + + return source.PixelBuffer.GetSafeRowMemory(rowIndex); + } + + /// + /// Gets the representation of the pixels as of contiguous memory + /// at row beginning from the first pixel on that row. + /// + /// The type of the pixel. + /// The source. + /// The row. + /// The + public static Memory DangerousGetPixelRowMemory(this Image source, int rowIndex) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(source, nameof(source)); + Guard.MustBeGreaterThanOrEqualTo(rowIndex, 0, nameof(rowIndex)); + Guard.MustBeLessThan(rowIndex, source.Height, nameof(rowIndex)); + + return source.Frames.RootFrame.PixelBuffer.GetSafeRowMemory(rowIndex); + } + } +} diff --git a/ImageSharp/Advanced/AotCompilerTools.cs b/ImageSharp/Advanced/AotCompilerTools.cs new file mode 100644 index 0000000..5137b6c --- /dev/null +++ b/ImageSharp/Advanced/AotCompilerTools.cs @@ -0,0 +1,598 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Jpeg.Components; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using SixLabors.ImageSharp.Formats.Pbm; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Qoi; +using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors; +using SixLabors.ImageSharp.Processing.Processors.Binarization; +using SixLabors.ImageSharp.Processing.Processors.Convolution; +using SixLabors.ImageSharp.Processing.Processors.Dithering; +using SixLabors.ImageSharp.Processing.Processors.Drawing; +using SixLabors.ImageSharp.Processing.Processors.Effects; +using SixLabors.ImageSharp.Processing.Processors.Filters; +using SixLabors.ImageSharp.Processing.Processors.Normalization; +using SixLabors.ImageSharp.Processing.Processors.Overlays; +using SixLabors.ImageSharp.Processing.Processors.Quantization; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Advanced { + /// + /// Unlike traditional Mono/.NET, code on the iPhone is statically compiled ahead of time instead of being + /// compiled on demand by a JIT compiler. This means there are a few limitations with respect to generics, + /// these are caused because not every possible generic instantiation can be determined up front at compile time. + /// The Aot Compiler is designed to overcome the limitations of this compiler. + /// None of the methods in this class should ever be called, the code only has to exist at compile-time to be picked up by the AoT compiler. + /// (Very similar to the LinkerIncludes.cs technique used in Xamarin.Android projects.) + /// + [ExcludeFromCodeCoverage] + internal static class AotCompilerTools + { + /// + /// This is the method that seeds the AoT compiler. + /// None of these seed methods needs to actually be called to seed the compiler. + /// The calls just need to be present when the code is compiled, and each implementation will be built. + /// + /// + /// This method doesn't actually do anything but serves an important purpose... + /// If you are running ImageSharp on iOS and try to call SaveAsGif, it will throw an exception: + /// "Attempting to JIT compile method... HexadecatreeQuantizer.ConstructPalette... while running in aot-only mode." + /// The reason this happens is the SaveAsGif method makes heavy use of generics, which are too confusing for the AoT + /// compiler used on Xamarin.iOS. It spins up the JIT compiler to try and figure it out, but that is an illegal op on + /// iOS so it bombs out. + /// If you are getting the above error, you need to call this method, which will pre-seed the AoT compiler with the + /// necessary methods to complete the SaveAsGif call. That's it, otherwise you should NEVER need this method!!! + /// + /// + /// This method is used for AOT code generation only. Do not call it at runtime. + /// + [Preserve] + private static void SeedPixelFormats() + { + try + { + Unsafe.SizeOf(); + Unsafe.SizeOf(); + Unsafe.SizeOf(); + Unsafe.SizeOf(); + Unsafe.SizeOf(); + Unsafe.SizeOf(); + Unsafe.SizeOf(); + Unsafe.SizeOf(); + Unsafe.SizeOf(); + + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + Seed(); + } + catch + { + // nop + } + + throw new InvalidOperationException("This method is used for AOT code generation only. Do not call it at runtime."); + } + + /// + /// Seeds the compiler using the given pixel format. + /// + /// The pixel format. + [Preserve] + private static void Seed() + where TPixel : unmanaged, IPixel + { + // This is we actually call all the individual methods you need to seed. + AotCompileImage(); + AotCompileImageProcessingContextFactory(); + AotCompileImageEncoderInternals(); + AotCompileImageDecoderInternals(); + AotCompileImageEncoders(); + AotCompileImageDecoders(); + AotCompileSpectralConverter(); + AotCompileImageProcessors(); + AotCompileGenericImageProcessors(); + AotCompileResamplers(); + AotCompileQuantizers(); + AotCompilePixelSamplingStrategys(); + AotCompilePixelMaps(); + AotCompileDithers(); + AotCompileMemoryManagers(); + + _ = Unsafe.SizeOf(); + + // TODO: Do the discovery work to figure out what works and what doesn't. + } + + /// + /// This method pre-seeds the for a given pixel format in the AoT compiler. + /// + /// The pixel format. + [Preserve] + private static unsafe void AotCompileImage() + where TPixel : unmanaged, IPixel + { + Image img = default; + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); + + ImageFrame.LoadPixelData(default, default(ReadOnlySpan), default, default); + ImageFrame.LoadPixelData(default, default(ReadOnlySpan), default, default); + } + + /// + /// This method pre-seeds the all in the AoT compiler. + /// + /// The pixel format. + [Preserve] + private static void AotCompileImageProcessingContextFactory() + where TPixel : unmanaged, IPixel + => default(DefaultImageOperationsProviderFactory).CreateImageProcessingContext(default, default, default); + + /// + /// This method pre-seeds the all core encoders in the AoT compiler. + /// + /// The pixel format. + [Preserve] + private static void AotCompileImageEncoderInternals() + where TPixel : unmanaged, IPixel + { + default(BmpEncoderCore).Encode(default, default, default); + default(GifEncoderCore).Encode(default, default, default); + default(JpegEncoderCore).Encode(default, default, default); + default(PbmEncoderCore).Encode(default, default, default); + default(PngEncoderCore).Encode(default, default, default); + default(QoiEncoderCore).Encode(default, default, default); + default(TgaEncoderCore).Encode(default, default, default); + default(TiffEncoderCore).Encode(default, default, default); + default(WebpEncoderCore).Encode(default, default, default); + } + + /// + /// This method pre-seeds the all in the AoT compiler. + /// + /// The pixel format. + [Preserve] + private static void AotCompileImageDecoderInternals() + where TPixel : unmanaged, IPixel + { + default(BmpDecoderCore).Decode(default, default, default); + default(GifDecoderCore).Decode(default, default, default); + default(JpegDecoderCore).Decode(default, default, default); + default(PbmDecoderCore).Decode(default, default, default); + default(PngDecoderCore).Decode(default, default, default); + default(QoiDecoderCore).Decode(default, default, default); + default(TgaDecoderCore).Decode(default, default, default); + default(TiffDecoderCore).Decode(default, default, default); + default(WebpDecoderCore).Decode(default, default, default); + } + + /// + /// This method pre-seeds the all in the AoT compiler. + /// + /// The pixel format. + [Preserve] + private static void AotCompileImageEncoders() + where TPixel : unmanaged, IPixel + { + AotCompileImageEncoder(); + AotCompileImageEncoder(); + AotCompileImageEncoder(); + AotCompileImageEncoder(); + AotCompileImageEncoder(); + AotCompileImageEncoder(); + AotCompileImageEncoder(); + AotCompileImageEncoder(); + } + + /// + /// This method pre-seeds the all in the AoT compiler. + /// + /// The pixel format. + [Preserve] + private static void AotCompileImageDecoders() + where TPixel : unmanaged, IPixel + { + AotCompileImageDecoder(); + AotCompileImageDecoder(); + AotCompileImageDecoder(); + AotCompileImageDecoder(); + AotCompileImageDecoder(); + AotCompileImageDecoder(); + AotCompileImageDecoder(); + AotCompileImageDecoder(); + } + + [Preserve] + private static void AotCompileSpectralConverter() + where TPixel : unmanaged, IPixel + { + default(SpectralConverter).GetPixelBuffer(default, default); + default(GrayJpegSpectralConverter).GetPixelBuffer(default, default); + default(RgbJpegSpectralConverter).GetPixelBuffer(default, default); + default(TiffJpegSpectralConverter).GetPixelBuffer(default, default); + default(TiffOldJpegSpectralConverter).GetPixelBuffer(default, default); + } + + /// + /// This method pre-seeds the in the AoT compiler. + /// + /// The pixel format. + /// The encoder. + [Preserve] + private static void AotCompileImageEncoder() + where TPixel : unmanaged, IPixel + where TEncoder : class, IImageEncoder + { + default(TEncoder).Encode(default, default); + default(TEncoder).EncodeAsync(default, default, default); + } + + /// + /// This method pre-seeds the in the AoT compiler. + /// + /// The pixel format. + /// The decoder. + [Preserve] + private static void AotCompileImageDecoder() + where TPixel : unmanaged, IPixel + where TDecoder : class, IImageDecoder + => default(TDecoder).Decode(default, default); + + /// + /// This method pre-seeds the all in the AoT compiler. + /// + /// + /// There is no structure that implements ISwizzler. + /// + /// The pixel format. + [Preserve] + private static void AotCompileImageProcessors() + where TPixel : unmanaged, IPixel + { + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + AotCompileImageProcessor(); + + AotCompilerCloningImageProcessor(); + AotCompilerCloningImageProcessor(); + AotCompilerCloningImageProcessor(); + AotCompilerCloningImageProcessor(); + AotCompilerCloningImageProcessor(); + AotCompilerCloningImageProcessor(); + AotCompilerCloningImageProcessor(); + } + + /// + /// This method pre-seeds the in the AoT compiler. + /// + /// The pixel format. + /// The processor type + [Preserve] + private static void AotCompileImageProcessor() + where TPixel : unmanaged, IPixel + where TProc : class, IImageProcessor + => default(TProc).CreatePixelSpecificProcessor(default, default, default); + + /// + /// This method pre-seeds the in the AoT compiler. + /// + /// The pixel format. + /// The processor type + [Preserve] + private static void AotCompilerCloningImageProcessor() + where TPixel : unmanaged, IPixel + where TProc : class, ICloningImageProcessor + => default(TProc).CreatePixelSpecificCloningProcessor(default, default, default); + + /// + /// This method pre-seeds the all in the AoT compiler. + /// + /// + /// There is no structure that implements ISwizzler. + /// + /// The pixel format. + [Preserve] + private static void AotCompileGenericImageProcessors() + where TPixel : unmanaged, IPixel + { + AotCompileGenericCloningImageProcessor>(); + AotCompileGenericCloningImageProcessor>(); + AotCompileGenericCloningImageProcessor>(); + AotCompileGenericCloningImageProcessor>(); + AotCompileGenericCloningImageProcessor>(); + } + + /// + /// This method pre-seeds the in the AoT compiler. + /// + /// The pixel format. + /// The processor type + [Preserve] + private static void AotCompileGenericCloningImageProcessor() + where TPixel : unmanaged, IPixel + where TProc : class, ICloningImageProcessor + => default(TProc).CloneAndExecute(); + + /// + /// This method pre-seeds the all in the AoT compiler. + /// + /// The pixel format. + [Preserve] + private static void AotCompileResamplers() + where TPixel : unmanaged, IPixel + { + AotCompileResampler(); + AotCompileResampler(); + AotCompileResampler(); + AotCompileResampler(); + AotCompileResampler(); + AotCompileResampler(); + AotCompileResampler(); + } + + /// + /// This method pre-seeds the in the AoT compiler. + /// + /// The pixel format. + /// The processor type + [Preserve] + private static void AotCompileResampler() + where TPixel : unmanaged, IPixel + where TResampler : struct, IResampler + { + default(TResampler).ApplyTransform(default); + + default(AffineTransformProcessor).ApplyTransform(default); + default(ProjectiveTransformProcessor).ApplyTransform(default); + default(ResizeProcessor).ApplyTransform(default); + default(RotateProcessor).ApplyTransform(default); + } + + /// + /// This method pre-seeds the all in the AoT compiler. + /// + /// The pixel format. + [Preserve] + private static void AotCompileQuantizers() + where TPixel : unmanaged, IPixel + { + AotCompileQuantizer(); + AotCompileQuantizer(); + AotCompileQuantizer(); + AotCompileQuantizer(); + AotCompileQuantizer(); + } + + /// + /// This method pre-seeds the in the AoT compiler. + /// + /// The pixel format. + /// The quantizer type + [Preserve] + private static void AotCompileQuantizer() + where TPixel : unmanaged, IPixel + + where TQuantizer : class, IQuantizer + { + default(TQuantizer).CreatePixelSpecificQuantizer(default); + default(TQuantizer).CreatePixelSpecificQuantizer(default, default); + } + + /// + /// This method pre-seeds the in the AoT compiler. + /// + /// The pixel format. + [Preserve] + private static void AotCompilePixelSamplingStrategys() + where TPixel : unmanaged, IPixel + { + default(DefaultPixelSamplingStrategy).EnumeratePixelRegions(default(Image)); + default(DefaultPixelSamplingStrategy).EnumeratePixelRegions(default(ImageFrame)); + default(ExtensivePixelSamplingStrategy).EnumeratePixelRegions(default(Image)); + default(ExtensivePixelSamplingStrategy).EnumeratePixelRegions(default(ImageFrame)); + } + + /// + /// This method pre-seeds the all in the AoT compiler. + /// + /// The pixel format. + [Preserve] + private static void AotCompilePixelMaps() + where TPixel : unmanaged, IPixel + { + default(EuclideanPixelMap).GetClosestColor(default, out _); + default(EuclideanPixelMap).GetClosestColor(default, out _); + } + + /// + /// This method pre-seeds the all in the AoT compiler. + /// + /// The pixel format. + [Preserve] + private static void AotCompileDithers() + where TPixel : unmanaged, IPixel + { + AotCompileDither(); + AotCompileDither(); + } + + /// + /// This method pre-seeds the in the AoT compiler. + /// + /// The pixel format. + /// The dither. + [Preserve] + private static void AotCompileDither() + where TPixel : unmanaged, IPixel + where TDither : struct, IDither + { + HexadecatreeQuantizer hexadecatree = default; + default(TDither).ApplyQuantizationDither, TPixel>(ref hexadecatree, default, default, default); + + PaletteQuantizer palette = default; + default(TDither).ApplyQuantizationDither, TPixel>(ref palette, default, default, default); + + WuQuantizer wu = default; + default(TDither).ApplyQuantizationDither, TPixel>(ref wu, default, default, default); + default(TDither).ApplyPaletteDither.DitherProcessor, TPixel>(default, default, default); + } + + /// + /// This method pre-seeds the all in the AoT compiler. + /// + /// The pixel format. + [Preserve] + private static void AotCompileMemoryManagers() + where TPixel : unmanaged, IPixel + { + AotCompileMemoryManager(); + AotCompileMemoryManager(); + } + + /// + /// This method pre-seeds the in the AoT compiler. + /// + /// The pixel format. + /// The buffer. + [Preserve] + private static void AotCompileMemoryManager() + where TPixel : unmanaged, IPixel + where TBuffer : MemoryAllocator + { + default(TBuffer).Allocate(default, default); + default(TBuffer).Allocate(default, default); + default(TBuffer).Allocate(default, default); + default(TBuffer).Allocate(default, default); + default(TBuffer).Allocate(default, default); + default(TBuffer).Allocate(default, default); + default(TBuffer).Allocate(default, default); + default(TBuffer).Allocate(default, default); + default(TBuffer).Allocate(default, default); + default(TBuffer).Allocate(default, default); + default(TBuffer).Allocate(default, default); + } + } +} diff --git a/ImageSharp/Advanced/IConfigurationProvider.cs b/ImageSharp/Advanced/IConfigurationProvider.cs new file mode 100644 index 0000000..5152e3c --- /dev/null +++ b/ImageSharp/Advanced/IConfigurationProvider.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Advanced { + /// + /// Defines the contract for objects that can provide access to configuration. + /// + public interface IConfigurationProvider + { + /// + /// Gets the configuration which allows altering default behaviour or extending the library. + /// + Configuration Configuration { get; } + } +} diff --git a/ImageSharp/Advanced/IImageFrameVisitor.cs b/ImageSharp/Advanced/IImageFrameVisitor.cs new file mode 100644 index 0000000..16af0b2 --- /dev/null +++ b/ImageSharp/Advanced/IImageFrameVisitor.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Advanced { + /// + /// A visitor to implement a double-dispatch pattern in order to apply pixel-specific operations + /// on non-generic instances. + /// + public interface IImageFrameVisitor + { + /// + /// Provides a pixel-specific implementation for a given operation. + /// + /// The image frame. + /// The pixel type. + public void Visit(ImageFrame frame) + where TPixel : unmanaged, IPixel; + } +} diff --git a/ImageSharp/Advanced/IImageVisitor.cs b/ImageSharp/Advanced/IImageVisitor.cs new file mode 100644 index 0000000..bc848d7 --- /dev/null +++ b/ImageSharp/Advanced/IImageVisitor.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System.Threading; +using System.Threading.Tasks; + +namespace SixLabors.ImageSharp.Advanced { + /// + /// A visitor to implement a double-dispatch pattern in order to apply pixel-specific operations + /// on non-generic instances. + /// + public interface IImageVisitor + { + /// + /// Provides a pixel-specific implementation for a given operation. + /// + /// The image. + /// The pixel type. + public void Visit(Image image) + where TPixel : unmanaged, IPixel; + } + + /// + /// A visitor to implement a double-dispatch pattern in order to apply pixel-specific operations + /// on non-generic instances. + /// + public interface IImageVisitorAsync + { + /// + /// Provides a pixel-specific implementation for a given operation. + /// + /// The image. + /// The token to monitor for cancellation requests. + /// The pixel type. + /// A representing the asynchronous operation. + public Task VisitAsync(Image image, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel; + } +} diff --git a/ImageSharp/Advanced/IPixelSource.cs b/ImageSharp/Advanced/IPixelSource.cs new file mode 100644 index 0000000..e3934ec --- /dev/null +++ b/ImageSharp/Advanced/IPixelSource.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Advanced { + /// + /// Encapsulates the basic properties and methods required to manipulate images. + /// + internal interface IPixelSource + { + /// + /// Gets the pixel buffer. + /// + Buffer2D PixelBuffer { get; } + } + + /// + /// Encapsulates the basic properties and methods required to manipulate images. + /// + /// The type of the pixel. + internal interface IPixelSource + where TPixel : unmanaged, IPixel + { + /// + /// Gets the pixel buffer. + /// + Buffer2D PixelBuffer { get; } + } +} diff --git a/ImageSharp/Advanced/IRowIntervalOperation.cs b/ImageSharp/Advanced/IRowIntervalOperation.cs new file mode 100644 index 0000000..3cae8ff --- /dev/null +++ b/ImageSharp/Advanced/IRowIntervalOperation.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Advanced { + /// + /// Defines the contract for an action that operates on a row interval. + /// + public interface IRowIntervalOperation + { + /// + /// Invokes the method passing the row interval. + /// + /// The row interval. + void Invoke(in RowInterval rows); + } +} diff --git a/ImageSharp/Advanced/IRowIntervalOperation{TBuffer}.cs b/ImageSharp/Advanced/IRowIntervalOperation{TBuffer}.cs new file mode 100644 index 0000000..a78eeca --- /dev/null +++ b/ImageSharp/Advanced/IRowIntervalOperation{TBuffer}.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using System; + +namespace SixLabors.ImageSharp.Advanced { + /// + /// Defines the contract for an action that operates on a row interval with a temporary buffer. + /// + /// The type of buffer elements. + public interface IRowIntervalOperation + where TBuffer : unmanaged + { + /// + /// Return the minimal required number of items in the buffer passed on . + /// + /// The bounds of the operation. + /// The required buffer length. + int GetRequiredBufferLength(Rectangle bounds); + + /// + /// Invokes the method passing the row interval and a buffer. + /// + /// The row interval. + /// The contiguous region of memory. + void Invoke(in RowInterval rows, Span span); + } +} diff --git a/ImageSharp/Advanced/IRowOperation.cs b/ImageSharp/Advanced/IRowOperation.cs new file mode 100644 index 0000000..cff47b5 --- /dev/null +++ b/ImageSharp/Advanced/IRowOperation.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Advanced { + /// + /// Defines the contract for an action that operates on a row. + /// + public interface IRowOperation + { + /// + /// Invokes the method passing the row y coordinate. + /// + /// The row y coordinate. + void Invoke(int y); + } +} diff --git a/ImageSharp/Advanced/IRowOperation{TBuffer}.cs b/ImageSharp/Advanced/IRowOperation{TBuffer}.cs new file mode 100644 index 0000000..81610e3 --- /dev/null +++ b/ImageSharp/Advanced/IRowOperation{TBuffer}.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Advanced { + /// + /// Defines the contract for an action that operates on a row with a temporary buffer. + /// + /// The type of buffer elements. + public interface IRowOperation + where TBuffer : unmanaged + { + /// + /// Return the minimal required number of items in the buffer passed on . + /// + /// The bounds of the operation. + /// The required buffer length. + public Int32 GetRequiredBufferLength(Rectangle bounds); + + /// + /// Invokes the method passing the row and a buffer. + /// + /// The row y coordinate. + /// The contiguous region of memory. + public void Invoke(Int32 y, Span span); + } +} diff --git a/ImageSharp/Advanced/ParallelExecutionSettings.cs b/ImageSharp/Advanced/ParallelExecutionSettings.cs new file mode 100644 index 0000000..abad153 --- /dev/null +++ b/ImageSharp/Advanced/ParallelExecutionSettings.cs @@ -0,0 +1,103 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using System; + +namespace SixLabors.ImageSharp.Advanced { + /// + /// Defines execution settings for methods in . + /// + public readonly struct ParallelExecutionSettings + { + /// + /// Default value for . + /// + public const int DefaultMinimumPixelsProcessedPerTask = 4096; + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The value used for initializing when using TPL. + /// If set to -1, there is no limit on the number of concurrently running operations. + /// + /// The value for . + /// The . + public ParallelExecutionSettings( + int maxDegreeOfParallelism, + int minimumPixelsProcessedPerTask, + MemoryAllocator memoryAllocator) + { + // Shall be compatible with ParallelOptions.MaxDegreeOfParallelism: + // https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.paralleloptions.maxdegreeofparallelism + if (maxDegreeOfParallelism is 0 or < -1) + { + throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism)); + } + + Guard.MustBeGreaterThan(minimumPixelsProcessedPerTask, 0, nameof(minimumPixelsProcessedPerTask)); + Guard.NotNull(memoryAllocator, nameof(memoryAllocator)); + + this.MaxDegreeOfParallelism = maxDegreeOfParallelism; + this.MinimumPixelsProcessedPerTask = minimumPixelsProcessedPerTask; + this.MemoryAllocator = memoryAllocator; + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The value used for initializing when using TPL. + /// If set to -1, there is no limit on the number of concurrently running operations. + /// + /// The . + public ParallelExecutionSettings(int maxDegreeOfParallelism, MemoryAllocator memoryAllocator) + : this(maxDegreeOfParallelism, DefaultMinimumPixelsProcessedPerTask, memoryAllocator) + { + } + + /// + /// Gets the . + /// + public MemoryAllocator MemoryAllocator { get; } + + /// + /// Gets the value used for initializing when using TPL. + /// A value of -1 means there is no limit on the number of concurrently running operations. + /// + public int MaxDegreeOfParallelism { get; } + + /// + /// Gets the minimum number of pixels being processed by a single task when parallelizing operations with TPL. + /// Launching tasks for pixel regions below this limit is not worth the overhead. + /// Initialized with by default, + /// the optimum value is operation specific. (The cheaper the operation, the larger the value is.) + /// + public int MinimumPixelsProcessedPerTask { get; } + + /// + /// Creates a new instance of + /// having multiplied by + /// + /// The value to multiply with. + /// The modified . + public ParallelExecutionSettings MultiplyMinimumPixelsPerTask(int multiplier) + { + Guard.MustBeGreaterThan(multiplier, 0, nameof(multiplier)); + + return new ParallelExecutionSettings( + this.MaxDegreeOfParallelism, + this.MinimumPixelsProcessedPerTask * multiplier, + this.MemoryAllocator); + } + + /// + /// Get the default for a + /// + /// The . + /// The . + public static ParallelExecutionSettings FromConfiguration(Configuration configuration) + => new(configuration.MaxDegreeOfParallelism, configuration.MemoryAllocator); + } +} diff --git a/ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs b/ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs new file mode 100644 index 0000000..ced5263 --- /dev/null +++ b/ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs @@ -0,0 +1,196 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Advanced { + /// + /// Utility methods for batched processing of pixel row intervals. + /// Parallel execution is optimized for image processing based on values defined + /// or . + /// Using this class is preferred over direct usage of utility methods. + /// + public static partial class ParallelRowIterator + { + private readonly struct RowOperationWrapper + where T : struct, IRowOperation + { + private readonly int minY; + private readonly int maxY; + private readonly int stepY; + private readonly T action; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperationWrapper( + int minY, + int maxY, + int stepY, + in T action) + { + this.minY = minY; + this.maxY = maxY; + this.stepY = stepY; + this.action = action; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int i) + { + int yMin = this.minY + (i * this.stepY); + + if (yMin >= this.maxY) + { + return; + } + + int yMax = Math.Min(yMin + this.stepY, this.maxY); + + for (int y = yMin; y < yMax; y++) + { + // Skip the safety copy when invoking a potentially impure method on a readonly field + Unsafe.AsRef(in this.action).Invoke(y); + } + } + } + + private readonly struct RowOperationWrapper + where T : struct, IRowOperation + where TBuffer : unmanaged + { + private readonly int minY; + private readonly int maxY; + private readonly int stepY; + private readonly int bufferLength; + private readonly MemoryAllocator allocator; + private readonly T action; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperationWrapper( + int minY, + int maxY, + int stepY, + int bufferLength, + MemoryAllocator allocator, + in T action) + { + this.minY = minY; + this.maxY = maxY; + this.stepY = stepY; + this.bufferLength = bufferLength; + this.allocator = allocator; + this.action = action; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int i) + { + int yMin = this.minY + (i * this.stepY); + + if (yMin >= this.maxY) + { + return; + } + + int yMax = Math.Min(yMin + this.stepY, this.maxY); + + using IMemoryOwner buffer = this.allocator.Allocate(this.bufferLength); + + Span span = buffer.Memory.Span; + + for (int y = yMin; y < yMax; y++) + { + Unsafe.AsRef(in this.action).Invoke(y, span); + } + } + } + + private readonly struct RowIntervalOperationWrapper + where T : struct, IRowIntervalOperation + { + private readonly int minY; + private readonly int maxY; + private readonly int stepY; + private readonly T operation; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowIntervalOperationWrapper( + int minY, + int maxY, + int stepY, + in T operation) + { + this.minY = minY; + this.maxY = maxY; + this.stepY = stepY; + this.operation = operation; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int i) + { + int yMin = this.minY + (i * this.stepY); + + if (yMin >= this.maxY) + { + return; + } + + int yMax = Math.Min(yMin + this.stepY, this.maxY); + RowInterval rows = new(yMin, yMax); + + // Skip the safety copy when invoking a potentially impure method on a readonly field + Unsafe.AsRef(in this.operation).Invoke(in rows); + } + } + + private readonly struct RowIntervalOperationWrapper + where T : struct, IRowIntervalOperation + where TBuffer : unmanaged + { + private readonly int minY; + private readonly int maxY; + private readonly int stepY; + private readonly int bufferLength; + private readonly MemoryAllocator allocator; + private readonly T operation; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowIntervalOperationWrapper( + int minY, + int maxY, + int stepY, + int bufferLength, + MemoryAllocator allocator, + in T operation) + { + this.minY = minY; + this.maxY = maxY; + this.stepY = stepY; + this.bufferLength = bufferLength; + this.allocator = allocator; + this.operation = operation; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int i) + { + int yMin = this.minY + (i * this.stepY); + + if (yMin >= this.maxY) + { + return; + } + + int yMax = Math.Min(yMin + this.stepY, this.maxY); + RowInterval rows = new(yMin, yMax); + + using IMemoryOwner buffer = this.allocator.Allocate(this.bufferLength); + + Unsafe.AsRef(in this.operation).Invoke(in rows, buffer.Memory.Span); + } + } + } +} diff --git a/ImageSharp/Advanced/ParallelRowIterator.cs b/ImageSharp/Advanced/ParallelRowIterator.cs new file mode 100644 index 0000000..b052bba --- /dev/null +++ b/ImageSharp/Advanced/ParallelRowIterator.cs @@ -0,0 +1,316 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Advanced { + /// + /// Utility methods for batched processing of pixel row intervals. + /// Parallel execution is optimized for image processing based on values defined + /// or . + /// Using this class is preferred over direct usage of utility methods. + /// + public static partial class ParallelRowIterator + { + /// + /// Iterate through the rows of a rectangle in optimized batches. + /// + /// The type of row operation to perform. + /// The to get the parallel settings from. + /// The . + /// The operation defining the iteration logic on a single row. + [MethodImpl(InliningOptions.ShortMethod)] + public static void IterateRows(Configuration configuration, Rectangle rectangle, in T operation) + where T : struct, IRowOperation + { + ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration); + IterateRows(rectangle, in parallelSettings, in operation); + } + + /// + /// Iterate through the rows of a rectangle in optimized batches. + /// + /// The type of row operation to perform. + /// The . + /// The . + /// The operation defining the iteration logic on a single row. + public static void IterateRows( + Rectangle rectangle, + in ParallelExecutionSettings parallelSettings, + in T operation) + where T : struct, IRowOperation + { + ValidateRectangle(rectangle); + + int top = rectangle.Top; + int bottom = rectangle.Bottom; + int width = rectangle.Width; + int height = rectangle.Height; + + int numOfSteps = GetNumberOfSteps(width, height, parallelSettings); + + // Avoid TPL overhead in this trivial case: + if (numOfSteps == 1) + { + for (int y = top; y < bottom; y++) + { + Unsafe.AsRef(in operation).Invoke(y); + } + + return; + } + + int verticalStep = DivideCeil(rectangle.Height, numOfSteps); + ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps); + RowOperationWrapper wrappingOperation = new(top, bottom, verticalStep, in operation); + + _ = Parallel.For( + 0, + numOfSteps, + parallelOptions, + wrappingOperation.Invoke); + } + + /// + /// Iterate through the rows of a rectangle in optimized batches. + /// instantiating a temporary buffer for each invocation. + /// + /// The type of row operation to perform. + /// The type of buffer elements. + /// The to get the parallel settings from. + /// The . + /// The operation defining the iteration logic on a single row. + public static void IterateRows(Configuration configuration, Rectangle rectangle, in T operation) + where T : struct, IRowOperation + where TBuffer : unmanaged + { + ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration); + IterateRows(rectangle, in parallelSettings, in operation); + } + + /// + /// Iterate through the rows of a rectangle in optimized batches. + /// instantiating a temporary buffer for each invocation. + /// + /// The type of row operation to perform. + /// The type of buffer elements. + /// The . + /// The . + /// The operation defining the iteration logic on a single row. + public static void IterateRows( + Rectangle rectangle, + in ParallelExecutionSettings parallelSettings, + in T operation) + where T : struct, IRowOperation + where TBuffer : unmanaged + { + ValidateRectangle(rectangle); + + int top = rectangle.Top; + int bottom = rectangle.Bottom; + int width = rectangle.Width; + int height = rectangle.Height; + + int numOfSteps = GetNumberOfSteps(width, height, parallelSettings); + MemoryAllocator allocator = parallelSettings.MemoryAllocator; + int bufferLength = Unsafe.AsRef(in operation).GetRequiredBufferLength(rectangle); + + // Avoid TPL overhead in this trivial case: + if (numOfSteps == 1) + { + using IMemoryOwner buffer = allocator.Allocate(bufferLength); + Span span = buffer.Memory.Span; + + for (int y = top; y < bottom; y++) + { + Unsafe.AsRef(in operation).Invoke(y, span); + } + + return; + } + + int verticalStep = DivideCeil(height, numOfSteps); + ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps); + RowOperationWrapper wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation); + + _ = Parallel.For( + 0, + numOfSteps, + parallelOptions, + wrappingOperation.Invoke); + } + + /// + /// Iterate through the rows of a rectangle in optimized batches defined by -s. + /// + /// The type of row operation to perform. + /// The to get the parallel settings from. + /// The . + /// The operation defining the iteration logic on a single . + [MethodImpl(InliningOptions.ShortMethod)] + public static void IterateRowIntervals(Configuration configuration, Rectangle rectangle, in T operation) + where T : struct, IRowIntervalOperation + { + ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration); + IterateRowIntervals(rectangle, in parallelSettings, in operation); + } + + /// + /// Iterate through the rows of a rectangle in optimized batches defined by -s. + /// + /// The type of row operation to perform. + /// The . + /// The . + /// The operation defining the iteration logic on a single . + public static void IterateRowIntervals( + Rectangle rectangle, + in ParallelExecutionSettings parallelSettings, + in T operation) + where T : struct, IRowIntervalOperation + { + ValidateRectangle(rectangle); + + int top = rectangle.Top; + int bottom = rectangle.Bottom; + int width = rectangle.Width; + int height = rectangle.Height; + + int numOfSteps = GetNumberOfSteps(width, height, parallelSettings); + + // Avoid TPL overhead in this trivial case: + if (numOfSteps == 1) + { + RowInterval rows = new(top, bottom); + Unsafe.AsRef(in operation).Invoke(in rows); + return; + } + + int verticalStep = DivideCeil(rectangle.Height, numOfSteps); + ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps); + RowIntervalOperationWrapper wrappingOperation = new(top, bottom, verticalStep, in operation); + + _ = Parallel.For( + 0, + numOfSteps, + parallelOptions, + wrappingOperation.Invoke); + } + + /// + /// Iterate through the rows of a rectangle in optimized batches defined by -s + /// instantiating a temporary buffer for each invocation. + /// + /// The type of row operation to perform. + /// The type of buffer elements. + /// The to get the parallel settings from. + /// The . + /// The operation defining the iteration logic on a single . + public static void IterateRowIntervals(Configuration configuration, Rectangle rectangle, in T operation) + where T : struct, IRowIntervalOperation + where TBuffer : unmanaged + { + ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration); + IterateRowIntervals(rectangle, in parallelSettings, in operation); + } + + /// + /// Iterate through the rows of a rectangle in optimized batches defined by -s + /// instantiating a temporary buffer for each invocation. + /// + /// The type of row operation to perform. + /// The type of buffer elements. + /// The . + /// The . + /// The operation defining the iteration logic on a single . + public static void IterateRowIntervals( + Rectangle rectangle, + in ParallelExecutionSettings parallelSettings, + in T operation) + where T : struct, IRowIntervalOperation + where TBuffer : unmanaged + { + ValidateRectangle(rectangle); + + int top = rectangle.Top; + int bottom = rectangle.Bottom; + int width = rectangle.Width; + int height = rectangle.Height; + + int numOfSteps = GetNumberOfSteps(width, height, parallelSettings); + MemoryAllocator allocator = parallelSettings.MemoryAllocator; + int bufferLength = Unsafe.AsRef(in operation).GetRequiredBufferLength(rectangle); + + // Avoid TPL overhead in this trivial case: + if (numOfSteps == 1) + { + RowInterval rows = new(top, bottom); + using IMemoryOwner buffer = allocator.Allocate(bufferLength); + + Unsafe.AsRef(in operation).Invoke(in rows, buffer.Memory.Span); + + return; + } + + int verticalStep = DivideCeil(height, numOfSteps); + ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps); + RowIntervalOperationWrapper wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation); + + _ = Parallel.For( + 0, + numOfSteps, + parallelOptions, + wrappingOperation.Invoke); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int DivideCeil(long dividend, int divisor) => (int)Math.Min(1 + ((dividend - 1) / divisor), int.MaxValue); + + /// + /// Creates the for the current iteration. + /// + /// The execution settings. + /// The number of row partitions to execute. + /// The instance. + [MethodImpl(InliningOptions.ShortMethod)] + private static ParallelOptions CreateParallelOptions(in ParallelExecutionSettings parallelSettings, int numOfSteps) + => new() { MaxDegreeOfParallelism = parallelSettings.MaxDegreeOfParallelism == -1 ? -1 : numOfSteps }; + + /// + /// Calculates the number of row partitions to execute for the given region. + /// + /// The width of the region. + /// The height of the region. + /// The execution settings. + /// The number of row partitions to execute. + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetNumberOfSteps(int width, int height, in ParallelExecutionSettings parallelSettings) + { + int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask); + + if (parallelSettings.MaxDegreeOfParallelism == -1) + { + // Row batching cannot produce more useful partitions than the number of rows available. + return Math.Min(height, maxSteps); + } + + return Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps); + } + + private static void ValidateRectangle(Rectangle rectangle) + { + Guard.MustBeGreaterThan( + rectangle.Width, + 0, + $"{nameof(rectangle)}.{nameof(rectangle.Width)}"); + + Guard.MustBeGreaterThan( + rectangle.Height, + 0, + $"{nameof(rectangle)}.{nameof(rectangle.Height)}"); + } + } +} diff --git a/ImageSharp/Advanced/PreserveAttribute.cs b/ImageSharp/Advanced/PreserveAttribute.cs new file mode 100644 index 0000000..eb06bc4 --- /dev/null +++ b/ImageSharp/Advanced/PreserveAttribute.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Advanced { + /// + /// This is necessary to avoid being excluded from compilation in environments that do AOT builds, such as Unity's IL2CPP and Xamarin. + /// The only thing that matters is the class name. + /// There is no need to use or inherit from the PreserveAttribute class in each environment. + /// + [AttributeUsage(AttributeTargets.Method)] + internal sealed class PreserveAttribute : Attribute + { + } +} diff --git a/ImageSharp/Color/Color.NamedColors.cs b/ImageSharp/Color/Color.NamedColors.cs new file mode 100644 index 0000000..476f582 --- /dev/null +++ b/ImageSharp/Color/Color.NamedColors.cs @@ -0,0 +1,916 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp { + /// + /// Contains static named color values. + /// + /// + public readonly partial struct Color + { + private static readonly Lazy> NamedColorsLookupLazy = new(CreateNamedColorsLookup, true); + + /// + /// Represents a matching the W3C definition that has an hex value of #F0F8FF. + /// + public static readonly Color AliceBlue = FromPixel(new Rgba32(240, 248, 255, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FAEBD7. + /// + public static readonly Color AntiqueWhite = FromPixel(new Rgba32(250, 235, 215, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #00FFFF. + /// + public static readonly Color Aqua = FromPixel(new Rgba32(0, 255, 255, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #7FFFD4. + /// + public static readonly Color Aquamarine = FromPixel(new Rgba32(127, 255, 212, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #F0FFFF. + /// + public static readonly Color Azure = FromPixel(new Rgba32(240, 255, 255, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #F5F5DC. + /// + public static readonly Color Beige = FromPixel(new Rgba32(245, 245, 220, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFE4C4. + /// + public static readonly Color Bisque = FromPixel(new Rgba32(255, 228, 196, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #000000. + /// + public static readonly Color Black = FromPixel(new Rgba32(0, 0, 0, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFEBCD. + /// + public static readonly Color BlanchedAlmond = FromPixel(new Rgba32(255, 235, 205, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #0000FF. + /// + public static readonly Color Blue = FromPixel(new Rgba32(0, 0, 255, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #8A2BE2. + /// + public static readonly Color BlueViolet = FromPixel(new Rgba32(138, 43, 226, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #A52A2A. + /// + public static readonly Color Brown = FromPixel(new Rgba32(165, 42, 42, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #DEB887. + /// + public static readonly Color BurlyWood = FromPixel(new Rgba32(222, 184, 135, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #5F9EA0. + /// + public static readonly Color CadetBlue = FromPixel(new Rgba32(95, 158, 160, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #7FFF00. + /// + public static readonly Color Chartreuse = FromPixel(new Rgba32(127, 255, 0, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #D2691E. + /// + public static readonly Color Chocolate = FromPixel(new Rgba32(210, 105, 30, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF7F50. + /// + public static readonly Color Coral = FromPixel(new Rgba32(255, 127, 80, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #6495ED. + /// + public static readonly Color CornflowerBlue = FromPixel(new Rgba32(100, 149, 237, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFF8DC. + /// + public static readonly Color Cornsilk = FromPixel(new Rgba32(255, 248, 220, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #DC143C. + /// + public static readonly Color Crimson = FromPixel(new Rgba32(220, 20, 60, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #00FFFF. + /// + public static readonly Color Cyan = Aqua; + + /// + /// Represents a matching the W3C definition that has an hex value of #00008B. + /// + public static readonly Color DarkBlue = FromPixel(new Rgba32(0, 0, 139, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #008B8B. + /// + public static readonly Color DarkCyan = FromPixel(new Rgba32(0, 139, 139, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #B8860B. + /// + public static readonly Color DarkGoldenrod = FromPixel(new Rgba32(184, 134, 11, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #A9A9A9. + /// + public static readonly Color DarkGray = FromPixel(new Rgba32(169, 169, 169, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #006400. + /// + public static readonly Color DarkGreen = FromPixel(new Rgba32(0, 100, 0, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #A9A9A9. + /// + public static readonly Color DarkGrey = DarkGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #BDB76B. + /// + public static readonly Color DarkKhaki = FromPixel(new Rgba32(189, 183, 107, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #8B008B. + /// + public static readonly Color DarkMagenta = FromPixel(new Rgba32(139, 0, 139, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #556B2F. + /// + public static readonly Color DarkOliveGreen = FromPixel(new Rgba32(85, 107, 47, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF8C00. + /// + public static readonly Color DarkOrange = FromPixel(new Rgba32(255, 140, 0, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #9932CC. + /// + public static readonly Color DarkOrchid = FromPixel(new Rgba32(153, 50, 204, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #8B0000. + /// + public static readonly Color DarkRed = FromPixel(new Rgba32(139, 0, 0, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #E9967A. + /// + public static readonly Color DarkSalmon = FromPixel(new Rgba32(233, 150, 122, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #8FBC8F. + /// + public static readonly Color DarkSeaGreen = FromPixel(new Rgba32(143, 188, 143, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #483D8B. + /// + public static readonly Color DarkSlateBlue = FromPixel(new Rgba32(72, 61, 139, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #2F4F4F. + /// + public static readonly Color DarkSlateGray = FromPixel(new Rgba32(47, 79, 79, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #2F4F4F. + /// + public static readonly Color DarkSlateGrey = DarkSlateGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #00CED1. + /// + public static readonly Color DarkTurquoise = FromPixel(new Rgba32(0, 206, 209, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #9400D3. + /// + public static readonly Color DarkViolet = FromPixel(new Rgba32(148, 0, 211, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF1493. + /// + public static readonly Color DeepPink = FromPixel(new Rgba32(255, 20, 147, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #00BFFF. + /// + public static readonly Color DeepSkyBlue = FromPixel(new Rgba32(0, 191, 255, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #696969. + /// + public static readonly Color DimGray = FromPixel(new Rgba32(105, 105, 105, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #696969. + /// + public static readonly Color DimGrey = DimGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #1E90FF. + /// + public static readonly Color DodgerBlue = FromPixel(new Rgba32(30, 144, 255, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #B22222. + /// + public static readonly Color Firebrick = FromPixel(new Rgba32(178, 34, 34, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFAF0. + /// + public static readonly Color FloralWhite = FromPixel(new Rgba32(255, 250, 240, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #228B22. + /// + public static readonly Color ForestGreen = FromPixel(new Rgba32(34, 139, 34, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF00FF. + /// + public static readonly Color Fuchsia = FromPixel(new Rgba32(255, 0, 255, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #DCDCDC. + /// + public static readonly Color Gainsboro = FromPixel(new Rgba32(220, 220, 220, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #F8F8FF. + /// + public static readonly Color GhostWhite = FromPixel(new Rgba32(248, 248, 255, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFD700. + /// + public static readonly Color Gold = FromPixel(new Rgba32(255, 215, 0, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #DAA520. + /// + public static readonly Color Goldenrod = FromPixel(new Rgba32(218, 165, 32, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #808080. + /// + public static readonly Color Gray = FromPixel(new Rgba32(128, 128, 128, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #008000. + /// + public static readonly Color Green = FromPixel(new Rgba32(0, 128, 0, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #ADFF2F. + /// + public static readonly Color GreenYellow = FromPixel(new Rgba32(173, 255, 47, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #808080. + /// + public static readonly Color Grey = Gray; + + /// + /// Represents a matching the W3C definition that has an hex value of #F0FFF0. + /// + public static readonly Color Honeydew = FromPixel(new Rgba32(240, 255, 240, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF69B4. + /// + public static readonly Color HotPink = FromPixel(new Rgba32(255, 105, 180, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #CD5C5C. + /// + public static readonly Color IndianRed = FromPixel(new Rgba32(205, 92, 92, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #4B0082. + /// + public static readonly Color Indigo = FromPixel(new Rgba32(75, 0, 130, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFFF0. + /// + public static readonly Color Ivory = FromPixel(new Rgba32(255, 255, 240, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #F0E68C. + /// + public static readonly Color Khaki = FromPixel(new Rgba32(240, 230, 140, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #E6E6FA. + /// + public static readonly Color Lavender = FromPixel(new Rgba32(230, 230, 250, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFF0F5. + /// + public static readonly Color LavenderBlush = FromPixel(new Rgba32(255, 240, 245, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #7CFC00. + /// + public static readonly Color LawnGreen = FromPixel(new Rgba32(124, 252, 0, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFACD. + /// + public static readonly Color LemonChiffon = FromPixel(new Rgba32(255, 250, 205, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #ADD8E6. + /// + public static readonly Color LightBlue = FromPixel(new Rgba32(173, 216, 230, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #F08080. + /// + public static readonly Color LightCoral = FromPixel(new Rgba32(240, 128, 128, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #E0FFFF. + /// + public static readonly Color LightCyan = FromPixel(new Rgba32(224, 255, 255, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FAFAD2. + /// + public static readonly Color LightGoldenrodYellow = FromPixel(new Rgba32(250, 250, 210, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #D3D3D3. + /// + public static readonly Color LightGray = FromPixel(new Rgba32(211, 211, 211, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #90EE90. + /// + public static readonly Color LightGreen = FromPixel(new Rgba32(144, 238, 144, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #D3D3D3. + /// + public static readonly Color LightGrey = LightGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #FFB6C1. + /// + public static readonly Color LightPink = FromPixel(new Rgba32(255, 182, 193, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFA07A. + /// + public static readonly Color LightSalmon = FromPixel(new Rgba32(255, 160, 122, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #20B2AA. + /// + public static readonly Color LightSeaGreen = FromPixel(new Rgba32(32, 178, 170, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #87CEFA. + /// + public static readonly Color LightSkyBlue = FromPixel(new Rgba32(135, 206, 250, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #778899. + /// + public static readonly Color LightSlateGray = FromPixel(new Rgba32(119, 136, 153, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #778899. + /// + public static readonly Color LightSlateGrey = LightSlateGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #B0C4DE. + /// + public static readonly Color LightSteelBlue = FromPixel(new Rgba32(176, 196, 222, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFFE0. + /// + public static readonly Color LightYellow = FromPixel(new Rgba32(255, 255, 224, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #00FF00. + /// + public static readonly Color Lime = FromPixel(new Rgba32(0, 255, 0, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #32CD32. + /// + public static readonly Color LimeGreen = FromPixel(new Rgba32(50, 205, 50, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FAF0E6. + /// + public static readonly Color Linen = FromPixel(new Rgba32(250, 240, 230, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF00FF. + /// + public static readonly Color Magenta = Fuchsia; + + /// + /// Represents a matching the W3C definition that has an hex value of #800000. + /// + public static readonly Color Maroon = FromPixel(new Rgba32(128, 0, 0, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #66CDAA. + /// + public static readonly Color MediumAquamarine = FromPixel(new Rgba32(102, 205, 170, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #0000CD. + /// + public static readonly Color MediumBlue = FromPixel(new Rgba32(0, 0, 205, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #BA55D3. + /// + public static readonly Color MediumOrchid = FromPixel(new Rgba32(186, 85, 211, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #9370DB. + /// + public static readonly Color MediumPurple = FromPixel(new Rgba32(147, 112, 219, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #3CB371. + /// + public static readonly Color MediumSeaGreen = FromPixel(new Rgba32(60, 179, 113, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #7B68EE. + /// + public static readonly Color MediumSlateBlue = FromPixel(new Rgba32(123, 104, 238, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #00FA9A. + /// + public static readonly Color MediumSpringGreen = FromPixel(new Rgba32(0, 250, 154, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #48D1CC. + /// + public static readonly Color MediumTurquoise = FromPixel(new Rgba32(72, 209, 204, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #C71585. + /// + public static readonly Color MediumVioletRed = FromPixel(new Rgba32(199, 21, 133, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #191970. + /// + public static readonly Color MidnightBlue = FromPixel(new Rgba32(25, 25, 112, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #F5FFFA. + /// + public static readonly Color MintCream = FromPixel(new Rgba32(245, 255, 250, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFE4E1. + /// + public static readonly Color MistyRose = FromPixel(new Rgba32(255, 228, 225, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFE4B5. + /// + public static readonly Color Moccasin = FromPixel(new Rgba32(255, 228, 181, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFDEAD. + /// + public static readonly Color NavajoWhite = FromPixel(new Rgba32(255, 222, 173, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #000080. + /// + public static readonly Color Navy = FromPixel(new Rgba32(0, 0, 128, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FDF5E6. + /// + public static readonly Color OldLace = FromPixel(new Rgba32(253, 245, 230, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #808000. + /// + public static readonly Color Olive = FromPixel(new Rgba32(128, 128, 0, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #6B8E23. + /// + public static readonly Color OliveDrab = FromPixel(new Rgba32(107, 142, 35, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFA500. + /// + public static readonly Color Orange = FromPixel(new Rgba32(255, 165, 0, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF4500. + /// + public static readonly Color OrangeRed = FromPixel(new Rgba32(255, 69, 0, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #DA70D6. + /// + public static readonly Color Orchid = FromPixel(new Rgba32(218, 112, 214, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #EEE8AA. + /// + public static readonly Color PaleGoldenrod = FromPixel(new Rgba32(238, 232, 170, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #98FB98. + /// + public static readonly Color PaleGreen = FromPixel(new Rgba32(152, 251, 152, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #AFEEEE. + /// + public static readonly Color PaleTurquoise = FromPixel(new Rgba32(175, 238, 238, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #DB7093. + /// + public static readonly Color PaleVioletRed = FromPixel(new Rgba32(219, 112, 147, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFEFD5. + /// + public static readonly Color PapayaWhip = FromPixel(new Rgba32(255, 239, 213, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFDAB9. + /// + public static readonly Color PeachPuff = FromPixel(new Rgba32(255, 218, 185, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #CD853F. + /// + public static readonly Color Peru = FromPixel(new Rgba32(205, 133, 63, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFC0CB. + /// + public static readonly Color Pink = FromPixel(new Rgba32(255, 192, 203, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #DDA0DD. + /// + public static readonly Color Plum = FromPixel(new Rgba32(221, 160, 221, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #B0E0E6. + /// + public static readonly Color PowderBlue = FromPixel(new Rgba32(176, 224, 230, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #800080. + /// + public static readonly Color Purple = FromPixel(new Rgba32(128, 0, 128, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #663399. + /// + public static readonly Color RebeccaPurple = FromPixel(new Rgba32(102, 51, 153, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF0000. + /// + public static readonly Color Red = FromPixel(new Rgba32(255, 0, 0, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #BC8F8F. + /// + public static readonly Color RosyBrown = FromPixel(new Rgba32(188, 143, 143, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #4169E1. + /// + public static readonly Color RoyalBlue = FromPixel(new Rgba32(65, 105, 225, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #8B4513. + /// + public static readonly Color SaddleBrown = FromPixel(new Rgba32(139, 69, 19, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FA8072. + /// + public static readonly Color Salmon = FromPixel(new Rgba32(250, 128, 114, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #F4A460. + /// + public static readonly Color SandyBrown = FromPixel(new Rgba32(244, 164, 96, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #2E8B57. + /// + public static readonly Color SeaGreen = FromPixel(new Rgba32(46, 139, 87, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFF5EE. + /// + public static readonly Color SeaShell = FromPixel(new Rgba32(255, 245, 238, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #A0522D. + /// + public static readonly Color Sienna = FromPixel(new Rgba32(160, 82, 45, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #C0C0C0. + /// + public static readonly Color Silver = FromPixel(new Rgba32(192, 192, 192, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #87CEEB. + /// + public static readonly Color SkyBlue = FromPixel(new Rgba32(135, 206, 235, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #6A5ACD. + /// + public static readonly Color SlateBlue = FromPixel(new Rgba32(106, 90, 205, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #708090. + /// + public static readonly Color SlateGray = FromPixel(new Rgba32(112, 128, 144, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #708090. + /// + public static readonly Color SlateGrey = SlateGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFAFA. + /// + public static readonly Color Snow = FromPixel(new Rgba32(255, 250, 250, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #00FF7F. + /// + public static readonly Color SpringGreen = FromPixel(new Rgba32(0, 255, 127, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #4682B4. + /// + public static readonly Color SteelBlue = FromPixel(new Rgba32(70, 130, 180, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #D2B48C. + /// + public static readonly Color Tan = FromPixel(new Rgba32(210, 180, 140, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #008080. + /// + public static readonly Color Teal = FromPixel(new Rgba32(0, 128, 128, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #D8BFD8. + /// + public static readonly Color Thistle = FromPixel(new Rgba32(216, 191, 216, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF6347. + /// + public static readonly Color Tomato = FromPixel(new Rgba32(255, 99, 71, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #00000000. + /// + public static readonly Color Transparent = FromPixel(new Rgba32(0, 0, 0, 0)); + + /// + /// Represents a matching the W3C definition that has an hex value of #40E0D0. + /// + public static readonly Color Turquoise = FromPixel(new Rgba32(64, 224, 208, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #EE82EE. + /// + public static readonly Color Violet = FromPixel(new Rgba32(238, 130, 238, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #F5DEB3. + /// + public static readonly Color Wheat = FromPixel(new Rgba32(245, 222, 179, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFFFF. + /// + public static readonly Color White = FromPixel(new Rgba32(255, 255, 255, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #F5F5F5. + /// + public static readonly Color WhiteSmoke = FromPixel(new Rgba32(245, 245, 245, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFF00. + /// + public static readonly Color Yellow = FromPixel(new Rgba32(255, 255, 0, 255)); + + /// + /// Represents a matching the W3C definition that has an hex value of #9ACD32. + /// + public static readonly Color YellowGreen = FromPixel(new Rgba32(154, 205, 50, 255)); + + private static Dictionary CreateNamedColorsLookup() + => new(StringComparer.OrdinalIgnoreCase) + { + { nameof(AliceBlue), AliceBlue }, + { nameof(AntiqueWhite), AntiqueWhite }, + { nameof(Aqua), Aqua }, + { nameof(Aquamarine), Aquamarine }, + { nameof(Azure), Azure }, + { nameof(Beige), Beige }, + { nameof(Bisque), Bisque }, + { nameof(Black), Black }, + { nameof(BlanchedAlmond), BlanchedAlmond }, + { nameof(Blue), Blue }, + { nameof(BlueViolet), BlueViolet }, + { nameof(Brown), Brown }, + { nameof(BurlyWood), BurlyWood }, + { nameof(CadetBlue), CadetBlue }, + { nameof(Chartreuse), Chartreuse }, + { nameof(Chocolate), Chocolate }, + { nameof(Coral), Coral }, + { nameof(CornflowerBlue), CornflowerBlue }, + { nameof(Cornsilk), Cornsilk }, + { nameof(Crimson), Crimson }, + { nameof(Cyan), Cyan }, + { nameof(DarkBlue), DarkBlue }, + { nameof(DarkCyan), DarkCyan }, + { nameof(DarkGoldenrod), DarkGoldenrod }, + { nameof(DarkGray), DarkGray }, + { nameof(DarkGreen), DarkGreen }, + { nameof(DarkGrey), DarkGrey }, + { nameof(DarkKhaki), DarkKhaki }, + { nameof(DarkMagenta), DarkMagenta }, + { nameof(DarkOliveGreen), DarkOliveGreen }, + { nameof(DarkOrange), DarkOrange }, + { nameof(DarkOrchid), DarkOrchid }, + { nameof(DarkRed), DarkRed }, + { nameof(DarkSalmon), DarkSalmon }, + { nameof(DarkSeaGreen), DarkSeaGreen }, + { nameof(DarkSlateBlue), DarkSlateBlue }, + { nameof(DarkSlateGray), DarkSlateGray }, + { nameof(DarkSlateGrey), DarkSlateGrey }, + { nameof(DarkTurquoise), DarkTurquoise }, + { nameof(DarkViolet), DarkViolet }, + { nameof(DeepPink), DeepPink }, + { nameof(DeepSkyBlue), DeepSkyBlue }, + { nameof(DimGray), DimGray }, + { nameof(DimGrey), DimGrey }, + { nameof(DodgerBlue), DodgerBlue }, + { nameof(Firebrick), Firebrick }, + { nameof(FloralWhite), FloralWhite }, + { nameof(ForestGreen), ForestGreen }, + { nameof(Fuchsia), Fuchsia }, + { nameof(Gainsboro), Gainsboro }, + { nameof(GhostWhite), GhostWhite }, + { nameof(Gold), Gold }, + { nameof(Goldenrod), Goldenrod }, + { nameof(Gray), Gray }, + { nameof(Green), Green }, + { nameof(GreenYellow), GreenYellow }, + { nameof(Grey), Grey }, + { nameof(Honeydew), Honeydew }, + { nameof(HotPink), HotPink }, + { nameof(IndianRed), IndianRed }, + { nameof(Indigo), Indigo }, + { nameof(Ivory), Ivory }, + { nameof(Khaki), Khaki }, + { nameof(Lavender), Lavender }, + { nameof(LavenderBlush), LavenderBlush }, + { nameof(LawnGreen), LawnGreen }, + { nameof(LemonChiffon), LemonChiffon }, + { nameof(LightBlue), LightBlue }, + { nameof(LightCoral), LightCoral }, + { nameof(LightCyan), LightCyan }, + { nameof(LightGoldenrodYellow), LightGoldenrodYellow }, + { nameof(LightGray), LightGray }, + { nameof(LightGreen), LightGreen }, + { nameof(LightGrey), LightGrey }, + { nameof(LightPink), LightPink }, + { nameof(LightSalmon), LightSalmon }, + { nameof(LightSeaGreen), LightSeaGreen }, + { nameof(LightSkyBlue), LightSkyBlue }, + { nameof(LightSlateGray), LightSlateGray }, + { nameof(LightSlateGrey), LightSlateGrey }, + { nameof(LightSteelBlue), LightSteelBlue }, + { nameof(LightYellow), LightYellow }, + { nameof(Lime), Lime }, + { nameof(LimeGreen), LimeGreen }, + { nameof(Linen), Linen }, + { nameof(Magenta), Magenta }, + { nameof(Maroon), Maroon }, + { nameof(MediumAquamarine), MediumAquamarine }, + { nameof(MediumBlue), MediumBlue }, + { nameof(MediumOrchid), MediumOrchid }, + { nameof(MediumPurple), MediumPurple }, + { nameof(MediumSeaGreen), MediumSeaGreen }, + { nameof(MediumSlateBlue), MediumSlateBlue }, + { nameof(MediumSpringGreen), MediumSpringGreen }, + { nameof(MediumTurquoise), MediumTurquoise }, + { nameof(MediumVioletRed), MediumVioletRed }, + { nameof(MidnightBlue), MidnightBlue }, + { nameof(MintCream), MintCream }, + { nameof(MistyRose), MistyRose }, + { nameof(Moccasin), Moccasin }, + { nameof(NavajoWhite), NavajoWhite }, + { nameof(Navy), Navy }, + { nameof(OldLace), OldLace }, + { nameof(Olive), Olive }, + { nameof(OliveDrab), OliveDrab }, + { nameof(Orange), Orange }, + { nameof(OrangeRed), OrangeRed }, + { nameof(Orchid), Orchid }, + { nameof(PaleGoldenrod), PaleGoldenrod }, + { nameof(PaleGreen), PaleGreen }, + { nameof(PaleTurquoise), PaleTurquoise }, + { nameof(PaleVioletRed), PaleVioletRed }, + { nameof(PapayaWhip), PapayaWhip }, + { nameof(PeachPuff), PeachPuff }, + { nameof(Peru), Peru }, + { nameof(Pink), Pink }, + { nameof(Plum), Plum }, + { nameof(PowderBlue), PowderBlue }, + { nameof(Purple), Purple }, + { nameof(RebeccaPurple), RebeccaPurple }, + { nameof(Red), Red }, + { nameof(RosyBrown), RosyBrown }, + { nameof(RoyalBlue), RoyalBlue }, + { nameof(SaddleBrown), SaddleBrown }, + { nameof(Salmon), Salmon }, + { nameof(SandyBrown), SandyBrown }, + { nameof(SeaGreen), SeaGreen }, + { nameof(SeaShell), SeaShell }, + { nameof(Sienna), Sienna }, + { nameof(Silver), Silver }, + { nameof(SkyBlue), SkyBlue }, + { nameof(SlateBlue), SlateBlue }, + { nameof(SlateGray), SlateGray }, + { nameof(SlateGrey), SlateGrey }, + { nameof(Snow), Snow }, + { nameof(SpringGreen), SpringGreen }, + { nameof(SteelBlue), SteelBlue }, + { nameof(Tan), Tan }, + { nameof(Teal), Teal }, + { nameof(Thistle), Thistle }, + { nameof(Tomato), Tomato }, + { nameof(Transparent), Transparent }, + { nameof(Turquoise), Turquoise }, + { nameof(Violet), Violet }, + { nameof(Wheat), Wheat }, + { nameof(White), White }, + { nameof(WhiteSmoke), WhiteSmoke }, + { nameof(Yellow), Yellow }, + { nameof(YellowGreen), YellowGreen } + }; + } +} diff --git a/ImageSharp/Color/Color.WebSafePalette.cs b/ImageSharp/Color/Color.WebSafePalette.cs new file mode 100644 index 0000000..a6f6857 --- /dev/null +++ b/ImageSharp/Color/Color.WebSafePalette.cs @@ -0,0 +1,165 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp { + /// + /// Contains the definition of . + /// + public partial struct Color + { + private static readonly Lazy WebSafePaletteLazy = new(CreateWebSafePalette, true); + + /// + /// Gets a collection of named, web safe colors as defined in the CSS Color Module Level 4. + /// + public static ReadOnlyMemory WebSafePalette => WebSafePaletteLazy.Value; + + private static Color[] CreateWebSafePalette() => + [ + AliceBlue, + AntiqueWhite, + Aqua, + Aquamarine, + Azure, + Beige, + Bisque, + Black, + BlanchedAlmond, + Blue, + BlueViolet, + Brown, + BurlyWood, + CadetBlue, + Chartreuse, + Chocolate, + Coral, + CornflowerBlue, + Cornsilk, + Crimson, + Cyan, + DarkBlue, + DarkCyan, + DarkGoldenrod, + DarkGray, + DarkGreen, + DarkKhaki, + DarkMagenta, + DarkOliveGreen, + DarkOrange, + DarkOrchid, + DarkRed, + DarkSalmon, + DarkSeaGreen, + DarkSlateBlue, + DarkSlateGray, + DarkTurquoise, + DarkViolet, + DeepPink, + DeepSkyBlue, + DimGray, + DodgerBlue, + Firebrick, + FloralWhite, + ForestGreen, + Fuchsia, + Gainsboro, + GhostWhite, + Gold, + Goldenrod, + Gray, + Green, + GreenYellow, + Honeydew, + HotPink, + IndianRed, + Indigo, + Ivory, + Khaki, + Lavender, + LavenderBlush, + LawnGreen, + LemonChiffon, + LightBlue, + LightCoral, + LightCyan, + LightGoldenrodYellow, + LightGray, + LightGreen, + LightPink, + LightSalmon, + LightSeaGreen, + LightSkyBlue, + LightSlateGray, + LightSteelBlue, + LightYellow, + Lime, + LimeGreen, + Linen, + Magenta, + Maroon, + MediumAquamarine, + MediumBlue, + MediumOrchid, + MediumPurple, + MediumSeaGreen, + MediumSlateBlue, + MediumSpringGreen, + MediumTurquoise, + MediumVioletRed, + MidnightBlue, + MintCream, + MistyRose, + Moccasin, + NavajoWhite, + Navy, + OldLace, + Olive, + OliveDrab, + Orange, + OrangeRed, + Orchid, + PaleGoldenrod, + PaleGreen, + PaleTurquoise, + PaleVioletRed, + PapayaWhip, + PeachPuff, + Peru, + Pink, + Plum, + PowderBlue, + Purple, + RebeccaPurple, + Red, + RosyBrown, + RoyalBlue, + SaddleBrown, + Salmon, + SandyBrown, + SeaGreen, + SeaShell, + Sienna, + Silver, + SkyBlue, + SlateBlue, + SlateGray, + Snow, + SpringGreen, + SteelBlue, + Tan, + Teal, + Thistle, + Tomato, + Transparent, + Turquoise, + Violet, + Wheat, + White, + WhiteSmoke, + Yellow, + YellowGreen + ]; + } +} diff --git a/ImageSharp/Color/Color.WernerPalette.cs b/ImageSharp/Color/Color.WernerPalette.cs new file mode 100644 index 0000000..9665791 --- /dev/null +++ b/ImageSharp/Color/Color.WernerPalette.cs @@ -0,0 +1,138 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp { + /// + /// Contains the definition of . + /// + public partial struct Color + { + private static readonly Lazy WernerPaletteLazy = new(CreateWernerPalette, true); + + /// + /// Gets a collection of colors as defined in the original second edition of Werner’s Nomenclature of Colours 1821. + /// The hex codes were collected and defined by Nicholas Rougeux . + /// + public static ReadOnlyMemory WernerPalette => WernerPaletteLazy.Value; + + private static Color[] CreateWernerPalette() => + [ + ParseHex("#f1e9cd"), + ParseHex("#f2e7cf"), + ParseHex("#ece6d0"), + ParseHex("#f2eacc"), + ParseHex("#f3e9ca"), + ParseHex("#f2ebcd"), + ParseHex("#e6e1c9"), + ParseHex("#e2ddc6"), + ParseHex("#cbc8b7"), + ParseHex("#bfbbb0"), + ParseHex("#bebeb3"), + ParseHex("#b7b5ac"), + ParseHex("#bab191"), + ParseHex("#9c9d9a"), + ParseHex("#8a8d84"), + ParseHex("#5b5c61"), + ParseHex("#555152"), + ParseHex("#413f44"), + ParseHex("#454445"), + ParseHex("#423937"), + ParseHex("#433635"), + ParseHex("#252024"), + ParseHex("#241f20"), + ParseHex("#281f3f"), + ParseHex("#1c1949"), + ParseHex("#4f638d"), + ParseHex("#383867"), + ParseHex("#5c6b8f"), + ParseHex("#657abb"), + ParseHex("#6f88af"), + ParseHex("#7994b5"), + ParseHex("#6fb5a8"), + ParseHex("#719ba2"), + ParseHex("#8aa1a6"), + ParseHex("#d0d5d3"), + ParseHex("#8590ae"), + ParseHex("#3a2f52"), + ParseHex("#39334a"), + ParseHex("#6c6d94"), + ParseHex("#584c77"), + ParseHex("#533552"), + ParseHex("#463759"), + ParseHex("#bfbac0"), + ParseHex("#77747f"), + ParseHex("#4a475c"), + ParseHex("#b8bfaf"), + ParseHex("#b2b599"), + ParseHex("#979c84"), + ParseHex("#5d6161"), + ParseHex("#61ac86"), + ParseHex("#a4b6a7"), + ParseHex("#adba98"), + ParseHex("#93b778"), + ParseHex("#7d8c55"), + ParseHex("#33431e"), + ParseHex("#7c8635"), + ParseHex("#8e9849"), + ParseHex("#c2c190"), + ParseHex("#67765b"), + ParseHex("#ab924b"), + ParseHex("#c8c76f"), + ParseHex("#ccc050"), + ParseHex("#ebdd99"), + ParseHex("#ab9649"), + ParseHex("#dbc364"), + ParseHex("#e6d058"), + ParseHex("#ead665"), + ParseHex("#d09b2c"), + ParseHex("#a36629"), + ParseHex("#a77d35"), + ParseHex("#f0d696"), + ParseHex("#d7c485"), + ParseHex("#f1d28c"), + ParseHex("#efcc83"), + ParseHex("#f3daa7"), + ParseHex("#dfa837"), + ParseHex("#ebbc71"), + ParseHex("#d17c3f"), + ParseHex("#92462f"), + ParseHex("#be7249"), + ParseHex("#bb603c"), + ParseHex("#c76b4a"), + ParseHex("#a75536"), + ParseHex("#b63e36"), + ParseHex("#b5493a"), + ParseHex("#cd6d57"), + ParseHex("#711518"), + ParseHex("#e9c49d"), + ParseHex("#eedac3"), + ParseHex("#eecfbf"), + ParseHex("#ce536b"), + ParseHex("#b74a70"), + ParseHex("#b7757c"), + ParseHex("#612741"), + ParseHex("#7a4848"), + ParseHex("#3f3033"), + ParseHex("#8d746f"), + ParseHex("#4d3635"), + ParseHex("#6e3b31"), + ParseHex("#864735"), + ParseHex("#553d3a"), + ParseHex("#613936"), + ParseHex("#7a4b3a"), + ParseHex("#946943"), + ParseHex("#c39e6d"), + ParseHex("#513e32"), + ParseHex("#8b7859"), + ParseHex("#9b856b"), + ParseHex("#766051"), + ParseHex("#453b32"), + + // Werner does not define a transparent color, but we need to add one to + // make the palette work with the rest of the library. + Transparent + ]; + } +} diff --git a/ImageSharp/Color/Color.cs b/ImageSharp/Color/Color.cs new file mode 100644 index 0000000..e5cc798 --- /dev/null +++ b/ImageSharp/Color/Color.cs @@ -0,0 +1,638 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Globalization; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp { + /// + /// Represents a color value that is convertible to any type. + /// + /// + /// The internal representation and layout of this structure is hidden by intention. + /// It's not serializable, and it should not be considered as part of a contract. + /// Unlike System.Drawing.Color, has to be converted to a specific pixel value + /// to query the color components. + /// + public readonly partial struct Color : IEquatable + { + private readonly Vector4 data; + private readonly IPixel? boxedHighPrecisionPixel; + + /// + /// Initializes a new instance of the struct. + /// + /// The containing the color information. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Color(Vector4 vector) + { + this.data = Numerics.Clamp(vector, Vector4.Zero, Vector4.One); + this.boxedHighPrecisionPixel = null; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The pixel containing color information. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Color(IPixel pixel) + { + this.boxedHighPrecisionPixel = pixel; + this.data = default; + } + + /// + /// Checks whether two structures are equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is equal to the parameter; + /// otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Color left, Color right) => left.Equals(right); + + /// + /// Checks whether two structures are not equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is not equal to the parameter; + /// otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Color left, Color right) => !left.Equals(right); + + /// + /// Creates a from the given . + /// + /// The pixel to convert from. + /// The pixel format. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Color FromPixel(TPixel source) + where TPixel : unmanaged, IPixel + { + // Avoid boxing in case we can convert to Vector4 safely and efficiently + PixelTypeInfo info = TPixel.GetPixelTypeInfo(); + if (info.ComponentInfo.HasValue && info.ComponentInfo.Value.GetMaximumComponentPrecision() <= (int)PixelComponentBitDepth.Bit32) + { + return new Color(source.ToScaledVector4()); + } + + return new Color(source); + } + + /// + /// Creates a from a generic scaled . + /// + /// The vector to load the pixel from. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Color FromScaledVector(Vector4 source) => new(source); + + /// + /// Bulk converts a span of generic scaled to a span of . + /// + /// The source vector span. + /// The destination color span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void FromScaledVector(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + for (int i = 0; i < source.Length; i++) + { + destination[i] = FromScaledVector(source[i]); + } + } + + /// + /// Bulk converts a span of a specified type to a span of . + /// + /// The pixel type to convert to. + /// The source pixel span. + /// The destination color span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void FromPixel(ReadOnlySpan source, Span destination) + where TPixel : unmanaged, IPixel + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // Avoid boxing in case we can convert to Vector4 safely and efficiently + PixelTypeInfo info = TPixel.GetPixelTypeInfo(); + if (info.ComponentInfo.HasValue && info.ComponentInfo.Value.GetMaximumComponentPrecision() <= (int)PixelComponentBitDepth.Bit32) + { + for (int i = 0; i < source.Length; i++) + { + destination[i] = FromScaledVector(source[i].ToScaledVector4()); + } + } + else + { + for (int i = 0; i < source.Length; i++) + { + destination[i] = new Color(source[i]); + } + } + } + + /// + /// Gets a from the given hexadecimal string. + /// + /// + /// The hexadecimal representation of the combined color components. + /// + /// + /// The format of the hexadecimal string to parse, if applicable. Defaults to . + /// + /// + /// The equivalent of the hexadecimal input. + /// + /// + /// Thrown when the is not in the correct format. + /// + public static Color ParseHex(string hex, ColorHexFormat format = ColorHexFormat.Rgba) + { + Guard.NotNull(hex, nameof(hex)); + + if (!TryParseHex(hex, out Color color, format)) + { + throw new ArgumentException("Hexadecimal string is not in the correct format.", nameof(hex)); + } + + return color; + } + + /// + /// Gets a from the given hexadecimal string. + /// + /// + /// The hexadecimal representation of the combined color components. + /// + /// + /// When this method returns, contains the equivalent of the hexadecimal input. + /// + /// + /// The format of the hexadecimal string to parse, if applicable. Defaults to . + /// + /// + /// if the parsing was successful; otherwise, . + /// + public static bool TryParseHex(string hex, out Color result, ColorHexFormat format = ColorHexFormat.Rgba) + { + result = default; + + if (format == ColorHexFormat.Argb) + { + if (TryParseArgbHex(hex, out Argb32 argb)) + { + result = FromPixel(argb); + return true; + } + } + else if (format == ColorHexFormat.Rgba) + { + if (TryParseRgbaHex(hex, out Rgba32 rgba)) + { + result = FromPixel(rgba); + return true; + } + } + + return false; + } + + /// + /// Gets a from the given input string. + /// + /// + /// The name of the color or the hexadecimal representation of the combined color components. + /// + /// + /// The format of the hexadecimal string to parse, if applicable. Defaults to . + /// + /// + /// The equivalent of the input string. + /// + /// + /// Thrown when the is not in the correct format. + /// + public static Color Parse(string input, ColorHexFormat format = ColorHexFormat.Rgba) + { + Guard.NotNull(input, nameof(input)); + + if (!TryParse(input, out Color color, format)) + { + throw new ArgumentException("Input string is not in the correct format.", nameof(input)); + } + + return color; + } + + /// + /// Tries to create a new instance of the struct from the given input string. + /// + /// + /// The name of the color or the hexadecimal representation of the combined color components. + /// + /// + /// When this method returns, contains the equivalent of the input string. + /// + /// + /// The format of the hexadecimal string to parse, if applicable. Defaults to . + /// + /// + /// if the parsing was successful; otherwise, . + /// + public static bool TryParse(string input, out Color result, ColorHexFormat format = ColorHexFormat.Rgba) + { + result = default; + + if (string.IsNullOrWhiteSpace(input)) + { + return false; + } + + if (NamedColorsLookupLazy.Value.TryGetValue(input, out result)) + { + return true; + } + + result = default; + if (string.IsNullOrWhiteSpace(input)) + { + return false; + } + + return TryParseHex(input, out result, format); + } + + /// + /// Alters the alpha channel of the color, returning a new instance. + /// + /// The new value of alpha [0..1]. + /// The color having it's alpha channel altered. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Color WithAlpha(float alpha) + { + Vector4 v = this.ToScaledVector4(); + v.W = alpha; + return FromScaledVector(v); + } + + /// + /// Gets the hexadecimal string representation of the color instance. + /// + /// + /// The format of the hexadecimal string to return. Defaults to . + /// + /// A hexadecimal string representation of the value. + /// Thrown when the is not supported. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ToHex(ColorHexFormat format = ColorHexFormat.Rgba) + { + Rgba32 rgba = (this.boxedHighPrecisionPixel is not null) + ? this.boxedHighPrecisionPixel.ToRgba32() + : Rgba32.FromScaledVector4(this.data); + + uint hexOrder = format switch + { + ColorHexFormat.Argb => (uint)((rgba.B << 0) | (rgba.G << 8) | (rgba.R << 16) | (rgba.A << 24)), + ColorHexFormat.Rgba => (uint)((rgba.A << 0) | (rgba.B << 8) | (rgba.G << 16) | (rgba.R << 24)), + _ => throw new ArgumentOutOfRangeException(nameof(format), format, "Unsupported color hex format.") + }; + + return hexOrder.ToString("X8", CultureInfo.InvariantCulture); + } + + /// + public override string ToString() => this.ToHex(ColorHexFormat.Rgba); + + /// + /// Converts the color instance to a specified type. + /// + /// The pixel type to convert to. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TPixel ToPixel() + where TPixel : unmanaged, IPixel + { + if (this.boxedHighPrecisionPixel is TPixel pixel) + { + return pixel; + } + + if (this.boxedHighPrecisionPixel is null) + { + return TPixel.FromScaledVector4(this.data); + } + + return TPixel.FromScaledVector4(this.boxedHighPrecisionPixel.ToScaledVector4()); + } + + /// + /// Expands the color into a generic ("scaled") representation + /// with values scaled and clamped between 0 and 1. + /// The vector components are typically expanded in least to greatest significance order. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 ToScaledVector4() + { + if (this.boxedHighPrecisionPixel is null) + { + return this.data; + } + + return this.boxedHighPrecisionPixel.ToScaledVector4(); + } + + /// + /// Bulk converts a span of to a span of a specified type. + /// + /// The pixel type to convert to. + /// The source color span. + /// The destination pixel span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ToPixel(ReadOnlySpan source, Span destination) + where TPixel : unmanaged, IPixel + { + // We cannot use bulk pixel operations here as there is no guarantee that the source colors are + // created from pixel formats which fit into the unboxed vector data. + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToPixel(); + } + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Color other) + { + if (this.boxedHighPrecisionPixel is null && other.boxedHighPrecisionPixel is null) + { + return this.data == other.data; + } + + return this.boxedHighPrecisionPixel?.Equals(other.boxedHighPrecisionPixel) == true; + } + + /// + public override bool Equals(object? obj) => obj is Color other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() + { + if (this.boxedHighPrecisionPixel is null) + { + return this.data.GetHashCode(); + } + + return this.boxedHighPrecisionPixel.GetHashCode(); + } + + /// + /// Gets the hexadecimal string representation of the color instance in the format RRGGBBAA. + /// + /// + /// The hexadecimal representation of the combined color components. + /// + /// + /// When this method returns, contains the equivalent of the hexadecimal input. + /// + /// + /// if the parsing was successful; otherwise, . + /// + private static bool TryParseRgbaHex(string? hex, out Rgba32 result) + { + result = default; + + if (!TryConvertToRgbaUInt32(hex, out uint packedValue)) + { + return false; + } + + result = Unsafe.As(ref packedValue); + return true; + } + + /// + /// Gets the hexadecimal string representation of the color instance in the format AARRGGBB. + /// + /// + /// The hexadecimal representation of the combined color components. + /// + /// + /// When this method returns, contains the equivalent of the hexadecimal input. + /// + /// + /// if the parsing was successful; otherwise, . + /// + private static bool TryParseArgbHex(string? hex, out Argb32 result) + { + result = default; + + if (!TryConvertToArgbUInt32(hex, out uint packedValue)) + { + return false; + } + + result = Unsafe.As(ref packedValue); + return true; + } + + private static bool TryConvertToRgbaUInt32(string? value, out uint result) + { + result = default; + + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + ReadOnlySpan hex = value.AsSpan(); + + if (hex[0] == '#') + { + hex = hex[1..]; + } + + byte a = 255, r, g, b; + + switch (hex.Length) + { + case 8: + if (!TryParseByte(hex[0], hex[1], out r) || + !TryParseByte(hex[2], hex[3], out g) || + !TryParseByte(hex[4], hex[5], out b) || + !TryParseByte(hex[6], hex[7], out a)) + { + return false; + } + + break; + + case 6: + if (!TryParseByte(hex[0], hex[1], out r) || + !TryParseByte(hex[2], hex[3], out g) || + !TryParseByte(hex[4], hex[5], out b)) + { + return false; + } + + break; + + case 4: + if (!TryExpand(hex[0], out r) || + !TryExpand(hex[1], out g) || + !TryExpand(hex[2], out b) || + !TryExpand(hex[3], out a)) + { + return false; + } + + break; + + case 3: + if (!TryExpand(hex[0], out r) || + !TryExpand(hex[1], out g) || + !TryExpand(hex[2], out b)) + { + return false; + } + + break; + + default: + return false; + } + + result = (uint)(r | (g << 8) | (b << 16) | (a << 24)); // RGBA layout + return true; + } + + private static bool TryConvertToArgbUInt32(string? value, out uint result) + { + result = default; + + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + ReadOnlySpan hex = value.AsSpan(); + + if (hex[0] == '#') + { + hex = hex[1..]; + } + + byte a = 255, r, g, b; + + switch (hex.Length) + { + case 8: + if (!TryParseByte(hex[0], hex[1], out a) || + !TryParseByte(hex[2], hex[3], out r) || + !TryParseByte(hex[4], hex[5], out g) || + !TryParseByte(hex[6], hex[7], out b)) + { + return false; + } + + break; + + case 6: + if (!TryParseByte(hex[0], hex[1], out r) || + !TryParseByte(hex[2], hex[3], out g) || + !TryParseByte(hex[4], hex[5], out b)) + { + return false; + } + + break; + + case 4: + if (!TryExpand(hex[0], out a) || + !TryExpand(hex[1], out r) || + !TryExpand(hex[2], out g) || + !TryExpand(hex[3], out b)) + { + return false; + } + + break; + + case 3: + if (!TryExpand(hex[0], out r) || + !TryExpand(hex[1], out g) || + !TryExpand(hex[2], out b)) + { + return false; + } + + break; + + default: + return false; + } + + result = (uint)((b << 24) | (g << 16) | (r << 8) | a); + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryParseByte(char hi, char lo, out byte value) + { + if (TryConvertHexCharToByte(hi, out byte high) && TryConvertHexCharToByte(lo, out byte low)) + { + value = (byte)((high << 4) | low); + return true; + } + + value = 0; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryExpand(char c, out byte value) + { + if (TryConvertHexCharToByte(c, out byte nibble)) + { + value = (byte)((nibble << 4) | nibble); + return true; + } + + value = 0; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryConvertHexCharToByte(char c, out byte value) + { + if ((uint)(c - '0') <= 9) + { + value = (byte)(c - '0'); + return true; + } + + char lower = (char)(c | 0x20); // Normalize to lowercase + + if ((uint)(lower - 'a') <= 5) + { + value = (byte)(lower - 'a' + 10); + return true; + } + + value = 0; + return false; + } + } +} diff --git a/ImageSharp/Color/ColorHexFormat.cs b/ImageSharp/Color/ColorHexFormat.cs new file mode 100644 index 0000000..21cdcb4 --- /dev/null +++ b/ImageSharp/Color/ColorHexFormat.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp { + /// + /// Specifies the channel order when formatting or parsing a color as a hexadecimal string. + /// + public enum ColorHexFormat + { + /// + /// Uses RRGGBBAA channel order where the red, green, and blue components come first, + /// followed by the alpha component. This matches the CSS Color Module Level 4 and common web standards. + /// + /// When parsing, supports the following formats: + /// + /// #RGB expands to RRGGBBFF (fully opaque) + /// #RGBA expands to RRGGBBAA + /// #RRGGBB expands to RRGGBBFF (fully opaque) + /// #RRGGBBAA used as-is + /// + /// + /// When formatting, outputs an 8-digit hex string in RRGGBBAA order. + /// + Rgba, + + /// + /// Uses AARRGGBB channel order where the alpha component comes first, + /// followed by the red, green, and blue components. This matches the Microsoft/XAML convention. + /// + /// When parsing, supports the following formats: + /// + /// #ARGB expands to AARRGGBB + /// #AARRGGBB used as-is + /// + /// + /// When formatting, outputs an 8-digit hex string in AARRGGBB order. + /// + Argb + } +} diff --git a/ImageSharp/ColorProfiles/ChromaticAdaptionWhitePointSource.cs b/ImageSharp/ColorProfiles/ChromaticAdaptionWhitePointSource.cs new file mode 100644 index 0000000..19d66ce --- /dev/null +++ b/ImageSharp/ColorProfiles/ChromaticAdaptionWhitePointSource.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Enumerate the possible sources of the white point used in chromatic adaptation. + /// + public enum ChromaticAdaptionWhitePointSource + { + /// + /// The white point of the source color space. + /// + WhitePoint, + + /// + /// The white point of the source working space. + /// + RgbWorkingSpace + } +} diff --git a/ImageSharp/ColorProfiles/CieConstants.cs b/ImageSharp/ColorProfiles/CieConstants.cs new file mode 100644 index 0000000..bfeb90a --- /dev/null +++ b/ImageSharp/ColorProfiles/CieConstants.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Constants use for Cie conversion calculations + /// + /// + internal static class CieConstants + { + /// + /// 216F / 24389F + /// + public const float Epsilon = 216f / 24389f; + + /// + /// 24389F / 27F + /// + public const float Kappa = 24389f / 27f; + } +} diff --git a/ImageSharp/ColorProfiles/CieLab.cs b/ImageSharp/ColorProfiles/CieLab.cs new file mode 100644 index 0000000..a103883 --- /dev/null +++ b/ImageSharp/ColorProfiles/CieLab.cs @@ -0,0 +1,221 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Represents a CIE L*a*b* 1976 color. + /// + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct CieLab : IProfileConnectingSpace + { + /// + /// Initializes a new instance of the struct. + /// + /// The lightness dimension. + /// The a (green - magenta) component. + /// The b (blue - yellow) component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CieLab(Single l, Single a, Single b) + { + // Not clamping as documentation about this space only indicates "usual" ranges + this.L = l; + this.A = a; + this.B = b; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the l, a, b components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CieLab(Vector3 vector) + { + this.L = vector.X; + this.A = vector.Y; + this.B = vector.Z; + } + + /// + /// Gets the lightness dimension. + /// A value usually ranging between 0 (black), 100 (diffuse white) or higher (specular white). + /// + public Single L { get; } + + /// + /// Gets the a color component. + /// A value usually ranging from -100 to 100. Negative is green, positive magenta. + /// + public Single A { get; } + + /// + /// Gets the b color component. + /// A value usually ranging from -100 to 100. Negative is blue, positive is yellow + /// + public Single B { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(CieLab left, CieLab right) => left.Equals(right); + + /// + /// Compares two objects for inequality + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(CieLab left, CieLab right) => !left.Equals(right); + + /// + public Vector4 ToScaledVector4() + { + Vector3 v3 = default; + v3 += this.AsVector3Unsafe(); + v3 += new Vector3(0, 128F, 128F); + v3 /= new Vector3(100F, 255F, 255F); + return new Vector4(v3, 1F); + } + + /// + public static CieLab FromScaledVector4(Vector4 source) + { + Vector3 v3 = source.AsVector3(); + v3 *= new Vector3(100F, 255, 255); + v3 -= new Vector3(0, 128F, 128F); + return new CieLab(v3); + } + + /// + public static void ToScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToScaledVector4(); + } + } + + /// + public static void FromScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = FromScaledVector4(source[i]); + } + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CieLab FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source) + { + // Conversion algorithm described here: + // http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html + CieXyz whitePoint = options.TargetWhitePoint; + float wx = whitePoint.X, wy = whitePoint.Y, wz = whitePoint.Z; + + float xr = source.X / wx, yr = source.Y / wy, zr = source.Z / wz; + + const float inv116 = 1 / 116F; + + float fx = xr > CieConstants.Epsilon ? MathF.Pow(xr, 0.3333333F) : ((CieConstants.Kappa * xr) + 16F) * inv116; + float fy = yr > CieConstants.Epsilon ? MathF.Pow(yr, 0.3333333F) : ((CieConstants.Kappa * yr) + 16F) * inv116; + float fz = zr > CieConstants.Epsilon ? MathF.Pow(zr, 0.3333333F) : ((CieConstants.Kappa * zr) + 16F) * inv116; + + float l = (116F * fy) - 16F; + float a = 500F * (fx - fy); + float b = 200F * (fy - fz); + + return new CieLab(l, a, b); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + for (int i = 0; i < source.Length; i++) + { + CieXyz xyz = source[i]; + destination[i] = FromProfileConnectingSpace(options, in xyz); + } + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CieXyz ToProfileConnectingSpace(ColorConversionOptions options) + { + // Conversion algorithm described here: http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html + float l = this.L, a = this.A, b = this.B; + float fy = (l + 16) / 116F; + float fx = (a / 500F) + fy; + float fz = fy - (b / 200F); + + float fx3 = Numerics.Pow3(fx); + float fz3 = Numerics.Pow3(fz); + + float xr = fx3 > CieConstants.Epsilon ? fx3 : ((116F * fx) - 16F) / CieConstants.Kappa; + float yr = l > CieConstants.Kappa * CieConstants.Epsilon ? Numerics.Pow3((l + 16F) / 116F) : l / CieConstants.Kappa; + float zr = fz3 > CieConstants.Epsilon ? fz3 : ((116F * fz) - 16F) / CieConstants.Kappa; + + CieXyz whitePoint = options.SourceWhitePoint; + Vector3 wxyz = new(whitePoint.X, whitePoint.Y, whitePoint.Z); + Vector3 xyzr = new(xr, yr, zr); + + return new CieXyz(xyzr * wxyz); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + for (int i = 0; i < source.Length; i++) + { + CieLab lab = source[i]; + destination[i] = lab.ToProfileConnectingSpace(options); + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() + => ChromaticAdaptionWhitePointSource.WhitePoint; + + /// + public override int GetHashCode() => HashCode.Combine(this.L, this.A, this.B); + + /// + public override string ToString() => FormattableString.Invariant($"CieLab({this.L:#0.##}, {this.A:#0.##}, {this.B:#0.##})"); + + /// + public override bool Equals(object? obj) => obj is CieLab other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(CieLab other) + => this.AsVector3Unsafe() == other.AsVector3Unsafe(); + + private Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this)); + } +} diff --git a/ImageSharp/ColorProfiles/CieLch.cs b/ImageSharp/ColorProfiles/CieLch.cs new file mode 100644 index 0000000..fc329c1 --- /dev/null +++ b/ImageSharp/ColorProfiles/CieLch.cs @@ -0,0 +1,221 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Represents the CIE L*C*h°, cylindrical form of the CIE L*a*b* 1976 color. + /// + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct CieLch : IColorProfile + { + private static readonly Vector3 Min = new(0, -200, 0); + private static readonly Vector3 Max = new(100, 200, 360); + + /// + /// Initializes a new instance of the struct. + /// + /// The lightness dimension. + /// The chroma, relative saturation. + /// The hue in degrees. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CieLch(float l, float c, float h) + : this(new Vector3(l, c, h)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the l, c, h components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CieLch(Vector3 vector) + { + vector = Vector3.Clamp(vector, Min, Max); + this.L = vector.X; + this.C = vector.Y; + this.H = vector.Z; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + private CieLch(Vector3 vector, bool _) +#pragma warning restore SA1313 // Parameter names should begin with lower-case letter + { + vector = Vector3.Clamp(vector, Min, Max); + this.L = vector.X; + this.C = vector.Y; + this.H = vector.Z; + } + + /// + /// Gets the lightness dimension. + /// A value ranging between 0 (black), 100 (diffuse white) or higher (specular white). + /// + public float L { get; } + + /// + /// Gets the a chroma component. + /// A value ranging from -200 to 200. + /// + public float C { get; } + + /// + /// Gets the h° hue component in degrees. + /// A value ranging from 0 to 360. + /// + public float H { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(CieLch left, CieLch right) => left.Equals(right); + + /// + /// Compares two objects for inequality + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(CieLch left, CieLch right) => !left.Equals(right); + + /// + public Vector4 ToScaledVector4() + { + Vector3 v3 = default; + v3 += this.AsVector3Unsafe(); + v3 += new Vector3(0, 200, 0); + v3 /= new Vector3(100, 400, 360); + return new Vector4(v3, 1F); + } + + /// + public static CieLch FromScaledVector4(Vector4 source) + { + Vector3 v3 = source.AsVector3(); + v3 *= new Vector3(100, 400, 360); + v3 -= new Vector3(0, 200, 0); + return new CieLch(v3, true); + } + + /// + public static void ToScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToScaledVector4(); + } + } + + /// + public static void FromScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = FromScaledVector4(source[i]); + } + } + + /// + public static CieLch FromProfileConnectingSpace(ColorConversionOptions options, in CieLab source) + { + // Conversion algorithm described here: + // https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC + float l = source.L, a = source.A, b = source.B; + float c = MathF.Sqrt((a * a) + (b * b)); + float hRadians = MathF.Atan2(b, a); + float hDegrees = GeometryUtilities.RadianToDegree(hRadians); + + // Wrap the angle round at 360. + hDegrees %= 360; + + // Make sure it's not negative. + while (hDegrees < 0) + { + hDegrees += 360; + } + + return new CieLch(l, c, hDegrees); + } + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + for (int i = 0; i < source.Length; i++) + { + CieLab lab = source[i]; + destination[i] = FromProfileConnectingSpace(options, in lab); + } + } + + /// + public CieLab ToProfileConnectingSpace(ColorConversionOptions options) + { + // Conversion algorithm described here: + // https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC + float l = this.L, c = this.C, hDegrees = this.H; + float hRadians = GeometryUtilities.DegreeToRadian(hDegrees); + + float a = c * MathF.Cos(hRadians); + float b = c * MathF.Sin(hRadians); + + return new CieLab(l, a, b); + } + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + for (int i = 0; i < source.Length; i++) + { + CieLch lch = source[i]; + destination[i] = lch.ToProfileConnectingSpace(options); + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() + => ChromaticAdaptionWhitePointSource.WhitePoint; + + /// + public override int GetHashCode() + => HashCode.Combine(this.L, this.C, this.H); + + /// + public override string ToString() => FormattableString.Invariant($"CieLch({this.L:#0.##}, {this.C:#0.##}, {this.H:#0.##})"); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) => obj is CieLch other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(CieLch other) + => this.AsVector3Unsafe() == other.AsVector3Unsafe(); + + private Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this)); + } +} diff --git a/ImageSharp/ColorProfiles/CieLchuv.cs b/ImageSharp/ColorProfiles/CieLchuv.cs new file mode 100644 index 0000000..326e0d2 --- /dev/null +++ b/ImageSharp/ColorProfiles/CieLchuv.cs @@ -0,0 +1,220 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Represents the CIE L*C*h°, cylindrical form of the CIE L*u*v* 1976 color. + /// + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct CieLchuv : IColorProfile + { + private static readonly Vector3 Min = new(0, -200, 0); + private static readonly Vector3 Max = new(100, 200, 360); + + /// + /// Initializes a new instance of the struct. + /// + /// The lightness dimension. + /// The chroma, relative saturation. + /// The hue in degrees. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CieLchuv(float l, float c, float h) + : this(new Vector3(l, c, h)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the l, c, h components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CieLchuv(Vector3 vector) + { + vector = Vector3.Clamp(vector, Min, Max); + this.L = vector.X; + this.C = vector.Y; + this.H = vector.Z; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + private CieLchuv(Vector3 vector, bool _) +#pragma warning restore SA1313 // Parameter names should begin with lower-case letter + { + this.L = vector.X; + this.C = vector.Y; + this.H = vector.Z; + } + + /// + /// Gets the lightness dimension. + /// A value ranging between 0 (black), 100 (diffuse white) or higher (specular white). + /// + public float L { get; } + + /// + /// Gets the a chroma component. + /// A value ranging from -200 to 200. + /// + public float C { get; } + + /// + /// Gets the h° hue component in degrees. + /// A value ranging from 0 to 360. + /// + public float H { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + public static bool operator ==(CieLchuv left, CieLchuv right) => left.Equals(right); + + /// + /// Compares two objects for inequality + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + public static bool operator !=(CieLchuv left, CieLchuv right) => !left.Equals(right); + + /// + public Vector4 ToScaledVector4() + { + Vector3 v3 = default; + v3 += this.AsVector3Unsafe(); + v3 += new Vector3(0, 200, 0); + v3 /= new Vector3(100, 400, 360); + return new Vector4(v3, 1F); + } + + /// + public static CieLchuv FromScaledVector4(Vector4 source) + { + Vector3 v3 = source.AsVector3(); + v3 *= new Vector3(100, 400, 360); + v3 -= new Vector3(0, 200, 0); + return new CieLchuv(v3, true); + } + + /// + public static void ToScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToScaledVector4(); + } + } + + /// + public static void FromScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = FromScaledVector4(source[i]); + } + } + + /// + public static CieLchuv FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source) + { + CieLuv luv = CieLuv.FromProfileConnectingSpace(options, source); + + // Conversion algorithm described here: + // https://en.wikipedia.org/wiki/CIELUV#Cylindrical_representation_.28CIELCH.29 + float l = luv.L, u = luv.U, v = luv.V; + float c = MathF.Sqrt((u * u) + (v * v)); + float hRadians = MathF.Atan2(v, u); + float hDegrees = GeometryUtilities.RadianToDegree(hRadians); + + // Wrap the angle round at 360. + hDegrees %= 360; + + // Make sure it's not negative. + while (hDegrees < 0) + { + hDegrees += 360; + } + + return new CieLchuv(l, c, hDegrees); + } + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + for (int i = 0; i < source.Length; i++) + { + CieXyz xyz = source[i]; + destination[i] = FromProfileConnectingSpace(options, in xyz); + } + } + + /// + public CieXyz ToProfileConnectingSpace(ColorConversionOptions options) + { + // Conversion algorithm described here: + // https://en.wikipedia.org/wiki/CIELUV#Cylindrical_representation_.28CIELCH.29 + float l = this.L, c = this.C, hDegrees = this.H; + float hRadians = GeometryUtilities.DegreeToRadian(hDegrees); + + float u = c * MathF.Cos(hRadians); + float v = c * MathF.Sin(hRadians); + + CieLuv luv = new(l, u, v); + return luv.ToProfileConnectingSpace(options); + } + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + for (int i = 0; i < source.Length; i++) + { + CieLchuv lch = source[i]; + destination[i] = lch.ToProfileConnectingSpace(options); + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() + => ChromaticAdaptionWhitePointSource.WhitePoint; + + /// + public override int GetHashCode() + => HashCode.Combine(this.L, this.C, this.H); + + /// + public override string ToString() + => FormattableString.Invariant($"CieLchuv({this.L:#0.##}, {this.C:#0.##}, {this.H:#0.##})"); + + /// + public override bool Equals(object? obj) + => obj is CieLchuv other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(CieLchuv other) + => this.AsVector3Unsafe() == other.AsVector3Unsafe(); + + private Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this)); + } +} diff --git a/ImageSharp/ColorProfiles/CieLuv.cs b/ImageSharp/ColorProfiles/CieLuv.cs new file mode 100644 index 0000000..d5cd2ed --- /dev/null +++ b/ImageSharp/ColorProfiles/CieLuv.cs @@ -0,0 +1,233 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// The CIE 1976 (L*, u*, v*) color space, commonly known by its abbreviation CIELUV, is a color space adopted by the International + /// Commission on Illumination (CIE) in 1976, as a simple-to-compute transformation of the 1931 CIE XYZ color space, but which + /// attempted perceptual uniformity + /// + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct CieLuv : IColorProfile + { + /// + /// Initializes a new instance of the struct. + /// + /// The lightness dimension. + /// The blue-yellow chromaticity coordinate of the given white point. + /// The red-green chromaticity coordinate of the given white point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CieLuv(float l, float u, float v) + { + // Not clamping as documentation about this space only indicates "usual" ranges + this.L = l; + this.U = u; + this.V = v; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the l, u, v components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CieLuv(Vector3 vector) + { + this.L = vector.X; + this.U = vector.Y; + this.V = vector.Z; + } + + /// + /// Gets the lightness dimension + /// A value usually ranging between 0 and 100. + /// + public float L { get; } + + /// + /// Gets the blue-yellow chromaticity coordinate of the given white point. + /// A value usually ranging between -100 and 100. + /// + public float U { get; } + + /// + /// Gets the red-green chromaticity coordinate of the given white point. + /// A value usually ranging between -100 and 100. + /// + public float V { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(CieLuv left, CieLuv right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(CieLuv left, CieLuv right) => !left.Equals(right); + + /// + public Vector4 ToScaledVector4() => throw new NotImplementedException(); + + /// + public static CieLuv FromScaledVector4(Vector4 source) => throw new NotImplementedException(); + + /// + public static void ToScaledVector4(ReadOnlySpan source, Span destination) => throw new NotImplementedException(); + + /// + public static void FromScaledVector4(ReadOnlySpan source, Span destination) => throw new NotImplementedException(); + + /// + public static CieLuv FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source) + { + // Use doubles here for accuracy. + // Conversion algorithm described here: + // http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Luv.html + CieXyz whitePoint = options.TargetWhitePoint; + + double yr = source.Y / whitePoint.Y; + + double den = source.X + (15 * source.Y) + (3 * source.Z); + double up = den > 0 ? ComputeU(in source) : 0; + double vp = den > 0 ? ComputeV(in source) : 0; + double upr = ComputeU(in whitePoint); + double vpr = ComputeV(in whitePoint); + + const double e = 1 / 3d; + double l = yr > CieConstants.Epsilon + ? ((116 * Math.Pow(yr, e)) - 16d) + : (CieConstants.Kappa * yr); + + if (double.IsNaN(l) || l == -0d) + { + l = 0; + } + + double u = 13 * l * (up - upr); + double v = 13 * l * (vp - vpr); + + if (double.IsNaN(u) || u == -0d) + { + u = 0; + } + + if (double.IsNaN(v) || v == -0d) + { + v = 0; + } + + return new CieLuv((float)l, (float)u, (float)v); + } + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + for (int i = 0; i < source.Length; i++) + { + CieXyz xyz = source[i]; + destination[i] = FromProfileConnectingSpace(options, in xyz); + } + } + + /// + public CieXyz ToProfileConnectingSpace(ColorConversionOptions options) + { + // Use doubles here for accuracy. + // Conversion algorithm described here: + // http://www.brucelindbloom.com/index.html?Eqn_Luv_to_XYZ.html + CieXyz whitePoint = options.SourceWhitePoint; + + double l = this.L, u = this.U, v = this.V; + + double u0 = ComputeU(in whitePoint); + double v0 = ComputeV(in whitePoint); + + double y = l > CieConstants.Kappa * CieConstants.Epsilon + ? Numerics.Pow3((l + 16) / 116d) + : l / CieConstants.Kappa; + + double a = ((52 * l / (u + (13 * l * u0))) - 1) / 3; + double b = -5 * y; + const double c = -1 / 3d; + double d = y * ((39 * l / (v + (13 * l * v0))) - 5); + + double x = (d - b) / (a - c); + double z = (x * a) + b; + + if (double.IsNaN(x) || x == -0d) + { + x = 0; + } + + if (double.IsNaN(y) || y == -0d) + { + y = 0; + } + + if (double.IsNaN(z) || z == -0d) + { + z = 0; + } + + return new CieXyz((float)x, (float)y, (float)z); + } + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + for (int i = 0; i < source.Length; i++) + { + CieLuv luv = source[i]; + destination[i] = luv.ToProfileConnectingSpace(options); + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() + => ChromaticAdaptionWhitePointSource.WhitePoint; + + /// + public override int GetHashCode() => HashCode.Combine(this.L, this.U, this.V); + + /// + public override string ToString() => FormattableString.Invariant($"CieLuv({this.L:#0.##}, {this.U:#0.##}, {this.V:#0.##})"); + + /// + public override bool Equals(object? obj) => obj is CieLuv other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(CieLuv other) + => this.AsVector3Unsafe() == other.AsVector3Unsafe(); + + private Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double ComputeU(in CieXyz source) + => (4 * source.X) / (source.X + (15 * source.Y) + (3 * source.Z)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double ComputeV(in CieXyz source) + => (9 * source.Y) / (source.X + (15 * source.Y) + (3 * source.Z)); + } +} diff --git a/ImageSharp/ColorProfiles/CieXyChromaticityCoordinates.cs b/ImageSharp/ColorProfiles/CieXyChromaticityCoordinates.cs new file mode 100644 index 0000000..c07e6e6 --- /dev/null +++ b/ImageSharp/ColorProfiles/CieXyChromaticityCoordinates.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Represents the coordinates of CIEXY chromaticity space. + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct CieXyChromaticityCoordinates : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// Chromaticity coordinate x (usually from 0 to 1) + /// Chromaticity coordinate y (usually from 0 to 1) + [MethodImpl(InliningOptions.ShortMethod)] + public CieXyChromaticityCoordinates(float x, float y) + { + this.X = x; + this.Y = y; + } + + /// + /// Gets the chromaticity X-coordinate. + /// + /// + /// Ranges usually from 0 to 1. + /// + public float X { get; } + + /// + /// Gets the chromaticity Y-coordinate + /// + /// + /// Ranges usually from 0 to 1. + /// + public float Y { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public static bool operator ==(CieXyChromaticityCoordinates left, CieXyChromaticityCoordinates right) + => left.Equals(right); + + /// + /// Compares two objects for inequality + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public static bool operator !=(CieXyChromaticityCoordinates left, CieXyChromaticityCoordinates right) + => !left.Equals(right); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public override int GetHashCode() + => HashCode.Combine(this.X, this.Y); + + /// + public override string ToString() + => FormattableString.Invariant($"CieXyChromaticityCoordinates({this.X:#0.##}, {this.Y:#0.##})"); + + /// + public override bool Equals(object? obj) + => obj is CieXyChromaticityCoordinates other && this.Equals(other); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool Equals(CieXyChromaticityCoordinates other) + => this.AsVector2Unsafe() == other.AsVector2Unsafe(); + + private Vector2 AsVector2Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this)); + } +} diff --git a/ImageSharp/ColorProfiles/CieXyy.cs b/ImageSharp/ColorProfiles/CieXyy.cs new file mode 100644 index 0000000..1c5f3e2 --- /dev/null +++ b/ImageSharp/ColorProfiles/CieXyy.cs @@ -0,0 +1,190 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Represents an CIE xyY 1931 color + /// + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct CieXyy : IColorProfile + { + /// + /// Initializes a new instance of the struct. + /// + /// The x chroma component. + /// The y chroma component. + /// The y luminance component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CieXyy(float x, float y, float yl) + { + // Not clamping as documentation about this space only indicates "usual" ranges + this.X = x; + this.Y = y; + this.Yl = yl; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the x, y, Y components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CieXyy(Vector3 vector) + { + // Not clamping as documentation about this space only indicates "usual" ranges + this.X = vector.X; + this.Y = vector.Y; + this.Yl = vector.Z; + } + + /// + /// Gets the X chrominance component. + /// A value usually ranging between 0 and 1. + /// + public float X { get; } + + /// + /// Gets the Y chrominance component. + /// A value usually ranging between 0 and 1. + /// + public float Y { get; } + + /// + /// Gets the Y luminance component. + /// A value usually ranging between 0 and 1. + /// + public float Yl { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(CieXyy left, CieXyy right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(CieXyy left, CieXyy right) => !left.Equals(right); + + /// + public Vector4 ToScaledVector4() + => new(this.AsVector3Unsafe(), 1F); + + /// + public static CieXyy FromScaledVector4(Vector4 source) + => new(source.AsVector3()); + + /// + public static void ToScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToScaledVector4(); + } + } + + /// + public static void FromScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = FromScaledVector4(source[i]); + } + } + + /// + public static CieXyy FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source) + { + float x = source.X / (source.X + source.Y + source.Z); + float y = source.Y / (source.X + source.Y + source.Z); + + if (float.IsNaN(x) || float.IsNaN(y)) + { + return new CieXyy(0, 0, source.Y); + } + + return new CieXyy(x, y, source.Y); + } + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + for (int i = 0; i < source.Length; i++) + { + CieXyz xyz = source[i]; + destination[i] = FromProfileConnectingSpace(options, in xyz); + } + } + + /// + public CieXyz ToProfileConnectingSpace(ColorConversionOptions options) + { + if (MathF.Abs(this.Y) < Constants.Epsilon) + { + return new CieXyz(0, 0, this.Yl); + } + + float x = (this.X * this.Yl) / this.Y; + float y = this.Yl; + float z = ((1 - this.X - this.Y) * y) / this.Y; + + return new CieXyz(x, y, z); + } + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + for (int i = 0; i < source.Length; i++) + { + CieXyy xyz = source[i]; + destination[i] = xyz.ToProfileConnectingSpace(options); + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() + => ChromaticAdaptionWhitePointSource.WhitePoint; + + /// + public override int GetHashCode() + => HashCode.Combine(this.X, this.Y, this.Yl); + + /// + public override string ToString() + => FormattableString.Invariant($"CieXyy({this.X:#0.##}, {this.Y:#0.##}, {this.Yl:#0.##})"); + + /// + public override bool Equals(object? obj) => obj is CieXyy other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(CieXyy other) + => this.AsVector3Unsafe() == other.AsVector3Unsafe(); + + private Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this)); + } +} diff --git a/ImageSharp/ColorProfiles/CieXyz.cs b/ImageSharp/ColorProfiles/CieXyz.cs new file mode 100644 index 0000000..3322232 --- /dev/null +++ b/ImageSharp/ColorProfiles/CieXyz.cs @@ -0,0 +1,204 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Represents an CIE XYZ 1931 color + /// + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct CieXyz : IProfileConnectingSpace + { + /// + /// Initializes a new instance of the struct. + /// + /// X is a mix (a linear combination) of cone response curves chosen to be nonnegative + /// The y luminance component. + /// Z is quasi-equal to blue stimulation, or the S cone of the human eye. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CieXyz(float x, float y, float z) + { + // Not clamping as documentation about this space only indicates "usual" ranges + this.X = x; + this.Y = y; + this.Z = z; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the x, y, z components. + public CieXyz(Vector3 vector) + { + this.X = vector.X; + this.Y = vector.Y; + this.Z = vector.Z; + } + + /// + /// Gets the X component. A mix (a linear combination) of cone response curves chosen to be nonnegative. + /// A value usually ranging between 0 and 1. + /// + public float X { get; } + + /// + /// Gets the Y luminance component. + /// A value usually ranging between 0 and 1. + /// + public float Y { get; } + + /// + /// Gets the Z component. Quasi-equal to blue stimulation, or the S cone response. + /// A value usually ranging between 0 and 1. + /// + public float Z { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(CieXyz left, CieXyz right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(CieXyz left, CieXyz right) => !left.Equals(right); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Vector3 ToVector3() => new(this.X, this.Y, this.Z); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Vector4 ToVector4() + { + Vector3 v3 = default; + v3 += this.AsVector3Unsafe(); + return new Vector4(v3, 1F); + } + + /// + public Vector4 ToScaledVector4() + { + Vector3 v3 = default; + v3 += this.AsVector3Unsafe(); + v3 *= 32768F / 65535; + return new Vector4(v3, 1F); + } + + internal static CieXyz FromVector4(Vector4 source) + { + Vector3 v3 = source.AsVector3(); + return new CieXyz(v3); + } + + /// + public static CieXyz FromScaledVector4(Vector4 source) + { + Vector3 v3 = source.AsVector3(); + v3 *= 65535 / 32768F; + return new CieXyz(v3); + } + + /// + public static void ToScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToScaledVector4(); + } + } + + /// + public static void FromScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = FromScaledVector4(source[i]); + } + } + + internal static void FromVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = FromVector4(source[i]); + } + } + + internal static void ToVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToVector4(); + } + } + + /// + public static CieXyz FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source) + => new(source.X, source.Y, source.Z); + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + source.CopyTo(destination[..source.Length]); + } + + /// + public CieXyz ToProfileConnectingSpace(ColorConversionOptions options) + => new(this.X, this.Y, this.Z); + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + source.CopyTo(destination[..source.Length]); + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() => ChromaticAdaptionWhitePointSource.WhitePoint; + + /// + public override int GetHashCode() => HashCode.Combine(this.X, this.Y, this.Z); + + /// + public override string ToString() => FormattableString.Invariant($"CieXyz({this.X:#0.##}, {this.Y:#0.##}, {this.Z:#0.##})"); + + /// + public override bool Equals(object? obj) => obj is CieXyz other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(CieXyz other) + => this.AsVector3Unsafe() == other.AsVector3Unsafe(); + + internal Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this)); + } +} diff --git a/ImageSharp/ColorProfiles/Cmyk.cs b/ImageSharp/ColorProfiles/Cmyk.cs new file mode 100644 index 0000000..332cf1c --- /dev/null +++ b/ImageSharp/ColorProfiles/Cmyk.cs @@ -0,0 +1,203 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Represents an CMYK (cyan, magenta, yellow, keyline) color. + /// + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct Cmyk : IColorProfile + { + private static readonly Vector4 Min = Vector4.Zero; + private static readonly Vector4 Max = Vector4.One; + + /// + /// Initializes a new instance of the struct. + /// + /// The cyan component. + /// The magenta component. + /// The yellow component. + /// The keyline black component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cmyk(float c, float m, float y, float k) + : this(new Vector4(c, m, y, k)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the c, m, y, k components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cmyk(Vector4 vector) + { + vector = Vector4.Clamp(vector, Min, Max); + this.C = vector.X; + this.M = vector.Y; + this.Y = vector.Z; + this.K = vector.W; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + private Cmyk(Vector4 vector, bool _) +#pragma warning restore SA1313 // Parameter names should begin with lower-case letter + { + this.C = vector.X; + this.M = vector.Y; + this.Y = vector.Z; + this.K = vector.W; + } + + /// + /// Gets the cyan color component. + /// A value ranging between 0 and 1. + /// + public float C { get; } + + /// + /// Gets the magenta color component. + /// A value ranging between 0 and 1. + /// + public float M { get; } + + /// + /// Gets the yellow color component. + /// A value ranging between 0 and 1. + /// + public float Y { get; } + + /// + /// Gets the keyline black color component. + /// A value ranging between 0 and 1. + /// + public float K { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Cmyk left, Cmyk right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Cmyk left, Cmyk right) => !left.Equals(right); + + /// + public Vector4 ToScaledVector4() + { + Vector4 v4 = default; + v4 += this.AsVector4Unsafe(); + return v4; + } + + /// + public static Cmyk FromScaledVector4(Vector4 source) + => new(source, true); + + /// + public static void ToScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + MemoryMarshal.Cast(source).CopyTo(destination); + } + + /// + public static void FromScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + MemoryMarshal.Cast(source).CopyTo(destination); + } + + /// + public static Cmyk FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source) + { + // To CMY + Vector3 cmy = Vector3.One - source.AsVector3Unsafe(); + + // To CMYK + Vector3 k = new(MathF.Min(cmy.X, MathF.Min(cmy.Y, cmy.Z))); + + if (k.X >= 1F - Constants.Epsilon) + { + return new Cmyk(0, 0, 0, 1F); + } + + cmy = (cmy - k) / (Vector3.One - k); + + return new Cmyk(cmy.X, cmy.Y, cmy.Z, k.X); + } + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: We can optimize this by using SIMD + for (int i = 0; i < source.Length; i++) + { + Rgb rgb = source[i]; + destination[i] = FromProfileConnectingSpace(options, in rgb); + } + } + + /// + public Rgb ToProfileConnectingSpace(ColorConversionOptions options) + { + Vector3 rgb = (Vector3.One - new Vector3(this.C, this.M, this.Y)) * (1F - this.K); + return Rgb.FromScaledVector3(rgb); + } + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + // TODO: We can possibly optimize this by using SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToProfileConnectingSpace(options); + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() + => ChromaticAdaptionWhitePointSource.RgbWorkingSpace; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() + => HashCode.Combine(this.C, this.M, this.Y, this.K); + + /// + public override string ToString() + => FormattableString.Invariant($"Cmyk({this.C:#0.##}, {this.M:#0.##}, {this.Y:#0.##}, {this.K:#0.##})"); + + /// + public override bool Equals(object? obj) + => obj is Cmyk other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Cmyk other) + => this.AsVector4Unsafe() == other.AsVector4Unsafe(); + + private Vector4 AsVector4Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this)); + } +} diff --git a/ImageSharp/ColorProfiles/ColorConversionOptions.cs b/ImageSharp/ColorProfiles/ColorConversionOptions.cs new file mode 100644 index 0000000..973b48d --- /dev/null +++ b/ImageSharp/ColorProfiles/ColorConversionOptions.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Provides options for color profile conversion. + /// + public class ColorConversionOptions + { + private Matrix4x4 adaptationMatrix; + private YCbCrTransform yCbCrTransform; + + /// + /// Initializes a new instance of the class. + /// + public ColorConversionOptions() + { + this.AdaptationMatrix = KnownChromaticAdaptationMatrices.Bradford; + this.YCbCrTransform = KnownYCbCrMatrices.BT601; + } + + /// + /// Gets the memory allocator. + /// + public MemoryAllocator MemoryAllocator { get; init; } = MemoryAllocator.Default; + + /// + /// Gets the source white point used for chromatic adaptation in conversions from/to XYZ color space. + /// + public CieXyz SourceWhitePoint { get; init; } = KnownIlluminants.D50; + + /// + /// Gets the destination white point used for chromatic adaptation in conversions from/to XYZ color space. + /// + public CieXyz TargetWhitePoint { get; init; } = KnownIlluminants.D50; + + /// + /// Gets the source working space used for companding in conversions from/to XYZ color space. + /// + public RgbWorkingSpace SourceRgbWorkingSpace { get; init; } = KnownRgbWorkingSpaces.SRgb; + + /// + /// Gets the destination working space used for companding in conversions from/to XYZ color space. + /// + public RgbWorkingSpace TargetRgbWorkingSpace { get; init; } = KnownRgbWorkingSpaces.SRgb; + + /// + /// Gets the YCbCr matrix to used to perform conversions from/to RGB. + /// + public YCbCrTransform YCbCrTransform + { + get => this.yCbCrTransform; + init + { + this.yCbCrTransform = value; + this.TransposedYCbCrTransform = value.Transpose(); + } + } + + /// + /// Gets the source ICC profile. + /// + public IccProfile? SourceIccProfile { get; init; } + + /// + /// Gets the target ICC profile. + /// + public IccProfile? TargetIccProfile { get; init; } + + /// + /// Gets the transformation matrix used in conversion to perform chromatic adaptation. + /// for further information. Default is Bradford. + /// + public Matrix4x4 AdaptationMatrix + { + get => this.adaptationMatrix; + init + { + this.adaptationMatrix = value; + _ = Matrix4x4.Invert(value, out Matrix4x4 inverted); + this.InverseAdaptationMatrix = inverted; + } + } + + internal YCbCrTransform TransposedYCbCrTransform { get; private set; } + + internal Matrix4x4 InverseAdaptationMatrix { get; private set; } + } +} diff --git a/ImageSharp/ColorProfiles/ColorProfileConverter.cs b/ImageSharp/ColorProfiles/ColorProfileConverter.cs new file mode 100644 index 0000000..3fcfcb5 --- /dev/null +++ b/ImageSharp/ColorProfiles/ColorProfileConverter.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Allows the conversion of color profiles. + /// + public class ColorProfileConverter + { + /// + /// Initializes a new instance of the class. + /// + public ColorProfileConverter() + : this(new ColorConversionOptions()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The color profile conversion options. + public ColorProfileConverter(ColorConversionOptions options) + => this.Options = options; + + /// + /// Gets the color profile conversion options. + /// + public ColorConversionOptions Options { get; } + + internal (CieXyz From, CieXyz To) GetChromaticAdaptionWhitePoints() + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + CieXyz sourceWhitePoint = TFrom.GetChromaticAdaptionWhitePointSource() == ChromaticAdaptionWhitePointSource.WhitePoint + ? this.Options.SourceWhitePoint + : this.Options.SourceRgbWorkingSpace.WhitePoint; + + CieXyz targetWhitePoint = TTo.GetChromaticAdaptionWhitePointSource() == ChromaticAdaptionWhitePointSource.WhitePoint + ? this.Options.TargetWhitePoint + : this.Options.TargetRgbWorkingSpace.WhitePoint; + + return (sourceWhitePoint, targetWhitePoint); + } + + internal bool ShouldUseIccProfiles() + => this.Options.SourceIccProfile != null && this.Options.TargetIccProfile != null; + } +} diff --git a/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs new file mode 100644 index 0000000..9a7a422 --- /dev/null +++ b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs @@ -0,0 +1,99 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Allows conversion between two color profiles based on the CIE Lab color space. + /// + public static class ColorProfileConverterExtensionsCieLabCieLab + { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. + public static TTo Convert(this ColorProfileConverter converter, in TFrom source) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + return converter.ConvertUsingIccProfile(source); + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS + CieLab pcsFromA = source.ToProfileConnectingSpace(options); + CieXyz pcsFromB = pcsFromA.ToProfileConnectingSpace(options); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + pcsFromB = VonKriesChromaticAdaptation.Transform(in pcsFromB, whitePoints, options.AdaptationMatrix); + + // Convert between PCS + CieLab pcsTo = CieLab.FromProfileConnectingSpace(options, in pcsFromB); + + // Convert to output from PCS + return TTo.FromProfileConnectingSpace(options, in pcsTo); + } + + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. + public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + converter.ConvertUsingIccProfile(source, destination); + return; + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS. + using IMemoryOwner pcsFromToOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFromTo = pcsFromToOwner.GetSpan(); + TFrom.ToProfileConnectionSpace(options, source, pcsFromTo); + + using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFrom = pcsFromOwner.GetSpan(); + CieLab.ToProfileConnectionSpace(options, pcsFromTo, pcsFrom); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + VonKriesChromaticAdaptation.Transform(pcsFrom, pcsFrom, whitePoints, options.AdaptationMatrix); + + // Convert between PCS. + CieLab.FromProfileConnectionSpace(options, pcsFrom, pcsFromTo); + + // Convert to output from PCS + TTo.FromProfileConnectionSpace(options, pcsFromTo, destination); + } + } +} diff --git a/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs new file mode 100644 index 0000000..5e7f5cf --- /dev/null +++ b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Allows conversion between two color profiles based on the CIE Lab and CIE XYZ color spaces. + /// + public static class ColorProfileConverterExtensionsCieLabCieXyz + { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. + public static TTo Convert(this ColorProfileConverter converter, in TFrom source) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + return converter.ConvertUsingIccProfile(source); + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS + CieLab pcsFrom = source.ToProfileConnectingSpace(options); + + // Convert between PCS + CieXyz pcsTo = pcsFrom.ToProfileConnectingSpace(options); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + pcsTo = VonKriesChromaticAdaptation.Transform(in pcsTo, whitePoints, options.AdaptationMatrix); + + // Convert to output from PCS + return TTo.FromProfileConnectingSpace(options, in pcsTo); + } + + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. + public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + converter.ConvertUsingIccProfile(source, destination); + return; + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS. + using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFrom = pcsFromOwner.GetSpan(); + TFrom.ToProfileConnectionSpace(options, source, pcsFrom); + + // Convert between PCS. + using IMemoryOwner pcsToOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsTo = pcsToOwner.GetSpan(); + CieLab.ToProfileConnectionSpace(options, pcsFrom, pcsTo); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + VonKriesChromaticAdaptation.Transform(pcsTo, pcsTo, whitePoints, options.AdaptationMatrix); + + // Convert to output from PCS + TTo.FromProfileConnectionSpace(options, pcsTo, destination); + } + } +} diff --git a/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs new file mode 100644 index 0000000..f2c7e05 --- /dev/null +++ b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Allows conversion between two color profiles based on the CIE Lab and RGB color spaces. + /// + public static class ColorProfileConverterExtensionsCieLabRgb + { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. + public static TTo Convert(this ColorProfileConverter converter, in TFrom source) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + return converter.ConvertUsingIccProfile(source); + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS + CieLab pcsFromA = source.ToProfileConnectingSpace(options); + CieXyz pcsFromB = pcsFromA.ToProfileConnectingSpace(options); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + pcsFromB = VonKriesChromaticAdaptation.Transform(in pcsFromB, whitePoints, options.AdaptationMatrix); + + // Convert between PCS + Rgb pcsTo = Rgb.FromProfileConnectingSpace(options, in pcsFromB); + + // Convert to output from PCS + return TTo.FromProfileConnectingSpace(options, in pcsTo); + } + + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. + public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + converter.ConvertUsingIccProfile(source, destination); + return; + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS. + using IMemoryOwner pcsFromAOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFromA = pcsFromAOwner.GetSpan(); + TFrom.ToProfileConnectionSpace(options, source, pcsFromA); + + using IMemoryOwner pcsFromBOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFromB = pcsFromBOwner.GetSpan(); + CieLab.ToProfileConnectionSpace(options, pcsFromA, pcsFromB); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + VonKriesChromaticAdaptation.Transform(pcsFromB, pcsFromB, whitePoints, options.AdaptationMatrix); + + // Convert between PCS. + using IMemoryOwner pcsToOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsTo = pcsToOwner.GetSpan(); + Rgb.FromProfileConnectionSpace(options, pcsFromB, pcsTo); + + // Convert to output from PCS + TTo.FromProfileConnectionSpace(options, pcsTo, destination); + } + } +} diff --git a/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs new file mode 100644 index 0000000..e7c66a0 --- /dev/null +++ b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Allows conversion between two color profiles based on the CIE XYZ and CIE Lab color spaces. + /// + public static class ColorProfileConverterExtensionsCieXyzCieLab + { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. + public static TTo Convert(this ColorProfileConverter converter, in TFrom source) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + return converter.ConvertUsingIccProfile(source); + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS + CieXyz pcsFrom = source.ToProfileConnectingSpace(options); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + pcsFrom = VonKriesChromaticAdaptation.Transform(in pcsFrom, whitePoints, options.AdaptationMatrix); + + // Convert between PCS + CieLab pcsTo = CieLab.FromProfileConnectingSpace(options, in pcsFrom); + + // Convert to output from PCS + return TTo.FromProfileConnectingSpace(options, in pcsTo); + } + + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. + public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + converter.ConvertUsingIccProfile(source, destination); + return; + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS. + using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFrom = pcsFromOwner.GetSpan(); + TFrom.ToProfileConnectionSpace(options, source, pcsFrom); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + VonKriesChromaticAdaptation.Transform(pcsFrom, pcsFrom, whitePoints, options.AdaptationMatrix); + + // Convert between PCS. + using IMemoryOwner pcsToOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsTo = pcsToOwner.GetSpan(); + CieLab.FromProfileConnectionSpace(options, pcsFrom, pcsTo); + + // Convert to output from PCS + TTo.FromProfileConnectionSpace(options, pcsTo, destination); + } + } +} diff --git a/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs new file mode 100644 index 0000000..ab4a26f --- /dev/null +++ b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Allows conversion between two color profiles based on the CIE XYZ color space. + /// + public static class ColorProfileConverterExtensionsCieXyzCieXyz + { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. + public static TTo Convert(this ColorProfileConverter converter, in TFrom source) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + return converter.ConvertUsingIccProfile(source); + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS + CieXyz pcsFrom = source.ToProfileConnectingSpace(options); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + pcsFrom = VonKriesChromaticAdaptation.Transform(in pcsFrom, whitePoints, options.AdaptationMatrix); + + // Convert to output from PCS + return TTo.FromProfileConnectingSpace(options, in pcsFrom); + } + + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. + public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + converter.ConvertUsingIccProfile(source, destination); + return; + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS. + using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFrom = pcsFromOwner.GetSpan(); + TFrom.ToProfileConnectionSpace(options, source, pcsFrom); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + VonKriesChromaticAdaptation.Transform(pcsFrom, pcsFrom, whitePoints, options.AdaptationMatrix); + + // Convert to output from PCS + TTo.FromProfileConnectionSpace(options, pcsFrom, destination); + } + } +} diff --git a/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs new file mode 100644 index 0000000..34fa76c --- /dev/null +++ b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Allows conversion between two color profiles based on the CIE XYZ and RGB color spaces. + /// + public static class ColorProfileConverterExtensionsCieXyzRgb + { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. + public static TTo Convert(this ColorProfileConverter converter, in TFrom source) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + return converter.ConvertUsingIccProfile(source); + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS + CieXyz pcsFrom = source.ToProfileConnectingSpace(options); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + pcsFrom = VonKriesChromaticAdaptation.Transform(in pcsFrom, whitePoints, options.AdaptationMatrix); + + // Convert between PCS + Rgb pcsTo = Rgb.FromProfileConnectingSpace(options, in pcsFrom); + + // Convert to output from PCS + return TTo.FromProfileConnectingSpace(options, in pcsTo); + } + + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. + public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + converter.ConvertUsingIccProfile(source, destination); + return; + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS. + using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFrom = pcsFromOwner.GetSpan(); + TFrom.ToProfileConnectionSpace(options, source, pcsFrom); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + VonKriesChromaticAdaptation.Transform(pcsFrom, pcsFrom, whitePoints, options.AdaptationMatrix); + + // Convert between PCS. + using IMemoryOwner pcsToOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsTo = pcsToOwner.GetSpan(); + Rgb.FromProfileConnectionSpace(options, pcsFrom, pcsTo); + + // Convert to output from PCS + TTo.FromProfileConnectionSpace(options, pcsTo, destination); + } + } +} diff --git a/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs new file mode 100644 index 0000000..9ff3498 --- /dev/null +++ b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs @@ -0,0 +1,773 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.ColorProfiles { + internal static class ColorProfileConverterExtensionsIcc + { + private static readonly float[] PcsV2FromBlackPointScale = + [0.9965153F, 0.9965269F, 0.9965208F, 1F, + 0.9965153F, 0.9965269F, 0.9965208F, 1F, + 0.9965153F, 0.9965269F, 0.9965208F, 1F, + 0.9965153F, 0.9965269F, 0.9965208F, 1F]; + + private static readonly float[] PcsV2FromBlackPointOffset = + [0.00336F, 0.0034731F, 0.00287F, 0F, + 0.00336F, 0.0034731F, 0.00287F, 0F, + 0.00336F, 0.0034731F, 0.00287F, 0F, + 0.00336F, 0.0034731F, 0.00287F, 0F]; + + private static readonly float[] PcsV2ToBlackPointScale = + [1.0034969F, 1.0034852F, 1.0034913F, 1F, + 1.0034969F, 1.0034852F, 1.0034913F, 1F, + 1.0034969F, 1.0034852F, 1.0034913F, 1F, + 1.0034969F, 1.0034852F, 1.0034913F, 1F]; + + private static readonly float[] PcsV2ToBlackPointOffset = + [0.0033717495F, 0.0034852044F, 0.0028800198F, 0F, + 0.0033717495F, 0.0034852044F, 0.0028800198F, 0F, + 0.0033717495F, 0.0034852044F, 0.0028800198F, 0F, + 0.0033717495F, 0.0034852044F, 0.0028800198F, 0F]; + + /// + /// Converts a color value from one ICC color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion using ICC profiles, ensuring accurate color mapping + /// between different color spaces. Both the source and target ICC profiles must be provided in the converter's + /// options. The method supports perceptual adjustments when required by the profiles. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter configured with source and target ICC profiles. + /// The color value to convert, defined in the source color profile. + /// + /// A color value in the target color profile, resulting from the ICC profile-based conversion of the source value. + /// + /// + /// Thrown if either the source or target ICC profile is missing from the converter options. + /// + internal static TTo ConvertUsingIccProfile(this ColorProfileConverter converter, in TFrom source) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + // TODO: Validation of ICC Profiles against color profile. Is this possible? + if (converter.Options.SourceIccProfile is null) + { + throw new InvalidOperationException("Source ICC profile is missing."); + } + + if (converter.Options.TargetIccProfile is null) + { + throw new InvalidOperationException("Target ICC profile is missing."); + } + + ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true); + ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false); + + ColorProfileConverter pcsConverter = new(new ColorConversionOptions + { + MemoryAllocator = converter.Options.MemoryAllocator, + SourceWhitePoint = KnownIlluminants.D50Icc, + TargetWhitePoint = KnownIlluminants.D50Icc + }); + + // Normalize the source, then convert to the PCS space. + Vector4 sourcePcs = sourceParams.Converter.Calculate(source.ToScaledVector4()); + + // If both profiles need PCS adjustment, they both share the same unadjusted PCS space + // cancelling out the need to make the adjustment + // except if using TRC transforms, which always requires perceptual handling + // TODO: this does not include adjustment for absolute intent, which would double existing complexity, suggest throwing exception and addressing in future update + bool anyProfileNeedsPerceptualAdjustment = sourceParams.HasNoPerceptualHandling || targetParams.HasNoPerceptualHandling; + bool oneProfileHasV2PerceptualAdjustment = sourceParams.HasV2PerceptualHandling ^ targetParams.HasV2PerceptualHandling; + + Vector4 targetPcs = anyProfileNeedsPerceptualAdjustment || oneProfileHasV2PerceptualAdjustment + ? GetTargetPcsWithPerceptualAdjustment(sourcePcs, sourceParams, targetParams, pcsConverter) + : GetTargetPcsWithoutAdjustment(sourcePcs, sourceParams, targetParams, pcsConverter); + + return TTo.FromScaledVector4(targetParams.Converter.Calculate(targetPcs)); + } + + /// + /// Converts a span of color values from a source color profile to a destination color profile using ICC profiles. + /// + /// + /// This method performs color conversion by transforming the input values through the Profile + /// Connection Space (PCS) as defined by the provided ICC profiles. Perceptual adjustments are applied as required + /// by the profiles. The method does not support absolute colorimetric intent and will not perform such + /// conversions. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter that provides conversion options and ICC profiles. + /// + /// A read-only span containing the source color values to convert. The values must conform to the source color + /// profile. + /// + /// + /// A span to receive the converted color values in the destination color profile. Must be at least as large as the + /// source span. + /// + /// + /// Thrown if the source or target ICC profile is missing from the converter options. + /// + internal static void ConvertUsingIccProfile(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + // TODO: Validation of ICC Profiles against color profile. Is this possible? + if (converter.Options.SourceIccProfile is null) + { + throw new InvalidOperationException("Source ICC profile is missing."); + } + + if (converter.Options.TargetIccProfile is null) + { + throw new InvalidOperationException("Target ICC profile is missing."); + } + + Guard.MustBeGreaterThanOrEqualTo(source.Length, destination.Length, nameof(destination)); + + ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true); + ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false); + + ColorProfileConverter pcsConverter = new(new ColorConversionOptions + { + MemoryAllocator = converter.Options.MemoryAllocator, + SourceWhitePoint = KnownIlluminants.D50Icc, + TargetWhitePoint = KnownIlluminants.D50Icc + }); + + using IMemoryOwner pcsBuffer = converter.Options.MemoryAllocator.Allocate(source.Length); + Span pcs = pcsBuffer.GetSpan(); + + // Normalize the source, then convert to the PCS space. + TFrom.ToScaledVector4(source, pcs); + sourceParams.Converter.Calculate(pcs, pcs); + + // If both profiles need PCS adjustment, they both share the same unadjusted PCS space + // cancelling out the need to make the adjustment + // except if using TRC transforms, which always requires perceptual handling + // TODO: this does not include adjustment for absolute intent, which would double existing complexity, suggest throwing exception and addressing in future update + bool anyProfileNeedsPerceptualAdjustment = sourceParams.HasNoPerceptualHandling || targetParams.HasNoPerceptualHandling; + bool oneProfileHasV2PerceptualAdjustment = sourceParams.HasV2PerceptualHandling ^ targetParams.HasV2PerceptualHandling; + + if (anyProfileNeedsPerceptualAdjustment || oneProfileHasV2PerceptualAdjustment) + { + GetTargetPcsWithPerceptualAdjustment(pcs, sourceParams, targetParams, pcsConverter); + } + else + { + GetTargetPcsWithoutAdjustment(pcs, sourceParams, targetParams, pcsConverter); + } + + // Convert to the target space. + targetParams.Converter.Calculate(pcs, pcs); + TTo.FromScaledVector4(pcs, destination); + } + + private static Vector4 GetTargetPcsWithoutAdjustment( + Vector4 sourcePcs, + ConversionParams sourceParams, + ConversionParams targetParams, + ColorProfileConverter pcsConverter) + { + // Profile connecting spaces can only be Lab, XYZ. + // 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version + // so ensure that Lab is using the correct encoding when a 16-bit LUT is used + switch (sourceParams.PcsType) + { + // Convert from Lab to XYZ. + case IccColorSpaceType.CieLab when targetParams.PcsType is IccColorSpaceType.CieXyz: + { + sourcePcs = sourceParams.Is16BitLutEntry ? LabV2ToLab(sourcePcs) : sourcePcs; + CieLab lab = CieLab.FromScaledVector4(sourcePcs); + CieXyz xyz = pcsConverter.Convert(in lab); + return xyz.ToScaledVector4(); + } + + // Convert from XYZ to Lab. + case IccColorSpaceType.CieXyz when targetParams.PcsType is IccColorSpaceType.CieLab: + { + CieXyz xyz = CieXyz.FromScaledVector4(sourcePcs); + CieLab lab = pcsConverter.Convert(in xyz); + Vector4 targetPcs = lab.ToScaledVector4(); + return targetParams.Is16BitLutEntry ? LabToLabV2(targetPcs) : targetPcs; + } + + // Convert from XYZ to XYZ. + case IccColorSpaceType.CieXyz when targetParams.PcsType is IccColorSpaceType.CieXyz: + { + CieXyz xyz = CieXyz.FromScaledVector4(sourcePcs); + CieXyz targetXyz = pcsConverter.Convert(in xyz); + return targetXyz.ToScaledVector4(); + } + + // Convert from Lab to Lab. + case IccColorSpaceType.CieLab when targetParams.PcsType is IccColorSpaceType.CieLab: + { + // if both source and target LUT use same v2 LAB encoding, no need to correct them + if (sourceParams.Is16BitLutEntry && targetParams.Is16BitLutEntry) + { + CieLab sourceLab = CieLab.FromScaledVector4(sourcePcs); + CieLab targetLab = pcsConverter.Convert(in sourceLab); + return targetLab.ToScaledVector4(); + } + else + { + sourcePcs = sourceParams.Is16BitLutEntry ? LabV2ToLab(sourcePcs) : sourcePcs; + CieLab sourceLab = CieLab.FromScaledVector4(sourcePcs); + CieLab targetLab = pcsConverter.Convert(in sourceLab); + Vector4 targetPcs = targetLab.ToScaledVector4(); + return targetParams.Is16BitLutEntry ? LabToLabV2(targetPcs) : targetPcs; + } + } + + default: + throw new ArgumentOutOfRangeException($"Source PCS {sourceParams.PcsType} to target PCS {targetParams.PcsType} is not supported"); + } + } + + private static void GetTargetPcsWithoutAdjustment( + Span pcs, + ConversionParams sourceParams, + ConversionParams targetParams, + ColorProfileConverter pcsConverter) + { + // Profile connecting spaces can only be Lab, XYZ. + // 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version + // so ensure that Lab is using the correct encoding when a 16-bit LUT is used + switch (sourceParams.PcsType) + { + // Convert from Lab to XYZ. + case IccColorSpaceType.CieLab when targetParams.PcsType is IccColorSpaceType.CieXyz: + { + if (sourceParams.Is16BitLutEntry) + { + LabV2ToLab(pcs, pcs); + } + + using IMemoryOwner pcsFromBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span pcsFrom = pcsFromBuffer.GetSpan(); + + using IMemoryOwner pcsToBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span pcsTo = pcsToBuffer.GetSpan(); + + CieLab.FromScaledVector4(pcs, pcsFrom); + pcsConverter.Convert(pcsFrom, pcsTo); + + CieXyz.ToScaledVector4(pcsTo, pcs); + break; + } + + // Convert from XYZ to Lab. + case IccColorSpaceType.CieXyz when targetParams.PcsType is IccColorSpaceType.CieLab: + { + using IMemoryOwner pcsFromBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span pcsFrom = pcsFromBuffer.GetSpan(); + + using IMemoryOwner pcsToBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span pcsTo = pcsToBuffer.GetSpan(); + + CieXyz.FromScaledVector4(pcs, pcsFrom); + pcsConverter.Convert(pcsFrom, pcsTo); + + CieLab.ToScaledVector4(pcsTo, pcs); + + if (targetParams.Is16BitLutEntry) + { + LabToLabV2(pcs, pcs); + } + + break; + } + + // Convert from XYZ to XYZ. + case IccColorSpaceType.CieXyz when targetParams.PcsType is IccColorSpaceType.CieXyz: + { + using IMemoryOwner pcsFromToBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span pcsFromTo = pcsFromToBuffer.GetSpan(); + + CieXyz.FromScaledVector4(pcs, pcsFromTo); + pcsConverter.Convert(pcsFromTo, pcsFromTo); + + CieXyz.ToScaledVector4(pcsFromTo, pcs); + break; + } + + // Convert from Lab to Lab. + case IccColorSpaceType.CieLab when targetParams.PcsType is IccColorSpaceType.CieLab: + { + using IMemoryOwner pcsFromToBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span pcsFromTo = pcsFromToBuffer.GetSpan(); + + // if both source and target LUT use same v2 LAB encoding, no need to correct them + if (sourceParams.Is16BitLutEntry && targetParams.Is16BitLutEntry) + { + CieLab.FromScaledVector4(pcs, pcsFromTo); + pcsConverter.Convert(pcsFromTo, pcsFromTo); + CieLab.ToScaledVector4(pcsFromTo, pcs); + } + else + { + if (sourceParams.Is16BitLutEntry) + { + LabV2ToLab(pcs, pcs); + } + + CieLab.FromScaledVector4(pcs, pcsFromTo); + pcsConverter.Convert(pcsFromTo, pcsFromTo); + CieLab.ToScaledVector4(pcsFromTo, pcs); + + if (targetParams.Is16BitLutEntry) + { + LabToLabV2(pcs, pcs); + } + } + + break; + } + + default: + throw new ArgumentOutOfRangeException($"Source PCS {sourceParams.PcsType} to target PCS {targetParams.PcsType} is not supported"); + } + } + + /// + /// Effectively this is with an extra step in the middle. + /// It adjusts PCS by compensating for the black point used for perceptual intent in v2 profiles. + /// The adjustment needs to be performed in XYZ space, potentially an overhead of 2 more conversions. + /// Not required if both spaces need V2 correction, since they both have the same understanding of the PCS. + /// Not compatible with PCS adjustment for absolute intent. + /// + /// The source PCS values. + /// The source profile parameters. + /// The target profile parameters. + /// The converter to use for the PCS adjustments. + /// Thrown when the source or target PCS is not supported. + private static Vector4 GetTargetPcsWithPerceptualAdjustment( + Vector4 sourcePcs, + ConversionParams sourceParams, + ConversionParams targetParams, + ColorProfileConverter pcsConverter) + { + // all conversions are funneled through XYZ in case PCS adjustments need to be made + CieXyz xyz; + + switch (sourceParams.PcsType) + { + // 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version + // so convert Lab to modern v4 encoding when returned from a 16-bit LUT + case IccColorSpaceType.CieLab: + sourcePcs = sourceParams.Is16BitLutEntry ? LabV2ToLab(sourcePcs) : sourcePcs; + CieLab lab = CieLab.FromScaledVector4(sourcePcs); + xyz = pcsConverter.Convert(in lab); + break; + case IccColorSpaceType.CieXyz: + xyz = CieXyz.FromScaledVector4(sourcePcs); + break; + default: + throw new ArgumentOutOfRangeException($"Source PCS {sourceParams.PcsType} is not supported"); + } + + bool oneProfileHasV2PerceptualAdjustment = sourceParams.HasV2PerceptualHandling ^ targetParams.HasV2PerceptualHandling; + + // when converting from device to PCS with v2 perceptual intent + // the black point needs to be adjusted to v4 after converting the PCS values + if (sourceParams.HasNoPerceptualHandling || + (oneProfileHasV2PerceptualAdjustment && sourceParams.HasV2PerceptualHandling)) + { + Vector3 vector = xyz.ToVector3(); + + // when using LAB PCS, negative values are clipped before PCS adjustment (in DemoIccMAX) + if (sourceParams.PcsType == IccColorSpaceType.CieLab) + { + vector = Vector3.Max(vector, Vector3.Zero); + } + + xyz = new CieXyz(AdjustPcsFromV2BlackPoint(vector)); + } + + // when converting from PCS to device with v2 perceptual intent + // the black point needs to be adjusted to v2 before converting the PCS values + if (targetParams.HasNoPerceptualHandling || + (oneProfileHasV2PerceptualAdjustment && targetParams.HasV2PerceptualHandling)) + { + Vector3 vector = AdjustPcsToV2BlackPoint(xyz.AsVector3Unsafe()); + + // when using XYZ PCS, negative values are clipped after PCS adjustment (in DemoIccMAX) + if (targetParams.PcsType == IccColorSpaceType.CieXyz) + { + vector = Vector3.Max(vector, Vector3.Zero); + } + + xyz = new CieXyz(vector); + } + + switch (targetParams.PcsType) + { + // 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version + // so convert Lab back to legacy encoding before using in a 16-bit LUT + case IccColorSpaceType.CieLab: + CieLab lab = pcsConverter.Convert(in xyz); + Vector4 targetPcs = lab.ToScaledVector4(); + return targetParams.Is16BitLutEntry ? LabToLabV2(targetPcs) : targetPcs; + case IccColorSpaceType.CieXyz: + return xyz.ToScaledVector4(); + default: + throw new ArgumentOutOfRangeException($"Target PCS {targetParams.PcsType} is not supported"); + } + } + + /// + /// Effectively this is with an extra step in the middle. + /// It adjusts PCS by compensating for the black point used for perceptual intent in v2 profiles. + /// The adjustment needs to be performed in XYZ space, potentially an overhead of 2 more conversions. + /// Not required if both spaces need V2 correction, since they both have the same understanding of the PCS. + /// Not compatible with PCS adjustment for absolute intent. + /// + /// The PCS values from the source. + /// The source profile parameters. + /// The target profile parameters. + /// The converter to use for the PCS adjustments. + /// Thrown when the source or target PCS is not supported. + private static void GetTargetPcsWithPerceptualAdjustment( + Span pcs, + ConversionParams sourceParams, + ConversionParams targetParams, + ColorProfileConverter pcsConverter) + { + // All conversions are funneled through XYZ in case PCS adjustments need to be made + using IMemoryOwner xyzBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span xyz = xyzBuffer.GetSpan(); + + switch (sourceParams.PcsType) + { + // 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version + // so convert Lab to modern v4 encoding when returned from a 16-bit LUT + case IccColorSpaceType.CieLab: + { + if (sourceParams.Is16BitLutEntry) + { + LabV2ToLab(pcs, pcs); + } + + using IMemoryOwner pcsFromBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span pcsFrom = pcsFromBuffer.GetSpan(); + CieLab.FromScaledVector4(pcs, pcsFrom); + pcsConverter.Convert(pcsFrom, xyz); + break; + } + + case IccColorSpaceType.CieXyz: + CieXyz.FromScaledVector4(pcs, xyz); + break; + default: + throw new ArgumentOutOfRangeException($"Source PCS {sourceParams.PcsType} is not supported"); + } + + bool oneProfileHasV2PerceptualAdjustment = sourceParams.HasV2PerceptualHandling ^ targetParams.HasV2PerceptualHandling; + + using IMemoryOwner vectorBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span vector = vectorBuffer.GetSpan(); + + // When converting from device to PCS with v2 perceptual intent + // the black point needs to be adjusted to v4 after converting the PCS values + if (sourceParams.HasNoPerceptualHandling || + (oneProfileHasV2PerceptualAdjustment && sourceParams.HasV2PerceptualHandling)) + { + CieXyz.ToVector4(xyz, vector); + + // When using LAB PCS, negative values are clipped before PCS adjustment (in DemoIccMAX) + if (sourceParams.PcsType == IccColorSpaceType.CieLab) + { + ClipNegative(vector); + } + + AdjustPcsFromV2BlackPoint(vector, vector); + CieXyz.FromVector4(vector, xyz); + } + + // When converting from PCS to device with v2 perceptual intent + // the black point needs to be adjusted to v2 before converting the PCS values + if (targetParams.HasNoPerceptualHandling || + (oneProfileHasV2PerceptualAdjustment && targetParams.HasV2PerceptualHandling)) + { + CieXyz.ToVector4(xyz, vector); + AdjustPcsToV2BlackPoint(vector, vector); + + // When using XYZ PCS, negative values are clipped after PCS adjustment (in DemoIccMAX) + if (targetParams.PcsType == IccColorSpaceType.CieXyz) + { + ClipNegative(vector); + } + + CieXyz.FromVector4(vector, xyz); + } + + switch (targetParams.PcsType) + { + // 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version + // so convert Lab back to legacy encoding before using in a 16-bit LUT + case IccColorSpaceType.CieLab: + { + using IMemoryOwner pcsToBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span pcsTo = pcsToBuffer.GetSpan(); + pcsConverter.Convert(xyz, pcsTo); + + CieLab.ToScaledVector4(pcsTo, pcs); + + if (targetParams.Is16BitLutEntry) + { + LabToLabV2(pcs, pcs); + } + + break; + } + + case IccColorSpaceType.CieXyz: + CieXyz.ToScaledVector4(xyz, pcs); + break; + default: + throw new ArgumentOutOfRangeException($"Target PCS {targetParams.PcsType} is not supported"); + } + } + + // as per DemoIccMAX icPerceptual values in IccCmm.h + // refBlack = 0.00336F, 0.0034731F, 0.00287F + // refWhite = 0.9642F, 1.0000F, 0.8249F + // scale = 1 - (refBlack / refWhite) + // offset = refBlack + private static Vector3 AdjustPcsFromV2BlackPoint(Vector3 xyz) + => (xyz * new Vector3(0.9965153F, 0.9965269F, 0.9965208F)) + new Vector3(0.00336F, 0.0034731F, 0.00287F); + + // as per DemoIccMAX icPerceptual values in IccCmm.h + // refBlack = 0.00336F, 0.0034731F, 0.00287F + // refWhite = 0.9642F, 1.0000F, 0.8249F + // scale = 1 / (1 - (refBlack / refWhite)) + // offset = -refBlack * scale + private static Vector3 AdjustPcsToV2BlackPoint(Vector3 xyz) + => (xyz * new Vector3(1.0034969F, 1.0034852F, 1.0034913F)) - new Vector3(0.0033717495F, 0.0034852044F, 0.0028800198F); + + private static void AdjustPcsFromV2BlackPoint(Span source, Span destination) + { + if (Vector.IsHardwareAccelerated && Vector.IsSupported && + Vector.Count <= Vector512.Count && + source.Length * 4 >= Vector.Count) + { + // TODO: Check our constants. They may require scaling. + Vector vScale = new(PcsV2FromBlackPointScale.AsSpan()[..Vector.Count]); + Vector vOffset = new(PcsV2FromBlackPointOffset.AsSpan()[..Vector.Count]); + + // SIMD loop + int i = 0; + int simdBatchSize = Vector.Count / 4; // Number of Vector4 elements per SIMD batch + for (; i <= source.Length - simdBatchSize; i += simdBatchSize) + { + // Load the vector from source span + Vector v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref source[i])); + + // Scale and offset the vector + v *= vScale; + v += vOffset; + + // Write the vector to the destination span + Unsafe.WriteUnaligned(ref Unsafe.As(ref destination[i]), v); + } + + // Scalar fallback for remaining elements + for (; i < source.Length; i++) + { + Vector4 s = source[i]; + s *= new Vector4(0.9965153F, 0.9965269F, 0.9965208F, 1F); + s += new Vector4(0.00336F, 0.0034731F, 0.00287F, 0F); + destination[i] = s; + } + } + else + { + // Scalar fallback if SIMD is not supported + for (int i = 0; i < source.Length; i++) + { + Vector4 s = source[i]; + s *= new Vector4(0.9965153F, 0.9965269F, 0.9965208F, 1F); + s += new Vector4(0.00336F, 0.0034731F, 0.00287F, 0F); + destination[i] = s; + } + } + } + + private static void AdjustPcsToV2BlackPoint(Span source, Span destination) + { + if (Vector.IsHardwareAccelerated && Vector.IsSupported && + Vector.Count <= Vector512.Count && + source.Length * 4 >= Vector.Count) + { + // TODO: Check our constants. They may require scaling. + Vector vScale = new(PcsV2ToBlackPointScale.AsSpan()[..Vector.Count]); + Vector vOffset = new(PcsV2ToBlackPointOffset.AsSpan()[..Vector.Count]); + + // SIMD loop + int i = 0; + int simdBatchSize = Vector.Count / 4; // Number of Vector4 elements per SIMD batch + for (; i <= source.Length - simdBatchSize; i += simdBatchSize) + { + // Load the vector from source span + Vector v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref source[i])); + + // Scale and offset the vector + v *= vScale; + v -= vOffset; + + // Write the vector to the destination span + Unsafe.WriteUnaligned(ref Unsafe.As(ref destination[i]), v); + } + + // Scalar fallback for remaining elements + for (; i < source.Length; i++) + { + Vector4 s = source[i]; + s *= new Vector4(1.0034969F, 1.0034852F, 1.0034913F, 1F); + s -= new Vector4(0.0033717495F, 0.0034852044F, 0.0028800198F, 0F); + destination[i] = s; + } + } + else + { + // Scalar fallback if SIMD is not supported + for (int i = 0; i < source.Length; i++) + { + Vector4 s = source[i]; + s *= new Vector4(1.0034969F, 1.0034852F, 1.0034913F, 1F); + s -= new Vector4(0.0033717495F, 0.0034852044F, 0.0028800198F, 0F); + destination[i] = s; + } + } + } + + private static void ClipNegative(Span source) + { + if (Vector.IsHardwareAccelerated && Vector.IsSupported && Vector.Count >= source.Length * 4) + { + // SIMD loop + int i = 0; + int simdBatchSize = Vector.Count / 4; // Number of Vector4 elements per SIMD batch + for (; i <= source.Length - simdBatchSize; i += simdBatchSize) + { + // Load the vector from source span + Vector v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref source[i])); + + v = Vector.Max(v, Vector.Zero); + + // Write the vector to the destination span + Unsafe.WriteUnaligned(ref Unsafe.As(ref source[i]), v); + } + + // Scalar fallback for remaining elements + for (; i < source.Length; i++) + { + ref Vector4 s = ref source[i]; + s = Vector4.Max(s, Vector4.Zero); + } + } + else + { + // Scalar fallback if SIMD is not supported + for (int i = 0; i < source.Length; i++) + { + ref Vector4 s = ref source[i]; + s = Vector4.Max(s, Vector4.Zero); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector4 LabToLabV2(Vector4 input) + => input * 65280F / 65535F; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector4 LabV2ToLab(Vector4 input) + => input * 65535F / 65280F; + + private static void LabToLabV2(Span source, Span destination) + => LabToLab(source, destination, 65280F / 65535F); + + private static void LabV2ToLab(Span source, Span destination) + => LabToLab(source, destination, 65535F / 65280F); + + private static void LabToLab(Span source, Span destination, [ConstantExpected] float scale) + { + if (Vector.IsHardwareAccelerated && Vector.IsSupported) + { + Vector vScale = new(scale); + int i = 0; + + // SIMD loop + int simdBatchSize = Vector.Count / 4; // Number of Vector4 elements per SIMD batch + for (; i <= source.Length - simdBatchSize; i += simdBatchSize) + { + // Load the vector from source span + Vector v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref source[i])); + + // Scale the vector + v *= vScale; + + // Write the scaled vector to the destination span + Unsafe.WriteUnaligned(ref Unsafe.As(ref destination[i]), v); + } + + // Scalar fallback for remaining elements + for (; i < source.Length; i++) + { + destination[i] = source[i] * scale; + } + } + else + { + // Scalar fallback if SIMD is not supported + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i] * scale; + } + } + } + + private class ConversionParams + { + private readonly IccProfile profile; + + internal ConversionParams(IccProfile profile, bool toPcs) + { + this.profile = profile; + this.Converter = toPcs ? new IccDataToPcsConverter(profile) : new IccPcsToDataConverter(profile); + } + + internal IccConverterBase Converter { get; } + + internal IccProfileHeader Header => this.profile.Header; + + internal IccRenderingIntent Intent => this.Header.RenderingIntent; + + internal IccColorSpaceType PcsType => this.Header.ProfileConnectionSpace; + + internal IccVersion Version => this.Header.Version; + + internal bool HasV2PerceptualHandling => this.Intent == IccRenderingIntent.Perceptual && this.Version.Major == 2; + + internal bool HasNoPerceptualHandling => this.Intent == IccRenderingIntent.Perceptual && this.Converter.IsTrc; + + internal bool Is16BitLutEntry => this.Converter.Is16BitLutEntry; + } + } +} diff --git a/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs new file mode 100644 index 0000000..1f909bf --- /dev/null +++ b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs @@ -0,0 +1,193 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.ColorProfiles { + internal static class ColorProfileConverterExtensionsPixelCompatible + { + /// + /// Converts the pixel data of the specified image from the source color profile to the target color profile using + /// the provided color profile converter. + /// + /// + /// This method modifies the source image in place by converting its pixel data according to the + /// color profiles specified in the converter. The method does not verify whether the profiles are RGB compatible; + /// if they are not, the conversion may produce incorrect results. Ensure that both the source and target ICC + /// profiles are set on the converter before calling this method. + /// + /// The pixel format. + /// The color profile converter configured with source and target ICC profiles. + /// + /// The image whose pixel data will be converted. The conversion is performed in place, modifying the original + /// image. + /// + /// + /// Thrown if the converter's source or target ICC profile is not specified. + /// + public static void Convert(this ColorProfileConverter converter, Image source) + where TPixel : unmanaged, IPixel + { + // These checks actually take place within the converter, but we want to fail fast here. + // Note. we do not check to see whether the profiles themselves are RGB compatible, + // if they are not, then the converter will simply produce incorrect results. + if (converter.Options.SourceIccProfile is null) + { + throw new InvalidOperationException("Source ICC profile is missing."); + } + + if (converter.Options.TargetIccProfile is null) + { + throw new InvalidOperationException("Target ICC profile is missing."); + } + + // Process the rows in parallel chunks, the converter itself is thread safe. + source.Mutate(o => o.ProcessPixelRowsAsVector4( + row => + { + // Gather and convert the pixels in the row to Rgb. + using IMemoryOwner rgbBuffer = converter.Options.MemoryAllocator.Allocate(row.Length); + Span rgbSpan = rgbBuffer.Memory.Span; + Rgb.FromScaledVector4(row, rgbSpan); + + // Perform the actual color conversion. + converter.ConvertUsingIccProfile(rgbSpan, rgbSpan); + + // Copy the converted Rgb pixels back to the row as TPixel. + // Important: Preserve alpha from the existing row Vector4 values. + // We merge RGB from rgbSpan into row, leaving W untouched. + ref float srcRgb = ref Unsafe.As(ref MemoryMarshal.GetReference(rgbSpan)); + ref float dstRow = ref Unsafe.As(ref MemoryMarshal.GetReference(row)); + + int count = rgbSpan.Length; + int i = 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static Vector512 ReadVector512(ref float f) + { + ref byte b = ref Unsafe.As(ref f); + return Unsafe.ReadUnaligned>(ref b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void WriteVector512(ref float f, Vector512 v) + { + ref byte b = ref Unsafe.As(ref f); + Unsafe.WriteUnaligned(ref b, v); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static Vector256 ReadVector256(ref float f) + { + ref byte b = ref Unsafe.As(ref f); + return Unsafe.ReadUnaligned>(ref b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void WriteVector256(ref float f, Vector256 v) + { + ref byte b = ref Unsafe.As(ref f); + Unsafe.WriteUnaligned(ref b, v); + } + + if (Avx512F.IsSupported) + { + // 4 pixels per iteration. + // + // Source layout (Rgb float stream, 12 floats): + // [r0 g0 b0 r1 g1 b1 r2 g2 b2 r3 g3 b3] + // + // Destination layout (row Vector4 float stream, 16 floats): + // [r0 g0 b0 a0 r1 g1 b1 a1 r2 g2 b2 a2 r3 g3 b3 a3] + // + // We use an overlapped load (16 floats) from the 3-float stride source. + // The permute selects the RGB we need and inserts placeholders for alpha lanes. + // + // Then we blend RGB lanes into the existing destination, preserving alpha lanes. + Vector512 rgbPerm = Vector512.Create(0, 1, 2, 0, 3, 4, 5, 0, 6, 7, 8, 0, 9, 10, 11, 0); + + // BlendVariable selects from the second operand where the sign bit of the mask lane is set. + // We want to overwrite lanes 0,1,2 then 4,5,6 then 8,9,10 then 12,13,14, and preserve lanes 3,7,11,15 (alpha). + Vector512 rgbSelect = Vector512.Create(-0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F); + + int quads = count >> 2; + int simdQuads = quads - 1; // Leave the last quad for the scalar tail to avoid the final overlapped load reading past the end. + + for (int q = 0; q < simdQuads; q++) + { + Vector512 dst = ReadVector512(ref dstRow); + Vector512 src = ReadVector512(ref srcRgb); + + Vector512 rgbx = Avx512F.PermuteVar16x32(src, rgbPerm); + Vector512 merged = Avx512F.BlendVariable(dst, rgbx, rgbSelect); + + WriteVector512(ref dstRow, merged); + + // Advance input by 4 pixels (4 * 3 = 12 floats) + srcRgb = ref Unsafe.Add(ref srcRgb, 12); + + // Advance output by 4 pixels (4 * 4 = 16 floats) + dstRow = ref Unsafe.Add(ref dstRow, 16); + + i += 4; + } + } + else if (Avx2.IsSupported) + { + // 2 pixels per iteration. + // + // Same idea as AVX-512, but on 256-bit vectors. + // We permute packed RGB into rgbx layout and blend into the existing destination, + // preserving alpha lanes. + Vector256 rgbPerm = Vector256.Create(0, 1, 2, 0, 3, 4, 5, 0); + + Vector256 rgbSelect = Vector256.Create(-0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F); + + int pairs = count >> 1; + int simdPairs = pairs - 1; // Leave the last pair for the scalar tail to avoid the final overlapped load reading past the end. + + for (int p = 0; p < simdPairs; p++) + { + Vector256 dst = ReadVector256(ref dstRow); + Vector256 src = ReadVector256(ref srcRgb); + + Vector256 rgbx = Avx2.PermuteVar8x32(src, rgbPerm); + Vector256 merged = Avx.BlendVariable(dst, rgbx, rgbSelect); + + WriteVector256(ref dstRow, merged); + + // Advance input by 2 pixels (2 * 3 = 6 floats) + srcRgb = ref Unsafe.Add(ref srcRgb, 6); + + // Advance output by 2 pixels (2 * 4 = 8 floats) + dstRow = ref Unsafe.Add(ref dstRow, 8); + + i += 2; + } + } + + // Scalar tail. + // Handles: + // - the last skipped SIMD block (quad or pair) + // - any remainder + // + // Preserve alpha by writing Vector3 into the Vector4 storage. + ref Vector4 rowRef = ref MemoryMarshal.GetReference(row); + for (; i < count; i++) + { + Vector3 rgb = rgbSpan[i].AsVector3Unsafe(); + Unsafe.As(ref Unsafe.Add(ref rowRef, (uint)i)) = rgb; + } + }, + PixelConversionModifiers.Scale)); + } + } +} diff --git a/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs new file mode 100644 index 0000000..ed2f4eb --- /dev/null +++ b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Allows conversion between two color profiles based on the RGB and CIE Lab color spaces. + /// + public static class ColorProfileConverterExtensionsRgbCieLab + { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. + public static TTo Convert(this ColorProfileConverter converter, in TFrom source) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + return converter.ConvertUsingIccProfile(source); + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS + Rgb pcsFromA = source.ToProfileConnectingSpace(options); + CieXyz pcsFromB = pcsFromA.ToProfileConnectingSpace(options); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + pcsFromB = VonKriesChromaticAdaptation.Transform(in pcsFromB, whitePoints, options.AdaptationMatrix); + + // Convert between PCS + CieLab pcsTo = CieLab.FromProfileConnectingSpace(options, in pcsFromB); + + // Convert to output from PCS + return TTo.FromProfileConnectingSpace(options, in pcsTo); + } + + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. + public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + converter.ConvertUsingIccProfile(source, destination); + return; + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS. + using IMemoryOwner pcsFromAOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFromA = pcsFromAOwner.GetSpan(); + TFrom.ToProfileConnectionSpace(options, source, pcsFromA); + + using IMemoryOwner pcsFromBOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFromB = pcsFromBOwner.GetSpan(); + Rgb.ToProfileConnectionSpace(options, pcsFromA, pcsFromB); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + VonKriesChromaticAdaptation.Transform(pcsFromB, pcsFromB, whitePoints, options.AdaptationMatrix); + + // Convert between PCS. + using IMemoryOwner pcsToOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsTo = pcsToOwner.GetSpan(); + CieLab.FromProfileConnectionSpace(options, pcsFromB, pcsTo); + + // Convert to output from PCS + TTo.FromProfileConnectionSpace(options, pcsTo, destination); + } + } +} diff --git a/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs new file mode 100644 index 0000000..778c8d2 --- /dev/null +++ b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Allows conversion between two color profiles based on the RGB and CIE XYZ color spaces. + /// + public static class ColorProfileConverterExtensionsRgbCieXyz + { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. + public static TTo Convert(this ColorProfileConverter converter, in TFrom source) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + return converter.ConvertUsingIccProfile(source); + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS + Rgb pcsFrom = source.ToProfileConnectingSpace(options); + + // Convert between PCS + CieXyz pcsTo = pcsFrom.ToProfileConnectingSpace(options); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + pcsTo = VonKriesChromaticAdaptation.Transform(in pcsTo, whitePoints, options.AdaptationMatrix); + + // Convert to output from PCS + return TTo.FromProfileConnectingSpace(options, in pcsTo); + } + + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. + public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + converter.ConvertUsingIccProfile(source, destination); + return; + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS. + using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFrom = pcsFromOwner.GetSpan(); + TFrom.ToProfileConnectionSpace(options, source, pcsFrom); + + // Convert between PCS. + using IMemoryOwner pcsToOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsTo = pcsToOwner.GetSpan(); + Rgb.ToProfileConnectionSpace(options, pcsFrom, pcsTo); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + VonKriesChromaticAdaptation.Transform(pcsTo, pcsTo, whitePoints, options.AdaptationMatrix); + + // Convert to output from PCS + TTo.FromProfileConnectionSpace(options, pcsTo, destination); + } + } +} diff --git a/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs new file mode 100644 index 0000000..accd827 --- /dev/null +++ b/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs @@ -0,0 +1,99 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Allows conversion between two color profiles based on the RGB color space. + /// + public static class ColorProfileConverterExtensionsRgbRgb + { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. + public static TTo Convert(this ColorProfileConverter converter, in TFrom source) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + return converter.ConvertUsingIccProfile(source); + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS + Rgb pcsFromA = source.ToProfileConnectingSpace(options); + CieXyz pcsFromB = pcsFromA.ToProfileConnectingSpace(options); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + pcsFromB = VonKriesChromaticAdaptation.Transform(in pcsFromB, whitePoints, options.AdaptationMatrix); + + // Convert between PCS + Rgb pcsTo = Rgb.FromProfileConnectingSpace(options, in pcsFromB); + + // Convert to output from PCS + return TTo.FromProfileConnectingSpace(options, in pcsTo); + } + + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. + public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + if (converter.ShouldUseIccProfiles()) + { + converter.ConvertUsingIccProfile(source, destination); + return; + } + + ColorConversionOptions options = converter.Options; + + // Convert to input PCS. + using IMemoryOwner pcsFromToOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFromTo = pcsFromToOwner.GetSpan(); + TFrom.ToProfileConnectionSpace(options, source, pcsFromTo); + + using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFrom = pcsFromOwner.GetSpan(); + Rgb.ToProfileConnectionSpace(options, pcsFromTo, pcsFrom); + + // Adapt to target white point + (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints(); + VonKriesChromaticAdaptation.Transform(pcsFrom, pcsFrom, whitePoints, options.AdaptationMatrix); + + // Convert between PCS. + Rgb.FromProfileConnectionSpace(options, pcsFrom, pcsFromTo); + + // Convert to output from PCS + TTo.FromProfileConnectionSpace(options, pcsFromTo, destination); + } + } +} diff --git a/ImageSharp/ColorProfiles/Companding/CompandingUtilities.cs b/ImageSharp/ColorProfiles/Companding/CompandingUtilities.cs new file mode 100644 index 0000000..655fb4e --- /dev/null +++ b/ImageSharp/ColorProfiles/Companding/CompandingUtilities.cs @@ -0,0 +1,183 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Concurrent; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.ColorProfiles.Companding { + /// + /// Companding utilities that allow the accelerated compression-expansion of color channels. + /// + public static class CompandingUtilities + { + private const int Length = Scale + 2; // 256kb @ 16bit precision. + private const int Scale = (1 << 16) - 1; + private static readonly ConcurrentDictionary<(Type, double), float[]> CompressLookupTables = new(); + private static readonly ConcurrentDictionary<(Type, double), float[]> ExpandLookupTables = new(); + + /// + /// Lazily creates and stores a companding compression lookup table using the given function and modifier. + /// + /// The type of companding function. + /// The companding function. + /// A modifier to pass to the function. + /// The array. + public static float[] GetCompressLookupTable(Func compandingFunction, double modifier = 0) + => CompressLookupTables.GetOrAdd((typeof(T), modifier), args => CreateLookupTableImpl(compandingFunction, args.Item2)); + + /// + /// Lazily creates and stores a companding expanding lookup table using the given function and modifier. + /// + /// The type of companding function. + /// The companding function. + /// A modifier to pass to the function. + /// The array. + public static float[] GetExpandLookupTable(Func compandingFunction, double modifier = 0) + => ExpandLookupTables.GetOrAdd((typeof(T), modifier), args => CreateLookupTableImpl(compandingFunction, args.Item2)); + + /// + /// Creates a companding lookup table using the given function. + /// + /// The companding function. + /// A modifier to pass to the function. + /// The array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float[] CreateLookupTableImpl(Func compandingFunction, double modifier = 0) + { + float[] result = new float[Length]; + + for (int i = 0; i < result.Length; i++) + { + double d = (double)i / Scale; + d = compandingFunction(d, modifier); + result[i] = (float)d; + } + + return result; + } + + /// + /// Performs the companding operation on the given vectors using the given table. + /// + /// The span of vectors. + /// The lookup table. + public static void Compand(Span vectors, float[] table) + { + DebugGuard.MustBeGreaterThanOrEqualTo(table.Length, Length, nameof(table)); + + if (Avx2.IsSupported && vectors.Length >= 2) + { + CompandAvx2(vectors, table); + + if (Numerics.Modulo2(vectors.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + ref Vector4 last = ref MemoryMarshal.GetReference(vectors[^1..]); + last = Compand(last, table); + } + } + else + { + CompandScalar(vectors, table); + } + } + + /// + /// Performs the companding operation on the given vector using the given table. + /// + /// The vector. + /// The lookup table. + /// The + public static Vector4 Compand(Vector4 vector, float[] table) + { + DebugGuard.MustBeGreaterThanOrEqualTo(table.Length, Length, nameof(table)); + + Vector4 zero = Vector4.Zero; + Vector4 scale = new(Scale); + + Vector4 multiplied = Numerics.Clamp(vector * Scale, zero, scale); + + float f0 = multiplied.X; + float f1 = multiplied.Y; + float f2 = multiplied.Z; + + uint i0 = (uint)f0; + uint i1 = (uint)f1; + uint i2 = (uint)f2; + + // Alpha is already a linear representation of opacity so we do not want to convert it. + vector.X = Numerics.Lerp(table[i0], table[i0 + 1], f0 - (int)i0); + vector.Y = Numerics.Lerp(table[i1], table[i1 + 1], f1 - (int)i1); + vector.Z = Numerics.Lerp(table[i2], table[i2 + 1], f2 - (int)i2); + + return vector; + } + + private static unsafe void CompandAvx2(Span vectors, float[] table) + { + fixed (float* tablePointer = &MemoryMarshal.GetArrayDataReference(table)) + { + Vector256 scale = Vector256.Create((float)Scale); + Vector256 zero = Vector256.Zero; + Vector256 offset = Vector256.Create(1); + + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 vectorsBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vectors)); + ref Vector256 vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length / 2u); + + while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast)) + { + Vector256 multiplied = Avx.Multiply(scale, vectorsBase); + multiplied = Avx.Min(Avx.Max(zero, multiplied), scale); + + Vector256 truncated = Avx.ConvertToVector256Int32WithTruncation(multiplied); + Vector256 truncatedF = Avx.ConvertToVector256Single(truncated); + + Vector256 low = Avx2.GatherVector256(tablePointer, truncated, sizeof(float)); + Vector256 high = Avx2.GatherVector256(tablePointer, Avx2.Add(truncated, offset), sizeof(float)); + + // Alpha is already a linear representation of opacity so we do not want to convert it. + Vector256 companded = Numerics.Lerp(low, high, Avx.Subtract(multiplied, truncatedF)); + vectorsBase = Avx.Blend(companded, vectorsBase, Numerics.BlendAlphaControl); + vectorsBase = ref Unsafe.Add(ref vectorsBase, 1); + } + } + } + + private static unsafe void CompandScalar(Span vectors, float[] table) + { + fixed (float* tablePointer = &MemoryMarshal.GetArrayDataReference(table)) + { + Vector4 zero = Vector4.Zero; + Vector4 scale = new(Scale); + ref Vector4 vectorsBase = ref MemoryMarshal.GetReference(vectors); + ref Vector4 vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length); + + while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast)) + { + Vector4 multiplied = Numerics.Clamp(vectorsBase * Scale, zero, scale); + + float f0 = multiplied.X; + float f1 = multiplied.Y; + float f2 = multiplied.Z; + + uint i0 = (uint)f0; + uint i1 = (uint)f1; + uint i2 = (uint)f2; + + // Alpha is already a linear representation of opacity so we do not want to convert it. + vectorsBase.X = Numerics.Lerp(tablePointer[i0], tablePointer[i0 + 1], f0 - (int)i0); + vectorsBase.Y = Numerics.Lerp(tablePointer[i1], tablePointer[i1 + 1], f1 - (int)i1); + vectorsBase.Z = Numerics.Lerp(tablePointer[i2], tablePointer[i2 + 1], f2 - (int)i2); + + vectorsBase = ref Unsafe.Add(ref vectorsBase, 1); + } + } + } + } +} diff --git a/ImageSharp/ColorProfiles/Companding/GammaCompanding.cs b/ImageSharp/ColorProfiles/Companding/GammaCompanding.cs new file mode 100644 index 0000000..4246a3e --- /dev/null +++ b/ImageSharp/ColorProfiles/Companding/GammaCompanding.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles.Companding { + /// + /// Implements gamma companding. + /// + /// + /// + /// + /// + public static class GammaCompanding + { + private static Func CompressFunction => (d, m) => Math.Pow(d, 1 / m); + + private static Func ExpandFunction => Math.Pow; + + /// + /// Compresses the linear vectors to their nonlinear equivalents with respect to the energy. + /// + /// The span of vectors. + /// The gamma value. + public static void Compress(Span vectors, double gamma) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetCompressLookupTable(CompressFunction, gamma)); + + /// + /// Expands the nonlinear vectors to their linear equivalents with respect to the energy. + /// + /// The span of vectors. + /// The gamma value. + public static void Expand(Span vectors, double gamma) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetExpandLookupTable(ExpandFunction, gamma)); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The gamma value. + /// The . + public static Vector4 Compress(Vector4 vector, double gamma) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetCompressLookupTable(CompressFunction, gamma)); + + /// + /// Expands the nonlinear vector to its linear equivalent with respect to the energy. + /// + /// The vector. + /// The gamma value. + /// The . + public static Vector4 Expand(Vector4 vector, double gamma) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetExpandLookupTable(ExpandFunction, gamma)); + + private class GammaCompandingKey; + } +} diff --git a/ImageSharp/ColorProfiles/Companding/LCompanding.cs b/ImageSharp/ColorProfiles/Companding/LCompanding.cs new file mode 100644 index 0000000..e7f14c9 --- /dev/null +++ b/ImageSharp/ColorProfiles/Companding/LCompanding.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles.Companding { + /// + /// Implements L* companding. + /// + /// + /// For more info see: + /// + /// + /// + public static class LCompanding + { + private static Func CompressFunction + => (d, _) => + { + if (d <= CieConstants.Epsilon) + { + return (d * CieConstants.Kappa) / 100; + } + + return (1.16 * Math.Pow(d, 0.3333333)) - 0.16; + }; + + private static Func ExpandFunction + => (d, _) => + { + if (d <= 0.08) + { + return (100 * d) / CieConstants.Kappa; + } + + return Numerics.Pow3(((float)(d + 0.16f)) / 1.16f); + }; + + /// + /// Compresses the linear vectors to their nonlinear equivalents with respect to the energy. + /// + /// The span of vectors. + public static void Compress(Span vectors) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetCompressLookupTable(CompressFunction)); + + /// + /// Expands the nonlinear vectors to their linear equivalents with respect to the energy. + /// + /// The span of vectors. + public static void Expand(Span vectors) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetExpandLookupTable(ExpandFunction)); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public static Vector4 Compress(Vector4 vector) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetCompressLookupTable(CompressFunction)); + + /// + /// Expands the nonlinear vector to its linear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public static Vector4 Expand(Vector4 vector) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetExpandLookupTable(ExpandFunction)); + + private class LCompandingKey; + } +} diff --git a/ImageSharp/ColorProfiles/Companding/Rec2020Companding.cs b/ImageSharp/ColorProfiles/Companding/Rec2020Companding.cs new file mode 100644 index 0000000..f4b829f --- /dev/null +++ b/ImageSharp/ColorProfiles/Companding/Rec2020Companding.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles.Companding { + /// + /// Implements Rec. 2020 companding function. + /// + /// + /// + /// + public static class Rec2020Companding + { + private const double Alpha = 1.09929682680944; + private const double AlphaMinusOne = Alpha - 1; + private const double Beta = 0.018053968510807; + private const double InverseBeta = Beta * 4.5; + private const double Epsilon = 1 / 0.45; + + private static Func CompressFunction + => (d, _) => + { + if (d < Beta) + { + return 4.5 * d; + } + + return (Alpha * Math.Pow(d, 0.45)) - AlphaMinusOne; + }; + + private static Func ExpandFunction + => (d, _) => + { + if (d < InverseBeta) + { + return d / 4.5; + } + + return Math.Pow((d + AlphaMinusOne) / Alpha, Epsilon); + }; + + /// + /// Compresses the linear vectors to their nonlinear equivalents with respect to the energy. + /// + /// The span of vectors. + public static void Compress(Span vectors) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetCompressLookupTable(CompressFunction)); + + /// + /// Expands the nonlinear vectors to their linear equivalents with respect to the energy. + /// + /// The span of vectors. + public static void Expand(Span vectors) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetExpandLookupTable(ExpandFunction)); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public static Vector4 Compress(Vector4 vector) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetCompressLookupTable(CompressFunction)); + + /// + /// Expands the nonlinear vector to its linear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public static Vector4 Expand(Vector4 vector) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetExpandLookupTable(ExpandFunction)); + + private class Rec2020CompandingKey; + } +} diff --git a/ImageSharp/ColorProfiles/Companding/Rec709Companding.cs b/ImageSharp/ColorProfiles/Companding/Rec709Companding.cs new file mode 100644 index 0000000..085b69e --- /dev/null +++ b/ImageSharp/ColorProfiles/Companding/Rec709Companding.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles.Companding { + /// + /// Implements the Rec. 709 companding function. + /// + /// + /// http://en.wikipedia.org/wiki/Rec._709 + /// + public static class Rec709Companding + { + private const double Epsilon = 1 / 0.45; + + private static Func CompressFunction + => (d, _) => + { + if (d < 0.018) + { + return 4.5 * d; + } + + return (1.099 * Math.Pow(d, 0.45)) - 0.099; + }; + + private static Func ExpandFunction + => (d, _) => + { + if (d < 0.081) + { + return d / 4.5; + } + + return Math.Pow((d + 0.099) / 1.099, Epsilon); + }; + + /// + /// Compresses the linear vectors to their nonlinear equivalents with respect to the energy. + /// + /// The span of vectors. + public static void Compress(Span vectors) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetCompressLookupTable(CompressFunction)); + + /// + /// Expands the nonlinear vectors to their linear equivalents with respect to the energy. + /// + /// The span of vectors. + public static void Expand(Span vectors) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetExpandLookupTable(ExpandFunction)); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public static Vector4 Compress(Vector4 vector) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetCompressLookupTable(CompressFunction)); + + /// + /// Expands the nonlinear vector to its linear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public static Vector4 Expand(Vector4 vector) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetExpandLookupTable(ExpandFunction)); + + private class Rec2020CompandingKey; + } +} diff --git a/ImageSharp/ColorProfiles/Companding/SRgbCompanding.cs b/ImageSharp/ColorProfiles/Companding/SRgbCompanding.cs new file mode 100644 index 0000000..894e440 --- /dev/null +++ b/ImageSharp/ColorProfiles/Companding/SRgbCompanding.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles.Companding { + /// + /// Implements sRGB companding. + /// + /// + /// For more info see: + /// + /// + /// + public static class SRgbCompanding + { + private static Func CompressFunction + => (d, _) => + { + if (d <= (0.04045 / 12.92)) + { + return d * 12.92; + } + + return (1.055 * Math.Pow(d, 1.0 / 2.4)) - 0.055; + }; + + private static Func ExpandFunction + => (d, _) => + { + if (d <= 0.04045) + { + return d / 12.92; + } + + return Math.Pow((d + 0.055) / 1.055, 2.4); + }; + + /// + /// Compresses the linear vectors to their nonlinear equivalents with respect to the energy. + /// + /// The span of vectors. + public static void Compress(Span vectors) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetCompressLookupTable(CompressFunction)); + + /// + /// Expands the nonlinear vectors to their linear equivalents with respect to the energy. + /// + /// The span of vectors. + public static void Expand(Span vectors) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetExpandLookupTable(ExpandFunction)); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public static Vector4 Compress(Vector4 vector) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetCompressLookupTable(CompressFunction)); + + /// + /// Expands the nonlinear vector to its linear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public static Vector4 Expand(Vector4 vector) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetExpandLookupTable(ExpandFunction)); + + private class SRgbCompandingKey; + } +} diff --git a/ImageSharp/ColorProfiles/Hsl.cs b/ImageSharp/ColorProfiles/Hsl.cs new file mode 100644 index 0000000..deb7369 --- /dev/null +++ b/ImageSharp/ColorProfiles/Hsl.cs @@ -0,0 +1,288 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Represents a Hsl (hue, saturation, lightness) color. + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct Hsl : IColorProfile + { + private static readonly Vector3 Min = Vector3.Zero; + private static readonly Vector3 Max = new(360, 1, 1); + + /// + /// Initializes a new instance of the struct. + /// + /// The h hue component. + /// The s saturation component. + /// The l value (lightness) component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Hsl(float h, float s, float l) + : this(new Vector3(h, s, l)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the h, s, l components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Hsl(Vector3 vector) + { + vector = Vector3.Clamp(vector, Min, Max); + this.H = vector.X; + this.S = vector.Y; + this.L = vector.Z; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + private Hsl(Vector3 vector, bool _) +#pragma warning restore SA1313 // Parameter names should begin with lower-case letter + { + this.H = vector.X; + this.S = vector.Y; + this.L = vector.Z; + } + + /// + /// Gets the hue component. + /// A value ranging between 0 and 360. + /// + public float H { get; } + + /// + /// Gets the saturation component. + /// A value ranging between 0 and 1. + /// + public float S { get; } + + /// + /// Gets the lightness component. + /// A value ranging between 0 and 1. + /// + public float L { get; } + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Hsl left, Hsl right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Hsl left, Hsl right) => !left.Equals(right); + + /// + public Vector4 ToScaledVector4() + => new(this.AsVector3Unsafe() / 360F, 1F); + + /// + public static Hsl FromScaledVector4(Vector4 source) + => new(source.AsVector3() * 360F, true); + + /// + public static void ToScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToScaledVector4(); + } + } + + /// + public static void FromScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = FromScaledVector4(source[i]); + } + } + + /// + public static Hsl FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source) + { + float r = source.R; + float g = source.G; + float b = source.B; + + float max = MathF.Max(r, MathF.Max(g, b)); + float min = MathF.Min(r, MathF.Min(g, b)); + float chroma = max - min; + float h = 0F; + float s = 0F; + float l = (max + min) / 2F; + + if (MathF.Abs(chroma) < Constants.Epsilon) + { + return new Hsl(0F, s, l); + } + + if (MathF.Abs(r - max) < Constants.Epsilon) + { + h = (g - b) / chroma; + } + else if (MathF.Abs(g - max) < Constants.Epsilon) + { + h = 2F + ((b - r) / chroma); + } + else if (MathF.Abs(b - max) < Constants.Epsilon) + { + h = 4F + ((r - g) / chroma); + } + + h *= 60F; + if (h < -Constants.Epsilon) + { + h += 360F; + } + + if (l <= .5F) + { + s = chroma / (max + min); + } + else + { + s = chroma / (2F - max - min); + } + + return new Hsl(h, s, l); + } + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + for (int i = 0; i < source.Length; i++) + { + Rgb rgb = source[i]; + destination[i] = FromProfileConnectingSpace(options, in rgb); + } + } + + /// + public Rgb ToProfileConnectingSpace(ColorConversionOptions options) + { + float rangedH = this.H / 360F; + float r = 0; + float g = 0; + float b = 0; + float s = this.S; + float l = this.L; + + if (MathF.Abs(l) > Constants.Epsilon) + { + if (MathF.Abs(s) < Constants.Epsilon) + { + r = g = b = l; + } + else + { + float temp2 = (l < .5F) ? l * (1F + s) : l + s - (l * s); + float temp1 = (2F * l) - temp2; + + r = GetColorComponent(temp1, temp2, rangedH + 0.3333333F); + g = GetColorComponent(temp1, temp2, rangedH); + b = GetColorComponent(temp1, temp2, rangedH - 0.3333333F); + } + } + + return new Rgb(r, g, b); + } + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + for (int i = 0; i < source.Length; i++) + { + Hsl hsl = source[i]; + destination[i] = hsl.ToProfileConnectingSpace(options); + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() + => ChromaticAdaptionWhitePointSource.RgbWorkingSpace; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => HashCode.Combine(this.H, this.S, this.L); + + /// + public override string ToString() => FormattableString.Invariant($"Hsl({this.H:#0.##}, {this.S:#0.##}, {this.L:#0.##})"); + + /// + public override bool Equals(object? obj) => obj is Hsl other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Hsl other) + => this.AsVector3Unsafe() == other.AsVector3Unsafe(); + + private Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float GetColorComponent(float first, float second, float third) + { + third = MoveIntoRange(third); + if (third < 0.1666667F) + { + return first + ((second - first) * 6F * third); + } + + if (third < .5F) + { + return second; + } + + if (third < 0.6666667F) + { + return first + ((second - first) * (0.6666667F - third) * 6F); + } + + return first; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float MoveIntoRange(float value) + { + if (value < 0F) + { + value++; + } + else if (value > 1F) + { + value--; + } + + return value; + } + } +} diff --git a/ImageSharp/ColorProfiles/Hsv.cs b/ImageSharp/ColorProfiles/Hsv.cs new file mode 100644 index 0000000..287b8b4 --- /dev/null +++ b/ImageSharp/ColorProfiles/Hsv.cs @@ -0,0 +1,274 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Represents a HSV (hue, saturation, value) color. Also known as HSB (hue, saturation, brightness). + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct Hsv : IColorProfile + { + private static readonly Vector3 Min = Vector3.Zero; + private static readonly Vector3 Max = new(360, 1, 1); + + /// + /// Initializes a new instance of the struct. + /// + /// The h hue component. + /// The s saturation component. + /// The v value (brightness) component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Hsv(float h, float s, float v) + : this(new Vector3(h, s, v)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the h, s, v components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Hsv(Vector3 vector) + { + vector = Vector3.Clamp(vector, Min, Max); + this.H = vector.X; + this.S = vector.Y; + this.V = vector.Z; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + private Hsv(Vector3 vector, bool _) +#pragma warning restore SA1313 // Parameter names should begin with lower-case letter + { + this.H = vector.X; + this.S = vector.Y; + this.V = vector.Z; + } + + /// + /// Gets the hue component. + /// A value ranging between 0 and 360. + /// + public float H { get; } + + /// + /// Gets the saturation component. + /// A value ranging between 0 and 1. + /// + public float S { get; } + + /// + /// Gets the value (brightness) component. + /// A value ranging between 0 and 1. + /// + public float V { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Hsv left, Hsv right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Hsv left, Hsv right) => !left.Equals(right); + + /// + public Vector4 ToScaledVector4() + => new(this.AsVector3Unsafe() / 360F, 1F); + + /// + public static Hsv FromScaledVector4(Vector4 source) + => new(source.AsVector3() * 360F, true); + + /// + public static void ToScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToScaledVector4(); + } + } + + /// + public static void FromScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = FromScaledVector4(source[i]); + } + } + + /// + public static Hsv FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source) + { + float r = source.R; + float g = source.G; + float b = source.B; + + float max = MathF.Max(r, MathF.Max(g, b)); + float min = MathF.Min(r, MathF.Min(g, b)); + float chroma = max - min; + float h = 0; + float s = 0; + float v = max; + + if (MathF.Abs(chroma) < Constants.Epsilon) + { + return new Hsv(0, s, v); + } + + if (MathF.Abs(r - max) < Constants.Epsilon) + { + h = (g - b) / chroma; + } + else if (MathF.Abs(g - max) < Constants.Epsilon) + { + h = 2 + ((b - r) / chroma); + } + else if (MathF.Abs(b - max) < Constants.Epsilon) + { + h = 4 + ((r - g) / chroma); + } + + h *= 60F; + if (h < -Constants.Epsilon) + { + h += 360F; + } + + s = chroma / v; + + return new Hsv(h, s, v); + } + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + for (int i = 0; i < source.Length; i++) + { + Rgb rgb = source[i]; + destination[i] = FromProfileConnectingSpace(options, in rgb); + } + } + + /// + public Rgb ToProfileConnectingSpace(ColorConversionOptions options) + { + float s = this.S; + float v = this.V; + + if (MathF.Abs(s) < Constants.Epsilon) + { + return new Rgb(v, v, v); + } + + float h = (MathF.Abs(this.H - 360) < Constants.Epsilon) ? 0 : this.H / 60; + int i = (int)Math.Truncate(h); + float f = h - i; + + float p = v * (1F - s); + float q = v * (1F - (s * f)); + float t = v * (1F - (s * (1F - f))); + + float r, g, b; + switch (i) + { + case 0: + r = v; + g = t; + b = p; + break; + + case 1: + r = q; + g = v; + b = p; + break; + + case 2: + r = p; + g = v; + b = t; + break; + + case 3: + r = p; + g = q; + b = v; + break; + + case 4: + r = t; + g = p; + b = v; + break; + + default: + r = v; + g = p; + b = q; + break; + } + + return new Rgb(r, g, b); + } + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + for (int i = 0; i < source.Length; i++) + { + Hsv hsv = source[i]; + destination[i] = hsv.ToProfileConnectingSpace(options); + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() + => ChromaticAdaptionWhitePointSource.RgbWorkingSpace; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => HashCode.Combine(this.H, this.S, this.V); + + /// + public override string ToString() => FormattableString.Invariant($"Hsv({this.H:#0.##}, {this.S:#0.##}, {this.V:#0.##})"); + + /// + public override bool Equals(object? obj) => obj is Hsv other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Hsv other) + => this.AsVector3Unsafe() == other.AsVector3Unsafe(); + + private Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this)); + } +} diff --git a/ImageSharp/ColorProfiles/HunterLab.cs b/ImageSharp/ColorProfiles/HunterLab.cs new file mode 100644 index 0000000..e8a26d2 --- /dev/null +++ b/ImageSharp/ColorProfiles/HunterLab.cs @@ -0,0 +1,244 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Represents an Hunter LAB color. + /// . + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct HunterLab : IColorProfile + { + /// + /// Initializes a new instance of the struct. + /// + /// The lightness dimension. + /// The a (green - magenta) component. + /// The b (blue - yellow) component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HunterLab(float l, float a, float b) + { + this.L = l; + this.A = a; + this.B = b; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the l a b components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HunterLab(Vector3 vector) + { + // Not clamping as documentation about this space only indicates "usual" ranges + this.L = vector.X; + this.A = vector.Y; + this.B = vector.Z; + } + + /// + /// Gets the lightness dimension. + /// A value usually ranging between 0 (black), 100 (diffuse white) or higher (specular white). + /// + public float L { get; } + + /// + /// Gets the a color component. + /// A value usually ranging from -100 to 100. Negative is green, positive magenta. + /// + public float A { get; } + + /// + /// Gets the b color component. + /// A value usually ranging from -100 to 100. Negative is blue, positive is yellow + /// + public float B { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + public static bool operator ==(HunterLab left, HunterLab right) => left.Equals(right); + + /// + /// Compares two objects for inequality + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(HunterLab left, HunterLab right) => !left.Equals(right); + + /// + public Vector4 ToScaledVector4() + { + Vector3 v3 = default; + v3 += this.AsVector3Unsafe(); + v3 += new Vector3(0, 128F, 128F); + v3 /= new Vector3(100F, 255F, 255F); + return new Vector4(v3, 1F); + } + + /// + public static HunterLab FromScaledVector4(Vector4 source) + { + Vector3 v3 = source.AsVector3(); + v3 *= new Vector3(100F, 255, 255); + v3 -= new Vector3(0, 128F, 128F); + return new HunterLab(v3); + } + + /// + public static void ToScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToScaledVector4(); + } + } + + /// + public static void FromScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = FromScaledVector4(source[i]); + } + } + + /// + public static HunterLab FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source) + { + // Conversion algorithm described here: + // http://en.wikipedia.org/wiki/Lab_color_space#Hunter_Lab + CieXyz whitePoint = options.TargetWhitePoint; + float x = source.X, y = source.Y, z = source.Z; + float xn = whitePoint.X, yn = whitePoint.Y, zn = whitePoint.Z; + + float ka = ComputeKa(in whitePoint); + float kb = ComputeKb(in whitePoint); + + float yByYn = y / yn; + float sqrtYbyYn = MathF.Sqrt(yByYn); + float l = 100 * sqrtYbyYn; + float a = ka * (((x / xn) - yByYn) / sqrtYbyYn); + float b = kb * ((yByYn - (z / zn)) / sqrtYbyYn); + + if (float.IsNaN(a)) + { + a = 0; + } + + if (float.IsNaN(b)) + { + b = 0; + } + + return new HunterLab(l, a, b); + } + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + for (int i = 0; i < source.Length; i++) + { + CieXyz xyz = source[i]; + destination[i] = FromProfileConnectingSpace(options, in xyz); + } + } + + /// + public CieXyz ToProfileConnectingSpace(ColorConversionOptions options) + { + // Conversion algorithm described here: + // http://en.wikipedia.org/wiki/Lab_color_space#Hunter_Lab + CieXyz whitePoint = options.SourceWhitePoint; + float l = this.L, a = this.A, b = this.B; + float xn = whitePoint.X, yn = whitePoint.Y, zn = whitePoint.Z; + + float ka = ComputeKa(in whitePoint); + float kb = ComputeKb(in whitePoint); + + float pow = Numerics.Pow2(l / 100F); + float sqrtPow = MathF.Sqrt(pow); + float y = pow * yn; + + float x = (((a / ka) * sqrtPow) + pow) * xn; + float z = (((b / kb) * sqrtPow) - pow) * (-zn); + + return new CieXyz(x, y, z); + } + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + for (int i = 0; i < source.Length; i++) + { + HunterLab lab = source[i]; + destination[i] = lab.ToProfileConnectingSpace(options); + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() + => ChromaticAdaptionWhitePointSource.WhitePoint; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => HashCode.Combine(this.L, this.A, this.B); + + /// + public override string ToString() => FormattableString.Invariant($"HunterLab({this.L:#0.##}, {this.A:#0.##}, {this.B:#0.##})"); + + /// + public override bool Equals(object? obj) => obj is HunterLab other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(HunterLab other) + => this.AsVector3Unsafe() == other.AsVector3Unsafe(); + + private Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float ComputeKa(in CieXyz whitePoint) + { + if (whitePoint.Equals(KnownIlluminants.C)) + { + return 175F; + } + + return 100F * (175F / 198.04F) * (whitePoint.X + whitePoint.Y); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float ComputeKb(in CieXyz whitePoint) + { + if (whitePoint == KnownIlluminants.C) + { + return 70F; + } + + return 100F * (70F / 218.11F) * (whitePoint.Y + whitePoint.Z); + } + } +} diff --git a/ImageSharp/ColorProfiles/IColorProfile.cs b/ImageSharp/ColorProfiles/IColorProfile.cs new file mode 100644 index 0000000..aaadced --- /dev/null +++ b/ImageSharp/ColorProfiles/IColorProfile.cs @@ -0,0 +1,104 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Defines the contract for all color profiles. + /// + public interface IColorProfile + { + /// + /// Gets the chromatic adaption white point source. + /// + /// The . + public static abstract ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource(); + } + + /// + /// Defines the contract for all color profiles. + /// + /// The type of color profile. + public interface IColorProfile : IColorProfile, IEquatable + where TSelf : IColorProfile + { + /// + /// Expands the pixel into a generic ("scaled") representation + /// with values scaled and clamped between 0 and 1. + /// The vector components are typically expanded in least to greatest significance order. + /// + /// The . + public Vector4 ToScaledVector4(); + +#pragma warning disable CA1000 // Do not declare static members on generic types + /// + /// Initializes the color instance from a generic a generic ("scaled") representation + /// with values scaled and clamped between 0 and 1. + /// + /// The vector to load the pixel from. + /// The . + public static abstract TSelf FromScaledVector4(Vector4 source); + + /// + /// Converts the span of colors to a generic ("scaled") representation + /// with values scaled and clamped between 0 and 1. + /// + /// The color span to convert from. + /// The vector span to write the results to. + public static abstract void ToScaledVector4(ReadOnlySpan source, Span destination); + + /// + /// Converts the span of colors from a generic ("scaled") representation + /// with values scaled and clamped between 0 and 1. + /// + /// The vector span to convert from. + /// The color span to write the results to. + public static abstract void FromScaledVector4(ReadOnlySpan source, Span destination); +#pragma warning restore CA1000 // Do not declare static members on generic types + } + + /// + /// Defines the contract for all color profiles. + /// + /// The type of color profile. + /// The type of color profile connecting space. + public interface IColorProfile : IColorProfile + where TSelf : IColorProfile + where TProfileSpace : struct, IProfileConnectingSpace + { +#pragma warning disable CA1000 // Do not declare static members on generic types + /// + /// Initializes the color instance from the profile connection space. + /// + /// The color profile conversion options. + /// The color profile connecting space. + /// The . + public static abstract TSelf FromProfileConnectingSpace(ColorConversionOptions options, in TProfileSpace source); + + /// + /// Converts the span of colors from the profile connection space. + /// + /// The color profile conversion options. + /// The color profile span to convert from. + /// The color span to write the results to. + public static abstract void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination); + + /// + /// Converts the color to the profile connection space. + /// + /// The color profile conversion options. + /// The . + public TProfileSpace ToProfileConnectingSpace(ColorConversionOptions options); + + /// + /// Converts the span of colors to the profile connection space. + /// + /// The color profile conversion options. + /// The color span to convert from. + /// The color profile span to write the results to. + public static abstract void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination); +#pragma warning restore CA1000 // Do not declare static members on generic types + } +} diff --git a/ImageSharp/ColorProfiles/IProfileConnectingSpace.cs b/ImageSharp/ColorProfiles/IProfileConnectingSpace.cs new file mode 100644 index 0000000..5c176db --- /dev/null +++ b/ImageSharp/ColorProfiles/IProfileConnectingSpace.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Defines the contract for all color profile connection spaces. + /// + public interface IProfileConnectingSpace; + + /// + /// Defines the contract for all color profile connection spaces. + /// + /// The type of color profile. + /// The type of color profile connecting space. + public interface IProfileConnectingSpace : IColorProfile, IProfileConnectingSpace + where TSelf : struct, IColorProfile, IProfileConnectingSpace + where TProfileSpace : struct, IProfileConnectingSpace; +} diff --git a/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs b/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs new file mode 100644 index 0000000..9dd253f --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs @@ -0,0 +1,507 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators { + /// + /// Implements interpolation methods for color profile lookup tables. + /// Adapted from ICC Reference implementation: + /// https://github.com/InternationalColorConsortium/DemoIccMAX/blob/79ecb74135ad47bac7d42692905a079839b7e105/IccProfLib/IccTagLut.cpp + /// + internal class ClutCalculator : IVector4Calculator + { + private readonly int inputCount; + private readonly int outputCount; + private readonly float[] lut; + private readonly byte[] gridPointCount; + private readonly byte[] maxGridPoint; + private readonly int[] indexFactor; + private readonly int[] dimSize; + private readonly int nodeCount; + private readonly float[][] nodes; + private readonly float[] g; + private readonly uint[] ig; + private readonly float[] s; + private readonly float[] df; + private readonly uint[] nPower; + private int n000; + private int n001; + private int n010; + private int n011; + private int n100; + private int n101; + private int n110; + private int n111; + private int n1000; + + public ClutCalculator(IccClut clut) + { + Guard.NotNull(clut, nameof(clut)); + Guard.MustBeGreaterThan(clut.InputChannelCount, 0, nameof(clut.InputChannelCount)); + Guard.MustBeGreaterThan(clut.OutputChannelCount, 0, nameof(clut.OutputChannelCount)); + + this.inputCount = clut.InputChannelCount; + this.outputCount = clut.OutputChannelCount; + this.g = new float[this.inputCount]; + this.ig = new uint[this.inputCount]; + this.s = new float[this.inputCount]; + this.nPower = new uint[16]; + this.lut = clut.Values; + this.nodeCount = (int)Math.Pow(2, clut.InputChannelCount); + this.df = new float[this.nodeCount]; + this.nodes = new float[this.nodeCount][]; + this.dimSize = new int[this.inputCount]; + this.gridPointCount = clut.GridPointCount; + this.maxGridPoint = new byte[this.inputCount]; + for (int i = 0; i < this.inputCount; i++) + { + this.maxGridPoint[i] = (byte)(this.gridPointCount[i] - 1); + } + + this.dimSize[this.inputCount - 1] = this.outputCount; + for (int i = this.inputCount - 2; i >= 0; i--) + { + this.dimSize[i] = this.dimSize[i + 1] * this.gridPointCount[i + 1]; + } + + this.indexFactor = this.CalculateIndexFactor(); + } + + public unsafe Vector4 Calculate(Vector4 value) + { + Vector4 result = default; + switch (this.inputCount) + { + case 1: + this.Interpolate1d((float*)&value, (float*)&result); + break; + case 2: + this.Interpolate2d((float*)&value, (float*)&result); + break; + case 3: + this.Interpolate3d((float*)&value, (float*)&result); + break; + case 4: + this.Interpolate4d((float*)&value, (float*)&result); + break; + default: + this.InterpolateNd((float*)&value, (float*)&result); + break; + } + + return result; + } + + private int[] CalculateIndexFactor() + { + int[] factors = new int[16]; + switch (this.inputCount) + { + case 1: + factors[0] = this.n000 = 0; + factors[1] = this.n001 = this.dimSize[0]; + break; + case 2: + factors[0] = this.n000 = 0; + factors[1] = this.n001 = this.dimSize[0]; + factors[2] = this.n010 = this.dimSize[1]; + factors[3] = this.n011 = this.n001 + this.n010; + break; + case 3: + factors[0] = this.n000 = 0; + factors[1] = this.n001 = this.dimSize[0]; + factors[2] = this.n010 = this.dimSize[1]; + factors[3] = this.n011 = this.n001 + this.n010; + factors[4] = this.n100 = this.dimSize[2]; + factors[5] = this.n101 = this.n100 + this.n001; + factors[6] = this.n110 = this.n100 + this.n010; + factors[7] = this.n111 = this.n110 + this.n001; + break; + case 4: + factors[0] = 0; + factors[1] = this.n001 = this.dimSize[0]; + factors[2] = this.n010 = this.dimSize[1]; + factors[3] = factors[2] + factors[1]; + factors[4] = this.n100 = this.dimSize[2]; + factors[5] = factors[4] + factors[1]; + factors[6] = factors[4] + factors[2]; + factors[7] = factors[4] + factors[3]; + factors[8] = this.n1000 = this.dimSize[3]; + factors[9] = factors[8] + factors[1]; + factors[10] = factors[8] + factors[2]; + factors[11] = factors[8] + factors[3]; + factors[12] = factors[8] + factors[4]; + factors[13] = factors[8] + factors[5]; + factors[14] = factors[8] + factors[6]; + factors[15] = factors[8] + factors[7]; + break; + default: + // Initialize ND interpolation variables. + factors[0] = 0; + int count; + for (count = 0; count < this.inputCount; count++) + { + this.nPower[count] = (uint)(1 << (this.inputCount - 1 - count)); + } + + uint[] nPower = [0, 1]; + count = 0; + int nFlag = 1; + for (uint j = 1; j < this.nodeCount; j++) + { + if (j == nPower[1]) + { + factors[j] = this.dimSize[count]; + nPower[0] = (uint)(1 << count); + count++; + nPower[1] = (uint)(1 << count); + nFlag = 1; + } + else + { + factors[j] = factors[nPower[0]] + factors[nFlag]; + nFlag++; + } + } + + break; + } + + return factors; + } + + /// + /// One dimensional interpolation function. + /// + /// The input pixel values, which will be interpolated. + /// The interpolated output pixels. + private unsafe void Interpolate1d(float* srcPixel, float* destPixel) + { + byte mx = this.maxGridPoint[0]; + + float x = UnitClip(srcPixel[0]) * mx; + + uint ix = (uint)x; + + float u = x - ix; + + if (ix == mx) + { + ix--; + u = 1.0f; + } + + float nu = (float)(1.0 - u); + + int i; + Span p = this.lut.AsSpan((int)(ix * this.n001)); + + // Normalize grid units. + float dF0 = nu; + float dF1 = u; + + int offset = 0; + for (i = 0; i < this.outputCount; i++) + { + destPixel[i] = (float)((p[offset + this.n000] * dF0) + (p[offset + this.n001] * dF1)); + offset++; + } + } + + /// + /// Two dimensional interpolation function. + /// + /// The input pixel values, which will be interpolated. + /// The interpolated output pixels. + private unsafe void Interpolate2d(float* srcPixel, float* destPixel) + { + byte mx = this.maxGridPoint[0]; + byte my = this.maxGridPoint[1]; + + float x = UnitClip(srcPixel[0]) * mx; + float y = UnitClip(srcPixel[1]) * my; + + uint ix = (uint)x; + uint iy = (uint)y; + + float u = x - ix; + float t = y - iy; + + if (ix == mx) + { + ix--; + u = 1.0f; + } + + if (iy == my) + { + iy--; + t = 1.0f; + } + + float nt = (float)(1.0 - t); + float nu = (float)(1.0 - u); + + int i; + Span p = this.lut.AsSpan((int)((ix * this.n001) + (iy * this.n010))); + + // Normalize grid units. + float dF0 = nt * nu; + float dF1 = nt * u; + float dF2 = t * nu; + float dF3 = t * u; + + int offset = 0; + for (i = 0; i < this.outputCount; i++) + { + destPixel[i] = (float)((p[offset + this.n000] * dF0) + (p[offset + this.n001] * dF1) + (p[offset + this.n010] * dF2) + (p[offset + this.n011] * dF3)); + offset++; + } + } + + /// + /// Three dimensional interpolation function. + /// + /// The input pixel values, which will be interpolated. + /// The interpolated output pixels. + private unsafe void Interpolate3d(float* srcPixel, float* destPixel) + { + byte mx = this.maxGridPoint[0]; + byte my = this.maxGridPoint[1]; + byte mz = this.maxGridPoint[2]; + + float x = UnitClip(srcPixel[0]) * mx; + float y = UnitClip(srcPixel[1]) * my; + float z = UnitClip(srcPixel[2]) * mz; + + uint ix = (uint)x; + uint iy = (uint)y; + uint iz = (uint)z; + + float u = x - ix; + float t = y - iy; + float s = z - iz; + + if (ix == mx) + { + ix--; + u = 1.0f; + } + + if (iy == my) + { + iy--; + t = 1.0f; + } + + if (iz == mz) + { + iz--; + s = 1.0f; + } + + float ns = (float)(1.0 - s); + float nt = (float)(1.0 - t); + float nu = (float)(1.0 - u); + + Span p = this.lut.AsSpan((int)((ix * this.n001) + (iy * this.n010) + (iz * this.n100))); + + // Normalize grid units + float dF0 = ns * nt * nu; + float dF1 = ns * nt * u; + float dF2 = ns * t * nu; + float dF3 = ns * t * u; + float dF4 = s * nt * nu; + float dF5 = s * nt * u; + float dF6 = s * t * nu; + float dF7 = s * t * u; + + int offset = 0; + for (int i = 0; i < this.outputCount; i++) + { + destPixel[i] = (float)((p[offset + this.n000] * dF0) + + (p[offset + this.n001] * dF1) + + (p[offset + this.n010] * dF2) + + (p[offset + this.n011] * dF3) + + (p[offset + this.n100] * dF4) + + (p[offset + this.n101] * dF5) + + (p[offset + this.n110] * dF6) + + (p[offset + this.n111] * dF7)); + offset++; + } + } + + /// + /// Four dimensional interpolation function. + /// + /// The input pixel values, which will be interpolated. + /// The interpolated output pixels. + private unsafe void Interpolate4d(float* srcPixel, float* destPixel) + { + byte mw = this.maxGridPoint[0]; + byte mx = this.maxGridPoint[1]; + byte my = this.maxGridPoint[2]; + byte mz = this.maxGridPoint[3]; + + float w = UnitClip(srcPixel[0]) * mw; + float x = UnitClip(srcPixel[1]) * mx; + float y = UnitClip(srcPixel[2]) * my; + float z = UnitClip(srcPixel[3]) * mz; + + uint iw = (uint)w; + uint ix = (uint)x; + uint iy = (uint)y; + uint iz = (uint)z; + + float v = w - iw; + float u = x - ix; + float t = y - iy; + float s = z - iz; + + if (iw == mw) + { + iw--; + v = 1.0f; + } + + if (ix == mx) + { + ix--; + u = 1.0f; + } + + if (iy == my) + { + iy--; + t = 1.0f; + } + + if (iz == mz) + { + iz--; + s = 1.0f; + } + + float ns = (float)(1.0 - s); + float nt = (float)(1.0 - t); + float nu = (float)(1.0 - u); + float nv = (float)(1.0 - v); + + Span p = this.lut.AsSpan((int)((iw * this.n001) + (ix * this.n010) + (iy * this.n100) + (iz * this.n1000))); + + // Normalize grid units. + float[] dF = + [ + ns * nt * nu * nv, + ns * nt * nu * v, + ns * nt * u * nv, + ns * nt * u * v, + ns * t * nu * nv, + ns * t * nu * v, + ns * t * u * nv, + ns * t * u * v, + s * nt * nu * nv, + s * nt * nu * v, + s * nt * u * nv, + s * nt * u * v, + s * t * nu * nv, + s * t * nu * v, + s * t * u * nv, + s * t * u * v, + ]; + + int offset = 0; + for (int i = 0; i < this.outputCount; i++) + { + float pv = 0.0f; + for (int j = 0; j < 16; j++) + { + pv += p[offset + this.indexFactor[j]] * dF[j]; + } + + destPixel[i] = pv; + offset++; + } + } + + /// + /// Generic N-dimensional interpolation function. + /// + /// The input pixel values, which will be interpolated. + /// The interpolated output pixels. + private unsafe void InterpolateNd(float* srcPixel, float* destPixel) + { + int index = 0; + for (int i = 0; i < this.inputCount; i++) + { + this.g[i] = UnitClip(srcPixel[i]) * this.maxGridPoint[i]; + this.ig[i] = (uint)this.g[i]; + this.s[this.inputCount - 1 - i] = this.g[i] - this.ig[i]; + if (this.ig[i] == this.maxGridPoint[i]) + { + this.ig[i]--; + this.s[this.inputCount - 1 - i] = 1.0f; + } + + index += (int)this.ig[i] * this.dimSize[i]; + } + + Span p = this.lut.AsSpan(index); + float[] temp = new float[2]; + bool nFlag = false; + + for (int i = 0; i < this.nodeCount; i++) + { + this.df[i] = 1.0f; + } + + for (int i = 0; i < this.inputCount; i++) + { + temp[0] = 1.0f - this.s[i]; + temp[1] = this.s[i]; + index = (int)this.nPower[i]; + for (int j = 0; j < this.nodeCount; j++) + { + this.df[j] *= temp[nFlag ? 1 : 0]; + if ((j + 1) % index == 0) + { + nFlag = !nFlag; + } + } + + nFlag = false; + } + + int offset = 0; + for (int i = 0; i < this.outputCount; i++) + { + float pv = 0; + for (int j = 0; j < this.nodeCount; j++) + { + pv += p[offset + this.indexFactor[j]] * this.df[j]; + } + + destPixel[i] = pv; + offset++; + } + } + + private static float UnitClip(float v) + { + if (v < 0) + { + return 0; + } + + if (v > 1.0) + { + return 1.0f; + } + + return v; + } + } +} diff --git a/ImageSharp/ColorProfiles/Icc/Calculators/ColorTrcCalculator.cs b/ImageSharp/ColorProfiles/Icc/Calculators/ColorTrcCalculator.cs new file mode 100644 index 0000000..d916d6d --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/Calculators/ColorTrcCalculator.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators { + internal class ColorTrcCalculator : IVector4Calculator + { + private readonly TrcCalculator curveCalculator; + private readonly Matrix4x4 matrix; + private readonly bool toPcs; + + public ColorTrcCalculator( + IccXyzTagDataEntry redMatrixColumn, + IccXyzTagDataEntry greenMatrixColumn, + IccXyzTagDataEntry blueMatrixColumn, + IccTagDataEntry redTrc, + IccTagDataEntry greenTrc, + IccTagDataEntry blueTrc, + bool toPcs) + { + this.toPcs = toPcs; + this.curveCalculator = new TrcCalculator([redTrc, greenTrc, blueTrc], !toPcs); + + Vector3 mr = redMatrixColumn.Data[0]; + Vector3 mg = greenMatrixColumn.Data[0]; + Vector3 mb = blueMatrixColumn.Data[0]; + this.matrix = new Matrix4x4(mr.X, mr.Y, mr.Z, 0, mg.X, mg.Y, mg.Z, 0, mb.X, mb.Y, mb.Z, 0, 0, 0, 0, 1); + + if (!toPcs) + { + Matrix4x4.Invert(this.matrix, out this.matrix); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 Calculate(Vector4 value) + { + if (this.toPcs) + { + // input is always linear RGB + value = this.curveCalculator.Calculate(value); + CieXyz xyz = new(Vector4.Transform(value, this.matrix).AsVector3()); + + // when data to PCS, output from calculator is descaled XYZ + // but downstream process requires scaled XYZ + // (see DemoMaxICC IccCmm.cpp : CIccXformMatrixTRC::Apply) + return xyz.ToScaledVector4(); + } + else + { + // input is always XYZ + Vector4 xyz = Vector4.Transform(value, this.matrix); + + // when data to PCS, upstream process provides scaled XYZ + // but input to calculator is descaled XYZ + // (see DemoMaxICC IccCmm.cpp : CIccXformMatrixTRC::Apply) + xyz = new Vector4(CieXyz.FromScaledVector4(xyz).AsVector3Unsafe(), 1); + return this.curveCalculator.Calculate(xyz); + } + } + } +} diff --git a/ImageSharp/ColorProfiles/Icc/Calculators/CurveCalculator.CalculationType.cs b/ImageSharp/ColorProfiles/Icc/Calculators/CurveCalculator.CalculationType.cs new file mode 100644 index 0000000..195d6bb --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/Calculators/CurveCalculator.CalculationType.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc { + internal partial class CurveCalculator + { + private enum CalculationType + { + Identity, + Gamma, + Lut, + } + } +} diff --git a/ImageSharp/ColorProfiles/Icc/Calculators/CurveCalculator.cs b/ImageSharp/ColorProfiles/Icc/Calculators/CurveCalculator.cs new file mode 100644 index 0000000..19ad546 --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/Calculators/CurveCalculator.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using SixLabors.ImageSharp.ColorProfiles.Icc.Calculators; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using System; + +namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc { + internal partial class CurveCalculator : ISingleCalculator + { + private readonly LutCalculator lutCalculator; + private readonly float gamma; + private readonly CalculationType type; + + public CurveCalculator(IccCurveTagDataEntry entry, bool inverted) + { + if (entry.IsIdentityResponse) + { + this.type = CalculationType.Identity; + } + else if (entry.IsGamma) + { + this.gamma = entry.Gamma; + if (inverted) + { + this.gamma = 1f / this.gamma; + } + + this.type = CalculationType.Gamma; + } + else + { + this.lutCalculator = new LutCalculator(entry.CurveData, inverted); + this.type = CalculationType.Lut; + } + } + + public float Calculate(float value) + => this.type switch + { + CalculationType.Identity => value, + CalculationType.Gamma => MathF.Pow(value, this.gamma), // TODO: This could be optimized using a LUT. See SrgbCompanding + CalculationType.Lut => this.lutCalculator.Calculate(value), + _ => throw new InvalidOperationException("Invalid calculation type"), + }; + } +} diff --git a/ImageSharp/ColorProfiles/Icc/Calculators/GrayTrcCalculator.cs b/ImageSharp/ColorProfiles/Icc/Calculators/GrayTrcCalculator.cs new file mode 100644 index 0000000..5e1cf71 --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/Calculators/GrayTrcCalculator.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators { + internal class GrayTrcCalculator : IVector4Calculator + { + private readonly TrcCalculator calculator; + + public GrayTrcCalculator(IccTagDataEntry grayTrc, bool toPcs) + => this.calculator = new TrcCalculator(new IccTagDataEntry[] { grayTrc }, !toPcs); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 Calculate(Vector4 value) => this.calculator.Calculate(value); + } +} diff --git a/ImageSharp/ColorProfiles/Icc/Calculators/ISingleCalculator.cs b/ImageSharp/ColorProfiles/Icc/Calculators/ISingleCalculator.cs new file mode 100644 index 0000000..1664421 --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/Calculators/ISingleCalculator.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators { + /// + /// Represents an ICC calculator with a single floating point value and result + /// + internal interface ISingleCalculator + { + /// + /// Calculates a result from the given value + /// + /// The input value + /// The calculated result + float Calculate(float value); + } +} diff --git a/ImageSharp/ColorProfiles/Icc/Calculators/IVector4Calculator.cs b/ImageSharp/ColorProfiles/Icc/Calculators/IVector4Calculator.cs new file mode 100644 index 0000000..b3dd14b --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/Calculators/IVector4Calculator.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators { + /// + /// Represents an ICC calculator with values and results + /// + internal interface IVector4Calculator + { + /// + /// Calculates a result from the given values + /// + /// The input values + /// The calculated result + Vector4 Calculate(Vector4 value); + } +} diff --git a/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs b/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs new file mode 100644 index 0000000..96937cc --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc { + internal partial class LutABCalculator + { + /// + /// Identifies the transform direction for the configured LUT calculator. + /// + private enum CalculationType + { + /// + /// Converts from device space to PCS using ICC mAB stage order. + /// + AtoB, + + /// + /// Converts from PCS to device space using ICC mBA stage order. + /// + BtoA, + } + } +} diff --git a/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs b/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs new file mode 100644 index 0000000..5bf157f --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs @@ -0,0 +1,159 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles.Icc.Calculators; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc { + internal partial class LutABCalculator : IVector4Calculator + { + private CalculationType type; + private TrcCalculator curveACalculator; + private TrcCalculator curveBCalculator; + private TrcCalculator curveMCalculator; + private MatrixCalculator matrixCalculator; + private ClutCalculator clutCalculator; + + /// + /// Initializes a new instance of the class for an ICC mAB transform. + /// + /// The parsed A-to-B LUT entry. + public LutABCalculator(IccLutAToBTagDataEntry entry) + { + Guard.NotNull(entry, nameof(entry)); + this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues); + this.type = CalculationType.AtoB; + } + + /// + /// Initializes a new instance of the class for an ICC mBA transform. + /// + /// The parsed B-to-A LUT entry. + public LutABCalculator(IccLutBToATagDataEntry entry) + { + Guard.NotNull(entry, nameof(entry)); + this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues); + this.type = CalculationType.BtoA; + } + + /// + /// Calculates the transformed value by applying the configured ICC LUT stages in specification order. + /// + /// The input value. + /// The transformed value. + public Vector4 Calculate(Vector4 value) + { + switch (this.type) + { + case CalculationType.AtoB: + // ICC mAB order: A, CLUT, M, Matrix, B. + if (this.curveACalculator != null) + { + value = this.curveACalculator.Calculate(value); + } + + if (this.clutCalculator != null) + { + value = this.clutCalculator.Calculate(value); + } + + if (this.curveMCalculator != null) + { + value = this.curveMCalculator.Calculate(value); + } + + if (this.matrixCalculator != null) + { + value = this.matrixCalculator.Calculate(value); + } + + if (this.curveBCalculator != null) + { + value = this.curveBCalculator.Calculate(value); + } + + return value; + + case CalculationType.BtoA: + // ICC mBA order: B, Matrix, M, CLUT, A. + if (this.curveBCalculator != null) + { + value = this.curveBCalculator.Calculate(value); + } + + if (this.matrixCalculator != null) + { + value = this.matrixCalculator.Calculate(value); + } + + if (this.curveMCalculator != null) + { + value = this.curveMCalculator.Calculate(value); + } + + if (this.clutCalculator != null) + { + value = this.clutCalculator.Calculate(value); + } + + if (this.curveACalculator != null) + { + value = this.curveACalculator.Calculate(value); + } + + return value; + + default: + throw new InvalidOperationException("Invalid calculation type"); + } + } + + /// + /// Creates calculators for the processing stages present in the LUT entry. + /// + /// + /// The tag entry classes already validate channel continuity, so this method only materializes the available stages. + /// + private void Init(IccTagDataEntry[] curveA, IccTagDataEntry[] curveB, IccTagDataEntry[] curveM, Vector3? matrix3x1, Matrix4x4? matrix3x3, IccClut clut) + { + bool hasACurve = curveA != null; + bool hasBCurve = curveB != null; + bool hasMCurve = curveM != null; + bool hasMatrix = matrix3x1 != null && matrix3x3 != null; + bool hasClut = clut != null; + + Guard.IsTrue( + hasACurve || hasBCurve || hasMCurve || hasMatrix || hasClut, + "entry", + "AToB or BToA tag must contain at least one processing element"); + + if (hasACurve) + { + this.curveACalculator = new TrcCalculator(curveA, false); + } + + if (hasBCurve) + { + this.curveBCalculator = new TrcCalculator(curveB, false); + } + + if (hasMCurve) + { + this.curveMCalculator = new TrcCalculator(curveM, false); + } + + if (hasMatrix) + { + this.matrixCalculator = new MatrixCalculator(matrix3x3.Value, matrix3x1.Value); + } + + if (hasClut) + { + this.clutCalculator = new ClutCalculator(clut); + } + } + } +} diff --git a/ImageSharp/ColorProfiles/Icc/Calculators/LutCalculator.cs b/ImageSharp/ColorProfiles/Icc/Calculators/LutCalculator.cs new file mode 100644 index 0000000..1190df6 --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/Calculators/LutCalculator.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators { + internal class LutCalculator : ISingleCalculator + { + private readonly float[] lut; + private readonly bool inverse; + + public LutCalculator(float[] lut, bool inverse) + { + Guard.NotNull(lut, nameof(lut)); + + this.lut = lut; + this.inverse = inverse; + } + + public float Calculate(float value) + { + if (this.inverse) + { + return this.LookupInverse(value); + } + + return this.Lookup(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float Lookup(float value) + { + value = Math.Max(value, 0); + + float factor = value * (this.lut.Length - 1); + int index = (int)factor; + float low = this.lut[index]; + + float high = 1F; + if (index < this.lut.Length - 1) + { + high = this.lut[index + 1]; + } + + return low + ((high - low) * (factor - index)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float LookupInverse(float value) + { + int index = Array.BinarySearch(this.lut, value); + if (index >= 0) + { + return index / (float)(this.lut.Length - 1); + } + + index = ~index; + if (index == 0) + { + return 0; + } + else if (index == this.lut.Length) + { + return 1; + } + + float high = this.lut[index]; + float low = this.lut[index - 1]; + + float valuePercent = (value - low) / (high - low); + float lutRange = 1 / (float)(this.lut.Length - 1); + float lutLow = (index - 1) / (float)(this.lut.Length - 1); + + return lutLow + (valuePercent * lutRange); + } + } +} diff --git a/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs b/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs new file mode 100644 index 0000000..5f4eefd --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators { + internal class LutEntryCalculator : IVector4Calculator + { + private LutCalculator[] inputCurve; + private LutCalculator[] outputCurve; + private ClutCalculator clutCalculator; + private Matrix4x4 matrix; + private bool doTransform; + + public LutEntryCalculator(IccLut8TagDataEntry lut) + { + Guard.NotNull(lut, nameof(lut)); + this.Init(lut.InputValues, lut.OutputValues, lut.ClutValues, lut.Matrix); + this.Is16Bit = false; + } + + public LutEntryCalculator(IccLut16TagDataEntry lut) + { + Guard.NotNull(lut, nameof(lut)); + this.Init(lut.InputValues, lut.OutputValues, lut.ClutValues, lut.Matrix); + this.Is16Bit = true; + } + + internal bool Is16Bit { get; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 Calculate(Vector4 value) + { + if (this.doTransform) + { + value = Vector4.Transform(value, this.matrix); + } + + value = CalculateLut(this.inputCurve, value); + value = this.clutCalculator.Calculate(value); + return CalculateLut(this.outputCurve, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector4 CalculateLut(LutCalculator[] lut, Vector4 value) + { + ref float f = ref Unsafe.As(ref value); + for (int i = 0; i < lut.Length; i++) + { + Unsafe.Add(ref f, i) = lut[i].Calculate(Unsafe.Add(ref f, i)); + } + + return value; + } + + private void Init(IccLut[] inputCurve, IccLut[] outputCurve, IccClut clut, Matrix4x4 matrix) + { + this.inputCurve = InitLut(inputCurve); + this.outputCurve = InitLut(outputCurve); + this.clutCalculator = new ClutCalculator(clut); + this.matrix = matrix; + + this.doTransform = !matrix.IsIdentity && inputCurve.Length == 3; + } + + private static LutCalculator[] InitLut(IccLut[] curves) + { + LutCalculator[] calculators = new LutCalculator[curves.Length]; + for (int i = 0; i < curves.Length; i++) + { + calculators[i] = new LutCalculator(curves[i].Values, false); + } + + return calculators; + } + } +} diff --git a/ImageSharp/ColorProfiles/Icc/Calculators/MatrixCalculator.cs b/ImageSharp/ColorProfiles/Icc/Calculators/MatrixCalculator.cs new file mode 100644 index 0000000..26eeaa3 --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/Calculators/MatrixCalculator.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators { + internal class MatrixCalculator : IVector4Calculator + { + private Matrix4x4 matrix2D; + private Vector4 matrix1D; + + public MatrixCalculator(Matrix4x4 matrix3x3, Vector3 matrix3x1) + { + this.matrix2D = matrix3x3; + this.matrix1D = new Vector4(matrix3x1, 0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 Calculate(Vector4 value) + { + Vector4 transformed = Vector4.Transform(value, this.matrix2D); + return Vector4.Add(this.matrix1D, transformed); + } + } +} diff --git a/ImageSharp/ColorProfiles/Icc/Calculators/ParametricCurveCalculator.cs b/ImageSharp/ColorProfiles/Icc/Calculators/ParametricCurveCalculator.cs new file mode 100644 index 0000000..be727d0 --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/Calculators/ParametricCurveCalculator.cs @@ -0,0 +1,131 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators { + internal class ParametricCurveCalculator : ISingleCalculator + { + private readonly IccParametricCurve curve; + private readonly IccParametricCurveType type; + private const IccParametricCurveType InvertedFlag = (IccParametricCurveType)(1 << 3); + + public ParametricCurveCalculator(IccParametricCurveTagDataEntry entry, bool inverted) + { + Guard.NotNull(entry, nameof(entry)); + this.curve = entry.Curve; + this.type = entry.Curve.Type; + + if (inverted) + { + this.type |= InvertedFlag; + } + } + + public float Calculate(float value) + => this.type switch + { + IccParametricCurveType.Type1 => this.CalculateGamma(value), + IccParametricCurveType.Cie122_1996 => this.CalculateCie122(value), + IccParametricCurveType.Iec61966_3 => this.CalculateIec61966(value), + IccParametricCurveType.SRgb => this.CalculateSRgb(value), + IccParametricCurveType.Type5 => this.CalculateType5(value), + IccParametricCurveType.Type1 | InvertedFlag => this.CalculateInvertedGamma(value), + IccParametricCurveType.Cie122_1996 | InvertedFlag => this.CalculateInvertedCie122(value), + IccParametricCurveType.Iec61966_3 | InvertedFlag => this.CalculateInvertedIec61966(value), + IccParametricCurveType.SRgb | InvertedFlag => this.CalculateInvertedSRgb(value), + IccParametricCurveType.Type5 | InvertedFlag => this.CalculateInvertedType5(value), + _ => throw new InvalidIccProfileException("ParametricCurve"), + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float CalculateGamma(float value) => MathF.Pow(value, this.curve.G); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float CalculateCie122(float value) + { + if (value >= -this.curve.B / this.curve.A) + { + return MathF.Pow((this.curve.A * value) + this.curve.B, this.curve.G); + } + + return 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float CalculateIec61966(float value) + { + if (value >= -this.curve.B / this.curve.A) + { + return MathF.Pow((this.curve.A * value) + this.curve.B, this.curve.G) + this.curve.C; + } + + return this.curve.C; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float CalculateSRgb(float value) + { + if (value >= this.curve.D) + { + return MathF.Pow((this.curve.A * value) + this.curve.B, this.curve.G); + } + + return this.curve.C * value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float CalculateType5(float value) + { + if (value >= this.curve.D) + { + return MathF.Pow((this.curve.A * value) + this.curve.B, this.curve.G) + this.curve.E; + } + + return (this.curve.C * value) + this.curve.F; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float CalculateInvertedGamma(float value) + => MathF.Pow(value, 1 / this.curve.G); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float CalculateInvertedCie122(float value) + => (MathF.Pow(value, 1 / this.curve.G) - this.curve.B) / this.curve.A; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float CalculateInvertedIec61966(float value) + { + if (value >= this.curve.C) + { + return (MathF.Pow(value - this.curve.C, 1 / this.curve.G) - this.curve.B) / this.curve.A; + } + + return -this.curve.B / this.curve.A; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float CalculateInvertedSRgb(float value) + { + if (value >= MathF.Pow((this.curve.A * this.curve.D) + this.curve.B, this.curve.G)) + { + return (MathF.Pow(value, 1 / this.curve.G) - this.curve.B) / this.curve.A; + } + + return value / this.curve.C; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float CalculateInvertedType5(float value) + { + if (value >= (this.curve.C * this.curve.D) + this.curve.F) + { + return (MathF.Pow(value - this.curve.E, 1 / this.curve.G) - this.curve.B) / this.curve.A; + } + + return (value - this.curve.F) / this.curve.C; + } + } +} diff --git a/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs b/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs new file mode 100644 index 0000000..79a2137 --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators { + internal class TrcCalculator : IVector4Calculator + { + private readonly ISingleCalculator[] calculators; + + public TrcCalculator(IccTagDataEntry[] entries, bool inverted) + { + Guard.NotNull(entries, nameof(entries)); + + this.calculators = new ISingleCalculator[entries.Length]; + for (int i = 0; i < entries.Length; i++) + { + this.calculators[i] = entries[i] switch + { + IccCurveTagDataEntry curve => new CurveCalculator(curve, inverted), + IccParametricCurveTagDataEntry parametricCurve => new ParametricCurveCalculator(parametricCurve, inverted), + _ => throw new InvalidIccProfileException("Invalid Entry."), + }; + } + } + + public unsafe Vector4 Calculate(Vector4 value) + { + ref float f = ref Unsafe.As(ref value); + for (int i = 0; i < this.calculators.Length; i++) + { + Unsafe.Add(ref f, i) = this.calculators[i].Calculate(Unsafe.Add(ref f, i)); + } + + return value; + } + } +} diff --git a/ImageSharp/ColorProfiles/Icc/CompactSrgbV4Profile.cs b/ImageSharp/ColorProfiles/Icc/CompactSrgbV4Profile.cs new file mode 100644 index 0000000..7bc60d6 --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/CompactSrgbV4Profile.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using System; + +namespace SixLabors.ImageSharp.ColorProfiles.Icc { + internal static class CompactSrgbV4Profile + { + private static readonly Lazy LazyIccProfile = new(GetIccProfile); + + // Generated using the sRGB-v4.icc profile found at https://github.com/saucecontrol/Compact-ICC-Profiles + private static ReadOnlySpan Data => + [ + 0, 0, 1, 224, 108, 99, 109, 115, 4, 32, 0, 0, 109, 110, 116, 114, 82, 71, 66, 32, 88, 89, 90, 32, 7, 226, 0, 3, 0, + 20, 0, 9, 0, 14, 0, 29, 97, 99, 115, 112, 77, 83, 70, 84, 0, 0, 0, 0, 115, 97, 119, 115, 99, 116, 114, 108, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 214, 0, 1, 0, 0, 0, 0, 211, 45, 104, 97, 110, 100, 163, 178, 171, + 223, 92, 167, 3, 18, 168, 85, 164, 236, 53, 122, 209, 243, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 100, 101, 115, 99, 0, 0, 0, 252, 0, 0, 0, 36, 99, + 112, 114, 116, 0, 0, 1, 32, 0, 0, 0, 34, 119, 116, 112, 116, 0, 0, 1, 68, 0, 0, 0, 20, 99, 104, 97, 100, 0, 0, + 1, 88, 0, 0, 0, 44, 114, 88, 89, 90, 0, 0, 1, 132, 0, 0, 0, 20, 103, 88, 89, 90, 0, 0, 1, 152, 0, 0, 0, + 20, 98, 88, 89, 90, 0, 0, 1, 172, 0, 0, 0, 20, 114, 84, 82, 67, 0, 0, 1, 192, 0, 0, 0, 32, 103, 84, 82, 67, + 0, 0, 1, 192, 0, 0, 0, 32, 98, 84, 82, 67, 0, 0, 1, 192, 0, 0, 0, 32, 109, 108, 117, 99, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 12, 101, 110, 85, 83, 0, 0, 0, 8, 0, 0, 0, 28, 0, 115, 0, 82, 0, 71, 0, 66, 109, 108, + 117, 99, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 12, 101, 110, 85, 83, 0, 0, 0, 6, 0, 0, 0, 28, 0, 67, 0, + 67, 0, 48, 0, 33, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, 246, 214, 0, 1, 0, 0, 0, 0, 211, 45, 115, 102, 51, 50, + 0, 0, 0, 0, 0, 1, 12, 63, 0, 0, 5, 221, 255, 255, 243, 38, 0, 0, 7, 144, 0, 0, 253, 146, 255, 255, 251, 161, 255, + 255, 253, 162, 0, 0, 3, 220, 0, 0, 192, 113, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, 111, 160, 0, 0, 56, 242, 0, 0, + 3, 143, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, 98, 150, 0, 0, 183, 137, 0, 0, 24, 218, 88, 89, 90, 32, 0, 0, 0, + 0, 0, 0, 36, 160, 0, 0, 15, 133, 0, 0, 182, 196, 112, 97, 114, 97, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 102, 105, + 0, 0, 242, 167, 0, 0, 13, 89, 0, 0, 19, 208, 0, 0, 10, 91, + ]; + + public static IccProfile Profile => LazyIccProfile.Value; + + private static IccProfile GetIccProfile() + { + byte[] buffer = new byte[Data.Length]; + Data.CopyTo(buffer); + return new IccProfile(buffer); + } + } +} diff --git a/ImageSharp/ColorProfiles/Icc/IccConverterBase.Checks.cs b/ImageSharp/ColorProfiles/Icc/IccConverterBase.Checks.cs new file mode 100644 index 0000000..a4693da --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/IccConverterBase.Checks.cs @@ -0,0 +1,157 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using System; +using System.Linq; + +namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc { + /// + /// Color converter for ICC profiles + /// + internal abstract partial class IccConverterBase + { + private static ConversionMethod GetConversionMethod(IccProfile profile, IccRenderingIntent renderingIntent) => profile.Header.Class switch + { + IccProfileClass.InputDevice or + IccProfileClass.DisplayDevice or + IccProfileClass.OutputDevice or + IccProfileClass.ColorSpace => CheckMethod1(profile, renderingIntent), + IccProfileClass.DeviceLink or IccProfileClass.Abstract => CheckMethod2(profile), + _ => ConversionMethod.Invalid, + }; + + private static ConversionMethod CheckMethod1(IccProfile profile, IccRenderingIntent renderingIntent) + { + ConversionMethod method = CheckMethodD(profile, renderingIntent); + if (method != ConversionMethod.Invalid) + { + return method; + } + + method = CheckMethodA(profile, renderingIntent); + if (method != ConversionMethod.Invalid) + { + return method; + } + + method = CheckMethodA0(profile); + if (method != ConversionMethod.Invalid) + { + return method; + } + + method = CheckMethodTrc(profile); + if (method != ConversionMethod.Invalid) + { + return method; + } + + return ConversionMethod.Invalid; + } + + private static ConversionMethod CheckMethodD(IccProfile profile, IccRenderingIntent renderingIntent) + { + if ((HasTag(profile, IccProfileTag.DToB0) || HasTag(profile, IccProfileTag.BToD0)) + && renderingIntent == IccRenderingIntent.Perceptual) + { + return ConversionMethod.D0; + } + + if ((HasTag(profile, IccProfileTag.DToB1) || HasTag(profile, IccProfileTag.BToD1)) + && renderingIntent == IccRenderingIntent.MediaRelativeColorimetric) + { + return ConversionMethod.D1; + } + + if ((HasTag(profile, IccProfileTag.DToB2) || HasTag(profile, IccProfileTag.BToD2)) + && renderingIntent == IccRenderingIntent.Saturation) + { + return ConversionMethod.D2; + } + + if ((HasTag(profile, IccProfileTag.DToB3) || HasTag(profile, IccProfileTag.BToD3)) + && renderingIntent == IccRenderingIntent.AbsoluteColorimetric) + { + return ConversionMethod.D3; + } + + return ConversionMethod.Invalid; + } + + private static ConversionMethod CheckMethodA(IccProfile profile, IccRenderingIntent renderingIntent) + { + if ((HasTag(profile, IccProfileTag.AToB0) || HasTag(profile, IccProfileTag.BToA0)) + && renderingIntent == IccRenderingIntent.Perceptual) + { + return ConversionMethod.A0; + } + + if ((HasTag(profile, IccProfileTag.AToB1) || HasTag(profile, IccProfileTag.BToA1)) + && renderingIntent == IccRenderingIntent.MediaRelativeColorimetric) + { + return ConversionMethod.A1; + } + + if ((HasTag(profile, IccProfileTag.AToB2) || HasTag(profile, IccProfileTag.BToA2)) + && renderingIntent == IccRenderingIntent.Saturation) + { + return ConversionMethod.A2; + } + + return ConversionMethod.Invalid; + } + + private static ConversionMethod CheckMethodA0(IccProfile profile) + { + bool valid = HasTag(profile, IccProfileTag.AToB0) || HasTag(profile, IccProfileTag.BToA0); + return valid ? ConversionMethod.A0 : ConversionMethod.Invalid; + } + + private static ConversionMethod CheckMethodTrc(IccProfile profile) + { + if (HasTag(profile, IccProfileTag.RedMatrixColumn) + && HasTag(profile, IccProfileTag.GreenMatrixColumn) + && HasTag(profile, IccProfileTag.BlueMatrixColumn) + && HasTag(profile, IccProfileTag.RedTrc) + && HasTag(profile, IccProfileTag.GreenTrc) + && HasTag(profile, IccProfileTag.BlueTrc)) + { + return ConversionMethod.ColorTrc; + } + + if (HasTag(profile, IccProfileTag.GrayTrc)) + { + return ConversionMethod.GrayTrc; + } + + return ConversionMethod.Invalid; + } + + private static ConversionMethod CheckMethod2(IccProfile profile) + { + if (HasTag(profile, IccProfileTag.DToB0) || HasTag(profile, IccProfileTag.BToD0)) + { + return ConversionMethod.D0; + } + + if (HasTag(profile, IccProfileTag.AToB0) || HasTag(profile, IccProfileTag.AToB0)) + { + return ConversionMethod.A0; + } + + return ConversionMethod.Invalid; + } + + private static bool HasTag(IccProfile profile, IccProfileTag tag) + => profile.Entries.Any(t => t.TagSignature == tag); + + private static IccTagDataEntry GetTag(IccProfile profile, IccProfileTag tag) + => Array.Find(profile.Entries, t => t.TagSignature == tag); + + private static T GetTag(IccProfile profile, IccProfileTag tag) + where T : IccTagDataEntry + => profile.Entries.OfType().FirstOrDefault(t => t.TagSignature == tag); + } +} diff --git a/ImageSharp/ColorProfiles/Icc/IccConverterBase.ConversionMethod.cs b/ImageSharp/ColorProfiles/Icc/IccConverterBase.ConversionMethod.cs new file mode 100644 index 0000000..2353226 --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/IccConverterBase.ConversionMethod.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc { + /// + /// Color converter for ICC profiles + /// + internal abstract partial class IccConverterBase + { + /// + /// Conversion methods with ICC profiles + /// + private enum ConversionMethod + { + /// + /// Conversion using anything but Multi Process Elements with perceptual rendering intent + /// + A0, + + /// + /// Conversion using anything but Multi Process Elements with relative colorimetric rendering intent + /// + A1, + + /// + /// Conversion using anything but Multi Process Elements with saturation rendering intent + /// + A2, + + /// + /// Conversion using Multi Process Elements with perceptual rendering intent + /// + D0, + + /// + /// Conversion using Multi Process Elements with relative colorimetric rendering intent + /// + D1, + + /// + /// Conversion using Multi Process Elements with saturation rendering intent + /// + D2, + + /// + /// Conversion using Multi Process Elements with absolute colorimetric rendering intent + /// + D3, + + /// + /// Conversion of more than one channel using tone reproduction curves + /// + ColorTrc, + + /// + /// Conversion of exactly one channel using a tone reproduction curve + /// + GrayTrc, + + /// + /// No valid conversion method available or found + /// + Invalid, + } + } +} diff --git a/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs b/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs new file mode 100644 index 0000000..fce5113 --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs @@ -0,0 +1,110 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles.Icc.Calculators; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using System; + +namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc { + /// + /// Color converter for ICC profiles + /// + internal abstract partial class IccConverterBase + { + private IVector4Calculator calculator; + + internal bool Is16BitLutEntry => this.calculator is LutEntryCalculator { Is16Bit: true }; + + internal bool IsTrc => this.calculator is ColorTrcCalculator or GrayTrcCalculator; + + /// + /// Checks the profile for available conversion methods and gathers all the information's necessary for it. + /// + /// The profile to use for the conversion. + /// True if the conversion is to the Profile Connection Space. + /// The wanted rendering intent. Can be ignored if not available. + /// Invalid conversion method. + protected void Init(IccProfile profile, bool toPcs, IccRenderingIntent renderingIntent) + => this.calculator = GetConversionMethod(profile, renderingIntent) switch + { + ConversionMethod.D0 => toPcs ? + InitD(profile, IccProfileTag.DToB0) : + InitD(profile, IccProfileTag.BToD0), + ConversionMethod.D1 => toPcs ? + InitD(profile, IccProfileTag.DToB1) : + InitD(profile, IccProfileTag.BToD1), + ConversionMethod.D2 => toPcs ? + InitD(profile, IccProfileTag.DToB2) : + InitD(profile, IccProfileTag.BToD2), + ConversionMethod.D3 => toPcs ? + InitD(profile, IccProfileTag.DToB3) : + InitD(profile, IccProfileTag.BToD3), + ConversionMethod.A0 => toPcs ? + InitA(profile, IccProfileTag.AToB0) : + InitA(profile, IccProfileTag.BToA0), + ConversionMethod.A1 => toPcs ? + InitA(profile, IccProfileTag.AToB1) : + InitA(profile, IccProfileTag.BToA1), + ConversionMethod.A2 => toPcs ? + InitA(profile, IccProfileTag.AToB2) : + InitA(profile, IccProfileTag.BToA2), + ConversionMethod.ColorTrc => InitColorTrc(profile, toPcs), + ConversionMethod.GrayTrc => InitGrayTrc(profile, toPcs), + _ => throw new InvalidIccProfileException("Invalid conversion method."), + }; + + private static IVector4Calculator InitA(IccProfile profile, IccProfileTag tag) + => GetTag(profile, tag) switch + { + IccLut8TagDataEntry lut8 => new LutEntryCalculator(lut8), + IccLut16TagDataEntry lut16 => new LutEntryCalculator(lut16), + IccLutAToBTagDataEntry lutAtoB => new LutABCalculator(lutAtoB), + IccLutBToATagDataEntry lutBtoA => new LutABCalculator(lutBtoA), + _ => throw new InvalidIccProfileException($"Invalid entry {tag}."), + }; + + private static IVector4Calculator InitD(IccProfile profile, IccProfileTag tag) + { + IccMultiProcessElementsTagDataEntry entry = GetTag(profile, tag) + ?? throw new InvalidIccProfileException("Entry is null."); + + throw new NotImplementedException("Multi process elements are not supported"); + } + + private static ColorTrcCalculator InitColorTrc(IccProfile profile, bool toPcs) + { + IccXyzTagDataEntry redMatrixColumn = GetTag(profile, IccProfileTag.RedMatrixColumn); + IccXyzTagDataEntry greenMatrixColumn = GetTag(profile, IccProfileTag.GreenMatrixColumn); + IccXyzTagDataEntry blueMatrixColumn = GetTag(profile, IccProfileTag.BlueMatrixColumn); + + IccTagDataEntry redTrc = GetTag(profile, IccProfileTag.RedTrc); + IccTagDataEntry greenTrc = GetTag(profile, IccProfileTag.GreenTrc); + IccTagDataEntry blueTrc = GetTag(profile, IccProfileTag.BlueTrc); + + if (redMatrixColumn == null || + greenMatrixColumn == null || + blueMatrixColumn == null || + redTrc == null || + greenTrc == null || + blueTrc == null) + { + throw new InvalidIccProfileException("Missing matrix column or channel."); + } + + return new ColorTrcCalculator( + redMatrixColumn, + greenMatrixColumn, + blueMatrixColumn, + redTrc, + greenTrc, + blueTrc, + toPcs); + } + + private static GrayTrcCalculator InitGrayTrc(IccProfile profile, bool toPcs) + { + IccTagDataEntry entry = GetTag(profile, IccProfileTag.GrayTrc); + return new GrayTrcCalculator(entry, toPcs); + } + } +} diff --git a/ImageSharp/ColorProfiles/Icc/IccConverterbase.cs b/ImageSharp/ColorProfiles/Icc/IccConverterbase.cs new file mode 100644 index 0000000..1dee7d5 --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/IccConverterbase.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc { + /// + /// Color converter for ICC profiles + /// + internal abstract partial class IccConverterBase + { + /// + /// Initializes a new instance of the class. + /// + /// The ICC profile to use for the conversions + /// True if the conversion is to the profile connection space (PCS); False if the conversion is to the data space + protected IccConverterBase(IccProfile profile, bool toPcs) + { + Guard.NotNull(profile, nameof(profile)); + this.Init(profile, toPcs, profile.Header.RenderingIntent); + } + + /// + /// Converts colors with the initially provided ICC profile + /// + /// The value to convert + /// The converted value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 Calculate(Vector4 value) => this.calculator.Calculate(value); + + /// + /// Converts colors with the initially provided ICC profile + /// + /// The source colors + /// The destination colors + public void Calculate(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + for (int i = 0; i < source.Length; i++) + { + destination[i] = this.Calculate(source[i]); + } + } + } +} diff --git a/ImageSharp/ColorProfiles/Icc/IccDataToDataConverter.cs b/ImageSharp/ColorProfiles/Icc/IccDataToDataConverter.cs new file mode 100644 index 0000000..6f213eb --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/IccDataToDataConverter.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.ColorProfiles.Icc { + /// + /// Color converter for ICC profiles + /// + internal class IccDataToDataConverter : IccConverterBase + { + /// + /// Initializes a new instance of the class. + /// + /// The ICC profile to use for the conversions + public IccDataToDataConverter(IccProfile profile) + : base(profile, true) // toPCS is true because in this case the PCS space is also a data space + { + } + } +} diff --git a/ImageSharp/ColorProfiles/Icc/IccDataToPcsConverter.cs b/ImageSharp/ColorProfiles/Icc/IccDataToPcsConverter.cs new file mode 100644 index 0000000..254fc7a --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/IccDataToPcsConverter.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.ColorProfiles.Icc { + /// + /// Color converter for ICC profiles + /// + internal class IccDataToPcsConverter : IccConverterBase + { + /// + /// Initializes a new instance of the class. + /// + /// The ICC profile to use for the conversions + public IccDataToPcsConverter(IccProfile profile) + : base(profile, true) + { + } + } +} diff --git a/ImageSharp/ColorProfiles/Icc/IccPcsToDataConverter.cs b/ImageSharp/ColorProfiles/Icc/IccPcsToDataConverter.cs new file mode 100644 index 0000000..4d80043 --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/IccPcsToDataConverter.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.ColorProfiles.Icc { + /// + /// Color converter for ICC profiles + /// + internal class IccPcsToDataConverter : IccConverterBase + { + /// + /// Initializes a new instance of the class. + /// + /// The ICC profile to use for the conversions + public IccPcsToDataConverter(IccProfile profile) + : base(profile, false) + { + } + } +} diff --git a/ImageSharp/ColorProfiles/Icc/IccPcsToPcsConverter.cs b/ImageSharp/ColorProfiles/Icc/IccPcsToPcsConverter.cs new file mode 100644 index 0000000..c4a48c8 --- /dev/null +++ b/ImageSharp/ColorProfiles/Icc/IccPcsToPcsConverter.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.ColorProfiles.Icc { + /// + /// Color converter for ICC profiles + /// + internal class IccPcsToPcsConverter : IccConverterBase + { + /// + /// Initializes a new instance of the class. + /// + /// The ICC profile to use for the conversions + public IccPcsToPcsConverter(IccProfile profile) + : base(profile, true) + { + } + } +} diff --git a/ImageSharp/ColorProfiles/KnownChromaticAdaptationMatrices.cs b/ImageSharp/ColorProfiles/KnownChromaticAdaptationMatrices.cs new file mode 100644 index 0000000..5d51d1b --- /dev/null +++ b/ImageSharp/ColorProfiles/KnownChromaticAdaptationMatrices.cs @@ -0,0 +1,135 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Provides matrices for chromatic adaptation, facilitating the adjustment of color values + /// under different light sources to maintain color constancy. This class supports common + /// adaptation transforms based on the von Kries coefficient law, which assumes independent + /// scaling of the cone responses in the human eye. These matrices can be applied to convert + /// color coordinates between different illuminants, ensuring consistent color appearance + /// across various lighting conditions. + /// + /// + /// Supported adaptation matrices include the Bradford, von Kries, and Sharp transforms. + /// These matrices are typically used in conjunction with color space conversions, such as from XYZ + /// to RGB, to achieve accurate color rendition in digital imaging applications. + /// + public static class KnownChromaticAdaptationMatrices + { + /// + /// von Kries chromatic adaptation transform matrix (Hunt-Pointer-Estevez adjusted for D65) + /// + public static readonly Matrix4x4 VonKriesHPEAdjusted + = Matrix4x4.Transpose(new Matrix4x4 + { + M11 = 0.40024F, + M12 = 0.7076F, + M13 = -0.08081F, + M21 = -0.2263F, + M22 = 1.16532F, + M23 = 0.0457F, + M31 = 0, + M32 = 0, + M33 = 0.91822F, + M44 = 1F // Important for inverse transforms. + }); + + /// + /// von Kries chromatic adaptation transform matrix (Hunt-Pointer-Estevez for equal energy) + /// + public static readonly Matrix4x4 VonKriesHPE + = Matrix4x4.Transpose(new Matrix4x4 + { + M11 = 0.3897F, + M12 = 0.6890F, + M13 = -0.0787F, + M21 = -0.2298F, + M22 = 1.1834F, + M23 = 0.0464F, + M31 = 0, + M32 = 0, + M33 = 1F, + M44 = 1F + }); + + /// + /// XYZ scaling chromatic adaptation transform matrix + /// + public static readonly Matrix4x4 XyzScaling = Matrix4x4.Transpose(Matrix4x4.Identity); + + /// + /// Bradford chromatic adaptation transform matrix (used in CMCCAT97) + /// + public static readonly Matrix4x4 Bradford + = Matrix4x4.Transpose(new Matrix4x4 + { + M11 = 0.8951F, + M12 = 0.2664F, + M13 = -0.1614F, + M21 = -0.7502F, + M22 = 1.7135F, + M23 = 0.0367F, + M31 = 0.0389F, + M32 = -0.0685F, + M33 = 1.0296F, + M44 = 1F + }); + + /// + /// Spectral sharpening and the Bradford transform + /// + public static readonly Matrix4x4 BradfordSharp + = Matrix4x4.Transpose(new Matrix4x4 + { + M11 = 1.2694F, + M12 = -0.0988F, + M13 = -0.1706F, + M21 = -0.8364F, + M22 = 1.8006F, + M23 = 0.0357F, + M31 = 0.0297F, + M32 = -0.0315F, + M33 = 1.0018F, + M44 = 1F + }); + + /// + /// CMCCAT2000 (fitted from all available color data sets) + /// + public static readonly Matrix4x4 CMCCAT2000 + = Matrix4x4.Transpose(new Matrix4x4 + { + M11 = 0.7982F, + M12 = 0.3389F, + M13 = -0.1371F, + M21 = -0.5918F, + M22 = 1.5512F, + M23 = 0.0406F, + M31 = 0.0008F, + M32 = 0.239F, + M33 = 0.9753F, + M44 = 1F + }); + + /// + /// CAT02 (optimized for minimizing CIELAB differences) + /// + public static readonly Matrix4x4 CAT02 + = Matrix4x4.Transpose(new Matrix4x4 + { + M11 = 0.7328F, + M12 = 0.4296F, + M13 = -0.1624F, + M21 = -0.7036F, + M22 = 1.6975F, + M23 = 0.0061F, + M31 = 0.0030F, + M32 = 0.0136F, + M33 = 0.9834F, + M44 = 1F + }); + } +} diff --git a/ImageSharp/ColorProfiles/KnownIlluminants.cs b/ImageSharp/ColorProfiles/KnownIlluminants.cs new file mode 100644 index 0000000..10161ee --- /dev/null +++ b/ImageSharp/ColorProfiles/KnownIlluminants.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// The well known standard illuminants. + /// Standard illuminants provide a basis for comparing images or colors recorded under different lighting + /// + /// + /// Coefficients taken from: http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html + /// and https://color.org/specification/ICC.1-2022-05.pdf + ///
+ /// Descriptions taken from: http://en.wikipedia.org/wiki/Standard_illuminant + ///
+ public static class KnownIlluminants + { + /// + /// Gets the Incandescent / Tungsten illuminant. + /// + public static CieXyz A { get; } = new(1.09850F, 1F, 0.35585F); + + /// + /// Gets the Direct sunlight at noon (obsoleteF) illuminant. + /// + public static CieXyz B { get; } = new(0.99072F, 1F, 0.85223F); + + /// + /// Gets the Average / North sky Daylight (obsoleteF) illuminant. + /// + public static CieXyz C { get; } = new(0.98074F, 1F, 1.18232F); + + /// + /// Gets the Horizon Light. + /// + public static CieXyz D50 { get; } = new(0.96422F, 1F, 0.82521F); + + /// + /// Gets the D50 illuminant used in the ICC profile specification. + /// + public static CieXyz D50Icc { get; } = new(0.9642F, 1F, 0.8249F); + + /// + /// Gets the Mid-morning / Mid-afternoon Daylight illuminant. + /// + public static CieXyz D55 { get; } = new(0.95682F, 1F, 0.92149F); + + /// + /// Gets the Noon Daylight: TelevisionF, sRGB color space illuminant. + /// + public static CieXyz D65 { get; } = new(0.95047F, 1F, 1.08883F); + + /// + /// Gets the North sky Daylight illuminant. + /// + public static CieXyz D75 { get; } = new(0.94972F, 1F, 1.22638F); + + /// + /// Gets the Equal energy illuminant. + /// + public static CieXyz E { get; } = new(1F, 1F, 1F); + + /// + /// Gets the Cool White Fluorescent illuminant. + /// + public static CieXyz F2 { get; } = new(0.99186F, 1F, 0.67393F); + + /// + /// Gets the D65 simulatorF, Daylight simulator illuminant. + /// + public static CieXyz F7 { get; } = new(0.95041F, 1F, 1.08747F); + + /// + /// Gets the Philips TL84F, Ultralume 40 illuminant. + /// + public static CieXyz F11 { get; } = new(1.00962F, 1F, 0.64350F); + } +} diff --git a/ImageSharp/ColorProfiles/KnownRgbWorkingSpaces.cs b/ImageSharp/ColorProfiles/KnownRgbWorkingSpaces.cs new file mode 100644 index 0000000..dbc86f2 --- /dev/null +++ b/ImageSharp/ColorProfiles/KnownRgbWorkingSpaces.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles.Companding; +using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Chromaticity coordinates based on: + /// + public static class KnownRgbWorkingSpaces + { + /// + /// sRgb working space. + /// + /// + /// Uses proper companding function, according to: + /// + /// + public static readonly RgbWorkingSpace SRgb = new SRgbWorkingSpace(KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6400F, 0.3300F), new CieXyChromaticityCoordinates(0.3000F, 0.6000F), new CieXyChromaticityCoordinates(0.1500F, 0.0600F))); + + /// + /// Simplified sRgb working space (uses gamma companding instead of ). + /// See also . + /// + public static readonly RgbWorkingSpace SRgbSimplified = new GammaWorkingSpace(2.2F, KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6400F, 0.3300F), new CieXyChromaticityCoordinates(0.3000F, 0.6000F), new CieXyChromaticityCoordinates(0.1500F, 0.0600F))); + + /// + /// Rec. 709 (ITU-R Recommendation BT.709) working space. + /// + public static readonly RgbWorkingSpace Rec709 = new Rec709WorkingSpace(KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.64F, 0.33F), new CieXyChromaticityCoordinates(0.30F, 0.60F), new CieXyChromaticityCoordinates(0.15F, 0.06F))); + + /// + /// Rec. 2020 (ITU-R Recommendation BT.2020F) working space. + /// + public static readonly RgbWorkingSpace Rec2020 = new Rec2020WorkingSpace(KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.708F, 0.292F), new CieXyChromaticityCoordinates(0.170F, 0.797F), new CieXyChromaticityCoordinates(0.131F, 0.046F))); + + /// + /// ECI Rgb v2 working space. + /// + public static readonly RgbWorkingSpace ECIRgbv2 = new LWorkingSpace(KnownIlluminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6700F, 0.3300F), new CieXyChromaticityCoordinates(0.2100F, 0.7100F), new CieXyChromaticityCoordinates(0.1400F, 0.0800F))); + + /// + /// Adobe Rgb (1998) working space. + /// + public static readonly RgbWorkingSpace AdobeRgb1998 = new GammaWorkingSpace(2.2F, KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6400F, 0.3300F), new CieXyChromaticityCoordinates(0.2100F, 0.7100F), new CieXyChromaticityCoordinates(0.1500F, 0.0600F))); + + /// + /// Apple sRgb working space. + /// + public static readonly RgbWorkingSpace ApplesRgb = new GammaWorkingSpace(1.8F, KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6250F, 0.3400F), new CieXyChromaticityCoordinates(0.2800F, 0.5950F), new CieXyChromaticityCoordinates(0.1550F, 0.0700F))); + + /// + /// Best Rgb working space. + /// + public static readonly RgbWorkingSpace BestRgb = new GammaWorkingSpace(2.2F, KnownIlluminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.7347F, 0.2653F), new CieXyChromaticityCoordinates(0.2150F, 0.7750F), new CieXyChromaticityCoordinates(0.1300F, 0.0350F))); + + /// + /// Beta Rgb working space. + /// + public static readonly RgbWorkingSpace BetaRgb = new GammaWorkingSpace(2.2F, KnownIlluminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6888F, 0.3112F), new CieXyChromaticityCoordinates(0.1986F, 0.7551F), new CieXyChromaticityCoordinates(0.1265F, 0.0352F))); + + /// + /// Bruce Rgb working space. + /// + public static readonly RgbWorkingSpace BruceRgb = new GammaWorkingSpace(2.2F, KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6400F, 0.3300F), new CieXyChromaticityCoordinates(0.2800F, 0.6500F), new CieXyChromaticityCoordinates(0.1500F, 0.0600F))); + + /// + /// CIE Rgb working space. + /// + public static readonly RgbWorkingSpace CIERgb = new GammaWorkingSpace(2.2F, KnownIlluminants.E, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.7350F, 0.2650F), new CieXyChromaticityCoordinates(0.2740F, 0.7170F), new CieXyChromaticityCoordinates(0.1670F, 0.0090F))); + + /// + /// ColorMatch Rgb working space. + /// + public static readonly RgbWorkingSpace ColorMatchRgb = new GammaWorkingSpace(1.8F, KnownIlluminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6300F, 0.3400F), new CieXyChromaticityCoordinates(0.2950F, 0.6050F), new CieXyChromaticityCoordinates(0.1500F, 0.0750F))); + + /// + /// Don Rgb 4 working space. + /// + public static readonly RgbWorkingSpace DonRgb4 = new GammaWorkingSpace(2.2F, KnownIlluminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6960F, 0.3000F), new CieXyChromaticityCoordinates(0.2150F, 0.7650F), new CieXyChromaticityCoordinates(0.1300F, 0.0350F))); + + /// + /// Ekta Space PS5 working space. + /// + public static readonly RgbWorkingSpace EktaSpacePS5 = new GammaWorkingSpace(2.2F, KnownIlluminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6950F, 0.3050F), new CieXyChromaticityCoordinates(0.2600F, 0.7000F), new CieXyChromaticityCoordinates(0.1100F, 0.0050F))); + + /// + /// NTSC Rgb working space. + /// + public static readonly RgbWorkingSpace NTSCRgb = new GammaWorkingSpace(2.2F, KnownIlluminants.C, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6700F, 0.3300F), new CieXyChromaticityCoordinates(0.2100F, 0.7100F), new CieXyChromaticityCoordinates(0.1400F, 0.0800F))); + + /// + /// PAL/SECAM Rgb working space. + /// + public static readonly RgbWorkingSpace PALSECAMRgb = new GammaWorkingSpace(2.2F, KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6400F, 0.3300F), new CieXyChromaticityCoordinates(0.2900F, 0.6000F), new CieXyChromaticityCoordinates(0.1500F, 0.0600F))); + + /// + /// ProPhoto Rgb working space. + /// + public static readonly RgbWorkingSpace ProPhotoRgb = new GammaWorkingSpace(1.8F, KnownIlluminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.7347F, 0.2653F), new CieXyChromaticityCoordinates(0.1596F, 0.8404F), new CieXyChromaticityCoordinates(0.0366F, 0.0001F))); + + /// + /// SMPTE-C Rgb working space. + /// + public static readonly RgbWorkingSpace SMPTECRgb = new GammaWorkingSpace(2.2F, KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6300F, 0.3400F), new CieXyChromaticityCoordinates(0.3100F, 0.5950F), new CieXyChromaticityCoordinates(0.1550F, 0.0700F))); + + /// + /// Wide Gamut Rgb working space. + /// + public static readonly RgbWorkingSpace WideGamutRgb = new GammaWorkingSpace(2.2F, KnownIlluminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.7350F, 0.2650F), new CieXyChromaticityCoordinates(0.1150F, 0.8260F), new CieXyChromaticityCoordinates(0.1570F, 0.0180F))); + } +} diff --git a/ImageSharp/ColorProfiles/KnownYCbCrMatrices.cs b/ImageSharp/ColorProfiles/KnownYCbCrMatrices.cs new file mode 100644 index 0000000..542244b --- /dev/null +++ b/ImageSharp/ColorProfiles/KnownYCbCrMatrices.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Provides standard YCbCr matrices for RGB to YCbCr conversion. + /// + public static class KnownYCbCrMatrices + { +#pragma warning disable SA1137 // Elements should have the same indentation +#pragma warning disable SA1117 // Parameters should be on same line or separate lines + /// + /// ITU-R BT.601 (SD video standard). + /// + public static readonly YCbCrTransform BT601 = new( + new Matrix4x4( + 0.299000F, 0.587000F, 0.114000F, 0F, + -0.168736F, -0.331264F, 0.500000F, 0F, + 0.500000F, -0.418688F, -0.081312F, 0F, + 0F, 0F, 0F, 1F), + new Matrix4x4( + 1.000000F, 0.000000F, 1.402000F, 0F, + 1.000000F, -0.344136F, -0.714136F, 0F, + 1.000000F, 1.772000F, 0.000000F, 0F, + 0F, 0F, 0F, 1F), + new Vector3(0F, 0.5F, 0.5F)); + + /// + /// ITU-R BT.709 (HD video, sRGB standard). + /// + public static readonly YCbCrTransform BT709 = new( + new Matrix4x4( + 0.212600F, 0.715200F, 0.072200F, 0F, + -0.114572F, -0.385428F, 0.500000F, 0F, + 0.500000F, -0.454153F, -0.045847F, 0F, + 0F, 0F, 0F, 1F), + new Matrix4x4( + 1.000000F, 0.000000F, 1.574800F, 0F, + 1.000000F, -0.187324F, -0.468124F, 0F, + 1.000000F, 1.855600F, 0.000000F, 0F, + 0F, 0F, 0F, 1F), + new Vector3(0F, 0.5F, 0.5F)); + + /// + /// ITU-R BT.2020 (UHD/4K video standard). + /// + public static readonly YCbCrTransform BT2020 = new( + new Matrix4x4( + 0.262700F, 0.678000F, 0.059300F, 0F, + -0.139630F, -0.360370F, 0.500000F, 0F, + 0.500000F, -0.459786F, -0.040214F, 0F, + 0F, 0F, 0F, 1F), + new Matrix4x4( + 1.000000F, 0.000000F, 1.474600F, 0F, + 1.000000F, -0.164553F, -0.571353F, 0F, + 1.000000F, 1.881400F, 0.000000F, 0F, + 0F, 0F, 0F, 1F), + new Vector3(0F, 0.5F, 0.5F)); + } +} diff --git a/ImageSharp/ColorProfiles/Lms.cs b/ImageSharp/ColorProfiles/Lms.cs new file mode 100644 index 0000000..f0e2cff --- /dev/null +++ b/ImageSharp/ColorProfiles/Lms.cs @@ -0,0 +1,179 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// LMS is a color space represented by the response of the three types of cones of the human eye, + /// named after their responsivity (sensitivity) at long, medium and short wavelengths. + /// + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct Lms : IColorProfile + { + /// + /// Initializes a new instance of the struct. + /// + /// L represents the responsivity at long wavelengths. + /// M represents the responsivity at medium wavelengths. + /// S represents the responsivity at short wavelengths. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Lms(float l, float m, float s) + { + this.L = l; + this.M = m; + this.S = s; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the l, m, s components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Lms(Vector3 vector) + { + // Not clamping as documentation about this space only indicates "usual" ranges + this.L = vector.X; + this.M = vector.Y; + this.S = vector.Z; + } + + /// + /// Gets the L long component. + /// A value usually ranging between -1 and 1. + /// + public float L { get; } + + /// + /// Gets the M medium component. + /// A value usually ranging between -1 and 1. + /// + public float M { get; } + + /// + /// Gets the S short component. + /// A value usually ranging between -1 and 1. + /// + public float S { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Lms left, Lms right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Lms left, Lms right) => !left.Equals(right); + + /// + public Vector4 ToScaledVector4() + { + Vector3 v3 = default; + v3 += this.AsVector3Unsafe(); + v3 += new Vector3(1F); + v3 /= 2F; + return new Vector4(v3, 1F); + } + + /// + public static Lms FromScaledVector4(Vector4 source) + { + Vector3 v3 = source.AsVector3(); + v3 *= 2F; + v3 -= new Vector3(1F); + return new Lms(v3); + } + + /// + public static void ToScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToScaledVector4(); + } + } + + /// + public static void FromScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = FromScaledVector4(source[i]); + } + } + + /// + public static Lms FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source) + => new(Vector3.Transform(source.AsVector3Unsafe(), options.AdaptationMatrix)); + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + for (int i = 0; i < source.Length; i++) + { + CieXyz xyz = source[i]; + destination[i] = FromProfileConnectingSpace(options, in xyz); + } + } + + /// + public CieXyz ToProfileConnectingSpace(ColorConversionOptions options) + => new(Vector3.Transform(this.AsVector3Unsafe(), options.InverseAdaptationMatrix)); + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + for (int i = 0; i < source.Length; i++) + { + Lms lms = source[i]; + destination[i] = lms.ToProfileConnectingSpace(options); + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() => ChromaticAdaptionWhitePointSource.WhitePoint; + + /// + public override int GetHashCode() => HashCode.Combine(this.L, this.M, this.S); + + /// + public override string ToString() => FormattableString.Invariant($"Lms({this.L:#0.##}, {this.M:#0.##}, {this.S:#0.##})"); + + /// + public override bool Equals(object? obj) => obj is Lms other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Lms other) + => this.AsVector3Unsafe() == other.AsVector3Unsafe(); + + private Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this)); + } +} diff --git a/ImageSharp/ColorProfiles/Rgb.cs b/ImageSharp/ColorProfiles/Rgb.cs new file mode 100644 index 0000000..b95ecca --- /dev/null +++ b/ImageSharp/ColorProfiles/Rgb.cs @@ -0,0 +1,464 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Represents an RGB (red, green, blue) color profile. + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct Rgb : IProfileConnectingSpace + { + /// + /// Initializes a new instance of the struct. + /// + /// The red component usually ranging between 0 and 1. + /// The green component usually ranging between 0 and 1. + /// The blue component usually ranging between 0 and 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgb(float r, float g, float b) + { + // Not clamping as this space can exceed "usual" ranges + this.R = r; + this.G = g; + this.B = b; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the r, g, b components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgb(Vector3 source) + { + this.R = source.X; + this.G = source.Y; + this.B = source.Z; + } + + /// + /// Gets the red component. + /// A value usually ranging between 0 and 1. + /// + public float R { get; } + + /// + /// Gets the green component. + /// A value usually ranging between 0 and 1. + /// + public float G { get; } + + /// + /// Gets the blue component. + /// A value usually ranging between 0 and 1. + /// + public float B { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Rgb left, Rgb right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Rgb left, Rgb right) => !left.Equals(right); + + /// + /// Initializes the color instance from a generic scaled . + /// + /// The vector to load the color from. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb FromScaledVector4(Vector4 source) + => new(source.AsVector3()); + + /// + /// Expands the color into a generic ("scaled") representation + /// with values scaled and usually clamped between 0 and 1. + /// The vector components are typically expanded in least to greatest significance order. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 ToScaledVector4() + => new(this.AsVector3Unsafe(), 1F); + + /// + public static void ToScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + int length = source.Length; + if (length == 0) + { + return; + } + + ref Rgb srcRgb = ref MemoryMarshal.GetReference(source); + ref Vector4 dstV4 = ref MemoryMarshal.GetReference(destination); + + // Float streams: + // src: r0 g0 b0 r1 g1 b1 ... + // dst: r0 g0 b0 a0 r1 g1 b1 a1 ... + ref float src = ref Unsafe.As(ref srcRgb); + ref float dst = ref Unsafe.As(ref dstV4); + + int i = 0; + + if (Avx512F.IsSupported) + { + // 4 pixels per iteration. Using overlapped 16-float loads. + Vector512 perm = Vector512.Create(0, 1, 2, 0, 3, 4, 5, 0, 6, 7, 8, 0, 9, 10, 11, 0); + Vector512 ones = Vector512.Create(1F); + + // BlendVariable selects from 'ones' where the sign-bit of mask lane is set. + // Using -0f sets only the sign bit, producing an efficient "select lane" mask. + Vector512 alphaSelect = Vector512.Create(0F, 0F, 0F, -0F, 0F, 0F, 0F, -0F, 0F, 0F, 0F, -0F, 0F, 0F, 0F, -0F); + + int quads = length >> 2; + + // Leave the last quad (4 pixels) for the scalar tail. + int simdQuads = quads - 1; + + for (int q = 0; q < simdQuads; q++) + { + Vector512 v = ReadVector512(ref src); + Vector512 rgbx = Avx512F.PermuteVar16x32(v, perm); + Vector512 rgba = Avx512F.BlendVariable(rgbx, ones, alphaSelect); + + WriteVector512(ref dst, rgba); + + src = ref Unsafe.Add(ref src, 12); + dst = ref Unsafe.Add(ref dst, 16); + + i += 4; + } + } + else if (Avx2.IsSupported) + { + // 2 pixels per iteration. Using overlapped 8-float loads. + Vector256 perm = Vector256.Create(0, 1, 2, 0, 3, 4, 5, 0); + + Vector256 ones = Vector256.Create(1F); + + // vblendps mask: bit i selects lane i from 'ones' when set. + // We want lanes 3 and 7 -> 0b10001000 = 0x88. + const byte alphaMask = 0x88; + + int pairs = length >> 1; + + // Leave the last pair (2 pixels) for the scalar tail. + int simdPairs = pairs - 1; + + for (int p = 0; p < simdPairs; p++) + { + Vector256 v = ReadVector256(ref src); + Vector256 rgbx = Avx2.PermuteVar8x32(v, perm); + Vector256 rgba = Avx.Blend(rgbx, ones, alphaMask); + + WriteVector256(ref dst, rgba); + + src = ref Unsafe.Add(ref src, 6); + dst = ref Unsafe.Add(ref dst, 8); + + i += 2; + } + } + + // Tail (and non-AVX paths) + for (; i < length; i++) + { + Unsafe.Add(ref dstV4, i) = Unsafe.Add(ref srcRgb, i).ToScaledVector4(); + } + } + + /// + public static void FromScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + int length = source.Length; + if (length == 0) + { + return; + } + + ref Vector4 srcV4 = ref MemoryMarshal.GetReference(source); + ref Rgb dstRgb = ref MemoryMarshal.GetReference(destination); + + // Float streams: + // src: r0 g0 b0 a0 r1 g1 b1 a1 ... + // dst: r0 g0 b0 r1 g1 b1 ... + ref float src = ref Unsafe.As(ref srcV4); + ref float dst = ref Unsafe.As(ref dstRgb); + + int i = 0; + + if (Avx512F.IsSupported) + { + // 4 pixels per iteration. Using overlapped 16-float stores: + Vector512 idx = Vector512.Create(0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 3, 7, 11, 15); + + // Number of 4-pixel groups in the input. + int quads = length >> 2; + + // Leave the last quad (4 pixels) for the scalar tail. + int simdQuads = quads - 1; + + for (int q = 0; q < simdQuads; q++) + { + Vector512 v = ReadVector512(ref src); + Vector512 packed = Avx512F.PermuteVar16x32(v, idx); + + WriteVector512(ref dst, packed); + + src = ref Unsafe.Add(ref src, 16); + dst = ref Unsafe.Add(ref dst, 12); + i += 4; + } + } + else if (Avx2.IsSupported) + { + // 2 pixels per iteration, using overlapped 8-float stores: + Vector256 idx = Vector256.Create(0, 1, 2, 4, 5, 6, 0, 0); + + int pairs = length >> 1; + + // Leave the last pair (2 pixels) for the scalar tail. + int simdPairs = pairs - 1; + + int pairIndex = 0; + for (; pairIndex < simdPairs; pairIndex++) + { + Vector256 v = ReadVector256(ref src); + Vector256 packed = Avx2.PermuteVar8x32(v, idx); + + WriteVector256(ref dst, packed); + + src = ref Unsafe.Add(ref src, 8); + dst = ref Unsafe.Add(ref dst, 6); + i += 2; + } + } + + // Tail (and non-AVX paths) + for (; i < length; i++) + { + Vector4 v = Unsafe.Add(ref srcV4, i); + Unsafe.Add(ref dstRgb, i) = FromScaledVector4(v); + } + } + + /// + public static Rgb FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source) + { + // Convert to linear rgb then compress. + Rgb linear = new(Vector3.Transform(source.AsVector3Unsafe(), GetCieXyzToRgbMatrix(options.TargetRgbWorkingSpace))); + return FromScaledVector4(options.TargetRgbWorkingSpace.Compress(linear.ToScaledVector4())); + } + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + Matrix4x4 matrix = GetCieXyzToRgbMatrix(options.TargetRgbWorkingSpace); + for (int i = 0; i < source.Length; i++) + { + // Convert to linear rgb then compress. + Rgb linear = new(Vector3.Transform(source[i].AsVector3Unsafe(), matrix)); + Vector4 nonlinear = options.TargetRgbWorkingSpace.Compress(linear.ToScaledVector4()); + destination[i] = FromScaledVector4(nonlinear); + } + } + + /// + public CieXyz ToProfileConnectingSpace(ColorConversionOptions options) + { + // First expand to linear rgb + Rgb linear = FromScaledVector4(options.SourceRgbWorkingSpace.Expand(this.ToScaledVector4())); + + // Then convert to xyz + return new CieXyz(Vector3.Transform(linear.AsVector3Unsafe(), GetRgbToCieXyzMatrix(options.SourceRgbWorkingSpace))); + } + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + Matrix4x4 matrix = GetRgbToCieXyzMatrix(options.SourceRgbWorkingSpace); + for (int i = 0; i < source.Length; i++) + { + Rgb rgb = source[i]; + + // First expand to linear rgb + Rgb linear = FromScaledVector4(options.SourceRgbWorkingSpace.Expand(rgb.ToScaledVector4())); + + // Then convert to xyz + destination[i] = new CieXyz(Vector3.Transform(linear.AsVector3Unsafe(), matrix)); + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() + => ChromaticAdaptionWhitePointSource.RgbWorkingSpace; + + /// + /// Initializes the color instance from a generic scaled . + /// + /// The vector to load the color from. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb FromScaledVector3(Vector3 source) + => new(source); + + /// + /// Initializes the color instance for a source clamped between 0 and 1 + /// + /// The source to load the color from. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb Clamp(Rgb source) + => new(Vector3.Clamp(source.AsVector3Unsafe(), Vector3.Zero, Vector3.One)); + + /// + /// Expands the color into a generic ("scaled") representation + /// with values scaled and usually clamped between 0 and 1. + /// The vector components are typically expanded in least to greatest significance order. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector3 ToScaledVector3() + { + Vector3 v3 = default; + v3 += this.AsVector3Unsafe(); + return v3; + } + + /// + public override int GetHashCode() => HashCode.Combine(this.R, this.G, this.B); + + /// + public override string ToString() => FormattableString.Invariant($"Rgb({this.R:#0.##}, {this.G:#0.##}, {this.B:#0.##})"); + + /// + public override bool Equals(object? obj) => obj is Rgb other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Rgb other) + => this.AsVector3Unsafe() == other.AsVector3Unsafe(); + + internal Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this)); + + private static Matrix4x4 GetCieXyzToRgbMatrix(RgbWorkingSpace workingSpace) + { + Matrix4x4 matrix = GetRgbToCieXyzMatrix(workingSpace); + Matrix4x4.Invert(matrix, out Matrix4x4 inverseMatrix); + return inverseMatrix; + } + + private static Matrix4x4 GetRgbToCieXyzMatrix(RgbWorkingSpace workingSpace) + { + DebugGuard.NotNull(workingSpace, nameof(workingSpace)); + RgbPrimariesChromaticityCoordinates chromaticity = workingSpace.ChromaticityCoordinates; + + float xr = chromaticity.R.X; + float xg = chromaticity.G.X; + float xb = chromaticity.B.X; + float yr = chromaticity.R.Y; + float yg = chromaticity.G.Y; + float yb = chromaticity.B.Y; + + float mXr = xr / yr; + float mZr = (1 - xr - yr) / yr; + + float mXg = xg / yg; + float mZg = (1 - xg - yg) / yg; + + float mXb = xb / yb; + float mZb = (1 - xb - yb) / yb; + + Matrix4x4 xyzMatrix = new() + { + M11 = mXr, + M21 = mXg, + M31 = mXb, + M12 = 1F, + M22 = 1F, + M32 = 1F, + M13 = mZr, + M23 = mZg, + M33 = mZb, + M44 = 1F + }; + + Matrix4x4.Invert(xyzMatrix, out Matrix4x4 inverseXyzMatrix); + + Vector3 vector = Vector3.Transform(workingSpace.WhitePoint.AsVector3Unsafe(), inverseXyzMatrix); + + // Use transposed Rows/Columns + return new Matrix4x4 + { + M11 = vector.X * mXr, + M21 = vector.Y * mXg, + M31 = vector.Z * mXb, + M12 = vector.X, + M22 = vector.Y, + M32 = vector.Z, + M13 = vector.X * mZr, + M23 = vector.Y * mZg, + M33 = vector.Z * mZb, + M44 = 1F + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 ReadVector512(ref float src) + { + ref byte b = ref Unsafe.As(ref src); + return Unsafe.ReadUnaligned>(ref b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ReadVector256(ref float src) + { + ref byte b = ref Unsafe.As(ref src); + return Unsafe.ReadUnaligned>(ref b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteVector512(ref float dst, Vector512 value) + { + ref byte b = ref Unsafe.As(ref dst); + Unsafe.WriteUnaligned(ref b, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteVector256(ref float dst, Vector256 value) + { + ref byte b = ref Unsafe.As(ref dst); + Unsafe.WriteUnaligned(ref b, value); + } + } +} diff --git a/ImageSharp/ColorProfiles/RgbPrimariesChromaticityCoordinates.cs b/ImageSharp/ColorProfiles/RgbPrimariesChromaticityCoordinates.cs new file mode 100644 index 0000000..bfe1f5d --- /dev/null +++ b/ImageSharp/ColorProfiles/RgbPrimariesChromaticityCoordinates.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; +using System; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Represents the chromaticity coordinates of RGB primaries. + /// One of the specifiers of . + /// + public readonly struct RgbPrimariesChromaticityCoordinates : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The chromaticity coordinates of the red channel. + /// The chromaticity coordinates of the green channel. + /// The chromaticity coordinates of the blue channel. + public RgbPrimariesChromaticityCoordinates(CieXyChromaticityCoordinates r, CieXyChromaticityCoordinates g, CieXyChromaticityCoordinates b) + { + this.R = r; + this.G = g; + this.B = b; + } + + /// + /// Gets the chromaticity coordinates of the red channel. + /// + public CieXyChromaticityCoordinates R { get; } + + /// + /// Gets the chromaticity coordinates of the green channel. + /// + public CieXyChromaticityCoordinates G { get; } + + /// + /// Gets the chromaticity coordinates of the blue channel. + /// + public CieXyChromaticityCoordinates B { get; } + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + public static bool operator ==(RgbPrimariesChromaticityCoordinates left, RgbPrimariesChromaticityCoordinates right) + => left.Equals(right); + + /// + /// Compares two objects for inequality + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + public static bool operator !=(RgbPrimariesChromaticityCoordinates left, RgbPrimariesChromaticityCoordinates right) + => !left.Equals(right); + + /// + public override bool Equals(object? obj) + => obj is RgbPrimariesChromaticityCoordinates other && this.Equals(other); + + /// + public bool Equals(RgbPrimariesChromaticityCoordinates other) + => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B); + + /// + public override int GetHashCode() => HashCode.Combine(this.R, this.G, this.B); + } +} diff --git a/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs b/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs new file mode 100644 index 0000000..251a406 --- /dev/null +++ b/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Implementation of the Von Kries chromatic adaptation model. + /// + /// + /// Transformation described here: + /// http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html + /// + public static class VonKriesChromaticAdaptation + { + /// + /// Performs a linear transformation of a source color in to the destination color. + /// + /// Doesn't crop the resulting color space coordinates (e.g. allows negative values for XYZ coordinates). + /// The source color. + /// The conversion white points. + /// The chromatic adaptation matrix. + /// The + public static CieXyz Transform(in CieXyz source, (CieXyz From, CieXyz To) whitePoints, Matrix4x4 matrix) + { + CieXyz from = whitePoints.From; + CieXyz to = whitePoints.To; + + if (from.Equals(to)) + { + return new CieXyz(source.X, source.Y, source.Z); + } + + Vector3 sourceColorLms = Vector3.Transform(source.AsVector3Unsafe(), matrix); + Vector3 sourceWhitePointLms = Vector3.Transform(from.AsVector3Unsafe(), matrix); + Vector3 targetWhitePointLms = Vector3.Transform(to.AsVector3Unsafe(), matrix); + + Vector3 vector = targetWhitePointLms / sourceWhitePointLms; + Vector3 targetColorLms = Vector3.Multiply(vector, sourceColorLms); + + Matrix4x4.Invert(matrix, out Matrix4x4 inverseMatrix); + return new CieXyz(Vector3.Transform(targetColorLms, inverseMatrix)); + } + + /// + /// Performs a bulk linear transformation of a source color in to the destination color. + /// + /// Doesn't crop the resulting color space coordinates (e. g. allows negative values for XYZ coordinates). + /// The span to the source colors. + /// The span to the destination colors. + /// The conversion white points. + /// The chromatic adaptation matrix. + public static void Transform( + ReadOnlySpan source, + Span destination, + (CieXyz From, CieXyz To) whitePoints, + Matrix4x4 matrix) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + int count = source.Length; + + CieXyz from = whitePoints.From; + CieXyz to = whitePoints.To; + + if (from.Equals(to)) + { + source.CopyTo(destination[..count]); + return; + } + + Matrix4x4.Invert(matrix, out Matrix4x4 inverseMatrix); + + ref CieXyz sourceBase = ref MemoryMarshal.GetReference(source); + ref CieXyz destinationBase = ref MemoryMarshal.GetReference(destination); + + Vector3 sourceWhitePointLms = Vector3.Transform(from.AsVector3Unsafe(), matrix); + Vector3 targetWhitePointLms = Vector3.Transform(to.AsVector3Unsafe(), matrix); + + Vector3 vector = targetWhitePointLms / sourceWhitePointLms; + + for (nuint i = 0; i < (uint)count; i++) + { + ref CieXyz sp = ref Unsafe.Add(ref sourceBase, i); + ref CieXyz dp = ref Unsafe.Add(ref destinationBase, i); + + Vector3 sourceColorLms = Vector3.Transform(sp.AsVector3Unsafe(), matrix); + + Vector3 targetColorLms = Vector3.Multiply(vector, sourceColorLms); + dp = new CieXyz(Vector3.Transform(targetColorLms, inverseMatrix)); + } + } + } +} diff --git a/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs b/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs new file mode 100644 index 0000000..3fe5032 --- /dev/null +++ b/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs @@ -0,0 +1,70 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles.Companding; + +namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces { + /// + /// The gamma working space. + /// + public sealed class GammaWorkingSpace : RgbWorkingSpace + { + /// + /// Initializes a new instance of the class. + /// + /// The gamma value. + /// The reference white point. + /// The chromaticity of the rgb primaries. + public GammaWorkingSpace(float gamma, CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates) + : base(referenceWhite, chromaticityCoordinates) => this.Gamma = gamma; + + /// + /// Gets the gamma value. + /// + public float Gamma { get; } + + /// + public override void Compress(Span vectors) => GammaCompanding.Compress(vectors, this.Gamma); + + /// + public override void Expand(Span vectors) => GammaCompanding.Expand(vectors, this.Gamma); + + /// + public override Vector4 Compress(Vector4 vector) => GammaCompanding.Compress(vector, this.Gamma); + + /// + public override Vector4 Expand(Vector4 vector) => GammaCompanding.Expand(vector, this.Gamma); + + /// + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is GammaWorkingSpace other) + { + return this.Gamma.Equals(other.Gamma) + && this.WhitePoint.Equals(other.WhitePoint) + && this.ChromaticityCoordinates.Equals(other.ChromaticityCoordinates); + } + + return false; + } + + /// + public override int GetHashCode() => HashCode.Combine( + typeof(GammaWorkingSpace), + this.WhitePoint, + this.ChromaticityCoordinates, + this.Gamma); + } +} diff --git a/ImageSharp/ColorProfiles/WorkingSpaces/LWorkingSpace.cs b/ImageSharp/ColorProfiles/WorkingSpaces/LWorkingSpace.cs new file mode 100644 index 0000000..5b58724 --- /dev/null +++ b/ImageSharp/ColorProfiles/WorkingSpaces/LWorkingSpace.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles.Companding; + +namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces { + /// + /// L* working space. + /// + public sealed class LWorkingSpace : RgbWorkingSpace + { + /// + /// Initializes a new instance of the class. + /// + /// The reference white point. + /// The chromaticity of the rgb primaries. + public LWorkingSpace(CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates) + : base(referenceWhite, chromaticityCoordinates) + { + } + + /// + public override void Compress(Span vectors) => LCompanding.Compress(vectors); + + /// + public override void Expand(Span vectors) => LCompanding.Expand(vectors); + + /// + public override Vector4 Compress(Vector4 vector) => LCompanding.Compress(vector); + + /// + public override Vector4 Expand(Vector4 vector) => LCompanding.Expand(vector); + } +} diff --git a/ImageSharp/ColorProfiles/WorkingSpaces/Rec2020WorkingSpace.cs b/ImageSharp/ColorProfiles/WorkingSpaces/Rec2020WorkingSpace.cs new file mode 100644 index 0000000..96f1575 --- /dev/null +++ b/ImageSharp/ColorProfiles/WorkingSpaces/Rec2020WorkingSpace.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles.Companding; + +namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces { + /// + /// Rec. 2020 (ITU-R Recommendation BT.2020F) working space. + /// + public sealed class Rec2020WorkingSpace : RgbWorkingSpace + { + /// + /// Initializes a new instance of the class. + /// + /// The reference white point. + /// The chromaticity of the rgb primaries. + public Rec2020WorkingSpace(CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates) + : base(referenceWhite, chromaticityCoordinates) + { + } + + /// + public override void Compress(Span vectors) => Rec2020Companding.Compress(vectors); + + /// + public override void Expand(Span vectors) => Rec2020Companding.Expand(vectors); + + /// + public override Vector4 Compress(Vector4 vector) => Rec2020Companding.Compress(vector); + + /// + public override Vector4 Expand(Vector4 vector) => Rec2020Companding.Expand(vector); + } +} diff --git a/ImageSharp/ColorProfiles/WorkingSpaces/Rec709WorkingSpace.cs b/ImageSharp/ColorProfiles/WorkingSpaces/Rec709WorkingSpace.cs new file mode 100644 index 0000000..6f870f3 --- /dev/null +++ b/ImageSharp/ColorProfiles/WorkingSpaces/Rec709WorkingSpace.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles.Companding; + +namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces { + /// + /// Rec. 709 (ITU-R Recommendation BT.709) working space. + /// + public sealed class Rec709WorkingSpace : RgbWorkingSpace + { + /// + /// Initializes a new instance of the class. + /// + /// The reference white point. + /// The chromaticity of the rgb primaries. + public Rec709WorkingSpace(CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates) + : base(referenceWhite, chromaticityCoordinates) + { + } + + /// + public override void Compress(Span vectors) => Rec709Companding.Compress(vectors); + + /// + public override void Expand(Span vectors) => Rec709Companding.Expand(vectors); + + /// + public override Vector4 Compress(Vector4 vector) => Rec709Companding.Compress(vector); + + /// + public override Vector4 Expand(Vector4 vector) => Rec709Companding.Expand(vector); + } +} diff --git a/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs b/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs new file mode 100644 index 0000000..e7dadfe --- /dev/null +++ b/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces { + /// + /// Base class for all implementations of . + /// + public abstract class RgbWorkingSpace + { + /// + /// Initializes a new instance of the class. + /// + /// The reference white point. + /// The chromaticity of the rgb primaries. + protected RgbWorkingSpace(CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates) + { + this.WhitePoint = referenceWhite; + this.ChromaticityCoordinates = chromaticityCoordinates; + } + + /// + /// Gets the reference white point + /// + public CieXyz WhitePoint { get; } + + /// + /// Gets the chromaticity of the rgb primaries. + /// + public RgbPrimariesChromaticityCoordinates ChromaticityCoordinates { get; } + + /// + /// Compresses the linear vectors to their nonlinear equivalents with respect to the energy. + /// + /// The span of vectors. + public abstract void Compress(Span vectors); + + /// + /// Expands the nonlinear vectors to their linear equivalents with respect to the energy. + /// + /// The span of vectors. + public abstract void Expand(Span vectors); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public abstract Vector4 Compress(Vector4 vector); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public abstract Vector4 Expand(Vector4 vector); + + /// + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() == this.GetType()) + { + RgbWorkingSpace other = (RgbWorkingSpace)obj; + + return this.WhitePoint.Equals(other.WhitePoint) + && this.ChromaticityCoordinates.Equals(other.ChromaticityCoordinates); + } + + return false; + } + + /// + public override int GetHashCode() + => HashCode.Combine(this.GetType(), this.WhitePoint, this.ChromaticityCoordinates); + } +} diff --git a/ImageSharp/ColorProfiles/WorkingSpaces/SRgbWorkingSpace.cs b/ImageSharp/ColorProfiles/WorkingSpaces/SRgbWorkingSpace.cs new file mode 100644 index 0000000..beba6ef --- /dev/null +++ b/ImageSharp/ColorProfiles/WorkingSpaces/SRgbWorkingSpace.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles.Companding; + +namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces { + /// + /// The sRgb working space. + /// + public sealed class SRgbWorkingSpace : RgbWorkingSpace + { + /// + /// Initializes a new instance of the class. + /// + /// The reference white point. + /// The chromaticity of the rgb primaries. + public SRgbWorkingSpace(CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates) + : base(referenceWhite, chromaticityCoordinates) + { + } + + /// + public override void Compress(Span vectors) => SRgbCompanding.Compress(vectors); + + /// + public override void Expand(Span vectors) => SRgbCompanding.Expand(vectors); + + /// + public override Vector4 Compress(Vector4 vector) => SRgbCompanding.Compress(vector); + + /// + public override Vector4 Expand(Vector4 vector) => SRgbCompanding.Expand(vector); + } +} diff --git a/ImageSharp/ColorProfiles/Y.cs b/ImageSharp/ColorProfiles/Y.cs new file mode 100644 index 0000000..230ce32 --- /dev/null +++ b/ImageSharp/ColorProfiles/Y.cs @@ -0,0 +1,143 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Represents a Y (luminance) color. + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct Y : IColorProfile + { + /// + /// Initializes a new instance of the struct. + /// + /// The luminance component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Y(float l) => this.L = Numerics.Clamp(l, 0, 1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + private Y(float l, bool _) => this.L = l; +#pragma warning restore SA1313 // Parameter names should begin with lower-case letter + + /// + /// Gets the luminance component. + /// + /// A value ranging between 0 and 1. + public float L { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Y left, Y right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Y left, Y right) => !left.Equals(right); + + /// + public Vector4 ToScaledVector4() => new(this.L); + + /// + public static Y FromScaledVector4(Vector4 source) => new(source.X, true); + + /// + public static void ToScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToScaledVector4(); + } + } + + /// + public static void FromScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = FromScaledVector4(source[i]); + } + } + + /// + public Rgb ToProfileConnectingSpace(ColorConversionOptions options) + => new(this.L, this.L, this.L); + + /// + public static Y FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source) + { + Matrix4x4 m = options.YCbCrTransform.Forward; + float offset = options.YCbCrTransform.Offset.X; + return new Y(Vector3.Dot(source.AsVector3Unsafe(), new Vector3(m.M11, m.M12, m.M13)) + offset); + } + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: We can optimize this by using SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToProfileConnectingSpace(options); + } + } + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: We can optimize this by using SIMD + for (int i = 0; i < source.Length; i++) + { + Rgb rgb = source[i]; + destination[i] = FromProfileConnectingSpace(options, in rgb); + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() + => ChromaticAdaptionWhitePointSource.RgbWorkingSpace; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() + => this.L.GetHashCode(); + + /// + public override string ToString() + => FormattableString.Invariant($"Y({this.L:#0.##})"); + + /// + public override bool Equals(object? obj) + => obj is Y other && this.Equals(other); + + /// + public bool Equals(Y other) => this.L == other.L; + } +} diff --git a/ImageSharp/ColorProfiles/YCbCr.cs b/ImageSharp/ColorProfiles/YCbCr.cs new file mode 100644 index 0000000..63c8dbb --- /dev/null +++ b/ImageSharp/ColorProfiles/YCbCr.cs @@ -0,0 +1,195 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Represents an YCbCr (luminance, blue chroma, red chroma) color. + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct YCbCr : IColorProfile + { + private static readonly Vector3 Min = Vector3.Zero; + private static readonly Vector3 Max = Vector3.One; + + /// + /// Initializes a new instance of the struct. + /// + /// The y luminance component. + /// The cb chroma component. + /// The cr chroma component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public YCbCr(float y, float cb, float cr) + : this(new Vector3(y, cb, cr)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the y, cb, cr components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public YCbCr(Vector3 vector) + { + vector = Vector3.Clamp(vector, Min, Max); + this.Y = vector.X; + this.Cb = vector.Y; + this.Cr = vector.Z; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + private YCbCr(Vector3 vector, bool _) +#pragma warning restore SA1313 // Parameter names should begin with lower-case letter + { + this.Y = vector.X; + this.Cb = vector.Y; + this.Cr = vector.Z; + } + + /// + /// Gets the Y luminance component. + /// A value ranging between 0 and 1. + /// + public float Y { get; } + + /// + /// Gets the Cb chroma component. + /// A value ranging between 0 and 1. + /// + public float Cb { get; } + + /// + /// Gets the Cr chroma component. + /// A value ranging between 0 and 1. + /// + public float Cr { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + public static bool operator ==(YCbCr left, YCbCr right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(YCbCr left, YCbCr right) => !left.Equals(right); + + /// + public Vector4 ToScaledVector4() + { + Vector3 v3 = default; + v3 += this.AsVector3Unsafe(); + return new Vector4(v3, 1F); + } + + /// + public static YCbCr FromScaledVector4(Vector4 source) + => new(source.AsVector3(), true); + + /// + public static void ToScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToScaledVector4(); + } + } + + /// + public static void FromScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = FromScaledVector4(source[i]); + } + } + + /// + public static YCbCr FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source) + { + Vector3 rgb = source.AsVector3Unsafe(); + Matrix4x4 m = options.TransposedYCbCrTransform.Forward; + Vector3 offset = options.TransposedYCbCrTransform.Offset; + + return new YCbCr(Vector3.Transform(rgb, m) + offset, true); + } + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: We can optimize this by using SIMD + for (int i = 0; i < source.Length; i++) + { + Rgb rgb = source[i]; + destination[i] = FromProfileConnectingSpace(options, in rgb); + } + } + + /// + public Rgb ToProfileConnectingSpace(ColorConversionOptions options) + { + Matrix4x4 m = options.TransposedYCbCrTransform.Inverse; + Vector3 offset = options.TransposedYCbCrTransform.Offset; + Vector3 normalized = this.AsVector3Unsafe() - offset; + + return Rgb.FromScaledVector3(Vector3.Transform(normalized, m)); + } + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: We can optimize this by using SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToProfileConnectingSpace(options); + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() + => ChromaticAdaptionWhitePointSource.RgbWorkingSpace; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => HashCode.Combine(this.Y, this.Cb, this.Cr); + + /// + public override string ToString() => FormattableString.Invariant($"YCbCr({this.Y}, {this.Cb}, {this.Cr})"); + + /// + public override bool Equals(object? obj) => obj is YCbCr other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(YCbCr other) + => this.AsVector3Unsafe() == other.AsVector3Unsafe(); + + private Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this)); + } +} diff --git a/ImageSharp/ColorProfiles/YCbCrTransform.cs b/ImageSharp/ColorProfiles/YCbCrTransform.cs new file mode 100644 index 0000000..3645f2e --- /dev/null +++ b/ImageSharp/ColorProfiles/YCbCrTransform.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// + /// Represents a YCbCr color transform containing forward and inverse transformation matrices, + /// and the chrominance offsets to apply for full-range encoding + /// + /// + /// These matrices must be selected to match the characteristics of the associated , + /// including its transfer function (gamma or companding) and chromaticity coordinates. Using mismatched matrices and + /// working spaces will produce incorrect conversions. + /// + /// + public readonly struct YCbCrTransform + { + /// + /// Initializes a new instance of the struct. + /// + /// + /// The forward transformation matrix from RGB to YCbCr. The matrix must include the + /// standard chrominance offsets in the fourth column, such as (0, 0.5, 0.5). + /// + /// + /// The inverse transformation matrix from YCbCr to RGB. This matrix expects that + /// chrominance offsets have already been subtracted prior to application. + /// + /// + /// The chrominance offsets to be added after the forward conversion, + /// and subtracted before the inverse conversion. Usually (0, 0.5, 0.5). + /// + public YCbCrTransform(Matrix4x4 forward, Matrix4x4 inverse, Vector3 offset) + { + this.Forward = forward; + this.Inverse = inverse; + this.Offset = offset; + } + + /// + /// Gets the matrix used to convert gamma-encoded RGB to YCbCr. + /// + public Matrix4x4 Forward { get; } + + /// + /// Gets the matrix used to convert YCbCr back to gamma-encoded RGB. + /// + public Matrix4x4 Inverse { get; } + + /// + /// Gets the chrominance offset vector to apply during encoding (add) or decoding (subtract). + /// + public Vector3 Offset { get; } + + internal YCbCrTransform Transpose() + => new(Matrix4x4.Transpose(this.Forward), Matrix4x4.Transpose(this.Inverse), this.Offset); + } +} diff --git a/ImageSharp/ColorProfiles/YccK.cs b/ImageSharp/ColorProfiles/YccK.cs new file mode 100644 index 0000000..aa112a0 --- /dev/null +++ b/ImageSharp/ColorProfiles/YccK.cs @@ -0,0 +1,207 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.ColorProfiles { + /// + /// Represents a YCCK (luminance, blue chroma, red chroma, black) color. + /// YCCK is not a true color space but a reversible transform of CMYK, where the CMY components + /// are converted to YCbCr using the ITU-R BT.601 standard, and the K (black) component is preserved separately. + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct YccK : IColorProfile + { + private static readonly Vector4 Min = Vector4.Zero; + private static readonly Vector4 Max = Vector4.One; + + /// + /// Initializes a new instance of the struct. + /// + /// The y luminance component. + /// The cb chroma component. + /// The cr chroma component. + /// The keyline black component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public YccK(float y, float cb, float cr, float k) + : this(new Vector4(y, cb, cr, k)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the c, m, y, k components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public YccK(Vector4 vector) + { + vector = Vector4.Clamp(vector, Min, Max); + this.Y = vector.X; + this.Cb = vector.Y; + this.Cr = vector.Z; + this.K = vector.W; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + private YccK(Vector4 vector, bool _) +#pragma warning restore SA1313 // Parameter names should begin with lower-case letter + { + this.Y = vector.X; + this.Cb = vector.Y; + this.Cr = vector.Z; + this.K = vector.W; + } + + /// + /// Gets the Y luminance component. + /// A value ranging between 0 and 1. + /// + public float Y { get; } + + /// + /// Gets the C (blue) chroma component. + /// A value ranging between 0 and 1. + /// + public float Cb { get; } + + /// + /// Gets the C (red) chroma component. + /// A value ranging between 0 and 1. + /// + public float Cr { get; } + + /// + /// Gets the keyline black color component. + /// A value ranging between 0 and 1. + /// + public float K { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(YccK left, YccK right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(YccK left, YccK right) => !left.Equals(right); + + /// + public Vector4 ToScaledVector4() + { + Vector4 v4 = default; + v4 += this.AsVector4Unsafe(); + return v4; + } + + /// + public static YccK FromScaledVector4(Vector4 source) + => new(source, true); + + /// + public static void ToScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + MemoryMarshal.Cast(source).CopyTo(destination); + } + + /// + public static void FromScaledVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + MemoryMarshal.Cast(source).CopyTo(destination); + } + + /// + public Rgb ToProfileConnectingSpace(ColorConversionOptions options) + { + Matrix4x4 m = options.TransposedYCbCrTransform.Inverse; + Vector3 offset = options.TransposedYCbCrTransform.Offset; + Vector3 normalized = this.AsVector3Unsafe() - offset; + + return Rgb.FromScaledVector3(Vector3.Transform(normalized, m) * (1F - this.K)); + } + + /// + public static YccK FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source) + { + Matrix4x4 m = options.TransposedYCbCrTransform.Forward; + Vector3 offset = options.TransposedYCbCrTransform.Offset; + + Vector3 rgb = source.AsVector3Unsafe(); + float k = 1F - MathF.Max(rgb.X, MathF.Max(rgb.Y, rgb.Z)); + + if (k >= 1F - Constants.Epsilon) + { + return new YccK(new Vector4(0F, 0.5F, 0.5F, 1F), true); + } + + rgb /= 1F - k; + return new YccK(new Vector4(Vector3.Transform(rgb, m), k) + new Vector4(offset, 0F)); + } + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + // TODO: We can possibly optimize this by using SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToProfileConnectingSpace(options); + } + } + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: We can optimize this by using SIMD + for (int i = 0; i < source.Length; i++) + { + Rgb rgb = source[i]; + destination[i] = FromProfileConnectingSpace(options, in rgb); + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() + => ChromaticAdaptionWhitePointSource.RgbWorkingSpace; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() + => HashCode.Combine(this.Y, this.Cb, this.Cr, this.K); + + /// + public override string ToString() + => FormattableString.Invariant($"YccK({this.Y:#0.##}, {this.Cb:#0.##}, {this.Cr:#0.##}, {this.K:#0.##})"); + + /// + public override bool Equals(object? obj) + => obj is YccK other && this.Equals(other); + + /// + public bool Equals(YccK other) + => this.AsVector4Unsafe() == other.AsVector4Unsafe(); + + private Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this)); + + private Vector4 AsVector4Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this)); + } +} diff --git a/ImageSharp/Common/ByteOrder.cs b/ImageSharp/Common/ByteOrder.cs new file mode 100644 index 0000000..c3704ab --- /dev/null +++ b/ImageSharp/Common/ByteOrder.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp { + /// + /// The byte order of the data stream. + /// + public enum ByteOrder + { + /// + /// The big-endian byte order (Motorola). + /// Most-significant byte comes first, and ends with the least-significant byte. + /// + BigEndian, + + /// + /// The little-endian byte order (Intel). + /// Least-significant byte comes first and ends with the most-significant byte. + /// + LittleEndian + } +} diff --git a/ImageSharp/Common/Constants.cs b/ImageSharp/Common/Constants.cs new file mode 100644 index 0000000..5af1725 --- /dev/null +++ b/ImageSharp/Common/Constants.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp { + /// + /// Common constants used throughout the project + /// + internal static class Constants + { + /// + /// The epsilon value for comparing floating point numbers. + /// + public static readonly float Epsilon = 0.001F; + + /// + /// The epsilon squared value for comparing floating point numbers. + /// + public static readonly float EpsilonSquared = Epsilon * Epsilon; + } +} diff --git a/ImageSharp/Common/Exceptions/ImageFormatException.cs b/ImageSharp/Common/Exceptions/ImageFormatException.cs new file mode 100644 index 0000000..dd0f7b4 --- /dev/null +++ b/ImageSharp/Common/Exceptions/ImageFormatException.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp { + /// + /// The exception that is thrown when the library tries to load + /// an image, which has format or content that is invalid or unsupported by ImageSharp. + /// + public class ImageFormatException : Exception + { + /// + /// Initializes a new instance of the class with the name of the + /// parameter that causes this exception. + /// + /// The error message that explains the reason for this exception. + internal ImageFormatException(string errorMessage) + : base(errorMessage) + { + } + + /// + /// Initializes a new instance of the class with a specified + /// error message and the exception that is the cause of this exception. + /// + /// The error message that explains the reason for this exception. + /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) + /// if no inner exception is specified. + internal ImageFormatException(string errorMessage, Exception innerException) + : base(errorMessage, innerException) + { + } + } +} diff --git a/ImageSharp/Common/Exceptions/ImageProcessingException.cs b/ImageSharp/Common/Exceptions/ImageProcessingException.cs new file mode 100644 index 0000000..1174e54 --- /dev/null +++ b/ImageSharp/Common/Exceptions/ImageProcessingException.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp { + /// + /// The exception that is thrown when an error occurs when applying a process to an image. + /// + public sealed class ImageProcessingException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public ImageProcessingException() + { + } + + /// + /// Initializes a new instance of the class with the name of the + /// parameter that causes this exception. + /// + /// The error message that explains the reason for this exception. + public ImageProcessingException(string errorMessage) + : base(errorMessage) + { + } + + /// + /// Initializes a new instance of the class with a specified + /// error message and the exception that is the cause of this exception. + /// + /// The error message that explains the reason for this exception. + /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) + /// if no inner exception is specified. + public ImageProcessingException(string errorMessage, Exception innerException) + : base(errorMessage, innerException) + { + } + } +} diff --git a/ImageSharp/Common/Exceptions/InvalidImageContentException.cs b/ImageSharp/Common/Exceptions/InvalidImageContentException.cs new file mode 100644 index 0000000..68bd3c8 --- /dev/null +++ b/ImageSharp/Common/Exceptions/InvalidImageContentException.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using System; + +namespace SixLabors.ImageSharp { + /// + /// The exception that is thrown when the library tries to load + /// an image which contains invalid content. + /// + public sealed class InvalidImageContentException : ImageFormatException + { + /// + /// Initializes a new instance of the class with the name of the + /// parameter that causes this exception. + /// + /// The error message that explains the reason for this exception. + public InvalidImageContentException(string errorMessage) + : base(errorMessage) + { + } + + /// + /// Initializes a new instance of the class with the name of the + /// parameter that causes this exception. + /// + /// The error message that explains the reason for this exception. + /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) + /// if no inner exception is specified. + public InvalidImageContentException(string errorMessage, Exception innerException) + : base(errorMessage, innerException) + { + } + + internal InvalidImageContentException(Size size, InvalidMemoryOperationException memoryException) + : this($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {size.Width}x{size.Height}.", memoryException) + { + } + } +} diff --git a/ImageSharp/Common/Exceptions/UnknownImageFormatException.cs b/ImageSharp/Common/Exceptions/UnknownImageFormatException.cs new file mode 100644 index 0000000..3462c01 --- /dev/null +++ b/ImageSharp/Common/Exceptions/UnknownImageFormatException.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp { + /// + /// The exception that is thrown when the library tries to load + /// an image which has an unknown format. + /// + public sealed class UnknownImageFormatException : ImageFormatException + { + /// + /// Initializes a new instance of the class with the name of the + /// parameter that causes this exception. + /// + /// The error message that explains the reason for this exception. + public UnknownImageFormatException(string errorMessage) + : base(errorMessage) + { + } + } +} diff --git a/ImageSharp/Common/Extensions/ConfigurationExtensions.cs b/ImageSharp/Common/Extensions/ConfigurationExtensions.cs new file mode 100644 index 0000000..369e95a --- /dev/null +++ b/ImageSharp/Common/Extensions/ConfigurationExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Threading.Tasks; + +namespace SixLabors.ImageSharp { + /// + /// Contains extension methods for + /// + internal static class ConfigurationExtensions + { + /// + /// Creates a object based on , + /// having set to + /// + public static ParallelOptions GetParallelOptions(this Configuration configuration) + { + return new ParallelOptions { MaxDegreeOfParallelism = configuration.MaxDegreeOfParallelism }; + } + } +} diff --git a/ImageSharp/Common/Extensions/EnumerableExtensions.cs b/ImageSharp/Common/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..71a51b6 --- /dev/null +++ b/ImageSharp/Common/Extensions/EnumerableExtensions.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp { + /// + /// Encapsulates a series of time saving extension methods to the interface. + /// + internal static class EnumerableExtensions + { + /// + /// Generates a sequence of integral numbers within a specified range. + /// + /// + /// The start index, inclusive. + /// + /// + /// A method that has one parameter and returns a calculating the end index. + /// + /// + /// The incremental step. + /// + /// + /// The that contains a range of sequential integral numbers. + /// + public static IEnumerable SteppedRange(int fromInclusive, Func toDelegate, int step) + { + return RangeIterator(fromInclusive, toDelegate, step); + } + + /// + /// Generates a sequence of integral numbers within a specified range. + /// + /// The start index, inclusive. + /// + /// A method that has one parameter and returns a calculating the end index. + /// + /// The incremental step. + /// + /// The that contains a range of sequential integral numbers. + /// + private static IEnumerable RangeIterator(int fromInclusive, Func toDelegate, int step) + { + int i = fromInclusive; + while (toDelegate(i)) + { + yield return i; + i += step; + } + } + } +} diff --git a/ImageSharp/Common/Extensions/StreamExtensions.cs b/ImageSharp/Common/Extensions/StreamExtensions.cs new file mode 100644 index 0000000..97f979d --- /dev/null +++ b/ImageSharp/Common/Extensions/StreamExtensions.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.IO; + +namespace SixLabors.ImageSharp { + /// + /// Extension methods for the type. + /// + internal static class StreamExtensions + { + /// + /// Writes data from a stream from the provided buffer. + /// + /// The stream. + /// The buffer. + /// The offset within the buffer to begin writing. + /// The number of bytes to write to the stream. + public static void Write(this Stream stream, Span buffer, int offset, int count) + => stream.Write(buffer.Slice(offset, count)); + + /// + /// Reads data from a stream into the provided buffer. + /// + /// The stream. + /// The buffer. + /// The offset within the buffer where the bytes are read into. + /// The number of bytes, if available, to read. + /// The actual number of bytes read. + public static int Read(this Stream stream, Span buffer, int offset, int count) + => stream.Read(buffer.Slice(offset, count)); + + /// + /// Skips the number of bytes in the given stream. + /// + /// The stream. + /// A byte offset relative to the origin parameter. + public static void Skip(this Stream stream, int count) + { + if (count < 1) + { + return; + } + + if (stream.CanSeek) + { + stream.Seek(count, SeekOrigin.Current); + return; + } + + byte[] buffer = ArrayPool.Shared.Rent(count); + try + { + while (count > 0) + { + int bytesRead = stream.Read(buffer, 0, count); + if (bytesRead == 0) + { + break; + } + + count -= bytesRead; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + } +} diff --git a/ImageSharp/Common/Extensions/Vector4Extensions.cs b/ImageSharp/Common/Extensions/Vector4Extensions.cs new file mode 100644 index 0000000..4e8ea61 --- /dev/null +++ b/ImageSharp/Common/Extensions/Vector4Extensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#if !NET9_0_OR_GREATER +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp; + +internal static class Vector4Extensions +{ + /// + /// Reinterprets a as a new . + /// + /// The vector to reinterpret. + /// reinterpreted as a new . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 AsVector3(this Vector4 value) => value.AsVector128().AsVector3(); +} +#endif diff --git a/ImageSharp/Common/Helpers/ColorNumerics.cs b/ImageSharp/Common/Helpers/ColorNumerics.cs new file mode 100644 index 0000000..1cc94e5 --- /dev/null +++ b/ImageSharp/Common/Helpers/ColorNumerics.cs @@ -0,0 +1,266 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp { + /// + /// Provides optimized static methods for common mathematical functions specific + /// to color processing. + /// + internal static class ColorNumerics + { + /// + /// Vector for converting pixel to gray value as specified by + /// ITU-R Recommendation BT.709. + /// + private static readonly Vector4 Bt709 = new(.2126f, .7152f, .0722f, 0.0f); + + /// + /// Convert a pixel value to grayscale using ITU-R Recommendation BT.709. + /// + /// The vector to get the luminance from. + /// + /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetBT709Luminance(ref Vector4 vector, int luminanceLevels) + => (int)MathF.Round(Vector4.Dot(vector, Bt709) * (luminanceLevels - 1)); + + /// + /// Gets the luminance from the rgb components using the formula + /// as specified by ITU-R Recommendation BT.709. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Get8BitBT709Luminance(byte r, byte g, byte b) + => (byte)((r * .2126F) + (g * .7152F) + (b * .0722F) + 0.5F); + + /// + /// Gets the luminance from the rgb components using the formula + /// as specified by ITU-R Recommendation BT.709. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Get8BitBT709Luminance(ushort r, ushort g, ushort b) + => (byte)((From16BitTo8Bit(r) * .2126F) + + (From16BitTo8Bit(g) * .7152F) + + (From16BitTo8Bit(b) * .0722F) + 0.5F); + + /// + /// Gets the luminance from the rgb components using the formula as + /// specified by ITU-R Recommendation BT.709. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort Get16BitBT709Luminance(byte r, byte g, byte b) + => (ushort)((From8BitTo16Bit(r) * .2126F) + + (From8BitTo16Bit(g) * .7152F) + + (From8BitTo16Bit(b) * .0722F) + 0.5F); + + /// + /// Gets the luminance from the rgb components using the formula as + /// specified by ITU-R Recommendation BT.709. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort Get16BitBT709Luminance(ushort r, ushort g, ushort b) + => (ushort)((r * .2126F) + (g * .7152F) + (b * .0722F) + 0.5F); + + /// + /// Gets the luminance from the rgb components using the formula as specified + /// by ITU-R Recommendation BT.709. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort Get16BitBT709Luminance(float r, float g, float b) + => (ushort)((r * .2126F) + (g * .7152F) + (b * .0722F) + 0.5F); + + /// + /// Scales a value from a 16 bit to an + /// 8 bit equivalent. + /// + /// The 16 bit component value. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte From16BitTo8Bit(ushort component) => + + // To scale to 8 bits From a 16-bit value V the required value (from the PNG specification) is: + // + // (V * 255) / 65535 + // + // This reduces to round(V / 257), or floor((V + 128.5)/257) + // + // Represent V as the two byte value vhi.vlo. Make a guess that the + // result is the top byte of V, vhi, then the correction to this value + // is: + // + // error = floor(((V-vhi.vhi) + 128.5) / 257) + // = floor(((vlo-vhi) + 128.5) / 257) + // + // This can be approximated using integer arithmetic (and a signed + // shift): + // + // error = (vlo-vhi+128) >> 8; + // + // The approximate differs from the exact answer only when (vlo-vhi) is + // 128; it then gives a correction of +1 when the exact correction is + // 0. This gives 128 errors. The exact answer (correct for all 16-bit + // input values) is: + // + // error = (vlo-vhi+128)*65535 >> 24; + // + // An alternative arithmetic calculation which also gives no errors is: + // + // (V * 255 + 32895) >> 16 + (byte)(((component * 255) + 32895) >> 16); + + /// + /// Scales a value from a 32 bit to an + /// 8 bit equivalent. + /// + /// The 32 bit component value. + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte From32BitTo8Bit(uint component) => + + // To scale to 8 bits from a 32-bit value V the required value is: + // + // (V * 255) / 4294967295 + // + // Since: + // + // 4294967295 = 255 * 16843009 + // + // this reduces exactly to: + // + // V / 16843009 + // + // To round to nearest using integer arithmetic we add half the divisor + // before dividing: + // + // (V + 16843009 / 2) / 16843009 + // + // where: + // + // 16843009 / 2 = 8421504.5 + // + // Using 8421504 ensures correct round-to-nearest behaviour: + // + // 8421504 -> 0 + // 8421505 -> 1 + // + // The addition must be performed in 64-bit to avoid overflow for large + // input values (for example uint.MaxValue). + // + // Final exact integer implementation: + // + // ((ulong)V + 8421504) / 16843009 + (byte)((component + 8421504UL) / 16843009UL); + + /// + /// Scales a value from an 8 bit to + /// an 16 bit equivalent. + /// + /// The 8 bit component value. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort From8BitTo16Bit(byte component) + => (ushort)(component * 257); + + /// + /// Scales a value from an 16 bit to + /// an 16 bit equivalent. + /// + /// The 16 bit component value. + /// The 32 bit + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint From16BitTo32Bit(ushort component) + => (uint)(component * 65537); + + /// + /// Scales a value from an 8 bit to + /// an 32 bit equivalent. + /// + /// The 8 bit component value. + /// The 32 bit + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint From8BitTo32Bit(byte component) + => (uint)(component * 16843009); + + /// + /// Returns how many bits are required to store the specified number of colors. + /// Performs a Log2() on the value. + /// + /// The number of colors. + /// + /// The + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetBitsNeededForColorDepth(int colors) + => Math.Max(1, (int)Math.Ceiling(Math.Log(colors, 2))); + + /// + /// Returns how many colors will be created by the specified number of bits. + /// + /// The bit depth. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetColorCountForBitDepth(int bitDepth) + => 1 << bitDepth; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector4 Transform(Vector4 vector, in ColorMatrix.Impl matrix) + { + Vector4 result = matrix.X * vector.X; + + result += matrix.Y * vector.Y; + result += matrix.Z * vector.Z; + result += matrix.W * vector.W; + result += matrix.V; + + return result; + } + + /// + /// Transforms a vector by the given color matrix. + /// + /// The source vector. + /// The transformation color matrix. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Transform(ref Vector4 vector, ref ColorMatrix matrix) + => vector = Transform(vector, matrix.AsImpl()); + + /// + /// Bulk variant of . + /// + /// The span of vectors + /// The transformation color matrix. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Transform(Span vectors, ref ColorMatrix matrix) + { + for (int i = 0; i < vectors.Length; i++) + { + ref Vector4 v = ref vectors[i]; + Transform(ref v, ref matrix); + } + } + } +} diff --git a/ImageSharp/Common/Helpers/DebugGuard.cs b/ImageSharp/Common/Helpers/DebugGuard.cs new file mode 100644 index 0000000..5a683b1 --- /dev/null +++ b/ImageSharp/Common/Helpers/DebugGuard.cs @@ -0,0 +1,84 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; + +// TODO: These should just call the guard equivalents +namespace SixLabors { + /// + /// Provides methods to protect against invalid parameters for a DEBUG build. + /// + internal static partial class DebugGuard + { + /// + /// Verifies whether a specific condition is met, throwing an exception if it's false. + /// + /// The condition + /// The error message + [Conditional("DEBUG")] + public static void IsTrue(bool target, string message) + { + if (!target) + { + throw new InvalidOperationException(message); + } + } + + /// + /// Verifies whether a condition (indicating disposed state) is met, throwing an ObjectDisposedException if it's true. + /// + /// Whether the object is disposed. + /// The name of the object. + [Conditional("DEBUG")] + public static void NotDisposed(bool isDisposed, string objectName) + { +#pragma warning disable CA1513 + if (isDisposed) + { + throw new ObjectDisposedException(objectName); + } +#pragma warning restore CA1513 + } + + /// + /// Verifies, that the target span is of same size than the 'other' span. + /// + /// The element type of the spans + /// The target span. + /// The 'other' span to compare 'target' to. + /// The name of the parameter that is to be checked. + /// + /// has a different size than + /// + [Conditional("DEBUG")] + public static void MustBeSameSized(ReadOnlySpan target, ReadOnlySpan other, string parameterName) + where T : struct + { + if (target.Length != other.Length) + { + throw new ArgumentException("Span-s must be the same size!", parameterName); + } + } + + /// + /// Verifies, that the `target` span has the length of 'minSpan', or longer. + /// + /// The element type of the spans + /// The target span. + /// The 'minSpan' span to compare 'target' to. + /// The name of the parameter that is to be checked. + /// + /// has less items than + /// + [Conditional("DEBUG")] + public static void MustBeSizedAtLeast(ReadOnlySpan target, ReadOnlySpan minSpan, string parameterName) + where T : struct + { + if (target.Length < minSpan.Length) + { + throw new ArgumentException($"Span-s must be at least of length {minSpan.Length}!", parameterName); + } + } + } +} diff --git a/ImageSharp/Common/Helpers/EnumUtils.cs b/ImageSharp/Common/Helpers/EnumUtils.cs new file mode 100644 index 0000000..fc89db4 --- /dev/null +++ b/ImageSharp/Common/Helpers/EnumUtils.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp { + /// + /// Common utility methods for working with enums. + /// + internal static class EnumUtils + { + /// + /// Converts the numeric representation of the enumerated constants to an equivalent enumerated object. + /// + /// The type of enum + /// The value to parse + /// The default value to return. + /// The . + public static TEnum Parse(int value, TEnum defaultValue) + where TEnum : struct, Enum + { + DebugGuard.IsTrue(Unsafe.SizeOf() == sizeof(int), "Only int-sized enums are supported."); + + TEnum valueEnum = Unsafe.As(ref value); + if (Enum.IsDefined(valueEnum)) + { + return valueEnum; + } + + return defaultValue; + } + + /// + /// Returns a value indicating whether the given enum has a flag of the given value. + /// + /// The type of enum. + /// The value. + /// The flag. + /// The . + public static bool HasFlag(TEnum value, TEnum flag) + where TEnum : struct, Enum + { + DebugGuard.IsTrue(Unsafe.SizeOf() == sizeof(int), "Only int-sized enums are supported."); + + uint flagValue = Unsafe.As(ref flag); + return (Unsafe.As(ref value) & flagValue) == flagValue; + } + } +} diff --git a/ImageSharp/Common/Helpers/ExifResolutionValues.cs b/ImageSharp/Common/Helpers/ExifResolutionValues.cs new file mode 100644 index 0000000..8aa0359 --- /dev/null +++ b/ImageSharp/Common/Helpers/ExifResolutionValues.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Common.Helpers { + internal readonly struct ExifResolutionValues + { + public ExifResolutionValues(ushort resolutionUnit, double? horizontalResolution, double? verticalResolution) + { + this.ResolutionUnit = resolutionUnit; + this.HorizontalResolution = horizontalResolution; + this.VerticalResolution = verticalResolution; + } + + public ushort ResolutionUnit { get; } + + public double? HorizontalResolution { get; } + + public double? VerticalResolution { get; } + } +} diff --git a/ImageSharp/Common/Helpers/Guard.cs b/ImageSharp/Common/Helpers/Guard.cs new file mode 100644 index 0000000..7af74cb --- /dev/null +++ b/ImageSharp/Common/Helpers/Guard.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp; + +namespace SixLabors { + internal static partial class Guard + { + /// + /// Ensures that the value is a value type. + /// + /// The target object, which cannot be null. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// is not a value type. + [MethodImpl(InliningOptions.ShortMethod)] + public static void MustBeValueType(TValue value, [CallerArgumentExpression("value")] String? parameterName = null) + where TValue : notnull + { + if (value.GetType().IsValueType) + { + return; + } + + ThrowHelper.ThrowArgumentException("Type must be a struct.", parameterName!); + } + } +} diff --git a/ImageSharp/Common/Helpers/HexConverter.cs b/ImageSharp/Common/Helpers/HexConverter.cs new file mode 100644 index 0000000..1767baa --- /dev/null +++ b/ImageSharp/Common/Helpers/HexConverter.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Common.Helpers { + internal static class HexConverter + { + /// + /// Parses a hexadecimal string into a byte array without allocations. Throws on non-hexadecimal character. + /// Adapted from https://source.dot.net/#System.Private.CoreLib/Convert.cs,c9e4fbeaca708991. + /// + /// The hexadecimal string to parse. + /// The destination for the parsed bytes. Must be at least .Length / 2 bytes long. + /// The number of bytes written to . + public static int HexStringToBytes(ReadOnlySpan chars, Span bytes) + { + if (Numerics.Modulo2(chars.Length) != 0) + { + throw new ArgumentException("Input string length must be a multiple of 2", nameof(chars)); + } + + if ((bytes.Length << 1 /* bit-hack for *2 */) < chars.Length) + { + throw new ArgumentException("Output span must be at least half the length of the input string"); + } + + // Slightly better performance in the loop below, allows us to skip a bounds check + // while still supporting output buffers that are larger than necessary + bytes = bytes[..(chars.Length >> 1)]; // bit-hack for / 2 + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static int FromChar(int c) + { + // Map from an ASCII char to its hex value, e.g. arr['b'] == 11. 0xFF means it's not a hex digit. + // This doesn't actually allocate. + ReadOnlySpan charToHexLookup = + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47 + 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63 + 0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95 + 0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 255 + ]; + + return (uint)c >= (uint)charToHexLookup.Length ? 0xFF : charToHexLookup[c]; + } + + // See https://source.dot.net/#System.Private.CoreLib/HexConverter.cs,4681d45a0aa0b361 + int i = 0; + int j = 0; + int byteLo = 0; + int byteHi = 0; + while (j < bytes.Length) + { + byteLo = FromChar(chars[i + 1]); + byteHi = FromChar(chars[i]); + + // byteHi hasn't been shifted to the high half yet, so the only way the bitwise or produces this pattern + // is if either byteHi or byteLo was not a hex character. + if ((byteLo | byteHi) == 0xFF) + { + break; + } + + bytes[j++] = (byte)((byteHi << 4) | byteLo); + i += 2; + } + + if (byteLo == 0xFF) + { + i++; + } + + if ((byteLo | byteHi) == 0xFF) + { + throw new ArgumentException("Input string contained non-hexadecimal characters", nameof(chars)); + } + + return j; + } + } +} diff --git a/ImageSharp/Common/Helpers/InliningOptions.cs b/ImageSharp/Common/Helpers/InliningOptions.cs new file mode 100644 index 0000000..be261fd --- /dev/null +++ b/ImageSharp/Common/Helpers/InliningOptions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// Uncomment this for verbose profiler results. DO NOT PUSH TO MAIN! +// #define PROFILING +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp { + /// + /// Global inlining options. Helps temporarily disable inlining for better profiler output. + /// + internal static class InliningOptions + { + /// + /// regardless of the build conditions. + /// + public const MethodImplOptions AlwaysInline = MethodImplOptions.AggressiveInlining; +#if PROFILING + public const MethodImplOptions HotPath = MethodImplOptions.NoInlining; + + public const MethodImplOptions ShortMethod = MethodImplOptions.NoInlining; +#else + public const MethodImplOptions HotPath = MethodImplOptions.AggressiveOptimization; + + public const MethodImplOptions ShortMethod = MethodImplOptions.AggressiveInlining; +#endif + public const MethodImplOptions ColdPath = MethodImplOptions.NoInlining; + } +} diff --git a/ImageSharp/Common/Helpers/Numerics.cs b/ImageSharp/Common/Helpers/Numerics.cs new file mode 100644 index 0000000..d55ee83 --- /dev/null +++ b/ImageSharp/Common/Helpers/Numerics.cs @@ -0,0 +1,1141 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp { + /// + /// Provides optimized static methods for trigonometric, logarithmic, + /// and other common mathematical functions. + /// + internal static class Numerics + { + public const int BlendAlphaControl = 0b_10_00_10_00; + private const int ShuffleAlphaControl = 0b_11_11_11_11; + + /// + /// Determine the Greatest CommonDivisor (GCD) of two numbers. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GreatestCommonDivisor(int a, int b) + { + while (b != 0) + { + int temp = b; + b = a % b; + a = temp; + } + + return a; + } + + /// + /// Determine the Least Common Multiple (LCM) of two numbers. + /// See https://en.wikipedia.org/wiki/Least_common_multiple#Reduction_by_the_greatest_common_divisor. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int LeastCommonMultiple(int a, int b) + => a / GreatestCommonDivisor(a, b) * b; + + /// + /// Calculates % 2 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Modulo2(int x) => x & 1; + + /// + /// Calculates % 4 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Modulo4(int x) => x & 3; + + /// + /// Calculates % 4 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint Modulo4(nint x) => x & 3; + + /// + /// Calculates % 4 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nuint Modulo4(nuint x) => x & 3; + + /// + /// Calculates % 8 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Modulo8(int x) => x & 7; + + /// + /// Calculates % 8 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint Modulo8(nint x) => x & 7; + + /// + /// Calculates % 64 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Modulo64(int x) => x & 63; + + /// + /// Calculates % 64 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint Modulo64(nint x) => x & 63; + + /// + /// Calculates % 256 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Modulo256(int x) => x & 255; + + /// + /// Calculates % 256 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint Modulo256(nint x) => x & 255; + + /// + /// Fast (x mod m) calculator, with the restriction that + /// should be power of 2. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ModuloP2(int x, int m) => x & (m - 1); + + /// + /// Returns the absolute value of a 32-bit signed integer. + /// Uses bit shifting to speed up the operation compared to . + /// + /// + /// A number that is greater than , but less than + /// or equal to + /// + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Abs(int x) + { + int y = x >> 31; + return (x ^ y) - y; + } + + /// + /// Returns a specified number raised to the power of 2 + /// + /// A single-precision floating-point number + /// The number raised to the power of 2. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Pow2(float x) => x * x; + + /// + /// Returns a specified number raised to the power of 3 + /// + /// A single-precision floating-point number + /// The number raised to the power of 3. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Pow3(float x) => x * x * x; + + /// + /// Returns a specified number raised to the power of 3 + /// + /// A double-precision floating-point number + /// The number raised to the power of 3. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Pow3(double x) => x * x * x; + + /// + /// Implementation of 1D Gaussian G(x) function + /// + /// The x provided to G(x). + /// The spread of the blur. + /// The Gaussian G(x) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Gaussian(float x, float sigma) + { + const float numerator = 1.0f; + float denominator = MathF.Sqrt(2 * MathF.PI) * sigma; + + float exponentNumerator = -x * x; + float exponentDenominator = 2 * Pow2(sigma); + + float left = numerator / denominator; + float right = MathF.Exp(exponentNumerator / exponentDenominator); + + return left * right; + } + + /// + /// Returns the result of a normalized sine cardinal function for the given value. + /// SinC(x) = sin(pi*x)/(pi*x). + /// + /// A single-precision floating-point number to calculate the result for. + /// + /// The sine cardinal of . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float SinC(float f) + { + if (MathF.Abs(f) > Constants.Epsilon) + { + f *= MathF.PI; + float result = MathF.Sin(f) / f; + return MathF.Abs(result) < Constants.Epsilon ? 0F : result; + } + + return 1F; + } + + /// + /// Returns the value clamped to the inclusive range of min and max. + /// + /// The value to clamp. + /// The minimum inclusive value. + /// The maximum inclusive value. + /// The clamped . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Clamp(byte value, byte min, byte max) + { + // Order is important here as someone might set min to higher than max. + if (value > max) + { + return max; + } + + if (value < min) + { + return min; + } + + return value; + } + + /// + /// Returns the value clamped to the inclusive range of min and max. + /// + /// The value to clamp. + /// The minimum inclusive value. + /// The maximum inclusive value. + /// The clamped . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Clamp(uint value, uint min, uint max) + { + if (value > max) + { + return max; + } + + if (value < min) + { + return min; + } + + return value; + } + + /// + /// Returns the value clamped to the inclusive range of min and max. + /// + /// The value to clamp. + /// The minimum inclusive value. + /// The maximum inclusive value. + /// The clamped . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Clamp(int value, int min, int max) + { + if (value > max) + { + return max; + } + + if (value < min) + { + return min; + } + + return value; + } + + /// + /// Returns the value clamped to the inclusive range of min and max. + /// + /// The value to clamp. + /// The minimum inclusive value. + /// The maximum inclusive value. + /// The clamped . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Clamp(float value, float min, float max) + { + if (value > max) + { + return max; + } + + if (value < min) + { + return min; + } + + return value; + } + + /// + /// Returns the value clamped to the inclusive range of min and max. + /// + /// The value to clamp. + /// The minimum inclusive value. + /// The maximum inclusive value. + /// The clamped . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Clamp(double value, double min, double max) + { + if (value > max) + { + return max; + } + + if (value < min) + { + return min; + } + + return value; + } + + /// + /// Returns the value clamped to the inclusive range of min and max. + /// 5x Faster than + /// on platforms < NET 5. + /// + /// The value to clamp. + /// The minimum inclusive value. + /// The maximum inclusive value. + /// The clamped . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Clamp(Vector4 value, Vector4 min, Vector4 max) + => Vector4.Min(Vector4.Max(value, min), max); + + /// + /// Clamps the span values to the inclusive range of min and max. + /// + /// The span containing the values to clamp. + /// The minimum inclusive value. + /// The maximum inclusive value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Clamp(Span span, byte min, byte max) + { + Span remainder = span[ClampReduce(span, min, max)..]; + + if (remainder.Length > 0) + { + ref byte remainderStart = ref MemoryMarshal.GetReference(remainder); + ref byte remainderEnd = ref Unsafe.Add(ref remainderStart, (uint)remainder.Length); + + while (Unsafe.IsAddressLessThan(ref remainderStart, ref remainderEnd)) + { + remainderStart = Clamp(remainderStart, min, max); + + remainderStart = ref Unsafe.Add(ref remainderStart, 1); + } + } + } + + /// + /// Clamps the span values to the inclusive range of min and max. + /// + /// The span containing the values to clamp. + /// The minimum inclusive value. + /// The maximum inclusive value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Clamp(Span span, uint min, uint max) + { + Span remainder = span[ClampReduce(span, min, max)..]; + + if (remainder.Length > 0) + { + ref uint remainderStart = ref MemoryMarshal.GetReference(remainder); + ref uint remainderEnd = ref Unsafe.Add(ref remainderStart, (uint)remainder.Length); + + while (Unsafe.IsAddressLessThan(ref remainderStart, ref remainderEnd)) + { + remainderStart = Clamp(remainderStart, min, max); + + remainderStart = ref Unsafe.Add(ref remainderStart, 1); + } + } + } + + /// + /// Clamps the span values to the inclusive range of min and max. + /// + /// The span containing the values to clamp. + /// The minimum inclusive value. + /// The maximum inclusive value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Clamp(Span span, int min, int max) + { + Span remainder = span[ClampReduce(span, min, max)..]; + + if (remainder.Length > 0) + { + ref int remainderStart = ref MemoryMarshal.GetReference(remainder); + ref int remainderEnd = ref Unsafe.Add(ref remainderStart, (uint)remainder.Length); + + while (Unsafe.IsAddressLessThan(ref remainderStart, ref remainderEnd)) + { + remainderStart = Clamp(remainderStart, min, max); + + remainderStart = ref Unsafe.Add(ref remainderStart, 1); + } + } + } + + /// + /// Clamps the span values to the inclusive range of min and max. + /// + /// The span containing the values to clamp. + /// The minimum inclusive value. + /// The maximum inclusive value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Clamp(Span span, float min, float max) + { + Span remainder = span[ClampReduce(span, min, max)..]; + + if (remainder.Length > 0) + { + ref float remainderStart = ref MemoryMarshal.GetReference(remainder); + ref float remainderEnd = ref Unsafe.Add(ref remainderStart, (uint)remainder.Length); + + while (Unsafe.IsAddressLessThan(ref remainderStart, ref remainderEnd)) + { + remainderStart = Clamp(remainderStart, min, max); + + remainderStart = ref Unsafe.Add(ref remainderStart, 1); + } + } + } + + /// + /// Clamps the span values to the inclusive range of min and max. + /// + /// The span containing the values to clamp. + /// The minimum inclusive value. + /// The maximum inclusive value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Clamp(Span span, double min, double max) + { + Span remainder = span[ClampReduce(span, min, max)..]; + + if (remainder.Length > 0) + { + ref double remainderStart = ref MemoryMarshal.GetReference(remainder); + ref double remainderEnd = ref Unsafe.Add(ref remainderStart, (uint)remainder.Length); + + while (Unsafe.IsAddressLessThan(ref remainderStart, ref remainderEnd)) + { + remainderStart = Clamp(remainderStart, min, max); + + remainderStart = ref Unsafe.Add(ref remainderStart, 1); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ClampReduce(Span span, T min, T max) + where T : unmanaged + { + if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) + { + int remainder = ModuloP2(span.Length, Vector.Count); + int adjustedCount = span.Length - remainder; + + if (adjustedCount > 0) + { + ClampImpl(span[..adjustedCount], min, max); + } + + return adjustedCount; + } + + return 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ClampImpl(Span span, T min, T max) + where T : unmanaged + { + ref T sRef = ref MemoryMarshal.GetReference(span); + Vector vmin = new(min); + Vector vmax = new(max); + + nint n = (nint)(uint)span.Length / Vector.Count; + nint m = Modulo4(n); + nint u = n - m; + + ref Vector vs0 = ref Unsafe.As>(ref MemoryMarshal.GetReference(span)); + ref Vector vs1 = ref Unsafe.Add(ref vs0, 1); + ref Vector vs2 = ref Unsafe.Add(ref vs0, 2); + ref Vector vs3 = ref Unsafe.Add(ref vs0, 3); + ref Vector vsEnd = ref Unsafe.Add(ref vs0, u); + + while (Unsafe.IsAddressLessThan(ref vs0, ref vsEnd)) + { + vs0 = Vector.Min(Vector.Max(vmin, vs0), vmax); + vs1 = Vector.Min(Vector.Max(vmin, vs1), vmax); + vs2 = Vector.Min(Vector.Max(vmin, vs2), vmax); + vs3 = Vector.Min(Vector.Max(vmin, vs3), vmax); + + vs0 = ref Unsafe.Add(ref vs0, 4); + vs1 = ref Unsafe.Add(ref vs1, 4); + vs2 = ref Unsafe.Add(ref vs2, 4); + vs3 = ref Unsafe.Add(ref vs3, 4); + } + + if (m > 0) + { + vs0 = ref vsEnd; + vsEnd = ref Unsafe.Add(ref vsEnd, m); + + while (Unsafe.IsAddressLessThan(ref vs0, ref vsEnd)) + { + vs0 = Vector.Min(Vector.Max(vmin, vs0), vmax); + + vs0 = ref Unsafe.Add(ref vs0, 1); + } + } + } + + /// + /// Pre-multiplies the "x", "y", "z" components of a vector by its "w" component leaving the "w" component intact. + /// + /// The to premultiply + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Premultiply(ref Vector4 source) + { + // Load into a local variable to prevent accessing the source from memory multiple times. + Vector4 src = source; + Vector4 alpha = PermuteW(src); + source = WithW(src * alpha, alpha); + } + + /// + /// Bulk variant of + /// + /// The span of vectors + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Premultiply(Span vectors) + { + if (Avx.IsSupported && vectors.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 vectorsBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vectors)); + ref Vector256 vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length / 2u); + + while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast)) + { + Vector256 source = vectorsBase; + Vector256 alpha = Avx.Permute(source, ShuffleAlphaControl); + vectorsBase = Avx.Blend(Avx.Multiply(source, alpha), source, BlendAlphaControl); + vectorsBase = ref Unsafe.Add(ref vectorsBase, 1); + } + + if (Modulo2(vectors.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + Premultiply(ref MemoryMarshal.GetReference(vectors[^1..])); + } + } + else + { + ref Vector4 vectorsStart = ref MemoryMarshal.GetReference(vectors); + ref Vector4 vectorsEnd = ref Unsafe.Add(ref vectorsStart, (uint)vectors.Length); + + while (Unsafe.IsAddressLessThan(ref vectorsStart, ref vectorsEnd)) + { + Premultiply(ref vectorsStart); + + vectorsStart = ref Unsafe.Add(ref vectorsStart, 1); + } + } + } + + /// + /// Reverses the result of premultiplying a vector via . + /// + /// The to premultiply + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void UnPremultiply(ref Vector4 source) + { + Vector4 alpha = PermuteW(source); + UnPremultiply(ref source, alpha); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void UnPremultiply(ref Vector4 source, Vector4 alpha) + { + if (alpha == Vector4.Zero) + { + return; + } + + // Divide source by alpha if alpha is nonzero, otherwise set all components to match the source value + // Blend the result with the alpha vector to ensure that the alpha component is unchanged + source = WithW(source / alpha, alpha); + } + + /// + /// Bulk variant of + /// + /// The span of vectors + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void UnPremultiply(Span vectors) + { + if (Avx.IsSupported && vectors.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 vectorsBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vectors)); + ref Vector256 vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length / 2u); + Vector256 epsilon = Vector256.Create(Constants.Epsilon); + + while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast)) + { + Vector256 source = vectorsBase; + Vector256 alpha = Avx.Permute(source, ShuffleAlphaControl); + vectorsBase = UnPremultiply(source, alpha); + vectorsBase = ref Unsafe.Add(ref vectorsBase, 1); + } + + if (Modulo2(vectors.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + UnPremultiply(ref MemoryMarshal.GetReference(vectors[^1..])); + } + } + else + { + ref Vector4 vectorsStart = ref MemoryMarshal.GetReference(vectors); + ref Vector4 vectorsEnd = ref Unsafe.Add(ref vectorsStart, (uint)vectors.Length); + + while (Unsafe.IsAddressLessThan(ref vectorsStart, ref vectorsEnd)) + { + UnPremultiply(ref vectorsStart); + + vectorsStart = ref Unsafe.Add(ref vectorsStart, 1); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 UnPremultiply(Vector256 source, Vector256 alpha) + { + // Check if alpha is zero to avoid division by zero + Vector256 zeroMask = Avx.CompareEqual(alpha, Vector256.Zero); + + // Divide source by alpha if alpha is nonzero, otherwise set all components to match the source value + Vector256 result = Avx.BlendVariable(Avx.Divide(source, alpha), source, zeroMask); + + // Blend the result with the alpha vector to ensure that the alpha component is unchanged + return Avx.Blend(result, alpha, BlendAlphaControl); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 UnPremultiply(Vector512 source, Vector512 alpha) + { + // Check if alpha is zero to avoid division by zero + Vector512 zeroMask = Vector512.Equals(alpha, Vector512.Zero); + + // Divide source by alpha if alpha is nonzero, otherwise set all components to match the source value + Vector512 result = Vector512.ConditionalSelect(zeroMask, source, source / alpha); + + // Blend the result with the alpha vector to ensure that the alpha component is unchanged + Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + return Vector512.ConditionalSelect(alphaMask, alpha, result); + } + + /// + /// Permutes the given vector return a new instance with all the values set to . + /// + /// The vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 PermuteW(Vector4 value) + { + if (Sse.IsSupported) + { + return Sse.Shuffle(value.AsVector128(), value.AsVector128(), ShuffleAlphaControl).AsVector4(); + } + + return new Vector4(value.W); + } + + /// + /// Sets the W component of the given vector to the given value from . + /// + /// The vector to set. + /// The vector containing the W value. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 WithW(Vector4 value, Vector4 w) + { + if (Sse41.IsSupported) + { + return Sse41.Insert(value.AsVector128(), w.AsVector128(), 0b11_11_0000).AsVector4(); + } + + if (Sse.IsSupported) + { + // Create tmp as + // Then return (which is ) + Vector128 tmp = Sse.Shuffle(w.AsVector128(), value.AsVector128(), 0b00_10_00_11); + return Sse.Shuffle(value.AsVector128(), tmp, 0b00_10_01_00).AsVector4(); + } + + value.W = w.W; + return value; + } + + /// + /// Calculates the cube pow of all the XYZ channels of the input vectors. + /// + /// The span of vectors + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CubePowOnXYZ(Span vectors) + { + ref Vector4 baseRef = ref MemoryMarshal.GetReference(vectors); + ref Vector4 endRef = ref Unsafe.Add(ref baseRef, (uint)vectors.Length); + + while (Unsafe.IsAddressLessThan(ref baseRef, ref endRef)) + { + Vector4 v = baseRef; + Vector4 a = PermuteW(v); + + // Fast path for the default gamma exposure, which is 3. In this case we can skip + // calling Math.Pow 3 times (one per component), as the method is an internal call and + // introduces quite a bit of overhead. Instead, we can just manually multiply the whole + // pixel in Vector4 format 3 times, and then restore the alpha channel before copying it + // back to the target index in the temporary span. The whole iteration will get completely + // inlined and traslated into vectorized instructions, with much better performance. + v = v * v * v; + v = WithW(v, a); + + baseRef = v; + baseRef = ref Unsafe.Add(ref baseRef, 1); + } + } + + /// + /// Calculates the cube root of all the XYZ channels of the input vectors. + /// + /// The span of vectors + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void CubeRootOnXYZ(Span vectors) + { + if (Sse41.IsSupported) + { + ref Vector128 vectors128Ref = ref Unsafe.As>(ref MemoryMarshal.GetReference(vectors)); + ref Vector128 vectors128End = ref Unsafe.Add(ref vectors128Ref, (uint)vectors.Length); + + Vector128 v128_341 = Vector128.Create(341); + Vector128 v128_negativeZero = Vector128.Create(-0.0f).AsInt32(); + Vector128 v128_one = Vector128.Create(1.0f).AsInt32(); + + Vector128 v128_13rd = Vector128.Create(1 / 3f); + Vector128 v128_23rds = Vector128.Create(2 / 3f); + + while (Unsafe.IsAddressLessThan(ref vectors128Ref, ref vectors128End)) + { + Vector128 vecx = vectors128Ref; + Vector128 veax = vecx.AsInt32(); + + // If we can use SSE41 instructions, we can vectorize the entire cube root calculation, and also execute it + // directly on 32 bit floating point values. What follows is a vectorized implementation of this method: + // https://www.musicdsp.org/en/latest/Other/206-fast-cube-root-square-root-and-reciprocal-for-x86-sse-cpus.html. + // Furthermore, after the initial setup in vectorized form, we're doing two Newton approximations here + // using a different succession (the same used below), which should be less unstable due to not having cube pow. + veax = Sse2.AndNot(v128_negativeZero, veax); + veax = Sse2.Subtract(veax, v128_one); + veax = Sse2.ShiftRightArithmetic(veax, 10); + veax = Sse41.MultiplyLow(veax, v128_341); + veax = Sse2.Add(veax, v128_one); + veax = Sse2.AndNot(v128_negativeZero, veax); + veax = Sse2.Or(veax, Sse2.And(vecx.AsInt32(), v128_negativeZero)); + + Vector128 y4 = veax.AsSingle(); + + if (Fma.IsSupported) + { + y4 = Fma.MultiplyAdd(v128_23rds, y4, Sse.Multiply(v128_13rd, Sse.Divide(vecx, Sse.Multiply(y4, y4)))); + y4 = Fma.MultiplyAdd(v128_23rds, y4, Sse.Multiply(v128_13rd, Sse.Divide(vecx, Sse.Multiply(y4, y4)))); + } + else + { + y4 = Sse.Add(Sse.Multiply(v128_23rds, y4), Sse.Multiply(v128_13rd, Sse.Divide(vecx, Sse.Multiply(y4, y4)))); + y4 = Sse.Add(Sse.Multiply(v128_23rds, y4), Sse.Multiply(v128_13rd, Sse.Divide(vecx, Sse.Multiply(y4, y4)))); + } + + y4 = Sse41.Insert(y4, vecx, 0xF0); + + vectors128Ref = y4; + vectors128Ref = ref Unsafe.Add(ref vectors128Ref, 1); + } + } + else + { + ref Vector4 vectorsRef = ref MemoryMarshal.GetReference(vectors); + ref Vector4 vectorsEnd = ref Unsafe.Add(ref vectorsRef, (uint)vectors.Length); + + // Fallback with scalar preprocessing and vectorized approximation steps + while (Unsafe.IsAddressLessThan(ref vectorsRef, ref vectorsEnd)) + { + Vector4 v = vectorsRef; + + double + x64 = v.X, + y64 = v.Y, + z64 = v.Z; + float a = v.W; + + ulong + xl = *(ulong*)&x64, + yl = *(ulong*)&y64, + zl = *(ulong*)&z64; + + // Here we use a trick to compute the starting value x0 for the cube root. This is because doing + // pow(x, 1 / gamma) is the same as the gamma-th root of x, and since gamme is 3 in this case, + // this means what we actually want is to find the cube root of our clamped values. + // For more info on the constant below, see: + // https://community.intel.com/t5/Intel-C-Compiler/Fast-approximate-of-transcendental-operations/td-p/1044543. + // Here we perform the same trick on all RGB channels separately to help the CPU execute them in paralle, and + // store the alpha channel to preserve it. Then we set these values to the fields of a temporary 128-bit + // register, and use it to accelerate two steps of the Newton approximation using SIMD. + xl = 0x2a9f8a7be393b600 + (xl / 3); + yl = 0x2a9f8a7be393b600 + (yl / 3); + zl = 0x2a9f8a7be393b600 + (zl / 3); + + Vector4 y4; + y4.X = (float)*(double*)&xl; + y4.Y = (float)*(double*)&yl; + y4.Z = (float)*(double*)&zl; + y4.W = 0; + + y4 = (2 / 3f * y4) + (1 / 3f * (v / (y4 * y4))); + y4 = (2 / 3f * y4) + (1 / 3f * (v / (y4 * y4))); + y4.W = a; + + vectorsRef = y4; + vectorsRef = ref Unsafe.Add(ref vectorsRef, 1); + } + } + } + + /// + /// Performs a linear interpolation between two values based on the given weighting. + /// + /// The first value. + /// The second value. + /// Values between 0 and 1 that indicates the weight of . + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Lerp( + in Vector256 value1, + in Vector256 value2, + in Vector256 amount) + { + Vector256 diff = Avx.Subtract(value2, value1); + if (Fma.IsSupported) + { + return Fma.MultiplyAdd(diff, amount, value1); + } + else + { + return Avx.Add(Avx.Multiply(diff, amount), value1); + } + } + + /// + /// Performs a linear interpolation between two values based on the given weighting. + /// + /// The first value. + /// The second value. + /// A value between 0 and 1 that indicates the weight of . + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Lerp(float value1, float value2, float amount) + => ((value2 - value1) * amount) + value1; + + /// + /// Accumulates 8-bit integers into by + /// widening them to 32-bit integers and performing four additions. + /// + /// + /// byte(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16) + /// is widened and added onto as such: + /// + /// accumulator += i32(1, 2, 3, 4); + /// accumulator += i32(5, 6, 7, 8); + /// accumulator += i32(9, 10, 11, 12); + /// accumulator += i32(13, 14, 15, 16); + /// + /// + /// The accumulator destination. + /// The values to accumulate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Accumulate(ref Vector accumulator, Vector values) + { + Vector.Widen(values, out Vector shortLow, out Vector shortHigh); + + Vector.Widen(shortLow, out Vector intLow, out Vector intHigh); + accumulator += intLow; + accumulator += intHigh; + + Vector.Widen(shortHigh, out intLow, out intHigh); + accumulator += intLow; + accumulator += intHigh; + } + + /// + /// Reduces elements of the vector into one sum. + /// + /// The accumulator to reduce. + /// The sum of all elements. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ReduceSum(Vector256 accumulator) + { + // Add upper lane to lower lane. + Vector128 vsum = Sse2.Add(accumulator.GetLower(), accumulator.GetUpper()); + + // Add odd to even. + vsum = Sse2.Add(vsum, Sse2.Shuffle(vsum, 0b_11_11_01_01)); + + // Add high to low. + vsum = Sse2.Add(vsum, Sse2.Shuffle(vsum, 0b_11_10_11_10)); + + return Sse2.ConvertToInt32(vsum); + } + + /// + /// Reduces even elements of the vector into one sum. + /// + /// The accumulator to reduce. + /// The sum of even elements. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int EvenReduceSum(Vector128 accumulator) + { + // Add high to low. + Vector128 vsum = Sse2.Add(accumulator, Sse2.Shuffle(accumulator, 0b_11_10_11_10)); + + return Sse2.ConvertToInt32(vsum); + } + + /// + /// Reduces even elements of the vector into one sum. + /// + /// The accumulator to reduce. + /// The sum of even elements. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int EvenReduceSum(Vector256 accumulator) + { + Vector128 vsum = Sse2.Add(accumulator.GetLower(), accumulator.GetUpper()); // add upper lane to lower lane + vsum = Sse2.Add(vsum, Sse2.Shuffle(vsum, 0b_11_10_11_10)); // add high to low + + // Vector128.ToScalar() isn't optimized pre-net5.0 https://github.com/dotnet/runtime/pull/37882 + return Sse2.ConvertToInt32(vsum); + } + + /// + /// Fast division with ceiling for numbers. + /// + /// Divident value. + /// Divisor value. + /// Ceiled division result. + public static uint DivideCeil(uint value, uint divisor) => (value + divisor - 1) / divisor; + + /// + /// Tells whether input value is outside of the given range. + /// + /// Value. + /// Minimum value, inclusive. + /// Maximum value, inclusive. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsOutOfRange(int value, int min, int max) + => (uint)(value - min) > (uint)(max - min); + + /// + /// Gets the count of vectors that safely fit into the given span. + /// + /// The type of the vector. + /// The given span. + /// Count of vectors that safely fit into the span. + public static nuint VectorCount(this Span span) + where TVector : struct + => (uint)span.Length / (uint)Vector.Count; + + /// + /// Gets the count of vectors that safely fit into the given span. + /// + /// The type of the vector. + /// The given span. + /// Count of vectors that safely fit into the span. + public static nuint Vector128Count(this Span span) + where TVector : struct + => (uint)span.Length / (uint)Vector128.Count; + + /// + /// Gets the count of vectors that safely fit into the given span. + /// + /// The type of the vector. + /// The given span. + /// Count of vectors that safely fit into the span. + public static nuint Vector128Count(this ReadOnlySpan span) + where TVector : struct + => (uint)span.Length / (uint)Vector128.Count; + + /// + /// Gets the count of vectors that safely fit into the given span. + /// + /// The type of the vector. + /// The given span. + /// Count of vectors that safely fit into the span. + public static nuint Vector256Count(this Span span) + where TVector : struct + => (uint)span.Length / (uint)Vector256.Count; + + /// + /// Gets the count of vectors that safely fit into the given span. + /// + /// The type of the vector. + /// The given span. + /// Count of vectors that safely fit into the span. + public static nuint Vector256Count(this ReadOnlySpan span) + where TVector : struct + => (uint)span.Length / (uint)Vector256.Count; + + /// + /// Gets the count of vectors that safely fit into the given span. + /// + /// The type of the vector. + /// The given span. + /// Count of vectors that safely fit into the span. + public static nuint Vector512Count(this Span span) + where TVector : struct + => (uint)span.Length / (uint)Vector512.Count; + + /// + /// Gets the count of vectors that safely fit into the given span. + /// + /// The type of the vector. + /// The given span. + /// Count of vectors that safely fit into the span. + public static nuint Vector512Count(this ReadOnlySpan span) + where TVector : struct + => (uint)span.Length / (uint)Vector512.Count; + + /// + /// Gets the count of vectors that safely fit into the given span. + /// + /// The type of the vector. + /// The given span. + /// Count of vectors that safely fit into the span. + public static nuint VectorCount(this Span span) + where TVector : struct + => (uint)span.Length / (uint)Vector.Count; + + /// + /// Gets the count of vectors that safely fit into the given span. + /// + /// The type of the vector. + /// The given span. + /// Count of vectors that safely fit into the span. + public static nuint Vector128Count(this Span span) + where TVector : struct + => (uint)span.Length / (uint)Vector128.Count; + + /// + /// Gets the count of vectors that safely fit into the given span. + /// + /// The type of the vector. + /// The given span. + /// Count of vectors that safely fit into the span. + public static nuint Vector256Count(this Span span) + where TVector : struct + => (uint)span.Length / (uint)Vector256.Count; + + /// + /// Gets the count of vectors that safely fit into length. + /// + /// The type of the vector. + /// The given length. + /// Count of vectors that safely fit into the length. + public static nuint Vector256Count(int length) + where TVector : struct + => (uint)length / (uint)Vector256.Count; + + /// + /// Gets the count of vectors that safely fit into the given span. + /// + /// The type of the vector. + /// The given span. + /// Count of vectors that safely fit into the span. + public static nuint Vector512Count(this Span span) + where TVector : struct + => (uint)span.Length / (uint)Vector512.Count; + + /// + /// Gets the count of vectors that safely fit into length. + /// + /// The type of the vector. + /// The given length. + /// Count of vectors that safely fit into the length. + public static nuint Vector512Count(int length) + where TVector : struct + => (uint)length / (uint)Vector512.Count; + + /// + /// Normalizes the values in a given . + /// + /// The sequence of values to normalize. + /// The sum of the values in . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Normalize(Span span, float sum) + { + if (Vector256.IsHardwareAccelerated) + { + ref float startRef = ref MemoryMarshal.GetReference(span); + ref float endRef = ref Unsafe.Add(ref startRef, span.Length & ~7); + Vector256 sum256 = Vector256.Create(sum); + + while (Unsafe.IsAddressLessThan(ref startRef, ref endRef)) + { + Unsafe.As>(ref startRef) /= sum256; + startRef = ref Unsafe.Add(ref startRef, (nuint)8); + } + + if ((span.Length & 7) >= 4) + { + Unsafe.As>(ref startRef) /= sum256.GetLower(); + startRef = ref Unsafe.Add(ref startRef, (nuint)4); + } + + endRef = ref Unsafe.Add(ref startRef, span.Length & 3); + + while (Unsafe.IsAddressLessThan(ref startRef, ref endRef)) + { + startRef /= sum; + startRef = ref Unsafe.Add(ref startRef, (nuint)1); + } + } + else + { + for (int i = 0; i < span.Length; i++) + { + span[i] /= sum; + } + } + } + } +} diff --git a/ImageSharp/Common/Helpers/RuntimeUtility.cs b/ImageSharp/Common/Helpers/RuntimeUtility.cs new file mode 100644 index 0000000..510b96b --- /dev/null +++ b/ImageSharp/Common/Helpers/RuntimeUtility.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Common.Helpers { + /// + /// A helper class that with utility methods for dealing with references, and other low-level details. + /// + internal static class RuntimeUtility + { + // Tuple swap uses 2 more IL bytes +#pragma warning disable IDE0180 // Use tuple to swap values + /// + /// Swaps the two references. + /// + /// The type to swap. + /// The first item. + /// The second item. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Swap(ref T a, ref T b) + { + T tmp = a; + a = b; + b = tmp; + } + + /// + /// Swaps the two references. + /// + /// The type to swap. + /// The first item. + /// The second item. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Swap(ref Span a, ref Span b) + { + // Tuple swap uses 2 more IL bytes + Span tmp = a; + a = b; + b = tmp; + } +#pragma warning restore IDE0180 // Use tuple to swap values + } +} diff --git a/ImageSharp/Common/Helpers/Shuffle/IComponentShuffle.cs b/ImageSharp/Common/Helpers/Shuffle/IComponentShuffle.cs new file mode 100644 index 0000000..b5917fc --- /dev/null +++ b/ImageSharp/Common/Helpers/Shuffle/IComponentShuffle.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// The JIT can detect and optimize rotation idioms ROTL (Rotate Left) +// and ROTR (Rotate Right) emitting efficient CPU instructions: +// https://github.com/dotnet/coreclr/pull/1830 +using System; + +namespace SixLabors.ImageSharp { + /// + /// Defines the contract for methods that allow the shuffling of pixel components. + /// Used for shuffling on platforms that do not support Hardware Intrinsics. + /// + internal interface IComponentShuffle + { + /// + /// Shuffles then slices 8-bit integers in + /// using a byte control and store the results in . + /// If successful, this method will reduce the length of length + /// by the shuffle amount. + /// + /// The source span of bytes. + /// The destination span of bytes. + void ShuffleReduce(ref ReadOnlySpan source, ref Span destination); + + /// + /// Shuffle 8-bit integers in + /// using the control and store the results in . + /// + /// The source span of bytes. + /// The destination span of bytes. + /// + /// Implementation can assume that source.Length is less or equal than destination.Length. + /// Loops should iterate using source.Length. + /// + void Shuffle(ReadOnlySpan source, Span destination); + } +} diff --git a/ImageSharp/Common/Helpers/Shuffle/IPad3Shuffle4.cs b/ImageSharp/Common/Helpers/Shuffle/IPad3Shuffle4.cs new file mode 100644 index 0000000..b91d977 --- /dev/null +++ b/ImageSharp/Common/Helpers/Shuffle/IPad3Shuffle4.cs @@ -0,0 +1,100 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static SixLabors.ImageSharp.SimdUtils; + +namespace SixLabors.ImageSharp { + /// + internal interface IPad3Shuffle4 : IComponentShuffle + { + } + + internal readonly struct DefaultPad3Shuffle4([ConstantExpected] byte control) : IPad3Shuffle4 + { + public byte Control { get; } = control; + + [MethodImpl(InliningOptions.ShortMethod)] + public void ShuffleReduce(ref ReadOnlySpan source, ref Span destination) +#pragma warning disable CA1857 // A constant is expected for the parameter + => HwIntrinsics.Pad3Shuffle4Reduce(ref source, ref destination, this.Control); +#pragma warning restore CA1857 // A constant is expected for the parameter + + [MethodImpl(InliningOptions.ShortMethod)] + public void Shuffle(ReadOnlySpan source, Span destination) + { + ref byte sBase = ref MemoryMarshal.GetReference(source); + ref byte dBase = ref MemoryMarshal.GetReference(destination); + + SimdUtils.Shuffle.InverseMMShuffle(this.Control, out uint p3, out uint p2, out uint p1, out uint p0); + + for (nuint i = 0, j = 0; i < (uint)source.Length; i += 3, j += 4) + { + // Expanding 3-byte pixels to 4 bytes can overwrite the next source + // triplet when spans overlap. Assemble the padded pixel first, then + // shuffle from the staged uint. + uint packed = + Unsafe.Add(ref sBase, i + 0u) | + ((uint)Unsafe.Add(ref sBase, i + 1u) << 8) | + ((uint)Unsafe.Add(ref sBase, i + 2u) << 16) | + 0xFF000000; + + ref byte pBase = ref Unsafe.As(ref packed); + + Unsafe.Add(ref dBase, j + 0u) = Unsafe.Add(ref pBase, p0); + Unsafe.Add(ref dBase, j + 1u) = Unsafe.Add(ref pBase, p1); + Unsafe.Add(ref dBase, j + 2u) = Unsafe.Add(ref pBase, p2); + Unsafe.Add(ref dBase, j + 3u) = Unsafe.Add(ref pBase, p3); + } + } + } + + internal readonly struct XYZWPad3Shuffle4 : IPad3Shuffle4 + { + [MethodImpl(InliningOptions.ShortMethod)] + public void ShuffleReduce(ref ReadOnlySpan source, ref Span destination) + => HwIntrinsics.Pad3Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle3210); + + [MethodImpl(InliningOptions.ShortMethod)] + public void Shuffle(ReadOnlySpan source, Span destination) + { + ref byte sBase = ref MemoryMarshal.GetReference(source); + ref byte dBase = ref MemoryMarshal.GetReference(destination); + + ref byte sEnd = ref Unsafe.Add(ref sBase, (uint)source.Length); + ref byte sLoopEnd = ref Unsafe.Subtract(ref sEnd, 4); + + while (Unsafe.IsAddressLessThan(ref sBase, ref sLoopEnd)) + { + // The fast scalar path reads one extra byte past the source triplet. + // Keep that widened read in a local before writing the expanded pixel + // so overlapping destinations cannot change what was read. + uint packed = Unsafe.As(ref sBase) | 0xFF000000; + + Unsafe.As(ref dBase) = packed; + + sBase = ref Unsafe.Add(ref sBase, 3); + dBase = ref Unsafe.Add(ref dBase, 4); + } + + while (Unsafe.IsAddressLessThan(ref sBase, ref sEnd)) + { + // The final triplet cannot use the widened read above, so assemble + // the same padded uint byte-by-byte before the overlapping store. + uint packed = + Unsafe.Add(ref sBase, 0u) | + ((uint)Unsafe.Add(ref sBase, 1u) << 8) | + ((uint)Unsafe.Add(ref sBase, 2u) << 16) | + 0xFF000000; + + Unsafe.As(ref dBase) = packed; + + sBase = ref Unsafe.Add(ref sBase, 3); + dBase = ref Unsafe.Add(ref dBase, 4); + } + } + } +} diff --git a/ImageSharp/Common/Helpers/Shuffle/IShuffle3.cs b/ImageSharp/Common/Helpers/Shuffle/IShuffle3.cs new file mode 100644 index 0000000..9039b35 --- /dev/null +++ b/ImageSharp/Common/Helpers/Shuffle/IShuffle3.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static SixLabors.ImageSharp.SimdUtils; + +namespace SixLabors.ImageSharp { + /// + internal interface IShuffle3 : IComponentShuffle + { + } + + internal readonly struct DefaultShuffle3([ConstantExpected] byte control) : IShuffle3 + { + public byte Control { get; } = control; + + [MethodImpl(InliningOptions.ShortMethod)] + public void ShuffleReduce(ref ReadOnlySpan source, ref Span destination) +#pragma warning disable CA1857 // A constant is expected for the parameter + => HwIntrinsics.Shuffle3Reduce(ref source, ref destination, this.Control); +#pragma warning restore CA1857 // A constant is expected for the parameter + + [MethodImpl(InliningOptions.ShortMethod)] + public void Shuffle(ReadOnlySpan source, Span destination) + { + ref byte sBase = ref MemoryMarshal.GetReference(source); + ref byte dBase = ref MemoryMarshal.GetReference(destination); + + SimdUtils.Shuffle.InverseMMShuffle(this.Control, out _, out uint p2, out uint p1, out uint p0); + + for (nuint i = 0; i < (uint)source.Length; i += 3) + { + // The scalar remainder can run in-place after the vector body. Load + // the full 3-byte pixel into a register-sized value before stores so + // channel swaps cannot corrupt later reads from the same pixel. + uint packed = + Unsafe.Add(ref sBase, i + 0u) | + ((uint)Unsafe.Add(ref sBase, i + 1u) << 8) | + ((uint)Unsafe.Add(ref sBase, i + 2u) << 16); + + ref byte pBase = ref Unsafe.As(ref packed); + + Unsafe.Add(ref dBase, i + 0u) = Unsafe.Add(ref pBase, p0); + Unsafe.Add(ref dBase, i + 1u) = Unsafe.Add(ref pBase, p1); + Unsafe.Add(ref dBase, i + 2u) = Unsafe.Add(ref pBase, p2); + } + } + } +} diff --git a/ImageSharp/Common/Helpers/Shuffle/IShuffle4.cs b/ImageSharp/Common/Helpers/Shuffle/IShuffle4.cs new file mode 100644 index 0000000..0b5d296 --- /dev/null +++ b/ImageSharp/Common/Helpers/Shuffle/IShuffle4.cs @@ -0,0 +1,185 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static SixLabors.ImageSharp.SimdUtils; + +namespace SixLabors.ImageSharp { + /// + internal interface IShuffle4 : IComponentShuffle + { + } + + internal readonly struct DefaultShuffle4([ConstantExpected] byte control) : IShuffle4 + { + public byte Control { get; } = control; + + [MethodImpl(InliningOptions.ShortMethod)] + public void ShuffleReduce(ref ReadOnlySpan source, ref Span destination) +#pragma warning disable CA1857 // A constant is expected for the parameter + => HwIntrinsics.Shuffle4Reduce(ref source, ref destination, this.Control); +#pragma warning restore CA1857 // A constant is expected for the parameter + + [MethodImpl(InliningOptions.ShortMethod)] + public void Shuffle(ReadOnlySpan source, Span destination) + { + ref byte sBase = ref MemoryMarshal.GetReference(source); + ref byte dBase = ref MemoryMarshal.GetReference(destination); + + SimdUtils.Shuffle.InverseMMShuffle(this.Control, out uint p3, out uint p2, out uint p1, out uint p0); + + for (nuint i = 0; i < (uint)source.Length; i += 4) + { + // The generic path may be used with source and destination pointing + // at the same pixel. Load all channels first so subsequent stores + // index only staged bytes, matching the specialized uint shuffles. + uint packed = Unsafe.As(ref Unsafe.Add(ref sBase, i)); + ref byte pBase = ref Unsafe.As(ref packed); + + Unsafe.Add(ref dBase, i + 0u) = Unsafe.Add(ref pBase, p0); + Unsafe.Add(ref dBase, i + 1u) = Unsafe.Add(ref pBase, p1); + Unsafe.Add(ref dBase, i + 2u) = Unsafe.Add(ref pBase, p2); + Unsafe.Add(ref dBase, i + 3u) = Unsafe.Add(ref pBase, p3); + } + } + } + + internal readonly struct WXYZShuffle4 : IShuffle4 + { + [MethodImpl(InliningOptions.ShortMethod)] + public void ShuffleReduce(ref ReadOnlySpan source, ref Span destination) + => HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle2103); + + [MethodImpl(InliningOptions.ShortMethod)] + public void Shuffle(ReadOnlySpan source, Span destination) + { + ref uint sBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref uint dBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + uint n = (uint)source.Length / 4; + + for (nuint i = 0; i < n; i++) + { + uint packed = Unsafe.Add(ref sBase, i); + + // packed = [W Z Y X] + // ROTL(8, packed) = [Z Y X W] + Unsafe.Add(ref dBase, i) = (packed << 8) | (packed >> 24); + } + } + } + + internal readonly struct WZYXShuffle4 : IShuffle4 + { + [MethodImpl(InliningOptions.ShortMethod)] + public void ShuffleReduce(ref ReadOnlySpan source, ref Span destination) + => HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle0123); + + [MethodImpl(InliningOptions.ShortMethod)] + public void Shuffle(ReadOnlySpan source, Span destination) + { + ref uint sBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref uint dBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + uint n = (uint)source.Length / 4; + + for (nuint i = 0; i < n; i++) + { + uint packed = Unsafe.Add(ref sBase, i); + + // packed = [W Z Y X] + // REVERSE(packedArgb) = [X Y Z W] + Unsafe.Add(ref dBase, i) = BinaryPrimitives.ReverseEndianness(packed); + } + } + } + + internal readonly struct YZWXShuffle4 : IShuffle4 + { + [MethodImpl(InliningOptions.ShortMethod)] + public void ShuffleReduce(ref ReadOnlySpan source, ref Span destination) + => HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle0321); + + [MethodImpl(InliningOptions.ShortMethod)] + public void Shuffle(ReadOnlySpan source, Span destination) + { + ref uint sBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref uint dBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + uint n = (uint)source.Length / 4; + + for (nuint i = 0; i < n; i++) + { + uint packed = Unsafe.Add(ref sBase, i); + + // packed = [W Z Y X] + // ROTR(8, packedArgb) = [Y Z W X] + Unsafe.Add(ref dBase, i) = BitOperations.RotateRight(packed, 8); + } + } + } + + internal readonly struct ZYXWShuffle4 : IShuffle4 + { + [MethodImpl(InliningOptions.ShortMethod)] + public void ShuffleReduce(ref ReadOnlySpan source, ref Span destination) + => HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle3012); + + [MethodImpl(InliningOptions.ShortMethod)] + public void Shuffle(ReadOnlySpan source, Span destination) + { + ref uint sBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref uint dBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + uint n = (uint)source.Length / 4; + + for (nuint i = 0; i < n; i++) + { + uint packed = Unsafe.Add(ref sBase, i); + + // packed = [W Z Y X] + // tmp1 = [W 0 Y 0] + // tmp2 = [0 Z 0 X] + // tmp3=ROTL(16, tmp2) = [0 X 0 Z] + // tmp1 + tmp3 = [W X Y Z] + uint tmp1 = packed & 0xFF00FF00; + uint tmp2 = packed & 0x00FF00FF; + uint tmp3 = BitOperations.RotateLeft(tmp2, 16); + + Unsafe.Add(ref dBase, i) = tmp1 + tmp3; + } + } + } + + internal readonly struct XWZYShuffle4 : IShuffle4 + { + [MethodImpl(InliningOptions.ShortMethod)] + public void ShuffleReduce(ref ReadOnlySpan source, ref Span destination) + => HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle1230); + + [MethodImpl(InliningOptions.ShortMethod)] + public void Shuffle(ReadOnlySpan source, Span destination) + { + ref uint sBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref uint dBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + uint n = (uint)source.Length / 4; + + for (nuint i = 0; i < n; i++) + { + uint packed = Unsafe.Add(ref sBase, i); + + // packed = [W Z Y X] + // tmp1 = [0 Z 0 X] + // tmp2 = [W 0 Y 0] + // tmp3=ROTL(16, tmp2) = [Y 0 W 0] + // tmp1 + tmp3 = [Y Z W X] + uint tmp1 = packed & 0x00FF00FF; + uint tmp2 = packed & 0xFF00FF00; + uint tmp3 = BitOperations.RotateLeft(tmp2, 16); + + Unsafe.Add(ref dBase, i) = tmp1 + tmp3; + } + } + } +} diff --git a/ImageSharp/Common/Helpers/Shuffle/IShuffle4Slice3.cs b/ImageSharp/Common/Helpers/Shuffle/IShuffle4Slice3.cs new file mode 100644 index 0000000..286dcb0 --- /dev/null +++ b/ImageSharp/Common/Helpers/Shuffle/IShuffle4Slice3.cs @@ -0,0 +1,105 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static SixLabors.ImageSharp.SimdUtils; + +namespace SixLabors.ImageSharp { + /// + internal interface IShuffle4Slice3 : IComponentShuffle + { + } + + internal readonly struct DefaultShuffle4Slice3([ConstantExpected] byte control) : IShuffle4Slice3 + { + public byte Control { get; } = control; + + [MethodImpl(InliningOptions.ShortMethod)] + public void ShuffleReduce(ref ReadOnlySpan source, ref Span destination) +#pragma warning disable CA1857 // A constant is expected for the parameter + => HwIntrinsics.Shuffle4Slice3Reduce(ref source, ref destination, this.Control); +#pragma warning restore CA1857 // A constant is expected for the parameter + + [MethodImpl(InliningOptions.ShortMethod)] + public void Shuffle(ReadOnlySpan source, Span destination) + { + ref byte sBase = ref MemoryMarshal.GetReference(source); + ref byte dBase = ref MemoryMarshal.GetReference(destination); + + SimdUtils.Shuffle.InverseMMShuffle(this.Control, out _, out uint p2, out uint p1, out uint p0); + + for (nuint i = 0, j = 0; i < (uint)destination.Length; i += 3, j += 4) + { + // Shrinking 4-byte pixels to 3 bytes can still be called in-place by + // tail code. Read the complete source pixel first, then write only + // the requested channels into the destination triplet. + uint packed = Unsafe.As(ref Unsafe.Add(ref sBase, j)); + ref byte pBase = ref Unsafe.As(ref packed); + + Unsafe.Add(ref dBase, i + 0u) = Unsafe.Add(ref pBase, p0); + Unsafe.Add(ref dBase, i + 1u) = Unsafe.Add(ref pBase, p1); + Unsafe.Add(ref dBase, i + 2u) = Unsafe.Add(ref pBase, p2); + } + } + } + + internal readonly struct XYZWShuffle4Slice3 : IShuffle4Slice3 + { + [MethodImpl(InliningOptions.ShortMethod)] + public void ShuffleReduce(ref ReadOnlySpan source, ref Span destination) + => HwIntrinsics.Shuffle4Slice3Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle3210); + + [MethodImpl(InliningOptions.ShortMethod)] + public void Shuffle(ReadOnlySpan source, Span destination) + { + ref uint sBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref Byte3 dBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + + nint n = (nint)(uint)source.Length / 4; + nint m = Numerics.Modulo4(n); + nint u = n - m; + + ref uint sLoopEnd = ref Unsafe.Add(ref sBase, u); + ref uint sEnd = ref Unsafe.Add(ref sBase, n); + + while (Unsafe.IsAddressLessThan(ref sBase, ref sLoopEnd)) + { + // Stage the four source pixels before the 3-byte stores. Even + // though this path preserves XYZ order, the packed loads must happen + // before destination writes when the spans overlap. + uint packed0 = Unsafe.Add(ref sBase, 0u); + uint packed1 = Unsafe.Add(ref sBase, 1u); + uint packed2 = Unsafe.Add(ref sBase, 2u); + uint packed3 = Unsafe.Add(ref sBase, 3u); + + Unsafe.Add(ref dBase, 0u) = Unsafe.As(ref packed0); + Unsafe.Add(ref dBase, 1u) = Unsafe.As(ref packed1); + Unsafe.Add(ref dBase, 2u) = Unsafe.As(ref packed2); + Unsafe.Add(ref dBase, 3u) = Unsafe.As(ref packed3); + + sBase = ref Unsafe.Add(ref sBase, 4); + dBase = ref Unsafe.Add(ref dBase, 4); + } + + while (Unsafe.IsAddressLessThan(ref sBase, ref sEnd)) + { + // Same overlap rule as the unrolled loop: take the 4-byte source + // pixel before storing the 3-byte destination value. + uint packed = Unsafe.Add(ref sBase, 0u); + + Unsafe.Add(ref dBase, 0u) = Unsafe.As(ref packed); + + sBase = ref Unsafe.Add(ref sBase, 1); + dBase = ref Unsafe.Add(ref dBase, 1); + } + } + } + + [StructLayout(LayoutKind.Explicit, Size = 3)] + internal readonly struct Byte3 + { + } +} diff --git a/ImageSharp/Common/Helpers/SimdUtils.Convert.cs b/ImageSharp/Common/Helpers/SimdUtils.Convert.cs new file mode 100644 index 0000000..7ed6c0f --- /dev/null +++ b/ImageSharp/Common/Helpers/SimdUtils.Convert.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp { + internal static partial class SimdUtils + { + /// + /// Converts all input -s to -s normalized into [0..1]. + /// should be the of the same size as , + /// but there are no restrictions on the span's length. + /// + /// The source span of bytes + /// The destination span of floats + [MethodImpl(InliningOptions.ShortMethod)] + internal static void ByteToNormalizedFloat(ReadOnlySpan source, Span destination) + { + DebugGuard.IsTrue(source.Length == destination.Length, nameof(source), "Input spans must be of same length!"); + + HwIntrinsics.ByteToNormalizedFloatReduce(ref source, ref destination); + + if (source.Length > 0) + { + ConvertByteToNormalizedFloatRemainder(source, destination); + } + } + + /// + /// Convert all values normalized into [0..1] from 'source' into 'destination' buffer of . + /// The values are scaled up into [0-255] and rounded, overflows are clamped. + /// should be the of the same size as , + /// but there are no restrictions on the span's length. + /// + /// The source span of floats + /// The destination span of bytes + [MethodImpl(InliningOptions.ShortMethod)] + internal static void NormalizedFloatToByteSaturate(ReadOnlySpan source, Span destination) + { + DebugGuard.IsTrue(source.Length == destination.Length, nameof(source), "Input spans must be of same length!"); + + HwIntrinsics.NormalizedFloatToByteSaturateReduce(ref source, ref destination); + + if (source.Length > 0) + { + ConvertNormalizedFloatToByteRemainder(source, destination); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ConvertByteToNormalizedFloatRemainder(ReadOnlySpan source, Span destination) + { + ref byte sBase = ref MemoryMarshal.GetReference(source); + ref float dBase = ref MemoryMarshal.GetReference(destination); + + for (int i = 0; i < source.Length; i++) + { + Unsafe.Add(ref dBase, (uint)i) = Unsafe.Add(ref sBase, (uint)i) / 255f; + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ConvertNormalizedFloatToByteRemainder(ReadOnlySpan source, Span destination) + { + ref float sBase = ref MemoryMarshal.GetReference(source); + ref byte dBase = ref MemoryMarshal.GetReference(destination); + + for (int i = 0; i < source.Length; i++) + { + Unsafe.Add(ref dBase, (uint)i) = ConvertToByte(Unsafe.Add(ref sBase, (uint)i)); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte ConvertToByte(float f) => (byte)Numerics.Clamp((f * 255f) + 0.5f, 0, 255f); + } +} diff --git a/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs b/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs new file mode 100644 index 0000000..6af2748 --- /dev/null +++ b/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs @@ -0,0 +1,1160 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp { + internal static partial class SimdUtils + { + public static class HwIntrinsics + { +#pragma warning disable SA1117 // Parameters should be on same line or separate lines +#pragma warning disable SA1137 // Elements should have the same indentation + [MethodImpl(MethodImplOptions.AggressiveInlining)] // too much IL for JIT to inline, so give a hint + public static Vector256 PermuteMaskDeinterleave8x32() => Vector256.Create(0, 4, 1, 5, 2, 6, 3, 7); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 PermuteMaskDeinterleave16x32() => Vector512.Create(0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 PermuteMaskEvenOdd8x32() => Vector256.Create(0, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0).AsUInt32(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 PermuteMaskSwitchInnerDWords8x32() => Vector256.Create(0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0).AsUInt32(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 MoveFirst24BytesToSeparateLanes() => Vector256.Create(0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 6, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0).AsUInt32(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector256 ExtractRgb() => Vector256.Create(0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, 0xFF, 0xFF, 0xFF, 0xFF, 0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, 0xFF, 0xFF, 0xFF, 0xFF); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ShuffleMaskPad4Nx16() => Vector128.Create(0, 1, 2, 0x80, 3, 4, 5, 0x80, 6, 7, 8, 0x80, 9, 10, 11, 0x80); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ShuffleMaskSlice4Nx16() => Vector128.Create(0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 0x80, 0x80, 0x80, 0x80); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ShuffleMaskShiftAlpha() => Vector256.Create( + (byte)0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 3, 7, 11, 15, + 0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 3, 7, 11, 15); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 PermuteMaskShiftAlpha8x32() => Vector256.Create(0u, 1, 2, 4, 5, 6, 3, 7); +#pragma warning restore SA1137 // Elements should have the same indentation +#pragma warning restore SA1117 // Parameters should be on same line or separate lines + + /// + /// Shuffle single-precision (32-bit) floating-point elements in + /// using the control and store the results in . + /// + /// The source span of floats. + /// The destination span of floats. + /// The byte control. + [MethodImpl(InliningOptions.ShortMethod)] + public static void Shuffle4Reduce( + ref ReadOnlySpan source, + ref Span destination, + [ConstantExpected] byte control) + { + if (Vector512.IsHardwareAccelerated || + Vector256.IsHardwareAccelerated || + Vector128.IsHardwareAccelerated) + { + int remainder = 0; + if (Vector512.IsHardwareAccelerated) + { + remainder = Numerics.ModuloP2(source.Length, Vector512.Count); + } + else if (Vector256.IsHardwareAccelerated) + { + remainder = Numerics.ModuloP2(source.Length, Vector256.Count); + } + else if (Vector128.IsHardwareAccelerated) + { + remainder = Numerics.ModuloP2(source.Length, Vector128.Count); + } + + int adjustedCount = source.Length - remainder; + + if (adjustedCount > 0) + { + Shuffle4( + source[..adjustedCount], + destination[..adjustedCount], + control); + + source = source[adjustedCount..]; + destination = destination[adjustedCount..]; + } + } + } + + /// + /// Shuffle 8-bit integers + /// using the control and store the results in . + /// + /// The source span of bytes. + /// The destination span of bytes. + /// The byte control. + [MethodImpl(InliningOptions.ShortMethod)] + public static void Shuffle4Reduce( + ref ReadOnlySpan source, + ref Span destination, + [ConstantExpected] byte control) + { + if (Vector512.IsHardwareAccelerated || + Vector256.IsHardwareAccelerated || + Vector128.IsHardwareAccelerated) + { + int remainder = 0; + if (Vector512.IsHardwareAccelerated) + { + remainder = Numerics.ModuloP2(source.Length, Vector512.Count); + } + else if (Vector256.IsHardwareAccelerated) + { + remainder = Numerics.ModuloP2(source.Length, Vector256.Count); + } + else if (Vector128.IsHardwareAccelerated) + { + remainder = Numerics.ModuloP2(source.Length, Vector128.Count); + } + + int adjustedCount = source.Length - remainder; + + if (adjustedCount > 0) + { + Shuffle4( + source[..adjustedCount], + destination[..adjustedCount], + control); + + source = source[adjustedCount..]; + destination = destination[adjustedCount..]; + } + } + } + + /// + /// Shuffles 8-bit integer triplets in + /// using the control and store the results in . + /// + /// The source span of bytes. + /// The destination span of bytes. + /// The byte control. + [MethodImpl(InliningOptions.ShortMethod)] + public static void Shuffle3Reduce( + ref ReadOnlySpan source, + ref Span destination, + [ConstantExpected] byte control) + { + if (Vector128.IsHardwareAccelerated) + { + int remainder = source.Length % (Vector128.Count * 3); + + int adjustedCount = source.Length - remainder; + + if (adjustedCount > 0) + { + Shuffle3( + source[..adjustedCount], + destination[..adjustedCount], + control); + + source = source[adjustedCount..]; + destination = destination[adjustedCount..]; + } + } + } + + /// + /// Pads then shuffles 8-bit integers in + /// using the control and store the results in . + /// + /// The source span of bytes. + /// The destination span of bytes. + /// The byte control. + [MethodImpl(InliningOptions.ShortMethod)] + public static void Pad3Shuffle4Reduce( + ref ReadOnlySpan source, + ref Span destination, + [ConstantExpected] byte control) + { + if (Vector128.IsHardwareAccelerated) + { + int remainder = source.Length % (Vector128.Count * 3); + + int sourceCount = source.Length - remainder; + int destinationCount = (int)((uint)sourceCount * 4 / 3); + + if (sourceCount > 0) + { + Pad3Shuffle4( + source[..sourceCount], + destination[..destinationCount], + control); + + source = source[sourceCount..]; + destination = destination[destinationCount..]; + } + } + } + + /// + /// Shuffles then slices 8-bit integers in + /// using the control and store the results in . + /// + /// The source span of bytes. + /// The destination span of bytes. + /// The byte control. + [MethodImpl(InliningOptions.ShortMethod)] + public static void Shuffle4Slice3Reduce( + ref ReadOnlySpan source, + ref Span destination, + [ConstantExpected] byte control) + { + if (Vector128.IsHardwareAccelerated) + { + int remainder = source.Length & ((Vector128.Count * 4) - 1); // bit-hack for modulo + + int sourceCount = source.Length - remainder; + int destinationCount = (int)((uint)sourceCount * 3 / 4); + + if (sourceCount > 0) + { + Shuffle4Slice3( + source[..sourceCount], + destination[..destinationCount], + control); + + source = source[sourceCount..]; + destination = destination[destinationCount..]; + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void Shuffle4( + ReadOnlySpan source, + Span destination, + [ConstantExpected] byte control) + { + if (Vector512.IsHardwareAccelerated) + { + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + nuint n = (uint)destination.Length / (uint)Vector512.Count; + nuint m = Numerics.Modulo4(n); + nuint u = n - m; + + for (nuint i = 0; i < u; i += 4) + { + ref Vector512 vs0 = ref Unsafe.Add(ref sourceBase, i); + ref Vector512 vd0 = ref Unsafe.Add(ref destinationBase, i); + + vd0 = Vector512_.ShuffleNative(vs0, control); + Unsafe.Add(ref vd0, (nuint)1) = Vector512_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)1), control); + Unsafe.Add(ref vd0, (nuint)2) = Vector512_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)2), control); + Unsafe.Add(ref vd0, (nuint)3) = Vector512_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)3), control); + } + + if (m > 0) + { + for (nuint i = u; i < n; i++) + { + Unsafe.Add(ref destinationBase, i) = Vector512_.ShuffleNative(Unsafe.Add(ref sourceBase, i), control); + } + } + } + else if (Vector256.IsHardwareAccelerated) + { + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + nuint n = (uint)destination.Length / (uint)Vector256.Count; + nuint m = Numerics.Modulo4(n); + nuint u = n - m; + + for (nuint i = 0; i < u; i += 4) + { + ref Vector256 vs0 = ref Unsafe.Add(ref sourceBase, i); + ref Vector256 vd0 = ref Unsafe.Add(ref destinationBase, i); + + vd0 = Vector256_.ShuffleNative(vs0, control); + Unsafe.Add(ref vd0, (nuint)1) = Vector256_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)1), control); + Unsafe.Add(ref vd0, (nuint)2) = Vector256_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)2), control); + Unsafe.Add(ref vd0, (nuint)3) = Vector256_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)3), control); + } + + if (m > 0) + { + for (nuint i = u; i < n; i++) + { + Unsafe.Add(ref destinationBase, i) = Vector256_.ShuffleNative(Unsafe.Add(ref sourceBase, i), control); + } + } + } + else if (Vector128.IsHardwareAccelerated) + { + ref Vector128 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector128 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + nuint n = (uint)destination.Length / (uint)Vector128.Count; + nuint m = Numerics.Modulo4(n); + nuint u = n - m; + + for (nuint i = 0; i < u; i += 4) + { + ref Vector128 vs0 = ref Unsafe.Add(ref sourceBase, i); + ref Vector128 vd0 = ref Unsafe.Add(ref destinationBase, i); + + vd0 = Vector128_.ShuffleNative(vs0, control); + Unsafe.Add(ref vd0, (nuint)1) = Vector128_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)1), control); + Unsafe.Add(ref vd0, (nuint)2) = Vector128_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)2), control); + Unsafe.Add(ref vd0, (nuint)3) = Vector128_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)3), control); + } + + if (m > 0) + { + for (nuint i = u; i < n; i++) + { + Unsafe.Add(ref destinationBase, i) = Vector128_.ShuffleNative(Unsafe.Add(ref sourceBase, i), control); + } + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void Shuffle4( + ReadOnlySpan source, + Span destination, + [ConstantExpected] byte control) + { + if (Vector512.IsHardwareAccelerated) + { + Span temp = stackalloc byte[Vector512.Count]; + Shuffle.MMShuffleSpan(ref temp, control); + Vector512 mask = Unsafe.As>(ref MemoryMarshal.GetReference(temp)); + + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + nuint n = (uint)destination.Length / (uint)Vector512.Count; + nuint m = Numerics.Modulo4(n); + nuint u = n - m; + + for (nuint i = 0; i < u; i += 4) + { + ref Vector512 vs0 = ref Unsafe.Add(ref sourceBase, i); + ref Vector512 vd0 = ref Unsafe.Add(ref destinationBase, i); + + vd0 = Vector512_.ShuffleNative(vs0, mask); + Unsafe.Add(ref vd0, (nuint)1) = Vector512_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)1), mask); + Unsafe.Add(ref vd0, (nuint)2) = Vector512_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)2), mask); + Unsafe.Add(ref vd0, (nuint)3) = Vector512_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)3), mask); + } + + if (m > 0) + { + for (nuint i = u; i < n; i++) + { + Unsafe.Add(ref destinationBase, i) = Vector512_.ShuffleNative(Unsafe.Add(ref sourceBase, i), mask); + } + } + } + else if (Vector256.IsHardwareAccelerated) + { + // ShufflePerLane performs per-128-bit-lane shuffling using Avx2.Shuffle (vpshufb). + // MMShuffleSpan generates indices in the range [0, 31] and never sets bit 7 in any byte, + // so the shuffle will not zero elements. Because vpshufb uses only the low 4 bits (b[i] & 0x0F) + // for indexing within each lane, and ignores the upper bits unless bit 7 is set, + // this usage is guaranteed to remain within-lane and non-zeroing. + Span temp = stackalloc byte[Vector256.Count]; + Shuffle.MMShuffleSpan(ref temp, control); + Vector256 mask = Unsafe.As>(ref MemoryMarshal.GetReference(temp)); + + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + nuint n = (uint)destination.Length / (uint)Vector256.Count; + nuint m = Numerics.Modulo4(n); + nuint u = n - m; + + for (nuint i = 0; i < u; i += 4) + { + ref Vector256 vs0 = ref Unsafe.Add(ref sourceBase, i); + ref Vector256 vd0 = ref Unsafe.Add(ref destinationBase, i); + + vd0 = Vector256_.ShufflePerLane(vs0, mask); + Unsafe.Add(ref vd0, (nuint)1) = Vector256_.ShufflePerLane(Unsafe.Add(ref vs0, (nuint)1), mask); + Unsafe.Add(ref vd0, (nuint)2) = Vector256_.ShufflePerLane(Unsafe.Add(ref vs0, (nuint)2), mask); + Unsafe.Add(ref vd0, (nuint)3) = Vector256_.ShufflePerLane(Unsafe.Add(ref vs0, (nuint)3), mask); + } + + if (m > 0) + { + for (nuint i = u; i < n; i++) + { + Unsafe.Add(ref destinationBase, i) = Vector256_.ShufflePerLane(Unsafe.Add(ref sourceBase, i), mask); + } + } + } + else if (Vector128.IsHardwareAccelerated) + { + Span temp = stackalloc byte[Vector128.Count]; + Shuffle.MMShuffleSpan(ref temp, control); + Vector128 mask = Unsafe.As>(ref MemoryMarshal.GetReference(temp)); + + ref Vector128 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector128 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + nuint n = (uint)destination.Length / (uint)Vector128.Count; + nuint m = Numerics.Modulo4(n); + nuint u = n - m; + + for (nuint i = 0; i < u; i += 4) + { + ref Vector128 vs0 = ref Unsafe.Add(ref sourceBase, i); + ref Vector128 vd0 = ref Unsafe.Add(ref destinationBase, i); + + vd0 = Vector128_.ShuffleNative(vs0, mask); + Unsafe.Add(ref vd0, (nuint)1) = Vector128_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)1), mask); + Unsafe.Add(ref vd0, (nuint)2) = Vector128_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)2), mask); + Unsafe.Add(ref vd0, (nuint)3) = Vector128_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)3), mask); + } + + if (m > 0) + { + for (nuint i = u; i < n; i++) + { + Unsafe.Add(ref destinationBase, i) = Vector128_.ShuffleNative(Unsafe.Add(ref sourceBase, i), mask); + } + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void Shuffle3( + ReadOnlySpan source, + Span destination, + [ConstantExpected] byte control) + { + if (Vector128.IsHardwareAccelerated) + { + Vector128 maskPad4Nx16 = ShuffleMaskPad4Nx16(); + Vector128 maskSlice4Nx16 = ShuffleMaskSlice4Nx16(); + Vector128 maskE = Vector128_.AlignRight(maskSlice4Nx16, maskSlice4Nx16, 12); + + Span bytes = stackalloc byte[Vector128.Count]; + Shuffle.MMShuffleSpan(ref bytes, control); + Vector128 mask = Unsafe.As>(ref MemoryMarshal.GetReference(bytes)); + + ref Vector128 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector128 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + nuint n = source.Vector128Count(); + + for (nuint i = 0; i < n; i += 3) + { + ref Vector128 vs = ref Unsafe.Add(ref sourceBase, i); + + Vector128 v0 = vs; + Vector128 v1 = Unsafe.Add(ref vs, (nuint)1); + Vector128 v2 = Unsafe.Add(ref vs, (nuint)2); + Vector128 v3 = Vector128_.ShiftRightBytesInVector(v2, 4); + + v2 = Vector128_.AlignRight(v2, v1, 8); + v1 = Vector128_.AlignRight(v1, v0, 12); + + v0 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v0, maskPad4Nx16), mask); + v1 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v1, maskPad4Nx16), mask); + v2 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v2, maskPad4Nx16), mask); + v3 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v3, maskPad4Nx16), mask); + + v0 = Vector128_.ShuffleNative(v0, maskE); + v1 = Vector128_.ShuffleNative(v1, maskSlice4Nx16); + v2 = Vector128_.ShuffleNative(v2, maskE); + v3 = Vector128_.ShuffleNative(v3, maskSlice4Nx16); + + v0 = Vector128_.AlignRight(v1, v0, 4); + v3 = Vector128_.AlignRight(v3, v2, 12); + + v1 = Vector128_.ShiftLeftBytesInVector(v1, 4); + v2 = Vector128_.ShiftRightBytesInVector(v2, 4); + + v1 = Vector128_.AlignRight(v2, v1, 8); + + ref Vector128 vd = ref Unsafe.Add(ref destinationBase, i); + + vd = v0; + Unsafe.Add(ref vd, (nuint)1) = v1; + Unsafe.Add(ref vd, (nuint)2) = v3; + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void Pad3Shuffle4( + ReadOnlySpan source, + Span destination, + [ConstantExpected] byte control) + { + if (Vector128.IsHardwareAccelerated) + { + Vector128 maskPad4Nx16 = ShuffleMaskPad4Nx16(); + Vector128 fill = Vector128.Create(0xff000000ff000000ul).AsByte(); + + Span temp = stackalloc byte[Vector128.Count]; + Shuffle.MMShuffleSpan(ref temp, control); + Vector128 mask = Unsafe.As>(ref MemoryMarshal.GetReference(temp)); + + ref Vector128 sourceBase = + ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + + ref Vector128 destinationBase = + ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + nuint n = source.Vector128Count(); + + for (nuint i = 0, j = 0; i < n; i += 3, j += 4) + { + ref Vector128 v0 = ref Unsafe.Add(ref sourceBase, i); + Vector128 v1 = Unsafe.Add(ref v0, 1); + Vector128 v2 = Unsafe.Add(ref v0, 2); + Vector128 v3 = Vector128_.ShiftRightBytesInVector(v2, 4); + + v2 = Vector128_.AlignRight(v2, v1, 8); + v1 = Vector128_.AlignRight(v1, v0, 12); + + ref Vector128 vd = ref Unsafe.Add(ref destinationBase, j); + + vd = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v0, maskPad4Nx16) | fill, mask); + Unsafe.Add(ref vd, 1) = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v1, maskPad4Nx16) | fill, mask); + Unsafe.Add(ref vd, 2) = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v2, maskPad4Nx16) | fill, mask); + Unsafe.Add(ref vd, 3) = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v3, maskPad4Nx16) | fill, mask); + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void Shuffle4Slice3( + ReadOnlySpan source, + Span destination, + [ConstantExpected] byte control) + { + if (Vector128.IsHardwareAccelerated) + { + Vector128 maskSlice4Nx16 = ShuffleMaskSlice4Nx16(); + Vector128 maskE = Vector128_.AlignRight(maskSlice4Nx16, maskSlice4Nx16, 12); + + Span temp = stackalloc byte[Vector128.Count]; + Shuffle.MMShuffleSpan(ref temp, control); + Vector128 mask = Unsafe.As>(ref MemoryMarshal.GetReference(temp)); + + ref Vector128 sourceBase = + ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + + ref Vector128 destinationBase = + ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + nuint n = source.Vector128Count(); + + for (nuint i = 0, j = 0; i < n; i += 4, j += 3) + { + ref Vector128 vs = ref Unsafe.Add(ref sourceBase, i); + + Vector128 v0 = vs; + Vector128 v1 = Unsafe.Add(ref vs, 1); + Vector128 v2 = Unsafe.Add(ref vs, 2); + Vector128 v3 = Unsafe.Add(ref vs, 3); + + v0 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v0, mask), maskE); + v1 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v1, mask), maskSlice4Nx16); + v2 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v2, mask), maskE); + v3 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v3, mask), maskSlice4Nx16); + + v0 = Vector128_.AlignRight(v1, v0, 4); + v3 = Vector128_.AlignRight(v3, v2, 12); + + v1 = Vector128_.ShiftLeftBytesInVector(v1, 4); + v2 = Vector128_.ShiftRightBytesInVector(v2, 4); + + v1 = Vector128_.AlignRight(v2, v1, 8); + + ref Vector128 vd = ref Unsafe.Add(ref destinationBase, j); + + vd = v0; + Unsafe.Add(ref vd, 1) = v1; + Unsafe.Add(ref vd, 2) = v3; + } + } + } + + /// + /// Blend packed 8-bit integers from and using . + /// The high bit of each corresponding byte determines the selection. + /// If the high bit is set the element of is selected. + /// The element of is selected otherwise. + /// + /// The left vector. + /// The right vector. + /// The mask vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 BlendVariable(Vector128 left, Vector128 right, Vector128 mask) + { + if (Sse41.IsSupported) + { + return Sse41.BlendVariable(left, right, mask); + } + else if (Sse2.IsSupported) + { + return Sse2.Or(Sse2.And(right, mask), Sse2.AndNot(mask, left)); + } + + // Use a signed shift right to create a mask with the sign bit. + Vector128 signedMask = AdvSimd.ShiftRightArithmetic(mask.AsInt16(), 7); + return AdvSimd.BitwiseSelect(signedMask, right.AsInt16(), left.AsInt16()).AsByte(); + } + + /// + /// Blend packed 32-bit unsigned integers from and using . + /// The high bit of each corresponding byte determines the selection. + /// If the high bit is set the element of is selected. + /// The element of is selected otherwise. + /// + /// The left vector. + /// The right vector. + /// The mask vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 BlendVariable(Vector128 left, Vector128 right, Vector128 mask) + => BlendVariable(left.AsByte(), right.AsByte(), mask.AsByte()).AsUInt32(); + + /// + /// Count the number of leading zero bits in a mask. + /// Similar in behavior to the x86 instruction LZCNT. + /// + /// The value. + public static ushort LeadingZeroCount(ushort value) + => (ushort)(BitOperations.LeadingZeroCount(value) - 16); + + /// + /// Count the number of trailing zero bits in an integer value. + /// Similar in behavior to the x86 instruction TZCNT. + /// + /// The value. + public static ushort TrailingZeroCount(ushort value) + => (ushort)(BitOperations.TrailingZeroCount(value << 16) - 16); + + /// + /// as many elements as possible, slicing them down (keeping the remainder). + /// + /// The source buffer. + /// The destination buffer. + [MethodImpl(InliningOptions.ShortMethod)] + internal static void ByteToNormalizedFloatReduce( + ref ReadOnlySpan source, + ref Span destination) + { + DebugGuard.IsTrue(source.Length == destination.Length, nameof(source), "Input spans must be of same length!"); + + if (Vector128.IsHardwareAccelerated) + { + int remainder; + if (Vector512.IsHardwareAccelerated && Avx512F.IsSupported) + { + remainder = Numerics.ModuloP2(source.Length, Vector512.Count); + } + else if (Avx2.IsSupported) + { + remainder = Numerics.ModuloP2(source.Length, Vector256.Count); + } + else + { + remainder = Numerics.ModuloP2(source.Length, Vector128.Count); + } + + int adjustedCount = source.Length - remainder; + + if (adjustedCount > 0) + { + ByteToNormalizedFloat(source[..adjustedCount], destination[..adjustedCount]); + + source = source[adjustedCount..]; + destination = destination[adjustedCount..]; + } + } + } + + /// + /// Implementation , which is faster on new RyuJIT runtime. + /// + /// The source buffer. + /// The destination buffer. + /// + /// Implementation is based on MagicScaler code: + /// https://github.com/saucecontrol/PhotoSauce/blob/b5811908041200488aa18fdfd17df5fc457415dc/src/MagicScaler/Magic/Processors/ConvertersFloat.cs#L80-L182 + /// + internal static void ByteToNormalizedFloat( + ReadOnlySpan source, + Span destination) + { + if (Vector512.IsHardwareAccelerated && Avx512F.IsSupported) + { + DebugVerifySpanInput(source, destination, Vector512.Count); + + nuint n = destination.Vector512Count(); + + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + for (nuint i = 0; i < n; i++) + { + nuint si = (uint)Vector512.Count * i; + Vector512 i0 = Avx512F.ConvertToVector512Int32(Vector128.LoadUnsafe(ref sourceBase, si)); + Vector512 i1 = Avx512F.ConvertToVector512Int32(Vector128.LoadUnsafe(ref sourceBase, si + (nuint)Vector512.Count)); + Vector512 i2 = Avx512F.ConvertToVector512Int32(Vector128.LoadUnsafe(ref sourceBase, si + (nuint)(Vector512.Count * 2))); + Vector512 i3 = Avx512F.ConvertToVector512Int32(Vector128.LoadUnsafe(ref sourceBase, si + (nuint)(Vector512.Count * 3))); + + // Declare multiplier on each line. Codegen is better. + Vector512 f0 = Vector512.Create(1 / (float)byte.MaxValue) * Avx512F.ConvertToVector512Single(i0); + Vector512 f1 = Vector512.Create(1 / (float)byte.MaxValue) * Avx512F.ConvertToVector512Single(i1); + Vector512 f2 = Vector512.Create(1 / (float)byte.MaxValue) * Avx512F.ConvertToVector512Single(i2); + Vector512 f3 = Vector512.Create(1 / (float)byte.MaxValue) * Avx512F.ConvertToVector512Single(i3); + + ref Vector512 d = ref Unsafe.Add(ref destinationBase, i * 4); + + d = f0; + Unsafe.Add(ref d, 1) = f1; + Unsafe.Add(ref d, 2) = f2; + Unsafe.Add(ref d, 3) = f3; + } + } + else if (Avx2.IsSupported) + { + DebugVerifySpanInput(source, destination, Vector256.Count); + + nuint n = destination.Vector256Count(); + + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + for (nuint i = 0; i < n; i++) + { + nuint si = (uint)Vector256.Count * i; + Vector256 i0 = Avx2.ConvertToVector256Int32(Vector128.LoadUnsafe(ref sourceBase, si)); + Vector256 i1 = Avx2.ConvertToVector256Int32(Vector128.LoadUnsafe(ref sourceBase, si + (nuint)Vector256.Count)); + Vector256 i2 = Avx2.ConvertToVector256Int32(Vector128.LoadUnsafe(ref sourceBase, si + (nuint)(Vector256.Count * 2))); + + // Ensure overreads past 16 byte boundary do not happen in debug due to lack of containment. + ref ulong refULong = ref Unsafe.As(ref Unsafe.Add(ref sourceBase, si)); + Vector256 i3 = Avx2.ConvertToVector256Int32(Vector128.CreateScalarUnsafe(Unsafe.Add(ref refULong, 3)).AsByte()); + + // Declare multiplier on each line. Codegen is better. + Vector256 f0 = Vector256.Create(1 / (float)byte.MaxValue) * Avx.ConvertToVector256Single(i0); + Vector256 f1 = Vector256.Create(1 / (float)byte.MaxValue) * Avx.ConvertToVector256Single(i1); + Vector256 f2 = Vector256.Create(1 / (float)byte.MaxValue) * Avx.ConvertToVector256Single(i2); + Vector256 f3 = Vector256.Create(1 / (float)byte.MaxValue) * Avx.ConvertToVector256Single(i3); + + ref Vector256 d = ref Unsafe.Add(ref destinationBase, i * 4); + + d = f0; + Unsafe.Add(ref d, 1) = f1; + Unsafe.Add(ref d, 2) = f2; + Unsafe.Add(ref d, 3) = f3; + } + } + else if (Vector128.IsHardwareAccelerated) + { + DebugVerifySpanInput(source, destination, Vector128.Count); + + nuint n = destination.Vector128Count(); + + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref Vector128 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + Vector128 scale = Vector128.Create(1 / (float)byte.MaxValue); + + for (nuint i = 0; i < n; i++) + { + nuint si = (uint)Vector128.Count * i; + + Vector128 i0, i1, i2, i3; + if (Sse41.IsSupported) + { + ref int refInt = ref Unsafe.As(ref Unsafe.Add(ref sourceBase, si)); + + i0 = Sse41.ConvertToVector128Int32(Vector128.CreateScalarUnsafe(refInt).AsByte()); + i1 = Sse41.ConvertToVector128Int32(Vector128.CreateScalarUnsafe(Unsafe.Add(ref refInt, 1)).AsByte()); + i2 = Sse41.ConvertToVector128Int32(Vector128.CreateScalarUnsafe(Unsafe.Add(ref refInt, 2)).AsByte()); + i3 = Sse41.ConvertToVector128Int32(Vector128.CreateScalarUnsafe(Unsafe.Add(ref refInt, 3)).AsByte()); + } + else + { + // Sse2, AdvSimd, etc + Vector128 b = Vector128.LoadUnsafe(ref sourceBase, si); + (Vector128 s0, Vector128 s1) = Vector128.Widen(b); + (i0, i1) = Vector128.Widen(s0.AsInt16()); + (i2, i3) = Vector128.Widen(s1.AsInt16()); + } + + Vector128 f0 = scale * Vector128.ConvertToSingle(i0); + Vector128 f1 = scale * Vector128.ConvertToSingle(i1); + Vector128 f2 = scale * Vector128.ConvertToSingle(i2); + Vector128 f3 = scale * Vector128.ConvertToSingle(i3); + + ref Vector128 d = ref Unsafe.Add(ref destinationBase, i * 4); + + d = f0; + Unsafe.Add(ref d, 1) = f1; + Unsafe.Add(ref d, 2) = f2; + Unsafe.Add(ref d, 3) = f3; + } + } + } + + /// + /// as many elements as possible, slicing them down (keeping the remainder). + /// + /// The source buffer. + /// The destination buffer. + [MethodImpl(InliningOptions.ShortMethod)] + internal static void NormalizedFloatToByteSaturateReduce( + ref ReadOnlySpan source, + ref Span destination) + { + DebugGuard.IsTrue(source.Length == destination.Length, nameof(source), "Input spans must be of same length!"); + + if (Sse2.IsSupported || AdvSimd.IsSupported) + { + int remainder; + + if (Vector512.IsHardwareAccelerated && Avx512BW.IsSupported) + { + remainder = Numerics.ModuloP2(source.Length, Vector512.Count); + } + else if (Avx2.IsSupported) + { + remainder = Numerics.ModuloP2(source.Length, Vector256.Count); + } + else + { + remainder = Numerics.ModuloP2(source.Length, Vector128.Count); + } + + int adjustedCount = source.Length - remainder; + + if (adjustedCount > 0) + { + NormalizedFloatToByteSaturate( + source[..adjustedCount], + destination[..adjustedCount]); + + source = source[adjustedCount..]; + destination = destination[adjustedCount..]; + } + } + } + + /// + /// Implementation of , which is faster on new .NET runtime. + /// + /// The source buffer. + /// The destination buffer. + /// + /// Implementation is based on MagicScaler code: + /// https://github.com/saucecontrol/PhotoSauce/blob/b5811908041200488aa18fdfd17df5fc457415dc/src/MagicScaler/Magic/Processors/ConvertersFloat.cs#L541-L622 + /// + internal static void NormalizedFloatToByteSaturate( + ReadOnlySpan source, + Span destination) + { + if (Vector512.IsHardwareAccelerated && Avx512BW.IsSupported) + { + DebugVerifySpanInput(source, destination, Vector512.Count); + + nuint n = destination.Vector512Count(); + + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + Vector512 scale = Vector512.Create((float)byte.MaxValue); + Vector512 mask = PermuteMaskDeinterleave16x32(); + + for (nuint i = 0; i < n; i++) + { + ref Vector512 s = ref Unsafe.Add(ref sourceBase, i * 4); + + Vector512 f0 = scale * s; + Vector512 f1 = scale * Unsafe.Add(ref s, 1); + Vector512 f2 = scale * Unsafe.Add(ref s, 2); + Vector512 f3 = scale * Unsafe.Add(ref s, 3); + + Vector512 w0 = Vector512_.ConvertToInt32RoundToEven(f0); + Vector512 w1 = Vector512_.ConvertToInt32RoundToEven(f1); + Vector512 w2 = Vector512_.ConvertToInt32RoundToEven(f2); + Vector512 w3 = Vector512_.ConvertToInt32RoundToEven(f3); + + Vector512 u0 = Avx512BW.PackSignedSaturate(w0, w1); + Vector512 u1 = Avx512BW.PackSignedSaturate(w2, w3); + Vector512 b = Avx512BW.PackUnsignedSaturate(u0, u1); + b = Avx512F.PermuteVar16x32(b.AsInt32(), mask).AsByte(); + + Unsafe.Add(ref destinationBase, i) = b; + } + } + else if (Avx2.IsSupported) + { + DebugVerifySpanInput(source, destination, Vector256.Count); + + nuint n = destination.Vector256Count(); + + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + Vector256 scale = Vector256.Create((float)byte.MaxValue); + Vector256 mask = PermuteMaskDeinterleave8x32(); + + for (nuint i = 0; i < n; i++) + { + ref Vector256 s = ref Unsafe.Add(ref sourceBase, i * 4); + + Vector256 f0 = scale * s; + Vector256 f1 = scale * Unsafe.Add(ref s, 1); + Vector256 f2 = scale * Unsafe.Add(ref s, 2); + Vector256 f3 = scale * Unsafe.Add(ref s, 3); + + Vector256 w0 = Vector256_.ConvertToInt32RoundToEven(f0); + Vector256 w1 = Vector256_.ConvertToInt32RoundToEven(f1); + Vector256 w2 = Vector256_.ConvertToInt32RoundToEven(f2); + Vector256 w3 = Vector256_.ConvertToInt32RoundToEven(f3); + + Vector256 u0 = Avx2.PackSignedSaturate(w0, w1); + Vector256 u1 = Avx2.PackSignedSaturate(w2, w3); + Vector256 b = Avx2.PackUnsignedSaturate(u0, u1); + b = Avx2.PermuteVar8x32(b.AsInt32(), mask).AsByte(); + + Unsafe.Add(ref destinationBase, i) = b; + } + } + else if (Vector128.IsHardwareAccelerated) + { + // Sse, AdvSimd, etc. + DebugVerifySpanInput(source, destination, Vector128.Count); + + nuint n = destination.Vector128Count(); + + ref Vector128 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector128 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + Vector128 scale = Vector128.Create((float)byte.MaxValue); + Vector128 min = Vector128.Zero; + Vector128 max = Vector128.Create((int)byte.MaxValue); + + for (nuint i = 0; i < n; i++) + { + ref Vector128 s = ref Unsafe.Add(ref sourceBase, i * 4); + + Vector128 f0 = scale * s; + Vector128 f1 = scale * Unsafe.Add(ref s, 1); + Vector128 f2 = scale * Unsafe.Add(ref s, 2); + Vector128 f3 = scale * Unsafe.Add(ref s, 3); + + Vector128 w0 = Vector128_.ConvertToInt32RoundToEven(f0); + Vector128 w1 = Vector128_.ConvertToInt32RoundToEven(f1); + Vector128 w2 = Vector128_.ConvertToInt32RoundToEven(f2); + Vector128 w3 = Vector128_.ConvertToInt32RoundToEven(f3); + + w0 = Vector128_.Clamp(w0, min, max); + w1 = Vector128_.Clamp(w1, min, max); + w2 = Vector128_.Clamp(w2, min, max); + w3 = Vector128_.Clamp(w3, min, max); + + Vector128 u0 = Vector128.Narrow(w0, w1).AsUInt16(); + Vector128 u1 = Vector128.Narrow(w2, w3).AsUInt16(); + + Unsafe.Add(ref destinationBase, i) = Vector128.Narrow(u0, u1); + } + } + } + + internal static void PackFromRgbPlanesAvx2Reduce( + ref ReadOnlySpan redChannel, + ref ReadOnlySpan greenChannel, + ref ReadOnlySpan blueChannel, + ref Span destination) + { + ref Vector256 rBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(redChannel)); + ref Vector256 gBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(greenChannel)); + ref Vector256 bBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(blueChannel)); + ref byte dBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + + nuint count = redChannel.Vector256Count(); + + Vector256 control1 = PermuteMaskEvenOdd8x32(); + + Vector256 control2 = PermuteMaskShiftAlpha8x32(); + Vector256 a = Vector256.Create((byte)255); + + Vector256 shuffleAlpha = ShuffleMaskShiftAlpha(); + + for (nuint i = 0; i < count; i++) + { + Vector256 r0 = Unsafe.Add(ref rBase, i); + Vector256 g0 = Unsafe.Add(ref gBase, i); + Vector256 b0 = Unsafe.Add(ref bBase, i); + + r0 = Avx2.PermuteVar8x32(r0.AsUInt32(), control1).AsByte(); + g0 = Avx2.PermuteVar8x32(g0.AsUInt32(), control1).AsByte(); + b0 = Avx2.PermuteVar8x32(b0.AsUInt32(), control1).AsByte(); + + Vector256 rg = Avx2.UnpackLow(r0, g0); + Vector256 b1 = Avx2.UnpackLow(b0, a); + + Vector256 rgb1 = Avx2.UnpackLow(rg.AsUInt16(), b1.AsUInt16()).AsByte(); + Vector256 rgb2 = Avx2.UnpackHigh(rg.AsUInt16(), b1.AsUInt16()).AsByte(); + + rg = Avx2.UnpackHigh(r0, g0); + b1 = Avx2.UnpackHigh(b0, a); + + Vector256 rgb3 = Avx2.UnpackLow(rg.AsUInt16(), b1.AsUInt16()).AsByte(); + Vector256 rgb4 = Avx2.UnpackHigh(rg.AsUInt16(), b1.AsUInt16()).AsByte(); + + rgb1 = Avx2.Shuffle(rgb1, shuffleAlpha); + rgb2 = Avx2.Shuffle(rgb2, shuffleAlpha); + rgb3 = Avx2.Shuffle(rgb3, shuffleAlpha); + rgb4 = Avx2.Shuffle(rgb4, shuffleAlpha); + + rgb1 = Avx2.PermuteVar8x32(rgb1.AsUInt32(), control2).AsByte(); + rgb2 = Avx2.PermuteVar8x32(rgb2.AsUInt32(), control2).AsByte(); + rgb3 = Avx2.PermuteVar8x32(rgb3.AsUInt32(), control2).AsByte(); + rgb4 = Avx2.PermuteVar8x32(rgb4.AsUInt32(), control2).AsByte(); + + ref byte d1 = ref Unsafe.Add(ref dBase, 24 * 4 * i); + ref byte d2 = ref Unsafe.Add(ref d1, 24); + ref byte d3 = ref Unsafe.Add(ref d2, 24); + ref byte d4 = ref Unsafe.Add(ref d3, 24); + + Unsafe.As>(ref d1) = rgb1; + Unsafe.As>(ref d2) = rgb2; + Unsafe.As>(ref d3) = rgb3; + Unsafe.As>(ref d4) = rgb4; + } + + int slice = (int)count * Vector256.Count; + redChannel = redChannel[slice..]; + greenChannel = greenChannel[slice..]; + blueChannel = blueChannel[slice..]; + destination = destination[slice..]; + } + + internal static void PackFromRgbPlanesAvx2Reduce( + ref ReadOnlySpan redChannel, + ref ReadOnlySpan greenChannel, + ref ReadOnlySpan blueChannel, + ref Span destination) + { + ref Vector256 rBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(redChannel)); + ref Vector256 gBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(greenChannel)); + ref Vector256 bBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(blueChannel)); + ref Vector256 dBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + nuint count = redChannel.Vector256Count(); + Vector256 control1 = PermuteMaskEvenOdd8x32(); + Vector256 a = Vector256.Create((byte)255); + + for (nuint i = 0; i < count; i++) + { + Vector256 r0 = Unsafe.Add(ref rBase, i); + Vector256 g0 = Unsafe.Add(ref gBase, i); + Vector256 b0 = Unsafe.Add(ref bBase, i); + + r0 = Avx2.PermuteVar8x32(r0.AsUInt32(), control1).AsByte(); + g0 = Avx2.PermuteVar8x32(g0.AsUInt32(), control1).AsByte(); + b0 = Avx2.PermuteVar8x32(b0.AsUInt32(), control1).AsByte(); + + Vector256 rg = Avx2.UnpackLow(r0, g0); + Vector256 b1 = Avx2.UnpackLow(b0, a); + + Vector256 rgb1 = Avx2.UnpackLow(rg.AsUInt16(), b1.AsUInt16()).AsByte(); + Vector256 rgb2 = Avx2.UnpackHigh(rg.AsUInt16(), b1.AsUInt16()).AsByte(); + + rg = Avx2.UnpackHigh(r0, g0); + b1 = Avx2.UnpackHigh(b0, a); + + Vector256 rgb3 = Avx2.UnpackLow(rg.AsUInt16(), b1.AsUInt16()).AsByte(); + Vector256 rgb4 = Avx2.UnpackHigh(rg.AsUInt16(), b1.AsUInt16()).AsByte(); + + ref Vector256 d0 = ref Unsafe.Add(ref dBase, i * 4); + d0 = rgb1; + Unsafe.Add(ref d0, 1) = rgb2; + Unsafe.Add(ref d0, 2) = rgb3; + Unsafe.Add(ref d0, 3) = rgb4; + } + + int slice = (int)count * Vector256.Count; + redChannel = redChannel[slice..]; + greenChannel = greenChannel[slice..]; + blueChannel = blueChannel[slice..]; + destination = destination[slice..]; + } + + internal static void UnpackToRgbPlanesAvx2Reduce( + ref Span redChannel, + ref Span greenChannel, + ref Span blueChannel, + ref ReadOnlySpan source) + { + ref Vector256 rgbByteSpan = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector256 destRRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(redChannel)); + ref Vector256 destGRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(greenChannel)); + ref Vector256 destBRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(blueChannel)); + + Vector256 extractToLanesMask = MoveFirst24BytesToSeparateLanes(); + Vector256 extractRgbMask = ExtractRgb(); + Vector256 rgb, rg, bx; + Vector256 r, g, b; + + // Each iteration consumes 8 Rgb24 pixels (24 bytes) but starts with a 32-byte load, + // so we need 3 extra pixels of addressable slack beyond the vectorized chunk. + const int bytesPerRgbStride = 24; + nuint count = source.Length > 3 ? (uint)(source.Length - 3) / 8 : 0; + for (nuint i = 0; i < count; i++) + { + rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref rgbByteSpan, (uint)(bytesPerRgbStride * i)).AsUInt32(), extractToLanesMask).AsByte(); + + rgb = Avx2.Shuffle(rgb, extractRgbMask); + + rg = Avx2.UnpackLow(rgb, Vector256.Zero); + bx = Avx2.UnpackHigh(rgb, Vector256.Zero); + + r = Avx.ConvertToVector256Single(Avx2.UnpackLow(rg, Vector256.Zero).AsInt32()); + g = Avx.ConvertToVector256Single(Avx2.UnpackHigh(rg, Vector256.Zero).AsInt32()); + b = Avx.ConvertToVector256Single(Avx2.UnpackLow(bx, Vector256.Zero).AsInt32()); + + Unsafe.Add(ref destRRef, i) = r; + Unsafe.Add(ref destGRef, i) = g; + Unsafe.Add(ref destBRef, i) = b; + } + + int sliceCount = (int)(count * 8); + redChannel = redChannel[sliceCount..]; + greenChannel = greenChannel[sliceCount..]; + blueChannel = blueChannel[sliceCount..]; + source = source[sliceCount..]; + } + } + } +} diff --git a/ImageSharp/Common/Helpers/SimdUtils.Pack.cs b/ImageSharp/Common/Helpers/SimdUtils.Pack.cs new file mode 100644 index 0000000..7475092 --- /dev/null +++ b/ImageSharp/Common/Helpers/SimdUtils.Pack.cs @@ -0,0 +1,238 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp { + internal static partial class SimdUtils + { + [MethodImpl(InliningOptions.ShortMethod)] + internal static void PackFromRgbPlanes( + ReadOnlySpan redChannel, + ReadOnlySpan greenChannel, + ReadOnlySpan blueChannel, + Span destination) + { + DebugGuard.IsTrue(greenChannel.Length == redChannel.Length, nameof(greenChannel), "Channels must be of same size!"); + DebugGuard.IsTrue(blueChannel.Length == redChannel.Length, nameof(blueChannel), "Channels must be of same size!"); + DebugGuard.IsTrue(destination.Length > redChannel.Length + 2, nameof(destination), "'destination' must contain a padding of 3 elements!"); + + if (Avx2.IsSupported) + { + HwIntrinsics.PackFromRgbPlanesAvx2Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination); + } + else + { + PackFromRgbPlanesScalarBatchedReduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination); + } + + PackFromRgbPlanesRemainder(redChannel, greenChannel, blueChannel, destination); + } + + [MethodImpl(InliningOptions.ShortMethod)] + internal static void PackFromRgbPlanes( + ReadOnlySpan redChannel, + ReadOnlySpan greenChannel, + ReadOnlySpan blueChannel, + Span destination) + { + DebugGuard.IsTrue(greenChannel.Length == redChannel.Length, nameof(greenChannel), "Channels must be of same size!"); + DebugGuard.IsTrue(blueChannel.Length == redChannel.Length, nameof(blueChannel), "Channels must be of same size!"); + DebugGuard.IsTrue(destination.Length > redChannel.Length, nameof(destination), "'destination' span should not be shorter than the source channels!"); + + if (Avx2.IsSupported) + { + HwIntrinsics.PackFromRgbPlanesAvx2Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination); + } + else + { + PackFromRgbPlanesScalarBatchedReduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination); + } + + PackFromRgbPlanesRemainder(redChannel, greenChannel, blueChannel, destination); + } + + [MethodImpl(InliningOptions.ShortMethod)] + internal static void UnpackToRgbPlanes( + Span redChannel, + Span greenChannel, + Span blueChannel, + ReadOnlySpan source) + { + DebugGuard.IsTrue(greenChannel.Length == redChannel.Length, nameof(greenChannel), "Channels must be of same size!"); + DebugGuard.IsTrue(blueChannel.Length == redChannel.Length, nameof(blueChannel), "Channels must be of same size!"); + DebugGuard.IsTrue(source.Length <= redChannel.Length, nameof(source), "'source' span should not be bigger than the destination channels!"); + + if (Avx2.IsSupported) + { + HwIntrinsics.UnpackToRgbPlanesAvx2Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref source); + } + + UnpackToRgbPlanesScalar(redChannel, greenChannel, blueChannel, source); + } + + private static void PackFromRgbPlanesScalarBatchedReduce( + ref ReadOnlySpan redChannel, + ref ReadOnlySpan greenChannel, + ref ReadOnlySpan blueChannel, + ref Span destination) + { + ref ByteTuple4 r = ref Unsafe.As(ref MemoryMarshal.GetReference(redChannel)); + ref ByteTuple4 g = ref Unsafe.As(ref MemoryMarshal.GetReference(greenChannel)); + ref ByteTuple4 b = ref Unsafe.As(ref MemoryMarshal.GetReference(blueChannel)); + ref Rgb24 rgb = ref MemoryMarshal.GetReference(destination); + + nuint count = (uint)redChannel.Length / 4; + for (nuint i = 0; i < count; i++) + { + ref Rgb24 d0 = ref Unsafe.Add(ref rgb, i * 4); + ref Rgb24 d1 = ref Unsafe.Add(ref d0, 1); + ref Rgb24 d2 = ref Unsafe.Add(ref d0, 2); + ref Rgb24 d3 = ref Unsafe.Add(ref d0, 3); + + ref ByteTuple4 rr = ref Unsafe.Add(ref r, i); + ref ByteTuple4 gg = ref Unsafe.Add(ref g, i); + ref ByteTuple4 bb = ref Unsafe.Add(ref b, i); + + d0.R = rr.V0; + d0.G = gg.V0; + d0.B = bb.V0; + + d1.R = rr.V1; + d1.G = gg.V1; + d1.B = bb.V1; + + d2.R = rr.V2; + d2.G = gg.V2; + d2.B = bb.V2; + + d3.R = rr.V3; + d3.G = gg.V3; + d3.B = bb.V3; + } + + int finished = (int)(count * 4); + redChannel = redChannel[finished..]; + greenChannel = greenChannel[finished..]; + blueChannel = blueChannel[finished..]; + destination = destination[finished..]; + } + + private static void PackFromRgbPlanesScalarBatchedReduce( + ref ReadOnlySpan redChannel, + ref ReadOnlySpan greenChannel, + ref ReadOnlySpan blueChannel, + ref Span destination) + { + ref ByteTuple4 r = ref Unsafe.As(ref MemoryMarshal.GetReference(redChannel)); + ref ByteTuple4 g = ref Unsafe.As(ref MemoryMarshal.GetReference(greenChannel)); + ref ByteTuple4 b = ref Unsafe.As(ref MemoryMarshal.GetReference(blueChannel)); + ref Rgba32 rgb = ref MemoryMarshal.GetReference(destination); + + nuint count = (uint)redChannel.Length / 4; + destination.Fill(new Rgba32(0, 0, 0, 255)); + for (nuint i = 0; i < count; i++) + { + ref Rgba32 d0 = ref Unsafe.Add(ref rgb, i * 4); + ref Rgba32 d1 = ref Unsafe.Add(ref d0, 1); + ref Rgba32 d2 = ref Unsafe.Add(ref d0, 2); + ref Rgba32 d3 = ref Unsafe.Add(ref d0, 3); + + ref ByteTuple4 rr = ref Unsafe.Add(ref r, i); + ref ByteTuple4 gg = ref Unsafe.Add(ref g, i); + ref ByteTuple4 bb = ref Unsafe.Add(ref b, i); + + d0.R = rr.V0; + d0.G = gg.V0; + d0.B = bb.V0; + + d1.R = rr.V1; + d1.G = gg.V1; + d1.B = bb.V1; + + d2.R = rr.V2; + d2.G = gg.V2; + d2.B = bb.V2; + + d3.R = rr.V3; + d3.G = gg.V3; + d3.B = bb.V3; + } + + int finished = (int)(count * 4); + redChannel = redChannel[finished..]; + greenChannel = greenChannel[finished..]; + blueChannel = blueChannel[finished..]; + destination = destination[finished..]; + } + + private static void PackFromRgbPlanesRemainder( + ReadOnlySpan redChannel, + ReadOnlySpan greenChannel, + ReadOnlySpan blueChannel, + Span destination) + { + ref byte r = ref MemoryMarshal.GetReference(redChannel); + ref byte g = ref MemoryMarshal.GetReference(greenChannel); + ref byte b = ref MemoryMarshal.GetReference(blueChannel); + ref Rgb24 rgb = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)destination.Length; i++) + { + ref Rgb24 d = ref Unsafe.Add(ref rgb, i); + d.R = Unsafe.Add(ref r, i); + d.G = Unsafe.Add(ref g, i); + d.B = Unsafe.Add(ref b, i); + } + } + + private static void PackFromRgbPlanesRemainder( + ReadOnlySpan redChannel, + ReadOnlySpan greenChannel, + ReadOnlySpan blueChannel, + Span destination) + { + ref byte r = ref MemoryMarshal.GetReference(redChannel); + ref byte g = ref MemoryMarshal.GetReference(greenChannel); + ref byte b = ref MemoryMarshal.GetReference(blueChannel); + ref Rgba32 rgba = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)destination.Length; i++) + { + ref Rgba32 d = ref Unsafe.Add(ref rgba, i); + d.R = Unsafe.Add(ref r, i); + d.G = Unsafe.Add(ref g, i); + d.B = Unsafe.Add(ref b, i); + d.A = 255; + } + } + + private static void UnpackToRgbPlanesScalar( + Span redChannel, + Span greenChannel, + Span blueChannel, + ReadOnlySpan source) + { + DebugGuard.IsTrue(greenChannel.Length == redChannel.Length, nameof(greenChannel), "Channels must be of same size!"); + DebugGuard.IsTrue(blueChannel.Length == redChannel.Length, nameof(blueChannel), "Channels must be of same size!"); + DebugGuard.IsTrue(source.Length <= redChannel.Length, nameof(source), "'source' span should not be bigger than the destination channels!"); + + ref float r = ref MemoryMarshal.GetReference(redChannel); + ref float g = ref MemoryMarshal.GetReference(greenChannel); + ref float b = ref MemoryMarshal.GetReference(blueChannel); + ref Rgb24 rgb = ref MemoryMarshal.GetReference(source); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + ref Rgb24 src = ref Unsafe.Add(ref rgb, i); + Unsafe.Add(ref r, i) = src.R; + Unsafe.Add(ref g, i) = src.G; + Unsafe.Add(ref b, i) = src.B; + } + } + } +} diff --git a/ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs b/ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs new file mode 100644 index 0000000..9b67b0b --- /dev/null +++ b/ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs @@ -0,0 +1,554 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp { + internal static partial class SimdUtils + { + /// + /// Shuffle single-precision (32-bit) floating-point elements in + /// using the control and store the results in . + /// + /// The source span of floats. + /// The destination span of floats. + /// The byte control. + [MethodImpl(InliningOptions.ShortMethod)] + public static void Shuffle4( + ReadOnlySpan source, + Span destination, + [ConstantExpected] byte control) + { + VerifyShuffle4SpanInput(source, destination); + + HwIntrinsics.Shuffle4Reduce(ref source, ref destination, control); + + // Deal with the remainder: + if (source.Length > 0) + { + Shuffle4Remainder(source, destination, control); + } + } + + /// + /// Shuffle 8-bit integers within 128-bit lanes in + /// using the control and store the results in . + /// + /// The type of shuffle struct. + /// The source span of bytes. + /// The destination span of bytes. + /// The type of shuffle to perform. + [MethodImpl(InliningOptions.ShortMethod)] + public static void Shuffle4( + ReadOnlySpan source, + Span destination, + TShuffle shuffle) + where TShuffle : struct, IShuffle4 + { + VerifyShuffle4SpanInput(source, destination); + + shuffle.ShuffleReduce(ref source, ref destination); + + // Deal with the remainder: + if (source.Length > 0) + { + shuffle.Shuffle(source, destination); + } + } + + /// + /// Shuffle 8-bit integer triplets within 128-bit lanes in + /// using the control and store the results in . + /// + /// The type of shuffle struct. + /// The source span of bytes. + /// The destination span of bytes. + /// The type of shuffle to perform. + [MethodImpl(InliningOptions.ShortMethod)] + public static void Shuffle3( + ReadOnlySpan source, + Span destination, + TShuffle shuffle) + where TShuffle : struct, IShuffle3 + { + // Source length should be smaller than destination length, and divisible by 3. + VerifyShuffle3SpanInput(source, destination); + + shuffle.ShuffleReduce(ref source, ref destination); + + // Deal with the remainder: + if (source.Length > 0) + { + shuffle.Shuffle(source, destination); + } + } + + /// + /// Pads then shuffles 8-bit integers within 128-bit lanes in + /// using the control and store the results in . + /// + /// The type of shuffle struct. + /// The source span of bytes. + /// The destination span of bytes. + /// The type of shuffle to perform. + [MethodImpl(InliningOptions.ShortMethod)] + public static void Pad3Shuffle4( + ReadOnlySpan source, + Span destination, + TShuffle shuffle) + where TShuffle : struct, IPad3Shuffle4 + { + VerifyPad3Shuffle4SpanInput(source, destination); + + shuffle.ShuffleReduce(ref source, ref destination); + + // Deal with the remainder: + if (source.Length > 0) + { + shuffle.Shuffle(source, destination); + } + } + + /// + /// Shuffles then slices 8-bit integers within 128-bit lanes in + /// using the control and store the results in . + /// + /// The type of shuffle struct. + /// The source span of bytes. + /// The destination span of bytes. + /// The type of shuffle to perform. + [MethodImpl(InliningOptions.ShortMethod)] + public static void Shuffle4Slice3( + ReadOnlySpan source, + Span destination, + TShuffle shuffle) + where TShuffle : struct, IShuffle4Slice3 + { + VerifyShuffle4Slice3SpanInput(source, destination); + + shuffle.ShuffleReduce(ref source, ref destination); + + // Deal with the remainder: + if (source.Length > 0) + { + shuffle.Shuffle(source, destination); + } + } + + private static void Shuffle4Remainder( + ReadOnlySpan source, + Span destination, + byte control) + { + ref float sBase = ref MemoryMarshal.GetReference(source); + ref float dBase = ref MemoryMarshal.GetReference(destination); + Shuffle.InverseMMShuffle(control, out uint p3, out uint p2, out uint p1, out uint p0); + + for (nuint i = 0; i < (uint)source.Length; i += 4) + { + // Stage the scalar tail in a local Vector4 so p0..p3 index source + // values that were captured before any overlapping destination writes. + Vector4 v = Unsafe.As(ref Unsafe.Add(ref sBase, i)); + ref float pBase = ref Unsafe.As(ref v); + + Unsafe.Add(ref dBase, i + 0u) = Unsafe.Add(ref pBase, p0); + Unsafe.Add(ref dBase, i + 1u) = Unsafe.Add(ref pBase, p1); + Unsafe.Add(ref dBase, i + 2u) = Unsafe.Add(ref pBase, p2); + Unsafe.Add(ref dBase, i + 3u) = Unsafe.Add(ref pBase, p3); + } + } + + [Conditional("DEBUG")] + internal static void VerifyShuffle4SpanInput(ReadOnlySpan source, Span destination) + where T : struct + { + DebugGuard.IsTrue( + source.Length == destination.Length, + nameof(source), + "Input spans must be of same length!"); + + DebugGuard.IsTrue( + source.Length % 4 == 0, + nameof(source), + "Input spans must be divisible by 4!"); + } + + [Conditional("DEBUG")] + private static void VerifyShuffle3SpanInput(ReadOnlySpan source, Span destination) + where T : struct + { + DebugGuard.IsTrue( + source.Length <= destination.Length, + nameof(source), + "Source should fit into destination!"); + + DebugGuard.IsTrue( + source.Length % 3 == 0, + nameof(source), + "Input spans must be divisible by 3!"); + } + + [Conditional("DEBUG")] + private static void VerifyPad3Shuffle4SpanInput(ReadOnlySpan source, Span destination) + { + DebugGuard.IsTrue( + source.Length % 3 == 0, + nameof(source), + "Input span must be divisible by 3!"); + + DebugGuard.IsTrue( + destination.Length % 4 == 0, + nameof(destination), + "Output span must be divisible by 4!"); + + DebugGuard.IsTrue( + source.Length == destination.Length * 3 / 4, + nameof(source), + "Input span must be 3/4 the length of the output span!"); + } + + [Conditional("DEBUG")] + private static void VerifyShuffle4Slice3SpanInput(ReadOnlySpan source, Span destination) + { + DebugGuard.IsTrue( + source.Length % 4 == 0, + nameof(source), + "Input span must be divisible by 4!"); + + DebugGuard.IsTrue( + destination.Length % 3 == 0, + nameof(destination), + "Output span must be divisible by 3!"); + + DebugGuard.IsTrue( + destination.Length >= source.Length * 3 / 4, + nameof(source), + "Output span must be at least 3/4 the length of the input span!"); + } + + public static class Shuffle + { + public const byte MMShuffle0000 = 0b00000000; + public const byte MMShuffle0001 = 0b00000001; + public const byte MMShuffle0002 = 0b00000010; + public const byte MMShuffle0003 = 0b00000011; + public const byte MMShuffle0010 = 0b00000100; + public const byte MMShuffle0011 = 0b00000101; + public const byte MMShuffle0012 = 0b00000110; + public const byte MMShuffle0013 = 0b00000111; + public const byte MMShuffle0020 = 0b00001000; + public const byte MMShuffle0021 = 0b00001001; + public const byte MMShuffle0022 = 0b00001010; + public const byte MMShuffle0023 = 0b00001011; + public const byte MMShuffle0030 = 0b00001100; + public const byte MMShuffle0031 = 0b00001101; + public const byte MMShuffle0032 = 0b00001110; + public const byte MMShuffle0033 = 0b00001111; + public const byte MMShuffle0100 = 0b00010000; + public const byte MMShuffle0101 = 0b00010001; + public const byte MMShuffle0102 = 0b00010010; + public const byte MMShuffle0103 = 0b00010011; + public const byte MMShuffle0110 = 0b00010100; + public const byte MMShuffle0111 = 0b00010101; + public const byte MMShuffle0112 = 0b00010110; + public const byte MMShuffle0113 = 0b00010111; + public const byte MMShuffle0120 = 0b00011000; + public const byte MMShuffle0121 = 0b00011001; + public const byte MMShuffle0122 = 0b00011010; + public const byte MMShuffle0123 = 0b00011011; + public const byte MMShuffle0130 = 0b00011100; + public const byte MMShuffle0131 = 0b00011101; + public const byte MMShuffle0132 = 0b00011110; + public const byte MMShuffle0133 = 0b00011111; + public const byte MMShuffle0200 = 0b00100000; + public const byte MMShuffle0201 = 0b00100001; + public const byte MMShuffle0202 = 0b00100010; + public const byte MMShuffle0203 = 0b00100011; + public const byte MMShuffle0210 = 0b00100100; + public const byte MMShuffle0211 = 0b00100101; + public const byte MMShuffle0212 = 0b00100110; + public const byte MMShuffle0213 = 0b00100111; + public const byte MMShuffle0220 = 0b00101000; + public const byte MMShuffle0221 = 0b00101001; + public const byte MMShuffle0222 = 0b00101010; + public const byte MMShuffle0223 = 0b00101011; + public const byte MMShuffle0230 = 0b00101100; + public const byte MMShuffle0231 = 0b00101101; + public const byte MMShuffle0232 = 0b00101110; + public const byte MMShuffle0233 = 0b00101111; + public const byte MMShuffle0300 = 0b00110000; + public const byte MMShuffle0301 = 0b00110001; + public const byte MMShuffle0302 = 0b00110010; + public const byte MMShuffle0303 = 0b00110011; + public const byte MMShuffle0310 = 0b00110100; + public const byte MMShuffle0311 = 0b00110101; + public const byte MMShuffle0312 = 0b00110110; + public const byte MMShuffle0313 = 0b00110111; + public const byte MMShuffle0320 = 0b00111000; + public const byte MMShuffle0321 = 0b00111001; + public const byte MMShuffle0322 = 0b00111010; + public const byte MMShuffle0323 = 0b00111011; + public const byte MMShuffle0330 = 0b00111100; + public const byte MMShuffle0331 = 0b00111101; + public const byte MMShuffle0332 = 0b00111110; + public const byte MMShuffle0333 = 0b00111111; + public const byte MMShuffle1000 = 0b01000000; + public const byte MMShuffle1001 = 0b01000001; + public const byte MMShuffle1002 = 0b01000010; + public const byte MMShuffle1003 = 0b01000011; + public const byte MMShuffle1010 = 0b01000100; + public const byte MMShuffle1011 = 0b01000101; + public const byte MMShuffle1012 = 0b01000110; + public const byte MMShuffle1013 = 0b01000111; + public const byte MMShuffle1020 = 0b01001000; + public const byte MMShuffle1021 = 0b01001001; + public const byte MMShuffle1022 = 0b01001010; + public const byte MMShuffle1023 = 0b01001011; + public const byte MMShuffle1030 = 0b01001100; + public const byte MMShuffle1031 = 0b01001101; + public const byte MMShuffle1032 = 0b01001110; + public const byte MMShuffle1033 = 0b01001111; + public const byte MMShuffle1100 = 0b01010000; + public const byte MMShuffle1101 = 0b01010001; + public const byte MMShuffle1102 = 0b01010010; + public const byte MMShuffle1103 = 0b01010011; + public const byte MMShuffle1110 = 0b01010100; + public const byte MMShuffle1111 = 0b01010101; + public const byte MMShuffle1112 = 0b01010110; + public const byte MMShuffle1113 = 0b01010111; + public const byte MMShuffle1120 = 0b01011000; + public const byte MMShuffle1121 = 0b01011001; + public const byte MMShuffle1122 = 0b01011010; + public const byte MMShuffle1123 = 0b01011011; + public const byte MMShuffle1130 = 0b01011100; + public const byte MMShuffle1131 = 0b01011101; + public const byte MMShuffle1132 = 0b01011110; + public const byte MMShuffle1133 = 0b01011111; + public const byte MMShuffle1200 = 0b01100000; + public const byte MMShuffle1201 = 0b01100001; + public const byte MMShuffle1202 = 0b01100010; + public const byte MMShuffle1203 = 0b01100011; + public const byte MMShuffle1210 = 0b01100100; + public const byte MMShuffle1211 = 0b01100101; + public const byte MMShuffle1212 = 0b01100110; + public const byte MMShuffle1213 = 0b01100111; + public const byte MMShuffle1220 = 0b01101000; + public const byte MMShuffle1221 = 0b01101001; + public const byte MMShuffle1222 = 0b01101010; + public const byte MMShuffle1223 = 0b01101011; + public const byte MMShuffle1230 = 0b01101100; + public const byte MMShuffle1231 = 0b01101101; + public const byte MMShuffle1232 = 0b01101110; + public const byte MMShuffle1233 = 0b01101111; + public const byte MMShuffle1300 = 0b01110000; + public const byte MMShuffle1301 = 0b01110001; + public const byte MMShuffle1302 = 0b01110010; + public const byte MMShuffle1303 = 0b01110011; + public const byte MMShuffle1310 = 0b01110100; + public const byte MMShuffle1311 = 0b01110101; + public const byte MMShuffle1312 = 0b01110110; + public const byte MMShuffle1313 = 0b01110111; + public const byte MMShuffle1320 = 0b01111000; + public const byte MMShuffle1321 = 0b01111001; + public const byte MMShuffle1322 = 0b01111010; + public const byte MMShuffle1323 = 0b01111011; + public const byte MMShuffle1330 = 0b01111100; + public const byte MMShuffle1331 = 0b01111101; + public const byte MMShuffle1332 = 0b01111110; + public const byte MMShuffle1333 = 0b01111111; + public const byte MMShuffle2000 = 0b10000000; + public const byte MMShuffle2001 = 0b10000001; + public const byte MMShuffle2002 = 0b10000010; + public const byte MMShuffle2003 = 0b10000011; + public const byte MMShuffle2010 = 0b10000100; + public const byte MMShuffle2011 = 0b10000101; + public const byte MMShuffle2012 = 0b10000110; + public const byte MMShuffle2013 = 0b10000111; + public const byte MMShuffle2020 = 0b10001000; + public const byte MMShuffle2021 = 0b10001001; + public const byte MMShuffle2022 = 0b10001010; + public const byte MMShuffle2023 = 0b10001011; + public const byte MMShuffle2030 = 0b10001100; + public const byte MMShuffle2031 = 0b10001101; + public const byte MMShuffle2032 = 0b10001110; + public const byte MMShuffle2033 = 0b10001111; + public const byte MMShuffle2100 = 0b10010000; + public const byte MMShuffle2101 = 0b10010001; + public const byte MMShuffle2102 = 0b10010010; + public const byte MMShuffle2103 = 0b10010011; + public const byte MMShuffle2110 = 0b10010100; + public const byte MMShuffle2111 = 0b10010101; + public const byte MMShuffle2112 = 0b10010110; + public const byte MMShuffle2113 = 0b10010111; + public const byte MMShuffle2120 = 0b10011000; + public const byte MMShuffle2121 = 0b10011001; + public const byte MMShuffle2122 = 0b10011010; + public const byte MMShuffle2123 = 0b10011011; + public const byte MMShuffle2130 = 0b10011100; + public const byte MMShuffle2131 = 0b10011101; + public const byte MMShuffle2132 = 0b10011110; + public const byte MMShuffle2133 = 0b10011111; + public const byte MMShuffle2200 = 0b10100000; + public const byte MMShuffle2201 = 0b10100001; + public const byte MMShuffle2202 = 0b10100010; + public const byte MMShuffle2203 = 0b10100011; + public const byte MMShuffle2210 = 0b10100100; + public const byte MMShuffle2211 = 0b10100101; + public const byte MMShuffle2212 = 0b10100110; + public const byte MMShuffle2213 = 0b10100111; + public const byte MMShuffle2220 = 0b10101000; + public const byte MMShuffle2221 = 0b10101001; + public const byte MMShuffle2222 = 0b10101010; + public const byte MMShuffle2223 = 0b10101011; + public const byte MMShuffle2230 = 0b10101100; + public const byte MMShuffle2231 = 0b10101101; + public const byte MMShuffle2232 = 0b10101110; + public const byte MMShuffle2233 = 0b10101111; + public const byte MMShuffle2300 = 0b10110000; + public const byte MMShuffle2301 = 0b10110001; + public const byte MMShuffle2302 = 0b10110010; + public const byte MMShuffle2303 = 0b10110011; + public const byte MMShuffle2310 = 0b10110100; + public const byte MMShuffle2311 = 0b10110101; + public const byte MMShuffle2312 = 0b10110110; + public const byte MMShuffle2313 = 0b10110111; + public const byte MMShuffle2320 = 0b10111000; + public const byte MMShuffle2321 = 0b10111001; + public const byte MMShuffle2322 = 0b10111010; + public const byte MMShuffle2323 = 0b10111011; + public const byte MMShuffle2330 = 0b10111100; + public const byte MMShuffle2331 = 0b10111101; + public const byte MMShuffle2332 = 0b10111110; + public const byte MMShuffle2333 = 0b10111111; + public const byte MMShuffle3000 = 0b11000000; + public const byte MMShuffle3001 = 0b11000001; + public const byte MMShuffle3002 = 0b11000010; + public const byte MMShuffle3003 = 0b11000011; + public const byte MMShuffle3010 = 0b11000100; + public const byte MMShuffle3011 = 0b11000101; + public const byte MMShuffle3012 = 0b11000110; + public const byte MMShuffle3013 = 0b11000111; + public const byte MMShuffle3020 = 0b11001000; + public const byte MMShuffle3021 = 0b11001001; + public const byte MMShuffle3022 = 0b11001010; + public const byte MMShuffle3023 = 0b11001011; + public const byte MMShuffle3030 = 0b11001100; + public const byte MMShuffle3031 = 0b11001101; + public const byte MMShuffle3032 = 0b11001110; + public const byte MMShuffle3033 = 0b11001111; + public const byte MMShuffle3100 = 0b11010000; + public const byte MMShuffle3101 = 0b11010001; + public const byte MMShuffle3102 = 0b11010010; + public const byte MMShuffle3103 = 0b11010011; + public const byte MMShuffle3110 = 0b11010100; + public const byte MMShuffle3111 = 0b11010101; + public const byte MMShuffle3112 = 0b11010110; + public const byte MMShuffle3113 = 0b11010111; + public const byte MMShuffle3120 = 0b11011000; + public const byte MMShuffle3121 = 0b11011001; + public const byte MMShuffle3122 = 0b11011010; + public const byte MMShuffle3123 = 0b11011011; + public const byte MMShuffle3130 = 0b11011100; + public const byte MMShuffle3131 = 0b11011101; + public const byte MMShuffle3132 = 0b11011110; + public const byte MMShuffle3133 = 0b11011111; + public const byte MMShuffle3200 = 0b11100000; + public const byte MMShuffle3201 = 0b11100001; + public const byte MMShuffle3202 = 0b11100010; + public const byte MMShuffle3203 = 0b11100011; + public const byte MMShuffle3210 = 0b11100100; + public const byte MMShuffle3211 = 0b11100101; + public const byte MMShuffle3212 = 0b11100110; + public const byte MMShuffle3213 = 0b11100111; + public const byte MMShuffle3220 = 0b11101000; + public const byte MMShuffle3221 = 0b11101001; + public const byte MMShuffle3222 = 0b11101010; + public const byte MMShuffle3223 = 0b11101011; + public const byte MMShuffle3230 = 0b11101100; + public const byte MMShuffle3231 = 0b11101101; + public const byte MMShuffle3232 = 0b11101110; + public const byte MMShuffle3233 = 0b11101111; + public const byte MMShuffle3300 = 0b11110000; + public const byte MMShuffle3301 = 0b11110001; + public const byte MMShuffle3302 = 0b11110010; + public const byte MMShuffle3303 = 0b11110011; + public const byte MMShuffle3310 = 0b11110100; + public const byte MMShuffle3311 = 0b11110101; + public const byte MMShuffle3312 = 0b11110110; + public const byte MMShuffle3313 = 0b11110111; + public const byte MMShuffle3320 = 0b11111000; + public const byte MMShuffle3321 = 0b11111001; + public const byte MMShuffle3322 = 0b11111010; + public const byte MMShuffle3323 = 0b11111011; + public const byte MMShuffle3330 = 0b11111100; + public const byte MMShuffle3331 = 0b11111101; + public const byte MMShuffle3332 = 0b11111110; + public const byte MMShuffle3333 = 0b11111111; + + [MethodImpl(InliningOptions.ShortMethod)] + public static byte MMShuffle(byte p3, byte p2, byte p1, byte p0) + => (byte)((p3 << 6) | (p2 << 4) | (p1 << 2) | p0); + + [MethodImpl(InliningOptions.ShortMethod)] + public static void MMShuffleSpan(ref Span span, byte control) + { + InverseMMShuffle( + control, + out uint p3, + out uint p2, + out uint p1, + out uint p0); + + ref byte spanBase = ref MemoryMarshal.GetReference(span); + + for (nuint i = 0; i < (uint)span.Length; i += 4) + { + Unsafe.Add(ref spanBase, i + 0) = (byte)(p0 + i); + Unsafe.Add(ref spanBase, i + 1) = (byte)(p1 + i); + Unsafe.Add(ref spanBase, i + 2) = (byte)(p2 + i); + Unsafe.Add(ref spanBase, i + 3) = (byte)(p3 + i); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void MMShuffleSpan(ref Span span, byte control) + { + InverseMMShuffle( + control, + out uint p3, + out uint p2, + out uint p1, + out uint p0); + + ref int spanBase = ref MemoryMarshal.GetReference(span); + + for (nuint i = 0; i < (uint)span.Length; i += 4) + { + Unsafe.Add(ref spanBase, i + 0) = (int)(p0 + i); + Unsafe.Add(ref spanBase, i + 1) = (int)(p1 + i); + Unsafe.Add(ref spanBase, i + 2) = (int)(p2 + i); + Unsafe.Add(ref spanBase, i + 3) = (int)(p3 + i); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void InverseMMShuffle( + byte control, + out uint p3, + out uint p2, + out uint p1, + out uint p0) + { + p3 = (uint)((control >> 6) & 0x3); + p2 = (uint)((control >> 4) & 0x3); + p1 = (uint)((control >> 2) & 0x3); + p0 = (uint)((control >> 0) & 0x3); + } + } + } +} diff --git a/ImageSharp/Common/Helpers/SimdUtils.cs b/ImageSharp/Common/Helpers/SimdUtils.cs new file mode 100644 index 0000000..1185c06 --- /dev/null +++ b/ImageSharp/Common/Helpers/SimdUtils.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp { + /// + /// Various extension and utility methods for and utilizing SIMD capabilities + /// + internal static partial class SimdUtils + { + /// + /// Gets a value indicating whether code is being JIT-ed to AVX2 instructions + /// where both float and integer registers are of size 256 byte. + /// + public static bool HasVector8 { get; } = + Vector.IsHardwareAccelerated && Vector.Count == 8 && Vector.Count == 8; + + /// + /// Transform all scalars in 'v' in a way that converting them to would have rounding semantics. + /// + /// The vector + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector4 PseudoRound(this Vector4 v) + { + Vector4 sign = Numerics.Clamp(v, new Vector4(-1), new Vector4(1)); + + return v + (sign * 0.5f); + } + + /// + /// Rounds all values in 'v' to the nearest integer following semantics. + /// + /// The vector + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector FastRound(this Vector v) + { + // .NET9+ has a built-in method for this Vector.Round + if (Avx2.IsSupported && Vector.Count == Vector256.Count) + { + ref Vector256 v256 = ref Unsafe.As, Vector256>(ref v); + Vector256 vRound = Avx.RoundToNearestInteger(v256); + return Unsafe.As, Vector>(ref vRound); + } + + if (Sse41.IsSupported && Vector.Count == Vector128.Count) + { + ref Vector128 v128 = ref Unsafe.As, Vector128>(ref v); + Vector128 vRound = Sse41.RoundToNearestInteger(v128); + return Unsafe.As, Vector>(ref vRound); + } + + if (AdvSimd.IsSupported && Vector.Count == Vector128.Count) + { + ref Vector128 v128 = ref Unsafe.As, Vector128>(ref v); + Vector128 vRound = AdvSimd.RoundToNearest(v128); + return Unsafe.As, Vector>(ref vRound); + } + + // https://github.com/g-truc/glm/blob/master/glm/simd/common.h#L11 + Vector sign = v & new Vector(-0F); + Vector val_2p23_f32 = sign | new Vector(8388608F); + + val_2p23_f32 = (v + val_2p23_f32) - val_2p23_f32; + return val_2p23_f32 | sign; + } + + [Conditional("DEBUG")] + private static void DebugVerifySpanInput(ReadOnlySpan source, Span dest, int shouldBeDivisibleBy) + { + DebugGuard.IsTrue(source.Length == dest.Length, nameof(source), "Input spans must be of same length!"); + DebugGuard.IsTrue( + Numerics.ModuloP2(dest.Length, shouldBeDivisibleBy) == 0, + nameof(source), + $"length should be divisible by {shouldBeDivisibleBy}!"); + } + + [Conditional("DEBUG")] + private static void DebugVerifySpanInput(ReadOnlySpan source, Span destination, int shouldBeDivisibleBy) + { + DebugGuard.IsTrue(source.Length == destination.Length, nameof(source), "Input spans must be of same length!"); + DebugGuard.IsTrue( + Numerics.ModuloP2(destination.Length, shouldBeDivisibleBy) == 0, + nameof(source), + $"length should be divisible by {shouldBeDivisibleBy}!"); + } + + private struct ByteTuple4 + { + public byte V0; + public byte V1; + public byte V2; + public byte V3; + } + } +} diff --git a/ImageSharp/Common/Helpers/TestHelpers.cs b/ImageSharp/Common/Helpers/TestHelpers.cs new file mode 100644 index 0000000..9b20aeb --- /dev/null +++ b/ImageSharp/Common/Helpers/TestHelpers.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Common.Helpers { + /// + /// Internal utilities intended to be only used in tests. + /// + internal static class TestHelpers + { + /// + /// This constant is useful to verify the target framework ImageSharp has been built against. + /// Only intended to be used in tests! + /// + internal const string ImageSharpBuiltAgainst = +#if NETCOREAPP3_1 + "netcoreapp3.1"; +#elif NETCOREAPP2_1 + "netcoreapp2.1"; +#elif NETSTANDARD2_1 + "netstandard2.1"; +#elif NETSTANDARD2_0 + "netstandard2.0"; +#elif NETSTANDARD1_3 + "netstandard1.3"; +#else + "net472"; +#endif + } +} diff --git a/ImageSharp/Common/Helpers/TolerantMath.cs b/ImageSharp/Common/Helpers/TolerantMath.cs new file mode 100644 index 0000000..ec47973 --- /dev/null +++ b/ImageSharp/Common/Helpers/TolerantMath.cs @@ -0,0 +1,105 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp { + /// + /// Implements basic math operations using tolerant comparison + /// whenever an equality check is needed. + /// + internal readonly struct TolerantMath + { + private readonly double epsilon; + + private readonly double negEpsilon; + + /// + /// A read-only default instance for using 1e-8 as epsilon. + /// It is a field so it can be passed as an 'in' parameter. + /// Does not necessarily fit all use cases! + /// + public static readonly TolerantMath Default = new(1e-8); + + public TolerantMath(double epsilon) + { + DebugGuard.MustBeGreaterThan(epsilon, 0, nameof(epsilon)); + + this.epsilon = epsilon; + this.negEpsilon = -epsilon; + } + + /// + /// == 0 + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsZero(double a) => a > this.negEpsilon && a < this.epsilon; + + /// + /// > 0 + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsPositive(double a) => a > this.epsilon; + + /// + /// < 0 + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsNegative(double a) => a < this.negEpsilon; + + /// + /// == + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool AreEqual(double a, double b) => this.IsZero(a - b); + + /// + /// > + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsGreater(double a, double b) => a > b + this.epsilon; + + /// + /// < + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsLess(double a, double b) => a < b - this.epsilon; + + /// + /// >= + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsGreaterOrEqual(double a, double b) => a >= b - this.epsilon; + + /// + /// <= + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsLessOrEqual(double a, double b) => b >= a - this.epsilon; + + [MethodImpl(InliningOptions.ShortMethod)] + public double Ceiling(double a) + { + double rem = Math.IEEERemainder(a, 1); + if (this.IsZero(rem)) + { + return Math.Round(a); + } + + return Math.Ceiling(a); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public double Floor(double a) + { + double rem = Math.IEEERemainder(a, 1); + if (this.IsZero(rem)) + { + return Math.Round(a); + } + + return Math.Floor(a); + } + } +} diff --git a/ImageSharp/Common/Helpers/UnitConverter.cs b/ImageSharp/Common/Helpers/UnitConverter.cs new file mode 100644 index 0000000..7c19204 --- /dev/null +++ b/ImageSharp/Common/Helpers/UnitConverter.cs @@ -0,0 +1,139 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; + +namespace SixLabors.ImageSharp.Common.Helpers { + /// + /// Contains methods for converting values between unit scales. + /// + internal static class UnitConverter + { + /// + /// The number of centimeters in a meter. + /// 1 cm is equal to exactly 0.01 meters. + /// + private const double CmsInMeter = 1 / 0.01D; + + /// + /// The number of centimeters in an inch. + /// 1 inch is equal to exactly 2.54 centimeters. + /// + private const double CmsInInch = 2.54D; + + /// + /// The number of inches in a meter. + /// 1 inch is equal to exactly 0.0254 meters. + /// + private const double InchesInMeter = 1 / 0.0254D; + + /// + /// The default resolution unit value. + /// + private const PixelResolutionUnit DefaultResolutionUnit = PixelResolutionUnit.PixelsPerInch; + + /// + /// Scales the value from centimeters to meters. + /// + /// The value to scale. + /// The . + [MethodImpl(InliningOptions.ShortMethod)] + public static double CmToMeter(double x) => x * CmsInMeter; + + /// + /// Scales the value from meters to centimeters. + /// + /// The value to scale. + /// The . + [MethodImpl(InliningOptions.ShortMethod)] + public static double MeterToCm(double x) => x / CmsInMeter; + + /// + /// Scales the value from meters to inches. + /// + /// The value to scale. + /// The . + [MethodImpl(InliningOptions.ShortMethod)] + public static double MeterToInch(double x) => x / InchesInMeter; + + /// + /// Scales the value from inches to meters. + /// + /// The value to scale. + /// The . + [MethodImpl(InliningOptions.ShortMethod)] + public static double InchToMeter(double x) => x * InchesInMeter; + + /// + /// Scales the value from centimeters to inches. + /// + /// The value to scale. + /// The . + [MethodImpl(InliningOptions.ShortMethod)] + public static double CmToInch(double x) => x / CmsInInch; + + /// + /// Scales the value from inches to centimeters. + /// + /// The value to scale. + /// The . + [MethodImpl(InliningOptions.ShortMethod)] + public static double InchToCm(double x) => x * CmsInInch; + + /// + /// Converts an to a . + /// + /// The EXIF profile containing the value. + /// The + [MethodImpl(InliningOptions.ShortMethod)] + public static PixelResolutionUnit ExifProfileToResolutionUnit(ExifProfile profile) + { + if (profile.TryGetValue(ExifTag.ResolutionUnit, out IExifValue? resolution)) + { + // EXIF is 1, 2, 3 so we minus "1" off the result. + return (PixelResolutionUnit)(byte)(resolution.Value - 1); + } + + return DefaultResolutionUnit; + } + + /// + /// Gets the exif profile resolution values. + /// + /// The resolution unit. + /// The horizontal resolution value. + /// The vertical resolution value. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public static ExifResolutionValues GetExifResolutionValues(PixelResolutionUnit unit, double horizontal, double vertical) + { + switch (unit) + { + case PixelResolutionUnit.AspectRatio: + case PixelResolutionUnit.PixelsPerInch: + case PixelResolutionUnit.PixelsPerCentimeter: + break; + case PixelResolutionUnit.PixelsPerMeter: + + unit = PixelResolutionUnit.PixelsPerCentimeter; + horizontal = MeterToCm(horizontal); + vertical = MeterToCm(vertical); + + break; + default: + unit = PixelResolutionUnit.PixelsPerInch; + break; + } + + ushort exifUnit = (ushort)(unit + 1); + if (unit == PixelResolutionUnit.AspectRatio) + { + return new ExifResolutionValues(exifUnit, null, null); + } + + return new ExifResolutionValues(exifUnit, horizontal, vertical); + } + } +} diff --git a/ImageSharp/Common/Helpers/Vector128Utilities.cs b/ImageSharp/Common/Helpers/Vector128Utilities.cs new file mode 100644 index 0000000..235aeb0 --- /dev/null +++ b/ImageSharp/Common/Helpers/Vector128Utilities.cs @@ -0,0 +1,1362 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.Wasm; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.Common.Helpers { + /// + /// Defines utility methods for that have either: + /// + /// Not yet been normalized in the runtime. + /// Produce codegen that is poorly optimized by the runtime. + /// + /// Should only be used if the intrinsics are available. + /// +#pragma warning disable SA1649 // File name should match first type name + internal static class Vector128_ +#pragma warning restore SA1649 // File name should match first type name + { + /// + /// Average packed unsigned 8-bit integers in and , and store the results. + /// + /// + /// The first vector containing packed unsigned 8-bit integers to average. + /// + /// + /// The second vector containing packed unsigned 8-bit integers to average. + /// + /// + /// A vector containing the average of the packed unsigned 8-bit integers + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Average(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.Average(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.FusedAddRoundedHalving(left, right); + } + + // Account for potential 9th bit to ensure correct rounded result. + return Vector128.Narrow( + (Vector128.WidenLower(left) + Vector128.WidenLower(right) + Vector128.One) >> 1, + (Vector128.WidenUpper(left) + Vector128.WidenUpper(right) + Vector128.One) >> 1); + } + + /// + /// Creates a new vector by selecting values from an input vector using the control. + /// + /// The input vector from which values are selected. + /// The shuffle control byte. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 ShuffleNative(Vector128 vector, [ConstantExpected] byte control) + { + if (Sse.IsSupported) + { + return Sse.Shuffle(vector, vector, control); + } + + // Don't use InverseMMShuffle here as we want to avoid the cast. + Vector128 indices = Vector128.Create( + control & 0x3, + (control >> 2) & 0x3, + (control >> 4) & 0x3, + (control >> 6) & 0x3); + + return Vector128.Shuffle(vector, indices); + } + + /// + /// Creates a new vector by selecting values from an input vector using the control. + /// + /// The input vector from which values are selected. + /// The shuffle control byte. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 ShuffleNative(Vector128 vector, [ConstantExpected] byte control) + { + // Don't use InverseMMShuffle here as we want to avoid the cast. + Vector128 indices = Vector128.Create( + control & 0x3, + (control >> 2) & 0x3, + (control >> 4) & 0x3, + (control >> 6) & 0x3); + + return Vector128.Shuffle(vector, indices); + } + + /// + /// Shuffle 16-bit integers in the high 64 bits of using the control in . + /// Store the results in the high 64 bits of the destination, with the low 64 bits being copied from . + /// + /// The input vector containing packed 16-bit integers to shuffle. + /// The shuffle control byte. + /// + /// A vector containing the shuffled 16-bit integers in the high 64 bits, with the low 64 bits copied from . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 ShuffleHigh(Vector128 value, [ConstantExpected] byte control) + { + if (Sse2.IsSupported) + { + return Sse2.ShuffleHigh(value, control); + } + + // Don't use InverseMMShuffle here as we want to avoid the cast. + Vector128 indices = Vector128.Create( + 0, + 1, + 2, + 3, + (short)((control & 0x3) + 4), + (short)(((control >> 2) & 0x3) + 4), + (short)(((control >> 4) & 0x3) + 4), + (short)(((control >> 6) & 0x3) + 4)); + + return Vector128.Shuffle(value, indices); + } + + /// + /// Shuffle 16-bit integers in the low 64 bits of using the control in . + /// Store the results in the low 64 bits of the destination, with the high 64 bits being copied from . + /// + /// The input vector containing packed 16-bit integers to shuffle. + /// The shuffle control byte. + /// + /// A vector containing the shuffled 16-bit integers in the low 64 bits, with the high 64 bits copied from . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 ShuffleLow(Vector128 value, [ConstantExpected] byte control) + { + if (Sse2.IsSupported) + { + return Sse2.ShuffleLow(value, control); + } + + // Don't use InverseMMShuffle here as we want to avoid the cast. + Vector128 indices = Vector128.Create( + (short)(control & 0x3), + (short)((control >> 2) & 0x3), + (short)((control >> 4) & 0x3), + (short)((control >> 6) & 0x3), + 4, + 5, + 6, + 7); + + return Vector128.Shuffle(value, indices); + } + + /// + /// Creates a new vector by selecting values from an input vector using a set of indices. + /// + /// + /// The input vector from which values are selected. + /// + /// The per-element indices used to select a value from . + /// + /// + /// A new vector containing the values from selected by the given . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 ShuffleNative(Vector128 vector, Vector128 indices) + { + // For x64 we use the SSSE3 shuffle intrinsic to avoid additional instructions. 3 vs 1. + if (Ssse3.IsSupported) + { + return Ssse3.Shuffle(vector, indices); + } + + // For ARM and WASM, codegen will be optimal. + // We don't throw for x86/x64 so we should never use this method without + // checking for support. + return Vector128.Shuffle(vector, indices); + } + + /// + /// Shifts a 128-bit value right by a specified number of bytes while shifting in zeros. + /// + /// The value to shift. + /// The number of bytes to shift by. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 ShiftRightBytesInVector(Vector128 value, [ConstantExpected(Max = (byte)15)] byte numBytes) + { + if (Sse2.IsSupported) + { + return Sse2.ShiftRightLogical128BitLane(value, numBytes); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.ExtractVector128(value, Vector128.Zero, numBytes); + } + + return Vector128.Shuffle(value, Vector128.Create((byte)0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15) + Vector128.Create(numBytes)); + } + + /// + /// Shifts a 128-bit value left by a specified number of bytes while shifting in zeros. + /// + /// The value to shift. + /// The number of bytes to shift by. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 ShiftLeftBytesInVector(Vector128 value, [ConstantExpected(Max = (byte)15)] byte numBytes) + { + if (Sse2.IsSupported) + { + return Sse2.ShiftLeftLogical128BitLane(value, numBytes); + } + + if (AdvSimd.IsSupported) + { +#pragma warning disable CA1857 // A constant is expected for the parameter + return AdvSimd.ExtractVector128(Vector128.Zero, value, (byte)(Vector128.Count - numBytes)); +#pragma warning restore CA1857 // A constant is expected for the parameter + } + + return Vector128.Shuffle(value, Vector128.Create((byte)0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15) - Vector128.Create(numBytes)); + } + + /// + /// Shift packed 16-bit integers in left by while + /// shifting in zeros, and store the results + /// + /// The vector containing packed 16-bit integers to shift. + /// The number of bits to shift left. + /// + /// A vector containing the packed 16-bit integers shifted left by , with zeros shifted in. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 ShiftLeftLogical(Vector128 value, [ConstantExpected] byte count) + { + // Zero lanes where count >= 16 to match SSE2 + if (count >= 16) + { + return Vector128.Zero; + } + + return value << count; + } + + /// + /// Right aligns elements of two source 128-bit values depending on bits in a mask. + /// + /// The left hand source vector. + /// The right hand source vector. + /// An 8-bit mask used for the operation. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 AlignRight(Vector128 left, Vector128 right, [ConstantExpected(Max = (byte)15)] byte mask) + { + if (Ssse3.IsSupported) + { + return Ssse3.AlignRight(left, right, mask); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.ExtractVector128(right, left, mask); + } + +#pragma warning disable CA1857 // A constant is expected for the parameter + return ShiftLeftBytesInVector(left, (byte)(Vector128.Count - mask)) | ShiftRightBytesInVector(right, mask); +#pragma warning restore CA1857 // A constant is expected for the parameter + } + + /// + /// Performs a conversion from a 128-bit vector of 4 single-precision floating-point values to a 128-bit vector of 4 signed 32-bit integer values. + /// Rounding is equivalent to . + /// + /// The value to convert. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 ConvertToInt32RoundToEven(Vector128 vector) + { + if (Sse2.IsSupported) + { + return Sse2.ConvertToVector128Int32(vector); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.ConvertToInt32RoundToEven(vector); + } + + if (PackedSimd.IsSupported) + { + return PackedSimd.ConvertToInt32Saturate(PackedSimd.RoundToNearest(vector)); + } + + Vector128 sign = vector & Vector128.Create(-0F); + Vector128 val_2p23_f32 = sign | Vector128.Create(8388608F); + + val_2p23_f32 = (vector + val_2p23_f32) - val_2p23_f32; + return Vector128.ConvertToInt32(val_2p23_f32 | sign); + } + + /// + /// Rounds all values in to the nearest integer + /// following semantics. + /// + /// The vector + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 RoundToNearestInteger(Vector128 vector) + { + if (Sse41.IsSupported) + { + return Sse41.RoundToNearestInteger(vector); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.RoundToNearest(vector); + } + + if (PackedSimd.IsSupported) + { + return PackedSimd.RoundToNearest(vector); + } + + Vector128 sign = vector & Vector128.Create(-0F); + Vector128 val_2p23_f32 = sign | Vector128.Create(8388608F); + + val_2p23_f32 = (vector + val_2p23_f32) - val_2p23_f32; + return val_2p23_f32 | sign; + } + + /// + /// Performs a multiplication and an addition of the . + /// + /// ret = (vm0 * vm1) + va + /// The vector to add to the intermediate result. + /// The first vector to multiply. + /// The second vector to multiply. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 MultiplyAdd( + Vector128 va, + Vector128 vm0, + Vector128 vm1) + { + if (Fma.IsSupported) + { + return Fma.MultiplyAdd(vm1, vm0, va); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.FusedMultiplyAdd(va, vm0, vm1); + } + + return va + (vm0 * vm1); + } + + /// + /// Packs signed 16-bit integers to unsigned 8-bit integers and saturates. + /// + /// The left hand source vector. + /// The right hand source vector. + /// The . + public static Vector128 PackUnsignedSaturate(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.PackUnsignedSaturate(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.ExtractNarrowingSaturateUnsignedUpper(AdvSimd.ExtractNarrowingSaturateUnsignedLower(left), right); + } + + if (PackedSimd.IsSupported) + { + return PackedSimd.ConvertNarrowingSaturateUnsigned(left, right); + } + + Vector128 min = Vector128.Create((short)byte.MinValue); + Vector128 max = Vector128.Create((short)byte.MaxValue); + Vector128 lefClamped = Clamp(left, min, max).AsUInt16(); + Vector128 rightClamped = Clamp(right, min, max).AsUInt16(); + return Vector128.Narrow(lefClamped, rightClamped); + } + + /// + /// Packs signed 32-bit integers to unsigned 16-bit integers and saturates. + /// + /// The left hand source vector. + /// The right hand source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 PackUnsignedSaturate(Vector128 left, Vector128 right) + { + if (Sse41.IsSupported) + { + return Sse41.PackUnsignedSaturate(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.ExtractNarrowingSaturateUnsignedUpper(AdvSimd.ExtractNarrowingSaturateUnsignedLower(left), right); + } + + if (PackedSimd.IsSupported) + { + return PackedSimd.ConvertNarrowingSaturateUnsigned(left, right); + } + + Vector128 min = Vector128.Create((int)ushort.MinValue); + Vector128 max = Vector128.Create((int)ushort.MaxValue); + Vector128 lefClamped = Clamp(left, min, max).AsUInt32(); + Vector128 rightClamped = Clamp(right, min, max).AsUInt32(); + return Vector128.Narrow(lefClamped, rightClamped); + } + + /// + /// Packs signed 32-bit integers to signed 16-bit integers and saturates. + /// + /// The left hand source vector. + /// The right hand source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 PackSignedSaturate(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.PackSignedSaturate(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.ExtractNarrowingSaturateUpper(AdvSimd.ExtractNarrowingSaturateLower(left), right); + } + + if (PackedSimd.IsSupported) + { + return PackedSimd.ConvertNarrowingSaturateSigned(left, right); + } + + Vector128 min = Vector128.Create((int)short.MinValue); + Vector128 max = Vector128.Create((int)short.MaxValue); + Vector128 lefClamped = Clamp(left, min, max); + Vector128 rightClamped = Clamp(right, min, max); + return Vector128.Narrow(lefClamped, rightClamped); + } + + /// + /// Packs signed 16-bit integers to signed 8-bit integers and saturates. + /// + /// The left hand source vector. + /// The right hand source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 PackSignedSaturate(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.PackSignedSaturate(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.ExtractNarrowingSaturateUpper(AdvSimd.ExtractNarrowingSaturateLower(left), right); + } + + if (PackedSimd.IsSupported) + { + return PackedSimd.ConvertNarrowingSaturateSigned(left, right); + } + + Vector128 min = Vector128.Create((short)sbyte.MinValue); + Vector128 max = Vector128.Create((short)sbyte.MaxValue); + Vector128 lefClamped = Clamp(left, min, max); + Vector128 rightClamped = Clamp(right, min, max); + return Vector128.Narrow(lefClamped, rightClamped); + } + + /// + /// Restricts a vector between a minimum and a maximum value. + /// + /// The type of the elements in the vector. + /// The vector to restrict. + /// The minimum value. + /// The maximum value. + /// The restricted . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Clamp(Vector128 value, Vector128 min, Vector128 max) + => Vector128.Min(Vector128.Max(value, min), max); + + /// + /// Multiply packed signed 16-bit integers in and , producing + /// intermediate signed 32-bit integers. Horizontally add adjacent pairs of intermediate 32-bit integers, and + /// pack the results. + /// + /// + /// The first vector containing packed signed 16-bit integers to multiply and add. + /// + /// + /// The second vector containing packed signed 16-bit integers to multiply and add. + /// + /// + /// A vector containing the results of multiplying and adding adjacent pairs of packed signed 16-bit integers + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 MultiplyAddAdjacent(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.MultiplyAddAdjacent(left, right); + } + + if (AdvSimd.IsSupported) + { + Vector128 prodLo = AdvSimd.MultiplyWideningLower(left.GetLower(), right.GetLower()); + Vector128 prodHi = AdvSimd.MultiplyWideningUpper(left, right); + + if (AdvSimd.Arm64.IsSupported) + { + return AdvSimd.Arm64.AddPairwise(prodLo, prodHi); + } + + Vector64 v0 = AdvSimd.AddPairwise(prodLo.GetLower(), prodLo.GetUpper()); + Vector64 v1 = AdvSimd.AddPairwise(prodHi.GetLower(), prodHi.GetUpper()); + return Vector128.Create(v0, v1); + } + + { + // Widen each half of the short vectors into two int vectors + (Vector128 leftLo, Vector128 leftHi) = Vector128.Widen(left); + (Vector128 rightLo, Vector128 rightHi) = Vector128.Widen(right); + + // Elementwise multiply: each int lane now holds the full 32-bit product + Vector128 prodLo = leftLo * rightLo; + Vector128 prodHi = leftHi * rightHi; + + // Extract the low and high parts of the products shuffling them to form a result we can add together. + // Use out-of-bounds to zero out the unused lanes. + Vector128 v0 = Vector128.Shuffle(prodLo, Vector128.Create(0, 2, 8, 8)); + Vector128 v1 = Vector128.Shuffle(prodHi, Vector128.Create(8, 8, 0, 2)); + Vector128 v2 = Vector128.Shuffle(prodLo, Vector128.Create(1, 3, 8, 8)); + Vector128 v3 = Vector128.Shuffle(prodHi, Vector128.Create(8, 8, 1, 3)); + + return v0 + v1 + v2 + v3; + } + } + + /// + /// Horizontally add adjacent pairs of 16-bit integers in and , and + /// pack the signed 16-bit results. + /// + /// + /// The first vector containing packed signed 16-bit integers to add. + /// + /// + /// The second vector containing packed signed 16-bit integers to add. + /// + /// + /// A vector containing the results of horizontally adding adjacent pairs of packed signed 16-bit integers + /// + public static Vector128 HorizontalAdd(Vector128 left, Vector128 right) + { + if (Ssse3.IsSupported) + { + return Ssse3.HorizontalAdd(left, right); + } + + if (AdvSimd.Arm64.IsSupported) + { + return AdvSimd.Arm64.AddPairwise(left, right); + } + + if (AdvSimd.IsSupported) + { + Vector128 v0 = AdvSimd.AddPairwiseWidening(left); + Vector128 v1 = AdvSimd.AddPairwiseWidening(right); + + return Vector128.Narrow(v0, v1); + } + + { + // Extract the low and high parts of the products shuffling them to form a result we can add together. + // Use out-of-bounds to zero out the unused lanes. + Vector128 even = Vector128.Create(0, 2, 4, 6, 8, 8, 8, 8); + Vector128 odd = Vector128.Create(1, 3, 5, 7, 8, 8, 8, 8); + Vector128 v0 = Vector128.Shuffle(right, even); + Vector128 v1 = Vector128.Shuffle(right, odd); + Vector128 v2 = Vector128.Shuffle(left, even); + Vector128 v3 = Vector128.Shuffle(left, odd); + + return v0 + v1 + v2 + v3; + } + } + + /// + /// Multiply the packed 16-bit integers in and , producing + /// intermediate 32-bit integers, and store the high 16 bits of the intermediate integers in the result. + /// + /// + /// The first vector containing packed 16-bit integers to multiply. + /// + /// + /// The second vector containing packed 16-bit integers to multiply. + /// + /// + /// A vector containing the high 16 bits of the products of the packed 16-bit integers + /// from and . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 MultiplyHigh(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.MultiplyHigh(left, right); + } + + if (AdvSimd.IsSupported) + { + Vector128 prodLo = AdvSimd.MultiplyWideningLower(left.GetLower(), right.GetLower()); + Vector128 prodHi = AdvSimd.MultiplyWideningUpper(left, right); + + prodLo >>= 16; + prodHi >>= 16; + + return Vector128.Narrow(prodLo, prodHi); + } + + { + // Widen each half of the short vectors into two int vectors + (Vector128 leftLo, Vector128 leftHi) = Vector128.Widen(left); + (Vector128 rightLo, Vector128 rightHi) = Vector128.Widen(right); + + // Elementwise multiply: each int lane now holds the full 32-bit product + Vector128 prodLo = leftLo * rightLo; + Vector128 prodHi = leftHi * rightHi; + + // Arithmetic shift right by 16 bits to extract the high word + prodLo >>= 16; + prodHi >>= 16; + + // Narrow the two int vectors back into one short vector + return Vector128.Narrow(prodLo, prodHi); + } + } + + /// + /// Multiply the packed 16-bit unsigned integers in and , producing + /// intermediate unsigned 32-bit integers, and store the high 16 bits of the intermediate integers in the result. + /// + /// + /// The first vector containing packed 16-bit unsigned integers to multiply. + /// + /// + /// The second vector containing packed 16-bit unsigned integers to multiply. + /// + /// + /// A vector containing the high 16 bits of the products of the packed 16-bit unsigned integers + /// from and . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 MultiplyHigh(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.MultiplyHigh(left, right); + } + + if (AdvSimd.IsSupported) + { + Vector128 prodLo = AdvSimd.MultiplyWideningLower(left.GetLower(), right.GetLower()); + Vector128 prodHi = AdvSimd.MultiplyWideningUpper(left, right); + + prodLo >>= 16; + prodHi >>= 16; + + return Vector128.Narrow(prodLo, prodHi); + } + + { + // Widen each half of the short vectors into two uint vectors + (Vector128 leftLo, Vector128 leftHi) = Vector128.Widen(left); + (Vector128 rightLo, Vector128 rightHi) = Vector128.Widen(right); + + // Elementwise multiply: each int lane now holds the full 32-bit product + Vector128 prodLo = leftLo * rightLo; + Vector128 prodHi = leftHi * rightHi; + + // Arithmetic shift right by 16 bits to extract the high word + prodLo >>= 16; + prodHi >>= 16; + + // Narrow the two int vectors back into one short vector + return Vector128.Narrow(prodLo, prodHi); + } + } + + /// + /// Unpack and interleave 64-bit integers from the high half of and + /// and store the results in the result. + /// + /// + /// The first vector containing packed 64-bit integers to unpack from the high half. + /// + /// + /// The second vector containing packed 64-bit integers to unpack from the high half. + /// + /// + /// A vector containing the unpacked and interleaved 64-bit integers from the high + /// halves of and . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 UnpackHigh(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.UnpackHigh(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.Arm64.ZipHigh(left, right); + } + + return Vector128.Create(left.GetUpper(), right.GetUpper()); + } + + /// + /// Unpack and interleave 64-bit integers from the low half of and + /// and store the results in the result. + /// + /// + /// The first vector containing packed 64-bit integers to unpack from the low half. + /// + /// + /// The second vector containing packed 64-bit integers to unpack from the low half. + /// + /// + /// A vector containing the unpacked and interleaved 64-bit integers from the low + /// halves of and . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 UnpackLow(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.UnpackLow(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.Arm64.ZipLow(left, right); + } + + return Vector128.Create(left.GetLower(), right.GetLower()); + } + + /// + /// Unpack and interleave 32-bit integers from the high half of and + /// and store the results in the result. + /// + /// + /// The first vector containing packed 32-bit integers to unpack from the high half. + /// + /// + /// The second vector containing packed 32-bit integers to unpack from the high half. + /// + /// + /// A vector containing the unpacked and interleaved 32-bit integers from the high + /// halves of and . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 UnpackHigh(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.UnpackHigh(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.Arm64.ZipHigh(left, right); + } + + Vector128 unpacked = Vector128.Create(left.GetUpper(), right.GetUpper()); + return Vector128.Shuffle(unpacked, Vector128.Create(0, 2, 1, 3)); + } + + /// + /// Unpack and interleave 32-bit integers from the low half of and + /// and store the results in the result. + /// + /// + /// The first vector containing packed 32-bit integers to unpack from the low half. + /// + /// + /// The second vector containing packed 32-bit integers to unpack from the low half. + /// + /// + /// A vector containing the unpacked and interleaved 32-bit integers from the low + /// halves of and . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 UnpackLow(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.UnpackLow(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.Arm64.ZipLow(left, right); + } + + Vector128 unpacked = Vector128.Create(left.GetLower(), right.GetLower()); + return Vector128.Shuffle(unpacked, Vector128.Create(0, 2, 1, 3)); + } + + /// + /// Unpack and interleave 16-bit integers from the high half of and + /// and store the results in the result. + /// + /// + /// The first vector containing packed 16-bit integers to unpack from the high half. + /// + /// + /// The second vector containing packed 16-bit integers to unpack from the high half. + /// + /// + /// A vector containing the unpacked and interleaved 16-bit integers from the high + /// halves of and . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 UnpackHigh(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.UnpackHigh(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.Arm64.ZipHigh(left, right); + } + + Vector128 unpacked = Vector128.Create(left.GetUpper(), right.GetUpper()); + return Vector128.Shuffle(unpacked, Vector128.Create(0, 4, 1, 5, 2, 6, 3, 7)); + } + + /// + /// Unpack and interleave 16-bit integers from the low half of and + /// and store the results in the result. + /// + /// + /// The first vector containing packed 16-bit integers to unpack from the low half. + /// + /// + /// The second vector containing packed 16-bit integers to unpack from the low half. + /// + /// + /// A vector containing the unpacked and interleaved 16-bit integers from the low + /// halves of and . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 UnpackLow(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.UnpackLow(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.Arm64.ZipLow(left, right); + } + + Vector128 unpacked = Vector128.Create(left.GetLower(), right.GetLower()); + return Vector128.Shuffle(unpacked, Vector128.Create(0, 4, 1, 5, 2, 6, 3, 7)); + } + + /// + /// Unpack and interleave 8-bit integers from the high half of and + /// and store the results in the result. + /// + /// + /// The first vector containing packed 8-bit integers to unpack from the high half. + /// + /// + /// The second vector containing packed 8-bit integers to unpack from the high half. + /// + /// + /// A vector containing the unpacked and interleaved 8-bit integers from the high + /// halves of and . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 UnpackHigh(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.UnpackHigh(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.Arm64.ZipHigh(left, right); + } + + Vector128 unpacked = Vector128.Create(left.GetUpper(), right.GetUpper()); + return Vector128.Shuffle(unpacked, Vector128.Create((byte)0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15)); + } + + /// + /// Unpack and interleave 8-bit integers from the low half of and + /// and store the results in the result. + /// + /// + /// The first vector containing packed 8-bit integers to unpack from the low half. + /// + /// + /// The second vector containing packed 8-bit integers to unpack from the low half. + /// + /// + /// A vector containing the unpacked and interleaved 8-bit integers from the low + /// halves of and . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 UnpackLow(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.UnpackLow(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.Arm64.ZipLow(left, right); + } + + Vector128 unpacked = Vector128.Create(left.GetLower(), right.GetLower()); + return Vector128.Shuffle(unpacked, Vector128.Create((byte)0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15)); + } + + /// + /// Unpack and interleave 8-bit signed integers from the high half of and + /// and store the results in the result. + /// + /// + /// The first vector containing packed 8-bit signed integers to unpack from the high half. + /// + /// + /// The second vector containing packed 8-bit signed integers to unpack from the high half. + /// + /// + /// A vector containing the unpacked and interleaved 8-bit signed integers from the high + /// halves of and . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 UnpackHigh(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.UnpackHigh(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.Arm64.ZipHigh(left, right); + } + + Vector128 unpacked = Vector128.Create(left.GetUpper(), right.GetUpper()); + return Vector128.Shuffle(unpacked, Vector128.Create(0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15)); + } + + /// + /// Unpack and interleave 8-bit signed integers from the low half of and + /// and store the results in the result. + /// + /// + /// The first vector containing packed 8-bit signed integers to unpack from the low half. + /// + /// + /// The second vector containing packed 8-bit signed integers to unpack from the low half. + /// + /// + /// A vector containing the unpacked and interleaved 8-bit signed integers from the low + /// halves of and . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 UnpackLow(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.UnpackLow(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.Arm64.ZipLow(left, right); + } + + Vector128 unpacked = Vector128.Create(left.GetLower(), right.GetLower()); + return Vector128.Shuffle(unpacked, Vector128.Create(0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15)); + } + + /// + /// Subtract packed signed 16-bit integers in from packed signed 16-bit integers + /// in using saturation, and store the results. + /// + /// + /// The first vector containing packed signed 16-bit integers to subtract from. + /// + /// + /// The second vector containing packed signed 16-bit integers to subtract. + /// + /// + /// A vector containing the results of subtracting packed signed 16-bit integers + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 SubtractSaturate(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.SubtractSaturate(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.SubtractSaturate(left, right); + } + + if (PackedSimd.IsSupported) + { + return PackedSimd.SubtractSaturate(left, right); + } + + // Widen inputs to 32-bit signed + (Vector128 leftLo, Vector128 leftHi) = Vector128.Widen(left); + (Vector128 rightLo, Vector128 rightHi) = Vector128.Widen(right); + + // Subtract + Vector128 diffLo = leftLo - rightLo; + Vector128 diffHi = leftHi - rightHi; + + // Clamp to signed 16-bit range + Vector128 min = Vector128.Create((int)short.MinValue); + Vector128 max = Vector128.Create((int)short.MaxValue); + + diffLo = Clamp(diffLo, min, max); + diffHi = Clamp(diffHi, min, max); + + // Narrow back to 16 bit signed. + return Vector128.Narrow(diffLo, diffHi); + } + + /// + /// Subtract packed unsigned 16-bit integers in from packed unsigned 16-bit integers + /// in using saturation, and store the results. + /// + /// + /// The first vector containing packed unsigned 16-bit integers to subtract from. + /// + /// + /// The second vector containing packed unsigned 16-bit integers to subtract. + /// + /// + /// A vector containing the results of subtracting packed unsigned 16-bit integers + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 SubtractSaturate(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.SubtractSaturate(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.SubtractSaturate(left, right); + } + + if (PackedSimd.IsSupported) + { + return PackedSimd.SubtractSaturate(left, right); + } + + // Widen inputs to 32-bit signed + (Vector128 leftLo, Vector128 leftHi) = Vector128.Widen(left); + (Vector128 rightLo, Vector128 rightHi) = Vector128.Widen(right); + + // Subtract + Vector128 diffLo = leftLo - rightLo; + Vector128 diffHi = leftHi - rightHi; + + // Clamp to signed 16-bit range + Vector128 min = Vector128.Create((uint)ushort.MinValue); + Vector128 max = Vector128.Create((uint)ushort.MaxValue); + + diffLo = Clamp(diffLo, min, max); + diffHi = Clamp(diffHi, min, max); + + // Narrow back to 16 bit signed. + return Vector128.Narrow(diffLo, diffHi); + } + + /// + /// Add packed unsigned 8-bit integers in to packed unsigned 8-bit integers + /// in using saturation, and store the results. + /// + /// + /// The first vector containing packed unsigned 8-bit integers to add to. + /// + /// + /// The second vector containing packed unsigned 8-bit integers to add. + /// + /// + /// A vector containing the results of adding packed unsigned 8-bit integers + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 AddSaturate(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.AddSaturate(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.AddSaturate(left, right); + } + + if (PackedSimd.IsSupported) + { + return PackedSimd.AddSaturate(left, right); + } + + // Widen inputs to 16-bit + (Vector128 leftLo, Vector128 leftHi) = Vector128.Widen(left); + (Vector128 rightLo, Vector128 rightHi) = Vector128.Widen(right); + + // Add + Vector128 sumLo = leftLo + rightLo; + Vector128 sumHi = leftHi + rightHi; + + // Clamp to signed 8-bit range + Vector128 max = Vector128.Create((ushort)byte.MaxValue); + + sumLo = Clamp(sumLo, Vector128.Zero, max); + sumHi = Clamp(sumHi, Vector128.Zero, max); + + // Narrow back to bytes + return Vector128.Narrow(sumLo, sumHi); + } + + /// + /// Add packed unsigned 16-bit integers in to packed unsigned 16-bit integers + /// in using saturation, and store the results. + /// + /// + /// The first vector containing packed unsigned 16-bit integers to add to. + /// + /// + /// The second vector containing packed unsigned 16-bit integers to add. + /// + /// + /// A vector containing the results of adding packed unsigned 16-bit integers + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 AddSaturate(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.AddSaturate(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.AddSaturate(left, right); + } + + if (PackedSimd.IsSupported) + { + return PackedSimd.AddSaturate(left, right); + } + + // Widen inputs to 32-bit + (Vector128 leftLo, Vector128 leftHi) = Vector128.Widen(left); + (Vector128 rightLo, Vector128 rightHi) = Vector128.Widen(right); + + // Add + Vector128 sumLo = leftLo + rightLo; + Vector128 sumHi = leftHi + rightHi; + + // Clamp to signed 16-bit range + Vector128 max = Vector128.Create((uint)ushort.MaxValue); + + sumLo = Clamp(sumLo, Vector128.Zero, max); + sumHi = Clamp(sumHi, Vector128.Zero, max); + + // Narrow back to 16 bit unsigned. + return Vector128.Narrow(sumLo, sumHi); + } + + /// + /// Subtract packed unsigned 8-bit integers in from packed unsigned 8-bit integers + /// in using saturation, and store the results. + /// + /// + /// The first vector containing packed unsigned 8-bit integers to subtract from. + /// + /// + /// The second vector containing packed unsigned 8-bit integers to subtract. + /// + /// + /// A vector containing the results of subtracting packed unsigned 8-bit integers + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 SubtractSaturate(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.SubtractSaturate(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.SubtractSaturate(left, right); + } + + if (PackedSimd.IsSupported) + { + return PackedSimd.SubtractSaturate(left, right); + } + + // Widen inputs to 16-bit + (Vector128 leftLo, Vector128 leftHi) = Vector128.Widen(left); + (Vector128 rightLo, Vector128 rightHi) = Vector128.Widen(right); + + // Subtract + Vector128 diffLo = leftLo - rightLo; + Vector128 diffHi = leftHi - rightHi; + + // Clamp to signed 8-bit range + Vector128 max = Vector128.Create((ushort)byte.MaxValue); + + diffLo = Clamp(diffLo, Vector128.Zero, max); + diffHi = Clamp(diffHi, Vector128.Zero, max); + + // Narrow back to bytes + return Vector128.Narrow(diffLo, diffHi); + } + + /// + /// Add packed unsigned 8-bit integers in from packed unsigned 8-bit integers + /// in using saturation, and store the results. + /// + /// + /// The first vector containing packed unsigned 8-bit integers to add to. + /// + /// + /// The second vector containing packed unsigned 8-bit integers to add. + /// + /// + /// A vector containing the results of adding packed unsigned 8-bit integers + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 AddSaturate(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.AddSaturate(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.AddSaturate(left, right); + } + + if (PackedSimd.IsSupported) + { + return PackedSimd.AddSaturate(left, right); + } + + // Widen inputs to 16-bit + (Vector128 leftLo, Vector128 leftHi) = Vector128.Widen(left); + (Vector128 rightLo, Vector128 rightHi) = Vector128.Widen(right); + + // Add + Vector128 sumLo = leftLo + rightLo; + Vector128 sumHi = leftHi + rightHi; + + // Clamp to signed 8-bit range + Vector128 min = Vector128.Create((short)sbyte.MinValue); + Vector128 max = Vector128.Create((short)sbyte.MaxValue); + + sumLo = Clamp(sumLo, min, max); + sumHi = Clamp(sumHi, min, max); + + // Narrow back to signed bytes + return Vector128.Narrow(sumLo, sumHi); + } + + /// + /// Subtract packed signed 8-bit integers in from packed signed 8-bit integers + /// in using saturation, and store the results. + /// + /// + /// The first vector containing packed signed 8-bit integers to subtract from. + /// + /// + /// The second vector containing packed signed 8-bit integers to subtract. + /// + /// + /// A vector containing the results of subtracting packed signed 8-bit integers + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 SubtractSaturate(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return Sse2.SubtractSaturate(left, right); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.SubtractSaturate(left, right); + } + + if (PackedSimd.IsSupported) + { + return PackedSimd.SubtractSaturate(left, right); + } + + // Widen inputs to 16-bit + (Vector128 leftLo, Vector128 leftHi) = Vector128.Widen(left); + (Vector128 rightLo, Vector128 rightHi) = Vector128.Widen(right); + + // Subtract + Vector128 diffLo = leftLo - rightLo; + Vector128 diffHi = leftHi - rightHi; + + // Clamp to signed 8-bit range + Vector128 min = Vector128.Create((short)sbyte.MinValue); + Vector128 max = Vector128.Create((short)sbyte.MaxValue); + + diffLo = Clamp(diffLo, min, max); + diffHi = Clamp(diffHi, min, max); + + // Narrow back to signed bytes + return Vector128.Narrow(diffLo, diffHi); + } + } +} diff --git a/ImageSharp/Common/Helpers/Vector256Utilities.cs b/ImageSharp/Common/Helpers/Vector256Utilities.cs new file mode 100644 index 0000000..e50c206 --- /dev/null +++ b/ImageSharp/Common/Helpers/Vector256Utilities.cs @@ -0,0 +1,486 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.Common.Helpers { + /// + /// Defines utility methods for that have either: + /// + /// Not yet been normalized in the runtime. + /// Produce codegen that is poorly optimized by the runtime. + /// + /// Should only be used if the intrinsics are available. + /// +#pragma warning disable SA1649 // File name should match first type name + internal static class Vector256_ +#pragma warning restore SA1649 // File name should match first type name + { + /// + /// Creates a new vector by selecting values from an input vector using a set of indices. + /// + /// The input vector from which values are selected. + /// The shuffle control byte. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ShuffleNative(Vector256 vector, [ConstantExpected] byte control) + => Avx.Shuffle(vector, vector, control); + + /// + /// Creates a new vector by selecting values from an input vector using a set of indices. + /// + /// The input vector from which values are selected. + /// + /// The per-element indices used to select a value from . + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ShufflePerLane(Vector256 vector, Vector256 indices) + { + if (Avx2.IsSupported) + { + return Avx2.Shuffle(vector, indices); + } + + Vector128 indicesLo = indices.GetLower(); + Vector128 lower = Vector128_.ShuffleNative(vector.GetLower(), indicesLo); + Vector128 upper = Vector128_.ShuffleNative(vector.GetUpper(), indicesLo); + return Vector256.Create(lower, upper); + } + + /// + /// Performs a conversion from a 256-bit vector of 8 single-precision floating-point values to a 256-bit vector of 8 signed 32-bit integer values. + /// Rounding is equivalent to . + /// + /// The value to convert. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ConvertToInt32RoundToEven(Vector256 vector) + { + if (Avx.IsSupported) + { + return Avx.ConvertToVector256Int32(vector); + } + + Vector256 sign = vector & Vector256.Create(-0F); + Vector256 val_2p23_f32 = sign | Vector256.Create(8388608F); + + val_2p23_f32 = (vector + val_2p23_f32) - val_2p23_f32; + return Vector256.ConvertToInt32(val_2p23_f32 | sign); + } + + /// + /// Rounds all values in to the nearest integer + /// following semantics. + /// + /// The vector + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 RoundToNearestInteger(Vector256 vector) + { + if (Avx.IsSupported) + { + return Avx.RoundToNearestInteger(vector); + } + + Vector256 sign = vector & Vector256.Create(-0F); + Vector256 val_2p23_f32 = sign | Vector256.Create(8388608F); + + val_2p23_f32 = (vector + val_2p23_f32) - val_2p23_f32; + return val_2p23_f32 | sign; + } + + /// + /// Performs a multiplication and an addition of the . + /// + /// ret = (vm0 * vm1) + va + /// The vector to add to the intermediate result. + /// The first vector to multiply. + /// The second vector to multiply. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyAdd( + Vector256 va, + Vector256 vm0, + Vector256 vm1) + { + if (Fma.IsSupported) + { + return Fma.MultiplyAdd(vm0, vm1, va); + } + + return va + (vm0 * vm1); + } + + /// + /// Performs a multiplication and a negated addition of the . + /// + /// ret = va - (vm0 * vm1) + /// The vector to add to the negated intermediate result. + /// The first vector to multiply. + /// The second vector to multiply. + /// The . + [MethodImpl(InliningOptions.ShortMethod)] + public static Vector256 MultiplyAddNegated( + Vector256 va, + Vector256 vm0, + Vector256 vm1) + { + if (Fma.IsSupported) + { + return Fma.MultiplyAddNegated(vm0, vm1, va); + } + + return va - (vm0 * vm1); + } + + /// + /// Performs a multiplication and a subtraction of the . + /// + /// ret = (vm0 * vm1) - vs + /// The vector to subtract from the intermediate result. + /// The first vector to multiply. + /// The second vector to multiply. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplySubtract( + Vector256 vs, + Vector256 vm0, + Vector256 vm1) + { + if (Fma.IsSupported) + { + return Fma.MultiplySubtract(vm1, vm0, vs); + } + + return (vm0 * vm1) - vs; + } + + /// + /// Multiply packed signed 16-bit integers in and , producing + /// intermediate signed 32-bit integers. Horizontally add adjacent pairs of intermediate 32-bit integers, and + /// pack the results. + /// + /// + /// The first vector containing packed signed 16-bit integers to multiply and add. + /// + /// + /// The second vector containing packed signed 16-bit integers to multiply and add. + /// + /// + /// A vector containing the results of multiplying and adding adjacent pairs of packed signed 16-bit integers + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyAddAdjacent(Vector256 left, Vector256 right) + { + if (Avx2.IsSupported) + { + return Avx2.MultiplyAddAdjacent(left, right); + } + + return Vector256.Create( + Vector128_.MultiplyAddAdjacent(left.GetLower(), right.GetLower()), + Vector128_.MultiplyAddAdjacent(left.GetUpper(), right.GetUpper())); + } + + /// + /// Packs signed 32-bit integers to signed 16-bit integers and saturates. + /// + /// The left hand source vector. + /// The right hand source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 PackUnsignedSaturate(Vector256 left, Vector256 right) + { + if (Avx2.IsSupported) + { + return Avx2.PackUnsignedSaturate(left, right); + } + + Vector256 min = Vector256.Create((int)ushort.MinValue); + Vector256 max = Vector256.Create((int)ushort.MaxValue); + Vector256 lefClamped = Clamp(left, min, max).AsUInt32(); + Vector256 rightClamped = Clamp(right, min, max).AsUInt32(); + return Vector256.Narrow(lefClamped, rightClamped); + } + + /// + /// Packs signed 32-bit integers to signed 16-bit integers and saturates. + /// + /// The left hand source vector. + /// The right hand source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 PackSignedSaturate(Vector256 left, Vector256 right) + { + if (Avx2.IsSupported) + { + return Avx2.PackSignedSaturate(left, right); + } + + Vector256 min = Vector256.Create((int)short.MinValue); + Vector256 max = Vector256.Create((int)short.MaxValue); + Vector256 lefClamped = Clamp(left, min, max); + Vector256 rightClamped = Clamp(right, min, max); + return Vector256.Narrow(lefClamped, rightClamped); + } + + /// + /// Packs signed 16-bit integers to signed 8-bit integers and saturates. + /// + /// The left hand source vector. + /// The right hand source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 PackSignedSaturate(Vector256 left, Vector256 right) + { + if (Avx2.IsSupported) + { + return Avx2.PackSignedSaturate(left, right); + } + + Vector256 min = Vector256.Create((short)sbyte.MinValue); + Vector256 max = Vector256.Create((short)sbyte.MaxValue); + Vector256 lefClamped = Clamp(left, min, max); + Vector256 rightClamped = Clamp(right, min, max); + return Vector256.Narrow(lefClamped, rightClamped); + } + + /// + /// Restricts a vector between a minimum and a maximum value. + /// + /// The type of the elements in the vector. + /// The vector to restrict. + /// The minimum value. + /// The maximum value. + /// The restricted . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Clamp(Vector256 value, Vector256 min, Vector256 max) + => Vector256.Min(Vector256.Max(value, min), max); + + /// + /// Widens a to a . + /// + /// The vector to widen. + /// The widened . + public static Vector256 Widen(Vector128 value) + { + if (Avx2.IsSupported) + { + return Avx2.ConvertToVector256Int32(value); + } + + return Vector256.WidenLower(value.ToVector256()); + } + + /// + /// Multiply the packed 16-bit integers in and , producing + /// intermediate 32-bit integers, and store the low 16 bits of the intermediate integers in the result. + /// + /// + /// The first vector containing packed 16-bit integers to multiply. + /// + /// + /// The second vector containing packed 16-bit integers to multiply. + /// + /// + /// A vector containing the low 16 bits of the products of the packed 16-bit integers + /// from and . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyLow(Vector256 left, Vector256 right) + { + if (Avx2.IsSupported) + { + return Avx2.MultiplyLow(left, right); + } + + // Widen each half of the short vectors into two int vectors + (Vector256 leftLower, Vector256 leftUpper) = Vector256.Widen(left); + (Vector256 rightLower, Vector256 rightUpper) = Vector256.Widen(right); + + // Elementwise multiply: each int lane now holds the full 32-bit product + Vector256 prodLo = leftLower * rightLower; + Vector256 prodHi = leftUpper * rightUpper; + + // Narrow the two int vectors back into one short vector + return Vector256.Narrow(prodLo, prodHi); + } + + /// + /// Multiply the packed 16-bit integers in and , producing + /// intermediate 32-bit integers, and store the high 16 bits of the intermediate integers in the result. + /// + /// + /// The first vector containing packed 16-bit integers to multiply. + /// + /// + /// The second vector containing packed 16-bit integers to multiply. + /// + /// + /// A vector containing the high 16 bits of the products of the packed 16-bit integers + /// from and . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyHigh(Vector256 left, Vector256 right) + { + if (Avx2.IsSupported) + { + return Avx2.MultiplyHigh(left, right); + } + + // Widen each half of the short vectors into two int vectors + (Vector256 leftLower, Vector256 leftUpper) = Vector256.Widen(left); + (Vector256 rightLower, Vector256 rightUpper) = Vector256.Widen(right); + + // Elementwise multiply: each int lane now holds the full 32-bit product + Vector256 prodLo = leftLower * rightLower; + Vector256 prodHi = leftUpper * rightUpper; + + // Arithmetic shift right by 16 bits to extract the high word + prodLo >>= 16; + prodHi >>= 16; + + // Narrow the two int vectors back into one short vector + return Vector256.Narrow(prodLo, prodHi); + } + + /// + /// Unpack and interleave 32-bit integers from the low half of and + /// and store the results in the result. + /// + /// + /// The first vector containing packed 32-bit integers to unpack from the low half. + /// + /// + /// The second vector containing packed 32-bit integers to unpack from the low half. + /// + /// + /// A vector containing the unpacked and interleaved 32-bit integers from the low + /// halves of and . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 UnpackLow(Vector256 left, Vector256 right) + { + if (Avx2.IsSupported) + { + return Avx2.UnpackLow(left, right); + } + + Vector128 lo = Vector128_.UnpackLow(left.GetLower(), right.GetLower()); + Vector128 hi = Vector128_.UnpackLow(left.GetUpper(), right.GetUpper()); + + return Vector256.Create(lo, hi); + } + + /// + /// Unpack and interleave 8-bit integers from the high half of and + /// and store the results in the result. + /// + /// + /// The first vector containing packed 8-bit integers to unpack from the high half. + /// + /// + /// The second vector containing packed 8-bit integers to unpack from the high half. + /// + /// + /// A vector containing the unpacked and interleaved 8-bit integers from the high + /// halves of and . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 UnpackHigh(Vector256 left, Vector256 right) + { + if (Avx2.IsSupported) + { + return Avx2.UnpackHigh(left, right); + } + + Vector128 lo = Vector128_.UnpackHigh(left.GetLower(), right.GetLower()); + Vector128 hi = Vector128_.UnpackHigh(left.GetUpper(), right.GetUpper()); + + return Vector256.Create(lo, hi); + } + + /// + /// Unpack and interleave 8-bit integers from the low half of and + /// and store the results in the result. + /// + /// + /// The first vector containing packed 8-bit integers to unpack from the low half. + /// + /// + /// The second vector containing packed 8-bit integers to unpack from the low half. + /// + /// + /// A vector containing the unpacked and interleaved 8-bit integers from the low + /// halves of and . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 UnpackLow(Vector256 left, Vector256 right) + { + if (Avx2.IsSupported) + { + return Avx2.UnpackLow(left, right); + } + + Vector128 lo = Vector128_.UnpackLow(left.GetLower(), right.GetLower()); + Vector128 hi = Vector128_.UnpackLow(left.GetUpper(), right.GetUpper()); + + return Vector256.Create(lo, hi); + } + + /// + /// Subtract packed signed 16-bit integers in from packed signed 16-bit integers + /// in using saturation, and store the results. + /// + /// + /// The first vector containing packed signed 16-bit integers to subtract from. + /// + /// + /// The second vector containing packed signed 16-bit integers to subtract. + /// + /// + /// A vector containing the results of subtracting packed unsigned 16-bit integers + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractSaturate(Vector256 left, Vector256 right) + { + if (Avx2.IsSupported) + { + return Avx2.SubtractSaturate(left, right); + } + + return Vector256.Create( + Vector128_.SubtractSaturate(left.GetLower(), right.GetLower()), + Vector128_.SubtractSaturate(left.GetUpper(), right.GetUpper())); + } + + /// + /// Subtract packed unsigned 8-bit integers in from packed unsigned 8-bit integers + /// in using saturation, and store the results. + /// + /// + /// The first vector containing packed unsigned 8-bit integers to subtract from. + /// + /// + /// The second vector containing packed unsigned 8-bit integers to subtract. + /// + /// + /// A vector containing the results of subtracting packed unsigned 8-bit integers + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractSaturate(Vector256 left, Vector256 right) + { + if (Avx2.IsSupported) + { + return Avx2.SubtractSaturate(left, right); + } + + return Vector256.Create( + Vector128_.SubtractSaturate(left.GetLower(), right.GetLower()), + Vector128_.SubtractSaturate(left.GetUpper(), right.GetUpper())); + } + } +} diff --git a/ImageSharp/Common/Helpers/Vector512Utilities.cs b/ImageSharp/Common/Helpers/Vector512Utilities.cs new file mode 100644 index 0000000..adb7120 --- /dev/null +++ b/ImageSharp/Common/Helpers/Vector512Utilities.cs @@ -0,0 +1,116 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.Common.Helpers { + /// + /// Defines utility methods for that have either: + /// + /// Not yet been normalized in the runtime. + /// Produce codegen that is poorly optimized by the runtime. + /// + /// Should only be used if the intrinsics are available. + /// +#pragma warning disable SA1649 // File name should match first type name + internal static class Vector512_ +#pragma warning restore SA1649 // File name should match first type name + { + /// + /// Creates a new vector by selecting values from an input vector using the control. + /// + /// The input vector from which values are selected. + /// The shuffle control byte. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ShuffleNative(Vector512 vector, [ConstantExpected] byte control) + => Avx512F.Shuffle(vector, vector, control); + + /// + /// Creates a new vector by selecting values from an input vector using a set of indices. + /// + /// The input vector from which values are selected. + /// + /// The per-element indices used to select a value from . + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ShuffleNative(Vector512 vector, Vector512 indices) + { + if (Avx512BW.IsSupported) + { + return Avx512BW.Shuffle(vector, indices); + } + + return Vector512.Shuffle(vector, indices); + } + + /// + /// Performs a conversion from a 512-bit vector of 16 single-precision floating-point values to a 512-bit vector of 16 signed 32-bit integer values. + /// Rounding is equivalent to . + /// + /// The value to convert. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ConvertToInt32RoundToEven(Vector512 vector) + => Avx512F.ConvertToVector512Int32(vector); + + /// + /// Rounds all values in to the nearest integer + /// following semantics. + /// + /// The vector + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 RoundToNearestInteger(Vector512 vector) + + // imm8 = 0b1000: + // imm8[7:4] = 0b0000 -> preserve 0 fractional bits (round to whole numbers) + // imm8[3:0] = 0b1000 -> _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC (round to nearest even, suppress exceptions) + => Avx512F.RoundScale(vector, 0b0000_1000); + + /// + /// Performs a multiplication and an addition of the . + /// + /// ret = (vm0 * vm1) + va + /// The vector to add to the intermediate result. + /// The first vector to multiply. + /// The second vector to multiply. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyAdd( + Vector512 va, + Vector512 vm0, + Vector512 vm1) + => Avx512F.FusedMultiplyAdd(vm0, vm1, va); + + /// + /// Performs a multiplication and a negated addition of the . + /// + /// ret = va - (vm0 * vm1) + /// The vector to add to the negated intermediate result. + /// The first vector to multiply. + /// The second vector to multiply. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyAddNegated( + Vector512 va, + Vector512 vm0, + Vector512 vm1) + => Avx512F.FusedMultiplyAddNegated(vm0, vm1, va); + + /// + /// Restricts a vector between a minimum and a maximum value. + /// + /// The type of the elements in the vector. + /// The vector to restrict. + /// The minimum value. + /// The maximum value. + /// The restricted . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Clamp(Vector512 value, Vector512 min, Vector512 max) + => Vector512.Min(Vector512.Max(value, min), max); + } +} diff --git a/ImageSharp/Common/InlineArray.cs b/ImageSharp/Common/InlineArray.cs new file mode 100644 index 0000000..778981f --- /dev/null +++ b/ImageSharp/Common/InlineArray.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp; + +/// +/// Represents a safe, fixed sized buffer of 4 elements. +/// +[InlineArray(4)] +internal struct InlineArray4 +{ + private T t; +} + +/// +/// Represents a safe, fixed sized buffer of 8 elements. +/// +[InlineArray(8)] +internal struct InlineArray8 +{ + private T t; +} + +/// +/// Represents a safe, fixed sized buffer of 16 elements. +/// +[InlineArray(16)] +internal struct InlineArray16 +{ + private T t; +} + + diff --git a/ImageSharp/Common/InlineArray.tt b/ImageSharp/Common/InlineArray.tt new file mode 100644 index 0000000..6c4f05f --- /dev/null +++ b/ImageSharp/Common/InlineArray.tt @@ -0,0 +1,38 @@ +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp; + +<#GenerateInlineArrays();#> + +<#+ +private static int[] Lengths = [4, 8, 16 ]; + +void GenerateInlineArrays() +{ + foreach (int length in Lengths) + { +#> +/// +/// Represents a safe, fixed sized buffer of <#=length#> elements. +/// +[InlineArray(<#=length#>)] +internal struct InlineArray<#=length#> +{ + private T t; +} + +<#+ + } +} +#> diff --git a/ImageSharp/Compression/Zlib/Adler32.cs b/ImageSharp/Compression/Zlib/Adler32.cs new file mode 100644 index 0000000..d3632a5 --- /dev/null +++ b/ImageSharp/Compression/Zlib/Adler32.cs @@ -0,0 +1,436 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; + +#pragma warning disable IDE0007 // Use implicit type +namespace SixLabors.ImageSharp.Compression.Zlib { + /// + /// Calculates the 32 bit Adler checksum of a given buffer according to + /// RFC 1950. ZLIB Compressed Data Format Specification version 3.3) + /// + internal static class Adler32 + { + /// + /// The default initial seed value of a Adler32 checksum calculation. + /// + public const uint SeedValue = 1U; + + // Largest prime smaller than 65536 + private const uint BASE = 65521; + + // NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1 + private const uint NMAX = 5552; + + private const int MinBufferSize = 64; + + private const int BlockSize = 1 << 5; + + // The C# compiler emits this as a compile-time constant embedded in the PE file. + private static ReadOnlySpan Tap1Tap2 => + [ + 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, // tap1 + 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 // tap2 + ]; + + /// + /// Calculates the Adler32 checksum with the bytes taken from the span. + /// + /// The readonly span of bytes. + /// The . + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Calculate(ReadOnlySpan buffer) + => Calculate(SeedValue, buffer); + + /// + /// Calculates the Adler32 checksum with the bytes taken from the span and seed. + /// + /// The input Adler32 value. + /// The readonly span of bytes. + /// The . + [MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)] + public static uint Calculate(uint adler, ReadOnlySpan buffer) + { + if (buffer.IsEmpty) + { + return adler; + } + + if (Avx2.IsSupported && buffer.Length >= MinBufferSize) + { + return CalculateAvx2(adler, buffer); + } + + if (Ssse3.IsSupported && buffer.Length >= MinBufferSize) + { + return CalculateSse(adler, buffer); + } + + if (AdvSimd.IsSupported) + { + return CalculateArm(adler, buffer); + } + + return CalculateScalar(adler, buffer); + } + + // Based on https://github.com/chromium/chromium/blob/master/third_party/zlib/adler32_simd.c + [MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)] + private static unsafe uint CalculateSse(uint adler, ReadOnlySpan buffer) + { + uint s1 = adler & 0xFFFF; + uint s2 = (adler >> 16) & 0xFFFF; + + // Process the data in blocks. + uint length = (uint)buffer.Length; + uint blocks = length / BlockSize; + length -= blocks * BlockSize; + + fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer)) + { + fixed (byte* tapPtr = &MemoryMarshal.GetReference(Tap1Tap2)) + { + byte* localBufferPtr = bufferPtr; + + // _mm_setr_epi8 on x86 + Vector128 tap1 = Sse2.LoadVector128((sbyte*)tapPtr); + Vector128 tap2 = Sse2.LoadVector128((sbyte*)(tapPtr + 0x10)); + Vector128 zero = Vector128.Zero; + Vector128 ones = Vector128.Create((short)1); + + while (blocks > 0) + { + uint n = NMAX / BlockSize; /* The NMAX constraint. */ + if (n > blocks) + { + n = blocks; + } + + blocks -= n; + + // Process n blocks of data. At most NMAX data bytes can be + // processed before s2 must be reduced modulo BASE. + Vector128 v_ps = Vector128.CreateScalar(s1 * n); + Vector128 v_s2 = Vector128.CreateScalar(s2); + Vector128 v_s1 = Vector128.Zero; + + do + { + // Load 32 input bytes. + Vector128 bytes1 = Sse3.LoadDquVector128(localBufferPtr); + Vector128 bytes2 = Sse3.LoadDquVector128(localBufferPtr + 0x10); + + // Add previous block byte sum to v_ps. + v_ps = Sse2.Add(v_ps, v_s1); + + // Horizontally add the bytes for s1, multiply-adds the + // bytes by [ 32, 31, 30, ... ] for s2. + v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsUInt32()); + Vector128 mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1); + v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones).AsUInt32()); + + v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes2, zero).AsUInt32()); + Vector128 mad2 = Ssse3.MultiplyAddAdjacent(bytes2, tap2); + v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad2, ones).AsUInt32()); + + localBufferPtr += BlockSize; + } + while (--n > 0); + + v_s2 = Sse2.Add(v_s2, Sse2.ShiftLeftLogical(v_ps, 5)); + + // Sum epi32 ints v_s1(s2) and accumulate in s1(s2). + const byte s2301 = 0b1011_0001; // A B C D -> B A D C + const byte s1032 = 0b0100_1110; // A B C D -> C D A B + + v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, s1032)); + + s1 += v_s1.ToScalar(); + + v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, s2301)); + v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, s1032)); + + s2 = v_s2.ToScalar(); + + // Reduce. + s1 %= BASE; + s2 %= BASE; + } + + if (length > 0) + { + HandleLeftOver(localBufferPtr, length, ref s1, ref s2); + } + + return s1 | (s2 << 16); + } + } + } + + // Based on: https://github.com/zlib-ng/zlib-ng/blob/develop/arch/x86/adler32_avx2.c + [MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)] + public static unsafe uint CalculateAvx2(uint adler, ReadOnlySpan buffer) + { + uint s1 = adler & 0xFFFF; + uint s2 = (adler >> 16) & 0xFFFF; + uint length = (uint)buffer.Length; + + fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer)) + { + byte* localBufferPtr = bufferPtr; + + Vector256 zero = Vector256.Zero; + Vector256 dot3v = Vector256.Create((short)1); + Vector256 dot2v = Vector256.Create(32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1); + + // Process n blocks of data. At most NMAX data bytes can be + // processed before s2 must be reduced modulo BASE. + Vector256 vs1 = Vector256.CreateScalar(s1); + Vector256 vs2 = Vector256.CreateScalar(s2); + + while (length >= 32) + { + int k = length < NMAX ? (int)length : (int)NMAX; + k -= k % 32; + length -= (uint)k; + + Vector256 vs10 = vs1; + Vector256 vs3 = Vector256.Zero; + + while (k >= 32) + { + // Load 32 input bytes. + Vector256 block = Avx.LoadVector256(localBufferPtr); + + // Sum of abs diff, resulting in 2 x int32's + Vector256 vs1sad = Avx2.SumAbsoluteDifferences(block, zero); + + vs1 = Avx2.Add(vs1, vs1sad.AsUInt32()); + vs3 = Avx2.Add(vs3, vs10); + + // sum 32 uint8s to 16 shorts. + Vector256 vshortsum2 = Avx2.MultiplyAddAdjacent(block, dot2v); + + // sum 16 shorts to 8 uint32s. + Vector256 vsum2 = Avx2.MultiplyAddAdjacent(vshortsum2, dot3v); + + vs2 = Avx2.Add(vsum2.AsUInt32(), vs2); + vs10 = vs1; + + localBufferPtr += BlockSize; + k -= 32; + } + + // Defer the multiplication with 32 to outside of the loop. + vs3 = Avx2.ShiftLeftLogical(vs3, 5); + vs2 = Avx2.Add(vs2, vs3); + + s1 = (uint)Numerics.EvenReduceSum(vs1.AsInt32()); + s2 = (uint)Numerics.ReduceSum(vs2.AsInt32()); + + s1 %= BASE; + s2 %= BASE; + + vs1 = Vector256.CreateScalar(s1); + vs2 = Vector256.CreateScalar(s2); + } + + if (length > 0) + { + HandleLeftOver(localBufferPtr, length, ref s1, ref s2); + } + + return s1 | (s2 << 16); + } + } + + // Based on: https://github.com/chromium/chromium/blob/master/third_party/zlib/adler32_simd.c + [MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)] + private static unsafe uint CalculateArm(uint adler, ReadOnlySpan buffer) + { + // Split Adler-32 into component sums. + uint s1 = adler & 0xFFFF; + uint s2 = (adler >> 16) & 0xFFFF; + uint length = (uint)buffer.Length; + + // Process the data in blocks. + long blocks = length / BlockSize; + length -= (uint)(blocks * BlockSize); + fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer)) + { + byte* localBufferPtr = bufferPtr; + + while (blocks != 0) + { + uint n = NMAX / BlockSize; + if (n > blocks) + { + n = (uint)blocks; + } + + blocks -= n; + + // Process n blocks of data. At most nMax data bytes can be + // processed before s2 must be reduced modulo Base. + Vector128 vs1 = Vector128.Zero; + Vector128 vs2 = vs1.WithElement(3, s1 * n); + Vector128 vColumnSum1 = Vector128.Zero; + Vector128 vColumnSum2 = Vector128.Zero; + Vector128 vColumnSum3 = Vector128.Zero; + Vector128 vColumnSum4 = Vector128.Zero; + + do + { + // Load 32 input bytes. + Vector128 bytes1 = AdvSimd.LoadVector128(localBufferPtr).AsUInt16(); + Vector128 bytes2 = AdvSimd.LoadVector128(localBufferPtr + 0x10).AsUInt16(); + + // Add previous block byte sum to v_s2. + vs2 = AdvSimd.Add(vs2, vs1); + + // Horizontally add the bytes for s1. + vs1 = AdvSimd.AddPairwiseWideningAndAdd( + vs1.AsUInt32(), + AdvSimd.AddPairwiseWideningAndAdd(AdvSimd.AddPairwiseWidening(bytes1.AsByte()).AsUInt16(), bytes2.AsByte())); + + // Vertically add the bytes for s2. + vColumnSum1 = AdvSimd.AddWideningLower(vColumnSum1, bytes1.GetLower().AsByte()); + vColumnSum2 = AdvSimd.AddWideningLower(vColumnSum2, bytes1.GetUpper().AsByte()); + vColumnSum3 = AdvSimd.AddWideningLower(vColumnSum3, bytes2.GetLower().AsByte()); + vColumnSum4 = AdvSimd.AddWideningLower(vColumnSum4, bytes2.GetUpper().AsByte()); + + localBufferPtr += BlockSize; + } + while (--n > 0); + + vs2 = AdvSimd.ShiftLeftLogical(vs2, 5); + + // Multiply-add bytes by [ 32, 31, 30, ... ] for s2. + vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum1.GetLower(), Vector64.Create((ushort)32, 31, 30, 29)); + vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum1.GetUpper(), Vector64.Create((ushort)28, 27, 26, 25)); + vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum2.GetLower(), Vector64.Create((ushort)24, 23, 22, 21)); + vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum2.GetUpper(), Vector64.Create((ushort)20, 19, 18, 17)); + vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum3.GetLower(), Vector64.Create((ushort)16, 15, 14, 13)); + vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum3.GetUpper(), Vector64.Create((ushort)12, 11, 10, 9)); + vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum4.GetLower(), Vector64.Create((ushort)8, 7, 6, 5)); + vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum4.GetUpper(), Vector64.Create((ushort)4, 3, 2, 1)); + + // Sum epi32 ints v_s1(s2) and accumulate in s1(s2). + Vector64 sum1 = AdvSimd.AddPairwise(vs1.GetLower(), vs1.GetUpper()); + Vector64 sum2 = AdvSimd.AddPairwise(vs2.GetLower(), vs2.GetUpper()); + Vector64 s1s2 = AdvSimd.AddPairwise(sum1, sum2); + + // Store the results. + s1 += AdvSimd.Extract(s1s2, 0); + s2 += AdvSimd.Extract(s1s2, 1); + + // Reduce. + s1 %= BASE; + s2 %= BASE; + } + + if (length > 0) + { + HandleLeftOver(localBufferPtr, length, ref s1, ref s2); + } + + return s1 | (s2 << 16); + } + } + + private static unsafe void HandleLeftOver(byte* localBufferPtr, uint length, ref uint s1, ref uint s2) + { + if (length >= 16) + { + s2 += s1 += localBufferPtr[0]; + s2 += s1 += localBufferPtr[1]; + s2 += s1 += localBufferPtr[2]; + s2 += s1 += localBufferPtr[3]; + s2 += s1 += localBufferPtr[4]; + s2 += s1 += localBufferPtr[5]; + s2 += s1 += localBufferPtr[6]; + s2 += s1 += localBufferPtr[7]; + s2 += s1 += localBufferPtr[8]; + s2 += s1 += localBufferPtr[9]; + s2 += s1 += localBufferPtr[10]; + s2 += s1 += localBufferPtr[11]; + s2 += s1 += localBufferPtr[12]; + s2 += s1 += localBufferPtr[13]; + s2 += s1 += localBufferPtr[14]; + s2 += s1 += localBufferPtr[15]; + + localBufferPtr += 16; + length -= 16; + } + + while (length-- > 0) + { + s2 += s1 += *localBufferPtr++; + } + + if (s1 >= BASE) + { + s1 -= BASE; + } + + s2 %= BASE; + } + + [MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)] + private static unsafe uint CalculateScalar(uint adler, ReadOnlySpan buffer) + { + uint s1 = adler & 0xFFFF; + uint s2 = (adler >> 16) & 0xFFFF; + + fixed (byte* bufferPtr = buffer) + { + byte* localBufferPtr = bufferPtr; + uint length = (uint)buffer.Length; + + while (length > 0) + { + uint k = length < NMAX ? length : NMAX; + length -= k; + + while (k >= 16) + { + s2 += s1 += localBufferPtr[0]; + s2 += s1 += localBufferPtr[1]; + s2 += s1 += localBufferPtr[2]; + s2 += s1 += localBufferPtr[3]; + s2 += s1 += localBufferPtr[4]; + s2 += s1 += localBufferPtr[5]; + s2 += s1 += localBufferPtr[6]; + s2 += s1 += localBufferPtr[7]; + s2 += s1 += localBufferPtr[8]; + s2 += s1 += localBufferPtr[9]; + s2 += s1 += localBufferPtr[10]; + s2 += s1 += localBufferPtr[11]; + s2 += s1 += localBufferPtr[12]; + s2 += s1 += localBufferPtr[13]; + s2 += s1 += localBufferPtr[14]; + s2 += s1 += localBufferPtr[15]; + + localBufferPtr += 16; + k -= 16; + } + + while (k-- > 0) + { + s2 += s1 += *localBufferPtr++; + } + + s1 %= BASE; + s2 %= BASE; + } + + return (s2 << 16) | s1; + } + } + } +} diff --git a/ImageSharp/Compression/Zlib/DeflateCompressionLevel.cs b/ImageSharp/Compression/Zlib/DeflateCompressionLevel.cs new file mode 100644 index 0000000..04bf36c --- /dev/null +++ b/ImageSharp/Compression/Zlib/DeflateCompressionLevel.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Compression.Zlib { + /// + /// Provides enumeration of available deflate compression levels. + /// + public enum DeflateCompressionLevel + { + /// + /// Level 0. Equivalent to . + /// + Level0 = 0, + + /// + /// No compression. Equivalent to . + /// + NoCompression = Level0, + + /// + /// Level 1. Equivalent to . + /// + Level1 = 1, + + /// + /// Best speed compression level. + /// + BestSpeed = Level1, + + /// + /// Level 2. + /// + Level2 = 2, + + /// + /// Level 3. + /// + Level3 = 3, + + /// + /// Level 4. + /// + Level4 = 4, + + /// + /// Level 5. + /// + Level5 = 5, + + /// + /// Level 6. Equivalent to . + /// + Level6 = 6, + + /// + /// The default compression level. Equivalent to . + /// + DefaultCompression = Level6, + + /// + /// Level 7. + /// + Level7 = 7, + + /// + /// Level 8. + /// + Level8 = 8, + + /// + /// Level 9. Equivalent to . + /// + Level9 = 9, + + /// + /// Best compression level. Equivalent to . + /// + BestCompression = Level9, + } +} diff --git a/ImageSharp/Compression/Zlib/DeflateThrowHelper.cs b/ImageSharp/Compression/Zlib/DeflateThrowHelper.cs new file mode 100644 index 0000000..3824a89 --- /dev/null +++ b/ImageSharp/Compression/Zlib/DeflateThrowHelper.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Compression.Zlib { + internal static class DeflateThrowHelper + { + [DoesNotReturn] + public static void ThrowAlreadyFinished() => throw new InvalidOperationException("Finish() already called."); + + [DoesNotReturn] + public static void ThrowAlreadyClosed() => throw new InvalidOperationException("Deflator already closed."); + + [DoesNotReturn] + public static void ThrowUnknownCompression() => throw new InvalidOperationException("Unknown compression function."); + + [DoesNotReturn] + public static void ThrowNotProcessed() => throw new InvalidOperationException("Old input was not completely processed."); + + [DoesNotReturn] + public static void ThrowNull(string name) => throw new ArgumentNullException(name); + + [DoesNotReturn] + public static void ThrowOutOfRange(string name) => throw new ArgumentOutOfRangeException(name); + + [DoesNotReturn] + public static void ThrowHeapViolated() => throw new InvalidOperationException("Huffman heap invariant violated."); + + [DoesNotReturn] + public static void ThrowNoDeflate() => throw new ImageFormatException("Cannot deflate all input."); + } +} diff --git a/ImageSharp/Compression/Zlib/Deflater.cs b/ImageSharp/Compression/Zlib/Deflater.cs new file mode 100644 index 0000000..9807ced --- /dev/null +++ b/ImageSharp/Compression/Zlib/Deflater.cs @@ -0,0 +1,291 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Compression.Zlib { + /// + /// This class compresses input with the deflate algorithm described in RFC 1951. + /// It has several compression levels and three different strategies described below. + /// + internal sealed class Deflater : IDisposable + { + /// + /// The best and slowest compression level. This tries to find very + /// long and distant string repetitions. + /// + public const int BestCompression = 9; + + /// + /// The worst but fastest compression level. + /// + public const int BestSpeed = 1; + + /// + /// The default compression level. + /// + public const int DefaultCompression = -1; + + /// + /// This level won't compress at all but output uncompressed blocks. + /// + public const int NoCompression = 0; + + /// + /// The compression method. This is the only method supported so far. + /// There is no need to use this constant at all. + /// + public const int Deflated = 8; + + /// + /// Compression level. + /// + private int level; + + /// + /// The current state. + /// + private int state; + + private DeflaterEngine engine; + private bool isDisposed; + + private const int IsFlushing = 0x04; + private const int IsFinishing = 0x08; + private const int BusyState = 0x10; + private const int FlushingState = 0x14; + private const int FinishingState = 0x1c; + private const int FinishedState = 0x1e; + private const int ClosedState = 0x7f; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator to use for buffer allocations. + /// The compression level, a value between NoCompression and BestCompression. + /// + /// if level is out of range. + public Deflater(MemoryAllocator memoryAllocator, int level) + { + if (level == DefaultCompression) + { + level = 6; + } + else if (level < NoCompression || level > BestCompression) + { + throw new ArgumentOutOfRangeException(nameof(level)); + } + + // TODO: Possibly provide DeflateStrategy as an option. + this.engine = new DeflaterEngine(memoryAllocator, DeflateStrategy.Default); + + this.SetLevel(level); + this.Reset(); + } + + /// + /// Compression Level as an enum for safer use + /// + public enum CompressionLevel + { + /// + /// The best and slowest compression level. This tries to find very + /// long and distant string repetitions. + /// + BestCompression = Deflater.BestCompression, + + /// + /// The worst but fastest compression level. + /// + BestSpeed = Deflater.BestSpeed, + + /// + /// The default compression level. + /// + DefaultCompression = Deflater.DefaultCompression, + + /// + /// This level won't compress at all but output uncompressed blocks. + /// + NoCompression = Deflater.NoCompression, + + /// + /// The compression method. This is the only method supported so far. + /// There is no need to use this constant at all. + /// + Deflated = Deflater.Deflated + } + + /// + /// Gets a value indicating whetherthe stream was finished and no more output bytes + /// are available. + /// + public bool IsFinished => (this.state == FinishedState) && this.engine.Pending.IsFlushed; + + /// + /// Gets a value indicating whether the input buffer is empty. + /// You should then call setInput(). + /// NOTE: This method can also return true when the stream + /// was finished. + /// + public bool IsNeedingInput => this.engine.NeedsInput(); + + /// + /// Resets the deflater. The deflater acts afterwards as if it was + /// just created with the same compression level and strategy as it + /// had before. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Reset() + { + this.state = BusyState; + this.engine.Pending.Reset(); + this.engine.Reset(); + } + + /// + /// Flushes the current input block. Further calls to Deflate() will + /// produce enough output to inflate everything in the current input + /// block. It is used by DeflaterOutputStream to implement Flush(). + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Flush() => this.state |= IsFlushing; + + /// + /// Finishes the deflater with the current input block. It is an error + /// to give more input after this method was called. This method must + /// be called to force all bytes to be flushed. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Finish() => this.state |= IsFlushing | IsFinishing; + + /// + /// Sets the data which should be compressed next. This should be + /// only called when needsInput indicates that more input is needed. + /// The given byte array should not be changed, before needsInput() returns + /// true again. + /// + /// The buffer containing the input data. + /// The start of the data. + /// The number of data bytes of input. + /// + /// if the buffer was finished or if previous input is still pending. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void SetInput(byte[] input, int offset, int count) + { + if ((this.state & IsFinishing) != 0) + { + DeflateThrowHelper.ThrowAlreadyFinished(); + } + + this.engine.SetInput(input, offset, count); + } + + /// + /// Sets the compression level. There is no guarantee of the exact + /// position of the change, but if you call this when needsInput is + /// true the change of compression level will occur somewhere near + /// before the end of the so far given input. + /// + /// + /// the new compression level. + /// + public void SetLevel(int level) + { + if (level == DefaultCompression) + { + level = 6; + } + else if (level < NoCompression || level > BestCompression) + { + throw new ArgumentOutOfRangeException(nameof(level)); + } + + if (this.level != level) + { + this.level = level; + this.engine.SetLevel(level); + } + } + + /// + /// Deflates the current input block to the given array. + /// + /// Buffer to store the compressed data. + /// Offset into the output array. + /// The maximum number of bytes that may be stored. + /// + /// The number of compressed bytes added to the output, or 0 if either + /// or returns true or length is zero. + /// + public int Deflate(Span output, int offset, int length) + { + int origLength = length; + + if (this.state == ClosedState) + { + DeflateThrowHelper.ThrowAlreadyClosed(); + } + + while (true) + { + int count = this.engine.Pending.Flush(output, offset, length); + offset += count; + length -= count; + + if (length == 0 || this.state == FinishedState) + { + break; + } + + if (!this.engine.Deflate((this.state & IsFlushing) != 0, (this.state & IsFinishing) != 0)) + { + switch (this.state) + { + case BusyState: + // We need more input now + return origLength - length; + + case FlushingState: + if (this.level != NoCompression) + { + // We have to supply some lookahead. 8 bit lookahead + // is needed by the zlib inflater, and we must fill + // the next byte, so that all bits are flushed. + int neededbits = 8 + ((-this.engine.Pending.BitCount) & 7); + while (neededbits > 0) + { + // Write a static tree block consisting solely of an EOF: + this.engine.Pending.WriteBits(2, 10); + neededbits -= 10; + } + } + + this.state = BusyState; + break; + + case FinishingState: + this.engine.Pending.AlignToByte(); + this.state = FinishedState; + break; + } + } + } + + return origLength - length; + } + + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.engine.Dispose(); + this.isDisposed = true; + } + } + } +} diff --git a/ImageSharp/Compression/Zlib/DeflaterConstants.cs b/ImageSharp/Compression/Zlib/DeflaterConstants.cs new file mode 100644 index 0000000..fbc2083 --- /dev/null +++ b/ImageSharp/Compression/Zlib/DeflaterConstants.cs @@ -0,0 +1,148 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// +using System; + +namespace SixLabors.ImageSharp.Compression.Zlib; + +/// +/// This class contains constants used for deflation. +/// +internal static class DeflaterConstants +{ + /// + /// Set to true to enable debugging + /// + public const bool DEBUGGING = false; + + /// + /// Written to Zip file to identify a stored block + /// + public const int STORED_BLOCK = 0; + + /// + /// Identifies static tree in Zip file + /// + public const int STATIC_TREES = 1; + + /// + /// Identifies dynamic tree in Zip file + /// + public const int DYN_TREES = 2; + + /// + /// Header flag indicating a preset dictionary for deflation + /// + public const int PRESET_DICT = 0x20; + + /// + /// Sets internal buffer sizes for Huffman encoding + /// + public const int DEFAULT_MEM_LEVEL = 8; + + /// + /// Internal compression engine constant + /// + public const int MAX_MATCH = 258; + + /// + /// Internal compression engine constant + /// + public const int MIN_MATCH = 3; + + /// + /// Internal compression engine constant + /// + public const int MAX_WBITS = 15; + + /// + /// Internal compression engine constant + /// + public const int WSIZE = 1 << MAX_WBITS; + + /// + /// Internal compression engine constant + /// + public const int WMASK = WSIZE - 1; + + /// + /// Internal compression engine constant + /// + public const int HASH_BITS = DEFAULT_MEM_LEVEL + 7; + + /// + /// Internal compression engine constant + /// + public const int HASH_SIZE = 1 << HASH_BITS; + + /// + /// Internal compression engine constant + /// + public const int HASH_MASK = HASH_SIZE - 1; + + /// + /// Internal compression engine constant + /// + public const int HASH_SHIFT = (HASH_BITS + MIN_MATCH - 1) / MIN_MATCH; + + /// + /// Internal compression engine constant + /// + public const int MIN_LOOKAHEAD = MAX_MATCH + MIN_MATCH + 1; + + /// + /// Internal compression engine constant + /// + public const int MAX_DIST = WSIZE - MIN_LOOKAHEAD; + + /// + /// Internal compression engine constant + /// + public const int PENDING_BUF_SIZE = 1 << (DEFAULT_MEM_LEVEL + 8); + + /// + /// Internal compression engine constant + /// + public static int MAX_BLOCK_SIZE = Math.Min(65535, PENDING_BUF_SIZE - 5); + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_STORED = 0; + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_FAST = 1; + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_SLOW = 2; + + /// + /// Internal compression engine constant + /// + public static int[] GOOD_LENGTH = [0, 4, 4, 4, 4, 8, 8, 8, 32, 32]; + + /// + /// Internal compression engine constant + /// + public static int[] MAX_LAZY = [0, 4, 5, 6, 4, 16, 16, 32, 128, 258]; + + /// + /// Internal compression engine constant + /// + public static int[] NICE_LENGTH = [0, 8, 16, 32, 16, 32, 128, 128, 258, 258]; + + /// + /// Internal compression engine constant + /// + public static int[] MAX_CHAIN = [0, 4, 8, 32, 16, 32, 128, 256, 1024, 4096]; + + /// + /// Internal compression engine constant + /// + public static int[] COMPR_FUNC = [0, 1, 1, 1, 1, 2, 2, 2, 2, 2]; +} diff --git a/ImageSharp/Compression/Zlib/DeflaterEngine.cs b/ImageSharp/Compression/Zlib/DeflaterEngine.cs new file mode 100644 index 0000000..7a1bb55 --- /dev/null +++ b/ImageSharp/Compression/Zlib/DeflaterEngine.cs @@ -0,0 +1,868 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Compression.Zlib { + /// + /// Strategies for deflater + /// + internal enum DeflateStrategy + { + /// + /// The default strategy + /// + Default = 0, + + /// + /// This strategy will only allow longer string repetitions. It is + /// useful for random data with a small character set. + /// + Filtered = 1, + + /// + /// This strategy will not look for string repetitions at all. It + /// only encodes with Huffman trees (which means, that more common + /// characters get a smaller encoding. + /// + HuffmanOnly = 2 + } + + // DEFLATE ALGORITHM: + // + // The uncompressed stream is inserted into the window array. When + // the window array is full the first half is thrown away and the + // second half is copied to the beginning. + // + // The head array is a hash table. Three characters build a hash value + // and they the value points to the corresponding index in window of + // the last string with this hash. The prev array implements a + // linked list of matches with the same hash: prev[index & WMASK] points + // to the previous index with the same hash. + // + + /// + /// Low level compression engine for deflate algorithm which uses a 32K sliding window + /// with secondary compression from Huffman/Shannon-Fano codes. + /// + internal sealed unsafe class DeflaterEngine : IDisposable + { + private const int TooFar = 4096; + + // Hash index of string to be inserted + private int insertHashIndex; + + private int matchStart; + + // Length of best match + private int matchLen; + + // Set if previous match exists + private bool prevAvailable; + + private int blockStart; + + /// + /// Points to the current character in the window. + /// + private int strstart; + + /// + /// lookahead is the number of characters starting at strstart in + /// window that are valid. + /// So window[strstart] until window[strstart+lookahead-1] are valid + /// characters. + /// + private int lookahead; + + /// + /// The current compression function. + /// + private int compressionFunction; + + /// + /// The input data for compression. + /// + private byte[]? inputBuf; + + /// + /// The offset into inputBuf, where input data starts. + /// + private int inputOff; + + /// + /// The end offset of the input data. + /// + private int inputEnd; + + private readonly DeflateStrategy strategy; + private DeflaterHuffman huffman; + private bool isDisposed; + + /// + /// Hashtable, hashing three characters to an index for window, so + /// that window[index]..window[index+2] have this hash code. + /// Note that the array should really be unsigned short, so you need + /// to and the values with 0xFFFF. + /// + private IMemoryOwner headMemoryOwner; + private MemoryHandle headMemoryHandle; + private readonly Memory head; + private readonly short* pinnedHeadPointer; + + /// + /// prev[index & WMASK] points to the previous index that has the + /// same hash code as the string starting at index. This way + /// entries with the same hash code are in a linked list. + /// Note that the array should really be unsigned short, so you need + /// to and the values with 0xFFFF. + /// + private IMemoryOwner prevMemoryOwner; + private MemoryHandle prevMemoryHandle; + private readonly Memory prev; + private readonly short* pinnedPrevPointer; + + /// + /// This array contains the part of the uncompressed stream that + /// is of relevance. The current character is indexed by strstart. + /// + private IMemoryOwner windowMemoryOwner; + private MemoryHandle windowMemoryHandle; + private readonly Memory window; + private readonly byte* pinnedWindowPointer; + + private int maxChain; + private int maxLazy; + private int niceLength; + private int goodLength; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator to use for buffer allocations. + /// The deflate strategy to use. + public DeflaterEngine(MemoryAllocator memoryAllocator, DeflateStrategy strategy) + { + this.huffman = new DeflaterHuffman(memoryAllocator); + this.Pending = this.huffman.Pending; + this.strategy = strategy; + + // Create pinned pointers to the various buffers to allow indexing + // without bounds checks. + this.windowMemoryOwner = memoryAllocator.Allocate(2 * DeflaterConstants.WSIZE); + this.window = this.windowMemoryOwner.Memory; + this.windowMemoryHandle = this.window.Pin(); + this.pinnedWindowPointer = (byte*)this.windowMemoryHandle.Pointer; + + this.headMemoryOwner = memoryAllocator.Allocate(DeflaterConstants.HASH_SIZE); + this.head = this.headMemoryOwner.Memory; + this.headMemoryHandle = this.head.Pin(); + this.pinnedHeadPointer = (short*)this.headMemoryHandle.Pointer; + + this.prevMemoryOwner = memoryAllocator.Allocate(DeflaterConstants.WSIZE); + this.prev = this.prevMemoryOwner.Memory; + this.prevMemoryHandle = this.prev.Pin(); + this.pinnedPrevPointer = (short*)this.prevMemoryHandle.Pointer; + + // We start at index 1, to avoid an implementation deficiency, that + // we cannot build a repeat pattern at index 0. + this.blockStart = this.strstart = 1; + } + + /// + /// Gets the pending buffer to use. + /// + public DeflaterPendingBuffer Pending { get; } + + /// + /// Deflate drives actual compression of data + /// + /// True to flush input buffers + /// Finish deflation with the current input. + /// Returns true if progress has been made. + public bool Deflate(bool flush, bool finish) + { + bool progress = false; + do + { + this.FillWindow(); + bool canFlush = flush && (this.inputOff == this.inputEnd); + + switch (this.compressionFunction) + { + case DeflaterConstants.DEFLATE_STORED: + progress = this.DeflateStored(canFlush, finish); + break; + + case DeflaterConstants.DEFLATE_FAST: + progress = this.DeflateFast(canFlush, finish); + break; + + case DeflaterConstants.DEFLATE_SLOW: + progress = this.DeflateSlow(canFlush, finish); + break; + + default: + DeflateThrowHelper.ThrowUnknownCompression(); + break; + } + } + while (this.Pending.IsFlushed && progress); // repeat while we have no pending output and progress was made + return progress; + } + + /// + /// Sets input data to be deflated. Should only be called when + /// returns true + /// + /// The buffer containing input data. + /// The offset of the first byte of data. + /// The number of bytes of data to use as input. + public void SetInput(byte[]? buffer, int offset, int count) + { + if (buffer is null) + { + DeflateThrowHelper.ThrowNull(nameof(buffer)); + } + + if (offset < 0) + { + DeflateThrowHelper.ThrowOutOfRange(nameof(offset)); + } + + if (count < 0) + { + DeflateThrowHelper.ThrowOutOfRange(nameof(count)); + } + + if (this.inputOff < this.inputEnd) + { + DeflateThrowHelper.ThrowNotProcessed(); + } + + int end = offset + count; + + // We want to throw an ArgumentOutOfRangeException early. + // The check is very tricky: it also handles integer wrap around. + if ((offset > end) || (end > buffer.Length)) + { + DeflateThrowHelper.ThrowOutOfRange(nameof(count)); + } + + this.inputBuf = buffer; + this.inputOff = offset; + this.inputEnd = end; + } + + /// + /// Determines if more input is needed. + /// + /// Return true if input is needed via SetInput + [MethodImpl(InliningOptions.ShortMethod)] + public bool NeedsInput() => this.inputEnd == this.inputOff; + + /// + /// Reset internal state + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Reset() + { + this.huffman.Reset(); + this.blockStart = this.strstart = 1; + this.lookahead = 0; + this.prevAvailable = false; + this.matchLen = DeflaterConstants.MIN_MATCH - 1; + this.head.Span[..DeflaterConstants.HASH_SIZE].Clear(); + this.prev.Span[..DeflaterConstants.WSIZE].Clear(); + } + + /// + /// Set the deflate level (0-9) + /// + /// The value to set the level to. + public void SetLevel(int level) + { + if (level is < 0 or > 9) + { + DeflateThrowHelper.ThrowOutOfRange(nameof(level)); + } + + this.goodLength = DeflaterConstants.GOOD_LENGTH[level]; + this.maxLazy = DeflaterConstants.MAX_LAZY[level]; + this.niceLength = DeflaterConstants.NICE_LENGTH[level]; + this.maxChain = DeflaterConstants.MAX_CHAIN[level]; + + if (DeflaterConstants.COMPR_FUNC[level] != this.compressionFunction) + { + switch (this.compressionFunction) + { + case DeflaterConstants.DEFLATE_STORED: + if (this.strstart > this.blockStart) + { + this.huffman.FlushStoredBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, false); + this.blockStart = this.strstart; + } + + this.UpdateHash(); + break; + + case DeflaterConstants.DEFLATE_FAST: + if (this.strstart > this.blockStart) + { + this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, false); + this.blockStart = this.strstart; + } + + break; + + case DeflaterConstants.DEFLATE_SLOW: + if (this.prevAvailable) + { + this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xFF); + } + + if (this.strstart > this.blockStart) + { + this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, false); + this.blockStart = this.strstart; + } + + this.prevAvailable = false; + this.matchLen = DeflaterConstants.MIN_MATCH - 1; + break; + } + + this.compressionFunction = DeflaterConstants.COMPR_FUNC[level]; + } + } + + /// + /// Fill the window + /// + public void FillWindow() + { + // If the window is almost full and there is insufficient lookahead, + // move the upper half to the lower one to make room in the upper half. + if (this.strstart >= DeflaterConstants.WSIZE + DeflaterConstants.MAX_DIST) + { + this.SlideWindow(); + } + + // If there is not enough lookahead, but still some input left, read in the input. + if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && this.inputOff < this.inputEnd) + { + int more = (2 * DeflaterConstants.WSIZE) - this.lookahead - this.strstart; + + if (more > this.inputEnd - this.inputOff) + { + more = this.inputEnd - this.inputOff; + } + + ArgumentNullException.ThrowIfNull(this.inputBuf); + + Unsafe.CopyBlockUnaligned( + ref this.window.Span[this.strstart + this.lookahead], + ref this.inputBuf[this.inputOff], + unchecked((uint)more)); + + this.inputOff += more; + this.lookahead += more; + } + + if (this.lookahead >= DeflaterConstants.MIN_MATCH) + { + this.UpdateHash(); + } + } + + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.huffman.Dispose(); + + this.windowMemoryHandle.Dispose(); + this.windowMemoryOwner.Dispose(); + + this.headMemoryHandle.Dispose(); + this.headMemoryOwner.Dispose(); + + this.prevMemoryHandle.Dispose(); + this.prevMemoryOwner.Dispose(); + + this.isDisposed = true; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private void UpdateHash() + { + byte* pinned = this.pinnedWindowPointer; + this.insertHashIndex = (pinned[this.strstart] << DeflaterConstants.HASH_SHIFT) ^ pinned[this.strstart + 1]; + } + + /// + /// Inserts the current string in the head hash and returns the previous + /// value for this hash. + /// + /// The previous hash value + [MethodImpl(InliningOptions.ShortMethod)] + private int InsertString() + { + short match; + int hash = ((this.insertHashIndex << DeflaterConstants.HASH_SHIFT) ^ this.pinnedWindowPointer[this.strstart + (DeflaterConstants.MIN_MATCH - 1)]) & DeflaterConstants.HASH_MASK; + + short* pinnedHead = this.pinnedHeadPointer; + this.pinnedPrevPointer[this.strstart & DeflaterConstants.WMASK] = match = pinnedHead[hash]; + pinnedHead[hash] = unchecked((short)this.strstart); + this.insertHashIndex = hash; + return match & 0xFFFF; + } + + private void SlideWindow() + { + Unsafe.CopyBlockUnaligned( + ref MemoryMarshal.GetReference(this.window.Span), + ref Unsafe.Add(ref MemoryMarshal.GetReference(this.window.Span), DeflaterConstants.WSIZE), + DeflaterConstants.WSIZE); + + this.matchStart -= DeflaterConstants.WSIZE; + this.strstart -= DeflaterConstants.WSIZE; + this.blockStart -= DeflaterConstants.WSIZE; + + // Slide the hash table (could be avoided with 32 bit values + // at the expense of memory usage). + short* pinnedHead = this.pinnedHeadPointer; + for (int i = 0; i < DeflaterConstants.HASH_SIZE; ++i) + { + int m = pinnedHead[i] & 0xFFFF; + pinnedHead[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); + } + + // Slide the prev table. + short* pinnedPrev = this.pinnedPrevPointer; + for (int i = 0; i < DeflaterConstants.WSIZE; i++) + { + int m = pinnedPrev[i] & 0xFFFF; + pinnedPrev[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); + } + } + + /// + /// + /// Find the best (longest) string in the window matching the + /// string starting at strstart. + /// + /// + /// Preconditions: + /// + /// strstart + DeflaterConstants.MAX_MATCH <= window.length. + /// + /// + /// The current match. + /// True if a match greater than the minimum length is found + [MethodImpl(InliningOptions.HotPath)] + private bool FindLongestMatch(int curMatch) + { + int match; + int scan = this.strstart; + + // scanMax is the highest position that we can look at + int scanMax = scan + Math.Min(DeflaterConstants.MAX_MATCH, this.lookahead) - 1; + int limit = Math.Max(scan - DeflaterConstants.MAX_DIST, 0); + + int chainLength = this.maxChain; + int niceLength = Math.Min(this.niceLength, this.lookahead); + + int matchStrt = this.matchStart; + int matchLength = this.matchLen; + matchLength = Math.Max(matchLength, DeflaterConstants.MIN_MATCH - 1); + this.matchLen = matchLength; + + if (scan > scanMax - matchLength) + { + return false; + } + + int scanEndPosition = scan + matchLength; + + byte* pinnedWindow = this.pinnedWindowPointer; + int scanStart = this.strstart; + byte scanEnd1 = pinnedWindow[scanEndPosition - 1]; + byte scanEnd = pinnedWindow[scanEndPosition]; + + // Do not waste too much time if we already have a good match: + if (matchLength >= this.goodLength) + { + chainLength >>= 2; + } + + short* pinnedPrev = this.pinnedPrevPointer; + do + { + match = curMatch; + scan = scanStart; + + int matchEndPosition = match + matchLength; + if (pinnedWindow[matchEndPosition] != scanEnd + || pinnedWindow[matchEndPosition - 1] != scanEnd1 + || pinnedWindow[match] != pinnedWindow[scan] + || pinnedWindow[++match] != pinnedWindow[++scan]) + { + continue; + } + + // scan is set to strstart+1 and the comparison passed, so + // scanMax - scan is the maximum number of bytes we can compare. + // below we compare 8 bytes at a time, so first we compare + // (scanMax - scan) % 8 bytes, so the remainder is a multiple of 8 + // n & (8 - 1) == n % 8. + switch ((scanMax - scan) & 7) + { + case 1: + if (pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + + case 2: + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + + case 3: + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + + case 4: + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + + case 5: + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + + case 6: + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + + case 7: + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + } + + if (pinnedWindow[scan] == pinnedWindow[match]) + { + // We check for insufficient lookahead only every 8th comparison; + // the 256th check will be made at strstart + 258 unless lookahead is + // exhausted first. + do + { + if (scan == scanMax) + { + ++scan; // advance to first position not matched + ++match; + + break; + } + } + while (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]); + } + + if (scan - scanStart > matchLength) + { + matchStrt = curMatch; + matchLength = scan - scanStart; + + if (matchLength >= niceLength) + { + break; + } + + scanEnd1 = pinnedWindow[scan - 1]; + scanEnd = pinnedWindow[scan]; + } + } + while ((curMatch = pinnedPrev[curMatch & DeflaterConstants.WMASK] & 0xFFFF) > limit && --chainLength != 0); + + this.matchStart = matchStrt; + this.matchLen = matchLength; + return matchLength >= DeflaterConstants.MIN_MATCH; + } + + private bool DeflateStored(bool flush, bool finish) + { + if (!flush && (this.lookahead == 0)) + { + return false; + } + + this.strstart += this.lookahead; + this.lookahead = 0; + + int storedLength = this.strstart - this.blockStart; + + if ((storedLength >= DeflaterConstants.MAX_BLOCK_SIZE) || // Block is full + (this.blockStart < DeflaterConstants.WSIZE && storedLength >= DeflaterConstants.MAX_DIST) || // Block may move out of window + flush) + { + bool lastBlock = finish; + if (storedLength > DeflaterConstants.MAX_BLOCK_SIZE) + { + storedLength = DeflaterConstants.MAX_BLOCK_SIZE; + lastBlock = false; + } + + this.huffman.FlushStoredBlock(this.window.Span, this.blockStart, storedLength, lastBlock); + this.blockStart += storedLength; + return !(lastBlock || storedLength == 0); + } + + return true; + } + + private bool DeflateFast(bool flush, bool finish) + { + if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) + { + return false; + } + + const int windowLen = (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD; + while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) + { + if (this.lookahead == 0) + { + // We are flushing everything + this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, finish); + this.blockStart = this.strstart; + return false; + } + + if (this.strstart > windowLen) + { + // slide window, as FindLongestMatch needs this. + // This should only happen when flushing and the window + // is almost full. + this.SlideWindow(); + } + + int hashHead; + if (this.lookahead >= DeflaterConstants.MIN_MATCH && + (hashHead = this.InsertString()) != 0 && + this.strategy != DeflateStrategy.HuffmanOnly && + this.strstart - hashHead <= DeflaterConstants.MAX_DIST && + this.FindLongestMatch(hashHead)) + { + // longestMatch sets matchStart and matchLen + bool full = this.huffman.TallyDist(this.strstart - this.matchStart, this.matchLen); + + this.lookahead -= this.matchLen; + if (this.matchLen <= this.maxLazy && this.lookahead >= DeflaterConstants.MIN_MATCH) + { + while (--this.matchLen > 0) + { + ++this.strstart; + this.InsertString(); + } + + ++this.strstart; + } + else + { + this.strstart += this.matchLen; + if (this.lookahead >= DeflaterConstants.MIN_MATCH - 1) + { + this.UpdateHash(); + } + } + + this.matchLen = DeflaterConstants.MIN_MATCH - 1; + if (!full) + { + continue; + } + } + else + { + // No match found + this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart] & 0xff); + ++this.strstart; + --this.lookahead; + } + + if (this.huffman.IsFull()) + { + bool lastBlock = finish && (this.lookahead == 0); + this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, lastBlock); + this.blockStart = this.strstart; + return !lastBlock; + } + } + + return true; + } + + private bool DeflateSlow(bool flush, bool finish) + { + if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) + { + return false; + } + + const int windowLen = (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD; + while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) + { + if (this.lookahead == 0) + { + if (this.prevAvailable) + { + this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xff); + } + + this.prevAvailable = false; + + // We are flushing everything + this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, finish); + this.blockStart = this.strstart; + return false; + } + + if (this.strstart >= windowLen) + { + // slide window, as FindLongestMatch needs this. + // This should only happen when flushing and the window + // is almost full. + this.SlideWindow(); + } + + int prevMatch = this.matchStart; + int prevLen = this.matchLen; + if (this.lookahead >= DeflaterConstants.MIN_MATCH) + { + int hashHead = this.InsertString(); + + if (this.strategy != DeflateStrategy.HuffmanOnly && + hashHead != 0 && + this.strstart - hashHead <= DeflaterConstants.MAX_DIST && + this.FindLongestMatch(hashHead)) + { + // longestMatch sets matchStart and matchLen + // Discard match if too small and too far away + if (this.matchLen <= 5 && (this.strategy == DeflateStrategy.Filtered || (this.matchLen == DeflaterConstants.MIN_MATCH && this.strstart - this.matchStart > TooFar))) + { + this.matchLen = DeflaterConstants.MIN_MATCH - 1; + } + } + } + + // previous match was better + if ((prevLen >= DeflaterConstants.MIN_MATCH) && (this.matchLen <= prevLen)) + { + this.huffman.TallyDist(this.strstart - 1 - prevMatch, prevLen); + prevLen -= 2; + do + { + this.strstart++; + this.lookahead--; + if (this.lookahead >= DeflaterConstants.MIN_MATCH) + { + this.InsertString(); + } + } + while (--prevLen > 0); + + this.strstart++; + this.lookahead--; + this.prevAvailable = false; + this.matchLen = DeflaterConstants.MIN_MATCH - 1; + } + else + { + if (this.prevAvailable) + { + this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xff); + } + + this.prevAvailable = true; + this.strstart++; + this.lookahead--; + } + + if (this.huffman.IsFull()) + { + int len = this.strstart - this.blockStart; + if (this.prevAvailable) + { + len--; + } + + bool lastBlock = finish && (this.lookahead == 0) && !this.prevAvailable; + this.huffman.FlushBlock(this.window.Span, this.blockStart, len, lastBlock); + this.blockStart += len; + return !lastBlock; + } + } + + return true; + } + } +} diff --git a/ImageSharp/Compression/Zlib/DeflaterHuffman.cs b/ImageSharp/Compression/Zlib/DeflaterHuffman.cs new file mode 100644 index 0000000..dbda153 --- /dev/null +++ b/ImageSharp/Compression/Zlib/DeflaterHuffman.cs @@ -0,0 +1,980 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Compression.Zlib { + /// + /// Performs Deflate Huffman encoding. + /// + internal sealed unsafe class DeflaterHuffman : IDisposable + { + private const int BufferSize = 1 << (DeflaterConstants.DEFAULT_MEM_LEVEL + 6); + + // The number of literal codes. + private const int LiteralNumber = 286; + + // Number of distance codes + private const int DistanceNumber = 30; + + // Number of codes used to transfer bit lengths + private const int BitLengthNumber = 19; + + // Repeat previous bit length 3-6 times (2 bits of repeat count) + private const int Repeat3To6 = 16; + + // Repeat a zero length 3-10 times (3 bits of repeat count) + private const int Repeat3To10 = 17; + + // Repeat a zero length 11-138 times (7 bits of repeat count) + private const int Repeat11To138 = 18; + + private const int EofSymbol = 256; + + private Tree literalTree; + private Tree distTree; + private Tree blTree; + + // Buffer for distances + private readonly IMemoryOwner distanceMemoryOwner; + private readonly short* pinnedDistanceBuffer; + private MemoryHandle distanceBufferHandle; + + private readonly IMemoryOwner literalMemoryOwner; + private readonly short* pinnedLiteralBuffer; + private MemoryHandle literalBufferHandle; + + private int lastLiteral; + private int extraBits; + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator to use for buffer allocations. + public DeflaterHuffman(MemoryAllocator memoryAllocator) + { + this.Pending = new DeflaterPendingBuffer(memoryAllocator); + + this.literalTree = new Tree(memoryAllocator, LiteralNumber, 257, 15); + this.distTree = new Tree(memoryAllocator, DistanceNumber, 1, 15); + this.blTree = new Tree(memoryAllocator, BitLengthNumber, 4, 7); + + this.distanceMemoryOwner = memoryAllocator.Allocate(BufferSize); + this.distanceBufferHandle = this.distanceMemoryOwner.Memory.Pin(); + this.pinnedDistanceBuffer = (short*)this.distanceBufferHandle.Pointer; + + this.literalMemoryOwner = memoryAllocator.Allocate(BufferSize); + this.literalBufferHandle = this.literalMemoryOwner.Memory.Pin(); + this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer; + } + +#pragma warning disable SA1201 // Elements should appear in the correct order + + // See RFC 1951 3.2.6 + // Literal codes + private static readonly short[] StaticLCodes = + [ + 12, 140, 76, 204, 44, 172, 108, 236, 28, 156, 92, 220, 60, 188, 124, 252, + 2, 130, 66, 194, 34, 162, 98, 226, 18, 146, 82, 210, 50, 178, 114, 242, + 10, 138, 74, 202, 42, 170, 106, 234, 26, 154, 90, 218, 58, 186, 122, 250, + 6, 134, 70, 198, 38, 166, 102, 230, 22, 150, 86, 214, 54, 182, 118, 246, + 14, 142, 78, 206, 46, 174, 110, 238, 30, 158, 94, 222, 62, 190, 126, 254, + 1, 129, 65, 193, 33, 161, 97, 225, 17, 145, 81, 209, 49, 177, 113, 241, 9, + 137, 73, 201, 41, 169, 105, 233, 25, 153, 89, 217, 57, 185, 121, 249, 5, + 133, 69, 197, 37, 165, 101, 229, 21, 149, 85, 213, 53, 181, 117, 245, 13, + 141, 77, 205, 45, 173, 109, 237, 29, 157, 93, 221, 61, 189, 125, 253, 19, + 275, 147, 403, 83, 339, 211, 467, 51, 307, 179, 435, 115, 371, 243, 499, + 11, 267, 139, 395, 75, 331, 203, 459, 43, 299, 171, 427, 107, 363, 235, 491, + 27, 283, 155, 411, 91, 347, 219, 475, 59, 315, 187, 443, 123, 379, 251, 507, + 7, 263, 135, 391, 71, 327, 199, 455, 39, 295, 167, 423, 103, 359, 231, 487, + 23, 279, 151, 407, 87, 343, 215, 471, 55, 311, 183, 439, 119, 375, 247, 503, + 15, 271, 143, 399, 79, 335, 207, 463, 47, 303, 175, 431, 111, 367, 239, 495, + 31, 287, 159, 415, 95, 351, 223, 479, 63, 319, 191, 447, 127, 383, 255, 511, + 0, 64, 32, 96, 16, 80, 48, 112, 8, 72, 40, 104, 24, 88, 56, 120, 4, 68, 36, + 100, 20, 84, 52, 116, 3, 131, 67, 195, 35, 163 + ]; + + private static ReadOnlySpan StaticLLength => + [ + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8 + ]; + + // Distance codes and lengths. + private static readonly short[] StaticDCodes = + [ + 0, 16, 8, 24, 4, 20, 12, 28, 2, 18, 10, 26, 6, 22, 14, + 30, 1, 17, 9, 25, 5, 21, 13, 29, 3, 19, 11, 27, 7, 23 + ]; + + private static ReadOnlySpan StaticDLength => + [ + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 + ]; +#pragma warning restore SA1201 // Elements should appear in the correct order + + /// + /// Gets the lengths of the bit length codes are sent in order of decreasing probability, to avoid transmitting the lengths for unused bit length codes. + /// + private static ReadOnlySpan BitLengthOrder => + [ + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 + ]; + + private static ReadOnlySpan Bit4Reverse => + [ + 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 + ]; + + /// + /// Gets the pending buffer to use. + /// + public DeflaterPendingBuffer Pending { get; private set; } + + /// + /// Reset internal state + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Reset() + { + this.lastLiteral = 0; + this.extraBits = 0; + this.literalTree.Reset(); + this.distTree.Reset(); + this.blTree.Reset(); + } + + /// + /// Write all trees to pending buffer + /// + /// The number/rank of treecodes to send. + public void SendAllTrees(int blTreeCodes) + { + this.blTree.BuildCodes(); + this.literalTree.BuildCodes(); + this.distTree.BuildCodes(); + this.Pending.WriteBits(this.literalTree.NumCodes - 257, 5); + this.Pending.WriteBits(this.distTree.NumCodes - 1, 5); + this.Pending.WriteBits(blTreeCodes - 4, 4); + + for (int rank = 0; rank < blTreeCodes; rank++) + { + this.Pending.WriteBits(this.blTree.Length[BitLengthOrder[rank]], 3); + } + + this.literalTree.WriteTree(this.Pending, this.blTree); + this.distTree.WriteTree(this.Pending, this.blTree); + } + + /// + /// Compress current buffer writing data to pending buffer + /// + public void CompressBlock() + { + DeflaterPendingBuffer pendingBuffer = this.Pending; + short* pinnedDistance = this.pinnedDistanceBuffer; + short* pinnedLiteral = this.pinnedLiteralBuffer; + + for (int i = 0; i < this.lastLiteral; i++) + { + int litlen = pinnedLiteral[i] & 0xFF; + int dist = pinnedDistance[i]; + if (dist-- != 0) + { + int lc = Lcode(litlen); + this.literalTree.WriteSymbol(pendingBuffer, lc); + + int bits = (int)(((uint)lc - 261) / 4); + if (bits is > 0 and <= 5) + { + this.Pending.WriteBits(litlen & ((1 << bits) - 1), bits); + } + + int dc = Dcode(dist); + this.distTree.WriteSymbol(pendingBuffer, dc); + + bits = (dc >> 1) - 1; + if (bits > 0) + { + this.Pending.WriteBits(dist & ((1 << bits) - 1), bits); + } + } + else + { + this.literalTree.WriteSymbol(pendingBuffer, litlen); + } + } + + this.literalTree.WriteSymbol(pendingBuffer, EofSymbol); + } + + /// + /// Flush block to output with no compression + /// + /// Data to write + /// Index of first byte to write + /// Count of bytes to write + /// True if this is the last block + [MethodImpl(InliningOptions.ShortMethod)] + public void FlushStoredBlock(ReadOnlySpan stored, int storedOffset, int storedLength, bool lastBlock) + { + this.Pending.WriteBits((DeflaterConstants.STORED_BLOCK << 1) + (lastBlock ? 1 : 0), 3); + this.Pending.AlignToByte(); + this.Pending.WriteShort(storedLength); + this.Pending.WriteShort(~storedLength); + this.Pending.WriteBlock(stored, storedOffset, storedLength); + this.Reset(); + } + + /// + /// Flush block to output with compression + /// + /// Data to flush + /// Index of first byte to flush + /// Count of bytes to flush + /// True if this is the last block + public void FlushBlock(ReadOnlySpan stored, int storedOffset, int storedLength, bool lastBlock) + { + this.literalTree.Frequencies[EofSymbol]++; + + // Build trees + this.literalTree.BuildTree(); + this.distTree.BuildTree(); + + // Calculate bitlen frequency + this.literalTree.CalcBLFreq(this.blTree); + this.distTree.CalcBLFreq(this.blTree); + + // Build bitlen tree + this.blTree.BuildTree(); + + int blTreeCodes = 4; + + for (int i = 18; i > blTreeCodes; i--) + { + if (this.blTree.Length[BitLengthOrder[i]] > 0) + { + blTreeCodes = i + 1; + } + } + + int opt_len = 14 + (blTreeCodes * 3) + this.blTree.GetEncodedLength() + + this.literalTree.GetEncodedLength() + this.distTree.GetEncodedLength() + + this.extraBits; + + int static_len = this.extraBits; + ref byte staticLLengthRef = ref MemoryMarshal.GetReference(StaticLLength); + for (nuint i = 0; i < LiteralNumber; i++) + { + static_len += this.literalTree.Frequencies[i] * Unsafe.Add(ref staticLLengthRef, i); + } + + ref byte staticDLengthRef = ref MemoryMarshal.GetReference(StaticDLength); + for (nuint i = 0; i < DistanceNumber; i++) + { + static_len += this.distTree.Frequencies[i] * Unsafe.Add(ref staticDLengthRef, i); + } + + if (opt_len >= static_len) + { + // Force static trees + opt_len = static_len; + } + + if (storedOffset >= 0 && storedLength + 4 < opt_len >> 3) + { + // Store Block + this.FlushStoredBlock(stored, storedOffset, storedLength, lastBlock); + } + else if (opt_len == static_len) + { + // Encode with static tree + this.Pending.WriteBits((DeflaterConstants.STATIC_TREES << 1) + (lastBlock ? 1 : 0), 3); + this.literalTree.SetStaticCodes(StaticLCodes, StaticLLength); + this.distTree.SetStaticCodes(StaticDCodes, StaticDLength); + this.CompressBlock(); + this.Reset(); + } + else + { + // Encode with dynamic tree + this.Pending.WriteBits((DeflaterConstants.DYN_TREES << 1) + (lastBlock ? 1 : 0), 3); + this.SendAllTrees(blTreeCodes); + this.CompressBlock(); + this.Reset(); + } + } + + /// + /// Get value indicating if internal buffer is full + /// + /// true if buffer is full + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsFull() => this.lastLiteral >= BufferSize; + + /// + /// Add literal to buffer + /// + /// Literal value to add to buffer. + /// Value indicating internal buffer is full + [MethodImpl(InliningOptions.ShortMethod)] + public bool TallyLit(int literal) + { + this.pinnedDistanceBuffer[this.lastLiteral] = 0; + this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)literal; + this.literalTree.Frequencies[literal]++; + return this.IsFull(); + } + + /// + /// Add distance code and length to literal and distance trees + /// + /// Distance code + /// Length + /// Value indicating if internal buffer is full + [MethodImpl(InliningOptions.ShortMethod)] + public bool TallyDist(int distance, int length) + { + this.pinnedDistanceBuffer[this.lastLiteral] = (short)distance; + this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)(length - 3); + + int lc = Lcode(length - 3); + this.literalTree.Frequencies[lc]++; + if (lc >= 265 && lc < 285) + { + this.extraBits += (int)(((uint)lc - 261) / 4); + } + + int dc = Dcode(distance - 1); + this.distTree.Frequencies[dc]++; + if (dc >= 4) + { + this.extraBits += (dc >> 1) - 1; + } + + return this.IsFull(); + } + + /// + /// Reverse the bits of a 16 bit value. + /// + /// Value to reverse bits + /// Value with bits reversed + [MethodImpl(InliningOptions.ShortMethod)] + public static short BitReverse(int toReverse) + { + /* Use unsafe offsetting and manually validate the input index to reduce the + * total number of conditional branches. There are two main cases to test here: + * 1. In the first 3, the input value (or some combination of it) is combined + * with & 0xF, which results in a maximum value of 0xF no matter what the + * input value was. That is 15, which is always in range for the target span. + * As a result, no input validation is needed at all in this case. + * 2. There are two cases where the input value might cause an invalid access: + * when it is either negative, or greater than 15 << 12. We can test both + * conditions in a single pass by casting the input value to uint and right + * shifting it by 12, which also preserves the sign. If it is a negative + * value (2-complement), the test will fail as the uint cast will result + * in a much larger value. If the value was simply too high, the test will + * fail as expected. We can't simply check whether the value is lower than + * 15 << 12, because higher values are acceptable in the first 3 accesses. + * Doing this reduces the total number of index checks from 4 down to just 1. */ + int toReverseRightShiftBy12 = toReverse >> 12; + Guard.MustBeLessThanOrEqualTo((uint)toReverseRightShiftBy12, 15, nameof(toReverse)); + + ref byte bit4ReverseRef = ref MemoryMarshal.GetReference(Bit4Reverse); + + return (short)((Unsafe.Add(ref bit4ReverseRef, (uint)toReverse & 0xF) << 12) + | (Unsafe.Add(ref bit4ReverseRef, (uint)(toReverse >> 4) & 0xF) << 8) + | (Unsafe.Add(ref bit4ReverseRef, (uint)(toReverse >> 8) & 0xF) << 4) + | Unsafe.Add(ref bit4ReverseRef, (uint)toReverseRightShiftBy12)); + } + + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.Pending.Dispose(); + this.distanceBufferHandle.Dispose(); + this.distanceMemoryOwner.Dispose(); + this.literalBufferHandle.Dispose(); + this.literalMemoryOwner.Dispose(); + + this.literalTree.Dispose(); + this.blTree.Dispose(); + this.distTree.Dispose(); + + this.isDisposed = true; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Lcode(int length) + { + if (length == 255) + { + return 285; + } + + int code = 257; + while (length >= 8) + { + code += 4; + length >>= 1; + } + + return code + length; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Dcode(int distance) + { + int code = 0; + while (distance >= 4) + { + code += 2; + distance >>= 1; + } + + return code + distance; + } + + private sealed class Tree : IDisposable + { + private readonly int minNumCodes; + private readonly int[] bitLengthCounts; + private readonly int maxLength; + private bool isDisposed; + + private readonly int elementCount; + + private readonly MemoryAllocator memoryAllocator; + + private IMemoryOwner codesMemoryOwner; + private MemoryHandle codesMemoryHandle; + private readonly short* codes; + + private IMemoryOwner frequenciesMemoryOwner; + private MemoryHandle frequenciesMemoryHandle; + + private IMemoryOwner lengthsMemoryOwner; + private MemoryHandle lengthsMemoryHandle; + + public Tree(MemoryAllocator memoryAllocator, int elements, int minCodes, int maxLength) + { + this.memoryAllocator = memoryAllocator; + this.elementCount = elements; + this.minNumCodes = minCodes; + this.maxLength = maxLength; + + this.frequenciesMemoryOwner = memoryAllocator.Allocate(elements); + this.frequenciesMemoryHandle = this.frequenciesMemoryOwner.Memory.Pin(); + this.Frequencies = (short*)this.frequenciesMemoryHandle.Pointer; + + this.lengthsMemoryOwner = memoryAllocator.Allocate(elements); + this.lengthsMemoryHandle = this.lengthsMemoryOwner.Memory.Pin(); + this.Length = (byte*)this.lengthsMemoryHandle.Pointer; + + this.codesMemoryOwner = memoryAllocator.Allocate(elements); + this.codesMemoryHandle = this.codesMemoryOwner.Memory.Pin(); + this.codes = (short*)this.codesMemoryHandle.Pointer; + + // Maxes out at 15. + this.bitLengthCounts = new int[maxLength]; + } + + public int NumCodes { get; private set; } + + public short* Frequencies { get; } + + public byte* Length { get; } + + /// + /// Resets the internal state of the tree + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Reset() + { + this.frequenciesMemoryOwner.Memory.Span.Clear(); + this.lengthsMemoryOwner.Memory.Span.Clear(); + this.codesMemoryOwner.Memory.Span.Clear(); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void WriteSymbol(DeflaterPendingBuffer pendingBuffer, int code) + => pendingBuffer.WriteBits(this.codes[code] & 0xFFFF, this.Length[code]); + + /// + /// Set static codes and length + /// + /// new codes + /// length for new codes + [MethodImpl(InliningOptions.ShortMethod)] + public void SetStaticCodes(ReadOnlySpan staticCodes, ReadOnlySpan staticLengths) + { + staticCodes.CopyTo(this.codesMemoryOwner.Memory.Span); + staticLengths.CopyTo(this.lengthsMemoryOwner.Memory.Span); + } + + /// + /// Build dynamic codes and lengths + /// + public void BuildCodes() + { + // Maxes out at 15 * 4 + Span nextCode = stackalloc int[this.maxLength]; + ref int nextCodeRef = ref MemoryMarshal.GetReference(nextCode); + ref int bitLengthCountsRef = ref MemoryMarshal.GetReference(this.bitLengthCounts); + + int code = 0; + for (int bits = 0; bits < this.maxLength; bits++) + { + Unsafe.Add(ref nextCodeRef, (uint)bits) = code; + code += Unsafe.Add(ref bitLengthCountsRef, (uint)bits) << (15 - bits); + } + + for (int i = 0; i < this.NumCodes; i++) + { + int bits = this.Length[i]; + if (bits > 0) + { + this.codes[i] = BitReverse(Unsafe.Add(ref nextCodeRef, (uint)(bits - 1))); + Unsafe.Add(ref nextCodeRef, (uint)(bits - 1)) += 1 << (16 - bits); + } + } + } + + [MethodImpl(InliningOptions.HotPath)] + public void BuildTree() + { + int numSymbols = this.elementCount; + + // heap is a priority queue, sorted by frequency, least frequent + // nodes first. The heap is a binary tree, with the property, that + // the parent node is smaller than both child nodes. This assures + // that the smallest node is the first parent. + // + // The binary tree is encoded in an array: 0 is root node and + // the nodes 2*n+1, 2*n+2 are the child nodes of node n. + // Maxes out at 286 * 4 so too large for the stack. + using (IMemoryOwner heapMemoryOwner = this.memoryAllocator.Allocate(numSymbols)) + { + ref int heapRef = ref MemoryMarshal.GetReference(heapMemoryOwner.Memory.Span); + + int heapLen = 0; + int maxCode = 0; + for (int n = 0; n < numSymbols; n++) + { + int freq = this.Frequencies[n]; + if (freq != 0) + { + // Insert n into heap + int pos = heapLen++; + int ppos; + while (pos > 0 && this.Frequencies[Unsafe.Add(ref heapRef, (uint)(ppos = (pos - 1) >> 1))] > freq) + { + Unsafe.Add(ref heapRef, pos) = Unsafe.Add(ref heapRef, (uint)ppos); + pos = ppos; + } + + Unsafe.Add(ref heapRef, (uint)pos) = n; + + maxCode = n; + } + } + + // We could encode a single literal with 0 bits but then we + // don't see the literals. Therefore we force at least two + // literals to avoid this case. We don't care about order in + // this case, both literals get a 1 bit code. + while (heapLen < 2) + { + Unsafe.Add(ref heapRef, (uint)heapLen++) = maxCode < 2 ? ++maxCode : 0; + } + + this.NumCodes = Math.Max(maxCode + 1, this.minNumCodes); + + int numLeafs = heapLen; + int childrenLength = (4 * heapLen) - 2; + using (IMemoryOwner childrenMemoryOwner = this.memoryAllocator.Allocate(childrenLength)) + using (IMemoryOwner valuesMemoryOwner = this.memoryAllocator.Allocate((2 * heapLen) - 1)) + { + ref int childrenRef = ref MemoryMarshal.GetReference(childrenMemoryOwner.Memory.Span); + ref int valuesRef = ref MemoryMarshal.GetReference(valuesMemoryOwner.Memory.Span); + int numNodes = numLeafs; + + for (nuint i = 0; i < (uint)heapLen; i++) + { + int node = Unsafe.Add(ref heapRef, i); + nuint i2 = 2 * i; + Unsafe.Add(ref childrenRef, i2) = node; + Unsafe.Add(ref childrenRef, i2 + 1) = -1; + Unsafe.Add(ref valuesRef, i) = this.Frequencies[node] << 8; + Unsafe.Add(ref heapRef, i) = (int)i; + } + + // Construct the Huffman tree by repeatedly combining the least two + // frequent nodes. + do + { + int first = Unsafe.Add(ref heapRef, 0); + int last = Unsafe.Add(ref heapRef, (uint)--heapLen); + + // Propagate the hole to the leafs of the heap + int ppos = 0; + int path = 1; + + while (path < heapLen) + { + if (path + 1 < heapLen && Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)path)) > Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)(path + 1)))) + { + path++; + } + + Unsafe.Add(ref heapRef, (uint)ppos) = Unsafe.Add(ref heapRef, (uint)path); + ppos = path; + path = (path * 2) + 1; + } + + // Now propagate the last element down along path. Normally + // it shouldn't go too deep. + int lastVal = Unsafe.Add(ref valuesRef, (uint)last); + while ((path = ppos) > 0 + && Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)(ppos = (path - 1) >> 1))) > lastVal) + { + Unsafe.Add(ref heapRef, (uint)path) = Unsafe.Add(ref heapRef, (uint)ppos); + } + + Unsafe.Add(ref heapRef, (uint)path) = last; + + int second = Unsafe.Add(ref heapRef, 0); + + // Create a new node father of first and second + last = numNodes++; + Unsafe.Add(ref childrenRef, (uint)(2 * last)) = first; + Unsafe.Add(ref childrenRef, (uint)((2 * last) + 1)) = second; + int mindepth = Math.Min(Unsafe.Add(ref valuesRef, (uint)first) & 0xFF, Unsafe.Add(ref valuesRef, (uint)second) & 0xFF); + Unsafe.Add(ref valuesRef, (uint)last) = lastVal = Unsafe.Add(ref valuesRef, (uint)first) + Unsafe.Add(ref valuesRef, (uint)second) - mindepth + 1; + + // Again, propagate the hole to the leafs + ppos = 0; + path = 1; + + while (path < heapLen) + { + if (path + 1 < heapLen + && Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)path)) > Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)(path + 1)))) + { + path++; + } + + Unsafe.Add(ref heapRef, (uint)ppos) = Unsafe.Add(ref heapRef, (uint)path); + ppos = path; + path = (ppos * 2) + 1; + } + + // Now propagate the new element down along path + while ((path = ppos) > 0 && Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)(ppos = (path - 1) >> 1))) > lastVal) + { + Unsafe.Add(ref heapRef, (uint)path) = Unsafe.Add(ref heapRef, (uint)ppos); + } + + Unsafe.Add(ref heapRef, (uint)path) = last; + } + while (heapLen > 1); + + if (Unsafe.Add(ref heapRef, 0) != (childrenLength >> 1) - 1) + { + DeflateThrowHelper.ThrowHeapViolated(); + } + + this.BuildLength(childrenMemoryOwner.Memory.Span); + } + } + } + + /// + /// Get encoded length + /// + /// Encoded length, the sum of frequencies * lengths + [MethodImpl(InliningOptions.ShortMethod)] + public int GetEncodedLength() + { + int len = 0; + for (int i = 0; i < this.elementCount; i++) + { + len += this.Frequencies[i] * this.Length[i]; + } + + return len; + } + + /// + /// Scan a literal or distance tree to determine the frequencies of the codes + /// in the bit length tree. + /// + public void CalcBLFreq(Tree blTree) + { + int maxCount; // max repeat count + int minCount; // min repeat count + int count; // repeat count of the current code + int curLen = -1; // length of current code + + int i = 0; + while (i < this.NumCodes) + { + count = 1; + int nextlen = this.Length[i]; + if (nextlen == 0) + { + maxCount = 138; + minCount = 3; + } + else + { + maxCount = 6; + minCount = 3; + if (curLen != nextlen) + { + blTree.Frequencies[nextlen]++; + count = 0; + } + } + + curLen = nextlen; + i++; + + while (i < this.NumCodes && curLen == this.Length[i]) + { + i++; + if (++count >= maxCount) + { + break; + } + } + + if (count < minCount) + { + blTree.Frequencies[curLen] += (short)count; + } + else if (curLen != 0) + { + blTree.Frequencies[Repeat3To6]++; + } + else if (count <= 10) + { + blTree.Frequencies[Repeat3To10]++; + } + else + { + blTree.Frequencies[Repeat11To138]++; + } + } + } + + /// + /// Write the tree values. + /// + /// The pending buffer. + /// The tree to write. + public void WriteTree(DeflaterPendingBuffer pendingBuffer, Tree bitLengthTree) + { + int maxCount; // max repeat count + int minCount; // min repeat count + int count; // repeat count of the current code + int curLen = -1; // length of current code + + int i = 0; + while (i < this.NumCodes) + { + count = 1; + int nextlen = this.Length[i]; + if (nextlen == 0) + { + maxCount = 138; + minCount = 3; + } + else + { + maxCount = 6; + minCount = 3; + if (curLen != nextlen) + { + bitLengthTree.WriteSymbol(pendingBuffer, nextlen); + count = 0; + } + } + + curLen = nextlen; + i++; + + while (i < this.NumCodes && curLen == this.Length[i]) + { + i++; + if (++count >= maxCount) + { + break; + } + } + + if (count < minCount) + { + while (count-- > 0) + { + bitLengthTree.WriteSymbol(pendingBuffer, curLen); + } + } + else if (curLen != 0) + { + bitLengthTree.WriteSymbol(pendingBuffer, Repeat3To6); + pendingBuffer.WriteBits(count - 3, 2); + } + else if (count <= 10) + { + bitLengthTree.WriteSymbol(pendingBuffer, Repeat3To10); + pendingBuffer.WriteBits(count - 3, 3); + } + else + { + bitLengthTree.WriteSymbol(pendingBuffer, Repeat11To138); + pendingBuffer.WriteBits(count - 11, 7); + } + } + } + + private void BuildLength(ReadOnlySpan children) + { + byte* lengthPtr = this.Length; + ref int childrenRef = ref MemoryMarshal.GetReference(children); + ref int bitLengthCountsRef = ref MemoryMarshal.GetReference(this.bitLengthCounts); + + int maxLen = this.maxLength; + int numNodes = children.Length >> 1; + int numLeafs = (numNodes + 1) >> 1; + int overflow = 0; + + Array.Clear(this.bitLengthCounts, 0, maxLen); + + // First calculate optimal bit lengths + using (IMemoryOwner lengthsMemoryOwner = this.memoryAllocator.Allocate(numNodes, AllocationOptions.Clean)) + { + ref int lengthsRef = ref MemoryMarshal.GetReference(lengthsMemoryOwner.Memory.Span); + + for (int i = numNodes - 1; i >= 0; i--) + { + if (children[(2 * i) + 1] != -1) + { + int bitLength = Unsafe.Add(ref lengthsRef, (uint)i) + 1; + if (bitLength > maxLen) + { + bitLength = maxLen; + overflow++; + } + + Unsafe.Add(ref lengthsRef, (uint)Unsafe.Add(ref childrenRef, (uint)(2 * i))) = Unsafe.Add(ref lengthsRef, (uint)Unsafe.Add(ref childrenRef, (uint)((2 * i) + 1))) = bitLength; + } + else + { + // A leaf node + int bitLength = Unsafe.Add(ref lengthsRef, (uint)i); + Unsafe.Add(ref bitLengthCountsRef, (uint)(bitLength - 1))++; + lengthPtr[Unsafe.Add(ref childrenRef, (uint)(2 * i))] = (byte)Unsafe.Add(ref lengthsRef, (uint)i); + } + } + } + + if (overflow == 0) + { + return; + } + + int incrBitLen = maxLen - 1; + do + { + // Find the first bit length which could increase: + while (Unsafe.Add(ref bitLengthCountsRef, (uint)--incrBitLen) == 0) + { + } + + // Move this node one down and remove a corresponding + // number of overflow nodes. + do + { + Unsafe.Add(ref bitLengthCountsRef, (uint)incrBitLen)--; + Unsafe.Add(ref bitLengthCountsRef, (uint)++incrBitLen)++; + overflow -= 1 << (maxLen - 1 - incrBitLen); + } + while (overflow > 0 && incrBitLen < maxLen - 1); + } + while (overflow > 0); + + // We may have overshot above. Move some nodes from maxLength to + // maxLength-1 in that case. + Unsafe.Add(ref bitLengthCountsRef, (uint)(maxLen - 1)) += overflow; + Unsafe.Add(ref bitLengthCountsRef, (uint)(maxLen - 2)) -= overflow; + + // Now recompute all bit lengths, scanning in increasing + // frequency. It is simpler to reconstruct all lengths instead of + // fixing only the wrong ones. This idea is taken from 'ar' + // written by Haruhiko Okumura. + // + // The nodes were inserted with decreasing frequency into the childs + // array. + int nodeIndex = 2 * numLeafs; + for (int bits = maxLen; bits != 0; bits--) + { + int n = Unsafe.Add(ref bitLengthCountsRef, (uint)(bits - 1)); + while (n > 0) + { + int childIndex = 2 * Unsafe.Add(ref childrenRef, (uint)nodeIndex++); + if (Unsafe.Add(ref childrenRef, (uint)(childIndex + 1)) == -1) + { + // We found another leaf + lengthPtr[Unsafe.Add(ref childrenRef, (uint)childIndex)] = (byte)bits; + n--; + } + } + } + } + + public void Dispose() + { + if (!this.isDisposed) + { + this.frequenciesMemoryHandle.Dispose(); + this.frequenciesMemoryOwner.Dispose(); + + this.lengthsMemoryHandle.Dispose(); + this.lengthsMemoryOwner.Dispose(); + + this.codesMemoryHandle.Dispose(); + this.codesMemoryOwner.Dispose(); + + this.isDisposed = true; + } + } + } + } +} diff --git a/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs b/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs new file mode 100644 index 0000000..d29419c --- /dev/null +++ b/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs @@ -0,0 +1,145 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Compression.Zlib { + /// + /// A special stream deflating or compressing the bytes that are + /// written to it. It uses a Deflater to perform actual deflating. + /// + internal sealed class DeflaterOutputStream : Stream + { + private const int BufferLength = 512; + private IMemoryOwner memoryOwner; + private readonly Memory buffer; + private Deflater deflater; + private readonly Stream rawStream; + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator to use for buffer allocations. + /// The output stream where deflated output is written. + /// The compression level. + public DeflaterOutputStream(MemoryAllocator memoryAllocator, Stream rawStream, int compressionLevel) + { + this.rawStream = rawStream; + this.memoryOwner = memoryAllocator.Allocate(BufferLength); + this.buffer = this.memoryOwner.Memory; + this.deflater = new Deflater(memoryAllocator, compressionLevel); + } + + /// + public override bool CanRead => false; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => this.rawStream.CanWrite; + + /// + public override long Length => this.rawStream.Length; + + /// + public override long Position + { + get => this.rawStream.Position; + + set => throw new NotSupportedException(); + } + + /// + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + /// + public override void SetLength(long value) => throw new NotSupportedException(); + + /// + public override int ReadByte() => throw new NotSupportedException(); + + /// + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + /// + public override void Flush() + { + this.deflater.Flush(); + this.Deflate(true); + this.rawStream.Flush(); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + this.deflater.SetInput(buffer, offset, count); + this.Deflate(); + } + + private void Deflate() => this.Deflate(false); + + private void Deflate(bool flushing) + { + while (flushing || !this.deflater.IsNeedingInput) + { + int deflateCount = this.deflater.Deflate(this.buffer.Span, 0, BufferLength); + + if (deflateCount <= 0) + { + break; + } + + this.rawStream.Write(this.buffer.Span[..deflateCount]); + } + + if (!this.deflater.IsNeedingInput) + { + DeflateThrowHelper.ThrowNoDeflate(); + } + } + + private void Finish() + { + this.deflater.Finish(); + while (!this.deflater.IsFinished) + { + int len = this.deflater.Deflate(this.buffer.Span, 0, BufferLength); + if (len <= 0) + { + break; + } + + this.rawStream.Write(this.buffer.Span[..len]); + } + + if (!this.deflater.IsFinished) + { + DeflateThrowHelper.ThrowNoDeflate(); + } + + this.rawStream.Flush(); + } + + /// + protected override void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + this.Finish(); + this.deflater.Dispose(); + this.memoryOwner.Dispose(); + } + + this.isDisposed = true; + base.Dispose(disposing); + } + } + } +} diff --git a/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs b/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs new file mode 100644 index 0000000..777e029 --- /dev/null +++ b/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs @@ -0,0 +1,186 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Compression.Zlib { + /// + /// Stores pending data for writing data to the Deflater. + /// + internal sealed unsafe class DeflaterPendingBuffer : IDisposable + { + private readonly Memory buffer; + private readonly byte* pinnedBuffer; + private IMemoryOwner bufferMemoryOwner; + private MemoryHandle bufferMemoryHandle; + + private int start; + private int end; + private uint bits; + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator to use for buffer allocations. + public DeflaterPendingBuffer(MemoryAllocator memoryAllocator) + { + this.bufferMemoryOwner = memoryAllocator.Allocate(DeflaterConstants.PENDING_BUF_SIZE); + this.buffer = this.bufferMemoryOwner.Memory; + this.bufferMemoryHandle = this.buffer.Pin(); + this.pinnedBuffer = (byte*)this.bufferMemoryHandle.Pointer; + } + + /// + /// Gets the number of bits written to the buffer. + /// + public int BitCount { get; private set; } + + /// + /// Gets a value indicating whether indicates the buffer has been flushed. + /// + public bool IsFlushed => this.end == 0; + + /// + /// Clear internal state/buffers. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Reset() => this.start = this.end = this.BitCount = 0; + + /// + /// Write a short value to buffer LSB first. + /// + /// The value to write. + [MethodImpl(InliningOptions.ShortMethod)] + public void WriteShort(int value) + { + byte* pinned = this.pinnedBuffer; + pinned[this.end++] = unchecked((byte)value); + pinned[this.end++] = unchecked((byte)(value >> 8)); + } + + /// + /// Write a block of data to the internal buffer. + /// + /// The data to write. + /// The offset of first byte to write. + /// The number of bytes to write. + [MethodImpl(InliningOptions.ShortMethod)] + public void WriteBlock(ReadOnlySpan block, int offset, int length) + { + Unsafe.CopyBlockUnaligned( + ref this.buffer.Span[this.end], + ref MemoryMarshal.GetReference(block[offset..]), + unchecked((uint)length)); + + this.end += length; + } + + /// + /// Aligns internal buffer on a byte boundary. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void AlignToByte() + { + if (this.BitCount > 0) + { + byte* pinned = this.pinnedBuffer; + pinned[this.end++] = unchecked((byte)this.bits); + if (this.BitCount > 8) + { + pinned[this.end++] = unchecked((byte)(this.bits >> 8)); + } + } + + this.bits = 0; + this.BitCount = 0; + } + + /// + /// Write bits to internal buffer + /// + /// source of bits + /// number of bits to write + [MethodImpl(InliningOptions.ShortMethod)] + public void WriteBits(int b, int count) + { + this.bits |= (uint)(b << this.BitCount); + this.BitCount += count; + if (this.BitCount >= 16) + { + byte* pinned = this.pinnedBuffer; + pinned[this.end++] = unchecked((byte)this.bits); + pinned[this.end++] = unchecked((byte)(this.bits >> 8)); + this.bits >>= 16; + this.BitCount -= 16; + } + } + + /// + /// Write a short value to internal buffer most significant byte first + /// + /// The value to write + [MethodImpl(InliningOptions.ShortMethod)] + public void WriteShortMSB(int value) + { + byte* pinned = this.pinnedBuffer; + pinned[this.end++] = unchecked((byte)(value >> 8)); + pinned[this.end++] = unchecked((byte)value); + } + + /// + /// Flushes the pending buffer into the given output array. + /// If the output array is to small, only a partial flush is done. + /// + /// The output array. + /// The offset into output array. + /// The maximum number of bytes to store. + /// The number of bytes flushed. + public int Flush(Span output, int offset, int length) + { + if (this.BitCount >= 8) + { + this.pinnedBuffer[this.end++] = unchecked((byte)this.bits); + this.bits >>= 8; + this.BitCount -= 8; + } + + if (length > this.end - this.start) + { + length = this.end - this.start; + + Unsafe.CopyBlockUnaligned( + ref output[offset], + ref this.buffer.Span[this.start], + unchecked((uint)length)); + this.start = 0; + this.end = 0; + } + else + { + Unsafe.CopyBlockUnaligned( + ref output[offset], + ref this.buffer.Span[this.start], + unchecked((uint)length)); + this.start += length; + } + + return length; + } + + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.bufferMemoryHandle.Dispose(); + this.bufferMemoryOwner.Dispose(); + this.isDisposed = true; + } + } + } +} diff --git a/ImageSharp/Compression/Zlib/README.md b/ImageSharp/Compression/Zlib/README.md new file mode 100644 index 0000000..3875f98 --- /dev/null +++ b/ImageSharp/Compression/Zlib/README.md @@ -0,0 +1,11 @@ +DeflateStream implementation adapted from + +https://github.com/icsharpcode/SharpZipLib + +Licensed under MIT + +Crc32 and Adler32 SIMD implementation adapted from + +https://github.com/chromium/chromium + +Licensed under BSD 3-Clause "New" or "Revised" License diff --git a/ImageSharp/Compression/Zlib/ZlibDeflateStream.cs b/ImageSharp/Compression/Zlib/ZlibDeflateStream.cs new file mode 100644 index 0000000..a2de311 --- /dev/null +++ b/ImageSharp/Compression/Zlib/ZlibDeflateStream.cs @@ -0,0 +1,179 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Compression.Zlib { + /// + /// Provides methods and properties for compressing streams by using the Zlib Deflate algorithm. + /// + internal sealed class ZlibDeflateStream : Stream + { + /// + /// The raw stream containing the uncompressed image data. + /// + private readonly Stream rawStream; + + /// + /// Computes the checksum for the data stream. + /// + private uint adler = Adler32.SeedValue; + + /// + /// A value indicating whether this instance of the given entity has been disposed. + /// + /// if this instance has been disposed; otherwise, . + /// + /// If the entity is disposed, it must not be disposed a second + /// time. The isDisposed field is set the first time the entity + /// is disposed. If the isDisposed field is true, then the Dispose() + /// method will not dispose again. This help not to prolong the entity's + /// life in the Garbage Collector. + /// + private bool isDisposed; + + /// + /// The stream responsible for compressing the input stream. + /// + private DeflaterOutputStream deflateStream; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator to use for buffer allocations. + /// The stream to compress. + /// The compression level. + public ZlibDeflateStream(MemoryAllocator memoryAllocator, Stream stream, DeflateCompressionLevel level) + : this(memoryAllocator, stream, (PngCompressionLevel)level) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator to use for buffer allocations. + /// The stream to compress. + /// The compression level. + public ZlibDeflateStream(MemoryAllocator memoryAllocator, Stream stream, PngCompressionLevel level) + { + int compressionLevel = (int)level; + this.rawStream = stream; + + // Write the zlib header : http://tools.ietf.org/html/rfc1950 + // CMF(Compression Method and flags) + // This byte is divided into a 4 - bit compression method and a + // 4-bit information field depending on the compression method. + // bits 0 to 3 CM Compression method + // bits 4 to 7 CINFO Compression info + // + // 0 1 + // +---+---+ + // |CMF|FLG| + // +---+---+ + const int Cmf = 0x78; + int flg = 218; + + // http://stackoverflow.com/a/2331025/277304 + if (compressionLevel >= 5 && compressionLevel <= 6) + { + flg = 156; + } + else if (compressionLevel >= 3 && compressionLevel <= 4) + { + flg = 94; + } + else if (compressionLevel <= 2) + { + flg = 1; + } + + // Just in case + flg -= ((Cmf * 256) + flg) % 31; + + if (flg < 0) + { + flg += 31; + } + + this.rawStream.WriteByte(Cmf); + this.rawStream.WriteByte((byte)flg); + + this.deflateStream = new DeflaterOutputStream(memoryAllocator, this.rawStream, compressionLevel); + } + + /// + public override bool CanRead => false; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => this.rawStream.CanWrite; + + /// + public override long Length => this.rawStream.Length; + + /// + public override long Position + { + get + { + return this.rawStream.Position; + } + + set + { + throw new NotSupportedException(); + } + } + + /// + public override void Flush() => this.deflateStream.Flush(); + + /// + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + /// + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + /// + public override void SetLength(long value) => throw new NotSupportedException(); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public override void Write(byte[] buffer, int offset, int count) + { + this.deflateStream.Write(buffer, offset, count); + this.adler = Adler32.Calculate(this.adler, buffer.AsSpan(offset, count)); + } + + /// + protected override void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + if (disposing) + { + // dispose managed resources + this.deflateStream.Dispose(); + + // Add the crc + uint crc = this.adler; + this.rawStream.WriteByte((byte)((crc >> 24) & 0xFF)); + this.rawStream.WriteByte((byte)((crc >> 16) & 0xFF)); + this.rawStream.WriteByte((byte)((crc >> 8) & 0xFF)); + this.rawStream.WriteByte((byte)(crc & 0xFF)); + } + + base.Dispose(disposing); + this.isDisposed = true; + } + } +} diff --git a/ImageSharp/Compression/Zlib/ZlibInflateStream.cs b/ImageSharp/Compression/Zlib/ZlibInflateStream.cs new file mode 100644 index 0000000..bdc7d73 --- /dev/null +++ b/ImageSharp/Compression/Zlib/ZlibInflateStream.cs @@ -0,0 +1,308 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Compression; +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp.Compression.Zlib { + /// + /// Provides methods and properties for deframing streams from PNGs. + /// + internal sealed class ZlibInflateStream : Stream + { + /// + /// Used to read the Adler-32 and Crc-32 checksums. + /// We don't actually use this for anything so it doesn't + /// have to be threadsafe. + /// + private static readonly byte[] ChecksumBuffer = new byte[4]; + + /// + /// A default delegate to get more data from the inner stream. + /// + private static readonly Func GetDataNoOp = () => 0; + + /// + /// The inner raw memory stream. + /// + private readonly BufferedReadStream innerStream; + + /// + /// A value indicating whether this instance of the given entity has been disposed. + /// + /// if this instance has been disposed; otherwise, . + /// + /// If the entity is disposed, it must not be disposed a second + /// time. The isDisposed field is set the first time the entity + /// is disposed. If the isDisposed field is true, then the Dispose() + /// method will not dispose again. This help not to prolong the entity's + /// life in the Garbage Collector. + /// + private bool isDisposed; + + /// + /// The current data remaining to be read. + /// + private int currentDataRemaining; + + /// + /// Delegate to get more data once we've exhausted the current data remaining. + /// + private readonly Func getData; + + /// + /// When true, the inflated payload is treated as a raw DEFLATE stream with no zlib + /// CMF/FLG header (and no Adler-32 trailer). This is required to decode IDATs in + /// Apple's proprietary CgBI PNG variant. + /// + private readonly bool noHeader; + + /// + /// Initializes a new instance of the class. + /// + /// The inner raw stream. + public ZlibInflateStream(BufferedReadStream innerStream) + : this(innerStream, GetDataNoOp, noHeader: false) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The inner raw stream. + /// A delegate to get more data from the inner stream. + public ZlibInflateStream(BufferedReadStream innerStream, Func getData) + : this(innerStream, getData, noHeader: false) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The inner raw stream. + /// A delegate to get more data from the inner stream. + /// + /// When , the payload is treated as raw DEFLATE with no zlib header. + /// + public ZlibInflateStream(BufferedReadStream innerStream, Func getData, bool noHeader) + { + this.innerStream = innerStream; + this.getData = getData; + this.noHeader = noHeader; + } + + /// + public override bool CanRead => this.innerStream.CanRead; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => throw new NotSupportedException(); + + /// + public override long Length => throw new NotSupportedException(); + + /// + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + /// + /// Gets the compressed stream over the deframed inner stream. + /// + public DeflateStream? CompressedStream { get; private set; } + + /// + /// Adds new bytes from a frame found in the original stream. + /// + /// The current remaining data according to the chunk length. + /// Whether the chunk to be inflated is a critical chunk. + /// The . + [MemberNotNullWhen(true, nameof(CompressedStream))] + public bool AllocateNewBytes(int bytes, bool isCriticalChunk) + { + this.currentDataRemaining = bytes; + if (this.CompressedStream is null) + { + return this.InitializeInflateStream(isCriticalChunk); + } + + return true; + } + + /// + public override void Flush() => throw new NotSupportedException(); + + /// + public override int ReadByte() + { + this.currentDataRemaining--; + return this.innerStream.ReadByte(); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + if (this.currentDataRemaining is 0) + { + // Last buffer was read in its entirety, let's make sure we don't actually have more in additional IDAT chunks. + this.currentDataRemaining = this.getData(); + + if (this.currentDataRemaining is 0) + { + return 0; + } + } + + int bytesToRead = Math.Min(count, this.currentDataRemaining); + this.currentDataRemaining -= bytesToRead; + int totalBytesRead = this.innerStream.Read(buffer, offset, bytesToRead); + long innerStreamLength = this.innerStream.Length; + + // Keep reading data until we've reached the end of the stream or filled the buffer. + int bytesRead = 0; + offset += totalBytesRead; + while (this.currentDataRemaining is 0 && totalBytesRead < count) + { + this.currentDataRemaining = this.getData(); + + if (this.currentDataRemaining is 0) + { + return totalBytesRead; + } + + offset += bytesRead; + + if (offset >= innerStreamLength || offset >= count) + { + return totalBytesRead; + } + + bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining); + this.currentDataRemaining -= bytesToRead; + bytesRead = this.innerStream.Read(buffer, offset, bytesToRead); + if (bytesRead == 0) + { + return totalBytesRead; + } + + totalBytesRead += bytesRead; + } + + return totalBytesRead; + } + + /// + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + /// + public override void SetLength(long value) => throw new NotSupportedException(); + + /// + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + /// + protected override void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + if (disposing) + { + // Dispose managed resources. + if (this.CompressedStream != null) + { + this.CompressedStream.Dispose(); + this.CompressedStream = null; + } + } + + base.Dispose(disposing); + + // Call the appropriate methods to clean up + // unmanaged resources here. + // Note disposing is done. + this.isDisposed = true; + } + + [MemberNotNullWhen(true, nameof(CompressedStream))] + private bool InitializeInflateStream(bool isCriticalChunk) + { + // Apple CgBI IDATs omit the zlib CMF/FLG header and the Adler-32 trailer, + // wrapping a raw DEFLATE payload directly. Skip the header parsing in that mode. + if (this.noHeader) + { + this.CompressedStream = new DeflateStream(this, CompressionMode.Decompress, true); + return true; + } + + // Read the zlib header : http://tools.ietf.org/html/rfc1950 + // CMF(Compression Method and flags) + // This byte is divided into a 4 - bit compression method and a + // 4-bit information field depending on the compression method. + // bits 0 to 3 CM Compression method + // bits 4 to 7 CINFO Compression info + // + // 0 1 + // +---+---+ + // |CMF|FLG| + // +---+---+ + int cmf = this.innerStream.ReadByte(); + int flag = this.innerStream.ReadByte(); + this.currentDataRemaining -= 2; + if (cmf == -1 || flag == -1) + { + return false; + } + + if ((cmf & 0x0F) == 8) + { + // CINFO is the base-2 logarithm of the LZ77 window size, minus eight. + int cinfo = (cmf & 0xF0) >> 4; + + if (cinfo > 7) + { + if (isCriticalChunk) + { + // Values of CINFO above 7 are not allowed in RFC1950. + // CINFO is not defined in this specification for CM not equal to 8. + throw new ImageFormatException($"Invalid window size for ZLIB header: cinfo={cinfo}"); + } + + return false; + } + } + else if (isCriticalChunk) + { + throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}"); + } + else + { + return false; + } + + // The preset dictionary. + bool fdict = (flag & 32) != 0; + if (fdict) + { + // We don't need this for inflate so simply skip by the next four bytes. + // https://tools.ietf.org/html/rfc1950#page-6 + if (this.innerStream.Read(ChecksumBuffer, 0, 4) != 4) + { + return false; + } + + this.currentDataRemaining -= 4; + } + + // Initialize the deflate BufferedReadStream. + this.CompressedStream = new DeflateStream(this, CompressionMode.Decompress, true); + + return true; + } + } +} diff --git a/ImageSharp/Configuration.cs b/ImageSharp/Configuration.cs new file mode 100644 index 0000000..7724059 --- /dev/null +++ b/ImageSharp/Configuration.cs @@ -0,0 +1,237 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Cur; +using SixLabors.ImageSharp.Formats.Exr; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Ico; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Pbm; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Qoi; +using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp { + /// + /// Provides configuration which allows altering default behaviour or extending the library. + /// + public sealed class Configuration + { + /// + /// A lazily initialized configuration default instance. + /// + private static readonly Lazy Lazy = new(CreateDefaultInstance); + private const int DefaultStreamProcessingBufferSize = 8096; + private int streamProcessingBufferSize = DefaultStreamProcessingBufferSize; + private int maxDegreeOfParallelism = Environment.ProcessorCount; + private MemoryAllocator memoryAllocator = MemoryAllocator.Default; + + /// + /// Initializes a new instance of the class. + /// + public Configuration() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// A collection of configuration modules to register. + public Configuration(params IImageFormatConfigurationModule[] configurationModules) + { + if (configurationModules != null) + { + foreach (IImageFormatConfigurationModule p in configurationModules) + { + p.Configure(this); + } + } + } + + /// + /// Gets the default instance. + /// + public static Configuration Default { get; } = Lazy.Value; + + /// + /// Gets or sets the maximum number of concurrent tasks enabled in ImageSharp algorithms + /// configured with this instance. + /// A positive value limits the number of concurrent operations to the set value. + /// If set to -1, there is no limit on the number of concurrently running operations. + /// Defaults to . + /// + public int MaxDegreeOfParallelism + { + get => this.maxDegreeOfParallelism; + set + { + if (value is 0 or < -1) + { + throw new ArgumentOutOfRangeException(nameof(this.MaxDegreeOfParallelism)); + } + + this.maxDegreeOfParallelism = value; + } + } + + /// + /// Gets or sets the size of the buffer to use when working with streams. + /// Initialized with by default. + /// + public int StreamProcessingBufferSize + { + get => this.streamProcessingBufferSize; + set + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); + + this.streamProcessingBufferSize = value; + } + } + + /// + /// Gets or sets a value indicating whether to force image buffers to be contiguous whenever possible. + /// + /// + /// Contiguous allocations are not possible, if the image needs a buffer larger than . + /// + public bool PreferContiguousImageBuffers { get; set; } + + /// + /// Gets a set of properties for the Configuration. + /// + /// This can be used for storing global settings and defaults to be accessible to processors. + public IDictionary Properties { get; } = new ConcurrentDictionary(); + + /// + /// Gets the currently registered s. + /// + public IEnumerable ImageFormats => this.ImageFormatsManager.ImageFormats; + + /// + /// Gets or sets the position in a stream to use for reading when using a seekable stream as an image data source. + /// + public ReadOrigin ReadOrigin { get; set; } = ReadOrigin.Current; + + /// + /// Gets or the that is currently in use. + /// + public ImageFormatManager ImageFormatsManager { get; private set; } = new(); + + /// + /// Gets or sets the that is currently in use. + /// Defaults to . + /// + /// Allocators are expensive, so it is strongly recommended to use only one busy instance per process. + /// In case you need to customize it, you can ensure this by changing + /// + /// + /// It's possible to reduce allocator footprint by assigning a custom instance created with + /// , but note that since the default pooling + /// allocators are expensive, it is strictly recommended to use a single process-wide allocator. + /// You can ensure this by altering the allocator of , or by implementing custom application logic that + /// manages allocator lifetime. + /// + /// If an allocator has to be dropped for some reason, + /// shall be invoked after disposing all associated instances. + /// + public MemoryAllocator MemoryAllocator + { + get => this.memoryAllocator; + set + { + Guard.NotNull(value, nameof(this.MemoryAllocator)); + this.memoryAllocator = value; + } + } + + /// + /// Gets the maximum header size of all the formats. + /// + internal int MaxHeaderSize => this.ImageFormatsManager.MaxHeaderSize; + + /// + /// Gets or sets the filesystem helper for accessing the local file system. + /// + internal IFileSystem FileSystem { get; set; } = new LocalFileSystem(); + + /// + /// Gets or sets the working buffer size hint for image processors. + /// The default value is 1MB. + /// + /// + /// Currently only used by Resize. If the working buffer is expected to be discontiguous, + /// min(WorkingBufferSizeHintInBytes, BufferCapacityInBytes) should be used. + /// + internal int WorkingBufferSizeHintInBytes { get; set; } = 1 * 1024 * 1024; + + /// + /// Gets or sets the image operations provider factory. + /// + internal IImageProcessingContextFactory ImageOperationsProvider { get; set; } = new DefaultImageOperationsProviderFactory(); + + /// + /// Registers a new format provider. + /// + /// The configuration provider to call configure on. + public void Configure(IImageFormatConfigurationModule configuration) + { + Guard.NotNull(configuration, nameof(configuration)); + configuration.Configure(this); + } + + /// + /// Creates a shallow copy of the . + /// + /// A new configuration instance. + public Configuration Clone() => new() + { + MaxDegreeOfParallelism = this.MaxDegreeOfParallelism, + StreamProcessingBufferSize = this.StreamProcessingBufferSize, + ImageFormatsManager = this.ImageFormatsManager, + memoryAllocator = this.memoryAllocator, + ImageOperationsProvider = this.ImageOperationsProvider, + ReadOrigin = this.ReadOrigin, + FileSystem = this.FileSystem, + WorkingBufferSizeHintInBytes = this.WorkingBufferSizeHintInBytes, + }; + + /// + /// Creates the default instance with the following s preregistered: + /// + /// + /// + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// + /// The default configuration of . + internal static Configuration CreateDefaultInstance() => new( + new PngConfigurationModule(), + new JpegConfigurationModule(), + new GifConfigurationModule(), + new BmpConfigurationModule(), + new PbmConfigurationModule(), + new TgaConfigurationModule(), + new TiffConfigurationModule(), + new WebpConfigurationModule(), + new ExrConfigurationModule(), + new QoiConfigurationModule(), + new IcoConfigurationModule(), + new CurConfigurationModule()); + } +} diff --git a/ImageSharp/Diagnostics/MemoryDiagnostics.cs b/ImageSharp/Diagnostics/MemoryDiagnostics.cs new file mode 100644 index 0000000..cc9e715 --- /dev/null +++ b/ImageSharp/Diagnostics/MemoryDiagnostics.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Threading; + +namespace SixLabors.ImageSharp.Diagnostics { + /// + /// Represents the method to handle . + /// + /// The allocation stack trace. + public delegate void UndisposedAllocationDelegate(string allocationStackTrace); + + /// + /// Utilities to track memory usage and detect memory leaks from not disposing ImageSharp objects. + /// + public static class MemoryDiagnostics + { + private static int totalUndisposedAllocationCount; + + private static UndisposedAllocationDelegate? undisposedAllocation; + private static int undisposedAllocationSubscriptionCounter; + private static readonly object SyncRoot = new(); + + /// + /// Fires when an ImageSharp object's undisposed memory resource leaks to the finalizer. + /// The event brings significant overhead, and is intended to be used for troubleshooting only. + /// For production diagnostics, use . + /// + public static event UndisposedAllocationDelegate UndisposedAllocation + { + add + { + lock (SyncRoot) + { + undisposedAllocationSubscriptionCounter++; + undisposedAllocation += value; + } + } + + remove + { + lock (SyncRoot) + { + undisposedAllocation -= value; + undisposedAllocationSubscriptionCounter--; + } + } + } + + /// + /// Fires when ImageSharp allocates memory from a MemoryAllocator + /// + internal static event Action? MemoryAllocated; + + /// + /// Fires when ImageSharp releases memory allocated from a MemoryAllocator + /// + internal static event Action? MemoryReleased; + + /// + /// Gets a value indicating the total number of memory resource objects leaked to the finalizer. + /// + public static int TotalUndisposedAllocationCount => totalUndisposedAllocationCount; + + internal static bool UndisposedAllocationSubscribed => Volatile.Read(ref undisposedAllocationSubscriptionCounter) > 0; + + internal static void IncrementTotalUndisposedAllocationCount() + { + Interlocked.Increment(ref totalUndisposedAllocationCount); + MemoryAllocated?.Invoke(); + } + + internal static void DecrementTotalUndisposedAllocationCount() + { + Interlocked.Decrement(ref totalUndisposedAllocationCount); + MemoryReleased?.Invoke(); + } + + internal static void RaiseUndisposedMemoryResource(string allocationStackTrace) + { + if (undisposedAllocation is null) + { + return; + } + + // Schedule on the ThreadPool, to avoid user callback messing up the finalizer thread. + ThreadPool.QueueUserWorkItem( + stackTrace => undisposedAllocation?.Invoke(stackTrace), + allocationStackTrace, + preferLocal: false); + } + } +} diff --git a/ImageSharp/Formats/AlphaAwareImageEncoder.cs b/ImageSharp/Formats/AlphaAwareImageEncoder.cs new file mode 100644 index 0000000..a4c2e00 --- /dev/null +++ b/ImageSharp/Formats/AlphaAwareImageEncoder.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Formats { + /// + /// Acts as a base encoder for all formats that are aware of and can handle alpha transparency. + /// + public abstract class AlphaAwareImageEncoder : ImageEncoder + { + /// + /// Gets or initializes the mode that determines how transparent pixels are handled during encoding. + /// This overrides any other settings that may affect the encoding of transparent pixels + /// including those passed via . + /// + public TransparentColorMode TransparentColorMode { get; init; } + } +} diff --git a/ImageSharp/Formats/AnimationUtilities.cs b/ImageSharp/Formats/AnimationUtilities.cs new file mode 100644 index 0000000..59d17d1 --- /dev/null +++ b/ImageSharp/Formats/AnimationUtilities.cs @@ -0,0 +1,291 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats { + /// + /// Utility methods for animated formats. + /// + internal static class AnimationUtilities + { + /// + /// Deduplicates pixels between the previous and current frame returning only the changed pixels and bounds. + /// + /// The type of pixel format. + /// The configuration. + /// The previous frame if present. + /// The current frame. + /// The next frame if present. + /// The resultant output. + /// The value to use when replacing duplicate pixels. + /// Whether the resultant frame represents an animation blend. + /// The clamping bound to apply when calculating difference bounds. + /// The representing the operation result. + public static (bool Difference, Rectangle Bounds) DeDuplicatePixels( + Configuration configuration, + ImageFrame? previousFrame, + ImageFrame currentFrame, + ImageFrame? nextFrame, + ImageFrame resultFrame, + Color replacement, + bool blend, + ClampingMode clampingMode = ClampingMode.None) + where TPixel : unmanaged, IPixel + { + MemoryAllocator memoryAllocator = configuration.MemoryAllocator; + using IMemoryOwner buffers = memoryAllocator.Allocate(currentFrame.Width * 4, AllocationOptions.Clean); + Span previous = buffers.GetSpan()[..currentFrame.Width]; + Span current = buffers.GetSpan().Slice(currentFrame.Width, currentFrame.Width); + Span next = buffers.GetSpan().Slice(currentFrame.Width * 2, currentFrame.Width); + Span result = buffers.GetSpan()[(currentFrame.Width * 3)..]; + + Rgba32 bg = replacement.ToPixel(); + + int top = int.MinValue; + int bottom = int.MaxValue; + int left = int.MaxValue; + int right = int.MinValue; + + bool hasDiff = false; + for (int y = 0; y < currentFrame.Height; y++) + { + if (previousFrame != null) + { + PixelOperations.Instance.ToRgba32(configuration, previousFrame.DangerousGetPixelRowMemory(y).Span, previous); + } + + PixelOperations.Instance.ToRgba32(configuration, currentFrame.DangerousGetPixelRowMemory(y).Span, current); + + if (nextFrame != null) + { + PixelOperations.Instance.ToRgba32(configuration, nextFrame.DangerousGetPixelRowMemory(y).Span, next); + } + + ref Vector256 previousBase256 = ref Unsafe.As>(ref MemoryMarshal.GetReference(previous)); + ref Vector256 currentBase256 = ref Unsafe.As>(ref MemoryMarshal.GetReference(current)); + ref Vector256 nextBase256 = ref Unsafe.As>(ref MemoryMarshal.GetReference(next)); + ref Vector256 resultBase256 = ref Unsafe.As>(ref MemoryMarshal.GetReference(result)); + + int i = 0; + uint x = 0; + bool hasRowDiff = false; + int length = current.Length; + int remaining = current.Length; + + if (Avx2.IsSupported && remaining >= 8) + { + Vector256 r256 = previousFrame != null ? Vector256.Create(bg.PackedValue) : Vector256.Zero; + Vector256 vmb256 = Vector256.Zero; + if (blend) + { + vmb256 = Avx2.CompareEqual(vmb256, vmb256); + } + + while (remaining >= 8) + { + Vector256 p = Unsafe.Add(ref previousBase256, x).AsUInt32(); + Vector256 c = Unsafe.Add(ref currentBase256, x).AsUInt32(); + + Vector256 eq = Avx2.CompareEqual(p, c); + Vector256 r = Avx2.BlendVariable(c, r256, Avx2.And(eq, vmb256)); + + if (nextFrame != null) + { + Vector256 n = Avx2.ShiftRightLogical(Unsafe.Add(ref nextBase256, x).AsUInt32(), 24).AsInt32(); + eq = Avx2.AndNot(Avx2.CompareGreaterThan(Avx2.ShiftRightLogical(c, 24).AsInt32(), n).AsUInt32(), eq); + } + + Unsafe.Add(ref resultBase256, x) = r.AsByte(); + + uint msk = (uint)Avx2.MoveMask(eq.AsByte()); + msk = ~msk; + + if (msk != 0) + { + // If is diff is found, the left side is marked by the min of previously found left side and the start position. + // The right is the max of the previously found right side and the end position. + int start = i + (BitOperations.TrailingZeroCount(msk) / sizeof(uint)); + int end = i + (8 - (BitOperations.LeadingZeroCount(msk) / sizeof(uint))); + left = Math.Min(left, start); + right = Math.Max(right, end); + hasRowDiff = true; + hasDiff = true; + } + + x++; + i += 8; + remaining -= 8; + } + } + + if (Sse2.IsSupported && remaining >= 4) + { + // Update offset since we may be operating on the remainder previously incremented by pixel steps of 8. + x *= 2; + Vector128 r128 = previousFrame != null ? Vector128.Create(bg.PackedValue) : Vector128.Zero; + Vector128 vmb128 = Vector128.Zero; + if (blend) + { + vmb128 = Sse2.CompareEqual(vmb128, vmb128); + } + + while (remaining >= 4) + { + Vector128 p = Unsafe.Add(ref Unsafe.As, Vector128>(ref previousBase256), x); + Vector128 c = Unsafe.Add(ref Unsafe.As, Vector128>(ref currentBase256), x); + + Vector128 eq = Sse2.CompareEqual(p, c); + Vector128 r = SimdUtils.HwIntrinsics.BlendVariable(c, r128, Sse2.And(eq, vmb128)); + + if (nextFrame != null) + { + Vector128 n = Sse2.ShiftRightLogical(Unsafe.Add(ref Unsafe.As, Vector128>(ref nextBase256), x), 24).AsInt32(); + eq = Sse2.AndNot(Sse2.CompareGreaterThan(Sse2.ShiftRightLogical(c, 24).AsInt32(), n).AsUInt32(), eq); + } + + Unsafe.Add(ref Unsafe.As, Vector128>(ref resultBase256), x) = r; + + ushort msk = (ushort)(uint)Sse2.MoveMask(eq.AsByte()); + msk = (ushort)~msk; + if (msk != 0) + { + // If is diff is found, the left side is marked by the min of previously found left side and the start position. + // The right is the max of the previously found right side and the end position. + int start = i + (SimdUtils.HwIntrinsics.TrailingZeroCount(msk) / sizeof(uint)); + int end = i + (4 - (SimdUtils.HwIntrinsics.LeadingZeroCount(msk) / sizeof(uint))); + left = Math.Min(left, start); + right = Math.Max(right, end); + hasRowDiff = true; + hasDiff = true; + } + + x++; + i += 4; + remaining -= 4; + } + } + + if (AdvSimd.IsSupported && remaining >= 4) + { + // Update offset since we may be operating on the remainder previously incremented by pixel steps of 8. + x *= 2; + Vector128 r128 = previousFrame != null ? Vector128.Create(bg.PackedValue) : Vector128.Zero; + Vector128 vmb128 = Vector128.Zero; + if (blend) + { + vmb128 = AdvSimd.CompareEqual(vmb128, vmb128); + } + + while (remaining >= 4) + { + Vector128 p = Unsafe.Add(ref Unsafe.As, Vector128>(ref previousBase256), x); + Vector128 c = Unsafe.Add(ref Unsafe.As, Vector128>(ref currentBase256), x); + + Vector128 eq = AdvSimd.CompareEqual(p, c); + Vector128 r = SimdUtils.HwIntrinsics.BlendVariable(c, r128, AdvSimd.And(eq, vmb128)); + + if (nextFrame != null) + { + Vector128 n = AdvSimd.ShiftRightLogical(Unsafe.Add(ref Unsafe.As, Vector128>(ref nextBase256), x), 24).AsInt32(); + eq = AdvSimd.BitwiseClear(eq, AdvSimd.CompareGreaterThan(AdvSimd.ShiftRightLogical(c, 24).AsInt32(), n).AsUInt32()); + } + + Unsafe.Add(ref Unsafe.As, Vector128>(ref resultBase256), x) = r; + + ulong msk = ~AdvSimd.ExtractNarrowingLower(eq).AsUInt64().ToScalar(); + if (msk != 0) + { + // If is diff is found, the left side is marked by the min of previously found left side and the start position. + // The right is the max of the previously found right side and the end position. + int start = i + (BitOperations.TrailingZeroCount(msk) / 16); + int end = i + (4 - (BitOperations.LeadingZeroCount(msk) / 16)); + left = Math.Min(left, start); + right = Math.Max(right, end); + hasRowDiff = true; + hasDiff = true; + } + + x++; + i += 4; + remaining -= 4; + } + } + + for (i = remaining; i > 0; i--) + { + x = (uint)(length - i); + + Rgba32 p = Unsafe.Add(ref MemoryMarshal.GetReference(previous), x); + Rgba32 c = Unsafe.Add(ref MemoryMarshal.GetReference(current), x); + Rgba32 n = Unsafe.Add(ref MemoryMarshal.GetReference(next), x); + ref Rgba32 r = ref Unsafe.Add(ref MemoryMarshal.GetReference(result), x); + + bool peq = c.Rgba == (previousFrame != null ? p.Rgba : bg.Rgba); + Rgba32 val = (blend & peq) ? bg : c; + + peq &= nextFrame == null || (n.Rgba >> 24 >= c.Rgba >> 24); + r = val; + + if (!peq) + { + // If is diff is found, the left side is marked by the min of previously found left side and the diff position. + // The right is the max of the previously found right side and the diff position + 1. + left = Math.Min(left, (int)x); + right = Math.Max(right, (int)x + 1); + hasRowDiff = true; + hasDiff = true; + } + } + + if (hasRowDiff) + { + if (top == int.MinValue) + { + top = y; + } + + bottom = y + 1; + } + + PixelOperations.Instance.FromRgba32(configuration, result, resultFrame.DangerousGetPixelRowMemory(y).Span); + } + + Rectangle bounds = Rectangle.FromLTRB( + left = Numerics.Clamp(left, 0, resultFrame.Width - 1), + top = Numerics.Clamp(top, 0, resultFrame.Height - 1), + Numerics.Clamp(right, left + 1, resultFrame.Width), + Numerics.Clamp(bottom, top + 1, resultFrame.Height)); + + // Webp requires even bounds + if (clampingMode == ClampingMode.Even) + { + bounds.Width = Math.Min(resultFrame.Width, bounds.Width + (bounds.X & 1)); + bounds.Height = Math.Min(resultFrame.Height, bounds.Height + (bounds.Y & 1)); + bounds.X = Math.Max(0, bounds.X - (bounds.X & 1)); + bounds.Y = Math.Max(0, bounds.Y - (bounds.Y & 1)); + } + + return (hasDiff, bounds); + } + } + +#pragma warning disable SA1201 // Elements should appear in the correct order + internal enum ClampingMode +#pragma warning restore SA1201 // Elements should appear in the correct order + { + None, + + Even, + } +} diff --git a/ImageSharp/Formats/Bmp/BmpArrayFileHeader.cs b/ImageSharp/Formats/Bmp/BmpArrayFileHeader.cs new file mode 100644 index 0000000..4deab0c --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpArrayFileHeader.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Bmp { + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal readonly struct BmpArrayFileHeader + { + public BmpArrayFileHeader(short type, int size, int offsetToNext, short width, short height) + { + this.Type = type; + this.Size = size; + this.OffsetToNext = offsetToNext; + this.ScreenWidth = width; + this.ScreenHeight = height; + } + + /// + /// Gets the Bitmap identifier. + /// The field used to identify the bitmap file: 0x42 0x41 (Hex code points for B and A). + /// + public short Type { get; } + + /// + /// Gets the size of this header. + /// + public int Size { get; } + + /// + /// Gets the offset to next OS2BMPARRAYFILEHEADER. + /// This offset is calculated from the starting byte of the file. A value of zero indicates that this header is for the last image in the array list. + /// + public int OffsetToNext { get; } + + /// + /// Gets the width of the image display in pixels. + /// + public short ScreenWidth { get; } + + /// + /// Gets the height of the image display in pixels. + /// + public short ScreenHeight { get; } + + public static BmpArrayFileHeader Parse(Span data) + { + return MemoryMarshal.Cast(data)[0]; + } + } +} diff --git a/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs b/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs new file mode 100644 index 0000000..d7fec9b --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Enumerates the available bits per pixel the bitmap encoder supports. + /// + public enum BmpBitsPerPixel : short + { + /// + /// 1 bit per pixel. + /// + Bit1 = 1, + + /// + /// 2 bits per pixel. + /// + Bit2 = 2, + + /// + /// 4 bits per pixel. + /// + Bit4 = 4, + + /// + /// 8 bits per pixel. Each pixel consists of 1 byte. + /// + Bit8 = 8, + + /// + /// 16 bits per pixel. Each pixel consists of 2 bytes. + /// + Bit16 = 16, + + /// + /// 24 bits per pixel. Each pixel consists of 3 bytes. + /// + Bit24 = 24, + + /// + /// 32 bits per pixel. Each pixel consists of 4 bytes. + /// + Bit32 = 32 + } +} diff --git a/ImageSharp/Formats/Bmp/BmpColorSpace.cs b/ImageSharp/Formats/Bmp/BmpColorSpace.cs new file mode 100644 index 0000000..a83d706 --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpColorSpace.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Enum for the different color spaces. + /// + internal enum BmpColorSpace + { + /// + /// This value implies that endpoints and gamma values are given in the appropriate fields. + /// + LCS_CALIBRATED_RGB = 0, + + /// + /// The Windows default color space ('Win '). + /// + LCS_WINDOWS_COLOR_SPACE = 1466527264, + + /// + /// Specifies that the bitmap is in sRGB color space ('sRGB'). + /// + LCS_sRGB = 1934772034, + + /// + /// This value indicates that bV5ProfileData points to the file name of the profile to use (gamma and endpoints values are ignored). + /// + PROFILE_LINKED = 1279872587, + + /// + /// This value indicates that bV5ProfileData points to a memory buffer that contains the profile to be used (gamma and endpoints values are ignored). + /// + PROFILE_EMBEDDED = 1296188740 + } +} diff --git a/ImageSharp/Formats/Bmp/BmpCompression.cs b/ImageSharp/Formats/Bmp/BmpCompression.cs new file mode 100644 index 0000000..3e0b789 --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpCompression.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Defines the compression type of the image data + /// in the bitmap file. + /// + internal enum BmpCompression : int + { + /// + /// Each image row has a multiple of four elements. If the + /// row has less elements, zeros will be added at the right side. + /// The format depends on the number of bits, stored in the info header. + /// If the number of bits are one, four or eight each pixel data is + /// a index to the palette. If the number of bits are sixteen, + /// twenty-four or thirty-two each pixel contains a color. + /// + RGB = 0, + + /// + /// Two bytes are one data record. If the first byte is not zero, the + /// next byte will be repeated as much as the value of the first byte. + /// If the first byte is zero, the record has different meanings, depending + /// on the second byte. If the second byte is zero, it is the end of the row, + /// if it is one, it is the end of the image. + /// + RLE8 = 1, + + /// + /// Two bytes are one data record. If the first byte is not zero, the + /// next two half bytes will be repeated as much as the value of the first byte. + /// If the first byte is zero, the record has different meanings, depending + /// on the second byte. If the second byte is zero, it is the end of the row, + /// if it is one, it is the end of the image. + /// + RLE4 = 2, + + /// + /// Each image row has a multiple of four elements. If the + /// row has less elements, zeros will be added at the right side. + /// + BitFields = 3, + + /// + /// The bitmap contains a JPG image. + /// Not supported at the moment. + /// + JPEG = 4, + + /// + /// The bitmap contains a PNG image. + /// Not supported at the moment. + /// + PNG = 5, + + /// + /// Introduced with Windows CE. + /// Specifies that the bitmap is not compressed and that the color table consists of four DWORD color + /// masks that specify the red, green, blue, and alpha components of each pixel. + /// + BI_ALPHABITFIELDS = 6, + + /// + /// OS/2 specific compression type. + /// Similar to run length encoding of 4 and 8 bit. + /// The only difference is that run values encoded are three bytes in size (one byte per RGB color component), + /// rather than four or eight bits in size. + /// + /// Note: Because compression value of 4 is ambiguous for BI_RGB for windows and RLE24 for OS/2, the enum value is remapped + /// to a different value, to be clearly separate from valid windows values. + /// + RLE24 = 100, + } +} diff --git a/ImageSharp/Formats/Bmp/BmpConfigurationModule.cs b/ImageSharp/Formats/Bmp/BmpConfigurationModule.cs new file mode 100644 index 0000000..01b850b --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpConfigurationModule.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Registers the image encoders, decoders and mime type detectors for the bmp format. + /// + public sealed class BmpConfigurationModule : IImageFormatConfigurationModule + { + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetEncoder(BmpFormat.Instance, new BmpEncoder()); + configuration.ImageFormatsManager.SetDecoder(BmpFormat.Instance, BmpDecoder.Instance); + configuration.ImageFormatsManager.AddImageFormatDetector(new BmpImageFormatDetector()); + } + } +} diff --git a/ImageSharp/Formats/Bmp/BmpConstants.cs b/ImageSharp/Formats/Bmp/BmpConstants.cs new file mode 100644 index 0000000..f9edf56 --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpConstants.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Defines constants relating to BMPs + /// + internal static class BmpConstants + { + /// + /// The list of mimetypes that equate to a bmp. + /// + public static readonly IEnumerable MimeTypes = + [ + "image/bmp", + "image/x-windows-bmp", + "image/x-win-bitmap" + ]; + + /// + /// The list of file extensions that equate to a bmp. + /// + public static readonly IEnumerable FileExtensions = ["bm", "bmp", "dip"]; + + /// + /// Valid magic bytes markers identifying a Bitmap file. + /// + internal static class TypeMarkers + { + /// + /// Single-image BMP file that may have been created under Windows or OS/2. + /// + public const int Bitmap = 0x4D42; + + /// + /// OS/2 Bitmap Array. + /// + public const int BitmapArray = 0x4142; + + /// + /// OS/2 Color Icon. + /// + public const int ColorIcon = 0x4943; + + /// + /// OS/2 Color Pointer. + /// + public const int ColorPointer = 0x5043; + + /// + /// OS/2 Icon. + /// + public const int Icon = 0x4349; + + /// + /// OS/2 Pointer. + /// + public const int Pointer = 0x5450; + } + } +} diff --git a/ImageSharp/Formats/Bmp/BmpDecoder.cs b/ImageSharp/Formats/Bmp/BmpDecoder.cs new file mode 100644 index 0000000..2b7161b --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpDecoder.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Image decoder for generating an image out of a Windows bitmap stream. + /// + public sealed class BmpDecoder : SpecializedImageDecoder + { + private BmpDecoder() + { + } + + /// + /// Gets the shared instance. + /// + public static BmpDecoder Instance { get; } = new(); + + /// + protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + return new BmpDecoderCore(new BmpDecoderOptions { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken); + } + + /// + protected override Image Decode(BmpDecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + Image image = new BmpDecoderCore(options).Decode(options.GeneralOptions.Configuration, stream, cancellationToken); + + ScaleToTargetSize(options.GeneralOptions, image); + + return image; + } + + /// + protected override Image Decode(BmpDecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(options, stream, cancellationToken); + + /// + protected override BmpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) + => new() { GeneralOptions = options }; + } +} diff --git a/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/ImageSharp/Formats/Bmp/BmpDecoderCore.cs new file mode 100644 index 0000000..704edbb --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -0,0 +1,1666 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Performs the bitmap decoding operation. + /// + /// + /// A useful decoding source example can be found at + /// + internal sealed class BmpDecoderCore : ImageDecoderCore + { + /// + /// The default mask for the red part of the color for 16 bit rgb bitmaps. + /// + private const int DefaultRgb16RMask = 0x7C00; + + /// + /// The default mask for the green part of the color for 16 bit rgb bitmaps. + /// + private const int DefaultRgb16GMask = 0x3E0; + + /// + /// The default mask for the blue part of the color for 16 bit rgb bitmaps. + /// + private const int DefaultRgb16BMask = 0x1F; + + /// + /// RLE flag value that indicates following byte has special meaning. + /// + private const int RleCommand = 0x00; + + /// + /// RLE flag value marking end of a scan line. + /// + private const int RleEndOfLine = 0x00; + + /// + /// RLE flag value marking end of bitmap data. + /// + private const int RleEndOfBitmap = 0x01; + + /// + /// RLE flag value marking the start of [x,y] offset instruction. + /// + private const int RleDelta = 0x02; + + /// + /// The metadata. + /// + private ImageMetadata? metadata; + + /// + /// The bitmap specific metadata. + /// + private BmpMetadata? bmpMetadata; + + /// + /// The file header containing general information. + /// + private BmpFileHeader? fileHeader; + + /// + /// Indicates which bitmap file marker was read. + /// + private BmpFileMarkerType fileMarkerType; + + /// + /// The info header containing detailed information about the bitmap. + /// + private BmpInfoHeader infoHeader; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// How to deal with skipped pixels, + /// which can occur during decoding run length encoded bitmaps. + /// + private readonly RleSkippedPixelHandling rleSkippedPixelHandling; + + /// + private readonly bool processedAlphaMask; + + /// + private readonly bool skipFileHeader; + + /// + private readonly bool isDoubleHeight; + + /// + /// Initializes a new instance of the class. + /// + /// The options. + public BmpDecoderCore(BmpDecoderOptions options) + : base(options.GeneralOptions) + { + this.rleSkippedPixelHandling = options.RleSkippedPixelHandling; + this.configuration = options.GeneralOptions.Configuration; + this.memoryAllocator = this.configuration.MemoryAllocator; + this.processedAlphaMask = options.ProcessedAlphaMask; + this.skipFileHeader = options.SkipFileHeader; + this.isDoubleHeight = options.UseDoubleHeight; + } + + /// + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + { + Image? image = null; + try + { + int bytesPerColorMapEntry = this.ReadImageHeaders(stream, out bool inverted, out byte[] palette); + ushort bitsPerPixel = this.infoHeader.BitsPerPixel; + + image = new Image(this.configuration, this.infoHeader.Width, this.infoHeader.Height, this.metadata); + + Buffer2D pixels = image.GetRootFramePixelBuffer(); + + switch (this.infoHeader.Compression) + { + case BmpCompression.RGB when bitsPerPixel is 32 && this.bmpMetadata.InfoHeaderType is BmpInfoHeaderType.WinVersion3: + this.ReadRgb32Slow(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); + + break; + + case BmpCompression.RGB when bitsPerPixel is 32: + this.ReadRgb32Fast(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); + + break; + + case BmpCompression.RGB when bitsPerPixel is 24: + this.ReadRgb24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); + + break; + + case BmpCompression.RGB when bitsPerPixel is 16: + this.ReadRgb16(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); + + break; + + case BmpCompression.RGB when bitsPerPixel is > 0 and <= 8 && this.processedAlphaMask: + this.ReadRgbPaletteWithAlphaMask( + stream, + pixels, + palette, + this.infoHeader.Width, + this.infoHeader.Height, + this.infoHeader.BitsPerPixel, + bytesPerColorMapEntry, + inverted); + + break; + + case BmpCompression.RGB when bitsPerPixel is > 0 and <= 8: + this.ReadRgbPalette( + stream, + pixels, + palette, + this.infoHeader.Width, + this.infoHeader.Height, + this.infoHeader.BitsPerPixel, + bytesPerColorMapEntry, + inverted); + + break; + + case BmpCompression.RGB when bitsPerPixel is <= 0 or > 32: + BmpThrowHelper.ThrowInvalidImageContentException($"Invalid bits per pixel: {bitsPerPixel}"); + break; + + case BmpCompression.RLE24: + this.ReadRle24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); + + break; + + case BmpCompression.RLE8: + case BmpCompression.RLE4: + this.ReadRle(stream, this.infoHeader.Compression, pixels, palette, this.infoHeader.Width, this.infoHeader.Height, inverted); + + break; + + case BmpCompression.BitFields: + case BmpCompression.BI_ALPHABITFIELDS: + this.ReadBitFields(stream, pixels, inverted); + + break; + + default: + BmpThrowHelper.ThrowNotSupportedException("ImageSharp does not support this kind of bitmap files."); + + break; + } + + return image; + } + catch (IndexOutOfRangeException e) + { + image?.Dispose(); + throw new ImageFormatException("Bitmap does not have a valid format.", e); + } + catch + { + image?.Dispose(); + throw; + } + } + + /// + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + this.ReadImageHeaders(stream, out _, out _); + return new ImageInfo(new Size(this.infoHeader.Width, this.infoHeader.Height), this.metadata); + } + + /// + /// Returns the y- value based on the given height. + /// + /// The y- value representing the current row. + /// The height of the bitmap. + /// Whether the bitmap is inverted. + /// The representing the inverted value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Invert(int y, int height, bool inverted) => (!inverted) ? height - y - 1 : y; + + /// + /// Calculates the amount of bytes to pad a row. + /// + /// The image width. + /// The pixel component count. + /// + /// The padding. + /// + private static int CalculatePadding(int width, int componentCount) + { + int padding = (width * componentCount) % 4; + + if (padding != 0) + { + padding = 4 - padding; + } + + return padding; + } + + /// + /// Decodes a bitmap containing the BITFIELDS Compression type. For each color channel, there will be a bitmask + /// which will be used to determine which bits belong to that channel. + /// + /// The pixel format. + /// The containing image data. + /// The output pixel buffer containing the decoded image. + /// Whether the bitmap is inverted. + private void ReadBitFields(BufferedReadStream stream, Buffer2D pixels, bool inverted) + where TPixel : unmanaged, IPixel + { + if (this.infoHeader.BitsPerPixel == 16) + { + this.ReadRgb16( + stream, + pixels, + this.infoHeader.Width, + this.infoHeader.Height, + inverted, + this.infoHeader.RedMask, + this.infoHeader.GreenMask, + this.infoHeader.BlueMask); + } + else + { + this.ReadRgb32BitFields( + stream, + pixels, + this.infoHeader.Width, + this.infoHeader.Height, + inverted, + this.infoHeader.RedMask, + this.infoHeader.GreenMask, + this.infoHeader.BlueMask, + this.infoHeader.AlphaMask); + } + } + + /// + /// Looks up color values and builds the image from de-compressed RLE8 or RLE4 data. + /// Compressed RLE4 stream is uncompressed by + /// + /// The pixel format. + /// The containing image data. + /// The compression type. Either RLE4 or RLE8. + /// The to assign the palette to. + /// The containing the colors. + /// The width of the bitmap. + /// The height of the bitmap. + /// Whether the bitmap is inverted. + private void ReadRle(BufferedReadStream stream, BmpCompression compression, Buffer2D pixels, byte[] colors, int width, int height, bool inverted) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height, AllocationOptions.Clean); + using IMemoryOwner undefinedPixels = this.memoryAllocator.Allocate(width * height, AllocationOptions.Clean); + using IMemoryOwner rowsWithUndefinedPixels = this.memoryAllocator.Allocate(height, AllocationOptions.Clean); + Span rowsWithUndefinedPixelsSpan = rowsWithUndefinedPixels.Memory.Span; + Span undefinedPixelsSpan = undefinedPixels.Memory.Span; + Span bufferSpan = buffer.Memory.Span; + if (compression is BmpCompression.RLE8) + { + this.UncompressRle8(stream, width, bufferSpan, undefinedPixelsSpan, rowsWithUndefinedPixelsSpan); + } + else + { + this.UncompressRle4(stream, width, bufferSpan, undefinedPixelsSpan, rowsWithUndefinedPixelsSpan); + } + + for (int y = 0; y < height; y++) + { + int newY = Invert(y, height, inverted); + int rowStartIdx = y * width; + Span bufferRow = bufferSpan.Slice(rowStartIdx, width); + Span pixelRow = pixels.DangerousGetRowSpan(newY); + + bool rowHasUndefinedPixels = rowsWithUndefinedPixelsSpan[y]; + if (rowHasUndefinedPixels) + { + // Slow path with undefined pixels. + for (int x = 0; x < width; x++) + { + byte colorIdx = bufferRow[x]; + if (undefinedPixelsSpan[rowStartIdx + x]) + { + pixelRow[x] = this.rleSkippedPixelHandling switch + { + RleSkippedPixelHandling.FirstColorOfPalette => TPixel.FromBgr24(Unsafe.As(ref colors[colorIdx * 4])), + RleSkippedPixelHandling.Transparent => TPixel.FromScaledVector4(Vector4.Zero), + + // Default handling for skipped pixels is black (which is what System.Drawing is also doing). + _ => TPixel.FromScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)), + }; + } + else + { + pixelRow[x] = TPixel.FromBgr24(Unsafe.As(ref colors[colorIdx * 4])); + } + } + } + else + { + // Fast path without any undefined pixels. + for (int x = 0; x < width; x++) + { + pixelRow[x] = TPixel.FromBgr24(Unsafe.As(ref colors[bufferRow[x] * 4])); + } + } + } + } + + /// + /// Looks up color values and builds the image from de-compressed RLE24. + /// + /// The pixel format. + /// The containing image data. + /// The to assign the palette to. + /// The width of the bitmap. + /// The height of the bitmap. + /// Whether the bitmap is inverted. + private void ReadRle24(BufferedReadStream stream, Buffer2D pixels, int width, int height, bool inverted) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * 3, AllocationOptions.Clean); + using IMemoryOwner undefinedPixels = this.memoryAllocator.Allocate(width * height, AllocationOptions.Clean); + using IMemoryOwner rowsWithUndefinedPixels = this.memoryAllocator.Allocate(height, AllocationOptions.Clean); + Span rowsWithUndefinedPixelsSpan = rowsWithUndefinedPixels.Memory.Span; + Span undefinedPixelsSpan = undefinedPixels.Memory.Span; + Span bufferSpan = buffer.GetSpan(); + + this.UncompressRle24(stream, width, bufferSpan, undefinedPixelsSpan, rowsWithUndefinedPixelsSpan); + for (int y = 0; y < height; y++) + { + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.DangerousGetRowSpan(newY); + bool rowHasUndefinedPixels = rowsWithUndefinedPixelsSpan[y]; + if (rowHasUndefinedPixels) + { + // Slow path with undefined pixels. + int yMulWidth = y * width; + int rowStartIdx = yMulWidth * 3; + for (int x = 0; x < width; x++) + { + int idx = rowStartIdx + (x * 3); + if (undefinedPixelsSpan[yMulWidth + x]) + { + pixelRow[x] = this.rleSkippedPixelHandling switch + { + RleSkippedPixelHandling.FirstColorOfPalette => TPixel.FromBgr24(Unsafe.As(ref bufferSpan[idx])), + RleSkippedPixelHandling.Transparent => TPixel.FromScaledVector4(Vector4.Zero), + + // Default handling for skipped pixels is black (which is what System.Drawing is also doing). + _ => TPixel.FromScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)), + }; + } + else + { + pixelRow[x] = TPixel.FromBgr24(Unsafe.As(ref bufferSpan[idx])); + } + } + } + else + { + // Fast path without any undefined pixels. + int rowStartIdx = y * width * 3; + for (int x = 0; x < width; x++) + { + int idx = rowStartIdx + (x * 3); + pixelRow[x] = TPixel.FromBgr24(Unsafe.As(ref bufferSpan[idx])); + } + } + } + } + + /// + /// Produce uncompressed bitmap data from a RLE4 stream. + /// + /// + /// RLE4 is a 2-byte run-length encoding. + ///
If first byte is 0, the second byte may have special meaning. + ///
Otherwise, the first byte is the length of the run and second byte contains two color indexes. + ///
+ /// The containing image data. + /// The width of the bitmap. + /// Buffer for uncompressed data. + /// Keeps track over skipped and therefore undefined pixels. + /// Keeps track of rows, which have undefined pixels. + private void UncompressRle4(BufferedReadStream stream, int w, Span buffer, Span undefinedPixels, Span rowsWithUndefinedPixels) + { + Span scratchBuffer = stackalloc byte[128]; + Span cmd = stackalloc byte[2]; + int count = 0; + + while (count < buffer.Length) + { + if (stream.Read(cmd, 0, cmd.Length) != 2) + { + BmpThrowHelper.ThrowInvalidImageContentException("Failed to read 2 bytes from the stream while uncompressing RLE4 bitmap."); + } + + if (cmd[0] == RleCommand) + { + switch (cmd[1]) + { + case RleEndOfBitmap: + int skipEoB = buffer.Length - count; + RleSkipEndOfBitmap(count, w, skipEoB, undefinedPixels, rowsWithUndefinedPixels); + + return; + + case RleEndOfLine: + count += RleSkipEndOfLine(count, w, undefinedPixels, rowsWithUndefinedPixels); + + break; + + case RleDelta: + int dx = stream.ReadByte(); + int dy = stream.ReadByte(); + count += RleSkipDelta(count, w, dx, dy, undefinedPixels, rowsWithUndefinedPixels); + + break; + + default: + // If the second byte > 2, we are in 'absolute mode'. + // The second byte contains the number of color indexes that follow. + int max = cmd[1]; + int bytesToRead = (int)(((uint)max + 1) / 2); + + Span run = bytesToRead <= 128 ? scratchBuffer[..bytesToRead] : new byte[bytesToRead]; + + stream.Read(run); + + int idx = 0; + for (int i = 0; i < max; i++) + { + byte twoPixels = run[idx]; + if (i % 2 == 0) + { + buffer[count++] = (byte)((twoPixels >> 4) & 0xF); + } + else + { + buffer[count++] = (byte)(twoPixels & 0xF); + idx++; + } + } + + // Absolute mode data is aligned to two-byte word-boundary. + int padding = bytesToRead & 1; + + stream.Skip(padding); + + break; + } + } + else + { + int max = cmd[0]; + + // The second byte contains two color indexes, one in its high-order 4 bits and one in its low-order 4 bits. + byte twoPixels = cmd[1]; + byte rightPixel = (byte)(twoPixels & 0xF); + byte leftPixel = (byte)((twoPixels >> 4) & 0xF); + + for (int idx = 0; idx < max; idx++) + { + if (idx % 2 == 0) + { + buffer[count] = leftPixel; + } + else + { + buffer[count] = rightPixel; + } + + count++; + } + } + } + } + + /// + /// Produce uncompressed bitmap data from a RLE8 stream. + /// + /// + /// RLE8 is a 2-byte run-length encoding. + ///
If first byte is 0, the second byte may have special meaning. + ///
Otherwise, the first byte is the length of the run and second byte is the color for the run. + ///
+ /// The containing image data. + /// The width of the bitmap. + /// Buffer for uncompressed data. + /// Keeps track of skipped and therefore undefined pixels. + /// Keeps track of rows, which have undefined pixels. + private void UncompressRle8(BufferedReadStream stream, int w, Span buffer, Span undefinedPixels, Span rowsWithUndefinedPixels) + { + Span scratchBuffer = stackalloc byte[128]; + Span cmd = stackalloc byte[2]; + int count = 0; + + while (count < buffer.Length) + { + if (stream.Read(cmd, 0, cmd.Length) != 2) + { + BmpThrowHelper.ThrowInvalidImageContentException("Failed to read 2 bytes from stream while uncompressing RLE8 bitmap."); + } + + if (cmd[0] == RleCommand) + { + switch (cmd[1]) + { + case RleEndOfBitmap: + int skipEoB = buffer.Length - count; + RleSkipEndOfBitmap(count, w, skipEoB, undefinedPixels, rowsWithUndefinedPixels); + + return; + + case RleEndOfLine: + count += RleSkipEndOfLine(count, w, undefinedPixels, rowsWithUndefinedPixels); + + break; + + case RleDelta: + int dx = stream.ReadByte(); + int dy = stream.ReadByte(); + count += RleSkipDelta(count, w, dx, dy, undefinedPixels, rowsWithUndefinedPixels); + + break; + + default: + // If the second byte > 2, we are in 'absolute mode'. + // Take this number of bytes from the stream as uncompressed data. + int length = cmd[1]; + + Span run = length <= 128 ? scratchBuffer[..length] : new byte[length]; + + stream.Read(run); + + run.CopyTo(buffer[count..]); + + count += length; + + // Absolute mode data is aligned to two-byte word-boundary. + int padding = length & 1; + + stream.Skip(padding); + + break; + } + } + else + { + int max = count + cmd[0]; // as we start at the current count in the following loop, max is count + cmd[0] + byte colorIdx = cmd[1]; // store the value to avoid the repeated indexer access inside the loop. + + for (; count < max; count++) + { + buffer[count] = colorIdx; + } + } + } + } + + /// + /// Produce uncompressed bitmap data from a RLE24 stream. + /// + /// + ///
If first byte is 0, the second byte may have special meaning. + ///
Otherwise, the first byte is the length of the run and following three bytes are the color for the run. + ///
+ /// The containing image data. + /// The width of the bitmap. + /// Buffer for uncompressed data. + /// Keeps track of skipped and therefore undefined pixels. + /// Keeps track of rows, which have undefined pixels. + private void UncompressRle24(BufferedReadStream stream, int w, Span buffer, Span undefinedPixels, Span rowsWithUndefinedPixels) + { + Span scratchBuffer = stackalloc byte[128]; + Span cmd = stackalloc byte[2]; + int uncompressedPixels = 0; + + while (uncompressedPixels < buffer.Length) + { + if (stream.Read(cmd, 0, cmd.Length) != 2) + { + BmpThrowHelper.ThrowInvalidImageContentException("Failed to read 2 bytes from stream while uncompressing RLE24 bitmap."); + } + + if (cmd[0] == RleCommand) + { + switch (cmd[1]) + { + case RleEndOfBitmap: + int skipEoB = (buffer.Length - (uncompressedPixels * 3)) / 3; + RleSkipEndOfBitmap(uncompressedPixels, w, skipEoB, undefinedPixels, rowsWithUndefinedPixels); + + return; + + case RleEndOfLine: + uncompressedPixels += RleSkipEndOfLine(uncompressedPixels, w, undefinedPixels, rowsWithUndefinedPixels); + + break; + + case RleDelta: + int dx = stream.ReadByte(); + int dy = stream.ReadByte(); + uncompressedPixels += RleSkipDelta(uncompressedPixels, w, dx, dy, undefinedPixels, rowsWithUndefinedPixels); + + break; + + default: + // If the second byte > 2, we are in 'absolute mode'. + // Take this number of bytes from the stream as uncompressed data. + int length = cmd[1]; + int length3 = length * 3; + + Span run = length3 <= 128 ? scratchBuffer[..length3] : new byte[length3]; + + stream.Read(run); + + run.CopyTo(buffer[(uncompressedPixels * 3)..]); + + uncompressedPixels += length; + + // Absolute mode data is aligned to two-byte word-boundary. + int padding = length3 & 1; + + stream.Skip(padding); + + break; + } + } + else + { + int max = uncompressedPixels + cmd[0]; + byte blueIdx = cmd[1]; + byte greenIdx = (byte)stream.ReadByte(); + byte redIdx = (byte)stream.ReadByte(); + + int bufferIdx = uncompressedPixels * 3; + for (; uncompressedPixels < max; uncompressedPixels++) + { + buffer[bufferIdx++] = blueIdx; + buffer[bufferIdx++] = greenIdx; + buffer[bufferIdx++] = redIdx; + } + } + } + } + + /// + /// Keeps track of skipped / undefined pixels, when the EndOfBitmap command occurs. + /// + /// The already processed pixel count. + /// The width of the image. + /// The skipped pixel count. + /// The undefined pixels. + /// Rows with undefined pixels. + private static void RleSkipEndOfBitmap( + int count, + int w, + int skipPixelCount, + Span undefinedPixels, + Span rowsWithUndefinedPixels) + { + for (int i = count; i < count + skipPixelCount; i++) + { + undefinedPixels[i] = true; + } + + int skippedRowIdx = count / w; + int skippedRows = (skipPixelCount / w) - 1; + int lastSkippedRow = Math.Min(skippedRowIdx + skippedRows, rowsWithUndefinedPixels.Length - 1); + for (int i = skippedRowIdx; i <= lastSkippedRow; i++) + { + rowsWithUndefinedPixels[i] = true; + } + } + + /// + /// Keeps track of undefined / skipped pixels, when the EndOfLine command occurs. + /// + /// The already uncompressed pixel count. + /// The width of image. + /// The undefined pixels. + /// The rows with undefined pixels. + /// The number of skipped pixels. + private static int RleSkipEndOfLine(int count, int w, Span undefinedPixels, Span rowsWithUndefinedPixels) + { + rowsWithUndefinedPixels[count / w] = true; + int remainingPixelsInRow = count % w; + if (remainingPixelsInRow > 0) + { + int skipEoL = w - remainingPixelsInRow; + for (int i = count; i < count + skipEoL; i++) + { + undefinedPixels[i] = true; + } + + return skipEoL; + } + + return 0; + } + + /// + /// Keeps track of undefined / skipped pixels, when the delta command occurs. + /// + /// The count. + /// The width of the image. + /// Delta skip in x direction. + /// Delta skip in y direction. + /// The undefined pixels. + /// The rows with undefined pixels. + /// The number of skipped pixels. + private static int RleSkipDelta( + int count, + int w, + int dx, + int dy, + Span undefinedPixels, + Span rowsWithUndefinedPixels) + { + int skipDelta = (w * dy) + dx; + for (int i = count; i < count + skipDelta; i++) + { + undefinedPixels[i] = true; + } + + int skippedRowIdx = count / w; + int lastSkippedRow = Math.Min(skippedRowIdx + dy, rowsWithUndefinedPixels.Length - 1); + for (int i = skippedRowIdx; i <= lastSkippedRow; i++) + { + rowsWithUndefinedPixels[i] = true; + } + + return skipDelta; + } + + /// + /// Reads the color palette from the stream. + /// + /// The pixel format. + /// The containing image data. + /// The to assign the palette to. + /// The containing the colors. + /// The width of the bitmap. + /// The height of the bitmap. + /// The number of bits per pixel. + /// Usually 4 bytes, but in case of Windows 2.x bitmaps or OS/2 1.x bitmaps + /// the bytes per color palette entry's can be 3 bytes instead of 4. + /// Whether the bitmap is inverted. + private void ReadRgbPalette(BufferedReadStream stream, Buffer2D pixels, byte[] colors, int width, int height, int bitsPerPixel, int bytesPerColorMapEntry, bool inverted) + where TPixel : unmanaged, IPixel + { + // Pixels per byte (bits per pixel). + int ppb = 8 / bitsPerPixel; + + int arrayWidth = (width + ppb - 1) / ppb; + + // Bit mask + int mask = 0xFF >> (8 - bitsPerPixel); + + // Rows are aligned on 4 byte boundaries. + int padding = arrayWidth % 4; + if (padding != 0) + { + padding = 4 - padding; + } + + using IMemoryOwner row = this.memoryAllocator.Allocate(arrayWidth + padding, AllocationOptions.Clean); + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) + { + int newY = Invert(y, height, inverted); + if (stream.Read(rowSpan) == 0) + { + BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); + } + + int offset = 0; + Span pixelRow = pixels.DangerousGetRowSpan(newY); + + for (int x = 0; x < arrayWidth; x++) + { + int colOffset = x * ppb; + for (int shift = 0, newX = colOffset; shift < ppb && newX < width; shift++, newX++) + { + int colorIndex = ((rowSpan[offset] >> (8 - bitsPerPixel - (shift * bitsPerPixel))) & mask) * bytesPerColorMapEntry; + + pixelRow[newX] = TPixel.FromBgr24(Unsafe.As(ref colors[colorIndex])); + } + + offset++; + } + } + } + + /// + private void ReadRgbPaletteWithAlphaMask(BufferedReadStream stream, Buffer2D pixels, byte[] colors, int width, int height, int bitsPerPixel, int bytesPerColorMapEntry, bool inverted) + where TPixel : unmanaged, IPixel + { + // Pixels per byte (bits per pixel). + int ppb = 8 / bitsPerPixel; + + int arrayWidth = (width + ppb - 1) / ppb; + + // Bit mask + int mask = 0xFF >> (8 - bitsPerPixel); + + // Rows are aligned on 4 byte boundaries. + int padding = arrayWidth % 4; + if (padding != 0) + { + padding = 4 - padding; + } + + Bgra32[,] image = new Bgra32[height, width]; + using (IMemoryOwner row = this.memoryAllocator.Allocate(arrayWidth + padding, AllocationOptions.Clean)) + { + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) + { + int newY = Invert(y, height, inverted); + if (stream.Read(rowSpan) == 0) + { + BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); + } + + int offset = 0; + + for (int x = 0; x < arrayWidth; x++) + { + int colOffset = x * ppb; + for (int shift = 0, newX = colOffset; shift < ppb && newX < width; shift++, newX++) + { + int colorIndex = ((rowSpan[offset] >> (8 - bitsPerPixel - (shift * bitsPerPixel))) & mask) * bytesPerColorMapEntry; + + image[newY, newX] = Bgra32.FromBgr24(Unsafe.As(ref colors[colorIndex])); + } + + offset++; + } + } + } + + arrayWidth = width / 8; + padding = arrayWidth % 4; + if (padding != 0) + { + padding = 4 - padding; + } + + for (int y = 0; y < height; y++) + { + int newY = Invert(y, height, inverted); + + for (int i = 0; i < arrayWidth; i++) + { + int x = i * 8; + int and = stream.ReadByte(); + if (and is -1) + { + throw new EndOfStreamException(); + } + + for (int j = 0; j < 8; j++) + { + SetAlpha(ref image[newY, x + j], and, j); + } + } + + stream.Skip(padding); + } + + for (int y = 0; y < height; y++) + { + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.DangerousGetRowSpan(newY); + + for (int x = 0; x < width; x++) + { + pixelRow[x] = TPixel.FromBgra32(image[newY, x]); + } + } + } + + /// + /// Set pixel's alpha with alpha mask. + /// + /// Bgra32 pixel. + /// alpha mask. + /// bit index of pixel. + private static void SetAlpha(ref Bgra32 pixel, in int mask, in int index) + { + bool isTransparently = (mask & (0b10000000 >> index)) is not 0; + pixel.A = isTransparently ? byte.MinValue : byte.MaxValue; + } + + /// + /// Reads the 16 bit color palette from the stream. + /// + /// The pixel format. + /// The containing image data. + /// The to assign the palette to. + /// The width of the bitmap. + /// The height of the bitmap. + /// Whether the bitmap is inverted. + /// The bitmask for the red channel. + /// The bitmask for the green channel. + /// The bitmask for the blue channel. + private void ReadRgb16(BufferedReadStream stream, Buffer2D pixels, int width, int height, bool inverted, int redMask = DefaultRgb16RMask, int greenMask = DefaultRgb16GMask, int blueMask = DefaultRgb16BMask) + where TPixel : unmanaged, IPixel + { + int padding = CalculatePadding(width, 2); + int stride = (width * 2) + padding; + int rightShiftRedMask = CalculateRightShift((uint)redMask); + int rightShiftGreenMask = CalculateRightShift((uint)greenMask); + int rightShiftBlueMask = CalculateRightShift((uint)blueMask); + + // Each color channel contains either 5 or 6 Bits values. + int redMaskBits = CountBits((uint)redMask); + int greenMaskBits = CountBits((uint)greenMask); + int blueMaskBits = CountBits((uint)blueMask); + + using IMemoryOwner buffer = this.memoryAllocator.Allocate(stride); + Span bufferSpan = buffer.GetSpan(); + + for (int y = 0; y < height; y++) + { + if (stream.Read(bufferSpan) == 0) + { + BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); + } + + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.DangerousGetRowSpan(newY); + + int offset = 0; + for (int x = 0; x < width; x++) + { + short temp = BinaryPrimitives.ReadInt16LittleEndian(bufferSpan[offset..]); + + // Rescale values, so the values range from 0 to 255. + int r = (redMaskBits == 5) ? GetBytesFrom5BitValue((temp & redMask) >> rightShiftRedMask) : GetBytesFrom6BitValue((temp & redMask) >> rightShiftRedMask); + int g = (greenMaskBits == 5) ? GetBytesFrom5BitValue((temp & greenMask) >> rightShiftGreenMask) : GetBytesFrom6BitValue((temp & greenMask) >> rightShiftGreenMask); + int b = (blueMaskBits == 5) ? GetBytesFrom5BitValue((temp & blueMask) >> rightShiftBlueMask) : GetBytesFrom6BitValue((temp & blueMask) >> rightShiftBlueMask); + Rgb24 rgb = new((byte)r, (byte)g, (byte)b); + + pixelRow[x] = TPixel.FromRgb24(rgb); + offset += 2; + } + } + } + + /// + /// Performs final shifting from a 5bit value to an 8bit one. + /// + /// The masked and shifted value. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte GetBytesFrom5BitValue(int value) => (byte)((value << 3) | (value >> 2)); + + /// + /// Performs final shifting from a 6bit value to an 8bit one. + /// + /// The masked and shifted value. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte GetBytesFrom6BitValue(int value) => (byte)((value << 2) | (value >> 4)); + + /// + /// Reads the 24 bit color palette from the stream. + /// + /// The pixel format. + /// The containing image data. + /// The to assign the palette to. + /// The width of the bitmap. + /// The height of the bitmap. + /// Whether the bitmap is inverted. + private void ReadRgb24(BufferedReadStream stream, Buffer2D pixels, int width, int height, bool inverted) + where TPixel : unmanaged, IPixel + { + int padding = CalculatePadding(width, 3); + using IMemoryOwner row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 3, padding); + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) + { + if (stream.Read(rowSpan) == 0) + { + BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); + } + + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.DangerousGetRowSpan(newY); + PixelOperations.Instance.FromBgr24Bytes( + this.configuration, + rowSpan, + pixelSpan, + width); + } + } + + /// + /// Reads the 32 bit color palette from the stream. + /// + /// The pixel format. + /// The containing image data. + /// The to assign the palette to. + /// The width of the bitmap. + /// The height of the bitmap. + /// Whether the bitmap is inverted. + private void ReadRgb32Fast(BufferedReadStream stream, Buffer2D pixels, int width, int height, bool inverted) + where TPixel : unmanaged, IPixel + { + int padding = CalculatePadding(width, 4); + using IMemoryOwner row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, padding); + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) + { + if (stream.Read(rowSpan) == 0) + { + BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); + } + + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.DangerousGetRowSpan(newY); + PixelOperations.Instance.FromBgra32Bytes( + this.configuration, + rowSpan, + pixelSpan, + width); + } + } + + /// + /// Reads the 32 bit color palette from the stream, checking the alpha component of each pixel. + /// This is a special case only used for 32bpp WinBMPv3 files, which could be in either BGR0 or BGRA format. + /// + /// The pixel format. + /// The containing image data. + /// The to assign the palette to. + /// The width of the bitmap. + /// The height of the bitmap. + /// Whether the bitmap is inverted. + private void ReadRgb32Slow(BufferedReadStream stream, Buffer2D pixels, int width, int height, bool inverted) + where TPixel : unmanaged, IPixel + { + int padding = CalculatePadding(width, 4); + using IMemoryOwner row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, padding); + using IMemoryOwner bgraRow = this.memoryAllocator.Allocate(width); + Span rowSpan = row.GetSpan(); + Span bgraRowSpan = bgraRow.GetSpan(); + long currentPosition = stream.Position; + bool hasAlpha = false; + + // Loop though the rows checking each pixel. We start by assuming it's + // an BGR0 image. If we hit a non-zero alpha value, then we know it's + // actually a BGRA image, and change tactics accordingly. + for (int y = 0; y < height; y++) + { + if (stream.Read(rowSpan) == 0) + { + BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); + } + + PixelOperations.Instance.FromBgra32Bytes( + this.configuration, + rowSpan, + bgraRowSpan, + width); + + // Check each pixel in the row to see if it has an alpha value. + for (int x = 0; x < width; x++) + { + Bgra32 bgra = bgraRowSpan[x]; + if (bgra.A > 0) + { + hasAlpha = true; + break; + } + } + + if (hasAlpha) + { + break; + } + } + + // Reset our stream for a second pass. + stream.Position = currentPosition; + + // Process the pixels in bulk taking the raw alpha component value. + if (hasAlpha) + { + for (int y = 0; y < height; y++) + { + if (stream.Read(rowSpan) == 0) + { + BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); + } + + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.DangerousGetRowSpan(newY); + + PixelOperations.Instance.FromBgra32Bytes( + this.configuration, + rowSpan, + pixelSpan, + width); + } + + return; + } + + // Slow path. We need to set each alpha component value to fully opaque. + for (int y = 0; y < height; y++) + { + if (stream.Read(rowSpan) == 0) + { + BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); + } + + PixelOperations.Instance.FromBgra32Bytes( + this.configuration, + rowSpan, + bgraRowSpan, + width); + + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.DangerousGetRowSpan(newY); + + for (int x = 0; x < width; x++) + { + Bgra32 bgra = bgraRowSpan[x]; + bgra.A = byte.MaxValue; + pixelSpan[x] = TPixel.FromBgra32(bgra); + } + } + } + + /// + /// Decode an 32 Bit Bitmap containing a bitmask for each color channel. + /// + /// The pixel format. + /// The containing image data. + /// The output pixel buffer containing the decoded image. + /// The width of the image. + /// The height of the image. + /// Whether the bitmap is inverted. + /// The bitmask for the red channel. + /// The bitmask for the green channel. + /// The bitmask for the blue channel. + /// The bitmask for the alpha channel. + private void ReadRgb32BitFields(BufferedReadStream stream, Buffer2D pixels, int width, int height, bool inverted, int redMask, int greenMask, int blueMask, int alphaMask) + where TPixel : unmanaged, IPixel + { + int padding = CalculatePadding(width, 4); + int stride = (width * 4) + padding; + + int rightShiftRedMask = CalculateRightShift((uint)redMask); + int rightShiftGreenMask = CalculateRightShift((uint)greenMask); + int rightShiftBlueMask = CalculateRightShift((uint)blueMask); + int rightShiftAlphaMask = CalculateRightShift((uint)alphaMask); + + int bitsRedMask = CountBits((uint)redMask); + int bitsGreenMask = CountBits((uint)greenMask); + int bitsBlueMask = CountBits((uint)blueMask); + int bitsAlphaMask = CountBits((uint)alphaMask); + float invMaxValueRed = 1.0f / (0xFFFFFFFF >> (32 - bitsRedMask)); + float invMaxValueGreen = 1.0f / (0xFFFFFFFF >> (32 - bitsGreenMask)); + float invMaxValueBlue = 1.0f / (0xFFFFFFFF >> (32 - bitsBlueMask)); + uint maxValueAlpha = 0xFFFFFFFF >> (32 - bitsAlphaMask); + float invMaxValueAlpha = 1.0f / maxValueAlpha; + + bool unusualBitMask = bitsRedMask > 8 || bitsGreenMask > 8 || bitsBlueMask > 8 || invMaxValueAlpha > 8; + + using IMemoryOwner buffer = this.memoryAllocator.Allocate(stride); + Span bufferSpan = buffer.GetSpan(); + + for (int y = 0; y < height; y++) + { + if (stream.Read(bufferSpan) == 0) + { + BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); + } + + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.DangerousGetRowSpan(newY); + + int offset = 0; + for (int x = 0; x < width; x++) + { + uint temp = BinaryPrimitives.ReadUInt32LittleEndian(bufferSpan[offset..]); + + if (unusualBitMask) + { + uint r = (uint)(temp & redMask) >> rightShiftRedMask; + uint g = (uint)(temp & greenMask) >> rightShiftGreenMask; + uint b = (uint)(temp & blueMask) >> rightShiftBlueMask; + float alpha = alphaMask != 0 ? invMaxValueAlpha * ((uint)(temp & alphaMask) >> rightShiftAlphaMask) : 1.0f; + Vector4 vector4 = new( + r * invMaxValueRed, + g * invMaxValueGreen, + b * invMaxValueBlue, + alpha); + pixelRow[x] = TPixel.FromScaledVector4(vector4); + } + else + { + byte r = (byte)((temp & redMask) >> rightShiftRedMask); + byte g = (byte)((temp & greenMask) >> rightShiftGreenMask); + byte b = (byte)((temp & blueMask) >> rightShiftBlueMask); + byte a = alphaMask != 0 ? (byte)((temp & alphaMask) >> rightShiftAlphaMask) : byte.MaxValue; + pixelRow[x] = TPixel.FromRgba32(new Rgba32(r, g, b, a)); + } + + offset += 4; + } + } + } + + /// + /// Calculates the necessary right shifts for a given color bitmask (the 0 bits to the right). + /// + /// The color bit mask. + /// Number of bits to shift right. + private static int CalculateRightShift(uint n) + { + int count = 0; + while (n > 0) + { + if ((1 & n) == 0) + { + count++; + } + else + { + break; + } + + n >>= 1; + } + + return count; + } + + /// + /// Counts none zero bits. + /// + /// A color mask. + /// The none zero bits. + private static int CountBits(uint n) + { + int count = 0; + while (n != 0) + { + count++; + n &= n - 1; + } + + return count; + } + + /// + /// Reads the from the stream. + /// + /// The containing image data. + [MemberNotNull(nameof(metadata))] + [MemberNotNull(nameof(bmpMetadata))] + private void ReadInfoHeader(BufferedReadStream stream) + { + Span buffer = stackalloc byte[BmpInfoHeader.MaxHeaderSize]; + long infoHeaderStart = stream.Position; + + // Resolution is stored in PPM. + this.metadata = new ImageMetadata + { + ResolutionUnits = PixelResolutionUnit.PixelsPerMeter + }; + + // Read the header size. + stream.Read(buffer, 0, BmpInfoHeader.HeaderSizeSize); + + int headerSize = BinaryPrimitives.ReadInt32LittleEndian(buffer); + if (headerSize is < BmpInfoHeader.CoreSize or > BmpInfoHeader.MaxHeaderSize) + { + BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. HeaderSize is '{headerSize}'."); + } + + // Read the rest of the header. + stream.Read(buffer, BmpInfoHeader.HeaderSizeSize, headerSize - BmpInfoHeader.HeaderSizeSize); + + BmpInfoHeaderType infoHeaderType = BmpInfoHeaderType.WinVersion2; + if (headerSize == BmpInfoHeader.CoreSize) + { + // 12 bytes + infoHeaderType = BmpInfoHeaderType.WinVersion2; + this.infoHeader = BmpInfoHeader.ParseCore(buffer); + } + else if (headerSize == BmpInfoHeader.Os22ShortSize) + { + // 16 bytes + infoHeaderType = BmpInfoHeaderType.Os2Version2Short; + this.infoHeader = BmpInfoHeader.ParseOs22Short(buffer); + } + else if (headerSize == BmpInfoHeader.SizeV3) + { + // == 40 bytes + infoHeaderType = BmpInfoHeaderType.WinVersion3; + this.infoHeader = BmpInfoHeader.ParseV3(buffer); + + // If the info header is BMP version 3 and the compression type is BITFIELDS, + // color masks for each color channel follow the info header. + if (this.infoHeader.Compression == BmpCompression.BitFields) + { + Span bitfieldsBuffer = stackalloc byte[12]; + stream.Read(bitfieldsBuffer); + Span data = bitfieldsBuffer; + this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data[..4]); + this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)); + this.infoHeader.BlueMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)); + } + else if (this.infoHeader.Compression == BmpCompression.BI_ALPHABITFIELDS) + { + Span bitfieldsBuffer = stackalloc byte[16]; + stream.Read(bitfieldsBuffer); + Span data = bitfieldsBuffer; + this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data[..4]); + this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)); + this.infoHeader.BlueMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)); + this.infoHeader.AlphaMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(12, 4)); + } + } + else if (headerSize == BmpInfoHeader.AdobeV3Size) + { + // == 52 bytes + infoHeaderType = BmpInfoHeaderType.AdobeVersion3; + this.infoHeader = BmpInfoHeader.ParseAdobeV3(buffer, withAlpha: false); + } + else if (headerSize == BmpInfoHeader.AdobeV3WithAlphaSize) + { + // == 56 bytes + infoHeaderType = BmpInfoHeaderType.AdobeVersion3WithAlpha; + this.infoHeader = BmpInfoHeader.ParseAdobeV3(buffer, withAlpha: true); + } + else if (headerSize == BmpInfoHeader.Os2v2Size) + { + // == 64 bytes + infoHeaderType = BmpInfoHeaderType.Os2Version2; + this.infoHeader = BmpInfoHeader.ParseOs2Version2(buffer); + } + else if (headerSize == BmpInfoHeader.SizeV4) + { + // == 108 bytes + infoHeaderType = BmpInfoHeaderType.WinVersion4; + this.infoHeader = BmpInfoHeader.ParseV4(buffer); + } + else if (headerSize > BmpInfoHeader.SizeV4) + { + // > 108 bytes + infoHeaderType = BmpInfoHeaderType.WinVersion5; + this.infoHeader = BmpInfoHeader.ParseV5(buffer); + if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0) + { + long streamPosition = stream.Position; + this.ExecuteAncillarySegmentAction(() => this.ReadIccProfile(stream, this.metadata, infoHeaderStart)); + stream.Position = streamPosition; + } + } + else + { + BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. HeaderSize '{headerSize}'."); + } + + if (this.infoHeader.XPelsPerMeter > 0 && this.infoHeader.YPelsPerMeter > 0) + { + this.metadata.HorizontalResolution = this.infoHeader.XPelsPerMeter; + this.metadata.VerticalResolution = this.infoHeader.YPelsPerMeter; + } + else + { + // Convert default metadata values to PPM. + this.metadata.HorizontalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultHorizontalResolution)); + this.metadata.VerticalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultVerticalResolution)); + } + + if (this.isDoubleHeight) + { + this.infoHeader.Height >>= 1; + } + + ushort bitsPerPixel = this.infoHeader.BitsPerPixel; + this.bmpMetadata = this.metadata.GetBmpMetadata(); + this.bmpMetadata.InfoHeaderType = infoHeaderType; + this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel; + + this.Dimensions = new Size(this.infoHeader.Width, this.infoHeader.Height); + } + + /// + /// Reads the embedded ICC profile from the BMP V5 info header. + /// + /// The containing image data. + /// The image metadata. + /// The stream position where the info header begins. + private void ReadIccProfile(BufferedReadStream stream, ImageMetadata imageMetadata, long infoHeaderStart) + { + byte[] iccProfileData = new byte[this.infoHeader.ProfileSize]; + stream.Position = infoHeaderStart + this.infoHeader.ProfileData; + + if (stream.Read(iccProfileData) != iccProfileData.Length) + { + BmpThrowHelper.ThrowInvalidImageContentException("Not enough data to read BMP ICC profile."); + } + + IccProfile profile = new(iccProfileData); + if (profile.CheckIsValid()) + { + imageMetadata.IccProfile = profile; + } + else + { + throw new InvalidIccProfileException("Invalid BMP ICC profile."); + } + } + + /// + /// Reads the from the stream. + /// + /// The containing image data. + private void ReadFileHeader(BufferedReadStream stream) + { + Span buffer = stackalloc byte[BmpFileHeader.Size]; + stream.Read(buffer, 0, BmpFileHeader.Size); + + short fileTypeMarker = BinaryPrimitives.ReadInt16LittleEndian(buffer); + switch (fileTypeMarker) + { + case BmpConstants.TypeMarkers.Bitmap: + this.fileMarkerType = BmpFileMarkerType.Bitmap; + this.fileHeader = BmpFileHeader.Parse(buffer); + break; + case BmpConstants.TypeMarkers.BitmapArray: + this.fileMarkerType = BmpFileMarkerType.BitmapArray; + + // Because we only decode the first bitmap in the array, the array header will be ignored. + // The bitmap file header of the first image follows the array header. + stream.Read(buffer, 0, BmpFileHeader.Size); + this.fileHeader = BmpFileHeader.Parse(buffer); + if (this.fileHeader.Value.Type != BmpConstants.TypeMarkers.Bitmap) + { + BmpThrowHelper.ThrowNotSupportedException($"Unsupported bitmap file inside a BitmapArray file. File header bitmap type marker '{this.fileHeader.Value.Type}'."); + } + + break; + + default: + BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. File header bitmap type marker '{fileTypeMarker}'."); + break; + } + } + + /// + /// Reads the and from the stream and sets the corresponding fields. + /// + /// The input stream. + /// Whether the image orientation is inverted. + /// The color palette. + /// Bytes per color palette entry. Usually 4 bytes, but in case of Windows 2.x bitmaps or OS/2 1.x bitmaps + /// the bytes per color palette entry's can be 3 bytes instead of 4. + [MemberNotNull(nameof(metadata))] + [MemberNotNull(nameof(bmpMetadata))] + private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out byte[] palette) + { + if (!this.skipFileHeader) + { + this.ReadFileHeader(stream); + } + + this.ReadInfoHeader(stream); + + // see http://www.drdobbs.com/architecture-and-design/the-bmp-file-format-part-1/184409517 + // If the height is negative, then this is a Windows bitmap whose origin + // is the upper-left corner and not the lower-left. The inverted flag + // indicates a lower-left origin.Our code will be outputting an + // upper-left origin pixel array. + inverted = false; + if (this.infoHeader.Height < 0) + { + inverted = true; + this.infoHeader.Height = -this.infoHeader.Height; + } + + int bytesPerColorMapEntry = 4; + int colorMapSizeBytes = -1; + if (this.infoHeader.ClrUsed == 0) + { + if (this.infoHeader.BitsPerPixel is 1 or 2 or 4 or 8) + { + switch (this.fileMarkerType) + { + case BmpFileMarkerType.Bitmap: + if (this.fileHeader.HasValue) + { + if (this.fileHeader.Value.Offset > stream.Length) + { + BmpThrowHelper.ThrowInvalidImageContentException( + $"Pixel data offset {this.fileHeader.Value.Offset} exceeds file size {stream.Length}."); + } + + colorMapSizeBytes = this.fileHeader.Value.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize; + } + else + { + colorMapSizeBytes = this.infoHeader.ClrUsed; + if (colorMapSizeBytes is 0 && this.infoHeader.BitsPerPixel is <= 8) + { + colorMapSizeBytes = ColorNumerics.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel); + } + + colorMapSizeBytes *= 4; + } + + int colorCountForBitDepth = ColorNumerics.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel); + bytesPerColorMapEntry = colorMapSizeBytes / colorCountForBitDepth; + + // Edge case for less-than-full-sized palette: bytesPerColorMapEntry should be at least 3. + bytesPerColorMapEntry = Math.Max(bytesPerColorMapEntry, 3); + + break; + case BmpFileMarkerType.BitmapArray: + case BmpFileMarkerType.ColorIcon: + case BmpFileMarkerType.ColorPointer: + case BmpFileMarkerType.Icon: + case BmpFileMarkerType.Pointer: + // OS/2 bitmaps always have 3 colors per color palette entry. + bytesPerColorMapEntry = 3; + colorMapSizeBytes = ColorNumerics.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel) * bytesPerColorMapEntry; + break; + } + } + } + else + { + colorMapSizeBytes = this.infoHeader.ClrUsed * bytesPerColorMapEntry; + } + + palette = []; + + if (colorMapSizeBytes > 0) + { + // Usually the color palette is 1024 byte (256 colors * 4), but the documentation does not mention a size limit. + // Make sure, that we will not read pass the bitmap offset (starting position of image data). + if (this.fileHeader.HasValue && stream.Position > this.fileHeader.Value.Offset - colorMapSizeBytes) + { + BmpThrowHelper.ThrowInvalidImageContentException( + $"Reading the color map would read beyond the bitmap offset. Either the color map size of '{colorMapSizeBytes}' is invalid or the bitmap offset."); + } + + palette = new byte[colorMapSizeBytes]; + + if (stream.Read(palette, 0, colorMapSizeBytes) == 0) + { + BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the palette!"); + } + } + + if (palette.Length > 0) + { + Color[] colorTable = new Color[palette.Length / Unsafe.SizeOf()]; + ReadOnlySpan rgbTable = MemoryMarshal.Cast(palette); + Color.FromPixel(rgbTable, colorTable); + this.bmpMetadata.ColorTable = colorTable; + } + + int skipAmount = 0; + if (this.fileHeader.HasValue) + { + skipAmount = this.fileHeader.Value.Offset - (int)stream.Position; + } + + if ((skipAmount + (int)stream.Position) > stream.Length) + { + BmpThrowHelper.ThrowInvalidImageContentException("Invalid file header offset found. Offset is greater than the stream length."); + } + + if (skipAmount > 0) + { + stream.Skip(skipAmount); + } + + return bytesPerColorMapEntry; + } + } +} diff --git a/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs b/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs new file mode 100644 index 0000000..da339d2 --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Configuration options for decoding Windows Bitmap images. + /// + public sealed class BmpDecoderOptions : ISpecializedDecoderOptions + { + /// + public DecoderOptions GeneralOptions { get; init; } = new(); + + /// + /// Gets the value indicating how to deal with skipped pixels, + /// which can occur during decoding run length encoded bitmaps. + /// + public RleSkippedPixelHandling RleSkippedPixelHandling { get; init; } + + /// + /// Gets a value indicating whether the additional alpha mask is processed at decoding time. + /// + /// + /// Used by the icon decoder. + /// + internal bool ProcessedAlphaMask { get; init; } + + /// + /// Gets a value indicating whether to skip loading the BMP file header. + /// + /// + /// Used by the icon decoder. + /// + internal bool SkipFileHeader { get; init; } + + /// + /// Gets a value indicating whether to treat the height as double of true height. + /// + /// + /// Used by the icon decoder. + /// + internal bool UseDoubleHeight { get; init; } + } +} diff --git a/ImageSharp/Formats/Bmp/BmpEncoder.cs b/ImageSharp/Formats/Bmp/BmpEncoder.cs new file mode 100644 index 0000000..d80d0e9 --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpEncoder.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Image encoder for writing an image to a stream as a Windows bitmap. + /// + public sealed class BmpEncoder : QuantizingImageEncoder + { + /// + /// Initializes a new instance of the class. + /// + public BmpEncoder() => this.Quantizer = KnownQuantizers.Hexadecatree; + + /// + /// Gets the number of bits per pixel. + /// + public BmpBitsPerPixel? BitsPerPixel { get; init; } + + /// + /// Gets a value indicating whether the encoder should support transparency. + /// Note: Transparency support only works together with 32 bits per pixel. This option will + /// change the default behavior of the encoder of writing a bitmap version 3 info header with no compression. + /// Instead a bitmap version 4 info header will be written with the BITFIELDS compression. + /// + public bool SupportTransparency { get; init; } + + /// + internal bool ProcessedAlphaMask { get; init; } + + /// + internal bool SkipFileHeader { get; init; } + + /// + internal bool UseDoubleHeight { get; init; } + + /// + protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) + { + BmpEncoderCore encoder = new(this, image.Configuration.MemoryAllocator); + encoder.Encode(image, stream, cancellationToken); + } + } +} diff --git a/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/ImageSharp/Formats/Bmp/BmpEncoderCore.cs new file mode 100644 index 0000000..a2a88a3 --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -0,0 +1,910 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Image encoder for writing an image to a stream as a Windows bitmap. + /// + internal sealed class BmpEncoderCore + { + /// + /// The amount to pad each row by. + /// + private int padding; + + /// + /// The mask for the alpha channel of the color for 32 bit rgba bitmaps. + /// + private const int Rgba32AlphaMask = 0xFF << 24; + + /// + /// The mask for the red part of the color for 32 bit rgba bitmaps. + /// + private const int Rgba32RedMask = 0xFF << 16; + + /// + /// The mask for the green part of the color for 32 bit rgba bitmaps. + /// + private const int Rgba32GreenMask = 0xFF << 8; + + /// + /// The mask for the blue part of the color for 32 bit rgba bitmaps. + /// + private const int Rgba32BlueMask = 0xFF; + + /// + /// The color palette for an 8 bit image will have 256 entry's with 4 bytes for each entry. + /// + private const int ColorPaletteSize8Bit = 1024; + + /// + /// The color palette for an 4 bit image will have 16 entry's with 4 bytes for each entry. + /// + private const int ColorPaletteSize4Bit = 64; + + /// + /// The color palette for an 2 bit image will have 4 entry's with 4 bytes for each entry. + /// + private const int ColorPaletteSize2Bit = 16; + + /// + /// The color palette for an 1 bit image will have 2 entry's with 4 bytes for each entry. + /// + private const int ColorPaletteSize1Bit = 8; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The color depth, in number of bits per pixel. + /// + private BmpBitsPerPixel? bitsPerPixel; + + /// + /// A bitmap v4 header will only be written, if the user explicitly wants support for transparency. + /// In this case the compression type BITFIELDS will be used. + /// If the image contains a color profile, a bitmap v5 header is written, which is needed to write this info. + /// Otherwise a bitmap v3 header will be written, which is supported by almost all decoders. + /// + private BmpInfoHeaderType infoHeaderType; + + /// + /// The quantizer for reducing the color count for 8-Bit, 4-Bit and 1-Bit images. + /// + private readonly IQuantizer quantizer; + + /// + /// The pixel sampling strategy for quantization. + /// + private readonly IPixelSamplingStrategy pixelSamplingStrategy; + + /// + /// The transparent color mode. + /// + private readonly TransparentColorMode transparentColorMode; + + /// + private readonly bool processedAlphaMask; + + /// + private readonly bool skipFileHeader; + + /// + private readonly bool isDoubleHeight; + + /// + /// Initializes a new instance of the class. + /// + /// The encoder with options. + /// The memory manager. + public BmpEncoderCore(BmpEncoder encoder, MemoryAllocator memoryAllocator) + { + this.memoryAllocator = memoryAllocator; + this.bitsPerPixel = encoder.BitsPerPixel; + + // TODO: Use a palette quantizer if supplied. + this.quantizer = encoder.Quantizer ?? KnownQuantizers.Hexadecatree; + this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; + this.transparentColorMode = encoder.TransparentColorMode; + this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3; + this.processedAlphaMask = encoder.ProcessedAlphaMask; + this.skipFileHeader = encoder.SkipFileHeader; + this.isDoubleHeight = encoder.UseDoubleHeight; + } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + // Stream may not at 0. + long basePosition = stream.Position; + + Configuration configuration = image.Configuration; + ImageMetadata metadata = image.Metadata; + BmpMetadata bmpMetadata = metadata.GetBmpMetadata(); + this.bitsPerPixel ??= bmpMetadata.BitsPerPixel; + + ushort bpp = (ushort)this.bitsPerPixel; + int bytesPerLine = (int)(4 * ((((uint)image.Width * bpp) + 31) / 32)); + this.padding = bytesPerLine - (int)(image.Width * (bpp / 8F)); + + int colorPaletteSize = this.bitsPerPixel switch + { + BmpBitsPerPixel.Bit8 => ColorPaletteSize8Bit, + BmpBitsPerPixel.Bit4 => ColorPaletteSize4Bit, + BmpBitsPerPixel.Bit2 => ColorPaletteSize2Bit, + BmpBitsPerPixel.Bit1 => ColorPaletteSize1Bit, + _ => 0 + }; + + byte[]? iccProfileData = null; + int iccProfileSize = 0; + if (metadata.IccProfile != null) + { + this.infoHeaderType = BmpInfoHeaderType.WinVersion5; + iccProfileData = metadata.IccProfile.ToByteArray(); + iccProfileSize = iccProfileData.Length; + } + + int infoHeaderSize = this.infoHeaderType switch + { + BmpInfoHeaderType.WinVersion3 => BmpInfoHeader.SizeV3, + BmpInfoHeaderType.WinVersion4 => BmpInfoHeader.SizeV4, + BmpInfoHeaderType.WinVersion5 => BmpInfoHeader.SizeV5, + _ => BmpInfoHeader.SizeV3 + }; + + // for ico/cur encoder. + int height = image.Height; + if (this.isDoubleHeight) + { + height <<= 1; + } + + BmpInfoHeader infoHeader = this.CreateBmpInfoHeader(image.Width, height, infoHeaderSize, bpp, bytesPerLine, metadata, iccProfileData); + + Span buffer = stackalloc byte[infoHeaderSize]; + + // For ico/cur encoder. + if (!this.skipFileHeader) + { + WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer); + } + + this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize); + this.WriteImage(configuration, stream, image, cancellationToken); + WriteColorProfile(stream, iccProfileData, buffer, basePosition); + + stream.Flush(); + } + + /// + /// Creates the bitmap information header. + /// + /// The width of the image. + /// The height of the image. + /// Size of the information header. + /// The bits per pixel. + /// The bytes per line. + /// The metadata. + /// The icc profile data. + /// The bitmap information header. + private BmpInfoHeader CreateBmpInfoHeader(int width, int height, int infoHeaderSize, ushort bpp, int bytesPerLine, ImageMetadata metadata, byte[]? iccProfileData) + { + int hResolution = 0; + int vResolution = 0; + + if (metadata.ResolutionUnits != PixelResolutionUnit.AspectRatio + && metadata.HorizontalResolution > 0 + && metadata.VerticalResolution > 0) + { + switch (metadata.ResolutionUnits) + { + case PixelResolutionUnit.PixelsPerInch: + + hResolution = (int)Math.Round(UnitConverter.InchToMeter(metadata.HorizontalResolution)); + vResolution = (int)Math.Round(UnitConverter.InchToMeter(metadata.VerticalResolution)); + break; + + case PixelResolutionUnit.PixelsPerCentimeter: + + hResolution = (int)Math.Round(UnitConverter.CmToMeter(metadata.HorizontalResolution)); + vResolution = (int)Math.Round(UnitConverter.CmToMeter(metadata.VerticalResolution)); + break; + + case PixelResolutionUnit.PixelsPerMeter: + hResolution = (int)Math.Round(metadata.HorizontalResolution); + vResolution = (int)Math.Round(metadata.VerticalResolution); + + break; + } + } + + BmpInfoHeader infoHeader = new( + headerSize: infoHeaderSize, + width: width, + height: height, + planes: 1, + bitsPerPixel: bpp, + imageSize: height * bytesPerLine, + xPelsPerMeter: hResolution, + yPelsPerMeter: vResolution, + clrUsed: 0, + clrImportant: 0); + + if ((this.infoHeaderType is BmpInfoHeaderType.WinVersion4 or BmpInfoHeaderType.WinVersion5) && this.bitsPerPixel == BmpBitsPerPixel.Bit32) + { + infoHeader.AlphaMask = Rgba32AlphaMask; + infoHeader.RedMask = Rgba32RedMask; + infoHeader.GreenMask = Rgba32GreenMask; + infoHeader.BlueMask = Rgba32BlueMask; + infoHeader.Compression = BmpCompression.BitFields; + } + + if (this.infoHeaderType is BmpInfoHeaderType.WinVersion5 && iccProfileData != null) + { + infoHeader.ProfileSize = iccProfileData.Length; + infoHeader.CsType = BmpColorSpace.PROFILE_EMBEDDED; + infoHeader.Intent = BmpRenderingIntent.LCS_GM_IMAGES; + } + + return infoHeader; + } + + /// + /// Writes the color profile to the stream. + /// + /// The stream to write to. + /// The color profile data. + /// The buffer. + /// The Stream may not be start with 0. + private static void WriteColorProfile(Stream stream, byte[]? iccProfileData, Span buffer, long basePosition) + { + if (iccProfileData != null) + { + // The offset, in bytes, from the beginning of the BITMAPV5HEADER structure to the start of the profile data. + int streamPositionAfterImageData = (int)stream.Position - BmpFileHeader.Size; + stream.Write(iccProfileData); + long position = stream.Position; // Storage Position + BinaryPrimitives.WriteInt32LittleEndian(buffer, streamPositionAfterImageData); + _ = stream.Seek(basePosition, SeekOrigin.Begin); + _ = stream.Seek(BmpFileHeader.Size + 112, SeekOrigin.Current); + stream.Write(buffer[..4]); + _ = stream.Seek(position, SeekOrigin.Begin); // Reset Position + } + } + + /// + /// Writes the bitmap file header. + /// + /// The stream to write the header to. + /// Size of the bitmap information header. + /// Size of the color palette. + /// The size in bytes of the color profile. + /// The information header to write. + /// The buffer to write to. + private static void WriteBitmapFileHeader(Stream stream, int infoHeaderSize, int colorPaletteSize, int iccProfileSize, BmpInfoHeader infoHeader, Span buffer) + { + BmpFileHeader fileHeader = new( + type: BmpConstants.TypeMarkers.Bitmap, + fileSize: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize + iccProfileSize + infoHeader.ImageSize, + reserved: 0, + offset: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize); + + fileHeader.WriteTo(buffer); + stream.Write(buffer, 0, BmpFileHeader.Size); + } + + /// + /// Writes the bitmap information header. + /// + /// The stream to write info header into. + /// The information header. + /// The buffer. + /// Size of the information header. + private void WriteBitmapInfoHeader(Stream stream, BmpInfoHeader infoHeader, Span buffer, int infoHeaderSize) + { + switch (this.infoHeaderType) + { + case BmpInfoHeaderType.WinVersion3: + infoHeader.WriteV3Header(buffer); + break; + case BmpInfoHeaderType.WinVersion4: + infoHeader.WriteV4Header(buffer); + break; + case BmpInfoHeaderType.WinVersion5: + infoHeader.WriteV5Header(buffer); + break; + } + + stream.Write(buffer, 0, infoHeaderSize); + } + + /// + /// Writes the pixel data to the binary stream. + /// + /// The pixel format. + /// The global configuration. + /// The to write to. + /// + /// The containing pixel data. + /// + /// The token to monitor for cancellation requests. + private void WriteImage( + Configuration configuration, + Stream stream, + Image image, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + ImageFrame? clonedFrame = null; + try + { + // No need to clone when quantizing. The quantizer will do it for us. + // TODO: We should really try to avoid the clone entirely. + int bpp = this.bitsPerPixel != null ? (int)this.bitsPerPixel : 32; + if (bpp > 8 && EncodingUtilities.ShouldReplaceTransparentPixels(this.transparentColorMode)) + { + clonedFrame = image.Frames.RootFrame.Clone(); + EncodingUtilities.ReplaceTransparentPixels(clonedFrame); + } + + ImageFrame encodingFrame = clonedFrame ?? image.Frames.RootFrame; + Buffer2D pixels = encodingFrame.PixelBuffer; + + switch (this.bitsPerPixel) + { + case BmpBitsPerPixel.Bit32: + this.Write32BitPixelData(configuration, stream, pixels, cancellationToken); + break; + + case BmpBitsPerPixel.Bit24: + this.Write24BitPixelData(configuration, stream, pixels, cancellationToken); + break; + + case BmpBitsPerPixel.Bit16: + this.Write16BitPixelData(configuration, stream, pixels, cancellationToken); + break; + + case BmpBitsPerPixel.Bit8: + this.Write8BitPixelData(configuration, stream, encodingFrame, cancellationToken); + break; + + case BmpBitsPerPixel.Bit4: + this.Write4BitPixelData(configuration, stream, encodingFrame, cancellationToken); + break; + + case BmpBitsPerPixel.Bit2: + this.Write2BitPixelData(configuration, stream, encodingFrame, cancellationToken); + break; + + case BmpBitsPerPixel.Bit1: + this.Write1BitPixelData(configuration, stream, encodingFrame, cancellationToken); + break; + } + + if (this.processedAlphaMask) + { + ProcessedAlphaMask(stream, encodingFrame); + } + } + finally + { + clonedFrame?.Dispose(); + } + } + + private IMemoryOwner AllocateRow(int width, int bytesPerPixel) + => this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, this.padding); + + /// + /// Writes 32-bit data with a color palette to the stream. + /// + /// The pixel format. + /// The global configuration. + /// The to write to. + /// The containing pixel data. + /// The token to monitor for cancellation requests. + private void Write32BitPixelData( + Configuration configuration, + Stream stream, + Buffer2D pixels, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner row = this.AllocateRow(pixels.Width, 4); + Span rowSpan = row.GetSpan(); + + for (int y = pixels.Height - 1; y >= 0; y--) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.ToBgra32Bytes( + configuration, + pixelSpan, + rowSpan, + pixelSpan.Length); + stream.Write(rowSpan); + } + } + + /// + /// Writes 24-bit pixel data with a color palette to the stream. + /// + /// The pixel format. + /// The global configuration. + /// The to write to. + /// The containing pixel data. + /// The token to monitor for cancellation requests. + private void Write24BitPixelData( + Configuration configuration, + Stream stream, + Buffer2D pixels, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + int width = pixels.Width; + int rowBytesWithoutPadding = width * 3; + using IMemoryOwner row = this.AllocateRow(width, 3); + Span rowSpan = row.GetSpan(); + + for (int y = pixels.Height - 1; y >= 0; y--) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.ToBgr24Bytes( + configuration, + pixelSpan, + row.Slice(0, rowBytesWithoutPadding), + width); + stream.Write(rowSpan); + } + } + + /// + /// Writes 16-bit pixel data with a color palette to the stream. + /// + /// The type of the pixel. + /// The global configuration. + /// The to write to. + /// The containing pixel data. + /// The token to monitor for cancellation requests. + private void Write16BitPixelData( + Configuration configuration, + Stream stream, + Buffer2D pixels, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + int width = pixels.Width; + int rowBytesWithoutPadding = width * 2; + using IMemoryOwner row = this.AllocateRow(width, 2); + Span rowSpan = row.GetSpan(); + + for (int y = pixels.Height - 1; y >= 0; y--) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + + PixelOperations.Instance.ToBgra5551Bytes( + configuration, + pixelSpan, + row.Slice(0, rowBytesWithoutPadding), + pixelSpan.Length); + + stream.Write(rowSpan); + } + } + + /// + /// Writes 8 bit pixel data with a color palette. The color palette has 256 entry's with 4 bytes for each entry. + /// + /// The type of the pixel. + /// The global configuration. + /// The to write to. + /// The containing pixel data. + /// The token to monitor for cancellation requests. + private void Write8BitPixelData( + Configuration configuration, + Stream stream, + ImageFrame encodingFrame, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + PixelTypeInfo info = TPixel.GetPixelTypeInfo(); + bool is8BitLuminance = + info.BitsPerPixel == 8 + && info.ColorType == PixelColorType.Luminance + && info.AlphaRepresentation == PixelAlphaRepresentation.None + && info.ComponentInfo!.Value.ComponentCount == 1; + + using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize8Bit, AllocationOptions.Clean); + Span colorPalette = colorPaletteBuffer.GetSpan(); + + if (is8BitLuminance) + { + this.Write8BitLuminancePixelData(stream, encodingFrame, colorPalette, cancellationToken); + } + else + { + this.Write8BitColor(configuration, stream, encodingFrame, colorPalette, cancellationToken); + } + } + + /// + /// Writes an 8 bit color image with a color palette. The color palette has 256 entry's with 4 bytes for each entry. + /// + /// The type of the pixel. + /// The global configuration. + /// The to write to. + /// The containing pixel data. + /// A byte span of size 1024 for the color palette. + /// The token to monitor for cancellation requests. + private void Write8BitColor( + Configuration configuration, + Stream stream, + ImageFrame encodingFrame, + Span colorPalette, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration); + + frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame); + using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds); + + ReadOnlySpan quantizedColorPalette = quantized.Palette.Span; + WriteColorPalette(configuration, stream, quantizedColorPalette, colorPalette); + + for (int y = encodingFrame.Height - 1; y >= 0; y--) + { + cancellationToken.ThrowIfCancellationRequested(); + + ReadOnlySpan pixelSpan = quantized.DangerousGetRowSpan(y); + stream.Write(pixelSpan); + + for (int i = 0; i < this.padding; i++) + { + stream.WriteByte(0); + } + } + } + + /// + /// Writes 8 bit gray pixel data with a color palette. The color palette has 256 entry's with 4 bytes for each entry. + /// + /// The type of the pixel. + /// The to write to. + /// The containing pixel data. + /// A byte span of size 1024 for the color palette. + /// The token to monitor for cancellation requests. + private void Write8BitLuminancePixelData( + Stream stream, + ImageFrame encodingFrame, + Span colorPalette, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + // Create a color palette with 256 different gray values. + for (int i = 0; i <= 255; i++) + { + int idx = i * 4; + byte grayValue = (byte)i; + colorPalette[idx] = grayValue; + colorPalette[idx + 1] = grayValue; + colorPalette[idx + 2] = grayValue; + + // Padding byte, always 0. + colorPalette[idx + 3] = 0; + } + + stream.Write(colorPalette); + Buffer2D imageBuffer = encodingFrame.PixelBuffer; + for (int y = encodingFrame.Height - 1; y >= 0; y--) + { + cancellationToken.ThrowIfCancellationRequested(); + + ReadOnlySpan inputPixelRow = imageBuffer.DangerousGetRowSpan(y); + ReadOnlySpan outputPixelRow = MemoryMarshal.AsBytes(inputPixelRow); + stream.Write(outputPixelRow); + + for (int i = 0; i < this.padding; i++) + { + stream.WriteByte(0); + } + } + } + + /// + /// Writes 4 bit pixel data with a color palette. The color palette has 16 entry's with 4 bytes for each entry. + /// + /// The type of the pixel. + /// The global configuration. + /// The to write to. + /// The containing pixel data. + /// The token to monitor for cancellation requests. + private void Write4BitPixelData( + Configuration configuration, + Stream stream, + ImageFrame encodingFrame, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration, new QuantizerOptions + { + MaxColors = 16, + Dither = this.quantizer.Options.Dither, + DitherScale = this.quantizer.Options.DitherScale + }); + + frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame); + + using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds); + using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize4Bit, AllocationOptions.Clean); + + Span colorPalette = colorPaletteBuffer.GetSpan(); + ReadOnlySpan quantizedColorPalette = quantized.Palette.Span; + WriteColorPalette(configuration, stream, quantizedColorPalette, colorPalette); + + ReadOnlySpan pixelRowSpan = quantized.DangerousGetRowSpan(0); + int rowPadding = pixelRowSpan.Length % 2 != 0 ? this.padding - 1 : this.padding; + for (int y = encodingFrame.Height - 1; y >= 0; y--) + { + cancellationToken.ThrowIfCancellationRequested(); + + pixelRowSpan = quantized.DangerousGetRowSpan(y); + + int endIdx = pixelRowSpan.Length % 2 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 1; + for (int i = 0; i < endIdx; i += 2) + { + stream.WriteByte((byte)((pixelRowSpan[i] << 4) | pixelRowSpan[i + 1])); + } + + if (pixelRowSpan.Length % 2 != 0) + { + stream.WriteByte((byte)((pixelRowSpan[^1] << 4) | 0)); + } + + for (int i = 0; i < rowPadding; i++) + { + stream.WriteByte(0); + } + } + } + + /// + /// Writes 2 bit pixel data with a color palette. The color palette has 4 entry's with 4 bytes for each entry. + /// + /// The type of the pixel. + /// The global configuration. + /// The to write to. + /// The containing pixel data. + /// The token to monitor for cancellation requests. + private void Write2BitPixelData( + Configuration configuration, + Stream stream, + ImageFrame encodingFrame, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration, new QuantizerOptions + { + MaxColors = 4, + Dither = this.quantizer.Options.Dither, + DitherScale = this.quantizer.Options.DitherScale + }); + + frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame); + + using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds); + using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize2Bit, AllocationOptions.Clean); + + Span colorPalette = colorPaletteBuffer.GetSpan(); + ReadOnlySpan quantizedColorPalette = quantized.Palette.Span; + WriteColorPalette(configuration, stream, quantizedColorPalette, colorPalette); + + ReadOnlySpan pixelRowSpan = quantized.DangerousGetRowSpan(0); + int rowPadding = pixelRowSpan.Length % 4 != 0 ? this.padding - 1 : this.padding; + for (int y = encodingFrame.Height - 1; y >= 0; y--) + { + cancellationToken.ThrowIfCancellationRequested(); + + pixelRowSpan = quantized.DangerousGetRowSpan(y); + + int endIdx = pixelRowSpan.Length % 4 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 4; + int i = 0; + for (i = 0; i < endIdx; i += 4) + { + stream.WriteByte((byte)((pixelRowSpan[i] << 6) | (pixelRowSpan[i + 1] << 4) | (pixelRowSpan[i + 2] << 2) | pixelRowSpan[i + 3])); + } + + if (pixelRowSpan.Length % 4 != 0) + { + int shift = 6; + byte pixelData = 0; + for (; i < pixelRowSpan.Length; i++) + { + pixelData = (byte)(pixelData | (pixelRowSpan[i] << shift)); + shift -= 2; + } + + stream.WriteByte(pixelData); + } + + for (i = 0; i < rowPadding; i++) + { + stream.WriteByte(0); + } + } + } + + /// + /// Writes 1 bit pixel data with a color palette. The color palette has 2 entry's with 4 bytes for each entry. + /// + /// The type of the pixel. + /// The global configuration. + /// The to write to. + /// The containing pixel data. + /// The token to monitor for cancellation requests. + private void Write1BitPixelData( + Configuration configuration, + Stream stream, + ImageFrame encodingFrame, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration, new QuantizerOptions + { + MaxColors = 2, + Dither = this.quantizer.Options.Dither, + DitherScale = this.quantizer.Options.DitherScale + }); + + frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame); + + using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds); + using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize1Bit, AllocationOptions.Clean); + + Span colorPalette = colorPaletteBuffer.GetSpan(); + ReadOnlySpan quantizedColorPalette = quantized.Palette.Span; + WriteColorPalette(configuration, stream, quantizedColorPalette, colorPalette); + + ReadOnlySpan quantizedPixelRow = quantized.DangerousGetRowSpan(0); + int rowPadding = quantizedPixelRow.Length % 8 != 0 ? this.padding - 1 : this.padding; + for (int y = encodingFrame.Height - 1; y >= 0; y--) + { + cancellationToken.ThrowIfCancellationRequested(); + + quantizedPixelRow = quantized.DangerousGetRowSpan(y); + + int endIdx = quantizedPixelRow.Length % 8 == 0 ? quantizedPixelRow.Length : quantizedPixelRow.Length - 8; + for (int i = 0; i < endIdx; i += 8) + { + Write1BitPalette(stream, i, i + 8, quantizedPixelRow); + } + + if (quantizedPixelRow.Length % 8 != 0) + { + int startIdx = quantizedPixelRow.Length - (quantizedPixelRow.Length % 8); + endIdx = quantizedPixelRow.Length; + Write1BitPalette(stream, startIdx, endIdx, quantizedPixelRow); + } + + for (int i = 0; i < rowPadding; i++) + { + stream.WriteByte(0); + } + } + } + + /// + /// Writes the color palette to the stream. The color palette has 4 bytes for each entry. + /// + /// The type of the pixel. + /// The global configuration. + /// The to write to. + /// The color palette from the quantized image. + /// A temporary byte span to write the color palette to. + private static void WriteColorPalette(Configuration configuration, Stream stream, ReadOnlySpan quantizedColorPalette, Span colorPalette) + where TPixel : unmanaged, IPixel + { + int quantizedColorBytes = quantizedColorPalette.Length * 4; + PixelOperations.Instance.ToBgra32(configuration, quantizedColorPalette, MemoryMarshal.Cast(colorPalette[..quantizedColorBytes])); + Span colorPaletteAsUInt = MemoryMarshal.Cast(colorPalette); + for (int i = 0; i < colorPaletteAsUInt.Length; i++) + { + colorPaletteAsUInt[i] &= 0x00FFFFFF; // Padding byte, always 0. + } + + stream.Write(colorPalette); + } + + /// + /// Writes a 1-bit palette. + /// + /// The stream to write the palette to. + /// The start index. + /// The end index. + /// A quantized pixel row. + private static void Write1BitPalette(Stream stream, int startIdx, int endIdx, ReadOnlySpan quantizedPixelRow) + { + int shift = 7; + byte indices = 0; + for (int j = startIdx; j < endIdx; j++) + { + indices = (byte)(indices | ((byte)(quantizedPixelRow[j] & 1) << shift)); + shift--; + } + + stream.WriteByte(indices); + } + + private static void ProcessedAlphaMask(Stream stream, ImageFrame encodingFrame) + where TPixel : unmanaged, IPixel + { + int arrayWidth = encodingFrame.Width / 8; + int padding = arrayWidth % 4; + if (padding is not 0) + { + padding = 4 - padding; + } + + Span mask = stackalloc byte[arrayWidth]; + for (int y = encodingFrame.Height - 1; y >= 0; y--) + { + mask.Clear(); + Span row = encodingFrame.PixelBuffer.DangerousGetRowSpan(y); + + for (int i = 0; i < arrayWidth; i++) + { + int x = i * 8; + + for (int j = 0; j < 8; j++) + { + WriteAlphaMask(row[x + j], ref mask[i], j); + } + } + + stream.Write(mask); + stream.Skip(padding); + } + } + + private static void WriteAlphaMask(in TPixel pixel, ref byte mask, in int index) + where TPixel : unmanaged, IPixel + { + Rgba32 rgba = pixel.ToRgba32(); + if (rgba.A is 0) + { + mask |= unchecked((byte)(0b10000000 >> index)); + } + } + } +} diff --git a/ImageSharp/Formats/Bmp/BmpFileHeader.cs b/ImageSharp/Formats/Bmp/BmpFileHeader.cs new file mode 100644 index 0000000..2ba0c32 --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpFileHeader.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Stores general information about the Bitmap file. + /// + /// + /// + /// The first two bytes of the Bitmap file format + /// (thus the Bitmap header) are stored in big-endian order. + /// All of the other integer values are stored in little-endian format + /// (i.e. least-significant byte first). + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal readonly struct BmpFileHeader + { + /// + /// Defines the size of the data structure in the bitmap file. + /// + public const int Size = 14; + + public BmpFileHeader(short type, int fileSize, int reserved, int offset) + { + this.Type = type; + this.FileSize = fileSize; + this.Reserved = reserved; + this.Offset = offset; + } + + /// + /// Gets the Bitmap identifier. + /// The field used to identify the bitmap file: 0x42 0x4D + /// (Hex code points for B and M) + /// + public short Type { get; } + + /// + /// Gets the size of the bitmap file in bytes. + /// + public int FileSize { get; } + + /// + /// Gets any reserved data; actual value depends on the application + /// that creates the image. + /// + public int Reserved { get; } + + /// + /// Gets the offset, i.e. starting address, of the byte where + /// the bitmap data can be found. + /// + public int Offset { get; } + + public static BmpFileHeader Parse(Span data) => MemoryMarshal.Cast(data)[0]; + + public void WriteTo(Span buffer) + { + ref BmpFileHeader dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); + + dest = this; + } + } +} diff --git a/ImageSharp/Formats/Bmp/BmpFileMarkerType.cs b/ImageSharp/Formats/Bmp/BmpFileMarkerType.cs new file mode 100644 index 0000000..81727c8 --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpFileMarkerType.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Indicates which bitmap file marker was read. + /// + public enum BmpFileMarkerType + { + /// + /// Single-image BMP file that may have been created under Windows or OS/2. + /// + Bitmap, + + /// + /// OS/2 Bitmap Array. + /// + BitmapArray, + + /// + /// OS/2 Color Icon. + /// + ColorIcon, + + /// + /// OS/2 Color Pointer. + /// + ColorPointer, + + /// + /// OS/2 Icon. + /// + Icon, + + /// + /// OS/2 Pointer. + /// + Pointer + } +} diff --git a/ImageSharp/Formats/Bmp/BmpFormat.cs b/ImageSharp/Formats/Bmp/BmpFormat.cs new file mode 100644 index 0000000..d28e3e9 --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpFormat.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Registers the image encoders, decoders and mime type detectors for the bmp format. + /// + public sealed class BmpFormat : IImageFormat + { + private BmpFormat() + { + } + + /// + /// Gets the shared instance. + /// + public static BmpFormat Instance { get; } = new(); + + /// + public string Name => "BMP"; + + /// + public string DefaultMimeType => "image/bmp"; + + /// + public IEnumerable MimeTypes => BmpConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => BmpConstants.FileExtensions; + + /// + public BmpMetadata CreateDefaultFormatMetadata() => new BmpMetadata(); + } +} diff --git a/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs b/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs new file mode 100644 index 0000000..bade488 --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Detects bmp file headers. + /// + public sealed class BmpImageFormatDetector : IImageFormatDetector + { + /// + public int HeaderSize => 2; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.IsSupportedFileFormat(header) ? BmpFormat.Instance : null; + + return format != null; + } + + private bool IsSupportedFileFormat(ReadOnlySpan header) + { + if (header.Length >= this.HeaderSize) + { + short fileTypeMarker = BinaryPrimitives.ReadInt16LittleEndian(header); + return fileTypeMarker == BmpConstants.TypeMarkers.Bitmap || + fileTypeMarker == BmpConstants.TypeMarkers.BitmapArray; + } + + return false; + } + } +} diff --git a/ImageSharp/Formats/Bmp/BmpInfoHeader.cs b/ImageSharp/Formats/Bmp/BmpInfoHeader.cs new file mode 100644 index 0000000..879d1ad --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpInfoHeader.cs @@ -0,0 +1,544 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// This block of bytes tells the application detailed information + /// about the image, which will be used to display the image on + /// the screen. + /// + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct BmpInfoHeader + { + /// + /// Defines the size of the BITMAPCOREHEADER data structure in the bitmap file. + /// + public const int CoreSize = 12; + + /// + /// Defines the size of the short variant of the OS22XBITMAPHEADER data structure in the bitmap file. + /// + public const int Os22ShortSize = 16; + + /// + /// Defines the size of the BITMAPINFOHEADER (BMP Version 3) data structure in the bitmap file. + /// + public const int SizeV3 = 40; + + /// + /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks are part of the info header instead of following it. + /// + public const int AdobeV3Size = 52; + + /// + /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks (including the alpha channel) are part of the info header instead of following it. + /// + public const int AdobeV3WithAlphaSize = 56; + + /// + /// Size of a IBM OS/2 2.x bitmap header. + /// + public const int Os2v2Size = 64; + + /// + /// Defines the size of the BITMAPINFOHEADER (BMP Version 4) data structure in the bitmap file. + /// + public const int SizeV4 = 108; + + /// + /// Defines the size of the BITMAPINFOHEADER (BMP Version 5) data structure in the bitmap file. + /// + public const int SizeV5 = 124; + + /// + /// Defines the size of the biggest supported header data structure in the bitmap file. + /// + public const int MaxHeaderSize = SizeV5; + + /// + /// Defines the size of the field. + /// + public const int HeaderSizeSize = 4; + + public BmpInfoHeader( + int headerSize, + int width, + int height, + short planes, + ushort bitsPerPixel, + BmpCompression compression = default, + int imageSize = 0, + int xPelsPerMeter = 0, + int yPelsPerMeter = 0, + int clrUsed = 0, + int clrImportant = 0, + int redMask = 0, + int greenMask = 0, + int blueMask = 0, + int alphaMask = 0, + BmpColorSpace csType = 0, + int redX = 0, + int redY = 0, + int redZ = 0, + int greenX = 0, + int greenY = 0, + int greenZ = 0, + int blueX = 0, + int blueY = 0, + int blueZ = 0, + int gammeRed = 0, + int gammeGreen = 0, + int gammeBlue = 0, + BmpRenderingIntent intent = BmpRenderingIntent.Invalid, + int profileData = 0, + int profileSize = 0, + int reserved = 0) + { + this.HeaderSize = headerSize; + this.Width = width; + this.Height = height; + this.Planes = planes; + this.BitsPerPixel = bitsPerPixel; + this.Compression = compression; + this.ImageSize = imageSize; + this.XPelsPerMeter = xPelsPerMeter; + this.YPelsPerMeter = yPelsPerMeter; + this.ClrUsed = clrUsed; + this.ClrImportant = clrImportant; + this.RedMask = redMask; + this.GreenMask = greenMask; + this.BlueMask = blueMask; + this.AlphaMask = alphaMask; + this.CsType = csType; + this.RedX = redX; + this.RedY = redY; + this.RedZ = redZ; + this.GreenX = greenX; + this.GreenY = greenY; + this.GreenZ = greenZ; + this.BlueX = blueX; + this.BlueY = blueY; + this.BlueZ = blueZ; + this.GammaRed = gammeRed; + this.GammaGreen = gammeGreen; + this.GammaBlue = gammeBlue; + this.Intent = intent; + this.ProfileData = profileData; + this.ProfileSize = profileSize; + this.Reserved = reserved; + } + + /// + /// Gets or sets the size of this header. + /// + public int HeaderSize { get; set; } + + /// + /// Gets or sets the bitmap width in pixels (signed integer). + /// + public int Width { get; set; } + + /// + /// Gets or sets the bitmap height in pixels (signed integer). + /// + public int Height { get; set; } + + /// + /// Gets or sets the number of color planes being used. Must be set to 1. + /// + public short Planes { get; set; } + + /// + /// Gets or sets the number of bits per pixel, which is the color depth of the image. + /// Typical values are 1, 4, 8, 16, 24 and 32. + /// + public ushort BitsPerPixel { get; set; } + + /// + /// Gets or sets the compression method being used. + /// See the next table for a list of possible values. + /// + public BmpCompression Compression { get; set; } + + /// + /// Gets or sets the image size. This is the size of the raw bitmap data (see below), + /// and should not be confused with the file size. + /// + public int ImageSize { get; set; } + + /// + /// Gets or sets the horizontal resolution of the image. + /// (pixel per meter, signed integer) + /// + public int XPelsPerMeter { get; set; } + + /// + /// Gets or sets the vertical resolution of the image. + /// (pixel per meter, signed integer) + /// + public int YPelsPerMeter { get; set; } + + /// + /// Gets or sets the number of colors in the color palette, + /// or 0 to default to 2^n. + /// + public int ClrUsed { get; set; } + + /// + /// Gets or sets the number of important colors used, + /// or 0 when every color is important{ get; set; } generally ignored. + /// + public int ClrImportant { get; set; } + + /// + /// Gets or sets red color mask. This is used with the BITFIELDS decoding. + /// + public int RedMask { get; set; } + + /// + /// Gets or sets green color mask. This is used with the BITFIELDS decoding. + /// + public int GreenMask { get; set; } + + /// + /// Gets or sets blue color mask. This is used with the BITFIELDS decoding. + /// + public int BlueMask { get; set; } + + /// + /// Gets or sets alpha color mask. This is not used yet. + /// + public int AlphaMask { get; set; } + + /// + /// Gets or sets the Color space type. Not used yet. + /// + public BmpColorSpace CsType { get; set; } + + /// + /// Gets or sets the X coordinate of red endpoint. Not used yet. + /// + public int RedX { get; set; } + + /// + /// Gets or sets the Y coordinate of red endpoint. Not used yet. + /// + public int RedY { get; set; } + + /// + /// Gets or sets the Z coordinate of red endpoint. Not used yet. + /// + public int RedZ { get; set; } + + /// + /// Gets or sets the X coordinate of green endpoint. Not used yet. + /// + public int GreenX { get; set; } + + /// + /// Gets or sets the Y coordinate of green endpoint. Not used yet. + /// + public int GreenY { get; set; } + + /// + /// Gets or sets the Z coordinate of green endpoint. Not used yet. + /// + public int GreenZ { get; set; } + + /// + /// Gets or sets the X coordinate of blue endpoint. Not used yet. + /// + public int BlueX { get; set; } + + /// + /// Gets or sets the Y coordinate of blue endpoint. Not used yet. + /// + public int BlueY { get; set; } + + /// + /// Gets or sets the Z coordinate of blue endpoint. Not used yet. + /// + public int BlueZ { get; set; } + + /// + /// Gets or sets the Gamma red coordinate scale value. Not used yet. + /// + public int GammaRed { get; set; } + + /// + /// Gets or sets the Gamma green coordinate scale value. Not used yet. + /// + public int GammaGreen { get; set; } + + /// + /// Gets or sets the Gamma blue coordinate scale value. Not used yet. + /// + public int GammaBlue { get; set; } + + /// + /// Gets or sets the rendering intent for bitmap. + /// + public BmpRenderingIntent Intent { get; set; } + + /// + /// Gets or sets the offset, in bytes, from the beginning of the BITMAPV5HEADER structure to the start of the profile data. + /// + public int ProfileData { get; set; } + + /// + /// Gets or sets the size, in bytes, of embedded profile data. + /// + public int ProfileSize { get; set; } + + /// + /// Gets or sets the reserved value. + /// + public int Reserved { get; set; } + + /// + /// Parses the BITMAPCOREHEADER (BMP Version 2) consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes). + /// + /// The data to parse. + /// The parsed header. + /// + public static BmpInfoHeader ParseCore(ReadOnlySpan data) => new( + headerSize: BinaryPrimitives.ReadInt32LittleEndian(data[..4]), + width: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(4, 2)), + height: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(6, 2)), + planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(8, 2)), + bitsPerPixel: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(10, 2))); + + /// + /// Parses a short variant of the OS22XBITMAPHEADER. It is identical to the BITMAPCOREHEADER, except that the width and height + /// are 4 bytes instead of 2, resulting in 16 bytes total. + /// + /// The data to parse. + /// The parsed header. + /// + public static BmpInfoHeader ParseOs22Short(ReadOnlySpan data) => new( + headerSize: BinaryPrimitives.ReadInt32LittleEndian(data[..4]), + width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), + height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), + planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)), + bitsPerPixel: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(14, 2))); + + /// + /// Parses the full BMP Version 3 BITMAPINFOHEADER header (40 bytes). + /// + /// The data to parse. + /// The parsed header. + /// + public static BmpInfoHeader ParseV3(ReadOnlySpan data) => new( + headerSize: BinaryPrimitives.ReadInt32LittleEndian(data[..4]), + width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), + height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), + planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)), + bitsPerPixel: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(14, 2)), + compression: (BmpCompression)BinaryPrimitives.ReadInt32LittleEndian(data.Slice(16, 4)), + imageSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(20, 4)), + xPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(24, 4)), + yPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)), + clrUsed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)), + clrImportant: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4))); + + /// + /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks are part of the info header instead of following it. + /// 52 bytes without the alpha mask, 56 bytes with the alpha mask. + /// + /// The data to parse. + /// Indicates, if the alpha bitmask is present. + /// The parsed header. + /// + public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan data, bool withAlpha = true) => new( + headerSize: BinaryPrimitives.ReadInt32LittleEndian(data[..4]), + width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), + height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), + planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)), + bitsPerPixel: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(14, 2)), + compression: (BmpCompression)BinaryPrimitives.ReadInt32LittleEndian(data.Slice(16, 4)), + imageSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(20, 4)), + xPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(24, 4)), + yPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)), + clrUsed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)), + clrImportant: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4)), + redMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(40, 4)), + greenMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(44, 4)), + blueMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(48, 4)), + alphaMask: withAlpha ? BinaryPrimitives.ReadInt32LittleEndian(data.Slice(52, 4)) : 0); + + /// + /// Parses a OS/2 version 2 bitmap header (64 bytes). Only the first 40 bytes are parsed which are + /// very similar to the Bitmap v3 header. The other 24 bytes are ignored, but they do not hold any + /// useful information for decoding the image. + /// + /// The data to parse. + /// The parsed header. + /// + public static BmpInfoHeader ParseOs2Version2(ReadOnlySpan data) + { + BmpInfoHeader infoHeader = new( + headerSize: BinaryPrimitives.ReadInt32LittleEndian(data[..4]), + width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), + height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), + planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)), + bitsPerPixel: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(14, 2))); + + // The compression value in OS/2 bitmap has a different meaning than in windows bitmaps. + // Map the OS/2 value to the windows values. + switch (BinaryPrimitives.ReadInt32LittleEndian(data.Slice(16, 4))) + { + case 0: + infoHeader.Compression = BmpCompression.RGB; + break; + case 1: + infoHeader.Compression = BmpCompression.RLE8; + break; + case 2: + infoHeader.Compression = BmpCompression.RLE4; + break; + case 4: + infoHeader.Compression = BmpCompression.RLE24; + break; + default: + // Compression type 3 (1DHuffman) is not supported. + BmpThrowHelper.ThrowInvalidImageContentException("Compression type is not supported. ImageSharp only supports uncompressed, RLE4, RLE8 and RLE24."); + break; + } + + infoHeader.ImageSize = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(20, 4)); + infoHeader.XPelsPerMeter = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(24, 4)); + infoHeader.YPelsPerMeter = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)); + infoHeader.ClrUsed = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)); + infoHeader.ClrImportant = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4)); + + // The following 24 bytes of the header are omitted. + return infoHeader; + } + + /// + /// Parses the full BMP Version 4 BITMAPINFOHEADER header (108 bytes). + /// + /// The data to parse. + /// The parsed header. + /// + public static BmpInfoHeader ParseV4(ReadOnlySpan data) => new( + headerSize: BinaryPrimitives.ReadInt32LittleEndian(data[..4]), + width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), + height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), + planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)), + bitsPerPixel: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(14, 2)), + compression: (BmpCompression)BinaryPrimitives.ReadInt32LittleEndian(data.Slice(16, 4)), + imageSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(20, 4)), + xPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(24, 4)), + yPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)), + clrUsed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)), + clrImportant: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4)), + redMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(40, 4)), + greenMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(44, 4)), + blueMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(48, 4)), + alphaMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(52, 4)), + csType: (BmpColorSpace)BinaryPrimitives.ReadInt32LittleEndian(data.Slice(56, 4)), + redX: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(60, 4)), + redY: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(64, 4)), + redZ: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(68, 4)), + greenX: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(72, 4)), + greenY: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(76, 4)), + greenZ: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(80, 4)), + blueX: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(84, 4)), + blueY: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(88, 4)), + blueZ: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(92, 4)), + gammeRed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(96, 4)), + gammeGreen: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(100, 4)), + gammeBlue: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(104, 4))); + + /// + /// Parses the full BMP Version 5 BITMAPINFOHEADER header (124 bytes). + /// + /// The data to parse. + /// The parsed header. + /// + /// Invalid size. + public static BmpInfoHeader ParseV5(ReadOnlySpan data) + { + if (data.Length < SizeV5) + { + throw new ArgumentException($"Must be {SizeV5} bytes. Was {data.Length} bytes.", nameof(data)); + } + + return MemoryMarshal.Cast(data)[0]; + } + + /// + /// Writes a bitmap version 3 (Microsoft Windows NT) header to a buffer (40 bytes). + /// + /// The buffer to write to. + public void WriteV3Header(Span buffer) + { + buffer.Clear(); + BinaryPrimitives.WriteInt32LittleEndian(buffer[..4], SizeV3); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(4, 4), this.Width); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(8, 4), this.Height); + BinaryPrimitives.WriteInt16LittleEndian(buffer.Slice(12, 2), this.Planes); + BinaryPrimitives.WriteUInt16LittleEndian(buffer.Slice(14, 2), this.BitsPerPixel); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(16, 4), (int)this.Compression); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(20, 4), this.ImageSize); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(24, 4), this.XPelsPerMeter); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(28, 4), this.YPelsPerMeter); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(32, 4), this.ClrUsed); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(36, 4), this.ClrImportant); + } + + /// + /// Writes a complete Bitmap V4 header to a buffer. + /// + /// The buffer to write to. + public void WriteV4Header(Span buffer) + { + buffer.Clear(); + BinaryPrimitives.WriteInt32LittleEndian(buffer[..4], SizeV4); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(4, 4), this.Width); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(8, 4), this.Height); + BinaryPrimitives.WriteInt16LittleEndian(buffer.Slice(12, 2), this.Planes); + BinaryPrimitives.WriteUInt16LittleEndian(buffer.Slice(14, 2), this.BitsPerPixel); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(16, 4), (int)this.Compression); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(20, 4), this.ImageSize); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(24, 4), this.XPelsPerMeter); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(28, 4), this.YPelsPerMeter); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(32, 4), this.ClrUsed); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(36, 4), this.ClrImportant); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(40, 4), this.RedMask); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(44, 4), this.GreenMask); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(48, 4), this.BlueMask); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(52, 4), this.AlphaMask); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(56, 4), (int)this.CsType); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(60, 4), this.RedX); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(64, 4), this.RedY); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(68, 4), this.RedZ); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(72, 4), this.GreenX); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(76, 4), this.GreenY); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(80, 4), this.GreenZ); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(84, 4), this.BlueX); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(88, 4), this.BlueY); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(92, 4), this.BlueZ); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(96, 4), this.GammaRed); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(100, 4), this.GammaGreen); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(104, 4), this.GammaBlue); + } + + /// + /// Writes a complete Bitmap V5 header to a buffer. + /// + /// The buffer to write to. + public void WriteV5Header(Span buffer) + { + ref BmpInfoHeader dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); + + dest = this; + } + } +} diff --git a/ImageSharp/Formats/Bmp/BmpInfoHeaderType.cs b/ImageSharp/Formats/Bmp/BmpInfoHeaderType.cs new file mode 100644 index 0000000..f43301f --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpInfoHeaderType.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Enum value for the different bitmap info header types. The enum value is the number of bytes for the specific bitmap header. + /// + public enum BmpInfoHeaderType + { + /// + /// Bitmap Core or BMP Version 2 header (Microsoft Windows 2.x). + /// + WinVersion2 = 12, + + /// + /// Short variant of the OS/2 Version 2 bitmap header. + /// + Os2Version2Short = 16, + + /// + /// BMP Version 3 header (Microsoft Windows 3.x or Microsoft Windows NT). + /// + WinVersion3 = 40, + + /// + /// Adobe variant of the BMP Version 3 header. + /// + AdobeVersion3 = 52, + + /// + /// Adobe variant of the BMP Version 3 header with an alpha mask. + /// + AdobeVersion3WithAlpha = 56, + + /// + /// BMP Version 2.x header (IBM OS/2 2.x). + /// + Os2Version2 = 64, + + /// + /// BMP Version 4 header (Microsoft Windows 95). + /// + WinVersion4 = 108, + + /// + /// BMP Version 5 header (Windows NT 5.0, 98 or later). + /// + WinVersion5 = 124, + } +} diff --git a/ImageSharp/Formats/Bmp/BmpMetadata.cs b/ImageSharp/Formats/Bmp/BmpMetadata.cs new file mode 100644 index 0000000..d64f923 --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpMetadata.cs @@ -0,0 +1,164 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +// TODO: Add color table information. +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Provides Bmp specific metadata information for the image. + /// + public class BmpMetadata : IFormatMetadata + { + /// + /// Initializes a new instance of the class. + /// + public BmpMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private BmpMetadata(BmpMetadata other) + { + this.BitsPerPixel = other.BitsPerPixel; + this.InfoHeaderType = other.InfoHeaderType; + + if (other.ColorTable?.Length > 0) + { + this.ColorTable = other.ColorTable.Value.ToArray(); + } + } + + /// + /// Gets or sets the bitmap info header type. + /// + public BmpInfoHeaderType InfoHeaderType { get; set; } + + /// + /// Gets or sets the number of bits per pixel. + /// + public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Bit24; + + /// + /// Gets or sets the color table, if any. + /// + public ReadOnlyMemory? ColorTable { get; set; } + + /// + public static BmpMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + int bpp = metadata.PixelTypeInfo.BitsPerPixel; + return bpp switch + { + 1 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Bit1 }, + 2 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Bit2 }, + <= 4 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Bit4 }, + <= 8 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Bit8 }, + <= 16 => new BmpMetadata + { + BitsPerPixel = BmpBitsPerPixel.Bit16, + InfoHeaderType = BmpInfoHeaderType.WinVersion3 + }, + <= 24 => new BmpMetadata + { + BitsPerPixel = BmpBitsPerPixel.Bit24, + InfoHeaderType = BmpInfoHeaderType.WinVersion4 + }, + _ => new BmpMetadata + { + BitsPerPixel = BmpBitsPerPixel.Bit32, + InfoHeaderType = BmpInfoHeaderType.WinVersion5 + } + }; + } + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp = (int)this.BitsPerPixel; + + PixelAlphaRepresentation alpha = this.InfoHeaderType switch + { + BmpInfoHeaderType.WinVersion2 or + BmpInfoHeaderType.Os2Version2Short or + BmpInfoHeaderType.WinVersion3 or + BmpInfoHeaderType.AdobeVersion3 or + BmpInfoHeaderType.Os2Version2 => PixelAlphaRepresentation.None, + BmpInfoHeaderType.AdobeVersion3WithAlpha or + BmpInfoHeaderType.WinVersion4 or + BmpInfoHeaderType.WinVersion5 or + _ => bpp < 32 ? PixelAlphaRepresentation.None : PixelAlphaRepresentation.Unassociated + }; + + PixelComponentInfo info; + PixelColorType color; + switch (this.BitsPerPixel) + { + case BmpBitsPerPixel.Bit1: + info = PixelComponentInfo.Create(1, bpp, 1); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit2: + info = PixelComponentInfo.Create(1, bpp, 2); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit4: + info = PixelComponentInfo.Create(1, bpp, 4); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit8: + info = PixelComponentInfo.Create(1, bpp, 8); + color = PixelColorType.Indexed; + break; + + // Could be 555 with padding but 565 is more common in newer bitmaps and offers + // greater accuracy due to extra green precision. + case BmpBitsPerPixel.Bit16: + info = PixelComponentInfo.Create(3, bpp, 5, 6, 5); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit24: + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit32 or _: + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + break; + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + EncodingType = this.BitsPerPixel <= BmpBitsPerPixel.Bit8 + ? EncodingType.Lossy + : EncodingType.Lossless, + PixelTypeInfo = this.GetPixelTypeInfo() + }; + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public BmpMetadata DeepClone() => new(this); + + /// + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + => this.ColorTable = null; + } +} diff --git a/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs b/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs new file mode 100644 index 0000000..0faa83c --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Enum for the different rendering intent's. + /// + internal enum BmpRenderingIntent + { + /// + /// Invalid default value. + /// + Invalid = 0, + + /// + /// Maintains saturation. Used for business charts and other situations in which undithered colors are required. + /// + LCS_GM_BUSINESS = 1, + + /// + /// Maintains colorimetric match. Used for graphic designs and named colors. + /// + LCS_GM_GRAPHICS = 2, + + /// + /// Maintains contrast. Used for photographs and natural images. + /// + LCS_GM_IMAGES = 4, + + /// + /// Maintains the white point. Matches the colors to their nearest color in the destination gamut. + /// + LCS_GM_ABS_COLORIMETRIC = 8, + } +} diff --git a/ImageSharp/Formats/Bmp/BmpThrowHelper.cs b/ImageSharp/Formats/Bmp/BmpThrowHelper.cs new file mode 100644 index 0000000..f3b1321 --- /dev/null +++ b/ImageSharp/Formats/Bmp/BmpThrowHelper.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Bmp { + internal static class BmpThrowHelper + { + public static void ThrowInvalidImageContentException(string errorMessage) + => throw new InvalidImageContentException(errorMessage); + + public static void ThrowNotSupportedException(string errorMessage) + => throw new NotSupportedException(errorMessage); + } +} diff --git a/ImageSharp/Formats/Bmp/README.md b/ImageSharp/Formats/Bmp/README.md new file mode 100644 index 0000000..f418307 --- /dev/null +++ b/ImageSharp/Formats/Bmp/README.md @@ -0,0 +1,17 @@ +### Encoder/Decoder adapted from: + +- [Nine.Imaging](https://github.com/yufeih/Nine.Imaging/) +- [imagetools.codeplex](https://imagetools.codeplex.com/) + +### Some useful links for documentation about the bitmap format: + +- [Microsoft Windows Bitmap File](http://www.fileformat.info/format/bmp/egff.htm) +- [OS/2 Bitmap File Format Summary](http://www.fileformat.info/format/os2bmp/egff.htm) +- [The DIB File Format](https://www-user.tu-chemnitz.de/~heha/viewchm.php/hs/petzold.chm/petzoldi/ch15b.htm) +- [Dr.Dobbs: The BMP File Format, Part 1](http://www.drdobbs.com/architecture-and-design/the-bmp-file-format-part-1/184409517) +- [Windows Bitmap File Format Specifications](ftp://ftp.nada.kth.se/pub/hacks/sgi/src/libwmf/doc/Bmpfrmat.html) + +### A set of bitmap test images: + +- [bmpsuite](http://entropymine.com/jason/bmpsuite/bmpsuite/html/bmpsuite.html) +- [eclecticgeek](http://eclecticgeek.com/dompdf/core_tests/image_bmp.html) \ No newline at end of file diff --git a/ImageSharp/Formats/Bmp/RleSkippedPixelHandling.cs b/ImageSharp/Formats/Bmp/RleSkippedPixelHandling.cs new file mode 100644 index 0000000..7cc4f88 --- /dev/null +++ b/ImageSharp/Formats/Bmp/RleSkippedPixelHandling.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Bmp { + /// + /// Defines possible options, how skipped pixels during decoding of run length encoded bitmaps should be treated. + /// + public enum RleSkippedPixelHandling : int + { + /// + /// Undefined pixels should be black. This is the default behavior and equal to how System.Drawing handles undefined pixels. + /// + Black = 0, + + /// + /// Undefined pixels should be transparent. + /// + Transparent = 1, + + /// + /// Undefined pixels should have the first color of the palette. + /// + FirstColorOfPalette = 2 + } +} diff --git a/ImageSharp/Formats/ColorProfileHandling.cs b/ImageSharp/Formats/ColorProfileHandling.cs new file mode 100644 index 0000000..5719c8b --- /dev/null +++ b/ImageSharp/Formats/ColorProfileHandling.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats { + /// + /// Provides enumeration of methods that control how ICC profiles are handled during decode. + /// + public enum ColorProfileHandling + { + /// + /// Leaves any embedded ICC color profiles intact. + /// + Preserve, + + /// + /// Removes any embedded Standard sRGB ICC color profiles without transforming the pixels of the image. + /// + Compact, + + /// + /// Transforms the pixels of the image based on the conversion of any embedded ICC color profiles to sRGB V4 profile. + /// The original profile is then removed. + /// + Convert + } +} diff --git a/ImageSharp/Formats/Cur/CurConfigurationModule.cs b/ImageSharp/Formats/Cur/CurConfigurationModule.cs new file mode 100644 index 0000000..d3f0057 --- /dev/null +++ b/ImageSharp/Formats/Cur/CurConfigurationModule.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Icon; + +namespace SixLabors.ImageSharp.Formats.Cur { + /// + /// Registers the image encoders, decoders and mime type detectors for the Ico format. + /// + public sealed class CurConfigurationModule : IImageFormatConfigurationModule + { + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetEncoder(CurFormat.Instance, new CurEncoder()); + configuration.ImageFormatsManager.SetDecoder(CurFormat.Instance, CurDecoder.Instance); + configuration.ImageFormatsManager.AddImageFormatDetector(new IconImageFormatDetector()); + } + } +} diff --git a/ImageSharp/Formats/Cur/CurConstants.cs b/ImageSharp/Formats/Cur/CurConstants.cs new file mode 100644 index 0000000..bd32d2b --- /dev/null +++ b/ImageSharp/Formats/Cur/CurConstants.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Cur { + /// + /// Defines constants relating to ICOs + /// + internal static class CurConstants + { + /// + /// The list of mime types that equate to a cur. + /// + /// + /// See + /// + public static readonly IEnumerable MimeTypes = + [ + + // IANA-registered + "image/vnd.microsoft.icon", + + // ICO & CUR types used by Windows + "image/x-icon", + + // Erroneous types but have been used + "image/ico", + "image/icon", + "text/ico", + "application/ico", + ]; + + /// + /// The list of file extensions that equate to a cur. + /// + public static readonly IEnumerable FileExtensions = ["cur"]; + + public const uint FileHeader = 0x00_02_00_00; + } +} diff --git a/ImageSharp/Formats/Cur/CurDecoder.cs b/ImageSharp/Formats/Cur/CurDecoder.cs new file mode 100644 index 0000000..450c975 --- /dev/null +++ b/ImageSharp/Formats/Cur/CurDecoder.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Cur { + /// + /// Decoder for generating an image out of a ico encoded stream. + /// + public sealed class CurDecoder : ImageDecoder + { + private CurDecoder() + { + } + + /// + /// Gets the shared instance. + /// + public static CurDecoder Instance { get; } = new(); + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + Image image = new CurDecoderCore(options).Decode(options.Configuration, stream, cancellationToken); + + ScaleToTargetSize(options, image); + + return image; + } + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(options, stream, cancellationToken); + + /// + protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + return new CurDecoderCore(options).Identify(options.Configuration, stream, cancellationToken); + } + } +} diff --git a/ImageSharp/Formats/Cur/CurDecoderCore.cs b/ImageSharp/Formats/Cur/CurDecoderCore.cs new file mode 100644 index 0000000..2a53698 --- /dev/null +++ b/ImageSharp/Formats/Cur/CurDecoderCore.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.Metadata; +using System; + +namespace SixLabors.ImageSharp.Formats.Cur { + internal sealed class CurDecoderCore : IconDecoderCore + { + public CurDecoderCore(DecoderOptions options) + : base(options) + { + } + + protected override void SetFrameMetadata( + ImageMetadata imageMetadata, + ImageFrameMetadata frameMetadata, + int index, + in IconDirEntry entry, + IconFrameCompression compression, + BmpBitsPerPixel bitsPerPixel, + ReadOnlyMemory? colorTable) + { + CurFrameMetadata curFrameMetadata = frameMetadata.GetCurMetadata(); + curFrameMetadata.FromIconDirEntry(entry); + curFrameMetadata.Compression = compression; + curFrameMetadata.BmpBitsPerPixel = bitsPerPixel; + curFrameMetadata.ColorTable = colorTable; + + if (index == 0) + { + CurMetadata curMetadata = imageMetadata.GetCurMetadata(); + curMetadata.Compression = compression; + curMetadata.BmpBitsPerPixel = bitsPerPixel; + curMetadata.ColorTable = colorTable; + } + } + } +} diff --git a/ImageSharp/Formats/Cur/CurEncoder.cs b/ImageSharp/Formats/Cur/CurEncoder.cs new file mode 100644 index 0000000..5f4af9a --- /dev/null +++ b/ImageSharp/Formats/Cur/CurEncoder.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Cur { + /// + /// Image encoder for writing an image to a stream as a Windows Cursor. + /// + public sealed class CurEncoder : QuantizingImageEncoder + { + /// + protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) + { + CurEncoderCore encoderCore = new(this); + encoderCore.Encode(image, stream, cancellationToken); + } + } +} diff --git a/ImageSharp/Formats/Cur/CurEncoderCore.cs b/ImageSharp/Formats/Cur/CurEncoderCore.cs new file mode 100644 index 0000000..8eb3032 --- /dev/null +++ b/ImageSharp/Formats/Cur/CurEncoderCore.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Icon; + +namespace SixLabors.ImageSharp.Formats.Cur { + internal sealed class CurEncoderCore : IconEncoderCore + { + public CurEncoderCore(QuantizingImageEncoder encoder) + : base(encoder, IconFileType.CUR) + { + } + } +} diff --git a/ImageSharp/Formats/Cur/CurFormat.cs b/ImageSharp/Formats/Cur/CurFormat.cs new file mode 100644 index 0000000..3f19a66 --- /dev/null +++ b/ImageSharp/Formats/Cur/CurFormat.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Linq; + +namespace SixLabors.ImageSharp.Formats.Cur { + /// + /// Registers the image encoders, decoders and mime type detectors for the ICO format. + /// + public sealed class CurFormat : IImageFormat + { + private CurFormat() + { + } + + /// + /// Gets the shared instance. + /// + public static CurFormat Instance { get; } = new(); + + /// + public string Name => "ICO"; + + /// + public string DefaultMimeType => CurConstants.MimeTypes.First(); + + /// + public IEnumerable MimeTypes => CurConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => CurConstants.FileExtensions; + + /// + public CurMetadata CreateDefaultFormatMetadata() => new(); + + /// + public CurFrameMetadata CreateDefaultFormatFrameMetadata() => new(); + } +} diff --git a/ImageSharp/Formats/Cur/CurFrameMetadata.cs b/ImageSharp/Formats/Cur/CurFrameMetadata.cs new file mode 100644 index 0000000..7f715bb --- /dev/null +++ b/ImageSharp/Formats/Cur/CurFrameMetadata.cs @@ -0,0 +1,241 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Cur { + /// + /// IcoFrameMetadata. + /// + public class CurFrameMetadata : IFormatFrameMetadata + { + /// + /// Initializes a new instance of the class. + /// + public CurFrameMetadata() + { + } + + private CurFrameMetadata(CurFrameMetadata other) + { + this.Compression = other.Compression; + this.HotspotX = other.HotspotX; + this.HotspotY = other.HotspotY; + this.EncodingWidth = other.EncodingWidth; + this.EncodingHeight = other.EncodingHeight; + this.BmpBitsPerPixel = other.BmpBitsPerPixel; + } + + /// + /// Gets or sets the frame compressions format. + /// + public IconFrameCompression Compression { get; set; } + + /// + /// Gets or sets the horizontal coordinates of the hotspot in number of pixels from the left. + /// + public ushort HotspotX { get; set; } + + /// + /// Gets or sets the vertical coordinates of the hotspot in number of pixels from the top. + /// + public ushort HotspotY { get; set; } + + /// + /// Gets or sets the encoding width.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. + ///
+ public byte? EncodingWidth { get; set; } + + /// + /// Gets or sets the encoding height.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. + ///
+ public byte? EncodingHeight { get; set; } + + /// + /// Gets or sets the number of bits per pixel.
+ /// Used when is + ///
+ public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32; + + /// + /// Gets or sets the color table, if any. + /// The underlying pixel format is represented by . + /// + public ReadOnlyMemory? ColorTable { get; set; } + + /// + public static CurFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) + { + if (!metadata.PixelTypeInfo.HasValue) + { + return new CurFrameMetadata + { + BmpBitsPerPixel = BmpBitsPerPixel.Bit32, + Compression = IconFrameCompression.Png + }; + } + + int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel; + BmpBitsPerPixel bbpp = bpp switch + { + 1 => BmpBitsPerPixel.Bit1, + 2 => BmpBitsPerPixel.Bit2, + <= 4 => BmpBitsPerPixel.Bit4, + <= 8 => BmpBitsPerPixel.Bit8, + <= 16 => BmpBitsPerPixel.Bit16, + <= 24 => BmpBitsPerPixel.Bit24, + _ => BmpBitsPerPixel.Bit32 + }; + + IconFrameCompression compression = IconFrameCompression.Bmp; + if (bbpp is BmpBitsPerPixel.Bit32) + { + compression = IconFrameCompression.Png; + } + + return new CurFrameMetadata + { + BmpBitsPerPixel = bbpp, + Compression = compression, + EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth), + EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight), + }; + } + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo(), + EncodingWidth = this.EncodingWidth, + EncodingHeight = this.EncodingHeight + }; + + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + float ratioX = destination.Width / (float)source.Width; + float ratioY = destination.Height / (float)source.Height; + this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX); + this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY); + this.ColorTable = null; + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public CurFrameMetadata DeepClone() => new(this); + + internal void FromIconDirEntry(IconDirEntry entry) + { + this.EncodingWidth = entry.Width; + this.EncodingHeight = entry.Height; + this.HotspotX = entry.Planes; + this.HotspotY = entry.BitCount; + } + + internal IconDirEntry ToIconDirEntry(Size size) + { + byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8 + ? (byte)0 + : (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel); + + return new IconDirEntry + { + Width = ClampEncodingDimension(this.EncodingWidth ?? size.Width), + Height = ClampEncodingDimension(this.EncodingHeight ?? size.Height), + Planes = this.HotspotX, + BitCount = this.HotspotY, + ColorCount = colorCount + }; + } + + private PixelTypeInfo GetPixelTypeInfo() + { + int bpp = (int)this.BmpBitsPerPixel; + PixelComponentInfo info; + PixelColorType color; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + + if (this.Compression is IconFrameCompression.Png) + { + bpp = 32; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + } + else + { + switch (this.BmpBitsPerPixel) + { + case BmpBitsPerPixel.Bit1: + info = PixelComponentInfo.Create(1, bpp, 1); + color = PixelColorType.Binary; + break; + case BmpBitsPerPixel.Bit2: + info = PixelComponentInfo.Create(1, bpp, 2); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit4: + info = PixelComponentInfo.Create(1, bpp, 4); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit8: + info = PixelComponentInfo.Create(1, bpp, 8); + color = PixelColorType.Indexed; + break; + + // Could be 555 with padding but 565 is more common in newer bitmaps and offers + // greater accuracy due to extra green precision. + case BmpBitsPerPixel.Bit16: + info = PixelComponentInfo.Create(3, bpp, 5, 6, 5); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit24: + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit32 or _: + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + }; + } + + private static byte ScaleEncodingDimension(byte? value, int destination, float ratio) + { + if (value is null) + { + return ClampEncodingDimension(destination); + } + + return ClampEncodingDimension(MathF.Ceiling(value.Value * ratio)); + } + + private static byte ClampEncodingDimension(float? dimension) + => dimension switch + { + // Encoding dimensions can be between 0-256 where 0 means 256 or greater. + > 255 => 0, + <= 255 and >= 1 => (byte)dimension, + _ => 0 + }; + } +} diff --git a/ImageSharp/Formats/Cur/CurMetadata.cs b/ImageSharp/Formats/Cur/CurMetadata.cs new file mode 100644 index 0000000..7eb8912 --- /dev/null +++ b/ImageSharp/Formats/Cur/CurMetadata.cs @@ -0,0 +1,162 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Cur { + /// + /// Provides Cur specific metadata information for the image. + /// + public class CurMetadata : IFormatMetadata + { + /// + /// Initializes a new instance of the class. + /// + public CurMetadata() + { + } + + private CurMetadata(CurMetadata other) + { + this.Compression = other.Compression; + this.BmpBitsPerPixel = other.BmpBitsPerPixel; + + if (other.ColorTable?.Length > 0) + { + this.ColorTable = other.ColorTable.Value.ToArray(); + } + } + + /// + /// Gets or sets the frame compressions format. Derived from the root frame. + /// + public IconFrameCompression Compression { get; set; } + + /// + /// Gets or sets the number of bits per pixel.
+ /// Used when is + ///
+ public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32; + + /// + /// Gets or sets the color table, if any. Derived from the root frame.
+ /// The underlying pixel format is represented by . + ///
+ public ReadOnlyMemory? ColorTable { get; set; } + + /// + public static CurMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + int bpp = metadata.PixelTypeInfo.BitsPerPixel; + BmpBitsPerPixel bbpp = bpp switch + { + 1 => BmpBitsPerPixel.Bit1, + 2 => BmpBitsPerPixel.Bit2, + <= 4 => BmpBitsPerPixel.Bit4, + <= 8 => BmpBitsPerPixel.Bit8, + <= 16 => BmpBitsPerPixel.Bit16, + <= 24 => BmpBitsPerPixel.Bit24, + _ => BmpBitsPerPixel.Bit32 + }; + + IconFrameCompression compression = IconFrameCompression.Bmp; + if (bbpp is BmpBitsPerPixel.Bit32) + { + compression = IconFrameCompression.Png; + } + + return new CurMetadata + { + BmpBitsPerPixel = bbpp, + Compression = compression + }; + } + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp = (int)this.BmpBitsPerPixel; + PixelComponentInfo info; + PixelColorType color; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + + if (this.Compression is IconFrameCompression.Png) + { + bpp = 32; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + } + else + { + switch (this.BmpBitsPerPixel) + { + case BmpBitsPerPixel.Bit1: + info = PixelComponentInfo.Create(1, bpp, 1); + color = PixelColorType.Binary; + break; + case BmpBitsPerPixel.Bit2: + info = PixelComponentInfo.Create(1, bpp, 2); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit4: + info = PixelComponentInfo.Create(1, bpp, 4); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit8: + info = PixelComponentInfo.Create(1, bpp, 8); + color = PixelColorType.Indexed; + break; + + // Could be 555 with padding but 565 is more common in newer bitmaps and offers + // greater accuracy due to extra green precision. + case BmpBitsPerPixel.Bit16: + info = PixelComponentInfo.Create(3, bpp, 5, 6, 5); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit24: + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit32 or _: + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + EncodingType = this.Compression == IconFrameCompression.Bmp && this.BmpBitsPerPixel <= BmpBitsPerPixel.Bit8 + ? EncodingType.Lossy + : EncodingType.Lossless, + PixelTypeInfo = this.GetPixelTypeInfo() + }; + + /// + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + => this.ColorTable = null; + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public CurMetadata DeepClone() => new(this); + } +} diff --git a/ImageSharp/Formats/DecoderOptions.cs b/ImageSharp/Formats/DecoderOptions.cs new file mode 100644 index 0000000..c1b274d --- /dev/null +++ b/ImageSharp/Formats/DecoderOptions.cs @@ -0,0 +1,110 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Formats { + /// + /// Provides general configuration options for decoding image formats. + /// + public sealed class DecoderOptions + { + private static readonly Lazy LazyOptions = new(() => new DecoderOptions()); + + private uint maxFrames = int.MaxValue; + + // Used by the FileProvider in the unit tests to set the configuration on the fly. +#pragma warning disable IDE0032 // Use auto property + private Configuration configuration = Configuration.Default; +#pragma warning restore IDE0032 // Use auto property + + /// + /// Gets the shared default general decoder options instance. + /// Used internally to reduce allocations for default decoding operations. + /// + internal static DecoderOptions Default { get; } = LazyOptions.Value; + + /// + /// Gets a custom configuration instance to be used by the image processing pipeline. + /// +#pragma warning disable IDE0032 // Use auto property +#pragma warning disable RCS1085 // Use auto-implemented property. + public Configuration Configuration { get => this.configuration; init => this.configuration = value; } +#pragma warning restore RCS1085 // Use auto-implemented property. +#pragma warning restore IDE0032 // Use auto property + + /// + /// Gets the target size to decode the image into. Scaling should use an operation equivalent to . + /// + public Size? TargetSize { get; init; } + + /// + /// Gets the sampler to use when resizing during decoding. + /// + public IResampler Sampler { get; init; } = KnownResamplers.Box; + + /// + /// Gets a value indicating whether to ignore encoded metadata when decoding. + /// + public bool SkipMetadata { get; init; } + + /// + /// Gets the maximum number of image frames to decode, inclusive. + /// + public uint MaxFrames { get => this.maxFrames; init => this.maxFrames = Math.Clamp(value, 1, int.MaxValue); } + + /// + /// Gets the segment error handling strategy to use during decoding. + /// + public SegmentIntegrityHandling SegmentIntegrityHandling { get; init; } = SegmentIntegrityHandling.IgnoreAncillary; + + /// + /// Gets a value that controls how ICC profiles are handled during decode. + /// + public ColorProfileHandling ColorProfileHandling { get; init; } + + internal void SetConfiguration(Configuration configuration) => this.configuration = configuration; + + internal bool TryGetIccProfileForColorConversion(IccProfile? profile, [NotNullWhen(true)] out IccProfile? value) + { + value = null; + + if (profile is null) + { + return false; + } + + if (this.ColorProfileHandling == ColorProfileHandling.Preserve) + { + return false; + } + + if (profile.IsCanonicalSrgbMatrixTrc()) + { + return false; + } + + value = profile; + return true; + } + + internal bool CanRemoveIccProfile(IccProfile? profile) + { + if (profile is null) + { + return false; + } + + if (this.ColorProfileHandling == ColorProfileHandling.Convert) + { + return true; + } + + return this.ColorProfileHandling == ColorProfileHandling.Compact && profile.IsCanonicalSrgbMatrixTrc(); + } + } +} diff --git a/ImageSharp/Formats/EncodingType.cs b/ImageSharp/Formats/EncodingType.cs new file mode 100644 index 0000000..e213f90 --- /dev/null +++ b/ImageSharp/Formats/EncodingType.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats { + /// + /// Provides a way to specify the type of encoding to be used. + /// + public enum EncodingType + { + /// + /// Lossless encoding, which compresses data without any loss of information. + /// + Lossless, + + /// + /// Lossy encoding, which compresses data by discarding some of it. + /// + Lossy + } +} diff --git a/ImageSharp/Formats/EncodingUtilities.cs b/ImageSharp/Formats/EncodingUtilities.cs new file mode 100644 index 0000000..694fa69 --- /dev/null +++ b/ImageSharp/Formats/EncodingUtilities.cs @@ -0,0 +1,170 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats { + /// + /// Provides utilities for encoding images. + /// + internal static class EncodingUtilities + { + /// + /// Determines if transparent pixels can be replaced based on the specified color mode and pixel type. + /// + /// The type of the pixel. + /// Indicates the color mode used to assess the ability to replace transparent pixels. + /// Returns true if transparent pixels can be replaced; otherwise, false. + public static bool ShouldReplaceTransparentPixels(TransparentColorMode mode) + where TPixel : unmanaged, IPixel + => mode == TransparentColorMode.Clear && TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Unassociated; + + /// + /// Replaces pixels with a transparent alpha component with fully transparent pixels. + /// + /// The type of the pixel. + /// The where the transparent pixels will be changed. + public static void ReplaceTransparentPixels(ImageFrame frame) + where TPixel : unmanaged, IPixel + => ReplaceTransparentPixels(frame.Configuration, frame.PixelBuffer); + + /// + /// Replaces pixels with a transparent alpha component with fully transparent pixels. + /// + /// The type of the pixel. + /// The configuration. + /// The where the transparent pixels will be changed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReplaceTransparentPixels(Configuration configuration, Buffer2D buffer) + where TPixel : unmanaged, IPixel + { + Buffer2DRegion region = buffer.GetRegion(); + ReplaceTransparentPixels(configuration, in region); + } + + /// + /// Replaces pixels with a transparent alpha component with fully transparent pixels. + /// + /// The type of the pixel. + /// The configuration. + /// The where the transparent pixels will be changed. + public static void ReplaceTransparentPixels( + Configuration configuration, + in Buffer2DRegion region) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner vectors = configuration.MemoryAllocator.Allocate(region.Width); + Span vectorsSpan = vectors.GetSpan(); + for (int y = 0; y < region.Height; y++) + { + Span span = region.DangerousGetRowSpan(y); + PixelOperations.Instance.ToVector4(configuration, span, vectorsSpan, PixelConversionModifiers.Scale); + ReplaceTransparentPixels(vectorsSpan); + PixelOperations.Instance.FromVector4Destructive(configuration, vectorsSpan, span, PixelConversionModifiers.Scale); + } + } + + /// + /// Replaces pixels with a transparent alpha component with fully transparent pixels. + /// + /// A span of color vectors that will be checked for transparency and potentially modified. + public static void ReplaceTransparentPixels(Span source) + { + if (Vector512.IsHardwareAccelerated && source.Length >= 4) + { + Span> source512 = MemoryMarshal.Cast>(source); + for (int i = 0; i < source512.Length; i++) + { + ref Vector512 v = ref source512[i]; + + // Do `vector < threshold` + Vector512 mask = Vector512.Equals(v, Vector512.Zero); + + // Replicate the result for W to all elements (is AllBitsSet if the W was 0 and Zero otherwise) + mask = Vector512.Shuffle(mask, Vector512.Create(3, 3, 3, 3, 7, 7, 7, 7, 11, 11, 11, 11, 15, 15, 15, 15)); + + // Use the mask to select the replacement vector + // (replacement & mask) | (v512 & ~mask) + v = Vector512.ConditionalSelect(mask, Vector512.Zero, v); + } + + int m = Numerics.Modulo4(source.Length); + if (m != 0) + { + for (int i = source.Length - m; i < source.Length; i++) + { + if (source[i].W == 0) + { + source[i] = Vector4.Zero; + } + } + } + } + else if (Vector256.IsHardwareAccelerated && source.Length >= 2) + { + Span> source256 = MemoryMarshal.Cast>(source); + for (int i = 0; i < source256.Length; i++) + { + ref Vector256 v = ref source256[i]; + + // Do `vector < threshold` + Vector256 mask = Vector256.Equals(v, Vector256.Zero); + + // Replicate the result for W to all elements (is AllBitsSet if the W was 0 and Zero otherwise) + mask = Vector256.Shuffle(mask, Vector256.Create(3, 3, 3, 3, 7, 7, 7, 7)); + + // Use the mask to select the replacement vector + // (replacement & mask) | (v256 & ~mask) + v = Vector256.ConditionalSelect(mask, Vector256.Zero, v); + } + + int m = Numerics.Modulo2(source.Length); + if (m != 0) + { + for (int i = source.Length - m; i < source.Length; i++) + { + if (source[i].W == 0) + { + source[i] = Vector4.Zero; + } + } + } + } + else if (Vector128.IsHardwareAccelerated) + { + for (int i = 0; i < source.Length; i++) + { + ref Vector4 v = ref source[i]; + Vector128 v128 = v.AsVector128(); + + // Do `vector == 0` + Vector128 mask = Vector128.Equals(v128, Vector128.Zero); + + // Replicate the result for W to all elements (is AllBitsSet if the W was 0 and Zero otherwise) + mask = Vector128.Shuffle(mask, Vector128.Create(3, 3, 3, 3)); + + // Use the mask to select the replacement vector + // (replacement & mask) | (v128 & ~mask) + v = Vector128.ConditionalSelect(mask, Vector128.Zero, v128).AsVector4(); + } + } + else + { + for (int i = 0; i < source.Length; i++) + { + if (source[i].W == 0F) + { + source[i] = Vector4.Zero; + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs b/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs new file mode 100644 index 0000000..d380cfb --- /dev/null +++ b/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors { + /// + /// Compressor for EXR image data which does not use any compression method. + /// + internal class NoneExrCompressor : ExrBaseCompressor + { + /// + /// Initializes a new instance of the class. + /// + /// The output stream to write the compressed image data to. + /// The memory allocator. + /// Bytes per row block. + /// Bytes per pixel row. + /// The pixel rows per block. + /// The witdh of one row in pixels. + public NoneExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) + { + } + + /// + public override uint CompressRowBlock(Span rows, int rowCount) + { + this.Output.Write(rows); + return (uint)rows.Length; + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs b/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs new file mode 100644 index 0000000..04aed01 --- /dev/null +++ b/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Memory; +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors { + /// + /// Compressor for EXR image data using the ZIP compression. + /// + internal class ZipExrCompressor : ExrBaseCompressor + { + private readonly DeflateCompressionLevel compressionLevel; + + private readonly MemoryStream memoryStream; + + private readonly System.Buffers.IMemoryOwner buffer; + + /// + /// Initializes a new instance of the class. + /// + /// The stream to write the compressed data to. + /// The memory allocator. + /// The bytes per block. + /// The bytes per row. + /// The pixel rows per block. + /// The witdh of one row in pixels. + /// The compression level for deflate compression. + public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, DeflateCompressionLevel compressionLevel) + : base(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) + { + this.compressionLevel = compressionLevel; + this.buffer = allocator.Allocate((int)bytesPerBlock); + this.memoryStream = new(); + } + + /// + public override uint CompressRowBlock(Span rows, int rowCount) + { + // Re-oder pixel values. + Span reordered = this.buffer.GetSpan()[..(int)(rowCount * this.BytesPerRow)]; + int n = reordered.Length; + int t1 = 0; + int t2 = (n + 1) >> 1; + for (int i = 0; i < n; i++) + { + bool isOdd = (i & 1) == 1; + reordered[isOdd ? t2++ : t1++] = rows[i]; + } + + // Predictor. + Span predicted = reordered; + byte p = predicted[0]; + for (int i = 1; i < predicted.Length; i++) + { + int d = (predicted[i] - p + 128 + 256) & 255; + p = predicted[i]; + predicted[i] = (byte)d; + } + + this.memoryStream.Seek(0, SeekOrigin.Begin); + using (ZlibDeflateStream stream = new(this.Allocator, this.memoryStream, this.compressionLevel)) + { + stream.Write(predicted); + stream.Flush(); + } + + int size = (int)this.memoryStream.Position; + byte[] buffer = this.memoryStream.GetBuffer(); + this.Output.Write(buffer, 0, size); + + // Reset memory stream for next pixel row. + this.memoryStream.Seek(0, SeekOrigin.Begin); + this.memoryStream.SetLength(0); + + return (uint)size; + } + + /// + protected override void Dispose(bool disposing) + { + this.buffer.Dispose(); + this.memoryStream?.Dispose(); + } + } +} diff --git a/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs b/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs new file mode 100644 index 0000000..5809573 --- /dev/null +++ b/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs @@ -0,0 +1,206 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors { + /// + /// Implementation of B44 decompressor for EXR image data. + /// + internal class B44ExrCompression : ExrBaseDecompressor + { + private readonly int channelCount; + + private readonly byte[] scratch = new byte[14]; + + private readonly ushort[] s = new ushort[16]; + + private readonly IMemoryOwner tmpBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The bytes per pixel row block. + /// The bytes per row. + /// The pixel rows per block. + /// The width of a pixel row in pixels. + /// The number of channels of the image. + public B44ExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) + { + this.channelCount = channelCount; + this.tmpBuffer = allocator.Allocate((int)(width * rowsPerBlock * channelCount)); + } + + /// + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + { + Span outputBuffer = MemoryMarshal.Cast(buffer); + Span decompressed = this.tmpBuffer.GetSpan(); + int outputOffset = 0; + int bytesLeft = (int)compressedBytes; + for (int i = 0; i < this.channelCount && bytesLeft > 0; i++) + { + for (int y = 0; y < this.RowsPerBlock; y += 4) + { + Span row0 = decompressed.Slice(outputOffset, this.Width); + outputOffset += this.Width; + Span row1 = decompressed.Slice(outputOffset, this.Width); + outputOffset += this.Width; + Span row2 = decompressed.Slice(outputOffset, this.Width); + outputOffset += this.Width; + Span row3 = decompressed.Slice(outputOffset, this.Width); + outputOffset += this.Width; + + int rowOffset = 0; + for (int x = 0; x < this.Width && bytesLeft > 0; x += 4) + { + int bytesRead = stream.Read(this.scratch, 0, 3); + if (bytesRead == 0) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!"); + } + + // Check if 3-byte encoded flat field. + if (this.scratch[2] >= 13 << 2) + { + Unpack3(this.scratch, this.s); + bytesLeft -= 3; + } + else + { + bytesRead = stream.Read(this.scratch, 3, 11); + if (bytesRead == 0) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!"); + } + + Unpack14(this.scratch, this.s); + bytesLeft -= 14; + } + + int n = x + 3 < this.Width ? 4 : this.Width - x; + if (y + 3 < this.RowsPerBlock) + { + this.s.AsSpan(0, n).CopyTo(row0[rowOffset..]); + this.s.AsSpan(4, n).CopyTo(row1[rowOffset..]); + this.s.AsSpan(8, n).CopyTo(row2[rowOffset..]); + this.s.AsSpan(12, n).CopyTo(row3[rowOffset..]); + } + else + { + this.s.AsSpan(0, n).CopyTo(row0[rowOffset..]); + if (y + 1 < this.RowsPerBlock) + { + this.s.AsSpan(4, n).CopyTo(row1[rowOffset..]); + } + + if (y + 2 < this.RowsPerBlock) + { + this.s.AsSpan(8, n).CopyTo(row2[rowOffset..]); + } + } + + rowOffset += 4; + } + + if (bytesLeft <= 0) + { + break; + } + } + } + + // Rearrange the decompressed data such that the data for each scan line form a contiguous block. + int offsetDecompressed = 0; + int offsetOutput = 0; + int blockSize = (int)(this.Width * this.RowsPerBlock); + for (int y = 0; y < this.RowsPerBlock; y++) + { + for (int i = 0; i < this.channelCount; i++) + { + decompressed.Slice(offsetDecompressed + (i * blockSize), this.Width).CopyTo(outputBuffer[offsetOutput..]); + offsetOutput += this.Width; + } + + offsetDecompressed += this.Width; + } + } + + /// + /// Unpack a 14-byte block into 4 by 4 16-bit pixels. + /// + /// The source byte data to unpack. + /// Destintation buffer. + private static void Unpack14(Span b, Span s) + { + s[0] = (ushort)((b[0] << 8) | b[1]); + + ushort shift = (ushort)(b[2] >> 2); + ushort bias = (ushort)(0x20u << shift); + + s[4] = (ushort)(s[0] + ((((b[2] << 4) | (b[3] >> 4)) & 0x3fu) << shift) - bias); + s[8] = (ushort)(s[4] + ((((b[3] << 2) | (b[4] >> 6)) & 0x3fu) << shift) - bias); + s[12] = (ushort)(s[8] + ((b[4] & 0x3fu) << shift) - bias); + + s[1] = (ushort)(s[0] + ((uint)(b[5] >> 2) << shift) - bias); + s[5] = (ushort)(s[4] + ((((b[5] << 4) | (b[6] >> 4)) & 0x3fu) << shift) - bias); + s[9] = (ushort)(s[8] + ((((b[6] << 2) | (b[7] >> 6)) & 0x3fu) << shift) - bias); + s[13] = (ushort)(s[12] + ((b[7] & 0x3fu) << shift) - bias); + + s[2] = (ushort)(s[1] + ((uint)(b[8] >> 2) << shift) - bias); + s[6] = (ushort)(s[5] + ((((b[8] << 4) | (b[9] >> 4)) & 0x3fu) << shift) - bias); + s[10] = (ushort)(s[9] + ((((b[9] << 2) | (b[10] >> 6)) & 0x3fu) << shift) - bias); + s[14] = (ushort)(s[13] + ((b[10] & 0x3fu) << shift) - bias); + + s[3] = (ushort)(s[2] + ((uint)(b[11] >> 2) << shift) - bias); + s[7] = (ushort)(s[6] + ((((b[11] << 4) | (b[12] >> 4)) & 0x3fu) << shift) - bias); + s[11] = (ushort)(s[10] + ((((b[12] << 2) | (b[13] >> 6)) & 0x3fu) << shift) - bias); + s[15] = (ushort)(s[14] + ((b[13] & 0x3fu) << shift) - bias); + + for (int i = 0; i < 16; ++i) + { + if ((s[i] & 0x8000) != 0) + { + s[i] &= 0x7fff; + } + else + { + s[i] = (ushort)~s[i]; + } + } + } + + /// + /// // Unpack a 3-byte block into 4 by 4 identical 16-bit pixels. + /// + /// The source byte data to unpack. + /// The destination buffer. + private static void Unpack3(Span b, Span s) + { + s[0] = (ushort)((b[0] << 8) | b[1]); + + if ((s[0] & 0x8000) != 0) + { + s[0] &= 0x7fff; + } + else + { + s[0] = (ushort)~s[0]; + } + + for (int i = 1; i < 16; ++i) + { + s[i] = s[0]; + } + } + + /// + protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); + } +} diff --git a/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs b/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs new file mode 100644 index 0000000..3705148 --- /dev/null +++ b/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using System; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors { + /// + /// Decompressor for EXR image data which do not use any compression. + /// + internal class NoneExrCompression : ExrBaseDecompressor + { + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The bytes per pixel row block. + /// The bytes per pixel row. + /// The pixel rows per block. + /// The number of pixels per row. + public NoneExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) + { + } + + /// + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + { + int bytesRead = stream.Read(buffer, 0, Math.Min(buffer.Length, (int)this.BytesPerBlock)); + if (bytesRead != (int)this.BytesPerBlock) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough pixel data from the stream!"); + } + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs b/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs new file mode 100644 index 0000000..7be1771 --- /dev/null +++ b/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs @@ -0,0 +1,154 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors { + /// + /// Implementation of PXR24 decompressor for EXR image data. + /// + internal class Pxr24Compression : ExrBaseDecompressor + { + private readonly IMemoryOwner tmpBuffer; + + private readonly int channelCount; + + private readonly ExrPixelType pixelType; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The bytes per pixel row block. + /// The bytes per pixel row. + /// The pixel rows per block. + /// The witdh of one row in pixels. + /// The number of channels for a pixel. + /// The pixel type. + public Pxr24Compression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount, ExrPixelType pixelType) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) + { + this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); + this.channelCount = channelCount; + this.pixelType = pixelType; + } + + /// + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + { + Span uncompressed = this.tmpBuffer.GetSpan(); + Span outputBufferHalf = MemoryMarshal.Cast(buffer); + Span outputBufferFloat = MemoryMarshal.Cast(buffer); + Span outputBufferUint = MemoryMarshal.Cast(buffer); + + uint uncompressedBytes = this.BytesPerBlock; + UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes); + + int lastIn = 0; + int outputOffset = 0; + for (int y = 0; y < this.RowsPerBlock; y++) + { + for (int c = 0; c < this.channelCount; c++) + { + switch (this.pixelType) + { + case ExrPixelType.UnsignedInt: + { + int offsetT0 = lastIn; + lastIn += this.Width; + int offsetT1 = lastIn; + lastIn += this.Width; + int offsetT2 = lastIn; + lastIn += this.Width; + int offsetT3 = lastIn; + lastIn += this.Width; + + uint pixel = 0; + for (int x = 0; x < this.Width; x++) + { + uint t0 = uncompressed[offsetT0]; + uint t1 = uncompressed[offsetT1]; + uint t2 = uncompressed[offsetT2]; + uint t3 = uncompressed[offsetT3]; + uint diff = (t0 << 24) | (t1 << 16) | (t2 << 8) | t3; + + pixel += diff; + outputBufferUint[outputOffset] = pixel; + + offsetT0++; + offsetT1++; + offsetT2++; + offsetT3++; + outputOffset++; + } + + break; + } + + case ExrPixelType.Half: + { + int offsetT0 = lastIn; + lastIn += this.Width; + int offsetT1 = lastIn; + lastIn += this.Width; + + uint pixel = 0; + for (int x = 0; x < this.Width; x++) + { + uint t0 = uncompressed[offsetT0]; + uint t1 = uncompressed[offsetT1]; + uint diff = (t0 << 8) | t1; + + pixel += diff; + outputBufferHalf[outputOffset] = (ushort)pixel; + + offsetT0++; + offsetT1++; + outputOffset++; + } + + break; + } + + case ExrPixelType.Float: + { + int offsetT0 = lastIn; + lastIn += this.Width; + int offsetT1 = lastIn; + lastIn += this.Width; + int offsetT2 = lastIn; + lastIn += this.Width; + + uint pixel = 0; + for (int x = 0; x < this.Width; x++) + { + uint t0 = uncompressed[offsetT0]; + uint t1 = uncompressed[offsetT1]; + uint t2 = uncompressed[offsetT2]; + uint diff = (t0 << 24) | (t1 << 16) | (t2 << 8); + + pixel += diff; + outputBufferFloat[outputOffset] = pixel; + + offsetT0++; + offsetT1++; + offsetT2++; + outputOffset++; + } + + break; + } + } + } + } + } + + /// + protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); + } +} diff --git a/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs b/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs new file mode 100644 index 0000000..47da277 --- /dev/null +++ b/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs @@ -0,0 +1,99 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors { + /// + /// Implementation of RLE decompressor for EXR images. + /// + internal class RunLengthExrCompression : ExrBaseDecompressor + { + private readonly IMemoryOwner tmpBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The bytes per pixel row block. + /// The bytes per row. + /// The pixel rows per block. + /// The witdh of one row in pixels. + public RunLengthExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); + + /// + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + { + Span uncompressed = this.tmpBuffer.GetSpan(); + int maxLength = (int)this.BytesPerBlock; + int offset = 0; + while (compressedBytes > 0) + { + byte nextByte = ReadNextByte(stream); + + sbyte input = (sbyte)nextByte; + if (input < 0) + { + int count = -input; + compressedBytes -= (uint)(count + 1); + + if ((maxLength -= count) < 0) + { + return; + } + + for (int i = 0; i < count; i++) + { + uncompressed[offset + i] = ReadNextByte(stream); + } + + offset += count; + } + else + { + int count = input; + byte value = ReadNextByte(stream); + compressedBytes -= 2; + + if ((maxLength -= count + 1) < 0) + { + return; + } + + for (int i = 0; i < count + 1; i++) + { + uncompressed[offset + i] = value; + } + + offset += count + 1; + } + } + + Reconstruct(uncompressed, this.BytesPerBlock); + Interleave(uncompressed, this.BytesPerBlock, buffer); + } + + /// + /// Reads the next byte from the stream. + /// + /// The stream. + /// The next byte. + private static byte ReadNextByte(BufferedReadStream stream) + { + int nextByte = stream.ReadByte(); + if (nextByte == -1) + { + ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to decompress RLE encoded EXR image!"); + } + + return (byte)nextByte; + } + + /// + protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); + } +} diff --git a/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs b/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs new file mode 100644 index 0000000..3286e0b --- /dev/null +++ b/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors { + /// + /// Implementation of zhe Zip decompressor for EXR image data. + /// + internal class ZipExrCompression : ExrBaseDecompressor + { + private readonly IMemoryOwner tmpBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The bytes per pixel row block. + /// The bytes per pixel row. + /// The pixel rows per block. + /// The witdh of one row in pixels. + public ZipExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); + + /// + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + { + Span uncompressed = this.tmpBuffer.GetSpan(); + + uint uncompressedBytes = (uint)buffer.Length; + int totalRead = UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes); + + Reconstruct(uncompressed, (uint)totalRead); + Interleave(uncompressed, (uint)totalRead, buffer); + } + + /// + protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); + } +} diff --git a/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs b/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs new file mode 100644 index 0000000..9e8be2c --- /dev/null +++ b/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using System; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression { + /// + /// Base class for EXR compression. + /// + internal abstract class ExrBaseCompression : IDisposable + { + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The bytes per block. + /// The bytes per row. + /// The number of pixel rows per block. + /// The number of pixels of a row. + protected ExrBaseCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + { + this.Allocator = allocator; + this.BytesPerBlock = bytesPerBlock; + this.BytesPerRow = bytesPerRow; + this.RowsPerBlock = rowsPerBlock; + this.Width = width; + } + + /// + /// Gets the memory allocator. + /// + protected MemoryAllocator Allocator { get; } + + /// + /// Gets the bits per pixel. + /// + public int BitsPerPixel { get; } + + /// + /// Gets the bytes per row. + /// + public uint BytesPerRow { get; } + + /// + /// Gets the uncompressed bytes per block. + /// + public uint BytesPerBlock { get; } + + /// + /// Gets the number of pixel rows per block. + /// + public uint RowsPerBlock { get; } + + /// + /// Gets the image width. + /// + public int Width { get; } + + /// + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.isDisposed = true; + this.Dispose(true); + } + + protected abstract void Dispose(bool disposing); + } +} diff --git a/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs b/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs new file mode 100644 index 0000000..21b2fd4 --- /dev/null +++ b/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO.Compression; +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression { + /// + /// The base EXR decompressor class. + /// + internal abstract class ExrBaseDecompressor : ExrBaseCompression + { + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The bytes per row block. + /// The bytes per row. + /// The pixel rows per block. + /// The number of pixels per row. + protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) + { + } + + /// + /// Decompresses the specified stream. + /// + /// The buffered stream to decompress. + /// The compressed bytes. + /// The buffer to write the decompressed data to. + public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer); + + /// + /// Decompresses zip compressed data. + /// + /// The buffered stream to decompress. + /// The compressed bytes. + /// The buffer to write the uncompressed data to. + /// The uncompressed bytes. + /// The total bytes read from the stream. + protected static int UndoZipCompression(BufferedReadStream stream, uint compressedBytes, Span uncompressed, uint uncompressedBytes) + { + long pos = stream.Position; + using ZlibInflateStream inflateStream = new( + stream, + () => + { + int left = (int)(compressedBytes - (stream.Position - pos)); + return left > 0 ? left : 0; + }); + inflateStream.AllocateNewBytes((int)compressedBytes, true); + using DeflateStream dataStream = inflateStream.CompressedStream!; + + int totalRead = 0; + while (totalRead < uncompressedBytes) + { + int bytesRead = dataStream.Read(uncompressed, totalRead, (int)uncompressedBytes - totalRead); + if (bytesRead <= 0) + { + break; + } + + totalRead += bytesRead; + } + + if (totalRead == 0) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data for zip compressed EXR image data!"); + } + + return totalRead; + } + + /// + /// Integrate over all differences to the previous value in order to + /// reconstruct sample values. + /// + /// The buffer with the data. + /// The un compressed bytes. + protected static void Reconstruct(Span buffer, uint unCompressedBytes) + { + int offset = 0; + for (int i = 0; i < unCompressedBytes - 1; i++) + { + byte d = (byte)(buffer[offset] + (buffer[offset + 1] - 128)); + buffer[offset + 1] = d; + offset++; + } + } + + /// + /// Interleaves the input data. + /// + /// The source data. + /// The uncompressed bytes. + /// The output to write to. + protected static void Interleave(Span source, uint unCompressedBytes, Span output) + { + int sourceOffset = 0; + int offset0 = 0; + int offset1 = (int)((unCompressedBytes + 1) / 2); + while (sourceOffset < unCompressedBytes) + { + output[sourceOffset++] = source[offset0++]; + output[sourceOffset++] = source[offset1++]; + } + } + } +} diff --git a/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs b/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs new file mode 100644 index 0000000..35265b5 --- /dev/null +++ b/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Exr.Compression.Compressors; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.Memory; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression { + /// + /// Factory class for creating a compressor for EXR image data. + /// + internal static class ExrCompressorFactory + { + /// + /// Creates the specified exr data compressor. + /// + /// The compression method. + /// The memory allocator. + /// The output stream. + /// The bytes per block. + /// The bytes per row. + /// The pixel rows per block. + /// The witdh of one row in pixels. + /// The deflate compression level. + /// A compressor for EXR image data. + public static ExrBaseCompressor Create( + ExrCompression method, + MemoryAllocator allocator, + Stream output, + uint bytesPerBlock, + uint bytesPerRow, + uint rowsPerBlock, + int width, + DeflateCompressionLevel compressionLevel = DeflateCompressionLevel.DefaultCompression) => method switch + { + ExrCompression.None => new NoneExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width), + ExrCompression.Zips => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, compressionLevel), + ExrCompression.Zip => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, compressionLevel), + _ => throw ExrThrowHelper.NotSupportedCompressor(method.ToString()), + }; + } +} diff --git a/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs b/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs new file mode 100644 index 0000000..42c5516 --- /dev/null +++ b/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression { + /// + /// The Factory class for creating a EXR data decompressor. + /// + internal static class ExrDecompressorFactory + { + /// + /// Creates a decomprssor for a specific EXR compression type. + /// + /// The compression method. + /// The memory allocator. + /// The width in pixels of the image. + /// The bytes per block. + /// The bytes per row. + /// The rows per block. + /// The number of image channels. + /// The pixel type. + /// Decompressor for EXR image data. + public static ExrBaseDecompressor Create( + ExrCompression method, + MemoryAllocator memoryAllocator, + int width, + uint bytesPerBlock, + uint bytesPerRow, + uint rowsPerBlock, + int channelCount, + ExrPixelType pixelType) => method switch + { + ExrCompression.None => new NoneExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width), + ExrCompression.Zips => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width), + ExrCompression.Zip => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width), + ExrCompression.RunLengthEncoded => new RunLengthExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width), + ExrCompression.B44 => new B44ExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount), + ExrCompression.Pxr24 => new Pxr24Compression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount, pixelType), + _ => throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)), + }; + } +} diff --git a/ImageSharp/Formats/Exr/Constants/ExrCompression.cs b/ImageSharp/Formats/Exr/Constants/ExrCompression.cs new file mode 100644 index 0000000..d3f2ac7 --- /dev/null +++ b/ImageSharp/Formats/Exr/Constants/ExrCompression.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Exr.Constants { + /// + /// Enumeration representing the compression formats defined by the EXR file-format. + /// + public enum ExrCompression + { + /// + /// Pixel data is not compressed. + /// + None = 0, + + /// + /// Differences between horizontally adjacent pixels are run-length encoded. + /// This method is fast, and works well for images with large flat areas, but for photographic images, + /// the compressed file size is usually between 60 and 75 percent of the uncompressed size. + /// Compression is lossless. + /// + RunLengthEncoded = 1, + + /// + /// Uses the open source zlib library for compression. Unlike ZIP compression, this operates one scan line at a time. + /// Compression is lossless. + /// + Zips = 2, + + /// + /// Differences between horizontally adjacent pixels are compressed using the open source zlib library. + /// Unlike ZIPS compression, this operates in in blocks of 16 scan lines. + /// Compression is lossless. + /// + Zip = 3, + + /// + /// A wavelet transform is applied to the pixel data, and the result is Huffman-encoded. + /// Compression is lossless. + /// + Piz = 4, + + /// + /// After reducing 32-bit floating-point data to 24 bits by rounding, differences between horizontally adjacent pixels are compressed with zlib, + /// similar to ZIP. PXR24 compression preserves image channels of type HALF and UINT exactly, but the relative error of FLOAT data increases to about 3×10-5. + /// Compression is lossy. + /// + Pxr24 = 5, + + /// + /// Channels of type HALF are split into blocks of four by four pixels or 32 bytes. Each block is then packed into 14 bytes, + /// reducing the data to 44 percent of their uncompressed size. + /// Compression is lossy. + /// + B44 = 6, + + /// + /// Like B44, except for blocks of four by four pixels where all pixels have the same value, which are packed into 3 instead of 14 bytes. + /// For images with large uniform areas, B44A produces smaller files than B44 compression. + /// Compression is lossy. + /// + B44A = 7 + } +} diff --git a/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs b/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs new file mode 100644 index 0000000..461ee9b --- /dev/null +++ b/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Exr.Constants { + /// + /// This enum represents the type of pixel data in the EXR image. + /// + public enum ExrImageDataType + { + /// + /// The pixel data is unknown. + /// + Unknown = 0, + + /// + /// The pixel data has 3 channels: red, green and blue. + /// + Rgb = 1, + + /// + /// The pixel data has four channels: red, green, blue and a alpha channel. + /// + Rgba = 2, + + /// + /// There is only one channel with the luminance. + /// + Gray = 3, + } +} diff --git a/ImageSharp/Formats/Exr/Constants/ExrImageType.cs b/ImageSharp/Formats/Exr/Constants/ExrImageType.cs new file mode 100644 index 0000000..2ce3003 --- /dev/null +++ b/ImageSharp/Formats/Exr/Constants/ExrImageType.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Exr.Constants { + /// + /// Enum for the differnt exr image type. + /// + internal enum ExrImageType + { + /// + /// The image data is stored in scan lines. + /// + ScanLine = 0, + + /// + /// The image data is stored in tile. + /// This is not yet supported. + /// + Tiled = 1 + } +} diff --git a/ImageSharp/Formats/Exr/Constants/ExrLineOrder.cs b/ImageSharp/Formats/Exr/Constants/ExrLineOrder.cs new file mode 100644 index 0000000..403c7f1 --- /dev/null +++ b/ImageSharp/Formats/Exr/Constants/ExrLineOrder.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Exr.Constants { + /// + /// Enum for the different scan line ordering. + /// + internal enum ExrLineOrder : byte + { + /// + /// The scan lines are written from top-to-bottom. + /// + IncreasingY = 0, + + /// + /// The scan lines are written from bottom-to-top. + /// + DecreasingY = 1, + + /// + /// The Scan lines are written in no particular oder. + /// + RandomY = 2 + } +} diff --git a/ImageSharp/Formats/Exr/Constants/ExrPixelType.cs b/ImageSharp/Formats/Exr/Constants/ExrPixelType.cs new file mode 100644 index 0000000..ae4b680 --- /dev/null +++ b/ImageSharp/Formats/Exr/Constants/ExrPixelType.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Exr.Constants { + /// + /// The different pixel formats for a OpenEXR image. + /// + public enum ExrPixelType + { + /// + /// unsigned int (32 bit). + /// + UnsignedInt = 0, + + /// + /// half (16 bit floating point). + /// + Half = 1, + + /// + /// float (32 bit floating point). + /// + Float = 2 + } +} diff --git a/ImageSharp/Formats/Exr/ExrAttribute.cs b/ImageSharp/Formats/Exr/ExrAttribute.cs new file mode 100644 index 0000000..ba610b1 --- /dev/null +++ b/ImageSharp/Formats/Exr/ExrAttribute.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Exr { + /// + /// Repressents an exr image attribute. + /// + [DebuggerDisplay("Name: {Name}, Type: {Type}, Length: {Length}")] + internal class ExrAttribute + { + public static readonly ExrAttribute EmptyAttribute = new(string.Empty, string.Empty, 0); + + /// + /// Initializes a new instance of the class. + /// + /// The name of the attribute. + /// The type of the attribute. + /// The length in bytes. + public ExrAttribute(string name, string type, int length) + { + this.Name = name; + this.Type = type; + this.Length = length; + } + + /// + /// Gets the name of the attribute. + /// + public string Name { get; } + + /// + /// Gets the type of the attribute. + /// + public string Type { get; } + + /// + /// Gets the length in bytes of the attribute. + /// + public int Length { get; } + } +} diff --git a/ImageSharp/Formats/Exr/ExrBaseCompressor.cs b/ImageSharp/Formats/Exr/ExrBaseCompressor.cs new file mode 100644 index 0000000..fca2002 --- /dev/null +++ b/ImageSharp/Formats/Exr/ExrBaseCompressor.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression { + internal abstract class ExrBaseCompressor : ExrBaseCompression + { + /// + /// Initializes a new instance of the class. + /// + /// The output stream to write the compressed image to. + /// The memory allocator. + /// Bytes per row block. + /// Bytes per pixel row. + /// The pixel rows per block. + /// The number of pixels per row. + protected ExrBaseCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) + => this.Output = output; + + /// + /// Gets the output stream to write the compressed image to. + /// + public Stream Output { get; } + + /// + /// Compresses a block of rows of the image. + /// + /// Image rows to compress. + /// The number of rows to compress. + /// Number of bytes of of the compressed data. + public abstract uint CompressRowBlock(Span rows, int rowCount); + } +} diff --git a/ImageSharp/Formats/Exr/ExrBox2i.cs b/ImageSharp/Formats/Exr/ExrBox2i.cs new file mode 100644 index 0000000..984f724 --- /dev/null +++ b/ImageSharp/Formats/Exr/ExrBox2i.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Exr { + /// + /// Integer region definition. + /// + [DebuggerDisplay("xMin: {XMin}, yMin: {YMin}, xMax: {XMax}, yMax: {YMax}")] + internal readonly struct ExrBox2i + { + /// + /// Initializes a new instance of the struct. + /// + /// The minimum x value. + /// The minimum y value. + /// The maximum x value. + /// The maximum y value. + public ExrBox2i(int xMin, int yMin, int xMax, int yMax) + { + this.XMin = xMin; + this.YMin = yMin; + this.XMax = xMax; + this.YMax = yMax; + } + + /// + /// Gets the minimum x value. + /// + public int XMin { get; } + + /// + /// Gets the minimum y value. + /// + public int YMin { get; } + + /// + /// Gets the maximum x value. + /// + public int XMax { get; } + + /// + /// Gets the maximum y value. + /// + public int YMax { get; } + } +} diff --git a/ImageSharp/Formats/Exr/ExrChannelInfo.cs b/ImageSharp/Formats/Exr/ExrChannelInfo.cs new file mode 100644 index 0000000..24c0ddc --- /dev/null +++ b/ImageSharp/Formats/Exr/ExrChannelInfo.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Exr.Constants; + +namespace SixLabors.ImageSharp.Formats.Exr { + /// + /// Information about a pixel channel. + /// + [DebuggerDisplay("Name: {ChannelName}, PixelType: {PixelType}")] + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal readonly struct ExrChannelInfo + { + /// + /// Initializes a new instance of the struct. + /// + /// Name of the channel. + /// The type of the pixel data. + /// Linear flag, possible values are 0 and 1. + /// X sampling. + /// Y sampling. + public ExrChannelInfo(string channelName, ExrPixelType pixelType, byte linear, int xSampling, int ySampling) + { + this.ChannelName = channelName; + this.PixelType = pixelType; + this.Linear = linear; + this.XSampling = xSampling; + this.YSampling = ySampling; + } + + /// + /// Gets the channel name. + /// + public string ChannelName { get; } + + /// + /// Gets the type of the pixel data. + /// + public ExrPixelType PixelType { get; } + + /// + /// Gets the linear flag. Hint to lossy compression methods that indicates whether + /// human perception of the quantity represented by this channel + /// is closer to linear or closer to logarithmic. + /// + public byte Linear { get; } + + /// + /// Gets the x sampling value. + /// + public int XSampling { get; } + + /// + /// Gets the y sampling value. + /// + public int YSampling { get; } + } +} diff --git a/ImageSharp/Formats/Exr/ExrConfigurationModule.cs b/ImageSharp/Formats/Exr/ExrConfigurationModule.cs new file mode 100644 index 0000000..b8ef2b4 --- /dev/null +++ b/ImageSharp/Formats/Exr/ExrConfigurationModule.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Exr { + /// + /// Registers the image encoders, decoders and mime type detectors for the OpenExr format. + /// + public sealed class ExrConfigurationModule : IImageFormatConfigurationModule + { + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetEncoder(ExrFormat.Instance, new ExrEncoder()); + configuration.ImageFormatsManager.SetDecoder(ExrFormat.Instance, ExrDecoder.Instance); + configuration.ImageFormatsManager.AddImageFormatDetector(new ExrImageFormatDetector()); + } + } +} diff --git a/ImageSharp/Formats/Exr/ExrConstants.cs b/ImageSharp/Formats/Exr/ExrConstants.cs new file mode 100644 index 0000000..7ba776d --- /dev/null +++ b/ImageSharp/Formats/Exr/ExrConstants.cs @@ -0,0 +1,84 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Exr { + /// + /// Defines constants relating to OpenExr images. + /// + internal static class ExrConstants + { + /// + /// The list of mimetypes that equate to a OpenExr image. + /// + public static readonly IEnumerable MimeTypes = new[] { "image/x-exr" }; + + /// + /// The list of file extensions that equate to a OpenExr image. + /// + public static readonly IEnumerable FileExtensions = new[] { "exr" }; + + /// + /// The magick bytes identifying an OpenExr image. + /// + public static readonly int MagickBytes = 20000630; + + /// + /// EXR attribute names. + /// + internal static class AttributeNames + { + public const string Channels = "channels"; + + public const string Compression = "compression"; + + public const string DataWindow = "dataWindow"; + + public const string DisplayWindow = "displayWindow"; + + public const string LineOrder = "lineOrder"; + + public const string PixelAspectRatio = "pixelAspectRatio"; + + public const string ScreenWindowCenter = "screenWindowCenter"; + + public const string ScreenWindowWidth = "screenWindowWidth"; + + public const string Tiles = "tiles"; + + public const string ChunkCount = "chunkCount"; + } + + /// + /// EXR attribute types. + /// + internal static class AttibuteTypes + { + public const string ChannelList = "chlist"; + + public const string Compression = "compression"; + + public const string Float = "float"; + + public const string LineOrder = "lineOrder"; + + public const string TwoFloat = "v2f"; + + public const string BoxInt = "box2i"; + } + + internal static class ChannelNames + { + public const string Red = "R"; + + public const string Green = "G"; + + public const string Blue = "B"; + + public const string Alpha = "A"; + + public const string Luminance = "Y"; + } + } +} diff --git a/ImageSharp/Formats/Exr/ExrDecoder.cs b/ImageSharp/Formats/Exr/ExrDecoder.cs new file mode 100644 index 0000000..000954a --- /dev/null +++ b/ImageSharp/Formats/Exr/ExrDecoder.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Exr { + /// + /// Image decoder for generating an image out of a OpenExr stream. + /// + public class ExrDecoder : ImageDecoder + { + private ExrDecoder() + { + } + + /// + /// Gets the shared instance. + /// + public static ExrDecoder Instance { get; } = new(); + + /// + protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + return new ExrDecoderCore(new ExrDecoderOptions { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken); + } + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + ExrDecoderCore decoder = new(new ExrDecoderOptions { GeneralOptions = options }); + Image image = decoder.Decode(options.Configuration, stream, cancellationToken); + + ScaleToTargetSize(options, image); + + return image; + } + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(options, stream, cancellationToken); + } +} diff --git a/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/ImageSharp/Formats/Exr/ExrDecoderCore.cs new file mode 100644 index 0000000..34ace8a --- /dev/null +++ b/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -0,0 +1,1021 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using SixLabors.ImageSharp.Formats.Exr.Compression; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Exr { + /// + /// Performs the OpenExr decoding operation. + /// + internal sealed class ExrDecoderCore : ImageDecoderCore + { + /// + /// Reusable buffer. + /// + private readonly byte[] buffer = new byte[8]; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// The metadata. + /// + private ImageMetadata metadata; + + /// + /// The exr specific metadata. + /// + private ExrMetadata exrMetadata; + + /// + /// Initializes a new instance of the class. + /// + /// The options. + public ExrDecoderCore(ExrDecoderOptions options) + : base(options.GeneralOptions) + { + this.configuration = options.GeneralOptions.Configuration; + this.memoryAllocator = this.configuration.MemoryAllocator; + } + + /// + /// Gets or sets the image width. + /// + private int Width { get; set; } + + /// + /// Gets or sets the image height. + /// + private int Height { get; set; } + + /// + /// Gets or sets the image channel info's. + /// + private IList Channels { get; set; } + + /// + /// Gets or sets the compression method. + /// + private ExrCompression Compression { get; set; } + + /// + /// Gets or sets the image data type, either RGB, RGBA or gray. + /// + private ExrImageDataType ImageDataType { get; set; } + + /// + /// Gets or sets the pixel type. + /// + private ExrPixelType PixelType { get; set; } + + /// + /// Gets or sets the header attributes. + /// + private ExrHeaderAttributes HeaderAttributes { get; set; } + + /// + /// Gets or sets the earliest valid stream position for a scanline chunk. + /// + private long MinimumChunkOffset { get; set; } + + /// + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + { + this.ReadExrHeader(stream); + if (!this.IsSupportedCompression()) + { + ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); + } + + Image image = null; + try + { + image = new Image(this.configuration, this.Width, this.Height, this.metadata); + Buffer2D pixels = image.GetRootFramePixelBuffer(); + + switch (this.PixelType) + { + case ExrPixelType.Half: + case ExrPixelType.Float: + this.DecodeFloatingPointPixelData(stream, pixels, cancellationToken); + break; + case ExrPixelType.UnsignedInt: + this.DecodeUnsignedIntPixelData(stream, pixels, cancellationToken); + break; + default: + ExrThrowHelper.ThrowNotSupported("Pixel type is not supported"); + break; + } + + return image; + } + catch + { + image?.Dispose(); + throw; + } + } + + /// + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + ExrHeaderAttributes header = this.ReadExrHeader(stream); + + return new ImageInfo(new Size(header.DataWindow.XMax, header.DataWindow.YMax), this.metadata); + } + + /// + /// Decodes image data with floating point pixel data. + /// + /// The type of the pixels. + /// The stream to read from. + /// The pixel buffer. + /// The cancellation token. + private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buffer2D pixels, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + bool hasAlpha = this.HasAlpha(); + ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width); + uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression); + ulong bytesPerBlock = bytesPerRow * rowsPerBlock; + if (bytesPerBlock > int.MaxValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("EXR block size exceeds the maximum allowed size."); + } + + int width = this.Width; + int height = this.Height; + int channelCount = this.Channels.Count; + + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); + using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); + Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); + Span redPixelData = rowBuffer.GetSpan()[..width]; + Span greenPixelData = rowBuffer.GetSpan().Slice(width, width); + Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); + Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); + + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create( + this.Compression, + this.memoryAllocator, + width, + (uint)bytesPerBlock, + (uint)bytesPerRow, + rowsPerBlock, + channelCount, + this.PixelType); + + int decodedRows = 0; + while (decodedRows < height) + { + ulong rowOffset = this.ReadUnsignedLong(stream); + long nextRowOffsetPosition = stream.Position; + + this.ValidateChunkOffset(rowOffset, stream); + stream.Position = (long)rowOffset; + uint rowStartIndex = this.ReadUnsignedInteger(stream); + + uint compressedBytesCount = this.ReadUnsignedInteger(stream); + decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); + + int offset = 0; + for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) + { + Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); + for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) + { + ExrChannelInfo channel = this.Channels[channelIdx]; + offset += ReadFloatChannelData(stream, channel, decompressedPixelData[offset..], redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); + } + + for (int x = 0; x < width; x++) + { + HalfVector4 pixelValue = new(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : 1.0f); + pixelRow[x] = TPixel.FromVector4(pixelValue.ToVector4()); + } + + decodedRows++; + } + + stream.Position = nextRowOffsetPosition; + + cancellationToken.ThrowIfCancellationRequested(); + } + } + + /// + /// Decodes image data with unsigned int pixel data. + /// + /// The type of the pixels. + /// The stream to read from. + /// The pixel buffer. + /// The cancellation token. + private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffer2D pixels, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + bool hasAlpha = this.HasAlpha(); + ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width); + uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression); + ulong bytesPerBlock = bytesPerRow * rowsPerBlock; + if (bytesPerBlock > int.MaxValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("EXR block size exceeds the maximum allowed size."); + } + + int width = this.Width; + int height = this.Height; + int channelCount = this.Channels.Count; + + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); + using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); + Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); + Span redPixelData = rowBuffer.GetSpan()[..width]; + Span greenPixelData = rowBuffer.GetSpan().Slice(width, width); + Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); + Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); + + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create( + this.Compression, + this.memoryAllocator, + width, + (uint)bytesPerBlock, + (uint)bytesPerRow, + rowsPerBlock, + channelCount, + this.PixelType); + + int decodedRows = 0; + while (decodedRows < height) + { + ulong rowOffset = this.ReadUnsignedLong(stream); + long nextRowOffsetPosition = stream.Position; + + this.ValidateChunkOffset(rowOffset, stream); + stream.Position = (long)rowOffset; + uint rowStartIndex = this.ReadUnsignedInteger(stream); + + uint compressedBytesCount = this.ReadUnsignedInteger(stream); + decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); + + int offset = 0; + for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) + { + Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); + for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) + { + ExrChannelInfo channel = this.Channels[channelIdx]; + offset += this.ReadUnsignedIntChannelData(stream, channel, decompressedPixelData[offset..], redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); + } + + for (int x = 0; x < width; x++) + { + Rgba128 pixelValue = new(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : uint.MaxValue); + pixelRow[x] = TPixel.FromVector4(pixelValue.ToVector4()); + } + + decodedRows++; + } + + stream.Position = nextRowOffsetPosition; + + cancellationToken.ThrowIfCancellationRequested(); + } + } + + /// + /// Reads float image channel data. + /// + /// The stream to read from. + /// The channel info. + /// The decompressed pixel data. + /// The red channel pixel data. + /// The green channel pixel data. + /// The blue channel pixel data. + /// The alpha channel pixel data. + /// The width of a row in pixels. + /// The bytes read. + private static int ReadFloatChannelData( + BufferedReadStream stream, + ExrChannelInfo channel, + Span decompressedPixelData, + Span redPixelData, + Span greenPixelData, + Span bluePixelData, + Span alphaPixelData, + int width) + { + switch (channel.ChannelName) + { + case ExrConstants.ChannelNames.Red: + return ReadChannelData(channel, decompressedPixelData, redPixelData, width); + + case ExrConstants.ChannelNames.Blue: + return ReadChannelData(channel, decompressedPixelData, bluePixelData, width); + + case ExrConstants.ChannelNames.Green: + return ReadChannelData(channel, decompressedPixelData, greenPixelData, width); + + case ExrConstants.ChannelNames.Alpha: + return ReadChannelData(channel, decompressedPixelData, alphaPixelData, width); + + case ExrConstants.ChannelNames.Luminance: + int bytesRead = ReadChannelData(channel, decompressedPixelData, redPixelData, width); + redPixelData.CopyTo(bluePixelData); + redPixelData.CopyTo(greenPixelData); + + return bytesRead; + + default: + // Skip unknown channel. + int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; + stream.Position += width * channelDataSizeInBytes; + return channelDataSizeInBytes; + } + } + + /// + /// Reads UINT image channel data. + /// + /// The stream to read from. + /// The channel info. + /// The decompressed pixel data. + /// The red channel pixel data. + /// The green channel pixel data. + /// The blue channel pixel data. + /// The alpha channel pixel data. + /// The width of a row in pixels. + /// The bytes read. + private int ReadUnsignedIntChannelData( + BufferedReadStream stream, + ExrChannelInfo channel, + Span decompressedPixelData, + Span redPixelData, + Span greenPixelData, + Span bluePixelData, + Span alphaPixelData, + int width) + { + switch (channel.ChannelName) + { + case ExrConstants.ChannelNames.Red: + return ReadChannelData(channel, decompressedPixelData, redPixelData, width); + + case ExrConstants.ChannelNames.Blue: + return ReadChannelData(channel, decompressedPixelData, bluePixelData, width); + + case ExrConstants.ChannelNames.Green: + return ReadChannelData(channel, decompressedPixelData, greenPixelData, width); + + case ExrConstants.ChannelNames.Alpha: + return ReadChannelData(channel, decompressedPixelData, alphaPixelData, width); + + case ExrConstants.ChannelNames.Luminance: + int bytesRead = ReadChannelData(channel, decompressedPixelData, redPixelData, width); + redPixelData.CopyTo(bluePixelData); + redPixelData.CopyTo(greenPixelData); + return bytesRead; + + default: + // Skip unknown channel. + int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; + stream.Position += this.Width * channelDataSizeInBytes; + return channelDataSizeInBytes; + } + } + + /// + /// Reads the channel data for pixel type HALF or FLOAT. + /// + /// The channel info. + /// The decompressed pixel data. + /// The pixel data as float. + /// The width in pixel of a row. + /// The bytes read. + private static int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) => channel.PixelType switch + { + ExrPixelType.Half => ReadPixelRowChannelHalfSingle(decompressedPixelData, pixelData, width), + ExrPixelType.Float => ReadPixelRowChannelSingle(decompressedPixelData, pixelData, width), + _ => 0, + }; + + /// + /// Reads the channel data for pixel type UINT. + /// + /// The channel info. + /// The decompressed pixel data. + /// The pixel data as uint. + /// The width in pixels. + /// The bytes read. + private static int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) => channel.PixelType switch + { + ExrPixelType.UnsignedInt => ReadPixelRowChannelUnsignedInt(decompressedPixelData, pixelData, width), + _ => 0, + }; + + /// + /// Reads a pixel row with the pixel data being 16 bit half values. + /// + /// The decompressed pixel data. + /// The channel data as float. + /// The width of a row in pixels. + /// The bytes read. + private static int ReadPixelRowChannelHalfSingle(Span decompressedPixelData, Span channelData, int width) + { + int offset = 0; + for (int x = 0; x < width; x++) + { + ushort shortValue = BinaryPrimitives.ReadUInt16LittleEndian(decompressedPixelData.Slice(offset, 2)); + channelData[x] = HalfTypeHelper.Unpack(shortValue); + offset += 2; + } + + return offset; + } + + /// + /// Reads a pixel row with 32 bit float pixel data. + /// + /// The decompressed pixel data. + /// The pixel data as float. + /// The width in pixels of a row. + /// The bytes read. + private static int ReadPixelRowChannelSingle(Span decompressedPixelData, Span channelData, int width) + { + int offset = 0; + for (int x = 0; x < width; x++) + { + int intValue = BinaryPrimitives.ReadInt32LittleEndian(decompressedPixelData.Slice(offset, 4)); + channelData[x] = Unsafe.As(ref intValue); + offset += 4; + } + + return offset; + } + + /// + /// Reads a pixel row with the pixel typ UINT. + /// + /// The decompressed pixel bytes. + /// The uint pixel data. + /// The width of a row in pixels. + /// The bytes read. + private static int ReadPixelRowChannelUnsignedInt(Span decompressedPixelData, Span channelData, int width) + { + int offset = 0; + for (int x = 0; x < width; x++) + { + channelData[x] = BinaryPrimitives.ReadUInt32LittleEndian(decompressedPixelData.Slice(offset, 4)); + offset += 4; + } + + return offset; + } + + /// + /// Validates that all image channels have the same type and are among the supported pixel types. + /// + /// The pixel type. + private ExrPixelType ValidateChannels() + { + if (this.Channels.Count == 0) + { + ExrThrowHelper.ThrowInvalidImageContentException("At least one channel of pixel data is expected!"); + } + + // Find pixel the type of any channel which is R, G, B or A. + ExrPixelType? pixelType = null; + for (int i = 0; i < this.Channels.Count; i++) + { + if (this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Blue, StringComparison.Ordinal) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Green, StringComparison.Ordinal) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Red, StringComparison.Ordinal) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Alpha, StringComparison.Ordinal) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Luminance, StringComparison.Ordinal)) + { + if (!pixelType.HasValue) + { + pixelType = this.Channels[i].PixelType; + } + else + { + if (pixelType != this.Channels[i].PixelType) + { + ExrThrowHelper.ThrowNotSupported("Pixel channel data is expected to be the same for all channels."); + } + } + } + } + + if (!pixelType.HasValue) + { + ExrThrowHelper.ThrowNotSupported("Pixel channel data is unknown! Only R, G, B, A and Y are supported."); + } + + return pixelType.Value; + } + + /// + /// Determines the type image from the channel information. + /// + /// The image data type. + private ExrImageDataType DetermineImageDataType() + { + bool hasRedChannel = false; + bool hasGreenChannel = false; + bool hasBlueChannel = false; + bool hasAlphaChannel = false; + bool hasLuminance = false; + foreach (ExrChannelInfo channelInfo in this.Channels) + { + if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal)) + { + hasAlphaChannel = true; + } + + if (channelInfo.ChannelName.Equals("R", StringComparison.Ordinal)) + { + hasRedChannel = true; + } + + if (channelInfo.ChannelName.Equals("G", StringComparison.Ordinal)) + { + hasGreenChannel = true; + } + + if (channelInfo.ChannelName.Equals("B", StringComparison.Ordinal)) + { + hasBlueChannel = true; + } + + if (channelInfo.ChannelName.Equals("Y", StringComparison.Ordinal)) + { + hasLuminance = true; + } + } + + if (hasRedChannel && hasGreenChannel && hasBlueChannel && hasAlphaChannel) + { + return ExrImageDataType.Rgba; + } + + if (hasRedChannel && hasGreenChannel && hasBlueChannel) + { + return ExrImageDataType.Rgb; + } + + if (hasLuminance && this.Channels.Count == 1) + { + return ExrImageDataType.Gray; + } + + return ExrImageDataType.Unknown; + } + + /// + /// Reads the exr image header. + /// + /// + /// The stream. + /// The image header attributes. + private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream) + { + // Skip over the magick bytes, we already know its an EXR image. + stream.Skip(4); + + // Read version number. + byte version = (byte)stream.ReadByte(); + if (version != 2) + { + ExrThrowHelper.ThrowNotSupportedVersion(); + } + + // Next three bytes contain info's about the image. + byte flagsByte0 = (byte)stream.ReadByte(); + if ((flagsByte0 & (1 << 1)) != 0) + { + ExrThrowHelper.ThrowNotSupported("Decoding tiled exr images is not supported yet!"); + } + + // Discard the next two bytes. + int bytesRead = stream.Read(this.buffer, 0, 2); + if (bytesRead != 2) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data for exr file!"); + } + + this.HeaderAttributes = this.ParseHeaderAttributes(stream); + + ExrBox2i dataWindow = this.HeaderAttributes.DataWindow; + if (dataWindow.XMax < dataWindow.XMin || dataWindow.YMax < dataWindow.YMin) + { + ExrThrowHelper.ThrowInvalidImageContentException("EXR DataWindow max values must be greater than or equal to min values."); + } + + long width = (long)dataWindow.XMax - dataWindow.XMin + 1; + long height = (long)dataWindow.YMax - dataWindow.YMin + 1; + + // Decoding stages each row as four color planes, so the width must be bounded + // before later width * 4 buffer sizing can overflow. + if (width > int.MaxValue / 4 || height > int.MaxValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("EXR DataWindow dimensions exceed the maximum allowed size."); + } + + this.Width = (int)width; + this.Height = (int)height; + this.Channels = this.HeaderAttributes.Channels; + this.Compression = this.HeaderAttributes.Compression; + uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression); + long chunkCount = (this.Height + (long)rowsPerBlock - 1) / rowsPerBlock; + long offsetTableByteCount = chunkCount * sizeof(ulong); + + // The scanline offset table sits between the header and pixel chunks; proving it + // fits in the stream keeps all later chunk offsets on the pixel-data side. + if (stream.Position > stream.Length || offsetTableByteCount > stream.Length - stream.Position) + { + ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk offset table is outside the bounds of the stream."); + } + + this.MinimumChunkOffset = stream.Position + offsetTableByteCount; + this.PixelType = this.ValidateChannels(); + this.ImageDataType = this.DetermineImageDataType(); + + this.metadata = new ImageMetadata(); + + this.exrMetadata = this.metadata.GetExrMetadata(); + this.exrMetadata.PixelType = this.PixelType; + this.exrMetadata.ImageDataType = this.ImageDataType; + this.exrMetadata.Compression = this.Compression; + + return this.HeaderAttributes; + } + + /// + /// Parses the image header attributes. + /// + /// The stream to read from. + /// The image header attributes. + private ExrHeaderAttributes ParseHeaderAttributes(BufferedReadStream stream) + { + ExrAttribute attribute = this.ReadAttribute(stream); + + IList channels = null; + ExrBox2i? dataWindow = null; + ExrCompression? compression = null; + ExrBox2i? displayWindow = null; + ExrLineOrder? lineOrder = null; + float? aspectRatio = null; + float? screenWindowCenterX = null; + float? screenWindowCenterY = null; + float? screenWindowWidth = null; + uint? tileXSize = null; + uint? tileYSize = null; + int? chunkCount = null; + while (!attribute.Equals(ExrAttribute.EmptyAttribute)) + { + switch (attribute.Name) + { + case ExrConstants.AttributeNames.Channels: + channels = this.ReadChannelList(stream, attribute.Length); + break; + case ExrConstants.AttributeNames.Compression: + compression = (ExrCompression)stream.ReadByte(); + break; + case ExrConstants.AttributeNames.DataWindow: + dataWindow = this.ReadBoxInteger(stream); + break; + case ExrConstants.AttributeNames.DisplayWindow: + displayWindow = this.ReadBoxInteger(stream); + break; + case ExrConstants.AttributeNames.LineOrder: + lineOrder = (ExrLineOrder)stream.ReadByte(); + break; + case ExrConstants.AttributeNames.PixelAspectRatio: + aspectRatio = this.ReadSingle(stream); + break; + case ExrConstants.AttributeNames.ScreenWindowCenter: + screenWindowCenterX = this.ReadSingle(stream); + screenWindowCenterY = this.ReadSingle(stream); + break; + case ExrConstants.AttributeNames.ScreenWindowWidth: + screenWindowWidth = this.ReadSingle(stream); + break; + case ExrConstants.AttributeNames.Tiles: + tileXSize = this.ReadUnsignedInteger(stream); + tileYSize = this.ReadUnsignedInteger(stream); + break; + case ExrConstants.AttributeNames.ChunkCount: + chunkCount = this.ReadSignedInteger(stream); + break; + default: + // Skip unknown attribute bytes. + stream.Skip(attribute.Length); + break; + } + + attribute = this.ReadAttribute(stream); + } + + if (!displayWindow.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the displayWindow attribute is missing!"); + } + + if (!dataWindow.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the dataWindow attribute is missing!"); + } + + if (channels is null) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the channels attribute is missing!"); + } + + if (!compression.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the compression attribute is missing!"); + } + + if (!lineOrder.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the lineOrder attribute is missing!"); + } + + if (!aspectRatio.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the aspectRatio attribute is missing!"); + } + + if (!screenWindowWidth.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the screenWindowWidth attribute is missing!"); + } + + if (!screenWindowCenterX.HasValue || !screenWindowCenterY.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the screenWindowCenter attribute is missing!"); + } + + ExrHeaderAttributes header = new( + channels, + compression.Value, + dataWindow.Value, + displayWindow.Value, + lineOrder.Value, + aspectRatio.Value, + screenWindowWidth.Value, + new PointF(screenWindowCenterX.Value, screenWindowCenterY.Value), + tileXSize, + tileYSize, + chunkCount); + return header; + } + + /// + /// Reads a attrbute from the stream, which consist of a name, a type and a size in bytes. + /// + /// The stream to read from. + /// A attribute. + private ExrAttribute ReadAttribute(BufferedReadStream stream) + { + string attributeName = ReadString(stream); + if (attributeName.Equals(string.Empty, StringComparison.Ordinal)) + { + return ExrAttribute.EmptyAttribute; + } + + string attributeType = ReadString(stream); + int attributeSize = this.ReadSignedInteger(stream); + + return new ExrAttribute(attributeName, attributeType, attributeSize); + } + + /// + /// Reads a box attribute, which is a xMin, xMax and yMin, yMax value. + /// + /// The stream to reaad from. + /// A box struct. + private ExrBox2i ReadBoxInteger(BufferedReadStream stream) + { + int xMin = this.ReadSignedInteger(stream); + int yMin = this.ReadSignedInteger(stream); + int xMax = this.ReadSignedInteger(stream); + int yMax = this.ReadSignedInteger(stream); + + return new ExrBox2i(xMin, yMin, xMax, yMax); + } + + /// + /// Reads the channel list from the stream. + /// + /// The stream to read from. + /// The size in bytes of the channel list attribute. + /// The channel list. + private List ReadChannelList(BufferedReadStream stream, int attributeSize) + { + List channels = []; + while (attributeSize > 1) + { + ExrChannelInfo channelInfo = this.ReadChannelInfo(stream, out int bytesRead); + channels.Add(channelInfo); + attributeSize -= bytesRead; + } + + // Last byte should be a null byte. + if (stream.ReadByte() == -1) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data to read the exr channel list!"); + } + + return channels; + } + + /// + /// Reads the channel information from the stream. + /// + /// The stream to read from. + /// The bytes read. + /// Channel info. + private ExrChannelInfo ReadChannelInfo(BufferedReadStream stream, out int bytesRead) + { + string channelName = ReadString(stream); + bytesRead = channelName.Length + 1; + + ExrPixelType pixelType = (ExrPixelType)this.ReadSignedInteger(stream); + bytesRead += 4; + + byte pLinear = (byte)stream.ReadByte(); + + // Next 3 bytes are reserved bytes and not use. + if (stream.Read(this.buffer, 0, 3) != 3) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data to read exr channel info!"); + } + + bytesRead += 4; + + int xSampling = this.ReadSignedInteger(stream); + bytesRead += 4; + + int ySampling = this.ReadSignedInteger(stream); + bytesRead += 4; + + return new ExrChannelInfo(channelName, pixelType, pLinear, xSampling, ySampling); + } + + /// + /// Reads a the string from the stream. + /// + /// The stream to read from. + /// A string. + private static string ReadString(BufferedReadStream stream) + { + StringBuilder str = new(); + int character = stream.ReadByte(); + if (character == 0) + { + // End of file header reached. + return string.Empty; + } + + while (character != 0) + { + if (character == -1) + { + ExrThrowHelper.ThrowInvalidImageHeader(); + } + + str.Append((char)character); + character = stream.ReadByte(); + } + + return str.ToString(); + } + + /// + /// Determines whether the compression is supported. + /// + /// True if the compression is supported; otherwise, false>. + private bool IsSupportedCompression() => this.Compression switch + { + ExrCompression.None or ExrCompression.Zip or ExrCompression.Zips or ExrCompression.RunLengthEncoded or ExrCompression.B44 or ExrCompression.Pxr24 => true, + _ => false, + }; + + /// + /// Validates a scanline chunk offset read from the EXR offset table. + /// + /// The chunk offset to validate. + /// The stream containing the image data. + private void ValidateChunkOffset(ulong chunkOffset, BufferedReadStream stream) + { + if (chunkOffset < (ulong)this.MinimumChunkOffset || chunkOffset >= (ulong)stream.Length) + { + ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk offset is outside the bounds of the stream."); + } + } + + /// + /// Determines whether this image has alpha channel. + /// + /// True if this image has a alpha channel; otherwise, false. + private bool HasAlpha() + { + foreach (ExrChannelInfo channelInfo in this.Channels) + { + if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + /// + /// Reads a unsigned long value from the stream. + /// + /// The stream to read the data from. + /// The unsigned long value. + private ulong ReadUnsignedLong(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer, 0, 8); + if (bytesRead != 8) + { + ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to read a unsigned long from the stream!"); + } + + return BinaryPrimitives.ReadUInt64LittleEndian(this.buffer); + } + + /// + /// Reads a unsigned integer value from the stream. + /// + /// The stream to read the data from. + /// The integer value. + private uint ReadUnsignedInteger(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer, 0, 4); + if (bytesRead != 4) + { + ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to read a unsigned int from the stream!"); + } + + return BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + } + + /// + /// Reads a signed integer value from the stream. + /// + /// The stream to read the data from. + /// The integer value. + private int ReadSignedInteger(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer, 0, 4); + if (bytesRead != 4) + { + ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to read a signed int from the stream!"); + } + + return BinaryPrimitives.ReadInt32LittleEndian(this.buffer); + } + + /// + /// Reads a float value from the stream. + /// + /// The stream to read the data from. + /// The float value. + private float ReadSingle(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer, 0, 4); + if (bytesRead != 4) + { + ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to read a float value from the stream!"); + } + + int intValue = BinaryPrimitives.ReadInt32BigEndian(this.buffer); + + return Unsafe.As(ref intValue); + } + } +} diff --git a/ImageSharp/Formats/Exr/ExrDecoderOptions.cs b/ImageSharp/Formats/Exr/ExrDecoderOptions.cs new file mode 100644 index 0000000..4beab2c --- /dev/null +++ b/ImageSharp/Formats/Exr/ExrDecoderOptions.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Exr { + /// + /// Image decoder options for decoding OpenExr streams. + /// + public sealed class ExrDecoderOptions : ISpecializedDecoderOptions + { + /// + public DecoderOptions GeneralOptions { get; init; } = new(); + } +} diff --git a/ImageSharp/Formats/Exr/ExrEncoder.cs b/ImageSharp/Formats/Exr/ExrEncoder.cs new file mode 100644 index 0000000..864b423 --- /dev/null +++ b/ImageSharp/Formats/Exr/ExrEncoder.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Exr.Constants; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Exr { + /// + /// Image encoder for writing an image to a stream in the OpenExr Format. + /// + public sealed class ExrEncoder : ImageEncoder + { + /// + /// Gets or sets the pixel type of the image. + /// + public ExrPixelType? PixelType { get; set; } + + /// + /// Gets the compression type to use. + /// + public ExrCompression? Compression { get; init; } + + /// + protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) + { + ExrEncoderCore encoder = new(this, image.Configuration, image.Configuration.MemoryAllocator); + encoder.Encode(image, stream, cancellationToken); + } + } +} diff --git a/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/ImageSharp/Formats/Exr/ExrEncoderCore.cs new file mode 100644 index 0000000..dad55ee --- /dev/null +++ b/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -0,0 +1,711 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Threading; +using SixLabors.ImageSharp.Formats.Exr.Compression; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Exr { + /// + /// Image encoder for writing an image to a stream in the OpenExr format. + /// + internal sealed class ExrEncoderCore + { + /// + /// Reusable buffer. + /// + private readonly byte[] buffer = new byte[8]; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// The encoder with options. + /// + private readonly ExrEncoder encoder; + + /// + /// The pixel type of the image. + /// + private ExrPixelType? pixelType; + + /// + /// Initializes a new instance of the class. + /// + /// The encoder with options. + /// The configuration. + /// The memory manager. + public ExrEncoderCore(ExrEncoder encoder, Configuration configuration, MemoryAllocator memoryAllocator) + { + this.configuration = configuration; + this.encoder = encoder; + this.memoryAllocator = memoryAllocator; + this.Compression = encoder.Compression ?? ExrCompression.None; + this.pixelType = encoder.PixelType; + } + + /// + /// Gets or sets the compression implementation to use when encoding the image. + /// + internal ExrCompression Compression { get; set; } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + Buffer2D pixels = image.Frames.RootFrame.PixelBuffer; + + ImageMetadata metadata = image.Metadata; + ExrMetadata exrMetadata = metadata.GetExrMetadata(); + this.pixelType ??= exrMetadata.PixelType; + int width = image.Width; + int height = image.Height; + float aspectRatio = 1.0f; + ExrBox2i dataWindow = new(0, 0, width - 1, height - 1); + ExrBox2i displayWindow = new(0, 0, width - 1, height - 1); + ExrLineOrder lineOrder = ExrLineOrder.IncreasingY; + PointF screenWindowCenter = new(0.0f, 0.0f); + int screenWindowWidth = 1; + List channels = + [ + new(ExrConstants.ChannelNames.Alpha, this.pixelType.Value, 0, 1, 1), + new(ExrConstants.ChannelNames.Blue, this.pixelType.Value, 0, 1, 1), + new(ExrConstants.ChannelNames.Green, this.pixelType.Value, 0, 1, 1), + new(ExrConstants.ChannelNames.Red, this.pixelType.Value, 0, 1, 1), + ]; + ExrHeaderAttributes header = new( + channels, + this.Compression, + dataWindow, + displayWindow, + lineOrder, + aspectRatio, + screenWindowWidth, + screenWindowCenter); + + // Write magick bytes. + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, ExrConstants.MagickBytes); + stream.Write(this.buffer.AsSpan(0, 4)); + + // Version number. + this.buffer[0] = 2; + + // Second, third and fourth bytes store info about the image, set all to default: zero. + this.buffer[1] = 0; + this.buffer[2] = 0; + this.buffer[3] = 0; + stream.Write(this.buffer.AsSpan(0, 4)); + + // Write EXR header. + this.WriteHeader(stream, header); + + // Next is offsets table to each pixel row, which will be written after the pixel data was written. + ulong startOfRowOffsetData = (ulong)stream.Position; + stream.Position += 8 * height; + + // Write pixel data. + switch (this.pixelType) + { + case ExrPixelType.Half: + case ExrPixelType.Float: + { + ulong[] rowOffsets = this.EncodeFloatingPointPixelData(stream, pixels, width, height, channels, this.Compression, cancellationToken); + stream.Position = (long)startOfRowOffsetData; + this.WriteRowOffsets(stream, height, rowOffsets); + break; + } + + case ExrPixelType.UnsignedInt: + { + ulong[] rowOffsets = this.EncodeUnsignedIntPixelData(stream, pixels, width, height, channels, this.Compression, cancellationToken); + stream.Position = (long)startOfRowOffsetData; + this.WriteRowOffsets(stream, height, rowOffsets); + break; + } + } + } + + /// + /// Encodes and writes pixel data with float pixel data to the stream. + /// + /// The type of the pixels. + /// The stream to write to. + /// The pixel bufer. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The imagechannels. + /// The compression to use. + /// The cancellation token. + /// The array of pixel row offsets. + private ulong[] EncodeFloatingPointPixelData( + Stream stream, + Buffer2D pixels, + int width, + int height, + List channels, + ExrCompression compression, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width); + uint rowsPerBlock = ExrUtils.RowsPerBlock(compression); + ulong bytesPerBlock = bytesPerRow * rowsPerBlock; + if (bytesPerRow > uint.MaxValue || bytesPerBlock > int.MaxValue) + { + throw new ImageFormatException("Image is too large to encode in EXR format."); + } + + using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 4, AllocationOptions.Clean); + using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); + Span redBuffer = rgbBuffer.GetSpan()[..width]; + Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); + Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); + Span alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width); + + using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, (uint)bytesPerBlock, (uint)bytesPerRow, rowsPerBlock, width); + + ulong[] rowOffsets = new ulong[height]; + for (uint y = 0; y < height; y += rowsPerBlock) + { + rowOffsets[y] = (ulong)stream.Position; + + // Write row index. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, y); + stream.Write(this.buffer.AsSpan(0, 4)); + + // At this point, it is not yet known how much bytes the compressed data will take up, keep stream position. + long pixelDataSizePos = stream.Position; + stream.Position = pixelDataSizePos + 4; + + uint rowsInBlockCount = 0; + for (uint rowIndex = y; rowIndex < y + rowsPerBlock && rowIndex < height; rowIndex++) + { + Span pixelRowSpan = pixels.DangerousGetRowSpan((int)rowIndex); + for (int x = 0; x < width; x++) + { + Vector4 vector4 = pixelRowSpan[x].ToVector4(); + redBuffer[x] = vector4.X; + greenBuffer[x] = vector4.Y; + blueBuffer[x] = vector4.Z; + alphaBuffer[x] = vector4.W; + } + + // Write pixel data to row block buffer. + Span rowBlockSpan = rowBlockBuffer.GetSpan().Slice((int)(rowsInBlockCount * bytesPerRow), (int)bytesPerRow); + switch (this.pixelType) + { + case ExrPixelType.Float: + WriteSingleRow(rowBlockSpan, width, alphaBuffer, blueBuffer, greenBuffer, redBuffer); + break; + case ExrPixelType.Half: + WriteHalfSingleRow(rowBlockSpan, width, alphaBuffer, blueBuffer, greenBuffer, redBuffer); + break; + } + + rowsInBlockCount++; + } + + // Write compressed pixel row data to the stream. + uint compressedBytes = compressor.CompressRowBlock(rowBlockBuffer.GetSpan(), (int)rowsInBlockCount); + long positionAfterPixelData = stream.Position; + + // Write pixel row data size. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, compressedBytes); + stream.Position = pixelDataSizePos; + stream.Write(this.buffer.AsSpan(0, 4)); + stream.Position = positionAfterPixelData; + + cancellationToken.ThrowIfCancellationRequested(); + } + + return rowOffsets; + } + + /// + /// Encodes and writes pixel data with the unsigned int pixel type to the stream. + /// + /// The type of the pixels. + /// The stream to write to. + /// The pixel bufer. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The imagechannels. + /// The compression to use. + /// The cancellation token. + /// The array of pixel row offsets. + private ulong[] EncodeUnsignedIntPixelData( + Stream stream, + Buffer2D pixels, + int width, + int height, + List channels, + ExrCompression compression, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width); + uint rowsPerBlock = ExrUtils.RowsPerBlock(compression); + ulong bytesPerBlock = bytesPerRow * rowsPerBlock; + if (bytesPerRow > uint.MaxValue || bytesPerBlock > int.MaxValue) + { + throw new ImageFormatException("Image is too large to encode in EXR format."); + } + + using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 4, AllocationOptions.Clean); + using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); + Span redBuffer = rgbBuffer.GetSpan()[..width]; + Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); + Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); + Span alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width); + + using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, (uint)bytesPerBlock, (uint)bytesPerRow, rowsPerBlock, width); + + Rgba128 rgb = default; + ulong[] rowOffsets = new ulong[height]; + for (uint y = 0; y < height; y += rowsPerBlock) + { + rowOffsets[y] = (ulong)stream.Position; + + // Write row index. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, y); + stream.Write(this.buffer.AsSpan(0, 4)); + + // At this point, it is not yet known how much bytes the compressed data will take up, keep stream position. + long pixelDataSizePos = stream.Position; + stream.Position = pixelDataSizePos + 4; + + uint rowsInBlockCount = 0; + for (uint rowIndex = y; rowIndex < y + rowsPerBlock && rowIndex < height; rowIndex++) + { + Span pixelRowSpan = pixels.DangerousGetRowSpan((int)rowIndex); + for (int x = 0; x < width; x++) + { + Vector4 vector4 = pixelRowSpan[x].ToVector4(); + rgb = Rgba128.FromVector4(vector4); + + redBuffer[x] = rgb.R; + greenBuffer[x] = rgb.G; + blueBuffer[x] = rgb.B; + alphaBuffer[x] = rgb.A; + } + + // Write row data to row block buffer. + Span rowBlockSpan = rowBlockBuffer.GetSpan().Slice((int)(rowsInBlockCount * bytesPerRow), (int)bytesPerRow); + WriteUnsignedIntRow(rowBlockSpan, width, alphaBuffer, blueBuffer, greenBuffer, redBuffer); + rowsInBlockCount++; + } + + // Write pixel row data compressed to the stream. + uint compressedBytes = compressor.CompressRowBlock(rowBlockBuffer.GetSpan(), (int)rowsInBlockCount); + long positionAfterPixelData = stream.Position; + + // Write pixel row data size. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, compressedBytes); + stream.Position = pixelDataSizePos; + stream.Write(this.buffer.AsSpan(0, 4)); + stream.Position = positionAfterPixelData; + + cancellationToken.ThrowIfCancellationRequested(); + } + + return rowOffsets; + } + + /// + /// Writes the image header to the stream. + /// + /// The stream to write to. + /// The header. + private void WriteHeader(Stream stream, ExrHeaderAttributes header) + { + this.WriteChannels(stream, header.Channels); + this.WriteCompression(stream, header.Compression); + this.WriteDataWindow(stream, header.DataWindow); + this.WriteDisplayWindow(stream, header.DisplayWindow); + this.WritePixelAspectRatio(stream, header.AspectRatio); + this.WriteLineOrder(stream, header.LineOrder); + this.WriteScreenWindowCenter(stream, header.ScreenWindowCenter); + this.WriteScreenWindowWidth(stream, header.ScreenWindowWidth); + stream.WriteByte(0); + } + + /// + /// Writes a row of pixels with the FLOAT pixel type to a buffer. + /// + /// The buffer to write to. + /// The width of a row in pixels. + /// The alpha channel buffer. + /// The blue channel buffer. + /// The green channel buffer. + /// The red channel buffer. + private static void WriteSingleRow(Span buffer, int width, Span alphaBuffer, Span blueBuffer, Span greenBuffer, Span redBuffer) + { + int offset = 0; + for (int x = 0; x < width; x++) + { + WriteSingleToBuffer(buffer.Slice(offset, 4), alphaBuffer[x]); + offset += 4; + } + + for (int x = 0; x < width; x++) + { + WriteSingleToBuffer(buffer.Slice(offset, 4), blueBuffer[x]); + offset += 4; + } + + for (int x = 0; x < width; x++) + { + WriteSingleToBuffer(buffer.Slice(offset, 4), greenBuffer[x]); + offset += 4; + } + + for (int x = 0; x < width; x++) + { + WriteSingleToBuffer(buffer.Slice(offset, 4), redBuffer[x]); + offset += 4; + } + } + + /// + /// Writes a row of pixels with the HALF pixel type to a buffer. + /// + /// The buffer to write to. + /// The width of a row in pixels. + /// The alpha channel buffer. + /// The blue channel buffer. + /// The green channel buffer. + /// The red channel buffer. + private static void WriteHalfSingleRow(Span buffer, int width, Span alphaBuffer, Span blueBuffer, Span greenBuffer, Span redBuffer) + { + int offset = 0; + for (int x = 0; x < width; x++) + { + WriteHalfSingleToBuffer(buffer.Slice(offset, 2), alphaBuffer[x]); + offset += 2; + } + + for (int x = 0; x < width; x++) + { + WriteHalfSingleToBuffer(buffer.Slice(offset, 2), blueBuffer[x]); + offset += 2; + } + + for (int x = 0; x < width; x++) + { + WriteHalfSingleToBuffer(buffer.Slice(offset, 2), greenBuffer[x]); + offset += 2; + } + + for (int x = 0; x < width; x++) + { + WriteHalfSingleToBuffer(buffer.Slice(offset, 2), redBuffer[x]); + offset += 2; + } + } + + /// + /// Writes a row of pixels with unsigned int pixel data to a buffer. + /// + /// The buffer to write to. + /// The width of the row in pixels. + /// The alpha channel buffer. + /// The blue channel buffer. + /// The green channel buffer. + /// The red channel buffer. + private static void WriteUnsignedIntRow(Span buffer, int width, Span alphaBuffer, Span blueBuffer, Span greenBuffer, Span redBuffer) + { + int offset = 0; + for (int x = 0; x < width; x++) + { + WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), alphaBuffer[x]); + offset += 4; + } + + for (int x = 0; x < width; x++) + { + WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), blueBuffer[x]); + offset += 4; + } + + for (int x = 0; x < width; x++) + { + WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), greenBuffer[x]); + offset += 4; + } + + for (int x = 0; x < width; x++) + { + WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), redBuffer[x]); + offset += 4; + } + } + + /// + /// Writes the row offsets to the stream. + /// + /// The stream to write to. + /// The height in pixels of the image. + /// The row offsets. + private void WriteRowOffsets(Stream stream, int height, ulong[] rowOffsets) + { + for (int i = 0; i < height; i++) + { + BinaryPrimitives.WriteUInt64LittleEndian(this.buffer, rowOffsets[i]); + stream.Write(this.buffer); + } + } + + /// + /// Writes the channel infos to the stream. + /// + /// The stream to write to. + /// The channels. + private void WriteChannels(Stream stream, IList channels) + { + int attributeSize = 0; + foreach (ExrChannelInfo channelInfo in channels) + { + attributeSize += channelInfo.ChannelName.Length + 1; + attributeSize += 16; + } + + // Last zero byte. + attributeSize++; + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Channels, ExrConstants.AttibuteTypes.ChannelList, attributeSize); + + foreach (ExrChannelInfo channelInfo in channels) + { + this.WriteChannelInfo(stream, channelInfo); + } + + // Last byte should be zero. + stream.WriteByte(0); + } + + /// + /// Writes info about a single channel to the stream. + /// + /// The stream to write to. + /// The channel information. + private void WriteChannelInfo(Stream stream, ExrChannelInfo channelInfo) + { + WriteString(stream, channelInfo.ChannelName); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, (int)channelInfo.PixelType); + stream.Write(this.buffer.AsSpan(0, 4)); + + stream.WriteByte(channelInfo.Linear); + + // Next 3 bytes are reserved and will set to zero. + stream.WriteByte(0); + stream.WriteByte(0); + stream.WriteByte(0); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.XSampling); + stream.Write(this.buffer.AsSpan(0, 4)); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.YSampling); + stream.Write(this.buffer.AsSpan(0, 4)); + } + + /// + /// Writes the compression type to the stream. + /// + /// The stream to write to. + /// The compression type. + private void WriteCompression(Stream stream, ExrCompression compression) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Compression, ExrConstants.AttibuteTypes.Compression, 1); + stream.WriteByte((byte)compression); + } + + /// + /// Writes the pixel aspect ratio to the stream. + /// + /// The stream to write to. + /// The aspect ratio. + private void WritePixelAspectRatio(Stream stream, float aspectRatio) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.PixelAspectRatio, ExrConstants.AttibuteTypes.Float, 4); + this.WriteSingle(stream, aspectRatio); + } + + /// + /// Writes the line order to the stream. + /// + /// The stream to write to. + /// The line order. + private void WriteLineOrder(Stream stream, ExrLineOrder lineOrder) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.LineOrder, ExrConstants.AttibuteTypes.LineOrder, 1); + stream.WriteByte((byte)lineOrder); + } + + /// + /// Writes the screen window center to the stream. + /// + /// The stream to write to. + /// The screen window center. + private void WriteScreenWindowCenter(Stream stream, PointF screenWindowCenter) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.ScreenWindowCenter, ExrConstants.AttibuteTypes.TwoFloat, 8); + this.WriteSingle(stream, screenWindowCenter.X); + this.WriteSingle(stream, screenWindowCenter.Y); + } + + /// + /// Writes the screen width to the stream. + /// + /// The stream to write to. + /// Width of the screen window. + private void WriteScreenWindowWidth(Stream stream, float screenWindowWidth) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.ScreenWindowWidth, ExrConstants.AttibuteTypes.Float, 4); + this.WriteSingle(stream, screenWindowWidth); + } + + /// + /// Writes the data window to the stream. + /// + /// The stream to write to. + /// The data window. + private void WriteDataWindow(Stream stream, ExrBox2i dataWindow) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.DataWindow, ExrConstants.AttibuteTypes.BoxInt, 16); + this.WriteBoxInteger(stream, dataWindow); + } + + /// + /// Writes the display window to the stream. + /// + /// The stream to write to. + /// The display window. + private void WriteDisplayWindow(Stream stream, ExrBox2i displayWindow) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.DisplayWindow, ExrConstants.AttibuteTypes.BoxInt, 16); + this.WriteBoxInteger(stream, displayWindow); + } + + /// + /// Writes attribute information to the stream. + /// + /// The stream to write to. + /// The name of the attribute. + /// The type of the attribute. + /// The size in bytes of the attribute. + private void WriteAttributeInformation(Stream stream, string name, string type, int size) + { + // Write attribute name. + WriteString(stream, name); + + // Write attribute type. + WriteString(stream, type); + + // Write attribute size. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)size); + stream.Write(this.buffer.AsSpan(0, 4)); + } + + /// + /// Writes a string to the stream. + /// + /// The stream to write to. + /// The string to write. + private static void WriteString(Stream stream, string str) + { + foreach (char c in str) + { + stream.WriteByte((byte)c); + } + + // Write termination byte. + stream.WriteByte(0); + } + + /// + /// Writes box struct with xmin, xmax, ymin and y max to the stream. + /// + /// The stream to write to. + /// The box to write. + private void WriteBoxInteger(Stream stream, ExrBox2i box) + { + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMin); + stream.Write(this.buffer.AsSpan(0, 4)); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMin); + stream.Write(this.buffer.AsSpan(0, 4)); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMax); + stream.Write(this.buffer.AsSpan(0, 4)); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMax); + stream.Write(this.buffer.AsSpan(0, 4)); + } + + /// + /// Writes 32 bit float value to the stream. + /// + /// The stream to write to. + /// The float value to write. + [MethodImpl(InliningOptions.ShortMethod)] + private unsafe void WriteSingle(Stream stream, float value) + { + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, *(int*)&value); + stream.Write(this.buffer.AsSpan(0, 4)); + } + + /// + /// Writes a 32 bit float value to a buffer. + /// + /// The buffer to write to. + /// The float value to write. + [MethodImpl(InliningOptions.ShortMethod)] + private static unsafe void WriteSingleToBuffer(Span buffer, float value) => BinaryPrimitives.WriteInt32LittleEndian(buffer, *(int*)&value); + + /// + /// Writes a 16 bit float value to a buffer. + /// + /// The buffer to write to. + /// The float value to write. + [MethodImpl(InliningOptions.ShortMethod)] + private static void WriteHalfSingleToBuffer(Span buffer, float value) + { + ushort valueAsShort = HalfTypeHelper.Pack(value); + BinaryPrimitives.WriteUInt16LittleEndian(buffer, valueAsShort); + } + + /// + /// Writes one unsigned int to a buffer. + /// + /// The buffer to write to. + /// The uint value to write. + [MethodImpl(InliningOptions.ShortMethod)] + private static void WriteUnsignedIntToBuffer(Span buffer, uint value) => BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); + } +} diff --git a/ImageSharp/Formats/Exr/ExrFormat.cs b/ImageSharp/Formats/Exr/ExrFormat.cs new file mode 100644 index 0000000..06e1f67 --- /dev/null +++ b/ImageSharp/Formats/Exr/ExrFormat.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Exr { + /// + /// Registers the image encoders, decoders and mime type detectors for the OpenExr format. + /// + public sealed class ExrFormat : IImageFormat + { + private ExrFormat() + { + } + + /// + /// Gets the current instance. + /// + public static ExrFormat Instance { get; } = new(); + + /// + public string Name => "EXR"; + + /// + public string DefaultMimeType => "image/x-exr"; + + /// + public IEnumerable MimeTypes => ExrConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => ExrConstants.FileExtensions; + + /// + public ExrMetadata CreateDefaultFormatMetadata() => new(); + } +} diff --git a/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs b/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs new file mode 100644 index 0000000..cf9fb04 --- /dev/null +++ b/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs @@ -0,0 +1,109 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Exr.Constants; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Exr { + /// + /// The header of an EXR image. + /// + /// + internal class ExrHeaderAttributes + { + /// + /// Initializes a new instance of the class. + /// + /// The image channels. + /// The compression used. + /// The data window. + /// The display window. + /// The line order. + /// The aspect ratio. + /// Width of the screen window. + /// The screen window center. + /// Size of the tile in x dimension. + /// Size of the tile in y dimension. + /// The chunk count. + public ExrHeaderAttributes( + IList channels, + ExrCompression compression, + ExrBox2i dataWindow, + ExrBox2i displayWindow, + ExrLineOrder lineOrder, + float aspectRatio, + float screenWindowWidth, + PointF screenWindowCenter, + uint? tileXSize = null, + uint? tileYSize = null, + int? chunkCount = null) + { + this.Channels = channels; + this.Compression = compression; + this.DataWindow = dataWindow; + this.DisplayWindow = displayWindow; + this.LineOrder = lineOrder; + this.AspectRatio = aspectRatio; + this.ScreenWindowWidth = screenWindowWidth; + this.ScreenWindowCenter = screenWindowCenter; + this.TileXSize = tileXSize; + this.TileYSize = tileYSize; + this.ChunkCount = chunkCount; + } + + /// + /// Gets or sets a description of the image channels stored in the file. + /// + public IList Channels { get; set; } + + /// + /// Gets or sets the compression method applied to the pixel data of all channels in the file. + /// + public ExrCompression Compression { get; set; } + + /// + /// Gets or sets the image’s data window. + /// + public ExrBox2i DataWindow { get; set; } + + /// + /// Gets or sets the image’s display window. + /// + public ExrBox2i DisplayWindow { get; set; } + + /// + /// Gets or sets in what order the scan lines in the file are stored in the file (increasing Y, decreasing Y, or, for tiled images, also random Y). + /// + public ExrLineOrder LineOrder { get; set; } + + /// + /// Gets or sets the aspect ratio of the image. + /// + public float AspectRatio { get; set; } + + /// + /// Gets or sets the screen width. + /// + public float ScreenWindowWidth { get; set; } + + /// + /// Gets or sets the screen window center. + /// + public PointF ScreenWindowCenter { get; set; } + + /// + /// Gets or sets the number of horizontal tiles. + /// + public uint? TileXSize { get; set; } + + /// + /// Gets or sets the number of vertical tiles. + /// + public uint? TileYSize { get; set; } + + /// + /// Gets or sets the chunk count. Indicates the number of chunks in this part. Required if the multipart bit (12) is set. + /// + public int? ChunkCount { get; set; } + } +} diff --git a/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs b/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs new file mode 100644 index 0000000..a9d5ff9 --- /dev/null +++ b/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Exr { + /// + /// Detects OpenExr file headers. + /// + public sealed class ExrImageFormatDetector : IImageFormatDetector + { + /// + public int HeaderSize => 4; + + private bool IsSupportedFileFormat(ReadOnlySpan header) + { + if (header.Length >= this.HeaderSize) + { + int fileTypeMarker = BinaryPrimitives.ReadInt32LittleEndian(header); + return fileTypeMarker == ExrConstants.MagickBytes; + } + + return false; + } + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.IsSupportedFileFormat(header) ? ExrFormat.Instance : null; + return format != null; + } + } +} diff --git a/ImageSharp/Formats/Exr/ExrMetadata.cs b/ImageSharp/Formats/Exr/ExrMetadata.cs new file mode 100644 index 0000000..1519a63 --- /dev/null +++ b/ImageSharp/Formats/Exr/ExrMetadata.cs @@ -0,0 +1,156 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Exr { + /// + /// Provides OpenExr specific metadata information for the image. + /// + public class ExrMetadata : IFormatMetadata + { + /// + /// Initializes a new instance of the class. + /// + public ExrMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private ExrMetadata(ExrMetadata other) => this.PixelType = other.PixelType; + + /// + /// Gets or sets the pixel format. + /// + public ExrPixelType PixelType { get; set; } = ExrPixelType.Half; + + /// + /// Gets or sets the image data type, either RGB, RGBA or gray. + /// + public ExrImageDataType ImageDataType { get; set; } = ExrImageDataType.Unknown; + + /// + /// Gets or sets the compression method. + /// + public ExrCompression Compression { get; set; } = ExrCompression.None; + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + bool hasAlpha = this.ImageDataType is ExrImageDataType.Rgba; + + int bitsPerComponent = 32; + int bitsPerPixel = hasAlpha ? bitsPerComponent * 4 : bitsPerComponent * 3; + if (this.PixelType == ExrPixelType.Half) + { + bitsPerComponent = 16; + bitsPerPixel = hasAlpha ? bitsPerComponent * 4 : bitsPerComponent * 3; + } + + PixelAlphaRepresentation alpha = hasAlpha ? PixelAlphaRepresentation.Unassociated : PixelAlphaRepresentation.None; + PixelColorType color = PixelColorType.RGB; + + int componentsCount = 0; + int[] precision = []; + switch (this.ImageDataType) + { + case ExrImageDataType.Rgb: + color = PixelColorType.RGB; + componentsCount = 3; + precision = new int[componentsCount]; + precision[0] = bitsPerComponent; + precision[1] = bitsPerComponent; + precision[2] = bitsPerComponent; + break; + case ExrImageDataType.Rgba: + color = PixelColorType.RGB | PixelColorType.Alpha; + componentsCount = 4; + precision = new int[componentsCount]; + precision[0] = bitsPerComponent; + precision[1] = bitsPerComponent; + precision[2] = bitsPerComponent; + precision[3] = bitsPerComponent; + break; + case ExrImageDataType.Gray: + color = PixelColorType.Luminance; + componentsCount = 1; + precision = new int[componentsCount]; + precision[0] = bitsPerComponent; + break; + } + + PixelComponentInfo info = PixelComponentInfo.Create(componentsCount, bitsPerPixel, precision); + return new PixelTypeInfo(bitsPerPixel) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + { + EncodingType type = this.Compression is ExrCompression.B44 or ExrCompression.B44A or ExrCompression.Pxr24 + ? EncodingType.Lossy + : EncodingType.Lossless; + + return new() + { + EncodingType = type, + PixelTypeInfo = this.GetPixelTypeInfo() + }; + } + + /// + public static ExrMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + PixelTypeInfo pixelTypeInfo = metadata.PixelTypeInfo; + PixelComponentInfo? info = pixelTypeInfo.ComponentInfo; + PixelColorType colorType = pixelTypeInfo.ColorType; + + int bitsPerComponent = info?.GetMaximumComponentPrecision() + ?? (pixelTypeInfo.BitsPerPixel <= 16 ? 16 : 32); + + int componentCount = info?.ComponentCount ?? 0; + ExrImageDataType imageDataType = colorType switch + { + PixelColorType.Luminance => ExrImageDataType.Gray, + PixelColorType.RGB or PixelColorType.BGR => ExrImageDataType.Rgb, + PixelColorType.RGB | PixelColorType.Alpha + or PixelColorType.BGR | PixelColorType.Alpha + or PixelColorType.Luminance | PixelColorType.Alpha => ExrImageDataType.Rgba, + _ => componentCount switch + { + >= 4 => ExrImageDataType.Rgba, + >= 3 => ExrImageDataType.Rgb, + 1 => ExrImageDataType.Gray, + _ => ExrImageDataType.Unknown, + } + }; + + return new() + { + PixelType = bitsPerComponent <= 16 ? ExrPixelType.Half : ExrPixelType.Float, + ImageDataType = imageDataType, + }; + } + + /// + ExrMetadata IDeepCloneable.DeepClone() => new(this); + + /// + public IDeepCloneable DeepClone() => new ExrMetadata(this); + + /// + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + } + } +} diff --git a/ImageSharp/Formats/Exr/ExrThrowHelper.cs b/ImageSharp/Formats/Exr/ExrThrowHelper.cs new file mode 100644 index 0000000..79f0c30 --- /dev/null +++ b/ImageSharp/Formats/Exr/ExrThrowHelper.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Exr { + /// + /// Cold path optimizations for throwing exr format based exceptions. + /// + internal static class ExrThrowHelper + { + [DoesNotReturn] + public static Exception NotSupportedDecompressor(string compressionType) => throw new NotSupportedException($"Not supported decoder compression method: {compressionType}"); + + [DoesNotReturn] + public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage); + + [DoesNotReturn] + public static void ThrowNotSupportedVersion() => throw new NotSupportedException("Unsupported EXR version"); + + [DoesNotReturn] + public static void ThrowNotSupported(string msg) => throw new NotSupportedException(msg); + + [DoesNotReturn] + public static void ThrowInvalidImageHeader() => throw new InvalidImageContentException("Invalid EXR image header"); + + [DoesNotReturn] + public static void ThrowInvalidImageHeader(string msg) => throw new InvalidImageContentException(msg); + + [DoesNotReturn] + public static Exception NotSupportedCompressor(string compressionType) => throw new NotSupportedException($"Not supported encoder compression method: {compressionType}"); + } +} diff --git a/ImageSharp/Formats/Exr/ExrUtils.cs b/ImageSharp/Formats/Exr/ExrUtils.cs new file mode 100644 index 0000000..a0a3a0c --- /dev/null +++ b/ImageSharp/Formats/Exr/ExrUtils.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Exr.Constants; +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Exr { + internal static class ExrUtils + { + /// + /// Calcualtes the required bytes for a pixel row. + /// + /// The image channels array. + /// The width in pixels of a row. + /// The number of bytes per row. + public static ulong CalculateBytesPerRow(IList channels, uint width) + { + ulong bytesPerRow = 0; + foreach (ExrChannelInfo channelInfo in channels) + { + if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("R", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("G", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("B", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("Y", StringComparison.Ordinal)) + { + if (channelInfo.PixelType == ExrPixelType.Half) + { + bytesPerRow += 2UL * width; + } + else + { + bytesPerRow += 4UL * width; + } + } + } + + return bytesPerRow; + } + + /// + /// Determines how many pixel rows there are in a block. This varies depending on the compression used. + /// + /// The compression used. + /// Pixel rows in a block. + public static uint RowsPerBlock(ExrCompression compression) => compression switch + { + ExrCompression.Zip or ExrCompression.Pxr24 => 16, + ExrCompression.B44 or ExrCompression.B44A or ExrCompression.Piz => 32, + _ => 1, + }; + } +} diff --git a/ImageSharp/Formats/Exr/README.md b/ImageSharp/Formats/Exr/README.md new file mode 100644 index 0000000..c71ab11 --- /dev/null +++ b/ImageSharp/Formats/Exr/README.md @@ -0,0 +1,4 @@ +### Some useful links for documentation about the OpenEXR format: + +- [Technical Introduction](https://openexr.readthedocs.io/en/latest/TechnicalIntroduction.html) +- [OpenExr file layout](https://openexr.readthedocs.io/en/latest/OpenEXRFileLayout.html) \ No newline at end of file diff --git a/ImageSharp/Formats/FormatConnectingFrameMetadata.cs b/ImageSharp/Formats/FormatConnectingFrameMetadata.cs new file mode 100644 index 0000000..37643cd --- /dev/null +++ b/ImageSharp/Formats/FormatConnectingFrameMetadata.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats { + /// + /// A metadata format designed to allow conversion between different image format frames. + /// + public class FormatConnectingFrameMetadata + { + /// + /// Gets information about the encoded pixel type if any. + /// + public PixelTypeInfo? PixelTypeInfo { get; init; } + + /// + /// Gets the frame color table mode. + /// + public FrameColorTableMode ColorTableMode { get; init; } + + /// + /// Gets the duration of the frame. + /// + public TimeSpan Duration { get; init; } + + /// + /// Gets the frame alpha blending mode. + /// + public FrameBlendMode BlendMode { get; init; } + + /// + /// Gets the frame disposal mode. + /// + public FrameDisposalMode DisposalMode { get; init; } + + /// + /// Gets or sets the encoding width.
+ /// Used for formats that require a specific frame size. + ///
+ public int? EncodingWidth { get; set; } + + /// + /// Gets or sets the encoding height.
+ /// Used for formats that require a specific frame size. + ///
+ public int? EncodingHeight { get; set; } + } +} diff --git a/ImageSharp/Formats/FormatConnectingMetadata.cs b/ImageSharp/Formats/FormatConnectingMetadata.cs new file mode 100644 index 0000000..bf208ff --- /dev/null +++ b/ImageSharp/Formats/FormatConnectingMetadata.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats { + /// + /// A metadata format designed to allow conversion between different image formats. + /// + public class FormatConnectingMetadata + { + /// + /// Gets the encoding type. + /// + public EncodingType EncodingType { get; init; } + + /// + /// Gets the quality to use when is . + /// + /// + /// The value is usually between 1 and 100. Defaults to 100. + /// + public int Quality { get; init; } = 100; + + /// + /// Gets information about the encoded pixel type. + /// + public PixelTypeInfo PixelTypeInfo { get; init; } + + /// + /// Gets the shared color table mode. + /// + /// + /// Defaults to . + /// + public FrameColorTableMode ColorTableMode { get; init; } = FrameColorTableMode.Global; + + /// + /// Gets the default background color of the canvas when animating. + /// This color may be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when a frame disposal mode is . + /// + /// + /// Defaults to . + /// + public Color BackgroundColor { get; init; } = Color.Transparent; + + /// + /// Gets the number of times any animation is repeated. + /// + /// + /// 0 means to repeat indefinitely, count is set as repeat n-1 times. Defaults to 1. + /// + public ushort RepeatCount { get; init; } = 1; + + /// + /// Gets a value indicating whether the root frame is shown as part of the animated sequence. + /// + /// + /// Defaults to . + /// + public bool AnimateRootFrame { get; init; } = true; + } +} diff --git a/ImageSharp/Formats/FrameBlendMode.cs b/ImageSharp/Formats/FrameBlendMode.cs new file mode 100644 index 0000000..a6b9337 --- /dev/null +++ b/ImageSharp/Formats/FrameBlendMode.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats { + /// + /// Provides a way to specify how the current frame should be blended with the previous frame in the animation sequence. + /// + public enum FrameBlendMode + { + /// + /// Do not blend. Render the current frame on the canvas by overwriting the rectangle covered by the current frame. + /// + Source = 0, + + /// + /// Blend the current frame with the previous frame in the animation sequence within the rectangle covered + /// by the current frame. + /// If the current has any transparent areas, the corresponding areas of the previous frame will be visible + /// through these transparent regions. + /// + Over = 1 + } +} diff --git a/ImageSharp/Formats/FrameColorTableMode.cs b/ImageSharp/Formats/FrameColorTableMode.cs new file mode 100644 index 0000000..56020df --- /dev/null +++ b/ImageSharp/Formats/FrameColorTableMode.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats { + /// + /// Provides a way to specify how the color table is used by the frame. + /// + public enum FrameColorTableMode + { + /// + /// The frame uses the shared color table specified by the image metadata. + /// + Global, + + /// + /// The frame uses a color table specified by the frame metadata. + /// + Local + } +} diff --git a/ImageSharp/Formats/FrameDisposalMode.cs b/ImageSharp/Formats/FrameDisposalMode.cs new file mode 100644 index 0000000..ad33329 --- /dev/null +++ b/ImageSharp/Formats/FrameDisposalMode.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats { + /// + /// Provides a way to specify how the current frame should be disposed of before rendering the next frame. + /// + public enum FrameDisposalMode + { + /// + /// No disposal specified. + /// The decoder is not required to take any action. + /// + Unspecified = 0, + + /// + /// Do not dispose. The current frame is not disposed of, or in other words, not cleared or altered when moving to + /// the next frame. This means that the next frame is drawn over the current frame, and if the next frame contains + /// transparency, the previous frame will be visible through these transparent areas. + /// + DoNotDispose = 1, + + /// + /// Restore to background color. When transitioning to the next frame, the area occupied by the current frame is + /// filled with the background color specified in the image metadata. + /// This effectively erases the current frame by replacing it with the background color before the next frame is displayed. + /// + RestoreToBackground = 2, + + /// + /// Restore to previous. This method restores the area affected by the current frame to what it was before the + /// current frame was displayed. It essentially "undoes" the current frame, reverting to the state of the image + /// before the frame was displayed, then the next frame is drawn. This is useful for animations where only a small + /// part of the image changes from frame to frame. + /// + RestoreToPrevious = 3 + } +} diff --git a/ImageSharp/Formats/Gif/GifConfigurationModule.cs b/ImageSharp/Formats/Gif/GifConfigurationModule.cs new file mode 100644 index 0000000..271fd42 --- /dev/null +++ b/ImageSharp/Formats/Gif/GifConfigurationModule.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Gif { + /// + /// Registers the image encoders, decoders and mime type detectors for the gif format. + /// + public sealed class GifConfigurationModule : IImageFormatConfigurationModule + { + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetEncoder(GifFormat.Instance, new GifEncoder()); + configuration.ImageFormatsManager.SetDecoder(GifFormat.Instance, GifDecoder.Instance); + configuration.ImageFormatsManager.AddImageFormatDetector(new GifImageFormatDetector()); + } + } +} diff --git a/ImageSharp/Formats/Gif/GifConstants.cs b/ImageSharp/Formats/Gif/GifConstants.cs new file mode 100644 index 0000000..bbabe44 --- /dev/null +++ b/ImageSharp/Formats/Gif/GifConstants.cs @@ -0,0 +1,135 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace SixLabors.ImageSharp.Formats.Gif { + /// + /// Constants that define specific points within a Gif. + /// + internal static class GifConstants + { + /// + /// The file type. + /// + public const string FileType = "GIF"; + + /// + /// The file version. + /// + public const string FileVersion = "89a"; + + /// + /// The extension block introducer !. + /// + public const byte ExtensionIntroducer = 0x21; + + /// + /// The graphic control label. + /// + public const byte GraphicControlLabel = 0xF9; + + /// + /// The application extension label. + /// + public const byte ApplicationExtensionLabel = 0xFF; + + /// + /// The application block size. + /// + public const byte ApplicationBlockSize = 11; + + /// + /// The application identification. + /// + public const string NetscapeApplicationIdentification = "NETSCAPE2.0"; + + /// + /// The Netscape looping application sub block size. + /// + public const byte NetscapeLoopingSubBlockSize = 3; + + /// + /// The comment label. + /// + public const byte CommentLabel = 0xFE; + + /// + /// The maximum length of a comment data sub-block is 255. + /// + public const int MaxCommentSubBlockLength = 255; + + /// + /// The image descriptor label ,. + /// + public const byte ImageDescriptorLabel = 0x2C; + + /// + /// The plain text label. + /// + public const byte PlainTextLabel = 0x01; + + /// + /// The image label introducer ,. + /// + public const byte ImageLabel = 0x2C; + + /// + /// The terminator. + /// + public const byte Terminator = 0; + + /// + /// The end introducer trailer ;. + /// + public const byte EndIntroducer = 0x3B; + + /// + /// The character encoding to use when reading and writing comments - (ASCII 7bit). + /// + public static readonly Encoding Encoding = Encoding.ASCII; + + /// + /// The collection of mimetypes that equate to a Gif. + /// + public static readonly IEnumerable MimeTypes = ["image/gif"]; + + /// + /// The collection of file extensions that equate to a Gif. + /// + public static readonly IEnumerable FileExtensions = ["gif"]; + + /// + /// Gets the ASCII encoded bytes used to identify the GIF file (combining and ). + /// + internal static ReadOnlySpan MagicNumber => + [ + (byte)'G', (byte)'I', (byte)'F', + (byte)'8', (byte)'9', (byte)'a' + ]; + + /// + /// Gets the ASCII encoded application identification bytes (representing ). + /// + internal static ReadOnlySpan NetscapeApplicationIdentificationBytes => + [ + (byte)'N', (byte)'E', (byte)'T', + (byte)'S', (byte)'C', (byte)'A', + (byte)'P', (byte)'E', + (byte)'2', (byte)'.', (byte)'0' + ]; + + /// + /// Gets the ASCII encoded application identification bytes. + /// + internal static ReadOnlySpan XmpApplicationIdentificationBytes => + [ + (byte)'X', (byte)'M', (byte)'P', + (byte)' ', (byte)'D', (byte)'a', + (byte)'t', (byte)'a', + (byte)'X', (byte)'M', (byte)'P' + ]; + } +} diff --git a/ImageSharp/Formats/Gif/GifDecoder.cs b/ImageSharp/Formats/Gif/GifDecoder.cs new file mode 100644 index 0000000..e2dbe45 --- /dev/null +++ b/ImageSharp/Formats/Gif/GifDecoder.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Gif { + /// + /// Decoder for generating an image out of a gif encoded stream. + /// + public sealed class GifDecoder : ImageDecoder + { + private GifDecoder() + { + } + + /// + /// Gets the shared instance. + /// + public static GifDecoder Instance { get; } = new(); + + /// + protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + return new GifDecoderCore(options).Identify(options.Configuration, stream, cancellationToken); + } + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + GifDecoderCore decoder = new(options); + Image image = decoder.Decode(options.Configuration, stream, cancellationToken); + + ScaleToTargetSize(options, image); + + return image; + } + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(options, stream, cancellationToken); + } +} diff --git a/ImageSharp/Formats/Gif/GifDecoderCore.cs b/ImageSharp/Formats/Gif/GifDecoderCore.cs new file mode 100644 index 0000000..1f309c6 --- /dev/null +++ b/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -0,0 +1,1011 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +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 { + /// + /// Performs the gif decoding operation. + /// + internal sealed class GifDecoderCore : ImageDecoderCore + { + /// + /// The temp buffer used to reduce allocations. + /// + private ScratchBuffer buffer; // mutable struct, don't make readonly + + /// + /// The global color table. + /// + private IMemoryOwner? globalColorTable; + + /// + /// The current local color table. + /// + private IMemoryOwner? currentLocalColorTable; + + /// + /// Gets the size in bytes of the current local color table. + /// + private int currentLocalColorTableSize; + + /// + /// The area to restore. + /// + private Rectangle? restoreArea; + + /// + /// The logical screen descriptor. + /// + private GifLogicalScreenDescriptor logicalScreenDescriptor; + + /// + /// The graphics control extension. + /// + private GifGraphicControlExtension graphicsControlExtension; + + /// + /// The image descriptor. + /// + private GifImageDescriptor imageDescriptor; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The maximum number of frames to decode. Inclusive. + /// + private readonly uint maxFrames; + + /// + /// Whether to skip metadata during decode. + /// + private readonly bool skipMetadata; + + /// + /// The abstract metadata. + /// + private ImageMetadata? metadata; + + /// + /// The gif specific metadata. + /// + private GifMetadata? gifMetadata; + + /// + /// The background color index. + /// + private byte backgroundColorIndex; + + /// + /// Initializes a new instance of the class. + /// + /// The decoder options. + public GifDecoderCore(DecoderOptions options) + : base(options) + { + this.configuration = options.Configuration; + this.skipMetadata = options.SkipMetadata; + this.maxFrames = options.MaxFrames; + this.memoryAllocator = this.configuration.MemoryAllocator; + } + + /// + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + { + uint frameCount = 0; + Image? image = null; + ImageFrame? previousFrame = null; + FrameDisposalMode? previousDisposalMode = null; + bool globalColorTableUsed = false; + Color backgroundColor = Color.Transparent; + + try + { + this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream); + + // Loop though the respective gif parts and read the data. + int nextFlag = stream.ReadByte(); + while (nextFlag != GifConstants.Terminator) + { + if (nextFlag == GifConstants.ImageLabel) + { + if (previousFrame != null && ++frameCount == this.maxFrames) + { + break; + } + + globalColorTableUsed |= this.ReadFrame(stream, ref image, ref previousFrame, ref previousDisposalMode, ref backgroundColor); + + // Reset per-frame state. + this.imageDescriptor = default; + this.graphicsControlExtension = default; + } + else if (nextFlag == GifConstants.ExtensionIntroducer) + { + switch (stream.ReadByte()) + { + case GifConstants.GraphicControlLabel: + this.ReadGraphicalControlExtension(stream); + break; + case GifConstants.CommentLabel: + this.ExecuteAncillarySegmentAction(() => this.ReadComments(stream)); + break; + case GifConstants.ApplicationExtensionLabel: + this.ExecuteAncillarySegmentAction(() => this.ReadApplicationExtension(stream)); + break; + case GifConstants.PlainTextLabel: + SkipBlock(stream); // Not supported by any known decoder. + break; + } + } + else if (nextFlag == GifConstants.EndIntroducer) + { + break; + } + + nextFlag = stream.ReadByte(); + if (nextFlag == -1) + { + break; + } + } + + // We cannot always trust the global GIF palette has actually been used. + // https://github.com/SixLabors/ImageSharp/issues/2866 + if (!globalColorTableUsed) + { + this.gifMetadata.ColorTableMode = FrameColorTableMode.Local; + } + } + finally + { + this.globalColorTable?.Dispose(); + this.currentLocalColorTable?.Dispose(); + } + + if (image is null) + { + GifThrowHelper.ThrowNoData(); + } + + return image; + } + + /// + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + uint frameCount = 0; + ImageFrameMetadata? previousFrame = null; + List framesMetadata = []; + bool globalColorTableUsed = false; + + try + { + this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream); + + // Loop though the respective gif parts and read the data. + int nextFlag = stream.ReadByte(); + while (nextFlag != GifConstants.Terminator) + { + if (nextFlag == GifConstants.ImageLabel) + { + if (previousFrame != null && ++frameCount == this.maxFrames) + { + break; + } + + globalColorTableUsed |= this.ReadFrameMetadata(stream, framesMetadata, ref previousFrame); + + // Reset per-frame state. + this.imageDescriptor = default; + this.graphicsControlExtension = default; + } + else if (nextFlag == GifConstants.ExtensionIntroducer) + { + switch (stream.ReadByte()) + { + case GifConstants.GraphicControlLabel: + this.ReadGraphicalControlExtension(stream); + break; + case GifConstants.CommentLabel: + this.ExecuteAncillarySegmentAction(() => this.ReadComments(stream)); + break; + case GifConstants.ApplicationExtensionLabel: + this.ExecuteAncillarySegmentAction(() => this.ReadApplicationExtension(stream)); + break; + case GifConstants.PlainTextLabel: + SkipBlock(stream); // Not supported by any known decoder. + break; + } + } + else if (nextFlag == GifConstants.EndIntroducer) + { + break; + } + + nextFlag = stream.ReadByte(); + if (nextFlag == -1) + { + break; + } + } + + // We cannot always trust the global GIF palette has actually been used. + // https://github.com/SixLabors/ImageSharp/issues/2866 + if (!globalColorTableUsed) + { + this.gifMetadata.ColorTableMode = FrameColorTableMode.Local; + } + } + finally + { + this.globalColorTable?.Dispose(); + this.currentLocalColorTable?.Dispose(); + } + + if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0) + { + GifThrowHelper.ThrowNoHeader(); + } + + // Ignoring a malformed ancillary extension must not let identify succeed for a file + // that never contained any readable image frame data. + if (previousFrame is null) + { + GifThrowHelper.ThrowNoData(); + } + + return new ImageInfo( + new Size(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height), + this.metadata, + framesMetadata); + } + + /// + /// Reads the graphic control extension. + /// + /// The containing image data. + private void ReadGraphicalControlExtension(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer.Span, 0, 6); + if (bytesRead != 6) + { + GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the graphic control extension"); + } + + this.graphicsControlExtension = GifGraphicControlExtension.Parse(this.buffer.Span); + } + + /// + /// Reads the image descriptor. + /// + /// The containing image data. + private void ReadImageDescriptor(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer.Span, 0, 9); + if (bytesRead != 9) + { + GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the image descriptor"); + } + + this.imageDescriptor = GifImageDescriptor.Parse(this.buffer.Span); + if (this.imageDescriptor.Height == 0 || this.imageDescriptor.Width == 0) + { + GifThrowHelper.ThrowInvalidImageContentException("Width or height should not be 0"); + } + + this.Dimensions = new Size(this.imageDescriptor.Width, this.imageDescriptor.Height); + } + + /// + /// Reads the logical screen descriptor. + /// + /// The containing image data. + private void ReadLogicalScreenDescriptor(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer.Span, 0, 7); + if (bytesRead != 7) + { + GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the logical screen descriptor"); + } + + this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer.Span); + } + + /// + /// Reads the application extension block parsing any animation or XMP information + /// if present. + /// + /// The containing image data. + private void ReadApplicationExtension(BufferedReadStream stream) + { + int appLength = stream.ReadByte(); + if (appLength == -1) + { + GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension"); + } + + if (appLength != GifConstants.ApplicationBlockSize) + { + this.ThrowOrIgnoreNonStrictSegmentError($"Gif application extension length '{appLength}' is invalid"); + SkipBlock(stream, appLength); + return; + } + + // If the length is 11 then it's a valid extension and most likely + // a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this. + long position = stream.Position; + int bytesRead = stream.Read(this.buffer.Span, 0, GifConstants.ApplicationBlockSize); + if (bytesRead != GifConstants.ApplicationBlockSize) + { + GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension"); + } + + bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes); + if (isXmp) + { + this.ReadXmpApplicationExtension(stream, position, appLength); + return; + } + + int subBlockSize = stream.ReadByte(); + if (subBlockSize == -1) + { + GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension"); + } + + // TODO: There's also a NETSCAPE buffer extension. + // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension + if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) + { + this.ReadNetscapeApplicationExtension(stream); + return; + } + + // Could be something else not supported yet. + // Skip the subblock and terminator. + SkipBlock(stream, subBlockSize); + } + + /// + /// Reads the GIF XMP application extension. + /// + /// The containing image data. + /// The stream position where the application identifier begins. + /// The application block length. + private void ReadXmpApplicationExtension(BufferedReadStream stream, long applicationPosition, int appLength) + { + if (this.skipMetadata) + { + stream.Position = applicationPosition; + SkipBlock(stream, appLength); + return; + } + + bool completed = false; + this.ExecuteAncillarySegmentAction( + () => + { + this.ReadXmpApplicationExtensionData(stream, applicationPosition, appLength); + completed = true; + }); + + if (!completed) + { + stream.Position = applicationPosition; + SkipBlock(stream, appLength); + } + } + + /// + /// Reads the GIF XMP application extension data. + /// + /// The containing image data. + /// The stream position where the application identifier begins. + /// The application block length. + private void ReadXmpApplicationExtensionData(BufferedReadStream stream, long applicationPosition, int appLength) + { + GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator); + if (extension.Data.Length > 0) + { + this.metadata!.XmpProfile = new XmpProfile(extension.Data); + return; + } + + stream.Position = applicationPosition; + SkipBlock(stream, appLength); + } + + /// + /// Reads the GIF NETSCAPE looping application extension. + /// + /// The containing image data. + private void ReadNetscapeApplicationExtension(BufferedReadStream stream) => + this.ExecuteAncillarySegmentAction(() => this.ReadNetscapeApplicationExtensionData(stream)); + + /// + /// Reads the GIF NETSCAPE looping application extension data. + /// + /// The containing image data. + private void ReadNetscapeApplicationExtensionData(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize); + if (bytesRead != GifConstants.NetscapeLoopingSubBlockSize) + { + throw new InvalidImageContentException("Unexpected end of stream while reading gif application extension"); + } + + this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span[1..]).RepeatCount; + + int terminator = stream.ReadByte(); + if (terminator == -1) + { + throw new InvalidImageContentException("Unexpected end of stream while reading gif application extension"); + } + } + + /// + /// Skips over a block or reads its terminator. + /// + /// The containing image data. + /// The length of the block to skip. + private static void SkipBlock(BufferedReadStream stream, int blockSize = 0) + { + if (blockSize > 0) + { + stream.Skip(blockSize); + } + + int flag; + + while ((flag = stream.ReadByte()) > 0) + { + stream.Skip(flag); + } + } + + /// + /// Reads the gif comments. + /// + /// The containing image data. + private void ReadComments(BufferedReadStream stream) + { + int length; + + StringBuilder stringBuilder = new(); + while ((length = stream.ReadByte()) != 0) + { + if (length > GifConstants.MaxCommentSubBlockLength) + { + GifThrowHelper.ThrowInvalidImageContentException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentSubBlockLength}' of a comment data block"); + } + + if (length == -1) + { + GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif comment"); + } + + if (this.skipMetadata) + { + stream.Seek(length, SeekOrigin.Current); + continue; + } + + using IMemoryOwner commentsBuffer = this.memoryAllocator.Allocate(length); + Span commentsSpan = commentsBuffer.GetSpan(); + + int bytesRead = stream.Read(commentsSpan); + if (bytesRead != length) + { + GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif comment"); + } + + string commentPart = GifConstants.Encoding.GetString(commentsSpan); + stringBuilder.Append(commentPart); + } + + if (stringBuilder.Length > 0) + { + this.gifMetadata!.Comments.Add(stringBuilder.ToString()); + } + } + + /// + /// Reads an individual gif frame. + /// + /// The pixel format. + /// The containing image data. + /// The image to decode the information to. + /// The previous frame. + /// The previous frame disposal mode. + /// The background color. + /// Whether the frame has a global color table. + private bool ReadFrame( + BufferedReadStream stream, + ref Image? image, + ref ImageFrame? previousFrame, + ref FrameDisposalMode? previousDisposalMode, + ref Color backgroundColor) + where TPixel : unmanaged, IPixel + { + this.ReadImageDescriptor(stream); + + // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table. + bool hasLocalColorTable = this.imageDescriptor.LocalColorTableFlag; + Span rawColorTable = default; + if (hasLocalColorTable) + { + // Read and store the local color table. We allocate the maximum possible size and slice to match. + int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3; + this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate(768, AllocationOptions.Clean); + stream.Read(this.currentLocalColorTable.GetSpan()[..length]); + rawColorTable = this.currentLocalColorTable.GetSpan()[..length]; + } + else if (this.globalColorTable != null) + { + rawColorTable = this.globalColorTable.GetSpan(); + } + + ReadOnlySpan colorTable = MemoryMarshal.Cast(rawColorTable); + + // First frame + if (image is null) + { + if (this.backgroundColorIndex < colorTable.Length) + { + backgroundColor = Color.FromPixel(colorTable[this.backgroundColorIndex]); + } + else + { + backgroundColor = Color.Transparent; + } + + // We zero the alpha only when this frame declares transparency so that + // frames with a transparent index coalesce over a transparent canvas rather than + // baking the LSD background as a matte. When the flag is not set, this frame will + // write an opaque color for every addressed pixel; keeping the LSD background + // opaque here allows ReadFrameColors to show that background in uncovered areas + // for non-transparent GIFs that rely on it. We still do not prefill the canvas here. + if (this.graphicsControlExtension.TransparencyFlag) + { + backgroundColor = backgroundColor.WithAlpha(0); + } + } + + this.ReadFrameColors(stream, ref image, ref previousFrame, ref previousDisposalMode, colorTable, backgroundColor.ToPixel()); + + // Update from newly decoded frame. + FrameDisposalMode disposalMethod = this.graphicsControlExtension.DisposalMethod; + if (disposalMethod != FrameDisposalMode.RestoreToPrevious) + { + // Do not key this on the transparency flag. Disposal handling is determined by + // the previous frame's disposal, not by whether the current frame declares a transparent + // index. For editing we carry a transparent background so that RestoreToBackground clears + // remove pixels to transparent rather than painting an opaque matte. The LSD background + // color is display advice and should be used only when explicitly flattening or when + // rendering with an option to honor it. + backgroundColor = (this.backgroundColorIndex < colorTable.Length) + ? Color.FromPixel(colorTable[this.backgroundColorIndex]).WithAlpha(0) + : Color.Transparent; + } + + // Skip any remaining blocks + SkipBlock(stream); + + return !hasLocalColorTable; + } + + /// + /// Reads the frames colors, mapping indices to colors. + /// + /// The pixel format. + /// The containing image data. + /// The image to decode the information to. + /// The previous frame. + /// The previous frame disposal mode. + /// The color table containing the available colors. + /// The background color pixel. + private void ReadFrameColors( + BufferedReadStream stream, + ref Image? image, + ref ImageFrame? previousFrame, + ref FrameDisposalMode? previousDisposalMode, + ReadOnlySpan colorTable, + TPixel backgroundPixel) + where TPixel : unmanaged, IPixel + { + GifImageDescriptor descriptor = this.imageDescriptor; + int imageWidth = this.logicalScreenDescriptor.Width; + int imageHeight = this.logicalScreenDescriptor.Height; + bool useTransparency = this.graphicsControlExtension.TransparencyFlag; + bool useBackground; + FrameDisposalMode disposalMethod = this.graphicsControlExtension.DisposalMethod; + ImageFrame currentFrame; + ImageFrame? restoreFrame = null; + + if (previousFrame is null && previousDisposalMode is null) + { + // First frame: prefill with LSD background iff a GCT exists (policy: HonorBackgroundColor). + useBackground = + this.logicalScreenDescriptor.GlobalColorTableFlag + && disposalMethod == FrameDisposalMode.RestoreToBackground; + + image = useBackground + ? new Image(this.configuration, imageWidth, imageHeight, backgroundPixel, this.metadata) + : new Image(this.configuration, imageWidth, imageHeight, this.metadata); + + this.SetFrameMetadata(image.Frames.RootFrame.Metadata); + currentFrame = image.Frames.RootFrame; + } + else + { + // Subsequent frames: use LSD background iff previous disposal was RestoreToBackground and a GCT exists. + useBackground = + this.logicalScreenDescriptor.GlobalColorTableFlag + && previousDisposalMode == FrameDisposalMode.RestoreToBackground; + + if (previousFrame != null) + { + currentFrame = image!.Frames.AddFrame(previousFrame); + } + else if (useBackground) + { + currentFrame = image!.Frames.CreateFrame(backgroundPixel); + } + else + { + currentFrame = image!.Frames.CreateFrame(); + } + + this.SetFrameMetadata(currentFrame.Metadata); + + if (this.graphicsControlExtension.DisposalMethod == FrameDisposalMode.RestoreToPrevious) + { + restoreFrame = previousFrame; + } + + if (previousDisposalMode == FrameDisposalMode.RestoreToBackground) + { + this.RestoreToBackground(currentFrame, backgroundPixel, !useBackground); + } + } + + if (this.graphicsControlExtension.DisposalMethod == FrameDisposalMode.RestoreToPrevious) + { + previousFrame = restoreFrame; + } + else + { + previousFrame = currentFrame; + } + + previousDisposalMode = disposalMethod; + + if (disposalMethod == FrameDisposalMode.RestoreToBackground) + { + this.restoreArea = Rectangle.Intersect(image.Bounds, new Rectangle(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height)); + } + + if (colorTable.Length == 0) + { + return; + } + + int interlacePass = 0; // The interlace pass + int interlaceIncrement = 8; // The interlacing line increment + int interlaceY = 0; // The current interlaced line + int descriptorTop = descriptor.Top; + int descriptorBottom = descriptorTop + descriptor.Height; + int descriptorLeft = descriptor.Left; + int descriptorRight = descriptorLeft + descriptor.Width; + byte transIndex = this.graphicsControlExtension.TransparencyIndex; + int colorTableMaxIdx = colorTable.Length - 1; + + // For a properly encoded gif the descriptor dimensions will never exceed the logical screen dimensions. + // However we have images that exceed this that can be decoded by other libraries. #1530 + using IMemoryOwner indicesRowOwner = this.memoryAllocator.Allocate(descriptor.Width); + Span indicesRow = indicesRowOwner.Memory.Span; + + int minCodeSize = stream.ReadByte(); + if (LzwDecoder.IsValidMinCodeSize(minCodeSize)) + { + using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream, minCodeSize); + + for (int y = descriptorTop; y < descriptorBottom && y < imageHeight; y++) + { + // Check if this image is interlaced. + int writeY; // the target y offset to write to + if (descriptor.InterlaceFlag) + { + // If so then we read lines at predetermined offsets. + // When an entire image height worth of offset lines has been read we consider this a pass. + // With each pass the number of offset lines changes and the starting line changes. + if (interlaceY >= descriptor.Height) + { + interlacePass++; + switch (interlacePass) + { + case 1: + interlaceY = 4; + break; + case 2: + interlaceY = 2; + interlaceIncrement = 4; + break; + case 3: + interlaceY = 1; + interlaceIncrement = 2; + break; + } + } + + writeY = Math.Min(interlaceY + descriptor.Top, image.Height); + interlaceY += interlaceIncrement; + } + else + { + writeY = y; + } + + lzwDecoder.DecodePixelRow(indicesRow); + + // #403 The left + width value can be larger than the image width + int maxX = Math.Min(descriptorRight, imageWidth); + Span row = currentFrame.PixelBuffer.DangerousGetRowSpan(writeY); + + // Take the descriptorLeft..maxX slice of the row, so the loop can be simplified. + row = row[descriptorLeft..maxX]; + + if (!useTransparency) + { + for (int x = 0; x < row.Length; x++) + { + int index = indicesRow[x]; + + // Treat any out of bounds values as background. + if (index > colorTableMaxIdx) + { + index = Numerics.Clamp(index, 0, colorTableMaxIdx); + } + + row[x] = TPixel.FromRgb24(colorTable[index]); + } + } + else + { + for (int x = 0; x < row.Length; x++) + { + int index = indicesRow[x]; + + // Treat any out of bounds values as transparent. + // We explicitly set the pixel to transparent rather than alter the inbound + // color palette. + if (index > colorTableMaxIdx || index == transIndex) + { + continue; + } + + row[x] = TPixel.FromRgb24(colorTable[index]); + } + } + } + } + } + + /// + /// Reads the frames metadata. + /// + /// The containing image data. + /// The collection of frame metadata. + /// The previous frame metadata. + /// Whether the frame has a global color table. + private bool ReadFrameMetadata(BufferedReadStream stream, List frameMetadata, ref ImageFrameMetadata? previousFrame) + { + this.ReadImageDescriptor(stream); + + // Skip the color table for this frame if local. + if (this.imageDescriptor.LocalColorTableFlag) + { + // Read and store the local color table. We allocate the maximum possible size and slice to match. + int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3; + this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate(768, AllocationOptions.Clean); + stream.Read(this.currentLocalColorTable.GetSpan()[..length]); + } + else + { + this.currentLocalColorTable = null; + this.currentLocalColorTableSize = 0; + } + + // Skip the frame indices. Pixels length + mincode size. + // The gif format does not tell us the length of the compressed data beforehand. + int minCodeSize = stream.ReadByte(); + if (LzwDecoder.IsValidMinCodeSize(minCodeSize)) + { + using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream, minCodeSize); + lzwDecoder.SkipIndices(this.imageDescriptor.Width * this.imageDescriptor.Height); + } + + ImageFrameMetadata currentFrame = new(); + frameMetadata.Add(currentFrame); + this.SetFrameMetadata(currentFrame); + previousFrame = currentFrame; + + // Skip any remaining blocks + SkipBlock(stream); + + return !this.imageDescriptor.LocalColorTableFlag; + } + + /// + /// Restores the current frame area to the background. + /// + /// The pixel format. + /// The frame. + /// The background color. + /// Whether the background is transparent. + private void RestoreToBackground(ImageFrame frame, TPixel background, bool transparent) + where TPixel : unmanaged, IPixel + { + if (this.restoreArea is null) + { + return; + } + + Rectangle interest = Rectangle.Intersect(frame.Bounds, this.restoreArea.Value); + Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(interest); + if (transparent) + { + pixelRegion.Clear(); + } + else + { + pixelRegion.Fill(background); + } + + this.restoreArea = null; + } + + /// + /// Sets the metadata for the image frame. + /// + /// The metadata. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetFrameMetadata(ImageFrameMetadata metadata) + { + // Frames can either use the global table or their own local table. + if (this.logicalScreenDescriptor.GlobalColorTableFlag + && this.logicalScreenDescriptor.GlobalColorTableSize > 0) + { + GifFrameMetadata gifMeta = metadata.GetGifMetadata(); + gifMeta.ColorTableMode = FrameColorTableMode.Global; + } + + if (this.imageDescriptor.LocalColorTableFlag + && this.imageDescriptor.LocalColorTableSize > 0) + { + GifFrameMetadata gifMeta = metadata.GetGifMetadata(); + gifMeta.ColorTableMode = FrameColorTableMode.Local; + + Color[] colorTable = new Color[this.imageDescriptor.LocalColorTableSize]; + ReadOnlySpan rgbTable = MemoryMarshal.Cast(this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize]); + Color.FromPixel(rgbTable, colorTable); + + gifMeta.LocalColorTable = colorTable; + } + + // Graphics control extensions is optional. + if (this.graphicsControlExtension != default) + { + GifFrameMetadata gifMeta = metadata.GetGifMetadata(); + gifMeta.HasTransparency = this.graphicsControlExtension.TransparencyFlag; + gifMeta.TransparencyIndex = this.graphicsControlExtension.TransparencyIndex; + gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime; + gifMeta.DisposalMode = this.graphicsControlExtension.DisposalMethod; + } + } + + /// + /// Reads the logical screen descriptor and global color table blocks + /// + /// The stream containing image data. + [MemberNotNull(nameof(metadata))] + [MemberNotNull(nameof(gifMetadata))] + private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream stream) + { + // Skip the identifier + stream.Skip(6); + this.ReadLogicalScreenDescriptor(stream); + + ImageMetadata meta = new(); + + // The Pixel Aspect Ratio is defined to be the quotient of the pixel's + // width over its height. The value range in this field allows + // specification of the widest pixel of 4:1 to the tallest pixel of + // 1:4 in increments of 1/64th. + // + // Values : 0 - No aspect ratio information is given. + // 1..255 - Value used in the computation. + // + // Aspect Ratio = (Pixel Aspect Ratio + 15) / 64 + if (this.logicalScreenDescriptor.PixelAspectRatio > 0) + { + meta.ResolutionUnits = PixelResolutionUnit.AspectRatio; + float ratio = (this.logicalScreenDescriptor.PixelAspectRatio + 15) / 64F; + + if (ratio > 1) + { + meta.HorizontalResolution = ratio; + meta.VerticalResolution = 1; + } + else + { + meta.VerticalResolution = 1 / ratio; + meta.HorizontalResolution = 1; + } + } + + this.metadata = meta; + this.gifMetadata = meta.GetGifMetadata(); + this.gifMetadata.ColorTableMode = this.logicalScreenDescriptor.GlobalColorTableFlag + ? FrameColorTableMode.Global + : FrameColorTableMode.Local; + + if (this.logicalScreenDescriptor.GlobalColorTableFlag) + { + int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3; + if (globalColorTableLength > 0) + { + this.globalColorTable = this.memoryAllocator.Allocate(globalColorTableLength, AllocationOptions.Clean); + + // Read the global color table data from the stream and preserve it in the gif metadata + Span globalColorTableSpan = this.globalColorTable.GetSpan(); + stream.Read(globalColorTableSpan); + + Color[] colorTable = new Color[this.logicalScreenDescriptor.GlobalColorTableSize]; + ReadOnlySpan rgbTable = MemoryMarshal.Cast(globalColorTableSpan); + Color.FromPixel(rgbTable, colorTable); + + this.gifMetadata.GlobalColorTable = colorTable; + } + } + + byte index = this.logicalScreenDescriptor.BackgroundColorIndex; + this.backgroundColorIndex = index; + ReadOnlyMemory? globalColorTable = this.gifMetadata.GlobalColorTable; + if (globalColorTable.HasValue && index < globalColorTable.Value.Length) + { + this.gifMetadata.BackgroundColor = globalColorTable.Value.Span[index]; + } + } + + private unsafe struct ScratchBuffer + { + private const int Size = 16; + private fixed byte scratch[Size]; + + public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); + } + } +} diff --git a/ImageSharp/Formats/Gif/GifEncoder.cs b/ImageSharp/Formats/Gif/GifEncoder.cs new file mode 100644 index 0000000..82e83b6 --- /dev/null +++ b/ImageSharp/Formats/Gif/GifEncoder.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Gif { + /// + /// Image encoder for writing image data to a stream in gif format. + /// + public sealed class GifEncoder : QuantizingAnimatedImageEncoder + { + /// + /// Gets the color table mode: Global or local. + /// + public FrameColorTableMode? ColorTableMode { get; init; } + + /// + protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) + { + GifEncoderCore encoder = new(image.Configuration, this); + encoder.Encode(image, stream, cancellationToken); + } + } +} diff --git a/ImageSharp/Formats/Gif/GifEncoderCore.cs b/ImageSharp/Formats/Gif/GifEncoderCore.cs new file mode 100644 index 0000000..9dde2f6 --- /dev/null +++ b/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -0,0 +1,845 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.IO; +using System.Numerics; +using System.Runtime.CompilerServices; +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; + +namespace SixLabors.ImageSharp.Formats.Gif { + /// + /// Implements the GIF encoding protocol. + /// + internal sealed class GifEncoderCore + { + private readonly GifEncoder encoder; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// Configuration bound to the encoding operation. + /// + private readonly Configuration configuration; + + /// + /// Whether to skip metadata during encode. + /// + private readonly bool skipMetadata; + + /// + /// The color table mode: Global or local. + /// + private FrameColorTableMode? colorTableMode; + + /// + /// The pixel sampling strategy for global quantization. + /// + private readonly IPixelSamplingStrategy pixelSamplingStrategy; + + /// + /// The number of times any animation is repeated. + /// + private readonly ushort? repeatCount; + + /// + /// The transparent color mode. + /// + private readonly TransparentColorMode transparentColorMode; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behavior or extending the library. + /// The encoder with options. + public GifEncoderCore(Configuration configuration, GifEncoder encoder) + { + this.configuration = configuration; + this.memoryAllocator = configuration.MemoryAllocator; + this.encoder = encoder; + this.skipMetadata = encoder.SkipMetadata; + this.colorTableMode = encoder.ColorTableMode; + this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; + this.repeatCount = encoder.RepeatCount; + this.transparentColorMode = encoder.TransparentColorMode; + } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + GifMetadata gifMetadata = image.Metadata.CloneGifMetadata(); + this.colorTableMode ??= gifMetadata.ColorTableMode; + bool useGlobalTable = this.colorTableMode == FrameColorTableMode.Global; + bool useGlobalTableForFirstFrame = useGlobalTable; + + // Work out if there is an explicit transparent index set for the frame. We use that to ensure the + // correct value is set for the background index when quantizing. + GifFrameMetadata frameMetadata = GetGifFrameMetadata(image.Frames.RootFrame, -1); + if (frameMetadata.ColorTableMode == FrameColorTableMode.Local) + { + useGlobalTableForFirstFrame = false; + } + + // Quantize the first image frame returning a palette. + IndexedImageFrame? quantized = null; + IQuantizer? globalQuantizer = this.encoder.Quantizer; + TransparentColorMode mode = this.transparentColorMode; + + // Create a new quantizer options instance augmenting the transparent color mode to match the encoder. + QuantizerOptions options = (this.encoder.Quantizer?.Options ?? new QuantizerOptions()).DeepClone(o => + { + o.TransparentColorMode = mode; + + // Animated GIF delta frames can use one padded color-table index as transparency. + // Express that through MaxColors so custom quantizers receive the same budget. + if (image.Frames.Count > 1 && o.MaxColors == QuantizerConstants.MaxColors) + { + o.MaxColors = QuantizerConstants.MaxColors - 1; + } + }); + + if (globalQuantizer is null) + { + // Is this a gif with color information. If so use that, otherwise use the adaptive hexadecatree quantizer. + if (gifMetadata.ColorTableMode == FrameColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0) + { + int ti = GetTransparentIndex(quantized, frameMetadata); + if (ti >= 0 || gifMetadata.GlobalColorTable.Value.Length < 256) + { + // We avoid dithering by default to preserve the original colors. + globalQuantizer = new PaletteQuantizer( + gifMetadata.GlobalColorTable.Value, + options.DeepClone(o => o.Dither = null), + ti, + Color.Transparent); + } + else + { + globalQuantizer = new HexadecatreeQuantizer(options); + } + } + else + { + globalQuantizer = new HexadecatreeQuantizer(options); + } + } + + // Quantize the first frame. + IPixelSamplingStrategy strategy = this.pixelSamplingStrategy; + + ImageFrame encodingFrame = image.Frames.RootFrame; + + // This color is encoded as the logical-screen background index and is also + // used when de-duplicating frames that restore to the GIF background. + Color backgroundColor = this.encoder.BackgroundColor ?? gifMetadata.BackgroundColor ?? Color.Transparent; + byte backgroundIndex = 0; + if (useGlobalTableForFirstFrame) + { + using IQuantizer firstFrameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer(this.configuration, options); + if (useGlobalTable) + { + firstFrameQuantizer.BuildPalette(strategy, image); + } + else + { + firstFrameQuantizer.BuildPalette(strategy, encodingFrame); + } + + quantized = firstFrameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds); + TPixel backgroundPixel = backgroundColor.ToPixel(); + backgroundIndex = firstFrameQuantizer.GetQuantizedColor(backgroundPixel, out _); + } + else + { + quantized = this.QuantizeFrameAndUpdateMetadata( + encodingFrame, + globalQuantizer, + default, + encodingFrame.Bounds, + frameMetadata, + true, + false, + frameMetadata.HasTransparency ? frameMetadata.TransparencyIndex : -1, + Color.Transparent); + } + + // Write the header. + WriteHeader(stream); + + // Write the LSD. + int transparencyIndex = GetTransparentIndex(quantized, null); + if (transparencyIndex >= 0) + { + frameMetadata.HasTransparency = true; + frameMetadata.TransparencyIndex = ClampIndex(transparencyIndex); + } + + // Get the number of bits. + int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); + this.WriteLogicalScreenDescriptor(image.Metadata, image.Width, image.Height, backgroundIndex, useGlobalTable, bitDepth, stream); + + if (useGlobalTable) + { + this.WriteColorTable(quantized, bitDepth, stream); + } + + if (!this.skipMetadata) + { + // Write the comments. + this.WriteComments(gifMetadata, stream); + + // Write application extensions. + XmpProfile? xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; + this.WriteApplicationExtensions(stream, image.Frames.Count, this.repeatCount ?? gifMetadata.RepeatCount, xmpProfile); + } + + // If the token is cancelled during encoding of frames we must ensure the + // quantized frame is disposed. + try + { + this.EncodeFirstFrame(stream, frameMetadata, quantized, cancellationToken); + + // Capture the global palette for reuse on subsequent frames and cleanup the quantized frame. + TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray(); + + if (image.Frames.Count > 1) + { + using PaletteQuantizer globalFrameQuantizer = new(this.configuration, globalQuantizer.Options, quantized.Palette.ToArray()); + this.EncodeAdditionalFrames( + stream, + image, + globalQuantizer, + globalFrameQuantizer, + backgroundColor, + transparencyIndex, + frameMetadata.DisposalMode, + cancellationToken); + } + } + finally + { + stream.WriteByte(GifConstants.EndIntroducer); + + quantized?.Dispose(); + } + } + + private static GifFrameMetadata GetGifFrameMetadata(ImageFrame frame, int transparencyIndex) + where TPixel : unmanaged, IPixel + { + GifFrameMetadata metadata = frame.Metadata.CloneGifMetadata(); + if (metadata.ColorTableMode == FrameColorTableMode.Global && transparencyIndex > -1) + { + metadata.HasTransparency = true; + metadata.TransparencyIndex = ClampIndex(transparencyIndex); + } + + return metadata; + } + + private void EncodeAdditionalFrames( + Stream stream, + Image image, + IQuantizer globalQuantizer, + PaletteQuantizer globalFrameQuantizer, + Color backgroundColor, + int globalTransparencyIndex, + FrameDisposalMode previousDisposalMode, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + // Store the first frame as a reference for de-duplication comparison. + ImageFrame previousFrame = image.Frames.RootFrame; + + // This frame is reused to store de-duplicated pixel buffers. + using ImageFrame encodingFrame = new(previousFrame.Configuration, previousFrame.Size); + + for (int i = 1; i < image.Frames.Count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Gather the metadata for this frame. + ImageFrame currentFrame = image.Frames[i]; + ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; + GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex); + bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local); + + this.EncodeAdditionalFrame( + stream, + previousFrame, + currentFrame, + nextFrame, + encodingFrame, + globalQuantizer, + globalFrameQuantizer, + useLocal, + gifMetadata, + backgroundColor, + previousDisposalMode); + + previousFrame = currentFrame; + previousDisposalMode = gifMetadata.DisposalMode; + } + } + + private void EncodeFirstFrame( + Stream stream, + GifFrameMetadata metadata, + IndexedImageFrame quantized, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + cancellationToken.ThrowIfCancellationRequested(); + + this.WriteGraphicalControlExtension(metadata, stream); + + Buffer2D indices = ((IPixelSource)quantized).PixelBuffer; + Rectangle interest = indices.Bounds; + bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (metadata.ColorTableMode == FrameColorTableMode.Local); + int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); + + this.WriteImageDescriptor(interest, useLocal, bitDepth, stream); + + if (useLocal) + { + this.WriteColorTable(quantized, bitDepth, stream); + } + + this.WriteImageData(indices, stream, quantized.Palette.Length, metadata.TransparencyIndex); + } + + private void EncodeAdditionalFrame( + Stream stream, + ImageFrame previousFrame, + ImageFrame currentFrame, + ImageFrame? nextFrame, + ImageFrame encodingFrame, + IQuantizer globalQuantizer, + PaletteQuantizer globalFrameQuantizer, + bool useLocal, + GifFrameMetadata metadata, + Color backgroundColor, + FrameDisposalMode previousDisposalMode) + where TPixel : unmanaged, IPixel + { + // Capture any explicit transparency index from the metadata. + // We use it to determine the value to use to replace duplicate pixels. + bool useTransparency = metadata.HasTransparency; + int transparencyIndex = useTransparency ? metadata.TransparencyIndex : -1; + + ImageFrame? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground + ? null : + previousFrame; + + // If the previous frame has a value we need to check the disposal mode of that frame + // to determine if we should use the background color to fill the encoding frame + // when de-duplicating. + FrameDisposalMode disposalMode = previous is null ? + metadata.DisposalMode : + previous.Metadata.GetGifMetadata().DisposalMode; + + Color background = !useTransparency && disposalMode == FrameDisposalMode.RestoreToBackground + ? backgroundColor + : Color.Transparent; + + // Deduplicate and quantize the frame capturing only required parts. + // Pixels matching the previous frame are replaced with the transparent placeholder. + // When the entire frame matches there is no captured difference, but every pixel is + // still a placeholder, so a transparent index is always required for additional frames. + (_, Rectangle bounds) = + AnimationUtilities.DeDuplicatePixels( + this.configuration, + previous, + currentFrame, + nextFrame, + encodingFrame, + background, + true); + + using IndexedImageFrame quantized = this.QuantizeFrameAndUpdateMetadata( + encodingFrame, + globalQuantizer, + globalFrameQuantizer, + bounds, + metadata, + useLocal, + true, + transparencyIndex, + background); + + this.WriteGraphicalControlExtension(metadata, stream); + + int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); + this.WriteImageDescriptor(bounds, useLocal, bitDepth, stream); + + if (useLocal) + { + this.WriteColorTable(quantized, bitDepth, stream); + } + + Buffer2D indices = ((IPixelSource)quantized).PixelBuffer; + this.WriteImageData(indices, stream, quantized.Palette.Length, metadata.TransparencyIndex); + } + + private IndexedImageFrame QuantizeFrameAndUpdateMetadata( + ImageFrame encodingFrame, + IQuantizer globalQuantizer, + PaletteQuantizer globalFrameQuantizer, + Rectangle bounds, + GifFrameMetadata metadata, + bool useLocal, + bool requiresTransparency, + int transparencyIndex, + Color transparentColor) + where TPixel : unmanaged, IPixel + { + IndexedImageFrame quantized; + if (useLocal) + { + // Reassign using the current frame and details. + if (metadata.LocalColorTable?.Length > 0) + { + // We can use the color data from the decoded metadata here. + // We avoid dithering by default to preserve the original colors. + ReadOnlyMemory palette = metadata.LocalColorTable.Value; + if (requiresTransparency && !metadata.HasTransparency) + { + // The frame was de-duplicated against the previous frame, replacing matching + // pixels with the transparent placeholder, but the metadata does not yet carry + // a transparent index. Reserve one so those pixels encode as transparent. + metadata.HasTransparency = true; + + if (palette.Length < 256) + { + // We can use the existing palette and set the transparent index as the length. + // decoders will ignore this value. + transparencyIndex = palette.Length; + metadata.TransparencyIndex = ClampIndex(transparencyIndex); + + QuantizerOptions options = globalQuantizer.Options.DeepClone(o => + { + o.MaxColors = palette.Length; + o.Dither = null; + }); + PaletteQuantizer quantizer = new(palette, options, transparencyIndex, transparentColor); + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration); + quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds); + } + else + { + // We must quantize the frame to generate a local color table. + using IQuantizer frameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer(this.configuration); + quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds); + + // The transparency index derived by the quantizer will differ from the index + // within the metadata. We need to update the metadata to reflect this. + int derivedTransparencyIndex = GetTransparentIndex(quantized, null); + metadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex); + } + } + else + { + // Just use the local palette. + QuantizerOptions paletteOptions = globalQuantizer.Options.DeepClone(o => + { + o.MaxColors = palette.Length; + o.Dither = null; + }); + PaletteQuantizer quantizer = new(palette, paletteOptions, transparencyIndex, transparentColor); + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options); + quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds); + } + } + else + { + // We must quantize the frame to generate a local color table. + using IQuantizer frameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer(this.configuration); + quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds); + + // The transparency index derived by the quantizer might differ from the index + // within the metadata. We need to update the metadata to reflect this. + int derivedTransparencyIndex = GetTransparentIndex(quantized, null); + if (derivedTransparencyIndex < 0) + { + // If no index is found set to the palette length, this trick allows us to fake transparency without an explicit index. + derivedTransparencyIndex = quantized.Palette.Length; + } + + metadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex); + + if (requiresTransparency) + { + metadata.HasTransparency = true; + } + } + } + else + { + // Quantize the image using the global palette. + // Individual frames, though using the shared palette, can use a different transparent index + // to represent transparency. + + // The frame was de-duplicated against the previous frame, replacing matching pixels with + // the transparent placeholder. When the whole frame matches there is no captured difference, + // yet every pixel is still a placeholder, so we must always reserve a transparent index here; + // otherwise the placeholder pixels are matched to the nearest (typically darkest) palette color. + if (requiresTransparency && !metadata.HasTransparency) + { + metadata.HasTransparency = true; + + // Normally we pad one index past the palette so the (out of range) value is treated as + // transparent by decoders without growing the color table. A full 256-color palette leaves + // no room to pad within the 8-bit index space (index 256 wraps to 0 when written and exceeds + // the maximum GIF bit depth), so reuse the last in-range index for transparency instead. + transparencyIndex = Math.Min(globalFrameQuantizer.Palette.Length, byte.MaxValue); + metadata.TransparencyIndex = ClampIndex(transparencyIndex); + } + + globalFrameQuantizer.SetTransparencyIndex(transparencyIndex, transparentColor.ToPixel()); + quantized = globalFrameQuantizer.QuantizeFrame(encodingFrame, bounds); + } + + return quantized; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte ClampIndex(int value) => (byte)Numerics.Clamp(value, byte.MinValue, byte.MaxValue); + + /// + /// Returns the index of the transparent color in the palette. + /// + /// The current quantized frame. + /// The current gif frame metadata. + /// The pixel format. + /// The . + private static int GetTransparentIndex(IndexedImageFrame? quantized, GifFrameMetadata? metadata) + where TPixel : unmanaged, IPixel + { + if (metadata?.HasTransparency == true) + { + return metadata.TransparencyIndex; + } + + int index = -1; + if (quantized != null) + { + TPixel transparentPixel = TPixel.FromScaledVector4(Vector4.Zero); + ReadOnlySpan palette = quantized.Palette.Span; + + // Transparent pixels are much more likely to be found at the end of a palette. + for (int i = palette.Length - 1; i >= 0; i--) + { + if (palette[i].Equals(transparentPixel)) + { + index = i; + } + } + } + + return index; + } + + /// + /// Writes the file header signature and version to the stream. + /// + /// The stream to write to. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteHeader(Stream stream) => stream.Write(GifConstants.MagicNumber); + + /// + /// Writes the logical screen descriptor to the stream. + /// + /// The image metadata. + /// The image width. + /// The image height. + /// The index to set the default background index to. + /// Whether to use a global or local color table. + /// The bit depth of the color palette. + /// The stream to write to. + private void WriteLogicalScreenDescriptor( + ImageMetadata metadata, + int width, + int height, + byte backgroundIndex, + bool useGlobalTable, + int bitDepth, + Stream stream) + { + byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, bitDepth - 1, false, bitDepth - 1); + + // The Pixel Aspect Ratio is defined to be the quotient of the pixel's + // width over its height. The value range in this field allows + // specification of the widest pixel of 4:1 to the tallest pixel of + // 1:4 in increments of 1/64th. + // + // Values : 0 - No aspect ratio information is given. + // 1..255 - Value used in the computation. + // + // Aspect Ratio = (Pixel Aspect Ratio + 15) / 64 + byte ratio = 0; + + if (metadata.ResolutionUnits == PixelResolutionUnit.AspectRatio) + { + double hr = metadata.HorizontalResolution; + double vr = metadata.VerticalResolution; + if (hr != vr) + { + if (hr > vr) + { + ratio = (byte)((hr * 64) - 15); + } + else + { + ratio = (byte)((1 / vr * 64) - 15); + } + } + } + + GifLogicalScreenDescriptor descriptor = new( + width: (ushort)width, + height: (ushort)height, + packed: packedValue, + backgroundColorIndex: backgroundIndex, + ratio); + + Span buffer = stackalloc byte[20]; + descriptor.WriteTo(buffer); + + stream.Write(buffer, 0, GifLogicalScreenDescriptor.Size); + } + + /// + /// Writes the application extension to the stream. + /// + /// The stream to write to. + /// The frame count fo this image. + /// The animated image repeat count. + /// 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: Loop repeat count. + if (frameCount > 1 && repeatCount != 1) + { + GifNetscapeLoopingApplicationExtension loopingExtension = new(repeatCount); + this.WriteExtension(loopingExtension, stream); + } + + // Application Extension: XMP Profile. + if (xmpProfile != null) + { + GifXmpApplicationExtension xmpExtension = new(xmpProfile.Data!); + this.WriteExtension(xmpExtension, stream); + } + } + + /// + /// Writes the image comments to the stream. + /// + /// The metadata to be extract the comment data. + /// The stream to write to. + private void WriteComments(GifMetadata metadata, Stream stream) + { + if (metadata.Comments.Count == 0) + { + return; + } + + Span buffer = stackalloc byte[2]; + + for (int i = 0; i < metadata.Comments.Count; i++) + { + string comment = metadata.Comments[i]; + buffer[1] = GifConstants.CommentLabel; + buffer[0] = GifConstants.ExtensionIntroducer; + stream.Write(buffer); + + // Comment will be stored in chunks of 255 bytes, if it exceeds this size. + ReadOnlySpan commentSpan = comment.AsSpan(); + int idx = 0; + for (; + idx <= comment.Length - GifConstants.MaxCommentSubBlockLength; + idx += GifConstants.MaxCommentSubBlockLength) + { + WriteCommentSubBlock(stream, commentSpan, idx, GifConstants.MaxCommentSubBlockLength); + } + + // Write the length bytes, if any, to another sub block. + if (idx < comment.Length) + { + int remaining = comment.Length - idx; + WriteCommentSubBlock(stream, commentSpan, idx, remaining); + } + + stream.WriteByte(GifConstants.Terminator); + } + } + + /// + /// Writes a comment sub-block to the stream. + /// + /// The stream to write to. + /// Comment as a Span. + /// Current start index. + /// The length of the string to write. Should not exceed 255 bytes. + private static void WriteCommentSubBlock(Stream stream, ReadOnlySpan commentSpan, int idx, int length) + { + string subComment = commentSpan.Slice(idx, length).ToString(); + byte[] subCommentBytes = GifConstants.Encoding.GetBytes(subComment); + stream.WriteByte((byte)length); + stream.Write(subCommentBytes, 0, length); + } + + /// + /// Writes the optional graphics control extension to the stream. + /// + /// The metadata of the image or frame. + /// The stream to write to. + private void WriteGraphicalControlExtension(GifFrameMetadata metadata, Stream stream) + { + bool hasTransparency = metadata.HasTransparency; + + byte packedValue = GifGraphicControlExtension.GetPackedValue( + disposalMode: metadata.DisposalMode, + transparencyFlag: hasTransparency); + + GifGraphicControlExtension extension = new( + packed: packedValue, + delayTime: (ushort)metadata.FrameDelay, + transparencyIndex: hasTransparency ? metadata.TransparencyIndex : byte.MinValue); + + this.WriteExtension(extension, stream); + } + + /// + /// Writes the provided extension to the stream. + /// + /// The type of gif extension. + /// The extension to write to the stream. + /// The stream to write to. + private void WriteExtension(TGifExtension extension, Stream stream) + where TGifExtension : struct, IGifExtension + { + int extensionSize = extension.ContentLength; + + if (extensionSize == 0) + { + return; + } + + IMemoryOwner? owner = null; + scoped Span extensionBuffer = []; // workaround compiler limitation + if (extensionSize > 128) + { + owner = this.memoryAllocator.Allocate(extensionSize + 3); + extensionBuffer = owner.GetSpan(); + } + else + { + extensionBuffer = stackalloc byte[extensionSize + 3]; + } + + extensionBuffer[0] = GifConstants.ExtensionIntroducer; + extensionBuffer[1] = extension.Label; + + extension.WriteTo(extensionBuffer[2..]); + + extensionBuffer[extensionSize + 2] = GifConstants.Terminator; + + stream.Write(extensionBuffer, 0, extensionSize + 3); + owner?.Dispose(); + } + + /// + /// Writes the image frame descriptor to the stream. + /// + /// The frame location and size. + /// Whether to use the global color table. + /// The bit depth of the color palette. + /// The stream to write to. + private void WriteImageDescriptor(Rectangle rectangle, bool hasColorTable, int bitDepth, Stream stream) + { + byte packedValue = GifImageDescriptor.GetPackedValue( + localColorTableFlag: hasColorTable, + interfaceFlag: false, + sortFlag: false, + localColorTableSize: bitDepth - 1); + + GifImageDescriptor descriptor = new( + left: (ushort)rectangle.X, + top: (ushort)rectangle.Y, + width: (ushort)rectangle.Width, + height: (ushort)rectangle.Height, + packed: packedValue); + + Span buffer = stackalloc byte[20]; + descriptor.WriteTo(buffer); + + stream.Write(buffer, 0, GifImageDescriptor.Size); + } + + /// + /// Writes the color table to the stream. + /// + /// The pixel format. + /// The to encode. + /// The bit depth of the color palette. + /// The stream to write to. + private void WriteColorTable(IndexedImageFrame image, int bitDepth, Stream stream) + where TPixel : unmanaged, IPixel + { + // The maximum number of colors for the bit depth + int colorTableLength = ColorNumerics.GetColorCountForBitDepth(bitDepth) * Unsafe.SizeOf(); + + using IMemoryOwner colorTable = this.memoryAllocator.Allocate(colorTableLength, AllocationOptions.Clean); + Span colorTableSpan = colorTable.GetSpan(); + + PixelOperations.Instance.ToRgb24Bytes( + this.configuration, + image.Palette.Span, + colorTableSpan, + image.Palette.Length); + + stream.Write(colorTableSpan); + } + + /// + /// Writes the image pixel data to the stream. + /// + /// The containing indexed pixels. + /// The stream to write to. + /// The length of the frame color palette. + /// The index of the color used to represent transparency. + private void WriteImageData(Buffer2D indices, Stream stream, int paletteLength, int transparencyIndex) + { + // Pad the bit depth when required for encoding the image data. + // This is a common trick which allows to use out of range indexes for transparency and avoid allocating a larger color palette + // as decoders skip indexes that are out of range. + int padding = transparencyIndex >= paletteLength + ? 1 + : 0; + + using LzwEncoder encoder = new(this.memoryAllocator, ColorNumerics.GetBitsNeededForColorDepth(paletteLength + padding)); + encoder.Encode(indices, stream); + } + } +} diff --git a/ImageSharp/Formats/Gif/GifFormat.cs b/ImageSharp/Formats/Gif/GifFormat.cs new file mode 100644 index 0000000..a55d2a0 --- /dev/null +++ b/ImageSharp/Formats/Gif/GifFormat.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Gif { + /// + /// Registers the image encoders, decoders and mime type detectors for the gif format. + /// + public sealed class GifFormat : IImageFormat + { + private GifFormat() + { + } + + /// + /// Gets the shared instance. + /// + public static GifFormat Instance { get; } = new(); + + /// + public string Name => "GIF"; + + /// + public string DefaultMimeType => "image/gif"; + + /// + public IEnumerable MimeTypes => GifConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => GifConstants.FileExtensions; + + /// + public GifMetadata CreateDefaultFormatMetadata() => new(); + + /// + public GifFrameMetadata CreateDefaultFormatFrameMetadata() => new(); + } +} diff --git a/ImageSharp/Formats/Gif/GifFrameMetadata.cs b/ImageSharp/Formats/Gif/GifFrameMetadata.cs new file mode 100644 index 0000000..953062a --- /dev/null +++ b/ImageSharp/Formats/Gif/GifFrameMetadata.cs @@ -0,0 +1,117 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Gif { + /// + /// Provides Gif specific metadata information for the image frame. + /// + public class GifFrameMetadata : IFormatFrameMetadata + { + /// + /// Initializes a new instance of the class. + /// + public GifFrameMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private GifFrameMetadata(GifFrameMetadata other) + { + this.ColorTableMode = other.ColorTableMode; + this.FrameDelay = other.FrameDelay; + this.DisposalMode = other.DisposalMode; + + if (other.LocalColorTable?.Length > 0) + { + this.LocalColorTable = other.LocalColorTable.Value.ToArray(); + } + + this.HasTransparency = other.HasTransparency; + this.TransparencyIndex = other.TransparencyIndex; + } + + /// + /// Gets or sets the color table mode. + /// + public FrameColorTableMode ColorTableMode { get; set; } + + /// + /// Gets or sets the local color table, if any. + /// The underlying pixel format is represented by . + /// + public ReadOnlyMemory? LocalColorTable { get; set; } + + /// + /// Gets or sets a value indicating whether the frame has transparency + /// + public bool HasTransparency { get; set; } + + /// + /// Gets or sets the transparency index. + /// When is set to this value indicates the index within + /// the color palette at which the transparent color is located. + /// + public byte TransparencyIndex { get; set; } + + /// + /// Gets or sets the frame delay for animated images. + /// If not 0, when utilized in Gif animation, this field specifies the number of hundredths (1/100) of a second to + /// wait before continuing with the processing of the Data Stream. + /// The clock starts ticking immediately after the graphic is rendered. + /// + public int FrameDelay { get; set; } + + /// + /// Gets or sets the disposal method for animated images. + /// Primarily used in Gif animation, this field indicates the way in which the graphic is to + /// be treated after being displayed. + /// + public FrameDisposalMode DisposalMode { get; set; } + + /// + public static GifFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) + => new() + { + ColorTableMode = metadata.ColorTableMode, + FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10), + DisposalMode = metadata.DisposalMode, + }; + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + { + // For most scenarios we would consider the blend method to be 'Over' however if a frame has a disposal method of 'RestoreToBackground' or + // has a local palette with 256 colors and is not transparent we should use 'Source'. + bool blendSource = this.DisposalMode == FrameDisposalMode.RestoreToBackground || (this.LocalColorTable?.Length == 256 && !this.HasTransparency); + + // If the color table is global and frame has no transparency. Consider it 'Source' also. + blendSource |= this.ColorTableMode == FrameColorTableMode.Global && !this.HasTransparency; + + return new FormatConnectingFrameMetadata + { + ColorTableMode = this.ColorTableMode, + Duration = TimeSpan.FromMilliseconds(this.FrameDelay * 10), + DisposalMode = this.DisposalMode, + BlendMode = blendSource ? FrameBlendMode.Source : FrameBlendMode.Over, + }; + } + + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + => this.LocalColorTable = null; + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public GifFrameMetadata DeepClone() => new(this); + } +} diff --git a/ImageSharp/Formats/Gif/GifImageFormatDetector.cs b/ImageSharp/Formats/Gif/GifImageFormatDetector.cs new file mode 100644 index 0000000..b9c9e93 --- /dev/null +++ b/ImageSharp/Formats/Gif/GifImageFormatDetector.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Gif { + /// + /// Detects gif file headers + /// + public sealed class GifImageFormatDetector : IImageFormatDetector + { + /// + public int HeaderSize => 6; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.IsSupportedFileFormat(header) ? GifFormat.Instance : null; + return format != null; + } + + private bool IsSupportedFileFormat(ReadOnlySpan header) + { + return header.Length >= this.HeaderSize && + header[0] == 0x47 && // G + header[1] == 0x49 && // I + header[2] == 0x46 && // F + header[3] == 0x38 && // 8 + (header[4] == 0x39 || header[4] == 0x37) && // 9 or 7 + header[5] == 0x61; // a + } + } +} diff --git a/ImageSharp/Formats/Gif/GifMetadata.cs b/ImageSharp/Formats/Gif/GifMetadata.cs new file mode 100644 index 0000000..4ddfb26 --- /dev/null +++ b/ImageSharp/Formats/Gif/GifMetadata.cs @@ -0,0 +1,120 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Gif { + /// + /// Provides Gif specific metadata information for the image. + /// + public class GifMetadata : IFormatMetadata + { + /// + /// Initializes a new instance of the class. + /// + public GifMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private GifMetadata(GifMetadata other) + { + this.RepeatCount = other.RepeatCount; + this.ColorTableMode = other.ColorTableMode; + this.BackgroundColor = other.BackgroundColor; + + if (other.GlobalColorTable?.Length > 0) + { + this.GlobalColorTable = other.GlobalColorTable.Value.ToArray(); + } + + for (int i = 0; i < other.Comments.Count; i++) + { + this.Comments.Add(other.Comments[i]); + } + } + + /// + /// Gets or sets the number of times any animation is repeated. + /// + /// 0 means to repeat indefinitely, count is set as repeat n-1 times. Defaults to 1. + /// + /// + public ushort RepeatCount { get; set; } = 1; + + /// + /// Gets or sets the color table mode. + /// + public FrameColorTableMode ColorTableMode { get; set; } + + /// + /// Gets or sets the global color table, if any. + /// The underlying pixel format is represented by . + /// + public ReadOnlyMemory? GlobalColorTable { get; set; } + + /// + /// Gets or sets the background color used for pixels on the screen that are not covered by an image. + /// + public Color? BackgroundColor { get; set; } + + /// + /// Gets or sets the collection of comments about the graphics, credits, descriptions or any + /// other type of non-control and non-graphic data. + /// + public IList Comments { get; set; } = []; + + /// + public static GifMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + => new() + { + // Do not copy the color table or bit depth. + // This will lead to a mismatch when the image is comprised of frames + // extracted individually from a multi-frame image. + ColorTableMode = metadata.ColorTableMode, + RepeatCount = metadata.RepeatCount, + }; + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp = this.ColorTableMode == FrameColorTableMode.Global && this.GlobalColorTable.HasValue + ? Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.GlobalColorTable.Value.Length), 1, 8) + : 8; + + return new PixelTypeInfo(bpp) + { + ColorType = PixelColorType.Indexed, + ComponentInfo = PixelComponentInfo.Create(1, bpp, bpp), + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + AnimateRootFrame = true, + ColorTableMode = this.ColorTableMode, + BackgroundColor = this.BackgroundColor ?? Color.Transparent, + PixelTypeInfo = this.GetPixelTypeInfo(), + RepeatCount = this.RepeatCount, + }; + + /// + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + => this.GlobalColorTable = null; + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public GifMetadata DeepClone() => new(this); + } +} diff --git a/ImageSharp/Formats/Gif/GifThrowHelper.cs b/ImageSharp/Formats/Gif/GifThrowHelper.cs new file mode 100644 index 0000000..15940f2 --- /dev/null +++ b/ImageSharp/Formats/Gif/GifThrowHelper.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Gif { + internal static class GifThrowHelper + { + [DoesNotReturn] + public static void ThrowInvalidImageContentException(string errorMessage) + => throw new InvalidImageContentException(errorMessage); + + [DoesNotReturn] + public static void ThrowNoHeader() => throw new InvalidImageContentException("Gif image does not contain a Logical Screen Descriptor."); + + [DoesNotReturn] + public static void ThrowNoData() => throw new InvalidImageContentException("Unable to read Gif image data"); + } +} diff --git a/ImageSharp/Formats/Gif/LzwDecoder.cs b/ImageSharp/Formats/Gif/LzwDecoder.cs new file mode 100644 index 0000000..01fd8b3 --- /dev/null +++ b/ImageSharp/Formats/Gif/LzwDecoder.cs @@ -0,0 +1,447 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Gif { + /// + /// Decompresses and decodes data using the dynamic LZW algorithms. + /// + internal sealed class LzwDecoder : IDisposable + { + /// + /// The max decoder pixel stack size. + /// + private const int MaxStackSize = 4096; + + /// + /// The maximum bits for a lzw code. + /// + private const int MaximumLzwBits = 12; + + /// + /// The null code. + /// + private const int NullCode = -1; + + /// + /// The stream to decode. + /// + private readonly BufferedReadStream stream; + + /// + /// The prefix buffer. + /// + private readonly IMemoryOwner prefixOwner; + + /// + /// The suffix buffer. + /// + private readonly IMemoryOwner suffixOwner; + + /// + /// The scratch buffer for reading data blocks. + /// + private readonly IMemoryOwner bufferOwner; + + /// + /// The pixel stack buffer. + /// + private readonly IMemoryOwner pixelStackOwner; + private readonly int minCodeSize; + private readonly int clearCode; + private readonly int endCode; + private int code; + private int codeSize; + private int codeMask; + private int availableCode; + private int oldCode = NullCode; + private int bits; + private int top; + private int count; + private int bufferIndex; + private int data; + private int first; + + /// + /// Initializes a new instance of the class + /// and sets the stream, where the compressed data should be read from. + /// + /// The to use for buffer allocations. + /// The stream to read from. + /// The minimum code size. + /// is null. + public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream, int minCodeSize) + { + this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); + Guard.IsTrue(IsValidMinCodeSize(minCodeSize), nameof(minCodeSize), "Invalid minimum code size."); + + this.prefixOwner = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); + this.suffixOwner = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); + this.pixelStackOwner = memoryAllocator.Allocate(MaxStackSize + 1, AllocationOptions.Clean); + this.bufferOwner = memoryAllocator.Allocate(byte.MaxValue, AllocationOptions.None); + this.minCodeSize = minCodeSize; + + // Calculate the clear code. The value of the clear code is 2 ^ minCodeSize + this.clearCode = 1 << minCodeSize; + this.codeSize = minCodeSize + 1; + this.codeMask = (1 << this.codeSize) - 1; + this.endCode = this.clearCode + 1; + this.availableCode = this.clearCode + 2; + + // Fill the suffix buffer with the initial values represented by the number of colors. + Span suffix = this.suffixOwner.GetSpan()[..this.clearCode]; + int i; + for (i = 0; i < suffix.Length; i++) + { + suffix[i] = i; + } + + this.code = i; + } + + /// + /// Gets a value indicating whether the minimum code size is valid. + /// + /// The minimum code size. + /// + /// if the minimum code size is valid; otherwise, . + /// + public static bool IsValidMinCodeSize(int minCodeSize) + { + // It is possible to specify a larger LZW minimum code size than the palette length in bits + // which may leave a gap in the codes where no colors are assigned. + // http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp#lzw_compression + if (minCodeSize < 2 || minCodeSize > MaximumLzwBits || 1 << minCodeSize > MaxStackSize) + { + // Don't attempt to decode the frame indices. + // Theoretically we could determine a min code size from the length of the provided + // color palette but we won't bother since the image is most likely corrupted. + return false; + } + + return true; + } + + /// + /// Decodes and decompresses all pixel indices for a single row from the stream, assigning the pixel values to the buffer. + /// + /// The pixel indices array to decode to. + public void DecodePixelRow(Span indices) + { + indices.Clear(); + + // Get span values from the owners. + Span prefix = this.prefixOwner.GetSpan(); + Span suffix = this.suffixOwner.GetSpan(); + Span pixelStack = this.pixelStackOwner.GetSpan(); + Span buffer = this.bufferOwner.GetSpan(); + + // Cache frequently accessed instance fields into locals. + // This helps avoid repeated field loads inside the tight loop. + BufferedReadStream stream = this.stream; + int top = this.top; + int bits = this.bits; + int codeSize = this.codeSize; + int codeMask = this.codeMask; + int minCodeSize = this.minCodeSize; + int availableCode = this.availableCode; + int oldCode = this.oldCode; + int first = this.first; + int data = this.data; + int count = this.count; + int bufferIndex = this.bufferIndex; + int code = this.code; + int clearCode = this.clearCode; + int endCode = this.endCode; + + int i = 0; + while (i < indices.Length) + { + if (top == 0) + { + if (bits < codeSize) + { + // Load bytes until there are enough bits for a code. + if (count == 0) + { + // Read a new data block. + count = ReadBlock(stream, buffer); + if (count == 0) + { + break; + } + + bufferIndex = 0; + } + + data += buffer[bufferIndex] << bits; + bits += 8; + bufferIndex++; + count--; + continue; + } + + // Get the next code + code = data & codeMask; + data >>= codeSize; + bits -= codeSize; + + // Interpret the code + if (code > availableCode || code == endCode) + { + break; + } + + if (code == clearCode) + { + // Reset the decoder + codeSize = minCodeSize + 1; + codeMask = (1 << codeSize) - 1; + availableCode = clearCode + 2; + oldCode = NullCode; + continue; + } + + if (oldCode == NullCode) + { + pixelStack[top++] = suffix[code]; + oldCode = code; + first = code; + continue; + } + + int inCode = code; + if (code == availableCode) + { + pixelStack[top++] = first; + code = oldCode; + } + + while (code > clearCode && top < MaxStackSize) + { + pixelStack[top++] = suffix[code]; + code = prefix[code]; + } + + int suffixCode = suffix[code]; + first = suffixCode; + pixelStack[top++] = suffixCode; + + // Fix for GIFs that have "deferred clear code" as per: + // https://bugzilla.mozilla.org/show_bug.cgi?id=55918 + if (availableCode < MaxStackSize) + { + prefix[availableCode] = oldCode; + suffix[availableCode] = first; + availableCode++; + if (availableCode == codeMask + 1 && availableCode < MaxStackSize) + { + codeSize++; + codeMask = (1 << codeSize) - 1; + } + } + + oldCode = inCode; + } + + // Pop a pixel off the pixel stack. + top--; + + // Clear missing pixels. + indices[i++] = (byte)pixelStack[top]; + } + + // Write back the local values to the instance fields. + this.top = top; + this.bits = bits; + this.codeSize = codeSize; + this.codeMask = codeMask; + this.availableCode = availableCode; + this.oldCode = oldCode; + this.first = first; + this.data = data; + this.count = count; + this.bufferIndex = bufferIndex; + this.code = code; + } + + /// + /// Decodes and decompresses all pixel indices from the stream allowing skipping of the data. + /// + /// The resulting index table length. + public void SkipIndices(int length) + { + // Get span values from the owners. + Span prefix = this.prefixOwner.GetSpan(); + Span suffix = this.suffixOwner.GetSpan(); + Span pixelStack = this.pixelStackOwner.GetSpan(); + Span buffer = this.bufferOwner.GetSpan(); + + // Cache frequently accessed instance fields into locals. + // This helps avoid repeated field loads inside the tight loop. + BufferedReadStream stream = this.stream; + int top = this.top; + int bits = this.bits; + int codeSize = this.codeSize; + int codeMask = this.codeMask; + int minCodeSize = this.minCodeSize; + int availableCode = this.availableCode; + int oldCode = this.oldCode; + int first = this.first; + int data = this.data; + int count = this.count; + int bufferIndex = this.bufferIndex; + int code = this.code; + int clearCode = this.clearCode; + int endCode = this.endCode; + + int i = 0; + while (i < length) + { + if (top == 0) + { + if (bits < codeSize) + { + // Load bytes until there are enough bits for a code. + if (count == 0) + { + // Read a new data block. + count = ReadBlock(stream, buffer); + if (count == 0) + { + break; + } + + bufferIndex = 0; + } + + data += buffer[bufferIndex] << bits; + bits += 8; + bufferIndex++; + count--; + continue; + } + + // Get the next code + code = data & codeMask; + data >>= codeSize; + bits -= codeSize; + + // Interpret the code + if (code > availableCode || code == endCode) + { + break; + } + + if (code == clearCode) + { + // Reset the decoder + codeSize = minCodeSize + 1; + codeMask = (1 << codeSize) - 1; + availableCode = clearCode + 2; + oldCode = NullCode; + continue; + } + + if (oldCode == NullCode) + { + pixelStack[top++] = suffix[code]; + oldCode = code; + first = code; + continue; + } + + int inCode = code; + if (code == availableCode) + { + pixelStack[top++] = first; + code = oldCode; + } + + while (code > clearCode && top < MaxStackSize) + { + pixelStack[top++] = suffix[code]; + code = prefix[code]; + } + + int suffixCode = suffix[code]; + first = suffixCode; + pixelStack[top++] = suffixCode; + + // Fix for GIFs that have "deferred clear code" as per: + // https://bugzilla.mozilla.org/show_bug.cgi?id=55918 + if (availableCode < MaxStackSize) + { + prefix[availableCode] = oldCode; + suffix[availableCode] = first; + availableCode++; + if (availableCode == codeMask + 1 && availableCode < MaxStackSize) + { + codeSize++; + codeMask = (1 << codeSize) - 1; + } + } + + oldCode = inCode; + } + + // Pop a pixel off the pixel stack. + top--; + + // Skip missing pixels. + i++; + } + + // Write back the local values to the instance fields. + this.top = top; + this.bits = bits; + this.codeSize = codeSize; + this.codeMask = codeMask; + this.availableCode = availableCode; + this.oldCode = oldCode; + this.first = first; + this.data = data; + this.count = count; + this.bufferIndex = bufferIndex; + this.code = code; + } + + /// + /// Reads the next data block from the stream. A data block begins with a byte, + /// which defines the size of the block, followed by the block itself. + /// + /// The stream to read from. + /// The buffer to store the block in. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ReadBlock(BufferedReadStream stream, Span buffer) + { + int bufferSize = stream.ReadByte(); + + if (bufferSize < 1) + { + return 0; + } + + int count = stream.Read(buffer, 0, bufferSize); + + return count != bufferSize ? 0 : bufferSize; + } + + /// + public void Dispose() + { + this.prefixOwner.Dispose(); + this.suffixOwner.Dispose(); + this.pixelStackOwner.Dispose(); + this.bufferOwner.Dispose(); + } + } +} diff --git a/ImageSharp/Formats/Gif/LzwEncoder.cs b/ImageSharp/Formats/Gif/LzwEncoder.cs new file mode 100644 index 0000000..a19134b --- /dev/null +++ b/ImageSharp/Formats/Gif/LzwEncoder.cs @@ -0,0 +1,423 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Gif { + /// + /// Encodes and compresses the image data using dynamic Lempel-Ziv compression. + /// + /// + /// Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott. K Weiner 12/00 + /// + /// GIFCOMPR.C - GIF Image compression routines + /// + /// + /// Lempel-Ziv compression based on 'compress'. GIF modifications by + /// David Rowley (mgardi@watdcsu.waterloo.edu) + /// + /// GIF Image compression - modified 'compress' + /// + /// Based on: compress.c - File compression ala IEEE Computer, June 1984. + /// By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas) + /// Jim McKie (decvax!mcvax!jim) + /// Steve Davies (decvax!vax135!petsd!peora!srd) + /// Ken Turkowski (decvax!decwrl!turtlevax!ken) + /// James A. Woods (decvax!ihnp4!ames!jaw) + /// Joe Orost (decvax!vax135!petsd!joe) + /// + /// + internal sealed class LzwEncoder : IDisposable + { + /// + /// 80% occupancy + /// + private const int HashSize = 5003; + + /// + /// The amount to shift each code. + /// + private const int HashShift = 4; + + /// + /// Mask used when shifting pixel values + /// + private static readonly int[] Masks = + [ + 0b0, + 0b1, + 0b11, + 0b111, + 0b1111, + 0b11111, + 0b111111, + 0b1111111, + 0b11111111, + 0b111111111, + 0b1111111111, + 0b11111111111, + 0b111111111111, + 0b1111111111111, + 0b11111111111111, + 0b111111111111111, + 0b1111111111111111 + ]; + + /// + /// The maximum number of bits/code. + /// + private const int MaxBits = 12; + + /// + /// Should NEVER generate this code. + /// + private const int MaxMaxCode = 1 << MaxBits; + + /// + /// The initial code size. + /// + private readonly int initialCodeSize; + + /// + /// The hash table. + /// + private readonly IMemoryOwner hashTable; + + /// + /// The code table. + /// + private readonly IMemoryOwner codeTable; + + /// + /// Define the storage for the packet accumulator. + /// + private readonly byte[] accumulators = new byte[256]; + + /// + /// Number of bits/code + /// + private int bitCount; + + /// + /// maximum code, given bitCount + /// + private int maxCode; + + /// + /// First unused entry + /// + private int freeEntry; + + /// + /// Block compression parameters -- after all codes are used up, + /// and compression rate changes, start over. + /// + private bool clearFlag; + + /// + /// Algorithm: use open addressing double hashing (no chaining) on the + /// prefix code / next character combination. We do a variant of Knuth's + /// algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime + /// secondary probe. Here, the modular division first probe is gives way + /// to a faster exclusive-or manipulation. Also do block compression with + /// an adaptive reset, whereby the code table is cleared when the compression + /// ratio decreases, but after the table fills. The variable-length output + /// codes are re-sized at this point, and a special CLEAR code is generated + /// for the decompressor. Late addition: construct the table according to + /// file size for noticeable speed improvement on small files. Please direct + /// questions about this implementation to ames!jaw. + /// + private int globalInitialBits; + + /// + /// The clear code. + /// + private int clearCode; + + /// + /// The end-of-file code. + /// + private int eofCode; + + /// + /// Output the given code. + /// Inputs: + /// code: A bitCount-bit integer. If == -1, then EOF. This assumes + /// that bitCount =< wordsize - 1. + /// Outputs: + /// Outputs code to the file. + /// Assumptions: + /// Chars are 8 bits long. + /// Algorithm: + /// Maintain a BITS character long buffer (so that 8 codes will + /// fit in it exactly). Use the VAX insv instruction to insert each + /// code in turn. When the buffer fills up empty it and start over. + /// + private int currentAccumulator; + + /// + /// The current bits. + /// + private int currentBits; + + /// + /// Number of characters so far in this 'packet' + /// + private int accumulatorCount; + + /// + /// Initializes a new instance of the class. + /// + /// The to use for buffer allocations. + /// The color depth in bits. + public LzwEncoder(MemoryAllocator memoryAllocator, int colorDepth) + { + this.initialCodeSize = Math.Max(2, colorDepth); + this.hashTable = memoryAllocator.Allocate(HashSize, AllocationOptions.Clean); + this.codeTable = memoryAllocator.Allocate(HashSize, AllocationOptions.Clean); + } + + /// + /// Encodes and compresses the indexed pixels to the stream. + /// + /// The 2D buffer of indexed pixels. + /// The stream to write to. + public void Encode(Buffer2D indexedPixels, Stream stream) + { + // Write "initial code size" byte + stream.WriteByte((byte)this.initialCodeSize); + + // Compress and write the pixel data + this.Compress(indexedPixels, this.initialCodeSize + 1, stream); + + // Write block terminator + stream.WriteByte(GifConstants.Terminator); + } + + /// + /// Gets the maximum code value. + /// + /// The number of bits + /// See + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetMaxCode(int bitCount) => (1 << bitCount) - 1; + + /// + /// Add a character to the end of the current packet, and if it is 254 characters, + /// flush the packet to disk. + /// + /// The character to add. + /// The reference to the storage for packet accumulators + /// The stream to write to. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddCharacter(byte c, ref byte accumulatorsRef, Stream stream) + { + Unsafe.Add(ref accumulatorsRef, (uint)this.accumulatorCount++) = c; + if (this.accumulatorCount >= 254) + { + this.FlushPacket(stream); + } + } + + /// + /// Table clear for block compress. + /// + /// The output stream. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ClearBlock(Stream stream) + { + this.ResetCodeTable(); + this.freeEntry = this.clearCode + 2; + this.clearFlag = true; + + this.Output(this.clearCode, stream); + } + + /// + /// Reset the code table. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ResetCodeTable() => this.hashTable.GetSpan().Fill(-1); + + /// + /// Compress the packets to the stream. + /// + /// The 2D buffer of indexed pixels. + /// The initial bits. + /// The stream to write to. + private void Compress(Buffer2D indexedPixels, int initialBits, Stream stream) + { + // Set up the globals: globalInitialBits - initial number of bits + this.globalInitialBits = initialBits; + + // Set up the necessary values + this.clearFlag = false; + this.bitCount = this.globalInitialBits; + this.maxCode = GetMaxCode(this.bitCount); + this.clearCode = 1 << (initialBits - 1); + this.eofCode = this.clearCode + 1; + this.freeEntry = this.clearCode + 2; + this.accumulatorCount = 0; // Clear packet + + this.ResetCodeTable(); // Clear hash table + this.Output(this.clearCode, stream); + + ref int hashTableRef = ref MemoryMarshal.GetReference(this.hashTable.GetSpan()); + ref int codeTableRef = ref MemoryMarshal.GetReference(this.codeTable.GetSpan()); + + int entry = indexedPixels[0, 0]; + + for (int y = 0; y < indexedPixels.Height; y++) + { + ref byte rowSpanRef = ref MemoryMarshal.GetReference(indexedPixels.DangerousGetRowSpan(y)); + int offsetX = y == 0 ? 1 : 0; + + for (int x = offsetX; x < indexedPixels.Width; x++) + { + int code = Unsafe.Add(ref rowSpanRef, (uint)x); + int freeCode = (code << MaxBits) + entry; + int hashIndex = (code << HashShift) ^ entry; + + if (Unsafe.Add(ref hashTableRef, (uint)hashIndex) == freeCode) + { + entry = Unsafe.Add(ref codeTableRef, (uint)hashIndex); + continue; + } + + // Non-empty slot + if (Unsafe.Add(ref hashTableRef, (uint)hashIndex) >= 0) + { + int disp = 1; + if (hashIndex != 0) + { + disp = HashSize - hashIndex; + } + + do + { + if ((hashIndex -= disp) < 0) + { + hashIndex += HashSize; + } + + if (Unsafe.Add(ref hashTableRef, (uint)hashIndex) == freeCode) + { + entry = Unsafe.Add(ref codeTableRef, (uint)hashIndex); + break; + } + } + while (Unsafe.Add(ref hashTableRef, (uint)hashIndex) >= 0); + + if (Unsafe.Add(ref hashTableRef, (uint)hashIndex) == freeCode) + { + continue; + } + } + + this.Output(entry, stream); + entry = code; + if (this.freeEntry < MaxMaxCode) + { + Unsafe.Add(ref codeTableRef, (uint)hashIndex) = this.freeEntry++; // code -> hashtable + Unsafe.Add(ref hashTableRef, (uint)hashIndex) = freeCode; + } + else + { + this.ClearBlock(stream); + } + } + } + + // Output the final code. + this.Output(entry, stream); + this.Output(this.eofCode, stream); + } + + /// + /// Flush the packet to disk and reset the accumulator. + /// + /// The output stream. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FlushPacket(Stream outStream) + { + outStream.WriteByte((byte)this.accumulatorCount); + outStream.Write(this.accumulators, 0, this.accumulatorCount); + this.accumulatorCount = 0; + } + + /// + /// Output the current code to the stream. + /// + /// The code. + /// The stream to write to. + private void Output(int code, Stream outs) + { + ref byte accumulatorsRef = ref MemoryMarshal.GetReference(this.accumulators.AsSpan()); + this.currentAccumulator &= Masks[this.currentBits]; + + if (this.currentBits > 0) + { + this.currentAccumulator |= code << this.currentBits; + } + else + { + this.currentAccumulator = code; + } + + this.currentBits += this.bitCount; + + while (this.currentBits >= 8) + { + this.AddCharacter((byte)(this.currentAccumulator & 0xFF), ref accumulatorsRef, outs); + this.currentAccumulator >>= 8; + this.currentBits -= 8; + } + + // If the next entry is going to be too big for the code size, + // then increase it, if possible. + if (this.freeEntry > this.maxCode || this.clearFlag) + { + if (this.clearFlag) + { + this.maxCode = GetMaxCode(this.bitCount = this.globalInitialBits); + this.clearFlag = false; + } + else + { + ++this.bitCount; + this.maxCode = this.bitCount == MaxBits + ? MaxMaxCode + : GetMaxCode(this.bitCount); + } + } + + if (code == this.eofCode) + { + // At EOF, write the rest of the buffer. + while (this.currentBits > 0) + { + this.AddCharacter((byte)(this.currentAccumulator & 0xFF), ref accumulatorsRef, outs); + this.currentAccumulator >>= 8; + this.currentBits -= 8; + } + + if (this.accumulatorCount > 0) + { + this.FlushPacket(outs); + } + } + } + + /// + public void Dispose() + { + this.hashTable?.Dispose(); + this.codeTable?.Dispose(); + } + } +} diff --git a/ImageSharp/Formats/Gif/README.md b/ImageSharp/Formats/Gif/README.md new file mode 100644 index 0000000..eeda20c --- /dev/null +++ b/ImageSharp/Formats/Gif/README.md @@ -0,0 +1,6 @@ +Encoder/Decoder adapted and extended from: + +- [Nine.Imaging](https://github.com/yufeih/Nine.Imaging/) +- [imagetools.codeplex](https://imagetools.codeplex.com/) + +A useful set of gif test images can be found at [pygif](https://github.com/robert-ancell/pygif/tree/master/test-suite) \ No newline at end of file diff --git a/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs b/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs new file mode 100644 index 0000000..8d65123 --- /dev/null +++ b/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs @@ -0,0 +1,132 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Gif { + /// + /// The Graphic Control Extension contains parameters used when + /// processing a graphic rendering block. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal readonly struct GifGraphicControlExtension : IGifExtension, IEquatable + { + public GifGraphicControlExtension( + byte packed, + ushort delayTime, + byte transparencyIndex) + { + this.BlockSize = 4; + this.Packed = packed; + this.DelayTime = delayTime; + this.TransparencyIndex = transparencyIndex; + } + + /// + /// Gets the size of the block. + /// + public byte BlockSize { get; } + + /// + /// Gets the packed disposalMethod and transparencyFlag value. + /// + public byte Packed { get; } + + /// + /// Gets the delay time in of hundredths (1/100) of a second + /// to wait before continuing with the processing of the Data Stream. + /// The clock starts ticking immediately after the graphic is rendered. + /// + public ushort DelayTime { get; } + + /// + /// Gets the transparency index. + /// The Transparency Index is such that when encountered, the corresponding pixel + /// of the display device is not modified and processing goes on to the next pixel. + /// + public byte TransparencyIndex { get; } + + /// + /// Gets the disposal method which indicates the way in which the + /// graphic is to be treated after being displayed. + /// + public FrameDisposalMode DisposalMethod => (FrameDisposalMode)((this.Packed & 0x1C) >> 2); + + /// + /// Gets a value indicating whether transparency flag is to be set. + /// This indicates whether a transparency index is given in the Transparent Index field. + /// + public bool TransparencyFlag => (this.Packed & 0x01) == 1; + + byte IGifExtension.Label => GifConstants.GraphicControlLabel; + + int IGifExtension.ContentLength => 5; + + public static bool operator ==(GifGraphicControlExtension left, GifGraphicControlExtension right) => left.Equals(right); + + public static bool operator !=(GifGraphicControlExtension left, GifGraphicControlExtension right) => !(left == right); + + public int WriteTo(Span buffer) + { + ref GifGraphicControlExtension dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); + + dest = this; + + return ((IGifExtension)this).ContentLength; + } + + public static GifGraphicControlExtension Parse(ReadOnlySpan buffer) + => MemoryMarshal.Cast(buffer)[0]; + + public static byte GetPackedValue(FrameDisposalMode disposalMode, bool userInputFlag = false, bool transparencyFlag = false) + { + /* + Reserved | 3 Bits + Disposal Method | 3 Bits + User Input Flag | 1 Bit + Transparent Color Flag | 1 Bit + */ + + byte value = 0; + + value |= (byte)((int)disposalMode << 2); + + if (userInputFlag) + { + value |= 1 << 1; + } + + if (transparencyFlag) + { + value |= 1; + } + + return value; + } + + public override bool Equals(object? obj) => obj is GifGraphicControlExtension extension && this.Equals(extension); + + public bool Equals(GifGraphicControlExtension other) + => this.BlockSize == other.BlockSize + && this.Packed == other.Packed + && this.DelayTime == other.DelayTime + && this.TransparencyIndex == other.TransparencyIndex + && this.DisposalMethod == other.DisposalMethod + && this.TransparencyFlag == other.TransparencyFlag + && ((IGifExtension)this).Label == ((IGifExtension)other).Label + && ((IGifExtension)this).ContentLength == ((IGifExtension)other).ContentLength; + + public override int GetHashCode() + => HashCode.Combine( + this.BlockSize, + this.Packed, + this.DelayTime, + this.TransparencyIndex, + this.DisposalMethod, + this.TransparencyFlag, + ((IGifExtension)this).Label, + ((IGifExtension)this).ContentLength); + } +} diff --git a/ImageSharp/Formats/Gif/Sections/GifImageDescriptor.cs b/ImageSharp/Formats/Gif/Sections/GifImageDescriptor.cs new file mode 100644 index 0000000..37eb71e --- /dev/null +++ b/ImageSharp/Formats/Gif/Sections/GifImageDescriptor.cs @@ -0,0 +1,115 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Gif { + /// + /// Each image in the Data Stream is composed of an Image Descriptor, + /// an optional Local Color Table, and the image data. + /// Each image must fit within the boundaries of the + /// Logical Screen, as defined in the Logical Screen Descriptor. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal readonly struct GifImageDescriptor + { + public const int Size = 10; + + public GifImageDescriptor( + ushort left, + ushort top, + ushort width, + ushort height, + byte packed) + { + this.Left = left; + this.Top = top; + this.Width = width; + this.Height = height; + this.Packed = packed; + } + + /// + /// Gets the column number, in pixels, of the left edge of the image, + /// with respect to the left edge of the Logical Screen. + /// Leftmost column of the Logical Screen is 0. + /// + public ushort Left { get; } + + /// + /// Gets the row number, in pixels, of the top edge of the image with + /// respect to the top edge of the Logical Screen. + /// Top row of the Logical Screen is 0. + /// + public ushort Top { get; } + + /// + /// Gets the width of the image in pixels. + /// + public ushort Width { get; } + + /// + /// Gets the height of the image in pixels. + /// + public ushort Height { get; } + + /// + /// Gets the packed value of localColorTableFlag, interlaceFlag, sortFlag, and localColorTableSize. + /// + public byte Packed { get; } + + public bool LocalColorTableFlag => ((this.Packed & 0x80) >> 7) == 1; + + public int LocalColorTableSize => 2 << (this.Packed & 0x07); + + public bool InterlaceFlag => ((this.Packed & 0x40) >> 6) == 1; + + public void WriteTo(Span buffer) + { + buffer[0] = GifConstants.ImageDescriptorLabel; + + ref GifImageDescriptor dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer[1..])); + + dest = this; + } + + public static GifImageDescriptor Parse(ReadOnlySpan buffer) + { + return MemoryMarshal.Cast(buffer)[0]; + } + + public static byte GetPackedValue(bool localColorTableFlag, bool interfaceFlag, bool sortFlag, int localColorTableSize) + { + /* + Local Color Table Flag | 1 Bit + Interlace Flag | 1 Bit + Sort Flag | 1 Bit + Reserved | 2 Bits + Size of Local Color Table | 3 Bits + */ + + byte value = 0; + + if (localColorTableFlag) + { + value |= 1 << 7; + } + + if (interfaceFlag) + { + value |= 1 << 6; + } + + if (sortFlag) + { + value |= 1 << 5; + } + + value |= (byte)localColorTableSize; + + return value; + } + } +} diff --git a/ImageSharp/Formats/Gif/Sections/GifLogicalScreenDescriptor.cs b/ImageSharp/Formats/Gif/Sections/GifLogicalScreenDescriptor.cs new file mode 100644 index 0000000..bc59dd9 --- /dev/null +++ b/ImageSharp/Formats/Gif/Sections/GifLogicalScreenDescriptor.cs @@ -0,0 +1,132 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Gif { + /// + /// The Logical Screen Descriptor contains the parameters + /// necessary to define the area of the display device + /// within which the images will be rendered + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal readonly struct GifLogicalScreenDescriptor + { + public const int Size = 7; + + public GifLogicalScreenDescriptor( + ushort width, + ushort height, + byte packed, + byte backgroundColorIndex, + byte pixelAspectRatio = 0) + { + this.Width = width; + this.Height = height; + this.Packed = packed; + this.BackgroundColorIndex = backgroundColorIndex; + this.PixelAspectRatio = pixelAspectRatio; + } + + /// + /// Gets the width, in pixels, of the Logical Screen where the images will + /// be rendered in the displaying device. + /// + public ushort Width { get; } + + /// + /// Gets the height, in pixels, of the Logical Screen where the images will be + /// rendered in the displaying device. + /// + public ushort Height { get; } + + /// + /// Gets the packed value consisting of: + /// globalColorTableFlag, colorResolution, sortFlag, and sizeOfGlobalColorTable. + /// + public byte Packed { get; } + + /// + /// Gets the index at the Global Color Table for the Background Color. + /// The Background Color is the color used for those + /// pixels on the screen that are not covered by an image. + /// + public byte BackgroundColorIndex { get; } + + /// + /// Gets the pixel aspect ratio. + /// + public byte PixelAspectRatio { get; } + + /// + /// Gets a value indicating whether a flag denoting the presence of a Global Color Table + /// should be set. + /// If the flag is set, the Global Color Table will included after + /// the Logical Screen Descriptor. + /// + public bool GlobalColorTableFlag => ((this.Packed & 0x80) >> 7) == 1; + + /// + /// Gets the global color table size. + /// If the Global Color Table Flag is set, + /// the value in this field is used to calculate the number of + /// bytes contained in the Global Color Table. + /// + public int GlobalColorTableSize => 2 << (this.Packed & 0x07); + + /// + /// Gets the color depth, in number of bits per pixel. + /// The lowest 3 packed bits represent the bit depth minus 1. + /// + public int BitsPerPixel => (this.Packed & 0x07) + 1; + + public void WriteTo(Span buffer) + { + ref GifLogicalScreenDescriptor dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); + + dest = this; + } + + public static GifLogicalScreenDescriptor Parse(ReadOnlySpan buffer) + { + GifLogicalScreenDescriptor result = MemoryMarshal.Cast(buffer)[0]; + + if (result.GlobalColorTableSize > 255 * 4) + { + throw new ImageFormatException($"Invalid gif colormap size '{result.GlobalColorTableSize}'"); + } + + return result; + } + + public static byte GetPackedValue(bool globalColorTableFlag, int colorResolution, bool sortFlag, int globalColorTableSize) + { + /* + Global Color Table Flag | 1 Bit + Color Resolution | 3 Bits + Sort Flag | 1 Bit + Size of Global Color Table | 3 Bits + */ + + byte value = 0; + + if (globalColorTableFlag) + { + value |= 1 << 7; + } + + value |= (byte)(colorResolution << 4); + + if (sortFlag) + { + value |= 1 << 3; + } + + value |= (byte)globalColorTableSize; + + return value; + } + } +} diff --git a/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs b/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs new file mode 100644 index 0000000..9de3819 --- /dev/null +++ b/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Gif { + internal readonly struct GifNetscapeLoopingApplicationExtension : IGifExtension + { + public GifNetscapeLoopingApplicationExtension(ushort repeatCount) => this.RepeatCount = repeatCount; + + 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. + /// + public ushort RepeatCount { get; } + + public static GifNetscapeLoopingApplicationExtension Parse(ReadOnlySpan buffer) + { + ushort repeatCount = BinaryPrimitives.ReadUInt16LittleEndian(buffer[..2]); + return new GifNetscapeLoopingApplicationExtension(repeatCount); + } + + public int WriteTo(Span buffer) + { + buffer[0] = GifConstants.ApplicationBlockSize; + + // Write NETSCAPE2.0 + GifConstants.NetscapeApplicationIdentificationBytes.CopyTo(buffer.Slice(1, 11)); + + // Application Data ---- + buffer[12] = 3; // Application block length (always 3) + buffer[13] = 1; // Data sub-block identity (always 1) + + // 0 means loop indefinitely. Count is set as play n + 1 times. + BinaryPrimitives.WriteUInt16LittleEndian(buffer.Slice(14, 2), this.RepeatCount); + + return this.ContentLength; // Length - Introducer + Label + Terminator. + } + } +} diff --git a/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs b/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs new file mode 100644 index 0000000..bd66345 --- /dev/null +++ b/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs @@ -0,0 +1,107 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Gif { + internal readonly struct GifXmpApplicationExtension : IGifExtension + { + public GifXmpApplicationExtension(byte[] data) => this.Data = data; + + public byte Label => GifConstants.ApplicationExtensionLabel; + + // size : 1 + // identifier : 11 + // magic trailer : 257 + public int ContentLength => (this.Data.Length > 0) ? this.Data.Length + 269 : 0; + + /// + /// Gets the raw Data. + /// + public byte[] Data { get; } + + /// + /// Reads the XMP metadata from the specified stream. + /// + /// The stream to read from. + /// The memory allocator. + /// The XMP metadata + public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator) + { + byte[] xmpBytes = ReadXmpData(stream, allocator, out bool terminated); + if (!terminated) + { + throw new InvalidImageContentException("Unexpected end of stream while reading gif XMP data"); + } + + // Exclude the "magic trailer", see XMP Specification Part 3, 1.1.2 GIF + int xmpLength = xmpBytes.Length - 256; // 257 - unread 0x0 + byte[] buffer = []; + if (xmpLength > 0) + { + buffer = new byte[xmpLength]; + xmpBytes.AsSpan(0, xmpLength).CopyTo(buffer); + stream.Skip(1); // Skip the terminator. + } + + return new GifXmpApplicationExtension(buffer); + } + + public int WriteTo(Span buffer) + { + int bytesWritten = 0; + buffer[bytesWritten++] = GifConstants.ApplicationBlockSize; + + // Write "XMP DataXMP" + ReadOnlySpan idBytes = GifConstants.XmpApplicationIdentificationBytes; + idBytes.CopyTo(buffer[bytesWritten..]); + bytesWritten += idBytes.Length; + + // XMP Data itself + this.Data.CopyTo(buffer[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 this.ContentLength; + } + + private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator, out bool terminated) + { + using ChunkedMemoryStream bytes = new(allocator); + + // XMP data doesn't have a fixed length nor is there an indicator of the length. + // So we simply read one byte at a time until we hit the 0x0 value at the end + // of the magic trailer or the end of the stream. + // Using ChunkedMemoryStream reduces the array resize allocation normally associated + // with writing from a non fixed-size buffer. + while (true) + { + int b = stream.ReadByte(); + if (b == 0) + { + terminated = true; + return bytes.ToArray(); + } + + if (b < 0) + { + terminated = false; + return bytes.ToArray(); + } + + bytes.WriteByte((byte)b); + } + } + } +} diff --git a/ImageSharp/Formats/Gif/Sections/IGifExtension.cs b/ImageSharp/Formats/Gif/Sections/IGifExtension.cs new file mode 100644 index 0000000..d4c8f58 --- /dev/null +++ b/ImageSharp/Formats/Gif/Sections/IGifExtension.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Gif { + /// + /// A base interface for GIF extensions. + /// + public interface IGifExtension + { + /// + /// Gets the label identifying the extensions. + /// + byte Label { get; } + + /// + /// Gets the length of the contents of this extension. + /// + int ContentLength { get; } + + /// + /// Writes the extension data to the buffer. + /// + /// The buffer to write the extension to. + /// The number of bytes written to the buffer. + int WriteTo(Span buffer); + } +} diff --git a/ImageSharp/Formats/Gif/spec-gif89a.txt b/ImageSharp/Formats/Gif/spec-gif89a.txt new file mode 100644 index 0000000..64a0729 --- /dev/null +++ b/ImageSharp/Formats/Gif/spec-gif89a.txt @@ -0,0 +1,2476 @@ + + + + + Cover Sheet for the GIF89a Specification + + + DEFERRED CLEAR CODE IN LZW COMPRESSION + + There has been confusion about where clear codes can be found in the + data stream. As the specification says, they may appear at anytime. There + is not a requirement to send a clear code when the string table is full. + + It is the encoder's decision as to when the table should be cleared. When + the table is full, the encoder can chose to use the table as is, making no + changes to it until the encoder chooses to clear it. The encoder during + this time sends out codes that are of the maximum Code Size. + + As we can see from the above, when the decoder's table is full, it must + not change the table until a clear code is received. The Code Size is that + of the maximum Code Size. Processing other than this is done normally. + + Because of a large base of decoders that do not handle the decompression in + this manner, we ask developers of GIF encoding software to NOT implement + this feature until at least January 1991 and later if they see that their + particular market is not ready for it. This will give developers of GIF + decoding software time to implement this feature and to get it into the + hands of their clients before the decoders start "breaking" on the new + GIF's. It is not required that encoders change their software to take + advantage of the deferred clear code, but it is for decoders. + + APPLICATION EXTENSION BLOCK - APPLICATION IDENTIFIER + + There will be a Courtesy Directory file located on CompuServe in the PICS + forum. This directory will contain Application Identifiers for Application + Extension Blocks that have been used by developers of GIF applications. + This file is intended to help keep developers that wish to create + Application Extension Blocks from using the same Application Identifiers. + This is not an official directory; it is for voluntary participation only + and does not guarantee that someone will not use the same identifier. + + E-Mail can be sent to Larry Wood (forum manager of PICS) indicating the + request for inclusion in this file with an identifier. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GRAPHICS INTERCHANGE FORMAT(sm) + + Version 89a + + (c)1987,1988,1989,1990 + + Copyright + CompuServe Incorporated + Columbus, Ohio + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +CompuServe Incorporated Graphics Interchange Format +Document Date : 31 July 1990 Programming Reference + + + + + + + + + + + Table of Contents + +Disclaimer................................................................. 1 + +Foreword................................................................... 1 + +Licensing.................................................................. 1 + +About the Document......................................................... 2 + +General Description........................................................ 2 + +Version Numbers............................................................ 2 + +The Encoder................................................................ 3 + +The Decoder................................................................ 3 + +Compliance................................................................. 3 + +About Recommendations...................................................... 4 + +About Color Tables......................................................... 4 + +Blocks, Extensions and Scope............................................... 4 + +Block Sizes................................................................ 5 + +Using GIF as an embedded protocol.......................................... 5 + +Data Sub-blocks............................................................ 5 + +Block Terminator........................................................... 6 + +Header..................................................................... 7 + +Logical Screen Descriptor.................................................. 8 + +Global Color Table......................................................... 10 + +Image Descriptor........................................................... 11 + +Local Color Table.......................................................... 13 + +Table Based Image Data..................................................... 14 + +Graphic Control Extension.................................................. 15 + +Comment Extension.......................................................... 17 + +Plain Text Extension....................................................... 18 + +Application Extension...................................................... 21 + +Trailer.................................................................... 23 + + + + + + + + + + + +Quick Reference Table...................................................... 24 + +GIF Grammar................................................................ 25 + +Glossary................................................................... 27 + +Conventions................................................................ 28 + +Interlaced Images.......................................................... 29 + +Variable-Length-Code LZW Compression....................................... 30 + +On-line Capabilities Dialogue.............................................. 33 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + +1. Disclaimer. + +The information provided herein is subject to change without notice. In no +event will CompuServe Incorporated be liable for damages, including any loss of +revenue, loss of profits or other incidental or consequential damages arising +out of the use or inability to use the information; CompuServe Incorporated +makes no claim as to the suitability of the information. + + +2. Foreword. + +This document defines the Graphics Interchange Format(sm). The specification +given here defines version 89a, which is an extension of version 87a. + +The Graphics Interchange Format(sm) as specified here should be considered +complete; any deviation from it should be considered invalid, including but not +limited to, the use of reserved or undefined fields within control or data +blocks, the inclusion of extraneous data within or between blocks, the use of +methods or algorithms not specifically listed as part of the format, etc. In +general, any and all deviations, extensions or modifications not specified in +this document should be considered to be in violation of the format and should +be avoided. + + +3. Licensing. + +The Graphics Interchange Format(c) is the copyright property of CompuServe +Incorporated. Only CompuServe Incorporated is authorized to define, redefine, +enhance, alter, modify or change in any way the definition of the format. + +CompuServe Incorporated hereby grants a limited, non-exclusive, royalty-free +license for the use of the Graphics Interchange Format(sm) in computer +software; computer software utilizing GIF(sm) must acknowledge ownership of the +Graphics Interchange Format and its Service Mark by CompuServe Incorporated, in +User and Technical Documentation. Computer software utilizing GIF, which is +distributed or may be distributed without User or Technical Documentation must +display to the screen or printer a message acknowledging ownership of the +Graphics Interchange Format and the Service Mark by CompuServe Incorporated; in +this case, the acknowledgement may be displayed in an opening screen or leading +banner, or a closing screen or trailing banner. A message such as the following +may be used: + + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + +For further information, please contact : + + CompuServe Incorporated + Graphics Technology Department + 5000 Arlington Center Boulevard + Columbus, Ohio 43220 + U. S. A. + +CompuServe Incorporated maintains a mailing list with all those individuals and +organizations who wish to receive copies of this document when it is corrected + + + + + + + + 2 + + +or revised. This service is offered free of charge; please provide us with your +mailing address. + + +4. About the Document. + +This document describes in detail the definition of the Graphics Interchange +Format. This document is intended as a programming reference; it is +recommended that the entire document be read carefully before programming, +because of the interdependence of the various parts. There is an individual +section for each of the Format blocks. Within each section, the sub-section +labeled Required Version refers to the version number that an encoder will have +to use if the corresponding block is used in the Data Stream. Within each +section, a diagram describes the individual fields in the block; the diagrams +are drawn vertically; top bytes in the diagram appear first in the Data Stream. +Bits within a byte are drawn most significant on the left end. Multi-byte +numeric fields are ordered Least Significant Byte first. Numeric constants are +represented as Hexadecimal numbers, preceded by "0x". Bit fields within a byte +are described in order from most significant bits to least significant bits. + + +5. General Description. + +The Graphics Interchange Format(sm) defines a protocol intended for the on-line +transmission and interchange of raster graphic data in a way that is +independent of the hardware used in their creation or display. + +The Graphics Interchange Format is defined in terms of blocks and sub-blocks +which contain relevant parameters and data used in the reproduction of a +graphic. A GIF Data Stream is a sequence of protocol blocks and sub-blocks +representing a collection of graphics. In general, the graphics in a Data +Stream are assumed to be related to some degree, and to share some control +information; it is recommended that encoders attempt to group together related +graphics in order to minimize hardware changes during processing and to +minimize control information overhead. For the same reason, unrelated graphics +or graphics which require resetting hardware parameters should be encoded +separately to the extent possible. + +A Data Stream may originate locally, as when read from a file, or it may +originate remotely, as when transmitted over a data communications line. The +Format is defined with the assumption that an error-free Transport Level +Protocol is used for communications; the Format makes no provisions for +error-detection and error-correction. + +The GIF Data Stream must be interpreted in context, that is, the application +program must rely on information external to the Data Stream to invoke the +decoder process. + + +6. Version Numbers. + +The version number in the Header of a Data Stream is intended to identify the +minimum set of capabilities required of a decoder in order to fully process the +Data Stream. An encoder should use the earliest possible version number that +includes all the blocks used in the Data Stream. Within each block section in +this document, there is an entry labeled Required Version which specifies the + + + + + + + + 3 + + +earliest version number that includes the corresponding block. The encoder +should make every attempt to use the earliest version number covering all the +blocks in the Data Stream; the unnecessary use of later version numbers will +hinder processing by some decoders. + + +7. The Encoder. + +The Encoder is the program used to create a GIF Data Stream. From raster data +and other information, the encoder produces the necessary control and data +blocks needed for reproducing the original graphics. + +The encoder has the following primary responsibilities. + + - Include in the Data Stream all the necessary information to + reproduce the graphics. + + - Insure that a Data Stream is labeled with the earliest possible + Version Number that will cover the definition of all the blocks in + it; this is to ensure that the largest number of decoders can + process the Data Stream. + + - Ensure encoding of the graphics in such a way that the decoding + process is optimized. Avoid redundant information as much as + possible. + + - To the extent possible, avoid grouping graphics which might + require resetting hardware parameters during the decoding process. + + - Set to zero (off) each of the bits of each and every field + designated as reserved. Note that some fields in the Logical Screen + Descriptor and the Image Descriptor were reserved under Version + 87a, but are used under version 89a. + + +8. The Decoder. + +The Decoder is the program used to process a GIF Data Stream. It processes the +Data Stream sequentially, parsing the various blocks and sub-blocks, using the +control information to set hardware and process parameters and interpreting the +data to render the graphics. + +The decoder has the following primary responsibilities. + + - Process each graphic in the Data Stream in sequence, without + delays other than those specified in the control information. + + - Set its hardware parameters to fit, as closely as possible, the + control information contained in the Data Stream. + + +9. Compliance. + +An encoder or a decoder is said to comply with a given version of the Graphics +Interchange Format if and only if it fully conforms with and correctly +implements the definition of the standard associated with that version. An + + + + + + + + 4 + + +encoder or a decoder may be compliant with a given version number and not +compliant with some subsequent version. + + +10. About Recommendations. + +Each block section in this document contains an entry labeled Recommendation; +this section lists a set of recommendations intended to guide and organize the +use of the particular blocks. Such recommendations are geared towards making +the functions of encoders and decoders more efficient, as well as making +optimal use of the communications bandwidth. It is advised that these +recommendations be followed. + + +11. About Color Tables. + +The GIF format utilizes color tables to render raster-based graphics. A color +table can have one of two different scopes: global or local. A Global Color +Table is used by all those graphics in the Data Stream which do not have a +Local Color Table associated with them. The scope of the Global Color Table is +the entire Data Stream. A Local Color Table is always associated with the +graphic that immediately follows it; the scope of a Local Color Table is +limited to that single graphic. A Local Color Table supersedes a Global Color +Table, that is, if a Data Stream contains a Global Color Table, and an image +has a Local Color Table associated with it, the decoder must save the Global +Color Table, use the Local Color Table to render the image, and then restore +the Global Color Table. Both types of color tables are optional, making it +possible for a Data Stream to contain numerous graphics without a color table +at all. For this reason, it is recommended that the decoder save the last +Global Color Table used until another Global Color Table is encountered. In +this way, a Data Stream which does not contain either a Global Color Table or +a Local Color Table may be processed using the last Global Color Table saved. +If a Global Color Table from a previous Stream is used, that table becomes the +Global Color Table of the present Stream. This is intended to reduce the +overhead incurred by color tables. In particular, it is recommended that an +encoder use only one Global Color Table if all the images in related Data +Streams can be rendered with the same table. If no color table is available at +all, the decoder is free to use a system color table or a table of its own. In +that case, the decoder may use a color table with as many colors as its +hardware is able to support; it is recommended that such a table have black and +white as its first two entries, so that monochrome images can be rendered +adequately. + +The Definition of the GIF Format allows for a Data Stream to contain only the +Header, the Logical Screen Descriptor, a Global Color Table and the GIF +Trailer. Such a Data Stream would be used to load a decoder with a Global Color +Table, in preparation for subsequent Data Streams without a color table at all. + + +12. Blocks, Extensions and Scope. + +Blocks can be classified into three groups : Control, Graphic-Rendering and +Special Purpose. Control blocks, such as the Header, the Logical Screen +Descriptor, the Graphic Control Extension and the Trailer, contain information +used to control the process of the Data Stream or information used in setting +hardware parameters. Graphic-Rendering blocks such as the Image Descriptor and + + + + + + + + 5 + + +the Plain Text Extension contain information and data used to render a graphic +on the display device. Special Purpose blocks such as the Comment Extension and +the Application Extension are neither used to control the process of the Data +Stream nor do they contain information or data used to render a graphic on the +display device. With the exception of the Logical Screen Descriptor and the +Global Color Table, whose scope is the entire Data Stream, all other Control +blocks have a limited scope, restricted to the Graphic-Rendering block that +follows them. Special Purpose blocks do not delimit the scope of any Control +blocks; Special Purpose blocks are transparent to the decoding process. +Graphic-Rendering blocks and extensions are used as scope delimiters for +Control blocks and extensions. The labels used to identify labeled blocks fall +into three ranges : 0x00-0x7F (0-127) are the Graphic Rendering blocks, +excluding the Trailer (0x3B); 0x80-0xF9 (128-249) are the Control blocks; +0xFA-0xFF (250-255) are the Special Purpose blocks. These ranges are defined so +that decoders can handle block scope by appropriately identifying block labels, +even when the block itself cannot be processed. + + +13. Block Sizes. + +The Block Size field in a block, counts the number of bytes remaining in the +block, not counting the Block Size field itself, and not counting the Block +Terminator, if one is to follow. Blocks other than Data Blocks are intended to +be of fixed length; the Block Size field is provided in order to facilitate +skipping them, not to allow their size to change in the future. Data blocks +and sub-blocks are of variable length to accommodate the amount of data. + + +14. Using GIF as an embedded protocol. + +As an embedded protocol, GIF may be part of larger application protocols, +within which GIF is used to render graphics. In such a case, the application +protocol could define a block within which the GIF Data Stream would be +contained. The application program would then invoke a GIF decoder upon +encountering a block of type GIF. This approach is recommended in favor of +using Application Extensions, which become overhead for all other applications +that do not process them. Because a GIF Data Stream must be processed in +context, the application must rely on some means of identifying the GIF Data +Stream outside of the Stream itself. + + +15. Data Sub-blocks. + + a. Description. Data Sub-blocks are units containing data. They do not + have a label, these blocks are processed in the context of control + blocks, wherever data blocks are specified in the format. The first byte + of the Data sub-block indicates the number of data bytes to follow. A + data sub-block may contain from 0 to 255 data bytes. The size of the + block does not account for the size byte itself, therefore, the empty + sub-block is one whose size field contains 0x00. + + b. Required Version. 87a. + + + + + + + + + + + + 6 + + + c. Syntax. + + 7 6 5 4 3 2 1 0 Field Name Type + +---------------+ + 0 | | Block Size Byte + +---------------+ + 1 | | + +- -+ + 2 | | + +- -+ + 3 | | + +- -+ + | | Data Values Byte + +- -+ + up | | + +- . . . . -+ + to | | + +- -+ + | | + +- -+ +255 | | + +---------------+ + + i) Block Size - Number of bytes in the Data Sub-block; the size + must be within 0 and 255 bytes, inclusive. + + ii) Data Values - Any 8-bit value. There must be exactly as many + Data Values as specified by the Block Size field. + + d. Extensions and Scope. This type of block always occurs as part of a + larger unit. It does not have a scope of itself. + + e. Recommendation. None. + + +16. Block Terminator. + + a. Description. This zero-length Data Sub-block is used to terminate a + sequence of Data Sub-blocks. It contains a single byte in the position of + the Block Size field and does not contain data. + + b. Required Version. 87a. + + c. Syntax. + + 7 6 5 4 3 2 1 0 Field Name Type + +---------------+ + 0 | | Block Size Byte + +---------------+ + + i) Block Size - Number of bytes in the Data Sub-block; this field + contains the fixed value 0x00. + + ii) Data Values - This block does not contain any data. + + + + + + + + + + 7 + + + d. Extensions and Scope. This block terminates the immediately preceding + sequence of Data Sub-blocks. This block cannot be modified by any + extension. + + e. Recommendation. None. + + +17. Header. + + a. Description. The Header identifies the GIF Data Stream in context. The + Signature field marks the beginning of the Data Stream, and the Version + field identifies the set of capabilities required of a decoder to fully + process the Data Stream. This block is REQUIRED; exactly one Header must + be present per Data Stream. + + b. Required Version. Not applicable. This block is not subject to a + version number. This block must appear at the beginning of every Data + Stream. + + c. Syntax. + + + 7 6 5 4 3 2 1 0 Field Name Type + +---------------+ + 0 | | Signature 3 Bytes + +- -+ + 1 | | + +- -+ + 2 | | + +---------------+ + 3 | | Version 3 Bytes + +- -+ + 4 | | + +- -+ + 5 | | + +---------------+ + + i) Signature - Identifies the GIF Data Stream. This field contains + the fixed value 'GIF'. + + ii) Version - Version number used to format the data stream. + Identifies the minimum set of capabilities necessary to a decoder + to fully process the contents of the Data Stream. + + Version Numbers as of 10 July 1990 : "87a" - May 1987 + "89a" - July 1989 + + Version numbers are ordered numerically increasing on the first two + digits starting with 87 (87,88,...,99,00,...,85,86) and + alphabetically increasing on the third character (a,...,z). + + iii) Extensions and Scope. The scope of this block is the entire + Data Stream. This block cannot be modified by any extension. + + + + + + + + + + + 8 + + + d. Recommendations. + + i) Signature - This field identifies the beginning of the GIF Data + Stream; it is not intended to provide a unique signature for the + identification of the data. It is recommended that the GIF Data + Stream be identified externally by the application. (Refer to + Appendix G for on-line identification of the GIF Data Stream.) + + ii) Version - ENCODER : An encoder should use the earliest possible + version number that defines all the blocks used in the Data Stream. + When two or more Data Streams are combined, the latest of the + individual version numbers should be used for the resulting Data + Stream. DECODER : A decoder should attempt to process the data + stream to the best of its ability; if it encounters a version + number which it is not capable of processing fully, it should + nevertheless, attempt to process the data stream to the best of its + ability, perhaps after warning the user that the data may be + incomplete. + + +18. Logical Screen Descriptor. + + a. Description. The Logical Screen Descriptor contains the parameters + necessary to define the area of the display device within which the + images will be rendered. The coordinates in this block are given with + respect to the top-left corner of the virtual screen; they do not + necessarily refer to absolute coordinates on the display device. This + implies that they could refer to window coordinates in a window-based + environment or printer coordinates when a printer is used. + + This block is REQUIRED; exactly one Logical Screen Descriptor must be + present per Data Stream. + + b. Required Version. Not applicable. This block is not subject to a + version number. This block must appear immediately after the Header. + + c. Syntax. + + 7 6 5 4 3 2 1 0 Field Name Type + +---------------+ + 0 | | Logical Screen Width Unsigned + +- -+ + 1 | | + +---------------+ + 2 | | Logical Screen Height Unsigned + +- -+ + 3 | | + +---------------+ + 4 | | | | | See below + +---------------+ + 5 | | Background Color Index Byte + +---------------+ + 6 | | Pixel Aspect Ratio Byte + +---------------+ + + + + + + + + + + 9 + + + = Global Color Table Flag 1 Bit + Color Resolution 3 Bits + Sort Flag 1 Bit + Size of Global Color Table 3 Bits + + i) Logical Screen Width - Width, in pixels, of the Logical Screen + where the images will be rendered in the displaying device. + + ii) Logical Screen Height - Height, in pixels, of the Logical + Screen where the images will be rendered in the displaying device. + + iii) Global Color Table Flag - Flag indicating the presence of a + Global Color Table; if the flag is set, the Global Color Table will + immediately follow the Logical Screen Descriptor. This flag also + selects the interpretation of the Background Color Index; if the + flag is set, the value of the Background Color Index field should + be used as the table index of the background color. (This field is + the most significant bit of the byte.) + + Values : 0 - No Global Color Table follows, the Background + Color Index field is meaningless. + 1 - A Global Color Table will immediately follow, the + Background Color Index field is meaningful. + + iv) Color Resolution - Number of bits per primary color available + to the original image, minus 1. This value represents the size of + the entire palette from which the colors in the graphic were + selected, not the number of colors actually used in the graphic. + For example, if the value in this field is 3, then the palette of + the original image had 4 bits per primary color available to create + the image. This value should be set to indicate the richness of + the original palette, even if not every color from the whole + palette is available on the source machine. + + v) Sort Flag - Indicates whether the Global Color Table is sorted. + If the flag is set, the Global Color Table is sorted, in order of + decreasing importance. Typically, the order would be decreasing + frequency, with most frequent color first. This assists a decoder, + with fewer available colors, in choosing the best subset of colors; + the decoder may use an initial segment of the table to render the + graphic. + + Values : 0 - Not ordered. + 1 - Ordered by decreasing importance, most + important color first. + + vi) Size of Global Color Table - If the Global Color Table Flag is + set to 1, the value in this field is used to calculate the number + of bytes contained in the Global Color Table. To determine that + actual size of the color table, raise 2 to [the value of the field + + 1]. Even if there is no Global Color Table specified, set this + field according to the above formula so that decoders can choose + the best graphics mode to display the stream in. (This field is + made up of the 3 least significant bits of the byte.) + + vii) Background Color Index - Index into the Global Color Table for + + + + + + + + 10 + + + the Background Color. The Background Color is the color used for + those pixels on the screen that are not covered by an image. If the + Global Color Table Flag is set to (zero), this field should be zero + and should be ignored. + + viii) Pixel Aspect Ratio - Factor used to compute an approximation + of the aspect ratio of the pixel in the original image. If the + value of the field is not 0, this approximation of the aspect ratio + is computed based on the formula: + + Aspect Ratio = (Pixel Aspect Ratio + 15) / 64 + + The Pixel Aspect Ratio is defined to be the quotient of the pixel's + width over its height. The value range in this field allows + specification of the widest pixel of 4:1 to the tallest pixel of + 1:4 in increments of 1/64th. + + Values : 0 - No aspect ratio information is given. + 1..255 - Value used in the computation. + + d. Extensions and Scope. The scope of this block is the entire Data + Stream. This block cannot be modified by any extension. + + e. Recommendations. None. + + +19. Global Color Table. + + a. Description. This block contains a color table, which is a sequence of + bytes representing red-green-blue color triplets. The Global Color Table + is used by images without a Local Color Table and by Plain Text + Extensions. Its presence is marked by the Global Color Table Flag being + set to 1 in the Logical Screen Descriptor; if present, it immediately + follows the Logical Screen Descriptor and contains a number of bytes + equal to + 3 x 2^(Size of Global Color Table+1). + + This block is OPTIONAL; at most one Global Color Table may be present + per Data Stream. + + b. Required Version. 87a + + + + + + + + + + + + + + + + + + + + + + + 11 + + + c. Syntax. + + 7 6 5 4 3 2 1 0 Field Name Type + +===============+ + 0 | | Red 0 Byte + +- -+ + 1 | | Green 0 Byte + +- -+ + 2 | | Blue 0 Byte + +- -+ + 3 | | Red 1 Byte + +- -+ + | | Green 1 Byte + +- -+ + up | | + +- . . . . -+ ... + to | | + +- -+ + | | Green 255 Byte + +- -+ +767 | | Blue 255 Byte + +===============+ + + + d. Extensions and Scope. The scope of this block is the entire Data + Stream. This block cannot be modified by any extension. + + e. Recommendation. None. + + +20. Image Descriptor. + + a. Description. Each image in the Data Stream is composed of an Image + Descriptor, an optional Local Color Table, and the image data. Each + image must fit within the boundaries of the Logical Screen, as defined + in the Logical Screen Descriptor. + + The Image Descriptor contains the parameters necessary to process a table + based image. The coordinates given in this block refer to coordinates + within the Logical Screen, and are given in pixels. This block is a + Graphic-Rendering Block, optionally preceded by one or more Control + blocks such as the Graphic Control Extension, and may be optionally + followed by a Local Color Table; the Image Descriptor is always followed + by the image data. + + This block is REQUIRED for an image. Exactly one Image Descriptor must + be present per image in the Data Stream. An unlimited number of images + may be present per Data Stream. + + b. Required Version. 87a. + + + + + + + + + + + + + + 12 + + + c. Syntax. + + 7 6 5 4 3 2 1 0 Field Name Type + +---------------+ + 0 | | Image Separator Byte + +---------------+ + 1 | | Image Left Position Unsigned + +- -+ + 2 | | + +---------------+ + 3 | | Image Top Position Unsigned + +- -+ + 4 | | + +---------------+ + 5 | | Image Width Unsigned + +- -+ + 6 | | + +---------------+ + 7 | | Image Height Unsigned + +- -+ + 8 | | + +---------------+ + 9 | | | | | | See below + +---------------+ + + = Local Color Table Flag 1 Bit + Interlace Flag 1 Bit + Sort Flag 1 Bit + Reserved 2 Bits + Size of Local Color Table 3 Bits + + i) Image Separator - Identifies the beginning of an Image + Descriptor. This field contains the fixed value 0x2C. + + ii) Image Left Position - Column number, in pixels, of the left edge + of the image, with respect to the left edge of the Logical Screen. + Leftmost column of the Logical Screen is 0. + + iii) Image Top Position - Row number, in pixels, of the top edge of + the image with respect to the top edge of the Logical Screen. Top + row of the Logical Screen is 0. + + iv) Image Width - Width of the image in pixels. + + v) Image Height - Height of the image in pixels. + + vi) Local Color Table Flag - Indicates the presence of a Local Color + Table immediately following this Image Descriptor. (This field is + the most significant bit of the byte.) + + + Values : 0 - Local Color Table is not present. Use + Global Color Table if available. + 1 - Local Color Table present, and to follow + immediately after this Image Descriptor. + + + + + + + + + 13 + + + vii) Interlace Flag - Indicates if the image is interlaced. An image + is interlaced in a four-pass interlace pattern; see Appendix E for + details. + + Values : 0 - Image is not interlaced. + 1 - Image is interlaced. + + viii) Sort Flag - Indicates whether the Local Color Table is + sorted. If the flag is set, the Local Color Table is sorted, in + order of decreasing importance. Typically, the order would be + decreasing frequency, with most frequent color first. This assists + a decoder, with fewer available colors, in choosing the best subset + of colors; the decoder may use an initial segment of the table to + render the graphic. + + Values : 0 - Not ordered. + 1 - Ordered by decreasing importance, most + important color first. + + ix) Size of Local Color Table - If the Local Color Table Flag is + set to 1, the value in this field is used to calculate the number + of bytes contained in the Local Color Table. To determine that + actual size of the color table, raise 2 to the value of the field + + 1. This value should be 0 if there is no Local Color Table + specified. (This field is made up of the 3 least significant bits + of the byte.) + + d. Extensions and Scope. The scope of this block is the Table-based Image + Data Block that follows it. This block may be modified by the Graphic + Control Extension. + + e. Recommendation. None. + + +21. Local Color Table. + + a. Description. This block contains a color table, which is a sequence of + bytes representing red-green-blue color triplets. The Local Color Table + is used by the image that immediately follows. Its presence is marked by + the Local Color Table Flag being set to 1 in the Image Descriptor; if + present, the Local Color Table immediately follows the Image Descriptor + and contains a number of bytes equal to + 3x2^(Size of Local Color Table+1). + If present, this color table temporarily becomes the active color table + and the following image should be processed using it. This block is + OPTIONAL; at most one Local Color Table may be present per Image + Descriptor and its scope is the single image associated with the Image + Descriptor that precedes it. + + b. Required Version. 87a. + + + + + + + + + + + + + + 14 + + + c. Syntax. + + 7 6 5 4 3 2 1 0 Field Name Type + +===============+ + 0 | | Red 0 Byte + +- -+ + 1 | | Green 0 Byte + +- -+ + 2 | | Blue 0 Byte + +- -+ + 3 | | Red 1 Byte + +- -+ + | | Green 1 Byte + +- -+ + up | | + +- . . . . -+ ... + to | | + +- -+ + | | Green 255 Byte + +- -+ +767 | | Blue 255 Byte + +===============+ + + + d. Extensions and Scope. The scope of this block is the Table-based Image + Data Block that immediately follows it. This block cannot be modified by + any extension. + + e. Recommendations. None. + + +22. Table Based Image Data. + + a. Description. The image data for a table based image consists of a + sequence of sub-blocks, of size at most 255 bytes each, containing an + index into the active color table, for each pixel in the image. Pixel + indices are in order of left to right and from top to bottom. Each index + must be within the range of the size of the active color table, starting + at 0. The sequence of indices is encoded using the LZW Algorithm with + variable-length code, as described in Appendix F + + b. Required Version. 87a. + + c. Syntax. The image data format is as follows: + + 7 6 5 4 3 2 1 0 Field Name Type + +---------------+ + | | LZW Minimum Code Size Byte + +---------------+ + + +===============+ + | | + / / Image Data Data Sub-blocks + | | + +===============+ + + + + + + + + + 15 + + + i) LZW Minimum Code Size. This byte determines the initial number + of bits used for LZW codes in the image data, as described in + Appendix F. + + d. Extensions and Scope. This block has no scope, it contains raster + data. Extensions intended to modify a Table-based image must appear + before the corresponding Image Descriptor. + + e. Recommendations. None. + + +23. Graphic Control Extension. + + a. Description. The Graphic Control Extension contains parameters used + when processing a graphic rendering block. The scope of this extension is + the first graphic rendering block to follow. The extension contains only + one data sub-block. + + This block is OPTIONAL; at most one Graphic Control Extension may precede + a graphic rendering block. This is the only limit to the number of + Graphic Control Extensions that may be contained in a Data Stream. + + b. Required Version. 89a. + + c. Syntax. + + 7 6 5 4 3 2 1 0 Field Name Type + +---------------+ + 0 | | Extension Introducer Byte + +---------------+ + 1 | | Graphic Control Label Byte + +---------------+ + + +---------------+ + 0 | | Block Size Byte + +---------------+ + 1 | | | | | See below + +---------------+ + 2 | | Delay Time Unsigned + +- -+ + 3 | | + +---------------+ + 4 | | Transparent Color Index Byte + +---------------+ + + +---------------+ + 0 | | Block Terminator Byte + +---------------+ + + + = Reserved 3 Bits + Disposal Method 3 Bits + User Input Flag 1 Bit + Transparent Color Flag 1 Bit + + i) Extension Introducer - Identifies the beginning of an extension + + + + + + + + 16 + + + block. This field contains the fixed value 0x21. + + ii) Graphic Control Label - Identifies the current block as a + Graphic Control Extension. This field contains the fixed value + 0xF9. + + iii) Block Size - Number of bytes in the block, after the Block + Size field and up to but not including the Block Terminator. This + field contains the fixed value 4. + + iv) Disposal Method - Indicates the way in which the graphic is to + be treated after being displayed. + + Values : 0 - No disposal specified. The decoder is + not required to take any action. + 1 - Do not dispose. The graphic is to be left + in place. + 2 - Restore to background color. The area used by the + graphic must be restored to the background color. + 3 - Restore to previous. The decoder is required to + restore the area overwritten by the graphic with + what was there prior to rendering the graphic. + 4-7 - To be defined. + + v) User Input Flag - Indicates whether or not user input is + expected before continuing. If the flag is set, processing will + continue when user input is entered. The nature of the User input + is determined by the application (Carriage Return, Mouse Button + Click, etc.). + + Values : 0 - User input is not expected. + 1 - User input is expected. + + When a Delay Time is used and the User Input Flag is set, + processing will continue when user input is received or when the + delay time expires, whichever occurs first. + + vi) Transparency Flag - Indicates whether a transparency index is + given in the Transparent Index field. (This field is the least + significant bit of the byte.) + + Values : 0 - Transparent Index is not given. + 1 - Transparent Index is given. + + vii) Delay Time - If not 0, this field specifies the number of + hundredths (1/100) of a second to wait before continuing with the + processing of the Data Stream. The clock starts ticking immediately + after the graphic is rendered. This field may be used in + conjunction with the User Input Flag field. + + viii) Transparency Index - The Transparency Index is such that when + encountered, the corresponding pixel of the display device is not + modified and processing goes on to the next pixel. The index is + present if and only if the Transparency Flag is set to 1. + + ix) Block Terminator - This zero-length data block marks the end of + + + + + + + + 17 + + the Graphic Control Extension. + + d. Extensions and Scope. The scope of this Extension is the graphic + rendering block that follows it; it is possible for other extensions to + be present between this block and its target. This block can modify the + Image Descriptor Block and the Plain Text Extension. + + e. Recommendations. + + i) Disposal Method - The mode Restore To Previous is intended to be + used in small sections of the graphic; the use of this mode imposes + severe demands on the decoder to store the section of the graphic + that needs to be saved. For this reason, this mode should be used + sparingly. This mode is not intended to save an entire graphic or + large areas of a graphic; when this is the case, the encoder should + make every attempt to make the sections of the graphic to be + restored be separate graphics in the data stream. In the case where + a decoder is not capable of saving an area of a graphic marked as + Restore To Previous, it is recommended that a decoder restore to + the background color. + + ii) User Input Flag - When the flag is set, indicating that user + input is expected, the decoder may sound the bell (0x07) to alert + the user that input is being expected. In the absence of a + specified Delay Time, the decoder should wait for user input + indefinitely. It is recommended that the encoder not set the User + Input Flag without a Delay Time specified. + + +24. Comment Extension. + + a. Description. The Comment Extension contains textual information which + is not part of the actual graphics in the GIF Data Stream. It is suitable + for including comments about the graphics, credits, descriptions or any + other type of non-control and non-graphic data. The Comment Extension + may be ignored by the decoder, or it may be saved for later processing; + under no circumstances should a Comment Extension disrupt or interfere + with the processing of the Data Stream. + + This block is OPTIONAL; any number of them may appear in the Data Stream. + + b. Required Version. 89a. + + + + + + + + + + + + + + + + + + + + + + + 18 + + + c. Syntax. + + 7 6 5 4 3 2 1 0 Field Name Type + +---------------+ + 0 | | Extension Introducer Byte + +---------------+ + 1 | | Comment Label Byte + +---------------+ + + +===============+ + | | + N | | Comment Data Data Sub-blocks + | | + +===============+ + + +---------------+ + 0 | | Block Terminator Byte + +---------------+ + + i) Extension Introducer - Identifies the beginning of an extension + block. This field contains the fixed value 0x21. + + ii) Comment Label - Identifies the block as a Comment Extension. + This field contains the fixed value 0xFE. + + iii) Comment Data - Sequence of sub-blocks, each of size at most + 255 bytes and at least 1 byte, with the size in a byte preceding + the data. The end of the sequence is marked by the Block + Terminator. + + iv) Block Terminator - This zero-length data block marks the end of + the Comment Extension. + + d. Extensions and Scope. This block does not have scope. This block + cannot be modified by any extension. + + e. Recommendations. + + i) Data - This block is intended for humans. It should contain + text using the 7-bit ASCII character set. This block should + not be used to store control information for custom processing. + + ii) Position - This block may appear at any point in the Data + Stream at which a block can begin; however, it is recommended that + Comment Extensions do not interfere with Control or Data blocks; + they should be located at the beginning or at the end of the Data + Stream to the extent possible. + + +25. Plain Text Extension. + + a. Description. The Plain Text Extension contains textual data and the + parameters necessary to render that data as a graphic, in a simple form. + The textual data will be encoded with the 7-bit printable ASCII + characters. Text data are rendered using a grid of character cells + + + + + + + + + 19 + + + defined by the parameters in the block fields. Each character is rendered + in an individual cell. The textual data in this block is to be rendered + as mono-spaced characters, one character per cell, with a best fitting + font and size. For further information, see the section on + Recommendations below. The data characters are taken sequentially from + the data portion of the block and rendered within a cell, starting with + the upper left cell in the grid and proceeding from left to right and + from top to bottom. Text data is rendered until the end of data is + reached or the character grid is filled. The Character Grid contains an + integral number of cells; in the case that the cell dimensions do not + allow for an integral number, fractional cells must be discarded; an + encoder must be careful to specify the grid dimensions accurately so that + this does not happen. This block requires a Global Color Table to be + available; the colors used by this block reference the Global Color Table + in the Stream if there is one, or the Global Color Table from a previous + Stream, if one was saved. This block is a graphic rendering block, + therefore it may be modified by a Graphic Control Extension. This block + is OPTIONAL; any number of them may appear in the Data Stream. + + b. Required Version. 89a. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 20 + + + c. Syntax. + + 7 6 5 4 3 2 1 0 Field Name Type + +---------------+ + 0 | | Extension Introducer Byte + +---------------+ + 1 | | Plain Text Label Byte + +---------------+ + + +---------------+ + 0 | | Block Size Byte + +---------------+ + 1 | | Text Grid Left Position Unsigned + +- -+ + 2 | | + +---------------+ + 3 | | Text Grid Top Position Unsigned + +- -+ + 4 | | + +---------------+ + 5 | | Text Grid Width Unsigned + +- -+ + 6 | | + +---------------+ + 7 | | Text Grid Height Unsigned + +- -+ + 8 | | + +---------------+ + 9 | | Character Cell Width Byte + +---------------+ + 10 | | Character Cell Height Byte + +---------------+ + 11 | | Text Foreground Color Index Byte + +---------------+ + 12 | | Text Background Color Index Byte + +---------------+ + + +===============+ + | | + N | | Plain Text Data Data Sub-blocks + | | + +===============+ + + +---------------+ + 0 | | Block Terminator Byte + +---------------+ + + i) Extension Introducer - Identifies the beginning of an extension + block. This field contains the fixed value 0x21. + + ii) Plain Text Label - Identifies the current block as a Plain Text + Extension. This field contains the fixed value 0x01. + + iii) Block Size - Number of bytes in the extension, after the Block + Size field and up to but not including the beginning of the data + portion. This field contains the fixed value 12. + + + + + + + + 21 + + + iv) Text Grid Left Position - Column number, in pixels, of the left + edge of the text grid, with respect to the left edge of the Logical + Screen. + + v) Text Grid Top Position - Row number, in pixels, of the top edge + of the text grid, with respect to the top edge of the Logical + Screen. + + vi) Image Grid Width - Width of the text grid in pixels. + + vii) Image Grid Height - Height of the text grid in pixels. + + viii) Character Cell Width - Width, in pixels, of each cell in the + grid. + + ix) Character Cell Height - Height, in pixels, of each cell in the + grid. + + x) Text Foreground Color Index - Index into the Global Color Table + to be used to render the text foreground. + + xi) Text Background Color Index - Index into the Global Color Table + to be used to render the text background. + + xii) Plain Text Data - Sequence of sub-blocks, each of size at most + 255 bytes and at least 1 byte, with the size in a byte preceding + the data. The end of the sequence is marked by the Block + Terminator. + + xiii) Block Terminator - This zero-length data block marks the end + of the Plain Text Data Blocks. + + d. Extensions and Scope. The scope of this block is the Plain Text Data + Block contained in it. This block may be modified by the Graphic Control + Extension. + + e. Recommendations. The data in the Plain Text Extension is assumed to be + preformatted. The selection of font and size is left to the discretion of + the decoder. If characters less than 0x20 or greater than 0xf7 are + encountered, it is recommended that the decoder display a Space character + (0x20). The encoder should use grid and cell dimensions such that an + integral number of cells fit in the grid both horizontally as well as + vertically. For broadest compatibility, character cell dimensions should + be around 8x8 or 8x16 (width x height); consider an image for unusual + sized text. + + +26. Application Extension. + + a. Description. The Application Extension contains application-specific + information; it conforms with the extension block syntax, as described + below, and its block label is 0xFF. + + b. Required Version. 89a. + + + + + + + + + + 22 + + + c. Syntax. + + 7 6 5 4 3 2 1 0 Field Name Type + +---------------+ + 0 | | Extension Introducer Byte + +---------------+ + 1 | | Extension Label Byte + +---------------+ + + +---------------+ + 0 | | Block Size Byte + +---------------+ + 1 | | + +- -+ + 2 | | + +- -+ + 3 | | Application Identifier 8 Bytes + +- -+ + 4 | | + +- -+ + 5 | | + +- -+ + 6 | | + +- -+ + 7 | | + +- -+ + 8 | | + +---------------+ + 9 | | + +- -+ + 10 | | Appl. Authentication Code 3 Bytes + +- -+ + 11 | | + +---------------+ + + +===============+ + | | + | | Application Data Data Sub-blocks + | | + | | + +===============+ + + +---------------+ + 0 | | Block Terminator Byte + +---------------+ + + i) Extension Introducer - Defines this block as an extension. This + field contains the fixed value 0x21. + + ii) Application Extension Label - Identifies the block as an + Application Extension. This field contains the fixed value 0xFF. + + iii) Block Size - Number of bytes in this extension block, + following the Block Size field, up to but not including the + beginning of the Application Data. This field contains the fixed + value 11. + + + + + + + + 23 + + + iv) Application Identifier - Sequence of eight printable ASCII + characters used to identify the application owning the Application + Extension. + + v) Application Authentication Code - Sequence of three bytes used + to authenticate the Application Identifier. An Application program + may use an algorithm to compute a binary code that uniquely + identifies it as the application owning the Application Extension. + + + d. Extensions and Scope. This block does not have scope. This block + cannot be modified by any extension. + + e. Recommendation. None. + + +27. Trailer. + + a. Description. This block is a single-field block indicating the end of + the GIF Data Stream. It contains the fixed value 0x3B. + + b. Required Version. 87a. + + c. Syntax. + + 7 6 5 4 3 2 1 0 Field Name Type + +---------------+ + 0 | | GIF Trailer Byte + +---------------+ + + d. Extensions and Scope. This block does not have scope, it terminates + the GIF Data Stream. This block may not be modified by any extension. + + e. Recommendations. None. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 24 + + +Appendix +A. Quick Reference Table. + +Block Name Required Label Ext. Vers. +Application Extension Opt. (*) 0xFF (255) yes 89a +Comment Extension Opt. (*) 0xFE (254) yes 89a +Global Color Table Opt. (1) none no 87a +Graphic Control Extension Opt. (*) 0xF9 (249) yes 89a +Header Req. (1) none no N/A +Image Descriptor Opt. (*) 0x2C (044) no 87a (89a) +Local Color Table Opt. (*) none no 87a +Logical Screen Descriptor Req. (1) none no 87a (89a) +Plain Text Extension Opt. (*) 0x01 (001) yes 89a +Trailer Req. (1) 0x3B (059) no 87a + +Unlabeled Blocks +Header Req. (1) none no N/A +Logical Screen Descriptor Req. (1) none no 87a (89a) +Global Color Table Opt. (1) none no 87a +Local Color Table Opt. (*) none no 87a + +Graphic-Rendering Blocks +Plain Text Extension Opt. (*) 0x01 (001) yes 89a +Image Descriptor Opt. (*) 0x2C (044) no 87a (89a) + +Control Blocks +Graphic Control Extension Opt. (*) 0xF9 (249) yes 89a + +Special Purpose Blocks +Trailer Req. (1) 0x3B (059) no 87a +Comment Extension Opt. (*) 0xFE (254) yes 89a +Application Extension Opt. (*) 0xFF (255) yes 89a + +legend: (1) if present, at most one occurrence + (*) zero or more occurrences + (+) one or more occurrences + +Notes : The Header is not subject to Version Numbers. +(89a) The Logical Screen Descriptor and the Image Descriptor retained their +syntax from version 87a to version 89a, but some fields reserved under version +87a are used under version 89a. + + + + + + + + + + + + + + + + + + + + + + + 25 + + +Appendix +B. GIF Grammar. + +A Grammar is a form of notation to represent the sequence in which certain +objects form larger objects. A grammar is also used to represent the number of +objects that can occur at a given position. The grammar given here represents +the sequence of blocks that form the GIF Data Stream. A grammar is given by +listing its rules. Each rule consists of the left-hand side, followed by some +form of equals sign, followed by the right-hand side. In a rule, the +right-hand side describes how the left-hand side is defined. The right-hand +side consists of a sequence of entities, with the possible presence of special +symbols. The following legend defines the symbols used in this grammar for GIF. + +Legend: <> grammar word + ::= defines symbol + * zero or more occurrences + + one or more occurrences + | alternate element + [] optional element + +Example: + + ::= Header * Trailer + +This rule defines the entity as follows. It must begin with a +Header. The Header is followed by an entity called Logical Screen, which is +defined below by another rule. The Logical Screen is followed by the entity +Data, which is also defined below by another rule. Finally, the entity Data is +followed by the Trailer. Since there is no rule defining the Header or the +Trailer, this means that these blocks are defined in the document. The entity +Data has a special symbol (*) following it which means that, at this position, +the entity Data may be repeated any number of times, including 0 times. For +further reading on this subject, refer to a standard text on Programming +Languages. + + +The Grammar. + + ::= Header * Trailer + + ::= Logical Screen Descriptor [Global Color Table] + + ::= | + + + ::= [Graphic Control Extension] + + ::= | + Plain Text Extension + + ::= Image Descriptor [Local Color Table] Image Data + + ::= Application Extension | + Comment Extension + + + + + + + + + + 26 + + +NOTE : The grammar indicates that it is possible for a GIF Data Stream to +contain the Header, the Logical Screen Descriptor, a Global Color Table and the +GIF Trailer. This special case is used to load a GIF decoder with a Global +Color Table, in preparation for subsequent Data Streams without color tables at +all. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 27 + + +Appendix +C. Glossary. + +Active Color Table - Color table used to render the next graphic. If the next +graphic is an image which has a Local Color Table associated with it, the +active color table becomes the Local Color Table associated with that image. +If the next graphic is an image without a Local Color Table, or a Plain Text +Extension, the active color table is the Global Color Table associated with the +Data Stream, if there is one; if there is no Global Color Table in the Data +Stream, the active color table is a color table saved from a previous Data +Stream, or one supplied by the decoder. + +Block - Collection of bytes forming a protocol unit. In general, the term +includes labeled and unlabeled blocks, as well as Extensions. + +Data Stream - The GIF Data Stream is composed of blocks and sub-blocks +representing images and graphics, together with control information to render +them on a display device. All control and data blocks in the Data Stream must +follow the Header and must precede the Trailer. + +Decoder - A program capable of processing a GIF Data Stream to render the +images and graphics contained in it. + +Encoder - A program capable of capturing and formatting image and graphic +raster data, following the definitions of the Graphics Interchange Format. + +Extension - A protocol block labeled by the Extension Introducer 0x21. + +Extension Introducer - Label (0x21) defining an Extension. + +Graphic - Data which can be rendered on the screen by virtue of some algorithm. +The term graphic is more general than the term image; in addition to images, +the term graphic also includes data such as text, which is rendered using +character bit-maps. + +Image - Data representing a picture or a drawing; an image is represented by an +array of pixels called the raster of the image. + +Raster - Array of pixel values representing an image. + + + + + + + + + + + + + + + + + + + + + + + + + 28 + + +Appendix +D. Conventions. + +Animation - The Graphics Interchange Format is not intended as a platform for +animation, even though it can be done in a limited way. + +Byte Ordering - Unless otherwise stated, multi-byte numeric fields are ordered +with the Least Significant Byte first. + +Color Indices - Color indices always refer to the active color table, either +the Global Color Table or the Local Color Table. + +Color Order - Unless otherwise stated, all triple-component RGB color values +are specified in Red-Green-Blue order. + +Color Tables - Both color tables, the Global and the Local, are optional; if +present, the Global Color Table is to be used with every image in the Data +Stream for which a Local Color Table is not given; if present, a Local Color +Table overrides the Global Color Table. However, if neither color table is +present, the application program is free to use an arbitrary color table. If +the graphics in several Data Streams are related and all use the same color +table, an encoder could place the color table as the Global Color Table in the +first Data Stream and leave subsequent Data Streams without a Global Color +Table or any Local Color Tables; in this way, the overhead for the table is +eliminated. It is recommended that the decoder save the previous Global Color +Table to be used with the Data Stream that follows, in case it does not contain +either a Global Color Table or any Local Color Tables. In general, this allows +the application program to use past color tables, significantly reducing +transmission overhead. + +Extension Blocks - Extensions are defined using the Extension Introducer code +to mark the beginning of the block, followed by a block label, identifying the +type of extension. Extension Codes are numbers in the range from 0x00 to 0xFF, +inclusive. Special purpose extensions are transparent to the decoder and may be +omitted when transmitting the Data Stream on-line. The GIF capabilities +dialogue makes the provision for the receiver to request the transmission of +all blocks; the default state in this regard is no transmission of Special +purpose blocks. + +Reserved Fields - All Reserved Fields are expected to have each bit set to zero +(off). + + + + + + + + + + + + + + + + + + + + + + + 29 + + +Appendix +E. Interlaced Images. + +The rows of an Interlaced images are arranged in the following order: + + Group 1 : Every 8th. row, starting with row 0. (Pass 1) + Group 2 : Every 8th. row, starting with row 4. (Pass 2) + Group 3 : Every 4th. row, starting with row 2. (Pass 3) + Group 4 : Every 2nd. row, starting with row 1. (Pass 4) + +The Following example illustrates how the rows of an interlaced image are +ordered. + + Row Number Interlace Pass + + 0 ----------------------------------------- 1 + 1 ----------------------------------------- 4 + 2 ----------------------------------------- 3 + 3 ----------------------------------------- 4 + 4 ----------------------------------------- 2 + 5 ----------------------------------------- 4 + 6 ----------------------------------------- 3 + 7 ----------------------------------------- 4 + 8 ----------------------------------------- 1 + 9 ----------------------------------------- 4 + 10 ----------------------------------------- 3 + 11 ----------------------------------------- 4 + 12 ----------------------------------------- 2 + 13 ----------------------------------------- 4 + 14 ----------------------------------------- 3 + 15 ----------------------------------------- 4 + 16 ----------------------------------------- 1 + 17 ----------------------------------------- 4 + 18 ----------------------------------------- 3 + 19 ----------------------------------------- 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 30 + + +Appendix +F. Variable-Length-Code LZW Compression. + +The Variable-Length-Code LZW Compression is a variation of the Lempel-Ziv +Compression algorithm in which variable-length codes are used to replace +patterns detected in the original data. The algorithm uses a code or +translation table constructed from the patterns encountered in the original +data; each new pattern is entered into the table and its index is used to +replace it in the compressed stream. + +The compressor takes the data from the input stream and builds a code or +translation table with the patterns as it encounters them; each new pattern is +entered into the code table and its index is added to the output stream; when a +pattern is encountered which had been detected since the last code table +refresh, its index from the code table is put on the output stream, thus +achieving the data compression. The expander takes input from the compressed +data stream and builds the code or translation table from it; as the compressed +data stream is processed, codes are used to index into the code table and the +corresponding data is put on the decompressed output stream, thus achieving +data decompression. The details of the algorithm are explained below. The +Variable-Length-Code aspect of the algorithm is based on an initial code size +(LZW-initial code size), which specifies the initial number of bits used for +the compression codes. When the number of patterns detected by the compressor +in the input stream exceeds the number of patterns encodable with the current +number of bits, the number of bits per LZW code is increased by one. + +The Raster Data stream that represents the actual output image can be +represented as: + + 7 6 5 4 3 2 1 0 + +---------------+ + | LZW code size | + +---------------+ + + +---------------+ ----+ + | block size | | + +---------------+ | + | | +-- Repeated as many + | data bytes | | times as necessary. + | | | + +---------------+ ----+ + + . . . . . . ------- The code that terminates the LZW + compressed data must appear before + Block Terminator. + +---------------+ + |0 0 0 0 0 0 0 0| Block Terminator + +---------------+ + +The conversion of the image from a series of pixel values to a transmitted or +stored character stream involves several steps. In brief these steps are: + +1. Establish the Code Size - Define the number of bits needed to represent the +actual data. + +2. Compress the Data - Compress the series of image pixels to a series of + + + + + + + + 31 + + +compression codes. + +3. Build a Series of Bytes - Take the set of compression codes and convert to a +string of 8-bit bytes. + +4. Package the Bytes - Package sets of bytes into blocks preceded by character +counts and output. + +ESTABLISH CODE SIZE + +The first byte of the Compressed Data stream is a value indicating the minimum +number of bits required to represent the set of actual pixel values. Normally +this will be the same as the number of color bits. Because of some algorithmic +constraints however, black & white images which have one color bit must be +indicated as having a code size of 2. +This code size value also implies that the compression codes must start out one +bit longer. + +COMPRESSION + +The LZW algorithm converts a series of data values into a series of codes which +may be raw values or a code designating a series of values. Using text +characters as an analogy, the output code consists of a character or a code +representing a string of characters. + +The LZW algorithm used in GIF matches algorithmically with the standard LZW +algorithm with the following differences: + +1. A special Clear code is defined which resets all compression/decompression +parameters and tables to a start-up state. The value of this code is 2**. For example if the code size indicated was 4 (image was 4 bits/pixel) +the Clear code value would be 16 (10000 binary). The Clear code can appear at +any point in the image data stream and therefore requires the LZW algorithm to +process succeeding codes as if a new data stream was starting. Encoders should +output a Clear code as the first code of each image data stream. + +2. An End of Information code is defined that explicitly indicates the end of +the image data stream. LZW processing terminates when this code is encountered. +It must be the last code output by the encoder for an image. The value of this +code is +1. + +3. The first available compression code value is +2. + +4. The output codes are of variable length, starting at +1 bits per +code, up to 12 bits per code. This defines a maximum code value of 4095 +(0xFFF). Whenever the LZW code value would exceed the current code length, the +code length is increased by one. The packing/unpacking of these codes must then +be altered to reflect the new code length. + +BUILD 8-BIT BYTES + +Because the LZW compression used for GIF creates a series of variable length +codes, of between 3 and 12 bits each, these codes must be reformed into a +series of 8-bit bytes that will be the characters actually stored or +transmitted. This provides additional compression of the image. The codes are +formed into a stream of bits as if they were packed right to left and then + + + + + + + + 32 + + +picked off 8 bits at a time to be output. + +Assuming a character array of 8 bits per character and using 5 bit codes to be +packed, an example layout would be similar to: + + + +---------------+ + 0 | | bbbaaaaa + +---------------+ + 1 | | dcccccbb + +---------------+ + 2 | | eeeedddd + +---------------+ + 3 | | ggfffffe + +---------------+ + 4 | | hhhhhggg + +---------------+ + . . . + +---------------+ + N | | + +---------------+ + + +Note that the physical packing arrangement will change as the number of bits +per compression code change but the concept remains the same. + +PACKAGE THE BYTES + +Once the bytes have been created, they are grouped into blocks for output by +preceding each block of 0 to 255 bytes with a character count byte. A block +with a zero byte count terminates the Raster Data stream for a given image. +These blocks are what are actually output for the GIF image. This block format +has the side effect of allowing a decoding program the ability to read past the +actual image data if necessary by reading block counts and then skipping over +the data. + + + +FURTHER READING + +[1] Ziv, J. and Lempel, A. : "A Universal Algorithm for Sequential Data +Compression", IEEE Transactions on Information Theory, May 1977. +[2] Welch, T. : "A Technique for High-Performance Data Compression", Computer, +June 1984. +[3] Nelson, M.R. : "LZW Data Compression", Dr. Dobb's Journal, October 1989. + + + + + + + + + + + + + + + + + + + 33 + + +Appendix +G. On-line Capabilities Dialogue. + +NOTE : This section is currently (10 July 1990) under revision; the information +provided here should be used as general guidelines. Code written based on this +information should be designed in a flexible way to accommodate any changes +resulting from the revisions. + +The following sequences are defined for use in mediating control between a GIF +sender and GIF receiver over an interactive communications line. These +sequences do not apply to applications that involve downloading of static GIF +files and are not considered part of a GIF file. + +GIF CAPABILITIES ENQUIRY + +The GIF Capabilities Enquiry sequence is issued from a host and requests an +interactive GIF decoder to return a response message that defines the graphics +parameters for the decoder. This involves returning information about available +screen sizes, number of bits/color supported and the amount of color detail +supported. The escape sequence for the GIF Capabilities Enquiry is defined as: + +ESC[>0g 0x1B 0x5B 0x3E 0x30 0x67 + +GIF CAPABILITIES RESPONSE + +The GIF Capabilities Response message is returned by an interactive GIF decoder +and defines the decoder's display capabilities for all graphics modes that are +supported by the software. Note that this can also include graphics printers as +well as a monitor screen. The general format of this message is: + +#version;protocol{;dev, width, height, color-bits, color-res}... + + +'#' GIF Capabilities Response identifier character. +version GIF format version number; initially '87a'. +protocol='0' No end-to-end protocol supported by decoder Transfer as direct + 8-bit data stream. +protocol='1' Can use CIS B+ error correction protocol to transfer GIF data + interactively from the host directly to the display. +dev = '0' Screen parameter set follows. +dev = '1' Printer parameter set follows. +width Maximum supported display width in pixels. +height Maximum supported display height in pixels. +color-bits Number of bits per pixel supported. The number of supported + colors is therefore 2**color-bits. +color-res Number of bits per color component supported in the hardware + color palette. If color-res is '0' then no hardware palette + table is available. + +Note that all values in the GIF Capabilities Response are returned as ASCII +decimal numbers and the message is terminated by a Carriage Return character. + +The following GIF Capabilities Response message describes three standard IBM PC +Enhanced Graphics Adapter configurations with no printer; the GIF data stream + + + + + + + + + + 34 + + +can be processed within an error correcting protocol: + +#87a;1;0,320,200,4,0;0,640,200,2,2;0,640,350,4,2 + +ENTER GIF GRAPHICS MODE + +Two sequences are currently defined to invoke an interactive GIF decoder into +action. The only difference between them is that different output media are +selected. These sequences are: + +ESC[>1g Display GIF image on screen + + 0x1B 0x5B 0x3E 0x31 0x67 + +ESC[>2g Display image directly to an attached graphics printer. The image may +optionally be displayed on the screen as well. + + 0x1B 0x5B 0x3E 0x32 0x67 + +Note that the 'g' character terminating each sequence is in lowercase. + +INTERACTIVE ENVIRONMENT + +The assumed environment for the transmission of GIF image data from an +interactive application is a full 8-bit data stream from host to micro. All +256 character codes must be transferrable. The establishing of an 8-bit data +path for communications will normally be taken care of by the host application +programs. It is however up to the receiving communications programs supporting +GIF to be able to receive and pass on all 256 8-bit codes to the GIF decoder +software. +. diff --git a/ImageSharp/Formats/IAnimatedImageEncoder.cs b/ImageSharp/Formats/IAnimatedImageEncoder.cs new file mode 100644 index 0000000..43d8557 --- /dev/null +++ b/ImageSharp/Formats/IAnimatedImageEncoder.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats { + /// + /// Defines the contract for all image encoders that allow encoding animation sequences. + /// + public interface IAnimatedImageEncoder + { + /// + /// Gets the default background color of the canvas when animating in supported encoders. + /// This color may be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when a frame disposal mode is . + /// + public Color? BackgroundColor { get; } + + /// + /// Gets the number of times any animation is repeated in supported encoders. + /// + public ushort? RepeatCount { get; } + + /// + /// Gets a value indicating whether the root frame is shown as part of the animated sequence in supported encoders. + /// + public bool? AnimateRootFrame { get; } + } + + /// + /// Acts as a base class for all image encoders that allow encoding animation sequences. + /// + public abstract class AnimatedImageEncoder : AlphaAwareImageEncoder, IAnimatedImageEncoder + { + /// + public Color? BackgroundColor { get; init; } + + /// + public ushort? RepeatCount { get; init; } + + /// + public bool? AnimateRootFrame { get; init; } = true; + } +} diff --git a/ImageSharp/Formats/IFormatFrameMetadata.cs b/ImageSharp/Formats/IFormatFrameMetadata.cs new file mode 100644 index 0000000..6d03525 --- /dev/null +++ b/ImageSharp/Formats/IFormatFrameMetadata.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats { + /// + /// An interface that provides metadata for a specific image format frames. + /// + public interface IFormatFrameMetadata : IDeepCloneable + { + /// + /// Converts the metadata to a instance. + /// + /// The . + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata(); + + /// + /// This method is called after a process has been applied to the image frame. + /// + /// The type of pixel format. + /// The source image frame. + /// The destination image frame. + /// The transformation matrix applied to the image frame. + public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel; + } + + /// + /// An interface that provides metadata for a specific image format frames. + /// + /// The metadata type implementing this interface. + public interface IFormatFrameMetadata : IFormatFrameMetadata, IDeepCloneable + where TSelf : class, IFormatFrameMetadata + { + /// + /// Creates a new instance of the class from the given . + /// + /// The . + /// The . +#pragma warning disable CA1000 // Do not declare static members on generic types + public static abstract TSelf FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata); +#pragma warning restore CA1000 // Do not declare static members on generic types + } +} diff --git a/ImageSharp/Formats/IFormatMetadata.cs b/ImageSharp/Formats/IFormatMetadata.cs new file mode 100644 index 0000000..776df7c --- /dev/null +++ b/ImageSharp/Formats/IFormatMetadata.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats { + /// + /// An interface that provides metadata for a specific image format. + /// + public interface IFormatMetadata : IDeepCloneable + { + /// + /// Converts the metadata to a instance. + /// + /// The pixel type info. + public PixelTypeInfo GetPixelTypeInfo(); + + /// + /// Converts the metadata to a instance. + /// + /// The . + public FormatConnectingMetadata ToFormatConnectingMetadata(); + + /// + /// This method is called after a process has been applied to the image. + /// + /// The type of pixel format. + /// The destination image . + /// The transformation matrix applied to the image. + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel; + } + + /// + /// An interface that provides metadata for a specific image format. + /// + /// The metadata type implementing this interface. + public interface IFormatMetadata : IFormatMetadata, IDeepCloneable + where TSelf : class, IFormatMetadata + { + /// + /// Creates a new instance of the class from the given . + /// + /// The . + /// The . +#pragma warning disable CA1000 // Do not declare static members on generic types + public static abstract TSelf FromFormatConnectingMetadata(FormatConnectingMetadata metadata); +#pragma warning restore CA1000 // Do not declare static members on generic types + } +} diff --git a/ImageSharp/Formats/IImageDecoder.cs b/ImageSharp/Formats/IImageDecoder.cs new file mode 100644 index 0000000..613b379 --- /dev/null +++ b/ImageSharp/Formats/IImageDecoder.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SixLabors.ImageSharp.Formats { + /// + /// Defines the contract for all image decoders. + /// + public interface IImageDecoder + { + /// + /// Reads the raw image information from the specified stream. + /// + /// The general decoder options. + /// The containing image data. + /// The object. + /// Thrown if the encoded image contains errors. + public ImageInfo Identify(DecoderOptions options, Stream stream); + + /// + /// Reads the raw image information from the specified stream. + /// + /// The general decoder options. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The object. + /// Thrown if the encoded image contains errors. + public Task IdentifyAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default); + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The pixel format. + /// The general decoder options. + /// The containing image data. + /// The . + /// Thrown if the encoded image contains errors. + public Image Decode(DecoderOptions options, Stream stream) + where TPixel : unmanaged, IPixel; + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The general decoder options. + /// The containing image data. + /// The . + /// Thrown if the encoded image contains errors. + public Image Decode(DecoderOptions options, Stream stream); + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The pixel format. + /// The general decoder options. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// Thrown if the encoded image contains errors. + public Task> DecodeAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default) + where TPixel : unmanaged, IPixel; + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The general decoder options. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// Thrown if the encoded image contains errors. + public Task DecodeAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default); + } +} diff --git a/ImageSharp/Formats/IImageEncoder.cs b/ImageSharp/Formats/IImageEncoder.cs new file mode 100644 index 0000000..03a9f1b --- /dev/null +++ b/ImageSharp/Formats/IImageEncoder.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SixLabors.ImageSharp.Formats { + /// + /// Defines the contract for all image encoders. + /// + public interface IImageEncoder + { + /// + /// Gets a value indicating whether to ignore decoded metadata when encoding. + /// + public bool SkipMetadata { get; init; } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + public void Encode(Image image, Stream stream) + where TPixel : unmanaged, IPixel; + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel; + } +} diff --git a/ImageSharp/Formats/IImageFormat.cs b/ImageSharp/Formats/IImageFormat.cs new file mode 100644 index 0000000..8c23d86 --- /dev/null +++ b/ImageSharp/Formats/IImageFormat.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats { + /// + /// Defines the contract for an image format. + /// + public interface IImageFormat + { + /// + /// Gets the name that describes this image format. + /// + string Name { get; } + + /// + /// Gets the default mime type that the image format uses + /// + string DefaultMimeType { get; } + + /// + /// Gets all the mime types that have been used by this image format. + /// + IEnumerable MimeTypes { get; } + + /// + /// Gets the file extensions this image format commonly uses. + /// + IEnumerable FileExtensions { get; } + } + + /// + /// Defines the contract for an image format containing metadata. + /// + /// The type of format metadata. + public interface IImageFormat : IImageFormat + where TFormatMetadata : class + { + /// + /// Creates a default instance of the format metadata. + /// + /// The . + TFormatMetadata CreateDefaultFormatMetadata(); + } + + /// + /// Defines the contract for an image format containing metadata with multiple frames. + /// + /// The type of format metadata. + /// The type of format frame metadata. + public interface IImageFormat : IImageFormat + where TFormatMetadata : class + where TFormatFrameMetadata : class + { + /// + /// Creates a default instance of the format frame metadata. + /// + /// The . + TFormatFrameMetadata CreateDefaultFormatFrameMetadata(); + } +} diff --git a/ImageSharp/Formats/IImageFormatConfigurationModule.cs b/ImageSharp/Formats/IImageFormatConfigurationModule.cs new file mode 100644 index 0000000..22c144c --- /dev/null +++ b/ImageSharp/Formats/IImageFormatConfigurationModule.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats { + /// + /// Represents an interface that can register image encoders, decoders and image format detectors. + /// + public interface IImageFormatConfigurationModule + { + /// + /// Called when loaded into a configuration object so the module can register items into the configuration. + /// + /// The configuration that will retain the encoders, decodes and mime type detectors. + void Configure(Configuration configuration); + } +} diff --git a/ImageSharp/Formats/IImageFormatDetector.cs b/ImageSharp/Formats/IImageFormatDetector.cs new file mode 100644 index 0000000..f96b2e0 --- /dev/null +++ b/ImageSharp/Formats/IImageFormatDetector.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats { + /// + /// Used for detecting mime types from a file header + /// + public interface IImageFormatDetector + { + /// + /// Gets the size of the header for this image type. + /// + /// The size of the header. + int HeaderSize { get; } + + /// + /// Detect mimetype + /// + /// The containing the file header. + /// The mime type of detected otherwise returns null + /// returns true when format was detected otherwise false. + bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format); + } +} diff --git a/ImageSharp/Formats/IQuantizingImageEncoder.cs b/ImageSharp/Formats/IQuantizingImageEncoder.cs new file mode 100644 index 0000000..e38ba69 --- /dev/null +++ b/ImageSharp/Formats/IQuantizingImageEncoder.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Formats { + /// + /// Defines the contract for all image encoders that allow color palette generation via quantization. + /// + public interface IQuantizingImageEncoder + { + /// + /// Gets the quantizer used to generate the color palette. + /// + public IQuantizer? Quantizer { get; } + + /// + /// Gets the used for quantization when building color palettes. + /// + public IPixelSamplingStrategy PixelSamplingStrategy { get; } + } + + /// + /// Acts as a base class for all image encoders that allow color palette generation via quantization. + /// + public abstract class QuantizingImageEncoder : AlphaAwareImageEncoder, IQuantizingImageEncoder + { + /// + public IQuantizer? Quantizer { get; init; } + + /// + public IPixelSamplingStrategy PixelSamplingStrategy { get; init; } = new DefaultPixelSamplingStrategy(); + } + + /// + /// Acts as a base class for all image encoders that allow color palette generation via quantization when + /// encoding animation sequences. + /// + public abstract class QuantizingAnimatedImageEncoder : QuantizingImageEncoder, IAnimatedImageEncoder + { + /// + public Color? BackgroundColor { get; } + + /// + public ushort? RepeatCount { get; } + + /// + public bool? AnimateRootFrame { get; } + } +} diff --git a/ImageSharp/Formats/ISpecializedDecoderOptions.cs b/ImageSharp/Formats/ISpecializedDecoderOptions.cs new file mode 100644 index 0000000..c38999c --- /dev/null +++ b/ImageSharp/Formats/ISpecializedDecoderOptions.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats { + /// + /// Provides specialized configuration options for decoding image formats. + /// + public interface ISpecializedDecoderOptions + { + /// + /// Gets the general decoder options. + /// + public DecoderOptions GeneralOptions { get; init; } + } +} diff --git a/ImageSharp/Formats/ISpecializedImageDecoder{T}.cs b/ImageSharp/Formats/ISpecializedImageDecoder{T}.cs new file mode 100644 index 0000000..d24c855 --- /dev/null +++ b/ImageSharp/Formats/ISpecializedImageDecoder{T}.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SixLabors.ImageSharp.Formats { + /// + /// Defines the contract for an image decoder that supports specialized options. + /// + /// The type of specialized options. + public interface ISpecializedImageDecoder : IImageDecoder + where T : ISpecializedDecoderOptions + { + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The pixel format. + /// The specialized decoder options. + /// The containing image data. + /// The . + /// Thrown if the encoded image contains errors. + public Image Decode(T options, Stream stream) + where TPixel : unmanaged, IPixel; + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The specialized decoder options. + /// The containing image data. + /// The . + /// Thrown if the encoded image contains errors. + public Image Decode(T options, Stream stream); + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The pixel format. + /// The specialized decoder options. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// Thrown if the encoded image contains errors. + public Task> DecodeAsync(T options, Stream stream, CancellationToken cancellationToken = default) + where TPixel : unmanaged, IPixel; + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The specialized decoder options. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// Thrown if the encoded image contains errors. + public Task DecodeAsync(T options, Stream stream, CancellationToken cancellationToken = default); + } +} diff --git a/ImageSharp/Formats/Ico/IcoConfigurationModule.cs b/ImageSharp/Formats/Ico/IcoConfigurationModule.cs new file mode 100644 index 0000000..584675e --- /dev/null +++ b/ImageSharp/Formats/Ico/IcoConfigurationModule.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Icon; + +namespace SixLabors.ImageSharp.Formats.Ico { + /// + /// Registers the image encoders, decoders and mime type detectors for the Ico format. + /// + public sealed class IcoConfigurationModule : IImageFormatConfigurationModule + { + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetEncoder(IcoFormat.Instance, new IcoEncoder()); + configuration.ImageFormatsManager.SetDecoder(IcoFormat.Instance, IcoDecoder.Instance); + configuration.ImageFormatsManager.AddImageFormatDetector(new IconImageFormatDetector()); + } + } +} diff --git a/ImageSharp/Formats/Ico/IcoConstants.cs b/ImageSharp/Formats/Ico/IcoConstants.cs new file mode 100644 index 0000000..b98b462 --- /dev/null +++ b/ImageSharp/Formats/Ico/IcoConstants.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Ico { + /// + /// Defines constants relating to ICOs + /// + internal static class IcoConstants + { + /// + /// The list of mime types that equate to a ico. + /// + /// + /// See + /// + public static readonly IEnumerable MimeTypes = + [ + + // IANA-registered + "image/vnd.microsoft.icon", + + // ICO & CUR types used by Windows + "image/x-icon", + + // Erroneous types but have been used + "image/ico", + "image/icon", + "text/ico", + "application/ico", + ]; + + /// + /// The list of file extensions that equate to a ico. + /// + public static readonly IEnumerable FileExtensions = ["ico"]; + + public const uint FileHeader = 0x00_01_00_00; + } +} diff --git a/ImageSharp/Formats/Ico/IcoDecoder.cs b/ImageSharp/Formats/Ico/IcoDecoder.cs new file mode 100644 index 0000000..ceb747f --- /dev/null +++ b/ImageSharp/Formats/Ico/IcoDecoder.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Ico { + /// + /// Decoder for generating an image out of a ico encoded stream. + /// + public sealed class IcoDecoder : ImageDecoder + { + private IcoDecoder() + { + } + + /// + /// Gets the shared instance. + /// + public static IcoDecoder Instance { get; } = new(); + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + Image image = new IcoDecoderCore(options).Decode(options.Configuration, stream, cancellationToken); + + ScaleToTargetSize(options, image); + + return image; + } + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(options, stream, cancellationToken); + + /// + protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + return new IcoDecoderCore(options).Identify(options.Configuration, stream, cancellationToken); + } + } +} diff --git a/ImageSharp/Formats/Ico/IcoDecoderCore.cs b/ImageSharp/Formats/Ico/IcoDecoderCore.cs new file mode 100644 index 0000000..ebae2db --- /dev/null +++ b/ImageSharp/Formats/Ico/IcoDecoderCore.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.Metadata; +using System; + +namespace SixLabors.ImageSharp.Formats.Ico { + internal sealed class IcoDecoderCore : IconDecoderCore + { + public IcoDecoderCore(DecoderOptions options) + : base(options) + { + } + + protected override void SetFrameMetadata( + ImageMetadata imageMetadata, + ImageFrameMetadata frameMetadata, + int index, + in IconDirEntry entry, + IconFrameCompression compression, + BmpBitsPerPixel bitsPerPixel, + ReadOnlyMemory? colorTable) + { + IcoFrameMetadata icoFrameMetadata = frameMetadata.GetIcoMetadata(); + icoFrameMetadata.FromIconDirEntry(entry); + icoFrameMetadata.Compression = compression; + icoFrameMetadata.BmpBitsPerPixel = bitsPerPixel; + icoFrameMetadata.ColorTable = colorTable; + + if (index == 0) + { + IcoMetadata curMetadata = imageMetadata.GetIcoMetadata(); + curMetadata.Compression = compression; + curMetadata.BmpBitsPerPixel = bitsPerPixel; + curMetadata.ColorTable = colorTable; + } + } + } +} diff --git a/ImageSharp/Formats/Ico/IcoEncoder.cs b/ImageSharp/Formats/Ico/IcoEncoder.cs new file mode 100644 index 0000000..4136a10 --- /dev/null +++ b/ImageSharp/Formats/Ico/IcoEncoder.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Ico { + /// + /// Image encoder for writing an image to a stream as a Windows Icon. + /// + public sealed class IcoEncoder : QuantizingImageEncoder + { + /// + protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) + { + IcoEncoderCore encoderCore = new(this); + encoderCore.Encode(image, stream, cancellationToken); + } + } +} diff --git a/ImageSharp/Formats/Ico/IcoEncoderCore.cs b/ImageSharp/Formats/Ico/IcoEncoderCore.cs new file mode 100644 index 0000000..140c01b --- /dev/null +++ b/ImageSharp/Formats/Ico/IcoEncoderCore.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Icon; + +namespace SixLabors.ImageSharp.Formats.Ico { + internal sealed class IcoEncoderCore : IconEncoderCore + { + public IcoEncoderCore(QuantizingImageEncoder encoder) + : base(encoder, IconFileType.ICO) + { + } + } +} diff --git a/ImageSharp/Formats/Ico/IcoFormat.cs b/ImageSharp/Formats/Ico/IcoFormat.cs new file mode 100644 index 0000000..0979ad9 --- /dev/null +++ b/ImageSharp/Formats/Ico/IcoFormat.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Linq; + +namespace SixLabors.ImageSharp.Formats.Ico { + /// + /// Registers the image encoders, decoders and mime type detectors for the ICO format. + /// + public sealed class IcoFormat : IImageFormat + { + private IcoFormat() + { + } + + /// + /// Gets the shared instance. + /// + public static IcoFormat Instance { get; } = new(); + + /// + public string Name => "ICO"; + + /// + public string DefaultMimeType => IcoConstants.MimeTypes.First(); + + /// + public IEnumerable MimeTypes => IcoConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => IcoConstants.FileExtensions; + + /// + public IcoMetadata CreateDefaultFormatMetadata() => new(); + + /// + public IcoFrameMetadata CreateDefaultFormatFrameMetadata() => new(); + } +} diff --git a/ImageSharp/Formats/Ico/IcoFrameMetadata.cs b/ImageSharp/Formats/Ico/IcoFrameMetadata.cs new file mode 100644 index 0000000..c154ef2 --- /dev/null +++ b/ImageSharp/Formats/Ico/IcoFrameMetadata.cs @@ -0,0 +1,236 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Ico { + /// + /// Provides Ico specific metadata information for the image frame. + /// + public class IcoFrameMetadata : IFormatFrameMetadata + { + /// + /// Initializes a new instance of the class. + /// + public IcoFrameMetadata() + { + } + + private IcoFrameMetadata(IcoFrameMetadata other) + { + this.Compression = other.Compression; + this.EncodingWidth = other.EncodingWidth; + this.EncodingHeight = other.EncodingHeight; + this.BmpBitsPerPixel = other.BmpBitsPerPixel; + + if (other.ColorTable?.Length > 0) + { + this.ColorTable = other.ColorTable.Value.ToArray(); + } + } + + /// + /// Gets or sets the frame compressions format. + /// + public IconFrameCompression Compression { get; set; } + + /// + /// Gets or sets the encoding width.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. + ///
+ public byte? EncodingWidth { get; set; } + + /// + /// Gets or sets the encoding height.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. + ///
+ public byte? EncodingHeight { get; set; } + + /// + /// Gets or sets the number of bits per pixel.
+ /// Used when is + ///
+ public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32; + + /// + /// Gets or sets the color table, if any. + /// The underlying pixel format is represented by . + /// + public ReadOnlyMemory? ColorTable { get; set; } + + /// + public static IcoFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) + { + if (!metadata.PixelTypeInfo.HasValue) + { + return new IcoFrameMetadata + { + BmpBitsPerPixel = BmpBitsPerPixel.Bit32, + Compression = IconFrameCompression.Png + }; + } + + int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel; + BmpBitsPerPixel bbpp = bpp switch + { + 1 => BmpBitsPerPixel.Bit1, + 2 => BmpBitsPerPixel.Bit2, + <= 4 => BmpBitsPerPixel.Bit4, + <= 8 => BmpBitsPerPixel.Bit8, + <= 16 => BmpBitsPerPixel.Bit16, + <= 24 => BmpBitsPerPixel.Bit24, + _ => BmpBitsPerPixel.Bit32 + }; + + IconFrameCompression compression = IconFrameCompression.Bmp; + if (bbpp is BmpBitsPerPixel.Bit32) + { + compression = IconFrameCompression.Png; + } + + return new IcoFrameMetadata + { + BmpBitsPerPixel = bbpp, + Compression = compression, + EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth), + EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight) + }; + } + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo(), + EncodingWidth = this.EncodingWidth, + EncodingHeight = this.EncodingHeight + }; + + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + float ratioX = destination.Width / (float)source.Width; + float ratioY = destination.Height / (float)source.Height; + this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX); + this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY); + this.ColorTable = null; + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public IcoFrameMetadata DeepClone() => new(this); + + internal void FromIconDirEntry(IconDirEntry entry) + { + this.EncodingWidth = entry.Width; + this.EncodingHeight = entry.Height; + } + + internal IconDirEntry ToIconDirEntry(Size size) + { + byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8 + ? (byte)0 + : (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel); + + return new IconDirEntry + { + Width = ClampEncodingDimension(this.EncodingWidth ?? size.Width), + Height = ClampEncodingDimension(this.EncodingHeight ?? size.Height), + Planes = 1, + ColorCount = colorCount, + BitCount = this.Compression switch + { + IconFrameCompression.Bmp => (ushort)this.BmpBitsPerPixel, + IconFrameCompression.Png or _ => 32, + }, + }; + } + + private PixelTypeInfo GetPixelTypeInfo() + { + int bpp = (int)this.BmpBitsPerPixel; + PixelComponentInfo info; + PixelColorType color; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + + if (this.Compression is IconFrameCompression.Png) + { + bpp = 32; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + } + else + { + switch (this.BmpBitsPerPixel) + { + case BmpBitsPerPixel.Bit1: + info = PixelComponentInfo.Create(1, bpp, 1); + color = PixelColorType.Binary; + break; + case BmpBitsPerPixel.Bit2: + info = PixelComponentInfo.Create(1, bpp, 2); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit4: + info = PixelComponentInfo.Create(1, bpp, 4); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit8: + info = PixelComponentInfo.Create(1, bpp, 8); + color = PixelColorType.Indexed; + break; + + // Could be 555 with padding but 565 is more common in newer bitmaps and offers + // greater accuracy due to extra green precision. + case BmpBitsPerPixel.Bit16: + info = PixelComponentInfo.Create(3, bpp, 5, 6, 5); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit24: + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit32 or _: + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + }; + } + + private static byte ScaleEncodingDimension(byte? value, int destination, float ratio) + { + if (value is null) + { + return ClampEncodingDimension(destination); + } + + return ClampEncodingDimension(MathF.Ceiling(value.Value * ratio)); + } + + private static byte ClampEncodingDimension(float? dimension) + => dimension switch + { + // Encoding dimensions can be between 0-256 where 0 means 256 or greater. + > 255 => 0, + <= 255 and >= 1 => (byte)dimension, + _ => 0 + }; + } +} diff --git a/ImageSharp/Formats/Ico/IcoMetadata.cs b/ImageSharp/Formats/Ico/IcoMetadata.cs new file mode 100644 index 0000000..696a770 --- /dev/null +++ b/ImageSharp/Formats/Ico/IcoMetadata.cs @@ -0,0 +1,162 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Ico { + /// + /// Provides Ico specific metadata information for the image. + /// + public class IcoMetadata : IFormatMetadata + { + /// + /// Initializes a new instance of the class. + /// + public IcoMetadata() + { + } + + private IcoMetadata(IcoMetadata other) + { + this.Compression = other.Compression; + this.BmpBitsPerPixel = other.BmpBitsPerPixel; + + if (other.ColorTable?.Length > 0) + { + this.ColorTable = other.ColorTable.Value.ToArray(); + } + } + + /// + /// Gets or sets the frame compressions format. Derived from the root frame. + /// + public IconFrameCompression Compression { get; set; } + + /// + /// Gets or sets the number of bits per pixel.
+ /// Used when is + ///
+ public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32; + + /// + /// Gets or sets the color table, if any. Derived from the root frame.
+ /// The underlying pixel format is represented by . + ///
+ public ReadOnlyMemory? ColorTable { get; set; } + + /// + public static IcoMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + int bpp = metadata.PixelTypeInfo.BitsPerPixel; + BmpBitsPerPixel bbpp = bpp switch + { + 1 => BmpBitsPerPixel.Bit1, + 2 => BmpBitsPerPixel.Bit2, + <= 4 => BmpBitsPerPixel.Bit4, + <= 8 => BmpBitsPerPixel.Bit8, + <= 16 => BmpBitsPerPixel.Bit16, + <= 24 => BmpBitsPerPixel.Bit24, + _ => BmpBitsPerPixel.Bit32 + }; + + IconFrameCompression compression = IconFrameCompression.Bmp; + if (bbpp is BmpBitsPerPixel.Bit32) + { + compression = IconFrameCompression.Png; + } + + return new IcoMetadata + { + BmpBitsPerPixel = bbpp, + Compression = compression + }; + } + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp = (int)this.BmpBitsPerPixel; + PixelComponentInfo info; + PixelColorType color; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + + if (this.Compression is IconFrameCompression.Png) + { + bpp = 32; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + } + else + { + switch (this.BmpBitsPerPixel) + { + case BmpBitsPerPixel.Bit1: + info = PixelComponentInfo.Create(1, bpp, 1); + color = PixelColorType.Binary; + break; + case BmpBitsPerPixel.Bit2: + info = PixelComponentInfo.Create(1, bpp, 2); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit4: + info = PixelComponentInfo.Create(1, bpp, 4); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit8: + info = PixelComponentInfo.Create(1, bpp, 8); + color = PixelColorType.Indexed; + break; + + // Could be 555 with padding but 565 is more common in newer bitmaps and offers + // greater accuracy due to extra green precision. + case BmpBitsPerPixel.Bit16: + info = PixelComponentInfo.Create(3, bpp, 5, 6, 5); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit24: + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit32 or _: + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + EncodingType = this.Compression == IconFrameCompression.Bmp && this.BmpBitsPerPixel <= BmpBitsPerPixel.Bit8 + ? EncodingType.Lossy + : EncodingType.Lossless, + PixelTypeInfo = this.GetPixelTypeInfo() + }; + + /// + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + => this.ColorTable = null; + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public IcoMetadata DeepClone() => new(this); + } +} diff --git a/ImageSharp/Formats/Icon/IconDecoderCore.cs b/ImageSharp/Formats/Icon/IconDecoderCore.cs new file mode 100644 index 0000000..ebaeb22 --- /dev/null +++ b/ImageSharp/Formats/Icon/IconDecoderCore.cs @@ -0,0 +1,315 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading; +using System.Linq; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Metadata; + +namespace SixLabors.ImageSharp.Formats.Icon { + internal abstract class IconDecoderCore : ImageDecoderCore + { + private IconDir fileHeader; + private IconDirEntry[]? entries; + + protected IconDecoderCore(DecoderOptions options) + : base(options) + { + } + + /// + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + { + // Stream may not at 0. + long basePosition = stream.Position; + this.ReadHeader(stream); + + Span flag = stackalloc byte[PngConstants.HeaderBytes.Length]; + + List<(Image Image, IconFrameCompression Compression, int Index)> decodedEntries + = new((int)Math.Min(this.entries.Length, this.Options.MaxFrames)); + + for (int i = 0; i < this.entries.Length; i++) + { + if (i == this.Options.MaxFrames) + { + break; + } + + ref IconDirEntry entry = ref this.entries[i]; + + // If we hit the end of the stream we should break. + if (stream.Seek(basePosition + entry.ImageOffset, SeekOrigin.Begin) >= stream.Length) + { + break; + } + + // There should always be enough bytes for this regardless of the entry type. + if (stream.Read(flag) != PngConstants.HeaderBytes.Length) + { + break; + } + + // Reset the stream position. + _ = stream.Seek(-PngConstants.HeaderBytes.Length, SeekOrigin.Current); + + bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes); + + // Decode the frame into a temp image buffer. This is disposed after the frame is copied to the result. + Image temp = this.GetDecoder(isPng).Decode(this.Options.Configuration, stream, cancellationToken); + decodedEntries.Add((temp, isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, i)); + + // Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data + // which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft. + this.Dimensions = new Size(Math.Max(this.Dimensions.Width, temp.Size.Width), Math.Max(this.Dimensions.Height, temp.Size.Height)); + } + + ImageMetadata metadata = new(); + BmpMetadata? bmpMetadata = null; + PngMetadata? pngMetadata = null; + Image result = new(this.Options.Configuration, metadata, decodedEntries.Select(x => + { + BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32; + ReadOnlyMemory? colorTable = null; + ImageFrame target = new(this.Options.Configuration, this.Dimensions); + ImageFrame source = x.Image.Frames.RootFrameUnsafe; + for (int y = 0; y < source.Height; y++) + { + source.PixelBuffer.DangerousGetRowSpan(y).CopyTo(target.PixelBuffer.DangerousGetRowSpan(y)); + } + + // Copy the format specific frame metadata to the image. + if (x.Compression is IconFrameCompression.Png) + { + if (x.Index == 0) + { + pngMetadata = x.Image.Metadata.GetPngMetadata(); + } + + target.Metadata.SetFormatMetadata(PngFormat.Instance, target.Metadata.GetPngMetadata()); + } + else + { + BmpMetadata meta = x.Image.Metadata.GetBmpMetadata(); + bitsPerPixel = meta.BitsPerPixel; + colorTable = meta.ColorTable; + + if (x.Index == 0) + { + bmpMetadata = meta; + } + } + + this.SetFrameMetadata( + metadata, + target.Metadata, + x.Index, + this.entries[x.Index], + x.Compression, + bitsPerPixel, + colorTable); + + x.Image.Dispose(); + + return target; + }).ToArray()); + + // Copy the format specific metadata to the image. + if (bmpMetadata != null) + { + result.Metadata.SetFormatMetadata(BmpFormat.Instance, bmpMetadata); + } + + if (pngMetadata != null) + { + result.Metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata); + } + + return result; + } + + /// + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + // Stream may not at 0. + long basePosition = stream.Position; + this.ReadHeader(stream); + + Span flag = stackalloc byte[PngConstants.HeaderBytes.Length]; + + ImageMetadata metadata = new(); + BmpMetadata? bmpMetadata = null; + PngMetadata? pngMetadata = null; + ImageFrameMetadata[] frames = new ImageFrameMetadata[Math.Min(this.fileHeader.Count, this.Options.MaxFrames)]; + int bpp = 0; + for (int i = 0; i < frames.Length; i++) + { + BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32; + ReadOnlyMemory? colorTable = null; + ref IconDirEntry entry = ref this.entries[i]; + + // If we hit the end of the stream we should break. + if (stream.Seek(basePosition + entry.ImageOffset, SeekOrigin.Begin) >= stream.Length) + { + break; + } + + // There should always be enough bytes for this regardless of the entry type. + if (stream.Read(flag) != PngConstants.HeaderBytes.Length) + { + break; + } + + // Reset the stream position. + _ = stream.Seek(-PngConstants.HeaderBytes.Length, SeekOrigin.Current); + + bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes); + + // Decode the frame into a temp image buffer. This is disposed after the frame is copied to the result. + ImageInfo frameInfo = this.GetDecoder(isPng).Identify(this.Options.Configuration, stream, cancellationToken); + + ImageFrameMetadata frameMetadata = new(); + + if (isPng) + { + if (i == 0) + { + pngMetadata = frameInfo.Metadata.GetPngMetadata(); + } + + frameMetadata.SetFormatMetadata(PngFormat.Instance, frameInfo.FrameMetadataCollection[0].GetPngMetadata()); + } + else + { + BmpMetadata meta = frameInfo.Metadata.GetBmpMetadata(); + bitsPerPixel = meta.BitsPerPixel; + colorTable = meta.ColorTable; + + if (i == 0) + { + bmpMetadata = meta; + } + } + + bpp = Math.Max(bpp, (int)bitsPerPixel); + + frames[i] = frameMetadata; + + this.SetFrameMetadata( + metadata, + frames[i], + i, + this.entries[i], + isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, + bitsPerPixel, + colorTable); + + // Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data + // which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft. + this.Dimensions = new Size(Math.Max(this.Dimensions.Width, frameInfo.Size.Width), Math.Max(this.Dimensions.Height, frameInfo.Size.Height)); + } + + // Copy the format specific metadata to the image. + if (bmpMetadata != null) + { + metadata.SetFormatMetadata(BmpFormat.Instance, bmpMetadata); + } + + if (pngMetadata != null) + { + metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata); + } + + return new ImageInfo(this.Dimensions, metadata, frames); + } + + protected abstract void SetFrameMetadata( + ImageMetadata imageMetadata, + ImageFrameMetadata frameMetadata, + int index, + in IconDirEntry entry, + IconFrameCompression compression, + BmpBitsPerPixel bitsPerPixel, + ReadOnlyMemory? colorTable); + + [MemberNotNull(nameof(entries))] + protected void ReadHeader(Stream stream) + { + Span buffer = stackalloc byte[IconDirEntry.Size]; + + // ICONDIR + _ = CheckEndOfStream(stream.Read(buffer[..IconDir.Size]), IconDir.Size); + this.fileHeader = IconDir.Parse(buffer); + + // ICONDIRENTRY + this.entries = new IconDirEntry[this.fileHeader.Count]; + for (int i = 0; i < this.entries.Length; i++) + { + _ = CheckEndOfStream(stream.Read(buffer[..IconDirEntry.Size]), IconDirEntry.Size); + this.entries[i] = IconDirEntry.Parse(buffer); + } + + int width = 0; + int height = 0; + foreach (IconDirEntry entry in this.entries) + { + // Since Windows 95 size of an image in the ICONDIRENTRY structure might + // be set to zero, which means 256 pixels. + if (entry.Width == 0) + { + width = 256; + } + + if (entry.Height == 0) + { + height = 256; + } + + if (width == 256 && height == 256) + { + break; + } + + width = Math.Max(width, entry.Width); + height = Math.Max(height, entry.Height); + } + + this.Dimensions = new Size(width, height); + } + + private ImageDecoderCore GetDecoder(bool isPng) + { + if (isPng) + { + return new PngDecoderCore(new PngDecoderOptions + { + GeneralOptions = this.Options, + }); + } + + return new BmpDecoderCore(new BmpDecoderOptions + { + GeneralOptions = this.Options, + ProcessedAlphaMask = true, + SkipFileHeader = true, + UseDoubleHeight = true, + }); + } + + private static int CheckEndOfStream(int v, int length) + { + if (v != length) + { + throw new InvalidImageContentException("Not enough bytes to read icon header."); + } + + return v; + } + } +} diff --git a/ImageSharp/Formats/Icon/IconDir.cs b/ImageSharp/Formats/Icon/IconDir.cs new file mode 100644 index 0000000..896cb24 --- /dev/null +++ b/ImageSharp/Formats/Icon/IconDir.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Icon { + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = Size)] + internal struct IconDir(ushort reserved, IconFileType type, ushort count) + { + public const int Size = 3 * sizeof(ushort); + + /// + /// Reserved. Must always be 0. + /// + public ushort Reserved = reserved; + + /// + /// Specifies image type: 1 for icon (.ICO) image, 2 for cursor (.CUR) image. Other values are invalid. + /// + public IconFileType Type = type; + + /// + /// Specifies number of images in the file. + /// + public ushort Count = count; + + public IconDir(IconFileType type) + : this(type, 0) + { + } + + public IconDir(IconFileType type, ushort count) + : this(0, type, count) + { + } + + public static IconDir Parse(ReadOnlySpan data) + => MemoryMarshal.Cast(data)[0]; + + public readonly unsafe void WriteTo(Stream stream) + => stream.Write(MemoryMarshal.Cast([this])); + } +} diff --git a/ImageSharp/Formats/Icon/IconDirEntry.cs b/ImageSharp/Formats/Icon/IconDirEntry.cs new file mode 100644 index 0000000..d49cd02 --- /dev/null +++ b/ImageSharp/Formats/Icon/IconDirEntry.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Icon { + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = Size)] + internal struct IconDirEntry + { + public const int Size = (4 * sizeof(byte)) + (2 * sizeof(ushort)) + (2 * sizeof(uint)); + + /// + /// Specifies image width in pixels. Can be any number between 0 and 255. Value 0 means image width is 256 pixels. + /// + public byte Width; + + /// + /// Specifies image height in pixels. Can be any number between 0 and 255. Value 0 means image height is 256 pixels.[ + /// + public byte Height; + + /// + /// Specifies number of colors in the color palette. Should be 0 if the image does not use a color palette. + /// + public byte ColorCount; + + /// + /// Reserved. Should be 0. + /// + public byte Reserved; + + /// + /// In ICO format: Specifies color planes. Should be 0 or 1.
+ /// In CUR format: Specifies the horizontal coordinates of the hotspot in number of pixels from the left. + ///
+ public ushort Planes; + + /// + /// In ICO format: Specifies bits per pixel.
+ /// In CUR format: Specifies the vertical coordinates of the hotspot in number of pixels from the top. + ///
+ public ushort BitCount; + + /// + /// Specifies the size of the image's data in bytes + /// + public uint BytesInRes; + + /// + /// Specifies the offset of BMP or PNG data from the beginning of the ICO/CUR file. + /// + public uint ImageOffset; + + public static IconDirEntry Parse(in ReadOnlySpan data) + => MemoryMarshal.Cast(data)[0]; + + public readonly unsafe void WriteTo(in Stream stream) + => stream.Write(MemoryMarshal.Cast([this])); + } +} diff --git a/ImageSharp/Formats/Icon/IconEncoderCore.cs b/ImageSharp/Formats/Icon/IconEncoderCore.cs new file mode 100644 index 0000000..9918874 --- /dev/null +++ b/ImageSharp/Formats/Icon/IconEncoderCore.cs @@ -0,0 +1,198 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading; +using System.Linq; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Cur; +using SixLabors.ImageSharp.Formats.Ico; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Formats.Icon { + internal abstract class IconEncoderCore + { + private readonly QuantizingImageEncoder encoder; + private readonly IconFileType iconFileType; + private IconDir fileHeader; + private EncodingFrameMetadata[]? entries; + + protected IconEncoderCore(QuantizingImageEncoder encoder, IconFileType iconFileType) + { + this.encoder = encoder; + this.iconFileType = iconFileType; + } + + public void Encode( + Image image, + Stream stream, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + // Stream may not at 0. + long basePosition = stream.Position; + this.InitHeader(image); + + // We don't write the header and entries yet as we need to write the image data first. + int dataOffset = IconDir.Size + (IconDirEntry.Size * this.entries.Length); + _ = stream.Seek(dataOffset, SeekOrigin.Current); + + for (int i = 0; i < image.Frames.Count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data + // which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft. + ImageFrame frame = image.Frames[i]; + int width = this.entries[i].Entry.Width; + if (width is 0) + { + width = frame.Width; + } + + int height = this.entries[i].Entry.Height; + if (height is 0) + { + height = frame.Height; + } + + this.entries[i].Entry.ImageOffset = (uint)stream.Position; + + // We crop the frame to the size specified in the metadata. + using Image encodingFrame = new(width, height); + for (int y = 0; y < height; y++) + { + frame.PixelBuffer.DangerousGetRowSpan(y)[..width] + .CopyTo(encodingFrame.GetRootFramePixelBuffer().DangerousGetRowSpan(y)); + } + + ref EncodingFrameMetadata encodingMetadata = ref this.entries[i]; + + QuantizingImageEncoder encoder = encodingMetadata.Compression switch + { + IconFrameCompression.Bmp => new BmpEncoder() + { + Quantizer = this.GetQuantizer(encodingMetadata), + ProcessedAlphaMask = true, + UseDoubleHeight = true, + SkipFileHeader = true, + SupportTransparency = false, + TransparentColorMode = this.encoder.TransparentColorMode, + PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, + BitsPerPixel = encodingMetadata.BmpBitsPerPixel + }, + IconFrameCompression.Png => new PngEncoder() + { + // Only 32bit Png supported. + // https://devblogs.microsoft.com/oldnewthing/20101022-00/?p=12473 + BitDepth = PngBitDepth.Bit8, + ColorType = PngColorType.RgbWithAlpha, + TransparentColorMode = this.encoder.TransparentColorMode, + CompressionLevel = PngCompressionLevel.BestCompression + }, + _ => throw new NotSupportedException(), + }; + + encoder.Encode(encodingFrame, stream); + encodingMetadata.Entry.BytesInRes = (uint)stream.Position - encodingMetadata.Entry.ImageOffset; + } + + // We now need to rewind the stream and write the header and the entries. + long endPosition = stream.Position; + _ = stream.Seek(basePosition, SeekOrigin.Begin); + this.fileHeader.WriteTo(stream); + foreach (EncodingFrameMetadata frame in this.entries) + { + frame.Entry.WriteTo(stream); + } + + _ = stream.Seek(endPosition, SeekOrigin.Begin); + } + + [MemberNotNull(nameof(entries))] + private void InitHeader(Image image) + { + this.fileHeader = new IconDir(this.iconFileType, (ushort)image.Frames.Count); + this.entries = this.iconFileType switch + { + IconFileType.ICO => + [.. image.Frames.Select(i => + { + IcoFrameMetadata metadata = i.Metadata.GetIcoMetadata(); + return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry(i.Size)); + })], + IconFileType.CUR => + [.. image.Frames.Select(i => + { + CurFrameMetadata metadata = i.Metadata.GetCurMetadata(); + return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry(i.Size)); + })], + _ => throw new NotSupportedException(), + }; + } + + private IQuantizer? GetQuantizer(EncodingFrameMetadata metadata) + { + if (metadata.Entry.BitCount > 8) + { + return null; + } + + if (this.encoder.Quantizer is not null) + { + return this.encoder.Quantizer; + } + + if (metadata.ColorTable is null) + { + int count = metadata.Entry.ColorCount; + if (count == 0) + { + count = 256; + } + + return new WuQuantizer(new QuantizerOptions + { + MaxColors = count + }); + } + + // Don't dither if we have a palette. We want to preserve as much information as possible. + return new PaletteQuantizer(metadata.ColorTable.Value, new QuantizerOptions { Dither = null }); + } + + internal sealed class EncodingFrameMetadata + { + private IconDirEntry iconDirEntry; + + public EncodingFrameMetadata( + IconFrameCompression compression, + BmpBitsPerPixel bmpBitsPerPixel, + ReadOnlyMemory? colorTable, + IconDirEntry iconDirEntry) + { + this.Compression = compression; + this.BmpBitsPerPixel = compression == IconFrameCompression.Png + ? BmpBitsPerPixel.Bit32 + : bmpBitsPerPixel; + this.ColorTable = colorTable; + this.iconDirEntry = iconDirEntry; + } + + public IconFrameCompression Compression { get; } + + public BmpBitsPerPixel BmpBitsPerPixel { get; } + + public ReadOnlyMemory? ColorTable { get; set; } + + public ref IconDirEntry Entry => ref this.iconDirEntry; + } + } +} diff --git a/ImageSharp/Formats/Icon/IconFileType.cs b/ImageSharp/Formats/Icon/IconFileType.cs new file mode 100644 index 0000000..5fa50b5 --- /dev/null +++ b/ImageSharp/Formats/Icon/IconFileType.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Icon { + /// + /// Ico file type + /// + internal enum IconFileType : ushort + { + /// + /// ICO file + /// + ICO = 1, + + /// + /// CUR file + /// + CUR = 2, + } +} diff --git a/ImageSharp/Formats/Icon/IconFrameCompression.cs b/ImageSharp/Formats/Icon/IconFrameCompression.cs new file mode 100644 index 0000000..6ba3644 --- /dev/null +++ b/ImageSharp/Formats/Icon/IconFrameCompression.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Icon { + /// + /// IconFrameCompression + /// + public enum IconFrameCompression + { + /// + /// Bmp + /// + Bmp, + + /// + /// Png + /// + Png + } +} diff --git a/ImageSharp/Formats/Icon/IconImageFormatDetector.cs b/ImageSharp/Formats/Icon/IconImageFormatDetector.cs new file mode 100644 index 0000000..ab9dc5c --- /dev/null +++ b/ImageSharp/Formats/Icon/IconImageFormatDetector.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Icon { + /// + /// Detects ico file headers. + /// + public class IconImageFormatDetector : IImageFormatDetector + { + /// + public int HeaderSize { get; } = IconDir.Size + IconDirEntry.Size; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.IsSupportedFileFormat(header) switch + { + true => Ico.IcoFormat.Instance, + false => Cur.CurFormat.Instance, + null => default + }; + + return format is not null; + } + + private bool? IsSupportedFileFormat(ReadOnlySpan header) + { + // There are no magic bytes in the first few bytes of a tga file, + // so we try to figure out if its a valid tga by checking for valid tga header bytes. + if (header.Length < this.HeaderSize) + { + return null; + } + + IconDir dir = IconDir.Parse(header); + if (dir is not { Reserved: 0 } // Should be 0. + or not { Type: IconFileType.ICO or IconFileType.CUR } // Unknown Type. + or { Count: 0 }) + { + return null; + } + + IconDirEntry entry = IconDirEntry.Parse(header[IconDir.Size..]); + if (entry is not { Reserved: 0 } // Should be 0. + or { BytesInRes: 0 } // Should not be 0. + || entry.ImageOffset < IconDir.Size + (dir.Count * IconDirEntry.Size)) + { + return null; + } + + if (dir.Type is IconFileType.ICO) + { + if (entry is not { BitCount: 1 or 4 or 8 or 16 or 24 or 32 } or not { Planes: 0 or 1 }) + { + return null; + } + + return true; + } + + return false; + } + } +} diff --git a/ImageSharp/Formats/ImageDecoder.cs b/ImageSharp/Formats/ImageDecoder.cs new file mode 100644 index 0000000..4834113 --- /dev/null +++ b/ImageSharp/Formats/ImageDecoder.cs @@ -0,0 +1,360 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SixLabors.ImageSharp.Formats { + /// + /// Acts as a base class for image decoders. + /// Types that inherit this decoder are required to implement cancellable synchronous decoding operations only. + /// + public abstract class ImageDecoder : IImageDecoder + { + /// + public Image Decode(DecoderOptions options, Stream stream) + where TPixel : unmanaged, IPixel + { + Image image = WithSeekableStream( + options, + stream, + s => this.Decode(options, s, default)); + + this.SetDecoderFormat(options.Configuration, image); + HandleIccProfile(options, image); + + return image; + } + + /// + public Image Decode(DecoderOptions options, Stream stream) + { + Image image = WithSeekableStream( + options, + stream, + s => this.Decode(options, s, default)); + + this.SetDecoderFormat(options.Configuration, image); + HandleIccProfile(options, image); + + return image; + } + + /// + public async Task> DecodeAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default) + where TPixel : unmanaged, IPixel + { + Image image = await WithSeekableMemoryStreamAsync( + options, + stream, + (s, ct) => this.Decode(options, s, ct), + cancellationToken).ConfigureAwait(false); + + this.SetDecoderFormat(options.Configuration, image); + HandleIccProfile(options, image); + + return image; + } + + /// + public async Task DecodeAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default) + { + Image image = await WithSeekableMemoryStreamAsync( + options, + stream, + (s, ct) => this.Decode(options, s, ct), + cancellationToken).ConfigureAwait(false); + + this.SetDecoderFormat(options.Configuration, image); + HandleIccProfile(options, image); + + return image; + } + + /// + public ImageInfo Identify(DecoderOptions options, Stream stream) + { + ImageInfo info = WithSeekableStream( + options, + stream, + s => this.Identify(options, s, default)); + + this.SetDecoderFormat(options.Configuration, info); + HandleIccProfile(options, info); + + return info; + } + + /// + public async Task IdentifyAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default) + { + ImageInfo info = await WithSeekableMemoryStreamAsync( + options, + stream, + (s, ct) => this.Identify(options, s, ct), + cancellationToken).ConfigureAwait(false); + + this.SetDecoderFormat(options.Configuration, info); + HandleIccProfile(options, info); + + return info; + } + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// + /// This method is designed to support the ImageSharp internal infrastructure and is not recommended for direct use. + /// + /// The pixel format. + /// The general decoder options. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The . + /// Thrown if the encoded image contains errors. + protected abstract Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel; + + /// + /// Decodes the image from the specified stream to an . + /// + /// + /// This method is designed to support the ImageSharp internal infrastructure and is not recommended for direct use. + /// + /// The general decoder options. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The . + /// Thrown if the encoded image contains errors. + protected abstract Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken); + + /// + /// Reads the raw image information from the specified stream. + /// + /// + /// This method is designed to support the ImageSharp internal infrastructure and is not recommended for direct use. + /// + /// The general decoder options. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The object. + /// Thrown if the encoded image contains errors. + protected abstract ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken); + + /// + /// Performs a scaling operation against the decoded image. If the target size is not set, or the image size + /// already matches the target size, the image is untouched. + /// + /// The decoder options. + /// The decoded image. + protected static void ScaleToTargetSize(DecoderOptions options, Image image) + { + if (ShouldResize(options, image)) + { + ResizeOptions resizeOptions = new() + { + Size = options.TargetSize!.Value, + Sampler = options.Sampler, + Mode = ResizeMode.Max + }; + + image.Mutate(x => x.Resize(resizeOptions)); + } + } + + /// + /// Determines whether the decoded image should be resized. + /// + /// The decoder options. + /// The decoded image. + /// if the image should be resized, otherwise; . + private static bool ShouldResize(DecoderOptions options, Image image) + { + if (options.TargetSize is null) + { + return false; + } + + Size targetSize = options.TargetSize.Value; + Size currentSize = image.Size; + return currentSize.Width != targetSize.Width && currentSize.Height != targetSize.Height; + } + + internal static T WithSeekableStream( + DecoderOptions options, + Stream stream, + Func action) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + if (!stream.CanRead) + { + throw new NotSupportedException("Cannot read from the stream."); + } + + T PerformActionAndResetPosition(Stream s, long position) + { + T result = action(s); + + // Issue #2259. Our buffered reads may have left the stream in an incorrect non-zero position. + // Reset the position of the seekable stream if we did not read to the end to allow additional reads. + // The stream is always seekable in this scenario. + if (stream.Position != s.Position && s.Position != s.Length) + { + stream.Position = position + s.Position; + } + + return result; + } + + if (stream.CanSeek) + { + return PerformActionAndResetPosition(stream, stream.Position); + } + + Configuration configuration = options.Configuration; + using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize); + memoryStream.Position = 0; + + return action(memoryStream); + } + + internal static Task WithSeekableMemoryStreamAsync( + DecoderOptions options, + Stream stream, + Func action, + CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + if (!stream.CanRead) + { + throw new NotSupportedException("Cannot read from the stream."); + } + + Task PerformActionAndResetPosition(Stream s, long position, CancellationToken ct) + { + try + { + T result = action(s, ct); + + // Issue #2259. Our buffered reads may have left the stream in an incorrect non-zero position. + // Reset the position of the seekable stream if we did not read to the end to allow additional reads. + // We check here that the input stream is seekable because it is not guaranteed to be so since + // we always copy input streams of unknown type. + if (stream.CanSeek && stream.Position != s.Position && s.Position != s.Length) + { + stream.Position = position + s.Position; + } + + return Task.FromResult(result); + } + catch (OperationCanceledException) + { + return Task.FromCanceled(cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + // NOTE: We are explicitly not executing the action against the stream here as we do in WithSeekableStream() because that + // would incur synchronous IO reads which must be avoided in this asynchronous method. Instead, we will *always* run the + // code below to copy the stream to an in-memory buffer before invoking the action. + if (stream is MemoryStream ms) + { + return PerformActionAndResetPosition(ms, ms.Position, cancellationToken); + } + + if (stream is ChunkedMemoryStream cms) + { + return PerformActionAndResetPosition(cms, cms.Position, cancellationToken); + } + + return CopyToMemoryStreamAndActionAsync(options, stream, PerformActionAndResetPosition, cancellationToken); + } + + private static async Task CopyToMemoryStreamAndActionAsync( + DecoderOptions options, + Stream stream, + Func> action, + CancellationToken cancellationToken) + { + long position = stream.CanSeek ? stream.Position : 0; + Configuration configuration = options.Configuration; + await using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); + memoryStream.Position = 0; + return await action(memoryStream, position, cancellationToken).ConfigureAwait(false); + } + + internal void SetDecoderFormat(Configuration configuration, Image image) + { + if (configuration.ImageFormatsManager.TryFindFormatByDecoder(this, out IImageFormat? format)) + { + image.Metadata.DecodedImageFormat = format; + + foreach (ImageFrame frame in image.Frames) + { + frame.Metadata.DecodedImageFormat = format; + } + } + } + + internal void SetDecoderFormat(Configuration configuration, ImageInfo info) + { + if (configuration.ImageFormatsManager.TryFindFormatByDecoder(this, out IImageFormat? format)) + { + info.Metadata.DecodedImageFormat = format; + info.PixelType = info.Metadata.GetDecodedPixelTypeInfo(); + + foreach (ImageFrameMetadata frame in info.FrameMetadataCollection) + { + frame.DecodedImageFormat = format; + } + } + } + + private static void HandleIccProfile(DecoderOptions options, Image image) + { + if (options.CanRemoveIccProfile(image.Metadata.IccProfile)) + { + image.Metadata.IccProfile = null; + } + + foreach (ImageFrame frame in image.Frames) + { + if (options.CanRemoveIccProfile(frame.Metadata.IccProfile)) + { + frame.Metadata.IccProfile = null; + } + } + } + + private static void HandleIccProfile(DecoderOptions options, ImageInfo image) + { + if (options.CanRemoveIccProfile(image.Metadata.IccProfile)) + { + image.Metadata.IccProfile = null; + } + + foreach (ImageFrameMetadata frame in image.FrameMetadataCollection) + { + if (options.CanRemoveIccProfile(frame.IccProfile)) + { + frame.IccProfile = null; + } + } + } + } +} diff --git a/ImageSharp/Formats/ImageDecoderCore.cs b/ImageSharp/Formats/ImageDecoderCore.cs new file mode 100644 index 0000000..59a456c --- /dev/null +++ b/ImageSharp/Formats/ImageDecoderCore.cs @@ -0,0 +1,292 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.IO; +using System.Linq; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats { + /// + /// The base class for all stateful image decoders. + /// + internal abstract class ImageDecoderCore + { + /// + /// Initializes a new instance of the class. + /// + /// The general decoder options. + protected ImageDecoderCore(DecoderOptions options) + => this.Options = options; + + /// + /// Gets the general decoder options. + /// + public DecoderOptions Options { get; } + + /// + /// Gets or sets the dimensions of the image being decoded. + /// + public Size Dimensions { get; protected internal set; } + + /// + /// Executes a known ancillary segment parsing action using the configured integrity policy. + /// + /// The action. + protected void ExecuteAncillarySegmentAction(Action action) + { + if (this.Options.SegmentIntegrityHandling is SegmentIntegrityHandling.Strict) + { + action(); + return; + } + + try + { + action(); + } + catch (Exception ex) when (ex + is ImageFormatException + or InvalidIccProfileException + or InvalidImageContentException + or InvalidOperationException + or NotSupportedException) + { + // Intentionally ignored in non-strict segment integrity modes. + } + } + + /// + /// Executes a known image data segment parsing action using the configured integrity policy. + /// + /// The action. + protected void ExecuteImageDataSegmentAction(Action action) + { + if (this.Options.SegmentIntegrityHandling is not SegmentIntegrityHandling.IgnoreImageData) + { + action(); + return; + } + + try + { + action(); + } + catch (Exception ex) when (ex + is ImageFormatException + or InvalidIccProfileException + or InvalidImageContentException + or InvalidOperationException + or NotSupportedException) + { + // Intentionally ignored when image data integrity handling is set to IgnoreImageData. + } + } + + /// + /// Throws unless the decoder is running in a non-strict segment integrity mode. + /// Use this only from within when local control flow + /// must continue after the error. + /// + /// The exception message. + protected void ThrowOrIgnoreNonStrictSegmentError(string message) + { + if (this.Options.SegmentIntegrityHandling is SegmentIntegrityHandling.Strict) + { + throw new InvalidImageContentException(message); + } + } + + /// + /// Reads the raw image information from the specified stream. + /// + /// The shared configuration. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The . + /// Thrown if the encoded image contains errors. + public ImageInfo Identify( + Configuration configuration, + Stream stream, + CancellationToken cancellationToken) + { + using BufferedReadStream bufferedReadStream = new(configuration, stream, cancellationToken); + + try + { + return this.Identify(bufferedReadStream, cancellationToken); + } + catch (InvalidMemoryOperationException ex) + { + throw new InvalidImageContentException(this.Dimensions, ex); + } + catch (Exception) + { + throw; + } + } + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The pixel format. + /// The shared configuration. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The . + /// Thrown if the encoded image contains errors. + public Image Decode( + Configuration configuration, + Stream stream, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + // Test may pass a BufferedReadStream in order to monitor EOF hits, if so, use the existing instance. + BufferedReadStream bufferedReadStream = + stream as BufferedReadStream ?? new BufferedReadStream(configuration, stream, cancellationToken); + + try + { + return this.Decode(bufferedReadStream, cancellationToken); + } + catch (InvalidMemoryOperationException ex) + { + throw new InvalidImageContentException(this.Dimensions, ex); + } + catch (Exception) + { + throw; + } + finally + { + if (bufferedReadStream != stream) + { + bufferedReadStream.Dispose(); + } + } + } + + /// + /// Reads the raw image information from the specified stream. + /// + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The . + /// + /// Cancellable synchronous method. In case of cancellation, + /// an shall be thrown which will be handled on the call site. + /// + protected abstract ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken); + + /// + /// Decodes the image from the specified stream. + /// + /// The pixel format. + /// The stream, where the image should be decoded from. Cannot be null. + /// The token to monitor for cancellation requests. + /// is null. + /// The decoded image. + /// + /// Cancellable synchronous method. In case of cancellation, an shall + /// be thrown which will be handled on the call site. + /// + protected abstract Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel; + + /// + /// Converts the ICC color profile of the specified image to the compact sRGB v4 profile if a source profile is + /// available. + /// + /// + /// This method should only be used by decoders that gurantee that the encoded image data is in a color space + /// compatible with sRGB (e.g. standard RGB, Adobe RGB, ProPhoto RGB). + ///
+ /// If the image does not have a valid ICC profile for color conversion, no changes are made. + /// This operation may affect the color appearance of the image to ensure consistency with the sRGB color + /// space. + ///
+ /// The pixel format. + /// The image whose ICC profile will be converted to the compact sRGB v4 profile. + /// + /// if the conversion was performed; otherwise, . + /// + protected bool TryConvertIccProfile(Image image) + where TPixel : unmanaged, IPixel + { + if (!this.Options.TryGetIccProfileForColorConversion(image.Metadata.IccProfile, out IccProfile? profile)) + { + return false; + } + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + MemoryAllocator = image.Configuration.MemoryAllocator, + }; + + ColorProfileConverter converter = new(options); + converter.Convert(image); + return true; + } + + /// + /// Converts the ICC color profile of the specified image frame to the compact sRGB v4 profile if a source profile is + /// available. + /// + /// + /// This method should only be used by decoders that gurantee that the encoded image data is in a color space + /// compatible with sRGB (e.g. standard RGB, Adobe RGB, ProPhoto RGB). + ///
+ /// If the image does not have a valid ICC profile for color conversion, no changes are made. + /// This operation may affect the color appearance of the image to ensure consistency with the sRGB color + /// space. + ///
+ /// The pixel format. + /// The image frame whose ICC profile will be converted to the compact sRGB v4 profile. + /// + /// if the conversion was performed; otherwise, . + /// + protected bool TryConvertIccProfile(ImageFrame frame) + where TPixel : unmanaged, IPixel + { + if (!this.Options.TryGetIccProfileForColorConversion(frame.Metadata.IccProfile, out IccProfile? profile)) + { + return false; + } + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + MemoryAllocator = frame.Configuration.MemoryAllocator, + }; + + ColorProfileConverter converter = new(options); + + ImageMetadata metadata = new() + { + IccProfile = frame.Metadata.IccProfile + }; + + IMemoryGroup m = frame.PixelBuffer.MemoryGroup; + + // Safe: ToArray only materializes the Memory segment list, not the underlying pixel buffers, + // and Wrap(Memory[]) creates a Consumed MemoryGroup that does not own the buffers (Dispose just + // invalidates the view). This means no pixel data is cloned and disposing the temporary image will + // not dispose or leak the frame's pixel buffer. + MemoryGroup memorySource = MemoryGroup.Wrap(m.ToArray()); + + using Image image = new(frame.Configuration, memorySource, frame.Width, frame.Height, metadata); + converter.Convert(image); + return true; + } + } +} diff --git a/ImageSharp/Formats/ImageEncoder.cs b/ImageSharp/Formats/ImageEncoder.cs new file mode 100644 index 0000000..1e16c10 --- /dev/null +++ b/ImageSharp/Formats/ImageEncoder.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SixLabors.ImageSharp.Formats { + /// + /// Acts as a base class for image encoders. + /// Types that inherit this encoder are required to implement cancellable synchronous encoding operations only. + /// + public abstract class ImageEncoder : IImageEncoder + { + /// + public bool SkipMetadata { get; init; } + + /// + public void Encode(Image image, Stream stream) + where TPixel : unmanaged, IPixel + => this.EncodeWithSeekableStream(image, stream, default); + + /// + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken = default) + where TPixel : unmanaged, IPixel + => this.EncodeWithSeekableStreamAsync(image, stream, cancellationToken); + + /// + /// Encodes the image to the specified stream from the . + /// + /// + /// This method is designed to support the ImageSharp internal infrastructure and is not recommended for direct use. + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to monitor for cancellation requests. + protected abstract void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel; + + private void EncodeWithSeekableStream(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + image.SynchronizeMetadata(); + + Configuration configuration = image.Configuration; + if (stream.CanSeek) + { + this.Encode(image, stream, cancellationToken); + } + else + { + using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); + this.Encode(image, ms, cancellationToken); + ms.Position = 0; + ms.CopyTo(stream, configuration.StreamProcessingBufferSize); + } + } + + private async Task EncodeWithSeekableStreamAsync(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + image.SynchronizeMetadata(); + + Configuration configuration = image.Configuration; + if (stream.CanSeek) + { + await DoEncodeAsync(stream).ConfigureAwait(false); + } + else + { + await using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); + await DoEncodeAsync(ms); + ms.Position = 0; + await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken) + .ConfigureAwait(false); + } + + Task DoEncodeAsync(Stream innerStream) + { + try + { + // TODO: Are synchronous IO writes OK? We avoid reads. + this.Encode(image, innerStream, cancellationToken); + return Task.CompletedTask; + } + catch (OperationCanceledException) + { + return Task.FromCanceled(cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + } + } +} diff --git a/ImageSharp/Formats/ImageFormatManager.cs b/ImageSharp/Formats/ImageFormatManager.cs new file mode 100644 index 0000000..b491e3b --- /dev/null +++ b/ImageSharp/Formats/ImageFormatManager.cs @@ -0,0 +1,234 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace SixLabors.ImageSharp.Formats { + /// + /// Collection of Image Formats to be used in class. + /// + public class ImageFormatManager + { + /// + /// Used for locking against as there is no ConcurrentSet type. + /// + /// + private static readonly object HashLock = new(); + + /// + /// The list of supported keyed to mime types. + /// + private readonly ConcurrentDictionary mimeTypeEncoders = new(); + + /// + /// The list of supported keyed to mime types. + /// + private readonly ConcurrentDictionary mimeTypeDecoders = new(); + + /// + /// The list of supported s. + /// + private readonly HashSet imageFormats = new(); + + /// + /// The list of supported s. + /// + private ConcurrentBag imageFormatDetectors = new(); + + /// + /// Initializes a new instance of the class. + /// + public ImageFormatManager() + { + } + + /// + /// Gets the maximum header size of all the formats. + /// + internal int MaxHeaderSize { get; private set; } + + /// + /// Gets the currently registered s. + /// + public IEnumerable ImageFormats => this.imageFormats; + + /// + /// Gets the currently registered s. + /// + internal IEnumerable FormatDetectors => this.imageFormatDetectors; + + /// + /// Gets the currently registered s. + /// + internal IEnumerable> ImageDecoders => this.mimeTypeDecoders; + + /// + /// Gets the currently registered s. + /// + internal IEnumerable> ImageEncoders => this.mimeTypeEncoders; + + /// + /// Registers a new format provider. + /// + /// The format to register as a known format. + public void AddImageFormat(IImageFormat format) + { + Guard.NotNull(format, nameof(format)); + Guard.NotNull(format.MimeTypes, nameof(format.MimeTypes)); + Guard.NotNull(format.FileExtensions, nameof(format.FileExtensions)); + + lock (HashLock) + { + this.imageFormats.Add(format); + } + } + + /// + /// For the specified file extensions type find the e . + /// + /// The extension to return the format for. + /// + /// When this method returns, contains the format that matches the given extension; + /// otherwise, the default value for the type of the parameter. + /// This parameter is passed uninitialized. + /// + /// if a match is found; otherwise, + public bool TryFindFormatByFileExtension(string extension, [NotNullWhen(true)] out IImageFormat? format) + { + if (!string.IsNullOrWhiteSpace(extension) && extension[0] == '.') + { + extension = extension[1..]; + } + + format = this.imageFormats.FirstOrDefault(x => + x.FileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)); + + return format is not null; + } + + /// + /// For the specified mime type find the . + /// + /// The mime-type to return the format for. + /// + /// When this method returns, contains the format that matches the given mime-type; + /// otherwise, the default value for the type of the parameter. + /// This parameter is passed uninitialized. + /// + /// if a match is found; otherwise, + public bool TryFindFormatByMimeType(string mimeType, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.imageFormats.FirstOrDefault(x => x.MimeTypes.Contains(mimeType, StringComparer.OrdinalIgnoreCase)); + return format is not null; + } + + internal bool TryFindFormatByDecoder(IImageDecoder decoder, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.mimeTypeDecoders.FirstOrDefault(x => x.Value.GetType() == decoder.GetType()).Key; + return format is not null; + } + + /// + /// Sets a specific image encoder as the encoder for a specific image format. + /// + /// The image format to register the encoder for. + /// The encoder to use, + public void SetEncoder(IImageFormat imageFormat, IImageEncoder encoder) + { + Guard.NotNull(imageFormat, nameof(imageFormat)); + Guard.NotNull(encoder, nameof(encoder)); + this.AddImageFormat(imageFormat); + this.mimeTypeEncoders.AddOrUpdate(imageFormat, encoder, (_, _) => encoder); + } + + /// + /// Sets a specific image decoder as the decoder for a specific image format. + /// + /// The image format to register the encoder for. + /// The decoder to use, + public void SetDecoder(IImageFormat imageFormat, IImageDecoder decoder) + { + Guard.NotNull(imageFormat, nameof(imageFormat)); + Guard.NotNull(decoder, nameof(decoder)); + this.AddImageFormat(imageFormat); + this.mimeTypeDecoders.AddOrUpdate(imageFormat, decoder, (_, _) => decoder); + } + + /// + /// Removes all the registered image format detectors. + /// + public void ClearImageFormatDetectors() => this.imageFormatDetectors = new ConcurrentBag(); + + /// + /// Adds a new detector for detecting mime types. + /// + /// The detector to add + public void AddImageFormatDetector(IImageFormatDetector detector) + { + Guard.NotNull(detector, nameof(detector)); + this.imageFormatDetectors.Add(detector); + this.SetMaxHeaderSize(); + } + + /// + /// For the specified mime type find the decoder. + /// + /// The format to discover + /// The . + /// The format is not registered. + public IImageDecoder GetDecoder(IImageFormat format) + { + Guard.NotNull(format, nameof(format)); + + if (!this.mimeTypeDecoders.TryGetValue(format, out IImageDecoder? decoder)) + { + ThrowInvalidDecoder(this); + } + + return decoder; + } + + /// + /// For the specified mime type find the encoder. + /// + /// The format to discover + /// The . + /// The format is not registered. + public IImageEncoder GetEncoder(IImageFormat format) + { + Guard.NotNull(format, nameof(format)); + + if (!this.mimeTypeEncoders.TryGetValue(format, out IImageEncoder? encoder)) + { + ThrowInvalidDecoder(this); + } + + return encoder; + } + + /// + /// Sets the max header size. + /// + private void SetMaxHeaderSize() => this.MaxHeaderSize = this.imageFormatDetectors.Max(x => x.HeaderSize); + + [DoesNotReturn] + internal static void ThrowInvalidDecoder(ImageFormatManager manager) + { + StringBuilder sb = new(); + sb = sb.AppendLine("Image cannot be loaded. Available decoders:"); + + foreach (KeyValuePair val in manager.ImageDecoders) + { + sb = sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine); + } + + throw new UnknownImageFormatException(sb.ToString()); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/5116.DCT_Filter.pdf b/ImageSharp/Formats/Jpeg/5116.DCT_Filter.pdf new file mode 100644 index 0000000..a5967a0 Binary files /dev/null and b/ImageSharp/Formats/Jpeg/5116.DCT_Filter.pdf differ diff --git a/ImageSharp/Formats/Jpeg/Components/Block8x8.Intrinsic.cs b/ImageSharp/Formats/Jpeg/Components/Block8x8.Intrinsic.cs new file mode 100644 index 0000000..943ed9c --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Block8x8.Intrinsic.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal unsafe partial struct Block8x8 + { + [FieldOffset(0)] + public Vector128 V0; + [FieldOffset(16)] + public Vector128 V1; + [FieldOffset(32)] + public Vector128 V2; + [FieldOffset(48)] + public Vector128 V3; + [FieldOffset(64)] + public Vector128 V4; + [FieldOffset(80)] + public Vector128 V5; + [FieldOffset(96)] + public Vector128 V6; + [FieldOffset(112)] + public Vector128 V7; + + [FieldOffset(0)] + public Vector256 V01; + [FieldOffset(32)] + public Vector256 V23; + [FieldOffset(64)] + public Vector256 V45; + [FieldOffset(96)] + public Vector256 V67; + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Block8x8.cs b/ImageSharp/Formats/Jpeg/Components/Block8x8.cs new file mode 100644 index 0000000..927b3f8 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Block8x8.cs @@ -0,0 +1,279 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using System.Text; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + /// + /// 8x8 matrix of coefficients. + /// + // ReSharper disable once InconsistentNaming + [StructLayout(LayoutKind.Explicit, Size = 2 * Size)] + internal partial struct Block8x8 + { + /// + /// A number of scalar coefficients in a + /// + public const int Size = 64; + + /// + /// Gets or sets a value at the given index + /// + /// The index + /// The value + public short this[int idx] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + DebugGuard.MustBeBetweenOrEqualTo(idx, 0, Size - 1, nameof(idx)); + + ref short selfRef = ref Unsafe.As(ref this); + return Unsafe.Add(ref selfRef, (uint)idx); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + DebugGuard.MustBeBetweenOrEqualTo(idx, 0, Size - 1, nameof(idx)); + + ref short selfRef = ref Unsafe.As(ref this); + Unsafe.Add(ref selfRef, (uint)idx) = value; + } + } + + /// + /// Gets or sets a value in a row+column of the 8x8 block + /// + /// The x position index in the row + /// The column index + /// The value + public short this[int x, int y] + { + get => this[(y * 8) + x]; + set => this[(y * 8) + x] = value; + } + + public static Block8x8 Load(Span data) + { + DebugGuard.MustBeGreaterThanOrEqualTo(data.Length, Size, "data is too small"); + + ref byte src = ref Unsafe.As(ref MemoryMarshal.GetReference(data)); + return Unsafe.ReadUnaligned(ref src); + } + + /// + /// Convert to + /// + public Block8x8F AsFloatBlock() + { + Block8x8F result = default; + result.LoadFrom(ref this); + return result; + } + + /// + /// Copy all elements to an array of . + /// + public short[] ToArray() + { + short[] result = new short[Size]; + this.CopyTo(result); + return result; + } + + /// + /// Copy elements into 'destination' Span of values + /// + public void CopyTo(Span destination) + { + DebugGuard.MustBeGreaterThanOrEqualTo(destination.Length, Size, "destination is too small"); + + ref byte destRef = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + Unsafe.WriteUnaligned(ref destRef, this); + } + + /// + /// Copy elements into 'destination' Span of values + /// + public void CopyTo(Span destination) + { + for (int i = 0; i < Size; i++) + { + destination[i] = this[i]; + } + } + + public static Block8x8 Load(ReadOnlySpan data) + { + Unsafe.SkipInit(out Block8x8 result); + result.LoadFrom(data); + return result; + } + + public void LoadFrom(ReadOnlySpan source) + { + for (int i = 0; i < Size; i++) + { + this[i] = source[i]; + } + } + + /// + /// Cast and copy -s from the beginning of 'source' span. + /// + public void LoadFrom(Span source) + { + for (int i = 0; i < Size; i++) + { + this[i] = (short)source[i]; + } + } + + /// + public override string ToString() + { + StringBuilder sb = new(); + sb.Append('['); + for (int i = 0; i < Size; i++) + { + sb.Append(this[i]); + if (i < Size - 1) + { + sb.Append(','); + } + } + + sb.Append(']'); + return sb.ToString(); + } + + /// + /// Returns index of the last non-zero element in given matrix. + /// + /// + /// Index of the last non-zero element. Returns -1 if all elements are equal to zero. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public nint GetLastNonZeroIndex() + { + if (Avx2.IsSupported) + { + const int equalityMask = unchecked((int)0b1111_1111_1111_1111_1111_1111_1111_1111); + + Vector256 zero16 = Vector256.Zero; + + ref Vector256 mcuStride = ref Unsafe.As>(ref this); + + for (nint i = 3; i >= 0; i--) + { + int areEqual = Avx2.MoveMask(Avx2.CompareEqual(Unsafe.Add(ref mcuStride, i), zero16).AsByte()); + + if (areEqual != equalityMask) + { + // Each 2 bits represents comparison operation for each 2-byte element in input vectors + // LSB represents first element in the stride + // MSB represents last element in the stride + // lzcnt operation would calculate number of zero numbers at the end + + // Given mask is not actually suitable for lzcnt as 1's represent zero elements and 0's represent non-zero elements + // So we need to invert it + uint lzcnt = (uint)BitOperations.LeadingZeroCount(~(uint)areEqual); + + // As input number is represented by 2 bits in the mask, we need to divide lzcnt result by 2 + // to get the exact number of zero elements in the stride + uint strideRelativeIndex = 15 - (lzcnt / 2); + return (i * 16) + (nint)strideRelativeIndex; + } + } + + return -1; + } + else + { + nint index = Size - 1; + ref short elemRef = ref Unsafe.As(ref this); + + while (index >= 0 && Unsafe.Add(ref elemRef, index) == 0) + { + index--; + } + + return index; + } + } + + /// + /// Transpose the block in place. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void TransposeInPlace() + { + ref short elemRef = ref Unsafe.As(ref this); + + // row #0 + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 1), ref Unsafe.Add(ref elemRef, 8)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 2), ref Unsafe.Add(ref elemRef, 16)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 3), ref Unsafe.Add(ref elemRef, 24)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 4), ref Unsafe.Add(ref elemRef, 32)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 5), ref Unsafe.Add(ref elemRef, 40)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 6), ref Unsafe.Add(ref elemRef, 48)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 7), ref Unsafe.Add(ref elemRef, 56)); + + // row #1 + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 10), ref Unsafe.Add(ref elemRef, 17)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 11), ref Unsafe.Add(ref elemRef, 25)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 12), ref Unsafe.Add(ref elemRef, 33)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 13), ref Unsafe.Add(ref elemRef, 41)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 14), ref Unsafe.Add(ref elemRef, 49)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 15), ref Unsafe.Add(ref elemRef, 57)); + + // row #2 + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 19), ref Unsafe.Add(ref elemRef, 26)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 20), ref Unsafe.Add(ref elemRef, 34)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 21), ref Unsafe.Add(ref elemRef, 42)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 22), ref Unsafe.Add(ref elemRef, 50)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 23), ref Unsafe.Add(ref elemRef, 58)); + + // row #3 + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 28), ref Unsafe.Add(ref elemRef, 35)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 29), ref Unsafe.Add(ref elemRef, 43)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 30), ref Unsafe.Add(ref elemRef, 51)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 31), ref Unsafe.Add(ref elemRef, 59)); + + // row #4 + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 37), ref Unsafe.Add(ref elemRef, 44)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 38), ref Unsafe.Add(ref elemRef, 52)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 39), ref Unsafe.Add(ref elemRef, 60)); + + // row #5 + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 46), ref Unsafe.Add(ref elemRef, 53)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 47), ref Unsafe.Add(ref elemRef, 61)); + + // row #6 + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 55), ref Unsafe.Add(ref elemRef, 62)); + } + + /// + /// Calculate the total sum of absolute differences of elements in 'a' and 'b'. + /// + public static long TotalDifference(ref Block8x8 a, ref Block8x8 b) + { + long result = 0; + for (int i = 0; i < Size; i++) + { + int d = a[i] - b[i]; + result += Math.Abs(d); + } + + return result; + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopy.cs b/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopy.cs new file mode 100644 index 0000000..0a986ac --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopy.cs @@ -0,0 +1,480 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +// ReSharper disable UseObjectOrCollectionInitializer +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal partial struct Block8x8F + { + [MethodImpl(InliningOptions.ShortMethod)] + public void ScaledCopyFrom(ref float areaOrigin, int areaStride) => + CopyFrom1x1Scale(ref Unsafe.As(ref areaOrigin), ref Unsafe.As(ref this), areaStride); + + [MethodImpl(InliningOptions.ColdPath)] + public void ScaledCopyTo(ref float areaOrigin, int areaStride, int horizontalScale, int verticalScale) + { + if (horizontalScale == 1 && verticalScale == 1) + { + CopyTo1x1Scale(ref Unsafe.As(ref this), ref Unsafe.As(ref areaOrigin), areaStride); + return; + } + + if (horizontalScale == 2 && verticalScale == 2) + { + this.CopyTo2x2Scale(ref areaOrigin, areaStride); + return; + } + + if (horizontalScale == 2 && verticalScale == 1) + { + this.CopyTo2x1Scale(ref areaOrigin, (uint)areaStride); + return; + } + + if (horizontalScale == 1 && verticalScale == 2) + { + this.CopyTo1x2Scale(ref areaOrigin, (uint)areaStride); + return; + } + + if (horizontalScale == 4 && verticalScale == 1) + { + this.CopyTo4x1Scale(ref areaOrigin, (uint)areaStride); + return; + } + + if (horizontalScale == 4 && verticalScale == 2) + { + this.CopyTo4x2Scale(ref areaOrigin, (uint)areaStride); + return; + } + + if (horizontalScale == 1 && verticalScale == 4) + { + this.CopyTo1x4Scale(ref areaOrigin, (uint)areaStride); + return; + } + + if (horizontalScale == 2 && verticalScale == 4) + { + this.CopyTo2x4Scale(ref areaOrigin, (uint)areaStride); + return; + } + + if (horizontalScale == 4 && verticalScale == 4) + { + this.CopyTo4x4Scale(ref areaOrigin, (uint)areaStride); + return; + } + + // The common 1x, 2x, and 4x integral scales are specialized above. + // Uncommon legal factor-3 scales use the generic fallback. + this.CopyArbitraryScale(ref areaOrigin, (uint)areaStride, (uint)horizontalScale, (uint)verticalScale); + } + + private void CopyTo2x2Scale(ref float areaOrigin, int areaStride) + { + ref Vector2 destBase = ref Unsafe.As(ref areaOrigin); + nuint destStride = (uint)areaStride / 2; + + WidenCopyRowImpl2x2(ref this.V0L, ref destBase, 0, destStride); + WidenCopyRowImpl2x2(ref this.V0L, ref destBase, 1, destStride); + WidenCopyRowImpl2x2(ref this.V0L, ref destBase, 2, destStride); + WidenCopyRowImpl2x2(ref this.V0L, ref destBase, 3, destStride); + WidenCopyRowImpl2x2(ref this.V0L, ref destBase, 4, destStride); + WidenCopyRowImpl2x2(ref this.V0L, ref destBase, 5, destStride); + WidenCopyRowImpl2x2(ref this.V0L, ref destBase, 6, destStride); + WidenCopyRowImpl2x2(ref this.V0L, ref destBase, 7, destStride); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void WidenCopyRowImpl2x2(ref Vector4 selfBase, ref Vector2 destBase, nuint row, nuint destStride) + { + ref Vector4 sLeft = ref Unsafe.Add(ref selfBase, 2 * row); + ref Vector4 sRight = ref Unsafe.Add(ref sLeft, 1); + + nuint offset = 2 * row * destStride; + ref Vector4 dTopLeft = ref Unsafe.As(ref Unsafe.Add(ref destBase, offset)); + ref Vector4 dBottomLeft = ref Unsafe.As(ref Unsafe.Add(ref destBase, offset + destStride)); + + Vector4 xyLeft = new(sLeft.X); + xyLeft.Z = sLeft.Y; + xyLeft.W = sLeft.Y; + + Vector4 zwLeft = new(sLeft.Z); + zwLeft.Z = sLeft.W; + zwLeft.W = sLeft.W; + + Vector4 xyRight = new(sRight.X); + xyRight.Z = sRight.Y; + xyRight.W = sRight.Y; + + Vector4 zwRight = new(sRight.Z); + zwRight.Z = sRight.W; + zwRight.W = sRight.W; + + dTopLeft = xyLeft; + Unsafe.Add(ref dTopLeft, 1) = zwLeft; + Unsafe.Add(ref dTopLeft, 2) = xyRight; + Unsafe.Add(ref dTopLeft, 3) = zwRight; + + dBottomLeft = xyLeft; + Unsafe.Add(ref dBottomLeft, 1) = zwLeft; + Unsafe.Add(ref dBottomLeft, 2) = xyRight; + Unsafe.Add(ref dBottomLeft, 3) = zwRight; + } + } + + /// + /// Copies the full 8x8 block into the destination buffer while doubling only the horizontal axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void CopyTo2x1Scale(ref float areaOrigin, uint areaStride) + { + ref Vector4 sourceBase = ref this.V0L; + + WidenRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 2u, 2u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 3u, 3u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 4u, 4u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 5u, 5u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 6u, 6u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 7u, 7u, areaStride); + } + + /// + /// Copies the full 8x8 block into the destination buffer while doubling only the vertical axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void CopyTo1x2Scale(ref float areaOrigin, uint areaStride) + { + ref Vector4 sourceBase = ref this.V0L; + + CopyRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 2u, 4u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 2u, 5u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 3u, 6u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 3u, 7u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 4u, 8u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 4u, 9u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 5u, 10u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 5u, 11u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 6u, 12u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 6u, 13u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 7u, 14u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 7u, 15u, areaStride); + } + + /// + /// Copies the full 8x8 block into the destination buffer while quadrupling only the horizontal axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void CopyTo4x1Scale(ref float areaOrigin, uint areaStride) + { + ref Vector4 sourceBase = ref this.V0L; + + ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 2u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 3u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 4u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 5u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 6u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 7u, areaStride); + } + + /// + /// Copies the full 8x8 block into the destination buffer while quadrupling horizontally and doubling vertically. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void CopyTo4x2Scale(ref float areaOrigin, uint areaStride) + { + ref Vector4 sourceBase = ref this.V0L; + + ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 4u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 5u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 6u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 7u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 8u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 9u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 10u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 11u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 12u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 13u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 14u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 15u, areaStride); + } + + /// + /// Copies the full 8x8 block into the destination buffer while quadrupling only the vertical axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void CopyTo1x4Scale(ref float areaOrigin, uint areaStride) + { + ref Vector4 sourceBase = ref this.V0L; + + CopyRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 2u, 8u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 2u, 9u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 2u, 10u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 2u, 11u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 3u, 12u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 3u, 13u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 3u, 14u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 3u, 15u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 4u, 16u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 4u, 17u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 4u, 18u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 4u, 19u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 5u, 20u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 5u, 21u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 5u, 22u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 5u, 23u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 6u, 24u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 6u, 25u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 6u, 26u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 6u, 27u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 7u, 28u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 7u, 29u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 7u, 30u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 7u, 31u, areaStride); + } + + /// + /// Copies the full 8x8 block into the destination buffer while doubling horizontally and quadrupling vertically. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void CopyTo2x4Scale(ref float areaOrigin, uint areaStride) + { + ref Vector4 sourceBase = ref this.V0L; + + WidenRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 2u, 8u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 2u, 9u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 2u, 10u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 2u, 11u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 3u, 12u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 3u, 13u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 3u, 14u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 3u, 15u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 4u, 16u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 4u, 17u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 4u, 18u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 4u, 19u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 5u, 20u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 5u, 21u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 5u, 22u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 5u, 23u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 6u, 24u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 6u, 25u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 6u, 26u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 6u, 27u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 7u, 28u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 7u, 29u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 7u, 30u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 7u, 31u, areaStride); + } + + /// + /// Copies the full 8x8 block into the destination buffer while quadrupling both axes. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void CopyTo4x4Scale(ref float areaOrigin, uint areaStride) + { + ref Vector4 sourceBase = ref this.V0L; + + ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 8u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 9u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 10u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 11u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 12u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 13u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 14u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 15u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 16u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 17u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 18u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 19u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 20u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 21u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 22u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 23u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 24u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 25u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 26u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 27u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 28u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 29u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 30u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 31u, areaStride); + } + + /// + /// Copies one eight-sample row from the full block to the destination row. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyRow8(ref Vector4 sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref Vector4 source = ref Unsafe.Add(ref sourceBase, sourceRow * 2u); + ref Vector4 dest = ref Unsafe.As(ref Unsafe.Add(ref areaOrigin, destRow * areaStride)); + + dest = source; + Unsafe.Add(ref dest, 1u) = Unsafe.Add(ref source, 1u); + } + + /// + /// Expands one eight-sample row to sixteen samples by duplicating each source value horizontally. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void WidenRow8(ref Vector4 sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref Vector4 sourceLeft = ref Unsafe.Add(ref sourceBase, sourceRow * 2u); + ref Vector4 sourceRight = ref Unsafe.Add(ref sourceLeft, 1u); + ref Vector4 dest = ref Unsafe.As(ref Unsafe.Add(ref areaOrigin, destRow * areaStride)); + + Vector4 xyLeft = new(sourceLeft.X); + xyLeft.Z = sourceLeft.Y; + xyLeft.W = sourceLeft.Y; + + Vector4 zwLeft = new(sourceLeft.Z); + zwLeft.Z = sourceLeft.W; + zwLeft.W = sourceLeft.W; + + Vector4 xyRight = new(sourceRight.X); + xyRight.Z = sourceRight.Y; + xyRight.W = sourceRight.Y; + + Vector4 zwRight = new(sourceRight.Z); + zwRight.Z = sourceRight.W; + zwRight.W = sourceRight.W; + + dest = xyLeft; + Unsafe.Add(ref dest, 1u) = zwLeft; + Unsafe.Add(ref dest, 2u) = xyRight; + Unsafe.Add(ref dest, 3u) = zwRight; + } + + /// + /// Expands one eight-sample row to thirty-two samples by duplicating each source value four times horizontally. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void ExpandRow8(ref Vector4 sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref Vector4 sourceLeft = ref Unsafe.Add(ref sourceBase, sourceRow * 2u); + ref Vector4 sourceRight = ref Unsafe.Add(ref sourceLeft, 1u); + ref Vector4 dest = ref Unsafe.As(ref Unsafe.Add(ref areaOrigin, destRow * areaStride)); + + dest = new Vector4(sourceLeft.X); + Unsafe.Add(ref dest, 1u) = new Vector4(sourceLeft.Y); + Unsafe.Add(ref dest, 2u) = new Vector4(sourceLeft.Z); + Unsafe.Add(ref dest, 3u) = new Vector4(sourceLeft.W); + Unsafe.Add(ref dest, 4u) = new Vector4(sourceRight.X); + Unsafe.Add(ref dest, 5u) = new Vector4(sourceRight.Y); + Unsafe.Add(ref dest, 6u) = new Vector4(sourceRight.Z); + Unsafe.Add(ref dest, 7u) = new Vector4(sourceRight.W); + } + + [MethodImpl(InliningOptions.ColdPath)] + private void CopyArbitraryScale(ref float areaOrigin, uint areaStride, uint horizontalScale, uint verticalScale) + { + for (nuint y = 0; y < 8; y++) + { + nuint yy = y * verticalScale; + nuint y8 = y * 8; + + for (nuint x = 0; x < 8; x++) + { + nuint xx = x * horizontalScale; + + float value = this[(int)(y8 + x)]; + nuint baseIdx = (yy * areaStride) + xx; + + for (nuint i = 0; i < verticalScale; i++, baseIdx += areaStride) + { + for (nuint j = 0; j < horizontalScale; j++) + { + // area[xx + j, yy + i] = value; + Unsafe.Add(ref areaOrigin, baseIdx + j) = value; + } + } + } + } + } + + private static void CopyTo1x1Scale(ref byte origin, ref byte dest, int areaStride) + { + int destStride = areaStride * sizeof(float); + + CopyRowImpl(ref origin, ref dest, destStride, 0); + CopyRowImpl(ref origin, ref dest, destStride, 1); + CopyRowImpl(ref origin, ref dest, destStride, 2); + CopyRowImpl(ref origin, ref dest, destStride, 3); + CopyRowImpl(ref origin, ref dest, destStride, 4); + CopyRowImpl(ref origin, ref dest, destStride, 5); + CopyRowImpl(ref origin, ref dest, destStride, 6); + CopyRowImpl(ref origin, ref dest, destStride, 7); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void CopyRowImpl(ref byte origin, ref byte dest, int destStride, int row) + { + origin = ref Unsafe.Add(ref origin, (uint)row * 8 * sizeof(float)); + dest = ref Unsafe.Add(ref dest, (uint)(row * destStride)); + Unsafe.CopyBlock(ref dest, ref origin, 8 * sizeof(float)); + } + } + + private static void CopyFrom1x1Scale(ref byte origin, ref byte dest, int areaStride) + { + int destStride = areaStride * sizeof(float); + + CopyRowImpl(ref origin, ref dest, destStride, 0); + CopyRowImpl(ref origin, ref dest, destStride, 1); + CopyRowImpl(ref origin, ref dest, destStride, 2); + CopyRowImpl(ref origin, ref dest, destStride, 3); + CopyRowImpl(ref origin, ref dest, destStride, 4); + CopyRowImpl(ref origin, ref dest, destStride, 5); + CopyRowImpl(ref origin, ref dest, destStride, 6); + CopyRowImpl(ref origin, ref dest, destStride, 7); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void CopyRowImpl(ref byte origin, ref byte dest, int sourceStride, int row) + { + origin = ref Unsafe.Add(ref origin, (uint)(row * sourceStride)); + dest = ref Unsafe.Add(ref dest, (uint)row * 8 * sizeof(float)); + Unsafe.CopyBlock(ref dest, ref origin, 8 * sizeof(float)); + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector128.cs b/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector128.cs new file mode 100644 index 0000000..f70810d --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector128.cs @@ -0,0 +1,93 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + /// + /// version of . + /// + internal partial struct Block8x8F + { + /// + /// version of . + /// + /// The maximum value to normalize to. + [MethodImpl(InliningOptions.ShortMethod)] + public void NormalizeColorsInPlaceVector128(float maximum) + { + Vector128 max = Vector128.Create(maximum); + Vector128 off = Vector128.Ceiling(max * .5F); + + this.V0L = NormalizeVector128(this.V0L.AsVector128(), off, max).AsVector4(); + this.V0R = NormalizeVector128(this.V0R.AsVector128(), off, max).AsVector4(); + this.V1L = NormalizeVector128(this.V1L.AsVector128(), off, max).AsVector4(); + this.V1R = NormalizeVector128(this.V1R.AsVector128(), off, max).AsVector4(); + this.V2L = NormalizeVector128(this.V2L.AsVector128(), off, max).AsVector4(); + this.V2R = NormalizeVector128(this.V2R.AsVector128(), off, max).AsVector4(); + this.V3L = NormalizeVector128(this.V3L.AsVector128(), off, max).AsVector4(); + this.V3R = NormalizeVector128(this.V3R.AsVector128(), off, max).AsVector4(); + this.V4L = NormalizeVector128(this.V4L.AsVector128(), off, max).AsVector4(); + this.V4R = NormalizeVector128(this.V4R.AsVector128(), off, max).AsVector4(); + this.V5L = NormalizeVector128(this.V5L.AsVector128(), off, max).AsVector4(); + this.V5R = NormalizeVector128(this.V5R.AsVector128(), off, max).AsVector4(); + this.V6L = NormalizeVector128(this.V6L.AsVector128(), off, max).AsVector4(); + this.V6R = NormalizeVector128(this.V6R.AsVector128(), off, max).AsVector4(); + this.V7L = NormalizeVector128(this.V7L.AsVector128(), off, max).AsVector4(); + this.V7R = NormalizeVector128(this.V7R.AsVector128(), off, max).AsVector4(); + } + + /// + /// Loads values from using extended AVX2 intrinsics. + /// + /// The source + public void LoadFromInt16ExtendedVector128(ref Block8x8 source) + { + DebugGuard.IsTrue(Vector128.IsHardwareAccelerated, "Vector128 support is required to run this operation!"); + + ref Vector128 srcBase = ref Unsafe.As>(ref source); + ref Vector128 destBase = ref Unsafe.As>(ref this); + + // Only 8 iterations, one per 128b short block + for (nuint i = 0; i < 8; i++) + { + Vector128 src = Unsafe.Add(ref srcBase, i); + + // Step 1: Widen short -> int + Vector128 lower = Vector128.WidenLower(src); // lower 4 shorts -> 4 ints + Vector128 upper = Vector128.WidenUpper(src); // upper 4 shorts -> 4 ints + + // Step 2: Convert int -> float + Vector128 lowerF = Vector128.ConvertToSingle(lower); + Vector128 upperF = Vector128.ConvertToSingle(upper); + + // Step 3: Store to destination (this is 16 lanes -> two Vector128 blocks) + Unsafe.Add(ref destBase, (i * 2) + 0) = lowerF; + Unsafe.Add(ref destBase, (i * 2) + 1) = upperF; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static Vector128 NormalizeVector128(Vector128 value, Vector128 off, Vector128 max) + => Vector128_.Clamp(value + off, Vector128.Zero, max); + + private static void MultiplyIntoInt16Vector128(ref Block8x8F a, ref Block8x8F b, ref Block8x8 dest) + { + DebugGuard.IsTrue(Vector128.IsHardwareAccelerated, "Vector128 support is required to run this operation!"); + + ref Vector128 aBase = ref Unsafe.As>(ref a); + ref Vector128 bBase = ref Unsafe.As>(ref b); + ref Vector128 destBase = ref Unsafe.As>(ref dest); + + for (nuint i = 0; i < 16; i += 2) + { + Vector128 left = Vector128_.ConvertToInt32RoundToEven(Unsafe.Add(ref aBase, i + 0) * Unsafe.Add(ref bBase, i + 0)); + Vector128 right = Vector128_.ConvertToInt32RoundToEven(Unsafe.Add(ref aBase, i + 1) * Unsafe.Add(ref bBase, i + 1)); + + Unsafe.Add(ref destBase, i / 2) = Vector128_.PackSignedSaturate(left, right); + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector256.cs b/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector256.cs new file mode 100644 index 0000000..699681e --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector256.cs @@ -0,0 +1,157 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + /// + /// version of . + /// + internal partial struct Block8x8F + { + /// + /// A number of rows of 8 scalar coefficients each in + /// + public const int RowCount = 8; + +#pragma warning disable SA1310 // Field names should not contain underscore + [FieldOffset(0)] + public Vector256 V256_0; + [FieldOffset(32)] + public Vector256 V256_1; + [FieldOffset(64)] + public Vector256 V256_2; + [FieldOffset(96)] + public Vector256 V256_3; + [FieldOffset(128)] + public Vector256 V256_4; + [FieldOffset(160)] + public Vector256 V256_5; + [FieldOffset(192)] + public Vector256 V256_6; + [FieldOffset(224)] + public Vector256 V256_7; +#pragma warning restore SA1310 // Field names should not contain underscore + + /// + /// version of . + /// + /// The maximum value to normalize to. + [MethodImpl(InliningOptions.ShortMethod)] + public void NormalizeColorsInPlaceVector256(float maximum) + { + Vector256 max = Vector256.Create(maximum); + Vector256 off = Vector256.Ceiling(max * .5F); + + this.V256_0 = NormalizeVector256(this.V256_0, off, max); + this.V256_1 = NormalizeVector256(this.V256_1, off, max); + this.V256_2 = NormalizeVector256(this.V256_2, off, max); + this.V256_3 = NormalizeVector256(this.V256_3, off, max); + this.V256_4 = NormalizeVector256(this.V256_4, off, max); + this.V256_5 = NormalizeVector256(this.V256_5, off, max); + this.V256_6 = NormalizeVector256(this.V256_6, off, max); + this.V256_7 = NormalizeVector256(this.V256_7, off, max); + } + + /// + /// Loads values from using intrinsics. + /// + /// The source + public void LoadFromInt16ExtendedVector256(ref Block8x8 source) + { + DebugGuard.IsTrue( + Vector256.IsHardwareAccelerated, + "LoadFromInt16ExtendedVector256 only works on Vector256 compatible architecture!"); + + ref short sRef = ref Unsafe.As(ref source); + ref Vector256 dRef = ref Unsafe.As>(ref this); + + // Vector256.Count == 16 + // We can process 2 block rows in a single step + Vector256 top = Vector256_.Widen(Vector128.LoadUnsafe(ref sRef)); + Vector256 bottom = Vector256_.Widen(Vector128.LoadUnsafe(ref sRef, (nuint)Vector256.Count)); + dRef = Vector256.ConvertToSingle(top); + Unsafe.Add(ref dRef, 1) = Vector256.ConvertToSingle(bottom); + + top = Vector256_.Widen(Vector128.LoadUnsafe(ref sRef, (nuint)(Vector256.Count * 2))); + bottom = Vector256_.Widen(Vector128.LoadUnsafe(ref sRef, (nuint)(Vector256.Count * 3))); + Unsafe.Add(ref dRef, 2) = Vector256.ConvertToSingle(top); + Unsafe.Add(ref dRef, 3) = Vector256.ConvertToSingle(bottom); + + top = Vector256_.Widen(Vector128.LoadUnsafe(ref sRef, (nuint)(Vector256.Count * 4))); + bottom = Vector256_.Widen(Vector128.LoadUnsafe(ref sRef, (nuint)(Vector256.Count * 5))); + Unsafe.Add(ref dRef, 4) = Vector256.ConvertToSingle(top); + Unsafe.Add(ref dRef, 5) = Vector256.ConvertToSingle(bottom); + + top = Vector256_.Widen(Vector128.LoadUnsafe(ref sRef, (nuint)(Vector256.Count * 6))); + bottom = Vector256_.Widen(Vector128.LoadUnsafe(ref sRef, (nuint)(Vector256.Count * 7))); + Unsafe.Add(ref dRef, 6) = Vector256.ConvertToSingle(top); + Unsafe.Add(ref dRef, 7) = Vector256.ConvertToSingle(bottom); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static Vector256 NormalizeVector256(Vector256 value, Vector256 off, Vector256 max) + => Vector256_.Clamp(value + off, Vector256.Zero, max); + + private static void MultiplyIntoInt16Vector256(ref Block8x8F a, ref Block8x8F b, ref Block8x8 dest) + { + DebugGuard.IsTrue(Vector256.IsHardwareAccelerated, "Vector256 support is required to run this operation!"); + + ref Vector256 aBase = ref a.V256_0; + ref Vector256 bBase = ref b.V256_0; + ref Vector256 destRef = ref dest.V01; + + for (nuint i = 0; i < 8; i += 2) + { + Vector256 row0 = Vector256_.ConvertToInt32RoundToEven(Unsafe.Add(ref aBase, i + 0) * Unsafe.Add(ref bBase, i + 0)); + Vector256 row1 = Vector256_.ConvertToInt32RoundToEven(Unsafe.Add(ref aBase, i + 1) * Unsafe.Add(ref bBase, i + 1)); + + Vector256 row = Vector256_.PackSignedSaturate(row0, row1); + row = Vector256.Shuffle(row.AsInt32(), Vector256.Create(0, 1, 4, 5, 2, 3, 6, 7)).AsInt16(); + + Unsafe.Add(ref destRef, i / 2) = row; + } + } + + private void TransposeInPlaceVector256() + { + // https://stackoverflow.com/questions/25622745/transpose-an-8x8-float-using-avx-avx2/25627536#25627536 + Vector256 r0 = this.V256_0.WithUpper(this.V4L.AsVector128()); + Vector256 r1 = this.V256_1.WithUpper(this.V5L.AsVector128()); + Vector256 r2 = this.V256_2.WithUpper(this.V6L.AsVector128()); + Vector256 r3 = this.V256_3.WithUpper(this.V7L.AsVector128()); + Vector256 r4 = this.V0R.AsVector128().ToVector256().WithUpper(this.V4R.AsVector128()); + Vector256 r5 = this.V1R.AsVector128().ToVector256().WithUpper(this.V5R.AsVector128()); + Vector256 r6 = this.V2R.AsVector128().ToVector256().WithUpper(this.V6R.AsVector128()); + Vector256 r7 = this.V3R.AsVector128().ToVector256().WithUpper(this.V7R.AsVector128()); + + Vector256 t0 = Avx.UnpackLow(r0, r1); + Vector256 t2 = Avx.UnpackLow(r2, r3); + Vector256 v = Avx.Shuffle(t0, t2, 0x4E); + this.V256_0 = Avx.Blend(t0, v, 0xCC); + this.V256_1 = Avx.Blend(t2, v, 0x33); + + Vector256 t4 = Avx.UnpackLow(r4, r5); + Vector256 t6 = Avx.UnpackLow(r6, r7); + v = Avx.Shuffle(t4, t6, 0x4E); + this.V256_4 = Avx.Blend(t4, v, 0xCC); + this.V256_5 = Avx.Blend(t6, v, 0x33); + + Vector256 t1 = Avx.UnpackHigh(r0, r1); + Vector256 t3 = Avx.UnpackHigh(r2, r3); + v = Avx.Shuffle(t1, t3, 0x4E); + this.V256_2 = Avx.Blend(t1, v, 0xCC); + this.V256_3 = Avx.Blend(t3, v, 0x33); + + Vector256 t5 = Avx.UnpackHigh(r4, r5); + Vector256 t7 = Avx.UnpackHigh(r6, r7); + v = Avx.Shuffle(t5, t7, 0x4E); + this.V256_6 = Avx.Blend(t5, v, 0xCC); + this.V256_7 = Avx.Blend(t7, v, 0x33); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs b/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs new file mode 100644 index 0000000..b58845d --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs @@ -0,0 +1,646 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Text; +using SixLabors.ImageSharp.Common.Helpers; + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + /// + /// 8x8 matrix of coefficients. + /// + [StructLayout(LayoutKind.Explicit)] + internal partial struct Block8x8F : IEquatable + { + /// + /// A number of scalar coefficients in a + /// + public const int Size = 64; + + [FieldOffset(0)] + public Vector4 V0L; + [FieldOffset(16)] + public Vector4 V0R; + + [FieldOffset(32)] + public Vector4 V1L; + [FieldOffset(48)] + public Vector4 V1R; + + [FieldOffset(64)] + public Vector4 V2L; + [FieldOffset(80)] + public Vector4 V2R; + + [FieldOffset(96)] + public Vector4 V3L; + [FieldOffset(112)] + public Vector4 V3R; + + [FieldOffset(128)] + public Vector4 V4L; + [FieldOffset(144)] + public Vector4 V4R; + + [FieldOffset(160)] + public Vector4 V5L; + [FieldOffset(176)] + public Vector4 V5R; + + [FieldOffset(192)] + public Vector4 V6L; + [FieldOffset(208)] + public Vector4 V6R; + + [FieldOffset(224)] + public Vector4 V7L; + [FieldOffset(240)] + public Vector4 V7R; + + /// + /// Get/Set scalar elements at a given index + /// + /// The index + /// The float value at the specified index + public float this[int idx] + { + get => this[(uint)idx]; + set => this[(uint)idx] = value; + } + + internal float this[nuint idx] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + DebugGuard.MustBeBetweenOrEqualTo((int)idx, 0, Size - 1, nameof(idx)); + ref float selfRef = ref Unsafe.As(ref this); + return Unsafe.Add(ref selfRef, idx); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + DebugGuard.MustBeBetweenOrEqualTo((int)idx, 0, Size - 1, nameof(idx)); + ref float selfRef = ref Unsafe.As(ref this); + Unsafe.Add(ref selfRef, idx) = value; + } + } + + public float this[int x, int y] + { + get => this[((uint)y * 8) + (uint)x]; + set => this[((uint)y * 8) + (uint)x] = value; + } + + /// + /// Load raw 32bit floating point data from source. + /// + /// Source + [MethodImpl(InliningOptions.ShortMethod)] + public static Block8x8F Load(Span data) + { + DebugGuard.MustBeGreaterThanOrEqualTo(data.Length, Size, "data is too small"); + + ref byte src = ref Unsafe.As(ref MemoryMarshal.GetReference(data)); + return Unsafe.ReadUnaligned(ref src); + } + + /// + /// Load raw 32bit floating point data from source + /// + /// Source + public unsafe void LoadFrom(Span source) + { + fixed (Vector4* ptr = &this.V0L) + { + float* fp = (float*)ptr; + for (int i = 0; i < Size; i++) + { + fp[i] = source[i]; + } + } + } + + /// + /// Copy raw 32bit floating point data to dest + /// + /// Destination + [MethodImpl(InliningOptions.ShortMethod)] + public readonly void ScaledCopyTo(float[] dest) + { + DebugGuard.MustBeGreaterThanOrEqualTo(dest.Length, Size, "dest is too small"); + + ref byte destRef = ref Unsafe.As(ref MemoryMarshal.GetArrayDataReference(dest)); + Unsafe.WriteUnaligned(ref destRef, this); + } + + public float[] ToArray() + { + float[] result = new float[Size]; + this.ScaledCopyTo(result); + return result; + } + + /// + /// Multiply all elements of the block. + /// + /// The value to multiply by. + [MethodImpl(InliningOptions.ShortMethod)] + public void MultiplyInPlace(float value) + { + if (Vector256.IsHardwareAccelerated) + { + Vector256 valueVec = Vector256.Create(value); + this.V256_0 *= valueVec; + this.V256_1 *= valueVec; + this.V256_2 *= valueVec; + this.V256_3 *= valueVec; + this.V256_4 *= valueVec; + this.V256_5 *= valueVec; + this.V256_6 *= valueVec; + this.V256_7 *= valueVec; + } + else + { + Vector4 valueVec = new(value); + this.V0L *= valueVec; + this.V0R *= valueVec; + this.V1L *= valueVec; + this.V1R *= valueVec; + this.V2L *= valueVec; + this.V2R *= valueVec; + this.V3L *= valueVec; + this.V3R *= valueVec; + this.V4L *= valueVec; + this.V4R *= valueVec; + this.V5L *= valueVec; + this.V5R *= valueVec; + this.V6L *= valueVec; + this.V6R *= valueVec; + this.V7L *= valueVec; + this.V7R *= valueVec; + } + } + + /// + /// Multiply all elements of the block by the corresponding elements of 'other'. + /// + /// The other block. + [MethodImpl(InliningOptions.ShortMethod)] + public void MultiplyInPlace(ref Block8x8F other) + { + if (Vector256.IsHardwareAccelerated) + { + this.V256_0 *= other.V256_0; + this.V256_1 *= other.V256_1; + this.V256_2 *= other.V256_2; + this.V256_3 *= other.V256_3; + this.V256_4 *= other.V256_4; + this.V256_5 *= other.V256_5; + this.V256_6 *= other.V256_6; + this.V256_7 *= other.V256_7; + } + else + { + this.V0L *= other.V0L; + this.V0R *= other.V0R; + this.V1L *= other.V1L; + this.V1R *= other.V1R; + this.V2L *= other.V2L; + this.V2R *= other.V2R; + this.V3L *= other.V3L; + this.V3R *= other.V3R; + this.V4L *= other.V4L; + this.V4R *= other.V4R; + this.V5L *= other.V5L; + this.V5R *= other.V5R; + this.V6L *= other.V6L; + this.V6R *= other.V6R; + this.V7L *= other.V7L; + this.V7R *= other.V7R; + } + } + + /// + /// Adds a vector to all elements of the block. + /// + /// The added vector. + [MethodImpl(InliningOptions.ShortMethod)] + public void AddInPlace(float value) + { + if (Vector256.IsHardwareAccelerated) + { + Vector256 valueVec = Vector256.Create(value); + this.V256_0 += valueVec; + this.V256_1 += valueVec; + this.V256_2 += valueVec; + this.V256_3 += valueVec; + this.V256_4 += valueVec; + this.V256_5 += valueVec; + this.V256_6 += valueVec; + this.V256_7 += valueVec; + } + else + { + Vector4 valueVec = new(value); + this.V0L += valueVec; + this.V0R += valueVec; + this.V1L += valueVec; + this.V1R += valueVec; + this.V2L += valueVec; + this.V2R += valueVec; + this.V3L += valueVec; + this.V3R += valueVec; + this.V4L += valueVec; + this.V4R += valueVec; + this.V5L += valueVec; + this.V5R += valueVec; + this.V6L += valueVec; + this.V6R += valueVec; + this.V7L += valueVec; + this.V7R += valueVec; + } + } + + /// + /// Quantize input block, transpose, apply zig-zag ordering and store as . + /// + /// Source block. + /// Destination block. + /// The quantization table. + public static void Quantize(ref Block8x8F block, ref Block8x8 dest, ref Block8x8F qt) + { + if (Vector256.IsHardwareAccelerated) + { + MultiplyIntoInt16Vector256(ref block, ref qt, ref dest); + ZigZag.ApplyTransposingZigZagOrderingAvx2(ref dest); + } + else if (Vector128.IsHardwareAccelerated) + { + MultiplyIntoInt16Vector128(ref block, ref qt, ref dest); + ZigZag.ApplyTransposingZigZagOrderingVector128(ref dest); + } + else + { + for (int i = 0; i < Size; i++) + { + int idx = ZigZag.TransposingOrder[i]; + float quantizedVal = block[idx] * qt[idx]; + quantizedVal += quantizedVal < 0 ? -0.5f : 0.5f; + dest[i] = (short)quantizedVal; + } + } + } + + public void RoundInto(ref Block8x8 dest) + { + for (int i = 0; i < Size; i++) + { + float val = this[i]; + if (val < 0) + { + val -= 0.5f; + } + else + { + val += 0.5f; + } + + dest[i] = (short)val; + } + } + + public Block8x8 RoundAsInt16Block() + { + Block8x8 result = default; + this.RoundInto(ref result); + return result; + } + + /// + /// Level shift by +maximum/2, clip to [0, maximum] + /// + /// The maximum value to normalize to. + public void NormalizeColorsInPlace(float maximum) + { + if (Vector256.IsHardwareAccelerated) + { + this.NormalizeColorsInPlaceVector256(maximum); + return; + } + else if (Vector128.IsHardwareAccelerated) + { + this.NormalizeColorsInPlaceVector128(maximum); + return; + } + else + { + Vector4 min = Vector4.Zero; + Vector4 max = new(maximum); + Vector4 off = new(MathF.Ceiling(maximum * 0.5F)); + + this.V0L = Vector4.Clamp(this.V0L + off, min, max); + this.V0R = Vector4.Clamp(this.V0R + off, min, max); + this.V1L = Vector4.Clamp(this.V1L + off, min, max); + this.V1R = Vector4.Clamp(this.V1R + off, min, max); + this.V2L = Vector4.Clamp(this.V2L + off, min, max); + this.V2R = Vector4.Clamp(this.V2R + off, min, max); + this.V3L = Vector4.Clamp(this.V3L + off, min, max); + this.V3R = Vector4.Clamp(this.V3R + off, min, max); + this.V4L = Vector4.Clamp(this.V4L + off, min, max); + this.V4R = Vector4.Clamp(this.V4R + off, min, max); + this.V5L = Vector4.Clamp(this.V5L + off, min, max); + this.V5R = Vector4.Clamp(this.V5R + off, min, max); + this.V6L = Vector4.Clamp(this.V6L + off, min, max); + this.V6R = Vector4.Clamp(this.V6R + off, min, max); + this.V7L = Vector4.Clamp(this.V7L + off, min, max); + this.V7R = Vector4.Clamp(this.V7R + off, min, max); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void LoadFrom(ref Block8x8 source) + { + if (Vector256.IsHardwareAccelerated) + { + this.LoadFromInt16ExtendedVector256(ref source); + return; + } + else if (Vector128.IsHardwareAccelerated) + { + this.LoadFromInt16ExtendedVector128(ref source); + return; + } + + this.LoadFromInt16Scalar(ref source); + } + + /// + /// Fill the block from doing short -> float conversion. + /// + /// The source block + public void LoadFromInt16Scalar(ref Block8x8 source) + { + ref short selfRef = ref Unsafe.As(ref source); + + this.V0L.X = Unsafe.Add(ref selfRef, 0); + this.V0L.Y = Unsafe.Add(ref selfRef, 1); + this.V0L.Z = Unsafe.Add(ref selfRef, 2); + this.V0L.W = Unsafe.Add(ref selfRef, 3); + this.V0R.X = Unsafe.Add(ref selfRef, 4); + this.V0R.Y = Unsafe.Add(ref selfRef, 5); + this.V0R.Z = Unsafe.Add(ref selfRef, 6); + this.V0R.W = Unsafe.Add(ref selfRef, 7); + + this.V1L.X = Unsafe.Add(ref selfRef, 8); + this.V1L.Y = Unsafe.Add(ref selfRef, 9); + this.V1L.Z = Unsafe.Add(ref selfRef, 10); + this.V1L.W = Unsafe.Add(ref selfRef, 11); + this.V1R.X = Unsafe.Add(ref selfRef, 12); + this.V1R.Y = Unsafe.Add(ref selfRef, 13); + this.V1R.Z = Unsafe.Add(ref selfRef, 14); + this.V1R.W = Unsafe.Add(ref selfRef, 15); + + this.V2L.X = Unsafe.Add(ref selfRef, 16); + this.V2L.Y = Unsafe.Add(ref selfRef, 17); + this.V2L.Z = Unsafe.Add(ref selfRef, 18); + this.V2L.W = Unsafe.Add(ref selfRef, 19); + this.V2R.X = Unsafe.Add(ref selfRef, 20); + this.V2R.Y = Unsafe.Add(ref selfRef, 21); + this.V2R.Z = Unsafe.Add(ref selfRef, 22); + this.V2R.W = Unsafe.Add(ref selfRef, 23); + + this.V3L.X = Unsafe.Add(ref selfRef, 24); + this.V3L.Y = Unsafe.Add(ref selfRef, 25); + this.V3L.Z = Unsafe.Add(ref selfRef, 26); + this.V3L.W = Unsafe.Add(ref selfRef, 27); + this.V3R.X = Unsafe.Add(ref selfRef, 28); + this.V3R.Y = Unsafe.Add(ref selfRef, 29); + this.V3R.Z = Unsafe.Add(ref selfRef, 30); + this.V3R.W = Unsafe.Add(ref selfRef, 31); + + this.V4L.X = Unsafe.Add(ref selfRef, 32); + this.V4L.Y = Unsafe.Add(ref selfRef, 33); + this.V4L.Z = Unsafe.Add(ref selfRef, 34); + this.V4L.W = Unsafe.Add(ref selfRef, 35); + this.V4R.X = Unsafe.Add(ref selfRef, 36); + this.V4R.Y = Unsafe.Add(ref selfRef, 37); + this.V4R.Z = Unsafe.Add(ref selfRef, 38); + this.V4R.W = Unsafe.Add(ref selfRef, 39); + + this.V5L.X = Unsafe.Add(ref selfRef, 40); + this.V5L.Y = Unsafe.Add(ref selfRef, 41); + this.V5L.Z = Unsafe.Add(ref selfRef, 42); + this.V5L.W = Unsafe.Add(ref selfRef, 43); + this.V5R.X = Unsafe.Add(ref selfRef, 44); + this.V5R.Y = Unsafe.Add(ref selfRef, 45); + this.V5R.Z = Unsafe.Add(ref selfRef, 46); + this.V5R.W = Unsafe.Add(ref selfRef, 47); + + this.V6L.X = Unsafe.Add(ref selfRef, 48); + this.V6L.Y = Unsafe.Add(ref selfRef, 49); + this.V6L.Z = Unsafe.Add(ref selfRef, 50); + this.V6L.W = Unsafe.Add(ref selfRef, 51); + this.V6R.X = Unsafe.Add(ref selfRef, 52); + this.V6R.Y = Unsafe.Add(ref selfRef, 53); + this.V6R.Z = Unsafe.Add(ref selfRef, 54); + this.V6R.W = Unsafe.Add(ref selfRef, 55); + + this.V7L.X = Unsafe.Add(ref selfRef, 56); + this.V7L.Y = Unsafe.Add(ref selfRef, 57); + this.V7L.Z = Unsafe.Add(ref selfRef, 58); + this.V7L.W = Unsafe.Add(ref selfRef, 59); + this.V7R.X = Unsafe.Add(ref selfRef, 60); + this.V7R.Y = Unsafe.Add(ref selfRef, 61); + this.V7R.Z = Unsafe.Add(ref selfRef, 62); + this.V7R.W = Unsafe.Add(ref selfRef, 63); + } + + /// + /// Compares entire 8x8 block to a single scalar value. + /// + /// Value to compare to. + public bool EqualsToScalar(int value) + { + if (Vector256.IsHardwareAccelerated) + { + Vector256 targetVector = Vector256.Create(value); + ref Vector256 blockStride = ref this.V256_0; + + for (nuint i = 0; i < RowCount; i++) + { + if (!Vector256.EqualsAll(Vector256.ConvertToInt32(Unsafe.Add(ref this.V256_0, i)), targetVector)) + { + return false; + } + } + + return true; + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 targetVector = Vector128.Create(value); + ref Vector4 blockStride = ref this.V0L; + + for (nuint i = 0; i < RowCount * 2; i++) + { + if (!Vector128.EqualsAll(Vector128.ConvertToInt32(Unsafe.Add(ref this.V0L, i).AsVector128()), targetVector)) + { + return false; + } + } + + return true; + } + + ref float scalars = ref Unsafe.As(ref this); + + for (nuint i = 0; i < Size; i++) + { + if ((int)Unsafe.Add(ref scalars, i) != value) + { + return false; + } + } + + return true; + } + + /// + public readonly bool Equals(Block8x8F other) + => this.V0L == other.V0L + && this.V0R == other.V0R + && this.V1L == other.V1L + && this.V1R == other.V1R + && this.V2L == other.V2L + && this.V2R == other.V2R + && this.V3L == other.V3L + && this.V3R == other.V3R + && this.V4L == other.V4L + && this.V4R == other.V4R + && this.V5L == other.V5L + && this.V5R == other.V5R + && this.V6L == other.V6L + && this.V6R == other.V6R + && this.V7L == other.V7L + && this.V7R == other.V7R; + + /// + public override bool Equals(object? obj) => this.Equals((Block8x8F?)obj); + + /// + public override int GetHashCode() + { + int left = HashCode.Combine( + this.V0L, + this.V1L, + this.V2L, + this.V3L, + this.V4L, + this.V5L, + this.V6L, + this.V7L); + + int right = HashCode.Combine( + this.V0R, + this.V1R, + this.V2R, + this.V3R, + this.V4R, + this.V5R, + this.V6R, + this.V7R); + + return HashCode.Combine(left, right); + } + + /// + public override string ToString() + { + StringBuilder sb = new(); + sb.Append('['); + for (int i = 0; i < Size - 1; i++) + { + sb.Append(this[i]).Append(','); + } + + sb.Append(this[Size - 1]).Append(']'); + return sb.ToString(); + } + + /// + /// Transpose the block in-place. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void TransposeInPlace() + { + if (Vector256.IsHardwareAccelerated) + { + this.TransposeInPlaceVector256(); + } + else + { + // TODO: Can we provide a Vector128 implementation for this? + this.TransposeInPlace_Scalar(); + } + } + + /// + /// Scalar in-place transpose implementation for + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void TransposeInPlace_Scalar() + { + ref float elemRef = ref Unsafe.As(ref this); + + // row #0 + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 1), ref Unsafe.Add(ref elemRef, 8)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 2), ref Unsafe.Add(ref elemRef, 16)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 3), ref Unsafe.Add(ref elemRef, 24)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 4), ref Unsafe.Add(ref elemRef, 32)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 5), ref Unsafe.Add(ref elemRef, 40)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 6), ref Unsafe.Add(ref elemRef, 48)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 7), ref Unsafe.Add(ref elemRef, 56)); + + // row #1 + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 10), ref Unsafe.Add(ref elemRef, 17)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 11), ref Unsafe.Add(ref elemRef, 25)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 12), ref Unsafe.Add(ref elemRef, 33)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 13), ref Unsafe.Add(ref elemRef, 41)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 14), ref Unsafe.Add(ref elemRef, 49)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 15), ref Unsafe.Add(ref elemRef, 57)); + + // row #2 + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 19), ref Unsafe.Add(ref elemRef, 26)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 20), ref Unsafe.Add(ref elemRef, 34)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 21), ref Unsafe.Add(ref elemRef, 42)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 22), ref Unsafe.Add(ref elemRef, 50)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 23), ref Unsafe.Add(ref elemRef, 58)); + + // row #3 + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 28), ref Unsafe.Add(ref elemRef, 35)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 29), ref Unsafe.Add(ref elemRef, 43)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 30), ref Unsafe.Add(ref elemRef, 51)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 31), ref Unsafe.Add(ref elemRef, 59)); + + // row #4 + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 37), ref Unsafe.Add(ref elemRef, 44)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 38), ref Unsafe.Add(ref elemRef, 52)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 39), ref Unsafe.Add(ref elemRef, 60)); + + // row #5 + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 46), ref Unsafe.Add(ref elemRef, 53)); + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 47), ref Unsafe.Add(ref elemRef, 61)); + + // row #6 + RuntimeUtility.Swap(ref Unsafe.Add(ref elemRef, 55), ref Unsafe.Add(ref elemRef, 62)); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykScalar.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykScalar.cs new file mode 100644 index 0000000..99b9cdf --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykScalar.cs @@ -0,0 +1,117 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class CmykScalar : JpegColorConverterScalar + { + public CmykScalar(int precision) + : base(JpegColorSpace.Cmyk, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) => + ConvertToRgbInPlace(values, this.MaximumValue); + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => ConvertFromRgb(values, this.MaximumValue, rLane, gLane, bLane); + + public static void ConvertToRgbInPlace(in ComponentValues values, float maxValue) + { + Span c0 = values.Component0; + Span c1 = values.Component1; + Span c2 = values.Component2; + Span c3 = values.Component3; + + float scale = 1 / (maxValue * maxValue); + for (int i = 0; i < c0.Length; i++) + { + float c = c0[i]; + float m = c1[i]; + float y = c2[i]; + float k = c3[i]; + + k *= scale; + c0[i] = c * k; + c1[i] = m * k; + c2[i] = y * k; + } + } + + public static void ConvertFromRgb(in ComponentValues values, float maxValue, Span rLane, Span gLane, Span bLane) + { + Span c = values.Component0; + Span m = values.Component1; + Span y = values.Component2; + Span k = values.Component3; + + for (int i = 0; i < c.Length; i++) + { + float ctmp = 255f - rLane[i]; + float mtmp = 255f - gLane[i]; + float ytmp = 255f - bLane[i]; + float ktmp = MathF.Min(MathF.Min(ctmp, mtmp), ytmp); + + if (ktmp >= 255f) + { + ctmp = 0f; + mtmp = 0f; + ytmp = 0f; + } + else + { + ctmp = (ctmp - ktmp) / (255f - ktmp); + mtmp = (mtmp - ktmp) / (255f - ktmp); + ytmp = (ytmp - ktmp) / (255f - ktmp); + } + + c[i] = maxValue - (ctmp * maxValue); + m[i] = maxValue - (mtmp * maxValue); + y[i] = maxValue - (ytmp * maxValue); + k[i] = maxValue - ktmp; + } + } + + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) + { + using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 4); + Span packed = memoryOwner.Memory.Span; + + Span c0 = values.Component0; + Span c1 = values.Component1; + Span c2 = values.Component2; + Span c3 = values.Component3; + + PackedInvertNormalizeInterleave4(c0, c1, c2, c3, packed, maxValue); + + Span source = MemoryMarshal.Cast(packed); + Span destination = MemoryMarshal.Cast(packed)[..source.Length]; + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + }; + ColorProfileConverter converter = new(options); + converter.Convert(source, destination); + + UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector128.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector128.cs new file mode 100644 index 0000000..c1813fa --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector128.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class CmykVector128 : JpegColorConverterVector128 + { + public CmykVector128(int precision) + : base(JpegColorSpace.Cmyk, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + { + ref Vector128 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector128 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector128 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector128 c3Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + // Used for the color conversion + Vector128 scale = Vector128.Create(1 / (this.MaximumValue * this.MaximumValue)); + + nuint n = values.Component0.Vector128Count(); + for (nuint i = 0; i < n; i++) + { + ref Vector128 c = ref Unsafe.Add(ref c0Base, i); + ref Vector128 m = ref Unsafe.Add(ref c1Base, i); + ref Vector128 y = ref Unsafe.Add(ref c2Base, i); + Vector128 k = Unsafe.Add(ref c3Base, i); + + k *= scale; + c *= k; + m *= k; + y *= k; + } + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => CmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane); + + public static void ConvertFromRgb(in ComponentValues values, float maxValue, Span rLane, Span gLane, Span bLane) + { + ref Vector128 destC = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector128 destM = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector128 destY = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector128 destK = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + ref Vector128 srcR = + ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); + ref Vector128 srcG = + ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); + ref Vector128 srcB = + ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); + + Vector128 scale = Vector128.Create(maxValue); + + nuint n = values.Component0.Vector128Count(); + for (nuint i = 0; i < n; i++) + { + Vector128 ctmp = scale - Unsafe.Add(ref srcR, i); + Vector128 mtmp = scale - Unsafe.Add(ref srcG, i); + Vector128 ytmp = scale - Unsafe.Add(ref srcB, i); + Vector128 ktmp = Vector128.Min(ctmp, Vector128.Min(mtmp, ytmp)); + + Vector128 kMask = ~Vector128.Equals(ktmp, scale); + Vector128 divisor = scale - ktmp; + + ctmp = ((ctmp - ktmp) / divisor) & kMask; + mtmp = ((mtmp - ktmp) / divisor) & kMask; + ytmp = ((ytmp - ktmp) / divisor) & kMask; + + Unsafe.Add(ref destC, i) = scale - (ctmp * scale); + Unsafe.Add(ref destM, i) = scale - (mtmp * scale); + Unsafe.Add(ref destY, i) = scale - (ytmp * scale); + Unsafe.Add(ref destK, i) = scale - ktmp; + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector256.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector256.cs new file mode 100644 index 0000000..af44d06 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector256.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class CmykVector256 : JpegColorConverterVector256 + { + public CmykVector256(int precision) + : base(JpegColorSpace.Cmyk, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + { + ref Vector256 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector256 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector256 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector256 c3Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + // Used for the color conversion + Vector256 scale = Vector256.Create(1 / (this.MaximumValue * this.MaximumValue)); + + nuint n = values.Component0.Vector256Count(); + for (nuint i = 0; i < n; i++) + { + ref Vector256 c = ref Unsafe.Add(ref c0Base, i); + ref Vector256 m = ref Unsafe.Add(ref c1Base, i); + ref Vector256 y = ref Unsafe.Add(ref c2Base, i); + Vector256 k = Unsafe.Add(ref c3Base, i); + + k *= scale; + c *= k; + m *= k; + y *= k; + } + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => CmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane); + + public static void ConvertFromRgb(in ComponentValues values, float maxValue, Span rLane, Span gLane, Span bLane) + { + ref Vector256 destC = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector256 destM = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector256 destY = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector256 destK = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + ref Vector256 srcR = + ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); + ref Vector256 srcG = + ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); + ref Vector256 srcB = + ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); + + Vector256 scale = Vector256.Create(maxValue); + + nuint n = values.Component0.Vector256Count(); + for (nuint i = 0; i < n; i++) + { + Vector256 ctmp = scale - Unsafe.Add(ref srcR, i); + Vector256 mtmp = scale - Unsafe.Add(ref srcG, i); + Vector256 ytmp = scale - Unsafe.Add(ref srcB, i); + Vector256 ktmp = Vector256.Min(ctmp, Vector256.Min(mtmp, ytmp)); + + Vector256 kMask = ~Vector256.Equals(ktmp, scale); + Vector256 divisor = scale - ktmp; + + ctmp = ((ctmp - ktmp) / divisor) & kMask; + mtmp = ((mtmp - ktmp) / divisor) & kMask; + ytmp = ((ytmp - ktmp) / divisor) & kMask; + + Unsafe.Add(ref destC, i) = scale - (ctmp * scale); + Unsafe.Add(ref destM, i) = scale - (mtmp * scale); + Unsafe.Add(ref destY, i) = scale - (ytmp * scale); + Unsafe.Add(ref destK, i) = scale - ktmp; + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector512.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector512.cs new file mode 100644 index 0000000..a4664ce --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector512.cs @@ -0,0 +1,109 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class CmykVector512 : JpegColorConverterVector512 + { + public CmykVector512(int precision) + : base(JpegColorSpace.Cmyk, precision) + { + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => CmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) + { + ref Vector512 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector512 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector512 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector512 c3Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + // Used for the color conversion + Vector512 scale = Vector512.Create(1 / (this.MaximumValue * this.MaximumValue)); + + nuint n = values.Component0.Vector512Count(); + for (nuint i = 0; i < n; i++) + { + ref Vector512 c = ref Unsafe.Add(ref c0Base, i); + ref Vector512 m = ref Unsafe.Add(ref c1Base, i); + ref Vector512 y = ref Unsafe.Add(ref c2Base, i); + Vector512 k = Unsafe.Add(ref c3Base, i); + + k *= scale; + c *= k; + m *= k; + y *= k; + } + } + + /// + protected override void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => ConvertFromRgbVectorized(in values, this.MaximumValue, rLane, gLane, bLane); + + /// + protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) + => CmykScalar.ConvertToRgbInPlace(values, this.MaximumValue); + + /// + protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => CmykScalar.ConvertFromRgb(values, this.MaximumValue, rLane, gLane, bLane); + + internal static void ConvertFromRgbVectorized(in ComponentValues values, float maxValue, Span rLane, Span gLane, Span bLane) + { + ref Vector512 destC = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector512 destM = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector512 destY = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector512 destK = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + ref Vector512 srcR = + ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); + ref Vector512 srcG = + ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); + ref Vector512 srcB = + ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); + + Vector512 scale = Vector512.Create(maxValue); + + nuint n = values.Component0.Vector512Count(); + for (nuint i = 0; i < n; i++) + { + Vector512 ctmp = scale - Unsafe.Add(ref srcR, i); + Vector512 mtmp = scale - Unsafe.Add(ref srcG, i); + Vector512 ytmp = scale - Unsafe.Add(ref srcB, i); + Vector512 ktmp = Vector512.Min(ctmp, Vector512.Min(mtmp, ytmp)); + + Vector512 kMask = ~Vector512.Equals(ktmp, scale); + Vector512 divisor = scale - ktmp; + + ctmp = ((ctmp - ktmp) / divisor) & kMask; + mtmp = ((mtmp - ktmp) / divisor) & kMask; + ytmp = ((ytmp - ktmp) / divisor) & kMask; + + Unsafe.Add(ref destC, i) = scale - (ctmp * scale); + Unsafe.Add(ref destM, i) = scale - (mtmp * scale); + Unsafe.Add(ref destY, i) = scale - (ytmp * scale); + Unsafe.Add(ref destK, i) = scale - ktmp; + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs new file mode 100644 index 0000000..dc6c94f --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs @@ -0,0 +1,98 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class GrayScaleScalar : JpegColorConverterScalar + { + public GrayScaleScalar(int precision) + : base(JpegColorSpace.Grayscale, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + => ConvertToRgbInPlace(in values, this.MaximumValue); + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => ConvertFromRgbScalar(values, rLane, gLane, bLane); + + internal static void ConvertToRgbInPlace(in ComponentValues values, float maxValue) + { + ref float c0Base = ref MemoryMarshal.GetReference(values.Component0); + ref float c1Base = ref MemoryMarshal.GetReference(values.Component1); + ref float c2Base = ref MemoryMarshal.GetReference(values.Component2); + + float scale = 1F / maxValue; + for (nuint i = 0; i < (nuint)values.Component0.Length; i++) + { + float c = Unsafe.Add(ref c0Base, i) * scale; + + Unsafe.Add(ref c0Base, i) = c; + Unsafe.Add(ref c1Base, i) = c; + Unsafe.Add(ref c2Base, i) = c; + } + } + + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) + { + using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 3); + Span packed = memoryOwner.Memory.Span; + + Span c0 = values.Component0; + Span c1 = values.Component1; + Span c2 = values.Component2; + + ref float c0Base = ref MemoryMarshal.GetReference(c0); + ref float c1Base = ref MemoryMarshal.GetReference(c1); + ref float c2Base = ref MemoryMarshal.GetReference(c2); + + float scale = 1F / maxValue; + for (nuint i = 0; i < (nuint)values.Component0.Length; i++) + { + ref float c = ref Unsafe.Add(ref c0Base, i); + c *= scale; + } + + Span source = MemoryMarshal.Cast(values.Component0); + Span destination = MemoryMarshal.Cast(packed); + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + }; + ColorProfileConverter converter = new(options); + converter.Convert(source, destination); + + UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); + } + + internal static void ConvertFromRgbScalar(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + Span c0 = values.Component0; + + for (int i = 0; i < c0.Length; i++) + { + // luminosity = (0.299 * r) + (0.587 * g) + (0.114 * b) + c0[i] = (float)((0.299f * rLane[i]) + (0.587f * gLane[i]) + (0.114f * bLane[i])); + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs new file mode 100644 index 0000000..0851119 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class GrayScaleVector128 : JpegColorConverterVector128 + { + public GrayScaleVector128(int precision) + : base(JpegColorSpace.Grayscale, precision) + { + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => GrayScaleScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + { + ref Vector128 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + + ref Vector128 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + + ref Vector128 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + + // Used for the color conversion + Vector128 scale = Vector128.Create(1 / this.MaximumValue); + + nuint n = values.Component0.Vector128Count(); + for (nuint i = 0; i < n; i++) + { + Vector128 c = Unsafe.Add(ref c0Base, i) * scale; + + Unsafe.Add(ref c0Base, i) = c; + Unsafe.Add(ref c1Base, i) = c; + Unsafe.Add(ref c2Base, i) = c; + } + } + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + ref Vector128 destLuminance = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + + ref Vector128 srcRed = + ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); + ref Vector128 srcGreen = + ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); + ref Vector128 srcBlue = + ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); + + // Used for the color conversion + Vector128 f0299 = Vector128.Create(0.299f); + Vector128 f0587 = Vector128.Create(0.587f); + Vector128 f0114 = Vector128.Create(0.114f); + + nuint n = values.Component0.Vector128Count(); + for (nuint i = 0; i < n; i++) + { + ref Vector128 r = ref Unsafe.Add(ref srcRed, i); + ref Vector128 g = ref Unsafe.Add(ref srcGreen, i); + ref Vector128 b = ref Unsafe.Add(ref srcBlue, i); + + // luminosity = (0.299 * r) + (0.587 * g) + (0.114 * b) + Unsafe.Add(ref destLuminance, i) = Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs new file mode 100644 index 0000000..bc79000 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class GrayScaleVector256 : JpegColorConverterVector256 + { + public GrayScaleVector256(int precision) + : base(JpegColorSpace.Grayscale, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + { + ref Vector256 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + + ref Vector256 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + + ref Vector256 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + + // Used for the color conversion + Vector256 scale = Vector256.Create(1 / this.MaximumValue); + + nuint n = values.Component0.Vector256Count(); + for (nuint i = 0; i < n; i++) + { + Vector256 c = Unsafe.Add(ref c0Base, i) * scale; + + Unsafe.Add(ref c0Base, i) = c; + Unsafe.Add(ref c1Base, i) = c; + Unsafe.Add(ref c2Base, i) = c; + } + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => GrayScaleScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + ref Vector256 destLuminance = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + + ref Vector256 srcRed = + ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); + ref Vector256 srcGreen = + ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); + ref Vector256 srcBlue = + ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); + + // Used for the color conversion + Vector256 f0299 = Vector256.Create(0.299f); + Vector256 f0587 = Vector256.Create(0.587f); + Vector256 f0114 = Vector256.Create(0.114f); + + nuint n = values.Component0.Vector256Count(); + for (nuint i = 0; i < n; i++) + { + ref Vector256 r = ref Unsafe.Add(ref srcRed, i); + ref Vector256 g = ref Unsafe.Add(ref srcGreen, i); + ref Vector256 b = ref Unsafe.Add(ref srcBlue, i); + + // luminosity = (0.299 * r) + (0.587 * g) + (0.114 * b) + Unsafe.Add(ref destLuminance, i) = Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs new file mode 100644 index 0000000..168ecb2 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs @@ -0,0 +1,90 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class GrayScaleVector512 : JpegColorConverterVector512 + { + public GrayScaleVector512(int precision) + : base(JpegColorSpace.Grayscale, precision) + { + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => GrayScaleScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) + { + ref Vector512 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + + ref Vector512 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + + ref Vector512 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + + // Used for the color conversion + Vector512 scale = Vector512.Create(1 / this.MaximumValue); + + nuint n = values.Component0.Vector512Count(); + for (nuint i = 0; i < n; i++) + { + Vector512 c = Unsafe.Add(ref c0Base, i) * scale; + + Unsafe.Add(ref c0Base, i) = c; + Unsafe.Add(ref c1Base, i) = c; + Unsafe.Add(ref c2Base, i) = c; + } + } + + /// + protected override void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + ref Vector512 destLuminance = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + + ref Vector512 srcRed = + ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); + ref Vector512 srcGreen = + ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); + ref Vector512 srcBlue = + ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); + + // Used for the color conversion + Vector512 f0299 = Vector512.Create(0.299f); + Vector512 f0587 = Vector512.Create(0.587f); + Vector512 f0114 = Vector512.Create(0.114f); + + nuint n = values.Component0.Vector512Count(); + for (nuint i = 0; i < n; i++) + { + ref Vector512 r = ref Unsafe.Add(ref srcRed, i); + ref Vector512 g = ref Unsafe.Add(ref srcGreen, i); + ref Vector512 b = ref Unsafe.Add(ref srcBlue, i); + + // luminosity = (0.299 * r) + (0.587 * g) + (0.114 * b) + Unsafe.Add(ref destLuminance, i) = Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); + } + } + + /// + protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) + => GrayScaleScalar.ConvertToRgbInPlace(in values, this.MaximumValue); + + /// + protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => GrayScaleScalar.ConvertFromRgbScalar(values, rLane, gLane, bLane); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs new file mode 100644 index 0000000..260eb0b --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class RgbScalar : JpegColorConverterScalar + { + public RgbScalar(int precision) + : base(JpegColorSpace.RGB, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + => ConvertToRgbInPlace(values, this.MaximumValue); + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => ConvertFromRgb(values, rLane, gLane, bLane); + + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) + { + using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 3); + Span packed = memoryOwner.Memory.Span; + + Span c0 = values.Component0; + Span c1 = values.Component1; + Span c2 = values.Component2; + + PackedNormalizeInterleave3(c0, c1, c2, packed, 1F / maxValue); + + Span source = MemoryMarshal.Cast(packed); + Span destination = MemoryMarshal.Cast(packed); + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + }; + ColorProfileConverter converter = new(options); + converter.Convert(source, destination); + + UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); + } + + internal static void ConvertToRgbInPlace(ComponentValues values, float maxValue) + { + ref float c0Base = ref MemoryMarshal.GetReference(values.Component0); + ref float c1Base = ref MemoryMarshal.GetReference(values.Component1); + ref float c2Base = ref MemoryMarshal.GetReference(values.Component2); + + float scale = 1F / maxValue; + + for (nuint i = 0; i < (nuint)values.Component0.Length; i++) + { + Unsafe.Add(ref c0Base, i) *= scale; + Unsafe.Add(ref c1Base, i) *= scale; + Unsafe.Add(ref c2Base, i) *= scale; + } + } + + internal static void ConvertFromRgb(ComponentValues values, Span rLane, Span gLane, Span bLane) + { + // TODO: This doesn't seem correct. We should be scaling to the maximum value here. + rLane.CopyTo(values.Component0); + gLane.CopyTo(values.Component1); + bLane.CopyTo(values.Component2); + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector128.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector128.cs new file mode 100644 index 0000000..79771e5 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector128.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class RgbVector128 : JpegColorConverterVector128 + { + public RgbVector128(int precision) + : base(JpegColorSpace.RGB, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + { + ref Vector128 rBase = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector128 gBase = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector128 bBase = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + + // Used for the color conversion + Vector128 scale = Vector128.Create(1 / this.MaximumValue); + nuint n = values.Component0.Vector128Count(); + for (nuint i = 0; i < n; i++) + { + ref Vector128 r = ref Unsafe.Add(ref rBase, i); + ref Vector128 g = ref Unsafe.Add(ref gBase, i); + ref Vector128 b = ref Unsafe.Add(ref bBase, i); + r *= scale; + g *= scale; + b *= scale; + } + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => RgbScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + rLane.CopyTo(values.Component0); + gLane.CopyTo(values.Component1); + bLane.CopyTo(values.Component2); + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector256.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector256.cs new file mode 100644 index 0000000..8a34dbd --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector256.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class RgbVector256 : JpegColorConverterVector256 + { + public RgbVector256(int precision) + : base(JpegColorSpace.RGB, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + { + ref Vector256 rBase = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector256 gBase = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector256 bBase = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + + // Used for the color conversion + Vector256 scale = Vector256.Create(1 / this.MaximumValue); + nuint n = values.Component0.Vector256Count(); + for (nuint i = 0; i < n; i++) + { + ref Vector256 r = ref Unsafe.Add(ref rBase, i); + ref Vector256 g = ref Unsafe.Add(ref gBase, i); + ref Vector256 b = ref Unsafe.Add(ref bBase, i); + r *= scale; + g *= scale; + b *= scale; + } + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => RgbScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + rLane.CopyTo(values.Component0); + gLane.CopyTo(values.Component1); + bLane.CopyTo(values.Component2); + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector512.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector512.cs new file mode 100644 index 0000000..8b65b57 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector512.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class RgbVector512 : JpegColorConverterVector512 + { + public RgbVector512(int precision) + : base(JpegColorSpace.RGB, precision) + { + } + + /// + protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) + { + ref Vector512 rBase = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector512 gBase = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector512 bBase = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + + // Used for the color conversion + Vector512 scale = Vector512.Create(1 / this.MaximumValue); + nuint n = values.Component0.Vector512Count(); + for (nuint i = 0; i < n; i++) + { + ref Vector512 r = ref Unsafe.Add(ref rBase, i); + ref Vector512 g = ref Unsafe.Add(ref gBase, i); + ref Vector512 b = ref Unsafe.Add(ref bBase, i); + r *= scale; + g *= scale; + b *= scale; + } + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => RgbScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + protected override void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + rLane.CopyTo(values.Component0); + gLane.CopyTo(values.Component1); + bLane.CopyTo(values.Component2); + } + + /// + protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) + => RgbScalar.ConvertToRgbInPlace(values, this.MaximumValue); + + /// + protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => RgbScalar.ConvertFromRgb(values, rLane, gLane, bLane); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykScalar.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykScalar.cs new file mode 100644 index 0000000..90d4579 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykScalar.cs @@ -0,0 +1,119 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + /// + /// Color converter for tiff images, which use the jpeg compression and CMYK colorspace. + /// + internal sealed class TiffCmykScalar : JpegColorConverterScalar + { + public TiffCmykScalar(int precision) + : base(JpegColorSpace.TiffCmyk, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + => ConvertToRgbInPlace(in values, this.MaximumValue); + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane); + + public static void ConvertToRgbInPlace(in ComponentValues values, float maxValue) + { + Span c0 = values.Component0; + Span c1 = values.Component1; + Span c2 = values.Component2; + Span c3 = values.Component3; + + float scale = 1 / maxValue; + for (int i = 0; i < c0.Length; i++) + { + float c = c0[i] * scale; + float m = c1[i] * scale; + float y = c2[i] * scale; + float k = 1 - (c3[i] * scale); + + c0[i] = (1 - c) * k; + c1[i] = (1 - m) * k; + c2[i] = (1 - y) * k; + } + } + + public static void ConvertFromRgb(in ComponentValues values, float maxValue, Span rLane, Span gLane, Span bLane) + { + Span c = values.Component0; + Span m = values.Component1; + Span y = values.Component2; + Span k = values.Component3; + + for (int i = 0; i < c.Length; i++) + { + float ctmp = 255F - rLane[i]; + float mtmp = 255F - gLane[i]; + float ytmp = 255F - bLane[i]; + float ktmp = MathF.Min(MathF.Min(ctmp, mtmp), ytmp); + + if (ktmp >= 255F) + { + ctmp = 0F; + mtmp = 0F; + ytmp = 0F; + } + else + { + float divisor = 1 / (255F - ktmp); + ctmp = (ctmp - ktmp) * divisor; + mtmp = (mtmp - ktmp) * divisor; + ytmp = (ytmp - ktmp) * divisor; + } + + c[i] = ctmp * maxValue; + m[i] = mtmp * maxValue; + y[i] = ytmp * maxValue; + k[i] = ktmp; + } + } + + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) + { + using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 4); + Span packed = memoryOwner.Memory.Span; + + Span c0 = values.Component0; + Span c1 = values.Component1; + Span c2 = values.Component2; + Span c3 = values.Component3; + + PackedNormalizeInterleave4(c0, c1, c2, c3, packed, maxValue); + + Span source = MemoryMarshal.Cast(packed); + Span destination = MemoryMarshal.Cast(packed)[..source.Length]; + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + }; + ColorProfileConverter converter = new(options); + converter.Convert(source, destination); + + UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector128.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector128.cs new file mode 100644 index 0000000..ff4df46 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector128.cs @@ -0,0 +1,100 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class TiffCmykVector128 : JpegColorConverterVector128 + { + public TiffCmykVector128(int precision) + : base(JpegColorSpace.TiffCmyk, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + { + ref Vector128 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector128 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector128 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector128 c3Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + Vector128 scale = Vector128.Create(1 / this.MaximumValue); + + nuint n = values.Component0.Vector128Count(); + for (nuint i = 0; i < n; i++) + { + ref Vector128 c = ref Unsafe.Add(ref c0Base, i); + ref Vector128 m = ref Unsafe.Add(ref c1Base, i); + ref Vector128 y = ref Unsafe.Add(ref c2Base, i); + Vector128 k = Unsafe.Add(ref c3Base, i); + + k = Vector128.One - (k * scale); + c = (Vector128.One - (c * scale)) * k; + m = (Vector128.One - (m * scale)) * k; + y = (Vector128.One - (y * scale)) * k; + } + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => TiffCmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane); + + public static void ConvertFromRgb(in ComponentValues values, float maxValue, Span rLane, Span gLane, Span bLane) + { + ref Vector128 destC = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector128 destM = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector128 destY = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector128 destK = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + ref Vector128 srcR = + ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); + ref Vector128 srcG = + ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); + ref Vector128 srcB = + ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); + + Vector128 scale = Vector128.Create(maxValue); + + nuint n = values.Component0.Vector128Count(); + for (nuint i = 0; i < n; i++) + { + Vector128 ctmp = scale - Unsafe.Add(ref srcR, i); + Vector128 mtmp = scale - Unsafe.Add(ref srcG, i); + Vector128 ytmp = scale - Unsafe.Add(ref srcB, i); + Vector128 ktmp = Vector128.Min(ctmp, Vector128.Min(mtmp, ytmp)); + + Vector128 kMask = ~Vector128.Equals(ktmp, scale); + Vector128 divisor = Vector128.One / (scale - ktmp); + + ctmp = ((ctmp - ktmp) * divisor) & kMask; + mtmp = ((mtmp - ktmp) * divisor) & kMask; + ytmp = ((ytmp - ktmp) * divisor) & kMask; + + Unsafe.Add(ref destC, i) = ctmp * scale; + Unsafe.Add(ref destM, i) = mtmp * scale; + Unsafe.Add(ref destY, i) = ytmp * scale; + Unsafe.Add(ref destK, i) = ktmp; + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector256.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector256.cs new file mode 100644 index 0000000..7d456c5 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector256.cs @@ -0,0 +1,100 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class TiffCmykVector256 : JpegColorConverterVector256 + { + public TiffCmykVector256(int precision) + : base(JpegColorSpace.TiffCmyk, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + { + ref Vector256 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector256 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector256 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector256 c3Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + Vector256 scale = Vector256.Create(1 / this.MaximumValue); + + nuint n = values.Component0.Vector256Count(); + for (nuint i = 0; i < n; i++) + { + ref Vector256 c = ref Unsafe.Add(ref c0Base, i); + ref Vector256 m = ref Unsafe.Add(ref c1Base, i); + ref Vector256 y = ref Unsafe.Add(ref c2Base, i); + Vector256 k = Unsafe.Add(ref c3Base, i); + + k = Vector256.One - (k * scale); + c = (Vector256.One - (c * scale)) * k; + m = (Vector256.One - (m * scale)) * k; + y = (Vector256.One - (y * scale)) * k; + } + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => CmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane); + + public static void ConvertFromRgb(in ComponentValues values, float maxValue, Span rLane, Span gLane, Span bLane) + { + ref Vector256 destC = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector256 destM = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector256 destY = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector256 destK = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + ref Vector256 srcR = + ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); + ref Vector256 srcG = + ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); + ref Vector256 srcB = + ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); + + Vector256 scale = Vector256.Create(maxValue); + + nuint n = values.Component0.Vector256Count(); + for (nuint i = 0; i < n; i++) + { + Vector256 ctmp = scale - Unsafe.Add(ref srcR, i); + Vector256 mtmp = scale - Unsafe.Add(ref srcG, i); + Vector256 ytmp = scale - Unsafe.Add(ref srcB, i); + Vector256 ktmp = Vector256.Min(ctmp, Vector256.Min(mtmp, ytmp)); + + Vector256 kMask = ~Vector256.Equals(ktmp, scale); + Vector256 divisor = Vector256.One / (scale - ktmp); + + ctmp = ((ctmp - ktmp) * divisor) & kMask; + mtmp = ((mtmp - ktmp) * divisor) & kMask; + ytmp = ((ytmp - ktmp) * divisor) & kMask; + + Unsafe.Add(ref destC, i) = ctmp * scale; + Unsafe.Add(ref destM, i) = mtmp * scale; + Unsafe.Add(ref destY, i) = ytmp * scale; + Unsafe.Add(ref destK, i) = ktmp; + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector512.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector512.cs new file mode 100644 index 0000000..ff6b54b --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector512.cs @@ -0,0 +1,109 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class TiffCmykVector512 : JpegColorConverterVector512 + { + public TiffCmykVector512(int precision) + : base(JpegColorSpace.TiffCmyk, precision) + { + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => TiffCmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) + { + ref Vector512 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector512 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector512 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector512 c3Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + // Used for the color conversion + Vector512 scale = Vector512.Create(1 / this.MaximumValue); + + nuint n = values.Component0.Vector512Count(); + for (nuint i = 0; i < n; i++) + { + ref Vector512 c = ref Unsafe.Add(ref c0Base, i); + ref Vector512 m = ref Unsafe.Add(ref c1Base, i); + ref Vector512 y = ref Unsafe.Add(ref c2Base, i); + Vector512 k = Unsafe.Add(ref c3Base, i); + + k = Vector512.One - (k * scale); + c = (Vector512.One - (c * scale)) * k; + m = (Vector512.One - (m * scale)) * k; + y = (Vector512.One - (y * scale)) * k; + } + } + + /// + protected override void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => ConvertFromRgbVectorized(in values, this.MaximumValue, rLane, gLane, bLane); + + /// + protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) + => TiffCmykScalar.ConvertToRgbInPlace(values, this.MaximumValue); + + /// + protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => TiffCmykScalar.ConvertFromRgb(values, this.MaximumValue, rLane, gLane, bLane); + + internal static void ConvertFromRgbVectorized(in ComponentValues values, float maxValue, Span rLane, Span gLane, Span bLane) + { + ref Vector512 destC = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector512 destM = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector512 destY = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector512 destK = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + ref Vector512 srcR = + ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); + ref Vector512 srcG = + ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); + ref Vector512 srcB = + ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); + + Vector512 scale = Vector512.Create(maxValue); + + nuint n = values.Component0.Vector512Count(); + for (nuint i = 0; i < n; i++) + { + Vector512 ctmp = scale - Unsafe.Add(ref srcR, i); + Vector512 mtmp = scale - Unsafe.Add(ref srcG, i); + Vector512 ytmp = scale - Unsafe.Add(ref srcB, i); + Vector512 ktmp = Vector512.Min(ctmp, Vector512.Min(mtmp, ytmp)); + + Vector512 kMask = ~Vector512.Equals(ktmp, scale); + Vector512 divisor = Vector512.One / (scale - ktmp); + + ctmp = ((ctmp - ktmp) * divisor) & kMask; + mtmp = ((mtmp - ktmp) * divisor) & kMask; + ytmp = ((ytmp - ktmp) * divisor) & kMask; + + Unsafe.Add(ref destC, i) = ctmp * scale; + Unsafe.Add(ref destM, i) = mtmp * scale; + Unsafe.Add(ref destY, i) = ytmp * scale; + Unsafe.Add(ref destK, i) = ktmp; + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKScalar.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKScalar.cs new file mode 100644 index 0000000..d8f14b5 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKScalar.cs @@ -0,0 +1,154 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + /// + /// Color converter for tiff images, which use the jpeg compression and CMYK colorspace. + /// + internal sealed class TiffYccKScalar : JpegColorConverterScalar + { + // Derived from ITU-T Rec. T.871 + internal const float RCrMult = 1.402f; + internal const float GCbMult = (float)(0.114 * 1.772 / 0.587); + internal const float GCrMult = (float)(0.299 * 1.402 / 0.587); + internal const float BCbMult = 1.772f; + + public TiffYccKScalar(int precision) + : base(JpegColorSpace.TiffYccK, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + => ConvertToRgbInPlace(in values, this.MaximumValue, this.HalfValue); + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => ConvertFromRgb(values, this.HalfValue, this.MaximumValue, rLane, gLane, bLane); + + public static void ConvertToRgbInPlace(in ComponentValues values, float maxValue, float halfValue) + { + Span c0 = values.Component0; + Span c1 = values.Component1; + Span c2 = values.Component2; + Span c3 = values.Component3; + + float scale = 1F / maxValue; + halfValue *= scale; + + for (int i = 0; i < values.Component0.Length; i++) + { + float y = c0[i] * scale; + float cb = (c1[i] * scale) - halfValue; + float cr = (c2[i] * scale) - halfValue; + float scaledK = 1 - (c3[i] * scale); + + // r = y + (1.402F * cr); + // g = y - (0.344136F * cb) - (0.714136F * cr); + // b = y + (1.772F * cb); + c0[i] = (y + (RCrMult * cr)) * scaledK; + c1[i] = (y - (GCbMult * cb) - (GCrMult * cr)) * scaledK; + c2[i] = (y + (BCbMult * cb)) * scaledK; + } + } + + public static void ConvertFromRgb(in ComponentValues values, float halfValue, float maxValue, Span rLane, Span gLane, Span bLane) + { + Span y = values.Component0; + Span cb = values.Component1; + Span cr = values.Component2; + Span k = values.Component3; + + for (int i = 0; i < cr.Length; i++) + { + // Scale down to [0-1] + const float divisor = 1F / 255F; + float r = rLane[i] * divisor; + float g = gLane[i] * divisor; + float b = bLane[i] * divisor; + + float ytmp; + float cbtmp; + float crtmp; + float ktmp = 1F - MathF.Max(r, MathF.Max(g, b)); + + if (ktmp >= 1F) + { + ytmp = 0F; + cbtmp = 0.5F; + crtmp = 0.5F; + ktmp = maxValue; + } + else + { + float kmask = 1F / (1F - ktmp); + r *= kmask; + g *= kmask; + b *= kmask; + + // Scale to [0-maxValue] + ytmp = ((0.299f * r) + (0.587f * g) + (0.114f * b)) * maxValue; + cbtmp = halfValue - (((0.168736f * r) - (0.331264f * g) + (0.5f * b)) * maxValue); + crtmp = halfValue + (((0.5f * r) - (0.418688f * g) - (0.081312f * b)) * maxValue); + ktmp *= maxValue; + } + + y[i] = ytmp; + cb[i] = cbtmp; + cr[i] = crtmp; + k[i] = ktmp; + } + } + + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) + { + using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 4); + Span packed = memoryOwner.Memory.Span; + + Span c0 = values.Component0; + Span c1 = values.Component1; + Span c2 = values.Component2; + Span c3 = values.Component3; + + PackedNormalizeInterleave4(c0, c1, c2, c3, packed, maxValue); + + ColorProfileConverter converter = new(); + Span source = MemoryMarshal.Cast(packed); + + // YccK is not a defined ICC color space � it's a JPEG-specific encoding used in Adobe-style CMYK JPEGs. + // ICC profiles expect colorimetric CMYK values, so we must first convert YccK to CMYK using a hardcoded inverse transform. + // This transform assumes Rec.601 YCbCr coefficients and an inverted K channel. + // + // The YccK => Cmyk conversion is independent of any embedded ICC profile. + // Since the same RGB working space is used during conversion to and from XYZ, + // colorimetric accuracy is preserved. + converter.Convert(MemoryMarshal.Cast(source), source); + + Span destination = MemoryMarshal.Cast(packed)[..source.Length]; + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + }; + converter = new ColorProfileConverter(options); + converter.Convert(source, destination); + + UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector128.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector128.cs new file mode 100644 index 0000000..c4e302c --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector128.cs @@ -0,0 +1,132 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class TiffYccKVector128 : JpegColorConverterVector128 + { + public TiffYccKVector128(int precision) + : base(JpegColorSpace.TiffYccK, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + { + ref Vector128 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector128 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector128 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector128 c3Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + Vector128 scale = Vector128.Create(1F / this.MaximumValue); + Vector128 chromaOffset = Vector128.Create(this.HalfValue) * scale; + Vector128 rCrMult = Vector128.Create(YCbCrScalar.RCrMult); + Vector128 gCbMult = Vector128.Create(-YCbCrScalar.GCbMult); + Vector128 gCrMult = Vector128.Create(-YCbCrScalar.GCrMult); + Vector128 bCbMult = Vector128.Create(YCbCrScalar.BCbMult); + + nuint n = values.Component0.Vector128Count(); + for (nuint i = 0; i < n; i++) + { + ref Vector128 c0 = ref Unsafe.Add(ref c0Base, i); + ref Vector128 c1 = ref Unsafe.Add(ref c1Base, i); + ref Vector128 c2 = ref Unsafe.Add(ref c2Base, i); + ref Vector128 c3 = ref Unsafe.Add(ref c3Base, i); + + Vector128 y = c0 * scale; + Vector128 cb = (c1 * scale) - chromaOffset; + Vector128 cr = (c2 * scale) - chromaOffset; + Vector128 scaledK = Vector128.One - (c3 * scale); + + // r = y + (1.402F * cr); + // g = y - (0.344136F * cb) - (0.714136F * cr); + // b = y + (1.772F * cb); + Vector128 r = Vector128_.MultiplyAdd(y, cr, rCrMult) * scaledK; + Vector128 g = Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult) * scaledK; + Vector128 b = Vector128_.MultiplyAdd(y, cb, bCbMult) * scaledK; + + c0 = r; + c1 = g; + c2 = b; + } + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => TiffYccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + ref Vector128 srcR = + ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); + ref Vector128 srcG = + ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); + ref Vector128 srcB = + ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); + + ref Vector128 destY = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector128 destCb = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector128 destCr = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector128 destK = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + Vector128 maxSourceValue = Vector128.Create(1 / 255F); + Vector128 maxSampleValue = Vector128.Create(this.MaximumValue); + Vector128 chromaOffset = Vector128.Create(this.HalfValue); + + Vector128 f0299 = Vector128.Create(0.299f); + Vector128 f0587 = Vector128.Create(0.587f); + Vector128 f0114 = Vector128.Create(0.114f); + Vector128 fn0168736 = Vector128.Create(-0.168736f); + Vector128 fn0331264 = Vector128.Create(-0.331264f); + Vector128 fn0418688 = Vector128.Create(-0.418688f); + Vector128 fn0081312F = Vector128.Create(-0.081312F); + Vector128 f05 = Vector128.Create(0.5f); + + nuint n = values.Component0.Vector128Count(); + for (nuint i = 0; i < n; i++) + { + Vector128 r = Unsafe.Add(ref srcR, i) * maxSourceValue; + Vector128 g = Unsafe.Add(ref srcG, i) * maxSourceValue; + Vector128 b = Unsafe.Add(ref srcB, i) * maxSourceValue; + Vector128 ktmp = Vector128.One - Vector128.Max(r, Vector128.Min(g, b)); + + Vector128 kMask = ~Vector128.Equals(ktmp, Vector128.One); + Vector128 divisor = Vector128.One / (Vector128.One - ktmp); + + r = (r * divisor) & kMask; + g = (g * divisor) & kMask; + b = (b * divisor) & kMask; + + // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) + // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) + // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) + Vector128 y = Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); + Vector128 cb = chromaOffset + Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); + Vector128 cr = chromaOffset + Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); + + Unsafe.Add(ref destY, i) = y * maxSampleValue; + Unsafe.Add(ref destCb, i) = chromaOffset + (cb * maxSampleValue); + Unsafe.Add(ref destCr, i) = chromaOffset + (cr * maxSampleValue); + Unsafe.Add(ref destK, i) = ktmp * maxSampleValue; + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector256.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector256.cs new file mode 100644 index 0000000..0720ab3 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector256.cs @@ -0,0 +1,132 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class TiffYccKVector256 : JpegColorConverterVector256 + { + public TiffYccKVector256(int precision) + : base(JpegColorSpace.TiffYccK, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + { + ref Vector256 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector256 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector256 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector256 c3Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + Vector256 scale = Vector256.Create(1F / this.MaximumValue); + Vector256 chromaOffset = Vector256.Create(this.HalfValue) * scale; + Vector256 rCrMult = Vector256.Create(YCbCrScalar.RCrMult); + Vector256 gCbMult = Vector256.Create(-YCbCrScalar.GCbMult); + Vector256 gCrMult = Vector256.Create(-YCbCrScalar.GCrMult); + Vector256 bCbMult = Vector256.Create(YCbCrScalar.BCbMult); + + nuint n = values.Component0.Vector256Count(); + for (nuint i = 0; i < n; i++) + { + ref Vector256 c0 = ref Unsafe.Add(ref c0Base, i); + ref Vector256 c1 = ref Unsafe.Add(ref c1Base, i); + ref Vector256 c2 = ref Unsafe.Add(ref c2Base, i); + ref Vector256 c3 = ref Unsafe.Add(ref c3Base, i); + + Vector256 y = c0 * scale; + Vector256 cb = (c1 * scale) - chromaOffset; + Vector256 cr = (c2 * scale) - chromaOffset; + Vector256 scaledK = Vector256.One - (c3 * scale); + + // r = y + (1.402F * cr); + // g = y - (0.344136F * cb) - (0.714136F * cr); + // b = y + (1.772F * cb); + Vector256 r = Vector256_.MultiplyAdd(y, cr, rCrMult) * scaledK; + Vector256 g = Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult) * scaledK; + Vector256 b = Vector256_.MultiplyAdd(y, cb, bCbMult) * scaledK; + + c0 = r; + c1 = g; + c2 = b; + } + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => TiffYccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + ref Vector256 srcR = + ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); + ref Vector256 srcG = + ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); + ref Vector256 srcB = + ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); + + ref Vector256 destY = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector256 destCb = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector256 destCr = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector256 destK = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + Vector256 maxSourceValue = Vector256.Create(255F); + Vector256 maxSampleValue = Vector256.Create(this.MaximumValue); + Vector256 chromaOffset = Vector256.Create(this.HalfValue); + + Vector256 f0299 = Vector256.Create(0.299f); + Vector256 f0587 = Vector256.Create(0.587f); + Vector256 f0114 = Vector256.Create(0.114f); + Vector256 fn0168736 = Vector256.Create(-0.168736f); + Vector256 fn0331264 = Vector256.Create(-0.331264f); + Vector256 fn0418688 = Vector256.Create(-0.418688f); + Vector256 fn0081312F = Vector256.Create(-0.081312F); + Vector256 f05 = Vector256.Create(0.5f); + + nuint n = values.Component0.Vector256Count(); + for (nuint i = 0; i < n; i++) + { + Vector256 r = Unsafe.Add(ref srcR, i) / maxSourceValue; + Vector256 g = Unsafe.Add(ref srcG, i) / maxSourceValue; + Vector256 b = Unsafe.Add(ref srcB, i) / maxSourceValue; + Vector256 ktmp = Vector256.One - Vector256.Max(r, Vector256.Min(g, b)); + + Vector256 kMask = ~Vector256.Equals(ktmp, Vector256.One); + Vector256 divisor = Vector256.One / (Vector256.One - ktmp); + + r = (r * divisor) & kMask; + g = (g * divisor) & kMask; + b = (b * divisor) & kMask; + + // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) + // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) + // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) + Vector256 y = Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); + Vector256 cb = chromaOffset + Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); + Vector256 cr = chromaOffset + Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); + + Unsafe.Add(ref destY, i) = y * maxSampleValue; + Unsafe.Add(ref destCb, i) = chromaOffset + (cb * maxSampleValue); + Unsafe.Add(ref destCr, i) = chromaOffset + (cr * maxSampleValue); + Unsafe.Add(ref destK, i) = ktmp * maxSampleValue; + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector512.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector512.cs new file mode 100644 index 0000000..d95bb6f --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector512.cs @@ -0,0 +1,143 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class TiffYccKVector512 : JpegColorConverterVector512 + { + public TiffYccKVector512(int precision) + : base(JpegColorSpace.TiffYccK, precision) + { + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => TiffYccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) + { + ref Vector512 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector512 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector512 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector512 c3Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + Vector512 scale = Vector512.Create(1F / this.MaximumValue); + Vector512 chromaOffset = Vector512.Create(this.HalfValue) * scale; + Vector512 rCrMult = Vector512.Create(YCbCrScalar.RCrMult); + Vector512 gCbMult = Vector512.Create(-YCbCrScalar.GCbMult); + Vector512 gCrMult = Vector512.Create(-YCbCrScalar.GCrMult); + Vector512 bCbMult = Vector512.Create(YCbCrScalar.BCbMult); + + nuint n = values.Component0.Vector512Count(); + for (nuint i = 0; i < n; i++) + { + ref Vector512 c0 = ref Unsafe.Add(ref c0Base, i); + ref Vector512 c1 = ref Unsafe.Add(ref c1Base, i); + ref Vector512 c2 = ref Unsafe.Add(ref c2Base, i); + ref Vector512 c3 = ref Unsafe.Add(ref c3Base, i); + + Vector512 y = c0 * scale; + Vector512 cb = (c1 * scale) - chromaOffset; + Vector512 cr = (c2 * scale) - chromaOffset; + Vector512 scaledK = Vector512.One - (c3 * scale); + + // r = y + (1.402F * cr); + // g = y - (0.344136F * cb) - (0.714136F * cr); + // b = y + (1.772F * cb); + Vector512 r = Vector512_.MultiplyAdd(y, cr, rCrMult) * scaledK; + Vector512 g = Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult) * scaledK; + Vector512 b = Vector512_.MultiplyAdd(y, cb, bCbMult) * scaledK; + + c0 = r; + c1 = g; + c2 = b; + } + } + + /// + protected override void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => ConvertFromRgbVectorized(in values, this.MaximumValue, this.HalfValue, rLane, gLane, bLane); + + /// + protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) + => TiffYccKScalar.ConvertToRgbInPlace(values, this.MaximumValue, this.HalfValue); + + /// + protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => TiffYccKScalar.ConvertFromRgb(values, this.HalfValue, this.MaximumValue, rLane, gLane, bLane); + + internal static void ConvertFromRgbVectorized(in ComponentValues values, float maxValue, float halfValue, Span rLane, Span gLane, Span bLane) + { + ref Vector512 srcR = + ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); + ref Vector512 srcG = + ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); + ref Vector512 srcB = + ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); + + ref Vector512 destY = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector512 destCb = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector512 destCr = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector512 destK = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + Vector512 maxSourceValue = Vector512.Create(255F); + Vector512 maxSampleValue = Vector512.Create(maxValue); + Vector512 chromaOffset = Vector512.Create(halfValue); + + Vector512 f0299 = Vector512.Create(0.299f); + Vector512 f0587 = Vector512.Create(0.587f); + Vector512 f0114 = Vector512.Create(0.114f); + Vector512 fn0168736 = Vector512.Create(-0.168736f); + Vector512 fn0331264 = Vector512.Create(-0.331264f); + Vector512 fn0418688 = Vector512.Create(-0.418688f); + Vector512 fn0081312F = Vector512.Create(-0.081312F); + Vector512 f05 = Vector512.Create(0.5f); + + nuint n = values.Component0.Vector512Count(); + for (nuint i = 0; i < n; i++) + { + Vector512 r = Unsafe.Add(ref srcR, i) / maxSourceValue; + Vector512 g = Unsafe.Add(ref srcG, i) / maxSourceValue; + Vector512 b = Unsafe.Add(ref srcB, i) / maxSourceValue; + Vector512 ktmp = Vector512.One - Vector512.Max(r, Vector512.Min(g, b)); + + Vector512 kMask = ~Vector512.Equals(ktmp, Vector512.One); + Vector512 divisor = Vector512.One / (Vector512.One - ktmp); + + r = (r * divisor) & kMask; + g = (g * divisor) & kMask; + b = (b * divisor) & kMask; + + // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) + // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) + // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) + Vector512 y = Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); + Vector512 cb = chromaOffset + Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); + Vector512 cr = chromaOffset + Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); + + Unsafe.Add(ref destY, i) = y * maxSampleValue; + Unsafe.Add(ref destCb, i) = chromaOffset + (cb * maxSampleValue); + Unsafe.Add(ref destCr, i) = chromaOffset + (cr * maxSampleValue); + Unsafe.Add(ref destK, i) = ktmp * maxSampleValue; + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs new file mode 100644 index 0000000..8cf85b8 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs @@ -0,0 +1,122 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class YCbCrScalar : JpegColorConverterScalar + { + // derived from ITU-T Rec. T.871 + internal const float RCrMult = 1.402f; + internal const float GCbMult = (float)(0.114 * 1.772 / 0.587); + internal const float GCrMult = (float)(0.299 * 1.402 / 0.587); + internal const float BCbMult = 1.772f; + + public YCbCrScalar(int precision) + : base(JpegColorSpace.YCbCr, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + => ConvertToRgbInPlace(values, this.MaximumValue, this.HalfValue); + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => ConvertFromRgb(values, this.HalfValue, rLane, gLane, bLane); + + public static void ConvertToRgbInPlace(in ComponentValues values, float maxValue, float halfValue) + { + Span c0 = values.Component0; + Span c1 = values.Component1; + Span c2 = values.Component2; + + float scale = 1 / maxValue; + + for (int i = 0; i < c0.Length; i++) + { + float y = c0[i]; + float cb = c1[i] - halfValue; + float cr = c2[i] - halfValue; + + // r = y + (1.402F * cr); + // g = y - (0.344136F * cb) - (0.714136F * cr); + // b = y + (1.772F * cb); + c0[i] = MathF.Round(y + (RCrMult * cr), MidpointRounding.AwayFromZero) * scale; + c1[i] = MathF.Round(y - (GCbMult * cb) - (GCrMult * cr), MidpointRounding.AwayFromZero) * scale; + c2[i] = MathF.Round(y + (BCbMult * cb), MidpointRounding.AwayFromZero) * scale; + } + } + + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) + { + using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 3); + Span packed = memoryOwner.Memory.Span; + + Span c0 = values.Component0; + Span c1 = values.Component1; + Span c2 = values.Component2; + + // Although YCbCr is a defined ICC color space, in practice ICC profiles + // do not implement transforms from it. + // Therefore, we first convert JPEG YCbCr to RGB manually, then perform + // color-managed conversion to the target profile. + // + // The YCbCr => RGB conversion is based on BT.601 and is independent of any embedded ICC profile. + // Since the same RGB working space is used during conversion to and from XYZ, + // colorimetric accuracy is preserved. + ColorProfileConverter converter = new(); + + PackedNormalizeInterleave3(c0, c1, c2, packed, 1F / maxValue); + + Span source = MemoryMarshal.Cast(packed); + Span destination = MemoryMarshal.Cast(packed); + + converter.Convert(source, destination); + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + }; + converter = new ColorProfileConverter(options); + converter.Convert(destination, destination); + + UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); + } + + public static void ConvertFromRgb(in ComponentValues values, float halfValue, Span rLane, Span gLane, Span bLane) + { + Span y = values.Component0; + Span cb = values.Component1; + Span cr = values.Component2; + + for (int i = 0; i < y.Length; i++) + { + float r = rLane[i]; + float g = gLane[i]; + float b = bLane[i]; + + // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) + // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) + // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) + y[i] = (0.299f * r) + (0.587f * g) + (0.114f * b); + cb[i] = halfValue - (0.168736f * r) - (0.331264f * g) + (0.5f * b); + cr[i] = halfValue + (0.5f * r) - (0.418688f * g) - (0.081312f * b); + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.cs new file mode 100644 index 0000000..cb70d26 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.cs @@ -0,0 +1,122 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class YCbCrVector128 : JpegColorConverterVector128 + { + public YCbCrVector128(int precision) + : base(JpegColorSpace.YCbCr, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + { + ref Vector128 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector128 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector128 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + + Vector128 chromaOffset = Vector128.Create(-this.HalfValue); + Vector128 scale = Vector128.Create(1 / this.MaximumValue); + Vector128 rCrMult = Vector128.Create(YCbCrScalar.RCrMult); + Vector128 gCbMult = Vector128.Create(-YCbCrScalar.GCbMult); + Vector128 gCrMult = Vector128.Create(-YCbCrScalar.GCrMult); + Vector128 bCbMult = Vector128.Create(YCbCrScalar.BCbMult); + + // Walking 8 elements at one step: + nuint n = values.Component0.Vector128Count(); + for (nuint i = 0; i < n; i++) + { + // y = yVals[i]; + // cb = cbVals[i] - 128F; + // cr = crVals[i] - 128F; + ref Vector128 c0 = ref Unsafe.Add(ref c0Base, i); + ref Vector128 c1 = ref Unsafe.Add(ref c1Base, i); + ref Vector128 c2 = ref Unsafe.Add(ref c2Base, i); + + Vector128 y = c0; + Vector128 cb = c1 + chromaOffset; + Vector128 cr = c2 + chromaOffset; + + // r = y + (1.402F * cr); + // g = y - (0.344136F * cb) - (0.714136F * cr); + // b = y + (1.772F * cb); + Vector128 r = Vector128_.MultiplyAdd(y, cr, rCrMult); + Vector128 g = Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult); + Vector128 b = Vector128_.MultiplyAdd(y, cb, bCbMult); + + r = Vector128_.RoundToNearestInteger(r) * scale; + g = Vector128_.RoundToNearestInteger(g) * scale; + b = Vector128_.RoundToNearestInteger(b) * scale; + + c0 = r; + c1 = g; + c2 = b; + } + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YCbCrScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + ref Vector128 destY = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector128 destCb = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector128 destCr = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + + ref Vector128 srcR = + ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); + ref Vector128 srcG = + ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); + ref Vector128 srcB = + ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); + + Vector128 chromaOffset = Vector128.Create(this.HalfValue); + Vector128 f0299 = Vector128.Create(0.299f); + Vector128 f0587 = Vector128.Create(0.587f); + Vector128 f0114 = Vector128.Create(0.114f); + Vector128 fn0168736 = Vector128.Create(-0.168736f); + Vector128 fn0331264 = Vector128.Create(-0.331264f); + Vector128 fn0418688 = Vector128.Create(-0.418688f); + Vector128 fn0081312F = Vector128.Create(-0.081312F); + Vector128 f05 = Vector128.Create(0.5f); + + nuint n = values.Component0.Vector128Count(); + for (nuint i = 0; i < n; i++) + { + Vector128 r = Unsafe.Add(ref srcR, i); + Vector128 g = Unsafe.Add(ref srcG, i); + Vector128 b = Unsafe.Add(ref srcB, i); + + // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) + // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) + // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) + Vector128 y = Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); + Vector128 cb = chromaOffset + Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); + Vector128 cr = chromaOffset + Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); + + Unsafe.Add(ref destY, i) = y; + Unsafe.Add(ref destCb, i) = cb; + Unsafe.Add(ref destCr, i) = cr; + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector256.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector256.cs new file mode 100644 index 0000000..efa753c --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector256.cs @@ -0,0 +1,122 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class YCbCrVector256 : JpegColorConverterVector256 + { + public YCbCrVector256(int precision) + : base(JpegColorSpace.YCbCr, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + { + ref Vector256 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector256 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector256 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + + Vector256 chromaOffset = Vector256.Create(-this.HalfValue); + Vector256 scale = Vector256.Create(1 / this.MaximumValue); + Vector256 rCrMult = Vector256.Create(YCbCrScalar.RCrMult); + Vector256 gCbMult = Vector256.Create(-YCbCrScalar.GCbMult); + Vector256 gCrMult = Vector256.Create(-YCbCrScalar.GCrMult); + Vector256 bCbMult = Vector256.Create(YCbCrScalar.BCbMult); + + // Walking 8 elements at one step: + nuint n = values.Component0.Vector256Count(); + for (nuint i = 0; i < n; i++) + { + // y = yVals[i]; + // cb = cbVals[i] - 128F; + // cr = crVals[i] - 128F; + ref Vector256 c0 = ref Unsafe.Add(ref c0Base, i); + ref Vector256 c1 = ref Unsafe.Add(ref c1Base, i); + ref Vector256 c2 = ref Unsafe.Add(ref c2Base, i); + + Vector256 y = c0; + Vector256 cb = c1 + chromaOffset; + Vector256 cr = c2 + chromaOffset; + + // r = y + (1.402F * cr); + // g = y - (0.344136F * cb) - (0.714136F * cr); + // b = y + (1.772F * cb); + Vector256 r = Vector256_.MultiplyAdd(y, cr, rCrMult); + Vector256 g = Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult); + Vector256 b = Vector256_.MultiplyAdd(y, cb, bCbMult); + + r = Vector256_.RoundToNearestInteger(r) * scale; + g = Vector256_.RoundToNearestInteger(g) * scale; + b = Vector256_.RoundToNearestInteger(b) * scale; + + c0 = r; + c1 = g; + c2 = b; + } + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YCbCrScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + ref Vector256 destY = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector256 destCb = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector256 destCr = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + + ref Vector256 srcR = + ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); + ref Vector256 srcG = + ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); + ref Vector256 srcB = + ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); + + Vector256 chromaOffset = Vector256.Create(this.HalfValue); + Vector256 f0299 = Vector256.Create(0.299f); + Vector256 f0587 = Vector256.Create(0.587f); + Vector256 f0114 = Vector256.Create(0.114f); + Vector256 fn0168736 = Vector256.Create(-0.168736f); + Vector256 fn0331264 = Vector256.Create(-0.331264f); + Vector256 fn0418688 = Vector256.Create(-0.418688f); + Vector256 fn0081312F = Vector256.Create(-0.081312F); + Vector256 f05 = Vector256.Create(0.5f); + + nuint n = values.Component0.Vector256Count(); + for (nuint i = 0; i < n; i++) + { + Vector256 r = Unsafe.Add(ref srcR, i); + Vector256 g = Unsafe.Add(ref srcG, i); + Vector256 b = Unsafe.Add(ref srcB, i); + + // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) + // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) + // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) + Vector256 y = Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); + Vector256 cb = chromaOffset + Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); + Vector256 cr = chromaOffset + Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); + + Unsafe.Add(ref destY, i) = y; + Unsafe.Add(ref destCb, i) = cb; + Unsafe.Add(ref destCr, i) = cr; + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector512.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector512.cs new file mode 100644 index 0000000..cf68005 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector512.cs @@ -0,0 +1,129 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class YCbCrVector512 : JpegColorConverterVector512 + { + public YCbCrVector512(int precision) + : base(JpegColorSpace.YCbCr, precision) + { + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YCbCrScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) + { + ref Vector512 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector512 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector512 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + + Vector512 chromaOffset = Vector512.Create(-this.HalfValue); + Vector512 scale = Vector512.Create(1 / this.MaximumValue); + Vector512 rCrMult = Vector512.Create(YCbCrScalar.RCrMult); + Vector512 gCbMult = Vector512.Create(-YCbCrScalar.GCbMult); + Vector512 gCrMult = Vector512.Create(-YCbCrScalar.GCrMult); + Vector512 bCbMult = Vector512.Create(YCbCrScalar.BCbMult); + + nuint n = values.Component0.Vector512Count(); + for (nuint i = 0; i < n; i++) + { + // y = yVals[i]; + // cb = cbVals[i] - 128F; + // cr = crVals[i] - 128F; + ref Vector512 c0 = ref Unsafe.Add(ref c0Base, i); + ref Vector512 c1 = ref Unsafe.Add(ref c1Base, i); + ref Vector512 c2 = ref Unsafe.Add(ref c2Base, i); + + Vector512 y = c0; + Vector512 cb = c1 + chromaOffset; + Vector512 cr = c2 + chromaOffset; + + // r = y + (1.402F * cr); + // g = y - (0.344136F * cb) - (0.714136F * cr); + // b = y + (1.772F * cb); + Vector512 r = Vector512_.MultiplyAdd(y, cr, rCrMult); + Vector512 g = Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult); + Vector512 b = Vector512_.MultiplyAdd(y, cb, bCbMult); + + r = Vector512_.RoundToNearestInteger(r) * scale; + g = Vector512_.RoundToNearestInteger(g) * scale; + b = Vector512_.RoundToNearestInteger(b) * scale; + + c0 = r; + c1 = g; + c2 = b; + } + } + + /// + protected override void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + ref Vector512 destY = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector512 destCb = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector512 destCr = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + + ref Vector512 srcR = + ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); + ref Vector512 srcG = + ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); + ref Vector512 srcB = + ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); + + Vector512 chromaOffset = Vector512.Create(this.HalfValue); + Vector512 f0299 = Vector512.Create(0.299f); + Vector512 f0587 = Vector512.Create(0.587f); + Vector512 f0114 = Vector512.Create(0.114f); + Vector512 fn0168736 = Vector512.Create(-0.168736f); + Vector512 fn0331264 = Vector512.Create(-0.331264f); + Vector512 fn0418688 = Vector512.Create(-0.418688f); + Vector512 fn0081312F = Vector512.Create(-0.081312F); + Vector512 f05 = Vector512.Create(0.5f); + + nuint n = values.Component0.Vector512Count(); + for (nuint i = 0; i < n; i++) + { + Vector512 r = Unsafe.Add(ref srcR, i); + Vector512 g = Unsafe.Add(ref srcG, i); + Vector512 b = Unsafe.Add(ref srcB, i); + + // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) + // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) + // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) + Vector512 y = Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); + Vector512 cb = chromaOffset + Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); + Vector512 cr = chromaOffset + Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); + + Unsafe.Add(ref destY, i) = y; + Unsafe.Add(ref destCb, i) = cb; + Unsafe.Add(ref destCr, i) = cr; + } + } + + /// + protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) + => YCbCrScalar.ConvertToRgbInPlace(values, this.MaximumValue, this.HalfValue); + + /// + protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => YCbCrScalar.ConvertFromRgb(values, this.HalfValue, rLane, gLane, bLane); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs new file mode 100644 index 0000000..8027808 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs @@ -0,0 +1,126 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class YccKScalar : JpegColorConverterScalar + { + // Derived from ITU-T Rec. T.871 + internal const float RCrMult = 1.402f; + internal const float GCbMult = (float)(0.114 * 1.772 / 0.587); + internal const float GCrMult = (float)(0.299 * 1.402 / 0.587); + internal const float BCbMult = 1.772f; + + public YccKScalar(int precision) + : base(JpegColorSpace.Ycck, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + => ConvertToRgbInPlace(values, this.MaximumValue, this.HalfValue); + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => ConvertFromRgb(values, this.HalfValue, this.MaximumValue, rLane, gLane, bLane); + + public static void ConvertToRgbInPlace(in ComponentValues values, float maxValue, float halfValue) + { + Span c0 = values.Component0; + Span c1 = values.Component1; + Span c2 = values.Component2; + Span c3 = values.Component3; + + float scale = 1 / (maxValue * maxValue); + + for (int i = 0; i < values.Component0.Length; i++) + { + float y = c0[i]; + float cb = c1[i] - halfValue; + float cr = c2[i] - halfValue; + float scaledK = c3[i] * scale; + + // r = y + (1.402F * cr); + // g = y - (0.344136F * cb) - (0.714136F * cr); + // b = y + (1.772F * cb); + c0[i] = (maxValue - MathF.Round(y + (RCrMult * cr), MidpointRounding.AwayFromZero)) * scaledK; + c1[i] = (maxValue - MathF.Round(y - (GCbMult * cb) - (GCrMult * cr), MidpointRounding.AwayFromZero)) * scaledK; + c2[i] = (maxValue - MathF.Round(y + (BCbMult * cb), MidpointRounding.AwayFromZero)) * scaledK; + } + } + + public static void ConvertFromRgb(in ComponentValues values, float halfValue, float maxValue, Span rLane, Span gLane, Span bLane) + { + // rgb -> cmyk + CmykScalar.ConvertFromRgb(in values, maxValue, rLane, gLane, bLane); + + // cmyk -> ycck + Span c = values.Component0; + Span m = values.Component1; + Span y = values.Component2; + + for (int i = 0; i < y.Length; i++) + { + float r = maxValue - c[i]; + float g = maxValue - m[i]; + float b = maxValue - y[i]; + + // k value is passed untouched from rgb -> cmyk conversion + c[i] = (0.299f * r) + (0.587f * g) + (0.114f * b); + m[i] = halfValue - (0.168736f * r) - (0.331264f * g) + (0.5f * b); + y[i] = halfValue + (0.5f * r) - (0.418688f * g) - (0.081312f * b); + } + } + + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) + { + using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 4); + Span packed = memoryOwner.Memory.Span; + + Span c0 = values.Component0; + Span c1 = values.Component1; + Span c2 = values.Component2; + Span c3 = values.Component3; + + PackedInvertNormalizeInterleave4(c0, c1, c2, c3, packed, maxValue); + + ColorProfileConverter converter = new(); + Span source = MemoryMarshal.Cast(packed); + + // YccK is not a defined ICC color space — it's a JPEG-specific encoding used in Adobe-style CMYK JPEGs. + // ICC profiles expect colorimetric CMYK values, so we must first convert YccK to CMYK using a hardcoded inverse transform. + // This transform assumes Rec.601 YCbCr coefficients and an inverted K channel. + // + // The YccK => Cmyk conversion is independent of any embedded ICC profile. + // Since the same RGB working space is used during conversion to and from XYZ, + // colorimetric accuracy is preserved. + converter.Convert(MemoryMarshal.Cast(source), source); + + Span destination = MemoryMarshal.Cast(packed)[..source.Length]; + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + }; + converter = new ColorProfileConverter(options); + converter.Convert(source, destination); + + UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector128.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector128.cs new file mode 100644 index 0000000..f7de623 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector128.cs @@ -0,0 +1,136 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class YccKVector128 : JpegColorConverterVector128 + { + public YccKVector128(int precision) + : base(JpegColorSpace.Ycck, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + { + ref Vector128 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector128 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector128 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector128 kBase = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + // Used for the color conversion + Vector128 chromaOffset = Vector128.Create(-this.HalfValue); + Vector128 scale = Vector128.Create(1 / (this.MaximumValue * this.MaximumValue)); + Vector128 max = Vector128.Create(this.MaximumValue); + Vector128 rCrMult = Vector128.Create(YCbCrScalar.RCrMult); + Vector128 gCbMult = Vector128.Create(-YCbCrScalar.GCbMult); + Vector128 gCrMult = Vector128.Create(-YCbCrScalar.GCrMult); + Vector128 bCbMult = Vector128.Create(YCbCrScalar.BCbMult); + + // Walking 8 elements at one step: + nuint n = values.Component0.Vector128Count(); + for (nuint i = 0; i < n; i++) + { + // y = yVals[i]; + // cb = cbVals[i] - 128F; + // cr = crVals[i] - 128F; + // k = kVals[i] / 256F; + ref Vector128 c0 = ref Unsafe.Add(ref c0Base, i); + ref Vector128 c1 = ref Unsafe.Add(ref c1Base, i); + ref Vector128 c2 = ref Unsafe.Add(ref c2Base, i); + Vector128 y = c0; + Vector128 cb = c1 + chromaOffset; + Vector128 cr = c2 + chromaOffset; + Vector128 scaledK = Unsafe.Add(ref kBase, i) * scale; + + // r = y + (1.402F * cr); + // g = y - (0.344136F * cb) - (0.714136F * cr); + // b = y + (1.772F * cb); + Vector128 r = Vector128_.MultiplyAdd(y, cr, rCrMult); + Vector128 g = Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult); + Vector128 b = Vector128_.MultiplyAdd(y, cb, bCbMult); + + r = max - Vector128_.RoundToNearestInteger(r); + g = max - Vector128_.RoundToNearestInteger(g); + b = max - Vector128_.RoundToNearestInteger(b); + + r *= scaledK; + g *= scaledK; + b *= scaledK; + + c0 = r; + c1 = g; + c2 = b; + } + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + // rgb -> cmyk + CmykVector128.ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane); + + // cmyk -> ycck + ref Vector128 destY = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector128 destCb = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector128 destCr = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + + ref Vector128 srcR = ref destY; + ref Vector128 srcG = ref destCb; + ref Vector128 srcB = ref destCr; + + // Used for the color conversion + Vector128 maxSampleValue = Vector128.Create(this.MaximumValue); + + Vector128 chromaOffset = Vector128.Create(this.HalfValue); + + Vector128 f0299 = Vector128.Create(0.299f); + Vector128 f0587 = Vector128.Create(0.587f); + Vector128 f0114 = Vector128.Create(0.114f); + Vector128 fn0168736 = Vector128.Create(-0.168736f); + Vector128 fn0331264 = Vector128.Create(-0.331264f); + Vector128 fn0418688 = Vector128.Create(-0.418688f); + Vector128 fn0081312F = Vector128.Create(-0.081312F); + Vector128 f05 = Vector128.Create(0.5f); + + nuint n = values.Component0.Vector128Count(); + for (nuint i = 0; i < n; i++) + { + Vector128 r = maxSampleValue - Unsafe.Add(ref srcR, i); + Vector128 g = maxSampleValue - Unsafe.Add(ref srcG, i); + Vector128 b = maxSampleValue - Unsafe.Add(ref srcB, i); + + // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) + // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) + // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) + Vector128 y = Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); + Vector128 cb = chromaOffset + Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); + Vector128 cr = chromaOffset + Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); + + Unsafe.Add(ref destY, i) = y; + Unsafe.Add(ref destCb, i) = cb; + Unsafe.Add(ref destCr, i) = cr; + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector256.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector256.cs new file mode 100644 index 0000000..050775c --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector256.cs @@ -0,0 +1,136 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class YccKVector256 : JpegColorConverterVector256 + { + public YccKVector256(int precision) + : base(JpegColorSpace.Ycck, precision) + { + } + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + { + ref Vector256 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector256 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector256 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector256 kBase = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + // Used for the color conversion + Vector256 chromaOffset = Vector256.Create(-this.HalfValue); + Vector256 scale = Vector256.Create(1 / (this.MaximumValue * this.MaximumValue)); + Vector256 max = Vector256.Create(this.MaximumValue); + Vector256 rCrMult = Vector256.Create(YCbCrScalar.RCrMult); + Vector256 gCbMult = Vector256.Create(-YCbCrScalar.GCbMult); + Vector256 gCrMult = Vector256.Create(-YCbCrScalar.GCrMult); + Vector256 bCbMult = Vector256.Create(YCbCrScalar.BCbMult); + + // Walking 8 elements at one step: + nuint n = values.Component0.Vector256Count(); + for (nuint i = 0; i < n; i++) + { + // y = yVals[i]; + // cb = cbVals[i] - 128F; + // cr = crVals[i] - 128F; + // k = kVals[i] / 256F; + ref Vector256 c0 = ref Unsafe.Add(ref c0Base, i); + ref Vector256 c1 = ref Unsafe.Add(ref c1Base, i); + ref Vector256 c2 = ref Unsafe.Add(ref c2Base, i); + Vector256 y = c0; + Vector256 cb = c1 + chromaOffset; + Vector256 cr = c2 + chromaOffset; + Vector256 scaledK = Unsafe.Add(ref kBase, i) * scale; + + // r = y + (1.402F * cr); + // g = y - (0.344136F * cb) - (0.714136F * cr); + // b = y + (1.772F * cb); + Vector256 r = Vector256_.MultiplyAdd(y, cr, rCrMult); + Vector256 g = Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult); + Vector256 b = Vector256_.MultiplyAdd(y, cb, bCbMult); + + r = max - Vector256_.RoundToNearestInteger(r); + g = max - Vector256_.RoundToNearestInteger(g); + b = max - Vector256_.RoundToNearestInteger(b); + + r *= scaledK; + g *= scaledK; + b *= scaledK; + + c0 = r; + c1 = g; + c2 = b; + } + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + // rgb -> cmyk + CmykVector256.ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane); + + // cmyk -> ycck + ref Vector256 destY = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector256 destCb = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector256 destCr = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + + ref Vector256 srcR = ref destY; + ref Vector256 srcG = ref destCb; + ref Vector256 srcB = ref destCr; + + // Used for the color conversion + Vector256 maxSampleValue = Vector256.Create(this.MaximumValue); + + Vector256 chromaOffset = Vector256.Create(this.HalfValue); + + Vector256 f0299 = Vector256.Create(0.299f); + Vector256 f0587 = Vector256.Create(0.587f); + Vector256 f0114 = Vector256.Create(0.114f); + Vector256 fn0168736 = Vector256.Create(-0.168736f); + Vector256 fn0331264 = Vector256.Create(-0.331264f); + Vector256 fn0418688 = Vector256.Create(-0.418688f); + Vector256 fn0081312F = Vector256.Create(-0.081312F); + Vector256 f05 = Vector256.Create(0.5f); + + nuint n = values.Component0.Vector256Count(); + for (nuint i = 0; i < n; i++) + { + Vector256 r = maxSampleValue - Unsafe.Add(ref srcR, i); + Vector256 g = maxSampleValue - Unsafe.Add(ref srcG, i); + Vector256 b = maxSampleValue - Unsafe.Add(ref srcB, i); + + // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) + // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) + // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) + Vector256 y = Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); + Vector256 cb = chromaOffset + Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); + Vector256 cr = chromaOffset + Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); + + Unsafe.Add(ref destY, i) = y; + Unsafe.Add(ref destCb, i) = cb; + Unsafe.Add(ref destCr, i) = cr; + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector512.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector512.cs new file mode 100644 index 0000000..b726ab4 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector512.cs @@ -0,0 +1,144 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + internal sealed class YccKVector512 : JpegColorConverterVector512 + { + public YccKVector512(int precision) + : base(JpegColorSpace.Ycck, precision) + { + } + + /// + protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) + { + ref Vector512 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector512 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector512 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector512 kBase = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + // Used for the color conversion + Vector512 chromaOffset = Vector512.Create(-this.HalfValue); + Vector512 scale = Vector512.Create(1 / (this.MaximumValue * this.MaximumValue)); + Vector512 max = Vector512.Create(this.MaximumValue); + Vector512 rCrMult = Vector512.Create(YCbCrScalar.RCrMult); + Vector512 gCbMult = Vector512.Create(-YCbCrScalar.GCbMult); + Vector512 gCrMult = Vector512.Create(-YCbCrScalar.GCrMult); + Vector512 bCbMult = Vector512.Create(YCbCrScalar.BCbMult); + + // Walking 8 elements at one step: + nuint n = values.Component0.Vector512Count(); + for (nuint i = 0; i < n; i++) + { + // y = yVals[i]; + // cb = cbVals[i] - 128F; + // cr = crVals[i] - 128F; + // k = kVals[i] / 256F; + ref Vector512 c0 = ref Unsafe.Add(ref c0Base, i); + ref Vector512 c1 = ref Unsafe.Add(ref c1Base, i); + ref Vector512 c2 = ref Unsafe.Add(ref c2Base, i); + Vector512 y = c0; + Vector512 cb = c1 + chromaOffset; + Vector512 cr = c2 + chromaOffset; + Vector512 scaledK = Unsafe.Add(ref kBase, i) * scale; + + // r = y + (1.402F * cr); + // g = y - (0.344136F * cb) - (0.714136F * cr); + // b = y + (1.772F * cb); + Vector512 r = Vector512_.MultiplyAdd(y, cr, rCrMult); + Vector512 g = Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult); + Vector512 b = Vector512_.MultiplyAdd(y, cb, bCbMult); + + r = max - Vector512_.RoundToNearestInteger(r); + g = max - Vector512_.RoundToNearestInteger(g); + b = max - Vector512_.RoundToNearestInteger(b); + + r *= scaledK; + g *= scaledK; + b *= scaledK; + + c0 = r; + c1 = g; + c2 = b; + } + } + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + + /// + protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) + => YccKScalar.ConvertToRgbInPlace(values, this.MaximumValue, this.HalfValue); + + /// + protected override void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + // rgb -> cmyk + CmykVector512.ConvertFromRgbVectorized(in values, this.MaximumValue, rLane, gLane, bLane); + + // cmyk -> ycck + ref Vector512 destY = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector512 destCb = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector512 destCr = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + + ref Vector512 srcR = ref destY; + ref Vector512 srcG = ref destCb; + ref Vector512 srcB = ref destCr; + + // Used for the color conversion + Vector512 maxSampleValue = Vector512.Create(this.MaximumValue); + + Vector512 chromaOffset = Vector512.Create(this.HalfValue); + + Vector512 f0299 = Vector512.Create(0.299f); + Vector512 f0587 = Vector512.Create(0.587f); + Vector512 f0114 = Vector512.Create(0.114f); + Vector512 fn0168736 = Vector512.Create(-0.168736f); + Vector512 fn0331264 = Vector512.Create(-0.331264f); + Vector512 fn0418688 = Vector512.Create(-0.418688f); + Vector512 fn0081312F = Vector512.Create(-0.081312F); + Vector512 f05 = Vector512.Create(0.5f); + + nuint n = values.Component0.Vector512Count(); + for (nuint i = 0; i < n; i++) + { + Vector512 r = maxSampleValue - Unsafe.Add(ref srcR, i); + Vector512 g = maxSampleValue - Unsafe.Add(ref srcG, i); + Vector512 b = maxSampleValue - Unsafe.Add(ref srcB, i); + + // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) + // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) + // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) + Vector512 y = Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); + Vector512 cb = chromaOffset + Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); + Vector512 cr = chromaOffset + Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); + + Unsafe.Add(ref destY, i) = y; + Unsafe.Add(ref destCb, i) = cb; + Unsafe.Add(ref destCr, i) = cr; + } + } + + /// + protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => YccKScalar.ConvertFromRgb(in values, this.HalfValue, this.MaximumValue, rLane, gLane, bLane); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs new file mode 100644 index 0000000..21bd334 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs @@ -0,0 +1,522 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + /// + /// Encapsulates the conversion of color channels from jpeg image to RGB channels. + /// + internal abstract partial class JpegColorConverterBase + { + /// + /// The available converters + /// + private static readonly JpegColorConverterBase[] Converters = CreateConverters(); + + /// + /// Initializes a new instance of the class. + /// + /// The color space. + /// The precision in bits. + protected JpegColorConverterBase(JpegColorSpace colorSpace, int precision) + { + this.ColorSpace = colorSpace; + this.Precision = precision; + this.MaximumValue = MathF.Pow(2, precision) - 1; + this.HalfValue = MathF.Ceiling(this.MaximumValue * 0.5F); // /2 + } + + /// + /// Gets a value indicating whether this is available + /// on the current runtime and CPU architecture. + /// + public abstract bool IsAvailable { get; } + + /// + /// Gets a value indicating how many pixels are processed in a single batch. + /// + /// + /// This generally should be equal to register size, + /// e.g. 1 for scalar implementation, 8 for AVX implementation and so on. + /// + public abstract int ElementsPerBatch { get; } + + /// + /// Gets the of this converter. + /// + public JpegColorSpace ColorSpace { get; } + + /// + /// Gets the Precision of this converter in bits. + /// + public int Precision { get; } + + /// + /// Gets the maximum value of a sample + /// + private float MaximumValue { get; } + + /// + /// Gets the half of the maximum value of a sample + /// + private float HalfValue { get; } + + /// + /// Returns the corresponding to the given + /// + /// The color space. + /// The precision in bits. + /// Invalid colorspace. + public static JpegColorConverterBase GetConverter(JpegColorSpace colorSpace, int precision) + => Array.Find(Converters, c => c.ColorSpace == colorSpace && c.Precision == precision) + ?? throw new InvalidImageContentException($"Could not find any converter for JpegColorSpace {colorSpace}!"); + + /// + /// Converts planar jpeg component values in to RGB color space in-place. + /// + /// The input/output as a stack-only struct + public abstract void ConvertToRgbInPlace(in ComponentValues values); + + /// + /// Converts planar jpeg component values in to RGB color space in-place using the given ICC profile. + /// + /// The configuration instance to use for the conversion. + /// The input/output as a stack-only struct. + /// The ICC profile to use for the conversion. + public abstract void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile); + + /// + /// Converts RGB lanes to jpeg component values. + /// + /// Jpeg component values. + /// Red colors lane. + /// Green colors lane. + /// Blue colors lane. + public abstract void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane); + + public static void PackedNormalizeInterleave3( + ReadOnlySpan xLane, + ReadOnlySpan yLane, + ReadOnlySpan zLane, + Span packed, + float scale) + { + DebugGuard.IsTrue(packed.Length % 3 == 0, "Packed length must be divisible by 3."); + DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!"); + DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!"); + DebugGuard.MustBeLessThanOrEqualTo(packed.Length / 3, xLane.Length, nameof(packed)); + + // TODO: Investigate SIMD version of this. + ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); + ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); + ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); + ref float packedRef = ref MemoryMarshal.GetReference(packed); + + for (nuint i = 0; i < (nuint)xLane.Length; i++) + { + nuint baseIdx = i * 3; + Unsafe.Add(ref packedRef, baseIdx) = Unsafe.Add(ref xLaneRef, i) * scale; + Unsafe.Add(ref packedRef, baseIdx + 1) = Unsafe.Add(ref yLaneRef, i) * scale; + Unsafe.Add(ref packedRef, baseIdx + 2) = Unsafe.Add(ref zLaneRef, i) * scale; + } + } + + public static void UnpackDeinterleave3( + ReadOnlySpan packed, + Span xLane, + Span yLane, + Span zLane) + { + DebugGuard.IsTrue(packed.Length == xLane.Length, nameof(packed), "Channels must be of same size!"); + DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!"); + DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!"); + + // TODO: Investigate SIMD version of this. + ref float packedRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast(packed)); + ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); + ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); + ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); + + for (nuint i = 0; i < (nuint)packed.Length; i++) + { + nuint baseIdx = i * 3; + Unsafe.Add(ref xLaneRef, i) = Unsafe.Add(ref packedRef, baseIdx); + Unsafe.Add(ref yLaneRef, i) = Unsafe.Add(ref packedRef, baseIdx + 1); + Unsafe.Add(ref zLaneRef, i) = Unsafe.Add(ref packedRef, baseIdx + 2); + } + } + + public static void PackedNormalizeInterleave4( + ReadOnlySpan xLane, + ReadOnlySpan yLane, + ReadOnlySpan zLane, + ReadOnlySpan wLane, + Span packed, + float maxValue) + { + DebugGuard.IsTrue(packed.Length % 4 == 0, "Packed length must be divisible by 4."); + DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!"); + DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!"); + DebugGuard.IsTrue(wLane.Length == xLane.Length, nameof(wLane), "Channels must be of same size!"); + DebugGuard.MustBeLessThanOrEqualTo(packed.Length / 4, xLane.Length, nameof(packed)); + + float scale = 1F / maxValue; + + // TODO: Investigate SIMD version of this. + ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); + ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); + ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); + ref float wLaneRef = ref MemoryMarshal.GetReference(wLane); + ref float packedRef = ref MemoryMarshal.GetReference(packed); + + for (nuint i = 0; i < (nuint)xLane.Length; i++) + { + nuint baseIdx = i * 4; + Unsafe.Add(ref packedRef, baseIdx) = Unsafe.Add(ref xLaneRef, i) * scale; + Unsafe.Add(ref packedRef, baseIdx + 1) = Unsafe.Add(ref yLaneRef, i) * scale; + Unsafe.Add(ref packedRef, baseIdx + 2) = Unsafe.Add(ref zLaneRef, i) * scale; + Unsafe.Add(ref packedRef, baseIdx + 3) = Unsafe.Add(ref wLaneRef, i) * scale; + } + } + + public static void PackedInvertNormalizeInterleave4( + ReadOnlySpan xLane, + ReadOnlySpan yLane, + ReadOnlySpan zLane, + ReadOnlySpan wLane, + Span packed, + float maxValue) + { + DebugGuard.IsTrue(packed.Length % 4 == 0, "Packed length must be divisible by 4."); + DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!"); + DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!"); + DebugGuard.IsTrue(wLane.Length == xLane.Length, nameof(wLane), "Channels must be of same size!"); + DebugGuard.MustBeLessThanOrEqualTo(packed.Length / 4, xLane.Length, nameof(packed)); + + float scale = 1F / maxValue; + + // TODO: Investigate SIMD version of this. + ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); + ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); + ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); + ref float wLaneRef = ref MemoryMarshal.GetReference(wLane); + ref float packedRef = ref MemoryMarshal.GetReference(packed); + + for (nuint i = 0; i < (nuint)xLane.Length; i++) + { + nuint baseIdx = i * 4; + Unsafe.Add(ref packedRef, baseIdx) = (maxValue - Unsafe.Add(ref xLaneRef, i)) * scale; + Unsafe.Add(ref packedRef, baseIdx + 1) = (maxValue - Unsafe.Add(ref yLaneRef, i)) * scale; + Unsafe.Add(ref packedRef, baseIdx + 2) = (maxValue - Unsafe.Add(ref zLaneRef, i)) * scale; + Unsafe.Add(ref packedRef, baseIdx + 3) = (maxValue - Unsafe.Add(ref wLaneRef, i)) * scale; + } + } + + /// + /// Returns the s for all supported color spaces and precisions. + /// + private static JpegColorConverterBase[] CreateConverters() + => [ + + // 8-bit converters + GetYCbCrConverter(8), + GetYccKConverter(8), + GetCmykConverter(8), + GetGrayScaleConverter(8), + GetRgbConverter(8), + GetTiffCmykConverter(8), + GetTiffYccKConverter(8), + + // 12-bit converters + GetYCbCrConverter(12), + GetYccKConverter(12), + GetCmykConverter(12), + GetGrayScaleConverter(12), + GetRgbConverter(12), + GetTiffCmykConverter(12), + GetTiffYccKConverter(12), + ]; + + /// + /// Returns the s for the YCbCr colorspace. + /// + /// The precision in bits. + private static JpegColorConverterBase GetYCbCrConverter(int precision) + { + if (JpegColorConverterVector512.IsSupported) + { + return new YCbCrVector512(precision); + } + + if (JpegColorConverterVector256.IsSupported) + { + return new YCbCrVector256(precision); + } + + if (JpegColorConverterVector128.IsSupported) + { + return new YCbCrVector128(precision); + } + + return new YCbCrScalar(precision); + } + + /// + /// Returns the s for the YccK colorspace. + /// + /// The precision in bits. + private static JpegColorConverterBase GetYccKConverter(int precision) + { + if (JpegColorConverterVector512.IsSupported) + { + return new YccKVector512(precision); + } + + if (JpegColorConverterVector256.IsSupported) + { + return new YccKVector256(precision); + } + + if (JpegColorConverterVector128.IsSupported) + { + return new YccKVector128(precision); + } + + return new YccKScalar(precision); + } + + /// + /// Returns the s for the CMYK colorspace. + /// + /// The precision in bits. + private static JpegColorConverterBase GetCmykConverter(int precision) + { + if (JpegColorConverterVector512.IsSupported) + { + return new CmykVector512(precision); + } + + if (JpegColorConverterVector256.IsSupported) + { + return new CmykVector256(precision); + } + + if (JpegColorConverterVector128.IsSupported) + { + return new CmykVector128(precision); + } + + return new CmykScalar(precision); + } + + /// + /// Returns the s for the gray scale colorspace. + /// + /// The precision in bits. + private static JpegColorConverterBase GetGrayScaleConverter(int precision) + { + if (JpegColorConverterVector512.IsSupported) + { + return new GrayScaleVector512(precision); + } + + if (JpegColorConverterVector256.IsSupported) + { + return new GrayScaleVector256(precision); + } + + if (JpegColorConverterVector128.IsSupported) + { + return new GrayScaleVector128(precision); + } + + return new GrayScaleScalar(precision); + } + + /// + /// Returns the s for the RGB colorspace. + /// + /// The precision in bits. + private static JpegColorConverterBase GetRgbConverter(int precision) + { + if (JpegColorConverterVector512.IsSupported) + { + return new RgbVector512(precision); + } + + if (JpegColorConverterVector256.IsSupported) + { + return new RgbVector256(precision); + } + + if (JpegColorConverterVector128.IsSupported) + { + return new RgbVector128(precision); + } + + return new RgbScalar(precision); + } + + private static JpegColorConverterBase GetTiffCmykConverter(int precision) + { + if (JpegColorConverterVector512.IsSupported) + { + return new TiffCmykVector512(precision); + } + + if (JpegColorConverterVector256.IsSupported) + { + return new TiffCmykVector256(precision); + } + + if (JpegColorConverterVector128.IsSupported) + { + return new TiffCmykVector128(precision); + } + + return new TiffCmykScalar(precision); + } + + private static JpegColorConverterBase GetTiffYccKConverter(int precision) + { + if (JpegColorConverterVector512.IsSupported) + { + return new TiffYccKVector512(precision); + } + + if (JpegColorConverterVector256.IsSupported) + { + return new TiffYccKVector256(precision); + } + + if (JpegColorConverterVector128.IsSupported) + { + return new TiffYccKVector128(precision); + } + + return new TiffYccKScalar(precision); + } + + /// + /// A stack-only struct to reference the input buffers using -s. + /// +#pragma warning disable SA1206 // Declaration keywords should follow order + public readonly ref struct ComponentValues +#pragma warning restore SA1206 // Declaration keywords should follow order + { + /// + /// The component count + /// + public readonly int ComponentCount; + + /// + /// The component 0 (eg. Y) + /// + public readonly Span Component0; + + /// + /// The component 1 (eg. Cb). In case of grayscale, it points to . + /// + public readonly Span Component1; + + /// + /// The component 2 (eg. Cr). In case of grayscale, it points to . + /// + public readonly Span Component2; + + /// + /// The component 4 + /// + public readonly Span Component3; + + /// + /// Initializes a new instance of the struct. + /// + /// List of component buffers. + /// Row to convert + public ComponentValues(IReadOnlyList> componentBuffers, int row) + { + DebugGuard.MustBeGreaterThan(componentBuffers.Count, 0, nameof(componentBuffers)); + + this.ComponentCount = componentBuffers.Count; + + this.Component0 = componentBuffers[0].DangerousGetRowSpan(row); + + // In case of grayscale, Component1 and Component2 point to Component0 memory area + this.Component1 = this.ComponentCount > 1 ? componentBuffers[1].DangerousGetRowSpan(row) : this.Component0; + this.Component2 = this.ComponentCount > 2 ? componentBuffers[2].DangerousGetRowSpan(row) : this.Component0; + this.Component3 = this.ComponentCount > 3 ? componentBuffers[3].DangerousGetRowSpan(row) : []; + } + + /// + /// Initializes a new instance of the struct. + /// + /// List of component color processors. + /// Row to convert + public ComponentValues(IReadOnlyList processors, int row) + { + DebugGuard.MustBeGreaterThan(processors.Count, 0, nameof(processors)); + + this.ComponentCount = processors.Count; + + this.Component0 = processors[0].GetColorBufferRowSpan(row); + + // In case of grayscale, Component1 and Component2 point to Component0 memory area + this.Component1 = this.ComponentCount > 1 ? processors[1].GetColorBufferRowSpan(row) : this.Component0; + this.Component2 = this.ComponentCount > 2 ? processors[2].GetColorBufferRowSpan(row) : this.Component0; + this.Component3 = this.ComponentCount > 3 ? processors[3].GetColorBufferRowSpan(row) : []; + } + + /// + /// Initializes a new instance of the struct. + /// + /// List of component color processors. + /// Row to convert + public ComponentValues(IReadOnlyList processors, int row) + { + DebugGuard.MustBeGreaterThan(processors.Count, 0, nameof(processors)); + + this.ComponentCount = processors.Count; + + this.Component0 = processors[0].GetColorBufferRowSpan(row); + + // In case of grayscale, Component1 and Component2 point to Component0 memory area + this.Component1 = this.ComponentCount > 1 ? processors[1].GetColorBufferRowSpan(row) : this.Component0; + this.Component2 = this.ComponentCount > 2 ? processors[2].GetColorBufferRowSpan(row) : this.Component0; + this.Component3 = this.ComponentCount > 3 ? processors[3].GetColorBufferRowSpan(row) : []; + } + + internal ComponentValues( + int componentCount, + Span c0, + Span c1, + Span c2, + Span c3) + { + this.ComponentCount = componentCount; + this.Component0 = c0; + this.Component1 = c1; + this.Component2 = c2; + this.Component3 = c3; + } + + public ComponentValues Slice(int start, int length) + { + Span c0 = this.Component0.Slice(start, length); + Span c1 = this.Component1.Length > 0 ? this.Component1.Slice(start, length) : []; + Span c2 = this.Component2.Length > 0 ? this.Component2.Slice(start, length) : []; + Span c3 = this.Component3.Length > 0 ? this.Component3.Slice(start, length) : []; + + return new ComponentValues(this.ComponentCount, c0, c1, c2, c3); + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterScalar.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterScalar.cs new file mode 100644 index 0000000..d4ecdfc --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterScalar.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + /// + /// abstract base for implementations + /// based on scalar instructions. + /// + internal abstract class JpegColorConverterScalar : JpegColorConverterBase + { + protected JpegColorConverterScalar(JpegColorSpace colorSpace, int precision) + : base(colorSpace, precision) + { + } + + public sealed override bool IsAvailable => true; + + public sealed override int ElementsPerBatch => 1; + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector.cs new file mode 100644 index 0000000..226b110 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector.cs @@ -0,0 +1,130 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + /// + /// abstract base for implementations + /// based on API. + /// + /// + /// Converters of this family can work with data of any size. + /// Even though real life data is guaranteed to be of size + /// divisible by 8 newer SIMD instructions like AVX512 won't work with + /// such data out of the box. These converters have fallback code + /// for 'remainder' data. + /// + internal abstract class JpegColorConverterVector : JpegColorConverterBase + { + protected JpegColorConverterVector(JpegColorSpace colorSpace, int precision) + : base(colorSpace, precision) + { + } + + /// + /// Gets a value indicating whether this converter is supported on current hardware. + /// + public static bool IsSupported => Vector.IsHardwareAccelerated && Vector.Count % 4 == 0; + + /// + public sealed override bool IsAvailable => IsSupported; + + public override int ElementsPerBatch => Vector.Count; + + /// + public sealed override void ConvertToRgbInPlace(in ComponentValues values) + { + DebugGuard.IsTrue(this.IsAvailable, $"{this.GetType().Name} converter is not supported on current hardware."); + + int length = values.Component0.Length; + int remainder = (int)((uint)length % (uint)Vector.Count); + + int simdCount = length - remainder; + if (simdCount > 0) + { + this.ConvertToRgbInPlaceVectorized(values.Slice(0, simdCount)); + } + + // Jpeg images width is always divisible by 8 without a remainder + // so it's safe to say SSE/AVX1/AVX2 implementations would never have + // 'remainder' pixels + // But some exotic simd implementations e.g. AVX-512 can have + // remainder pixels + if (remainder > 0) + { + this.ConvertToRgbInPlaceScalarRemainder(values.Slice(simdCount, remainder)); + } + } + + /// + public sealed override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + DebugGuard.IsTrue(this.IsAvailable, $"{this.GetType().Name} converter is not supported on current hardware."); + + int length = values.Component0.Length; + int remainder = (int)((uint)length % (uint)Vector.Count); + + int simdCount = length - remainder; + if (simdCount > 0) + { + this.ConvertFromRgbVectorized( + values.Slice(0, simdCount), + rLane[..simdCount], + gLane[..simdCount], + bLane[..simdCount]); + } + + // Jpeg images width is always divisible by 8 without a remainder + // so it's safe to say SSE/AVX1/AVX2 implementations would never have + // 'remainder' pixels + // But some exotic simd implementations e.g. AVX-512 can have + // remainder pixels + if (remainder > 0) + { + this.ConvertFromRgbScalarRemainder( + values.Slice(simdCount, remainder), + rLane.Slice(simdCount, remainder), + gLane.Slice(simdCount, remainder), + bLane.Slice(simdCount, remainder)); + } + } + + /// + /// Converts planar jpeg component values in + /// to RGB color space in place using API. + /// + /// The input/output as a stack-only struct + protected abstract void ConvertToRgbInPlaceVectorized(in ComponentValues values); + + /// + /// Converts remainder of the planar jpeg component values after + /// conversion in . + /// + /// The input/output as a stack-only struct + protected abstract void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values); + + /// + /// Converts RGB lanes to jpeg component values using API. + /// + /// Jpeg component values. + /// Red colors lane. + /// Green colors lane. + /// Blue colors lane. + protected abstract void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane); + + /// + /// Converts remainder of RGB lanes to jpeg component values after + /// conversion in . + /// + /// Jpeg component values. + /// Red colors lane. + /// Green colors lane. + /// Blue colors lane. + protected abstract void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector128.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector128.cs new file mode 100644 index 0000000..ca5c0bc --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector128.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + /// + /// abstract base for implementations + /// based on instructions. + /// + /// + /// Converters of this family would expect input buffers lengths to be + /// divisible by 8 without a remainder. + /// This is guaranteed by real-life data as jpeg stores pixels via 8x8 blocks. + /// DO NOT pass test data of invalid size to these converters as they + /// potentially won't do a bound check and return a false positive result. + /// + internal abstract class JpegColorConverterVector128 : JpegColorConverterBase + { + protected JpegColorConverterVector128(JpegColorSpace colorSpace, int precision) + : base(colorSpace, precision) + { + } + + public static bool IsSupported => Vector128.IsHardwareAccelerated; + + public sealed override bool IsAvailable => IsSupported; + + public sealed override int ElementsPerBatch => Vector128.Count; + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector256.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector256.cs new file mode 100644 index 0000000..c906e0b --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector256.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + /// + /// abstract base for implementations + /// based on instructions. + /// + /// + /// Converters of this family would expect input buffers lengths to be + /// divisible by 8 without a remainder. + /// This is guaranteed by real-life data as jpeg stores pixels via 8x8 blocks. + /// DO NOT pass test data of invalid size to these converters as they + /// potentially won't do a bound check and return a false positive result. + /// + internal abstract class JpegColorConverterVector256 : JpegColorConverterBase + { + protected JpegColorConverterVector256(JpegColorSpace colorSpace, int precision) + : base(colorSpace, precision) + { + } + + public static bool IsSupported => Vector256.IsHardwareAccelerated; + + public sealed override bool IsAvailable => IsSupported; + + public sealed override int ElementsPerBatch => Vector256.Count; + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector512.cs b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector512.cs new file mode 100644 index 0000000..9525181 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector512.cs @@ -0,0 +1,112 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal abstract partial class JpegColorConverterBase + { + /// + /// abstract base for implementations + /// based on instructions. + /// + internal abstract class JpegColorConverterVector512 : JpegColorConverterBase + { + protected JpegColorConverterVector512(JpegColorSpace colorSpace, int precision) + : base(colorSpace, precision) + { + } + + public static bool IsSupported => Vector512.IsHardwareAccelerated; + + /// + public override bool IsAvailable => IsSupported; + + /// + public override int ElementsPerBatch => Vector512.Count; + + /// + public sealed override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + DebugGuard.IsTrue(this.IsAvailable, $"{this.GetType().Name} converter is not supported on current hardware."); + + int length = values.Component0.Length; + int remainder = (int)((uint)length % (uint)Vector512.Count); + + int simdCount = length - remainder; + if (simdCount > 0) + { + this.ConvertFromRgbVectorized( + values.Slice(0, simdCount), + rLane[..simdCount], + gLane[..simdCount], + bLane[..simdCount]); + } + + if (remainder > 0) + { + this.ConvertFromRgbScalarRemainder( + values.Slice(simdCount, remainder), + rLane.Slice(simdCount, remainder), + gLane.Slice(simdCount, remainder), + bLane.Slice(simdCount, remainder)); + } + } + + /// + public sealed override void ConvertToRgbInPlace(in ComponentValues values) + { + DebugGuard.IsTrue(this.IsAvailable, $"{this.GetType().Name} converter is not supported on current hardware."); + + int length = values.Component0.Length; + int remainder = (int)((uint)length % (uint)Vector512.Count); + + int simdCount = length - remainder; + if (simdCount > 0) + { + this.ConvertToRgbInPlaceVectorized(values.Slice(0, simdCount)); + } + + if (remainder > 0) + { + this.ConvertToRgbInPlaceScalarRemainder(values.Slice(simdCount, remainder)); + } + } + + /// + /// Converts planar jpeg component values in + /// to RGB color space in place using API. + /// + /// The input/output as a stack-only struct + protected abstract void ConvertToRgbInPlaceVectorized(in ComponentValues values); + + /// + /// Converts remainder of the planar jpeg component values after + /// conversion in . + /// + /// The input/output as a stack-only struct + protected abstract void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values); + + /// + /// Converts RGB lanes to jpeg component values using API. + /// + /// Jpeg component values. + /// Red colors lane. + /// Green colors lane. + /// Blue colors lane. + protected abstract void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane); + + /// + /// Converts remainder of RGB lanes to jpeg component values after + /// conversion in . + /// + /// Jpeg component values. + /// Red colors lane. + /// Green colors lane. + /// Blue colors lane. + protected abstract void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ComponentType.cs b/ImageSharp/Formats/Jpeg/Components/ComponentType.cs new file mode 100644 index 0000000..cb329a2 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ComponentType.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal enum ComponentType + { + Huffman = 0, + + Arithmetic = 1 + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/AdobeMarker.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/AdobeMarker.cs new file mode 100644 index 0000000..0ee87aa --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/AdobeMarker.cs @@ -0,0 +1,108 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Provides information about the Adobe marker segment. + /// + /// See the included 5116.DCT.pdf file in the source for more information. + internal readonly struct AdobeMarker : IEquatable + { + /// + /// Gets the length of an adobe marker segment. + /// + public const int Length = 12; + + /// + /// Initializes a new instance of the struct. + /// + /// The DCT encode version + /// The horizontal downsampling hint used for DCT encoding + /// The vertical downsampling hint used for DCT encoding + /// The color transform model used + private AdobeMarker(short dctEncodeVersion, short app14Flags0, short app14Flags1, byte colorTransform) + { + this.DCTEncodeVersion = dctEncodeVersion; + this.APP14Flags0 = app14Flags0; + this.APP14Flags1 = app14Flags1; + this.ColorTransform = colorTransform; + } + + /// + /// Gets the DCT Encode Version + /// + public short DCTEncodeVersion { get; } + + /// + /// Gets the horizontal downsampling hint used for DCT encoding + /// 0x0 : (none - Chop) + /// Bit 15 : Encoded with Blend=1 downsampling. + /// + public short APP14Flags0 { get; } + + /// + /// Gets the vertical downsampling hint used for DCT encoding + /// 0x0 : (none - Chop) + /// Bit 15 : Encoded with Blend=1 downsampling + /// + public short APP14Flags1 { get; } + + /// + /// Gets the colorspace transform model used + /// 00 : Unknown (RGB or CMYK) + /// 01 : YCbCr + /// 02 : YCCK + /// + public byte ColorTransform { get; } + + /// + /// Converts the specified byte array representation of an Adobe marker to its equivalent and + /// returns a value that indicates whether the conversion succeeded. + /// + /// The byte array containing metadata to parse. + /// The marker to return. + public static bool TryParse(ReadOnlySpan bytes, out AdobeMarker marker) + { + if (ProfileResolver.IsProfile(bytes, ProfileResolver.AdobeMarker)) + { + short dctEncodeVersion = (short)((bytes[5] << 8) | bytes[6]); + short app14Flags0 = (short)((bytes[7] << 8) | bytes[8]); + short app14Flags1 = (short)((bytes[9] << 8) | bytes[10]); + byte colorTransform = bytes[11]; + + marker = new AdobeMarker(dctEncodeVersion, app14Flags0, app14Flags1, colorTransform); + return true; + } + + marker = default; + return false; + } + + /// + public bool Equals(AdobeMarker other) + { + return this.DCTEncodeVersion == other.DCTEncodeVersion + && this.APP14Flags0 == other.APP14Flags0 + && this.APP14Flags1 == other.APP14Flags1 + && this.ColorTransform == other.ColorTransform; + } + + /// + public override bool Equals(object? obj) + { + return obj is AdobeMarker other && this.Equals(other); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine( + this.DCTEncodeVersion, + this.APP14Flags0, + this.APP14Flags1, + this.ColorTransform); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingComponent.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingComponent.cs new file mode 100644 index 0000000..c2bfb1b --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingComponent.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + internal class ArithmeticDecodingComponent : JpegComponent + { + public ArithmeticDecodingComponent(MemoryAllocator memoryAllocator, JpegFrame frame, byte id, int horizontalFactor, int verticalFactor, byte quantizationTableIndex, int index) + : base(memoryAllocator, frame, id, horizontalFactor, verticalFactor, quantizationTableIndex, index) + { + } + + /// + /// Gets or sets the dc context. + /// + public int DcContext { get; set; } + + /// + /// Gets or sets the dc statistics. + /// + public ArithmeticStatistics DcStatistics { get; set; } + + /// + /// Gets or sets the ac statistics. + /// + public ArithmeticStatistics AcStatistics { get; set; } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingTable.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingTable.cs new file mode 100644 index 0000000..ae072f4 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingTable.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + internal class ArithmeticDecodingTable + { + public ArithmeticDecodingTable(byte tableClass, byte identifier) + { + this.TableClass = tableClass; + this.Identifier = identifier; + } + + public byte TableClass { get; } + + public byte Identifier { get; } + + public byte ConditioningTableValue { get; private set; } + + public int DcL { get; private set; } + + public int DcU { get; private set; } + + public int AcKx { get; private set; } + + public void Configure(byte conditioningTableValue) + { + this.ConditioningTableValue = conditioningTableValue; + if (this.TableClass == 0) + { + this.DcL = conditioningTableValue & 0x0F; + this.DcU = conditioningTableValue >> 4; + this.AcKx = 0; + } + else + { + this.DcL = 0; + this.DcU = 0; + this.AcKx = conditioningTableValue; + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs new file mode 100644 index 0000000..9acdd00 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs @@ -0,0 +1,1241 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Decodes a arithmetic encoded spectral scan. + /// Based on https://github.com/yigolden/JpegLibrary/blob/main/src/JpegLibrary/ScanDecoder/JpegArithmeticScanDecoder.cs + /// + internal class ArithmeticScanDecoder : IJpegScanDecoder + { + private readonly BufferedReadStream stream; + + private int c; + private int a; + private int ct; + + /// + /// instance containing decoding-related information. + /// + private JpegFrame frame; + + /// + /// Shortcut for .Components. + /// + private IJpegComponent[] components; + + /// + /// Number of component in the current scan. + /// + private int scanComponentCount; + + /// + /// The reset interval determined by RST markers. + /// + private int restartInterval; + + /// + /// How many mcu's are left to do. + /// + private int todo; + + private readonly SpectralConverter spectralConverter; + + private JpegBitReader scanBuffer; + + private ArithmeticDecodingTable[] dcDecodingTables; + + private ArithmeticDecodingTable[] acDecodingTables; + + // Don't make this a ReadOnlySpan, as the values need to get updated. + private readonly byte[] fixedBin = [113, 0, 0, 0]; + + private readonly CancellationToken cancellationToken; + + private static readonly int[] ArithmeticTable = + [ + Pack(0x5a1d, 1, 1, 1), + Pack(0x2586, 14, 2, 0), + Pack(0x1114, 16, 3, 0), + Pack(0x080b, 18, 4, 0), + Pack(0x03d8, 20, 5, 0), + Pack(0x01da, 23, 6, 0), + Pack(0x00e5, 25, 7, 0), + Pack(0x006f, 28, 8, 0), + Pack(0x0036, 30, 9, 0), + Pack(0x001a, 33, 10, 0), + Pack(0x000d, 35, 11, 0), + Pack(0x0006, 9, 12, 0), + Pack(0x0003, 10, 13, 0), + Pack(0x0001, 12, 13, 0), + Pack(0x5a7f, 15, 15, 1), + Pack(0x3f25, 36, 16, 0), + Pack(0x2cf2, 38, 17, 0), + Pack(0x207c, 39, 18, 0), + Pack(0x17b9, 40, 19, 0), + Pack(0x1182, 42, 20, 0), + Pack(0x0cef, 43, 21, 0), + Pack(0x09a1, 45, 22, 0), + Pack(0x072f, 46, 23, 0), + Pack(0x055c, 48, 24, 0), + Pack(0x0406, 49, 25, 0), + Pack(0x0303, 51, 26, 0), + Pack(0x0240, 52, 27, 0), + Pack(0x01b1, 54, 28, 0), + Pack(0x0144, 56, 29, 0), + Pack(0x00f5, 57, 30, 0), + Pack(0x00b7, 59, 31, 0), + Pack(0x008a, 60, 32, 0), + Pack(0x0068, 62, 33, 0), + Pack(0x004e, 63, 34, 0), + Pack(0x003b, 32, 35, 0), + Pack(0x002c, 33, 9, 0), + Pack(0x5ae1, 37, 37, 1), + Pack(0x484c, 64, 38, 0), + Pack(0x3a0d, 65, 39, 0), + Pack(0x2ef1, 67, 40, 0), + Pack(0x261f, 68, 41, 0), + Pack(0x1f33, 69, 42, 0), + Pack(0x19a8, 70, 43, 0), + Pack(0x1518, 72, 44, 0), + Pack(0x1177, 73, 45, 0), + Pack(0x0e74, 74, 46, 0), + Pack(0x0bfb, 75, 47, 0), + Pack(0x09f8, 77, 48, 0), + Pack(0x0861, 78, 49, 0), + Pack(0x0706, 79, 50, 0), + Pack(0x05cd, 48, 51, 0), + Pack(0x04de, 50, 52, 0), + Pack(0x040f, 50, 53, 0), + Pack(0x0363, 51, 54, 0), + Pack(0x02d4, 52, 55, 0), + Pack(0x025c, 53, 56, 0), + Pack(0x01f8, 54, 57, 0), + Pack(0x01a4, 55, 58, 0), + Pack(0x0160, 56, 59, 0), + Pack(0x0125, 57, 60, 0), + Pack(0x00f6, 58, 61, 0), + Pack(0x00cb, 59, 62, 0), + Pack(0x00ab, 61, 63, 0), + Pack(0x008f, 61, 32, 0), + Pack(0x5b12, 65, 65, 1), + Pack(0x4d04, 80, 66, 0), + Pack(0x412c, 81, 67, 0), + Pack(0x37d8, 82, 68, 0), + Pack(0x2fe8, 83, 69, 0), + Pack(0x293c, 84, 70, 0), + Pack(0x2379, 86, 71, 0), + Pack(0x1edf, 87, 72, 0), + Pack(0x1aa9, 87, 73, 0), + Pack(0x174e, 72, 74, 0), + Pack(0x1424, 72, 75, 0), + Pack(0x119c, 74, 76, 0), + Pack(0x0f6b, 74, 77, 0), + Pack(0x0d51, 75, 78, 0), + Pack(0x0bb6, 77, 79, 0), + Pack(0x0a40, 77, 48, 0), + Pack(0x5832, 80, 81, 1), + Pack(0x4d1c, 88, 82, 0), + Pack(0x438e, 89, 83, 0), + Pack(0x3bdd, 90, 84, 0), + Pack(0x34ee, 91, 85, 0), + Pack(0x2eae, 92, 86, 0), + Pack(0x299a, 93, 87, 0), + Pack(0x2516, 86, 71, 0), + Pack(0x5570, 88, 89, 1), + Pack(0x4ca9, 95, 90, 0), + Pack(0x44d9, 96, 91, 0), + Pack(0x3e22, 97, 92, 0), + Pack(0x3824, 99, 93, 0), + Pack(0x32b4, 99, 94, 0), + Pack(0x2e17, 93, 86, 0), + Pack(0x56a8, 95, 96, 1), + Pack(0x4f46, 101, 97, 0), + Pack(0x47e5, 102, 98, 0), + Pack(0x41cf, 103, 99, 0), + Pack(0x3c3d, 104, 100, 0), + Pack(0x375e, 99, 93, 0), + Pack(0x5231, 105, 102, 0), + Pack(0x4c0f, 106, 103, 0), + Pack(0x4639, 107, 104, 0), + Pack(0x415e, 103, 99, 0), + Pack(0x5627, 105, 106, 1), + Pack(0x50e7, 108, 107, 0), + Pack(0x4b85, 109, 103, 0), + Pack(0x5597, 110, 109, 0), + Pack(0x504f, 111, 107, 0), + Pack(0x5a10, 110, 111, 1), + Pack(0x5522, 112, 109, 0), + Pack(0x59eb, 112, 111, 1), + + // This last entry is used for fixed probability estimate of 0.5 + // as suggested in Section 10.3 Table 5 of ITU-T Rec. T.851. + Pack(0x5a1d, 113, 113, 0) + ]; + + private readonly List statistics = []; + + /// + /// Initializes a new instance of the class. + /// + /// The input stream. + /// Spectral to pixel converter. + /// The token to monitor cancellation. + public ArithmeticScanDecoder(BufferedReadStream stream, SpectralConverter converter, CancellationToken cancellationToken) + { + this.stream = stream; + this.spectralConverter = converter; + this.cancellationToken = cancellationToken; + + this.c = 0; + this.a = 0; + this.ct = -16; // Force reading 2 initial bytes to fill C. + } + + /// + public int ResetInterval + { + set + { + this.restartInterval = value; + this.todo = value; + } + } + + /// + public int SpectralStart { get; set; } + + /// + public int SpectralEnd { get; set; } + + /// + public int SuccessiveHigh { get; set; } + + /// + public int SuccessiveLow { get; set; } + + public void InitDecodingTables(List arithmeticDecodingTables) + { + for (int i = 0; i < this.components.Length; i++) + { + ArithmeticDecodingComponent component = this.components[i] as ArithmeticDecodingComponent; + this.dcDecodingTables[i] = GetArithmeticTable(arithmeticDecodingTables, true, component.DcTableId); + component.DcStatistics = this.CreateOrGetStatisticsBin(true, component.DcTableId); + this.acDecodingTables[i] = GetArithmeticTable(arithmeticDecodingTables, false, component.AcTableId); + component.AcStatistics = this.CreateOrGetStatisticsBin(false, component.AcTableId); + } + } + + private ref byte GetFixedBinReference() => ref MemoryMarshal.GetArrayDataReference(this.fixedBin); + + /// + public void ParseEntropyCodedData(int scanComponentCount, IccProfile iccProfile) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + this.scanComponentCount = scanComponentCount; + + this.scanBuffer = new JpegBitReader(this.stream); + + this.frame.AllocateComponents(); + + if (this.frame.Progressive) + { + this.ParseProgressiveData(); + } + else + { + this.ParseBaselineData(iccProfile); + } + + if (this.scanBuffer.HasBadMarker()) + { + this.stream.Position = this.scanBuffer.MarkerPosition; + } + } + + /// + public void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) + { + this.frame = frame; + this.components = frame.Components; + + this.dcDecodingTables = new ArithmeticDecodingTable[this.components.Length]; + this.acDecodingTables = new ArithmeticDecodingTable[this.components.Length]; + + this.spectralConverter.InjectFrameData(frame, jpegData); + } + + private static ArithmeticDecodingTable GetArithmeticTable(List arithmeticDecodingTables, bool isDcTable, int identifier) + { + int tableClass = isDcTable ? 0 : 1; + + foreach (ArithmeticDecodingTable item in arithmeticDecodingTables) + { + if (item.TableClass == tableClass && item.Identifier == identifier) + { + return item; + } + } + + return null; + } + + private ArithmeticStatistics CreateOrGetStatisticsBin(bool dc, int identifier, bool reset = false) + { + foreach (ArithmeticStatistics item in this.statistics) + { + if (item.IsDcStatistics == dc && item.Identifier == identifier) + { + if (reset) + { + item.Reset(); + } + + return item; + } + } + + ArithmeticStatistics statistic = new(dc, identifier); + this.statistics.Add(statistic); + return statistic; + } + + private void ParseBaselineData(IccProfile iccProfile) + { + for (int i = 0; i < this.components.Length; i++) + { + ArithmeticDecodingComponent component = (ArithmeticDecodingComponent)this.components[i]; + component.DcPredictor = 0; + component.DcContext = 0; + component.DcStatistics?.Reset(); + component.AcStatistics?.Reset(); + } + + this.Reset(); + + if (this.scanComponentCount != 1) + { + this.spectralConverter.PrepareForDecoding(); + this.ParseBaselineDataInterleaved(iccProfile); + this.spectralConverter.CommitConversion(); + } + else if (this.frame.ComponentCount == 1) + { + this.spectralConverter.PrepareForDecoding(); + this.ParseBaselineDataSingleComponent(iccProfile); + this.spectralConverter.CommitConversion(); + } + else + { + this.ParseBaselineDataNonInterleaved(); + } + } + + private void ParseProgressiveData() + { + this.CheckProgressiveData(); + + for (int i = 0; i < this.components.Length; i++) + { + ArithmeticDecodingComponent component = (ArithmeticDecodingComponent)this.components[i]; + if (this.SpectralStart == 0 && this.SuccessiveHigh == 0) + { + component.DcPredictor = 0; + component.DcContext = 0; + component.DcStatistics?.Reset(); + } + + if (this.SpectralStart != 0) + { + component.AcStatistics?.Reset(); + } + } + + this.Reset(); + + if (this.scanComponentCount == 1) + { + this.ParseProgressiveDataNonInterleaved(); + } + else + { + this.ParseProgressiveDataInterleaved(); + } + } + + private void CheckProgressiveData() + { + // Validate successive scan parameters. + // Logic has been adapted from libjpeg. + // See Table B.3 – Scan header parameter size and values. itu-t81.pdf + bool invalid = false; + if (this.SpectralStart == 0) + { + if (this.SpectralEnd != 0) + { + invalid = true; + } + } + else + { + // Need not check Ss/Se < 0 since they came from unsigned bytes. + if (this.SpectralEnd < this.SpectralStart || this.SpectralEnd > 63) + { + invalid = true; + } + + // AC scans may have only one component. + if (this.scanComponentCount != 1) + { + invalid = true; + } + } + + if (this.SuccessiveHigh != 0) + { + // Successive approximation refinement scan: must have Al = Ah-1. + if (this.SuccessiveHigh - 1 != this.SuccessiveLow) + { + invalid = true; + } + } + + // TODO: How does this affect 12bit jpegs. + // According to libjpeg the range covers 8bit only? + if (this.SuccessiveLow > 13) + { + invalid = true; + } + + if (invalid) + { + JpegThrowHelper.ThrowBadProgressiveScan(this.SpectralStart, this.SpectralEnd, this.SuccessiveHigh, this.SuccessiveLow); + } + } + + private void ParseBaselineDataInterleaved(IccProfile iccProfile) + { + int mcu = 0; + int mcusPerColumn = this.frame.McusPerColumn; + int mcusPerLine = this.frame.McusPerLine; + ref JpegBitReader reader = ref this.scanBuffer; + + for (int j = 0; j < mcusPerColumn; j++) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + // Decode from binary to spectral. + for (int i = 0; i < mcusPerLine; i++) + { + // Scan an interleaved mcu... process components in order. + int mcuCol = mcu % mcusPerLine; + for (int k = 0; k < this.scanComponentCount; k++) + { + int order = this.frame.ComponentOrder[k]; + ArithmeticDecodingComponent component = this.components[order] as ArithmeticDecodingComponent; + + ref ArithmeticDecodingTable dcDecodingTable = ref this.dcDecodingTables[component.DcTableId]; + ref ArithmeticDecodingTable acDecodingTable = ref this.acDecodingTables[component.AcTableId]; + + int h = component.HorizontalSamplingFactor; + int v = component.VerticalSamplingFactor; + + // Scan out an mcu's worth of this component; that's just determined + // by the basic H and V specified for the component. + int mcuColMulh = mcuCol * h; + for (int y = 0; y < v; y++) + { + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int x = 0; x < h; x++) + { + if (reader.NoData) + { + // It is very likely that some spectral data was decoded before we've encountered 'end of scan' + // so we need to decode what's left and return (or maybe throw?) + this.spectralConverter.ConvertStrideBaseline(iccProfile); + return; + } + + int blockCol = mcuColMulh + x; + + this.DecodeBlockBaseline( + component, + ref Unsafe.Add(ref blockRef, (uint)blockCol), + ref acDecodingTable, + ref dcDecodingTable); + } + } + } + + // After all interleaved components, that's an interleaved MCU, + // so now count down the restart interval. + mcu++; + this.HandleRestart(); + } + + // Convert from spectral to actual pixels via given converter. + this.spectralConverter.ConvertStrideBaseline(iccProfile); + } + } + + private void ParseBaselineDataSingleComponent(IccProfile iccProfile) + { + ArithmeticDecodingComponent component = this.frame.Components[0] as ArithmeticDecodingComponent; + int mcuLines = this.frame.McusPerColumn; + int w = component.WidthInBlocks; + int h = component.SamplingFactors.Height; + ref ArithmeticDecodingTable dcDecodingTable = ref this.dcDecodingTables[component.DcTableId]; + ref ArithmeticDecodingTable acDecodingTable = ref this.acDecodingTables[component.AcTableId]; + + ref JpegBitReader reader = ref this.scanBuffer; + + for (int i = 0; i < mcuLines; i++) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + // Decode from binary to spectral. + for (int j = 0; j < h; j++) + { + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int k = 0; k < w; k++) + { + if (reader.NoData) + { + // It is very likely that some spectral data was decoded before we've encountered 'end of scan' + // so we need to decode what's left and return (or maybe throw?) + this.spectralConverter.ConvertStrideBaseline(iccProfile); + return; + } + + this.DecodeBlockBaseline( + component, + ref Unsafe.Add(ref blockRef, (uint)k), + ref acDecodingTable, + ref dcDecodingTable); + + this.HandleRestart(); + } + } + + // Convert from spectral to actual pixels via given converter. + this.spectralConverter.ConvertStrideBaseline(iccProfile); + } + } + + private void ParseBaselineDataNonInterleaved() + { + ArithmeticDecodingComponent component = (ArithmeticDecodingComponent)this.components[this.frame.ComponentOrder[0]]; + ref JpegBitReader reader = ref this.scanBuffer; + + int w = component.WidthInBlocks; + int h = component.HeightInBlocks; + + ref ArithmeticDecodingTable dcDecodingTable = ref this.dcDecodingTables[component.DcTableId]; + ref ArithmeticDecodingTable acDecodingTable = ref this.acDecodingTables[component.AcTableId]; + + for (int j = 0; j < h; j++) + { + this.cancellationToken.ThrowIfCancellationRequested(); + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int i = 0; i < w; i++) + { + if (reader.NoData) + { + return; + } + + this.DecodeBlockBaseline( + component, + ref Unsafe.Add(ref blockRef, (uint)i), + ref acDecodingTable, + ref dcDecodingTable); + + this.HandleRestart(); + } + } + } + + private void ParseProgressiveDataInterleaved() + { + int mcu = 0; + int mcusPerColumn = this.frame.McusPerColumn; + int mcusPerLine = this.frame.McusPerLine; + ref JpegBitReader reader = ref this.scanBuffer; + + for (int j = 0; j < mcusPerColumn; j++) + { + for (int i = 0; i < mcusPerLine; i++) + { + // Scan an interleaved mcu... process components in order. + int mcuRow = Math.DivRem(mcu, mcusPerLine, out int mcuCol); + for (int k = 0; k < this.scanComponentCount; k++) + { + int order = this.frame.ComponentOrder[k]; + ArithmeticDecodingComponent component = this.components[order] as ArithmeticDecodingComponent; + ref ArithmeticDecodingTable dcDecodingTable = ref this.dcDecodingTables[component.DcTableId]; + + int h = component.HorizontalSamplingFactor; + int v = component.VerticalSamplingFactor; + + // Scan out an mcu's worth of this component; that's just determined + // by the basic H and V specified for the component. + int mcuColMulh = mcuCol * h; + for (int y = 0; y < v; y++) + { + int blockRow = (mcuRow * v) + y; + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(blockRow); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int x = 0; x < h; x++) + { + if (reader.NoData) + { + return; + } + + int blockCol = mcuColMulh + x; + + this.DecodeBlockProgressiveDc( + component, + ref Unsafe.Add(ref blockRef, (uint)blockCol), + ref dcDecodingTable); + } + } + } + + // After all interleaved components, that's an interleaved MCU, + // so now count down the restart interval. + mcu++; + this.HandleRestart(); + } + } + } + + private void ParseProgressiveDataNonInterleaved() + { + ArithmeticDecodingComponent component = this.components[this.frame.ComponentOrder[0]] as ArithmeticDecodingComponent; + ref JpegBitReader reader = ref this.scanBuffer; + + int w = component.WidthInBlocks; + int h = component.HeightInBlocks; + + if (this.SpectralStart == 0) + { + ref ArithmeticDecodingTable dcDecodingTable = ref this.dcDecodingTables[component.DcTableId]; + + for (int j = 0; j < h; j++) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int i = 0; i < w; i++) + { + if (reader.NoData) + { + return; + } + + this.DecodeBlockProgressiveDc( + component, + ref Unsafe.Add(ref blockRef, (uint)i), + ref dcDecodingTable); + + this.HandleRestart(); + } + } + } + else + { + ref ArithmeticDecodingTable acDecodingTable = ref this.acDecodingTables[component.AcTableId]; + + for (int j = 0; j < h; j++) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int i = 0; i < w; i++) + { + if (reader.NoData) + { + return; + } + + this.DecodeBlockProgressiveAc( + component, + ref Unsafe.Add(ref blockRef, (uint)i), + ref acDecodingTable); + + this.HandleRestart(); + } + } + } + } + + private void DecodeBlockProgressiveDc(ArithmeticDecodingComponent component, ref Block8x8 block, ref ArithmeticDecodingTable dcTable) + { + if (dcTable == null) + { + JpegThrowHelper.ThrowInvalidImageContentException("DC table is missing"); + } + + ref JpegBitReader reader = ref this.scanBuffer; + ref short blockDataRef = ref Unsafe.As(ref block); + + if (this.SuccessiveHigh == 0) + { + // First scan + // Sections F.2.4.1 & F.1.4.4.1: Decoding of DC coefficients. + + // Table F.4: Point to statistics bin S0 for DC coefficient coding. + ref byte st = ref Unsafe.Add(ref component.DcStatistics.GetReference(), (uint)component.DcContext); + + // Figure F.19: Decode_DC_DIFF + if (this.DecodeBinaryDecision(ref reader, ref st) == 0) + { + component.DcContext = 0; + } + else + { + // Figure F.21: Decoding nonzero value v. + // Figure F.22: Decoding the sign of v. + int sign = this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 1)); + st = ref Unsafe.Add(ref st, (uint)(2 + sign)); + + // Figure F.23: Decoding the magnitude category of v. + int m = this.DecodeBinaryDecision(ref reader, ref st); + if (m != 0) + { + st = ref component.DcStatistics.GetReference(20); + while (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + if ((m <<= 1) == 0x8000) + { + JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code."); + } + + st = ref Unsafe.Add(ref st, 1); + } + } + + // Section F.1.4.4.1.2: Establish dc_context conditioning category. + if (m < (int)((1L << dcTable.DcL) >> 1)) + { + component.DcContext = 0; // Zero diff category. + } + else if (m > (int)((1L << dcTable.DcU) >> 1)) + { + component.DcContext = 12 + (sign * 4); // Large diff category. + } + else + { + component.DcContext = 4 + (sign * 4); // Small diff category. + } + + int v = m; + + // Figure F.24: Decoding the magnitude bit pattern of v. + st = ref Unsafe.Add(ref st, 14); + while ((m >>= 1) != 0) + { + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + v |= m; + } + } + + v++; + if (sign != 0) + { + v = -v; + } + + component.DcPredictor = (short)(component.DcPredictor + v); + } + + blockDataRef = (short)(component.DcPredictor << this.SuccessiveLow); + } + else + { + // Refinement scan. + ref byte st = ref this.GetFixedBinReference(); + + blockDataRef |= (short)(this.DecodeBinaryDecision(ref reader, ref st) << this.SuccessiveLow); + } + } + + private void DecodeBlockProgressiveAc(ArithmeticDecodingComponent component, ref Block8x8 block, ref ArithmeticDecodingTable acTable) + { + ref JpegBitReader reader = ref this.scanBuffer; + ref short blockDataRef = ref Unsafe.As(ref block); + + ArithmeticStatistics acStatistics = component.AcStatistics; + if (acStatistics == null || acTable == null) + { + JpegThrowHelper.ThrowInvalidImageContentException("AC table is missing"); + } + + if (this.SuccessiveHigh == 0) + { + // Sections F.2.4.2 & F.1.4.4.2: Decoding of AC coefficients. + + // Figure F.20: Decode_AC_coefficients. + int start = this.SpectralStart; + int end = this.SpectralEnd; + int low = this.SuccessiveLow; + + for (int k = start; k <= end; k++) + { + ref byte st = ref acStatistics.GetReference(3 * (k - 1)); + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + break; + } + + while (this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 1)) == 0) + { + st = ref Unsafe.Add(ref st, 3); + k++; + if (k > 63) + { + JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code."); + } + } + + // Figure F.21: Decoding nonzero value v. + // Figure F.22: Decoding the sign of v. + int sign = this.DecodeBinaryDecision(ref reader, ref this.GetFixedBinReference()); + st = ref Unsafe.Add(ref st, 2); + + // Figure F.23: Decoding the magnitude category of v. + int m = this.DecodeBinaryDecision(ref reader, ref st); + if (m != 0) + { + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + m <<= 1; + st = ref acStatistics.GetReference(k <= acTable.AcKx ? 189 : 217); + while (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + if ((m <<= 1) == 0x8000) + { + JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code."); + } + + st = ref Unsafe.Add(ref st, 1); + } + } + } + + int v = m; + + // Figure F.24: Decoding the magnitude bit pattern of v. + st = ref Unsafe.Add(ref st, 14); + while ((m >>= 1) != 0) + { + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + v |= m; + } + } + + v++; + if (sign != 0) + { + v = -v; + } + + Unsafe.Add(ref blockDataRef, ZigZag.TransposingOrder[k]) = (short)(v << low); + } + } + else + { + // Refinement scan. + this.ReadBlockProgressiveAcRefined(acStatistics, ref blockDataRef); + } + } + + private void ReadBlockProgressiveAcRefined(ArithmeticStatistics acStatistics, ref short blockDataRef) + { + ref JpegBitReader reader = ref this.scanBuffer; + int start = this.SpectralStart; + int end = this.SpectralEnd; + + int p1 = 1 << this.SuccessiveLow; + int m1 = -1 << this.SuccessiveLow; + + // Establish EOBx (previous stage end-of-block) index. + int kex = end; + for (; kex > 0; kex--) + { + if (Unsafe.Add(ref blockDataRef, ZigZag.TransposingOrder[kex]) != 0) + { + break; + } + } + + for (int k = start; k <= end; k++) + { + ref byte st = ref acStatistics.GetReference(3 * (k - 1)); + if (k > kex) + { + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + break; + } + } + + while (true) + { + ref short coef = ref Unsafe.Add(ref blockDataRef, ZigZag.TransposingOrder[k]); + if (coef != 0) + { + if (this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 2)) != 0) + { + coef = (short)(coef + (coef < 0 ? m1 : p1)); + } + + break; + } + + if (this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 1)) != 0) + { + bool flag = this.DecodeBinaryDecision(ref reader, ref this.GetFixedBinReference()) != 0; + coef = (short)(coef + (flag ? m1 : p1)); + + break; + } + + st = ref Unsafe.Add(ref st, 3); + k++; + if (k > end) + { + JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code."); + } + } + } + } + + private void DecodeBlockBaseline( + ArithmeticDecodingComponent component, + ref Block8x8 destinationBlock, + ref ArithmeticDecodingTable acTable, + ref ArithmeticDecodingTable dcTable) + { + if (acTable is null) + { + JpegThrowHelper.ThrowInvalidImageContentException("AC table is missing."); + } + + if (dcTable is null) + { + JpegThrowHelper.ThrowInvalidImageContentException("DC table is missing."); + } + + ref JpegBitReader reader = ref this.scanBuffer; + ref short destinationRef = ref Unsafe.As(ref destinationBlock); + + // Sections F.2.4.1 & F.1.4.4.1: Decoding of DC coefficients. + + // Table F.4: Point to statistics bin S0 for DC coefficient coding. + ref byte st = ref Unsafe.Add(ref component.DcStatistics.GetReference(), (uint)component.DcContext); + + /* Figure F.19: Decode_DC_DIFF */ + if (this.DecodeBinaryDecision(ref reader, ref st) == 0) + { + component.DcContext = 0; + } + else + { + // Figure F.21: Decoding nonzero value v + // Figure F.22: Decoding the sign of v + int sign = this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 1)); + st = ref Unsafe.Add(ref st, (uint)(2 + sign)); + + // Figure F.23: Decoding the magnitude category of v. + int m = this.DecodeBinaryDecision(ref reader, ref st); + if (m != 0) + { + // Table F.4: X1 = 20 + st = ref component.DcStatistics.GetReference(20); + while (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + if ((m <<= 1) == 0x8000) + { + JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code."); + } + + st = ref Unsafe.Add(ref st, 1); + } + } + + // Section F.1.4.4.1.2: Establish dc_context conditioning category. + if (m < (int)((1L << dcTable.DcL) >> 1)) + { + component.DcContext = 0; // zero diff category + } + else if (m > (int)((1L << dcTable.DcU) >> 1)) + { + component.DcContext = 12 + (sign * 4); // large diff category + } + else + { + component.DcContext = 4 + (sign * 4); // small diff category + } + + int v = m; + + // Figure F.24: Decoding the magnitude bit pattern of v. + st = ref Unsafe.Add(ref st, 14); + while ((m >>= 1) != 0) + { + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + v |= m; + } + } + + v++; + if (sign != 0) + { + v = -v; + } + + component.DcPredictor = (short)(component.DcPredictor + v); + } + + destinationRef = (short)component.DcPredictor; + + // Sections F.2.4.2 & F.1.4.4.2: Decoding of AC coefficients. + ArithmeticStatistics acStatistics = component.AcStatistics; + + for (int k = 1; k <= 63; k++) + { + st = ref acStatistics.GetReference(3 * (k - 1)); + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + // EOB flag. + break; + } + + while (this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 1)) == 0) + { + st = ref Unsafe.Add(ref st, 3); + k++; + if (k > 63) + { + JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code."); + } + } + + // Figure F.21: Decoding nonzero value v. + // Figure F.22: Decoding the sign of v. + int sign = this.DecodeBinaryDecision(ref reader, ref this.GetFixedBinReference()); + st = ref Unsafe.Add(ref st, 2); + + // Figure F.23: Decoding the magnitude category of v. + int m = this.DecodeBinaryDecision(ref reader, ref st); + if (m != 0) + { + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + m <<= 1; + st = ref acStatistics.GetReference(k <= acTable.AcKx ? 189 : 217); + while (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + if ((m <<= 1) == 0x8000) + { + JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code."); + } + + st = ref Unsafe.Add(ref st, 1); + } + } + } + + int v = m; + + // Figure F.24: Decoding the magnitude bit pattern of v. + st = ref Unsafe.Add(ref st, 14); + while ((m >>= 1) != 0) + { + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + v |= m; + } + } + + v++; + if (sign != 0) + { + v = -v; + } + + Unsafe.Add(ref destinationRef, ZigZag.TransposingOrder[k]) = (short)v; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private bool HandleRestart() + { + if (this.restartInterval > 0 && (--this.todo) == 0) + { + if (this.scanBuffer.Marker == JpegConstants.Markers.XFF) + { + if (!this.scanBuffer.FindNextMarker()) + { + return false; + } + } + + this.todo = this.restartInterval; + + for (int i = 0; i < this.components.Length; i++) + { + ArithmeticDecodingComponent component = (ArithmeticDecodingComponent)this.components[i]; + component.DcPredictor = 0; + component.DcContext = 0; + component.DcStatistics?.Reset(); + component.AcStatistics?.Reset(); + } + + this.Reset(); + + if (this.scanBuffer.HasRestartMarker()) + { + this.Reset(); + return true; + } + + if (this.scanBuffer.HasBadMarker()) + { + this.stream.Position = this.scanBuffer.MarkerPosition; + this.Reset(); + return true; + } + } + + return false; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private void Reset() + { + for (int i = 0; i < this.components.Length; i++) + { + ArithmeticDecodingComponent component = this.components[i] as ArithmeticDecodingComponent; + component.DcPredictor = 0; + } + + this.c = 0; + this.a = 0; + this.ct = -16; // Force reading 2 initial bytes to fill C. + + this.scanBuffer.Reset(); + } + + private int DecodeBinaryDecision(ref JpegBitReader reader, ref byte st) + { + // Renormalization & data input per section D.2.6 + while (this.a < 0x8000) + { + if (--this.ct < 0) + { + // Need to fetch next data byte. + reader.CheckBits(); + int data = reader.GetBits(8); + + // Insert data into C register. + this.c = (this.c << 8) | data; + + // Update bit shift counter. + if ((this.ct += 8) < 0) + { + // Need more initial bytes. + if (++this.ct == 0) + { + // Got 2 initial bytes -> re-init A and exit loop + this.a = 0x8000; // e->a = 0x10000L after loop exit + } + } + } + + this.a <<= 1; + } + + // Fetch values from our compact representation of Table D.3(D.2): + // Qe values and probability estimation state machine + int sv = st; + int qe = ArithmeticTable[sv & 0x7f]; + byte nl = (byte)qe; + qe >>= 8; // Next_Index_LPS + Switch_MPS + byte nm = (byte)qe; + qe >>= 8; // Next_Index_MPS + + // Decode & estimation procedures per sections D.2.4 & D.2.5 + int temp = this.a - qe; + this.a = temp; + temp <<= this.ct; + if (this.c >= temp) + { + this.c -= temp; + + // Conditional LPS (less probable symbol) exchange + if (this.a < qe) + { + this.a = qe; + st = (byte)((sv & 0x80) ^ nm); // Estimate_after_MPS + } + else + { + this.a = qe; + st = (byte)((sv & 0x80) ^ nl); // Estimate_after_LPS + sv ^= 0x80; // Exchange LPS/MPS + } + } + else if (this.a < 0x8000) + { + // Conditional MPS (more probable symbol) exchange + if (this.a < qe) + { + st = (byte)((sv & 0x80) ^ nl); // Estimate_after_LPS + sv ^= 0x80; // Exchange LPS/MPS + } + else + { + st = (byte)((sv & 0x80) ^ nm); // Estimate_after_MPS + } + } + + return sv >> 7; + } + + // The following function specifies the packing of the four components + // into the compact INT32 representation. + // Note that this formula must match the actual arithmetic encoder and decoder implementation. The implementation has to be changed + // if this formula is changed. + // The current organization is leaned on Markus Kuhn's JBIG implementation (jbig_tab.c). + [MethodImpl(InliningOptions.ShortMethod)] + private static int Pack(int a, int b, int c, int d) + => (a << 16) | (c << 8) | (d << 7) | b; + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticStatistics.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticStatistics.cs new file mode 100644 index 0000000..a5dbebf --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticStatistics.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + internal class ArithmeticStatistics + { + private readonly byte[] statistics; + + public ArithmeticStatistics(bool dc, int identifier) + { + this.IsDcStatistics = dc; + this.Identifier = identifier; + this.statistics = dc ? new byte[64] : new byte[256]; + } + + public bool IsDcStatistics { get; private set; } + + public int Identifier { get; private set; } + + public ref byte GetReference() => ref MemoryMarshal.GetArrayDataReference(this.statistics); + + public ref byte GetReference(int offset) => ref this.statistics[offset]; + + public void Reset() => this.statistics.AsSpan().Clear(); + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/ComponentProcessor.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/ComponentProcessor.cs new file mode 100644 index 0000000..4482bff --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/ComponentProcessor.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using System; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Base class for processing component spectral data and converting it to raw color data. + /// + internal abstract class ComponentProcessor : IDisposable + { + public ComponentProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, Size postProcessorBufferSize, IJpegComponent component, int blockSize) + { + this.Frame = frame; + this.Component = component; + + this.BlockAreaSize = component.SubSamplingDivisors * blockSize; + this.ColorBuffer = memoryAllocator.Allocate2DOveraligned( + postProcessorBufferSize.Width, + postProcessorBufferSize.Height, + this.BlockAreaSize.Height); + } + + protected JpegFrame Frame { get; } + + protected IJpegComponent Component { get; } + + protected Buffer2D ColorBuffer { get; } + + protected Size BlockAreaSize { get; } + + /// + /// Converts spectral data to color data accessible via . + /// + /// Spectral row index to convert. + public abstract void CopyBlocksToColorBuffer(int row); + + /// + /// Clears spectral buffers. + /// + /// + /// Should only be called during baseline interleaved decoding. + /// + public void ClearSpectralBuffers() + { + Buffer2D spectralBlocks = this.Component.SpectralBlocks; + for (int i = 0; i < spectralBlocks.Height; i++) + { + spectralBlocks.DangerousGetRowSpan(i).Clear(); + } + } + + /// + /// Gets converted color buffer row. + /// + /// Row index. + /// Color buffer row. + public Span GetColorBufferRowSpan(int row) => + this.ColorBuffer.DangerousGetRowSpan(row); + + public void Dispose() => this.ColorBuffer.Dispose(); + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs new file mode 100644 index 0000000..ebab16e --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using System; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Processes component spectral data and converts it to color data in 1-to-1 scale. + /// + internal sealed class DirectComponentProcessor : ComponentProcessor + { + private Block8x8F dequantizationTable; + + public DirectComponentProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) + : base(memoryAllocator, frame, postProcessorBufferSize, component, blockSize: 8) + { + this.dequantizationTable = rawJpeg.QuantizationTables[component.QuantizationTableIndex]; + FloatingPointDCT.AdjustToIDCT(ref this.dequantizationTable); + } + + public override void CopyBlocksToColorBuffer(int spectralStep) + { + Buffer2D spectralBuffer = this.Component.SpectralBlocks; + + float maximumValue = this.Frame.MaxColorChannelValue; + + int destAreaStride = this.ColorBuffer.Width; + + int blocksRowsPerStep = this.Component.SamplingFactors.Height; + + int yBlockStart = spectralStep * blocksRowsPerStep; + + Size subSamplingDivisors = this.Component.SubSamplingDivisors; + + Block8x8F workspaceBlock = default; + + for (int y = 0; y < blocksRowsPerStep; y++) + { + int yBuffer = y * this.BlockAreaSize.Height; + + Span colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); + Span blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); + + for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) + { + // Integer to float + workspaceBlock.LoadFrom(ref blockRow[xBlock]); + + // Dequantize + workspaceBlock.MultiplyInPlace(ref this.dequantizationTable); + + // Convert from spectral to color + FloatingPointDCT.TransformIDCT(ref workspaceBlock); + + // Normalize into the component sample range without quantizing away + // fractional precision. The later color conversion / final pack stage + // performs the only rounding we actually need for output samples. + workspaceBlock.NormalizeColorsInPlace(maximumValue); + + // Write to color buffer acording to sampling factors + int xColorBufferStart = xBlock * this.BlockAreaSize.Width; + workspaceBlock.ScaledCopyTo( + ref colorBufferRow[xColorBufferStart], + destAreaStride, + subSamplingDivisors.Width, + subSamplingDivisors.Height); + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor2.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor2.cs new file mode 100644 index 0000000..d6c58a5 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor2.cs @@ -0,0 +1,404 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Processes component spectral data and converts it to color data in 2-to-1 scale. + /// + internal sealed class DownScalingComponentProcessor2 : ComponentProcessor + { + private Block8x8F dequantizationTable; + + public DownScalingComponentProcessor2(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) + : base(memoryAllocator, frame, postProcessorBufferSize, component, 4) + { + this.dequantizationTable = rawJpeg.QuantizationTables[component.QuantizationTableIndex]; + ScaledFloatingPointDCT.AdjustToIDCT(ref this.dequantizationTable); + } + + public override void CopyBlocksToColorBuffer(int spectralStep) + { + Buffer2D spectralBuffer = this.Component.SpectralBlocks; + + float maximumValue = this.Frame.MaxColorChannelValue; + float normalizationValue = MathF.Ceiling(maximumValue * 0.5F); + + int destAreaStride = this.ColorBuffer.Width; + + int blocksRowsPerStep = this.Component.SamplingFactors.Height; + Size subSamplingDivisors = this.Component.SubSamplingDivisors; + + Block8x8F workspaceBlock = default; + + int yBlockStart = spectralStep * blocksRowsPerStep; + + for (int y = 0; y < blocksRowsPerStep; y++) + { + int yBuffer = y * this.BlockAreaSize.Height; + + Span colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); + Span blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); + + for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) + { + // Integer to float + workspaceBlock.LoadFrom(ref blockRow[xBlock]); + + // IDCT/Normalization/Range + ScaledFloatingPointDCT.TransformIDCT_4x4(ref workspaceBlock, ref this.dequantizationTable, normalizationValue, maximumValue); + + // Save to the intermediate buffer + int xColorBufferStart = xBlock * this.BlockAreaSize.Width; + ScaledCopyTo( + ref workspaceBlock, + ref colorBufferRow[xColorBufferStart], + destAreaStride, + subSamplingDivisors.Width, + subSamplingDivisors.Height); + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void ScaledCopyTo(ref Block8x8F block, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale) + { + if (horizontalScale == 1 && verticalScale == 1) + { + CopyTo1x1Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 2 && verticalScale == 2) + { + CopyTo2x2Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 2 && verticalScale == 1) + { + CopyTo2x1Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 1 && verticalScale == 2) + { + CopyTo1x2Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 4 && verticalScale == 1) + { + CopyTo4x1Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 4 && verticalScale == 2) + { + CopyTo4x2Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 1 && verticalScale == 4) + { + CopyTo1x4Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 2 && verticalScale == 4) + { + CopyTo2x4Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 4 && verticalScale == 4) + { + CopyTo4x4Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + // The common 1x, 2x, and 4x integral scales are specialized above. + // Uncommon legal factor-3 scales use the generic fallback. + CopyArbitraryScale(ref block, ref destRef, (uint)destStrideWidth, (uint)horizontalScale, (uint)verticalScale); + } + + /// + /// Copies a 4x4 reduced block directly into the destination buffer when no chroma expansion is needed. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo1x1Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + CopyRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 2u, 2u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 3u, 3u, areaStride); + } + + /// + /// Copies a 4x4 reduced block into the destination buffer while doubling only the horizontal axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x1Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + WidenRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 2u, 2u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 3u, 3u, areaStride); + } + + /// + /// Copies a 4x4 reduced block into the destination buffer while doubling only the vertical axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo1x2Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + CopyRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 2u, 4u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 2u, 5u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 3u, 6u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 3u, 7u, areaStride); + } + + /// + /// Copies a 4x4 reduced block into the destination buffer while doubling both axes. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x2Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + WidenRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 2u, 4u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 2u, 5u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 3u, 6u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 3u, 7u, areaStride); + } + + /// + /// Copies a 4x4 reduced block into the destination buffer while quadrupling only the horizontal axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x1Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + ExpandRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 2u, 2u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 3u, 3u, areaStride); + } + + /// + /// Copies a 4x4 reduced block into the destination buffer while quadrupling horizontally and doubling vertically. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x2Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + ExpandRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 2u, 4u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 2u, 5u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 3u, 6u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 3u, 7u, areaStride); + } + + /// + /// Copies a 4x4 reduced block into the destination buffer while quadrupling only the vertical axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo1x4Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + CopyRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 2u, 8u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 2u, 9u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 2u, 10u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 2u, 11u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 3u, 12u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 3u, 13u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 3u, 14u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 3u, 15u, areaStride); + } + + /// + /// Copies a 4x4 reduced block into the destination buffer while doubling horizontally and quadrupling vertically. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x4Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + WidenRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 2u, 8u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 2u, 9u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 2u, 10u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 2u, 11u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 3u, 12u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 3u, 13u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 3u, 14u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 3u, 15u, areaStride); + } + + /// + /// Copies a 4x4 reduced block into the destination buffer while quadrupling both axes. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x4Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + ExpandRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 2u, 8u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 2u, 9u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 2u, 10u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 2u, 11u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 3u, 12u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 3u, 13u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 3u, 14u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 3u, 15u, areaStride); + } + + /// + /// Copies one four-sample row from the reduced block to the destination row. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyRow4(ref float sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref float source = ref Unsafe.Add(ref sourceBase, sourceRow * 8u); + ref float dest = ref Unsafe.Add(ref areaOrigin, destRow * areaStride); + + Unsafe.CopyBlock( + ref Unsafe.As(ref dest), + ref Unsafe.As(ref source), + 4u * sizeof(float)); + } + + /// + /// Expands one four-sample row to eight samples by duplicating each source value horizontally. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void WidenRow4(ref float sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref float source = ref Unsafe.Add(ref sourceBase, sourceRow * 8u); + ref float dest = ref Unsafe.Add(ref areaOrigin, destRow * areaStride); + + float value0 = source; + float value1 = Unsafe.Add(ref source, 1u); + float value2 = Unsafe.Add(ref source, 2u); + float value3 = Unsafe.Add(ref source, 3u); + + dest = value0; + Unsafe.Add(ref dest, 1u) = value0; + Unsafe.Add(ref dest, 2u) = value1; + Unsafe.Add(ref dest, 3u) = value1; + Unsafe.Add(ref dest, 4u) = value2; + Unsafe.Add(ref dest, 5u) = value2; + Unsafe.Add(ref dest, 6u) = value3; + Unsafe.Add(ref dest, 7u) = value3; + } + + /// + /// Expands one four-sample row to sixteen samples by duplicating each source value four times horizontally. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void ExpandRow4(ref float sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref float source = ref Unsafe.Add(ref sourceBase, sourceRow * 8u); + ref float dest = ref Unsafe.Add(ref areaOrigin, destRow * areaStride); + + float value0 = source; + float value1 = Unsafe.Add(ref source, 1u); + float value2 = Unsafe.Add(ref source, 2u); + float value3 = Unsafe.Add(ref source, 3u); + + dest = value0; + Unsafe.Add(ref dest, 1u) = value0; + Unsafe.Add(ref dest, 2u) = value0; + Unsafe.Add(ref dest, 3u) = value0; + Unsafe.Add(ref dest, 4u) = value1; + Unsafe.Add(ref dest, 5u) = value1; + Unsafe.Add(ref dest, 6u) = value1; + Unsafe.Add(ref dest, 7u) = value1; + Unsafe.Add(ref dest, 8u) = value2; + Unsafe.Add(ref dest, 9u) = value2; + Unsafe.Add(ref dest, 10u) = value2; + Unsafe.Add(ref dest, 11u) = value2; + Unsafe.Add(ref dest, 12u) = value3; + Unsafe.Add(ref dest, 13u) = value3; + Unsafe.Add(ref dest, 14u) = value3; + Unsafe.Add(ref dest, 15u) = value3; + } + + /// + /// Replicates each reduced sample into an arbitrary integral expansion rectangle for uncommon subsampling ratios. + /// + [MethodImpl(InliningOptions.ColdPath)] + private static void CopyArbitraryScale(ref Block8x8F block, ref float areaOrigin, uint areaStride, uint horizontalScale, uint verticalScale) + { + for (nuint y = 0u; y < 4u; y++) + { + nuint yy = y * verticalScale; + nuint y8 = y * 8u; + + for (nuint x = 0u; x < 4u; x++) + { + nuint xx = x * horizontalScale; + + float value = block[y8 + x]; + + for (nuint i = 0u; i < verticalScale; i++) + { + nuint baseIdx = ((yy + i) * areaStride) + xx; + + for (nuint j = 0u; j < horizontalScale; j++) + { + // area[xx + j, yy + i] = value; + Unsafe.Add(ref areaOrigin, baseIdx + j) = value; + } + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor4.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor4.cs new file mode 100644 index 0000000..56fab91 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor4.cs @@ -0,0 +1,346 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Processes component spectral data and converts it to color data in 4-to-1 scale. + /// + internal sealed class DownScalingComponentProcessor4 : ComponentProcessor + { + private Block8x8F dequantizationTable; + + public DownScalingComponentProcessor4(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) + : base(memoryAllocator, frame, postProcessorBufferSize, component, 2) + { + this.dequantizationTable = rawJpeg.QuantizationTables[component.QuantizationTableIndex]; + ScaledFloatingPointDCT.AdjustToIDCT(ref this.dequantizationTable); + } + + public override void CopyBlocksToColorBuffer(int spectralStep) + { + Buffer2D spectralBuffer = this.Component.SpectralBlocks; + + float maximumValue = this.Frame.MaxColorChannelValue; + float normalizationValue = MathF.Ceiling(maximumValue * 0.5F); + + int destAreaStride = this.ColorBuffer.Width; + + int blocksRowsPerStep = this.Component.SamplingFactors.Height; + Size subSamplingDivisors = this.Component.SubSamplingDivisors; + + Block8x8F workspaceBlock = default; + + int yBlockStart = spectralStep * blocksRowsPerStep; + + for (int y = 0; y < blocksRowsPerStep; y++) + { + int yBuffer = y * this.BlockAreaSize.Height; + + Span colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); + Span blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); + + for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) + { + // Integer to float + workspaceBlock.LoadFrom(ref blockRow[xBlock]); + + // IDCT/Normalization/Range + ScaledFloatingPointDCT.TransformIDCT_2x2(ref workspaceBlock, ref this.dequantizationTable, normalizationValue, maximumValue); + + // Save to the intermediate buffer + int xColorBufferStart = xBlock * this.BlockAreaSize.Width; + ScaledCopyTo( + ref workspaceBlock, + ref colorBufferRow[xColorBufferStart], + destAreaStride, + subSamplingDivisors.Width, + subSamplingDivisors.Height); + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void ScaledCopyTo(ref Block8x8F block, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale) + { + if (horizontalScale == 1 && verticalScale == 1) + { + CopyTo1x1Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 2 && verticalScale == 2) + { + CopyTo2x2Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 2 && verticalScale == 1) + { + CopyTo2x1Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 1 && verticalScale == 2) + { + CopyTo1x2Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 4 && verticalScale == 1) + { + CopyTo4x1Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 4 && verticalScale == 2) + { + CopyTo4x2Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 1 && verticalScale == 4) + { + CopyTo1x4Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 2 && verticalScale == 4) + { + CopyTo2x4Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 4 && verticalScale == 4) + { + CopyTo4x4Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + // The common 1x, 2x, and 4x integral scales are specialized above. + // Uncommon legal factor-3 scales use the generic fallback. + CopyArbitraryScale(ref block, ref destRef, (uint)destStrideWidth, (uint)horizontalScale, (uint)verticalScale); + } + + /// + /// Copies a 2x2 reduced block directly into the destination buffer when no chroma expansion is needed. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo1x1Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + CopyRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride); + } + + /// + /// Copies a 2x2 reduced block into the destination buffer while doubling only the horizontal axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x1Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + WidenRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride); + } + + /// + /// Copies a 2x2 reduced block into the destination buffer while doubling only the vertical axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo1x2Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + CopyRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride); + } + + /// + /// Copies a 2x2 reduced block into the destination buffer while doubling both axes. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x2Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + WidenRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride); + } + + /// + /// Copies a 2x2 reduced block into the destination buffer while quadrupling only the horizontal axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x1Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + ExpandRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride); + } + + /// + /// Copies a 2x2 reduced block into the destination buffer while quadrupling horizontally and doubling vertically. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x2Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + ExpandRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride); + } + + /// + /// Copies a 2x2 reduced block into the destination buffer while quadrupling only the vertical axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo1x4Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + CopyRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + } + + /// + /// Copies a 2x2 reduced block into the destination buffer while doubling horizontally and quadrupling vertically. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x4Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + WidenRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + } + + /// + /// Copies a 2x2 reduced block into the destination buffer while quadrupling both axes. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x4Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + ExpandRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + } + + /// + /// Copies one two-sample row from the reduced block to the destination row. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyRow2(ref float sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref float source = ref Unsafe.Add(ref sourceBase, sourceRow * 8u); + ref float dest = ref Unsafe.Add(ref areaOrigin, destRow * areaStride); + + Unsafe.CopyBlock( + ref Unsafe.As(ref dest), + ref Unsafe.As(ref source), + 2u * sizeof(float)); + } + + /// + /// Expands one two-sample row to four samples by duplicating each source value horizontally. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void WidenRow2(ref float sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref float source = ref Unsafe.Add(ref sourceBase, sourceRow * 8u); + ref float dest = ref Unsafe.Add(ref areaOrigin, destRow * areaStride); + + float value0 = source; + float value1 = Unsafe.Add(ref source, 1u); + + dest = value0; + Unsafe.Add(ref dest, 1u) = value0; + Unsafe.Add(ref dest, 2u) = value1; + Unsafe.Add(ref dest, 3u) = value1; + } + + /// + /// Expands one two-sample row to eight samples by duplicating each source value four times horizontally. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void ExpandRow2(ref float sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref float source = ref Unsafe.Add(ref sourceBase, sourceRow * 8u); + ref float dest = ref Unsafe.Add(ref areaOrigin, destRow * areaStride); + + float value0 = source; + float value1 = Unsafe.Add(ref source, 1u); + + dest = value0; + Unsafe.Add(ref dest, 1u) = value0; + Unsafe.Add(ref dest, 2u) = value0; + Unsafe.Add(ref dest, 3u) = value0; + Unsafe.Add(ref dest, 4u) = value1; + Unsafe.Add(ref dest, 5u) = value1; + Unsafe.Add(ref dest, 6u) = value1; + Unsafe.Add(ref dest, 7u) = value1; + } + + /// + /// Replicates each reduced sample into an arbitrary integral expansion rectangle for uncommon subsampling ratios. + /// + [MethodImpl(InliningOptions.ColdPath)] + private static void CopyArbitraryScale(ref Block8x8F block, ref float areaOrigin, uint areaStride, uint horizontalScale, uint verticalScale) + { + for (nuint y = 0u; y < 2u; y++) + { + nuint yy = y * verticalScale; + nuint y8 = y * 8u; + + for (nuint x = 0u; x < 2u; x++) + { + nuint xx = x * horizontalScale; + + float value = block[y8 + x]; + + for (nuint i = 0u; i < verticalScale; i++) + { + nuint baseIdx = ((yy + i) * areaStride) + xx; + + for (nuint j = 0u; j < horizontalScale; j++) + { + // area[xx + j, yy + i] = value; + Unsafe.Add(ref areaOrigin, baseIdx + j) = value; + } + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs new file mode 100644 index 0000000..224ebea --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs @@ -0,0 +1,241 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Processes component spectral data and converts it to color data in 8-to-1 scale. + /// + internal sealed class DownScalingComponentProcessor8 : ComponentProcessor + { + private readonly float dcDequantizatizer; + + public DownScalingComponentProcessor8(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) + : base(memoryAllocator, frame, postProcessorBufferSize, component, 1) + => this.dcDequantizatizer = 0.125f * rawJpeg.QuantizationTables[component.QuantizationTableIndex][0]; + + public override void CopyBlocksToColorBuffer(int spectralStep) + { + Buffer2D spectralBuffer = this.Component.SpectralBlocks; + + float maximumValue = this.Frame.MaxColorChannelValue; + float normalizationValue = MathF.Ceiling(maximumValue * 0.5F); + + int destAreaStride = this.ColorBuffer.Width; + + int blocksRowsPerStep = this.Component.SamplingFactors.Height; + Size subSamplingDivisors = this.Component.SubSamplingDivisors; + + int yBlockStart = spectralStep * blocksRowsPerStep; + + for (int y = 0; y < blocksRowsPerStep; y++) + { + int yBuffer = y * this.BlockAreaSize.Height; + + Span colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); + Span blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); + + for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) + { + float dc = ScaledFloatingPointDCT.TransformIDCT_1x1(blockRow[xBlock][0], this.dcDequantizatizer, normalizationValue, maximumValue); + + // Save to the intermediate buffer + int xColorBufferStart = xBlock * this.BlockAreaSize.Width; + ScaledCopyTo( + dc, + ref colorBufferRow[xColorBufferStart], + destAreaStride, + subSamplingDivisors.Width, + subSamplingDivisors.Height); + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void ScaledCopyTo(float value, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale) + { + if (horizontalScale == 1 && verticalScale == 1) + { + destRef = value; + return; + } + + if (horizontalScale == 2 && verticalScale == 1) + { + CopyTo2x1Scale(value, ref destRef); + return; + } + + if (horizontalScale == 1 && verticalScale == 2) + { + CopyTo1x2Scale(value, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 2 && verticalScale == 2) + { + CopyTo2x2Scale(value, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 4 && verticalScale == 1) + { + CopyTo4x1Scale(value, ref destRef); + return; + } + + if (horizontalScale == 4 && verticalScale == 2) + { + CopyTo4x2Scale(value, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 1 && verticalScale == 4) + { + CopyTo1x4Scale(value, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 2 && verticalScale == 4) + { + CopyTo2x4Scale(value, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 4 && verticalScale == 4) + { + CopyTo4x4Scale(value, ref destRef, (uint)destStrideWidth); + return; + } + + // The common 1x, 2x, and 4x integral scales are specialized above. + // Uncommon legal factor-3 scales use the generic fallback. + CopyArbitraryScale(value, ref destRef, destStrideWidth, horizontalScale, verticalScale); + } + + [MethodImpl(InliningOptions.ColdPath)] + private static float CopyArbitraryScale(float value, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale) + { + // The common 1x, 2x, and 4x integral scales are specialized above. + // Uncommon legal factor-3 scales use the generic fallback. + for (nuint y = 0; y < (uint)verticalScale; y++) + { + for (nuint x = 0; x < (uint)horizontalScale; x++) + { + Unsafe.Add(ref destRef, x) = value; + } + + destRef = ref Unsafe.Add(ref destRef, (uint)destStrideWidth); + } + + return destRef; + } + + /// + /// Writes a single source value to two horizontally adjacent samples. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x1Scale(float value, ref float areaOrigin) + { + areaOrigin = value; + Unsafe.Add(ref areaOrigin, 1u) = value; + } + + /// + /// Writes a single source value to two vertically adjacent samples. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo1x2Scale(float value, ref float areaOrigin, uint areaStride) + { + areaOrigin = value; + Unsafe.Add(ref areaOrigin, areaStride) = value; + } + + /// + /// Writes a single source value to a 2x2 rectangle. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x2Scale(float value, ref float areaOrigin, uint areaStride) + { + areaOrigin = value; + Unsafe.Add(ref areaOrigin, 1u) = value; + Unsafe.Add(ref areaOrigin, areaStride) = value; + Unsafe.Add(ref areaOrigin, areaStride + 1u) = value; + } + + /// + /// Writes a single source value to four horizontally adjacent samples. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x1Scale(float value, ref float areaOrigin) + { + areaOrigin = value; + Unsafe.Add(ref areaOrigin, 1u) = value; + Unsafe.Add(ref areaOrigin, 2u) = value; + Unsafe.Add(ref areaOrigin, 3u) = value; + } + + /// + /// Writes a single source value to a 4x2 rectangle. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x2Scale(float value, ref float areaOrigin, uint areaStride) + { + CopyTo4x1Scale(value, ref areaOrigin); + + ref float nextRow = ref Unsafe.Add(ref areaOrigin, areaStride); + CopyTo4x1Scale(value, ref nextRow); + } + + /// + /// Writes a single source value to four vertically adjacent samples. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo1x4Scale(float value, ref float areaOrigin, uint areaStride) + { + areaOrigin = value; + Unsafe.Add(ref areaOrigin, areaStride) = value; + Unsafe.Add(ref areaOrigin, areaStride * 2u) = value; + Unsafe.Add(ref areaOrigin, areaStride * 3u) = value; + } + + /// + /// Writes a single source value to a 2x4 rectangle. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x4Scale(float value, ref float areaOrigin, uint areaStride) + { + CopyTo2x1Scale(value, ref areaOrigin); + + ref float row1 = ref Unsafe.Add(ref areaOrigin, areaStride); + CopyTo2x1Scale(value, ref row1); + + ref float row2 = ref Unsafe.Add(ref areaOrigin, areaStride * 2u); + CopyTo2x1Scale(value, ref row2); + + ref float row3 = ref Unsafe.Add(ref areaOrigin, areaStride * 3u); + CopyTo2x1Scale(value, ref row3); + } + + /// + /// Writes a single source value to a 4x4 rectangle. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x4Scale(float value, ref float areaOrigin, uint areaStride) + { + CopyTo4x1Scale(value, ref areaOrigin); + + ref float row1 = ref Unsafe.Add(ref areaOrigin, areaStride); + CopyTo4x1Scale(value, ref row1); + + ref float row2 = ref Unsafe.Add(ref areaOrigin, areaStride * 2u); + CopyTo4x1Scale(value, ref row2); + + ref float row3 = ref Unsafe.Add(ref areaOrigin, areaStride * 3u); + CopyTo4x1Scale(value, ref row3); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs new file mode 100644 index 0000000..d0036d9 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -0,0 +1,791 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Decodes the Huffman encoded spectral scan. + /// Originally ported from + /// with additional fixes for both performance and common encoding errors. + /// + internal class HuffmanScanDecoder : IJpegScanDecoder + { + private readonly BufferedReadStream stream; + + /// + /// instance containing decoding-related information. + /// + private JpegFrame frame; + + /// + /// Shortcut for .Components. + /// + private IJpegComponent[] components; + + /// + /// Number of component in the current scan. + /// + private int scanComponentCount; + + /// + /// The reset interval determined by RST markers. + /// + private int restartInterval; + + /// + /// How many mcu's are left to do. + /// + private int todo; + + /// + /// The End-Of-Block countdown for ending the sequence prematurely when the remaining coefficients are zero. + /// + private int eobrun; + + /// + /// The DC Huffman tables. + /// + private readonly HuffmanTable[] dcHuffmanTables; + + /// + /// The AC Huffman tables. + /// + private readonly HuffmanTable[] acHuffmanTables; + + private JpegBitReader scanBuffer; + + private readonly SpectralConverter spectralConverter; + + private readonly CancellationToken cancellationToken; + + /// + /// Initializes a new instance of the class. + /// + /// The input stream. + /// Spectral to pixel converter. + /// The token to monitor cancellation. + public HuffmanScanDecoder( + BufferedReadStream stream, + SpectralConverter converter, + CancellationToken cancellationToken) + { + this.stream = stream; + this.spectralConverter = converter; + this.cancellationToken = cancellationToken; + + // TODO: this is actually a variable value depending on component count + const int maxTables = 4; + this.dcHuffmanTables = new HuffmanTable[maxTables]; + this.acHuffmanTables = new HuffmanTable[maxTables]; + } + + /// + /// Sets reset interval determined by RST markers. + /// + public int ResetInterval + { + set + { + this.restartInterval = value; + this.todo = value; + } + } + + // The spectral selection start. + public int SpectralStart { get; set; } + + // The spectral selection end. + public int SpectralEnd { get; set; } + + // The successive approximation high bit end. + public int SuccessiveHigh { get; set; } + + // The successive approximation low bit end. + public int SuccessiveLow { get; set; } + + /// + public void ParseEntropyCodedData(int scanComponentCount, IccProfile iccProfile) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + this.scanComponentCount = scanComponentCount; + + this.scanBuffer = new JpegBitReader(this.stream); + + this.frame.AllocateComponents(); + + this.todo = this.restartInterval; + + if (!this.frame.Progressive) + { + this.ParseBaselineData(iccProfile); + } + else + { + this.ParseProgressiveData(); + } + + if (this.scanBuffer.HasBadMarker()) + { + this.stream.Position = this.scanBuffer.MarkerPosition; + } + } + + /// + public void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) + { + this.frame = frame; + this.components = frame.Components; + + this.spectralConverter.InjectFrameData(frame, jpegData); + } + + private void ParseBaselineData(IccProfile iccProfile) + { + if (this.scanComponentCount != 1) + { + this.spectralConverter.PrepareForDecoding(); + this.ParseBaselineDataInterleaved(iccProfile); + this.spectralConverter.CommitConversion(); + } + else if (this.frame.ComponentCount == 1) + { + this.spectralConverter.PrepareForDecoding(); + this.ParseBaselineDataSingleComponent(iccProfile); + this.spectralConverter.CommitConversion(); + } + else + { + this.ParseBaselineDataNonInterleaved(); + } + } + + private void ParseBaselineDataInterleaved(IccProfile iccProfile) + { + int mcu = 0; + int mcusPerColumn = this.frame.McusPerColumn; + int mcusPerLine = this.frame.McusPerLine; + ref JpegBitReader buffer = ref this.scanBuffer; + + for (int j = 0; j < mcusPerColumn; j++) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + // decode from binary to spectral + for (int i = 0; i < mcusPerLine; i++) + { + // Scan an interleaved mcu... process components in order + int mcuCol = mcu % mcusPerLine; + for (int k = 0; k < this.scanComponentCount; k++) + { + int order = this.frame.ComponentOrder[k]; + JpegComponent component = this.components[order] as JpegComponent; + + ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; + ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; + + int h = component.HorizontalSamplingFactor; + int v = component.VerticalSamplingFactor; + + // Scan out an mcu's worth of this component; that's just determined + // by the basic H and V specified for the component + for (int y = 0; y < v; y++) + { + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int x = 0; x < h; x++) + { + if (buffer.NoData) + { + // It is very likely that some spectral data was decoded before we've encountered 'end of scan' + // so we need to decode what's left and return (or maybe throw?) + this.spectralConverter.ConvertStrideBaseline(iccProfile); + return; + } + + int blockCol = (mcuCol * h) + x; + + this.DecodeBlockBaseline( + component, + ref Unsafe.Add(ref blockRef, (uint)blockCol), + ref dcHuffmanTable, + ref acHuffmanTable); + } + } + } + + // After all interleaved components, that's an interleaved MCU, + // so now count down the restart interval + mcu++; + this.HandleRestart(); + } + + // Convert from spectral to actual pixels via given converter + this.spectralConverter.ConvertStrideBaseline(iccProfile); + } + } + + private void ParseBaselineDataNonInterleaved() + { + JpegComponent component = this.components[this.frame.ComponentOrder[0]] as JpegComponent; + ref JpegBitReader buffer = ref this.scanBuffer; + + int w = component.WidthInBlocks; + int h = component.HeightInBlocks; + + ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; + ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; + + for (int j = 0; j < h; j++) + { + this.cancellationToken.ThrowIfCancellationRequested(); + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int i = 0; i < w; i++) + { + if (buffer.NoData) + { + return; + } + + this.DecodeBlockBaseline( + component, + ref Unsafe.Add(ref blockRef, (uint)i), + ref dcHuffmanTable, + ref acHuffmanTable); + + this.HandleRestart(); + } + } + } + + private void ParseBaselineDataSingleComponent(IccProfile iccProfile) + { + JpegComponent component = this.frame.Components[0]; + int mcuLines = this.frame.McusPerColumn; + int w = component.WidthInBlocks; + int h = component.SamplingFactors.Height; + ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; + ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; + + ref JpegBitReader buffer = ref this.scanBuffer; + + for (int i = 0; i < mcuLines; i++) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + // decode from binary to spectral + for (int j = 0; j < h; j++) + { + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int k = 0; k < w; k++) + { + if (buffer.NoData) + { + // It is very likely that some spectral data was decoded before we've encountered 'end of scan' + // so we need to decode what's left and return (or maybe throw?) + this.spectralConverter.ConvertStrideBaseline(iccProfile); + return; + } + + this.DecodeBlockBaseline( + component, + ref Unsafe.Add(ref blockRef, (uint)k), + ref dcHuffmanTable, + ref acHuffmanTable); + + this.HandleRestart(); + } + } + + // Convert from spectral to actual pixels via given converter + this.spectralConverter.ConvertStrideBaseline(iccProfile); + } + } + + private void CheckProgressiveData() + { + // Validate successive scan parameters. + // Logic has been adapted from libjpeg. + // See Table B.3 – Scan header parameter size and values. itu-t81.pdf + bool invalid = false; + if (this.SpectralStart == 0) + { + if (this.SpectralEnd != 0) + { + invalid = true; + } + } + else + { + // Need not check Ss/Se < 0 since they came from unsigned bytes. + if (this.SpectralEnd < this.SpectralStart || this.SpectralEnd > 63) + { + invalid = true; + } + + // AC scans may have only one component. + if (this.scanComponentCount != 1) + { + invalid = true; + } + } + + if (this.SuccessiveHigh != 0) + { + // Successive approximation refinement scan: must have Al = Ah-1. + if (this.SuccessiveHigh - 1 != this.SuccessiveLow) + { + invalid = true; + } + } + + // TODO: How does this affect 12bit jpegs. + // According to libjpeg the range covers 8bit only? + if (this.SuccessiveLow > 13) + { + invalid = true; + } + + if (invalid) + { + JpegThrowHelper.ThrowBadProgressiveScan(this.SpectralStart, this.SpectralEnd, this.SuccessiveHigh, this.SuccessiveLow); + } + } + + private void ParseProgressiveData() + { + this.CheckProgressiveData(); + + if (this.scanComponentCount == 1) + { + this.ParseProgressiveDataNonInterleaved(); + } + else + { + this.ParseProgressiveDataInterleaved(); + } + } + + private void ParseProgressiveDataInterleaved() + { + // Interleaved + int mcu = 0; + int mcusPerColumn = this.frame.McusPerColumn; + int mcusPerLine = this.frame.McusPerLine; + ref JpegBitReader buffer = ref this.scanBuffer; + + for (int j = 0; j < mcusPerColumn; j++) + { + for (int i = 0; i < mcusPerLine; i++) + { + // Scan an interleaved mcu... process components in order + int mcuRow = mcu / mcusPerLine; + int mcuCol = mcu % mcusPerLine; + for (int k = 0; k < this.scanComponentCount; k++) + { + int order = this.frame.ComponentOrder[k]; + JpegComponent component = this.components[order] as JpegComponent; + ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; + + int h = component.HorizontalSamplingFactor; + int v = component.VerticalSamplingFactor; + + // Scan out an mcu's worth of this component; that's just determined + // by the basic H and V specified for the component + for (int y = 0; y < v; y++) + { + int blockRow = (mcuRow * v) + y; + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(blockRow); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int x = 0; x < h; x++) + { + if (buffer.NoData) + { + return; + } + + int blockCol = (mcuCol * h) + x; + + this.DecodeBlockProgressiveDC( + component, + ref Unsafe.Add(ref blockRef, (uint)blockCol), + ref dcHuffmanTable); + } + } + } + + // After all interleaved components, that's an interleaved MCU, + // so now count down the restart interval + mcu++; + this.HandleRestart(); + } + } + } + + private void ParseProgressiveDataNonInterleaved() + { + JpegComponent component = this.components[this.frame.ComponentOrder[0]] as JpegComponent; + ref JpegBitReader buffer = ref this.scanBuffer; + + int w = component.WidthInBlocks; + int h = component.HeightInBlocks; + + if (this.SpectralStart == 0) + { + ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; + + for (int j = 0; j < h; j++) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int i = 0; i < w; i++) + { + if (buffer.NoData) + { + return; + } + + this.DecodeBlockProgressiveDC( + component, + ref Unsafe.Add(ref blockRef, (uint)i), + ref dcHuffmanTable); + + this.HandleRestart(); + } + } + } + else + { + ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; + + for (int j = 0; j < h; j++) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int i = 0; i < w; i++) + { + if (buffer.NoData) + { + return; + } + + this.DecodeBlockProgressiveAC( + ref Unsafe.Add(ref blockRef, (uint)i), + ref acHuffmanTable); + + this.HandleRestart(); + } + } + } + } + + private void DecodeBlockBaseline( + JpegComponent component, + ref Block8x8 block, + ref HuffmanTable dcTable, + ref HuffmanTable acTable) + { + ref short blockDataRef = ref Unsafe.As(ref block); + ref JpegBitReader buffer = ref this.scanBuffer; + + // DC + int t = buffer.DecodeHuffman(ref dcTable); + if (t != 0) + { + t = buffer.Receive(t); + } + + t += component.DcPredictor; + component.DcPredictor = t; + blockDataRef = (short)t; + + // AC + for (int i = 1; i < 64;) + { + int s = buffer.DecodeHuffman(ref acTable); + + int r = s >> 4; + s &= 15; + + if (s != 0) + { + i += r; + s = buffer.Receive(s); + Unsafe.Add(ref blockDataRef, ZigZag.TransposingOrder[i++]) = (short)s; + } + else + { + if (r == 0) + { + break; + } + + i += 16; + } + } + } + + private void DecodeBlockProgressiveDC(JpegComponent component, ref Block8x8 block, ref HuffmanTable dcTable) + { + ref short blockDataRef = ref Unsafe.As(ref block); + ref JpegBitReader buffer = ref this.scanBuffer; + + if (this.SuccessiveHigh == 0) + { + // First scan for DC coefficient, must be first + int s = buffer.DecodeHuffman(ref dcTable); + if (s != 0) + { + s = buffer.Receive(s); + } + + s += component.DcPredictor; + component.DcPredictor = s; + blockDataRef = (short)(s << this.SuccessiveLow); + } + else + { + // Refinement scan for DC coefficient + buffer.CheckBits(); + blockDataRef |= (short)(buffer.GetBits(1) << this.SuccessiveLow); + } + } + + private void DecodeBlockProgressiveAC(ref Block8x8 block, ref HuffmanTable acTable) + { + ref short blockDataRef = ref Unsafe.As(ref block); + if (this.SuccessiveHigh == 0) + { + // MCU decoding for AC initial scan (either spectral selection, + // or first pass of successive approximation). + if (this.eobrun != 0) + { + --this.eobrun; + return; + } + + ref JpegBitReader buffer = ref this.scanBuffer; + int start = this.SpectralStart; + int end = this.SpectralEnd; + int low = this.SuccessiveLow; + + for (int i = start; i <= end; ++i) + { + int s = buffer.DecodeHuffman(ref acTable); + int r = s >> 4; + s &= 15; + + i += r; + + if (s != 0) + { + s = buffer.Receive(s); + Unsafe.Add(ref blockDataRef, ZigZag.TransposingOrder[i]) = (short)(s << low); + } + else + { + if (r != 15) + { + this.eobrun = 1 << r; + if (r != 0) + { + buffer.CheckBits(); + this.eobrun += buffer.GetBits(r); + } + + --this.eobrun; + break; + } + } + } + } + else + { + // Refinement scan for these AC coefficients + this.DecodeBlockProgressiveACRefined(ref blockDataRef, ref acTable); + } + } + + private void DecodeBlockProgressiveACRefined(ref short blockDataRef, ref HuffmanTable acTable) + { + // Refinement scan for these AC coefficients + ref JpegBitReader buffer = ref this.scanBuffer; + int start = this.SpectralStart; + int end = this.SpectralEnd; + + int p1 = 1 << this.SuccessiveLow; + int m1 = (-1) << this.SuccessiveLow; + + int k = start; + + if (this.eobrun == 0) + { + for (; k <= end; k++) + { + int s = buffer.DecodeHuffman(ref acTable); + int r = s >> 4; + s &= 15; + + if (s != 0) + { + buffer.CheckBits(); + if (buffer.GetBits(1) != 0) + { + s = p1; + } + else + { + s = m1; + } + } + else + { + if (r != 15) + { + this.eobrun = 1 << r; + + if (r != 0) + { + buffer.CheckBits(); + this.eobrun += buffer.GetBits(r); + } + + break; + } + } + + do + { + ref short coef = ref Unsafe.Add(ref blockDataRef, ZigZag.TransposingOrder[k]); + if (coef != 0) + { + buffer.CheckBits(); + if (buffer.GetBits(1) != 0) + { + if ((coef & p1) == 0) + { + coef += (short)(coef >= 0 ? p1 : m1); + } + } + } + else + { + if (--r < 0) + { + break; + } + } + + k++; + } + while (k <= end); + + if ((s != 0) && (k < 64)) + { + Unsafe.Add(ref blockDataRef, ZigZag.TransposingOrder[k]) = (short)s; + } + } + } + + if (this.eobrun > 0) + { + for (; k <= end; k++) + { + ref short coef = ref Unsafe.Add(ref blockDataRef, ZigZag.TransposingOrder[k]); + + if (coef != 0) + { + buffer.CheckBits(); + if (buffer.GetBits(1) != 0) + { + if ((coef & p1) == 0) + { + coef += (short)(coef >= 0 ? p1 : m1); + } + } + } + } + + --this.eobrun; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private void Reset() + { + for (int i = 0; i < this.components.Length; i++) + { + this.components[i].DcPredictor = 0; + } + + this.eobrun = 0; + this.scanBuffer.Reset(); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private bool HandleRestart() + { + if (this.restartInterval > 0 && (--this.todo) == 0) + { + if (this.scanBuffer.Marker == JpegConstants.Markers.XFF) + { + if (!this.scanBuffer.FindNextMarker()) + { + return false; + } + } + + this.todo = this.restartInterval; + + if (this.scanBuffer.HasRestartMarker()) + { + this.Reset(); + return true; + } + + if (this.scanBuffer.HasBadMarker()) + { + this.stream.Position = this.scanBuffer.MarkerPosition; + this.Reset(); + return true; + } + } + + return false; + } + + /// + /// Build the Huffman table using code lengths and code values. + /// + /// Table type. + /// 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, Span workspace) + { + HuffmanTable[] tables = type == 0 ? this.dcHuffmanTables : this.acHuffmanTables; + tables[index] = new HuffmanTable(codeLengths, values, workspace); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs new file mode 100644 index 0000000..299be37 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs @@ -0,0 +1,141 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Represents a Huffman coding table containing basic coding data plus tables for accelerated computation. + /// + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct HuffmanTable + { + /// + /// Memory workspace buffer size used in ctor. + /// + public const int WorkspaceByteSize = 256 * sizeof(uint); + + /// + /// Derived from the DHT marker. Contains the symbols, in order of incremental code length. + /// + public fixed byte Values[256]; + + /// + /// Contains the largest code of length k (0 if none). MaxCode[17] is a sentinel to + /// ensure terminates. + /// + public fixed ulong MaxCode[18]; + + /// + /// Values[] offset for codes of length k ValOffset[k] = Values[] index of 1st symbol of code length + /// k, less the smallest code of length k; so given a code of length k, the corresponding symbol is + /// Values[code + ValOffset[k]]. + /// + public fixed int ValOffset[19]; + + /// + /// Contains the length of bits for the given k value. + /// + public fixed byte LookaheadSize[JpegConstants.Huffman.LookupSize]; + + /// + /// Lookahead table: indexed by the next bits of + /// the input data stream. If the next Huffman code is no more + /// than bits long, we can obtain its length and + /// the corresponding symbol directly from this tables. + /// + /// The lower 8 bits of each table entry contain the number of + /// bits in the corresponding Huffman code, or + 1 + /// if too long. The next 8 bits of each entry contain the symbol. + /// + public fixed byte LookaheadValue[JpegConstants.Huffman.LookupSize]; + + /// + /// Initializes a new instance of the struct. + /// + /// The code lengths. + /// The huffman values. + /// The provided spare workspace memory, can be dirty. + public HuffmanTable(ReadOnlySpan codeLengths, ReadOnlySpan values, Span workspace) + { + Unsafe.CopyBlockUnaligned(ref this.Values[0], ref MemoryMarshal.GetReference(values), (uint)values.Length); + + // Generate codes + uint code = 0; + int si = 1; + int p = 0; + for (int i = 1; i <= 16; i++) + { + int count = codeLengths[i]; + for (int j = 0; j < count; j++) + { + workspace[p++] = code; + code++; + } + + // '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)) + { + JpegThrowHelper.ThrowInvalidImageContentException("Bad huffman table."); + } + + code <<= 1; + si++; + } + + // Figure F.15: generate decoding tables for bit-sequential decoding + p = 0; + for (int j = 1; j <= 16; j++) + { + if (codeLengths[j] != 0) + { + 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; + } + else + { + this.MaxCode[j] = 0; + } + } + + this.ValOffset[18] = 0; + this.MaxCode[17] = ulong.MaxValue; // Ensures huff decode terminates + + // Compute lookahead tables to speed up decoding. + // First we set all the table entries to JpegConstants.Huffman.SlowBits, indicating "too long"; + // then we iterate through the Huffman codes that are short enough and + // fill in all the entries that correspond to bit sequences starting + // with that code. + ref byte lookupSizeRef = ref this.LookaheadSize[0]; + Unsafe.InitBlockUnaligned(ref lookupSizeRef, JpegConstants.Huffman.SlowBits, JpegConstants.Huffman.LookupSize); + + p = 0; + for (int length = 1; length <= JpegConstants.Huffman.LookupBits; length++) + { + int jShift = JpegConstants.Huffman.LookupBits - length; + 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)(workspace[p] << jShift); + for (int ctr = 1 << (JpegConstants.Huffman.LookupBits - length); ctr > 0; ctr--) + { + this.LookaheadSize[lookBits] = (byte)length; + this.LookaheadValue[lookBits] = this.Values[p]; + lookBits++; + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegComponent.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegComponent.cs new file mode 100644 index 0000000..e5d4963 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegComponent.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Common interface to represent raw Jpeg components. + /// + internal interface IJpegComponent + { + /// + /// Gets the component id. + /// + byte Id { get; } + + /// + /// Gets the component's position in the components array. + /// + int Index { get; } + + /// + /// Gets the number of blocks in this component as + /// + Size SizeInBlocks { get; } + + /// + /// Gets the horizontal and the vertical sampling factor as + /// + Size SamplingFactors { get; } + + /// + /// Gets the horizontal sampling factor. + /// + int HorizontalSamplingFactor { get; } + + /// + /// Gets the vertical sampling factor. + /// + int VerticalSamplingFactor { get; } + + /// + /// Gets the divisors needed to apply when calculating colors. + /// + /// https://en.wikipedia.org/wiki/Chroma_subsampling + /// + /// In case of 4:2:0 subsampling the values are: Luma.SubSamplingDivisors = (1,1) Chroma.SubSamplingDivisors = (2,2) + /// + Size SubSamplingDivisors { get; } + + /// + /// Gets the index of the quantization table for this block. + /// + int QuantizationTableIndex { get; } + + /// + /// Gets the storing the "raw" frequency-domain decoded + unzigged blocks. + /// We need to apply IDCT and dequantization to transform them into color-space blocks. + /// + Buffer2D SpectralBlocks { get; } + + /// + /// Gets or sets DC coefficient predictor. + /// + int DcPredictor { get; set; } + + /// + /// Gets or sets the index for the DC table. + /// + int DcTableId { get; set; } + + /// + /// Gets or sets the index for the AC table. + /// + int AcTableId { get; set; } + + /// + /// Initializes component for future buffers initialization. + /// + /// Maximal horizontal subsampling factor among all the components. + /// Maximal vertical subsampling factor among all the components. + void Init(int maxSubFactorH, int maxSubFactorV); + + /// + /// Allocates the spectral blocks. + /// + /// if set to true, use the full height of a block, otherwise use the vertical sampling factor. + void AllocateSpectral(bool fullScan); + + /// + /// Releases resources. + /// + void Dispose(); + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegScanDecoder.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegScanDecoder.cs new file mode 100644 index 0000000..6ed5131 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegScanDecoder.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Interface for a JPEG scan decoder. + /// + internal interface IJpegScanDecoder + { + /// + /// Sets the reset interval. + /// + public int ResetInterval { set; } + + /// + /// Gets or sets the spectral selection start. + /// + public int SpectralStart { get; set; } + + /// + /// Gets or sets the spectral selection end. + /// + public int SpectralEnd { get; set; } + + /// + /// Gets or sets the successive approximation high bit end. + /// + public int SuccessiveHigh { get; set; } + + /// + /// Gets or sets the successive approximation low bit end. + /// + public int SuccessiveLow { get; set; } + + /// + /// Decodes the entropy coded data. + /// + /// Component count in the current scan. + /// + /// The ICC profile to use for color conversion. If null, the default color space. + /// + public void ParseEntropyCodedData(int scanComponentCount, IccProfile? iccProfile); + + /// + /// Sets the JpegFrame and its components and injects the frame data into the spectral converter. + /// + /// The frame. + /// The raw JPEG data. + public void InjectFrameData(JpegFrame frame, IRawJpegData jpegData); + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs new file mode 100644 index 0000000..a707fd9 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Represents decompressed, unprocessed jpeg data with spectral space -s. + /// + internal interface IRawJpegData : IDisposable + { + /// + /// Gets the color space + /// + JpegColorSpace ColorSpace { get; } + + /// + /// Gets the components. + /// + JpegComponent[] Components { get; } + + /// + /// Gets the quantization tables, in natural order. + /// + Block8x8F[] QuantizationTables { get; } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs new file mode 100644 index 0000000..b336e44 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs @@ -0,0 +1,111 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Metadata; +using System; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Provides information about the JFIF marker segment. + /// TODO: Thumbnail? + /// + internal readonly struct JFifMarker : IEquatable + { + /// + /// Gets the length of an JFIF marker segment. + /// + public const int Length = 13; + + /// + /// Initializes a new instance of the struct. + /// + /// The major version. + /// The minor version. + /// The units for the density values. + /// The horizontal pixel density. + /// The vertical pixel density. + private JFifMarker(byte majorVersion, byte minorVersion, byte densityUnits, short xDensity, short yDensity) + { + this.MajorVersion = majorVersion; + this.MinorVersion = minorVersion; + + // LibJpeg and co will simply cast and not try to enforce a range. + this.DensityUnits = (PixelResolutionUnit)densityUnits; + this.XDensity = xDensity; + this.YDensity = yDensity; + } + + /// + /// Gets the major version. + /// + public byte MajorVersion { get; } + + /// + /// Gets the minor version. + /// + public byte MinorVersion { get; } + + /// + /// Gets the units for the following pixel density fields + /// 00 : No units; width:height pixel aspect ratio = Ydensity:Xdensity + /// 01 : Pixels per inch (2.54 cm) + /// 02 : Pixels per centimeter + /// + public PixelResolutionUnit DensityUnits { get; } + + /// + /// Gets the horizontal pixel density. + /// + public short XDensity { get; } + + /// + /// Gets the vertical pixel density. + /// + public short YDensity { get; } + + /// + /// Converts the specified byte array representation of an JFIF marker to its equivalent and + /// returns a value that indicates whether the conversion succeeded. + /// + /// The byte array containing metadata to parse. + /// The marker to return. + public static bool TryParse(ReadOnlySpan bytes, out JFifMarker marker) + { + // Some images incorrectly use JFXX as the App0 marker (Issue 2478) + if (ProfileResolver.IsProfile(bytes, ProfileResolver.JFifMarker) + || ProfileResolver.IsProfile(bytes, ProfileResolver.JFxxMarker)) + { + byte majorVersion = bytes[5]; + byte minorVersion = bytes[6]; + byte densityUnits = bytes[7]; + short xDensity = (short)((bytes[8] << 8) | bytes[9]); + short yDensity = (short)((bytes[10] << 8) | bytes[11]); + marker = new JFifMarker(majorVersion, minorVersion, densityUnits, xDensity, yDensity); + return true; + } + + marker = default; + return false; + } + + /// + public bool Equals(JFifMarker other) + => 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() + => HashCode.Combine( + this.MajorVersion, + this.MinorVersion, + this.DensityUnits, + this.XDensity, + this.YDensity); + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs new file mode 100644 index 0000000..26e1e1a --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs @@ -0,0 +1,240 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Used to buffer and track the bits read from the Huffman entropy encoded data. + /// + internal struct JpegBitReader + { + private readonly BufferedReadStream stream; + + // The entropy encoded code buffer. + private ulong data; + + // The number of valid bits left to read in the buffer. + private int remainingBits; + + // Whether there is no more good data to pull from the stream for the current mcu. + private bool badData; + + // How many times have we hit the eof. + private int eofHitCount; + + public JpegBitReader(BufferedReadStream stream) + { + this.stream = stream; + this.data = 0ul; + this.remainingBits = 0; + this.Marker = JpegConstants.Markers.XFF; + this.MarkerPosition = 0; + this.badData = false; + this.NoData = false; + this.eofHitCount = 0; + } + + /// + /// Gets the current, if any, marker in the input stream. + /// + public byte Marker { get; private set; } + + /// + /// Gets the opening position of an identified marker. + /// + public long MarkerPosition { get; private set; } + + /// + /// Gets a value indicating whether to continue reading the input stream. + /// + public bool NoData { get; private set; } + + [MethodImpl(InliningOptions.ShortMethod)] + public void CheckBits() + { + if (this.remainingBits < JpegConstants.Huffman.MinBits) + { + this.FillBuffer(); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Reset() + { + this.data = 0ul; + this.remainingBits = 0; + this.Marker = JpegConstants.Markers.XFF; + this.MarkerPosition = 0; + this.badData = false; + this.NoData = false; + } + + /// + /// Whether a RST marker has been detected, I.E. One that is between RST0 and RST7 + /// + [MethodImpl(InliningOptions.ShortMethod)] + public readonly bool HasRestartMarker() => HasRestart(this.Marker); + + /// + /// Whether a bad marker has been detected, I.E. One that is not between RST0 and RST7 + /// + [MethodImpl(InliningOptions.ShortMethod)] + public readonly bool HasBadMarker() => this.Marker != JpegConstants.Markers.XFF && !this.HasRestartMarker(); + + [MethodImpl(InliningOptions.AlwaysInline)] + public void FillBuffer() + { + // Attempt to load at least the minimum number of required bits into the buffer. + // We fail to do so only if we hit a marker or reach the end of the input stream. + this.remainingBits += JpegConstants.Huffman.FetchBits; + this.data = (this.data << JpegConstants.Huffman.FetchBits) | this.GetBytes(); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public unsafe int DecodeHuffman(ref HuffmanTable h) + { + this.CheckBits(); + int index = this.PeekBits(JpegConstants.Huffman.LookupBits); + int size = h.LookaheadSize[index]; + + if (size < JpegConstants.Huffman.SlowBits) + { + this.remainingBits -= size; + return h.LookaheadValue[index]; + } + + ulong x = this.data << (JpegConstants.Huffman.RegisterSize - this.remainingBits); + while (x > h.MaxCode[size]) + { + size++; + } + + this.remainingBits -= size; + + return h.Values[(h.ValOffset[size] + (int)(x >> (JpegConstants.Huffman.RegisterSize - size))) & 0xFF]; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public int Receive(int nbits) + { + this.CheckBits(); + return Extend(this.GetBits(nbits), nbits); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static bool HasRestart(byte marker) + => marker >= JpegConstants.Markers.RST0 && marker <= JpegConstants.Markers.RST7; + + [MethodImpl(InliningOptions.ShortMethod)] + public int GetBits(int nbits) => (int)ExtractBits(this.data, this.remainingBits -= nbits, nbits); + + [MethodImpl(InliningOptions.ShortMethod)] + public readonly int PeekBits(int nbits) => (int)ExtractBits(this.data, this.remainingBits - nbits, nbits); + + [MethodImpl(InliningOptions.AlwaysInline)] + private static ulong ExtractBits(ulong value, int offset, int size) => (value >> offset) & (ulong)((1 << size) - 1); + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Extend(int v, int nbits) => v - ((((v + v) >> nbits) - 1) & ((1 << nbits) - 1)); + + [MethodImpl(InliningOptions.ShortMethod)] + private ulong GetBytes() + { + ulong temp = 0; + for (int i = 0; i < JpegConstants.Huffman.FetchLoop; i++) + { + int b = this.ReadStream(); + + // Found a marker. + if (b == JpegConstants.Markers.XFF) + { + int c = this.ReadStream(); + while (c == JpegConstants.Markers.XFF) + { + // Loop here to discard any padding FF bytes on terminating marker, + // so that we can save a valid marker value. + c = this.ReadStream(); + } + + // Found a marker + // We accept multiple FF bytes followed by a 0 as meaning a single FF data byte. + // even though it's considered 'invalid' according to the specs. + if (c != 0) + { + // It's a trick so we won't read past actual marker + this.badData = true; + this.Marker = (byte)c; + this.MarkerPosition = this.stream.Position - 2; + } + } + + temp = (temp << 8) | (ulong)(long)b; + } + + return temp; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public bool FindNextMarker() + { + while (true) + { + int b = this.stream.ReadByte(); + if (b == -1) + { + return false; + } + + // Found a marker. + if (b == JpegConstants.Markers.XFF) + { + while (b == JpegConstants.Markers.XFF) + { + // Loop here to discard any padding FF bytes on terminating marker. + b = this.stream.ReadByte(); + if (b == -1) + { + return false; + } + } + + // Found a valid marker. Exit loop + if (b != 0) + { + this.Marker = (byte)b; + this.MarkerPosition = this.stream.Position - 2; + return true; + } + } + } + } + + [MethodImpl(InliningOptions.AlwaysInline)] + private int ReadStream() + { + int value = this.badData ? 0 : this.stream.ReadByte(); + + // We've encountered the end of the file stream which means there's no EOI marker or the marker has been read + // during decoding of the SOS marker. + // When reading individual bits 'badData' simply means we have hit a marker, When data is '0' and the stream is exhausted + // we know we have hit the EOI and completed decoding the scan buffer. + if (value == -1 || (this.badData && this.data == 0 && this.stream.Position >= this.stream.Length)) + { + // We've hit the end of the file stream more times than allowed which means there's no EOI marker + // in the image or the SOS marker has the wrong dimensions set. + if (this.eofHitCount > JpegConstants.Huffman.FetchLoop) + { + this.badData = true; + this.NoData = true; + value = 0; + } + + this.eofHitCount++; + } + + return value; + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs new file mode 100644 index 0000000..e4396d1 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs @@ -0,0 +1,137 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using SixLabors.ImageSharp.Memory; +using System; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Represents a single frame component. + /// + internal class JpegComponent : IDisposable, IJpegComponent + { + private readonly MemoryAllocator memoryAllocator; + + public JpegComponent(MemoryAllocator memoryAllocator, JpegFrame frame, byte id, int horizontalFactor, int verticalFactor, byte quantizationTableIndex, int index) + { + this.memoryAllocator = memoryAllocator; + this.Frame = frame; + this.Id = id; + + this.HorizontalSamplingFactor = horizontalFactor; + this.VerticalSamplingFactor = verticalFactor; + this.SamplingFactors = new Size(this.HorizontalSamplingFactor, this.VerticalSamplingFactor); + + this.QuantizationTableIndex = quantizationTableIndex; + this.Index = index; + } + + /// + /// Gets the component id. + /// + public byte Id { get; } + + /// + /// Gets or sets DC coefficient predictor. + /// + public int DcPredictor { get; set; } + + /// + /// Gets the horizontal sampling factor. + /// + public int HorizontalSamplingFactor { get; } + + /// + /// Gets the vertical sampling factor. + /// + public int VerticalSamplingFactor { get; } + + /// + public Buffer2D SpectralBlocks { get; private set; } + + /// + public Size SubSamplingDivisors { get; private set; } + + /// + public int QuantizationTableIndex { get; } + + /// + public int Index { get; } + + /// + public Size SizeInBlocks { get; private set; } + + /// + public Size SamplingFactors { get; set; } + + /// + /// Gets the number of blocks per line. + /// + public int WidthInBlocks { get; private set; } + + /// + /// Gets the number of blocks per column. + /// + public int HeightInBlocks { get; private set; } + + /// + /// Gets or sets the index for the DC Huffman table. + /// + public int DcTableId { get; set; } + + /// + /// Gets or sets the index for the AC Huffman table. + /// + public int AcTableId { get; set; } + + public JpegFrame Frame { get; } + + /// + public void Dispose() + { + this.SpectralBlocks?.Dispose(); + this.SpectralBlocks = null; + } + + /// + /// Initializes component for future buffers initialization. + /// + /// Maximal horizontal subsampling factor among all the components. + /// Maximal vertical subsampling factor among all the components. + public void Init(int maxSubFactorH, int maxSubFactorV) + { + this.WidthInBlocks = (int)MathF.Ceiling( + MathF.Ceiling(this.Frame.PixelWidth / 8F) * this.HorizontalSamplingFactor / maxSubFactorH); + + this.HeightInBlocks = (int)MathF.Ceiling( + MathF.Ceiling(this.Frame.PixelHeight / 8F) * this.VerticalSamplingFactor / maxSubFactorV); + + int blocksPerLineForMcu = this.Frame.McusPerLine * this.HorizontalSamplingFactor; + int blocksPerColumnForMcu = this.Frame.McusPerColumn * this.VerticalSamplingFactor; + this.SizeInBlocks = new Size(blocksPerLineForMcu, blocksPerColumnForMcu); + + this.SubSamplingDivisors = new Size(maxSubFactorH, maxSubFactorV).DivideBy(this.SamplingFactors); + + if (this.SubSamplingDivisors.Width == 0 || this.SubSamplingDivisors.Height == 0) + { + JpegThrowHelper.ThrowBadSampling(); + } + } + + /// + public void AllocateSpectral(bool fullScan) + { + if (this.SpectralBlocks != null) + { + // This method will be called each scan marker so we need to allocate only once. + return; + } + + int spectralAllocWidth = this.SizeInBlocks.Width; + int spectralAllocHeight = fullScan ? this.SizeInBlocks.Height : this.VerticalSamplingFactor; + + this.SpectralBlocks = this.memoryAllocator.Allocate2D(spectralAllocWidth, spectralAllocHeight, AllocationOptions.Clean); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFileMarker.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFileMarker.cs new file mode 100644 index 0000000..467ea7c --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFileMarker.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Represents a jpeg file marker. + /// + internal readonly struct JpegFileMarker + { + /// + /// Initializes a new instance of the struct. + /// + /// The marker + /// The position within the stream + public JpegFileMarker(byte marker, long position) + : this(marker, position, false) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The marker + /// The position within the stream + /// Whether the current marker is invalid + public JpegFileMarker(byte marker, long position, bool invalid) + { + this.Marker = marker; + this.Position = position; + this.Invalid = invalid; + } + + /// + /// Gets a value indicating whether the current marker is invalid + /// + public bool Invalid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + /// + /// Gets the position of the marker within a stream + /// + public byte Marker + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + /// + /// Gets the position of the marker within a stream + /// + public long Position + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + /// + public override string ToString() + => this.Marker.ToString("X", CultureInfo.InvariantCulture); + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs new file mode 100644 index 0000000..7ad8ae6 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs @@ -0,0 +1,152 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Represent a single jpeg frame. + /// + internal sealed class JpegFrame : IDisposable + { + public JpegFrame(JpegFileMarker sofMarker, byte precision, int width, int height, byte componentCount) + { + this.IsExtended = sofMarker.Marker is JpegConstants.Markers.SOF1 or JpegConstants.Markers.SOF9; + this.Progressive = sofMarker.Marker is JpegConstants.Markers.SOF2 or JpegConstants.Markers.SOF10; + + this.Precision = precision; + this.MaxColorChannelValue = MathF.Pow(2, precision) - 1; + + this.PixelWidth = width; + this.PixelHeight = height; + + this.ComponentCount = componentCount; + } + + /// + /// Gets a value indicating whether the frame uses the extended specification. + /// + public bool IsExtended { get; private set; } + + /// + /// Gets a value indicating whether the frame uses the progressive specification. + /// + public bool Progressive { get; private set; } + + /// + /// Gets or sets a value indicating whether the frame is encoded using multiple scans (SOS markers). + /// + /// + /// This is true for progressive and baseline non-interleaved images. + /// + public bool Interleaved { get; set; } + + /// + /// Gets the precision. + /// + public byte Precision { get; private set; } + + /// + /// Gets the maximum color value derived from . + /// + public float MaxColorChannelValue { get; private set; } + + /// + /// Gets the number of pixel per row. + /// + public int PixelHeight { get; private set; } + + /// + /// Gets the number of pixels per line. + /// + public int PixelWidth { get; private set; } + + /// + /// Gets the pixel size of the image. + /// + public Size PixelSize => new(this.PixelWidth, this.PixelHeight); + + /// + /// Gets the number of components within a frame. + /// + public byte ComponentCount { get; private set; } + + /// + /// Gets or sets the component id collection. + /// + public byte[] ComponentIds { get; set; } + + /// + /// Gets or sets the order in which to process the components. + /// in interleaved mode. + /// + public byte[] ComponentOrder { get; set; } + + /// + /// Gets or sets the frame component collection. + /// + public JpegComponent[] Components { get; set; } + + /// + /// Gets or sets the number of MCU's per line. + /// + public int McusPerLine { get; set; } + + /// + /// Gets or sets the number of MCU's per column. + /// + public int McusPerColumn { get; set; } + + /// + /// Gets the mcu size of the image. + /// + public Size McuSize => new(this.McusPerLine, this.McusPerColumn); + + /// + /// Gets the color depth, in number of bits per pixel. + /// + public int BitsPerPixel => this.ComponentCount * this.Precision; + + /// + public void Dispose() + { + if (this.Components != null) + { + for (int i = 0; i < this.Components.Length; i++) + { + this.Components[i]?.Dispose(); + } + + this.Components = null; + } + } + + /// + /// Allocates the frame component blocks. + /// + /// Maximal horizontal subsampling factor among all the components. + /// Maximal vertical subsampling factor among all the components. + public void Init(int maxSubFactorH, int maxSubFactorV) + { + this.McusPerLine = (int)Numerics.DivideCeil((uint)this.PixelWidth, (uint)maxSubFactorH * 8); + this.McusPerColumn = (int)Numerics.DivideCeil((uint)this.PixelHeight, (uint)maxSubFactorV * 8); + + for (int i = 0; i < this.ComponentCount; i++) + { + JpegComponent component = this.Components[i]; + component.Init(maxSubFactorH, maxSubFactorV); + } + } + + public void AllocateComponents() + { + bool fullScan = this.Progressive || !this.Interleaved; + for (int i = 0; i < this.ComponentCount; i++) + { + JpegComponent component = this.Components[i]; + component.AllocateSpectral(fullScan); + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs new file mode 100644 index 0000000..f10eacc --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Provides methods for identifying metadata and color profiles within jpeg images. + /// + internal static class ProfileResolver + { + /// + /// Gets the JFIF specific markers. + /// + public static ReadOnlySpan JFifMarker => + [ + (byte)'J', (byte)'F', (byte)'I', (byte)'F', (byte)'\0' + ]; + + /// + /// Gets the JFXX specific markers. + /// + public static ReadOnlySpan JFxxMarker => + [ + (byte)'J', (byte)'F', (byte)'X', (byte)'X', (byte)'\0' + ]; + + /// + /// Gets the ICC specific markers. + /// + public static ReadOnlySpan IccMarker => + [ + (byte)'I', (byte)'C', (byte)'C', (byte)'_', + (byte)'P', (byte)'R', (byte)'O', (byte)'F', + (byte)'I', (byte)'L', (byte)'E', (byte)'\0' + ]; + + /// + /// Gets the adobe photoshop APP13 marker which can contain IPTC meta data. + /// + public static ReadOnlySpan AdobePhotoshopApp13Marker => + [ + (byte)'P', (byte)'h', (byte)'o', (byte)'t', (byte)'o', (byte)'s', (byte)'h', (byte)'o', (byte)'p', (byte)' ', (byte)'3', (byte)'.', (byte)'0', (byte)'\0' + ]; + + /// + /// Gets the 8BIM marker, which signals the start of a adobe specific image resource block. + /// + public static ReadOnlySpan AdobeImageResourceBlockMarker => + [ + (byte)'8', (byte)'B', (byte)'I', (byte)'M' + ]; + + /// + /// Gets a IPTC Image resource ID. + /// + public static ReadOnlySpan AdobeIptcMarker => + [ + (byte)4, (byte)4 + ]; + + /// + /// Gets the EXIF specific markers. + /// + public static ReadOnlySpan ExifMarker => + [ + (byte)'E', (byte)'x', (byte)'i', (byte)'f', (byte)'\0', (byte)'\0' + ]; + + /// + /// Gets the XMP specific markers. + /// + public static ReadOnlySpan XmpMarker => + [ + (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 . + /// + public static ReadOnlySpan AdobeMarker => + [ + (byte)'A', (byte)'d', (byte)'o', (byte)'b', (byte)'e' + ]; + + /// + /// Returns a value indicating whether the passed bytes are a match to the profile identifier. + /// + /// The bytes to check. + /// The profile identifier. + /// The . + public static bool IsProfile(ReadOnlySpan bytesToCheck, ReadOnlySpan profileIdentifier) + { + return bytesToCheck.Length >= profileIdentifier.Length + && bytesToCheck[..profileIdentifier.Length].SequenceEqual(profileIdentifier); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs new file mode 100644 index 0000000..f5c7fbb --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs @@ -0,0 +1,137 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// Converter used to convert jpeg spectral data to pixels. + /// + internal abstract class SpectralConverter + { + /// + /// Supported scaled spectral block sizes for scaled IDCT decoding. + /// + private static readonly int[] ScaledBlockSizes = + [ + + // 8 => 1, 1/8 of the original size + 1, + + // 8 => 2, 1/4 of the original size + 2, + + // 8 => 4, 1/2 of the original size + 4, + ]; + + /// + /// Gets a value indicating whether this converter has converted spectral + /// data of the current image or not. + /// + protected bool Converted { get; private set; } + + /// + /// Injects jpeg image decoding metadata. + /// + /// + /// This should be called exactly once during SOF (Start Of Frame) marker. + /// + /// Instance containing decoder-specific parameters. + /// Instance containing decoder-specific parameters. + public abstract void InjectFrameData(JpegFrame frame, IRawJpegData jpegData); + + /// + /// Initializes this spectral decoder instance for decoding. + /// This should be called exactly once after all markers which can alter + /// spectral decoding parameters. + /// + public abstract void PrepareForDecoding(); + + /// + /// Converts single spectral jpeg stride to color stride in baseline + /// decoding mode. + /// + /// + /// The ICC profile to use for color conversion. If , then the default color space is used. + /// + /// + /// Called once per decoded spectral stride in + /// only for baseline interleaved jpeg images. + /// Spectral 'stride' doesn't particularly mean 'single stride'. + /// Actual stride height depends on the subsampling factor of the given image. + /// + public abstract void ConvertStrideBaseline(IccProfile? iccProfile); + + /// + /// Marks current converter state as 'converted'. + /// + /// + /// This must be called only for baseline interleaved jpeg's. + /// + public void CommitConversion() + { + DebugGuard.IsFalse(this.Converted, nameof(this.Converted), $"{nameof(this.CommitConversion)} must be called only once"); + + this.Converted = true; + } + + /// + /// Gets the color converter. + /// + /// The jpeg frame with the color space to convert to. + /// The raw JPEG data. + /// The color converter. + protected virtual JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData) + => JpegColorConverterBase.GetConverter(jpegData.ColorSpace, frame.Precision); + + /// + /// Calculates image size with optional scaling. + /// + /// + /// Does not apply scaling if is null. + /// + /// Size of the image. + /// Target size of the image. + /// Spectral block size, equals to 8 if scaling is not applied. + /// Resulting image size, equals to if scaling is not applied. + public static Size CalculateResultingImageSize(Size size, Size? targetSize, out int blockPixelSize) + { + const int blockNativePixelSize = 8; + + blockPixelSize = blockNativePixelSize; + if (targetSize != null) + { + Size tSize = targetSize.Value; + + int fullBlocksWidth = (int)((uint)size.Width / blockNativePixelSize); + int fullBlocksHeight = (int)((uint)size.Height / blockNativePixelSize); + + // & (blockNativePixelSize - 1) is Numerics.Modulo8(), basically + int blockWidthRemainder = size.Width & (blockNativePixelSize - 1); + int blockHeightRemainder = size.Height & (blockNativePixelSize - 1); + + for (int i = 0; i < ScaledBlockSizes.Length; i++) + { + int blockSize = ScaledBlockSizes[i]; + int scaledWidth = (fullBlocksWidth * blockSize) + (int)Numerics.DivideCeil((uint)(blockWidthRemainder * blockSize), blockNativePixelSize); + int scaledHeight = (fullBlocksHeight * blockSize) + (int)Numerics.DivideCeil((uint)(blockHeightRemainder * blockSize), blockNativePixelSize); + + if (scaledWidth >= tSize.Width && scaledHeight >= tSize.Height) + { + blockPixelSize = blockSize; + return new Size(scaledWidth, scaledHeight); + } + } + } + + return size; + } + + /// + /// Gets a value indicating whether the converter has a pixel buffer. + /// + /// if the converter has a pixel buffer; otherwise, . + public abstract bool HasPixelBuffer(); + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs new file mode 100644 index 0000000..ec2c3ea --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -0,0 +1,284 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using System.Linq; +using System.Threading; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { + /// + /// + /// Color decoding scheme: + /// + /// + /// Decode spectral data to Jpeg color space + /// Convert from Jpeg color space to RGB + /// Convert from RGB to target pixel space + /// + /// + /// + internal class SpectralConverter : SpectralConverter, IDisposable + where TPixel : unmanaged, IPixel + { + private JpegFrame frame; + + private IRawJpegData jpegData; + + /// + /// Jpeg component converters from decompressed spectral to color data. + /// + private ComponentProcessor[] componentProcessors; + + /// + /// Color converter from jpeg color space to target pixel color space. + /// + private JpegColorConverterBase colorConverter; + + /// + /// Intermediate buffer of RGB components used in color conversion. + /// + private IMemoryOwner rgbBuffer; + + /// + /// Proxy buffer used in packing from RGB to target TPixel pixels. + /// + private IMemoryOwner paddedProxyPixelRow; + + /// + /// Resulting 2D pixel buffer. + /// + private Buffer2D pixelBuffer; + + /// + /// How many pixel rows are processed in one 'stride'. + /// + private int pixelRowsPerStep; + + /// + /// How many pixel rows were processed. + /// + private int pixelRowCounter; + + /// + /// Represent target size after decoding for scaling decoding mode. + /// + /// + /// Null if no scaling is required. + /// + private Size? targetSize; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// Optional target size for decoded image. + public SpectralConverter(Configuration configuration, Size? targetSize = null) + { + this.Configuration = configuration; + this.targetSize = targetSize; + } + + /// + /// Gets the configuration instance associated with current decoding routine. + /// + public Configuration Configuration { get; } + + /// + /// Gets a value indicating whether the converter has a pixel buffer. + /// + /// if the converter has a pixel buffer; otherwise, . + public override bool HasPixelBuffer() => this.pixelBuffer is not null; + + /// + /// Gets converted pixel buffer. + /// + /// + /// For non-baseline interleaved jpeg this method does a 'lazy' spectral + /// conversion from spectral to color. + /// + /// Optional ICC profile for color conversion. + /// Cancellation token. + /// Pixel buffer. + public Buffer2D GetPixelBuffer(IccProfile iccProfile, CancellationToken cancellationToken) + { + if (!this.Converted) + { + this.PrepareForDecoding(); + + int steps = (int)Math.Ceiling(this.pixelBuffer.Height / (float)this.pixelRowsPerStep); + + for (int step = 0; step < steps; step++) + { + cancellationToken.ThrowIfCancellationRequested(); + this.ConvertStride(step, iccProfile); + } + } + + Buffer2D buffer = this.pixelBuffer; + this.pixelBuffer = null; + return buffer; + } + + /// + /// Converts single spectral jpeg stride to color stride. + /// + /// Spectral stride index. + /// Optional ICC profile for color conversion. + private void ConvertStride(int spectralStep, IccProfile iccProfile) + { + int maxY = Math.Min(this.pixelBuffer.Height, this.pixelRowCounter + this.pixelRowsPerStep); + + for (int i = 0; i < this.componentProcessors.Length; i++) + { + this.componentProcessors[i].CopyBlocksToColorBuffer(spectralStep); + } + + int width = this.pixelBuffer.Width; + + for (int yy = this.pixelRowCounter; yy < maxY; yy++) + { + int y = yy - this.pixelRowCounter; + + JpegColorConverterBase.ComponentValues values = new(this.componentProcessors, y); + + if (iccProfile != null) + { + this.colorConverter.ConvertToRgbInPlaceWithIcc(this.Configuration, in values, iccProfile); + } + else + { + this.colorConverter.ConvertToRgbInPlace(in values); + } + + values = values.Slice(0, width); // slice away Jpeg padding + + Span r = this.rgbBuffer.Slice(0, width); + Span g = this.rgbBuffer.Slice(width, width); + Span b = this.rgbBuffer.Slice(width * 2, width); + + SimdUtils.NormalizedFloatToByteSaturate(values.Component0, r); + SimdUtils.NormalizedFloatToByteSaturate(values.Component1, g); + SimdUtils.NormalizedFloatToByteSaturate(values.Component2, b); + + // PackFromRgbPlanes expects the destination to be padded, so try to get padded span containing extra elements from the next row. + // If we can't get such a padded row because we are on a MemoryGroup boundary or at the last row, + // pack pixels to a temporary, padded proxy buffer, then copy the relevant values to the destination row. + if (this.pixelBuffer.DangerousTryGetPaddedRowSpan(yy, 3, out Span destRow)) + { + PixelOperations.Instance.PackFromRgbPlanes(r, g, b, destRow); + } + else + { + Span proxyRow = this.paddedProxyPixelRow.GetSpan(); + PixelOperations.Instance.PackFromRgbPlanes(r, g, b, proxyRow); + proxyRow[..width].CopyTo(this.pixelBuffer.DangerousGetRowSpan(yy)); + } + } + + this.pixelRowCounter += this.pixelRowsPerStep; + } + + /// + public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) + { + this.frame = frame; + this.jpegData = jpegData; + } + + /// + public override void PrepareForDecoding() + { + DebugGuard.IsTrue(this.colorConverter == null, "SpectralConverter.PrepareForDecoding() must be called once."); + + MemoryAllocator allocator = this.Configuration.MemoryAllocator; + + // Color converter from RGB to TPixel + JpegColorConverterBase converter = this.GetColorConverter(this.frame, this.jpegData); + this.colorConverter = converter; + + // Resulting image size + Size pixelSize = CalculateResultingImageSize(this.frame.PixelSize, this.targetSize, out int blockPixelSize); + + // Iteration data + int majorBlockWidth = this.frame.Components.Max((component) => component.SizeInBlocks.Width); + int majorVerticalSamplingFactor = this.frame.Components.Max((component) => component.SamplingFactors.Height); + + this.pixelRowsPerStep = majorVerticalSamplingFactor * blockPixelSize; + + // Pixel buffer for resulting image + this.pixelBuffer = allocator.Allocate2D( + pixelSize.Width, + pixelSize.Height, + this.Configuration.PreferContiguousImageBuffers, + AllocationOptions.Clean); + this.paddedProxyPixelRow = allocator.Allocate(pixelSize.Width + 3); + + // Component processors from spectral to RGB + int bufferWidth = majorBlockWidth * blockPixelSize; + + // Converters process pixels in batches and require target buffer size to be divisible by a batch size + // Corner case: image size including jpeg padding is already divisible by a batch size or remainder == 0 + int elementsPerBatch = converter.ElementsPerBatch; + int batchRemainder = bufferWidth & (elementsPerBatch - 1); + int widthComplementaryValue = batchRemainder == 0 ? 0 : elementsPerBatch - batchRemainder; + + Size postProcessorBufferSize = new(bufferWidth + widthComplementaryValue, this.pixelRowsPerStep); + this.componentProcessors = this.CreateComponentProcessors(this.frame, this.jpegData, blockPixelSize, postProcessorBufferSize); + + // Single 'stride' rgba32 buffer for conversion between spectral and TPixel + this.rgbBuffer = allocator.Allocate(pixelSize.Width * 3); + } + + /// + public override void ConvertStrideBaseline(IccProfile iccProfile) + { + // Convert next pixel stride using single spectral `stride' + // Note that zero passing eliminates extra virtual call + this.ConvertStride(spectralStep: 0, iccProfile); + + foreach (ComponentProcessor cpp in this.componentProcessors) + { + cpp.ClearSpectralBuffers(); + } + } + + protected ComponentProcessor[] CreateComponentProcessors(JpegFrame frame, IRawJpegData jpegData, int blockPixelSize, Size processorBufferSize) + { + MemoryAllocator allocator = this.Configuration.MemoryAllocator; + ComponentProcessor[] componentProcessors = new ComponentProcessor[frame.Components.Length]; + for (int i = 0; i < componentProcessors.Length; i++) + { + componentProcessors[i] = blockPixelSize switch + { + 4 => new DownScalingComponentProcessor2(allocator, frame, jpegData, processorBufferSize, frame.Components[i]), + 2 => new DownScalingComponentProcessor4(allocator, frame, jpegData, processorBufferSize, frame.Components[i]), + 1 => new DownScalingComponentProcessor8(allocator, frame, jpegData, processorBufferSize, frame.Components[i]), + _ => new DirectComponentProcessor(allocator, frame, jpegData, processorBufferSize, frame.Components[i]), + }; + } + + return componentProcessors; + } + + /// + public void Dispose() + { + if (this.componentProcessors != null) + { + foreach (ComponentProcessor cpp in this.componentProcessors) + { + cpp.Dispose(); + } + } + + this.rgbBuffer?.Dispose(); + this.paddedProxyPixelRow?.Dispose(); + this.pixelBuffer?.Dispose(); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Encoder/Component.cs b/ImageSharp/Formats/Jpeg/Components/Encoder/Component.cs new file mode 100644 index 0000000..bc67eb9 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Encoder/Component.cs @@ -0,0 +1,116 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using SixLabors.ImageSharp.Memory; +using System; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { + /// + /// Represents a single frame component. + /// + internal class Component : IDisposable + { + private readonly MemoryAllocator memoryAllocator; + + public Component(MemoryAllocator memoryAllocator, int horizontalFactor, int verticalFactor, int quantizationTableIndex) + { + this.memoryAllocator = memoryAllocator; + + this.HorizontalSamplingFactor = horizontalFactor; + this.VerticalSamplingFactor = verticalFactor; + this.SamplingFactors = new Size(horizontalFactor, verticalFactor); + + this.QuantizationTableIndex = quantizationTableIndex; + } + + /// + /// Gets or sets DC coefficient predictor. + /// + public int DcPredictor { get; set; } + + /// + /// Gets the horizontal sampling factor. + /// + public int HorizontalSamplingFactor { get; } + + /// + /// Gets the vertical sampling factor. + /// + public int VerticalSamplingFactor { get; } + + public Buffer2D SpectralBlocks { get; private set; } + + public Size SubSamplingDivisors { get; private set; } + + public int QuantizationTableIndex { get; } + + public Size SizeInBlocks { get; private set; } + + public Size SamplingFactors { get; set; } + + /// + /// Gets the number of blocks per line. + /// + public int WidthInBlocks { get; private set; } + + /// + /// Gets the number of blocks per column. + /// + public int HeightInBlocks { get; private set; } + + /// + /// Gets or sets the index for the DC Huffman table. + /// + public int DcTableId { get; set; } + + /// + /// Gets or sets the index for the AC Huffman table. + /// + public int AcTableId { get; set; } + + /// + public void Dispose() + { + this.SpectralBlocks?.Dispose(); + this.SpectralBlocks = null; + } + + /// + /// Initializes component for future buffers initialization. + /// + /// asdfasdf. + /// Maximal horizontal subsampling factor among all the components. + /// Maximal vertical subsampling factor among all the components. + public void Init(JpegFrame frame, int maxSubFactorH, int maxSubFactorV) + { + uint widthInBlocks = ((uint)frame.PixelWidth + 7) / 8; + uint heightInBlocks = ((uint)frame.PixelHeight + 7) / 8; + + this.WidthInBlocks = (int)MathF.Ceiling( + (float)widthInBlocks * this.HorizontalSamplingFactor / maxSubFactorH); + + this.HeightInBlocks = (int)MathF.Ceiling( + (float)heightInBlocks * this.VerticalSamplingFactor / maxSubFactorV); + + int blocksPerLineForMcu = frame.McusPerLine * this.HorizontalSamplingFactor; + int blocksPerColumnForMcu = frame.McusPerColumn * this.VerticalSamplingFactor; + this.SizeInBlocks = new Size(blocksPerLineForMcu, blocksPerColumnForMcu); + + this.SubSamplingDivisors = new Size(maxSubFactorH, maxSubFactorV).DivideBy(this.SamplingFactors); + + if (this.SubSamplingDivisors.Width == 0 || this.SubSamplingDivisors.Height == 0) + { + JpegThrowHelper.ThrowBadSampling(); + } + } + + public void AllocateSpectral(bool fullScan) + { + int spectralAllocWidth = this.SizeInBlocks.Width; + int spectralAllocHeight = fullScan ? this.SizeInBlocks.Height : this.VerticalSamplingFactor; + + this.SpectralBlocks = this.memoryAllocator.Allocate2D(spectralAllocWidth, spectralAllocHeight); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Encoder/ComponentProcessor.cs b/ImageSharp/Formats/Jpeg/Components/Encoder/ComponentProcessor.cs new file mode 100644 index 0000000..743c599 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Encoder/ComponentProcessor.cs @@ -0,0 +1,259 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { + internal class ComponentProcessor : IDisposable + { + private readonly Size blockAreaSize; + + private readonly Component component; + + private Block8x8F quantTable; + + public ComponentProcessor(MemoryAllocator memoryAllocator, Component component, Size postProcessorBufferSize, Block8x8F quantTable) + { + this.component = component; + this.quantTable = quantTable; + + this.component = component; + this.blockAreaSize = component.SubSamplingDivisors * 8; + + // alignment of 8 so each block stride can be sampled from a single 'ref pointer' + this.ColorBuffer = memoryAllocator.Allocate2DOveraligned( + postProcessorBufferSize.Width, + postProcessorBufferSize.Height, + 8, + AllocationOptions.Clean); + } + + /// + /// Gets the temporary working buffer of color values. + /// + public Buffer2D ColorBuffer { get; } + + public void CopyColorBufferToBlocks(int spectralStep) + { + Buffer2D spectralBuffer = this.component.SpectralBlocks; + int destAreaStride = this.ColorBuffer.Width; + int yBlockStart = spectralStep * this.component.SamplingFactors.Height; + + Block8x8F workspaceBlock = default; + + // handle subsampling + Size subsamplingFactors = this.component.SubSamplingDivisors; + if (subsamplingFactors.Width != 1 || subsamplingFactors.Height != 1) + { + this.PackColorBuffer(); + } + + int blocksRowsPerStep = this.component.SamplingFactors.Height; + + for (int y = 0; y < blocksRowsPerStep; y++) + { + int yBuffer = y * this.blockAreaSize.Height; + Span colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); + Span blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); + for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) + { + // load 8x8 block from 8 pixel strides + int xColorBufferStart = xBlock * 8; + workspaceBlock.ScaledCopyFrom( + ref colorBufferRow[xColorBufferStart], + destAreaStride); + + // level shift via -128f + workspaceBlock.AddInPlace(-128f); + + // FDCT + FloatingPointDCT.TransformFDCT(ref workspaceBlock); + + // Quantize and save to spectral blocks + Block8x8F.Quantize(ref workspaceBlock, ref blockRow[xBlock], ref this.quantTable); + } + } + } + + public Span GetColorBufferRowSpan(int row) + => this.ColorBuffer.DangerousGetRowSpan(row); + + public void Dispose() + => this.ColorBuffer.Dispose(); + + private void PackColorBuffer() + { + Size factors = this.component.SubSamplingDivisors; + + int packedWidth = this.ColorBuffer.Width / factors.Width; + + float averageMultiplier = 1f / (factors.Width * factors.Height); + for (int i = 0; i < this.ColorBuffer.Height; i += factors.Height) + { + Span sourceRow = this.ColorBuffer.DangerousGetRowSpan(i); + + // vertical sum + for (int j = 1; j < factors.Height; j++) + { + SumVertical(sourceRow, this.ColorBuffer.DangerousGetRowSpan(i + j)); + } + + // horizontal sum + SumHorizontal(sourceRow, factors.Width); + + // calculate average + MultiplyToAverage(sourceRow, averageMultiplier); + + // copy to the first 8 slots + sourceRow.Slice(0, packedWidth).CopyTo(this.ColorBuffer.DangerousGetRowSpan(i / factors.Height)); + } + + static void SumVertical(Span target, Span source) + { + if (Avx.IsSupported) + { + ref Vector256 targetVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(target)); + ref Vector256 sourceVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + + // Spans are guaranteed to be multiple of 8 so no extra 'remainder' steps are needed + DebugGuard.IsTrue(source.Length % 8 == 0, "source must be multiple of 8"); + nuint count = source.Vector256Count(); + for (nuint i = 0; i < count; i++) + { + Unsafe.Add(ref targetVectorRef, i) = Avx.Add(Unsafe.Add(ref targetVectorRef, i), Unsafe.Add(ref sourceVectorRef, i)); + } + } + else if (AdvSimd.IsSupported) + { + ref Vector128 targetVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(target)); + ref Vector128 sourceVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + + // Spans are guaranteed to be multiple of 8 so no extra 'remainder' steps are needed + DebugGuard.IsTrue(source.Length % 8 == 0, "source must be multiple of 8"); + nuint count = source.Vector128Count(); + for (nuint i = 0; i < count; i++) + { + Unsafe.Add(ref targetVectorRef, i) = AdvSimd.Add(Unsafe.Add(ref targetVectorRef, i), Unsafe.Add(ref sourceVectorRef, i)); + } + } + else + { + ref Vector targetVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(target)); + ref Vector sourceVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + + nuint count = source.VectorCount(); + for (nuint i = 0; i < count; i++) + { + Unsafe.Add(ref targetVectorRef, i) += Unsafe.Add(ref sourceVectorRef, i); + } + + ref float targetRef = ref MemoryMarshal.GetReference(target); + ref float sourceRef = ref MemoryMarshal.GetReference(source); + for (nuint i = count * (uint)Vector.Count; i < (uint)source.Length; i++) + { + Unsafe.Add(ref targetRef, i) += Unsafe.Add(ref sourceRef, i); + } + } + } + + static void SumHorizontal(Span target, int factor) + { + Span source = target; + if (Avx2.IsSupported) + { + ref Vector256 targetRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(target)); + + // Ideally we need to use log2: Numerics.Log2((uint)factor) + // but division by 2 works just fine in this case + uint haddIterationsCount = (uint)factor / 2; + + // Transform spans so that it only contains 'remainder' + // values for the scalar fallback code + int scalarRemainder = target.Length % (Vector.Count * factor); + int touchedCount = target.Length - scalarRemainder; + source = source.Slice(touchedCount); + target = target.Slice(touchedCount / factor); + + nuint length = Numerics.Vector256Count(touchedCount); + + for (uint i = 0; i < haddIterationsCount; i++) + { + length /= 2; + + for (nuint j = 0; j < length; j++) + { + nuint indexLeft = j * 2; + nuint indexRight = indexLeft + 1; + Vector256 sum = Avx.HorizontalAdd(Unsafe.Add(ref targetRef, indexLeft), Unsafe.Add(ref targetRef, indexRight)); + Unsafe.Add(ref targetRef, j) = Avx2.Permute4x64(sum.AsDouble(), 0b11_01_10_00).AsSingle(); + } + } + } + + // scalar remainder + for (int i = 0; i < source.Length / factor; i++) + { + target[i] = source[i * factor]; + for (int j = 1; j < factor; j++) + { + target[i] += source[(i * factor) + j]; + } + } + } + + static void MultiplyToAverage(Span target, float multiplier) + { + if (Avx.IsSupported) + { + ref Vector256 targetVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(target)); + + // Spans are guaranteed to be multiple of 8 so no extra 'remainder' steps are needed + DebugGuard.IsTrue(target.Length % 8 == 0, "target must be multiple of 8"); + nuint count = target.Vector256Count(); + Vector256 multiplierVector = Vector256.Create(multiplier); + for (nuint i = 0; i < count; i++) + { + Unsafe.Add(ref targetVectorRef, i) = Avx.Multiply(Unsafe.Add(ref targetVectorRef, i), multiplierVector); + } + } + else if (AdvSimd.IsSupported) + { + ref Vector128 targetVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(target)); + + // Spans are guaranteed to be multiple of 8 so no extra 'remainder' steps are needed + DebugGuard.IsTrue(target.Length % 8 == 0, "target must be multiple of 8"); + nuint count = target.Vector128Count(); + Vector128 multiplierVector = Vector128.Create(multiplier); + for (nuint i = 0; i < count; i++) + { + Unsafe.Add(ref targetVectorRef, i) = AdvSimd.Multiply(Unsafe.Add(ref targetVectorRef, i), multiplierVector); + } + } + else + { + ref Vector targetVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(target)); + + nuint count = target.VectorCount(); + Vector multiplierVector = new(multiplier); + for (nuint i = 0; i < count; i++) + { + Unsafe.Add(ref targetVectorRef, i) *= multiplierVector; + } + + ref float targetRef = ref MemoryMarshal.GetReference(target); + for (nuint i = count * (uint)Vector.Count; i < (uint)target.Length; i++) + { + Unsafe.Add(ref targetRef, i) *= multiplier; + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegComponentConfig.cs b/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegComponentConfig.cs new file mode 100644 index 0000000..62ca562 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegComponentConfig.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { + internal class JpegComponentConfig + { + public JpegComponentConfig(byte id, int hsf, int vsf, int quantIndex, int dcIndex, int acIndex) + { + this.Id = id; + this.HorizontalSampleFactor = hsf; + this.VerticalSampleFactor = vsf; + this.QuantizatioTableIndex = quantIndex; + this.DcTableSelector = dcIndex; + this.AcTableSelector = acIndex; + } + + public byte Id { get; } + + public int HorizontalSampleFactor { get; } + + public int VerticalSampleFactor { get; } + + public int QuantizatioTableIndex { get; } + + public int DcTableSelector { get; } + + public int AcTableSelector { get; } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegFrameConfig.cs b/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegFrameConfig.cs new file mode 100644 index 0000000..57b35ff --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegFrameConfig.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { + internal class JpegFrameConfig + { + public JpegFrameConfig(JpegColorSpace colorType, JpegColorType encodingColor, JpegComponentConfig[] components, JpegHuffmanTableConfig[] huffmanTables, JpegQuantizationTableConfig[] quantTables) + { + this.ColorType = colorType; + this.EncodingColor = encodingColor; + this.Components = components; + this.HuffmanTables = huffmanTables; + this.QuantizationTables = quantTables; + + this.MaxHorizontalSamplingFactor = components[0].HorizontalSampleFactor; + this.MaxVerticalSamplingFactor = components[0].VerticalSampleFactor; + for (int i = 1; i < components.Length; i++) + { + JpegComponentConfig component = components[i]; + this.MaxHorizontalSamplingFactor = Math.Max(this.MaxHorizontalSamplingFactor, component.HorizontalSampleFactor); + this.MaxVerticalSamplingFactor = Math.Max(this.MaxVerticalSamplingFactor, component.VerticalSampleFactor); + } + } + + public JpegColorSpace ColorType { get; } + + public JpegColorType EncodingColor { get; } + + public JpegComponentConfig[] Components { get; } + + public JpegHuffmanTableConfig[] HuffmanTables { get; } + + public JpegQuantizationTableConfig[] QuantizationTables { get; } + + public int MaxHorizontalSamplingFactor { get; } + + public int MaxVerticalSamplingFactor { get; } + + public byte? AdobeColorTransformMarkerFlag { get; set; } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegHuffmanTableConfig.cs b/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegHuffmanTableConfig.cs new file mode 100644 index 0000000..f9e7f14 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegHuffmanTableConfig.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { + internal class JpegHuffmanTableConfig + { + public JpegHuffmanTableConfig(int @class, int destIndex, HuffmanSpec table) + { + this.Class = @class; + this.DestinationIndex = destIndex; + this.Table = table; + } + + public int Class { get; } + + public int DestinationIndex { get; } + + public HuffmanSpec Table { get; } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegQuantizationTableConfig.cs b/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegQuantizationTableConfig.cs new file mode 100644 index 0000000..d1e5606 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegQuantizationTableConfig.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { + internal class JpegQuantizationTableConfig + { + public JpegQuantizationTableConfig(int destIndex, ReadOnlySpan quantizationTable) + { + this.DestinationIndex = destIndex; + this.Table = Block8x8.Load(quantizationTable); + } + + public int DestinationIndex { get; } + + public Block8x8 Table { get; } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanLut.cs b/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanLut.cs new file mode 100644 index 0000000..887b8ab --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanLut.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { + /// + /// A compiled look-up table representation of a huffmanSpec. + /// The maximum codeword size is 16 bits. + /// + /// + /// + /// Each value maps to a int32 of which the 24 most significant bits hold the + /// codeword in bits and the 8 least significant bits hold the codeword size. + /// + /// + /// Code value occupies 24 most significant bits as integer value. + /// This value is shifted to the MSB position for performance reasons. + /// For example, decimal value 10 is stored like this: + /// + /// MSB LSB + /// 1010 0000 00000000 00000000 | 00000100 + /// + /// This was done to eliminate extra binary shifts in the encoder. + /// While code length is represented as 8 bit integer value + /// + /// + internal readonly struct HuffmanLut + { + /// + /// Initializes a new instance of the struct. + /// + /// dasd + public HuffmanLut(HuffmanSpec spec) + { + int maxValue = 0; + + foreach (byte v in spec.Values) + { + if (v > maxValue) + { + maxValue = v; + } + } + + this.Values = new int[maxValue + 1]; + + int code = 0; + int k = 0; + + for (int i = 0; i < spec.Count.Length; i++) + { + int len = i + 1; + for (int j = 0; j < spec.Count[i]; j++) + { + this.Values[spec.Values[k]] = len | (code << (32 - len)); + code++; + k++; + } + + code <<= 1; + } + } + + /// + /// Gets the collection of huffman values. + /// + public int[] Values { get; } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs new file mode 100644 index 0000000..b9b2110 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs @@ -0,0 +1,844 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { + internal class HuffmanScanEncoder + { + /// + /// Maximum number of bytes encoded jpeg 8x8 block can occupy. + /// It's highly unlikely for block to occupy this much space - it's a theoretical limit. + /// + /// + /// Where 16 is maximum huffman code binary length according to itu + /// specs. 10 is maximum value binary length, value comes from discrete + /// cosine tranform with value range: [-1024..1023]. Block stores + /// 8x8 = 64 values thus multiplication by 64. Then divided by 8 to get + /// the number of bytes. This value is then multiplied by + /// for performance reasons. + /// + private const int MaxBytesPerBlock = (16 + 10) * 64 / 8 * MaxBytesPerBlockMultiplier; + + /// + /// Multiplier used within cache buffers size calculation. + /// + /// + /// + /// Theoretically, bytes buffer can fit + /// exactly one minimal coding unit. In reality, coding blocks occupy much + /// less space than the theoretical maximum - this can be exploited. + /// If temporal buffer size is multiplied by at least 2, second half of + /// the resulting buffer will be used as an overflow 'guard' if next + /// block would occupy maximum number of bytes. While first half may fit + /// many blocks before needing to flush. + /// + /// + /// This is subject to change. This can be equal to 1 but recomended + /// value is 2 or even greater - futher benchmarking needed. + /// + /// + private const int MaxBytesPerBlockMultiplier = 2; + + /// + /// size multiplier. + /// + /// + /// Jpeg specification requiers to insert 'stuff' bytes after each + /// 0xff byte value. Worst case scenarion is when all bytes are 0xff. + /// While it's highly unlikely (if not impossible) to get such + /// combination, it's theoretically possible so buffer size must be guarded. + /// + private const int OutputBufferLengthMultiplier = 2; + + /// + /// The DC Huffman tables. + /// + private readonly HuffmanLut[] dcHuffmanTables = new HuffmanLut[4]; + + /// + /// The AC Huffman tables. + /// + private readonly HuffmanLut[] acHuffmanTables = new HuffmanLut[4]; + + /// + /// Emitted bits 'micro buffer' before being transferred to the . + /// + private uint accumulatedBits; + + /// + /// Buffer for temporal storage of huffman rle encoding bit data. + /// + /// + /// Encoding bits are assembled to 4 byte unsigned integers and then copied to this buffer. + /// This process does NOT include inserting stuff bytes. + /// + private readonly uint[] emitBuffer; + + /// + /// Buffer for temporal storage which is then written to the output stream. + /// + /// + /// Encoding bits from are copied to this byte buffer including stuff bytes. + /// + private readonly byte[] streamWriteBuffer; + + private readonly int restartInterval; + + /// + /// Number of jagged bits stored in + /// + private int bitCount; + + private int emitWriteIndex; + + /// + /// The output stream. All attempted writes after the first error become no-ops. + /// + private readonly Stream target; + + /// + /// Initializes a new instance of the class. + /// + /// Amount of encoded 8x8 blocks per single jpeg macroblock. + /// Numbers of MCUs between restart markers. + /// Output stream for saving encoded data. + public HuffmanScanEncoder(int blocksPerCodingUnit, int restartInterval, Stream outputStream) + { + int emitBufferByteLength = MaxBytesPerBlock * blocksPerCodingUnit; + this.emitBuffer = new uint[emitBufferByteLength / sizeof(uint)]; + this.emitWriteIndex = this.emitBuffer.Length; + + this.restartInterval = restartInterval; + + this.streamWriteBuffer = new byte[emitBufferByteLength * OutputBufferLengthMultiplier]; + + this.target = outputStream; + } + + /// + /// Gets a value indicating whether is full + /// and must be flushed using + /// before encoding next 8x8 coding block. + /// + private bool IsStreamFlushNeeded + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.emitWriteIndex < (int)((uint)this.emitBuffer.Length / 2); + } + + public void BuildHuffmanTable(JpegHuffmanTableConfig tableConfig) + { + HuffmanLut[] tables = tableConfig.Class == 0 ? this.dcHuffmanTables : this.acHuffmanTables; + tables[tableConfig.DestinationIndex] = new HuffmanLut(tableConfig.Table); + } + + /// + /// Encodes scan in baseline interleaved mode. + /// + /// Output color space. + /// Frame to encode. + /// Converter from color to spectral. + /// The token to request cancellation. + public void EncodeScanBaselineInterleaved(JpegColorType color, JpegFrame frame, SpectralConverter converter, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + switch (color) + { + case JpegColorType.YCbCrRatio444: + case JpegColorType.Rgb: + this.EncodeThreeComponentBaselineInterleavedScanNoSubsampling(frame, converter, cancellationToken); + break; + default: + this.EncodeScanBaselineInterleaved(frame, converter, cancellationToken); + break; + } + } + + /// + /// Encodes grayscale scan in baseline interleaved mode. + /// + /// Component with grayscale data. + /// Converter from color to spectral. + /// The token to request cancellation. + public void EncodeScanBaselineSingleComponent(Component component, SpectralConverter converter, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + int h = component.HeightInBlocks; + int w = component.WidthInBlocks; + + ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; + ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; + + for (int i = 0; i < h; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Convert from pixels to spectral via given converter + converter.ConvertStrideBaseline(); + + // Encode spectral to binary + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: 0); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (nuint k = 0; k < (uint)w; k++) + { + this.WriteBlock( + component, + ref Unsafe.Add(ref blockRef, k), + ref dcHuffmanTable, + ref acHuffmanTable); + + if (this.IsStreamFlushNeeded) + { + this.FlushToStream(); + } + } + } + + this.FlushRemainingBytes(); + } + + /// + /// Encodes scan with a single component in baseline non-interleaved mode. + /// + /// Component with grayscale data. + /// The token to request cancellation. + public void EncodeScanBaseline(Component component, CancellationToken cancellationToken) + { + int h = component.HeightInBlocks; + int w = component.WidthInBlocks; + + ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; + ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; + + int restarts = 0; + int restartsToGo = this.restartInterval; + + for (int i = 0; i < h; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Encode spectral to binary + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: i); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (nuint k = 0; k < (uint)w; k++) + { + if (this.restartInterval > 0 && restartsToGo == 0) + { + this.FlushRemainingBytes(); + this.WriteRestart(restarts % 8); + component.DcPredictor = 0; + } + + this.WriteBlock( + component, + ref Unsafe.Add(ref blockRef, k), + ref dcHuffmanTable, + ref acHuffmanTable); + + if (this.IsStreamFlushNeeded) + { + this.FlushToStream(); + } + + if (this.restartInterval > 0) + { + if (restartsToGo == 0) + { + restartsToGo = this.restartInterval; + restarts++; + } + + restartsToGo--; + } + } + } + + this.FlushRemainingBytes(); + } + + /// + /// Encodes the DC coefficients for a given component's blocks in a scan. + /// + /// The component whose DC coefficients need to be encoded. + /// The token to request cancellation. + public void EncodeDcScan(Component component, CancellationToken cancellationToken) + { + int h = component.HeightInBlocks; + int w = component.WidthInBlocks; + + ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; + + int restarts = 0; + int restartsToGo = this.restartInterval; + + for (int i = 0; i < h; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: i); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (nuint k = 0; k < (uint)w; k++) + { + if (this.restartInterval > 0 && restartsToGo == 0) + { + this.FlushRemainingBytes(); + this.WriteRestart(restarts % 8); + component.DcPredictor = 0; + } + + this.WriteDc( + component, + ref Unsafe.Add(ref blockRef, k), + ref dcHuffmanTable); + + if (this.IsStreamFlushNeeded) + { + this.FlushToStream(); + } + + if (this.restartInterval > 0) + { + if (restartsToGo == 0) + { + restartsToGo = this.restartInterval; + restarts++; + } + + restartsToGo--; + } + } + } + + this.FlushRemainingBytes(); + } + + /// + /// Encodes the AC coefficients for a specified range of blocks in a component's scan. + /// + /// The component whose AC coefficients need to be encoded. + /// The starting index of the AC coefficient range to encode. + /// The ending index of the AC coefficient range to encode. + /// The token to request cancellation. + public void EncodeAcScan(Component component, nint start, nint end, CancellationToken cancellationToken) + { + int h = component.HeightInBlocks; + int w = component.WidthInBlocks; + + int restarts = 0; + int restartsToGo = this.restartInterval; + + ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; + + for (int i = 0; i < h; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: i); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (nuint k = 0; k < (uint)w; k++) + { + if (this.restartInterval > 0 && restartsToGo == 0) + { + this.FlushRemainingBytes(); + this.WriteRestart(restarts % 8); + } + + this.WriteAcBlock( + ref Unsafe.Add(ref blockRef, k), + start, + end, + ref acHuffmanTable); + + if (this.IsStreamFlushNeeded) + { + this.FlushToStream(); + } + + if (this.restartInterval > 0) + { + if (restartsToGo == 0) + { + restartsToGo = this.restartInterval; + restarts++; + } + + restartsToGo--; + } + } + } + + this.FlushRemainingBytes(); + } + + /// + /// Encodes scan in baseline interleaved mode for any amount of component with arbitrary sampling factors. + /// + /// Frame to encode. + /// Converter from color to spectral. + /// The token to request cancellation. + private void EncodeScanBaselineInterleaved(JpegFrame frame, SpectralConverter converter, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + int mcu = 0; + int mcusPerColumn = frame.McusPerColumn; + int mcusPerLine = frame.McusPerLine; + + int restarts = 0; + int restartsToGo = this.restartInterval; + + for (int j = 0; j < mcusPerColumn; j++) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Convert from pixels to spectral via given converter + converter.ConvertStrideBaseline(); + + // Encode spectral to binary + for (int i = 0; i < mcusPerLine; i++) + { + if (this.restartInterval > 0 && restartsToGo == 0) + { + this.FlushRemainingBytes(); + this.WriteRestart(restarts % 8); + foreach (Component component in frame.Components) + { + component.DcPredictor = 0; + } + } + + // Scan an interleaved mcu... process components in order + int mcuCol = mcu % mcusPerLine; + for (int k = 0; k < frame.Components.Length; k++) + { + Component component = frame.Components[k]; + + ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; + ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; + + int h = component.HorizontalSamplingFactor; + int v = component.VerticalSamplingFactor; + + nuint blockColBase = (uint)(mcuCol * h); + + // Scan out an mcu's worth of this component; that's just determined + // by the basic H and V specified for the component + for (int y = 0; y < v; y++) + { + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (nuint x = 0; x < (uint)h; x++) + { + nuint blockCol = blockColBase + x; + + this.WriteBlock( + component, + ref Unsafe.Add(ref blockRef, blockCol), + ref dcHuffmanTable, + ref acHuffmanTable); + } + } + } + + // After all interleaved components, that's an interleaved MCU + mcu++; + if (this.IsStreamFlushNeeded) + { + this.FlushToStream(); + } + + if (this.restartInterval > 0) + { + if (restartsToGo == 0) + { + restartsToGo = this.restartInterval; + restarts++; + } + + restartsToGo--; + } + } + } + + this.FlushRemainingBytes(); + } + + /// + /// Encodes scan in baseline interleaved mode with exactly 3 components with no subsampling. + /// + /// Frame to encode. + /// Converter from color to spectral. + /// The token to request cancellation. + private void EncodeThreeComponentBaselineInterleavedScanNoSubsampling(JpegFrame frame, SpectralConverter converter, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + nuint mcusPerColumn = (uint)frame.McusPerColumn; + nuint mcusPerLine = (uint)frame.McusPerLine; + + Component c2 = frame.Components[2]; + Component c1 = frame.Components[1]; + Component c0 = frame.Components[0]; + + ref HuffmanLut c0dcHuffmanTable = ref this.dcHuffmanTables[c0.DcTableId]; + ref HuffmanLut c0acHuffmanTable = ref this.acHuffmanTables[c0.AcTableId]; + ref HuffmanLut c1dcHuffmanTable = ref this.dcHuffmanTables[c1.DcTableId]; + ref HuffmanLut c1acHuffmanTable = ref this.acHuffmanTables[c1.AcTableId]; + ref HuffmanLut c2dcHuffmanTable = ref this.dcHuffmanTables[c2.DcTableId]; + ref HuffmanLut c2acHuffmanTable = ref this.acHuffmanTables[c2.AcTableId]; + + ref Block8x8 c0BlockRef = ref MemoryMarshal.GetReference(c0.SpectralBlocks.DangerousGetRowSpan(y: 0)); + ref Block8x8 c1BlockRef = ref MemoryMarshal.GetReference(c1.SpectralBlocks.DangerousGetRowSpan(y: 0)); + ref Block8x8 c2BlockRef = ref MemoryMarshal.GetReference(c2.SpectralBlocks.DangerousGetRowSpan(y: 0)); + + for (nuint j = 0; j < mcusPerColumn; j++) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Convert from pixels to spectral via given converter + converter.ConvertStrideBaseline(); + + // Encode spectral to binary + for (nuint i = 0; i < mcusPerLine; i++) + { + this.WriteBlock( + c0, + ref Unsafe.Add(ref c0BlockRef, i), + ref c0dcHuffmanTable, + ref c0acHuffmanTable); + + this.WriteBlock( + c1, + ref Unsafe.Add(ref c1BlockRef, i), + ref c1dcHuffmanTable, + ref c1acHuffmanTable); + + this.WriteBlock( + c2, + ref Unsafe.Add(ref c2BlockRef, i), + ref c2dcHuffmanTable, + ref c2acHuffmanTable); + + if (this.IsStreamFlushNeeded) + { + this.FlushToStream(); + } + } + } + + this.FlushRemainingBytes(); + } + + private void WriteDc( + Component component, + ref Block8x8 block, + ref HuffmanLut dcTable) + { + // Emit the DC delta. + int dc = block[0]; + this.EmitHuffRLE(dcTable.Values, 0, dc - component.DcPredictor); + component.DcPredictor = dc; + } + + private void WriteAcBlock( + ref Block8x8 block, + nint start, + nint end, + ref HuffmanLut acTable) + { + // Emit the AC components. + int[] acHuffTable = acTable.Values; + + int runLength = 0; + ref short blockRef = ref Unsafe.As(ref block); + for (nint zig = start; zig < end; zig++) + { + const int zeroRun1 = 1 << 4; + const int zeroRun16 = 16 << 4; + + int ac = Unsafe.Add(ref blockRef, zig); + if (ac == 0) + { + runLength += zeroRun1; + } + else + { + while (runLength >= zeroRun16) + { + this.EmitHuff(acHuffTable, 0xf0); + runLength -= zeroRun16; + } + + this.EmitHuffRLE(acHuffTable, runLength, ac); + runLength = 0; + } + } + + // if mcu block contains trailing zeros - we must write end of block (EOB) value indicating that current block is over + if (runLength > 0) + { + this.EmitHuff(acHuffTable, 0x00); + } + } + + private void WriteBlock( + Component component, + ref Block8x8 block, + ref HuffmanLut dcTable, + ref HuffmanLut acTable) + { + this.WriteDc(component, ref block, ref dcTable); + this.WriteAcBlock(ref block, 1, 64, ref acTable); + } + + private void WriteRestart(int restart) => + this.target.Write([0xff, (byte)(JpegConstants.Markers.RST0 + restart)], 0, 2); + + /// + /// Emits the most significant count of bits to the buffer. + /// + /// + /// + /// Supports up to 32 count of bits but, generally speaking, jpeg + /// standard assures that there won't be more than 16 bits per single + /// value. + /// + /// + /// Emitting algorithm uses 3 intermediate buffers for caching before + /// writing to the stream: + /// + /// + /// uint32 + /// + /// Bit buffer. Encoded spectral values can occupy up to 16 bits, bits + /// are assembled to whole bytes via this intermediate buffer. + /// + /// + /// + /// uint32[] + /// + /// Assembled bytes from uint32 buffer are saved into this buffer. + /// uint32 buffer values are saved using indices from the last to the first. + /// As bytes are saved to the memory as 4-byte packages endianness matters: + /// Jpeg stream is big-endian, indexing buffer bytes from the last index to the + /// first eliminates all operations to extract separate bytes. This only works for + /// little-endian machines (there are no known examples of big-endian users atm). + /// For big-endians this approach is slower due to the separate byte extraction. + /// + /// + /// + /// byte[] + /// + /// Byte buffer used only during method. + /// + /// + /// + /// + /// + /// Bits to emit, must be shifted to the left. + /// Bits count stored in the bits parameter. + [MethodImpl(InliningOptions.ShortMethod)] + private void Emit(uint bits, int count) + { + this.accumulatedBits |= bits >> this.bitCount; + + count += this.bitCount; + + if (count >= 32) + { + this.emitBuffer[--this.emitWriteIndex] = this.accumulatedBits; + this.accumulatedBits = bits << (32 - this.bitCount); + + count -= 32; + } + + this.bitCount = count; + } + + /// + /// Emits the given value with the given Huffman table. + /// + /// Huffman table. + /// Value to encode. + [MethodImpl(InliningOptions.ShortMethod)] + private void EmitHuff(int[] table, int value) + { + int x = table[value]; + this.Emit((uint)x & 0xffff_ff00u, x & 0xff); + } + + /// + /// Emits given value via huffman rle encoding. + /// + /// Huffman table. + /// The number of preceding zeroes, preshifted by 4 to the left. + /// Value to encode. + [MethodImpl(InliningOptions.ShortMethod)] + private void EmitHuffRLE(int[] table, int runLength, int value) + { + DebugGuard.IsTrue((runLength & 0xf) == 0, $"{nameof(runLength)} parameter must be shifted to the left by 4 bits"); + + int a = value; + int b = value; + if (a < 0) + { + a = -value; + b = value - 1; + } + + int valueLen = GetHuffmanEncodingLength((uint)a); + + // Huffman prefix code + int huffPackage = table[runLength | valueLen]; + int prefixLen = huffPackage & 0xff; + uint prefix = (uint)huffPackage & 0xffff_0000u; + + // Actual encoded value + uint encodedValue = (uint)b << (32 - valueLen); + + // Doing two binary shifts to get rid of leading 1's in negative value case + this.Emit(prefix | (encodedValue >> prefixLen), prefixLen + valueLen); + } + + /// + /// Calculates how many minimum bits needed to store given value for Huffman jpeg encoding. + /// + /// + /// This is an internal operation supposed to be used only in class for jpeg encoding. + /// + /// The value. + [MethodImpl(InliningOptions.ShortMethod)] + internal static int GetHuffmanEncodingLength(uint value) + { + DebugGuard.IsTrue(value <= (1 << 16), "Huffman encoder is supposed to encode a value of 16bit size max"); + + // This should have been implemented as (BitOperations.Log2(value) + 1) as in non-intrinsic implementation + // But internal log2 is implemented like this: (31 - (int)Lzcnt.LeadingZeroCount(value)) + + // BitOperations.Log2 implementation also checks if input value is zero for the convention 0->0 + // Lzcnt would return 32 for input value of 0 - no need to check that with branching + // Fallback code if Lzcnt is not supported still use if-check + // But most modern CPUs support this instruction so this should not be a problem + return 32 - BitOperations.LeadingZeroCount(value); + } + + /// + /// General method for flushing cached spectral data bytes to + /// the ouput stream respecting stuff bytes. + /// + /// + /// Bytes cached via are stored in 4-bytes blocks + /// which makes this method endianness dependent. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void FlushToStream(int endIndex) + { + Span emitBytes = MemoryMarshal.AsBytes(this.emitBuffer.AsSpan()); + + int writeIdx = 0; + int startIndex = emitBytes.Length - 1; + + // Some platforms may fail to eliminate this if-else branching + // Even if it happens - buffer is flushed in big packs, + // branching overhead shouldn't be noticeable + if (BitConverter.IsLittleEndian) + { + // For little endian case bytes are ordered and can be + // safely written to the stream with stuff bytes + // First byte is cached on the most significant index + // so we are going from the end of the array to its beginning: + // ... [ double word #1 ] [ double word #0 ] + // ... [idx3|idx2|idx1|idx0] [idx3|idx2|idx1|idx0] + for (int i = startIndex; i >= endIndex; i--) + { + byte value = emitBytes[i]; + this.streamWriteBuffer[writeIdx++] = value; + + // Inserting stuff byte + if (value == 0xff) + { + this.streamWriteBuffer[writeIdx++] = 0x00; + } + } + } + else + { + // For big endian case bytes are ordered in 4-byte packs + // which are ordered like bytes in the little endian case by in 4-byte packs: + // ... [ double word #1 ] [ double word #0 ] + // ... [idx0|idx1|idx2|idx3] [idx0|idx1|idx2|idx3] + // So we must write each 4-bytes in 'natural order' + for (int i = startIndex; i >= endIndex; i -= 4) + { + // This loop is caused by the nature of underlying byte buffer + // implementation and indeed causes performace by somewhat 5% + // compared to little endian scenario + // Even with this performance drop this cached buffer implementation + // is faster than individually writing bytes using binary shifts and binary and(s) + for (int j = i - 3; j <= i; j++) + { + byte value = emitBytes[j]; + this.streamWriteBuffer[writeIdx++] = value; + + // Inserting stuff byte + if (value == 0xff) + { + this.streamWriteBuffer[writeIdx++] = 0x00; + } + } + } + } + + this.target.Write(this.streamWriteBuffer, 0, writeIdx); + this.emitWriteIndex = this.emitBuffer.Length; + } + + /// + /// Flushes spectral data bytes after encoding all channel blocks + /// in a single jpeg macroblock using . + /// + /// + /// This must be called only if is true + /// only during the macroblocks encoding routine. + /// + private void FlushToStream() => + this.FlushToStream(this.emitWriteIndex * 4); + + /// + /// Flushes final cached bits to the stream padding 1's to + /// complement full bytes. + /// + /// + /// This must be called only once at the end of the encoding routine. + /// check is not needed. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void FlushRemainingBytes() + { + // Padding all 4 bytes with 1's while not corrupting initial bits stored in accumulatedBits + // And writing only valuable count of bytes count we want to write to the output stream + int valuableBytesCount = (int)Numerics.DivideCeil((uint)this.bitCount, 8); + uint packedBytes = this.accumulatedBits | (uint.MaxValue >> this.bitCount); + this.emitBuffer[this.emitWriteIndex - 1] = packedBytes; + + // Flush cached bytes to the output stream with padding bits + int lastByteIndex = (this.emitWriteIndex * 4) - valuableBytesCount; + this.FlushToStream(lastByteIndex); + + // Clear huffman register + // This is needed for for images with multiples scans + this.bitCount = 0; + this.accumulatedBits = 0; + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs b/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs new file mode 100644 index 0000000..83d6ceb --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs @@ -0,0 +1,141 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { + /// + /// The Huffman encoding specifications. + /// + internal readonly struct HuffmanSpec + { + /// + /// Huffman talbe specification for luminance DC. + /// + /// + /// This is an example specification taken from the jpeg specification paper. + /// + public static readonly HuffmanSpec LuminanceDC = new( + [ + 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 0, 0 + ], + [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 + ]); + + /// + /// Huffman talbe specification for luminance AC. + /// + /// + /// This is an example specification taken from the jpeg specification paper. + /// + public static readonly HuffmanSpec LuminanceAC = new( + [ + 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, + 0, 1, 125 + ], + [ + 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, + 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, + 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, + 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, + 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, + 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, + 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, + 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, + 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, + 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, + 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, + 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, + 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, + 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, + 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, + 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, + 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, + 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, + 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, + 0xf9, 0xfa + ]); + + /// + /// Huffman talbe specification for chrominance DC. + /// + /// + /// This is an example specification taken from the jpeg specification paper. + /// + public static readonly HuffmanSpec ChrominanceDC = new( + [ + 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, + 0, 0, 0 + ], + [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 + ]); + + /// + /// Huffman talbe specification for chrominance DC. + /// + /// + /// This is an example specification taken from the jpeg specification paper. + /// + public static readonly HuffmanSpec ChrominanceAC = new( + [ + 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, + 1, 2, 119 + ], + [ + 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, + 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, + 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, + 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, + 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, + 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, + 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, + 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, + 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, + 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, + 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, + 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, + 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, + 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, + 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, + 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, + 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, + 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, + 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, + 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, 0xe3, + 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, + 0xf9, 0xfa + ]); + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The number of codes. + /// + /// + /// The decoded values. + /// + public HuffmanSpec(byte[] count, byte[] values) + { + this.Count = count; + this.Values = values; + } + + /// + /// Gets the count[i] - The number of codes of length i bits. + /// + public readonly byte[] Count { get; } + + /// + /// Gets the value[i] - The decoded value of the codeword at the given index. + /// + public readonly byte[] Values { get; } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs b/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs new file mode 100644 index 0000000..fc59832 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs @@ -0,0 +1,84 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using System; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { + /// + /// Represent a single jpeg frame. + /// + internal sealed class JpegFrame : IDisposable + { + public JpegFrame(Image image, JpegFrameConfig frameConfig, bool interleaved) + { + this.ColorSpace = frameConfig.ColorType; + + this.Interleaved = interleaved; + + this.PixelWidth = image.Width; + this.PixelHeight = image.Height; + + MemoryAllocator allocator = image.Configuration.MemoryAllocator; + + JpegComponentConfig[] componentConfigs = frameConfig.Components; + this.Components = new Component[componentConfigs.Length]; + for (int i = 0; i < this.Components.Length; i++) + { + JpegComponentConfig componentConfig = componentConfigs[i]; + this.Components[i] = new Component(allocator, componentConfig.HorizontalSampleFactor, componentConfig.VerticalSampleFactor, componentConfig.QuantizatioTableIndex) + { + DcTableId = componentConfig.DcTableSelector, + AcTableId = componentConfig.AcTableSelector, + }; + + this.BlocksPerMcu += componentConfig.HorizontalSampleFactor * componentConfig.VerticalSampleFactor; + } + + int maxSubFactorH = frameConfig.MaxHorizontalSamplingFactor; + int maxSubFactorV = frameConfig.MaxVerticalSamplingFactor; + this.McusPerLine = (int)Numerics.DivideCeil((uint)image.Width, (uint)maxSubFactorH * 8); + this.McusPerColumn = (int)Numerics.DivideCeil((uint)image.Height, (uint)maxSubFactorV * 8); + + for (int i = 0; i < this.Components.Length; i++) + { + Component component = this.Components[i]; + component.Init(this, maxSubFactorH, maxSubFactorV); + } + } + + public JpegColorSpace ColorSpace { get; } + + public bool Interleaved { get; } + + public int PixelHeight { get; } + + public int PixelWidth { get; } + + public Component[] Components { get; } + + public int McusPerLine { get; } + + public int McusPerColumn { get; } + + public int BlocksPerMcu { get; } + + public void Dispose() + { + for (int i = 0; i < this.Components.Length; i++) + { + this.Components[i].Dispose(); + } + } + + public void AllocateComponents(bool fullScan) + { + for (int i = 0; i < this.Components.Length; i++) + { + Component component = this.Components[i]; + component.AllocateSpectral(fullScan); + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter.cs b/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter.cs new file mode 100644 index 0000000..ea85fef --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { + /// + /// Converter used to convert pixel data to jpeg spectral data. + /// + internal abstract class SpectralConverter + { + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs b/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs new file mode 100644 index 0000000..337bf35 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs @@ -0,0 +1,149 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Linq; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { + /// + internal class SpectralConverter : SpectralConverter, IDisposable + where TPixel : unmanaged, IPixel + { + private readonly ComponentProcessor[] componentProcessors; + + private readonly int pixelRowsPerStep; + + private int pixelRowCounter; + + private readonly Buffer2D pixelBuffer; + + private readonly IMemoryOwner redLane; + + private readonly IMemoryOwner greenLane; + + private readonly IMemoryOwner blueLane; + + private readonly int alignedPixelWidth; + + private readonly JpegColorConverterBase colorConverter; + + public SpectralConverter(JpegFrame frame, Image image, Block8x8F[] dequantTables) + { + MemoryAllocator allocator = image.Configuration.MemoryAllocator; + + // iteration data + int majorBlockWidth = frame.Components.Max((component) => component.SizeInBlocks.Width); + int majorVerticalSamplingFactor = frame.Components.Max((component) => component.SamplingFactors.Height); + + const int blockPixelHeight = 8; + this.pixelRowsPerStep = majorVerticalSamplingFactor * blockPixelHeight; + + // pixel buffer of the image + this.pixelBuffer = image.GetRootFramePixelBuffer(); + + // component processors from spectral to Rgb24 + const int blockPixelWidth = 8; + this.alignedPixelWidth = majorBlockWidth * blockPixelWidth; + Size postProcessorBufferSize = new(this.alignedPixelWidth, this.pixelRowsPerStep); + this.componentProcessors = new ComponentProcessor[frame.Components.Length]; + for (int i = 0; i < this.componentProcessors.Length; i++) + { + Component component = frame.Components[i]; + this.componentProcessors[i] = new ComponentProcessor( + allocator, + component, + postProcessorBufferSize, + dequantTables[component.QuantizationTableIndex]); + } + + this.redLane = allocator.Allocate(this.alignedPixelWidth, AllocationOptions.Clean); + this.greenLane = allocator.Allocate(this.alignedPixelWidth, AllocationOptions.Clean); + this.blueLane = allocator.Allocate(this.alignedPixelWidth, AllocationOptions.Clean); + + // color converter from Rgb24 to YCbCr + this.colorConverter = JpegColorConverterBase.GetConverter(colorSpace: frame.ColorSpace, precision: 8); + } + + public void ConvertStrideBaseline() + { + // Codestyle suggests expression body but it + // also requires empty line before comments + // which looks ugly with expression bodies thus this warning disable +#pragma warning disable IDE0022 + // Convert next pixel stride using single spectral `stride' + // Note that zero passing eliminates the need of virtual call + // from JpegComponentPostProcessor + this.ConvertStride(spectralStep: 0); +#pragma warning restore IDE0022 + } + + public void ConvertFull() + { + int steps = (int)Numerics.DivideCeil((uint)this.pixelBuffer.Height, (uint)this.pixelRowsPerStep); + for (int i = 0; i < steps; i++) + { + this.ConvertStride(i); + } + } + + private void ConvertStride(int spectralStep) + { + int start = this.pixelRowCounter; + int end = start + this.pixelRowsPerStep; + + int pixelBufferLastVerticalIndex = this.pixelBuffer.Height - 1; + + // Pixel strides must be padded with the last pixel of the stride + int paddingStartIndex = this.pixelBuffer.Width; + int paddedPixelsCount = this.alignedPixelWidth - this.pixelBuffer.Width; + + Span rLane = this.redLane.GetSpan(); + Span gLane = this.greenLane.GetSpan(); + Span bLane = this.blueLane.GetSpan(); + + for (int yy = start; yy < end; yy++) + { + int y = yy - this.pixelRowCounter; + + // Unpack TPixel to r/g/b planes + // TODO: The individual implementation code would be much easier here if + // we scaled to [0-1] before passing to the individual converters. + int srcIndex = Math.Min(yy, pixelBufferLastVerticalIndex); + Span sourceRow = this.pixelBuffer.DangerousGetRowSpan(srcIndex); + PixelOperations.Instance.UnpackIntoRgbPlanes(rLane, gLane, bLane, sourceRow); + + rLane.Slice(paddingStartIndex, paddedPixelsCount).Fill(rLane[paddingStartIndex - 1]); + gLane.Slice(paddingStartIndex, paddedPixelsCount).Fill(gLane[paddingStartIndex - 1]); + bLane.Slice(paddingStartIndex, paddedPixelsCount).Fill(bLane[paddingStartIndex - 1]); + + // Convert from rgb24 to target pixel type + JpegColorConverterBase.ComponentValues values = new(this.componentProcessors, y); + this.colorConverter.ConvertFromRgb(values, rLane, gLane, bLane); + } + + // Convert pixels to spectral + for (int i = 0; i < this.componentProcessors.Length; i++) + { + this.componentProcessors[i].CopyColorBufferToBlocks(spectralStep); + } + + this.pixelRowCounter = end; + } + + /// + public void Dispose() + { + foreach (ComponentProcessor cpp in this.componentProcessors) + { + cpp.Dispose(); + } + + this.redLane.Dispose(); + this.greenLane.Dispose(); + this.blueLane.Dispose(); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.Vector256.cs b/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.Vector256.cs new file mode 100644 index 0000000..f592ea0 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.Vector256.cs @@ -0,0 +1,142 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal static partial class FloatingPointDCT + { + /// + /// Apply floating point FDCT in place using simd operations. + /// + /// Input block. + private static void FDCT8x8_Vector256(ref Block8x8F block) + { + DebugGuard.IsTrue(Vector256.IsHardwareAccelerated, "Vector256 support is required to execute this operation."); + + // First pass - process columns + FDCT8x8_1D_Vector256(ref block); + + // Second pass - process rows + block.TransposeInPlace(); + FDCT8x8_1D_Vector256(ref block); + + // Applies 1D floating point FDCT in place + static void FDCT8x8_1D_Vector256(ref Block8x8F block) + { + Vector256 tmp0 = block.V256_0 + block.V256_7; + Vector256 tmp7 = block.V256_0 - block.V256_7; + Vector256 tmp1 = block.V256_1 + block.V256_6; + Vector256 tmp6 = block.V256_1 - block.V256_6; + Vector256 tmp2 = block.V256_2 + block.V256_5; + Vector256 tmp5 = block.V256_2 - block.V256_5; + Vector256 tmp3 = block.V256_3 + block.V256_4; + Vector256 tmp4 = block.V256_3 - block.V256_4; + + // Even part + Vector256 tmp10 = tmp0 + tmp3; + Vector256 tmp13 = tmp0 - tmp3; + Vector256 tmp11 = tmp1 + tmp2; + Vector256 tmp12 = tmp1 - tmp2; + + block.V256_0 = tmp10 + tmp11; + block.V256_4 = tmp10 - tmp11; + + Vector256 mm256_F_0_7071 = Vector256.Create(0.707106781f); + Vector256 z1 = (tmp12 + tmp13) * mm256_F_0_7071; + block.V256_2 = tmp13 + z1; + block.V256_6 = tmp13 - z1; + + // Odd part + tmp10 = tmp4 + tmp5; + tmp11 = tmp5 + tmp6; + tmp12 = tmp6 + tmp7; + + Vector256 z5 = (tmp10 - tmp12) * Vector256.Create(0.382683433f); // mm256_F_0_3826 + Vector256 z2 = Vector256_.MultiplyAdd(z5, Vector256.Create(0.541196100f), tmp10); // mm256_F_0_5411 + Vector256 z4 = Vector256_.MultiplyAdd(z5, Vector256.Create(1.306562965f), tmp12); // mm256_F_1_3065 + Vector256 z3 = tmp11 * mm256_F_0_7071; + + Vector256 z11 = tmp7 + z3; + Vector256 z13 = tmp7 - z3; + + block.V256_5 = z13 + z2; + block.V256_3 = z13 - z2; + block.V256_1 = z11 + z4; + block.V256_7 = z11 - z4; + } + } + + /// + /// Apply floating point IDCT in place using simd operations. + /// + /// Transposed input block. + private static void IDCT8x8_Vector256(ref Block8x8F transposedBlock) + { + DebugGuard.IsTrue(Vector256.IsHardwareAccelerated, "Vector256 support is required to execute this operation."); + + // First pass - process columns + IDCT8x8_1D_Vector256(ref transposedBlock); + + // Second pass - process rows + transposedBlock.TransposeInPlace(); + IDCT8x8_1D_Vector256(ref transposedBlock); + + // Applies 1D floating point FDCT in place + static void IDCT8x8_1D_Vector256(ref Block8x8F block) + { + // Even part + Vector256 tmp0 = block.V256_0; + Vector256 tmp1 = block.V256_2; + Vector256 tmp2 = block.V256_4; + Vector256 tmp3 = block.V256_6; + + Vector256 z5 = tmp0; + Vector256 tmp10 = z5 + tmp2; + Vector256 tmp11 = z5 - tmp2; + + Vector256 mm256_F_1_4142 = Vector256.Create(1.414213562f); + Vector256 tmp13 = tmp1 + tmp3; + Vector256 tmp12 = Vector256_.MultiplySubtract(tmp13, tmp1 - tmp3, mm256_F_1_4142); + + tmp0 = tmp10 + tmp13; + tmp3 = tmp10 - tmp13; + tmp1 = tmp11 + tmp12; + tmp2 = tmp11 - tmp12; + + // Odd part + Vector256 tmp4 = block.V256_1; + Vector256 tmp5 = block.V256_3; + Vector256 tmp6 = block.V256_5; + Vector256 tmp7 = block.V256_7; + + Vector256 z13 = tmp6 + tmp5; + Vector256 z10 = tmp6 - tmp5; + Vector256 z11 = tmp4 + tmp7; + Vector256 z12 = tmp4 - tmp7; + + tmp7 = z11 + z13; + tmp11 = (z11 - z13) * mm256_F_1_4142; + + z5 = (z10 + z12) * Vector256.Create(1.847759065f); // mm256_F_1_8477 + + tmp10 = Vector256_.MultiplyAdd(z5, z12, Vector256.Create(-1.082392200f)); // mm256_F_n1_0823 + tmp12 = Vector256_.MultiplyAdd(z5, z10, Vector256.Create(-2.613125930f)); // mm256_F_n2_6131 + + tmp6 = tmp12 - tmp7; + tmp5 = tmp11 - tmp6; + tmp4 = tmp10 - tmp5; + + block.V256_0 = tmp0 + tmp7; + block.V256_7 = tmp0 - tmp7; + block.V256_1 = tmp1 + tmp6; + block.V256_6 = tmp1 - tmp6; + block.V256_2 = tmp2 + tmp5; + block.V256_5 = tmp2 - tmp5; + block.V256_3 = tmp3 + tmp4; + block.V256_4 = tmp3 - tmp4; + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.cs b/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.cs new file mode 100644 index 0000000..9e58b54 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.cs @@ -0,0 +1,276 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + /// + /// Contains floating point forward and inverse DCT implementations + /// + /// + /// Based on "Arai, Agui and Nakajima" algorithm. + /// + internal static partial class FloatingPointDCT + { +#pragma warning disable SA1310, SA1311, IDE1006 // naming rules violation warnings + private static readonly Vector4 mm128_F_0_7071 = new(0.707106781f); + private static readonly Vector4 mm128_F_0_3826 = new(0.382683433f); + private static readonly Vector4 mm128_F_0_5411 = new(0.541196100f); + private static readonly Vector4 mm128_F_1_3065 = new(1.306562965f); + + private static readonly Vector4 mm128_F_1_4142 = new(1.414213562f); + private static readonly Vector4 mm128_F_1_8477 = new(1.847759065f); + private static readonly Vector4 mm128_F_n1_0823 = new(-1.082392200f); + private static readonly Vector4 mm128_F_n2_6131 = new(-2.613125930f); +#pragma warning restore SA1310, SA1311, IDE1006 + + /// + /// Gets adjustment table for quantization tables. + /// + /// + /// + /// Current IDCT and FDCT implementations are based on Arai, Agui, + /// and Nakajima's algorithm. Both DCT methods does not + /// produce finished DCT output, final step is fused into the + /// quantization step. Quantization and de-quantization coefficients + /// must be multiplied by these values. + /// + /// + /// Given values were generated by formula: + /// + /// scalefactor[row] * scalefactor[col], where + /// scalefactor[0] = 1 + /// scalefactor[k] = cos(k*PI/16) * sqrt(2) for k=1..7 + /// + /// + /// + private static readonly float[] AdjustmentCoefficients = + [ + 1f, 1.3870399f, 1.306563f, 1.1758755f, 1f, 0.78569496f, 0.5411961f, 0.27589938f, + 1.3870399f, 1.9238797f, 1.812255f, 1.6309863f, 1.3870399f, 1.0897902f, 0.7506606f, 0.38268346f, + 1.306563f, 1.812255f, 1.707107f, 1.5363555f, 1.306563f, 1.02656f, 0.7071068f, 0.36047992f, + 1.1758755f, 1.6309863f, 1.5363555f, 1.3826833f, 1.1758755f, 0.9238795f, 0.63637924f, 0.32442334f, + 1f, 1.3870399f, 1.306563f, 1.1758755f, 1f, 0.78569496f, 0.5411961f, 0.27589938f, + 0.78569496f, 1.0897902f, 1.02656f, 0.9238795f, 0.78569496f, 0.61731654f, 0.42521507f, 0.21677275f, + 0.5411961f, 0.7506606f, 0.7071068f, 0.63637924f, 0.5411961f, 0.42521507f, 0.29289323f, 0.14931567f, + 0.27589938f, 0.38268346f, 0.36047992f, 0.32442334f, 0.27589938f, 0.21677275f, 0.14931567f, 0.076120466f + ]; + + /// + /// Adjusts given quantization table for usage with . + /// + /// Quantization table to adjust. + public static void AdjustToIDCT(ref Block8x8F quantTable) + { + ref float tableRef = ref Unsafe.As(ref quantTable); + ref float multipliersRef = ref MemoryMarshal.GetReference(AdjustmentCoefficients); + for (nuint i = 0; i < Block8x8F.Size; i++) + { + ref float elemRef = ref Unsafe.Add(ref tableRef, i); + elemRef = 0.125f * elemRef * Unsafe.Add(ref multipliersRef, i); + } + + // Spectral macroblocks are transposed before quantization + // so we must transpose quantization table + quantTable.TransposeInPlace(); + } + + /// + /// Adjusts given quantization table for usage with . + /// + /// Quantization table to adjust. + public static void AdjustToFDCT(ref Block8x8F quantTable) + { + ref float tableRef = ref Unsafe.As(ref quantTable); + ref float multipliersRef = ref MemoryMarshal.GetReference(AdjustmentCoefficients); + for (nuint i = 0; i < Block8x8F.Size; i++) + { + ref float elemRef = ref Unsafe.Add(ref tableRef, i); + elemRef = 0.125f / (elemRef * Unsafe.Add(ref multipliersRef, i)); + } + + // Spectral macroblocks are not transposed before quantization + // Transpose is done after quantization at zig-zag stage + // so we must transpose quantization table + quantTable.TransposeInPlace(); + } + + /// + /// Apply 2D floating point IDCT in place. + /// + /// + /// Input block must be dequantized with quantization table + /// adjusted by . + /// + /// Input block. + public static void TransformIDCT(ref Block8x8F block) + { + if (Vector256.IsHardwareAccelerated) + { + IDCT8x8_Vector256(ref block); + } + else + { + IDCT_Vector4(ref block); + } + } + + /// + /// Apply 2D floating point IDCT in place. + /// + /// + /// Input block must be quantized after this method with quantization + /// table adjusted by . + /// + /// Input block. + public static void TransformFDCT(ref Block8x8F block) + { + if (Vector256.IsHardwareAccelerated) + { + FDCT8x8_Vector256(ref block); + } + else + { + FDCT_Vector4(ref block); + } + } + + /// + /// Apply floating point IDCT inplace using API. + /// + /// + /// This method can be used even if there's no SIMD intrinsics available + /// as can be compiled to scalar instructions. + /// + /// Input block. + private static void IDCT_Vector4(ref Block8x8F transposedBlock) + { + // First pass - process columns + IDCT8x4_Vector4(ref transposedBlock.V0L); + IDCT8x4_Vector4(ref transposedBlock.V0R); + + // Second pass - process rows + transposedBlock.TransposeInPlace(); + IDCT8x4_Vector4(ref transposedBlock.V0L); + IDCT8x4_Vector4(ref transposedBlock.V0R); + + // Applies 1D floating point IDCT inplace on 8x4 part of 8x8 block + static void IDCT8x4_Vector4(ref Vector4 vecRef) + { + // Even part + Vector4 tmp0 = Unsafe.Add(ref vecRef, 0 * 2); + Vector4 tmp1 = Unsafe.Add(ref vecRef, 2 * 2); + Vector4 tmp2 = Unsafe.Add(ref vecRef, 4 * 2); + Vector4 tmp3 = Unsafe.Add(ref vecRef, 6 * 2); + + Vector4 z5 = tmp0; + Vector4 tmp10 = z5 + tmp2; + Vector4 tmp11 = z5 - tmp2; + + Vector4 tmp13 = tmp1 + tmp3; + Vector4 tmp12 = ((tmp1 - tmp3) * mm128_F_1_4142) - tmp13; + + tmp0 = tmp10 + tmp13; + tmp3 = tmp10 - tmp13; + tmp1 = tmp11 + tmp12; + tmp2 = tmp11 - tmp12; + + // Odd part + Vector4 tmp4 = Unsafe.Add(ref vecRef, 1 * 2); + Vector4 tmp5 = Unsafe.Add(ref vecRef, 3 * 2); + Vector4 tmp6 = Unsafe.Add(ref vecRef, 5 * 2); + Vector4 tmp7 = Unsafe.Add(ref vecRef, 7 * 2); + + Vector4 z13 = tmp6 + tmp5; + Vector4 z10 = tmp6 - tmp5; + Vector4 z11 = tmp4 + tmp7; + Vector4 z12 = tmp4 - tmp7; + + tmp7 = z11 + z13; + tmp11 = (z11 - z13) * mm128_F_1_4142; + + z5 = (z10 + z12) * mm128_F_1_8477; + + tmp10 = (z12 * mm128_F_n1_0823) + z5; + tmp12 = (z10 * mm128_F_n2_6131) + z5; + + tmp6 = tmp12 - tmp7; + tmp5 = tmp11 - tmp6; + tmp4 = tmp10 - tmp5; + + Unsafe.Add(ref vecRef, 0 * 2) = tmp0 + tmp7; + Unsafe.Add(ref vecRef, 7 * 2) = tmp0 - tmp7; + Unsafe.Add(ref vecRef, 1 * 2) = tmp1 + tmp6; + Unsafe.Add(ref vecRef, 6 * 2) = tmp1 - tmp6; + Unsafe.Add(ref vecRef, 2 * 2) = tmp2 + tmp5; + Unsafe.Add(ref vecRef, 5 * 2) = tmp2 - tmp5; + Unsafe.Add(ref vecRef, 3 * 2) = tmp3 + tmp4; + Unsafe.Add(ref vecRef, 4 * 2) = tmp3 - tmp4; + } + } + + /// + /// Apply floating point FDCT inplace using API. + /// + /// Input block. + private static void FDCT_Vector4(ref Block8x8F block) + { + // First pass - process columns + FDCT8x4_Vector4(ref block.V0L); + FDCT8x4_Vector4(ref block.V0R); + + // Second pass - process rows + block.TransposeInPlace(); + FDCT8x4_Vector4(ref block.V0L); + FDCT8x4_Vector4(ref block.V0R); + + // Applies 1D floating point FDCT inplace on 8x4 part of 8x8 block + static void FDCT8x4_Vector4(ref Vector4 vecRef) + { + Vector4 tmp0 = Unsafe.Add(ref vecRef, 0) + Unsafe.Add(ref vecRef, 14); + Vector4 tmp7 = Unsafe.Add(ref vecRef, 0) - Unsafe.Add(ref vecRef, 14); + Vector4 tmp1 = Unsafe.Add(ref vecRef, 2) + Unsafe.Add(ref vecRef, 12); + Vector4 tmp6 = Unsafe.Add(ref vecRef, 2) - Unsafe.Add(ref vecRef, 12); + Vector4 tmp2 = Unsafe.Add(ref vecRef, 4) + Unsafe.Add(ref vecRef, 10); + Vector4 tmp5 = Unsafe.Add(ref vecRef, 4) - Unsafe.Add(ref vecRef, 10); + Vector4 tmp3 = Unsafe.Add(ref vecRef, 6) + Unsafe.Add(ref vecRef, 8); + Vector4 tmp4 = Unsafe.Add(ref vecRef, 6) - Unsafe.Add(ref vecRef, 8); + + // Even part + Vector4 tmp10 = tmp0 + tmp3; + Vector4 tmp13 = tmp0 - tmp3; + Vector4 tmp11 = tmp1 + tmp2; + Vector4 tmp12 = tmp1 - tmp2; + + Unsafe.Add(ref vecRef, 0) = tmp10 + tmp11; + Unsafe.Add(ref vecRef, 8) = tmp10 - tmp11; + + Vector4 z1 = (tmp12 + tmp13) * mm128_F_0_7071; + Unsafe.Add(ref vecRef, 4) = tmp13 + z1; + Unsafe.Add(ref vecRef, 12) = tmp13 - z1; + + // Odd part + tmp10 = tmp4 + tmp5; + tmp11 = tmp5 + tmp6; + tmp12 = tmp6 + tmp7; + + Vector4 z5 = (tmp10 - tmp12) * mm128_F_0_3826; + Vector4 z2 = (mm128_F_0_5411 * tmp10) + z5; + Vector4 z4 = (mm128_F_1_3065 * tmp12) + z5; + Vector4 z3 = tmp11 * mm128_F_0_7071; + + Vector4 z11 = tmp7 + z3; + Vector4 z13 = tmp7 - z3; + + Unsafe.Add(ref vecRef, 10) = z13 + z2; + Unsafe.Add(ref vecRef, 6) = z13 - z2; + Unsafe.Add(ref vecRef, 2) = z11 + z4; + Unsafe.Add(ref vecRef, 14) = z11 - z4; + } + } + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/JpegColorSpace.cs b/ImageSharp/Formats/Jpeg/Components/JpegColorSpace.cs new file mode 100644 index 0000000..441ce58 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/JpegColorSpace.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + /// + /// Identifies the colorspace of a Jpeg image. + /// + internal enum JpegColorSpace + { + /// + /// Color space with 1 component. + /// + Grayscale, + + /// + /// Color space with 4 components. + /// + Ycck, + + /// + /// Color space with 4 components. + /// + Cmyk, + + /// + /// YccK color space with 4 components, used with tiff images, which use jpeg compression. + /// + TiffYccK, + + /// + /// Cmyk color space with 4 components, used with tiff images, which use jpeg compression. + /// + TiffCmyk, + + /// + /// Color space with 3 components. + /// + RGB, + + /// + /// Color space with 3 components. + /// + YCbCr + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/ImageSharp/Formats/Jpeg/Components/Quantization.cs new file mode 100644 index 0000000..2ba4dfa --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -0,0 +1,211 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + /// + /// Provides methods and properties related to jpeg quantization. + /// + internal static class Quantization + { + /// + /// Upper bound (inclusive) for jpeg quality setting. + /// + public const int MaxQualityFactor = 100; + + /// + /// Lower bound (inclusive) for jpeg quality setting. + /// + public const int MinQualityFactor = 1; + + /// + /// Default JPEG quality for both luminance and chominance tables. + /// + public const int DefaultQualityFactor = 75; + + /// + /// Represents lowest quality setting which can be estimated with enough confidence. + /// Any quality below it results in a highly compressed jpeg image + /// which shouldn't use standard itu quantization tables for re-encoding. + /// + public const int QualityEstimationConfidenceLowerThreshold = 25; + + /// + /// Represents highest quality setting which can be estimated with enough confidence. + /// + public const int QualityEstimationConfidenceUpperThreshold = 98; + + /// + /// Gets unscaled luminance quantization table. + /// + /// + /// The values are derived from ITU section K.1. + /// + // The C# compiler emits this as a compile-time constant embedded in the PE file. + // This is effectively compiled down to: return new ReadOnlySpan(&data, length) + // More details can be found: https://github.com/dotnet/roslyn/pull/24621 + public static ReadOnlySpan LuminanceTable => + [ + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68, 109, 103, 77, + 24, 35, 55, 64, 81, 104, 113, 92, + 49, 64, 78, 87, 103, 121, 120, 101, + 72, 92, 95, 98, 112, 100, 103, 99 + ]; + + /// + /// Gets unscaled chrominance quantization table. + /// + /// + /// The values are derived from ITU section K.1. + /// + // The C# compiler emits this as a compile-time constant embedded in the PE file. + // This is effectively compiled down to: return new ReadOnlySpan(&data, length) + // More details can be found: https://github.com/dotnet/roslyn/pull/24621 + public static ReadOnlySpan ChrominanceTable => + [ + 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99 + ]; + + /// Ported from JPEGsnoop: + /// https://github.com/ImpulseAdventure/JPEGsnoop/blob/9732ee0961f100eb69bbff4a0c47438d5997abee/source/JfifDecode.cpp#L4570-L4694 + /// + /// Estimates jpeg quality based on standard quantization table. + /// + /// + /// Technically, this can be used with any given table but internal decoder code uses ITU spec tables: + /// and . + /// + /// Input quantization table. + /// Natural order quantization table to estimate against. + /// Estimated quality. + public static int EstimateQuality(ref Block8x8F table, ReadOnlySpan target) + { + // This method can be SIMD'ified if standard table is injected as Block8x8F. + // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8. + double comparePercent; + double sumPercent = 0; + + // Corner case - all 1's => 100 quality + // It would fail to deduce using algorithm below without this check + if (table.EqualsToScalar(1)) + { + // While this is a 100% to be 100 quality, any given table can be scaled to all 1's. + // According to jpeg creators, top of the line quality is 99, 100 is just a technical 'limit' which will affect result filesize drastically. + // Quality=100 shouldn't be used in usual use case. + return 100; + } + + int quality; + for (int i = 0; i < Block8x8F.Size; i++) + { + int coeff = (int)table[i]; + + // Coefficients are actually int16 casted to float numbers so there's no truncating error. + if (coeff != 0) + { + comparePercent = 100.0 * (table[i] / target[i]); + } + else + { + // No 'valid' quantization table should contain zero at any position + // while this is okay to decode with, it will throw DivideByZeroException at encoding proces stage. + // Not sure what to do here, we can't throw as this technically correct + // but this will screw up the encoder. + comparePercent = 999.99; + } + + sumPercent += comparePercent; + } + + // Perform some statistical analysis of the quality factor + // to determine the likelihood of the current quantization + // table being a scaled version of the "standard" tables. + // If the variance is high, it is unlikely to be the case. + sumPercent /= 64.0; + + // Generate the equivalent IJQ "quality" factor + if (sumPercent <= 100.0) + { + quality = (int)Math.Round((200 - sumPercent) / 2); + } + else + { + quality = (int)Math.Round(5000.0 / sumPercent); + } + + return Numerics.Clamp(quality, MinQualityFactor, MaxQualityFactor); + } + + /// + /// Estimates jpeg quality based on quantization table in zig-zag order. + /// + /// Luminance quantization table. + /// Estimated quality + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int EstimateLuminanceQuality(ref Block8x8F luminanceTable) + => EstimateQuality(ref luminanceTable, LuminanceTable); + + /// + /// Estimates jpeg quality based on quantization table in zig-zag order. + /// + /// Chrominance quantization table. + /// Estimated quality + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int EstimateChrominanceQuality(ref Block8x8F chrominanceTable) + => EstimateQuality(ref chrominanceTable, ChrominanceTable); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int QualityToScale(int quality) + { + DebugGuard.MustBeBetweenOrEqualTo(quality, MinQualityFactor, MaxQualityFactor, nameof(quality)); + + return quality < 50 ? (5000 / quality) : (200 - (quality * 2)); + } + + public static Block8x8F ScaleQuantizationTable(int scale, ReadOnlySpan unscaledTable) + { + Block8x8F table = default; + for (int j = 0; j < Block8x8F.Size; j++) + { + int x = ((unscaledTable[j] * scale) + 50) / 100; + table[j] = Numerics.Clamp(x, 1, 255); + } + + return table; + } + + public static Block8x8 ScaleQuantizationTable(int quality, Block8x8 unscaledTable) + { + int scale = QualityToScale(quality); + Block8x8 table = default; + for (int j = 0; j < Block8x8.Size; j++) + { + int x = ((unscaledTable[j] * scale) + 50) / 100; + table[j] = (short)(uint)Numerics.Clamp(x, 1, 255); + } + + return table; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Block8x8F ScaleLuminanceTable(int quality) + => ScaleQuantizationTable(scale: QualityToScale(quality), LuminanceTable); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Block8x8F ScaleChrominanceTable(int quality) + => ScaleQuantizationTable(scale: QualityToScale(quality), ChrominanceTable); + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/RowOctet.cs b/ImageSharp/Formats/Jpeg/Components/RowOctet.cs new file mode 100644 index 0000000..0e19358 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/RowOctet.cs @@ -0,0 +1,99 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + /// + /// Cache 8 pixel rows on the stack, which may originate from different buffers of a . + /// + /// The type of element in each row. + [StructLayout(LayoutKind.Sequential)] + internal ref struct RowOctet + where T : struct + { + private Span row0; + private Span row1; + private Span row2; + private Span row3; + private Span row4; + private Span row5; + private Span row6; + private Span row7; + + // No unsafe tricks, since Span can't be used as a generic argument + public Span this[int y] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => + y switch + { + 0 => this.row0, + 1 => this.row1, + 2 => this.row2, + 3 => this.row3, + 4 => this.row4, + 5 => this.row5, + 6 => this.row6, + 7 => this.row7, + _ => ThrowIndexOutOfRangeException() + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private set + { + switch (y) + { + case 0: + this.row0 = value; + break; + case 1: + this.row1 = value; + break; + case 2: + this.row2 = value; + break; + case 3: + this.row3 = value; + break; + case 4: + this.row4 = value; + break; + case 5: + this.row5 = value; + break; + case 6: + this.row6 = value; + break; + default: + this.row7 = value; + break; + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Update(Buffer2D buffer, int startY) + { + // We don't actually have to assign values outside of the + // frame pixel buffer since they are never requested. + int y = startY; + int yEnd = Math.Min(y + 8, buffer.Height); + + int i = 0; + while (y < yEnd) + { + this[i++] = buffer.DangerousGetRowSpan(y++); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static Span ThrowIndexOutOfRangeException() +#pragma warning disable CA2201 // Do not raise reserved exception types + => throw new IndexOutOfRangeException(); +#pragma warning restore CA2201 // Do not raise reserved exception types + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ScaledFloatingPointDCT.cs b/ImageSharp/Formats/Jpeg/Components/ScaledFloatingPointDCT.cs new file mode 100644 index 0000000..0d8ef4d --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ScaledFloatingPointDCT.cs @@ -0,0 +1,218 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +#pragma warning disable IDE0078 +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + /// + /// Contains floating point forward DCT implementations with built-in scaling. + /// + /// + /// Based on "Loeffler, Ligtenberg, and Moschytz" algorithm. + /// + internal static class ScaledFloatingPointDCT + { +#pragma warning disable SA1310 + private const float FP32_0_541196100 = 0.541196100f; + private const float FP32_0_765366865 = 0.765366865f; + private const float FP32_1_847759065 = 1.847759065f; + private const float FP32_0_211164243 = 0.211164243f; + private const float FP32_1_451774981 = 1.451774981f; + private const float FP32_2_172734803 = 2.172734803f; + private const float FP32_1_061594337 = 1.061594337f; + private const float FP32_0_509795579 = 0.509795579f; + private const float FP32_0_601344887 = 0.601344887f; + private const float FP32_0_899976223 = 0.899976223f; + private const float FP32_2_562915447 = 2.562915447f; + private const float FP32_0_720959822 = 0.720959822f; + private const float FP32_0_850430095 = 0.850430095f; + private const float FP32_1_272758580 = 1.272758580f; + private const float FP32_3_624509785 = 3.624509785f; +#pragma warning restore SA1310 + + /// + /// Adjusts given quantization table for usage with IDCT algorithms + /// from . + /// + /// Quantization table to adjust. + public static void AdjustToIDCT(ref Block8x8F quantTable) + { + ref float tableRef = ref Unsafe.As(ref quantTable); + for (nuint i = 0; i < Block8x8F.Size; i++) + { + ref float elemRef = ref Unsafe.Add(ref tableRef, i); + elemRef = 0.125f * elemRef; + } + + // Spectral macroblocks are transposed before quantization + // so we must transpose quantization table + quantTable.TransposeInPlace(); + } + + /// + /// Apply 2D floating point 'donwscaling' IDCT inplace producing + /// 8x8 -> 4x4 result. + /// + /// + /// Resulting matrix is stored in the top left 4x4 part of the + /// . + /// + /// Input block. + /// Dequantization table adjusted by . + /// Output range normalization value, 1/2 of the . + /// Maximum value of the output range. + public static void TransformIDCT_4x4(ref Block8x8F block, ref Block8x8F dequantTable, float normalizationValue, float maxValue) + { + for (int ctr = 0; ctr < 8; ctr++) + { + // Don't process row 4, second pass doesn't use it + if (ctr == 4) + { + continue; + } + + // Even part + float tmp0 = block[(ctr * 8) + 0] * dequantTable[(ctr * 8) + 0] * 2; + + float z2 = block[(ctr * 8) + 2] * dequantTable[(ctr * 8) + 2]; + float z3 = block[(ctr * 8) + 6] * dequantTable[(ctr * 8) + 6]; + + float tmp2 = (z2 * FP32_1_847759065) + (z3 * -FP32_0_765366865); + + float tmp10 = tmp0 + tmp2; + float tmp12 = tmp0 - tmp2; + + // Odd part + float z1 = block[(ctr * 8) + 7] * dequantTable[(ctr * 8) + 7]; + z2 = block[(ctr * 8) + 5] * dequantTable[(ctr * 8) + 5]; + z3 = block[(ctr * 8) + 3] * dequantTable[(ctr * 8) + 3]; + float z4 = block[(ctr * 8) + 1] * dequantTable[(ctr * 8) + 1]; + + tmp0 = (z1 * -FP32_0_211164243) + + (z2 * FP32_1_451774981) + + (z3 * -FP32_2_172734803) + + (z4 * FP32_1_061594337); + + tmp2 = (z1 * -FP32_0_509795579) + + (z2 * -FP32_0_601344887) + + (z3 * FP32_0_899976223) + + (z4 * FP32_2_562915447); + + // temporal result is saved to +4 shifted indices + // because result is saved into the top left 2x2 region of the + // input block + block[(ctr * 8) + 0 + 4] = (tmp10 + tmp2) * 0.5F; + block[(ctr * 8) + 3 + 4] = (tmp10 - tmp2) * 0.5F; + block[(ctr * 8) + 1 + 4] = (tmp12 + tmp0) * 0.5F; + block[(ctr * 8) + 2 + 4] = (tmp12 - tmp0) * 0.5F; + } + + for (int ctr = 0; ctr < 4; ctr++) + { + // Even part + float tmp0 = block[ctr + (8 * 0) + 4] * 2; + + float tmp2 = (block[ctr + (8 * 2) + 4] * FP32_1_847759065) + (block[ctr + (8 * 6) + 4] * -FP32_0_765366865); + + float tmp10 = tmp0 + tmp2; + float tmp12 = tmp0 - tmp2; + + // Odd part + float z1 = block[ctr + (8 * 7) + 4]; + float z2 = block[ctr + (8 * 5) + 4]; + float z3 = block[ctr + (8 * 3) + 4]; + float z4 = block[ctr + (8 * 1) + 4]; + + tmp0 = (z1 * -FP32_0_211164243) + + (z2 * FP32_1_451774981) + + (z3 * -FP32_2_172734803) + + (z4 * FP32_1_061594337); + + tmp2 = (z1 * -FP32_0_509795579) + + (z2 * -FP32_0_601344887) + + (z3 * FP32_0_899976223) + + (z4 * FP32_2_562915447); + + // Save results to the top left 4x4 subregion + block[(ctr * 8) + 0] = Numerics.Clamp(((tmp10 + tmp2) * 0.5F) + normalizationValue, 0, maxValue); + block[(ctr * 8) + 3] = Numerics.Clamp(((tmp10 - tmp2) * 0.5F) + normalizationValue, 0, maxValue); + block[(ctr * 8) + 1] = Numerics.Clamp(((tmp12 + tmp0) * 0.5F) + normalizationValue, 0, maxValue); + block[(ctr * 8) + 2] = Numerics.Clamp(((tmp12 - tmp0) * 0.5F) + normalizationValue, 0, maxValue); + } + } + + /// + /// Apply 2D floating point 'donwscaling' IDCT inplace producing + /// 8x8 -> 2x2 result. + /// + /// + /// Resulting matrix is stored in the top left 2x2 part of the + /// . + /// + /// Input block. + /// Dequantization table adjusted by . + /// Output range normalization value, 1/2 of the . + /// Maximum value of the output range. + public static void TransformIDCT_2x2(ref Block8x8F block, ref Block8x8F dequantTable, float normalizationValue, float maxValue) + { + for (int ctr = 0; ctr < 8; ctr++) + { + // Don't process rows 2/4/6, second pass doesn't use it + if (ctr == 2 || ctr == 4 || ctr == 6) + { + continue; + } + + // Even part + float tmp0; + float z1 = block[(ctr * 8) + 0] * dequantTable[(ctr * 8) + 0]; + float tmp10 = z1 * 4; + + // Odd part + z1 = block[(ctr * 8) + 7] * dequantTable[(ctr * 8) + 7]; + tmp0 = z1 * -FP32_0_720959822; + z1 = block[(ctr * 8) + 5] * dequantTable[(ctr * 8) + 5]; + tmp0 += z1 * FP32_0_850430095; + z1 = block[(ctr * 8) + 3] * dequantTable[(ctr * 8) + 3]; + tmp0 += z1 * -FP32_1_272758580; + z1 = block[(ctr * 8) + 1] * dequantTable[(ctr * 8) + 1]; + tmp0 += z1 * FP32_3_624509785; + + // temporal result is saved to +2 shifted indices + // because result is saved into the top left 2x2 region of the + // input block + block[(ctr * 8) + 2] = (tmp10 + tmp0) * 0.25F; + block[(ctr * 8) + 3] = (tmp10 - tmp0) * 0.25F; + } + + for (int ctr = 0; ctr < 2; ctr++) + { + // Even part + float tmp10 = block[ctr + (8 * 0) + 2] * 4; + + // Odd part + float tmp0 = (block[ctr + (8 * 7) + 2] * -FP32_0_720959822) + + (block[ctr + (8 * 5) + 2] * FP32_0_850430095) + + (block[ctr + (8 * 3) + 2] * -FP32_1_272758580) + + (block[ctr + (8 * 1) + 2] * FP32_3_624509785); + + // Save results to the top left 2x2 subregion + block[(ctr * 8) + 0] = Numerics.Clamp(((tmp10 + tmp0) * 0.25F) + normalizationValue, 0, maxValue); + block[(ctr * 8) + 1] = Numerics.Clamp(((tmp10 - tmp0) * 0.25F) + normalizationValue, 0, maxValue); + } + } + + /// + /// Apply 2D floating point 'donwscaling' IDCT inplace producing + /// 8x8 -> 1x1 result. + /// + /// Direct current term value from input block. + /// Dequantization value. + /// Output range normalization value, 1/2 of the . + /// Maximum value of the output range. + public static float TransformIDCT_1x1(float dc, float dequantizer, float normalizationValue, float maxValue) + => Numerics.Clamp((dc * dequantizer) + normalizationValue, 0, maxValue); + } +#pragma warning restore IDE0078 +} diff --git a/ImageSharp/Formats/Jpeg/Components/SizeExtensions.cs b/ImageSharp/Formats/Jpeg/Components/SizeExtensions.cs new file mode 100644 index 0000000..438387f --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/SizeExtensions.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + /// + /// Extension methods for + /// + internal static class SizeExtensions + { + /// + /// Multiplies 'a.Width' with 'b.Width' and 'a.Height' with 'b.Height'. + /// TODO: Shouldn't we expose this as operator in SixLabors.Core? + /// + public static Size MultiplyBy(this Size a, Size b) => new(a.Width * b.Width, a.Height * b.Height); + + /// + /// Divides 'a.Width' with 'b.Width' and 'a.Height' with 'b.Height'. + /// TODO: Shouldn't we expose this as operator in SixLabors.Core? + /// + public static Size DivideBy(this Size a, Size b) => new(a.Width / b.Width, a.Height / b.Height); + + /// + /// Divide Width and Height as real numbers and return the Ceiling. + /// + public static Size DivideRoundUp(this Size originalSize, int divX, int divY) + { + Vector2 sizeVect = (Vector2)(SizeF)originalSize; + sizeVect /= new Vector2(divX, divY); + sizeVect.X = MathF.Ceiling(sizeVect.X); + sizeVect.Y = MathF.Ceiling(sizeVect.Y); + + return new Size((int)sizeVect.X, (int)sizeVect.Y); + } + + /// + /// Divide Width and Height as real numbers and return the Ceiling. + /// + public static Size DivideRoundUp(this Size originalSize, int divisor) => + DivideRoundUp(originalSize, divisor, divisor); + + /// + /// Divide Width and Height as real numbers and return the Ceiling. + /// + public static Size DivideRoundUp(this Size originalSize, Size divisor) => + DivideRoundUp(originalSize, divisor.Width, divisor.Height); + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ZigZag.Intrinsic.cs b/ImageSharp/Formats/Jpeg/Components/ZigZag.Intrinsic.cs new file mode 100644 index 0000000..5da83bc --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ZigZag.Intrinsic.cs @@ -0,0 +1,323 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal static partial class ZigZag + { +#pragma warning disable SA1309 // naming rules violation warnings + /// + /// Special byte value to zero out elements during Sse/Avx shuffle intrinsics. + /// + private const byte _ = 0xff; +#pragma warning restore SA1309 + + /// + /// Gets shuffle vectors for + /// zig zag implementation. + /// + private static ReadOnlySpan SseShuffleMasks => + [ +#pragma warning disable SA1515 + /* row0 - A0 B0 A1 A2 B1 C0 D0 C1 */ + // A + 0, 1, _, _, 2, 3, 4, 5, _, _, _, _, _, _, _, _, + // B + _, _, 0, 1, _, _, _, _, 2, 3, _, _, _, _, _, _, + // C + _, _, _, _, _, _, _, _, _, _, 0, 1, _, _, 2, 3, + + /* row1 - B2 A3 A4 B3 C2 D1 E0 F0 */ + // A + _, _, 6, 7, 8, 9, _, _, _, _, _, _, _, _, _, _, + // B + 4, 5, _, _, _, _, 6, 7, _, _, _, _, _, _, _, _, + + /* row2 - E1 D2 C3 B4 A5 A6 B5 C4 */ + // A + _, _, _, _, _, _, _, _, 10, 11, 12, 13, _, _, _, _, + // B + _, _, _, _, _, _, 8, 9, _, _, _, _, 10, 11, _, _, + // C + _, _, _, _, 6, 7, _, _, _, _, _, _, _, _, 8, 9, + + /* row3 - D3 E2 F1 G0 H0 G1 F2 E3 */ + // E + _, _, 4, 5, _, _, _, _, _, _, _, _, _, _, 6, 7, + // F + _, _, _, _, 2, 3, _, _, _, _, _, _, 4, 5, _, _, + // G + _, _, _, _, _, _, 0, 1, _, _, 2, 3, _, _, _, _, + + /* row4 - D4 C5 B6 A7 B7 C6 D5 E4 */ + // B + _, _, _, _, 12, 13, _, _, 14, 15, _, _, _, _, _, _, + // C + _, _, 10, 11, _, _, _, _, _, _, 12, 13, _, _, _, _, + // D + 8, 9, _, _, _, _, _, _, _, _, _, _, 10, 11, _, _, + + /* row5 - F3 G2 H1 H2 G3 F4 E5 D6 */ + // F + 6, 7, _, _, _, _, _, _, _, _, 8, 9, _, _, _, _, + // G + _, _, 4, 5, _, _, _, _, 6, 7, _, _, _, _, _, _, + // H + _, _, _, _, 2, 3, 4, 5, _, _, _, _, _, _, _, _, + + /* row6 - C7 D7 E6 F5 G4 H3 H4 G5 */ + // G + _, _, _, _, _, _, _, _, 8, 9, _, _, _, _, 10, 11, + // H + _, _, _, _, _, _, _, _, _, _, 6, 7, 8, 9, _, _, + + /* row7 - F6 E7 F7 G6 H5 H6 G7 H7 */ + // F + 12, 13, _, _, 14, 15, _, _, _, _, _, _, _, _, _, _, + // G + _, _, _, _, _, _, 12, 13, _, _, _, _, 14, 15, _, _, + // H + _, _, _, _, _, _, _, _, 10, 11, 12, 13, _, _, 14, 15, +#pragma warning restore SA1515 + ]; + + /// + /// Gets shuffle vectors for + /// zig zag implementation. + /// + private static ReadOnlySpan AvxShuffleMasks => + [ +#pragma warning disable SA1515 + /* 01 */ + // [cr] crln_01_AB_CD + 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, _, _, _, _, 1, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, + // (in) AB + 0, 1, 8, 9, 2, 3, 4, 5, 10, 11, _, _, _, _, _, _, 12, 13, 2, 3, 4, 5, 14, 15, _, _, _, _, _, _, _, _, + // (in) CD + _, _, _, _, _, _, _, _, _, _, 0, 1, 8, 9, 2, 3, _, _, _, _, _, _, _, _, 0, 1, 10, 11, _, _, _, _, + // [cr] crln_01_23_EF_23_CD + 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, + // (in) EF + _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 0, 1, 8, 9, + + /* 23 */ + // [cr] crln_23_AB_23_45_GH + 2, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, + // (in) AB + _, _, _, _, _, _, 8, 9, 2, 3, 4, 5, 10, 11, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, + // (in) CDe + _, _, 12, 13, 6, 7, _, _, _, _, _, _, _, _, 8, 9, 14, 15, _, _, _, _, _, _, _, _, _, _, _, _, _, _, + // (in) EF + 2, 3, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 4, 5, 10, 11, _, _, _, _, _, _, 12, 13, 6, 7, + // (in) GH + _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 0, 1, 8, 9, 2, 3, _, _, _, _, + + /* 45 */ + // (in) AB + _, _, _, _, 12, 13, 6, 7, 14, 15, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, + // [cr] crln_45_67_CD_45_EF + 2, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, 2, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, + // (in) CD + 8, 9, 2, 3, _, _, _, _, _, _, 4, 5, 10, 11, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 12, 13, + // (in) EF + _, _, _, _, _, _, _, _, _, _, _, _, _, _, 0, 1, 6, 7, _, _, _, _, _, _, _, _, 8, 9, 2, 3, _, _, + // (in) GH + _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 4, 5, 10, 11, 12, 13, 6, 7, _, _, _, _, _, _, + + /* 67 */ + // (in) CD + 6, 7, 14, 15, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, + // [cr] crln_67_EF_67_GH + 2, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, _, _, _, _, + // (in) EF + _, _, _, _, 4, 5, 14, 15, _, _, _, _, _, _, _, _, 8, 9, 2, 3, 10, 11, _, _, _, _, _, _, _, _, _, _, + // (in) GH + _, _, _, _, _, _, _, _, 0, 1, 10, 11, 12, 13, 2, 3, _, _, _, _, _, _, 0, 1, 6, 7, 8, 9, 2, 3, 10, 11, +#pragma warning restore SA1515 + ]; + + /// + /// Applies zig zag ordering for given 8x8 matrix using cpu intrinsics. + /// + /// Input matrix. + public static unsafe void ApplyTransposingZigZagOrderingVector128(ref Block8x8 block) + { + DebugGuard.IsTrue(Vector128.IsHardwareAccelerated, "Vector128 support is required to run this operation!"); + + fixed (byte* shuffleVectorsPtr = &MemoryMarshal.GetReference(SseShuffleMasks)) + { + Vector128 rowA = block.V0.AsByte(); + Vector128 rowB = block.V1.AsByte(); + Vector128 rowC = block.V2.AsByte(); + Vector128 rowD = block.V3.AsByte(); + Vector128 rowE = block.V4.AsByte(); + Vector128 rowF = block.V5.AsByte(); + Vector128 rowG = block.V6.AsByte(); + Vector128 rowH = block.V7.AsByte(); + + // row0 - A0 B0 A1 A2 B1 C0 D0 C1 + Vector128 row0_A = ZShuffle(rowA, Vector128.Load(shuffleVectorsPtr + (16 * 0))).AsInt16(); + Vector128 row0_B = ZShuffle(rowB, Vector128.Load(shuffleVectorsPtr + (16 * 1))).AsInt16(); + Vector128 row0_C = ZShuffle(rowC, Vector128.Load(shuffleVectorsPtr + (16 * 2))).AsInt16(); + Vector128 row0 = row0_A | row0_B | row0_C; + row0 = row0.AsUInt16().WithElement(6, rowD.AsUInt16().GetElement(0)).AsInt16(); + + // row1 - B2 A3 A4 B3 C2 D1 E0 F0 + Vector128 row1_A = ZShuffle(rowA, Vector128.Load(shuffleVectorsPtr + (16 * 3))).AsInt16(); + Vector128 row1_B = ZShuffle(rowB, Vector128.Load(shuffleVectorsPtr + (16 * 4))).AsInt16(); + Vector128 row1 = row1_A | row1_B; + row1 = row1.AsUInt16().WithElement(4, rowC.AsUInt16().GetElement(2)).AsInt16(); + row1 = row1.AsUInt16().WithElement(5, rowD.AsUInt16().GetElement(1)).AsInt16(); + row1 = row1.AsUInt16().WithElement(6, rowE.AsUInt16().GetElement(0)).AsInt16(); + row1 = row1.AsUInt16().WithElement(7, rowF.AsUInt16().GetElement(0)).AsInt16(); + + // row2 - E1 D2 C3 B4 A5 A6 B5 C4 + Vector128 row2_A = ZShuffle(rowA, Vector128.Load(shuffleVectorsPtr + (16 * 5))).AsInt16(); + Vector128 row2_B = ZShuffle(rowB, Vector128.Load(shuffleVectorsPtr + (16 * 6))).AsInt16(); + Vector128 row2_C = ZShuffle(rowC, Vector128.Load(shuffleVectorsPtr + (16 * 7))).AsInt16(); + Vector128 row2 = row2_A | row2_B | row2_C; + row2 = row2.AsUInt16().WithElement(1, rowD.AsUInt16().GetElement(2)).AsInt16(); + row2 = row2.AsUInt16().WithElement(0, rowE.AsUInt16().GetElement(1)).AsInt16(); + + // row3 - D3 E2 F1 G0 H0 G1 F2 E3 + Vector128 row3_E = ZShuffle(rowE, Vector128.Load(shuffleVectorsPtr + (16 * 8))).AsInt16(); + Vector128 row3_F = ZShuffle(rowF, Vector128.Load(shuffleVectorsPtr + (16 * 9))).AsInt16(); + Vector128 row3_G = ZShuffle(rowG, Vector128.Load(shuffleVectorsPtr + (16 * 10))).AsInt16(); + Vector128 row3 = row3_E | row3_F | row3_G; + row3 = row3.AsUInt16().WithElement(0, rowD.AsUInt16().GetElement(3)).AsInt16(); + row3 = row3.AsUInt16().WithElement(4, rowH.AsUInt16().GetElement(0)).AsInt16(); + + // row4 - D4 C5 B6 A7 B7 C6 D5 E4 + Vector128 row4_B = ZShuffle(rowB, Vector128.Load(shuffleVectorsPtr + (16 * 11))).AsInt16(); + Vector128 row4_C = ZShuffle(rowC, Vector128.Load(shuffleVectorsPtr + (16 * 12))).AsInt16(); + Vector128 row4_D = ZShuffle(rowD, Vector128.Load(shuffleVectorsPtr + (16 * 13))).AsInt16(); + Vector128 row4 = row4_B | row4_C | row4_D; + row4 = row4.AsUInt16().WithElement(3, rowA.AsUInt16().GetElement(7)).AsInt16(); + row4 = row4.AsUInt16().WithElement(7, rowE.AsUInt16().GetElement(4)).AsInt16(); + + // row5 - F3 G2 H1 H2 G3 F4 E5 D6 + Vector128 row5_F = ZShuffle(rowF, Vector128.Load(shuffleVectorsPtr + (16 * 14))).AsInt16(); + Vector128 row5_G = ZShuffle(rowG, Vector128.Load(shuffleVectorsPtr + (16 * 15))).AsInt16(); + Vector128 row5_H = ZShuffle(rowH, Vector128.Load(shuffleVectorsPtr + (16 * 16))).AsInt16(); + Vector128 row5 = row5_F | row5_G | row5_H; + row5 = row5.AsUInt16().WithElement(7, rowD.AsUInt16().GetElement(6)).AsInt16(); + row5 = row5.AsUInt16().WithElement(6, rowE.AsUInt16().GetElement(5)).AsInt16(); + + // row6 - C7 D7 E6 F5 G4 H3 H4 G5 + Vector128 row6_G = ZShuffle(rowG, Vector128.Load(shuffleVectorsPtr + (16 * 17))).AsInt16(); + Vector128 row6_H = ZShuffle(rowH, Vector128.Load(shuffleVectorsPtr + (16 * 18))).AsInt16(); + Vector128 row6 = row6_G | row6_H; + row6 = row6.AsUInt16().WithElement(0, rowC.AsUInt16().GetElement(7)).AsInt16(); + row6 = row6.AsUInt16().WithElement(1, rowD.AsUInt16().GetElement(7)).AsInt16(); + row6 = row6.AsUInt16().WithElement(2, rowE.AsUInt16().GetElement(6)).AsInt16(); + row6 = row6.AsUInt16().WithElement(3, rowF.AsUInt16().GetElement(5)).AsInt16(); + + // row7 - F6 E7 F7 G6 H5 H6 G7 H7 + Vector128 row7_F = ZShuffle(rowF, Vector128.Load(shuffleVectorsPtr + (16 * 19))).AsInt16(); + Vector128 row7_G = ZShuffle(rowG, Vector128.Load(shuffleVectorsPtr + (16 * 20))).AsInt16(); + Vector128 row7_H = ZShuffle(rowH, Vector128.Load(shuffleVectorsPtr + (16 * 21))).AsInt16(); + Vector128 row7 = row7_F | row7_G | row7_H; + row7 = row7.AsUInt16().WithElement(1, rowE.AsUInt16().GetElement(7)).AsInt16(); + + block.V0 = row0; + block.V1 = row1; + block.V2 = row2; + block.V3 = row3; + block.V4 = row4; + block.V5 = row5; + block.V6 = row6; + block.V7 = row7; + } + } + + /// + /// Applies zig zag ordering for given 8x8 matrix using AVX cpu intrinsics. + /// + /// Input matrix. + public static unsafe void ApplyTransposingZigZagOrderingAvx2(ref Block8x8 block) + { + DebugGuard.IsTrue(Avx2.IsSupported, "Avx2 support is required to run this operation!"); + + fixed (byte* shuffleVectorsPtr = &MemoryMarshal.GetReference(AvxShuffleMasks)) + { + Vector256 rowAB = block.V01.AsByte(); + Vector256 rowCD = block.V23.AsByte(); + Vector256 rowEF = block.V45.AsByte(); + Vector256 rowGH = block.V67.AsByte(); + + /* row01 - A0 B0 A1 A2 B1 C0 D0 C1 | B2 A3 A4 B3 C2 D1 E0 F0 */ + Vector256 crln_01_AB_CD = Avx.LoadVector256(shuffleVectorsPtr + (0 * 32)).AsInt32(); + Vector256 row01_AB = Avx2.PermuteVar8x32(rowAB.AsInt32(), crln_01_AB_CD).AsByte(); + row01_AB = Avx2.Shuffle(row01_AB, Avx.LoadVector256(shuffleVectorsPtr + (1 * 32))).AsByte(); + Vector256 row01_CD = Avx2.PermuteVar8x32(rowCD.AsInt32(), crln_01_AB_CD).AsByte(); + row01_CD = Avx2.Shuffle(row01_CD, Avx.LoadVector256(shuffleVectorsPtr + (2 * 32))).AsByte(); + Vector256 crln_01_23_EF_23_CD = Avx.LoadVector256(shuffleVectorsPtr + (3 * 32)).AsInt32(); + Vector256 row01_23_EF = Avx2.PermuteVar8x32(rowEF.AsInt32(), crln_01_23_EF_23_CD).AsByte(); + Vector256 row01_EF = Avx2.Shuffle(row01_23_EF, Avx.LoadVector256(shuffleVectorsPtr + (4 * 32))).AsByte(); + + Vector256 row01 = Avx2.Or(row01_AB, Avx2.Or(row01_CD, row01_EF)); + + /* row23 - E1 D2 C3 B4 A5 A6 B5 C4 | D3 E2 F1 G0 H0 G1 F2 E3 */ + Vector256 crln_23_AB_23_45_GH = Avx.LoadVector256(shuffleVectorsPtr + (5 * 32)).AsInt32(); + Vector256 row23_45_AB = Avx2.PermuteVar8x32(rowAB.AsInt32(), crln_23_AB_23_45_GH).AsByte(); + Vector256 row23_AB = Avx2.Shuffle(row23_45_AB, Avx.LoadVector256(shuffleVectorsPtr + (6 * 32))).AsByte(); + Vector256 row23_CD = Avx2.PermuteVar8x32(rowCD.AsInt32(), crln_01_23_EF_23_CD).AsByte(); + row23_CD = Avx2.Shuffle(row23_CD, Avx.LoadVector256(shuffleVectorsPtr + (7 * 32))).AsByte(); + Vector256 row23_EF = Avx2.Shuffle(row01_23_EF, Avx.LoadVector256(shuffleVectorsPtr + (8 * 32))).AsByte(); + Vector256 row23_45_GH = Avx2.PermuteVar8x32(rowGH.AsInt32(), crln_23_AB_23_45_GH).AsByte(); + Vector256 row23_GH = Avx2.Shuffle(row23_45_GH, Avx.LoadVector256(shuffleVectorsPtr + (9 * 32))).AsByte(); + + Vector256 row23 = Avx2.Or(Avx2.Or(row23_AB, row23_CD), Avx2.Or(row23_EF, row23_GH)); + + /* row45 - D4 C5 B6 A7 B7 C6 D5 E4 | F3 G2 H1 H2 G3 F4 E5 D6 */ + Vector256 row45_AB = Avx2.Shuffle(row23_45_AB, Avx.LoadVector256(shuffleVectorsPtr + (10 * 32))).AsByte(); + Vector256 crln_45_67_CD_45_EF = Avx.LoadVector256(shuffleVectorsPtr + (11 * 32)).AsInt32(); + Vector256 row45_67_CD = Avx2.PermuteVar8x32(rowCD.AsInt32(), crln_45_67_CD_45_EF).AsByte(); + Vector256 row45_CD = Avx2.Shuffle(row45_67_CD, Avx.LoadVector256(shuffleVectorsPtr + (12 * 32))).AsByte(); + Vector256 row45_EF = Avx2.PermuteVar8x32(rowEF.AsInt32(), crln_45_67_CD_45_EF).AsByte(); + row45_EF = Avx2.Shuffle(row45_EF, Avx.LoadVector256(shuffleVectorsPtr + (13 * 32))).AsByte(); + Vector256 row45_GH = Avx2.Shuffle(row23_45_GH, Avx.LoadVector256(shuffleVectorsPtr + (14 * 32))).AsByte(); + + Vector256 row45 = Avx2.Or(Avx2.Or(row45_AB, row45_CD), Avx2.Or(row45_EF, row45_GH)); + + /* row67 - C7 D7 E6 F5 G4 H3 H4 G5 | F6 E7 F7 G6 H5 H6 G7 H7 */ + Vector256 row67_CD = Avx2.Shuffle(row45_67_CD, Avx.LoadVector256(shuffleVectorsPtr + (15 * 32))).AsByte(); + Vector256 crln_67_EF_67_GH = Avx.LoadVector256(shuffleVectorsPtr + (16 * 32)).AsInt32(); + Vector256 row67_EF = Avx2.PermuteVar8x32(rowEF.AsInt32(), crln_67_EF_67_GH).AsByte(); + row67_EF = Avx2.Shuffle(row67_EF, Avx.LoadVector256(shuffleVectorsPtr + (17 * 32))).AsByte(); + Vector256 row67_GH = Avx2.PermuteVar8x32(rowGH.AsInt32(), crln_67_EF_67_GH).AsByte(); + row67_GH = Avx2.Shuffle(row67_GH, Avx.LoadVector256(shuffleVectorsPtr + (18 * 32))).AsByte(); + + Vector256 row67 = Avx2.Or(row67_CD, Avx2.Or(row67_EF, row67_GH)); + + block.V01 = row01.AsInt16(); + block.V23 = row23.AsInt16(); + block.V45 = row45.AsInt16(); + block.V67 = row67.AsInt16(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ZShuffle(Vector128 source, Vector128 mask) + { + // For x64 we use the SSSE3 shuffle intrinsic to avoid additional instructions. 3 vs 1. + if (Ssse3.IsSupported) + { + return Ssse3.Shuffle(source, mask); + } + + // For ARM and WASM, codegen will be optimal. + return Vector128.Shuffle(source, mask); + } + + [DoesNotReturn] + private static void ThrowUnreachableException() => throw new UnreachableException(); + } +} diff --git a/ImageSharp/Formats/Jpeg/Components/ZigZag.cs b/ImageSharp/Formats/Jpeg/Components/ZigZag.cs new file mode 100644 index 0000000..2c803f3 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/Components/ZigZag.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components { + internal static partial class ZigZag + { + /// + /// Gets span of zig-zag ordering indices. + /// + /// + /// When reading corrupted data, the Huffman decoders could attempt + /// to reference an entry beyond the end of this array (if the decoded + /// zero run length reaches past the end of the block). To prevent + /// wild stores without adding an inner-loop test, we put some extra + /// "63"s after the real entries. This will cause the extra coefficient + /// to be stored in location 63 of the block, not somewhere random. + /// The worst case would be a run-length of 15, which means we need 16 + /// fake entries. + /// + public static ReadOnlySpan ZigZagOrder => + [ + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63, + + // Extra entries for safety in decoder + 63, 63, 63, 63, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63 + ]; + + /// + /// Gets span of zig-zag with fused transpose step ordering indices. + /// + /// + /// When reading corrupted data, the Huffman decoders could attempt + /// to reference an entry beyond the end of this array (if the decoded + /// zero run length reaches past the end of the block). To prevent + /// wild stores without adding an inner-loop test, we put some extra + /// "63"s after the real entries. This will cause the extra coefficient + /// to be stored in location 63 of the block, not somewhere random. + /// The worst case would be a run-length of 15, which means we need 16 + /// fake entries. + /// + public static ReadOnlySpan TransposingOrder => + [ + 0, 8, 1, 2, 9, 16, 24, 17, + 10, 3, 4, 11, 18, 25, 32, 40, + 33, 26, 19, 12, 5, 6, 13, 20, + 27, 34, 41, 48, 56, 49, 42, 35, + 28, 21, 14, 7, 15, 22, 29, 36, + 43, 50, 57, 58, 51, 44, 37, 30, + 23, 31, 38, 45, 52, 59, 60, 53, + 46, 39, 47, 54, 61, 62, 55, 63, + + // Extra entries for safety in decoder + 63, 63, 63, 63, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63 + ]; + } +} diff --git a/ImageSharp/Formats/Jpeg/JpegColorType.cs b/ImageSharp/Formats/Jpeg/JpegColorType.cs new file mode 100644 index 0000000..2776366 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/JpegColorType.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jpeg { + /// + /// Provides enumeration of available JPEG color types. + /// + public enum JpegColorType : byte + { + /// + /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. + /// Medium Quality - The horizontal sampling is halved and the Cb and Cr channels are only + /// sampled on each alternate line. + /// + YCbCrRatio420 = 0, + + /// + /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. + /// High Quality - Each of the three Y'CbCr components have the same sample rate, + /// thus there is no chroma subsampling. + /// + YCbCrRatio444 = 1, + + /// + /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. + /// The two chroma components are sampled at half the horizontal sample rate of luma while vertically it has full resolution. + /// + YCbCrRatio422 = 2, + + /// + /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. + /// In 4:1:1 chroma subsampling, the horizontal color resolution is quartered. + /// + YCbCrRatio411 = 3, + + /// + /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. + /// This ratio uses half of the vertical and one-fourth the horizontal color resolutions. + /// + YCbCrRatio410 = 4, + + /// + /// Single channel, luminance. + /// + Luminance = 5, + + /// + /// The pixel data will be preserved as RGB without any sub sampling. + /// + Rgb = 6, + + /// + /// CMYK colorspace (cyan, magenta, yellow, and key black) intended for printing. + /// + Cmyk = 7, + + /// + /// YCCK colorspace (Y, Cb, Cr, and key black). + /// + Ycck = 8, + } +} diff --git a/ImageSharp/Formats/Jpeg/JpegComData.cs b/ImageSharp/Formats/Jpeg/JpegComData.cs new file mode 100644 index 0000000..5f6ba19 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/JpegComData.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Jpeg { + /// + /// Represents a JPEG comment + /// + public readonly struct JpegComData + { + /// + /// Initializes a new instance of the struct. + /// + /// The comment buffer. + public JpegComData(ReadOnlyMemory value) + => this.Value = value; + + /// + /// Gets the value. + /// + public ReadOnlyMemory Value { get; } + + /// + /// Converts string to + /// + /// The comment string. + /// The + public static JpegComData FromString(string value) => new(value.AsMemory()); + + /// + public override string ToString() => this.Value.ToString(); + } +} diff --git a/ImageSharp/Formats/Jpeg/JpegConfigurationModule.cs b/ImageSharp/Formats/Jpeg/JpegConfigurationModule.cs new file mode 100644 index 0000000..056bbde --- /dev/null +++ b/ImageSharp/Formats/Jpeg/JpegConfigurationModule.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jpeg { + /// + /// Registers the image encoders, decoders and mime type detectors for the jpeg format. + /// + public sealed class JpegConfigurationModule : IImageFormatConfigurationModule + { + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetEncoder(JpegFormat.Instance, new JpegEncoder()); + configuration.ImageFormatsManager.SetDecoder(JpegFormat.Instance, JpegDecoder.Instance); + configuration.ImageFormatsManager.AddImageFormatDetector(new JpegImageFormatDetector()); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/JpegConstants.cs b/ImageSharp/Formats/Jpeg/JpegConstants.cs new file mode 100644 index 0000000..1e1c428 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/JpegConstants.cs @@ -0,0 +1,339 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Jpeg { + /// + /// Contains jpeg constant values defined in the specification. + /// + internal static class JpegConstants + { + /// + /// The maximum allowable length in each dimension of a jpeg image. + /// + public const ushort MaxLength = 65535; + + /// + /// The list of mimetypes that equate to a jpeg. + /// + public static readonly IEnumerable MimeTypes = ["image/jpeg", "image/pjpeg"]; + + /// + /// The list of file extensions that equate to a jpeg. + /// + public static readonly IEnumerable FileExtensions = ["jpg", "jpeg", "jfif"]; + + /// + /// Contains marker specific constants. + /// + // ReSharper disable InconsistentNaming + internal static class Markers + { + /// + /// The prefix used for all markers. + /// + public const byte XFF = 0xFF; + + /// + /// Same as but of type + /// + public const int XFFInt = XFF; + + /// + /// The Start of Image marker + /// + public const byte SOI = 0xD8; + + /// + /// The End of Image marker + /// + public const byte EOI = 0xD9; + + /// + /// Application specific marker for marking the jpeg format. + /// + /// + public const byte APP0 = 0xE0; + + /// + /// Application specific marker for marking where to store metadata. + /// + public const byte APP1 = 0xE1; + + /// + /// Application specific marker for marking where to store ICC profile information. + /// + public const byte APP2 = 0xE2; + + /// + /// Application specific marker. + /// + public const byte APP3 = 0xE3; + + /// + /// Application specific marker. + /// + public const byte APP4 = 0xE4; + + /// + /// Application specific marker. + /// + public const byte APP5 = 0xE5; + + /// + /// Application specific marker. + /// + public const byte APP6 = 0xE6; + + /// + /// Application specific marker. + /// + public const byte APP7 = 0xE7; + + /// + /// Application specific marker. + /// + public const byte APP8 = 0xE8; + + /// + /// Application specific marker. + /// + public const byte APP9 = 0xE9; + + /// + /// Application specific marker. + /// + public const byte APP10 = 0xEA; + + /// + /// Application specific marker. + /// + public const byte APP11 = 0xEB; + + /// + /// Application specific marker. + /// + public const byte APP12 = 0xEC; + + /// + /// Application specific marker. + /// + public const byte APP13 = 0xED; + + /// + /// Application specific marker used by Adobe for storing encoding information for DCT filters. + /// + public const byte APP14 = 0xEE; + + /// + /// Application specific marker used by GraphicConverter to store JPEG quality. + /// + public const byte APP15 = 0xEF; + + /// + /// Define arithmetic coding conditioning marker. + /// + public const byte DAC = 0xCC; + + /// + /// The text comment marker + /// + public const byte COM = 0xFE; + + /// + /// Define Quantization Table(s) marker + /// + /// Specifies one or more quantization tables. + /// + /// + public const byte DQT = 0xDB; + + /// + /// Start of Frame (baseline DCT) + /// + /// Indicates that this is a baseline DCT-based JPEG, and specifies the width, height, number of components, + /// and component subsampling (e.g., 4:2:0). + /// + /// + public const byte SOF0 = 0xC0; + + /// + /// Start Of Frame (Extended Sequential DCT) + /// + /// Indicates that this is a progressive DCT-based JPEG, and specifies the width, height, number of components, + /// and component subsampling (e.g., 4:2:0). + /// + /// + public const byte SOF1 = 0xC1; + + /// + /// Start Of Frame (progressive DCT) + /// + /// Indicates that this is a progressive DCT-based JPEG, and specifies the width, height, number of components, + /// and component subsampling (e.g., 4:2:0). + /// + /// + public const byte SOF2 = 0xC2; + + /// + /// Start of Frame marker, non differential lossless, Huffman coding. + /// + public const byte SOF3 = 0xC3; + + /// + /// Start of Frame marker, differential, Huffman coding, Differential sequential DCT. + /// + public const byte SOF5 = 0xC5; + + /// + /// Start of Frame marker, differential, Huffman coding, Differential progressive DCT. + /// + public const byte SOF6 = 0xC6; + + /// + /// Start of Frame marker, differential lossless, Huffman coding. + /// + public const byte SOF7 = 0xC7; + + /// + /// Start of Frame marker, non-differential, arithmetic coding, Extended sequential DCT. + /// + public const byte SOF9 = 0xC9; + + /// + /// Start of Frame marker, non-differential, arithmetic coding, Progressive DCT. + /// + public const byte SOF10 = 0xCA; + + /// + /// Start of Frame marker, non-differential, arithmetic coding, Lossless (sequential). + /// + public const byte SOF11 = 0xCB; + + /// + /// Start of Frame marker, differential, arithmetic coding, Differential sequential DCT. + /// + public const byte SOF13 = 0xCD; + + /// + /// Start of Frame marker, differential, arithmetic coding, Differential progressive DCT. + /// + public const byte SOF14 = 0xCE; + + /// + /// Start of Frame marker, differential, arithmetic coding, Differential lossless (sequential). + /// + public const byte SOF15 = 0xCF; + + /// + /// Define Huffman Table(s) + /// + /// Specifies one or more Huffman tables. + /// + /// + public const byte DHT = 0xC4; + + /// + /// Define Restart Interval + /// + /// Specifies the interval between RSTn markers, in macroblocks.This marker is followed by two bytes indicating the fixed size so + /// it can be treated like any other variable size segment. + /// + /// + public const byte DRI = 0xDD; + + /// + /// Start of Scan + /// + /// Begins a top-to-bottom scan of the image. In baseline DCT JPEG images, there is generally a single scan. + /// Progressive DCT JPEG images usually contain multiple scans. This marker specifies which slice of data it + /// will contain, and is immediately followed by entropy-coded data. + /// + /// + public const byte SOS = 0xDA; + + /// + /// Define First Restart + /// + /// Inserted every r macroblocks, where r is the restart interval set by a DRI marker. + /// Not used if there was no DRI marker. The low three bits of the marker code cycle in value from 0 to 7. + /// + /// + public const byte RST0 = 0xD0; + + /// + /// Define Eigth Restart + /// + /// Inserted every r macroblocks, where r is the restart interval set by a DRI marker. + /// Not used if there was no DRI marker. The low three bits of the marker code cycle in value from 0 to 7. + /// + /// + public const byte RST7 = 0xD7; + } + + /// + /// Contains Adobe specific constants. + /// + internal static class Adobe + { + /// + /// The color transform is unknown.(RGB or CMYK) + /// + public const byte ColorTransformUnknown = 0; + + /// + /// The color transform is YCbCr (luminance, red chroma, blue chroma) + /// + public const byte ColorTransformYCbCr = 1; + + /// + /// The color transform is YCCK (luminance, red chroma, blue chroma, keyline) + /// + public const byte ColorTransformYcck = 2; + } + + /// + /// Contains Huffman specific constants. + /// + internal static class Huffman + { + /// + /// The size of the huffman decoder register. + /// + public const int RegisterSize = 64; + + /// + /// The number of bits to fetch when filling the buffer. + /// + public const int FetchBits = 48; + + /// + /// The number of times to read the input stream when filling the buffer. + /// + public const int FetchLoop = FetchBits / 8; + + /// + /// The minimum number of bits allowed before by the before fetching. + /// + public const int MinBits = RegisterSize - FetchBits; + + /// + /// If the next Huffman code is no more than this number of bits, we can obtain its length + /// and the corresponding symbol directly from this tables. + /// + public const int LookupBits = 8; + + /// + /// If a Huffman code is this number of bits we cannot use the lookup table to determine its value. + /// + public const int SlowBits = LookupBits + 1; + + /// + /// The size of the lookup table. + /// + public const int LookupSize = 1 << LookupBits; + } + } +} diff --git a/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/ImageSharp/Formats/Jpeg/JpegDecoder.cs new file mode 100644 index 0000000..9dfc7e1 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Jpeg { + /// + /// Decoder for generating an image out of a jpeg encoded stream. + /// + public sealed class JpegDecoder : SpecializedImageDecoder + { + private JpegDecoder() + { + } + + /// + /// Gets the shared instance. + /// + public static JpegDecoder Instance { get; } = new(); + + /// + protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + using JpegDecoderCore decoder = new(new JpegDecoderOptions { GeneralOptions = options }); + return decoder.Identify(options.Configuration, stream, cancellationToken); + } + + /// + protected override Image Decode(JpegDecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + using JpegDecoderCore decoder = new(options); + Image image = decoder.Decode(options.GeneralOptions.Configuration, stream, cancellationToken); + + if (options.ResizeMode != JpegDecoderResizeMode.IdctOnly) + { + ScaleToTargetSize(options.GeneralOptions, image); + } + + return image; + } + + /// + protected override Image Decode(JpegDecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(options, stream, cancellationToken); + + /// + protected override JpegDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) + => new() { GeneralOptions = options }; + } +} diff --git a/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs new file mode 100644 index 0000000..d0ccbf3 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -0,0 +1,1663 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Jpeg.Components; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using SixLabors.ImageSharp.IO; +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.Iptc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; + +namespace SixLabors.ImageSharp.Formats.Jpeg { + /// + /// Performs the jpeg decoding operation. + /// Originally ported from + /// with additional fixes for both performance and common encoding errors. + /// + internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData + { + /// + /// Whether the image has an EXIF marker. + /// + private bool hasExif; + + /// + /// Contains exif data. + /// + private byte[] exifData; + + /// + /// Whether the image has an ICC marker. + /// + private bool hasIcc; + + /// + /// Contains ICC data. + /// + private byte[] iccData; + + /// + /// Whether the image has a IPTC data. + /// + 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; + + /// + /// Whether the image has a APP14 adobe marker. This is needed to determine image encoded colorspace. + /// + private bool hasAdobeMarker; + + /// + /// Whether the image has a SOS marker. + /// + private bool hasSOSMarker; + + /// + /// Contains information about the JFIF marker. + /// + private JFifMarker jFif; + + /// + /// Contains information about the Adobe marker. + /// + private AdobeMarker adobe; + + /// + /// Scan decoder. + /// + private IJpegScanDecoder scanDecoder; + + /// + /// The arithmetic decoding tables. + /// + private List arithmeticDecodingTables; + + /// + /// The restart interval. + /// + private int? resetInterval; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// Whether to skip metadata during decode. + /// + private readonly bool skipMetadata; + + /// + /// The jpeg specific resize options. + /// + private readonly JpegDecoderResizeMode resizeMode; + + /// + /// Initializes a new instance of the class. + /// + /// The decoder options. + /// The ICC profile to use for color conversion. + public JpegDecoderCore(JpegDecoderOptions options, IccProfile iccProfile = null) + : base(options.GeneralOptions) + { + this.resizeMode = options.ResizeMode; + this.configuration = options.GeneralOptions.Configuration; + this.skipMetadata = options.GeneralOptions.SkipMetadata; + this.SetIccMetadata(iccProfile); + } + + /// + /// Gets the only supported precisions + /// + // Refers to assembly's static data segment, no allocation occurs. + private static ReadOnlySpan SupportedPrecisions => [8, 12]; + + /// + /// Gets the frame + /// + public JpegFrame Frame { get; private set; } + + /// + /// Gets the decoded by this decoder instance. + /// + public ImageMetadata Metadata { get; private set; } + + /// + public JpegColorSpace ColorSpace { get; private set; } + + /// + /// Gets the components. + /// + public JpegComponent[] Components => this.Frame.Components; + + /// + JpegComponent[] IRawJpegData.Components => this.Components; + + /// + public Block8x8F[] QuantizationTables { get; private set; } + + /// + /// Finds the next file marker within the byte stream. + /// + /// The input stream. + /// The . + public static JpegFileMarker FindNextFileMarker(BufferedReadStream stream) + { + while (true) + { + int b = stream.ReadByte(); + if (b == -1) + { + return new JpegFileMarker(JpegConstants.Markers.EOI, stream.Length - 2); + } + + // Found a marker. + if (b == JpegConstants.Markers.XFF) + { + while (b == JpegConstants.Markers.XFF) + { + // Loop here to discard any padding FF bytes on terminating marker. + b = stream.ReadByte(); + if (b == -1) + { + return new JpegFileMarker(JpegConstants.Markers.EOI, stream.Length - 2); + } + } + + // Found a valid marker. Exit loop + if (b is not 0 and (< JpegConstants.Markers.RST0 or > JpegConstants.Markers.RST7)) + { + return new JpegFileMarker((byte)(uint)b, stream.Position - 2); + } + } + } + } + + /// + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + { + using SpectralConverter spectralConverter = new(this.configuration, this.resizeMode == JpegDecoderResizeMode.ScaleOnly ? null : this.Options.TargetSize); + this.ParseStream(stream, spectralConverter, cancellationToken); + + if (!this.hasSOSMarker) + { + JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker."); + } + + this.InitializeMetadataProfiles(); + + _ = this.Options.TryGetIccProfileForColorConversion(this.Metadata.IccProfile, out IccProfile profile); + + return new Image( + this.configuration, + spectralConverter.GetPixelBuffer(profile, cancellationToken), + this.Metadata); + } + + /// + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + this.ParseStream(stream, spectralConverter: null, cancellationToken); + + if (!this.hasSOSMarker) + { + JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker."); + } + + this.InitializeMetadataProfiles(); + + Size pixelSize = this.Frame.PixelSize; + return new ImageInfo(new Size(pixelSize.Width, pixelSize.Height), this.Metadata); + } + + /// + /// Load quantization and/or Huffman tables for subsequent use for jpeg's embedded in tiff's, + /// so those tables do not need to be duplicated with segmented tiff's (tiff's with multiple strips). + /// + /// The table bytes. + /// The scan decoder. + public void LoadTables(byte[] tableBytes, IJpegScanDecoder scanDecoder) + { + this.Metadata ??= new ImageMetadata(); + this.QuantizationTables = new Block8x8F[4]; + this.scanDecoder = scanDecoder; + if (tableBytes.Length < 4) + { + JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read marker"); + } + + using MemoryStream ms = new(tableBytes); + using BufferedReadStream stream = new(this.configuration, ms); + + Span markerBuffer = stackalloc byte[2]; + + // Check for the Start Of Image marker. + int bytesRead = stream.Read(markerBuffer); + JpegFileMarker fileMarker = new(markerBuffer[1], 0); + if (fileMarker.Marker != JpegConstants.Markers.SOI) + { + JpegThrowHelper.ThrowInvalidImageContentException("Missing SOI marker."); + } + + // Read next marker. + bytesRead = stream.Read(markerBuffer); + fileMarker = new JpegFileMarker(markerBuffer[1], (int)stream.Position - 2); + + while (fileMarker.Marker != JpegConstants.Markers.EOI || (fileMarker.Marker == JpegConstants.Markers.EOI && fileMarker.Invalid)) + { + if (!fileMarker.Invalid) + { + // Get the marker length. + int markerContentByteSize = ReadUint16(stream, markerBuffer) - 2; + + // Check whether the stream actually has enough bytes to read + // markerContentByteSize is always positive so we cast + // to uint to avoid sign extension + if (stream.RemainingBytes < (uint)markerContentByteSize) + { + JpegThrowHelper.ThrowNotEnoughBytesForMarker(fileMarker.Marker); + } + + switch (fileMarker.Marker) + { + case JpegConstants.Markers.SOI: + case JpegConstants.Markers.RST0: + case JpegConstants.Markers.RST7: + break; + case JpegConstants.Markers.DHT: + this.ProcessDefineHuffmanTablesMarker(stream, markerContentByteSize); + break; + case JpegConstants.Markers.DQT: + this.ProcessDefineQuantizationTablesMarker(stream, markerContentByteSize); + break; + case JpegConstants.Markers.DRI: + this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize, markerBuffer); + break; + case JpegConstants.Markers.EOI: + return; + } + } + + // Read next marker. + bytesRead = stream.Read(markerBuffer); + if (bytesRead != 2) + { + JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read marker"); + } + + fileMarker = new JpegFileMarker(markerBuffer[1], 0); + } + } + + /// + /// Parses the input stream for file markers. + /// + /// The input stream. + /// The spectral converter to use. + /// The token to monitor cancellation. + internal void ParseStream(BufferedReadStream stream, SpectralConverter spectralConverter, CancellationToken cancellationToken) + { + bool metadataOnly = spectralConverter == null; + + this.scanDecoder ??= new HuffmanScanDecoder(stream, spectralConverter, cancellationToken); + + this.Metadata ??= new ImageMetadata(); + + Span markerBuffer = stackalloc byte[2]; + + // Check for the Start Of Image marker. + stream.Read(markerBuffer); + JpegFileMarker fileMarker = new(markerBuffer[1], 0); + if (fileMarker.Marker != JpegConstants.Markers.SOI) + { + JpegThrowHelper.ThrowInvalidImageContentException("Missing SOI marker."); + } + + fileMarker = FindNextFileMarker(stream); + this.QuantizationTables ??= new Block8x8F[4]; + + // Break only when we discover a valid EOI marker. + // https://github.com/SixLabors/ImageSharp/issues/695 + while (fileMarker.Marker != JpegConstants.Markers.EOI) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!fileMarker.Invalid) + { + // Get the marker length. + int markerContentByteSize = ReadUint16(stream, markerBuffer) - 2; + + // Check whether stream actually has enough bytes to read + // markerContentByteSize is always positive so we cast + // to uint to avoid sign extension. + if (stream.RemainingBytes < (uint)markerContentByteSize) + { + if (metadataOnly && this.Metadata != null && this.Frame != null) + { + // We have enough data to decode the image, so we can stop parsing. + return; + } + + if (this.Metadata != null && this.Frame != null && spectralConverter.HasPixelBuffer()) + { + // We have enough data to decode the image, so we can stop parsing. + return; + } + + JpegThrowHelper.ThrowNotEnoughBytesForMarker(fileMarker.Marker); + } + + switch (fileMarker.Marker) + { + case JpegConstants.Markers.SOF0: + case JpegConstants.Markers.SOF1: + case JpegConstants.Markers.SOF2: + + if (!this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Huffman, metadataOnly)) + { + return; + } + + break; + + case JpegConstants.Markers.SOF9: + case JpegConstants.Markers.SOF10: + case JpegConstants.Markers.SOF13: + case JpegConstants.Markers.SOF14: + this.scanDecoder = new ArithmeticScanDecoder(stream, spectralConverter, cancellationToken); + if (this.resetInterval.HasValue) + { + this.scanDecoder.ResetInterval = this.resetInterval.Value; + } + + if (!this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Arithmetic, metadataOnly)) + { + return; + } + + break; + + case JpegConstants.Markers.SOF5: + JpegThrowHelper.ThrowNotSupportedException("Decoding jpeg files with differential sequential DCT is not supported."); + break; + + case JpegConstants.Markers.SOF6: + JpegThrowHelper.ThrowNotSupportedException("Decoding jpeg files with differential progressive DCT is not supported."); + break; + + case JpegConstants.Markers.SOF3: + case JpegConstants.Markers.SOF7: + JpegThrowHelper.ThrowNotSupportedException("Decoding lossless jpeg files is not supported."); + break; + + case JpegConstants.Markers.SOF11: + case JpegConstants.Markers.SOF15: + JpegThrowHelper.ThrowNotSupportedException("Decoding jpeg files with lossless arithmetic coding is not supported."); + break; + + case JpegConstants.Markers.SOS: + + this.hasSOSMarker = true; + if (!metadataOnly) + { + this.ProcessStartOfScanMarker(stream, markerContentByteSize); + break; + } + + // It's highly unlikely that APPn related data will be found after the SOS marker + // So we can stop parsing here and return the metadata we have parsed so far, instead + // of trying to parse any APPn markers after the SOS marker and risking running out of + // memory or other exceptions. + return; + + case JpegConstants.Markers.DHT: + + if (metadataOnly) + { + stream.Skip(markerContentByteSize); + } + else + { + this.ProcessDefineHuffmanTablesMarker(stream, markerContentByteSize); + } + + break; + + case JpegConstants.Markers.DQT: + this.ProcessDefineQuantizationTablesMarker(stream, markerContentByteSize); + break; + + case JpegConstants.Markers.DRI: + if (metadataOnly) + { + stream.Skip(markerContentByteSize); + } + else + { + this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize, markerBuffer); + } + + break; + + case JpegConstants.Markers.APP0: + this.ProcessApplicationHeaderMarker(stream, markerContentByteSize); + break; + + case JpegConstants.Markers.APP1: + this.ProcessApp1Marker(stream, markerContentByteSize); + break; + + case JpegConstants.Markers.APP2: + this.ProcessApp2Marker(stream, markerContentByteSize); + break; + + case JpegConstants.Markers.APP3: + case JpegConstants.Markers.APP4: + case JpegConstants.Markers.APP5: + case JpegConstants.Markers.APP6: + case JpegConstants.Markers.APP7: + case JpegConstants.Markers.APP8: + case JpegConstants.Markers.APP9: + case JpegConstants.Markers.APP10: + case JpegConstants.Markers.APP11: + case JpegConstants.Markers.APP12: + stream.Skip(markerContentByteSize); + break; + + case JpegConstants.Markers.APP13: + this.ProcessApp13Marker(stream, markerContentByteSize); + break; + + case JpegConstants.Markers.APP14: + this.ProcessApp14Marker(stream, markerContentByteSize); + break; + + case JpegConstants.Markers.APP15: + stream.Skip(markerContentByteSize); + break; + case JpegConstants.Markers.COM: + this.ProcessComMarker(stream, markerContentByteSize); + break; + + case JpegConstants.Markers.DAC: + if (metadataOnly) + { + stream.Skip(markerContentByteSize); + } + else + { + this.ProcessArithmeticTable(stream, markerContentByteSize); + } + + break; + } + } + + // Read on. + fileMarker = FindNextFileMarker(stream); + } + + if (!metadataOnly && this.Frame is null) + { + JpegThrowHelper.ThrowInvalidImageContentException("No readable SOFn (Start Of Frame) marker found."); + } + + this.Metadata.GetJpegMetadata().Interleaved = this.Frame.Interleaved; + } + + /// + public void Dispose() + { + this.Frame?.Dispose(); + + // Set large fields to null. + this.Frame = null; + this.scanDecoder = null; + } + + /// + /// Assigns COM marker bytes to comment property + /// + /// The input stream. + /// The remaining bytes in the segment block. + private void ProcessComMarker(BufferedReadStream stream, int markerContentByteSize) + { + char[] chars = new char[markerContentByteSize]; + JpegMetadata metadata = this.Metadata.GetFormatMetadata(JpegFormat.Instance); + + for (int i = 0; i < markerContentByteSize; i++) + { + int read = stream.ReadByte(); + chars[i] = (char)read; + } + + metadata.Comments.Add(new JpegComData(chars)); + } + + /// + /// Returns encoded colorspace based on the adobe APP14 marker. + /// + /// Number of components. + /// Parsed adobe APP14 marker. + /// The + internal static JpegColorSpace DeduceJpegColorSpace(byte componentCount, ref AdobeMarker adobeMarker) + { + if (componentCount == 1) + { + return JpegColorSpace.Grayscale; + } + + if (componentCount == 3) + { + if (adobeMarker.ColorTransform == JpegConstants.Adobe.ColorTransformUnknown) + { + return JpegColorSpace.RGB; + } + + return JpegColorSpace.YCbCr; + } + + if (componentCount == 4) + { + if (adobeMarker.ColorTransform == JpegConstants.Adobe.ColorTransformYcck) + { + return JpegColorSpace.Ycck; + } + + return JpegColorSpace.Cmyk; + } + + JpegThrowHelper.ThrowNotSupportedComponentCount(componentCount); + return default; + } + + /// + /// Returns encoded colorspace based on the component count. + /// + /// Number of components. + /// The + internal static JpegColorSpace DeduceJpegColorSpace(byte componentCount) + { + if (componentCount == 1) + { + return JpegColorSpace.Grayscale; + } + + if (componentCount == 3) + { + return JpegColorSpace.YCbCr; + } + + if (componentCount == 4) + { + return JpegColorSpace.Cmyk; + } + + JpegThrowHelper.ThrowNotSupportedComponentCount(componentCount); + return default; + } + + /// + /// Returns the jpeg color type based on the colorspace and subsampling used. + /// + /// Jpeg color type. + private JpegColorType DeduceJpegColorType() + { + switch (this.ColorSpace) + { + case JpegColorSpace.Grayscale: + return JpegColorType.Luminance; + + case JpegColorSpace.RGB: + return JpegColorType.Rgb; + + case JpegColorSpace.YCbCr: + if (this.Frame.Components[0].HorizontalSamplingFactor == 1 && this.Frame.Components[0].VerticalSamplingFactor == 1 && + this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && + this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) + { + return JpegColorType.YCbCrRatio444; + } + else if (this.Frame.Components[0].HorizontalSamplingFactor == 2 && this.Frame.Components[0].VerticalSamplingFactor == 1 && + this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && + this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) + { + return JpegColorType.YCbCrRatio422; + } + else if (this.Frame.Components[0].HorizontalSamplingFactor == 2 && this.Frame.Components[0].VerticalSamplingFactor == 2 && + this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && + this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) + { + return JpegColorType.YCbCrRatio420; + } + else if (this.Frame.Components[0].HorizontalSamplingFactor == 4 && this.Frame.Components[0].VerticalSamplingFactor == 1 && + this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && + this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) + { + return JpegColorType.YCbCrRatio411; + } + else if (this.Frame.Components[0].HorizontalSamplingFactor == 4 && this.Frame.Components[0].VerticalSamplingFactor == 2 && + this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && + this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) + { + return JpegColorType.YCbCrRatio410; + } + else + { + return JpegColorType.YCbCrRatio420; + } + + case JpegColorSpace.Cmyk: + return JpegColorType.Cmyk; + case JpegColorSpace.Ycck: + return JpegColorType.Ycck; + default: + return JpegColorType.YCbCrRatio420; + } + } + + /// + /// Initializes the EXIF profile. + /// + private void InitExifProfile() + { + if (this.hasExif) + { + this.Metadata.ExifProfile = new ExifProfile(this.exifData); + } + } + + /// + /// Initializes the ICC profile. + /// + private void InitIccProfile() + { + if (this.hasIcc && this.Metadata.IccProfile == null) + { + IccProfile profile = new(this.iccData); + if (profile.CheckIsValid()) + { + this.Metadata.IccProfile = profile; + } + else + { + throw new InvalidIccProfileException("Invalid ICC profile."); + } + } + } + + private void SetIccMetadata(IccProfile profile) + { + if (!this.skipMetadata && profile?.CheckIsValid() == true) + { + this.hasIcc = true; + this.Metadata ??= new ImageMetadata(); + this.Metadata.IccProfile = profile; + } + } + + /// + /// Initializes the IPTC profile. + /// + private void InitIptcProfile() + { + if (this.hasIptc) + { + this.Metadata.IptcProfile = new IptcProfile(this.iptcData); + } + } + + /// + /// Initializes the XMP profile. + /// + private void InitXmpProfile() + { + if (this.hasXmp) + { + this.Metadata.XmpProfile = new XmpProfile(this.xmpData); + } + } + + /// + /// Assigns derived metadata properties to , eg. horizontal and vertical resolution if it has a JFIF header. + /// + private void InitDerivedMetadataProperties() + { + if (this.jFif.XDensity > 0 && this.jFif.YDensity > 0) + { + this.Metadata.HorizontalResolution = this.jFif.XDensity; + this.Metadata.VerticalResolution = this.jFif.YDensity; + this.Metadata.ResolutionUnits = this.jFif.DensityUnits; + } + else if (this.hasExif) + { + double horizontalValue = this.GetExifResolutionValue(ExifTag.XResolution); + double verticalValue = this.GetExifResolutionValue(ExifTag.YResolution); + + if (horizontalValue > 0 && verticalValue > 0) + { + this.Metadata.HorizontalResolution = horizontalValue; + this.Metadata.VerticalResolution = verticalValue; + this.Metadata.ResolutionUnits = UnitConverter.ExifProfileToResolutionUnit(this.Metadata.ExifProfile); + } + } + } + + private double GetExifResolutionValue(ExifTag tag) + { + if (this.Metadata.ExifProfile.TryGetValue(tag, out IExifValue resolution)) + { + return resolution.Value.ToDouble(); + } + + return 0; + } + + /// + /// Initializes decoded metadata profiles using the configured ancillary segment handling policy. + /// + private void InitializeMetadataProfiles() + { + this.ExecuteAncillarySegmentAction(this.InitExifProfile); + this.ExecuteAncillarySegmentAction(this.InitIccProfile); + this.ExecuteAncillarySegmentAction(this.InitIptcProfile); + this.ExecuteAncillarySegmentAction(this.InitXmpProfile); + this.ExecuteAncillarySegmentAction(this.InitDerivedMetadataProperties); + } + + /// + /// Extends the profile with additional data. + /// + /// The profile data array. + /// The array containing addition profile data. + private static void ExtendProfile(ref byte[] profile, byte[] extension) + { + int currentLength = profile.Length; + + Array.Resize(ref profile, currentLength + extension.Length); + Buffer.BlockCopy(extension, 0, profile, currentLength, extension.Length); + } + + /// + /// Processes the application header containing the JFIF identifier plus extra data. + /// + /// The input stream. + /// The remaining bytes in the segment block. + private void ProcessApplicationHeaderMarker(BufferedReadStream stream, int remaining) + { + // We can only decode JFif identifiers. + // Some images contain multiple JFIF markers (Issue 1932) so we check to see + // if it's already been read. + if (remaining < JFifMarker.Length) + { + this.ThrowOrIgnoreNonStrictSegmentError("Bad App0 Marker length."); + + // Skip the application header length + stream.Skip(remaining); + return; + } + + if (!this.jFif.Equals(default)) + { + // Skip the application header length + stream.Skip(remaining); + return; + } + + Span temp = stackalloc byte[2 * 16 * 4]; + + stream.Read(temp, 0, JFifMarker.Length); + if (!JFifMarker.TryParse(temp, out this.jFif)) + { + this.ThrowOrIgnoreNonStrictSegmentError("Invalid App0 marker."); + } + + remaining -= JFifMarker.Length; + + // TODO: thumbnail + if (remaining > 0) + { + if (stream.Position + remaining >= stream.Length) + { + this.ThrowOrIgnoreNonStrictSegmentError("Bad App0 Marker length."); + stream.Skip(remaining); + return; + } + + stream.Skip(remaining); + } + } + + /// + /// Processes the App1 marker retrieving any stored metadata. + /// + /// The input stream. + /// The remaining bytes in the segment block. + private void ProcessApp1Marker(BufferedReadStream stream, int remaining) + { + const int exifMarkerLength = 6; + const int xmpMarkerLength = 29; + if (remaining < exifMarkerLength) + { + this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length."); + + // Skip the application header length. + stream.Skip(remaining); + return; + } + + if (this.skipMetadata) + { + // Skip the application header length. + stream.Skip(remaining); + return; + } + + if (stream.Position + remaining >= stream.Length) + { + this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length."); + stream.Skip(remaining); + return; + } + + Span temp = stackalloc byte[2 * 16 * 4]; + + // XMP marker is the longer then the EXIF marker, so first try read the EXIF marker bytes. + stream.Read(temp, 0, exifMarkerLength); + remaining -= exifMarkerLength; + + if (ProfileResolver.IsProfile(temp, ProfileResolver.ExifMarker)) + { + this.hasExif = true; + byte[] profile = new byte[remaining]; + stream.Read(profile, 0, remaining); + + if (this.exifData is null) + { + this.exifData = profile; + } + else + { + // If the EXIF information exceeds 64K, it will be split over multiple APP1 markers. + ExtendProfile(ref this.exifData, profile); + } + + remaining = 0; + } + + if (ProfileResolver.IsProfile(temp, ProfileResolver.XmpMarker[..exifMarkerLength])) + { + const int remainingXmpMarkerBytes = xmpMarkerLength - exifMarkerLength; + if (remaining < remainingXmpMarkerBytes) + { + this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length."); + + // Skip the application header length. + stream.Skip(remaining); + return; + } + + stream.Read(temp, exifMarkerLength, remainingXmpMarkerBytes); + remaining -= remainingXmpMarkerBytes; + if (ProfileResolver.IsProfile(temp, ProfileResolver.XmpMarker)) + { + 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. + ExtendProfile(ref this.xmpData, profile); + } + + remaining = 0; + } + else + { + this.ThrowOrIgnoreNonStrictSegmentError("Invalid App1 marker."); + } + } + + // Skip over any remaining bytes of this header. + stream.Skip(remaining); + } + + /// + /// Processes the App2 marker retrieving any stored ICC profile information + /// + /// The input stream. + /// The remaining bytes in the segment block. + private void ProcessApp2Marker(BufferedReadStream stream, int remaining) + { + // Length is 14 though we only need to check 12. + const int icclength = 14; + if (remaining < icclength) + { + this.ThrowOrIgnoreNonStrictSegmentError("Bad App2 Marker length."); + + stream.Skip(remaining); + return; + } + + if (this.skipMetadata) + { + stream.Skip(remaining); + return; + } + + Span identifier = stackalloc byte[icclength]; + stream.Read(identifier); + remaining -= icclength; // We have read it by this point + + if (ProfileResolver.IsProfile(identifier, ProfileResolver.IccMarker)) + { + this.hasIcc = true; + byte[] profile = new byte[remaining]; + stream.Read(profile, 0, remaining); + + if (this.iccData is null) + { + this.iccData = profile; + } + else + { + // If the ICC information exceeds 64K, it will be split over multiple APP2 markers + ExtendProfile(ref this.iccData, profile); + } + } + else + { + // Not an ICC profile we can handle. Skip the remaining bytes so we can carry on and ignore this. + stream.Skip(remaining); + } + } + + /// + /// Processes a App13 marker, which contains IPTC data stored with Adobe Photoshop. + /// The tableBytes of an APP13 segment is formed by an identifier string followed by a sequence of resource data blocks. + /// + /// The input stream. + /// The remaining bytes in the segment block. + private void ProcessApp13Marker(BufferedReadStream stream, int remaining) + { + if (remaining < ProfileResolver.AdobePhotoshopApp13Marker.Length) + { + this.ThrowOrIgnoreNonStrictSegmentError("Bad App13 Marker length."); + + stream.Skip(remaining); + return; + } + + if (this.skipMetadata) + { + stream.Skip(remaining); + return; + } + + Span temp = stackalloc byte[2 * 16 * 4]; + stream.Read(temp, 0, ProfileResolver.AdobePhotoshopApp13Marker.Length); + remaining -= ProfileResolver.AdobePhotoshopApp13Marker.Length; + if (ProfileResolver.IsProfile(temp, ProfileResolver.AdobePhotoshopApp13Marker)) + { + Span blockDataSpan = remaining <= 128 ? stackalloc byte[remaining] : new byte[remaining]; + stream.Read(blockDataSpan); + + while (blockDataSpan.Length > 12) + { + if (!ProfileResolver.IsProfile(blockDataSpan[..4], ProfileResolver.AdobeImageResourceBlockMarker)) + { + this.ThrowOrIgnoreNonStrictSegmentError("Invalid App13 marker."); + return; + } + + blockDataSpan = blockDataSpan[4..]; + Span imageResourceBlockId = blockDataSpan[..2]; + if (ProfileResolver.IsProfile(imageResourceBlockId, ProfileResolver.AdobeIptcMarker)) + { + int resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan); + int resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength); + int dataStartIdx = 2 + resourceBlockNameLength + 4; + if (resourceDataSize > 0 && blockDataSpan.Length >= dataStartIdx + resourceDataSize) + { + this.hasIptc = true; + this.iptcData = blockDataSpan.Slice(dataStartIdx, resourceDataSize).ToArray(); + break; + } + + this.ThrowOrIgnoreNonStrictSegmentError("Invalid App13 marker."); + return; + } + else + { + int resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan); + int resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength); + int dataStartIdx = 2 + resourceBlockNameLength + 4; + if (blockDataSpan.Length < dataStartIdx + resourceDataSize) + { + // Not enough data or the resource data size is wrong. + this.ThrowOrIgnoreNonStrictSegmentError("Invalid App13 marker."); + break; + } + + blockDataSpan = blockDataSpan[(dataStartIdx + resourceDataSize)..]; + } + } + } + else + { + // If the profile is unknown skip over the rest of it. + stream.Skip(remaining); + } + } + + /// + /// Processes a DAC marker, decoding the arithmetic tables. + /// + /// The input stream. + /// The remaining bytes in the segment block. + private void ProcessArithmeticTable(BufferedReadStream stream, int remaining) + { + this.arithmeticDecodingTables ??= new List(4); + + while (remaining > 0) + { + int tableClassAndIdentifier = stream.ReadByte(); + remaining--; + byte tableClass = (byte)(tableClassAndIdentifier >> 4); + byte identifier = (byte)(tableClassAndIdentifier & 0xF); + + byte conditioningTableValue = (byte)stream.ReadByte(); + remaining--; + + ArithmeticDecodingTable arithmeticTable = new(tableClass, identifier); + arithmeticTable.Configure(conditioningTableValue); + + bool tableEntryReplaced = false; + for (int i = 0; i < this.arithmeticDecodingTables.Count; i++) + { + ArithmeticDecodingTable item = this.arithmeticDecodingTables[i]; + if (item.TableClass == arithmeticTable.TableClass && item.Identifier == arithmeticTable.Identifier) + { + this.arithmeticDecodingTables[i] = arithmeticTable; + tableEntryReplaced = true; + break; + } + } + + if (!tableEntryReplaced) + { + this.arithmeticDecodingTables.Add(arithmeticTable); + } + } + } + + /// + /// Reads the adobe image resource block name: a Pascal string (padded to make size even). + /// + /// The span holding the block resource data. + /// The length of the name. + [MethodImpl(InliningOptions.ShortMethod)] + private static int ReadImageResourceNameLength(Span blockDataSpan) + { + byte nameLength = blockDataSpan[2]; + int nameDataSize = nameLength == 0 ? 2 : nameLength; + if (nameDataSize % 2 != 0) + { + nameDataSize++; + } + + return nameDataSize; + } + + /// + /// Reads the length of a adobe image resource data block. + /// + /// The span holding the block resource data. + /// The length of the block name. + /// The block length. + [MethodImpl(InliningOptions.ShortMethod)] + private static int ReadResourceDataLength(Span blockDataSpan, int resourceBlockNameLength) + => BinaryPrimitives.ReadInt32BigEndian(blockDataSpan.Slice(2 + resourceBlockNameLength, 4)); + + /// + /// Processes the application header containing the Adobe identifier + /// which stores image encoding information for DCT filters. + /// + /// The input stream. + /// The remaining bytes in the segment block. + private void ProcessApp14Marker(BufferedReadStream stream, int remaining) + { + const int markerLength = AdobeMarker.Length; + if (remaining < markerLength) + { + this.ThrowOrIgnoreNonStrictSegmentError("Bad App14 Marker length."); + + // Skip the application header length + stream.Skip(remaining); + return; + } + + Span temp = stackalloc byte[2 * 16 * 4]; + + stream.Read(temp, 0, markerLength); + remaining -= markerLength; + + if (AdobeMarker.TryParse(temp, out this.adobe)) + { + this.hasAdobeMarker = true; + } + else + { + this.ThrowOrIgnoreNonStrictSegmentError("Invalid App14 marker."); + } + + if (remaining > 0) + { + stream.Skip(remaining); + } + } + + /// + /// Processes the Define Quantization Marker and tables. Specified in section B.2.4.1. + /// + /// The input stream. + /// The remaining bytes in the segment block. + /// + /// Thrown if the tables do not match the header. + /// + private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, int remaining) + { + JpegMetadata jpegMetadata = this.Metadata.GetFormatMetadata(JpegFormat.Instance); + Span temp = stackalloc byte[2 * 16 * 4]; + + while (remaining > 0) + { + // 1 byte: quantization table spec + // bit 0..3: table index (0..3) + // bit 4..7: table precision (0 = 8 bit, 1 = 16 bit) + int quantizationTableSpec = stream.ReadByte(); + int tableIndex = quantizationTableSpec & 15; + int tablePrecision = quantizationTableSpec >> 4; + + // Validate: + if (tableIndex > 3) + { + JpegThrowHelper.ThrowBadQuantizationTableIndex(tableIndex); + } + + remaining--; + + // Decoding single 8x8 table + ref Block8x8F table = ref this.QuantizationTables[tableIndex]; + switch (tablePrecision) + { + // 8 bit values + case 0: + // Validate: 8 bit table needs exactly 64 bytes + if (remaining < 64) + { + JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DQT), remaining); + } + + stream.Read(temp, 0, 64); + remaining -= 64; + + // Parsing quantization table & saving it in natural order + for (int j = 0; j < 64; j++) + { + table[ZigZag.ZigZagOrder[j]] = temp[j]; + } + + break; + + // 16 bit values + case 1: + // Validate: 16 bit table needs exactly 128 bytes + if (remaining < 128) + { + JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DQT), remaining); + } + + stream.Read(temp, 0, 128); + remaining -= 128; + + // Parsing quantization table & saving it in natural order + for (int j = 0; j < 64; j++) + { + table[ZigZag.ZigZagOrder[j]] = (temp[2 * j] << 8) | temp[(2 * j) + 1]; + } + + break; + + // Unknown precision - error + default: + JpegThrowHelper.ThrowBadQuantizationTablePrecision(tablePrecision); + break; + } + + // Estimating quality + switch (tableIndex) + { + // luminance table + case 0: + jpegMetadata.LuminanceQuality = Quantization.EstimateLuminanceQuality(ref table); + break; + + // chrominance table + case 1: + jpegMetadata.ChrominanceQuality = Quantization.EstimateChrominanceQuality(ref table); + break; + } + } + } + + /// + /// Processes the Start of Frame marker. Specified in section B.2.2. + /// + /// The input stream. + /// The remaining bytes in the segment block. + /// The current frame marker. + /// The jpeg decoding component type. + /// Whether to parse metadata only. + private bool ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, in JpegFileMarker frameMarker, ComponentType decodingComponentType, bool metadataOnly) + { + if (this.Frame != null) + { + // If we have found the SOS marker, we can stop parsing as we have all + // the information we need to decode the image. + // It's possible that there are APPn related markers after the SOS marker, + // but it's highly unlikely and we would be better off stopping parsing + // and decoding the image instead of trying to parse those APPn markers + // and risking running out of memory or other exceptions. + if (this.hasSOSMarker) + { + return false; + } + + JpegThrowHelper.ThrowInvalidImageContentException("Multiple SOF markers. Only single frame jpegs supported."); + } + + Span temp = stackalloc byte[2 * 16 * 4]; + + // Read initial marker definitions. + const int length = 6; + int bytesRead = stream.Read(temp, 0, length); + if (bytesRead != length) + { + JpegThrowHelper.ThrowInvalidImageContentException("SOF marker does not contain enough data."); + } + + // 1 byte: Bits/sample precision. + byte precision = temp[0]; + + // Validate: only 8-bit and 12-bit precisions are supported. + if (SupportedPrecisions.IndexOf(precision) < 0) + { + JpegThrowHelper.ThrowInvalidImageContentException("Only 8-Bit and 12-Bit precision is supported."); + } + + // 2 byte: Height + int frameHeight = (temp[1] << 8) | temp[2]; + + // 2 byte: Width + int frameWidth = (temp[3] << 8) | temp[4]; + + // Validate: width/height > 0 (they are upper-bounded by 2 byte max value so no need to check that). + if (frameHeight == 0 || frameWidth == 0) + { + JpegThrowHelper.ThrowInvalidImageDimensions(frameWidth, frameHeight); + } + + // 1 byte: Number of components. + byte componentCount = temp[5]; + + // Validate: componentCount more than 4 can lead to a buffer overflow during stream + // reading so we must limit it to 4. + // We do not support jpeg images with more than 4 components anyway. + if (componentCount > 4) + { + JpegThrowHelper.ThrowNotSupportedComponentCount(componentCount); + } + + this.Frame = new JpegFrame(frameMarker, precision, frameWidth, frameHeight, componentCount); + this.Dimensions = new Size(frameWidth, frameHeight); + this.Metadata.GetJpegMetadata().Progressive = this.Frame.Progressive; + + remaining -= length; + + // Validate: remaining part must be equal to components * 3 + const int componentBytes = 3; + if (remaining != componentCount * componentBytes) + { + JpegThrowHelper.ThrowBadMarker("SOFn", remaining); + } + + // components*3 bytes: component data + stream.Read(temp, 0, remaining); + + // No need to pool this. They max out at 4 + this.Frame.ComponentIds = new byte[componentCount]; + this.Frame.ComponentOrder = new byte[componentCount]; + this.Frame.Components = new JpegComponent[componentCount]; + + int maxH = 0; + int maxV = 0; + int index = 0; + for (int i = 0; i < this.Frame.Components.Length; i++) + { + // 1 byte: component identifier + byte componentId = temp[index]; + + // 1 byte: component sampling factors + byte hv = temp[index + 1]; + int h = (hv >> 4) & 15; + int v = hv & 15; + + // Validate: 1-4 range + if (Numerics.IsOutOfRange(h, 1, 4)) + { + JpegThrowHelper.ThrowBadSampling(h); + } + + // Validate: 1-4 range + if (Numerics.IsOutOfRange(v, 1, 4)) + { + JpegThrowHelper.ThrowBadSampling(v); + } + + if (maxH < h) + { + maxH = h; + } + + if (maxV < v) + { + maxV = v; + } + + // 1 byte: quantization table destination selector + byte quantTableIndex = temp[index + 2]; + + // Validate: 0-3 range + if (quantTableIndex > 3) + { + JpegThrowHelper.ThrowBadQuantizationTableIndex(quantTableIndex); + } + + IJpegComponent component = decodingComponentType is ComponentType.Huffman ? + new JpegComponent(this.configuration.MemoryAllocator, this.Frame, componentId, h, v, quantTableIndex, i) : + new ArithmeticDecodingComponent(this.configuration.MemoryAllocator, this.Frame, componentId, h, v, quantTableIndex, i); + + this.Frame.Components[i] = (JpegComponent)component; + this.Frame.ComponentIds[i] = componentId; + + index += componentBytes; + } + + this.ColorSpace = this.hasAdobeMarker + ? DeduceJpegColorSpace(componentCount, ref this.adobe) + : DeduceJpegColorSpace(componentCount); + this.Metadata.GetJpegMetadata().ColorType = this.DeduceJpegColorType(); + + if (!metadataOnly) + { + this.Frame.Init(maxH, maxV); + this.scanDecoder.InjectFrameData(this.Frame, this); + } + + return true; + } + + /// + /// Processes a Define Huffman Table marker, and initializes a huffman + /// struct from its contents. Specified in section B.2.4.2. + /// + /// The input stream. + /// The remaining bytes in the segment block. + private void ProcessDefineHuffmanTablesMarker(BufferedReadStream stream, int remaining) + { + const int codeLengthsByteSize = 17; + const int codeValuesMaxByteSize = 256; + const int totalBufferSize = codeLengthsByteSize + codeValuesMaxByteSize + HuffmanTable.WorkspaceByteSize; + + HuffmanScanDecoder huffmanScanDecoder = this.scanDecoder as HuffmanScanDecoder; + if (huffmanScanDecoder is null) + { + JpegThrowHelper.ThrowInvalidImageContentException("missing huffman table data"); + } + + int length = remaining; + using (IMemoryOwner buffer = this.configuration.MemoryAllocator.Allocate(totalBufferSize)) + { + Span bufferSpan = buffer.GetSpan(); + Span huffmanLengthsSpan = bufferSpan[..codeLengthsByteSize]; + Span huffmanValuesSpan = bufferSpan.Slice(codeLengthsByteSize, codeValuesMaxByteSize); + Span tableWorkspace = MemoryMarshal.Cast(bufferSpan[(codeLengthsByteSize + codeValuesMaxByteSize)..]); + + for (int i = 2; i < remaining;) + { + byte huffmanTableSpec = (byte)stream.ReadByte(); + int tableType = huffmanTableSpec >> 4; + int tableIndex = huffmanTableSpec & 15; + + // Types 0..1 DC..AC + if (tableType > 1) + { + JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table type: {tableType}."); + } + + // Max tables of each type + if (tableIndex > 3) + { + JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table index: {tableIndex}."); + } + + stream.Read(huffmanLengthsSpan, 1, 16); + + int codeLengthSum = 0; + for (int j = 1; j < 17; j++) + { + codeLengthSum += huffmanLengthsSpan[j]; + } + + length -= 17; + + if (codeLengthSum > 256 || codeLengthSum > length) + { + JpegThrowHelper.ThrowInvalidImageContentException("Huffman table has excessive length."); + } + + stream.Read(huffmanValuesSpan, 0, codeLengthSum); + + i += 17 + codeLengthSum; + + huffmanScanDecoder!.BuildHuffmanTable( + tableType, + tableIndex, + huffmanLengthsSpan, + huffmanValuesSpan[..codeLengthSum], + tableWorkspace); + } + } + } + + /// + /// Processes the DRI (Define Restart Interval Marker) Which specifies the interval between RSTn markers, + /// in macroblocks. + /// + /// The input stream. + /// The remaining bytes in the segment block. + /// Scratch buffer. + private void ProcessDefineRestartIntervalMarker(BufferedReadStream stream, int remaining, Span markerBuffer) + { + if (remaining != 2) + { + JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DRI), remaining); + } + + // Save the reset interval, because it can come before or after the SOF marker. + // If the reset interval comes after the SOF marker, the scanDecoder has not been created. + this.resetInterval = ReadUint16(stream, markerBuffer); + + if (this.scanDecoder != null) + { + this.scanDecoder.ResetInterval = this.resetInterval.Value; + } + } + + /// + /// Processes the SOS (Start of scan marker). + /// + /// The input stream. + /// The remaining bytes in the segment block. + private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining) + { + if (this.Frame is null) + { + JpegThrowHelper.ThrowInvalidImageContentException("No readable SOFn (Start Of Frame) marker found."); + } + + // 1 byte: Number of components in scan. + int selectorsCount = stream.ReadByte(); + + // Validate: 0 < count <= totalComponents + if (selectorsCount == 0 || selectorsCount > this.Frame.ComponentCount) + { + // TODO: extract as separate method? + JpegThrowHelper.ThrowInvalidImageContentException($"Invalid number of components in scan: {selectorsCount}."); + } + + // Validate: Marker must contain exactly (4 + selectorsCount*2) bytes + int selectorsBytes = selectorsCount * 2; + if (remaining != 4 + selectorsBytes) + { + JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.SOS), remaining); + } + + Span temp = stackalloc byte[2 * 16 * 4]; + + // selectorsCount*2 bytes: component index + huffman tables indices + stream.Read(temp, 0, selectorsBytes); + + this.Frame.Interleaved = this.Frame.ComponentCount == selectorsCount; + for (int i = 0; i < selectorsBytes; i += 2) + { + // 1 byte: Component id + int componentSelectorId = temp[i]; + + int componentIndex = -1; + for (int j = 0; j < this.Frame.ComponentIds.Length; j++) + { + byte id = this.Frame.ComponentIds[j]; + if (componentSelectorId == id) + { + componentIndex = j; + break; + } + } + + // Validate: Must be found among registered components. + if (componentIndex == -1) + { + // TODO: extract as separate method? + JpegThrowHelper.ThrowInvalidImageContentException($"Unknown component id in scan: {componentSelectorId}."); + } + + this.Frame.ComponentOrder[i / 2] = (byte)componentIndex; + + JpegComponent component = this.Frame.Components[componentIndex]; + + // 1 byte: Huffman table selectors. + // 4 bits - dc + // 4 bits - ac + int tableSpec = temp[i + 1]; + int dcTableIndex = tableSpec >> 4; + int acTableIndex = tableSpec & 15; + + // Validate: both must be < 4 + if (dcTableIndex >= 4 || acTableIndex >= 4) + { + // TODO: extract as separate method? + JpegThrowHelper.ThrowInvalidImageContentException($"Invalid huffman table for component:{componentSelectorId}: dc={dcTableIndex}, ac={acTableIndex}"); + } + + component.DcTableId = dcTableIndex; + component.AcTableId = acTableIndex; + } + + // 3 bytes: Progressive scan decoding data. + int bytesRead = stream.Read(temp, 0, 3); + if (bytesRead != 3) + { + JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read progressive scan decoding data"); + } + + this.scanDecoder.SpectralStart = temp[0]; + + this.scanDecoder.SpectralEnd = temp[1]; + + int successiveApproximation = temp[2]; + this.scanDecoder.SuccessiveHigh = successiveApproximation >> 4; + this.scanDecoder.SuccessiveLow = successiveApproximation & 15; + + if (this.scanDecoder is ArithmeticScanDecoder arithmeticScanDecoder) + { + arithmeticScanDecoder.InitDecodingTables(this.arithmeticDecodingTables); + } + + this.ExecuteAncillarySegmentAction(this.InitIccProfile); + _ = this.Options.TryGetIccProfileForColorConversion(this.Metadata.IccProfile, out IccProfile profile); + this.scanDecoder.ParseEntropyCodedData(selectorsCount, profile); + } + + /// + /// Reads a from the stream advancing it by two bytes. + /// + /// The input stream. + /// The scratch buffer used for reading from the stream. + /// The + [MethodImpl(InliningOptions.ShortMethod)] + private static ushort ReadUint16(BufferedReadStream stream, Span markerBuffer) + { + int bytesRead = stream.Read(markerBuffer, 0, 2); + if (bytesRead != 2) + { + JpegThrowHelper.ThrowInvalidImageContentException("jpeg stream does not contain enough data, could not read ushort."); + } + + return BinaryPrimitives.ReadUInt16BigEndian(markerBuffer); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/JpegDecoderOptions.cs b/ImageSharp/Formats/Jpeg/JpegDecoderOptions.cs new file mode 100644 index 0000000..30105e7 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/JpegDecoderOptions.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jpeg { + /// + /// Configuration options for decoding Jpeg images. + /// + public sealed class JpegDecoderOptions : ISpecializedDecoderOptions + { + /// + public DecoderOptions GeneralOptions { get; init; } = new(); + + /// + /// Gets the resize mode. + /// + public JpegDecoderResizeMode ResizeMode { get; init; } + } +} diff --git a/ImageSharp/Formats/Jpeg/JpegDecoderResizeMode.cs b/ImageSharp/Formats/Jpeg/JpegDecoderResizeMode.cs new file mode 100644 index 0000000..58eb298 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/JpegDecoderResizeMode.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Formats.Jpeg { + /// + /// Provides enumeration for resize modes taken during decoding. + /// Applicable only when has a value. + /// + public enum JpegDecoderResizeMode + { + /// + /// Both and . + /// + Combined, + + /// + /// IDCT-only to nearest block scale. Similar in output to . + /// + IdctOnly, + + /// + /// Opt-out the IDCT part and only Resize. Can be useful in case of quality concerns. + /// + ScaleOnly + } +} diff --git a/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/ImageSharp/Formats/Jpeg/JpegEncoder.cs new file mode 100644 index 0000000..1a6ad82 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -0,0 +1,120 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Jpeg { + /// + /// Encoder for writing the data image to a stream in jpeg format. + /// + public sealed class JpegEncoder : ImageEncoder + { + /// + /// Backing field for . + /// + private int? quality; + + /// + /// Backing field for + /// + private int progressiveScans = 4; + + /// + /// Backing field for + /// + private int restartInterval; + + /// + /// Gets the quality, that will be used to encode the image. Quality + /// index must be between 1 and 100 (compression from max to min). + /// Defaults to 75. + /// + /// Quality factor must be in [1..100] range. + public int? Quality + { + get => this.quality; + init + { + if (value is < 1 or > 100) + { + throw new ArgumentException("Quality factor must be in [1..100] range."); + } + + this.quality = value; + } + } + + /// + /// Gets a value indicating whether progressive encoding is used. + /// + public bool Progressive { get; init; } + + /// + /// Gets number of scans per component for progressive encoding. + /// Defaults to 4. + /// + /// + /// Number of scans must be between 2 and 64. + /// There is at least one scan for the DC coefficients and one for the remaining 63 AC coefficients. + /// + /// Progressive scans must be in [2..64] range. + public int ProgressiveScans + { + get => this.progressiveScans; + init + { + if (value is < 2 or > 64) + { + throw new ArgumentException("Progressive scans must be in [2..64] range."); + } + + this.progressiveScans = value; + } + } + + /// + /// Gets numbers of MCUs between restart markers. + /// Defaults to 0. + /// + /// + /// Currently supported in progressive encoding only. + /// + /// Restart interval must be in [0..65535] range. + public int RestartInterval + { + get => this.restartInterval; + init + { + if (value is < 0 or > 65535) + { + throw new ArgumentException("Restart interval must be in [0..65535] range."); + } + + this.restartInterval = value; + } + } + + /// + /// Gets the component encoding mode. + /// + /// + /// Interleaved encoding mode encodes all color components in a single scan. + /// Non-interleaved encoding mode encodes each color component in a separate scan. + /// + public bool? Interleaved { get; init; } + + /// + /// Gets the jpeg color for encoding. + /// + public JpegColorType? ColorType { get; init; } + + /// + protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) + { + JpegEncoderCore encoder = new(this); + encoder.Encode(image, stream, cancellationToken); + } + } +} diff --git a/ImageSharp/Formats/Jpeg/JpegEncoderCore.FrameConfig.cs b/ImageSharp/Formats/Jpeg/JpegEncoderCore.FrameConfig.cs new file mode 100644 index 0000000..d1ccba8 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/JpegEncoderCore.FrameConfig.cs @@ -0,0 +1,195 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jpeg.Components; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder; + +namespace SixLabors.ImageSharp.Formats.Jpeg { + /// + /// Image encoder for writing an image to a stream as a jpeg. + /// + internal sealed unsafe partial class JpegEncoderCore + { + private static JpegFrameConfig[] CreateFrameConfigs() + { + JpegHuffmanTableConfig defaultLuminanceHuffmanDC = new(@class: 0, destIndex: 0, HuffmanSpec.LuminanceDC); + JpegHuffmanTableConfig defaultLuminanceHuffmanAC = new(@class: 1, destIndex: 0, HuffmanSpec.LuminanceAC); + JpegHuffmanTableConfig defaultChrominanceHuffmanDC = new(@class: 0, destIndex: 1, HuffmanSpec.ChrominanceDC); + JpegHuffmanTableConfig defaultChrominanceHuffmanAC = new(@class: 1, destIndex: 1, HuffmanSpec.ChrominanceAC); + + JpegQuantizationTableConfig defaultLuminanceQuantTable = new(0, Quantization.LuminanceTable); + JpegQuantizationTableConfig defaultChrominanceQuantTable = new(1, Quantization.ChrominanceTable); + + JpegHuffmanTableConfig[] yCbCrHuffmanConfigs = new JpegHuffmanTableConfig[] + { + defaultLuminanceHuffmanDC, + defaultLuminanceHuffmanAC, + defaultChrominanceHuffmanDC, + defaultChrominanceHuffmanAC, + }; + + JpegQuantizationTableConfig[] yCbCrQuantTableConfigs = new JpegQuantizationTableConfig[] + { + defaultLuminanceQuantTable, + defaultChrominanceQuantTable, + }; + + return new JpegFrameConfig[] + { + // YCbCr 4:4:4 + new( + JpegColorSpace.YCbCr, + JpegColorType.YCbCrRatio444, + new JpegComponentConfig[] + { + new(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), + new(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), + new(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), + }, + yCbCrHuffmanConfigs, + yCbCrQuantTableConfigs), + + // YCbCr 4:2:2 + new( + JpegColorSpace.YCbCr, + JpegColorType.YCbCrRatio422, + new JpegComponentConfig[] + { + new(id: 1, hsf: 2, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), + new(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), + new(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), + }, + yCbCrHuffmanConfigs, + yCbCrQuantTableConfigs), + + // YCbCr 4:2:0 + new( + JpegColorSpace.YCbCr, + JpegColorType.YCbCrRatio420, + new JpegComponentConfig[] + { + new(id: 1, hsf: 2, vsf: 2, quantIndex: 0, dcIndex: 0, acIndex: 0), + new(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), + new(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), + }, + yCbCrHuffmanConfigs, + yCbCrQuantTableConfigs), + + // YCbCr 4:1:1 + new( + JpegColorSpace.YCbCr, + JpegColorType.YCbCrRatio411, + new JpegComponentConfig[] + { + new(id: 1, hsf: 4, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), + new(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), + new(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), + }, + yCbCrHuffmanConfigs, + yCbCrQuantTableConfigs), + + // YCbCr 4:1:0 + new( + JpegColorSpace.YCbCr, + JpegColorType.YCbCrRatio410, + new JpegComponentConfig[] + { + new(id: 1, hsf: 4, vsf: 2, quantIndex: 0, dcIndex: 0, acIndex: 0), + new(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), + new(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), + }, + yCbCrHuffmanConfigs, + yCbCrQuantTableConfigs), + + // Luminance + new( + JpegColorSpace.Grayscale, + JpegColorType.Luminance, + new JpegComponentConfig[] + { + new(id: 0, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), + }, + new JpegHuffmanTableConfig[] + { + defaultLuminanceHuffmanDC, + defaultLuminanceHuffmanAC + }, + new JpegQuantizationTableConfig[] + { + defaultLuminanceQuantTable + }), + + // Rgb + new( + JpegColorSpace.RGB, + JpegColorType.Rgb, + new JpegComponentConfig[] + { + new(id: 82, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), + new(id: 71, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), + new(id: 66, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), + }, + new JpegHuffmanTableConfig[] + { + defaultLuminanceHuffmanDC, + defaultLuminanceHuffmanAC + }, + new JpegQuantizationTableConfig[] + { + defaultLuminanceQuantTable + }) + { + AdobeColorTransformMarkerFlag = JpegConstants.Adobe.ColorTransformUnknown + }, + + // Cmyk + new( + JpegColorSpace.Cmyk, + JpegColorType.Cmyk, + new JpegComponentConfig[] + { + new(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), + new(id: 2, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), + new(id: 3, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), + new(id: 4, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), + }, + new JpegHuffmanTableConfig[] + { + defaultLuminanceHuffmanDC, + defaultLuminanceHuffmanAC + }, + new JpegQuantizationTableConfig[] + { + defaultLuminanceQuantTable + }) + { + AdobeColorTransformMarkerFlag = JpegConstants.Adobe.ColorTransformUnknown, + }, + + // YccK + new( + JpegColorSpace.Ycck, + JpegColorType.Ycck, + new JpegComponentConfig[] + { + new(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), + new(id: 2, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), + new(id: 3, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), + new(id: 4, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), + }, + new JpegHuffmanTableConfig[] + { + defaultLuminanceHuffmanDC, + defaultLuminanceHuffmanAC + }, + new JpegQuantizationTableConfig[] + { + defaultLuminanceQuantTable + }) + { + AdobeColorTransformMarkerFlag = JpegConstants.Adobe.ColorTransformYcck, + }, + }; + } + } +} diff --git a/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs new file mode 100644 index 0000000..a152f3e --- /dev/null +++ b/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -0,0 +1,877 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.IO; +using System.Linq; +using System.Threading; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Jpeg.Components; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder; +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 { + /// + /// Image encoder for writing an image to a stream as a jpeg. + /// + internal sealed unsafe partial class JpegEncoderCore + { + /// + /// The available encodable frame configs. + /// + private static readonly JpegFrameConfig[] FrameConfigs = CreateFrameConfigs(); + + /// + /// The current calling encoder. + /// + private readonly JpegEncoder encoder; + + /// + /// The output stream. All attempted writes after the first error become no-ops. + /// + private Stream outputStream; + + /// + /// Initializes a new instance of the class. + /// + /// The parent encoder. + public JpegEncoderCore(JpegEncoder encoder) + => this.encoder = encoder; + + public Block8x8F[] QuantizationTables { get; } = new Block8x8F[4]; + + /// + /// Encode writes the image to the jpeg baseline format with the given options. + /// + /// The pixel format. + /// The image to write from. + /// The stream to write to. + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + if (image.Width > JpegConstants.MaxLength || image.Height > JpegConstants.MaxLength) + { + JpegThrowHelper.ThrowDimensionsTooLarge(image.Width, image.Height); + } + + cancellationToken.ThrowIfCancellationRequested(); + + this.outputStream = stream; + Span buffer = stackalloc byte[20]; + + ImageMetadata metadata = image.Metadata; + JpegMetadata jpegMetadata = metadata.GetJpegMetadata(); + JpegFrameConfig frameConfig = this.GetFrameConfig(jpegMetadata); + + bool interleaved = this.encoder.Interleaved ?? jpegMetadata.Interleaved; + using JpegFrame frame = new(image, frameConfig, interleaved); + + // Write the Start Of Image marker. + this.WriteStartOfImage(buffer); + + // Write APP0 marker + if (frameConfig.AdobeColorTransformMarkerFlag is null) + { + this.WriteJfifApplicationHeader(metadata, buffer); + } + + // Write APP14 marker with adobe color extension + else + { + this.WriteApp14Marker(frameConfig.AdobeColorTransformMarkerFlag.Value, buffer); + } + + // Write Exif, XMP, ICC and IPTC profiles + this.WriteProfiles(metadata, buffer); + + // Write comments + this.WriteComments(image.Configuration, jpegMetadata); + + // Write the image dimensions. + this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer); + + // Write the Huffman tables. + HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, this.encoder.RestartInterval, stream); + this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder, buffer); + + // Write the quantization tables. + this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.encoder.Quality, jpegMetadata, buffer); + + // Write define restart interval + this.WriteDri(this.encoder.RestartInterval, buffer); + + // Write scans with actual pixel data + using SpectralConverter spectralConverter = new(frame, image, this.QuantizationTables); + this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken); + + // Write the End Of Image marker. + this.WriteEndOfImageMarker(buffer); + + stream.Flush(); + } + + /// + /// Write the start of image marker. + /// + private void WriteStartOfImage(Span buffer) + { + // Markers are always prefixed with 0xff. + buffer[1] = JpegConstants.Markers.SOI; + buffer[0] = JpegConstants.Markers.XFF; + + this.outputStream.Write(buffer, 0, 2); + } + + /// + /// Writes the application header containing the JFIF identifier plus extra data. + /// + /// The image metadata. + /// Temporary buffer. + private void WriteJfifApplicationHeader(ImageMetadata meta, Span buffer) + { + // Write the JFIF headers (highest index first to avoid additional bound checks) + buffer[10] = 0x01; // versionlo + buffer[0] = JpegConstants.Markers.XFF; + buffer[1] = JpegConstants.Markers.APP0; // Application Marker + buffer[2] = 0x00; + buffer[3] = 0x10; + buffer[4] = 0x4a; // J + buffer[5] = 0x46; // F + buffer[6] = 0x49; // I + buffer[7] = 0x46; // F + buffer[8] = 0x00; // = "JFIF",'\0' + buffer[9] = 0x01; // versionhi + + // Resolution. Big Endian + Span hResolution = buffer.Slice(12, 2); + Span vResolution = buffer.Slice(14, 2); + + if (meta.ResolutionUnits == PixelResolutionUnit.PixelsPerMeter) + { + // Scale down to PPI + buffer[11] = (byte)PixelResolutionUnit.PixelsPerInch; // xyunits + BinaryPrimitives.WriteInt16BigEndian(hResolution, (short)Math.Round(UnitConverter.MeterToInch(meta.HorizontalResolution))); + BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(UnitConverter.MeterToInch(meta.VerticalResolution))); + } + else + { + // We can simply pass the value. + buffer[11] = (byte)meta.ResolutionUnits; // xyunits + BinaryPrimitives.WriteInt16BigEndian(hResolution, (short)Math.Round(meta.HorizontalResolution)); + BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(meta.VerticalResolution)); + } + + // No thumbnail + buffer[17] = 0x00; // Thumbnail height + buffer[16] = 0x00; // Thumbnail width + + this.outputStream.Write(buffer, 0, 18); + } + + /// + /// Writes the COM tags. + /// + /// The configuration. + /// The image metadata. + private void WriteComments(Configuration configuration, JpegMetadata metadata) + { + if (metadata.Comments.Count == 0) + { + return; + } + + const int maxCommentLength = 65533; + using IMemoryOwner bufferOwner = configuration.MemoryAllocator.Allocate(maxCommentLength); + Span buffer = bufferOwner.Memory.Span; + foreach (JpegComData comment in metadata.Comments) + { + int totalLength = comment.Value.Length; + if (totalLength == 0) + { + continue; + } + + // Loop through and split the comment into multiple comments if the comment length + // is greater than the maximum allowed length. + while (totalLength > 0) + { + int currentLength = Math.Min(totalLength, maxCommentLength); + + // Write the marker header. + this.WriteMarkerHeader(JpegConstants.Markers.COM, currentLength + 2, buffer); + + ReadOnlySpan commentValue = comment.Value.Span.Slice(comment.Value.Length - totalLength, currentLength); + for (int i = 0; i < commentValue.Length; i++) + { + buffer[i] = (byte)commentValue[i]; + } + + // Write the comment. + this.outputStream.Write(buffer, 0, currentLength); + totalLength -= currentLength; + } + } + } + + /// + /// Writes the Define Huffman Table marker and tables. + /// + /// The table configuration. + /// The scan encoder. + /// Temporary buffer. + /// is . + private void WriteDefineHuffmanTables(JpegHuffmanTableConfig[] tableConfigs, HuffmanScanEncoder scanEncoder, Span buffer) + { + ArgumentNullException.ThrowIfNull(tableConfigs); + + int markerlen = 2; + + for (int i = 0; i < tableConfigs.Length; i++) + { + markerlen += 1 + 16 + tableConfigs[i].Table.Values.Length; + } + + this.WriteMarkerHeader(JpegConstants.Markers.DHT, markerlen, buffer); + for (int i = 0; i < tableConfigs.Length; i++) + { + JpegHuffmanTableConfig tableConfig = tableConfigs[i]; + + int header = (tableConfig.Class << 4) | tableConfig.DestinationIndex; + this.outputStream.WriteByte((byte)header); + this.outputStream.Write(tableConfig.Table.Count); + this.outputStream.Write(tableConfig.Table.Values); + + scanEncoder.BuildHuffmanTable(tableConfig); + } + } + + /// + /// Writes the APP14 marker to indicate the image is in RGB color space. + /// + /// The color transform byte. + /// Temporary buffer. + private void WriteApp14Marker(byte colorTransform, Span buffer) + { + this.WriteMarkerHeader(JpegConstants.Markers.APP14, 2 + Components.Decoder.AdobeMarker.Length, buffer); + + // Identifier: ASCII "Adobe" (highest index first to avoid additional bound checks). + buffer[4] = 0x65; + buffer[0] = 0x41; + buffer[1] = 0x64; + buffer[2] = 0x6F; + buffer[3] = 0x62; + + // Version, currently 100. + BinaryPrimitives.WriteInt16BigEndian(buffer.Slice(5, 2), 100); + + // Flags0 + BinaryPrimitives.WriteInt16BigEndian(buffer.Slice(7, 2), 0); + + // Flags1 + BinaryPrimitives.WriteInt16BigEndian(buffer.Slice(9, 2), 0); + + // Color transform byte + buffer[11] = colorTransform; + + this.outputStream.Write(buffer.Slice(0, 12)); + } + + /// + /// Writes the EXIF profile. + /// + /// The exif profile. + /// Temporary buffer. + private void WriteExifProfile(ExifProfile exifProfile, Span buffer) + { + if (exifProfile is null || exifProfile.Values.Count == 0) + { + return; + } + + const int maxBytesApp1 = 65533; // 64k - 2 padding bytes + const int maxBytesWithExifId = 65527; // Max - 6 bytes for EXIF header. + + byte[] data = exifProfile.ToByteArray(); + + if (data.Length == 0) + { + return; + } + + // We can write up to a maximum of 64 data to the initial marker so calculate boundaries. + int exifMarkerLength = Components.Decoder.ProfileResolver.ExifMarker.Length; + int remaining = exifMarkerLength + data.Length; + int bytesToWrite = remaining > maxBytesApp1 ? maxBytesApp1 : remaining; + int app1Length = bytesToWrite + 2; + + // Write the app marker, EXIF marker, and data + this.WriteApp1Header(app1Length, buffer); + this.outputStream.Write(Components.Decoder.ProfileResolver.ExifMarker); + this.outputStream.Write(data, 0, bytesToWrite - exifMarkerLength); + remaining -= bytesToWrite; + + // If the exif data exceeds 64K, write it in multiple APP1 Markers + for (int idx = maxBytesWithExifId; idx < data.Length; idx += maxBytesWithExifId) + { + bytesToWrite = remaining > maxBytesWithExifId ? maxBytesWithExifId : remaining; + app1Length = bytesToWrite + 2 + exifMarkerLength; + + this.WriteApp1Header(app1Length, buffer); + + // Write Exif00 marker + this.outputStream.Write(Components.Decoder.ProfileResolver.ExifMarker); + + // Write the exif data + this.outputStream.Write(data, idx, bytesToWrite); + + remaining -= bytesToWrite; + } + } + + /// + /// Writes the IPTC metadata. + /// + /// The iptc metadata to write. + /// Temporary buffer. + /// + /// Thrown if the IPTC profile size exceeds the limit of 65533 bytes. + /// + private void WriteIptcProfile(IptcProfile iptcProfile, Span buffer) + { + const int maxBytes = 65533; + if (iptcProfile is null || !iptcProfile.Values.Any()) + { + return; + } + + iptcProfile.UpdateData(); + byte[] data = iptcProfile.Data; + if (data.Length == 0) + { + return; + } + + if (data.Length > maxBytes) + { + throw new ImageFormatException($"Iptc profile size exceeds limit of {maxBytes} bytes"); + } + + int app13Length = 2 + Components.Decoder.ProfileResolver.AdobePhotoshopApp13Marker.Length + + Components.Decoder.ProfileResolver.AdobeImageResourceBlockMarker.Length + + Components.Decoder.ProfileResolver.AdobeIptcMarker.Length + + 2 + 4 + data.Length; + this.WriteAppHeader(app13Length, JpegConstants.Markers.APP13, buffer); + this.outputStream.Write(Components.Decoder.ProfileResolver.AdobePhotoshopApp13Marker); + this.outputStream.Write(Components.Decoder.ProfileResolver.AdobeImageResourceBlockMarker); + this.outputStream.Write(Components.Decoder.ProfileResolver.AdobeIptcMarker); + this.outputStream.WriteByte(0); // a empty pascal string (padded to make size even) + this.outputStream.WriteByte(0); + BinaryPrimitives.WriteInt32BigEndian(buffer, data.Length); + this.outputStream.Write(buffer, 0, 4); + this.outputStream.Write(data, 0, data.Length); + } + + /// + /// Writes the XMP metadata. + /// + /// The XMP metadata to write. + /// Temporary buffer. + /// + /// Thrown if the XMP profile size exceeds the limit of 65533 bytes. + /// + private void WriteXmpProfile(XmpProfile xmpProfile, Span buffer) + { + if (xmpProfile is null) + { + return; + } + + const int xmpOverheadLength = 29; + const int maxBytes = 65533; + const int maxData = maxBytes - 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 + Components.Decoder.ProfileResolver.XmpMarker.Length + length; + this.WriteApp1Header(app1Length, buffer); + this.outputStream.Write(Components.Decoder.ProfileResolver.XmpMarker); + this.outputStream.Write(data, offset, length); + + offset += length; + } + } + + /// + /// Writes the DRI marker + /// + /// Numbers of MCUs between restart markers. + /// Temporary buffer. + private void WriteDri(int restartInterval, Span buffer) + { + if (restartInterval <= 0) + { + return; + } + + this.WriteMarkerHeader(JpegConstants.Markers.DRI, 4, buffer); + + buffer[1] = (byte)(restartInterval & 0xff); + buffer[0] = (byte)(restartInterval >> 8); + this.outputStream.Write(buffer, 0, 2); + } + + /// + /// Writes the App1 header. + /// + /// The length of the data the app1 marker contains. + /// Temporary buffer. + private void WriteApp1Header(int app1Length, Span buffer) + => this.WriteAppHeader(app1Length, JpegConstants.Markers.APP1, buffer); + + /// + /// Writes a AppX header. + /// + /// The length of the data the app marker contains. + /// The app marker to write. + /// Temporary buffer. + private void WriteAppHeader(int length, byte appMarker, Span buffer) + { + buffer[0] = JpegConstants.Markers.XFF; + buffer[1] = appMarker; + buffer[2] = (byte)((length >> 8) & 0xFF); + buffer[3] = (byte)(length & 0xFF); + + this.outputStream.Write(buffer, 0, 4); + } + + /// + /// Writes the ICC profile. + /// + /// The ICC profile to write. + /// Temporary buffer. + /// + /// Thrown if any of the ICC profiles size exceeds the limit. + /// + private void WriteIccProfile(IccProfile iccProfile, Span buffer) + { + if (iccProfile is null) + { + return; + } + + const int iccOverheadLength = 14; + const int maxBytes = 65533; + const int maxData = maxBytes - iccOverheadLength; + + byte[] data = iccProfile.ToByteArray(); + + if (data is null || data.Length == 0) + { + return; + } + + // Calculate the number of markers we'll need, rounding up of course. + int dataLength = data.Length; + int count = dataLength / maxData; + + if (count * maxData != dataLength) + { + count++; + } + + // Per spec, counting starts at 1. + int current = 1; + int offset = 0; + + while (dataLength > 0) + { + int length = dataLength; // Number of bytes to write. + + if (length > maxData) + { + length = maxData; + } + + dataLength -= length; + + buffer[0] = JpegConstants.Markers.XFF; + buffer[1] = JpegConstants.Markers.APP2; // Application Marker + int markerLength = length + 16; + buffer[2] = (byte)((markerLength >> 8) & 0xFF); + buffer[3] = (byte)(markerLength & 0xFF); + + this.outputStream.Write(buffer, 0, 4); + + // We write the highest index first, to have only one bound check. + buffer[13] = (byte)count; // The total number of profiles. + buffer[12] = (byte)current; // The position within the collection. + buffer[11] = 0x00; + buffer[0] = (byte)'I'; + buffer[1] = (byte)'C'; + buffer[2] = (byte)'C'; + buffer[3] = (byte)'_'; + buffer[4] = (byte)'P'; + buffer[5] = (byte)'R'; + buffer[6] = (byte)'O'; + buffer[7] = (byte)'F'; + buffer[8] = (byte)'I'; + buffer[9] = (byte)'L'; + buffer[10] = (byte)'E'; + + this.outputStream.Write(buffer, 0, iccOverheadLength); + this.outputStream.Write(data, offset, length); + + current++; + offset += length; + } + } + + /// + /// Writes the metadata profiles to the image. + /// + /// The image metadata. + /// Temporary buffer. + private void WriteProfiles(ImageMetadata metadata, Span buffer) + { + // For compatibility, place the profiles in the following order: + // - APP1 EXIF + // - APP1 XMP + // - APP2 ICC + // - APP13 IPTC + this.WriteExifProfile(metadata.ExifProfile, buffer); + this.WriteXmpProfile(metadata.XmpProfile, buffer); + this.WriteIccProfile(metadata.IccProfile, buffer); + this.WriteIptcProfile(metadata.IptcProfile, buffer); + } + + /// + /// Writes the Start Of Frame (Baseline) marker. + /// + /// The frame width. + /// The frame height. + /// The frame configuration. + /// Temporary buffer. + private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame, Span buffer) + { + JpegComponentConfig[] components = frame.Components; + + // Length (high byte, low byte), 8 + components * 3. + int markerlen = 8 + (3 * components.Length); + byte marker = this.encoder.Progressive ? JpegConstants.Markers.SOF2 : JpegConstants.Markers.SOF0; + this.WriteMarkerHeader(marker, markerlen, buffer); + buffer[5] = (byte)components.Length; + buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported + buffer[1] = (byte)(height >> 8); + buffer[2] = (byte)(height & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported + buffer[3] = (byte)(width >> 8); + buffer[4] = (byte)(width & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported + + // Components data + for (int i = 0; i < components.Length; i++) + { + int i3 = 3 * i; + Span bufferSpan = buffer.Slice(i3 + 6, 3); + + // Quantization table selector + bufferSpan[2] = (byte)components[i].QuantizatioTableIndex; + + // Sampling factors + // 4 bits + int samplingFactors = (components[i].HorizontalSampleFactor << 4) | components[i].VerticalSampleFactor; + bufferSpan[1] = (byte)samplingFactors; + + // Id + bufferSpan[0] = components[i].Id; + } + + this.outputStream.Write(buffer, 0, (3 * (components.Length - 1)) + 9); + } + + /// + /// Writes the StartOfScan marker. + /// + /// The collecction of component configuration items. + /// Temporary buffer. + private void WriteStartOfScan(Span components, Span buffer) => + this.WriteStartOfScan(components, buffer, 0x00, 0x3f); + + /// + /// Writes the StartOfScan marker. + /// + /// The collecction of component configuration items. + /// Temporary buffer. + /// Start of spectral selection + /// End of spectral selection + private void WriteStartOfScan(Span components, Span buffer, byte spectralStart, byte spectralEnd) + { + // Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes: + // - the marker length "\x00\x0c", + // - the number of components "\x03", + // - component 1 uses DC table 0 and AC table 0 "\x01\x00", + // - component 2 uses DC table 1 and AC table 1 "\x02\x11", + // - component 3 uses DC table 1 and AC table 1 "\x03\x11", + // - the bytes "\x00\x3f\x00". Section B.2.3 of the spec says that for + // sequential DCTs, those bytes (8-bit Ss, 8-bit Se, 4-bit Ah, 4-bit Al) + // should be 0x00, 0x3f, 0x00<<4 | 0x00. + buffer[1] = JpegConstants.Markers.SOS; + buffer[0] = JpegConstants.Markers.XFF; + + // Length (high byte, low byte), must be 6 + 2 * (number of components in scan) + int sosSize = 6 + (2 * components.Length); + buffer[4] = (byte)components.Length; // Number of components in a scan + buffer[3] = (byte)sosSize; + buffer[2] = 0x00; + + // Components data + for (int i = 0; i < components.Length; i++) + { + int i2 = 2 * i; + + // Id + buffer[i2 + 5] = components[i].Id; + + // Table selectors + int tableSelectors = (components[i].DcTableSelector << 4) | components[i].AcTableSelector; + buffer[i2 + 6] = (byte)tableSelectors; + } + + buffer[sosSize - 1] = spectralStart; // Ss - Start of spectral selection. + buffer[sosSize] = spectralEnd; // Se - End of spectral selection. + buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low) + this.outputStream.Write(buffer, 0, sosSize + 2); + } + + /// + /// Writes the EndOfImage marker. + /// + /// Temporary buffer. + private void WriteEndOfImageMarker(Span buffer) + { + buffer[1] = JpegConstants.Markers.EOI; + buffer[0] = JpegConstants.Markers.XFF; + this.outputStream.Write(buffer, 0, 2); + } + + /// + /// Writes scans for given config. + /// + /// The type of pixel format. + /// The current frame. + /// The frame configuration. + /// The spectral converter. + /// The scan encoder. + /// Temporary buffer. + /// The cancellation token. + private void WriteHuffmanScans( + JpegFrame frame, + JpegFrameConfig frameConfig, + SpectralConverter spectralConverter, + HuffmanScanEncoder encoder, + Span buffer, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + if (this.encoder.Progressive) + { + frame.AllocateComponents(fullScan: true); + spectralConverter.ConvertFull(); + + this.WriteProgressiveScans(frame, frameConfig, encoder, buffer, cancellationToken); + } + else if (frame.Components.Length == 1) + { + frame.AllocateComponents(fullScan: false); + + this.WriteStartOfScan(frameConfig.Components, buffer); + encoder.EncodeScanBaselineSingleComponent(frame.Components[0], spectralConverter, cancellationToken); + } + else if (frame.Interleaved) + { + frame.AllocateComponents(fullScan: false); + + this.WriteStartOfScan(frameConfig.Components, buffer); + encoder.EncodeScanBaselineInterleaved(frameConfig.EncodingColor, frame, spectralConverter, cancellationToken); + } + else + { + frame.AllocateComponents(fullScan: true); + spectralConverter.ConvertFull(); + + Span components = frameConfig.Components; + for (int i = 0; i < frame.Components.Length; i++) + { + this.WriteStartOfScan(components.Slice(i, 1), buffer); + encoder.EncodeScanBaseline(frame.Components[i], cancellationToken); + } + } + } + + /// + /// Writes the progressive scans + /// + /// The type of pixel format. + /// The current frame. + /// The frame configuration. + /// The scan encoder. + /// Temporary buffer. + /// The cancellation token. + private void WriteProgressiveScans( + JpegFrame frame, + JpegFrameConfig frameConfig, + HuffmanScanEncoder encoder, + Span buffer, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Span components = frameConfig.Components; + + // Phase 1: DC scan + for (int i = 0; i < frame.Components.Length; i++) + { + this.WriteStartOfScan(components.Slice(i, 1), buffer, 0x00, 0x00); + + encoder.EncodeDcScan(frame.Components[i], cancellationToken); + } + + // Phase 2: AC scans + int acScans = this.encoder.ProgressiveScans - 1; + int valuesPerScan = 64 / acScans; + for (int scan = 0; scan < acScans; scan++) + { + int start = Math.Max(1, scan * valuesPerScan); + int end = scan == acScans - 1 ? 64 : (scan + 1) * valuesPerScan; + + for (int i = 0; i < components.Length; i++) + { + this.WriteStartOfScan(components.Slice(i, 1), buffer, (byte)start, (byte)(end - 1)); + + encoder.EncodeAcScan(frame.Components[i], start, end, cancellationToken); + } + } + } + + /// + /// Writes the header for a marker with the given length. + /// + /// The marker to write. + /// The marker length. + /// Temporary buffer. + private void WriteMarkerHeader(byte marker, int length, Span buffer) + { + // Markers are always prefixed with 0xff. + buffer[3] = (byte)(length & 0xff); + buffer[2] = (byte)(length >> 8); + buffer[1] = marker; + buffer[0] = JpegConstants.Markers.XFF; + + this.outputStream.Write(buffer, 0, 4); + } + + /// + /// Writes the Define Quantization Marker and prepares tables for encoding. + /// + /// + /// We take quality values in a hierarchical order: + /// + /// Check if encoder has set quality. + /// Check if metadata has set quality. + /// Take default quality value from + /// + /// + /// Quantization tables configs. + /// Optional quality value from the options. + /// Jpeg metadata instance. + /// Temporary buffer. + private void WriteDefineQuantizationTables(JpegQuantizationTableConfig[] configs, int? optionsQuality, JpegMetadata metadata, Span tmpBuffer) + { + int dataLen = configs.Length * (1 + Block8x8.Size); + + // Marker + quantization table lengths. + int markerlen = 2 + dataLen; + this.WriteMarkerHeader(JpegConstants.Markers.DQT, markerlen, tmpBuffer); + + Span buffer = dataLen <= 256 ? stackalloc byte[dataLen] : new byte[dataLen]; + int offset = 0; + + Block8x8F workspaceBlock = default; + + for (int i = 0; i < configs.Length; i++) + { + JpegQuantizationTableConfig config = configs[i]; + + int quality = GetQualityForTable(config.DestinationIndex, optionsQuality, metadata); + Block8x8 scaledTable = Quantization.ScaleQuantizationTable(quality, config.Table); + + // write to the output stream + buffer[offset++] = (byte)config.DestinationIndex; + + for (int j = 0; j < Block8x8.Size; j++) + { + buffer[offset++] = (byte)(uint)scaledTable[ZigZag.ZigZagOrder[j]]; + } + + // apply FDCT multipliers and inject to the destination index + workspaceBlock.LoadFrom(ref scaledTable); + FloatingPointDCT.AdjustToFDCT(ref workspaceBlock); + + this.QuantizationTables[config.DestinationIndex] = workspaceBlock; + } + + // write filled buffer to the stream + this.outputStream.Write(buffer); + + static int GetQualityForTable(int destIndex, int? encoderQuality, JpegMetadata metadata) => destIndex switch + { + 0 => encoderQuality ?? metadata.LuminanceQuality ?? Quantization.DefaultQualityFactor, + 1 => encoderQuality ?? metadata.ChrominanceQuality ?? Quantization.DefaultQualityFactor, + _ => encoderQuality ?? metadata.Quality, + }; + } + + private JpegFrameConfig GetFrameConfig(JpegMetadata metadata) + { + JpegColorType color = this.encoder.ColorType ?? metadata.ColorType; + JpegFrameConfig frameConfig = Array.Find( + FrameConfigs, + cfg => cfg.EncodingColor == color); + + if (frameConfig == null) + { + throw new ArgumentException(nameof(color)); + } + + return frameConfig; + } + } +} diff --git a/ImageSharp/Formats/Jpeg/JpegFormat.cs b/ImageSharp/Formats/Jpeg/JpegFormat.cs new file mode 100644 index 0000000..b1022ab --- /dev/null +++ b/ImageSharp/Formats/Jpeg/JpegFormat.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Jpeg { + /// + /// Registers the image encoders, decoders and mime type detectors for the jpeg format. + /// + public sealed class JpegFormat : IImageFormat + { + private JpegFormat() + { + } + + /// + /// Gets the shared instance. + /// + public static JpegFormat Instance { get; } = new(); + + /// + public string Name => "JPEG"; + + /// + public string DefaultMimeType => "image/jpeg"; + + /// + public IEnumerable MimeTypes => JpegConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => JpegConstants.FileExtensions; + + /// + public JpegMetadata CreateDefaultFormatMetadata() => new(); + } +} diff --git a/ImageSharp/Formats/Jpeg/JpegImageFormatDetector.cs b/ImageSharp/Formats/Jpeg/JpegImageFormatDetector.cs new file mode 100644 index 0000000..4723f1e --- /dev/null +++ b/ImageSharp/Formats/Jpeg/JpegImageFormatDetector.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Jpeg { + /// + /// Detects Jpeg file headers + /// + public sealed class JpegImageFormatDetector : IImageFormatDetector + { + /// + public int HeaderSize => 11; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.IsSupportedFileFormat(header) ? JpegFormat.Instance : null; + return format != null; + } + + private bool IsSupportedFileFormat(ReadOnlySpan header) + => header.Length >= this.HeaderSize + && (IsJfif(header) || IsExif(header) || IsJpeg(header)); + + /// + /// Returns a value indicating whether the given bytes identify Jfif data. + /// + /// The bytes representing the file header. + /// The + private static bool IsJfif(ReadOnlySpan header) => + header[6] == 0x4A && // J + header[7] == 0x46 && // F + header[8] == 0x49 && // I + header[9] == 0x46 && // F + header[10] == 0x00; + + /// + /// Returns a value indicating whether the given bytes identify EXIF data. + /// + /// The bytes representing the file header. + /// The + private static bool IsExif(ReadOnlySpan header) => + header[6] == 0x45 && // E + header[7] == 0x78 && // X + header[8] == 0x69 && // I + header[9] == 0x66 && // F + header[10] == 0x00; + + /// + /// Returns a value indicating whether the given bytes identify Jpeg data. + /// This is a last chance resort for jpegs that contain ICC information. + /// + /// The bytes representing the file header. + /// The + private static bool IsJpeg(ReadOnlySpan header) => + header[0] == 0xFF && // 255 + header[1] == 0xD8; // 216 + } +} diff --git a/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/ImageSharp/Formats/Jpeg/JpegMetadata.cs new file mode 100644 index 0000000..ed512c5 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -0,0 +1,216 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jpeg.Components; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Jpeg { + /// + /// Provides Jpeg specific metadata information for the image. + /// + public class JpegMetadata : IFormatMetadata + { + /// + /// Initializes a new instance of the class. + /// + public JpegMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private JpegMetadata(JpegMetadata other) + { + this.ColorType = other.ColorType; + + this.Comments = other.Comments; + this.LuminanceQuality = other.LuminanceQuality; + this.ChrominanceQuality = other.ChrominanceQuality; + } + + /// + /// Gets or sets the jpeg luminance quality. + /// + /// + /// This value might not be accurate if it was calculated during jpeg decoding + /// with non-compliant ITU quantization tables. + /// + internal int? LuminanceQuality { get; set; } + + /// + /// Gets or sets the jpeg chrominance quality. + /// + /// + /// This value might not be accurate if it was calculated during jpeg decoding + /// with non-compliant ITU quantization tables. + /// + internal int? ChrominanceQuality { get; set; } + + /// + /// Gets or sets the encoded quality. + /// + /// + /// Note that jpeg image can have different quality for luminance and chrominance components. + /// This property returns maximum value of luma/chroma qualities if both are present. + /// Setting the quality will update both values. + /// + public int Quality + { + get + { + if (this.LuminanceQuality.HasValue) + { + if (this.ChrominanceQuality.HasValue) + { + return Math.Max(this.LuminanceQuality.Value, this.ChrominanceQuality.Value); + } + + return this.LuminanceQuality.Value; + } + + return this.ChrominanceQuality ?? Quantization.DefaultQualityFactor; + } + + set + { + this.LuminanceQuality = value; + this.ChrominanceQuality = value; + } + } + + /// + /// Gets or sets the color type. + /// + public JpegColorType ColorType { get; set; } = JpegColorType.YCbCrRatio420; + + /// + /// Gets or sets a value indicating whether the component encoding mode should be interleaved. + /// + /// + /// Interleaved encoding mode encodes all color components in a single scan. + /// Non-interleaved encoding mode encodes each color component in a separate scan. + /// + public bool Interleaved { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the scan encoding mode is progressive. + /// + /// + /// Progressive jpeg images encode component data across multiple scans. + /// + public bool Progressive { get; set; } + + /// + /// Gets or sets collection of comments. + /// + public IList Comments { get; set; } = []; + + /// + public static JpegMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + JpegColorType color; + PixelColorType colorType = metadata.PixelTypeInfo.ColorType; + switch (colorType) + { + case PixelColorType.Luminance: + color = JpegColorType.Luminance; + break; + case PixelColorType.CMYK: + color = JpegColorType.Cmyk; + break; + case PixelColorType.YCCK: + color = JpegColorType.Ycck; + break; + default: + if (colorType.HasFlag(PixelColorType.RGB) || colorType.HasFlag(PixelColorType.BGR)) + { + color = JpegColorType.Rgb; + } + else + { + color = metadata.Quality <= Quantization.DefaultQualityFactor + ? JpegColorType.YCbCrRatio420 + : JpegColorType.YCbCrRatio444; + } + + break; + } + + return new JpegMetadata + { + ColorType = color, + ChrominanceQuality = metadata.Quality, + LuminanceQuality = metadata.Quality, + }; + } + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp; + PixelColorType colorType; + PixelComponentInfo info; + switch (this.ColorType) + { + case JpegColorType.Luminance: + bpp = 8; + colorType = PixelColorType.Luminance; + info = PixelComponentInfo.Create(1, bpp, 8); + break; + case JpegColorType.Cmyk: + bpp = 32; + colorType = PixelColorType.CMYK; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + break; + case JpegColorType.Ycck: + bpp = 32; + colorType = PixelColorType.YCCK; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + break; + case JpegColorType.Rgb: + bpp = 24; + colorType = PixelColorType.RGB; + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + break; + default: + bpp = 24; + colorType = PixelColorType.YCbCr; + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + break; + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = PixelAlphaRepresentation.None, + ColorType = colorType, + ComponentInfo = info, + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + EncodingType = EncodingType.Lossy, + PixelTypeInfo = this.GetPixelTypeInfo(), + Quality = this.Quality, + }; + + /// + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public JpegMetadata DeepClone() => new(this); + } +} diff --git a/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs b/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs new file mode 100644 index 0000000..3411756 --- /dev/null +++ b/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Jpeg { + internal static class JpegThrowHelper + { + public static void ThrowNotSupportedException(string errorMessage) => throw new NotSupportedException(errorMessage); + + public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage); + + public static void ThrowBadMarker(string marker, int length) => throw new InvalidImageContentException($"Marker {marker} has bad length {length}."); + + public static void ThrowNotEnoughBytesForMarker(byte marker) => throw new InvalidImageContentException($"Input stream does not have enough bytes to parse declared contents of the {marker:X2} marker."); + + public static void ThrowBadQuantizationTableIndex(int index) => throw new InvalidImageContentException($"Bad Quantization Table index {index}."); + + public static void ThrowBadQuantizationTablePrecision(int precision) => throw new InvalidImageContentException($"Unknown Quantization Table precision {precision}."); + + public static void ThrowBadSampling() => throw new InvalidImageContentException("Bad sampling factor."); + + public static void ThrowBadSampling(int factor) => throw new InvalidImageContentException($"Bad sampling factor: {factor}"); + + public static void ThrowBadProgressiveScan(int ss, int se, int ah, int al) => throw new InvalidImageContentException($"Invalid progressive parameters Ss={ss} Se={se} Ah={ah} Al={al}."); + + public static void ThrowInvalidImageDimensions(int width, int height) => throw new InvalidImageContentException($"Invalid image dimensions: {width}x{height}."); + + public static void ThrowDimensionsTooLarge(int width, int height) => throw new ImageFormatException($"Image is too large to encode at {width}x{height} for JPEG format."); + + public static void ThrowNotSupportedComponentCount(int componentCount) => throw new NotSupportedException($"Images with {componentCount} components are not supported."); + + public static void ThrowNotSupportedColorSpace() => throw new NotSupportedException("Image color space could not be deduced."); + } +} diff --git a/ImageSharp/Formats/Jpeg/README.md b/ImageSharp/Formats/Jpeg/README.md new file mode 100644 index 0000000..2f766ca --- /dev/null +++ b/ImageSharp/Formats/Jpeg/README.md @@ -0,0 +1,8 @@ +Encoder adapted and extended from: +https://golang.org/src/image/jpeg/ + +Decoder orchestration code is based on: +https://github.com/mozilla/pdf.js + +Huffmann decoder is based on: +https://github.com/rds1983/StbSharp \ No newline at end of file diff --git a/ImageSharp/Formats/Jpeg/itu-t81.pdf b/ImageSharp/Formats/Jpeg/itu-t81.pdf new file mode 100644 index 0000000..1d57c76 Binary files /dev/null and b/ImageSharp/Formats/Jpeg/itu-t81.pdf differ diff --git a/ImageSharp/Formats/Pbm/BinaryDecoder.cs b/ImageSharp/Formats/Pbm/BinaryDecoder.cs new file mode 100644 index 0000000..1f944f3 --- /dev/null +++ b/ImageSharp/Formats/Pbm/BinaryDecoder.cs @@ -0,0 +1,202 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Pbm { + /// + /// Pixel decoding methods for the PBM binary encoding. + /// + internal class BinaryDecoder + { + private static L8 white = new(255); + private static L8 black = new(0); + + /// + /// Decode the specified pixels. + /// + /// The type of pixel to encode to. + /// The configuration. + /// The pixel array to encode into. + /// The stream to read the data from. + /// The ColorType to decode. + /// Data type of the pixles components. + /// + /// Thrown if an invalid combination of setting is requested. + /// + public static void Process(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, PbmColorType colorType, PbmComponentType componentType) + where TPixel : unmanaged, IPixel + { + if (colorType == PbmColorType.Grayscale) + { + if (componentType == PbmComponentType.Byte) + { + ProcessGrayscale(configuration, pixels, stream); + } + else + { + ProcessWideGrayscale(configuration, pixels, stream); + } + } + else if (colorType == PbmColorType.Rgb) + { + if (componentType == PbmComponentType.Byte) + { + ProcessRgb(configuration, pixels, stream); + } + else + { + ProcessWideRgb(configuration, pixels, stream); + } + } + else + { + ProcessBlackAndWhite(configuration, pixels, stream); + } + } + + private static void ProcessGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + where TPixel : unmanaged, IPixel + { + const int bytesPerPixel = 1; + int width = pixels.Width; + int height = pixels.Height; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width * bytesPerPixel); + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) + { + if (stream.Read(rowSpan) < rowSpan.Length) + { + return; + } + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.FromL8Bytes( + configuration, + rowSpan, + pixelSpan, + width); + } + } + + private static void ProcessWideGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + where TPixel : unmanaged, IPixel + { + const int bytesPerPixel = 2; + int width = pixels.Width; + int height = pixels.Height; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width * bytesPerPixel); + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) + { + if (stream.Read(rowSpan) < rowSpan.Length) + { + return; + } + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.FromL16Bytes( + configuration, + rowSpan, + pixelSpan, + width); + } + } + + private static void ProcessRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + where TPixel : unmanaged, IPixel + { + const int bytesPerPixel = 3; + int width = pixels.Width; + int height = pixels.Height; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width * bytesPerPixel); + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) + { + if (stream.Read(rowSpan) < rowSpan.Length) + { + return; + } + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.FromRgb24Bytes( + configuration, + rowSpan, + pixelSpan, + width); + } + } + + private static void ProcessWideRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + where TPixel : unmanaged, IPixel + { + const int bytesPerPixel = 6; + int width = pixels.Width; + int height = pixels.Height; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width * bytesPerPixel); + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) + { + if (stream.Read(rowSpan) < rowSpan.Length) + { + return; + } + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.FromRgb48Bytes( + configuration, + rowSpan, + pixelSpan, + width); + } + } + + private static void ProcessBlackAndWhite(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + where TPixel : unmanaged, IPixel + { + int width = pixels.Width; + int height = pixels.Height; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width); + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width;) + { + int raw = stream.ReadByte(); + if (raw < 0) + { + return; + } + + int stopBit = Math.Min(8, width - x); + for (int bit = 0; bit < stopBit; bit++) + { + bool bitValue = (raw & (0x80 >> bit)) != 0; + rowSpan[x] = bitValue ? black : white; + x++; + } + } + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.FromL8( + configuration, + rowSpan, + pixelSpan); + } + } + } +} diff --git a/ImageSharp/Formats/Pbm/BinaryEncoder.cs b/ImageSharp/Formats/Pbm/BinaryEncoder.cs new file mode 100644 index 0000000..5fc99bd --- /dev/null +++ b/ImageSharp/Formats/Pbm/BinaryEncoder.cs @@ -0,0 +1,243 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.IO; +using System.Threading; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Pbm { + /// + /// Pixel encoding methods for the PBM binary encoding. + /// + internal class BinaryEncoder + { + /// + /// Decode pixels into the PBM binary encoding. + /// + /// The type of input pixel. + /// The configuration. + /// The byte stream to write to. + /// The input image. + /// The ColorType to use. + /// Data type of the pixels components. + /// The token to monitor for cancellation requests. + /// + /// Thrown if an invalid combination of setting is requested. + /// + public static void WritePixels( + Configuration configuration, + Stream stream, + ImageFrame image, + PbmColorType colorType, + PbmComponentType componentType, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + if (colorType == PbmColorType.Grayscale) + { + if (componentType == PbmComponentType.Byte) + { + WriteGrayscale(configuration, stream, image, cancellationToken); + } + else if (componentType == PbmComponentType.Short) + { + WriteWideGrayscale(configuration, stream, image, cancellationToken); + } + else + { + throw new ImageFormatException("Component type not supported for Grayscale PBM."); + } + } + else if (colorType == PbmColorType.Rgb) + { + if (componentType == PbmComponentType.Byte) + { + WriteRgb(configuration, stream, image, cancellationToken); + } + else if (componentType == PbmComponentType.Short) + { + WriteWideRgb(configuration, stream, image, cancellationToken); + } + else + { + throw new ImageFormatException("Component type not supported for Color PBM."); + } + } + else if (componentType == PbmComponentType.Bit) + { + WriteBlackAndWhite(configuration, stream, image, cancellationToken); + } + } + + private static void WriteGrayscale( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + int width = image.Width; + int height = image.Height; + Buffer2D pixelBuffer = image.PixelBuffer; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width); + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); + + PixelOperations.Instance.ToL8Bytes( + configuration, + pixelSpan, + rowSpan, + width); + + stream.Write(rowSpan); + } + } + + private static void WriteWideGrayscale( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + const int bytesPerPixel = 2; + int width = image.Width; + int height = image.Height; + Buffer2D pixelBuffer = image.PixelBuffer; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width * bytesPerPixel); + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); + + PixelOperations.Instance.ToL16Bytes( + configuration, + pixelSpan, + rowSpan, + width); + + stream.Write(rowSpan); + } + } + + private static void WriteRgb( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + const int bytesPerPixel = 3; + int width = image.Width; + int height = image.Height; + Buffer2D pixelBuffer = image.PixelBuffer; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width * bytesPerPixel); + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); + + PixelOperations.Instance.ToRgb24Bytes( + configuration, + pixelSpan, + rowSpan, + width); + + stream.Write(rowSpan); + } + } + + private static void WriteWideRgb( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + const int bytesPerPixel = 6; + int width = image.Width; + int height = image.Height; + Buffer2D pixelBuffer = image.PixelBuffer; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width * bytesPerPixel); + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); + + PixelOperations.Instance.ToRgb48Bytes( + configuration, + pixelSpan, + rowSpan, + width); + + stream.Write(rowSpan); + } + } + + private static void WriteBlackAndWhite( + Configuration + configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + int width = image.Width; + int height = image.Height; + Buffer2D pixelBuffer = image.PixelBuffer; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width); + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); + + PixelOperations.Instance.ToL8( + configuration, + pixelSpan, + rowSpan); + + for (int x = 0; x < width;) + { + int value = 0; + int stopBit = Math.Min(8, width - x); + for (int i = 0; i < stopBit; i++) + { + if (rowSpan[x].PackedValue < 128) + { + value |= 0x80 >> i; + } + + x++; + } + + stream.WriteByte((byte)value); + } + } + } + } +} diff --git a/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs b/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs new file mode 100644 index 0000000..8c09c94 --- /dev/null +++ b/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs @@ -0,0 +1,87 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Pbm { + /// + /// Extensions methods for . + /// + internal static class BufferedReadStreamExtensions + { + /// + /// Skip over any whitespace or any comments and signal if EOF has been reached. + /// + /// The buffered read stream. + /// if EOF has been reached while reading the stream; see langword="true"/> otherwise. + public static bool SkipWhitespaceAndComments(this BufferedReadStream stream) + { + bool isWhitespace; + do + { + int val = stream.ReadByte(); + if (val < 0) + { + return false; + } + + // Comments start with '#' and end at the next new-line. + if (val == 0x23) + { + int innerValue; + do + { + innerValue = stream.ReadByte(); + if (innerValue < 0) + { + return false; + } + } + while (innerValue is not 0x0a); + + // Continue searching for whitespace. + val = innerValue; + } + + isWhitespace = val is 0x09 or 0x0a or 0x0d or 0x20; + } + while (isWhitespace); + stream.Seek(-1, SeekOrigin.Current); + return true; + } + + /// + /// Read a decimal text value and signal if EOF has been reached. + /// + /// The buffered read stream. + /// The read value. + /// if EOF has been reached while reading the stream; otherwise. + /// + /// A 'false' return value doesn't mean that the parsing has been failed, since it's possible to reach EOF while reading the last decimal in the file. + /// It's up to the call site to handle such a situation. + /// + public static bool ReadDecimal(this BufferedReadStream stream, out int value) + { + value = 0; + while (true) + { + int current = stream.ReadByte(); + if (current < 0) + { + return false; + } + + current -= 0x30; + if ((uint)current > 9) + { + break; + } + + value = (value * 10) + current; + } + + return true; + } + } +} diff --git a/ImageSharp/Formats/Pbm/PbmColorType.cs b/ImageSharp/Formats/Pbm/PbmColorType.cs new file mode 100644 index 0000000..7e677fd --- /dev/null +++ b/ImageSharp/Formats/Pbm/PbmColorType.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Pbm { + /// + /// Provides enumeration of available PBM color types. + /// + public enum PbmColorType : byte + { + /// + /// PBM + /// + BlackAndWhite = 0, + + /// + /// PGM - Greyscale. Single component. + /// + Grayscale = 1, + + /// + /// PPM - RGB Color. 3 components. + /// + Rgb = 2, + } +} diff --git a/ImageSharp/Formats/Pbm/PbmComponentType.cs b/ImageSharp/Formats/Pbm/PbmComponentType.cs new file mode 100644 index 0000000..b4ed04a --- /dev/null +++ b/ImageSharp/Formats/Pbm/PbmComponentType.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Pbm { + /// + /// The data type of the components of the pixels. + /// + public enum PbmComponentType : byte + { + /// + /// Single bit per pixel, exclusively for . + /// + Bit = 0, + + /// + /// 8 bits unsigned integer per component. + /// + Byte = 1, + + /// + /// 16 bits unsigned integer per component. + /// + Short = 2 + } +} diff --git a/ImageSharp/Formats/Pbm/PbmConfigurationModule.cs b/ImageSharp/Formats/Pbm/PbmConfigurationModule.cs new file mode 100644 index 0000000..fac480b --- /dev/null +++ b/ImageSharp/Formats/Pbm/PbmConfigurationModule.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Pbm { + /// + /// Registers the image encoders, decoders and mime type detectors for the Pbm format. + /// + public sealed class PbmConfigurationModule : IImageFormatConfigurationModule + { + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetEncoder(PbmFormat.Instance, new PbmEncoder()); + configuration.ImageFormatsManager.SetDecoder(PbmFormat.Instance, PbmDecoder.Instance); + configuration.ImageFormatsManager.AddImageFormatDetector(new PbmImageFormatDetector()); + } + } +} diff --git a/ImageSharp/Formats/Pbm/PbmConstants.cs b/ImageSharp/Formats/Pbm/PbmConstants.cs new file mode 100644 index 0000000..333433a --- /dev/null +++ b/ImageSharp/Formats/Pbm/PbmConstants.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Pbm { + /// + /// Contains PBM constant values defined in the specification. + /// + internal static class PbmConstants + { + /// + /// The maximum allowable pixel value of a ppm image. + /// + public const ushort MaxLength = 65535; + + /// + /// The list of mimetypes that equate to a ppm. + /// + public static readonly IEnumerable MimeTypes = ["image/x-portable-pixmap", "image/x-portable-anymap"]; + + /// + /// The list of file extensions that equate to a ppm. + /// + public static readonly IEnumerable FileExtensions = ["ppm", "pbm", "pgm"]; + } +} diff --git a/ImageSharp/Formats/Pbm/PbmDecoder.cs b/ImageSharp/Formats/Pbm/PbmDecoder.cs new file mode 100644 index 0000000..dc28a67 --- /dev/null +++ b/ImageSharp/Formats/Pbm/PbmDecoder.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Pbm { + /// + /// Image decoder for reading PGM, PBM or PPM bitmaps from a stream. These images are from + /// the family of PNM images. + /// + /// + /// PBM + /// Black and white images. + /// + /// + /// PGM + /// Grayscale images. + /// + /// + /// PPM + /// Color images, with RGB pixels. + /// + /// + /// The specification of these images is found at . + /// + public sealed class PbmDecoder : ImageDecoder + { + private PbmDecoder() + { + } + + /// + /// Gets the shared instance. + /// + public static PbmDecoder Instance { get; } = new(); + + /// + protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + return new PbmDecoderCore(options).Identify(options.Configuration, stream, cancellationToken); + } + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + PbmDecoderCore decoder = new(options); + Image image = decoder.Decode(options.Configuration, stream, cancellationToken); + + ScaleToTargetSize(options, image); + + return image; + } + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(options, stream, cancellationToken); + } +} diff --git a/ImageSharp/Formats/Pbm/PbmDecoderCore.cs b/ImageSharp/Formats/Pbm/PbmDecoderCore.cs new file mode 100644 index 0000000..0fe4001 --- /dev/null +++ b/ImageSharp/Formats/Pbm/PbmDecoderCore.cs @@ -0,0 +1,195 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Formats.Pbm { + /// + /// Performs the PBM decoding operation. + /// + internal sealed class PbmDecoderCore : ImageDecoderCore { + private int maxPixelValue; + + /// + /// The general configuration. + /// + private readonly Configuration configuration; + + /// + /// The colortype to use + /// + private PbmColorType colorType; + + /// + /// The size of the pixel array + /// + private Size pixelSize; + + /// + /// The component data type + /// + private PbmComponentType componentType; + + /// + /// The Encoding of pixels + /// + private PbmEncoding encoding; + + /// + /// The decoded by this decoder instance. + /// + private ImageMetadata? metadata; + + /// + /// Initializes a new instance of the class. + /// + /// The decoder options. + public PbmDecoderCore(DecoderOptions options) + : base(options) { + this.configuration = options.Configuration; + } + + /// + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { + this.ProcessHeader(stream); + + Image image = new(this.configuration, this.pixelSize.Width, this.pixelSize.Height, this.metadata); + + Buffer2D pixels = image.GetRootFramePixelBuffer(); + + this.ProcessPixels(stream, pixels); + if(this.NeedsUpscaling()) { + this.ProcessUpscaling(image); + } + + return image; + } + + /// + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { + this.ProcessHeader(stream); + return new ImageInfo( + new Size(this.pixelSize.Width, this.pixelSize.Height), + this.metadata); + } + + /// + /// Processes the ppm header. + /// + /// The input stream. + /// An EOF marker has been read before the image has been decoded. + [MemberNotNull(nameof(metadata))] + private void ProcessHeader(BufferedReadStream stream) { + Span buffer = stackalloc byte[2]; + + int bytesRead = stream.Read(buffer); + if(bytesRead != 2 || buffer[0] != 'P') { + throw new InvalidImageContentException("Empty or not an PPM image."); + } + + switch((char)buffer[1]) { + case '1': + // Plain PBM format: 1 component per pixel, boolean value ('0' or '1'). + this.colorType = PbmColorType.BlackAndWhite; + this.encoding = PbmEncoding.Plain; + break; + case '2': + // Plain PGM format: 1 component per pixel, in decimal text. + this.colorType = PbmColorType.Grayscale; + this.encoding = PbmEncoding.Plain; + break; + case '3': + // Plain PPM format: 3 components per pixel, in decimal text. + this.colorType = PbmColorType.Rgb; + this.encoding = PbmEncoding.Plain; + break; + case '4': + // Binary PBM format: 1 component per pixel, 8 pixels per byte. + this.colorType = PbmColorType.BlackAndWhite; + this.encoding = PbmEncoding.Binary; + break; + case '5': + // Binary PGM format: 1 components per pixel, in binary integers. + this.colorType = PbmColorType.Grayscale; + this.encoding = PbmEncoding.Binary; + break; + case '6': + // Binary PPM format: 3 components per pixel, in binary integers. + this.colorType = PbmColorType.Rgb; + this.encoding = PbmEncoding.Binary; + break; + case '7': + // PAM image: sequence of images. + // Not implemented yet + default: + throw new InvalidImageContentException("Unknown of not implemented image type encountered."); + } + + if(!stream.SkipWhitespaceAndComments() || + !stream.ReadDecimal(out int width) || + !stream.SkipWhitespaceAndComments() || + !stream.ReadDecimal(out int height) || + !stream.SkipWhitespaceAndComments()) { + ThrowPrematureEof(); + } + + if(this.colorType != PbmColorType.BlackAndWhite) { + if(!stream.ReadDecimal(out this.maxPixelValue)) { + ThrowPrematureEof(); + } + + if(this.maxPixelValue <= 0 || this.maxPixelValue >= 65536) { + throw new InvalidImageContentException("Invalid max pixel value."); + } + + if(this.maxPixelValue > 255) { + this.componentType = PbmComponentType.Short; + } else { + this.componentType = PbmComponentType.Byte; + } + + stream.SkipWhitespaceAndComments(); + } else { + this.componentType = PbmComponentType.Bit; + } + + this.pixelSize = new Size(width, height); + this.Dimensions = this.pixelSize; + this.metadata = new ImageMetadata(); + PbmMetadata meta = this.metadata.GetPbmMetadata(); + meta.Encoding = this.encoding; + meta.ColorType = this.colorType; + meta.ComponentType = this.componentType; + + [DoesNotReturn] + static void ThrowPrematureEof() => throw new InvalidImageContentException("Reached EOF while reading the header."); + } + + private void ProcessPixels(BufferedReadStream stream, Buffer2D pixels) + where TPixel : unmanaged, IPixel { + if(this.encoding == PbmEncoding.Binary) { + BinaryDecoder.Process(this.configuration, pixels, stream, this.colorType, this.componentType); + } else { + PlainDecoder.Process(this.configuration, pixels, stream, this.colorType, this.componentType); + } + } + + private void ProcessUpscaling(Image image) + where TPixel : unmanaged, IPixel { + int maxAllocationValue = this.componentType == PbmComponentType.Short ? 65535 : 255; + float factor = maxAllocationValue / this.maxPixelValue; + image.Mutate(x => x.Brightness(factor)); + } + + private bool NeedsUpscaling() => this.colorType != PbmColorType.BlackAndWhite && this.maxPixelValue is not 255 and not 65535; + //protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) => throw new System.NotImplementedException(); + //protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) => throw new System.NotImplementedException(); + } +} diff --git a/ImageSharp/Formats/Pbm/PbmEncoder.cs b/ImageSharp/Formats/Pbm/PbmEncoder.cs new file mode 100644 index 0000000..a00ab41 --- /dev/null +++ b/ImageSharp/Formats/Pbm/PbmEncoder.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Pbm { + /// + /// Image encoder for writing an image to a stream as PGM, PBM or PPM bitmap. These images are from + /// the family of PNM images. + /// + /// The PNM formats are a fairly simple image format. They share a plain text header, consisting of: + /// signature, width, height and max_pixel_value only. The pixels follow thereafter and can be in + /// plain text decimals separated by spaces, or binary encoded. + /// + /// + /// PBM + /// Black and white images, with 1 representing black and 0 representing white. + /// + /// + /// PGM + /// Grayscale images, scaling from 0 to max_pixel_value, 0 representing black and max_pixel_value representing white. + /// + /// + /// PPM + /// Color images, with RGB pixels (in that order), with 0 representing black and 2 representing full color. + /// + /// + /// + /// The specification of these images is found at . + /// + public sealed class PbmEncoder : ImageEncoder + { + /// + /// Gets the encoding of the pixels. + /// + public PbmEncoding? Encoding { get; init; } + + /// + /// Gets the Color type of the resulting image. + /// + public PbmColorType? ColorType { get; init; } + + /// + /// Gets the data type of the pixel components. + /// + public PbmComponentType? ComponentType { get; init; } + + /// + protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) + { + PbmEncoderCore encoder = new(image.Configuration, this); + encoder.Encode(image, stream, cancellationToken); + } + } +} diff --git a/ImageSharp/Formats/Pbm/PbmEncoderCore.cs b/ImageSharp/Formats/Pbm/PbmEncoderCore.cs new file mode 100644 index 0000000..ddf787d --- /dev/null +++ b/ImageSharp/Formats/Pbm/PbmEncoderCore.cs @@ -0,0 +1,197 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Text; +using System.IO; +using System.Threading; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Pbm { + /// + /// Image encoder for writing an image to a stream as a PGM, PBM, PPM or PAM bitmap. + /// + internal sealed class PbmEncoderCore + { + private const byte NewLine = (byte)'\n'; + private const byte Space = (byte)' '; + private const byte P = (byte)'P'; + + /// + /// The global configuration. + /// + private Configuration configuration; + + /// + /// The encoder with options. + /// + private readonly PbmEncoder encoder; + + /// + /// The encoding for the pixels. + /// + private PbmEncoding encoding; + + /// + /// Gets the Color type of the resulting image. + /// + private PbmColorType colorType; + + /// + /// Gets the maximum pixel value, per component. + /// + private PbmComponentType componentType; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The encoder with options. + public PbmEncoderCore(Configuration configuration, PbmEncoder encoder) + { + this.configuration = configuration; + this.encoder = encoder; + } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + this.SanitizeAndSetEncoderOptions(image); + + byte signature = this.DeduceSignature(); + this.WriteHeader(stream, signature, image.Size); + this.WritePixels(stream, image.Frames.RootFrame, cancellationToken); + + stream.Flush(); + } + + private void SanitizeAndSetEncoderOptions(Image image) + where TPixel : unmanaged, IPixel + { + this.configuration = image.Configuration; + PbmMetadata metadata = image.Metadata.GetPbmMetadata(); + this.encoding = this.encoder.Encoding ?? metadata.Encoding; + this.colorType = this.encoder.ColorType ?? metadata.ColorType; + if (this.colorType != PbmColorType.BlackAndWhite) + { + this.componentType = this.encoder.ComponentType ?? metadata.ComponentType; + } + else + { + this.componentType = PbmComponentType.Bit; + } + } + + private byte DeduceSignature() + { + byte signature; + if (this.colorType == PbmColorType.BlackAndWhite) + { + if (this.encoding == PbmEncoding.Plain) + { + signature = (byte)'1'; + } + else + { + signature = (byte)'4'; + } + } + else if (this.colorType == PbmColorType.Grayscale) + { + if (this.encoding == PbmEncoding.Plain) + { + signature = (byte)'2'; + } + else + { + signature = (byte)'5'; + } + } + else + { + // RGB ColorType + if (this.encoding == PbmEncoding.Plain) + { + signature = (byte)'3'; + } + else + { + signature = (byte)'6'; + } + } + + return signature; + } + + private void WriteHeader(Stream stream, byte signature, Size pixelSize) + { + Span buffer = stackalloc byte[128]; + + int written = 3; + buffer[0] = P; + buffer[1] = signature; + buffer[2] = NewLine; + + Utf8Formatter.TryFormat(pixelSize.Width, buffer[written..], out int bytesWritten); + written += bytesWritten; + buffer[written++] = Space; + Utf8Formatter.TryFormat(pixelSize.Height, buffer[written..], out bytesWritten); + written += bytesWritten; + buffer[written++] = NewLine; + + if (this.colorType != PbmColorType.BlackAndWhite) + { + int maxPixelValue = this.componentType == PbmComponentType.Short ? 65535 : 255; + Utf8Formatter.TryFormat(maxPixelValue, buffer[written..], out bytesWritten); + written += bytesWritten; + buffer[written++] = NewLine; + } + + stream.Write(buffer, 0, written); + } + + /// + /// Writes the pixel data to the binary stream. + /// + /// The pixel format. + /// The to write to. + /// + /// The containing pixel data. + /// + /// The token to monitor for cancellation requests. + private void WritePixels(Stream stream, ImageFrame image, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + if (this.encoding == PbmEncoding.Plain) + { + PlainEncoder.WritePixels( + this.configuration, + stream, + image, + this.colorType, + this.componentType, + cancellationToken); + } + else + { + BinaryEncoder.WritePixels( + this.configuration, + stream, + image, + this.colorType, + this.componentType, + cancellationToken); + } + } + } +} diff --git a/ImageSharp/Formats/Pbm/PbmEncoding.cs b/ImageSharp/Formats/Pbm/PbmEncoding.cs new file mode 100644 index 0000000..8d26569 --- /dev/null +++ b/ImageSharp/Formats/Pbm/PbmEncoding.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Pbm { + /// + /// Provides enumeration of available PBM encodings. + /// + public enum PbmEncoding : byte + { + /// + /// Plain text decimal encoding. + /// + Plain = 0, + + /// + /// Binary integer encoding. + /// + Binary = 1, + } +} diff --git a/ImageSharp/Formats/Pbm/PbmFormat.cs b/ImageSharp/Formats/Pbm/PbmFormat.cs new file mode 100644 index 0000000..fddf942 --- /dev/null +++ b/ImageSharp/Formats/Pbm/PbmFormat.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Pbm { + /// + /// Registers the image encoders, decoders and mime type detectors for the PBM format. + /// + public sealed class PbmFormat : IImageFormat + { + private PbmFormat() + { + } + + /// + /// Gets the shared instance. + /// + public static PbmFormat Instance { get; } = new(); + + /// + public string Name => "PBM"; + + /// + public string DefaultMimeType => "image/x-portable-pixmap"; + + /// + public IEnumerable MimeTypes => PbmConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => PbmConstants.FileExtensions; + + /// + public PbmMetadata CreateDefaultFormatMetadata() => new(); + } +} diff --git a/ImageSharp/Formats/Pbm/PbmImageFormatDetector.cs b/ImageSharp/Formats/Pbm/PbmImageFormatDetector.cs new file mode 100644 index 0000000..cc3fddc --- /dev/null +++ b/ImageSharp/Formats/Pbm/PbmImageFormatDetector.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Pbm { + /// + /// Detects Pbm file headers. + /// + public sealed class PbmImageFormatDetector : IImageFormatDetector + { + private const byte P = (byte)'P'; + private const byte Zero = (byte)'0'; + private const byte Seven = (byte)'7'; + + /// + public int HeaderSize => 2; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = IsSupportedFileFormat(header) ? PbmFormat.Instance : null; + return format != null; + } + + private static bool IsSupportedFileFormat(ReadOnlySpan header) + { + if ((uint)header.Length > 1) + { + // Signature should be between P1 and P6. + return header[0] == P && (uint)(header[1] - Zero - 1) < (Seven - Zero - 1); + } + + return false; + } + } +} diff --git a/ImageSharp/Formats/Pbm/PbmMetadata.cs b/ImageSharp/Formats/Pbm/PbmMetadata.cs new file mode 100644 index 0000000..bef9d67 --- /dev/null +++ b/ImageSharp/Formats/Pbm/PbmMetadata.cs @@ -0,0 +1,144 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Pbm { + /// + /// Provides PBM specific metadata information for the image. + /// + public class PbmMetadata : IFormatMetadata + { + /// + /// Initializes a new instance of the class. + /// + public PbmMetadata() => + this.ComponentType = this.ColorType == PbmColorType.BlackAndWhite ? PbmComponentType.Bit : PbmComponentType.Byte; + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private PbmMetadata(PbmMetadata other) + { + this.Encoding = other.Encoding; + this.ColorType = other.ColorType; + this.ComponentType = other.ComponentType; + } + + /// + /// Gets or sets the encoding of the pixels. + /// + public PbmEncoding Encoding { get; set; } = PbmEncoding.Plain; + + /// + /// Gets or sets the color type. + /// + public PbmColorType ColorType { get; set; } = PbmColorType.Grayscale; + + /// + /// Gets or sets the data type of the pixel components. + /// + public PbmComponentType ComponentType { get; set; } + + /// + public static PbmMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + PbmColorType color; + PixelColorType colorType = metadata.PixelTypeInfo.ColorType; + + switch (colorType) + { + case PixelColorType.Binary: + color = PbmColorType.BlackAndWhite; + break; + case PixelColorType.Luminance: + color = PbmColorType.Grayscale; + break; + default: + if (colorType.HasFlag(PixelColorType.RGB) || colorType.HasFlag(PixelColorType.BGR)) + { + color = PbmColorType.Rgb; + } + else + { + color = PbmColorType.Grayscale; + } + + break; + } + + int bpp = metadata.PixelTypeInfo.BitsPerPixel; + PbmComponentType componentType = bpp switch + { + 1 => PbmComponentType.Bit, + <= 8 => PbmComponentType.Byte, + _ => PbmComponentType.Short + }; + + return new PbmMetadata + { + ColorType = color, + ComponentType = componentType + }; + } + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp; + PixelColorType colorType; + PixelComponentInfo info; + switch (this.ColorType) + { + case PbmColorType.BlackAndWhite: + bpp = 1; + colorType = PixelColorType.Binary; + info = PixelComponentInfo.Create(1, bpp, 1); + break; + case PbmColorType.Rgb: + bpp = this.ComponentType == PbmComponentType.Short ? 48 : 24; + colorType = PixelColorType.RGB; + info = this.ComponentType == PbmComponentType.Short + ? PixelComponentInfo.Create(3, bpp, 16, 16, 16) + : PixelComponentInfo.Create(3, bpp, 8, 8, 8); + break; + case PbmColorType.Grayscale: + default: + bpp = this.ComponentType == PbmComponentType.Short ? 16 : 8; + colorType = PixelColorType.Luminance; + info = this.ComponentType == PbmComponentType.Short + ? PixelComponentInfo.Create(1, bpp, bpp) + : PixelComponentInfo.Create(1, bpp, bpp); + break; + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = PixelAlphaRepresentation.None, + ColorType = colorType, + ComponentInfo = info, + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo(), + }; + + /// + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public PbmMetadata DeepClone() => new(this); + } +} diff --git a/ImageSharp/Formats/Pbm/PlainDecoder.cs b/ImageSharp/Formats/Pbm/PlainDecoder.cs new file mode 100644 index 0000000..5f32626 --- /dev/null +++ b/ImageSharp/Formats/Pbm/PlainDecoder.cs @@ -0,0 +1,262 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Pbm { + /// + /// Pixel decoding methods for the PBM plain encoding. + /// + internal class PlainDecoder + { + private static readonly L8 White = new(255); + private static readonly L8 Black = new(0); + + /// + /// Decode the specified pixels. + /// + /// The type of pixel to encode to. + /// The configuration. + /// The pixel array to encode into. + /// The stream to read the data from. + /// The ColorType to decode. + /// Data type of the pixles components. + public static void Process(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, PbmColorType colorType, PbmComponentType componentType) + where TPixel : unmanaged, IPixel + { + if (colorType == PbmColorType.Grayscale) + { + if (componentType == PbmComponentType.Byte) + { + ProcessGrayscale(configuration, pixels, stream); + } + else + { + ProcessWideGrayscale(configuration, pixels, stream); + } + } + else if (colorType == PbmColorType.Rgb) + { + if (componentType == PbmComponentType.Byte) + { + ProcessRgb(configuration, pixels, stream); + } + else + { + ProcessWideRgb(configuration, pixels, stream); + } + } + else + { + ProcessBlackAndWhite(configuration, pixels, stream); + } + } + + private static void ProcessGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + where TPixel : unmanaged, IPixel + { + int width = pixels.Width; + int height = pixels.Height; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width); + Span rowSpan = row.GetSpan(); + + bool eofReached = false; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + stream.ReadDecimal(out int value); + rowSpan[x] = new L8((byte)value); + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } + } + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.FromL8( + configuration, + rowSpan, + pixelSpan); + + if (eofReached) + { + return; + } + } + } + + private static void ProcessWideGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + where TPixel : unmanaged, IPixel + { + int width = pixels.Width; + int height = pixels.Height; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width); + Span rowSpan = row.GetSpan(); + + bool eofReached = false; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + stream.ReadDecimal(out int value); + rowSpan[x] = new L16((ushort)value); + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } + } + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.FromL16( + configuration, + rowSpan, + pixelSpan); + + if (eofReached) + { + return; + } + } + } + + private static void ProcessRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + where TPixel : unmanaged, IPixel + { + int width = pixels.Width; + int height = pixels.Height; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width); + Span rowSpan = row.GetSpan(); + + bool eofReached = false; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + if (!stream.ReadDecimal(out int red) || + !stream.SkipWhitespaceAndComments() || + !stream.ReadDecimal(out int green) || + !stream.SkipWhitespaceAndComments()) + { + // Reached EOF before reading a full RGB value + eofReached = true; + break; + } + + stream.ReadDecimal(out int blue); + + rowSpan[x] = new Rgb24((byte)red, (byte)green, (byte)blue); + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } + } + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.FromRgb24( + configuration, + rowSpan, + pixelSpan); + + if (eofReached) + { + return; + } + } + } + + private static void ProcessWideRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + where TPixel : unmanaged, IPixel + { + int width = pixels.Width; + int height = pixels.Height; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width); + Span rowSpan = row.GetSpan(); + + bool eofReached = false; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + if (!stream.ReadDecimal(out int red) || + !stream.SkipWhitespaceAndComments() || + !stream.ReadDecimal(out int green) || + !stream.SkipWhitespaceAndComments()) + { + // Reached EOF before reading a full RGB value + eofReached = true; + break; + } + + stream.ReadDecimal(out int blue); + + rowSpan[x] = new Rgb48((ushort)red, (ushort)green, (ushort)blue); + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } + } + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.FromRgb48( + configuration, + rowSpan, + pixelSpan); + + if (eofReached) + { + return; + } + } + } + + private static void ProcessBlackAndWhite(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + where TPixel : unmanaged, IPixel + { + int width = pixels.Width; + int height = pixels.Height; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width); + Span rowSpan = row.GetSpan(); + + bool eofReached = false; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + stream.ReadDecimal(out int value); + + rowSpan[x] = value == 0 ? White : Black; + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } + } + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.FromL8( + configuration, + rowSpan, + pixelSpan); + + if (eofReached) + { + return; + } + } + } + } +} diff --git a/ImageSharp/Formats/Pbm/PlainEncoder.cs b/ImageSharp/Formats/Pbm/PlainEncoder.cs new file mode 100644 index 0000000..598cfc4 --- /dev/null +++ b/ImageSharp/Formats/Pbm/PlainEncoder.cs @@ -0,0 +1,287 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Buffers.Text; +using System.IO; +using System.Threading; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Pbm { + /// + /// Pixel encoding methods for the PBM plain encoding. + /// + internal static class PlainEncoder + { + private const byte NewLine = 0x0a; + private const byte Space = 0x20; + private const byte Zero = 0x30; + private const byte One = 0x31; + + private const int MaxCharsPerPixelBlackAndWhite = 2; + private const int MaxCharsPerPixelGrayscale = 4; + private const int MaxCharsPerPixelGrayscaleWide = 6; + private const int MaxCharsPerPixelRgb = 4 * 3; + private const int MaxCharsPerPixelRgbWide = 6 * 3; + + private static readonly StandardFormat DecimalFormat = StandardFormat.Parse("D"); + + /// + /// Decode pixels into the PBM plain encoding. + /// + /// The type of input pixel. + /// The configuration. + /// The byte stream to write to. + /// The input image. + /// The ColorType to use. + /// Data type of the pixels components. + /// The token to monitor for cancellation requests. + public static void WritePixels( + Configuration configuration, + Stream stream, + ImageFrame image, + PbmColorType colorType, + PbmComponentType componentType, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + if (colorType == PbmColorType.Grayscale) + { + if (componentType == PbmComponentType.Byte) + { + WriteGrayscale(configuration, stream, image, cancellationToken); + } + else + { + WriteWideGrayscale(configuration, stream, image, cancellationToken); + } + } + else if (colorType == PbmColorType.Rgb) + { + if (componentType == PbmComponentType.Byte) + { + WriteRgb(configuration, stream, image, cancellationToken); + } + else + { + WriteWideRgb(configuration, stream, image, cancellationToken); + } + } + else + { + WriteBlackAndWhite(configuration, stream, image, cancellationToken); + } + + // Write EOF indicator, as some encoders expect it. + stream.WriteByte(Space); + } + + private static void WriteGrayscale( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + int width = image.Width; + int height = image.Height; + Buffer2D pixelBuffer = image.PixelBuffer; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width); + Span rowSpan = row.GetSpan(); + using IMemoryOwner plainMemory = allocator.Allocate(width * MaxCharsPerPixelGrayscale); + Span plainSpan = plainMemory.GetSpan(); + + for (int y = 0; y < height; y++) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); + PixelOperations.Instance.ToL8( + configuration, + pixelSpan, + rowSpan); + + int written = 0; + for (int x = 0; x < width; x++) + { + Utf8Formatter.TryFormat(rowSpan[x].PackedValue, plainSpan[written..], out int bytesWritten, DecimalFormat); + written += bytesWritten; + plainSpan[written++] = Space; + } + + plainSpan[written - 1] = NewLine; + stream.Write(plainSpan, 0, written); + } + } + + private static void WriteWideGrayscale( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + int width = image.Width; + int height = image.Height; + Buffer2D pixelBuffer = image.PixelBuffer; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width); + Span rowSpan = row.GetSpan(); + using IMemoryOwner plainMemory = allocator.Allocate(width * MaxCharsPerPixelGrayscaleWide); + Span plainSpan = plainMemory.GetSpan(); + + for (int y = 0; y < height; y++) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); + PixelOperations.Instance.ToL16( + configuration, + pixelSpan, + rowSpan); + + int written = 0; + for (int x = 0; x < width; x++) + { + Utf8Formatter.TryFormat(rowSpan[x].PackedValue, plainSpan[written..], out int bytesWritten, DecimalFormat); + written += bytesWritten; + plainSpan[written++] = Space; + } + + plainSpan[written - 1] = NewLine; + stream.Write(plainSpan, 0, written); + } + } + + private static void WriteRgb( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + int width = image.Width; + int height = image.Height; + Buffer2D pixelBuffer = image.PixelBuffer; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width); + Span rowSpan = row.GetSpan(); + using IMemoryOwner plainMemory = allocator.Allocate(width * MaxCharsPerPixelRgb); + Span plainSpan = plainMemory.GetSpan(); + + for (int y = 0; y < height; y++) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); + PixelOperations.Instance.ToRgb24( + configuration, + pixelSpan, + rowSpan); + + int written = 0; + for (int x = 0; x < width; x++) + { + Utf8Formatter.TryFormat(rowSpan[x].R, plainSpan[written..], out int bytesWritten, DecimalFormat); + written += bytesWritten; + plainSpan[written++] = Space; + Utf8Formatter.TryFormat(rowSpan[x].G, plainSpan[written..], out bytesWritten, DecimalFormat); + written += bytesWritten; + plainSpan[written++] = Space; + Utf8Formatter.TryFormat(rowSpan[x].B, plainSpan[written..], out bytesWritten, DecimalFormat); + written += bytesWritten; + plainSpan[written++] = Space; + } + + plainSpan[written - 1] = NewLine; + stream.Write(plainSpan, 0, written); + } + } + + private static void WriteWideRgb( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + int width = image.Width; + int height = image.Height; + Buffer2D pixelBuffer = image.PixelBuffer; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width); + Span rowSpan = row.GetSpan(); + using IMemoryOwner plainMemory = allocator.Allocate(width * MaxCharsPerPixelRgbWide); + Span plainSpan = plainMemory.GetSpan(); + + for (int y = 0; y < height; y++) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); + PixelOperations.Instance.ToRgb48( + configuration, + pixelSpan, + rowSpan); + + int written = 0; + for (int x = 0; x < width; x++) + { + Utf8Formatter.TryFormat(rowSpan[x].R, plainSpan[written..], out int bytesWritten, DecimalFormat); + written += bytesWritten; + plainSpan[written++] = Space; + Utf8Formatter.TryFormat(rowSpan[x].G, plainSpan[written..], out bytesWritten, DecimalFormat); + written += bytesWritten; + plainSpan[written++] = Space; + Utf8Formatter.TryFormat(rowSpan[x].B, plainSpan[written..], out bytesWritten, DecimalFormat); + written += bytesWritten; + plainSpan[written++] = Space; + } + + plainSpan[written - 1] = NewLine; + stream.Write(plainSpan, 0, written); + } + } + + private static void WriteBlackAndWhite( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + int width = image.Width; + int height = image.Height; + Buffer2D pixelBuffer = image.PixelBuffer; + MemoryAllocator allocator = configuration.MemoryAllocator; + using IMemoryOwner row = allocator.Allocate(width); + Span rowSpan = row.GetSpan(); + using IMemoryOwner plainMemory = allocator.Allocate(width * MaxCharsPerPixelBlackAndWhite); + Span plainSpan = plainMemory.GetSpan(); + + for (int y = 0; y < height; y++) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); + PixelOperations.Instance.ToL8( + configuration, + pixelSpan, + rowSpan); + + int written = 0; + for (int x = 0; x < width; x++) + { + plainSpan[written++] = (rowSpan[x].PackedValue < 128) ? One : Zero; + plainSpan[written++] = Space; + } + + plainSpan[written - 1] = NewLine; + stream.Write(plainSpan, 0, written); + } + } + } +} diff --git a/ImageSharp/Formats/Png/Adam7.cs b/ImageSharp/Formats/Png/Adam7.cs new file mode 100644 index 0000000..a314132 --- /dev/null +++ b/ImageSharp/Formats/Png/Adam7.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Constants and helper methods for the Adam7 interlacing algorithm. + /// + internal static class Adam7 + { + /// + /// The amount to increment when processing each column per scanline for each interlaced pass. + /// + public static readonly int[] ColumnIncrement = [8, 8, 4, 4, 2, 2, 1]; + + /// + /// The index to start at when processing each column per scanline for each interlaced pass. + /// + public static readonly int[] FirstColumn = [0, 4, 0, 2, 0, 1, 0]; + + /// + /// The index to start at when processing each row per scanline for each interlaced pass. + /// + public static readonly int[] FirstRow = [0, 0, 4, 0, 2, 0, 1]; + + /// + /// The amount to increment when processing each row per scanline for each interlaced pass. + /// + public static readonly int[] RowIncrement = [8, 8, 8, 4, 4, 2, 2]; + + /// + /// Gets the width of the block. + /// + /// The width. + /// The pass. + /// + /// The + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ComputeBlockWidth(int width, int pass) + { + return (width + ColumnIncrement[pass] - 1 - FirstColumn[pass]) / ColumnIncrement[pass]; + } + + /// + /// Gets the height of the block. + /// + /// The height. + /// The pass. + /// + /// The + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ComputeBlockHeight(int height, int pass) + { + return (height + RowIncrement[pass] - 1 - FirstRow[pass]) / RowIncrement[pass]; + } + + /// + /// Returns the correct number of columns for each interlaced pass. + /// + /// The line width. + /// The current pass index. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ComputeColumns(int width, int passIndex) + { + uint w = (uint)width; + + uint result = passIndex switch + { + 0 => (w + 7) / 8, + 1 => (w + 3) / 8, + 2 => (w + 3) / 4, + 3 => (w + 1) / 4, + 4 => (w + 1) / 2, + 5 => w / 2, + 6 => w, + _ => Throw(passIndex) + }; + + return (int)result; + + static uint Throw(int passIndex) => throw new ArgumentException($"Not a valid pass index: {passIndex}"); + } + } +} diff --git a/ImageSharp/Formats/Png/Chunks/AnimationControl.cs b/ImageSharp/Formats/Png/Chunks/AnimationControl.cs new file mode 100644 index 0000000..6c46a83 --- /dev/null +++ b/ImageSharp/Formats/Png/Chunks/AnimationControl.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Png.Chunks { + internal readonly struct AnimationControl + { + public const int Size = 8; + + public AnimationControl(uint numberFrames, uint numberPlays) + { + this.NumberFrames = numberFrames; + this.NumberPlays = numberPlays; + } + + /// + /// Gets the number of frames + /// + public uint NumberFrames { get; } + + /// + /// Gets the number of times to loop this APNG. 0 indicates infinite looping. + /// + public uint NumberPlays { get; } + + /// + /// Writes the acTL to the given buffer. + /// + /// The buffer to write to. + public void WriteTo(Span buffer) + { + BinaryPrimitives.WriteInt32BigEndian(buffer[..4], (int)this.NumberFrames); + BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], (int)this.NumberPlays); + } + + /// + /// Parses the APngAnimationControl from the given data buffer. + /// + /// The data to parse. + /// The parsed acTL. + public static AnimationControl Parse(ReadOnlySpan data) + => new( + numberFrames: BinaryPrimitives.ReadUInt32BigEndian(data[..4]), + numberPlays: BinaryPrimitives.ReadUInt32BigEndian(data[4..8])); + } +} diff --git a/ImageSharp/Formats/Png/Chunks/FrameControl.cs b/ImageSharp/Formats/Png/Chunks/FrameControl.cs new file mode 100644 index 0000000..8dca5a3 --- /dev/null +++ b/ImageSharp/Formats/Png/Chunks/FrameControl.cs @@ -0,0 +1,168 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Png.Chunks { + internal readonly struct FrameControl + { + public const int Size = 26; + + public FrameControl(uint width, uint height) + : this(0, width, height, 0, 0, 0, 0, default, default) + { + } + + public FrameControl( + uint sequenceNumber, + uint width, + uint height, + uint xOffset, + uint yOffset, + ushort delayNumerator, + ushort delayDenominator, + FrameDisposalMode disposalMode, + FrameBlendMode blendMode) + { + this.SequenceNumber = sequenceNumber; + this.Width = width; + this.Height = height; + this.XOffset = xOffset; + this.YOffset = yOffset; + this.DelayNumerator = delayNumerator; + this.DelayDenominator = delayDenominator; + this.DisposalMode = disposalMode; + this.BlendMode = blendMode; + } + + /// + /// Gets the sequence number of the animation chunk, starting from 0 + /// + public uint SequenceNumber { get; } + + /// + /// Gets the width of the following frame + /// + public uint Width { get; } + + /// + /// Gets the height of the following frame + /// + public uint Height { get; } + + /// + /// Gets the X position at which to render the following frame + /// + public uint XOffset { get; } + + /// + /// Gets the Y position at which to render the following frame + /// + public uint YOffset { get; } + + /// + /// Gets the X limit at which to render the following frame + /// + public uint XMax => this.XOffset + this.Width; + + /// + /// Gets the Y limit at which to render the following frame + /// + public uint YMax => this.YOffset + this.Height; + + /// + /// Gets the frame delay fraction numerator + /// + public ushort DelayNumerator { get; } + + /// + /// Gets the frame delay fraction denominator + /// + public ushort DelayDenominator { get; } + + /// + /// Gets the type of frame area disposal to be done after rendering this frame + /// + public FrameDisposalMode DisposalMode { get; } + + /// + /// Gets the type of frame area rendering for this frame + /// + public FrameBlendMode BlendMode { get; } + + public Rectangle Bounds => new((int)this.XOffset, (int)this.YOffset, (int)this.Width, (int)this.Height); + + /// + /// Validates the APng fcTL. + /// + /// The header. + /// + /// Thrown if the image does pass validation. + /// + public void Validate(PngHeader header) + { + if (this.Width == 0) + { + PngThrowHelper.ThrowInvalidParameter(this.Width, "Expected > 0"); + } + + if (this.Height == 0) + { + PngThrowHelper.ThrowInvalidParameter(this.Height, "Expected > 0"); + } + + if (this.XMax > header.Width) + { + PngThrowHelper.ThrowInvalidParameter(this.XOffset, this.Width, $"The x-offset plus width > {nameof(PngHeader)}.{nameof(PngHeader.Width)}"); + } + + if (this.YMax > header.Height) + { + PngThrowHelper.ThrowInvalidParameter(this.YOffset, this.Height, $"The y-offset plus height > {nameof(PngHeader)}.{nameof(PngHeader.Height)}"); + } + } + + /// + /// Writes the fcTL to the given buffer. + /// + /// The buffer to write to. + public void WriteTo(Span buffer) + { + BinaryPrimitives.WriteUInt32BigEndian(buffer[..4], this.SequenceNumber); + BinaryPrimitives.WriteUInt32BigEndian(buffer[4..8], this.Width); + BinaryPrimitives.WriteUInt32BigEndian(buffer[8..12], this.Height); + BinaryPrimitives.WriteUInt32BigEndian(buffer[12..16], this.XOffset); + BinaryPrimitives.WriteUInt32BigEndian(buffer[16..20], this.YOffset); + BinaryPrimitives.WriteUInt16BigEndian(buffer[20..22], this.DelayNumerator); + BinaryPrimitives.WriteUInt16BigEndian(buffer[22..24], this.DelayDenominator); + + buffer[24] = (byte)(this.DisposalMode - 1); + buffer[25] = (byte)this.BlendMode; + } + + /// + /// Parses the APngFrameControl from the given data buffer. + /// + /// The data to parse. + /// The parsed fcTL. + public static FrameControl Parse(ReadOnlySpan data) + { + if (data.Length < Size) + { + PngThrowHelper.ThrowInvalidImageContentException("The frame control chunk does not contain enough data!"); + } + + return new( + sequenceNumber: BinaryPrimitives.ReadUInt32BigEndian(data[..4]), + width: BinaryPrimitives.ReadUInt32BigEndian(data[4..8]), + height: BinaryPrimitives.ReadUInt32BigEndian(data[8..12]), + xOffset: BinaryPrimitives.ReadUInt32BigEndian(data[12..16]), + yOffset: BinaryPrimitives.ReadUInt32BigEndian(data[16..20]), + delayNumerator: BinaryPrimitives.ReadUInt16BigEndian(data[20..22]), + delayDenominator: BinaryPrimitives.ReadUInt16BigEndian(data[22..24]), + disposalMode: (FrameDisposalMode)(data[24] + 1), + blendMode: (FrameBlendMode)data[25]); + } + } +} diff --git a/ImageSharp/Formats/Png/Chunks/PngHeader.cs b/ImageSharp/Formats/Png/Chunks/PngHeader.cs new file mode 100644 index 0000000..bc44928 --- /dev/null +++ b/ImageSharp/Formats/Png/Chunks/PngHeader.cs @@ -0,0 +1,144 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Png.Chunks { + /// + /// Represents the png header chunk. + /// + internal readonly struct PngHeader + { + public const int Size = 13; + + public PngHeader( + int width, + int height, + byte bitDepth, + PngColorType colorType, + byte compressionMethod, + byte filterMethod, + PngInterlaceMode interlaceMethod) + { + this.Width = width; + this.Height = height; + this.BitDepth = bitDepth; + this.ColorType = colorType; + this.CompressionMethod = compressionMethod; + this.FilterMethod = filterMethod; + this.InterlaceMethod = interlaceMethod; + } + + /// + /// Gets the dimension in x-direction of the image in pixels. + /// + public int Width { get; } + + /// + /// Gets the dimension in y-direction of the image in pixels. + /// + public int Height { get; } + + /// + /// Gets the bit depth. + /// Bit depth is a single-byte integer giving the number of bits per sample + /// or per palette index (not per pixel). Valid values are 1, 2, 4, 8, and 16, + /// although not all values are allowed for all color types. + /// + public byte BitDepth { get; } + + /// + /// Gets the color type. + /// Color type is a integer that describes the interpretation of the + /// image data. Color type codes represent sums of the following values: + /// 1 (palette used), 2 (color used), and 4 (alpha channel used). + /// + public PngColorType ColorType { get; } + + /// + /// Gets the compression method. + /// Indicates the method used to compress the image data. At present, + /// only compression method 0 (deflate/inflate compression with a sliding + /// window of at most 32768 bytes) is defined. + /// + public byte CompressionMethod { get; } + + /// + /// Gets the preprocessing method. + /// Indicates the preprocessing method applied to the image + /// data before compression. At present, only filter method 0 + /// (adaptive filtering with five basic filter types) is defined. + /// + public byte FilterMethod { get; } + + /// + /// Gets the transmission order. + /// Indicates the transmission order of the image data. + /// Two values are currently defined: 0 (no interlace) or 1 (Adam7 interlace). + /// + public PngInterlaceMode InterlaceMethod { get; } + + /// + /// Validates the png header. + /// + /// + /// Thrown if the image does pass validation. + /// + public void Validate() + { + if (!PngConstants.ColorTypes.TryGetValue(this.ColorType, out byte[] supportedBitDepths)) + { + throw new NotSupportedException($"Invalid or unsupported color type. Was '{this.ColorType}'."); + } + + if (supportedBitDepths.AsSpan().IndexOf(this.BitDepth) == -1) + { + throw new NotSupportedException($"Invalid or unsupported bit depth. Was '{this.BitDepth}'."); + } + + if (this.FilterMethod != 0) + { + throw new NotSupportedException($"Invalid filter method. Expected 0. Was '{this.FilterMethod}'."); + } + + // The png specification only defines 'None' and 'Adam7' as interlaced methods. + if (this.InterlaceMethod is not PngInterlaceMode.None and not PngInterlaceMode.Adam7) + { + throw new NotSupportedException($"Invalid interlace method. Expected 'None' or 'Adam7'. Was '{this.InterlaceMethod}'."); + } + } + + /// + /// Writes the header to the given buffer. + /// + /// The buffer to write to. + public void WriteTo(Span buffer) + { + BinaryPrimitives.WriteInt32BigEndian(buffer[..4], this.Width); + BinaryPrimitives.WriteInt32BigEndian(buffer.Slice(4, 4), this.Height); + + buffer[8] = this.BitDepth; + buffer[9] = (byte)this.ColorType; + buffer[10] = this.CompressionMethod; + buffer[11] = this.FilterMethod; + buffer[12] = (byte)this.InterlaceMethod; + } + + /// + /// Parses the PngHeader from the given data buffer. + /// + /// The data to parse. + /// The parsed PngHeader. + public static PngHeader Parse(ReadOnlySpan data) + => new( + width: BinaryPrimitives.ReadInt32BigEndian(data[..4]), + height: BinaryPrimitives.ReadInt32BigEndian(data.Slice(4, 4)), + bitDepth: data[8], + colorType: (PngColorType)data[9], + compressionMethod: data[10], + filterMethod: data[11], + interlaceMethod: (PngInterlaceMode)data[12]); + } +} diff --git a/ImageSharp/Formats/Png/Chunks/PngPhysical.cs b/ImageSharp/Formats/Png/Chunks/PngPhysical.cs new file mode 100644 index 0000000..84e6f96 --- /dev/null +++ b/ImageSharp/Formats/Png/Chunks/PngPhysical.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata; + +namespace SixLabors.ImageSharp.Formats.Png.Chunks { + /// + /// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. + /// + internal readonly struct PngPhysical + { + public const int Size = 9; + + public PngPhysical(uint x, uint y, byte unitSpecifier) + { + this.XAxisPixelsPerUnit = x; + this.YAxisPixelsPerUnit = y; + this.UnitSpecifier = unitSpecifier; + } + + /// + /// Gets the number of pixels per unit on the X axis. + /// + public uint XAxisPixelsPerUnit { get; } + + /// + /// Gets the number of pixels per unit on the Y axis. + /// + public uint YAxisPixelsPerUnit { get; } + + /// + /// Gets the unit specifier. + /// 0: unit is unknown + /// 1: unit is the meter + /// When the unit specifier is 0, the pHYs chunk defines pixel aspect ratio only; the actual size of the pixels remains unspecified. + /// + public byte UnitSpecifier { get; } + + /// + /// Parses the PhysicalChunkData from the given buffer. + /// + /// The data buffer. + /// The parsed PhysicalChunkData. + public static PngPhysical Parse(ReadOnlySpan data) + { + if (data.Length < 9) + { + PngThrowHelper.ThrowInvalidImageContentException("pHYs chunk is too short"); + } + + uint hResolution = BinaryPrimitives.ReadUInt32BigEndian(data[..4]); + uint vResolution = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(4, 4)); + byte unit = data[8]; + + return new PngPhysical(hResolution, vResolution, unit); + } + + /// + /// Constructs the PngPhysicalChunkData from the provided metadata. + /// If the resolution units are not in meters, they are automatically converted. + /// + /// The metadata. + /// The constructed PngPhysicalChunkData instance. + public static PngPhysical FromMetadata(ImageMetadata meta) + { + uint x; + uint y; + + byte unitSpecifier; + switch (meta.ResolutionUnits) + { + case PixelResolutionUnit.AspectRatio: + unitSpecifier = 0; // Unspecified + x = (uint)Math.Round(meta.HorizontalResolution); + y = (uint)Math.Round(meta.VerticalResolution); + break; + + case PixelResolutionUnit.PixelsPerInch: + unitSpecifier = 1; // Per meter + x = (uint)Math.Round(UnitConverter.InchToMeter(meta.HorizontalResolution)); + y = (uint)Math.Round(UnitConverter.InchToMeter(meta.VerticalResolution)); + break; + + case PixelResolutionUnit.PixelsPerCentimeter: + unitSpecifier = 1; // Per meter + x = (uint)Math.Round(UnitConverter.CmToMeter(meta.HorizontalResolution)); + y = (uint)Math.Round(UnitConverter.CmToMeter(meta.VerticalResolution)); + break; + + default: + unitSpecifier = 1; // Per meter + x = (uint)Math.Round(meta.HorizontalResolution); + y = (uint)Math.Round(meta.VerticalResolution); + break; + } + + return new PngPhysical(x, y, unitSpecifier); + } + + /// + /// Writes the data to the given buffer. + /// + /// The buffer. + public void WriteTo(Span buffer) + { + BinaryPrimitives.WriteUInt32BigEndian(buffer[..4], this.XAxisPixelsPerUnit); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(4, 4), this.YAxisPixelsPerUnit); + buffer[8] = this.UnitSpecifier; + } + } +} diff --git a/ImageSharp/Formats/Png/Chunks/PngTextData.cs b/ImageSharp/Formats/Png/Chunks/PngTextData.cs new file mode 100644 index 0000000..7bd68a1 --- /dev/null +++ b/ImageSharp/Formats/Png/Chunks/PngTextData.cs @@ -0,0 +1,136 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Png.Chunks { + /// + /// Stores text data contained in the iTXt, tEXt, and zTXt chunks. + /// Used for conveying textual information associated with the image, like the name of the author, + /// the copyright information, the date, where the image was created, or some other information. + /// + public readonly struct PngTextData : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The keyword of the property. + /// The value of the property. + /// An optional language tag. + /// A optional translated keyword. + public PngTextData(string keyword, string value, string languageTag, string translatedKeyword) + { + Guard.NotNullOrWhiteSpace(keyword, nameof(keyword)); + + // No leading or trailing whitespace is allowed in keywords. + this.Keyword = keyword.Trim(); + this.Value = value; + this.LanguageTag = languageTag; + this.TranslatedKeyword = translatedKeyword; + } + + /// + /// Gets the keyword of this which indicates + /// the type of information represented by the text string as described in https://www.w3.org/TR/PNG/#11keywords. + /// + /// + /// Typical properties are the author, copyright information or other meta information. + /// + public string Keyword { get; } + + /// + /// Gets the value of this . + /// + public string Value { get; } + + /// + /// Gets an optional language tag defined in https://www.w3.org/TR/PNG/#2-RFC-3066 indicates the human language used by the translated keyword and the text. + /// If the first word is two or three letters long, it is an ISO language code https://www.w3.org/TR/PNG/#2-ISO-639. + /// + /// + /// Examples: cn, en-uk, no-bok, x-klingon, x-KlInGoN. + /// + public string LanguageTag { get; } + + /// + /// Gets an optional translated keyword, should contain a translation of the keyword into the language indicated by the language tag. + /// + public string TranslatedKeyword { get; } + + /// + /// Compares two objects. The result specifies whether the values + /// of the properties of the two objects are equal. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + public static bool operator ==(PngTextData left, PngTextData right) + => left.Equals(right); + + /// + /// Compares two objects. The result specifies whether the values + /// of the properties of the two objects are unequal. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + public static bool operator !=(PngTextData left, PngTextData right) + => !(left == right); + + /// + /// Indicates whether this instance and a specified object are equal. + /// + /// + /// The object to compare with the current instance. + /// + /// + /// true if and this instance are the same type and represent the + /// same value; otherwise, false. + /// + public override bool Equals(object? obj) + => obj is PngTextData other && this.Equals(other); + + /// + /// Returns the hash code for this instance. + /// + /// + /// A 32-bit signed integer that is the hash code for this instance. + /// + public override int GetHashCode() + => HashCode.Combine(this.Keyword, this.Value, this.LanguageTag, this.TranslatedKeyword); + + /// + /// Returns the fully qualified type name of this instance. + /// + /// + /// A containing a fully qualified type name. + /// + public override string ToString() + => $"PngTextData [ Name={this.Keyword}, Value={this.Value} ]"; + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// + /// True if the current object is equal to the parameter; otherwise, false. + /// + /// An object to compare with this object. + public bool Equals(PngTextData other) + => this.Keyword.Equals(other.Keyword, StringComparison.OrdinalIgnoreCase) + && this.Value.Equals(other.Value, StringComparison.OrdinalIgnoreCase) + && this.LanguageTag.Equals(other.LanguageTag, StringComparison.OrdinalIgnoreCase) + && this.TranslatedKeyword.Equals(other.TranslatedKeyword, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/ImageSharp/Formats/Png/Filters/AverageFilter.cs new file mode 100644 index 0000000..24d07b2 --- /dev/null +++ b/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -0,0 +1,245 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.Formats.Png.Filters { + /// + /// The Average filter uses the average of the two neighboring pixels (left and above) to predict + /// the value of a pixel. + /// + /// + internal static class AverageFilter + { + /// + /// Decodes a scanline, which was filtered with the average filter. + /// + /// The scanline to decode. + /// The previous scanline. + /// The bytes per pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Decode(Span scanline, Span previousScanline, int bytesPerPixel) + { + DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); + + // The Avg filter predicts each pixel as the (truncated) average of a and b: + // Average(x) + floor((Raw(x-bpp)+Prior(x))/2) + // With pixels positioned like this: + // prev: c b + // row: a d + if (Sse2.IsSupported && bytesPerPixel is 4) + { + DecodeSse2(scanline, previousScanline); + } + else if (AdvSimd.IsSupported && bytesPerPixel is 4) + { + DecodeArm(scanline, previousScanline); + } + else + { + DecodeScalar(scanline, previousScanline, (uint)bytesPerPixel); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeSse2(Span scanline, Span previousScanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + Vector128 d = Vector128.Zero; + Vector128 ones = Vector128.Create((byte)1); + + int rb = scanline.Length; + nuint offset = 1; + while (rb >= 4) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + Vector128 a = d; + Vector128 b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(); + d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(); + + // PNG requires a truncating average, so we can't just use _mm_avg_epu8, + // but we can fix it up by subtracting off 1 if it rounded up. + Vector128 avg = Sse2.Average(a, b); + Vector128 xor = Sse2.Xor(a, b); + Vector128 and = Sse2.And(xor, ones); + avg = Sse2.Subtract(avg, and); + d = Sse2.Add(d, avg); + + // Store the result. + Unsafe.As(ref scanRef) = Sse2.ConvertToInt32(d.AsInt32()); + + rb -= 4; + offset += 4; + } + } + + public static void DecodeArm(Span scanline, Span previousScanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + Vector64 d = Vector64.Zero; + + int rb = scanline.Length; + nuint offset = 1; + const int bytesPerBatch = 4; + while (rb >= bytesPerBatch) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + Vector64 a = d; + Vector64 b = Vector64.CreateScalar(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(); + d = Vector64.CreateScalar(Unsafe.As(ref scanRef)).AsByte(); + + Vector64 avg = AdvSimd.FusedAddHalving(a, b); + d = AdvSimd.Add(d, avg); + + Unsafe.As(ref scanRef) = d.AsInt32().ToScalar(); + + rb -= bytesPerBatch; + offset += bytesPerBatch; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeScalar(Span scanline, Span previousScanline, uint bytesPerPixel) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + nuint x = 1; + for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); + byte above = Unsafe.Add(ref prevBaseRef, x); + scan = (byte)(scan + (above >> 1)); + } + + for (; x < (uint)scanline.Length; ++x) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); + byte left = Unsafe.Add(ref scanBaseRef, x - bytesPerPixel); + byte above = Unsafe.Add(ref prevBaseRef, x); + scan = (byte)(scan + Average(left, above)); + } + } + + /// + /// Encodes a scanline with the average filter applied. + /// + /// The scanline to encode. + /// The previous scanline. + /// The filtered scanline result. + /// The bytes per pixel. + /// The sum of the total variance of the filtered row. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, uint bytesPerPixel, out int sum) + { + DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); + DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); + + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); + sum = 0; + + // Average(x) = Raw(x) - floor((Raw(x-bpp)+Prior(x))/2) + resultBaseRef = (byte)FilterType.Average; + + nuint x = 0; + for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) + { + byte scan = Unsafe.Add(ref scanBaseRef, x); + byte above = Unsafe.Add(ref prevBaseRef, x); + ++x; + ref byte res = ref Unsafe.Add(ref resultBaseRef, x); + res = (byte)(scan - (above >> 1)); + sum += Numerics.Abs(unchecked((sbyte)res)); + } + + if (Avx2.IsSupported) + { + Vector256 zero = Vector256.Zero; + Vector256 sumAccumulator = Vector256.Zero; + Vector256 allBitsSet = Avx2.CompareEqual(sumAccumulator, sumAccumulator).AsByte(); + + for (nuint xLeft = x - bytesPerPixel; (int)x <= scanline.Length - Vector256.Count; xLeft += (uint)Vector256.Count) + { + Vector256 scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); + Vector256 left = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); + Vector256 above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); + + Vector256 avg = Avx2.Xor(Avx2.Average(Avx2.Xor(left, allBitsSet), Avx2.Xor(above, allBitsSet)), allBitsSet); + Vector256 res = Avx2.Subtract(scan, avg); + + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type + x += (uint)Vector256.Count; + + sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32()); + } + + sum += Numerics.EvenReduceSum(sumAccumulator); + } + else if (Sse2.IsSupported) + { + Vector128 zero = Vector128.Zero; + Vector128 sumAccumulator = Vector128.Zero; + Vector128 allBitsSet = Sse2.CompareEqual(sumAccumulator, sumAccumulator).AsByte(); + + for (nuint xLeft = x - bytesPerPixel; (int)x <= scanline.Length - Vector128.Count; xLeft += (uint)Vector128.Count) + { + Vector128 scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); + Vector128 left = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); + Vector128 above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); + + Vector128 avg = Sse2.Xor(Sse2.Average(Sse2.Xor(left, allBitsSet), Sse2.Xor(above, allBitsSet)), allBitsSet); + Vector128 res = Sse2.Subtract(scan, avg); + + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type + x += (uint)Vector128.Count; + + Vector128 absRes; + if (Ssse3.IsSupported) + { + absRes = Ssse3.Abs(res.AsSByte()); + } + else + { + Vector128 mask = Sse2.CompareGreaterThan(zero.AsSByte(), res.AsSByte()); + absRes = Sse2.Xor(Sse2.Add(res.AsSByte(), mask), mask).AsByte(); + } + + sumAccumulator = Sse2.Add(sumAccumulator, Sse2.SumAbsoluteDifferences(absRes, zero).AsInt32()); + } + + sum += Numerics.EvenReduceSum(sumAccumulator); + } + + for (nuint xLeft = x - bytesPerPixel; x < (uint)scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) + { + byte scan = Unsafe.Add(ref scanBaseRef, x); + byte left = Unsafe.Add(ref scanBaseRef, xLeft); + byte above = Unsafe.Add(ref prevBaseRef, x); + ++x; + ref byte res = ref Unsafe.Add(ref resultBaseRef, x); + res = (byte)(scan - Average(left, above)); + sum += Numerics.Abs(unchecked((sbyte)res)); + } + } + + /// + /// Calculates the average value of two bytes + /// + /// The left byte + /// The above byte + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Average(byte left, byte above) => (left + above) >> 1; + } +} diff --git a/ImageSharp/Formats/Png/Filters/FilterType.cs b/ImageSharp/Formats/Png/Filters/FilterType.cs new file mode 100644 index 0000000..ba0942a --- /dev/null +++ b/ImageSharp/Formats/Png/Filters/FilterType.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Png.Filters { + /// + /// Provides enumeration of the various PNG filter types. + /// + /// + internal enum FilterType + { + /// + /// With the None filter, the scanline is transmitted unmodified; it is only necessary to + /// insert a filter type byte before the data. + /// + None = 0, + + /// + /// The Sub filter transmits the difference between each byte and the value of the corresponding + /// byte of the prior pixel. + /// + Sub = 1, + + /// + /// The Up filter is just like the Sub filter except that the pixel immediately above the current + /// pixel, rather than just to its left, is used as the predictor. + /// + Up = 2, + + /// + /// The Average filter uses the average of the two neighboring pixels (left and above) to + /// predict the value of a pixel. + /// + Average = 3, + + /// + /// The Paeth filter computes a simple linear function of the three neighboring pixels (left, above, upper left), + /// then chooses as predictor the neighboring pixel closest to the computed value. + /// This technique is due to Alan W. Paeth + /// + Paeth = 4 + } +} diff --git a/ImageSharp/Formats/Png/Filters/NoneFilter.cs b/ImageSharp/Formats/Png/Filters/NoneFilter.cs new file mode 100644 index 0000000..5bb719b --- /dev/null +++ b/ImageSharp/Formats/Png/Filters/NoneFilter.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Png.Filters { + /// + /// The None filter, the scanline is transmitted unmodified; it is only necessary to + /// insert a filter type byte before the data. + /// + /// + internal static class NoneFilter + { + /// + /// Encodes the scanline + /// + /// The scanline to encode + /// The filtered scanline result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Encode(ReadOnlySpan scanline, Span result) + { + // Insert row filter byte before the data. + result[0] = (byte)FilterType.None; + result = result[1..]; + scanline[..Math.Min(scanline.Length, result.Length)].CopyTo(result); + } + } +} diff --git a/ImageSharp/Formats/Png/Filters/PaethFilter.cs b/ImageSharp/Formats/Png/Filters/PaethFilter.cs new file mode 100644 index 0000000..135367f --- /dev/null +++ b/ImageSharp/Formats/Png/Filters/PaethFilter.cs @@ -0,0 +1,374 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.Formats.Png.Filters { + /// + /// The Paeth filter computes a simple linear function of the three neighboring pixels (left, above, upper left), + /// then chooses as predictor the neighboring pixel closest to the computed value. + /// This technique is due to Alan W. Paeth. + /// + /// + internal static class PaethFilter + { + /// + /// Decodes a scanline, which was filtered with the paeth filter. + /// + /// The scanline to decode. + /// The previous scanline. + /// The bytes per pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Decode(Span scanline, Span previousScanline, int bytesPerPixel) + { + DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); + + // Paeth tries to predict pixel d using the pixel to the left of it, a, + // and two pixels from the previous row, b and c: + // prev: c b + // row: a d + // The Paeth function predicts d to be whichever of a, b, or c is nearest to + // p = a + b - c. + if (Ssse3.IsSupported && bytesPerPixel is 4) + { + DecodeSsse3(scanline, previousScanline); + } + else if (AdvSimd.Arm64.IsSupported && bytesPerPixel is 4) + { + DecodeArm(scanline, previousScanline); + } + else + { + DecodeScalar(scanline, previousScanline, (uint)bytesPerPixel); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeSsse3(Span scanline, Span previousScanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + Vector128 b = Vector128.Zero; + Vector128 d = Vector128.Zero; + + int rb = scanline.Length; + nuint offset = 1; + while (rb >= 4) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + + // It's easiest to do this math (particularly, deal with pc) with 16-bit intermediates. + Vector128 c = b; + Vector128 a = d; + b = Sse2.UnpackLow( + Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(), + Vector128.Zero); + d = Sse2.UnpackLow( + Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(), + Vector128.Zero); + + // (p-a) == (a+b-c - a) == (b-c) + Vector128 pa = Sse2.Subtract(b.AsInt16(), c.AsInt16()); + + // (p-b) == (a+b-c - b) == (a-c) + Vector128 pb = Sse2.Subtract(a.AsInt16(), c.AsInt16()); + + // (p-c) == (a+b-c - c) == (a+b-c-c) == (b-c)+(a-c) + Vector128 pc = Sse2.Add(pa.AsInt16(), pb.AsInt16()); + + pa = Ssse3.Abs(pa.AsInt16()).AsInt16(); /* |p-a| */ + pb = Ssse3.Abs(pb.AsInt16()).AsInt16(); /* |p-b| */ + pc = Ssse3.Abs(pc.AsInt16()).AsInt16(); /* |p-c| */ + + Vector128 smallest = Sse2.Min(pc, Sse2.Min(pa, pb)); + + // Paeth breaks ties favoring a over b over c. + Vector128 mask = SimdUtils.HwIntrinsics.BlendVariable(c, b, Sse2.CompareEqual(smallest, pb).AsByte()); + Vector128 nearest = SimdUtils.HwIntrinsics.BlendVariable(mask, a, Sse2.CompareEqual(smallest, pa).AsByte()); + + // Note `_epi8`: we need addition to wrap modulo 255. + d = Sse2.Add(d, nearest); + + // Store the result. + Unsafe.As(ref scanRef) = Sse2.ConvertToInt32(Sse2.PackUnsignedSaturate(d.AsInt16(), d.AsInt16()).AsInt32()); + + rb -= 4; + offset += 4; + } + } + + public static void DecodeArm(Span scanline, Span previousScanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + Vector128 b = Vector128.Zero; + Vector128 d = Vector128.Zero; + + int rb = scanline.Length; + nuint offset = 1; + const int bytesPerBatch = 4; + while (rb >= bytesPerBatch) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + Vector128 c = b; + Vector128 a = d; + b = AdvSimd.Arm64.ZipLow( + Vector128.CreateScalar(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(), + Vector128.Zero).AsByte(); + d = AdvSimd.Arm64.ZipLow( + Vector128.CreateScalar(Unsafe.As(ref scanRef)).AsByte(), + Vector128.Zero).AsByte(); + + // (p-a) == (a+b-c - a) == (b-c) + Vector128 pa = AdvSimd.Subtract(b.AsInt16(), c.AsInt16()); + + // (p-b) == (a+b-c - b) == (a-c) + Vector128 pb = AdvSimd.Subtract(a.AsInt16(), c.AsInt16()); + + // (p-c) == (a+b-c - c) == (a+b-c-c) == (b-c)+(a-c) + Vector128 pc = AdvSimd.Add(pa.AsInt16(), pb.AsInt16()); + + pa = AdvSimd.Abs(pa.AsInt16()).AsInt16(); /* |p-a| */ + pb = AdvSimd.Abs(pb.AsInt16()).AsInt16(); /* |p-b| */ + pc = AdvSimd.Abs(pc.AsInt16()).AsInt16(); /* |p-c| */ + + Vector128 smallest = AdvSimd.Min(pc, AdvSimd.Min(pa, pb)); + + // Paeth breaks ties favoring a over b over c. + Vector128 mask = SimdUtils.HwIntrinsics.BlendVariable(c, b, AdvSimd.CompareEqual(smallest, pb).AsByte()); + Vector128 nearest = SimdUtils.HwIntrinsics.BlendVariable(mask, a, AdvSimd.CompareEqual(smallest, pa).AsByte()); + + d = AdvSimd.Add(d, nearest); + + Vector64 e = AdvSimd.ExtractNarrowingSaturateUnsignedLower(d.AsInt16()); + + Unsafe.As(ref scanRef) = Vector128.Create(e, e).AsInt32().ToScalar(); + + rb -= bytesPerBatch; + offset += bytesPerBatch; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeScalar(Span scanline, Span previousScanline, uint bytesPerPixel) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + // Paeth(x) + PaethPredictor(Raw(x-bpp), Prior(x), Prior(x-bpp)) + nuint offset = bytesPerPixel + 1; // Add one because x starts at one. + nuint x = 1; + for (; x < offset; x++) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); + byte above = Unsafe.Add(ref prevBaseRef, x); + scan = (byte)(scan + above); + } + + for (; x < (uint)scanline.Length; x++) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); + byte left = Unsafe.Add(ref scanBaseRef, x - bytesPerPixel); + byte above = Unsafe.Add(ref prevBaseRef, x); + byte upperLeft = Unsafe.Add(ref prevBaseRef, x - bytesPerPixel); + scan = (byte)(scan + PaethPredictor(left, above, upperLeft)); + } + } + + /// + /// Encodes a scanline and applies the paeth filter. + /// + /// The scanline to encode + /// The previous scanline. + /// The filtered scanline result. + /// The bytes per pixel. + /// The sum of the total variance of the filtered row. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, int bytesPerPixel, out int sum) + { + DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); + DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); + + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); + sum = 0; + + // Paeth(x) = Raw(x) - PaethPredictor(Raw(x-bpp), Prior(x), Prior(x - bpp)) + resultBaseRef = (byte)FilterType.Paeth; + + nuint x = 0; + for (; x < (uint)bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) + { + byte scan = Unsafe.Add(ref scanBaseRef, x); + byte above = Unsafe.Add(ref prevBaseRef, x); + ++x; + ref byte res = ref Unsafe.Add(ref resultBaseRef, x); + res = (byte)(scan - PaethPredictor(0, above, 0)); + sum += Numerics.Abs(unchecked((sbyte)res)); + } + + if (Avx2.IsSupported) + { + Vector256 zero = Vector256.Zero; + Vector256 sumAccumulator = Vector256.Zero; + + for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= scanline.Length - Vector256.Count; xLeft += (uint)Vector256.Count) + { + Vector256 scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); + Vector256 left = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); + Vector256 above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); + Vector256 upperLeft = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, xLeft)); + + Vector256 res = Avx2.Subtract(scan, PaethPredictor(left, above, upperLeft)); + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type + x += (uint)Vector256.Count; + + sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32()); + } + + sum += Numerics.EvenReduceSum(sumAccumulator); + } + else if (Vector.IsHardwareAccelerated) + { + Vector sumAccumulator = Vector.Zero; + + for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= scanline.Length - Vector.Count; xLeft += (uint)Vector.Count) + { + Vector scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); + Vector left = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); + Vector above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); + Vector upperLeft = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, xLeft)); + + Vector res = scan - PaethPredictor(left, above, upperLeft); + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type + x += (uint)Vector.Count; + + Numerics.Accumulate(ref sumAccumulator, Vector.AsVectorByte(Vector.Abs(Vector.AsVectorSByte(res)))); + } + + for (int i = 0; i < Vector.Count; i++) + { + sum += (int)sumAccumulator[i]; + } + } + + for (nuint xLeft = x - (uint)bytesPerPixel; (int)x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) + { + byte scan = Unsafe.Add(ref scanBaseRef, x); + byte left = Unsafe.Add(ref scanBaseRef, xLeft); + byte above = Unsafe.Add(ref prevBaseRef, x); + byte upperLeft = Unsafe.Add(ref prevBaseRef, xLeft); + ++x; + ref byte res = ref Unsafe.Add(ref resultBaseRef, x); + res = (byte)(scan - PaethPredictor(left, above, upperLeft)); + sum += Numerics.Abs(unchecked((sbyte)res)); + } + } + + /// + /// Computes a simple linear function of the three neighboring pixels (left, above, upper left), then chooses + /// as predictor the neighboring pixel closest to the computed value. + /// + /// The left neighbor pixel. + /// The above neighbor pixel. + /// The upper left neighbor pixel. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte PaethPredictor(byte left, byte above, byte upperLeft) + { + int p = left + above - upperLeft; + int pa = Numerics.Abs(p - left); + int pb = Numerics.Abs(p - above); + int pc = Numerics.Abs(p - upperLeft); + + if (pa <= pb && pa <= pc) + { + return left; + } + + if (pb <= pc) + { + return above; + } + + return upperLeft; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 PaethPredictor(Vector256 left, Vector256 above, Vector256 upleft) + { + Vector256 zero = Vector256.Zero; + + // Here, we refactor pa = abs(p - left) = abs(left + above - upleft - left) + // to pa = abs(above - upleft). Same deal for pb. + // Using saturated subtraction, if the result is negative, the output is zero. + // If we subtract in both directions and `or` the results, only one can be + // non-zero, so we end up with the absolute value. + Vector256 sac = Avx2.SubtractSaturate(above, upleft); + Vector256 sbc = Avx2.SubtractSaturate(left, upleft); + Vector256 pa = Avx2.Or(Avx2.SubtractSaturate(upleft, above), sac); + Vector256 pb = Avx2.Or(Avx2.SubtractSaturate(upleft, left), sbc); + + // pc = abs(left + above - upleft - upleft), or abs(left - upleft + above - upleft). + // We've already calculated left - upleft and above - upleft in `sac` and `sbc`. + // If they are both negative or both positive, the absolute value of their + // sum can't possibly be less than `pa` or `pb`, so we'll never use the value. + // We make a mask that sets the value to 255 if they either both got + // saturated to zero or both didn't. Then we calculate the absolute value + // of their difference using saturated subtract and `or`, same as before, + // keeping the value only where the mask isn't set. + Vector256 pm = Avx2.CompareEqual(Avx2.CompareEqual(sac, zero), Avx2.CompareEqual(sbc, zero)); + Vector256 pc = Avx2.Or(pm, Avx2.Or(Avx2.SubtractSaturate(pb, pa), Avx2.SubtractSaturate(pa, pb))); + + // Finally, blend the values together. We start with `upleft` and overwrite on + // tied values so that the `left`, `above`, `upleft` precedence is preserved. + Vector256 minbc = Avx2.Min(pc, pb); + Vector256 resbc = Avx2.BlendVariable(upleft, above, Avx2.CompareEqual(minbc, pb)); + return Avx2.BlendVariable(resbc, left, Avx2.CompareEqual(Avx2.Min(minbc, pa), pa)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector PaethPredictor(Vector left, Vector above, Vector upperLeft) + { + Vector.Widen(left, out Vector a1, out Vector a2); + Vector.Widen(above, out Vector b1, out Vector b2); + Vector.Widen(upperLeft, out Vector c1, out Vector c2); + + Vector p1 = PaethPredictor(Vector.AsVectorInt16(a1), Vector.AsVectorInt16(b1), Vector.AsVectorInt16(c1)); + Vector p2 = PaethPredictor(Vector.AsVectorInt16(a2), Vector.AsVectorInt16(b2), Vector.AsVectorInt16(c2)); + return Vector.AsVectorByte(Vector.Narrow(p1, p2)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector PaethPredictor(Vector left, Vector above, Vector upperLeft) + { + Vector p = left + above - upperLeft; + Vector pa = Vector.Abs(p - left); + Vector pb = Vector.Abs(p - above); + Vector pc = Vector.Abs(p - upperLeft); + + Vector pa_pb = Vector.LessThanOrEqual(pa, pb); + Vector pa_pc = Vector.LessThanOrEqual(pa, pc); + Vector pb_pc = Vector.LessThanOrEqual(pb, pc); + + return Vector.ConditionalSelect( + condition: Vector.BitwiseAnd(pa_pb, pa_pc), + left: left, + right: Vector.ConditionalSelect( + condition: pb_pc, + left: above, + right: upperLeft)); + } + } +} diff --git a/ImageSharp/Formats/Png/Filters/SubFilter.cs b/ImageSharp/Formats/Png/Filters/SubFilter.cs new file mode 100644 index 0000000..1d3e13b --- /dev/null +++ b/ImageSharp/Formats/Png/Filters/SubFilter.cs @@ -0,0 +1,187 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.Formats.Png.Filters { + /// + /// The Sub filter transmits the difference between each byte and the value of the corresponding byte + /// of the prior pixel. + /// + /// + internal static class SubFilter + { + /// + /// Decodes a scanline, which was filtered with the sub filter. + /// + /// The scanline to decode. + /// The bytes per pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Decode(Span scanline, int bytesPerPixel) + { + // The Sub filter predicts each pixel as the previous pixel. + if (Sse2.IsSupported && bytesPerPixel is 4) + { + DecodeSse2(scanline); + } + else if (AdvSimd.IsSupported && bytesPerPixel is 4) + { + DecodeArm(scanline); + } + else + { + DecodeScalar(scanline, (uint)bytesPerPixel); + } + } + + private static void DecodeSse2(Span scanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + + Vector128 d = Vector128.Zero; + + int rb = scanline.Length; + nuint offset = 1; + while (rb >= 4) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + Vector128 a = d; + d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(); + + d = Sse2.Add(d, a); + + Unsafe.As(ref scanRef) = Sse2.ConvertToInt32(d.AsInt32()); + + rb -= 4; + offset += 4; + } + } + + public static void DecodeArm(Span scanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + + Vector64 d = Vector64.Zero; + + int rb = scanline.Length; + nuint offset = 1; + const int bytesPerBatch = 4; + while (rb >= bytesPerBatch) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + Vector64 a = d; + d = Vector64.CreateScalar(Unsafe.As(ref scanRef)).AsByte(); + + d = AdvSimd.Add(d, a); + + Unsafe.As(ref scanRef) = d.AsInt32().ToScalar(); + + rb -= bytesPerBatch; + offset += bytesPerBatch; + } + } + + private static void DecodeScalar(Span scanline, nuint bytesPerPixel) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + + // Sub(x) + Raw(x-bpp) + nuint x = bytesPerPixel + 1; + Unsafe.Add(ref scanBaseRef, x); + for (; x < (uint)scanline.Length; ++x) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); + byte prev = Unsafe.Add(ref scanBaseRef, x - bytesPerPixel); + scan = (byte)(scan + prev); + } + } + + /// + /// Encodes a scanline with the sup filter applied. + /// + /// The scanline to encode. + /// The filtered scanline result. + /// The bytes per pixel. + /// The sum of the total variance of the filtered row. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Encode(ReadOnlySpan scanline, ReadOnlySpan result, int bytesPerPixel, out int sum) + { + DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); + + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); + sum = 0; + + // Sub(x) = Raw(x) - Raw(x-bpp) + resultBaseRef = (byte)FilterType.Sub; + + nuint x = 0; + for (; x < (uint)bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) + { + byte scan = Unsafe.Add(ref scanBaseRef, x); + ++x; + ref byte res = ref Unsafe.Add(ref resultBaseRef, x); + res = scan; + sum += Numerics.Abs(unchecked((sbyte)res)); + } + + if (Avx2.IsSupported) + { + Vector256 zero = Vector256.Zero; + Vector256 sumAccumulator = Vector256.Zero; + + for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= (scanline.Length - Vector256.Count); xLeft += (uint)Vector256.Count) + { + Vector256 scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); + Vector256 prev = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); + + Vector256 res = Avx2.Subtract(scan, prev); + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type + x += (uint)Vector256.Count; + + sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32()); + } + + sum += Numerics.EvenReduceSum(sumAccumulator); + } + else + if (Vector.IsHardwareAccelerated) + { + Vector sumAccumulator = Vector.Zero; + + for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= (scanline.Length - Vector.Count); xLeft += (uint)Vector.Count) + { + Vector scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); + Vector prev = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); + + Vector res = scan - prev; + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type + x += (uint)Vector.Count; + + Numerics.Accumulate(ref sumAccumulator, Vector.AsVectorByte(Vector.Abs(Vector.AsVectorSByte(res)))); + } + + for (int i = 0; i < Vector.Count; i++) + { + sum += (int)sumAccumulator[i]; + } + } + + for (nuint xLeft = x - (uint)bytesPerPixel; x < (uint)scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) + { + byte scan = Unsafe.Add(ref scanBaseRef, x); + byte prev = Unsafe.Add(ref scanBaseRef, xLeft); + ++x; + ref byte res = ref Unsafe.Add(ref resultBaseRef, x); + res = (byte)(scan - prev); + sum += Numerics.Abs(unchecked((sbyte)res)); + } + } + } +} diff --git a/ImageSharp/Formats/Png/Filters/UpFilter.cs b/ImageSharp/Formats/Png/Filters/UpFilter.cs new file mode 100644 index 0000000..23b4003 --- /dev/null +++ b/ImageSharp/Formats/Png/Filters/UpFilter.cs @@ -0,0 +1,229 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.Formats.Png.Filters { + /// + /// The Up filter is just like the Sub filter except that the pixel immediately above the current pixel, + /// rather than just to its left, is used as the predictor. + /// + /// + internal static class UpFilter + { + /// + /// Decodes a scanline, which was filtered with the up filter. + /// + /// The scanline to decode + /// The previous scanline. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Decode(Span scanline, Span previousScanline) + { + DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); + + if (Avx2.IsSupported) + { + DecodeAvx2(scanline, previousScanline); + } + else if (Sse2.IsSupported) + { + DecodeSse2(scanline, previousScanline); + } + else if (AdvSimd.IsSupported) + { + DecodeArm(scanline, previousScanline); + } + else + { + DecodeScalar(scanline, previousScanline); + } + } + + private static void DecodeAvx2(Span scanline, Span previousScanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + // Up(x) + Prior(x) + int rb = scanline.Length; + nuint offset = 1; + while (rb >= Vector256.Count) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + Vector256 prior = Unsafe.As>(ref scanRef); + Vector256 up = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, offset)); + + Unsafe.As>(ref scanRef) = Avx2.Add(up, prior); + + offset += (uint)Vector256.Count; + rb -= Vector256.Count; + } + + // Handle left over. + for (nuint i = offset; i < (uint)scanline.Length; i++) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, offset); + byte above = Unsafe.Add(ref prevBaseRef, offset); + scan = (byte)(scan + above); + offset++; + } + } + + private static void DecodeSse2(Span scanline, Span previousScanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + // Up(x) + Prior(x) + int rb = scanline.Length; + nuint offset = 1; + while (rb >= Vector128.Count) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + Vector128 prior = Unsafe.As>(ref scanRef); + Vector128 up = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, offset)); + + Unsafe.As>(ref scanRef) = Sse2.Add(up, prior); + + offset += (uint)Vector128.Count; + rb -= Vector128.Count; + } + + // Handle left over. + for (nuint i = offset; i < (uint)scanline.Length; i++) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, offset); + byte above = Unsafe.Add(ref prevBaseRef, offset); + scan = (byte)(scan + above); + offset++; + } + } + + private static void DecodeArm(Span scanline, Span previousScanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + // Up(x) + Prior(x) + int rb = scanline.Length; + nuint offset = 1; + const int bytesPerBatch = 16; + while (rb >= bytesPerBatch) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + Vector128 prior = Unsafe.As>(ref scanRef); + Vector128 up = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, offset)); + + Unsafe.As>(ref scanRef) = AdvSimd.Add(prior, up); + + offset += bytesPerBatch; + rb -= bytesPerBatch; + } + + // Handle left over. + for (nuint i = offset; i < (uint)scanline.Length; i++) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, offset); + byte above = Unsafe.Add(ref prevBaseRef, offset); + scan = (byte)(scan + above); + offset++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeScalar(Span scanline, Span previousScanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + // Up(x) + Prior(x) + for (nuint x = 1; x < (uint)scanline.Length; x++) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); + byte above = Unsafe.Add(ref prevBaseRef, x); + scan = (byte)(scan + above); + } + } + + /// + /// Encodes a scanline with the up filter applied. + /// + /// The scanline to encode. + /// The previous scanline. + /// The filtered scanline result. + /// The sum of the total variance of the filtered row. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, out int sum) + { + DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); + DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); + + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); + sum = 0; + + // Up(x) = Raw(x) - Prior(x) + resultBaseRef = (byte)FilterType.Up; + + nuint x = 0; + + if (Avx2.IsSupported) + { + Vector256 zero = Vector256.Zero; + Vector256 sumAccumulator = Vector256.Zero; + + for (; (int)x <= scanline.Length - Vector256.Count;) + { + Vector256 scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); + Vector256 above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); + + Vector256 res = Avx2.Subtract(scan, above); + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type + x += (uint)Vector256.Count; + + sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32()); + } + + sum += Numerics.EvenReduceSum(sumAccumulator); + } + else if (Vector.IsHardwareAccelerated) + { + Vector sumAccumulator = Vector.Zero; + + for (; (int)x <= scanline.Length - Vector.Count;) + { + Vector scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); + Vector above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); + + Vector res = scan - above; + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type + x += (uint)Vector.Count; + + Numerics.Accumulate(ref sumAccumulator, Vector.AsVectorByte(Vector.Abs(Vector.AsVectorSByte(res)))); + } + + for (int i = 0; i < Vector.Count; i++) + { + sum += (int)sumAccumulator[i]; + } + } + + for (; x < (uint)scanline.Length; /* Note: ++x happens in the body to avoid one add operation */) + { + byte scan = Unsafe.Add(ref scanBaseRef, x); + byte above = Unsafe.Add(ref prevBaseRef, x); + ++x; + ref byte res = ref Unsafe.Add(ref resultBaseRef, x); + res = (byte)(scan - above); + sum += Numerics.Abs(unchecked((sbyte)res)); + } + } + } +} diff --git a/ImageSharp/Formats/Png/PngBitDepth.cs b/ImageSharp/Formats/Png/PngBitDepth.cs new file mode 100644 index 0000000..6edadd0 --- /dev/null +++ b/ImageSharp/Formats/Png/PngBitDepth.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// Note the value assignment, This will allow us to add 1, 2, and 4 bit encoding when we support it. +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Provides enumeration for the available PNG bit depths. + /// + public enum PngBitDepth : byte + { + /// + /// 1 bit per sample or per palette index (not per pixel). + /// + Bit1 = 1, + + /// + /// 2 bits per sample or per palette index (not per pixel). + /// + Bit2 = 2, + + /// + /// 4 bits per sample or per palette index (not per pixel). + /// + Bit4 = 4, + + /// + /// 8 bits per sample or per palette index (not per pixel). + /// + Bit8 = 8, + + /// + /// 16 bits per sample or per palette index (not per pixel). + /// + Bit16 = 16 + } +} diff --git a/ImageSharp/Formats/Png/PngChunk.cs b/ImageSharp/Formats/Png/PngChunk.cs new file mode 100644 index 0000000..5837661 --- /dev/null +++ b/ImageSharp/Formats/Png/PngChunk.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System.Buffers; + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Stores header information about a chunk. + /// + internal readonly struct PngChunk + { + public PngChunk(int length, PngChunkType type, IMemoryOwner data = null) + { + this.Length = length; + this.Type = type; + this.Data = data; + } + + /// + /// Gets the length. + /// An unsigned integer giving the number of bytes in the chunk's + /// data field. The length counts only the data field, not itself, + /// the chunk type code, or the CRC. Zero is a valid length + /// + public int Length { get; } + + /// + /// Gets the chunk type. + /// The value is the equal to the UInt32BigEndian encoding of its 4 ASCII characters. + /// + public PngChunkType Type { get; } + + /// + /// Gets the data bytes appropriate to the chunk type, if any. + /// This field can be of zero length or null. + /// + public IMemoryOwner Data { get; } + + /// + /// Gets a value indicating whether the given chunk is critical to decoding + /// + /// The segment handling behavior. + public bool IsCritical(SegmentIntegrityHandling handling) + => this.Type switch + { + PngChunkType.Header => true, + PngChunkType.Palette => true, + PngChunkType.Data or PngChunkType.FrameData => handling < SegmentIntegrityHandling.IgnoreImageData, + _ => handling < SegmentIntegrityHandling.IgnoreAncillary, + }; + } +} diff --git a/ImageSharp/Formats/Png/PngChunkFilter.cs b/ImageSharp/Formats/Png/PngChunkFilter.cs new file mode 100644 index 0000000..24db346 --- /dev/null +++ b/ImageSharp/Formats/Png/PngChunkFilter.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Provides enumeration of available PNG optimization methods. + /// + [Flags] + public enum PngChunkFilter + { + /// + /// With the None filter, all chunks will be written. + /// + None = 0, + + /// + /// Excludes the physical dimension information chunk from encoding. + /// + ExcludePhysicalChunk = 1 << 0, + + /// + /// Excludes the gamma information chunk from encoding. + /// + ExcludeGammaChunk = 1 << 1, + + /// + /// Excludes the eXIf chunk from encoding. + /// + ExcludeExifChunk = 1 << 2, + + /// + /// Excludes the tTXt, iTXt or zTXt chunk from encoding. + /// + ExcludeTextChunks = 1 << 3, + + /// + /// All ancillary chunks will be excluded. + /// + ExcludeAll = ~None + } +} diff --git a/ImageSharp/Formats/Png/PngChunkType.cs b/ImageSharp/Formats/Png/PngChunkType.cs new file mode 100644 index 0000000..1948af0 --- /dev/null +++ b/ImageSharp/Formats/Png/PngChunkType.cs @@ -0,0 +1,176 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Contains a list of chunk types. + /// + internal enum PngChunkType : uint + { + /// + /// This chunk contains the actual image data. The image can contains more + /// than one chunk of this type. All chunks together are the whole image. + /// + /// IDAT (Multiple) + Data = 0x49444154U, + + /// + /// This chunk must appear last. It marks the end of the PNG data stream. + /// The chunk's data field is empty. + /// + /// IEND (Single) + End = 0x49454E44U, + + /// + /// The first chunk in a png file. Can only exists once. Contains + /// common information like the width and the height of the image or + /// the used compression method. + /// + /// IHDR (Single) + Header = 0x49484452U, + + /// + /// The PLTE chunk contains from 1 to 256 palette entries, each a three byte + /// series in the RGB format. + /// + /// PLTE (Single) + Palette = 0x504C5445U, + + /// + /// The eXIf data chunk which contains the Exif profile. + /// + /// eXIF (Single) + Exif = 0x65584966U, + + /// + /// This chunk specifies the relationship between the image samples and the desired + /// display output intensity. + /// + /// gAMA (Single) + Gamma = 0x67414D41U, + + /// + /// This chunk specifies the intended pixel size or aspect ratio for display of the image. + /// + /// pHYs (Single) + Physical = 0x70485973U, + + /// + /// Textual information that the encoder wishes to record with the image can be stored in + /// tEXt chunks. Each tEXt chunk contains a keyword and a text string. + /// + /// tEXT (Multiple) + Text = 0x74455874U, + + /// + /// Textual information that the encoder wishes to record with the image. The zTXt and tEXt chunks are semantically equivalent, + /// but the zTXt chunk is recommended for storing large blocks of text. Each zTXt chunk contains a (uncompressed) keyword and + /// a compressed text string. + /// + /// zTXt (Multiple) + CompressedText = 0x7A545874U, + + /// + /// This chunk contains International textual data. It contains a keyword, an optional language tag, an optional translated keyword + /// and the actual text string, which can be compressed or uncompressed. + /// + /// iTXt (Multiple) + InternationalText = 0x69545874U, + + /// + /// This chunk specifies that the image uses simple transparency: + /// either alpha values associated with palette entries (for indexed-color images) + /// or a single transparent color (for grayscale and true color images). + /// + /// tRNS (Single) + Transparency = 0x74524E53U, + + /// + /// This chunk gives the time of the last image modification (not the time of initial image creation). + /// + /// tIME (Single) + Time = 0x74494d45, + + /// + /// This chunk specifies a default background colour to present the image against. + /// If there is any other preferred background, either user-specified or part of a larger page (as in a browser), + /// the bKGD chunk should be ignored. + /// + /// bKGD (Single) + Background = 0x624b4744, + + /// + /// This chunk contains a embedded color profile. If the iCCP chunk is present, + /// the image samples conform to the colour space represented by the embedded ICC profile as defined by the International Color Consortium. + /// + /// iCCP (Single) + EmbeddedColorProfile = 0x69434350, + + /// + /// This chunk defines the original number of significant bits (which can be less than or equal to the sample depth). + /// This allows PNG decoders to recover the original data losslessly even if the data had a sample depth not directly supported by PNG. + /// + /// sBIT (Single) + SignificantBits = 0x73424954, + + /// + /// If the this chunk is present, the image samples conform to the sRGB colour space [IEC 61966-2-1] and should be displayed + /// using the specified rendering intent defined by the International Color Consortium. + /// + /// sRGB (Single) + StandardRgbColourSpace = 0x73524742, + + /// + /// This chunk gives the approximate usage frequency of each colour in the palette. + /// + /// hIST (Single) + Histogram = 0x68495354, + + /// + /// This chunk contains the suggested palette. + /// + /// sPLT (Single) + SuggestedPalette = 0x73504c54, + + /// + /// This chunk may be used to specify the 1931 CIE x,y chromaticities of the red, + /// green, and blue display primaries used in the image, and the referenced white point. + /// + /// cHRM (Single) + Chroma = 0x6348524d, + + /// + /// If this chunk is present, it specifies the color space, transfer function, matrix coefficients of the image + /// using the code points specified in [ITU-T-H.273] + /// + Cicp = 0x63494350, + + /// + /// This chunk is an ancillary chunk as defined in the PNG Specification. + /// It must appear before the first IDAT chunk within a valid PNG stream. + /// + /// acTL (Single, APNG) + AnimationControl = 0x6163544cU, + + /// + /// This chunk is an ancillary chunk as defined in the PNG Specification. + /// It must appear before the IDAT or fdAT chunks of the frame to which it applies. + /// + /// fcTL (Multiple, APNG) + FrameControl = 0x6663544cU, + + /// + /// This chunk has the same purpose as an IDAT chunk. + /// It has the same structure as an IDAT chunk, except preceded by a sequence number. + /// + /// fdAT (Multiple, APNG) + FrameData = 0x66644154U, + + /// + /// Malformed chunk named CgBI produced by apple, which is not conform to the specification. + /// Related issue is here https://github.com/SixLabors/ImageSharp/issues/410 + /// + /// CgBI + ProprietaryApple = 0x43674249 + } +} diff --git a/ImageSharp/Formats/Png/PngColorType.cs b/ImageSharp/Formats/Png/PngColorType.cs new file mode 100644 index 0000000..17e9fb0 --- /dev/null +++ b/ImageSharp/Formats/Png/PngColorType.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Provides enumeration of available PNG color types. + /// + public enum PngColorType : byte + { + /// + /// Each pixel is a grayscale sample. + /// + Grayscale = 0, + + /// + /// Each pixel is an R,G,B triple. + /// + Rgb = 2, + + /// + /// Each pixel is a palette index; a PLTE chunk must appear. + /// + Palette = 3, + + /// + /// Each pixel is a grayscale sample, followed by an alpha sample. + /// + GrayscaleWithAlpha = 4, + + /// + /// Each pixel is an R,G,B triple, followed by an alpha sample. + /// + RgbWithAlpha = 6 + } +} diff --git a/ImageSharp/Formats/Png/PngCompressionLevel.cs b/ImageSharp/Formats/Png/PngCompressionLevel.cs new file mode 100644 index 0000000..4b84b9b --- /dev/null +++ b/ImageSharp/Formats/Png/PngCompressionLevel.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.ComponentModel; + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Provides enumeration of available PNG compression levels. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public enum PngCompressionLevel + { + /// + /// Level 0. Equivalent to . + /// + Level0 = 0, + + /// + /// No compression. Equivalent to . + /// + NoCompression = Level0, + + /// + /// Level 1. Equivalent to . + /// + Level1 = 1, + + /// + /// Best speed compression level. + /// + BestSpeed = Level1, + + /// + /// Level 2. + /// + Level2 = 2, + + /// + /// Level 3. + /// + Level3 = 3, + + /// + /// Level 4. + /// + Level4 = 4, + + /// + /// Level 5. + /// + Level5 = 5, + + /// + /// Level 6. Equivalent to . + /// + Level6 = 6, + + /// + /// The default compression level. Equivalent to . + /// + DefaultCompression = Level6, + + /// + /// Level 7. + /// + Level7 = 7, + + /// + /// Level 8. + /// + Level8 = 8, + + /// + /// Level 9. Equivalent to . + /// + Level9 = 9, + + /// + /// Best compression level. Equivalent to . + /// + BestCompression = Level9, + } +} diff --git a/ImageSharp/Formats/Png/PngConfigurationModule.cs b/ImageSharp/Formats/Png/PngConfigurationModule.cs new file mode 100644 index 0000000..af7116a --- /dev/null +++ b/ImageSharp/Formats/Png/PngConfigurationModule.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Registers the image encoders, decoders and mime type detectors for the png format. + /// + public sealed class PngConfigurationModule : IImageFormatConfigurationModule + { + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetEncoder(PngFormat.Instance, new PngEncoder()); + configuration.ImageFormatsManager.SetDecoder(PngFormat.Instance, PngDecoder.Instance); + configuration.ImageFormatsManager.AddImageFormatDetector(new PngImageFormatDetector()); + } + } +} diff --git a/ImageSharp/Formats/Png/PngConstants.cs b/ImageSharp/Formats/Png/PngConstants.cs new file mode 100644 index 0000000..75d894b --- /dev/null +++ b/ImageSharp/Formats/Png/PngConstants.cs @@ -0,0 +1,147 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Defines Png constants defined in the specification. + /// + internal static class PngConstants + { + /// + /// The character encoding to use when reading and writing textual data keywords and text - (Latin-1 ISO-8859-1). + /// + public static readonly Encoding Encoding = Encoding.GetEncoding("ISO-8859-1"); + + /// + /// The character encoding to use when reading and writing language tags within iTXt chunks - (ASCII 7bit). + /// + public static readonly Encoding LanguageEncoding = Encoding.ASCII; + + /// + /// The character encoding to use when reading and writing translated textual data keywords and text - (UTF8). + /// + public static readonly Encoding TranslatedEncoding = Encoding.UTF8; + + /// + /// The list of mimetypes that equate to a Png. + /// + public static readonly IEnumerable MimeTypes = ["image/png", "image/apng"]; + + /// + /// The list of file extensions that equate to a Png. + /// + public static readonly IEnumerable FileExtensions = ["png", "apng"]; + + /// + /// The header bytes as a big-endian coded ulong. + /// + public const ulong HeaderValue = 0x89504E470D0A1A0AUL; + + /// + /// The dictionary of available color types. + /// + public static readonly Dictionary ColorTypes = new() + { + [PngColorType.Grayscale] = [1, 2, 4, 8, 16], + [PngColorType.Rgb] = [8, 16], + [PngColorType.Palette] = [1, 2, 4, 8], + [PngColorType.GrayscaleWithAlpha] = [8, 16], + [PngColorType.RgbWithAlpha] = [8, 16] + }; + + /// + /// The maximum length of keyword in a text chunk is 79 bytes. + /// + public const int MaxTextKeywordLength = 79; + + /// + /// The minimum length of a keyword in a text chunk is 1 byte. + /// + public const int MinTextKeywordLength = 1; + + /// + /// Specifies the keyword used to identify the Exif raw profile in image metadata. + /// + public const string ExifRawProfileKeyword = "Raw profile type exif"; + + /// + /// Specifies the profile keyword used to identify raw IPTC metadata within image files. + /// + public const string IptcRawProfileKeyword = "Raw profile type iptc"; + + /// + /// The IPTC resource id in Photoshop IRB. 0x0404 (big endian). + /// + public const ushort AdobeIptcResourceId = 0x0404; + + /// + /// Gets the header bytes identifying a Png. + /// + public static ReadOnlySpan HeaderBytes => + [ + 0x89, // Set the high bit. + 0x50, // P + 0x4E, // N + 0x47, // G + 0x0D, // Line ending CRLF + 0x0A, // Line ending CRLF + 0x1A, // EOF + 0x0A // LF + ]; + + /// + /// Gets the keyword of the XMP metadata, encoded in an iTXT chunk. + /// + public static ReadOnlySpan XmpKeyword => + [ + (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' + ]; + + /// + /// Gets the ASCII bytes for the "Photoshop 3.0" identifier used in some PNG metadata payloads. + /// This value is null-terminated. + /// + public static ReadOnlySpan AdobePhotoshop30 => + [ + (byte)'P', + (byte)'h', + (byte)'o', + (byte)'t', + (byte)'o', + (byte)'s', + (byte)'h', + (byte)'o', + (byte)'p', + (byte)' ', + (byte)'3', + (byte)'.', + (byte)'0', + 0 + ]; + + /// + /// Gets the ASCII bytes for the "8BIM" signature used in Photoshop resources. + /// + public static ReadOnlySpan EightBim => [(byte)'8', (byte)'B', (byte)'I', (byte)'M']; + } +} diff --git a/ImageSharp/Formats/Png/PngDecoder.cs b/ImageSharp/Formats/Png/PngDecoder.cs new file mode 100644 index 0000000..6094c83 --- /dev/null +++ b/ImageSharp/Formats/Png/PngDecoder.cs @@ -0,0 +1,107 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Decoder for generating an image out of a png encoded stream. + /// + public sealed class PngDecoder : SpecializedImageDecoder + { + private PngDecoder() + { + } + + /// + /// Gets the shared instance. + /// + public static PngDecoder Instance { get; } = new(); + + /// + protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + return new PngDecoderCore(new PngDecoderOptions { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken); + } + + /// + protected override Image Decode(PngDecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + PngDecoderCore decoder = new(options); + Image image = decoder.Decode(options.GeneralOptions.Configuration, stream, cancellationToken); + + ScaleToTargetSize(options.GeneralOptions, image); + + return image; + } + + /// + protected override Image Decode(PngDecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + PngDecoderCore decoder = new(options, true); + ImageInfo info = decoder.Identify(options.GeneralOptions.Configuration, stream, cancellationToken); + stream.Position = 0; + + PngMetadata meta = info.Metadata.GetPngMetadata(); + PngColorType color = meta.ColorType; + PngBitDepth bits = meta.BitDepth; + + switch (color) + { + case PngColorType.Grayscale: + if (bits == PngBitDepth.Bit16) + { + return !meta.TransparentColor.HasValue + ? this.Decode(options, stream, cancellationToken) + : this.Decode(options, stream, cancellationToken); + } + + return !meta.TransparentColor.HasValue + ? this.Decode(options, stream, cancellationToken) + : this.Decode(options, stream, cancellationToken); + + case PngColorType.Rgb: + if (bits == PngBitDepth.Bit16) + { + return !meta.TransparentColor.HasValue + ? this.Decode(options, stream, cancellationToken) + : this.Decode(options, stream, cancellationToken); + } + + return !meta.TransparentColor.HasValue + ? this.Decode(options, stream, cancellationToken) + : this.Decode(options, stream, cancellationToken); + + case PngColorType.Palette: + return this.Decode(options, stream, cancellationToken); + + case PngColorType.GrayscaleWithAlpha: + return (bits == PngBitDepth.Bit16) + ? this.Decode(options, stream, cancellationToken) + : this.Decode(options, stream, cancellationToken); + + case PngColorType.RgbWithAlpha: + return (bits == PngBitDepth.Bit16) + ? this.Decode(options, stream, cancellationToken) + : this.Decode(options, stream, cancellationToken); + + default: + return this.Decode(options, stream, cancellationToken); + } + } + + /// + protected override PngDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new() { GeneralOptions = options }; + } +} diff --git a/ImageSharp/Formats/Png/PngDecoderCore.cs b/ImageSharp/Formats/Png/PngDecoderCore.cs new file mode 100644 index 0000000..f3031ef --- /dev/null +++ b/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -0,0 +1,2799 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.IO.Hashing; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using System.Text; +using System.Threading; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Png.Chunks; +using SixLabors.ImageSharp.Formats.Png.Filters; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Memory.Internals; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +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.Png { + /// + /// Performs the png decoding operation. + /// + internal sealed class PngDecoderCore : ImageDecoderCore + { + /// + /// The general decoder options. + /// + private readonly Configuration configuration; + + /// + /// Whether the metadata should be ignored when the image is being decoded. + /// + private readonly uint maxFrames; + + /// + /// Whether the metadata should be ignored when the image is being decoded. + /// + private readonly bool skipMetadata; + + /// + /// Whether to read the IHDR and tRNS chunks only. + /// + private readonly bool colorMetadataOnly; + + /// + /// Used the manage memory allocations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The stream to decode from. + /// + private BufferedReadStream currentStream = null!; + + /// + /// The png header. + /// + private PngHeader header; + + /// + /// The png animation control. + /// + private AnimationControl animationControl; + + /// + /// The number of bytes per pixel. + /// + private int bytesPerPixel; + + /// + /// The number of bytes per sample. + /// + private int bytesPerSample; + + /// + /// The number of bytes per scanline. + /// + private int bytesPerScanline; + + /// + /// The palette containing color information for indexed png's. + /// + private byte[] palette = null!; + + /// + /// The palette containing alpha channel color information for indexed png's. + /// + private byte[] paletteAlpha = null!; + + /// + /// Previous scanline processed. + /// + private IMemoryOwner previousScanline = null!; + + /// + /// The current scanline that is being processed. + /// + private IMemoryOwner scanline = null!; + + /// + /// Gets or sets the png color type. + /// + private PngColorType pngColorType; + + /// + /// The next chunk of data to return. + /// + private PngChunk? nextChunk; + + /// + /// How to handle CRC errors. + /// + private readonly SegmentIntegrityHandling segmentIntegrityHandling; + + /// + /// A reusable Crc32 hashing instance. + /// + private readonly Crc32 crc32 = new(); + + /// + /// The maximum memory in bytes that a zTXt, sPLT, iTXt, iCCP, or unknown chunk can occupy when decompressed. + /// + private readonly int maxUncompressedLength; + + /// + /// A value indicating whether the image data has been read. + /// + private bool hasImageData; + + /// + /// Whether this is an Apple CgBI PNG. CgBI files store IDATs as raw DEFLATE + /// (no zlib header/Adler-32) and pixels as premultiplied BGRA, so they need + /// extra inversion steps to round-trip back to standard PNG semantics. + /// + private bool isCgbi; + + /// + /// Initializes a new instance of the class. + /// + /// The decoder options. + public PngDecoderCore(PngDecoderOptions options) + : base(options.GeneralOptions) + { + this.configuration = options.GeneralOptions.Configuration; + this.maxFrames = options.GeneralOptions.MaxFrames; + this.skipMetadata = options.GeneralOptions.SkipMetadata; + this.memoryAllocator = this.configuration.MemoryAllocator; + this.segmentIntegrityHandling = options.GeneralOptions.SegmentIntegrityHandling; + this.maxUncompressedLength = options.MaxUncompressedAncillaryChunkSizeBytes; + } + + internal PngDecoderCore(PngDecoderOptions options, bool colorMetadataOnly) + : base(options.GeneralOptions) + { + this.colorMetadataOnly = colorMetadataOnly; + this.maxFrames = options.GeneralOptions.MaxFrames; + this.skipMetadata = true; + this.configuration = options.GeneralOptions.Configuration; + this.memoryAllocator = this.configuration.MemoryAllocator; + this.segmentIntegrityHandling = options.GeneralOptions.SegmentIntegrityHandling; + this.maxUncompressedLength = options.MaxUncompressedAncillaryChunkSizeBytes; + } + + /// + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + { + uint frameCount = 0; + ImageMetadata metadata = new(); + PngMetadata pngMetadata = metadata.GetPngMetadata(); + this.currentStream = stream; + this.currentStream.Skip(8); + Image? image = null; + FrameControl? previousFrameControl = null; + FrameControl? currentFrameControl = null; + ImageFrame? previousFrame = null; + ImageFrame? currentFrame = null; + Span buffer = stackalloc byte[20]; + + try + { + while (this.TryReadChunk(buffer, out PngChunk chunk)) + { + try + { + switch (chunk.Type) + { + case PngChunkType.Header: + if (!Equals(this.header, default(PngHeader))) + { + PngThrowHelper.ThrowInvalidHeader(); + } + + this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan()); + break; + case PngChunkType.AnimationControl: + this.ReadAnimationControlChunk(pngMetadata, chunk.Data.GetSpan()); + break; + case PngChunkType.Physical: + ReadPhysicalChunk(metadata, chunk.Data.GetSpan()); + break; + case PngChunkType.Gamma: + ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); + break; + case PngChunkType.Cicp: + ReadCicpChunk(metadata, chunk.Data.GetSpan()); + break; + case PngChunkType.FrameControl: + frameCount++; + currentFrame = null; + currentFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); + break; + case PngChunkType.FrameData: + { + if (frameCount > this.maxFrames) + { + goto EOF; + } + + if (image is null) + { + PngThrowHelper.ThrowMissingDefaultData(); + } + + if (currentFrameControl is null) + { + PngThrowHelper.ThrowMissingFrameControl(); + } + + this.InitializeFrame(previousFrameControl, currentFrameControl.Value, image, previousFrame, out currentFrame); + + this.currentStream.Position += 4; + this.ReadScanlines( + chunk.Length - 4, + currentFrame, + pngMetadata, + this.ReadNextFrameDataChunk, + currentFrameControl.Value, + cancellationToken); + + // if current frame dispose is restore to previous, then from future frame's perspective, it never happened + if (currentFrameControl.Value.DisposalMode != FrameDisposalMode.RestoreToPrevious) + { + previousFrame = currentFrame; + previousFrameControl = currentFrameControl; + } + + break; + } + + case PngChunkType.Data: + { + pngMetadata.AnimateRootFrame = currentFrameControl != null; + currentFrameControl ??= new FrameControl((uint)this.header.Width, (uint)this.header.Height); + if (image is null) + { + this.InitializeImage(metadata, currentFrameControl.Value, out image); + + // Both PLTE and tRNS chunks, if present, have been read at this point as per spec. + AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); + } + + this.ReadScanlines( + chunk.Length, + image.Frames.RootFrame, + pngMetadata, + this.ReadNextDataChunk, + currentFrameControl.Value, + cancellationToken); + if (pngMetadata.AnimateRootFrame) + { + previousFrame = currentFrame; + previousFrameControl = currentFrameControl; + } + + if (frameCount > this.maxFrames) + { + goto EOF; + } + + break; + } + + case PngChunkType.Palette: + this.palette = chunk.Data.GetSpan().ToArray(); + break; + case PngChunkType.Transparency: + this.paletteAlpha = chunk.Data.GetSpan().ToArray(); + this.AssignTransparentMarkers(this.paletteAlpha, pngMetadata); + break; + case PngChunkType.Text: + this.ReadTextChunk(metadata, pngMetadata, chunk.Data.GetSpan()); + break; + case PngChunkType.CompressedText: + this.ReadCompressedTextChunk(metadata, pngMetadata, chunk.Data.GetSpan()); + break; + case PngChunkType.InternationalText: + this.ReadInternationalTextChunk(metadata, chunk.Data.GetSpan()); + break; + case PngChunkType.Exif: + if (!this.skipMetadata) + { + byte[] exifData = new byte[chunk.Length]; + chunk.Data.GetSpan().CopyTo(exifData); + MergeOrSetExifProfile(metadata, new ExifProfile(exifData), replaceExistingKeys: true); + } + + break; + case PngChunkType.EmbeddedColorProfile: + this.ReadColorProfileChunk(metadata, chunk.Data.GetSpan()); + break; + case PngChunkType.End: + goto EOF; + case PngChunkType.ProprietaryApple: + this.isCgbi = true; + break; + } + } + finally + { + chunk.Data?.Dispose(); // Data is rented in ReadChunkData() + } + } + + EOF: + if (image is null) + { + PngThrowHelper.ThrowNoData(); + } + + _ = this.TryConvertIccProfile(image); + return image; + } + catch + { + image?.Dispose(); + throw; + } + finally + { + this.scanline?.Dispose(); + this.previousScanline?.Dispose(); + this.nextChunk?.Data?.Dispose(); + } + } + + /// + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + uint frameCount = 0; + ImageMetadata metadata = new(); + List framesMetadata = []; + PngMetadata pngMetadata = metadata.GetPngMetadata(); + this.currentStream = stream; + FrameControl? currentFrameControl = null; + Span buffer = stackalloc byte[20]; + + this.currentStream.Skip(8); + + try + { + while (this.TryReadChunk(buffer, out PngChunk chunk)) + { + try + { + switch (chunk.Type) + { + case PngChunkType.Header: + this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan()); + break; + case PngChunkType.AnimationControl: + this.ReadAnimationControlChunk(pngMetadata, chunk.Data.GetSpan()); + break; + case PngChunkType.Physical: + if (this.colorMetadataOnly) + { + this.SkipChunkDataAndCrc(chunk); + break; + } + + ReadPhysicalChunk(metadata, chunk.Data.GetSpan()); + break; + case PngChunkType.Gamma: + if (this.colorMetadataOnly) + { + this.SkipChunkDataAndCrc(chunk); + break; + } + + ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); + break; + case PngChunkType.Cicp: + if (this.colorMetadataOnly) + { + this.SkipChunkDataAndCrc(chunk); + break; + } + + ReadCicpChunk(metadata, chunk.Data.GetSpan()); + break; + case PngChunkType.FrameControl: + ++frameCount; + if (frameCount > this.maxFrames) + { + break; + } + + currentFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); + + break; + case PngChunkType.FrameData: + if (frameCount > this.maxFrames) + { + // Must skip the chunk data even when we've hit maxFrames, because TryReadChunk + // restores the stream position to the start of the fdAT data after CRC validation. + this.SkipChunkDataAndCrc(chunk); + this.SkipRemainingFrameDataChunks(buffer); + break; + } + + if (this.colorMetadataOnly) + { + goto EOF; + } + + if (currentFrameControl is null) + { + PngThrowHelper.ThrowMissingFrameControl(); + } + + InitializeFrameMetadata(framesMetadata, currentFrameControl.Value); + + // Skip data for this and all remaining FrameData chunks belonging to the same frame + // (comparable to how Decode consumes them via ReadScanlines + ReadNextFrameDataChunk). + this.SkipChunkDataAndCrc(chunk); + this.SkipRemainingFrameDataChunks(buffer); + break; + case PngChunkType.Data: + + // Spec says tRNS must be before IDAT so safe to exit. + if (this.colorMetadataOnly) + { + goto EOF; + } + + pngMetadata.AnimateRootFrame = currentFrameControl != null; + currentFrameControl ??= new FrameControl((uint)this.header.Width, (uint)this.header.Height); + if (framesMetadata.Count == 0) + { + InitializeFrameMetadata(framesMetadata, currentFrameControl.Value); + + // Both PLTE and tRNS chunks, if present, have been read at this point as per spec. + AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); + } + + this.SkipChunkDataAndCrc(chunk); + break; + case PngChunkType.Palette: + this.palette = chunk.Data.GetSpan().ToArray(); + break; + + case PngChunkType.Transparency: + this.paletteAlpha = chunk.Data.GetSpan().ToArray(); + this.AssignTransparentMarkers(this.paletteAlpha, pngMetadata); + + // Spec says tRNS must be after PLTE so safe to exit. + if (this.colorMetadataOnly) + { + goto EOF; + } + + break; + case PngChunkType.Text: + if (this.colorMetadataOnly) + { + this.SkipChunkDataAndCrc(chunk); + break; + } + + this.ReadTextChunk(metadata, pngMetadata, chunk.Data.GetSpan()); + break; + case PngChunkType.CompressedText: + if (this.colorMetadataOnly) + { + this.SkipChunkDataAndCrc(chunk); + break; + } + + this.ReadCompressedTextChunk(metadata, pngMetadata, chunk.Data.GetSpan()); + break; + case PngChunkType.InternationalText: + if (this.colorMetadataOnly) + { + this.SkipChunkDataAndCrc(chunk); + break; + } + + this.ReadInternationalTextChunk(metadata, chunk.Data.GetSpan()); + break; + case PngChunkType.Exif: + if (this.colorMetadataOnly) + { + this.SkipChunkDataAndCrc(chunk); + break; + } + + if (!this.skipMetadata) + { + byte[] exifData = new byte[chunk.Length]; + chunk.Data.GetSpan().CopyTo(exifData); + MergeOrSetExifProfile(metadata, new ExifProfile(exifData), replaceExistingKeys: true); + } + + break; + case PngChunkType.End: + goto EOF; + + case PngChunkType.ProprietaryApple: + this.isCgbi = true; + break; + + default: + if (this.colorMetadataOnly) + { + this.SkipChunkDataAndCrc(chunk); + } + + break; + } + } + finally + { + chunk.Data?.Dispose(); // Data is rented in ReadChunkData() + } + } + + EOF: + if (this.header.Width == 0 && this.header.Height == 0) + { + PngThrowHelper.ThrowInvalidHeader(); + } + + return new ImageInfo(new Size(this.header.Width, this.header.Height), metadata, framesMetadata); + } + finally + { + this.scanline?.Dispose(); + this.previousScanline?.Dispose(); + } + } + + /// + /// Reads the least significant bits from the byte pair with the others set to 0. + /// + /// The source buffer. + /// THe offset. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte ReadByteLittleEndian(ReadOnlySpan buffer, int offset) + => (byte)(((buffer[offset] & 0xFF) << 16) | (buffer[offset + 1] & 0xFF)); + + /// + /// Attempts to convert a byte array to a new array where each value in the original array is represented by the + /// specified number of bits. + /// + /// The bytes to convert from. Cannot be empty. + /// The number of bytes per scanline. + /// The number of bits per value. + /// The new array. + /// The resulting array. + private bool TryScaleUpTo8BitArray(ReadOnlySpan source, int bytesPerScanline, int bits, [NotNullWhen(true)] out IMemoryOwner? buffer) + { + if (bits >= 8) + { + buffer = null; + return false; + } + + buffer = this.memoryAllocator.Allocate(bytesPerScanline * 8 / bits, AllocationOptions.Clean); + ref byte sourceRef = ref MemoryMarshal.GetReference(source); + ref byte resultRef = ref buffer.GetReference(); + int mask = 0xFF >> (8 - bits); + int resultOffset = 0; + + for (int i = 0; i < bytesPerScanline; i++) + { + byte b = Unsafe.Add(ref sourceRef, (uint)i); + for (int shift = 0; shift < 8; shift += bits) + { + int colorIndex = (b >> (8 - bits - shift)) & mask; + Unsafe.Add(ref resultRef, (uint)resultOffset) = (byte)colorIndex; + resultOffset++; + } + } + + return true; + } + + /// + /// Reads the data chunk containing physical dimension data. + /// + /// The metadata to read to. + /// The data containing physical data. + private static void ReadPhysicalChunk(ImageMetadata metadata, ReadOnlySpan data) + { + PngPhysical physicalChunk = PngPhysical.Parse(data); + + metadata.ResolutionUnits = physicalChunk.UnitSpecifier == byte.MinValue + ? PixelResolutionUnit.AspectRatio + : PixelResolutionUnit.PixelsPerMeter; + + metadata.HorizontalResolution = physicalChunk.XAxisPixelsPerUnit; + metadata.VerticalResolution = physicalChunk.YAxisPixelsPerUnit; + } + + /// + /// Reads the data chunk containing gamma data. + /// + /// The metadata to read to. + /// The data containing physical data. + private static void ReadGammaChunk(PngMetadata pngMetadata, ReadOnlySpan data) + { + if (data.Length < 4) + { + // Ignore invalid gamma chunks. + return; + } + + // For example, a gamma of 1/2.2 would be stored as 45455. + // The value is encoded as a 4-byte unsigned integer, representing gamma times 100000. + pngMetadata.Gamma = BinaryPrimitives.ReadUInt32BigEndian(data) * 1e-5F; + } + + /// + /// Initializes the image and various buffers needed for processing + /// + /// The type the pixels will be + /// The metadata information for the image + /// The frame control information for the frame + /// The image that we will populate + private void InitializeImage(ImageMetadata metadata, FrameControl frameControl, out Image image) + where TPixel : unmanaged, IPixel + { + image = new Image(this.configuration, this.header.Width, this.header.Height, metadata); + + PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngMetadata(); + frameMetadata.FromChunk(in frameControl); + + this.bytesPerPixel = this.CalculateBytesPerPixel(); + this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1; + this.bytesPerSample = 1; + if (this.header.BitDepth >= 8) + { + this.bytesPerSample = this.header.BitDepth / 8; + } + + this.previousScanline?.Dispose(); + this.scanline?.Dispose(); + this.previousScanline = this.memoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); + this.scanline = this.configuration.MemoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); + } + + /// + /// Initializes the image and various buffers needed for processing + /// + /// The type the pixels will be + /// The frame control information for the previous frame. + /// The frame control information for the current frame. + /// The image that we will populate + /// The previous frame. + /// The created frame + private void InitializeFrame( + FrameControl? previousFrameControl, + FrameControl currentFrameControl, + Image image, + ImageFrame? previousFrame, + out ImageFrame frame) + where TPixel : unmanaged, IPixel + { + // We create a clone of the previous frame and add it. + // We will overpaint the difference of pixels on the current frame to create a complete image. + // This ensures that we have enough pixel data to process without distortion. #2450 + frame = image.Frames.AddFrame(previousFrame ?? image.Frames.RootFrame); + + // If the first `fcTL` chunk uses a `dispose_op` of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND. + // So, if restoring to before first frame, clear entire area. Same if first frame (previousFrameControl null). + if (previousFrameControl == null || (previousFrame is null && previousFrameControl.Value.DisposalMode == FrameDisposalMode.RestoreToPrevious)) + { + Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(); + pixelRegion.Clear(); + } + else if (previousFrameControl.Value.DisposalMode == FrameDisposalMode.RestoreToBackground) + { + Rectangle restoreArea = previousFrameControl.Value.Bounds; + Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(restoreArea); + pixelRegion.Clear(); + } + + PngFrameMetadata frameMetadata = frame.Metadata.GetPngMetadata(); + frameMetadata.FromChunk(currentFrameControl); + + this.previousScanline?.Dispose(); + this.scanline?.Dispose(); + this.previousScanline = this.memoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); + this.scanline = this.configuration.MemoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); + } + + private static void InitializeFrameMetadata(List imageFrameMetadata, FrameControl currentFrameControl) + { + ImageFrameMetadata meta = new(); + PngFrameMetadata frameMetadata = meta.GetPngMetadata(); + frameMetadata.FromChunk(currentFrameControl); + imageFrameMetadata.Add(meta); + } + + /// + /// Calculates the correct number of bytes per pixel for the given color type. + /// + /// The + private int CalculateBytesPerPixel() + => this.pngColorType + switch + { + PngColorType.Grayscale => this.header.BitDepth == 16 ? 2 : 1, + PngColorType.GrayscaleWithAlpha => this.header.BitDepth == 16 ? 4 : 2, + PngColorType.Palette => 1, + PngColorType.Rgb => this.header.BitDepth == 16 ? 6 : 3, + _ => this.header.BitDepth == 16 ? 8 : 4, + }; + + /// + /// Calculates the scanline length. + /// + /// The width of the row. + /// + /// The representing the length. + /// + private int CalculateScanlineLength(int width) + { + int mod = this.header.BitDepth == 16 ? 16 : 8; + int scanlineLength = width * this.header.BitDepth * this.bytesPerPixel; + + int amount = scanlineLength % mod; + if (amount != 0) + { + scanlineLength += mod - amount; + } + + return scanlineLength / mod; + } + + /// + /// Reads the scanlines within the image. + /// + /// The pixel format. + /// The length of the chunk that containing the compressed scanline data. + /// The pixel data. + /// The png metadata + /// A delegate to get more data from the inner stream for . + /// The frame control + /// The cancellation token. + private void ReadScanlines( + int chunkLength, + ImageFrame image, + PngMetadata pngMetadata, + Func getData, + in FrameControl frameControl, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + using ZlibInflateStream inflateStream = new(this.currentStream, getData, noHeader: this.isCgbi); + if (!inflateStream.AllocateNewBytes(chunkLength, !this.hasImageData)) + { + return; + } + + DeflateStream dataStream = inflateStream.CompressedStream!; + + if (this.header.InterlaceMethod is PngInterlaceMode.Adam7) + { + this.DecodeInterlacedPixelData(frameControl, dataStream, image, pngMetadata, cancellationToken); + } + else + { + this.DecodePixelData(frameControl, dataStream, image, pngMetadata, cancellationToken); + } + } + + /// + /// Decodes the raw pixel data row by row + /// + /// The pixel format. + /// The frame control + /// The compressed pixel data stream. + /// The image frame to decode to. + /// The png metadata + /// The CancellationToken + private void DecodePixelData( + FrameControl frameControl, + DeflateStream compressedStream, + ImageFrame imageFrame, + PngMetadata pngMetadata, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner? blendMemory = frameControl.BlendMode == FrameBlendMode.Over + ? this.memoryAllocator.Allocate(imageFrame.Width, AllocationOptions.Clean) + : null; + + this.ExecuteImageDataSegmentAction(() => this.DecodePixelDataCore( + frameControl, + compressedStream, + imageFrame, + pngMetadata, + blendMemory, + cancellationToken)); + + this.hasImageData = true; + } + + /// + /// Decodes the raw pixel data row by row. + /// + /// The pixel format. + /// The frame control. + /// The compressed pixel data stream. + /// The image frame to decode to. + /// The png metadata. + /// The optional row blending buffer. + /// The cancellation token. + private void DecodePixelDataCore( + FrameControl frameControl, + DeflateStream compressedStream, + ImageFrame imageFrame, + PngMetadata pngMetadata, + IMemoryOwner? blendMemory, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + int currentRow = (int)frameControl.YOffset; + int currentRowBytesRead = 0; + int height = (int)frameControl.YMax; + + Span blendRowBuffer = blendMemory is null ? [] : blendMemory.Memory.Span; + + while (currentRow < height) + { + cancellationToken.ThrowIfCancellationRequested(); + int bytesPerFrameScanline = this.CalculateScanlineLength((int)frameControl.Width) + 1; + Span scanSpan = this.scanline.GetSpan()[..bytesPerFrameScanline]; + Span prevSpan = this.previousScanline.GetSpan()[..bytesPerFrameScanline]; + + while (currentRowBytesRead < bytesPerFrameScanline) + { + int bytesRead = compressedStream.Read(scanSpan, currentRowBytesRead, bytesPerFrameScanline - currentRowBytesRead); + if (bytesRead <= 0) + { + goto EXIT; + } + + currentRowBytesRead += bytesRead; + } + + currentRowBytesRead = 0; + + switch ((FilterType)scanSpan[0]) + { + case FilterType.None: + break; + + case FilterType.Sub: + SubFilter.Decode(scanSpan, this.bytesPerPixel); + break; + + case FilterType.Up: + UpFilter.Decode(scanSpan, prevSpan); + break; + + case FilterType.Average: + AverageFilter.Decode(scanSpan, prevSpan, this.bytesPerPixel); + break; + + case FilterType.Paeth: + PaethFilter.Decode(scanSpan, prevSpan, this.bytesPerPixel); + break; + + default: + PngThrowHelper.ThrowUnknownFilter(); + break; + } + + if (this.isCgbi) + { + this.ApplyCgbiTransform(scanSpan[1..], this.pngColorType); + } + + this.ProcessDefilteredScanline(frameControl, currentRow, scanSpan, imageFrame, pngMetadata, blendRowBuffer); + this.SwapScanlineBuffers(); + currentRow++; + } + + EXIT: + return; + } + + /// + /// Decodes the raw interlaced pixel data row by row + /// + /// The pixel format. + /// The frame control + /// The compressed pixel data stream. + /// The current image frame. + /// The png metadata. + /// The cancellation token. + private void DecodeInterlacedPixelData( + in FrameControl frameControl, + DeflateStream compressedStream, + ImageFrame imageFrame, + PngMetadata pngMetadata, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner? blendMemory = frameControl.BlendMode == FrameBlendMode.Over + ? this.memoryAllocator.Allocate(imageFrame.Width, AllocationOptions.Clean) + : null; + + FrameControl frameControlCopy = frameControl; + this.ExecuteImageDataSegmentAction(() => this.DecodeInterlacedPixelDataCore( + frameControlCopy, + compressedStream, + imageFrame, + pngMetadata, + blendMemory, + cancellationToken)); + + this.hasImageData = true; + } + + /// + /// Decodes the raw interlaced pixel data row by row. + /// + /// The pixel format. + /// The frame control. + /// The compressed pixel data stream. + /// The current image frame. + /// The png metadata. + /// The optional row blending buffer. + /// The cancellation token. + private void DecodeInterlacedPixelDataCore( + FrameControl frameControl, + DeflateStream compressedStream, + ImageFrame imageFrame, + PngMetadata pngMetadata, + IMemoryOwner? blendMemory, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + int currentRow = Adam7.FirstRow[0] + (int)frameControl.YOffset; + int currentRowBytesRead = 0; + int pass = 0; + int width = (int)frameControl.Width; + int endRow = (int)frameControl.YMax; + + Buffer2D imageBuffer = imageFrame.PixelBuffer; + + Span blendRowBuffer = blendMemory is null ? [] : blendMemory.Memory.Span; + + while (true) + { + int numColumns = Adam7.ComputeColumns(width, pass); + + if (numColumns == 0) + { + pass++; + + // This pass contains no data; skip to next pass + continue; + } + + int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1; + + while (currentRow < endRow) + { + cancellationToken.ThrowIfCancellationRequested(); + while (currentRowBytesRead < bytesPerInterlaceScanline) + { + int bytesRead = compressedStream.Read(this.scanline.GetSpan(), currentRowBytesRead, bytesPerInterlaceScanline - currentRowBytesRead); + if (bytesRead <= 0) + { + goto EXIT; + } + + currentRowBytesRead += bytesRead; + } + + currentRowBytesRead = 0; + + Span scanSpan = this.scanline.Slice(0, bytesPerInterlaceScanline); + Span prevSpan = this.previousScanline.Slice(0, bytesPerInterlaceScanline); + + switch ((FilterType)scanSpan[0]) + { + case FilterType.None: + break; + + case FilterType.Sub: + SubFilter.Decode(scanSpan, this.bytesPerPixel); + break; + + case FilterType.Up: + UpFilter.Decode(scanSpan, prevSpan); + break; + + case FilterType.Average: + AverageFilter.Decode(scanSpan, prevSpan, this.bytesPerPixel); + break; + + case FilterType.Paeth: + PaethFilter.Decode(scanSpan, prevSpan, this.bytesPerPixel); + break; + + default: + PngThrowHelper.ThrowUnknownFilter(); + break; + } + + if (this.isCgbi) + { + this.ApplyCgbiTransform(scanSpan[1..], this.pngColorType); + } + + Span rowSpan = imageBuffer.DangerousGetRowSpan(currentRow); + this.ProcessInterlacedDefilteredScanline( + frameControl, + this.scanline.GetSpan(), + rowSpan, + pngMetadata, + blendRowBuffer, + pixelOffset: Adam7.FirstColumn[pass], + increment: Adam7.ColumnIncrement[pass]); + + blendRowBuffer.Clear(); + this.SwapScanlineBuffers(); + + currentRow += Adam7.RowIncrement[pass]; + } + + pass++; + this.previousScanline.Clear(); + + if (pass < 7) + { + currentRow = Adam7.FirstRow[pass]; + } + else + { + pass = 0; + break; + } + } + + EXIT: + return; + } + + /// + /// Processes the de-filtered scanline filling the image pixel data + /// + /// The pixel format. + /// The frame control + /// The index of the current scanline being processed. + /// The de-filtered scanline + /// The image + /// The png metadata. + /// A span used to temporarily hold the decoded row pixel data for alpha blending. + private void ProcessDefilteredScanline( + in FrameControl frameControl, + int currentRow, + ReadOnlySpan scanline, + ImageFrame pixels, + PngMetadata pngMetadata, + Span blendRowBuffer) + where TPixel : unmanaged, IPixel + { + Span destination = pixels.PixelBuffer.DangerousGetRowSpan(currentRow); + + bool blend = frameControl.BlendMode == FrameBlendMode.Over; + Span rowSpan = blend + ? blendRowBuffer + : destination; + + // Trim the first marker byte from the buffer + ReadOnlySpan trimmed = scanline[1..]; + + // Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent. + IMemoryOwner? buffer = null; + try + { + // TODO: The allocation here could be per frame, not per scanline. + ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray( + trimmed, + this.bytesPerScanline - 1, + this.header.BitDepth, + out buffer) + ? buffer.GetSpan() + : trimmed; + + switch (this.pngColorType) + { + case PngColorType.Grayscale: + PngScanlineProcessor.ProcessGrayscaleScanline( + this.header.BitDepth, + in frameControl, + scanlineSpan, + rowSpan, + pngMetadata.TransparentColor); + + break; + + case PngColorType.GrayscaleWithAlpha: + PngScanlineProcessor.ProcessGrayscaleWithAlphaScanline( + this.header.BitDepth, + in frameControl, + scanlineSpan, + rowSpan, + (uint)this.bytesPerPixel, + (uint)this.bytesPerSample); + + break; + + case PngColorType.Palette: + PngScanlineProcessor.ProcessPaletteScanline( + in frameControl, + scanlineSpan, + rowSpan, + pngMetadata.ColorTable); + + break; + + case PngColorType.Rgb: + PngScanlineProcessor.ProcessRgbScanline( + this.configuration, + this.header.BitDepth, + frameControl, + scanlineSpan, + rowSpan, + this.bytesPerPixel, + this.bytesPerSample, + pngMetadata.TransparentColor); + + break; + + case PngColorType.RgbWithAlpha: + PngScanlineProcessor.ProcessRgbaScanline( + this.configuration, + this.header.BitDepth, + in frameControl, + scanlineSpan, + rowSpan, + this.bytesPerPixel, + this.bytesPerSample); + + break; + } + + if (blend) + { + PixelBlender blender = + PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); + blender.Blend(this.configuration, destination, destination, rowSpan, 1F); + } + } + finally + { + buffer?.Dispose(); + } + } + + /// + /// Processes the interlaced de-filtered scanline filling the image pixel data + /// + /// The pixel format. + /// The frame control + /// The de-filtered scanline + /// The current image row. + /// The png metadata. + /// A span used to temporarily hold the decoded row pixel data for alpha blending. + /// The column start index. Always 0 for none interlaced images. + /// The column increment. Always 1 for none interlaced images. + private void ProcessInterlacedDefilteredScanline( + in FrameControl frameControl, + ReadOnlySpan scanline, + Span destination, + PngMetadata pngMetadata, + Span blendRowBuffer, + int pixelOffset = 0, + int increment = 1) + where TPixel : unmanaged, IPixel + { + bool blend = frameControl.BlendMode == FrameBlendMode.Over; + Span rowSpan = blend + ? blendRowBuffer + : destination; + + // Trim the first marker byte from the buffer + ReadOnlySpan trimmed = scanline[1..]; + + // Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent. + IMemoryOwner? buffer = null; + try + { + ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray( + trimmed, + this.bytesPerScanline, + this.header.BitDepth, + out buffer) + ? buffer.GetSpan() + : trimmed; + + switch (this.pngColorType) + { + case PngColorType.Grayscale: + PngScanlineProcessor.ProcessInterlacedGrayscaleScanline( + this.header.BitDepth, + in frameControl, + scanlineSpan, + rowSpan, + (uint)pixelOffset, + (uint)increment, + pngMetadata.TransparentColor); + + break; + + case PngColorType.GrayscaleWithAlpha: + PngScanlineProcessor.ProcessInterlacedGrayscaleWithAlphaScanline( + this.header.BitDepth, + in frameControl, + scanlineSpan, + rowSpan, + (uint)pixelOffset, + (uint)increment, + (uint)this.bytesPerPixel, + (uint)this.bytesPerSample); + + break; + + case PngColorType.Palette: + PngScanlineProcessor.ProcessInterlacedPaletteScanline( + in frameControl, + scanlineSpan, + rowSpan, + (uint)pixelOffset, + (uint)increment, + pngMetadata.ColorTable); + + break; + + case PngColorType.Rgb: + PngScanlineProcessor.ProcessInterlacedRgbScanline( + this.configuration, + this.header.BitDepth, + in frameControl, + scanlineSpan, + rowSpan, + (uint)pixelOffset, + (uint)increment, + this.bytesPerPixel, + this.bytesPerSample, + pngMetadata.TransparentColor); + + break; + + case PngColorType.RgbWithAlpha: + PngScanlineProcessor.ProcessInterlacedRgbaScanline( + this.configuration, + this.header.BitDepth, + in frameControl, + scanlineSpan, + rowSpan, + (uint)pixelOffset, + (uint)increment, + this.bytesPerPixel, + this.bytesPerSample); + + break; + } + + if (blend) + { + PixelBlender blender = + PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); + blender.Blend(this.configuration, destination, destination, rowSpan, 1F); + } + } + finally + { + buffer?.Dispose(); + } + } + + /// + /// Decodes and assigns the color palette to the metadata + /// + /// The palette buffer. + /// The alpha palette buffer. + /// The png metadata. + private static void AssignColorPalette(ReadOnlySpan palette, ReadOnlySpan alpha, PngMetadata pngMetadata) + { + if (palette.Length == 0) + { + return; + } + + Color[] colorTable = new Color[palette.Length / Unsafe.SizeOf()]; + ReadOnlySpan rgbTable = MemoryMarshal.Cast(palette); + Color.FromPixel(rgbTable, colorTable); + + // The tRNS chunk must not contain more alpha values than there are palette entries. + if (alpha.Length > colorTable.Length) + { + alpha = alpha.Slice(0, colorTable.Length); + } + + if (alpha.Length > 0) + { + // The alpha chunk may contain as many transparency entries as there are palette entries + // (more than that would not make any sense) or as few as one. + for (int i = 0; i < alpha.Length; i++) + { + ref Color color = ref colorTable[i]; + color = color.WithAlpha(alpha[i] / 255F); + } + } + + pngMetadata.ColorTable = colorTable; + } + + /// + /// Decodes and assigns marker colors that identify transparent pixels in non indexed images. + /// + /// The alpha tRNS buffer. + /// The png metadata. + private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetadata pngMetadata) + { + if (this.pngColorType == PngColorType.Rgb) + { + if (alpha.Length >= 6) + { + if (this.header.BitDepth == 16) + { + ushort rc = BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2]); + ushort gc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(2, 2)); + ushort bc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(4, 2)); + + pngMetadata.TransparentColor = Color.FromPixel(new Rgb48(rc, gc, bc)); + return; + } + + byte r = ReadByteLittleEndian(alpha, 0); + byte g = ReadByteLittleEndian(alpha, 2); + byte b = ReadByteLittleEndian(alpha, 4); + pngMetadata.TransparentColor = Color.FromPixel(new Rgb24(r, g, b)); + } + } + else if (this.pngColorType == PngColorType.Grayscale) + { + if (alpha.Length >= 2) + { + if (this.header.BitDepth == 16) + { + pngMetadata.TransparentColor = Color.FromPixel(new L16(BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2]))); + } + else + { + pngMetadata.TransparentColor = Color.FromPixel(new L8(ReadByteLittleEndian(alpha, 0))); + } + } + } + } + + /// + /// Reads a animation control chunk from the data. + /// + /// The png metadata. + /// The containing data. + private void ReadAnimationControlChunk(PngMetadata pngMetadata, ReadOnlySpan data) + { + this.animationControl = AnimationControl.Parse(data); + + pngMetadata.RepeatCount = this.animationControl.NumberPlays; + } + + /// + /// Reads a header chunk from the data. + /// + /// The containing data. + private FrameControl ReadFrameControlChunk(ReadOnlySpan data) + { + FrameControl fcTL = FrameControl.Parse(data); + + fcTL.Validate(this.header); + + return fcTL; + } + + /// + /// Reads a header chunk from the data. + /// + /// The png metadata. + /// The containing data. + private void ReadHeaderChunk(PngMetadata pngMetadata, ReadOnlySpan data) + { + this.header = PngHeader.Parse(data); + + this.header.Validate(); + + pngMetadata.BitDepth = (PngBitDepth)this.header.BitDepth; + pngMetadata.ColorType = this.header.ColorType; + pngMetadata.InterlaceMethod = this.header.InterlaceMethod; + + this.pngColorType = this.header.ColorType; + this.Dimensions = new Size(this.header.Width, this.header.Height); + } + + /// + /// Reads a text chunk containing image properties from the data. + /// + /// The object. + /// The metadata to decode to. + /// The containing the data. + private void ReadTextChunk(ImageMetadata baseMetadata, PngMetadata metadata, ReadOnlySpan data) + { + if (this.skipMetadata) + { + return; + } + + int zeroIndex = data.IndexOf((byte)0); + + // Keywords are restricted to 1 to 79 bytes in length. + if (zeroIndex is < PngConstants.MinTextKeywordLength or > PngConstants.MaxTextKeywordLength) + { + return; + } + + ReadOnlySpan keywordBytes = data[..zeroIndex]; + if (!TryReadTextKeyword(keywordBytes, out string name)) + { + return; + } + + string value = PngConstants.Encoding.GetString(data[(zeroIndex + 1)..]); + + if (!TryReadTextChunkMetadata(baseMetadata, name, value)) + { + metadata.TextData.Add(new PngTextData(name, value, string.Empty, string.Empty)); + } + } + + /// + /// Reads the compressed text chunk. Contains a uncompressed keyword and a compressed text string. + /// + /// The object. + /// The metadata to decode to. + /// The containing the data. + private void ReadCompressedTextChunk(ImageMetadata baseMetadata, PngMetadata metadata, ReadOnlySpan data) + { + if (this.skipMetadata) + { + return; + } + + int keywordEnd = data.IndexOf((byte)0); + if (keywordEnd is < PngConstants.MinTextKeywordLength or > PngConstants.MaxTextKeywordLength) + { + return; + } + + if (keywordEnd < 0 || keywordEnd + 2 > data.Length) + { + return; // Not enough data for keyword + null + compression method. + } + + byte compressionMethod = data[keywordEnd + 1]; + if (compressionMethod != 0) + { + // Only compression method 0 is supported (zlib datastream with deflate compression). + return; + } + + ReadOnlySpan keywordBytes = data[..keywordEnd]; + if (!TryReadTextKeyword(keywordBytes, out string name)) + { + return; + } + + ReadOnlySpan compressedData = data[(keywordEnd + 2)..]; + + if (this.TryDecompressTextData(compressedData, PngConstants.Encoding, out string? uncompressed) + && !TryReadTextChunkMetadata(baseMetadata, name, uncompressed)) + { + metadata.TextData.Add(new PngTextData(name, uncompressed, string.Empty, string.Empty)); + } + } + + /// + /// Checks if the given text chunk is actually storing parsable metadata. + /// + /// The object to store the parsed metadata in. + /// The name of the text chunk. + /// The contents of the text chunk. + /// True if metadata was successfully parsed from the text chunk. False if the + /// text chunk was not identified as metadata, and should be stored in the metadata + /// object unmodified. + private static bool TryReadTextChunkMetadata(ImageMetadata baseMetadata, string chunkName, string chunkText) + { + if (chunkName.Equals(PngConstants.ExifRawProfileKeyword, StringComparison.OrdinalIgnoreCase) && + TryReadLegacyExifTextChunk(baseMetadata, chunkText)) + { + // Successfully parsed legacy exif data from text + return true; + } + + if (chunkName.Equals(PngConstants.IptcRawProfileKeyword, StringComparison.OrdinalIgnoreCase) && + TryReadLegacyIptcTextChunk(baseMetadata, chunkText)) + { + // Successfully parsed legacy iptc data from text + return true; + } + + // No special chunk data identified + return false; + } + + /// + /// Reads the CICP color profile chunk. + /// + /// The metadata. + /// The bytes containing the profile. + private static void ReadCicpChunk(ImageMetadata metadata, ReadOnlySpan data) + { + if (data.Length < 4) + { + // Ignore invalid cICP chunks. + return; + } + + byte colorPrimaries = data[0]; + byte transferFunction = data[1]; + byte matrixCoefficients = data[2]; + bool? fullRange; + if (data[3] == 1) + { + fullRange = true; + } + else if (data[3] == 0) + { + fullRange = false; + } + else + { + fullRange = null; + } + + metadata.CicpProfile = new CicpProfile(colorPrimaries, transferFunction, matrixCoefficients, fullRange); + } + + /// + /// Reads exif data encoded into a text chunk with the name "raw profile type exif". + /// This method was used by ImageMagick, exiftool, exiv2, digiKam, etc, before the + /// 2017 update to png that allowed a true exif chunk. + /// + /// The to store the decoded exif tags into. + /// The contents of the "raw profile type exif" text chunk. + private static bool TryReadLegacyExifTextChunk(ImageMetadata metadata, string data) + { + ReadOnlySpan dataSpan = data.AsSpan(); + dataSpan = dataSpan.TrimStart(); + + if (!StringEqualsInsensitive(dataSpan[..4], "exif".AsSpan())) + { + // "exif" identifier is missing from the beginning of the text chunk + return false; + } + + // Skip to the data length + dataSpan = dataSpan[4..].TrimStart(); + int dataLengthEnd = dataSpan.IndexOf('\n'); + int dataLength = ParseInt32(dataSpan[..dataSpan.IndexOf('\n')]); + + // Skip to the hex-encoded data + dataSpan = dataSpan[dataLengthEnd..].Trim(); + + // Sequence of bytes for the exif header ("Exif" ASCII and two zero bytes). + // This doesn't actually allocate. + ReadOnlySpan exifHeader = [0x45, 0x78, 0x69, 0x66, 0x00, 0x00]; + + if (dataLength < exifHeader.Length) + { + // Not enough room for the required exif header, this data couldn't possibly be valid + return false; + } + + // Parse the hex-encoded data into the byte array we are going to hand off to ExifProfile + byte[] exifBlob = new byte[dataLength - exifHeader.Length]; + + try + { + // Check for the presence of the exif header in the hex-encoded binary data + byte[] tempExifBuf = exifBlob; + if (exifBlob.Length < exifHeader.Length) + { + // Need to allocate a temporary array, this should be an extremely uncommon (TODO: impossible?) case + tempExifBuf = new byte[exifHeader.Length]; + } + + HexConverter.HexStringToBytes(dataSpan[..(exifHeader.Length * 2)], tempExifBuf); + if (!tempExifBuf.AsSpan()[..exifHeader.Length].SequenceEqual(exifHeader)) + { + // Exif header in the hex data is not valid + return false; + } + + // Skip over the exif header we just tested + dataSpan = dataSpan[(exifHeader.Length * 2)..]; + dataLength -= exifHeader.Length; + + // Load the hex-encoded data, one line at a time + for (int i = 0; i < dataLength;) + { + ReadOnlySpan lineSpan = dataSpan; + + int newlineIndex = dataSpan.IndexOf('\n'); + if (newlineIndex != -1) + { + lineSpan = dataSpan[..newlineIndex]; + } + + i += HexConverter.HexStringToBytes(lineSpan, exifBlob.AsSpan()[i..]); + + dataSpan = dataSpan[(newlineIndex + 1)..]; + } + } + catch + { + return false; + } + + MergeOrSetExifProfile(metadata, new ExifProfile(exifBlob), replaceExistingKeys: false); + return true; + } + + /// + /// Reads iptc data encoded into a text chunk with the name "Raw profile type iptc". + /// This convention is used by ImageMagick/exiftool/exiv2/digiKam and stores a byte-count + /// followed by hex-encoded bytes. + /// + /// The to store the decoded iptc tags into. + /// The contents of the "Raw profile type iptc" text chunk. + private static bool TryReadLegacyIptcTextChunk(ImageMetadata metadata, string data) + { + // Preserve first IPTC found. + if (metadata.IptcProfile != null) + { + return true; + } + + ReadOnlySpan dataSpan = data.AsSpan().TrimStart(); + + // Must start with the "iptc" identifier (case-insensitive). + // Common real-world format (ImageMagick/ExifTool) is: + // "IPTC profile\n \n" + if (dataSpan.Length < 4 || !StringEqualsInsensitive(dataSpan[..4], "iptc".AsSpan())) + { + return false; + } + + // Skip the remainder of the first line ("IPTC profile", etc). + int firstLineEnd = dataSpan.IndexOf('\n'); + if (firstLineEnd < 0) + { + return false; + } + + dataSpan = dataSpan[(firstLineEnd + 1)..].TrimStart(); + + // Next line contains the decimal byte length (often indented). + int dataLengthEnd = dataSpan.IndexOf('\n'); + if (dataLengthEnd < 0) + { + return false; + } + + int dataLength; + try + { + dataLength = ParseInt32(dataSpan[..dataLengthEnd]); + } + catch + { + return false; + } + + if (dataLength <= 0) + { + return false; + } + + // Skip to the hex-encoded data. + dataSpan = dataSpan[(dataLengthEnd + 1)..].Trim(); + + byte[] iptcBlob = new byte[dataLength]; + + try + { + int written = 0; + + for (; written < dataLength;) + { + ReadOnlySpan lineSpan = dataSpan; + + int newlineIndex = dataSpan.IndexOf('\n'); + if (newlineIndex != -1) + { + lineSpan = dataSpan[..newlineIndex]; + } + + // Important: handle CRLF and any incidental whitespace. + lineSpan = lineSpan.Trim(); // removes ' ', '\t', '\r', '\n', etc. + + if (!lineSpan.IsEmpty) + { + written += HexConverter.HexStringToBytes(lineSpan, iptcBlob.AsSpan()[written..]); + } + + if (newlineIndex == -1) + { + break; + } + + dataSpan = dataSpan[(newlineIndex + 1)..]; + } + + if (written != dataLength) + { + return false; + } + } + catch + { + return false; + } + + // Prefer IRB extraction if this is Photoshop-style data (8BIM resource blocks). + byte[] iptcPayload = TryExtractIptcFromPhotoshopIrb(iptcBlob, out byte[] extracted) + ? extracted + : iptcBlob; + + metadata.IptcProfile = new IptcProfile(iptcPayload); + return true; + } + + /// + /// Attempts to extract IPTC metadata from a Photoshop Image Resource Block (IRB) contained within the specified + /// data buffer. + /// + /// This method scans the provided data for a Photoshop IRB block containing IPTC metadata and + /// extracts it if present. The method does not validate the contents of the IPTC data beyond locating the + /// appropriate resource block. + /// A read-only span of bytes containing the Photoshop IRB data to search for embedded IPTC metadata. + /// When this method returns, contains the extracted IPTC metadata as a byte array if found; otherwise, an undefined + /// value. + /// if IPTC metadata is successfully extracted from the IRB data; otherwise, . + private static bool TryExtractIptcFromPhotoshopIrb(ReadOnlySpan data, out byte[] iptcBytes) + { + iptcBytes = default!; + + ReadOnlySpan adobePhotoshop30 = PngConstants.AdobePhotoshop30; + + // Some writers include the "Photoshop 3.0\0" header, some store just IRB blocks. + if (data.Length >= adobePhotoshop30.Length && data[..adobePhotoshop30.Length].SequenceEqual(adobePhotoshop30)) + { + data = data[adobePhotoshop30.Length..]; + } + + ReadOnlySpan eightBim = PngConstants.EightBim; + ushort adobeIptcResourceId = PngConstants.AdobeIptcResourceId; + while (data.Length >= 12) + { + if (!data[..4].SequenceEqual(eightBim)) + { + return false; + } + + data = data[4..]; + + // Resource ID (2 bytes, big endian) + if (data.Length < 2) + { + return false; + } + + ushort resourceId = (ushort)((data[0] << 8) | data[1]); + data = data[2..]; + + // Pascal string name (1-byte length, then bytes), padded to even. + if (data.Length < 1) + { + return false; + } + + int nameLen = data[0]; + int nameFieldLen = 1 + nameLen; + if ((nameFieldLen & 1) != 0) + { + nameFieldLen++; // pad to even + } + + if (data.Length < nameFieldLen + 4) + { + return false; + } + + data = data[nameFieldLen..]; + + // Resource data size (4 bytes, big endian) + int size = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]; + data = data[4..]; + + if (size < 0 || data.Length < size) + { + return false; + } + + ReadOnlySpan payload = data[..size]; + + // Data is padded to even. + int advance = size; + if ((advance & 1) != 0) + { + advance++; + } + + if (resourceId == adobeIptcResourceId) + { + iptcBytes = payload.ToArray(); + return true; + } + + if (data.Length < advance) + { + return false; + } + + data = data[advance..]; + } + + return false; + } + + /// + /// Reads the color profile chunk. The data is stored similar to the zTXt chunk. + /// + /// The metadata. + /// The bytes containing the profile. + private void ReadColorProfileChunk(ImageMetadata metadata, ReadOnlySpan data) + { + int zeroIndex = data.IndexOf((byte)0); + if (zeroIndex is < PngConstants.MinTextKeywordLength or > PngConstants.MaxTextKeywordLength) + { + return; + } + + byte compressionMethod = data[zeroIndex + 1]; + if (compressionMethod != 0) + { + // Only compression method 0 is supported (zlib datastream with deflate compression). + return; + } + + ReadOnlySpan keywordBytes = data[..zeroIndex]; + if (!TryReadTextKeyword(keywordBytes, out string name)) + { + return; + } + + ReadOnlySpan compressedData = data[(zeroIndex + 2)..]; + + if (this.TryDecompressZlibData(compressedData, this.maxUncompressedLength, out byte[] iccpProfileBytes)) + { + metadata.IccProfile = new IccProfile(iccpProfileBytes); + } + } + + /// + /// Tries to decompress zlib compressed data. + /// + /// The compressed data. + /// The maximum uncompressed length. + /// The uncompressed bytes array. + /// True, if de-compressing was successful. + private unsafe bool TryDecompressZlibData(ReadOnlySpan compressedData, int maxLength, out byte[] uncompressedBytesArray) + { + fixed (byte* compressedDataBase = compressedData) + { + using IMemoryOwner destBuffer = this.memoryAllocator.Allocate(this.configuration.StreamProcessingBufferSize); + using MemoryStream memoryStreamOutput = new(compressedData.Length); + using UnmanagedMemoryStream memoryStreamInput = new(compressedDataBase, compressedData.Length); + using BufferedReadStream bufferedStream = new(this.configuration, memoryStreamInput); + using ZlibInflateStream inflateStream = new(bufferedStream); + + Span destUncompressedData = destBuffer.GetSpan(); + if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) + { + uncompressedBytesArray = []; + return false; + } + + int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length); + while (bytesRead != 0) + { + if (memoryStreamOutput.Length > maxLength) + { + uncompressedBytesArray = []; + return false; + } + + memoryStreamOutput.Write(destUncompressedData[..bytesRead]); + bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length); + } + + uncompressedBytesArray = memoryStreamOutput.ToArray(); + return true; + } + } + + /// + /// Compares two ReadOnlySpan<char>s in a case-insensitive method. + /// This is only needed because older frameworks are missing the extension method. + /// + /// The first to compare. + /// The second to compare. + /// True if the spans were identical, false otherwise. + private static bool StringEqualsInsensitive(ReadOnlySpan span1, ReadOnlySpan span2) + => span1.Equals(span2, StringComparison.OrdinalIgnoreCase); + + /// + /// int.Parse() a ReadOnlySpan<char>, with a fallback for older frameworks. + /// + /// The to parse. + /// The parsed . + private static int ParseInt32(ReadOnlySpan span) => int.Parse(span, provider: CultureInfo.InvariantCulture); + + /// + /// Sets the in to , + /// or copies exif tags if already contains an . + /// + /// The to store the exif data in. + /// The to copy exif tags from. + /// If already contains an , + /// controls whether existing exif tags in will be overwritten with any conflicting + /// tags from . + private static void MergeOrSetExifProfile(ImageMetadata metadata, ExifProfile newProfile, bool replaceExistingKeys) + { + if (metadata.ExifProfile is null) + { + // No exif metadata was loaded yet, so just assign it + metadata.ExifProfile = newProfile; + } + else + { + // Try to merge existing keys with the ones from the new profile + foreach (IExifValue newKey in newProfile.Values) + { + if (replaceExistingKeys || metadata.ExifProfile.GetValueInternal(newKey.Tag) is null) + { + metadata.ExifProfile.SetValueInternal(newKey.Tag, newKey.GetValue()); + } + } + } + } + + /// + /// Reads a iTXt chunk, which contains international text data. It contains: + /// - A uncompressed keyword. + /// - Compression flag, indicating if a compression is used. + /// - Compression method. + /// - Language tag (optional). + /// - A translated keyword (optional). + /// - Text data, which is either compressed or uncompressed. + /// + /// The metadata to decode to. + /// The containing the data. + private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan data) + { + if (this.skipMetadata) + { + return; + } + + PngMetadata pngMetadata = metadata.GetPngMetadata(); + int zeroIndexKeyword = data.IndexOf((byte)0); + if (zeroIndexKeyword is < PngConstants.MinTextKeywordLength or > PngConstants.MaxTextKeywordLength) + { + return; + } + + if (zeroIndexKeyword < 0 || zeroIndexKeyword + 4 > data.Length) + { + return; // Not enough data for keyword + null + flag + method + language. + } + + byte compressionFlag = data[zeroIndexKeyword + 1]; + if (compressionFlag is not (0 or 1)) + { + return; + } + + byte compressionMethod = data[zeroIndexKeyword + 2]; + if (compressionMethod != 0) + { + // Only compression method 0 is supported (zlib datastream with deflate compression). + return; + } + + int langStartIdx = zeroIndexKeyword + 3; + int languageLength = data[langStartIdx..].IndexOf((byte)0); + if (languageLength < 0) + { + return; + } + + string language = PngConstants.LanguageEncoding.GetString(data.Slice(langStartIdx, languageLength)); + + int translatedKeywordStartIdx = langStartIdx + languageLength + 1; + int translatedKeywordLength = data[translatedKeywordStartIdx..].IndexOf((byte)0); + if (translatedKeywordLength < 0) + { + return; + } + + string translatedKeyword = PngConstants.TranslatedEncoding.GetString(data.Slice(translatedKeywordStartIdx, translatedKeywordLength)); + + ReadOnlySpan keywordBytes = data[..zeroIndexKeyword]; + if (!TryReadTextKeyword(keywordBytes, out string keyword)) + { + return; + } + + int dataStartIdx = translatedKeywordStartIdx + translatedKeywordLength + 1; + if (compressionFlag == 1) + { + ReadOnlySpan compressedData = data[dataStartIdx..]; + + if (this.TryDecompressTextData(compressedData, PngConstants.TranslatedEncoding, out string? uncompressed)) + { + pngMetadata.TextData.Add(new PngTextData(keyword, uncompressed, language, translatedKeyword)); + } + } + else if (IsXmpTextData(keywordBytes)) + { + metadata.XmpProfile = new XmpProfile(data[dataStartIdx..].ToArray()); + } + else + { + string value = PngConstants.TranslatedEncoding.GetString(data[dataStartIdx..]); + pngMetadata.TextData.Add(new PngTextData(keyword, value, language, translatedKeyword)); + } + } + + /// + /// Decompresses a byte array with zlib compressed text data. + /// + /// Compressed text data bytes. + /// The string encoding to use. + /// The uncompressed value. + /// The . + private bool TryDecompressTextData(ReadOnlySpan compressedData, Encoding encoding, [NotNullWhen(true)] out string? value) + { + if (this.TryDecompressZlibData(compressedData, this.maxUncompressedLength, out byte[] uncompressedData)) + { + value = encoding.GetString(uncompressedData); + return true; + } + + value = null; + return false; + } + + /// + /// Reads the next data chunk. + /// + /// Count of bytes in the next data chunk, or 0 if there are no more data chunks left. + private int ReadNextDataChunk() + { + if (this.nextChunk != null) + { + return 0; + } + + Span buffer = stackalloc byte[20]; + + int length = this.currentStream.Read(buffer, 0, 4); + if (length == 0) + { + return 0; + } + + if (this.TryReadChunk(buffer, out PngChunk chunk)) + { + if (chunk.Type is PngChunkType.Data or PngChunkType.FrameData) + { + chunk.Data?.Dispose(); + return chunk.Length; + } + + this.nextChunk = chunk; + } + + return 0; + } + + /// + /// Reads the next animated frame data chunk. + /// + /// Count of bytes in the next data chunk, or 0 if there are no more data chunks left. + private int ReadNextFrameDataChunk() + { + if (this.nextChunk != null) + { + return 0; + } + + Span buffer = stackalloc byte[20]; + + int length = this.currentStream.Read(buffer, 0, 4); + if (length == 0) + { + return 0; + } + + if (this.TryReadChunk(buffer, out PngChunk chunk)) + { + if (chunk.Type is PngChunkType.FrameData) + { + chunk.Data?.Dispose(); + + this.currentStream.Position += 4; // Skip sequence number + return chunk.Length - 4; + } + + this.nextChunk = chunk; + } + + return 0; + } + + /// + /// Skips any remaining chunks belonging to the current frame. + /// This mirrors how is used during decoding: + /// consecutive fdAT chunks are consumed until a non-fdAT chunk is encountered, + /// which is stored in for the next iteration. + /// + /// Temporary buffer. + private void SkipRemainingFrameDataChunks(Span buffer) + { + while (this.TryReadChunk(buffer, out PngChunk chunk)) + { + if (chunk.Type is PngChunkType.FrameData) + { + chunk.Data?.Dispose(); + this.SkipChunkDataAndCrc(chunk); + } + else + { + // Not a FrameData chunk; store it so the next TryReadChunk call returns it. + this.nextChunk = chunk; + return; + } + } + } + + /// + /// Reads a chunk from the stream. + /// + /// Temporary buffer. + /// The image format chunk. + /// + /// The . + /// + private bool TryReadChunk(Span buffer, out PngChunk chunk) + { + if (this.nextChunk != null) + { + chunk = this.nextChunk.Value; + + this.nextChunk = null; + + return true; + } + + if (this.currentStream.Position >= this.currentStream.Length - 1) + { + // IEND + chunk = default; + return false; + } + + // Capture the current position so we can revert back to it if we fail to read a valid chunk. + long position = this.currentStream.Position; + + if (!this.TryReadChunkLength(buffer, out int length)) + { + // IEND + chunk = default; + return false; + } + + while (length < 0) + { + // Not a valid chunk so try again until we reach a known chunk. + if (!this.TryReadChunkLength(buffer, out length)) + { + // IEND + chunk = default; + return false; + } + } + + PngChunkType type; + + // Loop until we get a chunk type that is valid. + while (true) + { + type = this.ReadChunkType(buffer); + if (!IsValidChunkType(type)) + { + // The chunk type is invalid. + // Revert back to the next byte past the previous position and try again. + this.currentStream.Position = ++position; + + // If we are now at the end of the stream, we're done. + if (this.currentStream.Position >= this.currentStream.Length) + { + chunk = default; + return false; + } + + // Read the next chunk’s length. + if (!this.TryReadChunkLength(buffer, out length)) + { + chunk = default; + return false; + } + + while (length < 0) + { + if (!this.TryReadChunkLength(buffer, out length)) + { + chunk = default; + return false; + } + } + + // Continue to try reading the next chunk. + continue; + } + + // We have a valid chunk type. + break; + } + + // If we're reading color metadata only we're only interested in the IHDR and tRNS chunks. + // We can skip most other chunk data in the stream for better performance. + if (this.colorMetadataOnly && + type != PngChunkType.Header && + type != PngChunkType.Transparency && + type != PngChunkType.Palette && + type != PngChunkType.AnimationControl && + type != PngChunkType.FrameControl) + { + chunk = new PngChunk(length, type); + return true; + } + + // A chunk might report a length that exceeds the length of the stream. + // Take the minimum of the two values to ensure we don't read past the end of the stream. + position = this.currentStream.Position; + chunk = new PngChunk( + length: (int)Math.Min(length, this.currentStream.Length - position), + type: type, + data: this.ReadChunkData(length)); + + this.ValidateChunk(chunk, buffer); + + // Restore the stream position for IDAT and fdAT chunks, because it will be decoded later and + // was only read to verifying the CRC is correct. + if (type is PngChunkType.Data or PngChunkType.FrameData) + { + this.currentStream.Position = position; + } + + return true; + } + + /// + /// Determines whether the 4-byte chunk type is valid (all ASCII letters). + /// + /// The chunk type. + [MethodImpl(InliningOptions.ShortMethod)] + private static bool IsValidChunkType(PngChunkType type) + { + uint value = (uint)type; + byte b0 = (byte)(value >> 24); + byte b1 = (byte)(value >> 16); + byte b2 = (byte)(value >> 8); + byte b3 = (byte)value; + return IsAsciiLetter(b0) && IsAsciiLetter(b1) && IsAsciiLetter(b2) && IsAsciiLetter(b3); + } + + /// + /// Returns a value indicating whether the given byte is an ASCII letter. + /// + /// The byte to check. + /// + /// if the byte is an ASCII letter; otherwise, . + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static bool IsAsciiLetter(byte b) + => (b >= (byte)'A' && b <= (byte)'Z') || (b >= (byte)'a' && b <= (byte)'z'); + + /// + /// Validates the png chunk. + /// + /// The . + /// Temporary buffer. + private void ValidateChunk(in PngChunk chunk, Span buffer) + { + uint inputCrc = this.ReadChunkCrc(buffer); + if (chunk.IsCritical(this.segmentIntegrityHandling)) + { + Span chunkType = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32BigEndian(chunkType, (uint)chunk.Type); + + this.crc32.Reset(); + this.crc32.Append(chunkType); + this.crc32.Append(chunk.Data.GetSpan()); + + if (this.crc32.GetCurrentHashAsUInt32() != inputCrc) + { + string chunkTypeName = Encoding.ASCII.GetString(chunkType); + + // ensure when throwing we dispose the data back to the memory allocator + chunk.Data?.Dispose(); + PngThrowHelper.ThrowInvalidChunkCrc(chunkTypeName); + } + } + } + + /// + /// Reads the cycle redundancy chunk from the data. + /// + /// Temporary buffer. + [MethodImpl(InliningOptions.ShortMethod)] + private uint ReadChunkCrc(Span buffer) + { + uint crc = 0; + if (this.currentStream.Read(buffer, 0, 4) == 4) + { + crc = BinaryPrimitives.ReadUInt32BigEndian(buffer); + } + + return crc; + } + + /// + /// Skips the chunk data and the cycle redundancy chunk read from the data. + /// + /// The image format chunk. + [MethodImpl(InliningOptions.ShortMethod)] + private void SkipChunkDataAndCrc(in PngChunk chunk) + { + this.currentStream.Skip(chunk.Length); + this.currentStream.Skip(4); + } + + /// + /// Reads the chunk data from the stream. + /// + /// The length of the chunk data to read. + [MethodImpl(InliningOptions.ShortMethod)] + private IMemoryOwner ReadChunkData(int length) + { + if (length == 0) + { + return new BasicArrayBuffer([]); + } + + // We rent the buffer here to return it afterwards in Decode() + // We don't want to throw a degenerated memory exception here as we want to allow partial decoding + // so limit the length. + length = (int)Math.Min(length, this.currentStream.Length - this.currentStream.Position); + IMemoryOwner buffer = this.configuration.MemoryAllocator.Allocate(length, AllocationOptions.Clean); + + this.currentStream.Read(buffer.GetSpan(), 0, length); + + return buffer; + } + + /// + /// Identifies the chunk type from the chunk. + /// + /// Temporary buffer. + /// + /// Thrown if the input stream is not valid. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private PngChunkType ReadChunkType(Span buffer) + { + if (this.currentStream.Read(buffer, 0, 4) == 4) + { + return (PngChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer); + } + + PngThrowHelper.ThrowInvalidChunkType(); + + // The IDE cannot detect the throw here. + return default; + } + + /// + /// Attempts to read the length of the next chunk. + /// + /// Temporary buffer. + /// The result length. If the return type is this parameter is passed uninitialized. + /// + /// Whether the length was read. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private bool TryReadChunkLength(Span buffer, out int result) + { + if (this.currentStream.Read(buffer, 0, 4) == 4) + { + result = BinaryPrimitives.ReadInt32BigEndian(buffer); + + return true; + } + + result = default; + return false; + } + + /// + /// Tries to reads a text chunk keyword, which have some restrictions to be valid: + /// Keywords shall contain only printable Latin-1 characters and should not have leading or trailing whitespace. + /// See: https://www.w3.org/TR/PNG/#11zTXt + /// + /// The keyword bytes. + /// The name. + /// True, if the keyword could be read and is valid. + private static bool TryReadTextKeyword(ReadOnlySpan keywordBytes, out string name) + { + name = string.Empty; + + // Keywords shall contain only printable Latin-1. + foreach (byte c in keywordBytes) + { + if (c is not ((>= 32 and <= 126) or (>= 161 and <= 255))) + { + return false; + } + } + + // Keywords should not be empty or have leading or trailing whitespace. + name = PngConstants.Encoding.GetString(keywordBytes); + return !string.IsNullOrWhiteSpace(name) + && !name.StartsWith(' ') && !name.EndsWith(' '); + } + + private static bool IsXmpTextData(ReadOnlySpan keywordBytes) + => keywordBytes.SequenceEqual(PngConstants.XmpKeyword); + + private void SwapScanlineBuffers() + => (this.scanline, this.previousScanline) = (this.previousScanline, this.scanline); + + /// + /// Applies the inverse of Apple's CgBI pixel mangling to a defiltered scanline. + /// CgBI PNGs are emitted by pngcrush -iphone with channel order swapped + /// from RGB(A) to BGR(A) and RGB samples premultiplied by alpha. This converts + /// the bytes back to standard PNG semantics in place so the existing scanline + /// processors can consume them unchanged. CgBI is only emitted for 8-bit + /// truecolor (with or without alpha); other color types are left alone. + /// + /// + /// See https://theapplewiki.com/wiki/PNG_CgBI_Format + /// + /// The defiltered pixel bytes (without the leading filter byte). + /// The PNG color type from IHDR. + private void ApplyCgbiTransform(Span scanline, PngColorType colorType) + { + if (colorType == PngColorType.RgbWithAlpha) + { + Span pixels = MemoryMarshal.Cast(scanline); + int i = 0; + + if (Vector512.IsHardwareAccelerated && pixels.Length >= 16) + { + i = ApplyCgbiTransformVector512(scanline, pixels.Length); + } + + if (Vector256.IsHardwareAccelerated && Avx2.IsSupported && (pixels.Length - i) >= 8) + { + i = ApplyCgbiTransformVector256(scanline, i, pixels.Length); + } + + if (Vector128.IsHardwareAccelerated && (pixels.Length - i) >= 4) + { + i = ApplyCgbiTransformVector128(scanline, i, pixels.Length); + } + + for (; i < pixels.Length; i++) + { + ref Rgba32 pixel = ref pixels[i]; + pixel = new Rgba32(pixel.B, pixel.G, pixel.R, pixel.A); + UndoCgbiPremultiplicationScalar(ref pixel); + } + } + else if (colorType == PngColorType.Rgb) + { + // No alpha channel, so just swap R and B using built in SIMD-optimized pixel operations. + Span target = MemoryMarshal.Cast(scanline); + PixelOperations.Instance.FromBgr24Bytes(this.configuration, scanline, target, target.Length); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void UndoCgbiPremultiplicationScalar(ref Rgba32 pixel) + { + byte a = pixel.A; + if (a is 0 or byte.MaxValue) + { + return; + } + + // Reverse: c' = c * a / 255 => c = round(c' * 255 / a) + int half = a >> 1; + byte r = (byte)Math.Min(byte.MaxValue, ((pixel.R * byte.MaxValue) + half) / a); + byte g = (byte)Math.Min(byte.MaxValue, ((pixel.G * byte.MaxValue) + half) / a); + byte b = (byte)Math.Min(byte.MaxValue, ((pixel.B * byte.MaxValue) + half) / a); + pixel = new Rgba32(r, g, b, a); + } + + private static int ApplyCgbiTransformVector512(Span scanline, int pixelCount) + { + ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline); + int i = 0; + + Span temp = stackalloc byte[Vector512.Count]; + SimdUtils.Shuffle.MMShuffleSpan(ref temp, SimdUtils.Shuffle.MMShuffle3012); + + // MMShuffle3012 expands to [2, 1, 0, 3] for each 4-byte pixel, converting + // CgBI's BGRA byte order to Rgba32's RGBA layout while keeping alpha in place. + // The generated mask only swaps bytes inside each pixel, so it remains + // correct for the optimized 512-bit byte shuffle helper. + Vector512 shuffleMask = Unsafe.As>(ref MemoryMarshal.GetReference(temp)); + + Vector512 zero = Vector512.Zero; + Vector512 one = Vector512.One; + Vector512 byteMask = Vector512.Create(0xFF); + Vector512 opaque = Vector512.Create(0xFF); + Vector512 byteMax = Vector512.Create((int)byte.MaxValue); + + for (; i <= pixelCount - 16; i += 16) + { + ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf()); + Vector512 bgra = Unsafe.ReadUnaligned>(ref blockRef); + Vector512 rgba = Vector512_.ShuffleNative(bgra, shuffleMask); + Vector512 packed = rgba.AsInt32(); + Vector512 alpha = Vector512.ShiftRightLogical(packed, 24); + + // Fully transparent and fully opaque pixels are identity cases for + // unpremultiplication. Masking them keeps the scalar behavior and lets + // safeAlpha avoid dividing by zero for alpha == 0. + Vector512 partialMask = ~(Vector512.Equals(alpha, zero) | Vector512.Equals(alpha, opaque)); + + Vector512 r = packed & byteMask; + Vector512 g = Vector512.ShiftRightLogical(packed, 8) & byteMask; + Vector512 b = Vector512.ShiftRightLogical(packed, 16) & byteMask; + + Vector512 safeAlpha = Vector512.ConditionalSelect(partialMask, alpha, one); + Vector512 halfAlpha = Vector512.ShiftRightLogical(safeAlpha, 1); + Vector512 safeAlphaF = Vector512.ConvertToSingle(safeAlpha); + + // The scalar path computes ((c * 255) + (a >> 1)) / a with integer + // division. Floor the positive quotient before converting so SIMD does + // not use the default round-to-nearest conversion and drift by one. + Vector512 unpremultipliedR = Vector512.Min( + byteMax, + Vector512.ConvertToInt32(Vector512.Floor(Vector512.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF))); + + Vector512 unpremultipliedG = Vector512.Min( + byteMax, + Vector512.ConvertToInt32(Vector512.Floor(Vector512.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF))); + + Vector512 unpremultipliedB = Vector512.Min( + byteMax, + Vector512.ConvertToInt32(Vector512.Floor(Vector512.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF))); + + // ConditionalSelect applies the expensive unpremultiply only to pixels + // where alpha is between 1 and 254; alpha 0 and 255 lanes keep the + // shuffled channel values exactly as the scalar path does. + Vector512 finalR = Vector512.ConditionalSelect(partialMask, unpremultipliedR, r); + Vector512 finalG = Vector512.ConditionalSelect(partialMask, unpremultipliedG, g); + Vector512 finalB = Vector512.ConditionalSelect(partialMask, unpremultipliedB, b); + + // Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so + // shifting the unpacked channels back to byte offsets 0, 1, 2, and 3 + // recreates the in-memory RGBA bytes for the unaligned store. + Vector512 result = + finalR | + Vector512.ShiftLeft(finalG, 8) | + Vector512.ShiftLeft(finalB, 16) | + Vector512.ShiftLeft(alpha, 24); + + Unsafe.WriteUnaligned(ref blockRef, result.AsByte()); + } + + return i; + } + + private static int ApplyCgbiTransformVector256(Span scanline, int startPixel, int pixelCount) + { + ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline); + int i = startPixel; + + Span temp = stackalloc byte[Vector512.Count]; + SimdUtils.Shuffle.MMShuffleSpan(ref temp, SimdUtils.Shuffle.MMShuffle3012); + + // MMShuffle3012 expands to [2, 1, 0, 3] for each 4-byte pixel, converting + // CgBI's BGRA byte order to Rgba32's RGBA layout while keeping alpha in place. + // Avx2.Shuffle is 128-bit lane-local, and the generated mask repeats inside + // each lane, so no byte ever needs to cross the lane boundary. + Vector256 shuffleMask = Unsafe.As>(ref MemoryMarshal.GetReference(temp)); + + Vector256 zero = Vector256.Zero; + Vector256 one = Vector256.One; + Vector256 byteMask = Vector256.Create(0xFF); + Vector256 opaque = Vector256.Create(0xFF); + Vector256 byteMax = Vector256.Create((int)byte.MaxValue); + + for (; i <= pixelCount - 8; i += 8) + { + ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf()); + Vector256 bgra = Unsafe.ReadUnaligned>(ref blockRef); + Vector256 rgba = Vector256_.ShufflePerLane(bgra, shuffleMask); + Vector256 packed = rgba.AsInt32(); + Vector256 alpha = Vector256.ShiftRightLogical(packed, 24); + + // Fully transparent and fully opaque pixels are identity cases for + // unpremultiplication. Masking them keeps the scalar behavior and lets + // safeAlpha avoid dividing by zero for alpha == 0. + Vector256 partialMask = ~(Vector256.Equals(alpha, zero) | Vector256.Equals(alpha, opaque)); + + Vector256 r = packed & byteMask; + Vector256 g = Vector256.ShiftRightLogical(packed, 8) & byteMask; + Vector256 b = Vector256.ShiftRightLogical(packed, 16) & byteMask; + + Vector256 safeAlpha = Vector256.ConditionalSelect(partialMask, alpha, one); + Vector256 halfAlpha = Vector256.ShiftRightLogical(safeAlpha, 1); + Vector256 safeAlphaF = Vector256.ConvertToSingle(safeAlpha); + + // The scalar path computes ((c * 255) + (a >> 1)) / a with integer + // division. Floor the positive quotient before converting so SIMD does + // not use the default round-to-nearest conversion and drift by one. + Vector256 unpremultipliedR = Vector256.Min( + byteMax, + Vector256.ConvertToInt32(Vector256.Floor(Vector256.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF))); + + Vector256 unpremultipliedG = Vector256.Min( + byteMax, + Vector256.ConvertToInt32(Vector256.Floor(Vector256.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF))); + + Vector256 unpremultipliedB = Vector256.Min( + byteMax, + Vector256.ConvertToInt32(Vector256.Floor(Vector256.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF))); + + // ConditionalSelect applies the expensive unpremultiply only to pixels + // where alpha is between 1 and 254; alpha 0 and 255 lanes keep the + // shuffled channel values exactly as the scalar path does. + Vector256 finalR = Vector256.ConditionalSelect(partialMask, unpremultipliedR, r); + Vector256 finalG = Vector256.ConditionalSelect(partialMask, unpremultipliedG, g); + Vector256 finalB = Vector256.ConditionalSelect(partialMask, unpremultipliedB, b); + + // Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so + // shifting the unpacked channels back to byte offsets 0, 1, 2, and 3 + // recreates the in-memory RGBA bytes for the unaligned store. + Vector256 result = + finalR | + Vector256.ShiftLeft(finalG, 8) | + Vector256.ShiftLeft(finalB, 16) | + Vector256.ShiftLeft(alpha, 24); + + Unsafe.WriteUnaligned(ref blockRef, result.AsByte()); + } + + return i; + } + + private static int ApplyCgbiTransformVector128(Span scanline, int startPixel, int pixelCount) + { + ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline); + int i = startPixel; + + Span temp = stackalloc byte[Vector512.Count]; + SimdUtils.Shuffle.MMShuffleSpan(ref temp, SimdUtils.Shuffle.MMShuffle3012); + + // MMShuffle3012 expands to [2, 1, 0, 3] for each 4-byte pixel, converting + // CgBI's BGRA byte order to Rgba32's RGBA layout while keeping alpha in place. + Vector128 shuffleMask = Unsafe.As>(ref MemoryMarshal.GetReference(temp)); + + Vector128 zero = Vector128.Zero; + Vector128 one = Vector128.One; + Vector128 byteMask = Vector128.Create(0xFF); + Vector128 opaque = Vector128.Create(0xFF); + Vector128 byteMax = Vector128.Create((int)byte.MaxValue); + + for (; i <= pixelCount - 4; i += 4) + { + ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf()); + Vector128 bgra = Unsafe.ReadUnaligned>(ref blockRef); + Vector128 rgba = Vector128_.ShuffleNative(bgra, shuffleMask); + Vector128 packed = rgba.AsInt32(); + Vector128 alpha = Vector128.ShiftRightLogical(packed, 24); + + // Fully transparent and fully opaque pixels are identity cases for + // unpremultiplication. Masking them keeps the scalar behavior and lets + // safeAlpha avoid dividing by zero for alpha == 0. + Vector128 partialMask = ~(Vector128.Equals(alpha, zero) | Vector128.Equals(alpha, opaque)); + + Vector128 r = packed & byteMask; + Vector128 g = Vector128.ShiftRightLogical(packed, 8) & byteMask; + Vector128 b = Vector128.ShiftRightLogical(packed, 16) & byteMask; + + Vector128 safeAlpha = Vector128.ConditionalSelect(partialMask, alpha, one); + Vector128 halfAlpha = Vector128.ShiftRightLogical(safeAlpha, 1); + Vector128 safeAlphaF = Vector128.ConvertToSingle(safeAlpha); + + // The scalar path computes ((c * 255) + (a >> 1)) / a with integer + // division. Floor the positive quotient before converting so SIMD does + // not use the default round-to-nearest conversion and drift by one. + Vector128 unpremultipliedR = Vector128.Min( + byteMax, + Vector128.ConvertToInt32(Vector128.Floor(Vector128.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF))); + + Vector128 unpremultipliedG = Vector128.Min( + byteMax, + Vector128.ConvertToInt32(Vector128.Floor(Vector128.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF))); + + Vector128 unpremultipliedB = Vector128.Min( + byteMax, + Vector128.ConvertToInt32(Vector128.Floor(Vector128.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF))); + + // ConditionalSelect applies the expensive unpremultiply only to pixels + // where alpha is between 1 and 254; alpha 0 and 255 lanes keep the + // shuffled channel values exactly as the scalar path does. + Vector128 finalR = Vector128.ConditionalSelect(partialMask, unpremultipliedR, r); + Vector128 finalG = Vector128.ConditionalSelect(partialMask, unpremultipliedG, g); + Vector128 finalB = Vector128.ConditionalSelect(partialMask, unpremultipliedB, b); + + // Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so + // shifting the unpacked channels back to byte offsets 0, 1, 2, and 3 + // recreates the in-memory RGBA bytes for the unaligned store. + Vector128 result = + finalR | + Vector128.ShiftLeft(finalG, 8) | + Vector128.ShiftLeft(finalB, 16) | + Vector128.ShiftLeft(alpha, 24); + + Unsafe.WriteUnaligned(ref blockRef, result.AsByte()); + } + + return i; + } + } +} diff --git a/ImageSharp/Formats/Png/PngDecoderOptions.cs b/ImageSharp/Formats/Png/PngDecoderOptions.cs new file mode 100644 index 0000000..85e6b51 --- /dev/null +++ b/ImageSharp/Formats/Png/PngDecoderOptions.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Configuration options for decoding png images. + /// + public sealed class PngDecoderOptions : ISpecializedDecoderOptions + { + /// + public DecoderOptions GeneralOptions { get; init; } = new(); + + /// + /// Gets the maximum memory in bytes that a zTXt, sPLT, iTXt, iCCP, or unknown chunk can occupy when decompressed. + /// Defaults to 8MB + /// + public int MaxUncompressedAncillaryChunkSizeBytes { get; init; } = 8 * 1024 * 1024; // 8MB + } +} diff --git a/ImageSharp/Formats/Png/PngEncoder.cs b/ImageSharp/Formats/Png/PngEncoder.cs new file mode 100644 index 0000000..5b764ee --- /dev/null +++ b/ImageSharp/Formats/Png/PngEncoder.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Image encoder for writing image data to a stream in png format. + /// + public class PngEncoder : QuantizingAnimatedImageEncoder + { + /// + /// Gets the number of bits per sample or per palette index (not per pixel). + /// Not all values are allowed for all values. + /// + public PngBitDepth? BitDepth { get; init; } + + /// + /// Gets the color type. + /// + public PngColorType? ColorType { get; init; } + + /// + /// Gets the filter method. + /// + public PngFilterMethod? FilterMethod { get; init; } + + /// + /// Gets the compression level 1-9. + /// Defaults to . + /// + public PngCompressionLevel CompressionLevel { get; init; } = PngCompressionLevel.DefaultCompression; + + /// + /// Gets the threshold of characters in text metadata, when compression should be used. + /// + public int TextCompressionThreshold { get; init; } = 1024; + + /// + /// Gets the gamma value, that will be written the image. + /// + /// The gamma value of the image. + public float? Gamma { get; init; } + + /// + /// Gets a value indicating whether this instance should write an Adam7 interlaced image. + /// + public PngInterlaceMode? InterlaceMethod { get; init; } + + /// + /// Gets the chunk filter method. This allows to filter ancillary chunks. + /// + public PngChunkFilter? ChunkFilter { get; init; } + + /// + protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) + { + using PngEncoderCore encoder = new(image.Configuration, this); + encoder.Encode(image, stream, cancellationToken); + } + } +} diff --git a/ImageSharp/Formats/Png/PngEncoderCore.cs b/ImageSharp/Formats/Png/PngEncoderCore.cs new file mode 100644 index 0000000..947cddf --- /dev/null +++ b/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -0,0 +1,1849 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Hashing; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Text; +using System.Threading; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Png.Chunks; +using SixLabors.ImageSharp.Formats.Png.Filters; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Performs the png encoding operation. + /// + internal sealed class PngEncoderCore : IDisposable + { + /// + /// The maximum block size, defaults at 64k for uncompressed blocks. + /// + private const int MaxBlockSize = 65535; + + /// + /// Used the manage memory allocations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The configuration instance for the encoding operation. + /// + private readonly Configuration configuration; + + /// + /// Reusable buffer for writing chunk data. + /// + private ScratchBuffer chunkDataBuffer; // mutable struct, don't make readonly + + /// + /// The encoder with options + /// + private readonly PngEncoder encoder; + + /// + /// The gamma value + /// + private float? gamma; + + /// + /// The color type. + /// + private PngColorType colorType; + + /// + /// The number of bits per sample or per palette index (not per pixel). + /// + private byte bitDepth; + + /// + /// The filter method used to prefilter the encoded pixels before compression. + /// + private PngFilterMethod filterMethod; + + /// + /// Gets the interlace mode. + /// + private PngInterlaceMode interlaceMode; + + /// + /// The chunk filter method. This allows to filter ancillary chunks. + /// + private PngChunkFilter chunkFilter; + + /// + /// A value indicating whether to use 16 bit encoding for supported color types. + /// + private bool use16Bit; + + /// + /// The number of bytes per pixel. + /// + private int bytesPerPixel; + + /// + /// The image width. + /// + private int width; + + /// + /// The image height. + /// + private int height; + + /// + /// The raw data of previous scanline. + /// + private IMemoryOwner previousScanline = null!; + + /// + /// The raw data of current scanline. + /// + private IMemoryOwner currentScanline = null!; + + /// + /// The color profile name. + /// + private const string ColorProfileName = "ICC Profile"; + + /// + /// The encoder quantizer, if present. + /// + private IQuantizer? quantizer; + + /// + /// The default background color of the canvas when animating. + /// This color may be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when a frame disposal mode is . + /// + private Color? backgroundColor; + + /// + /// The number of times any animation is repeated. + /// + private readonly ushort? repeatCount; + + /// + /// Whether the root frame is shown as part of the animated sequence. + /// + private readonly bool? animateRootFrame; + + /// + /// A reusable Crc32 hashing instance. + /// + private readonly Crc32 crc32 = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The encoder with options. + public PngEncoderCore(Configuration configuration, PngEncoder encoder) + { + this.configuration = configuration; + this.memoryAllocator = configuration.MemoryAllocator; + this.encoder = encoder; + this.quantizer = encoder.Quantizer; + this.repeatCount = encoder.RepeatCount; + this.animateRootFrame = encoder.AnimateRootFrame; + } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + this.width = image.Width; + this.height = image.Height; + + ImageMetadata metadata = image.Metadata; + PngMetadata pngMetadata = metadata.ClonePngMetadata(); + this.SanitizeAndSetEncoderOptions(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel); + + stream.Write(PngConstants.HeaderBytes); + + ImageFrame? clonedFrame = null; + ImageFrame currentFrame = image.Frames.RootFrame; + IndexedImageFrame? quantized = null; + PaletteQuantizer? paletteQuantizer = null; + Buffer2DRegion currentFrameRegion = currentFrame.PixelBuffer.GetRegion(); + + try + { + int currentFrameIndex = 0; + + bool clearTransparency = EncodingUtilities.ShouldReplaceTransparentPixels(this.encoder.TransparentColorMode); + + // No need to clone when quantizing. The quantizer will do it for us. + // TODO: We should really try to avoid the clone entirely. + if (clearTransparency && this.colorType is not PngColorType.Palette) + { + currentFrame = clonedFrame = currentFrame.Clone(); + currentFrameRegion = currentFrame.PixelBuffer.GetRegion(); + EncodingUtilities.ReplaceTransparentPixels(this.configuration, in currentFrameRegion); + } + + // Do not move this. We require an accurate bit depth for the header chunk. + quantized = this.CreateQuantizedImageAndUpdateBitDepth( + pngMetadata, + image, + currentFrame, + currentFrame.Bounds, + null); + + this.WriteHeaderChunk(stream); + this.WriteGammaChunk(stream); + this.WriteCicpChunk(stream, metadata); + this.WriteColorProfileChunk(stream, metadata); + this.WritePaletteChunk(stream, quantized); + this.WriteTransparencyChunk(stream, pngMetadata); + this.WritePhysicalChunk(stream, metadata); + this.WriteExifChunk(stream, metadata); + this.WriteXmpChunk(stream, metadata); + this.WriteIptcChunk(stream, metadata); + this.WriteTextChunks(stream, pngMetadata); + + if (image.Frames.Count > 1) + { + this.WriteAnimationControlChunk( + stream, + (uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)), + this.repeatCount ?? pngMetadata.RepeatCount); + } + + // If the first frame isn't animated, write it as usual and skip it when writing animated frames + bool userAnimateRootFrame = this.animateRootFrame == true; + if ((!userAnimateRootFrame && !pngMetadata.AnimateRootFrame) || image.Frames.Count == 1) + { + cancellationToken.ThrowIfCancellationRequested(); + FrameControl frameControl = new((uint)this.width, (uint)this.height); + this.WriteDataChunks(in frameControl, in currentFrameRegion, quantized, stream, false); + currentFrameIndex++; + } + + if (image.Frames.Count > 1) + { + // Write the first animated frame. + currentFrame = image.Frames[currentFrameIndex]; + currentFrameRegion = currentFrame.PixelBuffer.GetRegion(); + + PngFrameMetadata frameMetadata = currentFrame.Metadata.GetPngMetadata(); + FrameDisposalMode previousDisposal = frameMetadata.DisposalMode; + FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds, 0); + uint sequenceNumber = 1; + if (pngMetadata.AnimateRootFrame) + { + this.WriteDataChunks(in frameControl, in currentFrameRegion, quantized, stream, false); + } + else + { + sequenceNumber += this.WriteDataChunks(in frameControl, in currentFrameRegion, quantized, stream, true); + } + + currentFrameIndex++; + + // Capture the global palette for reuse on subsequent frames. + ReadOnlyMemory previousPalette = quantized?.Palette.ToArray(); + + if (!previousPalette.IsEmpty) + { + // Use the previously derived global palette and a shared quantizer to + // quantize the subsequent frames. This allows us to cache the color matching resolution. + paletteQuantizer ??= new PaletteQuantizer( + this.configuration, + this.quantizer!.Options, + previousPalette); + } + + // Write following frames. + ImageFrame previousFrame = image.Frames.RootFrame; + + // This frame is reused to store de-duplicated pixel buffers. + using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size); + + for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++) + { + cancellationToken.ThrowIfCancellationRequested(); + + ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; + + currentFrame = image.Frames[currentFrameIndex]; + currentFrameRegion = currentFrame.PixelBuffer.GetRegion(); + + ImageFrame? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null; + + frameMetadata = currentFrame.Metadata.GetPngMetadata(); + + // Determine whether to blend the current frame over the existing canvas. + // Blending is applied only when the blend method is 'Over' (source-over blending) + // and when the frame's disposal method is not 'RestoreToPrevious', which indicates that + // the frame should not permanently alter the canvas. + bool blend = frameMetadata.BlendMode == FrameBlendMode.Over + && frameMetadata.DisposalMode != FrameDisposalMode.RestoreToPrevious; + + // Establish the background color for the current frame. + // If the disposal method is 'RestoreToBackground', use the predefined background color; + // otherwise, use transparent, as no explicit background restoration is needed. + Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground + ? this.backgroundColor.Value + : Color.Transparent; + + (bool difference, Rectangle bounds) = + AnimationUtilities.DeDuplicatePixels( + image.Configuration, + prev, + currentFrame, + nextFrame, + encodingFrame, + background, + blend); + + if (clearTransparency && this.colorType is not PngColorType.Palette) + { + EncodingUtilities.ReplaceTransparentPixels(encodingFrame); + } + + // Each frame control sequence number must be incremented by the number of frame data chunks that follow. + frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, sequenceNumber); + + // Dispose of previous quantized frame and reassign. + quantized?.Dispose(); + + quantized = this.CreateQuantizedFrame( + this.encoder, + this.colorType, + this.bitDepth, + pngMetadata, + image, + encodingFrame, + bounds, + paletteQuantizer, + default); + + Buffer2DRegion encodingFrameRegion = encodingFrame.PixelBuffer.GetRegion(bounds); + sequenceNumber += this.WriteDataChunks(in frameControl, in encodingFrameRegion, quantized, stream, true) + 1; + + previousFrame = currentFrame; + previousDisposal = frameMetadata.DisposalMode; + } + } + + this.WriteEndChunk(stream); + + stream.Flush(); + } + finally + { + // Dispose of allocations from final frame. + clonedFrame?.Dispose(); + quantized?.Dispose(); + paletteQuantizer?.Dispose(); + } + } + + /// + public void Dispose() + { + this.previousScanline?.Dispose(); + this.currentScanline?.Dispose(); + } + + /// + /// Creates the quantized image and calculates and sets the bit depth. + /// + /// The type of the pixel. + /// The image metadata. + /// The image. + /// The current image frame. + /// The area of interest within the frame. + /// The quantizer containing any previously derived palette. + /// The quantized image. + private IndexedImageFrame? CreateQuantizedImageAndUpdateBitDepth( + PngMetadata metadata, + Image image, + ImageFrame frame, + Rectangle bounds, + PaletteQuantizer? paletteQuantizer) + where TPixel : unmanaged, IPixel + { + PngFrameMetadata frameMetadata = frame.Metadata.GetPngMetadata(); + Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground + ? this.backgroundColor ?? Color.Transparent + : Color.Transparent; + + IndexedImageFrame? quantized = this.CreateQuantizedFrame( + this.encoder, + this.colorType, + this.bitDepth, + metadata, + image, + frame, + bounds, + paletteQuantizer, + background); + + this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized); + return quantized; + } + + /// Collects a row of grayscale pixels. + /// The pixel format. + /// The image row span. + private void CollectGrayscaleBytes(ReadOnlySpan rowSpan) + where TPixel : unmanaged, IPixel + { + Span rawScanlineSpan = this.currentScanline.GetSpan(); + + if (this.colorType == PngColorType.Grayscale) + { + if (this.use16Bit) + { + // 16 bit grayscale + using IMemoryOwner luminanceBuffer = this.memoryAllocator.Allocate(rowSpan.Length); + Span luminanceSpan = luminanceBuffer.GetSpan(); + ref L16 luminanceRef = ref MemoryMarshal.GetReference(luminanceSpan); + PixelOperations.Instance.ToL16(this.configuration, rowSpan, luminanceSpan); + + // Can't map directly to byte array as it's big-endian. + for (int x = 0, o = 0; x < luminanceSpan.Length; x++, o += 2) + { + L16 luminance = Unsafe.Add(ref luminanceRef, (uint)x); + BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance.PackedValue); + } + } + else if (this.bitDepth == 8) + { + // 8 bit grayscale + PixelOperations.Instance.ToL8Bytes( + this.configuration, + rowSpan, + rawScanlineSpan, + rowSpan.Length); + } + else + { + // 1, 2, and 4 bit grayscale + using IMemoryOwner temp = this.memoryAllocator.Allocate(rowSpan.Length, AllocationOptions.Clean); + int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(this.bitDepth) - 1); + Span tempSpan = temp.GetSpan(); + + // We need to first create an array of luminance bytes then scale them down to the correct bit depth. + PixelOperations.Instance.ToL8Bytes( + this.configuration, + rowSpan, + tempSpan, + rowSpan.Length); + PngEncoderHelpers.ScaleDownFrom8BitArray(tempSpan, rawScanlineSpan, this.bitDepth, scaleFactor); + } + } + else if (this.use16Bit) + { + // 16 bit grayscale + alpha + using IMemoryOwner laBuffer = this.memoryAllocator.Allocate(rowSpan.Length); + Span laSpan = laBuffer.GetSpan(); + ref La32 laRef = ref MemoryMarshal.GetReference(laSpan); + PixelOperations.Instance.ToLa32(this.configuration, rowSpan, laSpan); + + // Can't map directly to byte array as it's big endian. + for (int x = 0, o = 0; x < laSpan.Length; x++, o += 4) + { + La32 la = Unsafe.Add(ref laRef, (uint)x); + BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), la.L); + BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), la.A); + } + } + else + { + // 8 bit grayscale + alpha + PixelOperations.Instance.ToLa16Bytes( + this.configuration, + rowSpan, + rawScanlineSpan, + rowSpan.Length); + } + } + + /// + /// Collects a row of true color pixel data. + /// + /// The pixel format. + /// The row span. + private void CollectTPixelBytes(ReadOnlySpan rowSpan) + where TPixel : unmanaged, IPixel + { + Span rawScanlineSpan = this.currentScanline.GetSpan(); + + switch (this.bytesPerPixel) + { + case 4: + + // 8 bit Rgba + PixelOperations.Instance.ToRgba32Bytes( + this.configuration, + rowSpan, + rawScanlineSpan, + rowSpan.Length); + break; + + case 3: + + // 8 bit Rgb + PixelOperations.Instance.ToRgb24Bytes( + this.configuration, + rowSpan, + rawScanlineSpan, + rowSpan.Length); + break; + + case 8: + + // 16 bit Rgba + using (IMemoryOwner rgbaBuffer = this.memoryAllocator.Allocate(rowSpan.Length)) + { + Span rgbaSpan = rgbaBuffer.GetSpan(); + ref Rgba64 rgbaRef = ref MemoryMarshal.GetReference(rgbaSpan); + PixelOperations.Instance.ToRgba64(this.configuration, rowSpan, rgbaSpan); + + // Can't map directly to byte array as it's big endian. + for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 8) + { + Rgba64 rgba = Unsafe.Add(ref rgbaRef, (uint)x); + BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), rgba.R); + BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgba.G); + BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 4, 2), rgba.B); + BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 6, 2), rgba.A); + } + } + + break; + + default: + + // 16 bit Rgb + using (IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(rowSpan.Length)) + { + Span rgbSpan = rgbBuffer.GetSpan(); + ref Rgb48 rgbRef = ref MemoryMarshal.GetReference(rgbSpan); + PixelOperations.Instance.ToRgb48(this.configuration, rowSpan, rgbSpan); + + // Can't map directly to byte array as it's big endian. + for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 6) + { + Rgb48 rgb = Unsafe.Add(ref rgbRef, (uint)x); + BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), rgb.R); + BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgb.G); + BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 4, 2), rgb.B); + } + } + + break; + } + } + + /// + /// Encodes the pixel data line by line. + /// Each scanline is encoded in the most optimal manner to improve compression. + /// + /// The pixel format. + /// The row span. + /// The quantized pixels. Can be null. + /// The row. + private void CollectPixelBytes(ReadOnlySpan rowSpan, IndexedImageFrame? quantized, int row) + where TPixel : unmanaged, IPixel + { + switch (this.colorType) + { + case PngColorType.Palette: + if (this.bitDepth < 8) + { + PngEncoderHelpers.ScaleDownFrom8BitArray(quantized!.DangerousGetRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth); + } + else + { + quantized?.DangerousGetRowSpan(row).CopyTo(this.currentScanline.GetSpan()); + } + + break; + case PngColorType.Grayscale: + case PngColorType.GrayscaleWithAlpha: + this.CollectGrayscaleBytes(rowSpan); + break; + default: + this.CollectTPixelBytes(rowSpan); + break; + } + } + + /// + /// Apply the line filter for the raw scanline to enable better compression. + /// + /// The filtered buffer. + /// Used for attempting optimized filtering. + private void FilterPixelBytes(ref Span filter, ref Span attempt) + { + switch (this.filterMethod) + { + case PngFilterMethod.None: + NoneFilter.Encode(this.currentScanline.GetSpan(), filter); + break; + case PngFilterMethod.Sub: + SubFilter.Encode(this.currentScanline.GetSpan(), filter, this.bytesPerPixel, out int _); + break; + + case PngFilterMethod.Up: + UpFilter.Encode(this.currentScanline.GetSpan(), this.previousScanline.GetSpan(), filter, out int _); + break; + + case PngFilterMethod.Average: + AverageFilter.Encode(this.currentScanline.GetSpan(), this.previousScanline.GetSpan(), filter, (uint)this.bytesPerPixel, out int _); + break; + + case PngFilterMethod.Paeth: + PaethFilter.Encode(this.currentScanline.GetSpan(), this.previousScanline.GetSpan(), filter, this.bytesPerPixel, out int _); + break; + default: + this.ApplyOptimalFilteredScanline(ref filter, ref attempt); + break; + } + } + + /// + /// Collects the pixel data line by line for compressing. + /// Each scanline is filtered in the most optimal manner to improve compression. + /// + /// The pixel format. + /// The row span. + /// The filtered buffer. + /// Used for attempting optimized filtering. + /// The quantized pixels. Can be . + /// The row number. + private void CollectAndFilterPixelRow( + ReadOnlySpan rowSpan, + ref Span filter, + ref Span attempt, + IndexedImageFrame? quantized, + int row) + where TPixel : unmanaged, IPixel + { + this.CollectPixelBytes(rowSpan, quantized, row); + this.FilterPixelBytes(ref filter, ref attempt); + } + + /// + /// Encodes the indexed pixel data (with palette) for Adam7 interlaced mode. + /// + /// The row span. + /// The filtered buffer. + /// Used for attempting optimized filtering. + private void EncodeAdam7IndexedPixelRow( + ReadOnlySpan row, + ref Span filter, + ref Span attempt) + { + // CollectPixelBytes + if (this.bitDepth < 8) + { + PngEncoderHelpers.ScaleDownFrom8BitArray(row, this.currentScanline.GetSpan(), this.bitDepth); + } + else + { + row.CopyTo(this.currentScanline.GetSpan()); + } + + this.FilterPixelBytes(ref filter, ref attempt); + } + + /// + /// Applies all PNG filters to the given scanline and returns the filtered scanline that is deemed + /// to be most compressible, using lowest total variation as proxy for compressibility. + /// + /// The filtered buffer. + /// Used for attempting optimized filtering. + private void ApplyOptimalFilteredScanline(ref Span filter, ref Span attempt) + { + // Palette images don't compress well with adaptive filtering. + // Nor do images comprising a single row. + if (this.colorType == PngColorType.Palette || this.height == 1 || this.bitDepth < 8) + { + NoneFilter.Encode(this.currentScanline.GetSpan(), filter); + return; + } + + Span current = this.currentScanline.GetSpan(); + Span previous = this.previousScanline.GetSpan(); + + int min = int.MaxValue; + SubFilter.Encode(current, attempt, this.bytesPerPixel, out int sum); + if (sum < min) + { + min = sum; + RuntimeUtility.Swap(ref filter, ref attempt); + } + + UpFilter.Encode(current, previous, attempt, out sum); + if (sum < min) + { + min = sum; + RuntimeUtility.Swap(ref filter, ref attempt); + } + + AverageFilter.Encode(current, previous, attempt, (uint)this.bytesPerPixel, out sum); + if (sum < min) + { + min = sum; + RuntimeUtility.Swap(ref filter, ref attempt); + } + + PaethFilter.Encode(current, previous, attempt, this.bytesPerPixel, out sum); + if (sum < min) + { + RuntimeUtility.Swap(ref filter, ref attempt); + } + } + + /// + /// Writes the header chunk to the stream. + /// + /// The containing image data. + private void WriteHeaderChunk(Stream stream) + { + PngHeader header = new( + width: this.width, + height: this.height, + bitDepth: this.bitDepth, + colorType: this.colorType, + compressionMethod: 0, // None + filterMethod: 0, + interlaceMethod: this.interlaceMode); + + header.WriteTo(this.chunkDataBuffer.Span); + + this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer.Span, 0, PngHeader.Size); + } + + /// + /// Writes the animation control chunk to the stream. + /// + /// The containing image data. + /// The number of frames. + /// The number of times to loop this APNG. + private void WriteAnimationControlChunk(Stream stream, uint framesCount, uint playsCount) + { + AnimationControl acTL = new(framesCount, playsCount); + + acTL.WriteTo(this.chunkDataBuffer.Span); + + this.WriteChunk(stream, PngChunkType.AnimationControl, this.chunkDataBuffer.Span, 0, AnimationControl.Size); + } + + /// + /// Writes the palette chunk to the stream. + /// Should be written before the first IDAT chunk. + /// + /// The pixel format. + /// The containing image data. + /// The quantized frame. + private void WritePaletteChunk(Stream stream, IndexedImageFrame? quantized) + where TPixel : unmanaged, IPixel + { + if (quantized is null) + { + return; + } + + // Grab the palette and write it to the stream. + ReadOnlySpan palette = quantized.Palette.Span; + int paletteLength = palette.Length; + int colorTableLength = paletteLength * Unsafe.SizeOf(); + bool hasAlpha = false; + + using IMemoryOwner colorTable = this.memoryAllocator.Allocate(colorTableLength); + using IMemoryOwner alphaTable = this.memoryAllocator.Allocate(paletteLength); + + ref Rgb24 colorTableRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast(colorTable.GetSpan())); + ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan()); + + // Bulk convert our palette to RGBA to allow assignment to tables. + using IMemoryOwner rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate(paletteLength); + Span rgbaPaletteSpan = rgbaOwner.GetSpan(); + PixelOperations.Instance.ToRgba32(quantized.Configuration, quantized.Palette.Span, rgbaPaletteSpan); + ref Rgba32 rgbaPaletteRef = ref MemoryMarshal.GetReference(rgbaPaletteSpan); + + // Loop, assign, and extract alpha values from the palette. + for (int i = 0; i < paletteLength; i++) + { + Rgba32 rgba = Unsafe.Add(ref rgbaPaletteRef, (uint)i); + byte alpha = rgba.A; + + Unsafe.Add(ref colorTableRef, (uint)i) = rgba.Rgb; + hasAlpha = hasAlpha || alpha < byte.MaxValue; + Unsafe.Add(ref alphaTableRef, (uint)i) = alpha; + } + + this.WriteChunk(stream, PngChunkType.Palette, colorTable.GetSpan(), 0, colorTableLength); + + // Write the transparency data + if (hasAlpha) + { + this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.GetSpan(), 0, paletteLength); + } + } + + /// + /// Writes the physical dimension information to the stream. + /// Should be written before IDAT chunk. + /// + /// The containing image data. + /// The image metadata. + private void WritePhysicalChunk(Stream stream, ImageMetadata meta) + { + if (this.chunkFilter.HasFlag(PngChunkFilter.ExcludePhysicalChunk)) + { + return; + } + + PngPhysical.FromMetadata(meta).WriteTo(this.chunkDataBuffer.Span); + + this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer.Span, 0, PngPhysical.Size); + } + + /// + /// Writes the eXIf chunk to the stream, if any EXIF Profile values are present in the metadata. + /// + /// The containing image data. + /// The image metadata. + private void WriteExifChunk(Stream stream, ImageMetadata meta) + { + if ((this.chunkFilter & PngChunkFilter.ExcludeExifChunk) == PngChunkFilter.ExcludeExifChunk) + { + return; + } + + if (meta.ExifProfile is null || meta.ExifProfile.Values.Count == 0) + { + return; + } + + this.WriteChunk(stream, PngChunkType.Exif, meta.ExifProfile.ToByteArray()); + } + + /// + /// Writes an iTXT chunk, containing the XMP metadata 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.chunkFilter & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks) + { + return; + } + + if (meta.XmpProfile is null) + { + return; + } + + byte[]? xmpData = meta.XmpProfile.Data; + + if (xmpData?.Length is 0 or null) + { + 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). + Span iTxtHeader = payload[bytesWritten..]; + iTxtHeader[4] = 0; + iTxtHeader[3] = 0; + iTxtHeader[2] = 0; + iTxtHeader[1] = 0; + iTxtHeader[0] = 0; + bytesWritten += 5; + + // And the XMP data itself. + xmpData.CopyTo(payload[bytesWritten..]); + this.WriteChunk(stream, PngChunkType.InternationalText, payload); + } + + /// + /// Writes the IPTC metadata from the specified image metadata to the provided stream as a compressed zTXt chunk in + /// PNG format, if IPTC data is present. + /// + /// The containing image data. + /// The image metadata. + private void WriteIptcChunk(Stream stream, ImageMetadata meta) + { + if ((this.chunkFilter & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks) + { + return; + } + + if (meta.IptcProfile is null || !meta.IptcProfile.Values.Any()) + { + return; + } + + meta.IptcProfile.UpdateData(); + + byte[]? iptcData = meta.IptcProfile.Data; + if (iptcData?.Length is 0 or null) + { + return; + } + + // For interoperability, wrap raw IPTC (IIM) in a Photoshop IRB (8BIM, resource 0x0404), + // since "Raw profile type iptc" commonly stores IRB payloads. + using IMemoryOwner irb = this.BuildPhotoshopIrbForIptc(iptcData); + + Span irbSpan = irb.GetSpan(); + + // Build "raw profile" textual wrapper: + // "IPTC profile\n\n\n" + string rawProfileText = BuildRawProfileText("IPTC profile", irbSpan); + + byte[] compressedData = this.GetZlibCompressedBytes(PngConstants.Encoding.GetBytes(rawProfileText)); + + // zTXt layout: keyword (latin-1) + 0 + compression-method(0) + compressed-data + const string iptcRawProfileKeyword = PngConstants.IptcRawProfileKeyword; + int payloadLength = iptcRawProfileKeyword.Length + compressedData.Length + 2; + + using IMemoryOwner payload = this.memoryAllocator.Allocate(payloadLength); + Span outputBytes = payload.GetSpan(); + + PngConstants.Encoding.GetBytes(iptcRawProfileKeyword).CopyTo(outputBytes); + int bytesWritten = iptcRawProfileKeyword.Length; + outputBytes[bytesWritten++] = 0; // Null separator + outputBytes[bytesWritten++] = 0; // Compression method: deflate + compressedData.CopyTo(outputBytes[bytesWritten..]); + + this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes); + } + + /// + /// Builds a Photoshop Image Resource Block (IRB) containing the specified IPTC-IIM data. + /// + /// The returned IRB uses resource ID 0x0404 and an empty Pascal string for the name, as required + /// for IPTC-NAA record embedding in Photoshop files. The data is padded to ensure even length, as specified by the + /// IRB format. + /// + /// The IPTC-IIM data to embed in the IRB, provided as a read-only span of bytes. The data is included as-is in the + /// resulting block. + /// + /// + /// A byte array representing the Photoshop IRB with the embedded IPTC-IIM data, formatted according to the + /// Photoshop specification. + /// + private IMemoryOwner BuildPhotoshopIrbForIptc(ReadOnlySpan iptcIim) + { + // IRB block: + // 4 bytes: "8BIM" + // 2 bytes: resource id 0x0404 (big endian) + // 2 bytes: pascal name (len=0) + pad to even => 0x00 0x00 + // 4 bytes: data size (big endian) + // n bytes: IPTC-IIM data + // pad to even + int pad = (iptcIim.Length & 1) != 0 ? 1 : 0; + IMemoryOwner bufferOwner = this.memoryAllocator.Allocate(4 + 2 + 2 + 4 + iptcIim.Length + pad); + Span buffer = bufferOwner.GetSpan(); + + int bytesWritten = 0; + PngConstants.EightBim.CopyTo(buffer); + bytesWritten += 4; + + buffer[bytesWritten++] = 0x04; + buffer[bytesWritten++] = 0x04; + + buffer[bytesWritten++] = 0x00; // Pascal name length + buffer[bytesWritten++] = 0x00; // pad to even + + int size = iptcIim.Length; + buffer[bytesWritten++] = (byte)((size >> 24) & 0xFF); + buffer[bytesWritten++] = (byte)((size >> 16) & 0xFF); + buffer[bytesWritten++] = (byte)((size >> 8) & 0xFF); + buffer[bytesWritten++] = (byte)(size & 0xFF); + + iptcIim.CopyTo(buffer[bytesWritten..]); + + // Final pad byte already zero-initialized if needed + return bufferOwner; + } + + /// + /// Builds a formatted text representation of a binary profile, including a header, the payload length, and the + /// payload as hexadecimal text. + /// + /// + /// The hexadecimal payload is formatted with 64 bytes per line to improve readability. The + /// output consists of the header line, a line with the payload length, and one or more lines of hexadecimal + /// text. + /// + /// The header text to include at the beginning of the profile. This is written as the first line of the output. + /// The binary payload to encode as hexadecimal text. The payload is split into lines of 64 bytes each. + /// + /// A string containing the header, the payload length, and the hexadecimal representation of the payload, each on + /// separate lines. + /// + private static string BuildRawProfileText(string header, ReadOnlySpan payload) + { + // Hex text can be multi-line + // Use 64 bytes per line (128 hex chars) to keep the chunk readable. + const int bytesPerLine = 64; + + int hexChars = payload.Length * 2; + int lineCount = (payload.Length + (bytesPerLine - 1)) / bytesPerLine; + int newlineCount = 2 + lineCount; // header line + length line + hex lines + int capacity = header.Length + 32 + hexChars + newlineCount; + + StringBuilder sb = new(capacity); + sb.Append(header).Append('\n'); + sb.Append(payload.Length).Append('\n'); + + int i = 0; + while (i < payload.Length) + { + int take = Math.Min(bytesPerLine, payload.Length - i); + AppendHex(sb, payload.Slice(i, take)); + sb.Append('\n'); + i += take; + } + + return sb.ToString(); + } + + private static void AppendHex(StringBuilder sb, ReadOnlySpan data) + { + const string hex = "0123456789ABCDEF"; + + for (int i = 0; i < data.Length; i++) + { + byte b = data[i]; + _ = sb.Append(hex[b >> 4]); + _ = sb.Append(hex[b & 0x0F]); + } + } + + /// + /// Writes the CICP profile chunk + /// + /// The containing image data. + /// The image meta data. + /// CICP matrix coefficients other than Identity are not supported in PNG. + private void WriteCicpChunk(Stream stream, ImageMetadata metaData) + { + if (metaData.CicpProfile is null) + { + return; + } + + // by spec, the matrix coefficients must be set to Identity + if (metaData.CicpProfile.MatrixCoefficients != Metadata.Profiles.Cicp.CicpMatrixCoefficients.Identity) + { + throw new NotSupportedException("CICP matrix coefficients other than Identity are not supported in PNG"); + } + + Span outputBytes = this.chunkDataBuffer.Span[..4]; + outputBytes[0] = (byte)metaData.CicpProfile.ColorPrimaries; + outputBytes[1] = (byte)metaData.CicpProfile.TransferCharacteristics; + outputBytes[2] = (byte)metaData.CicpProfile.MatrixCoefficients; + outputBytes[3] = (byte)(metaData.CicpProfile.FullRange ? 1 : 0); + this.WriteChunk(stream, PngChunkType.Cicp, outputBytes); + } + + /// + /// Writes the color profile chunk. + /// + /// The stream to write to. + /// The image meta data. + private void WriteColorProfileChunk(Stream stream, ImageMetadata metaData) + { + if (metaData.IccProfile is null) + { + return; + } + + byte[] iccProfileBytes = metaData.IccProfile.ToByteArray(); + + byte[] compressedData = this.GetZlibCompressedBytes(iccProfileBytes); + int payloadLength = ColorProfileName.Length + compressedData.Length + 2; + + using IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength); + Span outputBytes = owner.GetSpan(); + PngConstants.Encoding.GetBytes(ColorProfileName).CopyTo(outputBytes); + int bytesWritten = ColorProfileName.Length; + outputBytes[bytesWritten++] = 0; // Null separator. + outputBytes[bytesWritten++] = 0; // Compression. + compressedData.CopyTo(outputBytes[bytesWritten..]); + this.WriteChunk(stream, PngChunkType.EmbeddedColorProfile, outputBytes); + } + + /// + /// 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. + /// + /// The containing image data. + /// The image metadata. + private void WriteTextChunks(Stream stream, PngMetadata meta) + { + if ((this.chunkFilter & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks) + { + return; + } + + const int maxLatinCode = 255; + foreach (PngTextData textData in meta.TextData) + { + bool hasUnicodeCharacters = textData.Value.Any(c => c > maxLatinCode); + + if (hasUnicodeCharacters || !string.IsNullOrWhiteSpace(textData.LanguageTag) || !string.IsNullOrWhiteSpace(textData.TranslatedKeyword)) + { + // Write iTXt chunk. + byte[] keywordBytes = PngConstants.Encoding.GetBytes(textData.Keyword); + byte[] textBytes = textData.Value.Length > this.encoder.TextCompressionThreshold + ? this.GetZlibCompressedBytes(PngConstants.TranslatedEncoding.GetBytes(textData.Value)) + : PngConstants.TranslatedEncoding.GetBytes(textData.Value); + + byte[] translatedKeyword = PngConstants.TranslatedEncoding.GetBytes(textData.TranslatedKeyword); + byte[] languageTag = PngConstants.LanguageEncoding.GetBytes(textData.LanguageTag); + + int payloadLength = keywordBytes.Length + textBytes.Length + translatedKeyword.Length + languageTag.Length + 5; + + using IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength); + Span outputBytes = owner.GetSpan(); + keywordBytes.CopyTo(outputBytes); + int bytesWritten = keywordBytes.Length; + outputBytes[bytesWritten++] = 0; + if (textData.Value.Length > this.encoder.TextCompressionThreshold) + { + // Indicate that the text is compressed. + outputBytes[bytesWritten++] = 1; + } + else + { + outputBytes[bytesWritten++] = 0; + } + + outputBytes[bytesWritten++] = 0; + languageTag.CopyTo(outputBytes[bytesWritten..]); + bytesWritten += languageTag.Length; + outputBytes[bytesWritten++] = 0; + translatedKeyword.CopyTo(outputBytes[bytesWritten..]); + bytesWritten += translatedKeyword.Length; + outputBytes[bytesWritten++] = 0; + textBytes.CopyTo(outputBytes[bytesWritten..]); + this.WriteChunk(stream, PngChunkType.InternationalText, outputBytes); + } + else if (textData.Value.Length > this.encoder.TextCompressionThreshold) + { + // Write zTXt chunk. + byte[] compressedData = this.GetZlibCompressedBytes(PngConstants.Encoding.GetBytes(textData.Value)); + 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; // Null separator. + outputBytes[bytesWritten++] = 0; // Compression. + compressedData.CopyTo(outputBytes[bytesWritten..]); + this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes); + } + else + { + // Write tEXt chunk. + 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[bytesWritten..]); + this.WriteChunk(stream, PngChunkType.Text, outputBytes); + } + } + } + + /// + /// Compresses a given text using Zlib compression. + /// + /// The bytes to compress. + /// The compressed byte array. + private byte[] GetZlibCompressedBytes(byte[] dataBytes) + { + using MemoryStream memoryStream = new(); + using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.encoder.CompressionLevel)) + { + deflateStream.Write(dataBytes); + } + + return memoryStream.ToArray(); + } + + /// + /// Writes the gamma information to the stream. + /// Should be written before PLTE and IDAT chunk. + /// + /// The containing image data. + private void WriteGammaChunk(Stream stream) + { + if ((this.chunkFilter & PngChunkFilter.ExcludeGammaChunk) == PngChunkFilter.ExcludeGammaChunk) + { + return; + } + + if (this.gamma > 0) + { + // 4-byte unsigned integer of gamma * 100,000. + uint gammaValue = (uint)(this.gamma * 100_000F); + + BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.Span[..4], gammaValue); + + this.WriteChunk(stream, PngChunkType.Gamma, this.chunkDataBuffer.Span, 0, 4); + } + } + + /// + /// Writes the transparency chunk to the stream. + /// Should be written after PLTE and before IDAT. + /// + /// The containing image data. + /// The image metadata. + private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) + { + if (pngMetadata.TransparentColor is null) + { + return; + } + + Span alpha = this.chunkDataBuffer.Span; + if (pngMetadata.ColorType == PngColorType.Rgb) + { + if (this.use16Bit) + { + Rgb48 rgb = pngMetadata.TransparentColor.Value.ToPixel(); + BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb.R); + BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G); + BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B); + + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); + } + else + { + alpha.Clear(); + Rgb24 rgb = pngMetadata.TransparentColor.Value.ToPixel(); + alpha[1] = rgb.R; + alpha[3] = rgb.G; + alpha[5] = rgb.B; + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); + } + } + else if (pngMetadata.ColorType == PngColorType.Grayscale) + { + if (this.use16Bit) + { + L16 l16 = pngMetadata.TransparentColor.Value.ToPixel(); + BinaryPrimitives.WriteUInt16LittleEndian(alpha, l16.PackedValue); + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); + } + else + { + L8 l8 = pngMetadata.TransparentColor.Value.ToPixel(); + alpha.Clear(); + alpha[1] = l8.PackedValue; + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); + } + } + } + + /// + /// Writes the animation control chunk to the stream. + /// + /// The containing image data. + /// The frame metadata. + /// The frame area of interest. + /// The frame sequence number. + private FrameControl WriteFrameControlChunk(Stream stream, PngFrameMetadata frameMetadata, Rectangle bounds, uint sequenceNumber) + { + FrameControl fcTL = new( + sequenceNumber: sequenceNumber, + width: (uint)bounds.Width, + height: (uint)bounds.Height, + xOffset: (uint)bounds.Left, + yOffset: (uint)bounds.Top, + delayNumerator: (ushort)frameMetadata.FrameDelay.Numerator, + delayDenominator: (ushort)frameMetadata.FrameDelay.Denominator, + disposalMode: frameMetadata.DisposalMode, + blendMode: frameMetadata.BlendMode); + + fcTL.WriteTo(this.chunkDataBuffer.Span); + + this.WriteChunk(stream, PngChunkType.FrameControl, this.chunkDataBuffer.Span, 0, FrameControl.Size); + + return fcTL; + } + + /// + /// Writes the pixel information to the stream. + /// + /// The pixel format. + /// The frame control + /// The image frame. + /// The quantized pixel data. Can be null. + /// The stream. + /// Is writing fdAT or IDAT. + private uint WriteDataChunks(in FrameControl frameControl, in Buffer2DRegion frame, IndexedImageFrame? quantized, Stream stream, bool isFrame) + where TPixel : unmanaged, IPixel + { + byte[] buffer; + int bufferLength; + + using (MemoryStream memoryStream = new()) + { + using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.encoder.CompressionLevel)) + { + if (this.interlaceMode is PngInterlaceMode.Adam7) + { + if (quantized is not null) + { + this.EncodeAdam7IndexedPixels(quantized, deflateStream); + } + else + { + this.EncodeAdam7Pixels(in frame, deflateStream); + } + } + else + { + this.EncodePixels(in frame, quantized, deflateStream); + } + } + + buffer = memoryStream.ToArray(); + bufferLength = buffer.Length; + } + + // Store the chunks in repeated 64k blocks. + // This reduces the memory load for decoding the image for many decoders. + int maxBlockSize = MaxBlockSize; + if (isFrame) + { + maxBlockSize -= 4; + } + + int numChunks = bufferLength / maxBlockSize; + + if (bufferLength % maxBlockSize != 0) + { + numChunks++; + } + + for (int i = 0; i < numChunks; i++) + { + int length = bufferLength - (i * maxBlockSize); + + if (length > maxBlockSize) + { + length = maxBlockSize; + } + + if (isFrame) + { + // We increment the sequence number for each frame chunk. + // '1' is added to the sequence number to account for the preceding frame control chunk. + uint sequenceNumber = (uint)(frameControl.SequenceNumber + 1 + i); + this.WriteFrameDataChunk(stream, sequenceNumber, buffer, i * maxBlockSize, length); + } + else + { + this.WriteChunk(stream, PngChunkType.Data, buffer, i * maxBlockSize, length); + } + } + + return (uint)numChunks; + } + + /// + /// Allocates the buffers for each scanline. + /// + /// The bytes per scanline. + private void AllocateScanlineBuffers(int bytesPerScanline) + { + // Clean up from any potential previous runs. + this.previousScanline?.Dispose(); + this.currentScanline?.Dispose(); + this.previousScanline = this.memoryAllocator.Allocate(bytesPerScanline, AllocationOptions.Clean); + this.currentScanline = this.memoryAllocator.Allocate(bytesPerScanline, AllocationOptions.Clean); + } + + /// + /// Encodes the pixels. + /// + /// The type of the pixel. + /// The image frame pixel buffer. + /// The quantized pixels. + /// The deflate stream. + private void EncodePixels(in Buffer2DRegion pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream) + where TPixel : unmanaged, IPixel + { + int bytesPerScanline = this.CalculateScanlineLength(pixels.Width); + int filterLength = bytesPerScanline + 1; + this.AllocateScanlineBuffers(bytesPerScanline); + + using IMemoryOwner filterBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean); + using IMemoryOwner attemptBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean); + + Span filter = filterBuffer.GetSpan(); + Span attempt = attemptBuffer.GetSpan(); + for (int y = 0; y < pixels.Height; y++) + { + ReadOnlySpan rowSpan = pixels.DangerousGetRowSpan(y); + this.CollectAndFilterPixelRow(rowSpan, ref filter, ref attempt, quantized, y); + deflateStream.Write(filter); + this.SwapScanlineBuffers(); + } + } + + /// + /// Interlaced encoding the pixels. + /// + /// The type of the pixel. + /// The image frame pixel buffer. + /// The deflate stream. + private void EncodeAdam7Pixels(in Buffer2DRegion pixels, ZlibDeflateStream deflateStream) + where TPixel : unmanaged, IPixel + { + for (int pass = 0; pass < 7; pass++) + { + int startRow = Adam7.FirstRow[pass]; + int startCol = Adam7.FirstColumn[pass]; + int blockWidth = Adam7.ComputeBlockWidth(pixels.Width, pass); + + int bytesPerScanline = this.bytesPerPixel <= 1 + ? ((blockWidth * this.bitDepth) + 7) / 8 + : blockWidth * this.bytesPerPixel; + + int filterLength = bytesPerScanline + 1; + this.AllocateScanlineBuffers(bytesPerScanline); + + using IMemoryOwner blockBuffer = this.memoryAllocator.Allocate(blockWidth); + using IMemoryOwner filterBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean); + using IMemoryOwner attemptBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean); + + Span block = blockBuffer.GetSpan(); + Span filter = filterBuffer.GetSpan(); + Span attempt = attemptBuffer.GetSpan(); + + for (int row = startRow; row < pixels.Height; row += Adam7.RowIncrement[pass]) + { + // Collect pixel data + Span srcRow = pixels.DangerousGetRowSpan(row); + for (int col = startCol, i = 0; col < pixels.Width; col += Adam7.ColumnIncrement[pass], i++) + { + block[i] = srcRow[col]; + } + + // Encode data + // Note: quantized parameter not used + // Note: row parameter not used + ReadOnlySpan blockSpan = block; + this.CollectAndFilterPixelRow(blockSpan, ref filter, ref attempt, null, -1); + deflateStream.Write(filter); + + this.SwapScanlineBuffers(); + } + } + } + + /// + /// Interlaced encoding the quantized (indexed, with palette) pixels. + /// + /// The type of the pixel. + /// The quantized. + /// The deflate stream. + private void EncodeAdam7IndexedPixels(IndexedImageFrame quantized, ZlibDeflateStream deflateStream) + where TPixel : unmanaged, IPixel + { + for (int pass = 0; pass < 7; pass++) + { + int startRow = Adam7.FirstRow[pass]; + int startCol = Adam7.FirstColumn[pass]; + int blockWidth = Adam7.ComputeBlockWidth(quantized.Width, pass); + + int bytesPerScanline = this.bytesPerPixel <= 1 + ? ((blockWidth * this.bitDepth) + 7) / 8 + : blockWidth * this.bytesPerPixel; + + int filterLength = bytesPerScanline + 1; + + this.AllocateScanlineBuffers(bytesPerScanline); + + using IMemoryOwner blockBuffer = this.memoryAllocator.Allocate(blockWidth); + using IMemoryOwner filterBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean); + using IMemoryOwner attemptBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean); + + Span block = blockBuffer.GetSpan(); + Span filter = filterBuffer.GetSpan(); + Span attempt = attemptBuffer.GetSpan(); + + for (int row = startRow; row < quantized.Height; row += Adam7.RowIncrement[pass]) + { + // Collect data + ReadOnlySpan srcRow = quantized.DangerousGetRowSpan(row); + for (int col = startCol, i = 0; col < quantized.Width; col += Adam7.ColumnIncrement[pass], i++) + { + block[i] = srcRow[col]; + } + + // Encode data + this.EncodeAdam7IndexedPixelRow(block, ref filter, ref attempt); + deflateStream.Write(filter); + + this.SwapScanlineBuffers(); + } + } + } + + /// + /// Writes the chunk end to the stream. + /// + /// The containing image data. + private void WriteEndChunk(Stream stream) => this.WriteChunk(stream, PngChunkType.End, null); + + /// + /// Writes a chunk to the stream. + /// + /// The to write to. + /// The type of chunk to write. + /// The containing data. + private void WriteChunk(Stream stream, PngChunkType type, Span data) + => this.WriteChunk(stream, type, data, 0, data.Length); + + /// + /// Writes a chunk of a specified length to the stream at the given offset. + /// + /// The to write to. + /// The type of chunk to write. + /// The containing data. + /// The position to offset the data at. + /// The of the data to write. + private void WriteChunk(Stream stream, PngChunkType type, Span data, int offset, int length) + { + Span buffer = stackalloc byte[8]; + + BinaryPrimitives.WriteInt32BigEndian(buffer, length); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(4, 4), (uint)type); + + stream.Write(buffer); + + this.crc32.Reset(); + this.crc32.Append(buffer[4..]); // Write the type buffer + + if (data.Length > 0 && length > 0) + { + stream.Write(data, offset, length); + + this.crc32.Append(data.Slice(offset, length)); + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer, this.crc32.GetCurrentHashAsUInt32()); + + stream.Write(buffer, 0, 4); // write the crc + } + + /// + /// Writes a frame data chunk of a specified length to the stream at the given offset. + /// + /// The to write to. + /// The frame sequence number. + /// The containing data. + /// The position to offset the data at. + /// The of the data to write. + private void WriteFrameDataChunk(Stream stream, uint sequenceNumber, Span data, int offset, int length) + { + Span buffer = stackalloc byte[12]; + + BinaryPrimitives.WriteInt32BigEndian(buffer, length + 4); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(4, 4), (uint)PngChunkType.FrameData); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(8, 4), sequenceNumber); + + stream.Write(buffer); + + this.crc32.Reset(); + this.crc32.Append(buffer[4..]); // Write the type buffer + + if (data.Length > 0 && length > 0) + { + stream.Write(data, offset, length); + + this.crc32.Append(data.Slice(offset, length)); + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer, this.crc32.GetCurrentHashAsUInt32()); + + stream.Write(buffer, 0, 4); // write the crc + } + + /// + /// Calculates the scanline length. + /// + /// The width of the row. + /// + /// The representing the length. + /// + private int CalculateScanlineLength(int width) + { + int mod = this.bitDepth is 16 ? 16 : 8; + int scanlineLength = width * this.bitDepth * this.bytesPerPixel; + + int amount = scanlineLength % mod; + if (amount != 0) + { + scanlineLength += mod - amount; + } + + return scanlineLength / mod; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SwapScanlineBuffers() + { + ref IMemoryOwner prev = ref this.previousScanline; + ref IMemoryOwner current = ref this.currentScanline; + RuntimeUtility.Swap(ref prev, ref current); + } + + /// + /// Adjusts the options based upon the given metadata. + /// + /// The type of pixel format. + /// The encoder with options. + /// The PNG metadata. + /// if set to true [use16 bit]. + /// The bytes per pixel. + [MemberNotNull(nameof(backgroundColor))] + private void SanitizeAndSetEncoderOptions( + PngEncoder encoder, + PngMetadata pngMetadata, + out bool use16Bit, + out int bytesPerPixel) + where TPixel : unmanaged, IPixel + { + // Always take the encoder options over the metadata values. + this.gamma = encoder.Gamma ?? pngMetadata.Gamma; + + // Use options, then check metadata, if nothing set there then we suggest + // a sensible default based upon the pixel format. + PngColorType color = encoder.ColorType ?? pngMetadata.ColorType; + byte bits = (byte)(encoder.BitDepth ?? pngMetadata.BitDepth); + + // Ensure the bit depth and color type are a supported combination. + // Bit8 is the only bit depth supported by all color types. + byte[] validBitDepths = PngConstants.ColorTypes[color]; + if (Array.IndexOf(validBitDepths, bits) == -1) + { + bits = (byte)PngBitDepth.Bit8; + } + + this.colorType = color; + this.bitDepth = bits; + + if (encoder.FilterMethod.HasValue) + { + this.filterMethod = encoder.FilterMethod.Value; + } + else + { + // Specification recommends default filter method None for paletted images and Paeth for others. + this.filterMethod = this.colorType is PngColorType.Palette ? PngFilterMethod.None : PngFilterMethod.Paeth; + } + + use16Bit = bits == (byte)PngBitDepth.Bit16; + bytesPerPixel = CalculateBytesPerPixel(this.colorType, use16Bit); + + this.interlaceMode = encoder.InterlaceMethod ?? pngMetadata.InterlaceMethod; + this.chunkFilter = encoder.SkipMetadata ? PngChunkFilter.ExcludeAll : encoder.ChunkFilter ?? PngChunkFilter.None; + this.backgroundColor = encoder.BackgroundColor ?? pngMetadata.TransparentColor ?? Color.Transparent; + } + + /// + /// Creates the quantized frame. + /// + /// The type of the pixel. + /// The png encoder. + /// The color type. + /// The bits per component. + /// The image metadata. + /// The image. + /// The current image frame. + /// The frame area of interest. + /// The quantizer containing any previously derived palette. + /// The background color. + private IndexedImageFrame? CreateQuantizedFrame( + QuantizingImageEncoder encoder, + PngColorType colorType, + byte bitDepth, + PngMetadata metadata, + Image image, + ImageFrame frame, + Rectangle bounds, + PaletteQuantizer? paletteQuantizer, + Color backgroundColor) + where TPixel : unmanaged, IPixel + { + if (colorType is not PngColorType.Palette) + { + return null; + } + + if (paletteQuantizer.HasValue) + { + return paletteQuantizer.Value.QuantizeFrame(frame, bounds); + } + + // Use the metadata to determine what quantization depth to use if no quantizer has been set. + if (this.quantizer is null) + { + if (metadata.ColorTable?.Length > 0) + { + // We can use the color data from the decoded metadata here. + // We avoid dithering by default to preserve the original colors. + QuantizerOptions options = new() { Dither = null, TransparentColorMode = encoder.TransparentColorMode }; + this.quantizer = new PaletteQuantizer(metadata.ColorTable.Value, options); + } + else + { + // Don't use the default transparency threshold for quantization as PNG can handle multiple transparent colors. + // We choose a value that is close to zero so that edge cases causes by lower bit depths for the alpha channel are handled correctly. + QuantizerOptions options = new() + { + TransparencyThreshold = 0, + MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth), + TransparentColorMode = encoder.TransparentColorMode + }; + + this.quantizer = new WuQuantizer(options); + } + } + + // Create quantized frame returning the palette and set the bit depth. + using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(frame.Configuration); + + if (image.Frames.Count > 1) + { + // Encoding animated frames with a global palette requires a transparent pixel in the palette + // since we only encode the delta between frames. To ensure that we have a transparent pixel + // we create a fake frame with a containing only transparent pixels and add it to the palette. + using Buffer2D fake = image.Configuration.MemoryAllocator.Allocate2D(Math.Min(256, image.Width), Math.Min(256, image.Height)); + TPixel backGroundPixel = backgroundColor.ToPixel(); + for (int i = 0; i < fake.Height; i++) + { + fake.DangerousGetRowSpan(i).Fill(backGroundPixel); + } + + Buffer2DRegion fakeRegion = fake.GetRegion(); + frameQuantizer.AddPaletteColors(in fakeRegion); + } + + frameQuantizer.BuildPalette( + encoder.PixelSamplingStrategy, + image); + + return frameQuantizer.QuantizeFrame(frame, bounds); + } + + /// + /// Calculates the bit depth value. + /// + /// The type of the pixel. + /// The color type. + /// The bits per component. + /// The quantized frame. + /// Bit depth is not supported or not valid. + private static byte CalculateBitDepth( + PngColorType colorType, + byte bitDepth, + IndexedImageFrame? quantizedFrame) + where TPixel : unmanaged, IPixel + { + if (colorType is PngColorType.Palette) + { + byte quantizedBits = (byte)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(quantizedFrame!.Palette.Length), 1, 8); + byte bits = Math.Max(bitDepth, quantizedBits); + + // Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk + // We check again for the bit depth as the bit depth of the color palette from a given quantizer might not + // be within the acceptable range. + bits = bits switch + { + 3 => 4, + >= 5 and <= 7 => 8, + _ => bits + }; + + bitDepth = bits; + } + + if (Array.IndexOf(PngConstants.ColorTypes[colorType], bitDepth) < 0) + { + throw new NotSupportedException("Bit depth is not supported or not valid."); + } + + return bitDepth; + } + + /// + /// Calculates the correct number of bytes per pixel for the given color type. + /// + /// The color type. + /// Whether to use 16 bits per component. + /// Bytes per pixel. + private static int CalculateBytesPerPixel(PngColorType? pngColorType, bool use16Bit) + => pngColorType switch + { + PngColorType.Grayscale => use16Bit ? 2 : 1, + PngColorType.GrayscaleWithAlpha => use16Bit ? 4 : 2, + PngColorType.Palette => 1, + PngColorType.Rgb => use16Bit ? 6 : 3, + + // PngColorType.RgbWithAlpha + _ => use16Bit ? 8 : 4, + }; + + private unsafe struct ScratchBuffer + { + private const int Size = 26; + private fixed byte scratch[Size]; + + public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); + } + } +} diff --git a/ImageSharp/Formats/Png/PngEncoderHelpers.cs b/ImageSharp/Formats/Png/PngEncoderHelpers.cs new file mode 100644 index 0000000..bb2c196 --- /dev/null +++ b/ImageSharp/Formats/Png/PngEncoderHelpers.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// The helper methods for class. + /// + internal static class PngEncoderHelpers + { + /// + /// Packs the given 8 bit array into and array of depths. + /// + /// The source span in 8 bits. + /// The resultant span in . + /// The bit depth. + /// The scaling factor. + public static void ScaleDownFrom8BitArray(ReadOnlySpan source, Span result, int bits, float scale = 1) + { + ref byte sourceRef = ref MemoryMarshal.GetReference(source); + ref byte resultRef = ref MemoryMarshal.GetReference(result); + + int shift = 8 - bits; + byte mask = (byte)(0xFF >> shift); + byte shift0 = (byte)shift; + int v = 0; + int resultOffset = 0; + + for (int i = 0; i < source.Length; i++) + { + int value = ((int)MathF.Round(Unsafe.Add(ref sourceRef, (uint)i) / scale)) & mask; + v |= value << shift; + + if (shift == 0) + { + shift = shift0; + Unsafe.Add(ref resultRef, (uint)resultOffset) = (byte)v; + resultOffset++; + v = 0; + } + else + { + shift -= bits; + } + } + + if (shift != shift0) + { + Unsafe.Add(ref resultRef, (uint)resultOffset) = (byte)v; + } + } + } +} diff --git a/ImageSharp/Formats/Png/PngFilterMethod.cs b/ImageSharp/Formats/Png/PngFilterMethod.cs new file mode 100644 index 0000000..0f927a0 --- /dev/null +++ b/ImageSharp/Formats/Png/PngFilterMethod.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Provides enumeration of available PNG filter methods. + /// + public enum PngFilterMethod + { + /// + /// With the None filter, the scanline is transmitted unmodified. + /// + None, + + /// + /// The Sub filter transmits the difference between each byte and the value of the corresponding + /// byte of the prior pixel. + /// + Sub, + + /// + /// The Up filter is just like the filter except that the pixel immediately above the current pixel, + /// rather than just to its left, is used as the predictor. + /// + Up, + + /// + /// The Average filter uses the average of the two neighboring pixels (left and above) to predict the value of a pixel. + /// + Average, + + /// + /// The Paeth filter computes a simple linear function of the three neighboring pixels (left, above, upper left), + /// then chooses as predictor the neighboring pixel closest to the computed value. + /// + Paeth, + + /// + /// Computes the output scanline using all five filters, and selects the filter that gives the smallest sum of + /// absolute values of outputs. + /// This method usually outperforms any single fixed filter choice. + /// + Adaptive, + } +} diff --git a/ImageSharp/Formats/Png/PngFormat.cs b/ImageSharp/Formats/Png/PngFormat.cs new file mode 100644 index 0000000..4f5e307 --- /dev/null +++ b/ImageSharp/Formats/Png/PngFormat.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Registers the image encoders, decoders and mime type detectors for the png format. + /// + public sealed class PngFormat : IImageFormat + { + private PngFormat() + { + } + + /// + /// Gets the shared instance. + /// + public static PngFormat Instance { get; } = new(); + + /// + public string Name => "PNG"; + + /// + public string DefaultMimeType => "image/png"; + + /// + public IEnumerable MimeTypes => PngConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => PngConstants.FileExtensions; + + /// + public PngMetadata CreateDefaultFormatMetadata() => new(); + + /// + public PngFrameMetadata CreateDefaultFormatFrameMetadata() => new(); + } +} diff --git a/ImageSharp/Formats/Png/PngFrameMetadata.cs b/ImageSharp/Formats/Png/PngFrameMetadata.cs new file mode 100644 index 0000000..a92feea --- /dev/null +++ b/ImageSharp/Formats/Png/PngFrameMetadata.cs @@ -0,0 +1,109 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Png.Chunks; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Provides APng specific metadata information for the image frame. + /// + public class PngFrameMetadata : IFormatFrameMetadata + { + /// + /// Initializes a new instance of the class. + /// + public PngFrameMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private PngFrameMetadata(PngFrameMetadata other) + { + this.FrameDelay = other.FrameDelay; + this.DisposalMode = other.DisposalMode; + this.BlendMode = other.BlendMode; + } + + /// + /// Gets or sets the frame delay for animated images. + /// If not 0, when utilized in Png animation, this field specifies the number of seconds to + /// wait before continuing with the processing of the Data Stream. + /// The clock starts ticking immediately after the graphic is rendered. + /// + public Rational FrameDelay { get; set; } = new(0); + + /// + /// Gets or sets the type of frame area disposal to be done after rendering this frame + /// + public FrameDisposalMode DisposalMode { get; set; } + + /// + /// Gets or sets the type of frame area rendering for this frame + /// + public FrameBlendMode BlendMode { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The chunk to create an instance from. + internal void FromChunk(in FrameControl frameControl) + { + this.FrameDelay = new Rational(frameControl.DelayNumerator, frameControl.DelayDenominator); + this.DisposalMode = frameControl.DisposalMode; + this.BlendMode = frameControl.BlendMode; + } + + /// + public static PngFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) + => new() + { + FrameDelay = new Rational(metadata.Duration.TotalMilliseconds / 1000), + DisposalMode = GetMode(metadata.DisposalMode), + BlendMode = metadata.BlendMode, + }; + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + { + double delay = this.FrameDelay.ToDouble(); + if (double.IsNaN(delay)) + { + delay = 0; + } + + return new FormatConnectingFrameMetadata + { + ColorTableMode = FrameColorTableMode.Global, + Duration = TimeSpan.FromMilliseconds(delay * 1000), + DisposalMode = this.DisposalMode, + BlendMode = this.BlendMode, + }; + } + + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public PngFrameMetadata DeepClone() => new(this); + + private static FrameDisposalMode GetMode(FrameDisposalMode mode) => mode switch + { + FrameDisposalMode.RestoreToBackground => FrameDisposalMode.RestoreToBackground, + FrameDisposalMode.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious, + FrameDisposalMode.DoNotDispose => FrameDisposalMode.DoNotDispose, + _ => FrameDisposalMode.DoNotDispose, + }; + } +} diff --git a/ImageSharp/Formats/Png/PngImageFormatDetector.cs b/ImageSharp/Formats/Png/PngImageFormatDetector.cs new file mode 100644 index 0000000..4ea1911 --- /dev/null +++ b/ImageSharp/Formats/Png/PngImageFormatDetector.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Detects png file headers + /// + public sealed class PngImageFormatDetector : IImageFormatDetector + { + /// + public int HeaderSize => 8; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.IsSupportedFileFormat(header) ? PngFormat.Instance : null; + return format != null; + } + + private bool IsSupportedFileFormat(ReadOnlySpan header) + { + return header.Length >= this.HeaderSize && BinaryPrimitives.ReadUInt64BigEndian(header) == PngConstants.HeaderValue; + } + } +} diff --git a/ImageSharp/Formats/Png/PngInterlaceMode.cs b/ImageSharp/Formats/Png/PngInterlaceMode.cs new file mode 100644 index 0000000..d7003f6 --- /dev/null +++ b/ImageSharp/Formats/Png/PngInterlaceMode.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Provides enumeration of available PNG interlace modes. + /// + public enum PngInterlaceMode : byte + { + /// + /// Non interlaced + /// + None = 0, + + /// + /// Adam 7 interlacing. + /// + Adam7 = 1 + } +} diff --git a/ImageSharp/Formats/Png/PngMetadata.cs b/ImageSharp/Formats/Png/PngMetadata.cs new file mode 100644 index 0000000..2e43eb6 --- /dev/null +++ b/ImageSharp/Formats/Png/PngMetadata.cs @@ -0,0 +1,259 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Png.Chunks; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Provides Png specific metadata information for the image. + /// + public class PngMetadata : IFormatMetadata + { + /// + /// Initializes a new instance of the class. + /// + public PngMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private PngMetadata(PngMetadata other) + { + this.BitDepth = other.BitDepth; + this.ColorType = other.ColorType; + this.Gamma = other.Gamma; + this.InterlaceMethod = other.InterlaceMethod; + this.TransparentColor = other.TransparentColor; + this.RepeatCount = other.RepeatCount; + this.AnimateRootFrame = other.AnimateRootFrame; + + if (other.ColorTable?.Length > 0) + { + this.ColorTable = other.ColorTable.Value.ToArray(); + } + + for (int i = 0; i < other.TextData.Count; i++) + { + this.TextData.Add(other.TextData[i]); + } + } + + /// + /// Gets or sets the number of bits per sample or per palette index (not per pixel). + /// Not all values are allowed for all values. + /// + public PngBitDepth BitDepth { get; set; } = PngBitDepth.Bit8; + + /// + /// Gets or sets the color type. + /// + public PngColorType ColorType { get; set; } = PngColorType.RgbWithAlpha; + + /// + /// Gets or sets a value indicating whether this instance should write an Adam7 interlaced image. + /// + public PngInterlaceMode InterlaceMethod { get; set; } = PngInterlaceMode.None; + + /// + /// Gets or sets the gamma value for the image. + /// + public float Gamma { get; set; } + + /// + /// Gets or sets the color table, if any. + /// + public ReadOnlyMemory? ColorTable { get; set; } + + /// + /// Gets or sets the transparent color used with non palette based images, if a transparency chunk and markers were decoded. + /// + public Color? TransparentColor { get; set; } + + /// + /// Gets or sets the collection of text data stored within the iTXt, tEXt, and zTXt chunks. + /// Used for conveying textual information associated with the image. + /// + public IList TextData { get; set; } = []; + + /// + /// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping. + /// + public uint RepeatCount { get; set; } = 1; + + /// + /// Gets or sets a value indicating whether the root frame is shown as part of the animated sequence + /// + public bool AnimateRootFrame { get; set; } = true; + + /// + public static PngMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + PngColorType color; + PixelColorType colorType = metadata.PixelTypeInfo.ColorType; + + switch (colorType) + { + case PixelColorType.Binary: + case PixelColorType.Indexed: + color = PngColorType.Palette; + break; + case PixelColorType.Luminance: + color = PngColorType.Grayscale; + break; + case PixelColorType.RGB: + case PixelColorType.BGR: + color = PngColorType.Rgb; + break; + default: + if (colorType.HasFlag(PixelColorType.Luminance | PixelColorType.Alpha)) + { + color = PngColorType.GrayscaleWithAlpha; + break; + } + + color = PngColorType.RgbWithAlpha; + break; + } + + // PNG uses bits per component not per pixel. + int bpc = metadata.PixelTypeInfo.ComponentInfo?.GetMaximumComponentPrecision() ?? 8; + PngBitDepth bitDepth = bpc switch + { + 1 => PngBitDepth.Bit1, + 2 => PngBitDepth.Bit2, + 4 => PngBitDepth.Bit4, + _ => (bpc <= 8) ? PngBitDepth.Bit8 : PngBitDepth.Bit16, + }; + return new PngMetadata + { + ColorType = color, + BitDepth = bitDepth, + RepeatCount = metadata.RepeatCount, + }; + } + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp; + PixelColorType colorType; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + PixelComponentInfo info; + switch (this.ColorType) + { + case PngColorType.Palette: + bpp = this.ColorTable.HasValue + ? Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.ColorTable.Value.Length), 1, 8) + : 8; + + colorType = PixelColorType.Indexed; + info = PixelComponentInfo.Create(1, bpp, bpp); + break; + + case PngColorType.Grayscale: + bpp = (int)this.BitDepth; + colorType = PixelColorType.Luminance; + info = PixelComponentInfo.Create(1, bpp, bpp); + break; + + case PngColorType.GrayscaleWithAlpha: + + alpha = PixelAlphaRepresentation.Unassociated; + if (this.BitDepth == PngBitDepth.Bit16) + { + bpp = 32; + colorType = PixelColorType.Luminance | PixelColorType.Alpha; + info = PixelComponentInfo.Create(2, bpp, 16, 16); + break; + } + + bpp = 16; + colorType = PixelColorType.Luminance | PixelColorType.Alpha; + info = PixelComponentInfo.Create(2, bpp, 8, 8); + break; + + case PngColorType.Rgb: + if (this.BitDepth == PngBitDepth.Bit16) + { + bpp = 48; + colorType = PixelColorType.RGB; + info = PixelComponentInfo.Create(3, bpp, 16, 16, 16); + break; + } + + bpp = 24; + colorType = PixelColorType.RGB; + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + break; + + case PngColorType.RgbWithAlpha: + default: + + alpha = PixelAlphaRepresentation.Unassociated; + if (this.BitDepth == PngBitDepth.Bit16) + { + bpp = 64; + colorType = PixelColorType.RGB | PixelColorType.Alpha; + info = PixelComponentInfo.Create(4, bpp, 16, 16, 16, 16); + break; + } + + bpp = 32; + colorType = PixelColorType.RGB | PixelColorType.Alpha; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + break; + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ColorType = colorType, + ComponentInfo = info, + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + ColorTableMode = FrameColorTableMode.Global, + PixelTypeInfo = this.GetPixelTypeInfo(), + RepeatCount = (ushort)Numerics.Clamp(this.RepeatCount, 0, ushort.MaxValue), + }; + + /// + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + this.ColorTable = null; + + // If the color type is RGB and we have a transparent color, we need to switch to RGBA + // so that we do not incorrectly preserve the obsolete tRNS chunk. + if (this.ColorType == PngColorType.Rgb && this.TransparentColor.HasValue) + { + this.ColorType = PngColorType.RgbWithAlpha; + this.TransparentColor = null; + } + + // The same applies for Grayscale. + if (this.ColorType == PngColorType.Grayscale && this.TransparentColor.HasValue) + { + this.ColorType = PngColorType.GrayscaleWithAlpha; + this.TransparentColor = null; + } + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public PngMetadata DeepClone() => new(this); + } +} diff --git a/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/ImageSharp/Formats/Png/PngScanlineProcessor.cs new file mode 100644 index 0000000..7b468e6 --- /dev/null +++ b/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -0,0 +1,368 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Png.Chunks; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Png { + /// + /// Provides methods to allow the decoding of raw scanlines to image rows of different pixel formats. + /// TODO: We should make this a stateful class or struct to reduce the number of arguments on methods (most are invariant). + /// + internal static class PngScanlineProcessor + { + public static void ProcessGrayscaleScanline( + int bitDepth, + in FrameControl frameControl, + ReadOnlySpan scanlineSpan, + Span rowSpan, + Color? transparentColor) + where TPixel : unmanaged, IPixel => + ProcessInterlacedGrayscaleScanline( + bitDepth, + frameControl, + scanlineSpan, + rowSpan, + 0, + 1, + transparentColor); + + public static void ProcessInterlacedGrayscaleScanline( + int bitDepth, + in FrameControl frameControl, + ReadOnlySpan scanlineSpan, + Span rowSpan, + uint pixelOffset, + uint increment, + Color? transparentColor) + where TPixel : unmanaged, IPixel + { + uint offset = pixelOffset + frameControl.XOffset; + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(bitDepth) - 1); + + if (transparentColor is null) + { + if (bitDepth == 16) + { + int o = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment, o += 2) + { + ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromL16(Unsafe.As(ref luminance)); + } + } + else + { + for (nuint x = offset, o = 0; x < frameControl.XMax; x += increment, o++) + { + byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromL8(Unsafe.As(ref luminance)); + } + } + + return; + } + + if (bitDepth == 16) + { + L16 transparent = transparentColor.Value.ToPixel(); + int o = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment, o += 2) + { + ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); + La32 source = new(luminance, luminance.Equals(transparent.PackedValue) ? ushort.MinValue : ushort.MaxValue); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromLa32(source); + } + } + else + { + byte transparent = (byte)(transparentColor.Value.ToPixel().PackedValue * scaleFactor); + for (nuint x = offset, o = 0; x < frameControl.XMax; x += increment, o++) + { + byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor); + La16 source = new(luminance, luminance.Equals(transparent) ? byte.MinValue : byte.MaxValue); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromLa16(source); + } + } + } + + public static void ProcessGrayscaleWithAlphaScanline( + int bitDepth, + in FrameControl frameControl, + ReadOnlySpan scanlineSpan, + Span rowSpan, + uint bytesPerPixel, + uint bytesPerSample) + where TPixel : unmanaged, IPixel => + ProcessInterlacedGrayscaleWithAlphaScanline( + bitDepth, + frameControl, + scanlineSpan, + rowSpan, + 0, + 1, + bytesPerPixel, + bytesPerSample); + + public static void ProcessInterlacedGrayscaleWithAlphaScanline( + int bitDepth, + in FrameControl frameControl, + ReadOnlySpan scanlineSpan, + Span rowSpan, + uint pixelOffset, + uint increment, + uint bytesPerPixel, + uint bytesPerSample) + where TPixel : unmanaged, IPixel + { + uint offset = pixelOffset + frameControl.XOffset; + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + + if (bitDepth == 16) + { + int o = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment, o += 4) + { + ushort l = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); + ushort a = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2)); + + Unsafe.Add(ref rowSpanRef, (uint)x) = TPixel.FromLa32(new La32(l, a)); + } + } + else + { + nuint offset2 = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment) + { + byte l = Unsafe.Add(ref scanlineSpanRef, offset2); + byte a = Unsafe.Add(ref scanlineSpanRef, offset2 + bytesPerSample); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromLa16(new La16(l, a)); + offset2 += bytesPerPixel; + } + } + } + + public static void ProcessPaletteScanline( + in FrameControl frameControl, + ReadOnlySpan scanlineSpan, + Span rowSpan, + ReadOnlyMemory? palette) + where TPixel : unmanaged, IPixel => + ProcessInterlacedPaletteScanline( + frameControl, + scanlineSpan, + rowSpan, + 0, + 1, + palette); + + public static void ProcessInterlacedPaletteScanline( + in FrameControl frameControl, + ReadOnlySpan scanlineSpan, + Span rowSpan, + uint pixelOffset, + uint increment, + ReadOnlyMemory? palette) + where TPixel : unmanaged, IPixel + { + if (palette is null) + { + PngThrowHelper.ThrowMissingPalette(); + } + + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span); + uint offset = pixelOffset + frameControl.XOffset; + int maxIndex = palette.Value.Length - 1; + + for (nuint x = offset, o = 0; x < frameControl.XMax; x += increment, o++) + { + uint index = Unsafe.Add(ref scanlineSpanRef, o); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba32(Unsafe.Add(ref paletteBase, (int)Math.Min(index, maxIndex)).ToPixel()); + } + } + + public static void ProcessRgbScanline( + Configuration configuration, + int bitDepth, + in FrameControl frameControl, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int bytesPerPixel, + int bytesPerSample, + Color? transparentColor) + where TPixel : unmanaged, IPixel => + ProcessInterlacedRgbScanline( + configuration, + bitDepth, + frameControl, + scanlineSpan, + rowSpan, + 0, + 1, + bytesPerPixel, + bytesPerSample, + transparentColor); + + public static void ProcessInterlacedRgbScanline( + Configuration configuration, + int bitDepth, + in FrameControl frameControl, + ReadOnlySpan scanlineSpan, + Span rowSpan, + uint pixelOffset, + uint increment, + int bytesPerPixel, + int bytesPerSample, + Color? transparentColor) + where TPixel : unmanaged, IPixel + { + uint offset = pixelOffset + frameControl.XOffset; + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + + if (transparentColor is null) + { + if (bitDepth == 16) + { + int o = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) + { + ushort r = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); + ushort g = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); + ushort b = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgb48(new Rgb48(r, g, b)); + } + } + else if (pixelOffset == 0 && increment == 1) + { + PixelOperations.Instance.FromRgb24Bytes( + configuration, + scanlineSpan[..(int)(frameControl.Width * bytesPerPixel)], + rowSpan.Slice((int)frameControl.XOffset, (int)frameControl.Width), + (int)frameControl.Width); + } + else + { + int o = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) + { + byte r = Unsafe.Add(ref scanlineSpanRef, (uint)o); + byte g = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); + byte b = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgb24(new Rgb24(r, g, b)); + } + } + + return; + } + + if (bitDepth == 16) + { + Rgb48 transparent = transparentColor.Value.ToPixel(); + Rgba64 rgba = default; + int o = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) + { + rgba.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); + rgba.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); + rgba.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); + rgba.A = rgba.Rgb.Equals(transparent) ? ushort.MinValue : ushort.MaxValue; + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba64(rgba); + } + } + else + { + Rgb24 transparent = transparentColor.Value.ToPixel(); + Rgba32 rgba = default; + int o = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) + { + rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); + rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); + rgba.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); + rgba.A = transparent.Equals(rgba.Rgb) ? byte.MinValue : byte.MaxValue; + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba32(rgba); + } + } + } + + public static void ProcessRgbaScanline( + Configuration configuration, + int bitDepth, + in FrameControl frameControl, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int bytesPerPixel, + int bytesPerSample) + where TPixel : unmanaged, IPixel => + ProcessInterlacedRgbaScanline( + configuration, + bitDepth, + frameControl, + scanlineSpan, + rowSpan, + 0, + 1, + bytesPerPixel, + bytesPerSample); + + public static void ProcessInterlacedRgbaScanline( + Configuration configuration, + int bitDepth, + in FrameControl frameControl, + ReadOnlySpan scanlineSpan, + Span rowSpan, + uint pixelOffset, + uint increment, + int bytesPerPixel, + int bytesPerSample) + where TPixel : unmanaged, IPixel + { + uint offset = pixelOffset + frameControl.XOffset; + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + + if (bitDepth == 16) + { + int o = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) + { + ushort r = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); + ushort g = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); + ushort b = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); + ushort a = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample)); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba64(new Rgba64(r, g, b, a)); + } + } + else if (pixelOffset == 0 && increment == 1) + { + PixelOperations.Instance.FromRgba32Bytes( + configuration, + scanlineSpan[..(int)(frameControl.Width * bytesPerPixel)], + rowSpan.Slice((int)frameControl.XOffset, (int)frameControl.Width), + (int)frameControl.Width); + } + else + { + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + int o = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) + { + byte r = Unsafe.Add(ref scanlineSpanRef, (uint)o); + byte g = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); + byte b = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); + byte a = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample))); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba32(new Rgba32(r, g, b, a)); + } + } + } + } +} diff --git a/ImageSharp/Formats/Png/PngThrowHelper.cs b/ImageSharp/Formats/Png/PngThrowHelper.cs new file mode 100644 index 0000000..455af88 --- /dev/null +++ b/ImageSharp/Formats/Png/PngThrowHelper.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Png { + internal static class PngThrowHelper + { + [DoesNotReturn] + public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage); + + [DoesNotReturn] + public static void ThrowInvalidHeader() => throw new InvalidImageContentException("PNG Image must contain a header chunk and it must be located before any other chunks."); + + [DoesNotReturn] + public static void ThrowNoData() => throw new InvalidImageContentException("PNG Image does not contain a data chunk."); + + [DoesNotReturn] + public static void ThrowMissingDefaultData() => throw new InvalidImageContentException("APNG Image does not contain a default data chunk."); + + [DoesNotReturn] + public static void ThrowInvalidAnimationControl() => throw new InvalidImageContentException("APNG Image must contain a acTL chunk and it must be located before any IDAT and fdAT chunks."); + + [DoesNotReturn] + public static void ThrowMissingFrameControl() => throw new InvalidImageContentException("One of APNG Image's frames do not have a frame control chunk."); + + [DoesNotReturn] + public static void ThrowMissingPalette() => throw new InvalidImageContentException("PNG Image does not contain a palette chunk."); + + [DoesNotReturn] + public static void ThrowInvalidChunkType() => throw new InvalidImageContentException("Invalid PNG data."); + + [DoesNotReturn] + public static void ThrowInvalidChunkType(string message) => throw new InvalidImageContentException(message); + + [DoesNotReturn] + public static void ThrowInvalidChunkCrc(string chunkTypeName) => throw new InvalidImageContentException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!"); + + [DoesNotReturn] + public static void ThrowInvalidParameter(object value, string message, [CallerArgumentExpression(nameof(value))] string name = "") + => throw new NotSupportedException($"Invalid {name}. {message}. Was '{value}'."); + + [DoesNotReturn] + public static void ThrowInvalidParameter(object value1, object value2, string message, [CallerArgumentExpression(nameof(value1))] string name1 = "", [CallerArgumentExpression(nameof(value2))] string name2 = "") + => throw new NotSupportedException($"Invalid {name1} or {name2}. {message}. Was '{value1}' and '{value2}'."); + + [DoesNotReturn] + public static void ThrowNotSupportedColor() => throw new NotSupportedException("Unsupported PNG color type."); + + [DoesNotReturn] + public static void ThrowUnknownFilter() => throw new InvalidImageContentException("Unknown filter type."); + } +} diff --git a/ImageSharp/Formats/Png/README.md b/ImageSharp/Formats/Png/README.md new file mode 100644 index 0000000..8ade379 --- /dev/null +++ b/ImageSharp/Formats/Png/README.md @@ -0,0 +1,6 @@ +Encoder/Decoder adapted from: + +https://github.com/yufeih/Nine.Imaging/ +https://imagetools.codeplex.com/ +https://github.com/leonbloy/pngcs + diff --git a/ImageSharp/Formats/Qoi/QoiChannels.cs b/ImageSharp/Formats/Qoi/QoiChannels.cs new file mode 100644 index 0000000..ee805be --- /dev/null +++ b/ImageSharp/Formats/Qoi/QoiChannels.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Qoi { + /// + /// Provides enumeration of available QOI color channels. + /// + public enum QoiChannels + { + /// + /// Each pixel is an R,G,B triple. + /// + Rgb = 3, + + /// + /// Each pixel is an R,G,B triple, followed by an alpha sample. + /// + Rgba = 4 + } +} diff --git a/ImageSharp/Formats/Qoi/QoiChunk.cs b/ImageSharp/Formats/Qoi/QoiChunk.cs new file mode 100644 index 0000000..b6e87cb --- /dev/null +++ b/ImageSharp/Formats/Qoi/QoiChunk.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Qoi { + /// + /// Enum that contains the operations that encoder and decoder must process, written + /// in binary to be easier to compare them in the reference + /// + internal enum QoiChunk + { + /// + /// Indicates that the operation is QOI_OP_RGB where the RGB values are written + /// in one byte each one after this marker + /// + QoiOpRgb = 0b11111110, + + /// + /// Indicates that the operation is QOI_OP_RGBA where the RGBA values are written + /// in one byte each one after this marker + /// + QoiOpRgba = 0b11111111, + + /// + /// Indicates that the operation is QOI_OP_INDEX where one byte contains a 2-bit + /// marker (0b00) followed by an index on the previously seen pixels array 0..63 + /// + QoiOpIndex = 0b00000000, + + /// + /// Indicates that the operation is QOI_OP_DIFF where one byte contains a 2-bit + /// marker (0b01) followed by 2-bit differences in red, green and blue channel + /// with the previous pixel with a bias of 2 (-2..1) + /// + QoiOpDiff = 0b01000000, + + /// + /// Indicates that the operation is QOI_OP_LUMA where one byte contains a 2-bit + /// marker (0b01) followed by a 6-bits number that indicates the difference of + /// the green channel with the previous pixel. Then another byte that contains + /// a 4-bit number that indicates the difference of the red channel minus the + /// previous difference, and another 4-bit number that indicates the difference + /// of the blue channel minus the green difference + /// Example: 0b10[6-bits diff green] 0b[6-bits dr-dg][6-bits db-dg] + /// dr_dg = (cur_px.r - prev_px.r) - (cur_px.g - prev_px.g) + /// db_dg = (cur_px.b - prev_px.b) - (cur_px.g - prev_px.g) + /// + QoiOpLuma = 0b10000000, + + /// + /// Indicates that the operation is QOI_OP_RUN where one byte contains a 2-bit + /// marker (0b11) followed by a 6-bits number that indicates the times that the + /// previous pixel is repeated + /// + QoiOpRun = 0b11000000 + } +} diff --git a/ImageSharp/Formats/Qoi/QoiColorSpace.cs b/ImageSharp/Formats/Qoi/QoiColorSpace.cs new file mode 100644 index 0000000..fcbf126 --- /dev/null +++ b/ImageSharp/Formats/Qoi/QoiColorSpace.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// ReSharper disable InconsistentNaming +// ReSharper disable IdentifierTypo +namespace SixLabors.ImageSharp.Formats.Qoi { + /// + /// Enum for the different QOI color spaces. + /// + public enum QoiColorSpace + { + /// + /// sRGB color space with linear alpha value + /// + SrgbWithLinearAlpha, + + /// + /// All the values in the color space are linear + /// + AllChannelsLinear + } +} diff --git a/ImageSharp/Formats/Qoi/QoiConfigurationModule.cs b/ImageSharp/Formats/Qoi/QoiConfigurationModule.cs new file mode 100644 index 0000000..841492f --- /dev/null +++ b/ImageSharp/Formats/Qoi/QoiConfigurationModule.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Qoi { + /// + /// Registers the image encoders, decoders and mime type detectors for the qoi format. + /// + public sealed class QoiConfigurationModule : IImageFormatConfigurationModule + { + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetDecoder(QoiFormat.Instance, QoiDecoder.Instance); + configuration.ImageFormatsManager.SetEncoder(QoiFormat.Instance, new QoiEncoder()); + configuration.ImageFormatsManager.AddImageFormatDetector(new QoiImageFormatDetector()); + } + } +} diff --git a/ImageSharp/Formats/Qoi/QoiConstants.cs b/ImageSharp/Formats/Qoi/QoiConstants.cs new file mode 100644 index 0000000..9fe79b6 --- /dev/null +++ b/ImageSharp/Formats/Qoi/QoiConstants.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Text; + +namespace SixLabors.ImageSharp.Formats.Qoi { + internal static class QoiConstants + { + private static readonly byte[] SMagic = Encoding.UTF8.GetBytes("qoif"); + + /// + /// Gets the bytes that indicates the image is QOI + /// + public static ReadOnlySpan Magic => SMagic; + + /// + /// Gets the list of mimetypes that equate to a QOI. + /// See https://github.com/phoboslab/qoi/issues/167 + /// + public static string[] MimeTypes { get; } = ["image/qoi", "image/x-qoi", "image/vnd.qoi"]; + + /// + /// Gets the list of file extensions that equate to a QOI. + /// + public static string[] FileExtensions { get; } = ["qoi"]; + } +} diff --git a/ImageSharp/Formats/Qoi/QoiDecoder.cs b/ImageSharp/Formats/Qoi/QoiDecoder.cs new file mode 100644 index 0000000..c2e3c75 --- /dev/null +++ b/ImageSharp/Formats/Qoi/QoiDecoder.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Qoi { + internal class QoiDecoder : ImageDecoder + { + private QoiDecoder() + { + } + + public static QoiDecoder Instance { get; } = new(); + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + QoiDecoderCore decoder = new(options); + Image image = decoder.Decode(options.Configuration, stream, cancellationToken); + + ScaleToTargetSize(options, image); + + return image; + } + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + return this.Decode(options, stream, cancellationToken); + } + + protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + return new QoiDecoderCore(options).Identify(options.Configuration, stream, cancellationToken); + } + } +} diff --git a/ImageSharp/Formats/Qoi/QoiDecoderCore.cs b/ImageSharp/Formats/Qoi/QoiDecoderCore.cs new file mode 100644 index 0000000..a8d16e1 --- /dev/null +++ b/ImageSharp/Formats/Qoi/QoiDecoderCore.cs @@ -0,0 +1,282 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Qoi { + internal class QoiDecoderCore : ImageDecoderCore + { + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// Used the manage memory allocations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The QOI header. + /// + private QoiHeader header; + + public QoiDecoderCore(DecoderOptions options) + : base(options) + { + this.configuration = options.Configuration; + this.memoryAllocator = this.configuration.MemoryAllocator; + } + + /// + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + { + // Process the header to get metadata + this.ProcessHeader(stream); + + // Create Image object + ImageMetadata metadata = new(); + QoiMetadata qoiMetadata = metadata.GetQoiMetadata(); + qoiMetadata.Channels = this.header.Channels; + qoiMetadata.ColorSpace = this.header.ColorSpace; + Image image = new(this.configuration, (int)this.header.Width, (int)this.header.Height, metadata); + Buffer2D pixels = image.GetRootFramePixelBuffer(); + + this.ProcessPixels(stream, pixels); + + return image; + } + + /// + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + this.ProcessHeader(stream); + PixelTypeInfo pixelType = new(8 * (int)this.header.Channels); + Size size = new((int)this.header.Width, (int)this.header.Height); + + ImageMetadata metadata = new(); + QoiMetadata qoiMetadata = metadata.GetQoiMetadata(); + qoiMetadata.Channels = this.header.Channels; + qoiMetadata.ColorSpace = this.header.ColorSpace; + + return new ImageInfo(size, metadata); + } + + /// + /// Processes the 14-byte header to validate the image and save the metadata + /// in + /// + /// The stream where the bytes are being read + /// If the stream doesn't store a qoi image + private void ProcessHeader(BufferedReadStream stream) + { + Span magicBytes = stackalloc byte[4]; + Span widthBytes = stackalloc byte[4]; + Span heightBytes = stackalloc byte[4]; + + // Read magic bytes + int read = stream.Read(magicBytes); + if (read != 4 || !magicBytes.SequenceEqual(QoiConstants.Magic.ToArray())) + { + ThrowInvalidImageContentException(); + } + + // If it's a qoi image, read the rest of properties + read = stream.Read(widthBytes); + if (read != 4) + { + ThrowInvalidImageContentException(); + } + + read = stream.Read(heightBytes); + if (read != 4) + { + ThrowInvalidImageContentException(); + } + + // These numbers are in Big Endian so we have to reverse them to get the real number + uint width = BinaryPrimitives.ReadUInt32BigEndian(widthBytes); + uint height = BinaryPrimitives.ReadUInt32BigEndian(heightBytes); + if (width == 0 || height == 0) + { + throw new InvalidImageContentException( + $"The image has an invalid size: width = {width}, height = {height}"); + } + + int channels = stream.ReadByte(); + if (channels is -1 or (not 3 and not 4)) + { + ThrowInvalidImageContentException(); + } + + int colorSpace = stream.ReadByte(); + if (colorSpace is -1 or (not 0 and not 1)) + { + ThrowInvalidImageContentException(); + } + + this.header = new QoiHeader(width, height, (QoiChannels)channels, (QoiColorSpace)colorSpace); + } + + [DoesNotReturn] + private static void ThrowInvalidImageContentException() + => throw new InvalidImageContentException("The image is not a valid QOI image."); + + private void ProcessPixels(BufferedReadStream stream, Buffer2D pixels) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner previouslySeenPixelsBuffer = this.memoryAllocator.Allocate(64, AllocationOptions.Clean); + Span previouslySeenPixels = previouslySeenPixelsBuffer.GetSpan(); + Rgba32 previousPixel = new(0, 0, 0, 255); + + // We save the pixel to avoid losing the fully opaque black pixel + // See https://github.com/phoboslab/qoi/issues/258 + int pixelArrayPosition = GetArrayPosition(previousPixel); + previouslySeenPixels[pixelArrayPosition] = previousPixel; + byte operationByte; + Rgba32 readPixel = default; + Span pixelBytes = MemoryMarshal.CreateSpan(ref Unsafe.As(ref readPixel), 4); + TPixel pixel = default; + + for (int i = 0; i < this.header.Height; i++) + { + Span row = pixels.DangerousGetRowSpan(i); + for (int j = 0; j < row.Length; j++) + { + operationByte = (byte)stream.ReadByte(); + switch ((QoiChunk)operationByte) + { + // Reading one pixel with previous alpha intact + case QoiChunk.QoiOpRgb: + if (stream.Read(pixelBytes[..3]) < 3) + { + ThrowInvalidImageContentException(); + } + + readPixel.A = previousPixel.A; + pixel = TPixel.FromRgba32(readPixel); + pixelArrayPosition = GetArrayPosition(readPixel); + previouslySeenPixels[pixelArrayPosition] = readPixel; + break; + + // Reading one pixel with new alpha + case QoiChunk.QoiOpRgba: + if (stream.Read(pixelBytes) < 4) + { + ThrowInvalidImageContentException(); + } + + pixel = TPixel.FromRgba32(readPixel); + pixelArrayPosition = GetArrayPosition(readPixel); + previouslySeenPixels[pixelArrayPosition] = readPixel; + break; + + default: + switch ((QoiChunk)(operationByte & 0b11000000)) + { + // Getting one pixel from previously seen pixels + case QoiChunk.QoiOpIndex: + readPixel = previouslySeenPixels[operationByte]; + pixel = TPixel.FromRgba32(readPixel); + break; + + // Get one pixel from the difference (-2..1) of the previous pixel + case QoiChunk.QoiOpDiff: + int redDifference = (operationByte & 0b00110000) >> 4; + int greenDifference = (operationByte & 0b00001100) >> 2; + int blueDifference = operationByte & 0b00000011; + readPixel = previousPixel with + { + R = (byte)Numerics.Modulo256(previousPixel.R + (redDifference - 2)), + G = (byte)Numerics.Modulo256(previousPixel.G + (greenDifference - 2)), + B = (byte)Numerics.Modulo256(previousPixel.B + (blueDifference - 2)) + }; + pixel = TPixel.FromRgba32(readPixel); + pixelArrayPosition = GetArrayPosition(readPixel); + previouslySeenPixels[pixelArrayPosition] = readPixel; + break; + + // Get green difference in 6 bits and red and blue differences + // depending on the green one + case QoiChunk.QoiOpLuma: + int diffGreen = operationByte & 0b00111111; + int currentGreen = Numerics.Modulo256(previousPixel.G + (diffGreen - 32)); + int nextByte = stream.ReadByte(); + int diffRedDG = nextByte >> 4; + int diffBlueDG = nextByte & 0b00001111; + int currentRed = Numerics.Modulo256(diffRedDG - 8 + (diffGreen - 32) + previousPixel.R); + int currentBlue = Numerics.Modulo256(diffBlueDG - 8 + (diffGreen - 32) + previousPixel.B); + readPixel = previousPixel with { R = (byte)currentRed, B = (byte)currentBlue, G = (byte)currentGreen }; + pixel = TPixel.FromRgba32(readPixel); + pixelArrayPosition = GetArrayPosition(readPixel); + previouslySeenPixels[pixelArrayPosition] = readPixel; + break; + + // Repeating the previous pixel 1..63 times + case QoiChunk.QoiOpRun: + int repetitions = operationByte & 0b00111111; + if (repetitions is 62 or 63) + { + ThrowInvalidImageContentException(); + } + + readPixel = previousPixel; + pixel = TPixel.FromRgba32(readPixel); + for (int k = -1; k < repetitions; k++, j++) + { + if (j == row.Length) + { + j = 0; + i++; + row = pixels.DangerousGetRowSpan(i); + } + + row[j] = pixel; + } + + j--; + continue; + + default: + ThrowInvalidImageContentException(); + return; + } + + break; + } + + row[j] = pixel; + previousPixel = readPixel; + } + } + + // Check stream end + for (int i = 0; i < 7; i++) + { + if (stream.ReadByte() != 0) + { + ThrowInvalidImageContentException(); + } + } + + if (stream.ReadByte() != 1) + { + ThrowInvalidImageContentException(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetArrayPosition(Rgba32 pixel) + => Numerics.Modulo64((pixel.R * 3) + (pixel.G * 5) + (pixel.B * 7) + (pixel.A * 11)); + } +} diff --git a/ImageSharp/Formats/Qoi/QoiEncoder.cs b/ImageSharp/Formats/Qoi/QoiEncoder.cs new file mode 100644 index 0000000..4decaad --- /dev/null +++ b/ImageSharp/Formats/Qoi/QoiEncoder.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Qoi { + /// + /// Image encoder for writing an image to a stream as a QOI image + /// + public class QoiEncoder : AlphaAwareImageEncoder + { + /// + /// Gets the color channels on the image that can be + /// RGB or RGBA. This is purely informative. It doesn't + /// change the way data chunks are encoded. + /// + public QoiChannels? Channels { get; init; } + + /// + /// Gets the color space of the image that can be sRGB with + /// linear alpha or all channels linear. This is purely + /// informative. It doesn't change the way data chunks are encoded. + /// + public QoiColorSpace? ColorSpace { get; init; } + + /// + protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) + { + QoiEncoderCore encoder = new(this, image.Configuration); + encoder.Encode(image, stream, cancellationToken); + } + } +} diff --git a/ImageSharp/Formats/Qoi/QoiEncoderCore.cs b/ImageSharp/Formats/Qoi/QoiEncoderCore.cs new file mode 100644 index 0000000..3bd50c1 --- /dev/null +++ b/ImageSharp/Formats/Qoi/QoiEncoderCore.cs @@ -0,0 +1,260 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Qoi { + /// + /// Image encoder for writing an image to a stream as a QOi image + /// + internal class QoiEncoderCore + { + /// + /// The encoder with options + /// + private readonly QoiEncoder encoder; + + /// + /// Used the manage memory allocations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The configuration instance for the encoding operation. + /// + private readonly Configuration configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The encoder with options. + /// The configuration of the Encoder. + public QoiEncoderCore(QoiEncoder encoder, Configuration configuration) + { + this.encoder = encoder; + this.configuration = configuration; + this.memoryAllocator = configuration.MemoryAllocator; + } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + this.WriteHeader(image, stream); + this.WritePixels(image, stream, cancellationToken); + WriteEndOfStream(stream); + stream.Flush(); + } + + private void WriteHeader(Image image, Stream stream) + { + // Get metadata + Span width = stackalloc byte[4]; + Span height = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32BigEndian(width, (uint)image.Width); + BinaryPrimitives.WriteUInt32BigEndian(height, (uint)image.Height); + QoiChannels qoiChannels = this.encoder.Channels ?? QoiChannels.Rgba; + QoiColorSpace qoiColorSpace = this.encoder.ColorSpace ?? QoiColorSpace.SrgbWithLinearAlpha; + + // Write header to the stream + stream.Write(QoiConstants.Magic); + stream.Write(width); + stream.Write(height); + stream.WriteByte((byte)qoiChannels); + stream.WriteByte((byte)qoiColorSpace); + } + + private void WritePixels(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + // Start image encoding + using IMemoryOwner previouslySeenPixelsBuffer = this.memoryAllocator.Allocate(64, AllocationOptions.Clean); + Span previouslySeenPixels = previouslySeenPixelsBuffer.GetSpan(); + Rgba32 previousPixel = new(0, 0, 0, 255); + Rgba32 currentRgba32 = default; + + ImageFrame? clonedFrame = null; + try + { + // TODO: Try to avoid cloning the frame if possible. + // We should be cloning individual scanlines instead. + if (EncodingUtilities.ShouldReplaceTransparentPixels(this.encoder.TransparentColorMode)) + { + clonedFrame = image.Frames.RootFrame.Clone(); + EncodingUtilities.ReplaceTransparentPixels(clonedFrame); + } + + ImageFrame encodingFrame = clonedFrame ?? image.Frames.RootFrame; + Buffer2D pixels = encodingFrame.PixelBuffer; + + using IMemoryOwner rgbaRowBuffer = this.memoryAllocator.Allocate(pixels.Width); + Span rgbaRow = rgbaRowBuffer.GetSpan(); + Configuration configuration = this.configuration; + for (int i = 0; i < pixels.Height; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span row = pixels.DangerousGetRowSpan(i); + PixelOperations.Instance.ToRgba32(this.configuration, row, rgbaRow); + for (int j = 0; j < row.Length && i < pixels.Height; j++) + { + // We get the RGBA value from pixels + currentRgba32 = rgbaRow[j]; + + // First, we check if the current pixel is equal to the previous one + // If so, we do a QOI_OP_RUN + if (currentRgba32.Equals(previousPixel)) + { + /* It looks like this isn't an error, but this makes possible that + * files start with a QOI_OP_RUN if their first pixel is a fully opaque + * black. However, the decoder of this project takes that into consideration + * + * To further details, see https://github.com/phoboslab/qoi/issues/258, + * and we should discuss what to do about this approach and + * if it's correct + */ + int repetitions = 0; + do + { + repetitions++; + j++; + if (j == row.Length) + { + j = 0; + i++; + if (i == pixels.Height) + { + break; + } + + row = pixels.DangerousGetRowSpan(i); + PixelOperations.Instance.ToRgba32(configuration, row, rgbaRow); + } + + currentRgba32 = rgbaRow[j]; + } + while (currentRgba32.Equals(previousPixel) && repetitions < 62); + + j--; + stream.WriteByte((byte)((int)QoiChunk.QoiOpRun | (repetitions - 1))); + + /* If it's a QOI_OP_RUN, we don't overwrite the previous pixel since + * it will be taken and compared on the next iteration + */ + continue; + } + + // else, we check if it exists in the previously seen pixels + // If so, we do a QOI_OP_INDEX + int pixelArrayPosition = GetArrayPosition(currentRgba32); + if (previouslySeenPixels[pixelArrayPosition].Equals(currentRgba32)) + { + stream.WriteByte((byte)pixelArrayPosition); + } + else + { + // else, we check if the difference is less than -2..1 + // Since it wasn't found on the previously seen pixels, we save it + previouslySeenPixels[pixelArrayPosition] = currentRgba32; + + int diffRed = currentRgba32.R - previousPixel.R; + int diffGreen = currentRgba32.G - previousPixel.G; + int diffBlue = currentRgba32.B - previousPixel.B; + + // If so, we do a QOI_OP_DIFF + if (diffRed is >= -2 and <= 1 && + diffGreen is >= -2 and <= 1 && + diffBlue is >= -2 and <= 1 && + currentRgba32.A == previousPixel.A) + { + // Bottom limit is -2, so we add 2 to make it equal to 0 + int dr = diffRed + 2; + int dg = diffGreen + 2; + int db = diffBlue + 2; + byte valueToWrite = (byte)((int)QoiChunk.QoiOpDiff | (dr << 4) | (dg << 2) | db); + stream.WriteByte(valueToWrite); + } + else + { + // else, we check if the green difference is less than -32..31 and the rest -8..7 + // If so, we do a QOI_OP_LUMA + int diffRedGreen = diffRed - diffGreen; + int diffBlueGreen = diffBlue - diffGreen; + if (diffGreen is >= -32 and <= 31 && + diffRedGreen is >= -8 and <= 7 && + diffBlueGreen is >= -8 and <= 7 && + currentRgba32.A == previousPixel.A) + { + int dr_dg = diffRedGreen + 8; + int db_dg = diffBlueGreen + 8; + byte byteToWrite1 = (byte)((int)QoiChunk.QoiOpLuma | (diffGreen + 32)); + byte byteToWrite2 = (byte)((dr_dg << 4) | db_dg); + stream.WriteByte(byteToWrite1); + stream.WriteByte(byteToWrite2); + } + else + { + // else, we check if the alpha is equal to the previous pixel + // If so, we do a QOI_OP_RGB + if (currentRgba32.A == previousPixel.A) + { + stream.WriteByte((byte)QoiChunk.QoiOpRgb); + stream.WriteByte(currentRgba32.R); + stream.WriteByte(currentRgba32.G); + stream.WriteByte(currentRgba32.B); + } + else + { + // else, we do a QOI_OP_RGBA + stream.WriteByte((byte)QoiChunk.QoiOpRgba); + stream.WriteByte(currentRgba32.R); + stream.WriteByte(currentRgba32.G); + stream.WriteByte(currentRgba32.B); + stream.WriteByte(currentRgba32.A); + } + } + } + } + + previousPixel = currentRgba32; + } + } + } + finally + { + clonedFrame?.Dispose(); + } + } + + private static void WriteEndOfStream(Stream stream) + { + // Write bytes to end stream + for (int i = 0; i < 7; i++) + { + stream.WriteByte(0); + } + + stream.WriteByte(1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetArrayPosition(Rgba32 pixel) + => Numerics.Modulo64((pixel.R * 3) + (pixel.G * 5) + (pixel.B * 7) + (pixel.A * 11)); + } +} diff --git a/ImageSharp/Formats/Qoi/QoiFormat.cs b/ImageSharp/Formats/Qoi/QoiFormat.cs new file mode 100644 index 0000000..f799f93 --- /dev/null +++ b/ImageSharp/Formats/Qoi/QoiFormat.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Qoi { + /// + /// Registers the image encoders, decoders and mime type detectors for the qoi format. + /// + public sealed class QoiFormat : IImageFormat + { + private QoiFormat() + { + } + + /// + /// Gets the shared instance. + /// + public static QoiFormat Instance { get; } = new(); + + /// + public string DefaultMimeType => "image/qoi"; + + /// + public string Name => "QOI"; + + /// + public IEnumerable MimeTypes => QoiConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => QoiConstants.FileExtensions; + + /// + public QoiMetadata CreateDefaultFormatMetadata() => new(); + } +} diff --git a/ImageSharp/Formats/Qoi/QoiHeader.cs b/ImageSharp/Formats/Qoi/QoiHeader.cs new file mode 100644 index 0000000..c80e9d5 --- /dev/null +++ b/ImageSharp/Formats/Qoi/QoiHeader.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Text; + +namespace SixLabors.ImageSharp.Formats.Qoi { + /// + /// Represents the qoi header chunk. + /// + internal readonly struct QoiHeader + { + public QoiHeader(uint width, uint height, QoiChannels channels, QoiColorSpace colorSpace) + { + this.Width = width; + this.Height = height; + this.Channels = channels; + this.ColorSpace = colorSpace; + } + + /// + /// Gets the magic bytes "qoif" + /// + public byte[] Magic { get; } = Encoding.UTF8.GetBytes("qoif"); + + /// + /// Gets the image width in pixels (Big Endian) + /// + public uint Width { get; } + + /// + /// Gets the image height in pixels (Big Endian) + /// + public uint Height { get; } + + /// + /// Gets the color channels of the image. 3 = RGB, 4 = RGBA. + /// + public QoiChannels Channels { get; } + + /// + /// Gets the color space of the image. 0 = sRGB with linear alpha, 1 = All channels linear + /// + public QoiColorSpace ColorSpace { get; } + } +} diff --git a/ImageSharp/Formats/Qoi/QoiImageFormatDetector.cs b/ImageSharp/Formats/Qoi/QoiImageFormatDetector.cs new file mode 100644 index 0000000..186addd --- /dev/null +++ b/ImageSharp/Formats/Qoi/QoiImageFormatDetector.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Qoi { + /// + /// Detects qoi file headers + /// + public class QoiImageFormatDetector : IImageFormatDetector + { + /// + public int HeaderSize => 14; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.IsSupportedFileFormat(header) ? QoiFormat.Instance : null; + return format != null; + } + + private bool IsSupportedFileFormat(ReadOnlySpan header) + => header.Length >= this.HeaderSize && QoiConstants.Magic.SequenceEqual(header[..4]); + } +} diff --git a/ImageSharp/Formats/Qoi/QoiMetadata.cs b/ImageSharp/Formats/Qoi/QoiMetadata.cs new file mode 100644 index 0000000..816bea9 --- /dev/null +++ b/ImageSharp/Formats/Qoi/QoiMetadata.cs @@ -0,0 +1,103 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Qoi { + /// + /// Provides Qoi specific metadata information for the image. + /// + public class QoiMetadata : IFormatMetadata + { + /// + /// Initializes a new instance of the class. + /// + public QoiMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private QoiMetadata(QoiMetadata other) + { + this.Channels = other.Channels; + this.ColorSpace = other.ColorSpace; + } + + /// + /// Gets or sets color channels of the image. 3 = RGB, 4 = RGBA. + /// + public QoiChannels Channels { get; set; } + + /// + /// Gets or sets color space of the image. 0 = sRGB with linear alpha, 1 = All channels linear + /// + public QoiColorSpace ColorSpace { get; set; } + + /// + public static QoiMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + PixelColorType color = metadata.PixelTypeInfo.ColorType; + + if (color.HasFlag(PixelColorType.Alpha)) + { + return new QoiMetadata { Channels = QoiChannels.Rgba }; + } + + return new QoiMetadata { Channels = QoiChannels.Rgb }; + } + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp; + PixelColorType colorType; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + PixelComponentInfo info; + + switch (this.Channels) + { + case QoiChannels.Rgb: + bpp = 24; + colorType = PixelColorType.RGB; + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + break; + default: + bpp = 32; + colorType = PixelColorType.RGB | PixelColorType.Alpha; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ColorType = colorType, + ComponentInfo = info, + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo() + }; + + /// + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public QoiMetadata DeepClone() => new(this); + } +} diff --git a/ImageSharp/Formats/Qoi/qoi-specification.pdf b/ImageSharp/Formats/Qoi/qoi-specification.pdf new file mode 100644 index 0000000..3ffa4bd Binary files /dev/null and b/ImageSharp/Formats/Qoi/qoi-specification.pdf differ diff --git a/ImageSharp/Formats/SegmentIntegrityHandling.cs b/ImageSharp/Formats/SegmentIntegrityHandling.cs new file mode 100644 index 0000000..9d88984 --- /dev/null +++ b/ImageSharp/Formats/SegmentIntegrityHandling.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats { + /// + /// Specifies how to handle validation of recoverable errors in ancillary and image data segments. + /// Structural errors that prevent safe decoding remain fatal regardless of the selected mode. + /// + public enum SegmentIntegrityHandling + { + /// + /// Do not ignore any recoverable ancillary or image data segment errors. + /// + Strict = 0, + + /// + /// Ignore recoverable errors in ancillary segments, such as optional metadata. + /// + IgnoreAncillary = 1, + + /// + /// Ignore recoverable errors in image data segments in addition to ancillary segments. + /// + IgnoreImageData = 2, + } +} diff --git a/ImageSharp/Formats/SpecializedImageDecoder{T}.cs b/ImageSharp/Formats/SpecializedImageDecoder{T}.cs new file mode 100644 index 0000000..f97a1ac --- /dev/null +++ b/ImageSharp/Formats/SpecializedImageDecoder{T}.cs @@ -0,0 +1,118 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SixLabors.ImageSharp.Formats { + /// + /// Acts as a base class for specialized image decoders. + /// Specialized decoders allow for additional options to be passed to the decoder. + /// Types that inherit this decoder are required to implement cancellable synchronous decoding operations only. + /// + /// The type of specialized options. + public abstract class SpecializedImageDecoder : ImageDecoder, ISpecializedImageDecoder + where T : ISpecializedDecoderOptions + { + /// + public Image Decode(T options, Stream stream) + where TPixel : unmanaged, IPixel + { + Image image = WithSeekableStream( + options.GeneralOptions, + stream, + s => this.Decode(options, s, default)); + + this.SetDecoderFormat(options.GeneralOptions.Configuration, image); + + return image; + } + + /// + public Image Decode(T options, Stream stream) + { + Image image = WithSeekableStream( + options.GeneralOptions, + stream, + s => this.Decode(options, s, default)); + + this.SetDecoderFormat(options.GeneralOptions.Configuration, image); + + return image; + } + + /// + public async Task> DecodeAsync(T options, Stream stream, CancellationToken cancellationToken = default) + where TPixel : unmanaged, IPixel + { + Image image = await WithSeekableMemoryStreamAsync( + options.GeneralOptions, + stream, + (s, ct) => this.Decode(options, s, ct), + cancellationToken).ConfigureAwait(false); + + this.SetDecoderFormat(options.GeneralOptions.Configuration, image); + + return image; + } + + /// + public async Task DecodeAsync(T options, Stream stream, CancellationToken cancellationToken = default) + { + Image image = await WithSeekableMemoryStreamAsync( + options.GeneralOptions, + stream, + (s, ct) => this.Decode(options, s, ct), + cancellationToken).ConfigureAwait(false); + + this.SetDecoderFormat(options.GeneralOptions.Configuration, image); + + return image; + } + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// + /// This method is designed to support the ImageSharp internal infrastructure and is not recommended for direct use. + /// + /// The pixel format. + /// The specialized decoder options. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The . + /// Thrown if the encoded image contains errors. + protected abstract Image Decode(T options, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel; + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// + /// This method is designed to support the ImageSharp internal infrastructure and is not recommended for direct use. + /// + /// The specialized decoder options. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The . + /// Thrown if the encoded image contains errors. + protected abstract Image Decode(T options, Stream stream, CancellationToken cancellationToken); + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(this.CreateDefaultSpecializedOptions(options), stream, cancellationToken); + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(this.CreateDefaultSpecializedOptions(options), stream, cancellationToken); + + /// + /// A factory method for creating the default specialized options. + /// + /// The general decoder options. + /// The new . + protected abstract T CreateDefaultSpecializedOptions(DecoderOptions options); + } +} diff --git a/ImageSharp/Formats/Tga/README.md b/ImageSharp/Formats/Tga/README.md new file mode 100644 index 0000000..219f111 --- /dev/null +++ b/ImageSharp/Formats/Tga/README.md @@ -0,0 +1,6 @@ +# Encoder/Decoder for true vision targa files + +Useful links for reference: + +- [FileFront](https://www.fileformat.info/format/tga/egff.htm) +- [Tga Specification](http://www.dca.fee.unicamp.br/~martino/disciplinas/ea978/tgaffs.pdf) diff --git a/ImageSharp/Formats/Tga/TGA_Specification.pdf b/ImageSharp/Formats/Tga/TGA_Specification.pdf new file mode 100644 index 0000000..09c9a4d Binary files /dev/null and b/ImageSharp/Formats/Tga/TGA_Specification.pdf differ diff --git a/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs b/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs new file mode 100644 index 0000000..9290f6a --- /dev/null +++ b/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tga { + /// + /// Enumerates the available bits per pixel the tga encoder supports. + /// + public enum TgaBitsPerPixel : byte + { + /// + /// 8 bits per pixel. Each pixel consists of 1 byte. + /// + Bit8 = 8, + + /// + /// 16 bits per pixel. Each pixel consists of 2 bytes. + /// + Bit16 = 16, + + /// + /// 24 bits per pixel. Each pixel consists of 3 bytes. + /// + Bit24 = 24, + + /// + /// 32 bits per pixel. Each pixel consists of 4 bytes. + /// + Bit32 = 32 + } +} diff --git a/ImageSharp/Formats/Tga/TgaCompression.cs b/ImageSharp/Formats/Tga/TgaCompression.cs new file mode 100644 index 0000000..88d788a --- /dev/null +++ b/ImageSharp/Formats/Tga/TgaCompression.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tga { + /// + /// Indicates if compression is used. + /// + public enum TgaCompression + { + /// + /// No compression is used. + /// + None, + + /// + /// Run length encoding is used. + /// + RunLength, + } +} diff --git a/ImageSharp/Formats/Tga/TgaConfigurationModule.cs b/ImageSharp/Formats/Tga/TgaConfigurationModule.cs new file mode 100644 index 0000000..8a1000f --- /dev/null +++ b/ImageSharp/Formats/Tga/TgaConfigurationModule.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tga { + /// + /// Registers the image encoders, decoders and mime type detectors for the tga format. + /// + public sealed class TgaConfigurationModule : IImageFormatConfigurationModule + { + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetEncoder(TgaFormat.Instance, new TgaEncoder()); + configuration.ImageFormatsManager.SetDecoder(TgaFormat.Instance, TgaDecoder.Instance); + configuration.ImageFormatsManager.AddImageFormatDetector(new TgaImageFormatDetector()); + } + } +} diff --git a/ImageSharp/Formats/Tga/TgaConstants.cs b/ImageSharp/Formats/Tga/TgaConstants.cs new file mode 100644 index 0000000..d8c0b76 --- /dev/null +++ b/ImageSharp/Formats/Tga/TgaConstants.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Tga { + internal static class TgaConstants + { + /// + /// The list of mimetypes that equate to a targa file. + /// + public static readonly IEnumerable MimeTypes = ["image/x-tga", "image/x-targa"]; + + /// + /// The list of file extensions that equate to a targa file. + /// + public static readonly IEnumerable FileExtensions = ["tga", "vda", "icb", "vst"]; + + /// + /// The file header length of a tga image in bytes. + /// + public const int FileHeaderLength = 18; + } +} diff --git a/ImageSharp/Formats/Tga/TgaDecoder.cs b/ImageSharp/Formats/Tga/TgaDecoder.cs new file mode 100644 index 0000000..3887afc --- /dev/null +++ b/ImageSharp/Formats/Tga/TgaDecoder.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Tga { + /// + /// Image decoder for Truevision TGA images. + /// + public sealed class TgaDecoder : ImageDecoder + { + private TgaDecoder() + { + } + + /// + /// Gets the shared instance. + /// + public static TgaDecoder Instance { get; } = new(); + + /// + protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + return new TgaDecoderCore(options).Identify(options.Configuration, stream, cancellationToken); + } + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + TgaDecoderCore decoder = new(options); + Image image = decoder.Decode(options.Configuration, stream, cancellationToken); + + ScaleToTargetSize(options, image); + + return image; + } + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(options, stream, cancellationToken); + } +} diff --git a/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/ImageSharp/Formats/Tga/TgaDecoderCore.cs new file mode 100644 index 0000000..2425483 --- /dev/null +++ b/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -0,0 +1,936 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tga { + /// + /// Performs the tga decoding operation. + /// + internal sealed class TgaDecoderCore : ImageDecoderCore + { + /// + /// General configuration options. + /// + private readonly Configuration configuration; + + /// + /// The metadata. + /// + private ImageMetadata? metadata; + + /// + /// The tga specific metadata. + /// + private TgaMetadata? tgaMetadata; + + /// + /// The file header containing general information about the image. + /// + private TgaFileHeader fileHeader; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// Indicates whether there is a alpha channel present. + /// + private bool hasAlpha; + + /// + /// Initializes a new instance of the class. + /// + /// The options. + public TgaDecoderCore(DecoderOptions options) + : base(options) + { + this.configuration = options.Configuration; + this.memoryAllocator = this.configuration.MemoryAllocator; + } + + /// + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + { + try + { + TgaImageOrigin origin = this.ReadFileHeader(stream); + stream.Skip(this.fileHeader.IdLength); + + // Parse the color map, if present. + if (this.fileHeader.ColorMapType is not 0 and not 1) + { + TgaThrowHelper.ThrowNotSupportedException($"Unknown tga colormap type {this.fileHeader.ColorMapType} found"); + } + + if (this.fileHeader.Width == 0 || this.fileHeader.Height == 0) + { + throw new UnknownImageFormatException("Width or height cannot be 0"); + } + + Image image = new(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); + Buffer2D pixels = image.GetRootFramePixelBuffer(); + + if (this.fileHeader.ColorMapType == 1) + { + if (this.fileHeader.CMapLength <= 0) + { + TgaThrowHelper.ThrowInvalidImageContentException("Missing tga color map length"); + } + + if (this.fileHeader.CMapDepth <= 0) + { + TgaThrowHelper.ThrowInvalidImageContentException("Missing tga color map depth"); + } + + int colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; + int colorMapSizeInBytes = this.fileHeader.CMapLength * colorMapPixelSizeInBytes; + using (IMemoryOwner palette = this.memoryAllocator.Allocate(colorMapSizeInBytes, AllocationOptions.Clean)) + { + Span paletteSpan = palette.GetSpan(); + int bytesRead = stream.Read(paletteSpan, this.fileHeader.CMapStart, colorMapSizeInBytes); + if (bytesRead != colorMapSizeInBytes) + { + TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read the color map"); + } + + if (this.fileHeader.ImageType == TgaImageType.RleColorMapped) + { + this.ReadPalettedRle( + stream, + this.fileHeader.Width, + this.fileHeader.Height, + pixels, + paletteSpan, + colorMapPixelSizeInBytes, + origin); + } + else + { + this.ReadPaletted( + stream, + this.fileHeader.Width, + this.fileHeader.Height, + pixels, + paletteSpan, + colorMapPixelSizeInBytes, + origin); + } + } + + return image; + } + + // Even if the image type indicates it is not a paletted image, it can still contain a palette. Skip those bytes. + if (this.fileHeader.CMapLength > 0) + { + int colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; + stream.Skip(this.fileHeader.CMapLength * colorMapPixelSizeInBytes); + } + + switch (this.fileHeader.PixelDepth) + { + case 8: + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle(stream, this.fileHeader.Width, this.fileHeader.Height, pixels, 1, origin); + } + else + { + this.ReadMonoChrome(stream, this.fileHeader.Width, this.fileHeader.Height, pixels, origin); + } + + break; + + case 15: + case 16: + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle(stream, this.fileHeader.Width, this.fileHeader.Height, pixels, 2, origin); + } + else + { + this.ReadBgra16(stream, this.fileHeader.Width, this.fileHeader.Height, pixels, origin); + } + + break; + + case 24: + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle(stream, this.fileHeader.Width, this.fileHeader.Height, pixels, 3, origin); + } + else + { + this.ReadBgr24(stream, this.fileHeader.Width, this.fileHeader.Height, pixels, origin); + } + + break; + + case 32: + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle(stream, this.fileHeader.Width, this.fileHeader.Height, pixels, 4, origin); + } + else + { + this.ReadBgra32(stream, this.fileHeader.Width, this.fileHeader.Height, pixels, origin); + } + + break; + + default: + TgaThrowHelper.ThrowNotSupportedException("ImageSharp does not support this kind of tga files."); + break; + } + + return image; + } + catch (IndexOutOfRangeException e) + { + throw new ImageFormatException("TGA image does not have a valid format.", e); + } + } + + /// + /// Reads a uncompressed TGA image with a palette. + /// + /// The pixel type. + /// The containing image data. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// The color palette. + /// Color map size of one entry in bytes. + /// The image origin. + private void ReadPaletted(BufferedReadStream stream, int width, int height, Buffer2D pixels, Span palette, int colorMapPixelSizeInBytes, TgaImageOrigin origin) + where TPixel : unmanaged, IPixel + { + bool invertX = InvertX(origin); + + for (int y = 0; y < height; y++) + { + int newY = InvertY(y, height, origin); + Span pixelRow = pixels.DangerousGetRowSpan(newY); + + switch (colorMapPixelSizeInBytes) + { + case 2: + if (invertX) + { + for (int x = width - 1; x >= 0; x--) + { + this.ReadPalettedBgra16Pixel(stream, palette, colorMapPixelSizeInBytes, x, pixelRow); + } + } + else + { + for (int x = 0; x < width; x++) + { + this.ReadPalettedBgra16Pixel(stream, palette, colorMapPixelSizeInBytes, x, pixelRow); + } + } + + break; + + case 3: + if (invertX) + { + for (int x = width - 1; x >= 0; x--) + { + ReadPalettedBgr24Pixel(stream, palette, colorMapPixelSizeInBytes, x, pixelRow); + } + } + else + { + for (int x = 0; x < width; x++) + { + ReadPalettedBgr24Pixel(stream, palette, colorMapPixelSizeInBytes, x, pixelRow); + } + } + + break; + + case 4: + if (invertX) + { + for (int x = width - 1; x >= 0; x--) + { + ReadPalettedBgra32Pixel(stream, palette, colorMapPixelSizeInBytes, x, pixelRow); + } + } + else + { + for (int x = 0; x < width; x++) + { + ReadPalettedBgra32Pixel(stream, palette, colorMapPixelSizeInBytes, x, pixelRow); + } + } + + break; + } + } + } + + /// + /// Reads a run length encoded TGA image with a palette. + /// + /// The pixel type. + /// The containing image data. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// The color palette. + /// Color map size of one entry in bytes. + /// The image origin. + private void ReadPalettedRle(BufferedReadStream stream, int width, int height, Buffer2D pixels, Span palette, int colorMapPixelSizeInBytes, TgaImageOrigin origin) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height, AllocationOptions.Clean); + TPixel color = default; + Span bufferSpan = buffer.GetSpan(); + this.UncompressRle(stream, width, height, bufferSpan, bytesPerPixel: 1); + + for (int y = 0; y < height; y++) + { + int newY = InvertY(y, height, origin); + Span pixelRow = pixels.DangerousGetRowSpan(newY); + int rowStartIdx = y * width; + for (int x = 0; x < width; x++) + { + int idx = rowStartIdx + x; + switch (colorMapPixelSizeInBytes) + { + case 1: + color = TPixel.FromL8(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); + break; + case 2: + color = this.ReadPalettedBgra16Pixel(palette, bufferSpan[idx], colorMapPixelSizeInBytes); + break; + case 3: + color = TPixel.FromBgr24(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); + break; + case 4: + color = TPixel.FromBgra32(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); + break; + } + + int newX = InvertX(x, width, origin); + pixelRow[newX] = color; + } + } + } + + /// + /// Reads a uncompressed monochrome TGA image. + /// + /// The pixel type. + /// The containing image data. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// the image origin. + private void ReadMonoChrome(BufferedReadStream stream, int width, int height, Buffer2D pixels, TgaImageOrigin origin) + where TPixel : unmanaged, IPixel + { + if (InvertX(origin)) + { + for (int y = 0; y < height; y++) + { + int newY = InvertY(y, height, origin); + Span pixelSpan = pixels.DangerousGetRowSpan(newY); + for (int x = width - 1; x >= 0; x--) + { + ReadL8Pixel(stream, x, pixelSpan); + } + } + + return; + } + + using IMemoryOwner row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 1, 0); + Span rowSpan = row.GetSpan(); + if (InvertY(origin)) + { + for (int y = height - 1; y >= 0; y--) + { + this.ReadL8Row(stream, width, pixels, rowSpan, y); + } + } + else + { + for (int y = 0; y < height; y++) + { + this.ReadL8Row(stream, width, pixels, rowSpan, y); + } + } + } + + /// + /// Reads a uncompressed TGA image where each pixels has 16 bit. + /// + /// The pixel type. + /// The containing image data. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// The image origin. + private void ReadBgra16(BufferedReadStream stream, int width, int height, Buffer2D pixels, TgaImageOrigin origin) + where TPixel : unmanaged, IPixel + { + bool invertX = InvertX(origin); + using IMemoryOwner row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 2, 0); + Span rowSpan = row.GetSpan(); + Span scratchBuffer = stackalloc byte[2]; + + for (int y = 0; y < height; y++) + { + int newY = InvertY(y, height, origin); + Span pixelSpan = pixels.DangerousGetRowSpan(newY); + + if (invertX) + { + for (int x = width - 1; x >= 0; x--) + { + int bytesRead = stream.Read(scratchBuffer); + if (bytesRead != 2) + { + TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row"); + } + + if (!this.hasAlpha) + { + scratchBuffer[1] |= 1 << 7; + } + + if (this.fileHeader.ImageType == TgaImageType.BlackAndWhite) + { + pixelSpan[x] = TPixel.FromLa16(Unsafe.As(ref MemoryMarshal.GetReference(scratchBuffer))); + } + else + { + pixelSpan[x] = TPixel.FromBgra5551(Unsafe.As(ref MemoryMarshal.GetReference(scratchBuffer))); + } + } + } + else + { + int bytesRead = stream.Read(rowSpan); + if (bytesRead != rowSpan.Length) + { + TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row"); + } + + if (!this.hasAlpha) + { + // We need to set the alpha component value to fully opaque. + for (int x = 1; x < rowSpan.Length; x += 2) + { + rowSpan[x] |= 1 << 7; + } + } + + if (this.fileHeader.ImageType == TgaImageType.BlackAndWhite) + { + PixelOperations.Instance.FromLa16Bytes(this.configuration, rowSpan, pixelSpan, width); + } + else + { + PixelOperations.Instance.FromBgra5551Bytes(this.configuration, rowSpan, pixelSpan, width); + } + } + } + } + + /// + /// Reads a uncompressed TGA image where each pixels has 24 bit. + /// + /// The pixel type. + /// The containing image data. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// The image origin. + private void ReadBgr24(BufferedReadStream stream, int width, int height, Buffer2D pixels, TgaImageOrigin origin) + where TPixel : unmanaged, IPixel + { + if (InvertX(origin)) + { + Span scratchBuffer = stackalloc byte[4]; + for (int y = 0; y < height; y++) + { + int newY = InvertY(y, height, origin); + Span pixelSpan = pixels.DangerousGetRowSpan(newY); + for (int x = width - 1; x >= 0; x--) + { + ReadBgr24Pixel(stream, x, pixelSpan, scratchBuffer); + } + } + + return; + } + + using IMemoryOwner row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 3, 0); + Span rowSpan = row.GetSpan(); + + if (InvertY(origin)) + { + for (int y = height - 1; y >= 0; y--) + { + this.ReadBgr24Row(stream, width, pixels, rowSpan, y); + } + } + else + { + for (int y = 0; y < height; y++) + { + this.ReadBgr24Row(stream, width, pixels, rowSpan, y); + } + } + } + + /// + /// Reads a uncompressed TGA image where each pixels has 32 bit. + /// + /// The pixel type. + /// The containing image data. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// The image origin. + private void ReadBgra32(BufferedReadStream stream, int width, int height, Buffer2D pixels, TgaImageOrigin origin) + where TPixel : unmanaged, IPixel + { + bool invertX = InvertX(origin); + + Guard.NotNull(this.tgaMetadata); + + if (this.tgaMetadata.AlphaChannelBits == 8 && !invertX) + { + using IMemoryOwner row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, 0); + Span rowSpan = row.GetSpan(); + + if (InvertY(origin)) + { + for (int y = height - 1; y >= 0; y--) + { + this.ReadBgra32Row(stream, width, pixels, rowSpan, y); + } + } + else + { + for (int y = 0; y < height; y++) + { + this.ReadBgra32Row(stream, width, pixels, rowSpan, y); + } + } + + return; + } + + Span scratchBuffer = stackalloc byte[4]; + + for (int y = 0; y < height; y++) + { + int newY = InvertY(y, height, origin); + Span pixelRow = pixels.DangerousGetRowSpan(newY); + if (invertX) + { + for (int x = width - 1; x >= 0; x--) + { + this.ReadBgra32Pixel(stream, x, pixelRow, scratchBuffer); + } + } + else + { + for (int x = 0; x < width; x++) + { + this.ReadBgra32Pixel(stream, x, pixelRow, scratchBuffer); + } + } + } + } + + /// + /// Reads a run length encoded TGA image. + /// + /// The pixel type. + /// The containing image data. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// The bytes per pixel. + /// The image origin. + private void ReadRle(BufferedReadStream stream, int width, int height, Buffer2D pixels, int bytesPerPixel, TgaImageOrigin origin) + where TPixel : unmanaged, IPixel + { + TPixel color = default; + + Guard.NotNull(this.tgaMetadata); + + byte alphaBits = this.tgaMetadata.AlphaChannelBits; + using IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * bytesPerPixel, AllocationOptions.Clean); + Span bufferSpan = buffer.GetSpan(); + this.UncompressRle(stream, width, height, bufferSpan, bytesPerPixel); + for (int y = 0; y < height; y++) + { + int newY = InvertY(y, height, origin); + Span pixelRow = pixels.DangerousGetRowSpan(newY); + int rowStartIdx = y * width * bytesPerPixel; + for (int x = 0; x < width; x++) + { + int idx = rowStartIdx + (x * bytesPerPixel); + switch (bytesPerPixel) + { + case 1: + color = TPixel.FromL8(Unsafe.As(ref bufferSpan[idx])); + break; + case 2: + if (!this.hasAlpha) + { + // Set alpha value to 1, to treat it as opaque for Bgra5551. + bufferSpan[idx + 1] = (byte)(bufferSpan[idx + 1] | 128); + } + + if (this.fileHeader.ImageType == TgaImageType.RleBlackAndWhite) + { + color = TPixel.FromLa16(Unsafe.As(ref bufferSpan[idx])); + } + else + { + color = TPixel.FromBgra5551(Unsafe.As(ref bufferSpan[idx])); + } + + break; + case 3: + color = TPixel.FromBgr24(Unsafe.As(ref bufferSpan[idx])); + break; + case 4: + if (this.hasAlpha) + { + color = TPixel.FromBgra32(Unsafe.As(ref bufferSpan[idx])); + } + else + { + byte alpha = alphaBits == 0 ? byte.MaxValue : bufferSpan[idx + 3]; + color = TPixel.FromBgra32(new Bgra32(bufferSpan[idx + 2], bufferSpan[idx + 1], bufferSpan[idx], alpha)); + } + + break; + } + + int newX = InvertX(x, width, origin); + pixelRow[newX] = color; + } + } + } + + /// + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + this.ReadFileHeader(stream); + return new ImageInfo( + new Size(this.fileHeader.Width, this.fileHeader.Height), + this.metadata); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ReadL8Row(BufferedReadStream stream, int width, Buffer2D pixels, Span row, int y) + where TPixel : unmanaged, IPixel + { + int bytesRead = stream.Read(row); + if (bytesRead != row.Length) + { + TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row"); + } + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.FromL8Bytes(this.configuration, row, pixelSpan, width); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ReadL8Pixel(BufferedReadStream stream, int x, Span pixelSpan) + where TPixel : unmanaged, IPixel + { + byte pixelValue = (byte)stream.ReadByte(); + pixelSpan[x] = TPixel.FromL8(Unsafe.As(ref pixelValue)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ReadBgr24Pixel(BufferedReadStream stream, int x, Span pixelSpan, Span scratchBuffer) + where TPixel : unmanaged, IPixel + { + int bytesRead = stream.Read(scratchBuffer, 0, 3); + if (bytesRead != 3) + { + TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a bgr pixel"); + } + + pixelSpan[x] = TPixel.FromBgr24(Unsafe.As(ref MemoryMarshal.GetReference(scratchBuffer))); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ReadBgr24Row(BufferedReadStream stream, int width, Buffer2D pixels, Span row, int y) + where TPixel : unmanaged, IPixel + { + int bytesRead = stream.Read(row); + if (bytesRead != row.Length) + { + TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row"); + } + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.FromBgr24Bytes(this.configuration, row, pixelSpan, width); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ReadBgra32Pixel(BufferedReadStream stream, int x, Span pixelRow, Span scratchBuffer) + where TPixel : unmanaged, IPixel + { + int bytesRead = stream.Read(scratchBuffer, 0, 4); + if (bytesRead != 4) + { + TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a bgra pixel"); + } + + Guard.NotNull(this.tgaMetadata); + + byte alpha = this.tgaMetadata.AlphaChannelBits == 0 ? byte.MaxValue : scratchBuffer[3]; + pixelRow[x] = TPixel.FromBgra32(new Bgra32(scratchBuffer[2], scratchBuffer[1], scratchBuffer[0], alpha)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ReadBgra32Row(BufferedReadStream stream, int width, Buffer2D pixels, Span row, int y) + where TPixel : unmanaged, IPixel + { + int bytesRead = stream.Read(row); + if (bytesRead != row.Length) + { + TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row"); + } + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.FromBgra32Bytes(this.configuration, row, pixelSpan, width); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ReadPalettedBgra16Pixel(BufferedReadStream stream, Span palette, int colorMapPixelSizeInBytes, int x, Span pixelRow) + where TPixel : unmanaged, IPixel + { + int colorIndex = stream.ReadByte(); + if (colorIndex == -1) + { + TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read color index"); + } + + pixelRow[x] = this.ReadPalettedBgra16Pixel(palette, colorIndex, colorMapPixelSizeInBytes); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private TPixel ReadPalettedBgra16Pixel(Span palette, int index, int colorMapPixelSizeInBytes) + where TPixel : unmanaged, IPixel + { + Bgra5551 bgra = Unsafe.As(ref palette[index * colorMapPixelSizeInBytes]); + + if (!this.hasAlpha) + { + // Set alpha value to 1, to treat it as opaque. + bgra.PackedValue = (ushort)(bgra.PackedValue | 0x8000); + } + + return TPixel.FromBgra5551(bgra); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ReadPalettedBgr24Pixel(BufferedReadStream stream, Span palette, int colorMapPixelSizeInBytes, int x, Span pixelRow) + where TPixel : unmanaged, IPixel + { + int colorIndex = stream.ReadByte(); + if (colorIndex == -1) + { + TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read color index"); + } + + pixelRow[x] = TPixel.FromBgr24(Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes])); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ReadPalettedBgra32Pixel(BufferedReadStream stream, Span palette, int colorMapPixelSizeInBytes, int x, Span pixelRow) + where TPixel : unmanaged, IPixel + { + int colorIndex = stream.ReadByte(); + if (colorIndex == -1) + { + TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read color index"); + } + + pixelRow[x] = TPixel.FromBgra32(Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes])); + } + + /// + /// Produce uncompressed tga data from a run length encoded stream. + /// + /// The containing image data. + /// The width of the image. + /// The height of the image. + /// Buffer for uncompressed data. + /// The bytes used per pixel. + private void UncompressRle(BufferedReadStream stream, int width, int height, Span buffer, int bytesPerPixel) + { + int uncompressedPixels = 0; + Span pixel = stackalloc byte[bytesPerPixel]; + int totalPixels = width * height; + while (uncompressedPixels < totalPixels) + { + byte runLengthByte = (byte)stream.ReadByte(); + + // The high bit of a run length packet is set to 1. + int highBit = runLengthByte >> 7; + if (highBit == 1) + { + int runLength = runLengthByte & 127; + int bytesRead = stream.Read(pixel); + if (bytesRead != bytesPerPixel) + { + TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel from the stream"); + } + + int bufferIdx = uncompressedPixels * bytesPerPixel; + for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) + { + pixel.CopyTo(buffer[bufferIdx..]); + bufferIdx += bytesPerPixel; + } + } + else + { + // Non-run-length encoded packet. + int runLength = runLengthByte; + int bufferIdx = uncompressedPixels * bytesPerPixel; + for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) + { + int bytesRead = stream.Read(pixel); + if (bytesRead != bytesPerPixel) + { + TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel from the stream"); + } + + pixel.CopyTo(buffer[bufferIdx..]); + bufferIdx += bytesPerPixel; + } + } + } + } + + /// + /// Returns the y- value based on the given height. + /// + /// The y- value representing the current row. + /// The height of the image. + /// The image origin. + /// The representing the inverted value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int InvertY(int y, int height, TgaImageOrigin origin) + { + if (InvertY(origin)) + { + return height - y - 1; + } + + return y; + } + + /// + /// Indicates whether the y coordinates needs to be inverted, to keep a top left origin. + /// + /// The image origin. + /// True, if y coordinate needs to be inverted. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool InvertY(TgaImageOrigin origin) => origin switch + { + TgaImageOrigin.BottomLeft => true, + TgaImageOrigin.BottomRight => true, + _ => false + }; + + /// + /// Returns the x- value based on the given width. + /// + /// The x- value representing the current column. + /// The width of the image. + /// The image origin. + /// The representing the inverted value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int InvertX(int x, int width, TgaImageOrigin origin) + { + if (InvertX(origin)) + { + return width - x - 1; + } + + return x; + } + + /// + /// Indicates whether the x coordinates needs to be inverted, to keep a top left origin. + /// + /// The image origin. + /// True, if x coordinate needs to be inverted. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool InvertX(TgaImageOrigin origin) => + origin switch + { + TgaImageOrigin.TopRight => true, + TgaImageOrigin.BottomRight => true, + _ => false + }; + + /// + /// Reads the tga file header from the stream. + /// + /// The containing image data. + /// The image origin. + [MemberNotNull(nameof(metadata))] + [MemberNotNull(nameof(tgaMetadata))] + private TgaImageOrigin ReadFileHeader(BufferedReadStream stream) + { + Span buffer = stackalloc byte[TgaFileHeader.Size]; + + stream.Read(buffer, 0, TgaFileHeader.Size); + this.fileHeader = TgaFileHeader.Parse(buffer); + this.Dimensions = new Size(this.fileHeader.Width, this.fileHeader.Height); + + this.metadata = new ImageMetadata(); + this.tgaMetadata = this.metadata.GetTgaMetadata(); + this.tgaMetadata.BitsPerPixel = (TgaBitsPerPixel)this.fileHeader.PixelDepth; + + // TrueColor images with 32 bits per pixel are assumed to always have 8 bit alpha channel, + // because some encoders do not set correctly the alpha bits in the image descriptor. + int alphaBits = this.IsTrueColor32BitPerPixel(this.tgaMetadata.BitsPerPixel) ? 8 : this.fileHeader.ImageDescriptor & 0xf; + if (alphaBits is not 0 and not 1 and not 8) + { + TgaThrowHelper.ThrowInvalidImageContentException("Invalid alpha channel bits"); + } + + this.tgaMetadata.AlphaChannelBits = (byte)alphaBits; + this.hasAlpha = alphaBits > 0; + + // Bits 4 and 5 describe the image origin. + return (TgaImageOrigin)((this.fileHeader.ImageDescriptor & 0x30) >> 4); + } + + private bool IsTrueColor32BitPerPixel(TgaBitsPerPixel bitsPerPixel) => bitsPerPixel == TgaBitsPerPixel.Bit32 && + (this.fileHeader.ImageType == TgaImageType.TrueColor || + this.fileHeader.ImageType == TgaImageType.RleTrueColor); + } +} diff --git a/ImageSharp/Formats/Tga/TgaEncoder.cs b/ImageSharp/Formats/Tga/TgaEncoder.cs new file mode 100644 index 0000000..3695c89 --- /dev/null +++ b/ImageSharp/Formats/Tga/TgaEncoder.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Tga { + /// + /// Image encoder for writing an image to a stream as a Targa true-vision image. + /// + public sealed class TgaEncoder : AlphaAwareImageEncoder + { + /// + /// Gets the number of bits per pixel. + /// + public TgaBitsPerPixel? BitsPerPixel { get; init; } + + /// + /// Gets a value indicating whether no compression or run length compression should be used. + /// + public TgaCompression Compression { get; init; } = TgaCompression.RunLength; + + /// + protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) + { + TgaEncoderCore encoder = new(this, image.Configuration.MemoryAllocator); + encoder.Encode(image, stream, cancellationToken); + } + } +} diff --git a/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/ImageSharp/Formats/Tga/TgaEncoderCore.cs new file mode 100644 index 0000000..eea2fab --- /dev/null +++ b/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -0,0 +1,447 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.IO; +using System.Threading; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tga { + /// + /// Image encoder for writing an image to a stream as a truevision targa image. + /// + internal sealed class TgaEncoderCore + { + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The color depth, in number of bits per pixel. + /// + private TgaBitsPerPixel? bitsPerPixel; + + /// + /// Indicates if run length compression should be used. + /// + private readonly TgaCompression compression; + + private readonly TransparentColorMode transparentColorMode; + + /// + /// Initializes a new instance of the class. + /// + /// The encoder with options. + /// The memory manager. + public TgaEncoderCore(TgaEncoder encoder, MemoryAllocator memoryAllocator) + { + this.memoryAllocator = memoryAllocator; + this.bitsPerPixel = encoder.BitsPerPixel; + this.compression = encoder.Compression; + this.transparentColorMode = encoder.TransparentColorMode; + } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + ImageMetadata metadata = image.Metadata; + TgaMetadata tgaMetadata = metadata.GetTgaMetadata(); + this.bitsPerPixel ??= tgaMetadata.BitsPerPixel; + + TgaImageType imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleTrueColor : TgaImageType.TrueColor; + if (this.bitsPerPixel == TgaBitsPerPixel.Bit8) + { + imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleBlackAndWhite : TgaImageType.BlackAndWhite; + } + + byte imageDescriptor = 0; + if (this.compression is TgaCompression.RunLength) + { + // If compression is used, set bit 5 of the image descriptor to indicate a left top origin. + imageDescriptor |= 0x20; + } + + if (this.bitsPerPixel is TgaBitsPerPixel.Bit32) + { + // Indicate, that 8 bit are used for the alpha channel. + imageDescriptor |= 0x8; + } + + if (this.bitsPerPixel is TgaBitsPerPixel.Bit16) + { + // Indicate, that 1 bit is used for the alpha channel. + imageDescriptor |= 0x1; + } + + TgaFileHeader fileHeader = new( + idLength: 0, + colorMapType: 0, + imageType: imageType, + cMapStart: 0, + cMapLength: 0, + cMapDepth: 0, + xOffset: 0, + + // When run length encoding is used, the origin should be top left instead of the default bottom left. + yOffset: this.compression is TgaCompression.RunLength ? (short)image.Height : (short)0, + width: (short)image.Width, + height: (short)image.Height, + pixelDepth: (byte)this.bitsPerPixel.Value, + imageDescriptor: imageDescriptor); + + Span buffer = stackalloc byte[TgaFileHeader.Size]; + fileHeader.WriteTo(buffer); + + stream.Write(buffer, 0, TgaFileHeader.Size); + + ImageFrame? clonedFrame = null; + try + { + // TODO: Try to avoid cloning the frame if possible. + // We should be cloning individual scanlines instead. + if (EncodingUtilities.ShouldReplaceTransparentPixels(this.transparentColorMode)) + { + clonedFrame = image.Frames.RootFrame.Clone(); + EncodingUtilities.ReplaceTransparentPixels(clonedFrame); + } + + ImageFrame encodingFrame = clonedFrame ?? image.Frames.RootFrame; + + if (this.compression is TgaCompression.RunLength) + { + this.WriteRunLengthEncodedImage(stream, encodingFrame, cancellationToken); + } + else + { + this.WriteImage(image.Configuration, stream, encodingFrame, cancellationToken); + } + + stream.Flush(); + } + finally + { + clonedFrame?.Dispose(); + } + } + + /// + /// Writes the pixel data to the binary stream. + /// + /// The pixel format. + /// The global configuration. + /// The to write to. + /// /// The containing pixel data. + /// The token to request cancellation. + private void WriteImage(Configuration configuration, Stream stream, ImageFrame image, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Buffer2D pixels = image.PixelBuffer; + switch (this.bitsPerPixel) + { + case TgaBitsPerPixel.Bit8: + this.Write8Bit(configuration, stream, pixels, cancellationToken); + break; + + case TgaBitsPerPixel.Bit16: + this.Write16Bit(configuration, stream, pixels, cancellationToken); + break; + + case TgaBitsPerPixel.Bit24: + this.Write24Bit(configuration, stream, pixels, cancellationToken); + break; + + case TgaBitsPerPixel.Bit32: + this.Write32Bit(configuration, stream, pixels, cancellationToken); + break; + } + } + + /// + /// Writes a run length encoded tga image to the stream. + /// + /// The pixel type. + /// The stream to write the image to. + /// The image to encode. + /// The token to request cancellation. + private void WriteRunLengthEncodedImage(Stream stream, ImageFrame image, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Buffer2D pixels = image.PixelBuffer; + + using IMemoryOwner rgbaOwner = this.memoryAllocator.Allocate(image.Width); + Span rgbaRow = rgbaOwner.GetSpan(); + + for (int y = 0; y < image.Height; y++) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelRow = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.ToRgba32(image.Configuration, pixelRow, rgbaRow); + + for (int x = 0; x < image.Width;) + { + TPixel currentPixel = pixelRow[x]; + Rgba32 rgba = rgbaRow[x]; + byte equalPixelCount = FindEqualPixels(pixelRow, x); + + if (equalPixelCount > 0) + { + // Write the number of equal pixels, with the high bit set, indicating it's a compressed pixel run. + stream.WriteByte((byte)(equalPixelCount | 128)); + this.WritePixel(stream, rgba); + x += equalPixelCount + 1; + } + else + { + // Write Raw Packet (i.e., Non-Run-Length Encoded): + byte unEqualPixelCount = FindUnEqualPixels(pixelRow, x); + stream.WriteByte(unEqualPixelCount); + this.WritePixel(stream, rgba); + x++; + for (int i = 0; i < unEqualPixelCount; i++) + { + currentPixel = pixelRow[x]; + rgba = rgbaRow[x]; + this.WritePixel(stream, rgba); + x++; + } + } + } + } + } + + /// + /// Writes a the pixel to the stream. + /// + /// The stream to write to. + /// The color of the pixel to write. + private void WritePixel(Stream stream, Rgba32 color) + { + switch (this.bitsPerPixel) + { + case TgaBitsPerPixel.Bit8: + L8 l8 = L8.FromRgba32(color); + stream.WriteByte(l8.PackedValue); + break; + + case TgaBitsPerPixel.Bit16: + Bgra5551 bgra5551 = Bgra5551.FromRgba32(color); + Span buffer = stackalloc byte[2]; + BinaryPrimitives.WriteInt16LittleEndian(buffer, (short)bgra5551.PackedValue); + stream.WriteByte(buffer[0]); + stream.WriteByte(buffer[1]); + + break; + + case TgaBitsPerPixel.Bit24: + stream.WriteByte(color.B); + stream.WriteByte(color.G); + stream.WriteByte(color.R); + break; + + case TgaBitsPerPixel.Bit32: + stream.WriteByte(color.B); + stream.WriteByte(color.G); + stream.WriteByte(color.R); + stream.WriteByte(color.A); + break; + } + } + + /// + /// Finds consecutive pixels which have the same value up to 128 pixels maximum. + /// + /// The pixel type. + /// A pixel row of the image to encode. + /// X coordinate to start searching for the same pixels. + /// The number of equal pixels. + private static byte FindEqualPixels(Span pixelRow, int xStart) + where TPixel : unmanaged, IPixel + { + byte equalPixelCount = 0; + TPixel startPixel = pixelRow[xStart]; + for (int x = xStart + 1; x < pixelRow.Length; x++) + { + TPixel nextPixel = pixelRow[x]; + if (startPixel.Equals(nextPixel)) + { + equalPixelCount++; + } + else + { + return equalPixelCount; + } + + if (equalPixelCount >= 127) + { + return equalPixelCount; + } + } + + return equalPixelCount; + } + + /// + /// Finds consecutive pixels which are unequal up to 128 pixels maximum. + /// + /// The pixel type. + /// A pixel row of the image to encode. + /// X coordinate to start searching for the unequal pixels. + /// The number of equal pixels. + private static byte FindUnEqualPixels(Span pixelRow, int xStart) + where TPixel : unmanaged, IPixel + { + byte unEqualPixelCount = 0; + TPixel currentPixel = pixelRow[xStart]; + for (int x = xStart + 1; x < pixelRow.Length; x++) + { + TPixel nextPixel = pixelRow[x]; + if (currentPixel.Equals(nextPixel)) + { + return unEqualPixelCount; + } + + unEqualPixelCount++; + + if (unEqualPixelCount >= 127) + { + return unEqualPixelCount; + } + + currentPixel = nextPixel; + } + + return unEqualPixelCount; + } + + private IMemoryOwner AllocateRow(int width, int bytesPerPixel) + => this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, 0); + + /// + /// Writes the 8bit pixels uncompressed to the stream. + /// + /// The pixel format. + /// The global configuration. + /// The to write to. + /// The containing pixel data. + /// The token to request cancellation. + private void Write8Bit(Configuration configuration, Stream stream, Buffer2D pixels, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner row = this.AllocateRow(pixels.Width, 1); + Span rowSpan = row.GetSpan(); + + for (int y = pixels.Height - 1; y >= 0; y--) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.ToL8Bytes( + configuration, + pixelSpan, + rowSpan, + pixelSpan.Length); + stream.Write(rowSpan); + } + } + + /// + /// Writes the 16bit pixels uncompressed to the stream. + /// + /// The pixel format. + /// The global configuration. + /// The to write to. + /// The containing pixel data. + /// The token to request cancellation. + private void Write16Bit(Configuration configuration, Stream stream, Buffer2D pixels, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner row = this.AllocateRow(pixels.Width, 2); + Span rowSpan = row.GetSpan(); + + for (int y = pixels.Height - 1; y >= 0; y--) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.ToBgra5551Bytes( + configuration, + pixelSpan, + rowSpan, + pixelSpan.Length); + stream.Write(rowSpan); + } + } + + /// + /// Writes the 24bit pixels uncompressed to the stream. + /// + /// The pixel format. + /// The global configuration. + /// The to write to. + /// The containing pixel data. + /// The token to request cancellation. + private void Write24Bit(Configuration configuration, Stream stream, Buffer2D pixels, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner row = this.AllocateRow(pixels.Width, 3); + Span rowSpan = row.GetSpan(); + + for (int y = pixels.Height - 1; y >= 0; y--) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.ToBgr24Bytes( + configuration, + pixelSpan, + rowSpan, + pixelSpan.Length); + stream.Write(rowSpan); + } + } + + /// + /// Writes the 32bit pixels uncompressed to the stream. + /// + /// The pixel format. + /// The global configuration. + /// The to write to. + /// The containing pixel data. + /// The token to request cancellation. + private void Write32Bit(Configuration configuration, Stream stream, Buffer2D pixels, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner row = this.AllocateRow(pixels.Width, 4); + Span rowSpan = row.GetSpan(); + + for (int y = pixels.Height - 1; y >= 0; y--) + { + cancellationToken.ThrowIfCancellationRequested(); + + Span pixelSpan = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.ToBgra32Bytes( + configuration, + pixelSpan, + rowSpan, + pixelSpan.Length); + stream.Write(rowSpan); + } + } + } +} diff --git a/ImageSharp/Formats/Tga/TgaFileHeader.cs b/ImageSharp/Formats/Tga/TgaFileHeader.cs new file mode 100644 index 0000000..bb3cde3 --- /dev/null +++ b/ImageSharp/Formats/Tga/TgaFileHeader.cs @@ -0,0 +1,143 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Tga { + /// + /// This block of bytes tells the application detailed information about the targa image. + /// + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal readonly struct TgaFileHeader + { + /// + /// Defines the size of the data structure in the targa file. + /// + public const int Size = TgaConstants.FileHeaderLength; + + public TgaFileHeader( + byte idLength, + byte colorMapType, + TgaImageType imageType, + short cMapStart, + short cMapLength, + byte cMapDepth, + short xOffset, + short yOffset, + short width, + short height, + byte pixelDepth, + byte imageDescriptor) + { + this.IdLength = idLength; + this.ColorMapType = colorMapType; + this.ImageType = imageType; + this.CMapStart = cMapStart; + this.CMapLength = cMapLength; + this.CMapDepth = cMapDepth; + this.XOffset = xOffset; + this.YOffset = yOffset; + this.Width = width; + this.Height = height; + this.PixelDepth = pixelDepth; + this.ImageDescriptor = imageDescriptor; + } + + /// + /// Gets the id length. + /// This field identifies the number of bytes contained in Field 6, the Image ID Field. The maximum number + /// of characters is 255. A value of zero indicates that no Image ID field is included with the image. + /// + public byte IdLength { get; } + + /// + /// Gets the color map type. + /// This field indicates the type of color map (if any) included with the image. There are currently 2 defined + /// values for this field: + /// 0 - indicates that no color-map data is included with this image. + /// 1 - indicates that a color-map is included with this image. + /// + public byte ColorMapType { get; } + + /// + /// Gets the image type. + /// The TGA File Format can be used to store Pseudo-Color, True-Color and Direct-Color images of various + /// pixel depths. + /// + public TgaImageType ImageType { get; } + + /// + /// Gets the start of the color map. + /// This field and its sub-fields describe the color map (if any) used for the image. If the Color Map Type field + /// is set to zero, indicating that no color map exists, then these 5 bytes should be set to zero. + /// + public short CMapStart { get; } + + /// + /// Gets the total number of color map entries included. + /// + public short CMapLength { get; } + + /// + /// Gets the number of bits per entry. Typically 15, 16, 24 or 32-bit values are used. + /// + public byte CMapDepth { get; } + + /// + /// Gets the XOffset. + /// These bytes specify the absolute horizontal coordinate for the lower left + /// corner of the image as it is positioned on a display device having an + /// origin at the lower left of the screen. + /// + public short XOffset { get; } + + /// + /// Gets the YOffset. + /// These bytes specify the absolute vertical coordinate for the lower left + /// corner of the image as it is positioned on a display device having an + /// origin at the lower left of the screen. + /// + public short YOffset { get; } + + /// + /// Gets the width of the image in pixels. + /// + public short Width { get; } + + /// + /// Gets the height of the image in pixels. + /// + public short Height { get; } + + /// + /// Gets the number of bits per pixel. This number includes + /// the Attribute or Alpha channel bits. Common values are 8, 16, 24 and + /// 32 but other pixel depths could be used. + /// + public byte PixelDepth { get; } + + /// + /// Gets the ImageDescriptor. + /// ImageDescriptor contains two pieces of information. + /// Bits 0 through 3 contain the number of attribute bits per pixel. + /// Attribute bits are found only in pixels for the 16- and 32-bit flavors of the TGA format and are called alpha channel, + /// overlay, or interrupt bits. Bits 4 and 5 contain the image origin location (coordinate 0,0) of the image. + /// This position may be any of the four corners of the display screen. + /// When both of these bits are set to zero, the image origin is the lower-left corner of the screen. + /// Bits 6 and 7 of the ImageDescriptor field are unused and should be set to 0. + /// + public byte ImageDescriptor { get; } + + public static TgaFileHeader Parse(Span data) => MemoryMarshal.Cast(data)[0]; + + public void WriteTo(Span buffer) + { + ref TgaFileHeader dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); + + dest = this; + } + } +} diff --git a/ImageSharp/Formats/Tga/TgaFormat.cs b/ImageSharp/Formats/Tga/TgaFormat.cs new file mode 100644 index 0000000..b32297b --- /dev/null +++ b/ImageSharp/Formats/Tga/TgaFormat.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Tga { + /// + /// Registers the image encoders, decoders and mime type detectors for the tga format. + /// + public sealed class TgaFormat : IImageFormat + { + /// + /// Gets the shared instance. + /// + public static TgaFormat Instance { get; } = new(); + + /// + public string Name => "TGA"; + + /// + public string DefaultMimeType => "image/tga"; + + /// + public IEnumerable MimeTypes => TgaConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => TgaConstants.FileExtensions; + + /// + public TgaMetadata CreateDefaultFormatMetadata() => new(); + } +} diff --git a/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs b/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs new file mode 100644 index 0000000..359826e --- /dev/null +++ b/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Tga { + /// + /// Detects tga file headers. + /// + public sealed class TgaImageFormatDetector : IImageFormatDetector + { + /// + public int HeaderSize => 16; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.IsSupportedFileFormat(header) ? TgaFormat.Instance : null; + return format != null; + } + + private bool IsSupportedFileFormat(ReadOnlySpan header) + { + if (header.Length >= this.HeaderSize) + { + // There are no magic bytes in the first few bytes of a tga file, + // so we try to figure out if its a valid tga by checking for valid tga header bytes. + + // The color map type should be either 0 or 1, other values are not valid. + if (header[1] != 0 && header[1] != 1) + { + return false; + } + + // The third byte is the image type. + TgaImageType imageType = (TgaImageType)header[2]; + if (!imageType.IsValid()) + { + return false; + } + + // If the color map typ is zero, all bytes of the color map specification should also be zeros. + if (header[1] == 0) + { + if (header[3] != 0 || header[4] != 0 || header[5] != 0 || header[6] != 0 || header[7] != 0) + { + return false; + } + } + + // The height or the width of the image should not be zero. + if ((header[12] == 0 && header[13] == 0) || (header[14] == 0 && header[15] == 0)) + { + return false; + } + + return true; + } + + return false; + } + } +} diff --git a/ImageSharp/Formats/Tga/TgaImageOrigin.cs b/ImageSharp/Formats/Tga/TgaImageOrigin.cs new file mode 100644 index 0000000..d77b01f --- /dev/null +++ b/ImageSharp/Formats/Tga/TgaImageOrigin.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tga { + internal enum TgaImageOrigin + { + /// + /// Bottom left origin. + /// + BottomLeft = 0, + + /// + /// Bottom right origin. + /// + BottomRight = 1, + + /// + /// Top left origin. + /// + TopLeft = 2, + + /// + /// Top right origin. + /// + TopRight = 3, + } +} diff --git a/ImageSharp/Formats/Tga/TgaImageType.cs b/ImageSharp/Formats/Tga/TgaImageType.cs new file mode 100644 index 0000000..9f2a4b5 --- /dev/null +++ b/ImageSharp/Formats/Tga/TgaImageType.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors. + ImageSharp.Formats.Tga { + /// + /// Defines the tga image type. The TGA File Format can be used to store Pseudo-Color, + /// True-Color and Direct-Color images of various pixel depths. + /// + public enum TgaImageType : byte + { + /// + /// No image data included. + /// + NoImageData = 0, + + /// + /// Uncompressed, color mapped image. + /// + ColorMapped = 1, + + /// + /// Uncompressed true color image. + /// + TrueColor = 2, + + /// + /// Uncompressed Black and white (grayscale) image. + /// + BlackAndWhite = 3, + + /// + /// Run length encoded, color mapped image. + /// + RleColorMapped = 9, + + /// + /// Run length encoded, true color image. + /// + RleTrueColor = 10, + + /// + /// Run length encoded, black and white (grayscale) image. + /// + RleBlackAndWhite = 11, + } +} diff --git a/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs b/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs new file mode 100644 index 0000000..6e5d39a --- /dev/null +++ b/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tga { + /// + /// Extension methods for TgaImageType enum. + /// + public static class TgaImageTypeExtensions + { + /// + /// Checks if this tga image type is run length encoded. + /// + /// The tga image type. + /// True, if this image type is run length encoded, otherwise false. + public static bool IsRunLengthEncoded(this TgaImageType imageType) + { + if (imageType is TgaImageType.RleColorMapped || imageType is TgaImageType.RleBlackAndWhite || imageType is TgaImageType.RleTrueColor) + { + return true; + } + + return false; + } + + /// + /// Checks, if the image type has valid value. + /// + /// The image type. + /// true, if its a valid tga image type. + public static bool IsValid(this TgaImageType imageType) + { + switch (imageType) + { + case TgaImageType.NoImageData: + case TgaImageType.ColorMapped: + case TgaImageType.TrueColor: + case TgaImageType.BlackAndWhite: + case TgaImageType.RleColorMapped: + case TgaImageType.RleTrueColor: + case TgaImageType.RleBlackAndWhite: + return true; + + default: + return false; + } + } + } +} diff --git a/ImageSharp/Formats/Tga/TgaMetadata.cs b/ImageSharp/Formats/Tga/TgaMetadata.cs new file mode 100644 index 0000000..a0b2bbf --- /dev/null +++ b/ImageSharp/Formats/Tga/TgaMetadata.cs @@ -0,0 +1,109 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tga { + /// + /// Provides TGA specific metadata information for the image. + /// + public class TgaMetadata : IFormatMetadata + { + /// + /// Initializes a new instance of the class. + /// + public TgaMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private TgaMetadata(TgaMetadata other) + => this.BitsPerPixel = other.BitsPerPixel; + + /// + /// Gets or sets the number of bits per pixel. + /// + public TgaBitsPerPixel BitsPerPixel { get; set; } = TgaBitsPerPixel.Bit24; + + /// + /// Gets or sets the number of alpha bits per pixel. + /// + public byte AlphaChannelBits { get; set; } + + /// + public static TgaMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + // TODO: AlphaChannelBits is not used during encoding. + int bpp = metadata.PixelTypeInfo.BitsPerPixel; + return bpp switch + { + <= 8 => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Bit8 }, + <= 16 => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Bit16 }, + <= 24 => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Bit24 }, + _ => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Bit32 } + }; + } + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp = (int)this.BitsPerPixel; + PixelComponentInfo info; + PixelColorType color; + PixelAlphaRepresentation alpha; + switch (this.BitsPerPixel) + { + case TgaBitsPerPixel.Bit8: + info = PixelComponentInfo.Create(1, bpp, 8); + color = PixelColorType.Luminance; + alpha = PixelAlphaRepresentation.None; + break; + case TgaBitsPerPixel.Bit16: + info = PixelComponentInfo.Create(1, bpp, 5, 5, 5, 1); + color = PixelColorType.BGR | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + case TgaBitsPerPixel.Bit24: + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + color = PixelColorType.RGB; + alpha = PixelAlphaRepresentation.None; + break; + case TgaBitsPerPixel.Bit32 or _: + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo() + }; + + /// + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public TgaMetadata DeepClone() => new(this); + } +} diff --git a/ImageSharp/Formats/Tga/TgaThrowHelper.cs b/ImageSharp/Formats/Tga/TgaThrowHelper.cs new file mode 100644 index 0000000..a9fa6f4 --- /dev/null +++ b/ImageSharp/Formats/Tga/TgaThrowHelper.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Tga { + internal static class TgaThrowHelper + { + public static void ThrowInvalidImageContentException(string errorMessage) + => throw new InvalidImageContentException(errorMessage); + + public static void ThrowInvalidImageContentException(string errorMessage, Exception innerException) + => throw new InvalidImageContentException(errorMessage, innerException); + + public static void ThrowNotSupportedException(string errorMessage) + => throw new NotSupportedException(errorMessage); + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/BitWriterUtils.cs b/ImageSharp/Formats/Tiff/Compression/BitWriterUtils.cs new file mode 100644 index 0000000..ddb3795 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/BitWriterUtils.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression { + internal static class BitWriterUtils + { + public static void WriteBits(Span buffer, nint pos, nint count, byte value) + { + nint bitPos = Numerics.Modulo8(pos); + nint bufferPos = pos / 8; + nint startIdx = bufferPos + bitPos; + nint endIdx = startIdx + count; + + if (value == 1) + { + for (nint i = startIdx; i < endIdx; i++) + { + WriteBit(buffer, bufferPos, bitPos); + + bitPos++; + if (bitPos >= 8) + { + bitPos = 0; + bufferPos++; + } + } + } + else + { + for (nint i = startIdx; i < endIdx; i++) + { + WriteZeroBit(buffer, bufferPos, bitPos); + + bitPos++; + if (bitPos >= 8) + { + bitPos = 0; + bufferPos++; + } + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void WriteBit(Span buffer, nint bufferPos, nint bitPos) + { + ref byte b = ref Unsafe.Add(ref MemoryMarshal.GetReference(buffer), bufferPos); + b |= (byte)(1 << (int)(7 - bitPos)); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void WriteZeroBit(Span buffer, nint bufferPos, nint bitPos) + { + ref byte b = ref Unsafe.Add(ref MemoryMarshal.GetReference(buffer), bufferPos); + b = (byte)(b & ~(1 << (int)(7 - bitPos))); + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Compressors/DeflateCompressor.cs b/ImageSharp/Formats/Tiff/Compression/Compressors/DeflateCompressor.cs new file mode 100644 index 0000000..a129c58 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Compressors/DeflateCompressor.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors { + internal sealed class DeflateCompressor : TiffBaseCompressor + { + private readonly DeflateCompressionLevel compressionLevel; + + private readonly MemoryStream memoryStream = new(); + + public DeflateCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel, TiffPredictor predictor, DeflateCompressionLevel compressionLevel) + : base(output, allocator, width, bitsPerPixel, predictor) + => this.compressionLevel = compressionLevel; + + /// + public override TiffCompression Method => TiffCompression.Deflate; + + /// + public override void Initialize(int rowsPerStrip) + { + } + + /// + public override void CompressStrip(Span rows, int height) + { + this.memoryStream.Seek(0, SeekOrigin.Begin); + using (ZlibDeflateStream stream = new(this.Allocator, this.memoryStream, this.compressionLevel)) + { + if (this.Predictor == TiffPredictor.Horizontal) + { + HorizontalPredictor.ApplyHorizontalPrediction(rows, this.BytesPerRow, this.BitsPerPixel); + } + + stream.Write(rows); + stream.Flush(); + } + + int size = (int)this.memoryStream.Position; + byte[] buffer = this.memoryStream.GetBuffer(); + this.Output.Write(buffer, 0, size); + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Compressors/LzwCompressor.cs b/ImageSharp/Formats/Tiff/Compression/Compressors/LzwCompressor.cs new file mode 100644 index 0000000..af05043 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Compressors/LzwCompressor.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors { + internal sealed class LzwCompressor : TiffBaseCompressor + { + private TiffLzwEncoder lzwEncoder; + + public LzwCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel, TiffPredictor predictor) + : base(output, allocator, width, bitsPerPixel, predictor) + { + } + + /// + public override TiffCompression Method => TiffCompression.Lzw; + + /// + public override void Initialize(int rowsPerStrip) => this.lzwEncoder = new TiffLzwEncoder(this.Allocator); + + /// + public override void CompressStrip(Span rows, int height) + { + if (this.Predictor == TiffPredictor.Horizontal) + { + HorizontalPredictor.ApplyHorizontalPrediction(rows, this.BytesPerRow, this.BitsPerPixel); + } + + this.lzwEncoder.Encode(rows, this.Output); + } + + /// + protected override void Dispose(bool disposing) => this.lzwEncoder?.Dispose(); + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Compressors/NoCompressor.cs b/ImageSharp/Formats/Tiff/Compression/Compressors/NoCompressor.cs new file mode 100644 index 0000000..9cc1637 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Compressors/NoCompressor.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors { + internal sealed class NoCompressor : TiffBaseCompressor + { + public NoCompressor(Stream output, MemoryAllocator memoryAllocator, int width, int bitsPerPixel) + : base(output, memoryAllocator, width, bitsPerPixel) + { + } + + /// + public override TiffCompression Method => TiffCompression.None; + + /// + public override void Initialize(int rowsPerStrip) + { + } + + /// + public override void CompressStrip(Span rows, int height) => this.Output.Write(rows); + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsCompressor.cs b/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsCompressor.cs new file mode 100644 index 0000000..dc247a0 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsCompressor.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using System.IO; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors { + internal sealed class PackBitsCompressor : TiffBaseCompressor + { + private IMemoryOwner pixelData; + + public PackBitsCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel) + : base(output, allocator, width, bitsPerPixel) + { + } + + /// + public override TiffCompression Method => TiffCompression.PackBits; + + /// + public override void Initialize(int rowsPerStrip) + { + int additionalBytes = ((this.BytesPerRow + 126) / 127) + 1; + this.pixelData = this.Allocator.Allocate(this.BytesPerRow + additionalBytes); + } + + /// + public override void CompressStrip(Span rows, int height) + { + DebugGuard.IsTrue(rows.Length % height == 0, "Invalid height"); + DebugGuard.IsTrue(this.BytesPerRow == rows.Length / height, "The widths must match"); + + Span span = this.pixelData.GetSpan(); + for (int i = 0; i < height; i++) + { + Span row = rows.Slice(i * this.BytesPerRow, this.BytesPerRow); + int size = PackBitsWriter.PackBits(row, span); + this.Output.Write(span[..size]); + } + } + + /// + protected override void Dispose(bool disposing) => this.pixelData?.Dispose(); + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs b/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs new file mode 100644 index 0000000..e4619a7 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs @@ -0,0 +1,127 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors { + /// + /// Pack Bits compression for tiff images. See Tiff Spec v6, section 9. + /// + internal static class PackBitsWriter + { + public static int PackBits(ReadOnlySpan rowSpan, Span compressedRowSpan) + { + int maxRunLength = 127; + int posInRowSpan = 0; + int bytesWritten = 0; + int literalRunLength = 0; + + while (posInRowSpan < rowSpan.Length) + { + bool useReplicateRun = IsReplicateRun(rowSpan, posInRowSpan); + if (useReplicateRun) + { + if (literalRunLength > 0) + { + WriteLiteralRun(rowSpan, posInRowSpan, literalRunLength, compressedRowSpan, bytesWritten); + bytesWritten += literalRunLength + 1; + } + + // Write a run with the same bytes. + int runLength = FindRunLength(rowSpan, posInRowSpan, maxRunLength); + WriteRun(rowSpan, posInRowSpan, runLength, compressedRowSpan, bytesWritten); + + bytesWritten += 2; + literalRunLength = 0; + posInRowSpan += runLength; + continue; + } + + literalRunLength++; + posInRowSpan++; + + if (literalRunLength >= maxRunLength) + { + WriteLiteralRun(rowSpan, posInRowSpan, literalRunLength, compressedRowSpan, bytesWritten); + bytesWritten += literalRunLength + 1; + literalRunLength = 0; + } + } + + if (literalRunLength > 0) + { + WriteLiteralRun(rowSpan, posInRowSpan, literalRunLength, compressedRowSpan, bytesWritten); + bytesWritten += literalRunLength + 1; + } + + return bytesWritten; + } + + private static void WriteLiteralRun(ReadOnlySpan rowSpan, int end, int literalRunLength, Span compressedRowSpan, int compressedRowPos) + { + DebugGuard.MustBeLessThanOrEqualTo(literalRunLength, 127, nameof(literalRunLength)); + + int literalRunStart = end - literalRunLength; + sbyte runLength = (sbyte)(literalRunLength - 1); + compressedRowSpan[compressedRowPos] = (byte)runLength; + rowSpan.Slice(literalRunStart, literalRunLength).CopyTo(compressedRowSpan[(compressedRowPos + 1)..]); + } + + private static void WriteRun(ReadOnlySpan rowSpan, int start, int runLength, Span compressedRowSpan, int compressedRowPos) + { + DebugGuard.MustBeLessThanOrEqualTo(runLength, 127, nameof(runLength)); + + sbyte headerByte = (sbyte)(-runLength + 1); + compressedRowSpan[compressedRowPos] = (byte)headerByte; + compressedRowSpan[compressedRowPos + 1] = rowSpan[start]; + } + + private static bool IsReplicateRun(ReadOnlySpan rowSpan, int startPos) + { + // We consider run which has at least 3 same consecutive bytes a candidate for a run. + byte startByte = rowSpan[startPos]; + int count = 0; + for (int i = startPos + 1; i < rowSpan.Length; i++) + { + if (rowSpan[i] == startByte) + { + count++; + if (count >= 2) + { + return true; + } + } + else + { + break; + } + } + + return false; + } + + private static int FindRunLength(ReadOnlySpan rowSpan, int startPos, int maxRunLength) + { + byte startByte = rowSpan[startPos]; + int count = 1; + for (int i = startPos + 1; i < rowSpan.Length; i++) + { + if (rowSpan[i] == startByte) + { + count++; + } + else + { + break; + } + + if (count == maxRunLength) + { + break; + } + } + + return count; + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs b/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs new file mode 100644 index 0000000..c7f0763 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs @@ -0,0 +1,143 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors { + /// + /// Bitwriter for writing compressed CCITT T4 1D data. + /// + internal sealed class T4BitCompressor : TiffCcittCompressor + { + /// + /// The modified huffman is basically the same as CCITT T4, but without EOL markers and padding at the end of the rows. + /// + private readonly bool useModifiedHuffman; + + /// + /// Initializes a new instance of the class. + /// + /// The output stream to write the compressed data. + /// The memory allocator. + /// The width of the image. + /// The bits per pixel. + /// Indicates if the modified huffman RLE should be used. + public T4BitCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel, bool useModifiedHuffman = false) + : base(output, allocator, width, bitsPerPixel) => this.useModifiedHuffman = useModifiedHuffman; + + /// + public override TiffCompression Method => this.useModifiedHuffman ? TiffCompression.Ccitt1D : TiffCompression.CcittGroup3Fax; + + /// + /// Writes a image compressed with CCITT T4 to the output buffer. + /// + /// The pixels as 8-bit gray array. + /// The strip height. + /// The destination for the compressed data. + protected override void CompressStrip(Span pixelsAsGray, int height, Span compressedData) + { + if (!this.useModifiedHuffman) + { + // An EOL code is expected at the start of the data. + this.WriteCode(12, 1, compressedData); + } + + for (int y = 0; y < height; y++) + { + bool isWhiteRun = true; + bool isStartOrRow = true; + int x = 0; + + Span row = pixelsAsGray.Slice(y * this.Width, this.Width); + while (x < this.Width) + { + uint runLength = 0; + for (int i = x; i < this.Width; i++) + { + if (isWhiteRun && row[i] != 255) + { + break; + } + + if (isWhiteRun && row[i] == 255) + { + runLength++; + continue; + } + + if (!isWhiteRun && row[i] != 0) + { + break; + } + + if (!isWhiteRun && row[i] == 0) + { + runLength++; + } + } + + if (isStartOrRow && runLength == 0) + { + this.WriteCode(8, WhiteZeroRunTermCode, compressedData); + + isWhiteRun = false; + isStartOrRow = false; + continue; + } + + uint code; + uint codeLength; + if (runLength <= 63) + { + code = GetTermCode(runLength, out codeLength, isWhiteRun); + this.WriteCode(codeLength, code, compressedData); + x += (int)runLength; + } + else + { + runLength = GetBestFittingMakeupRunLength(runLength); + code = GetMakeupCode(runLength, out codeLength, isWhiteRun); + this.WriteCode(codeLength, code, compressedData); + x += (int)runLength; + + // If we are at the end of the line with a makeup code, we need to write a final term code with a length of zero. + if (x == this.Width) + { + if (isWhiteRun) + { + this.WriteCode(8, WhiteZeroRunTermCode, compressedData); + } + else + { + this.WriteCode(10, BlackZeroRunTermCode, compressedData); + } + } + + continue; + } + + isStartOrRow = false; + isWhiteRun = !isWhiteRun; + } + + this.WriteEndOfLine(compressedData); + } + } + + private void WriteEndOfLine(Span compressedData) + { + if (this.useModifiedHuffman) + { + this.PadByte(); + } + else + { + // Write EOL. + this.WriteCode(12, 1, compressedData); + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs b/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs new file mode 100644 index 0000000..457b2f6 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs @@ -0,0 +1,201 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using System.IO; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors { + /// + /// Bitwriter for writing compressed CCITT T6 2D data. + /// + internal sealed class T6BitCompressor : TiffCcittCompressor + { + /// + /// Vertical codes from -3 to +3. + /// + private static readonly (uint Length, uint Code)[] VerticalCodes = + [ + (7u, 3u), + (6u, 3u), + (3u, 3u), + (1u, 1u), + (3u, 2u), + (6u, 2u), + (7u, 2u) + ]; + + private IMemoryOwner referenceLineBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The output stream to write the compressed data. + /// The memory allocator. + /// The width of the image. + /// The bits per pixel. + public T6BitCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel) + : base(output, allocator, width, bitsPerPixel) + { + } + + /// + public override TiffCompression Method => TiffCompression.CcittGroup4Fax; + + /// + /// Writes a image compressed with CCITT T6 to the output buffer. + /// + /// The pixels as 8-bit gray array. + /// The strip height. + /// The destination for the compressed data. + protected override void CompressStrip(Span pixelsAsGray, int height, Span compressedData) + { + // Initial reference line is all white. + Span referenceLine = this.referenceLineBuffer.GetSpan(); + referenceLine.Fill(0xff); + + for (int y = 0; y < height; y++) + { + Span row = pixelsAsGray.Slice(y * this.Width, this.Width); + uint a0 = 0; + uint a1 = row[0] == 0 ? 0 : FindRunEnd(row, 0); + uint b1 = referenceLine[0] == 0 ? 0 : FindRunEnd(referenceLine, 0); + + while (true) + { + uint b2 = FindRunEnd(referenceLine, b1); + if (b2 < a1) + { + // Pass mode. + this.WriteCode(4, 1, compressedData); + a0 = b2; + } + else + { + int d = int.MaxValue; + if ((b1 >= a1) && (b1 - a1 <= 3)) + { + d = (int)(b1 - a1); + } + else if ((b1 < a1) && (a1 - b1 <= 3)) + { + d = -(int)(a1 - b1); + } + + if (d is >= -3 and <= 3) + { + // Vertical mode. + (uint length, uint code) = VerticalCodes[d + 3]; + this.WriteCode(length, code, compressedData); + a0 = a1; + } + else + { + // Horizontal mode. + this.WriteCode(3, 1, compressedData); + + uint a2 = FindRunEnd(row, a1); + if ((a0 + a1 == 0) || (row[(int)a0] != 0)) + { + this.WriteRun(a1 - a0, true, compressedData); + this.WriteRun(a2 - a1, false, compressedData); + } + else + { + this.WriteRun(a1 - a0, false, compressedData); + this.WriteRun(a2 - a1, true, compressedData); + } + + a0 = a2; + } + } + + if (a0 >= row.Length) + { + break; + } + + byte thisPixel = row[(int)a0]; + a1 = FindRunEnd(row, a0, thisPixel); + b1 = FindRunEnd(referenceLine, a0, (byte)~thisPixel); + b1 = FindRunEnd(referenceLine, b1, thisPixel); + } + + // This row is now the reference line. + row.CopyTo(referenceLine); + } + + this.WriteCode(12, 1, compressedData); + this.WriteCode(12, 1, compressedData); + } + + /// + protected override void Dispose(bool disposing) + { + this.referenceLineBuffer?.Dispose(); + base.Dispose(disposing); + } + + /// + /// Finds the end of a pixel run. + /// + /// The row of pixels to examine. + /// The index of the first pixel in to examine. + /// Color of pixels in the run. If not specified, the color at + /// will be used. + /// The index of the first pixel at or after + /// that does not match , or the length of , + /// whichever comes first. + private static uint FindRunEnd(Span row, uint startIndex, byte? color = null) + { + if (startIndex >= row.Length) + { + return (uint)row.Length; + } + + byte colorValue = color ?? row[(int)startIndex]; + for (int i = (int)startIndex; i < row.Length; i++) + { + if (row[i] != colorValue) + { + return (uint)i; + } + } + + return (uint)row.Length; + } + + /// + public override void Initialize(int rowsPerStrip) + { + base.Initialize(rowsPerStrip); + this.referenceLineBuffer = this.Allocator.Allocate(this.Width); + } + + /// + /// Writes a run to the output buffer. + /// + /// The length of the run. + /// If true the run is white pixels, + /// if false the run is black pixels. + /// The destination to write the run to. + private void WriteRun(uint runLength, bool isWhiteRun, Span compressedData) + { + uint code; + uint codeLength; + while (runLength > 63) + { + uint makeupLength = GetBestFittingMakeupRunLength(runLength); + code = GetMakeupCode(makeupLength, out codeLength, isWhiteRun); + this.WriteCode(codeLength, code, compressedData); + runLength -= makeupLength; + } + + code = GetTermCode(runLength, out codeLength, isWhiteRun); + this.WriteCode(codeLength, code, compressedData); + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs b/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs new file mode 100644 index 0000000..69b046a --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs @@ -0,0 +1,536 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors { + /// + /// Common functionality for CCITT T4 and T6 Compression + /// + internal abstract class TiffCcittCompressor : TiffBaseCompressor + { + protected const uint WhiteZeroRunTermCode = 0x35; + + protected const uint BlackZeroRunTermCode = 0x37; + + private static readonly uint[] MakeupRunLength = + [ + 64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 1792, 1856, 1920, 1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560 + ]; + + private static readonly Dictionary WhiteLen4TermCodes = new() + { + { 2, 0x7 }, { 3, 0x8 }, { 4, 0xB }, { 5, 0xC }, { 6, 0xE }, { 7, 0xF } + }; + + private static readonly Dictionary WhiteLen5TermCodes = new() + { + { 8, 0x13 }, { 9, 0x14 }, { 10, 0x7 }, { 11, 0x8 } + }; + + private static readonly Dictionary WhiteLen6TermCodes = new() + { + { 1, 0x7 }, { 12, 0x8 }, { 13, 0x3 }, { 14, 0x34 }, { 15, 0x35 }, { 16, 0x2A }, { 17, 0x2B } + }; + + private static readonly Dictionary WhiteLen7TermCodes = new() + { + { 18, 0x27 }, { 19, 0xC }, { 20, 0x8 }, { 21, 0x17 }, { 22, 0x3 }, { 23, 0x4 }, { 24, 0x28 }, { 25, 0x2B }, { 26, 0x13 }, + { 27, 0x24 }, { 28, 0x18 } + }; + + private static readonly Dictionary WhiteLen8TermCodes = new() + { + { 0, WhiteZeroRunTermCode }, { 29, 0x2 }, { 30, 0x3 }, { 31, 0x1A }, { 32, 0x1B }, { 33, 0x12 }, { 34, 0x13 }, { 35, 0x14 }, + { 36, 0x15 }, { 37, 0x16 }, { 38, 0x17 }, { 39, 0x28 }, { 40, 0x29 }, { 41, 0x2A }, { 42, 0x2B }, { 43, 0x2C }, { 44, 0x2D }, + { 45, 0x4 }, { 46, 0x5 }, { 47, 0xA }, { 48, 0xB }, { 49, 0x52 }, { 50, 0x53 }, { 51, 0x54 }, { 52, 0x55 }, { 53, 0x24 }, + { 54, 0x25 }, { 55, 0x58 }, { 56, 0x59 }, { 57, 0x5A }, { 58, 0x5B }, { 59, 0x4A }, { 60, 0x4B }, { 61, 0x32 }, { 62, 0x33 }, + { 63, 0x34 } + }; + + private static readonly Dictionary BlackLen2TermCodes = new() + { + { 2, 0x3 }, { 3, 0x2 } + }; + + private static readonly Dictionary BlackLen3TermCodes = new() + { + { 1, 0x2 }, { 4, 0x3 } + }; + + private static readonly Dictionary BlackLen4TermCodes = new() + { + { 5, 0x3 }, { 6, 0x2 } + }; + + private static readonly Dictionary BlackLen5TermCodes = new() + { + { 7, 0x3 } + }; + + private static readonly Dictionary BlackLen6TermCodes = new() + { + { 8, 0x5 }, { 9, 0x4 } + }; + + private static readonly Dictionary BlackLen7TermCodes = new() + { + { 10, 0x4 }, { 11, 0x5 }, { 12, 0x7 } + }; + + private static readonly Dictionary BlackLen8TermCodes = new() + { + { 13, 0x4 }, { 14, 0x7 } + }; + + private static readonly Dictionary BlackLen9TermCodes = new() + { + { 15, 0x18 } + }; + + private static readonly Dictionary BlackLen10TermCodes = new() + { + { 0, BlackZeroRunTermCode }, { 16, 0x17 }, { 17, 0x18 }, { 18, 0x8 } + }; + + private static readonly Dictionary BlackLen11TermCodes = new() + { + { 19, 0x67 }, { 20, 0x68 }, { 21, 0x6C }, { 22, 0x37 }, { 23, 0x28 }, { 24, 0x17 }, { 25, 0x18 } + }; + + private static readonly Dictionary BlackLen12TermCodes = new() + { + { 26, 0xCA }, { 27, 0xCB }, { 28, 0xCC }, { 29, 0xCD }, { 30, 0x68 }, { 31, 0x69 }, { 32, 0x6A }, { 33, 0x6B }, { 34, 0xD2 }, + { 35, 0xD3 }, { 36, 0xD4 }, { 37, 0xD5 }, { 38, 0xD6 }, { 39, 0xD7 }, { 40, 0x6C }, { 41, 0x6D }, { 42, 0xDA }, { 43, 0xDB }, + { 44, 0x54 }, { 45, 0x55 }, { 46, 0x56 }, { 47, 0x57 }, { 48, 0x64 }, { 49, 0x65 }, { 50, 0x52 }, { 51, 0x53 }, { 52, 0x24 }, + { 53, 0x37 }, { 54, 0x38 }, { 55, 0x27 }, { 56, 0x28 }, { 57, 0x58 }, { 58, 0x59 }, { 59, 0x2B }, { 60, 0x2C }, { 61, 0x5A }, + { 62, 0x66 }, { 63, 0x67 } + }; + + private static readonly Dictionary WhiteLen5MakeupCodes = new() + { + { 64, 0x1B }, { 128, 0x12 } + }; + + private static readonly Dictionary WhiteLen6MakeupCodes = new() + { + { 192, 0x17 }, { 1664, 0x18 } + }; + + private static readonly Dictionary WhiteLen8MakeupCodes = new() + { + { 320, 0x36 }, { 384, 0x37 }, { 448, 0x64 }, { 512, 0x65 }, { 576, 0x68 }, { 640, 0x67 } + }; + + private static readonly Dictionary WhiteLen7MakeupCodes = new() + { + { 256, 0x37 } + }; + + private static readonly Dictionary WhiteLen9MakeupCodes = new() + { + { 704, 0xCC }, { 768, 0xCD }, { 832, 0xD2 }, { 896, 0xD3 }, { 960, 0xD4 }, { 1024, 0xD5 }, { 1088, 0xD6 }, + { 1152, 0xD7 }, { 1216, 0xD8 }, { 1280, 0xD9 }, { 1344, 0xDA }, { 1408, 0xDB }, { 1472, 0x98 }, { 1536, 0x99 }, + { 1600, 0x9A }, { 1728, 0x9B } + }; + + private static readonly Dictionary WhiteLen11MakeupCodes = new() + { + { 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD } + }; + + private static readonly Dictionary WhiteLen12MakeupCodes = new() + { + { 1984, 0x12 }, { 2048, 0x13 }, { 2112, 0x14 }, { 2176, 0x15 }, { 2240, 0x16 }, { 2304, 0x17 }, { 2368, 0x1C }, + { 2432, 0x1D }, { 2496, 0x1E }, { 2560, 0x1F } + }; + + private static readonly Dictionary BlackLen10MakeupCodes = new() + { + { 64, 0xF } + }; + + private static readonly Dictionary BlackLen11MakeupCodes = new() + { + { 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD } + }; + + private static readonly Dictionary BlackLen12MakeupCodes = new() + { + { 128, 0xC8 }, { 192, 0xC9 }, { 256, 0x5B }, { 320, 0x33 }, { 384, 0x34 }, { 448, 0x35 }, + { 1984, 0x12 }, { 2048, 0x13 }, { 2112, 0x14 }, { 2176, 0x15 }, { 2240, 0x16 }, { 2304, 0x17 }, { 2368, 0x1C }, + { 2432, 0x1D }, { 2496, 0x1E }, { 2560, 0x1F } + }; + + private static readonly Dictionary BlackLen13MakeupCodes = new() + { + { 512, 0x6C }, { 576, 0x6D }, { 640, 0x4A }, { 704, 0x4B }, { 768, 0x4C }, { 832, 0x4D }, { 896, 0x72 }, + { 960, 0x73 }, { 1024, 0x74 }, { 1088, 0x75 }, { 1152, 0x76 }, { 1216, 0x77 }, { 1280, 0x52 }, { 1344, 0x53 }, + { 1408, 0x54 }, { 1472, 0x55 }, { 1536, 0x5A }, { 1600, 0x5B }, { 1664, 0x64 }, { 1728, 0x65 } + }; + + private int bytePosition; + + private byte bitPosition; + + private IMemoryOwner compressedDataBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The output. + /// The allocator. + /// The width. + /// The bits per pixel. + protected TiffCcittCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel) + : base(output, allocator, width, bitsPerPixel) + { + DebugGuard.IsTrue(bitsPerPixel == 1, nameof(bitsPerPixel), "CCITT compression requires one bit per pixel"); + this.bytePosition = 0; + this.bitPosition = 0; + } + + private static uint GetWhiteMakeupCode(uint runLength, out uint codeLength) + { + codeLength = 0; + + if (WhiteLen5MakeupCodes.TryGetValue(runLength, out uint value)) + { + codeLength = 5; + return value; + } + + if (WhiteLen6MakeupCodes.TryGetValue(runLength, out value)) + { + codeLength = 6; + return value; + } + + if (WhiteLen7MakeupCodes.TryGetValue(runLength, out value)) + { + codeLength = 7; + return value; + } + + if (WhiteLen8MakeupCodes.TryGetValue(runLength, out value)) + { + codeLength = 8; + return value; + } + + if (WhiteLen9MakeupCodes.TryGetValue(runLength, out value)) + { + codeLength = 9; + return value; + } + + if (WhiteLen11MakeupCodes.TryGetValue(runLength, out value)) + { + codeLength = 11; + return value; + } + + if (WhiteLen12MakeupCodes.TryGetValue(runLength, out value)) + { + codeLength = 12; + return value; + } + + return 0; + } + + private static uint GetBlackMakeupCode(uint runLength, out uint codeLength) + { + codeLength = 0; + + if (BlackLen10MakeupCodes.TryGetValue(runLength, out uint value)) + { + codeLength = 10; + return value; + } + + if (BlackLen11MakeupCodes.TryGetValue(runLength, out value)) + { + codeLength = 11; + return value; + } + + if (BlackLen12MakeupCodes.TryGetValue(runLength, out value)) + { + codeLength = 12; + return value; + } + + if (BlackLen13MakeupCodes.TryGetValue(runLength, out value)) + { + codeLength = 13; + return value; + } + + return 0; + } + + private static uint GetWhiteTermCode(uint runLength, out uint codeLength) + { + codeLength = 0; + + if (WhiteLen4TermCodes.TryGetValue(runLength, out uint value)) + { + codeLength = 4; + return value; + } + + if (WhiteLen5TermCodes.TryGetValue(runLength, out value)) + { + codeLength = 5; + return value; + } + + if (WhiteLen6TermCodes.TryGetValue(runLength, out value)) + { + codeLength = 6; + return value; + } + + if (WhiteLen7TermCodes.TryGetValue(runLength, out value)) + { + codeLength = 7; + return value; + } + + if (WhiteLen8TermCodes.TryGetValue(runLength, out value)) + { + codeLength = 8; + return value; + } + + return 0; + } + + private static uint GetBlackTermCode(uint runLength, out uint codeLength) + { + codeLength = 0; + + if (BlackLen2TermCodes.TryGetValue(runLength, out uint value)) + { + codeLength = 2; + return value; + } + + if (BlackLen3TermCodes.TryGetValue(runLength, out value)) + { + codeLength = 3; + return value; + } + + if (BlackLen4TermCodes.TryGetValue(runLength, out value)) + { + codeLength = 4; + return value; + } + + if (BlackLen5TermCodes.TryGetValue(runLength, out value)) + { + codeLength = 5; + return value; + } + + if (BlackLen6TermCodes.TryGetValue(runLength, out value)) + { + codeLength = 6; + return value; + } + + if (BlackLen7TermCodes.TryGetValue(runLength, out value)) + { + codeLength = 7; + return value; + } + + if (BlackLen8TermCodes.TryGetValue(runLength, out value)) + { + codeLength = 8; + return value; + } + + if (BlackLen9TermCodes.TryGetValue(runLength, out value)) + { + codeLength = 9; + return value; + } + + if (BlackLen10TermCodes.TryGetValue(runLength, out value)) + { + codeLength = 10; + return value; + } + + if (BlackLen11TermCodes.TryGetValue(runLength, out value)) + { + codeLength = 11; + return value; + } + + if (BlackLen12TermCodes.TryGetValue(runLength, out value)) + { + codeLength = 12; + return value; + } + + return 0; + } + + /// + /// Gets the best makeup run length for a given run length + /// + /// A run length needing a makeup code + /// The makeup length for . + protected static uint GetBestFittingMakeupRunLength(uint runLength) + { + DebugGuard.MustBeGreaterThanOrEqualTo(runLength, MakeupRunLength[0], nameof(runLength)); + + for (int i = 0; i < MakeupRunLength.Length - 1; i++) + { + if (MakeupRunLength[i] <= runLength && MakeupRunLength[i + 1] > runLength) + { + return MakeupRunLength[i]; + } + } + + return MakeupRunLength[^1]; + } + + /// + /// Gets the terminating code for a run length. + /// + /// The run length to get the terminating code for. + /// The length of the terminating code. + /// If true, the run is of white pixels. + /// If false the run is of black pixels + /// The terminating code for a run of length + protected static uint GetTermCode(uint runLength, out uint codeLength, bool isWhiteRun) + { + if (isWhiteRun) + { + return GetWhiteTermCode(runLength, out codeLength); + } + + return GetBlackTermCode(runLength, out codeLength); + } + + /// + /// Gets the makeup code for a run length. + /// + /// The run length to get the makeup code for. + /// The length of the makeup code. + /// If true, the run is of white pixels. + /// If false the run is of black pixels + /// The makeup code for a run of length + protected static uint GetMakeupCode(uint runLength, out uint codeLength, bool isWhiteRun) + { + if (isWhiteRun) + { + return GetWhiteMakeupCode(runLength, out codeLength); + } + + return GetBlackMakeupCode(runLength, out codeLength); + } + + /// + /// Pads output to the next byte. + /// + /// + /// If the output is not currently on a byte boundary, + /// zero-pad it to the next byte. + /// + protected void PadByte() + { + // Check if padding is necessary. + if (Numerics.Modulo8(this.bitPosition) != 0) + { + // Skip padding bits, move to next byte. + this.bytePosition++; + this.bitPosition = 0; + } + } + + /// + /// Writes a code to the output. + /// + /// The length of the code to write. + /// The code to be written. + /// The destination buffer to write the code to. + protected void WriteCode(uint codeLength, uint code, Span compressedData) + { + while (codeLength > 0) + { + int bitNumber = (int)codeLength; + bool bit = (code & (1 << (bitNumber - 1))) != 0; + if (bit) + { + BitWriterUtils.WriteBit(compressedData, this.bytePosition, this.bitPosition); + } + else + { + BitWriterUtils.WriteZeroBit(compressedData, this.bytePosition, this.bitPosition); + } + + this.bitPosition++; + if (this.bitPosition == 8) + { + this.bytePosition++; + this.bitPosition = 0; + } + + codeLength--; + } + } + + /// + /// Writes a image compressed with CCITT T6 to the stream. + /// + /// The pixels as 8-bit gray array. + /// The strip height. + public override void CompressStrip(Span rows, int height) + { + DebugGuard.IsTrue(rows.Length / height == this.Width, "Values must be equals"); + DebugGuard.IsTrue(rows.Length % height == 0, "Values must be equals"); + + this.compressedDataBuffer.Clear(); + Span compressedData = this.compressedDataBuffer.GetSpan(); + + this.bytePosition = 0; + this.bitPosition = 0; + + this.CompressStrip(rows, height, compressedData); + + // Write the compressed data to the stream. + int bytesToWrite = this.bitPosition != 0 ? this.bytePosition + 1 : this.bytePosition; + this.Output.Write(compressedData[..bytesToWrite]); + } + + /// + /// Compress a data strip + /// + /// The pixels as 8-bit gray array. + /// The strip height. + /// The destination for the compressed data. + protected abstract void CompressStrip(Span pixelsAsGray, int height, Span compressedData); + + /// + protected override void Dispose(bool disposing) => this.compressedDataBuffer?.Dispose(); + + /// + public override void Initialize(int rowsPerStrip) + { + // This is too much memory allocated, but just 1 bit per pixel will not do, if the compression rate is not good. + int maxNeededBytes = this.Width * rowsPerStrip; + this.compressedDataBuffer = this.Allocator.Allocate(maxNeededBytes); + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs b/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs new file mode 100644 index 0000000..6b4686d --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors { + internal class TiffJpegCompressor : TiffBaseCompressor + { + public TiffJpegCompressor(Stream output, MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None) + : base(output, memoryAllocator, width, bitsPerPixel, predictor) + { + } + + /// + public override TiffCompression Method => TiffCompression.Jpeg; + + /// + public override void Initialize(int rowsPerStrip) + { + } + + /// + public override void CompressStrip(Span rows, int height) + { + int pixelCount = rows.Length / 3; + int width = pixelCount / height; + + using MemoryStream memoryStream = new(); + Image image = Image.LoadPixelData(rows, width, height); + image.Save(memoryStream, new JpegEncoder() + { + ColorType = JpegColorType.Rgb + }); + memoryStream.Position = 0; + memoryStream.WriteTo(this.Output); + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Compressors/TiffLzwEncoder.cs b/ImageSharp/Formats/Tiff/Compression/Compressors/TiffLzwEncoder.cs new file mode 100644 index 0000000..304d1c7 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Compressors/TiffLzwEncoder.cs @@ -0,0 +1,269 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.IO; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors { + /* + This implementation is a port of a java tiff encoder by Harald Kuhr: https://github.com/haraldk/TwelveMonkeys + + Original licence: + + BSD 3-Clause License + + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + ** Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED.IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + /// + /// Encodes and compresses the image data using dynamic Lempel-Ziv compression. + /// + /// + /// + /// This code is based on the used for GIF encoding. There is potential + /// for a shared implementation. Differences between the GIF and TIFF implementations of the LZW + /// encoding are: (i) The GIF implementation includes an initial 'data size' byte, whilst this is + /// always 8 for TIFF. (ii) The GIF implementation writes a number of sub-blocks with an initial + /// byte indicating the length of the sub-block. In TIFF the data is written as a single block + /// with no length indicator (this can be determined from the 'StripByteCounts' entry). + /// + /// + internal sealed class TiffLzwEncoder : IDisposable + { + // Clear: Re-initialize tables. + private static readonly int ClearCode = 256; + + // End of Information. + private static readonly int EoiCode = 257; + + private static readonly int MinBits = 9; + private static readonly int MaxBits = 12; + + private static readonly int TableSize = 1 << MaxBits; + + // A child is made up of a parent (or prefix) code plus a suffix byte + // and siblings are strings with a common parent(or prefix) and different suffix bytes. + private readonly IMemoryOwner children; + + private readonly IMemoryOwner siblings; + + private readonly IMemoryOwner suffixes; + + // Initial setup + private int parent; + private int bitsPerCode; + private int nextValidCode; + private int maxCode; + + // Buffer for partial codes + private int bits; + private int bitPos; + private int bufferPosition; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + public TiffLzwEncoder(MemoryAllocator memoryAllocator) + { + this.children = memoryAllocator.Allocate(TableSize); + this.siblings = memoryAllocator.Allocate(TableSize); + this.suffixes = memoryAllocator.Allocate(TableSize); + } + + /// + /// Encodes and compresses the indexed pixels to the stream. + /// + /// The data to compress. + /// The stream to write to. + public void Encode(Span data, Stream stream) + { + this.Reset(); + + Span childrenSpan = this.children.GetSpan(); + Span suffixesSpan = this.suffixes.GetSpan(); + Span siblingsSpan = this.siblings.GetSpan(); + int length = data.Length; + + if (length == 0) + { + return; + } + + if (this.parent == -1) + { + // Init stream. + this.WriteCode(stream, ClearCode); + this.parent = this.ReadNextByte(data); + } + + while (this.bufferPosition < data.Length) + { + int value = this.ReadNextByte(data); + int child = childrenSpan[this.parent]; + + if (child > 0) + { + if (suffixesSpan[child] == value) + { + this.parent = child; + } + else + { + int sibling = child; + + while (true) + { + if (siblingsSpan[sibling] > 0) + { + sibling = siblingsSpan[sibling]; + + if (suffixesSpan[sibling] == value) + { + this.parent = sibling; + break; + } + } + else + { + siblingsSpan[sibling] = (short)this.nextValidCode; + suffixesSpan[this.nextValidCode] = (short)value; + this.WriteCode(stream, this.parent); + this.parent = value; + this.nextValidCode++; + + this.IncreaseCodeSizeOrResetIfNeeded(stream); + + break; + } + } + } + } + else + { + childrenSpan[this.parent] = (short)this.nextValidCode; + suffixesSpan[this.nextValidCode] = (short)value; + this.WriteCode(stream, this.parent); + this.parent = value; + this.nextValidCode++; + + this.IncreaseCodeSizeOrResetIfNeeded(stream); + } + } + + // Write EOI when we are done. + this.WriteCode(stream, this.parent); + this.WriteCode(stream, EoiCode); + + // Flush partial codes by writing 0 pad. + if (this.bitPos > 0) + { + this.WriteCode(stream, 0); + } + } + + /// + public void Dispose() + { + this.children.Dispose(); + this.siblings.Dispose(); + this.suffixes.Dispose(); + } + + private void Reset() + { + this.children.Clear(); + this.siblings.Clear(); + this.suffixes.Clear(); + + this.parent = -1; + this.bitsPerCode = MinBits; + this.nextValidCode = EoiCode + 1; + this.maxCode = (1 << this.bitsPerCode) - 1; + + this.bits = 0; + this.bitPos = 0; + this.bufferPosition = 0; + } + + private byte ReadNextByte(Span data) => data[this.bufferPosition++]; + + private void IncreaseCodeSizeOrResetIfNeeded(Stream stream) + { + if (this.nextValidCode > this.maxCode) + { + if (this.bitsPerCode == MaxBits) + { + // Reset stream by writing Clear code. + this.WriteCode(stream, ClearCode); + + // Reset tables. + this.ResetTables(); + } + else + { + // Increase code size. + this.bitsPerCode++; + this.maxCode = MaxValue(this.bitsPerCode); + } + } + } + + private void WriteCode(Stream stream, int code) + { + this.bits = (this.bits << this.bitsPerCode) | (code & this.maxCode); + this.bitPos += this.bitsPerCode; + + while (this.bitPos >= 8) + { + int b = (this.bits >> (this.bitPos - 8)) & 0xff; + stream.WriteByte((byte)b); + this.bitPos -= 8; + } + + this.bits &= BitmaskFor(this.bitPos); + } + + private void ResetTables() + { + this.children.GetSpan().Clear(); + this.siblings.GetSpan().Clear(); + this.bitsPerCode = MinBits; + this.maxCode = MaxValue(this.bitsPerCode); + this.nextValidCode = EoiCode + 1; + } + + private static int MaxValue(int codeLen) => (1 << codeLen) - 1; + + private static int BitmaskFor(int bits) => MaxValue(bits); + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittReferenceScanline.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittReferenceScanline.cs new file mode 100644 index 0000000..cbae2c7 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittReferenceScanline.cs @@ -0,0 +1,155 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Represents a reference scan line for CCITT 2D decoding. + /// + internal readonly ref struct CcittReferenceScanline + { + private readonly ReadOnlySpan scanLine; + private readonly int width; + private readonly byte whiteByte; + + /// + /// Initializes a new instance of the struct. + /// + /// Indicates, if white is zero, otherwise black is zero. + /// The scan line. + public CcittReferenceScanline(bool whiteIsZero, ReadOnlySpan scanLine) + { + this.scanLine = scanLine; + this.width = scanLine.Length; + this.whiteByte = whiteIsZero ? (byte)0 : (byte)255; + } + + /// + /// Initializes a new instance of the struct. + /// + /// Indicates, if white is zero, otherwise black is zero. + /// The width of the scanline. + public CcittReferenceScanline(bool whiteIsZero, int width) + { + this.scanLine = default; + this.width = width; + this.whiteByte = whiteIsZero ? (byte)0 : (byte)255; + } + + public bool IsEmpty => this.scanLine.IsEmpty; + + /// + /// Finds b1: The first changing element on the reference line to the right of a0 and of opposite color to a0. + /// + /// The reference or starting element om the coding line. + /// Fill byte. + /// Position of b1. + public int FindB1(int a0, byte a0Byte) + { + if (this.IsEmpty) + { + return this.FindB1ForImaginaryWhiteLine(a0, a0Byte); + } + + return this.FindB1ForNormalLine(a0, a0Byte); + } + + /// + /// Finds b2: The next changing element to the right of b1 on the reference line. + /// + /// The first changing element on the reference line to the right of a0 and opposite of color to a0. + /// Position of b1. + public int FindB2(int b1) + { + if (this.IsEmpty) + { + return this.FindB2ForImaginaryWhiteLine(); + } + + return this.FindB2ForNormalLine(b1); + } + + private int FindB1ForImaginaryWhiteLine(int a0, byte a0Byte) + { + if (a0 < 0) + { + if (a0Byte != this.whiteByte) + { + return 0; + } + } + + return this.width; + } + + private int FindB1ForNormalLine(int a0, byte a0Byte) + { + int offset = 0; + if (a0 < 0) + { + if (a0Byte != this.scanLine[0]) + { + return 0; + } + } + else + { + offset = a0; + } + + ReadOnlySpan searchSpace = this.scanLine[offset..]; + byte searchByte = (byte)~a0Byte; + int index = searchSpace.IndexOf(searchByte); + if (index < 0) + { + return this.scanLine.Length; + } + + if (index != 0) + { + return offset + index; + } + + searchByte = (byte)~searchSpace[0]; + index = searchSpace.IndexOf(searchByte); + if (index < 0) + { + return this.scanLine.Length; + } + + searchSpace = searchSpace[index..]; + offset += index; + index = searchSpace.IndexOf((byte)~searchByte); + if (index < 0) + { + return this.scanLine.Length; + } + + return index + offset; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int FindB2ForImaginaryWhiteLine() => this.width; + + private int FindB2ForNormalLine(int b1) + { + if (b1 >= this.scanLine.Length) + { + return this.scanLine.Length; + } + + byte searchByte = (byte)~this.scanLine[b1]; + int offset = b1 + 1; + ReadOnlySpan searchSpace = this.scanLine[offset..]; + int index = searchSpace.IndexOf(searchByte); + if (index == -1) + { + return this.scanLine.Length; + } + + return offset + index; + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittTwoDimensionalCode.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittTwoDimensionalCode.cs new file mode 100644 index 0000000..4cfec59 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittTwoDimensionalCode.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + [DebuggerDisplay("Type = {Type}")] + internal readonly struct CcittTwoDimensionalCode + { + private readonly ushort value; + + /// + /// Initializes a new instance of the struct. + /// + /// The code word. + /// The type of the code. + /// The bits required. + /// The extension bits. + public CcittTwoDimensionalCode(int code, CcittTwoDimensionalCodeType type, int bitsRequired, int extensionBits = 0) + { + this.Code = code; + this.value = (ushort)((byte)type | ((bitsRequired & 0b1111) << 8) | ((extensionBits & 0b111) << 11)); + } + + /// + /// Gets the code type. + /// + public CcittTwoDimensionalCodeType Type => (CcittTwoDimensionalCodeType)(this.value & 0b11111111); + + public int Code { get; } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittTwoDimensionalCodeType.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittTwoDimensionalCodeType.cs new file mode 100644 index 0000000..57f3ef7 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittTwoDimensionalCodeType.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Enum for the different two dimensional code words for the ccitt fax compression. + /// + internal enum CcittTwoDimensionalCodeType + { + /// + /// No valid code word was read. + /// + None = 0, + + /// + /// Pass mode: This mode is identified when the position of b2 lies to the left of a1. + /// + Pass = 1, + + /// + /// Indicates horizontal mode. + /// + Horizontal = 2, + + /// + /// Vertical 0 code word: relative distance between a1 and b1 is 0. + /// + Vertical0 = 3, + + /// + /// Vertical r1 code word: relative distance between a1 and b1 is 1, a1 is to the right of b1. + /// + VerticalR1 = 4, + + /// + /// Vertical r2 code word: relative distance between a1 and b1 is 2, a1 is to the right of b1. + /// + VerticalR2 = 5, + + /// + /// Vertical r3 code word: relative distance between a1 and b1 is 3, a1 is to the right of b1. + /// + VerticalR3 = 6, + + /// + /// Vertical l1 code word: relative distance between a1 and b1 is 1, a1 is to the left of b1. + /// + VerticalL1 = 7, + + /// + /// Vertical l2 code word: relative distance between a1 and b1 is 2, a1 is to the left of b1. + /// + VerticalL2 = 8, + + /// + /// Vertical l3 code word: relative distance between a1 and b1 is 3, a1 is to the left of b1. + /// + VerticalL3 = 9, + + /// + /// 1d extensions code word, extension code is used to indicate the change from the current mode to another mode, e.g., another coding scheme. + /// Not supported. + /// + Extensions1D = 10, + + /// + /// 2d extensions code word, extension code is used to indicate the change from the current mode to another mode, e.g., another coding scheme. + /// Not supported. + /// + Extensions2D = 11, + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs new file mode 100644 index 0000000..d2b6e0d --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs @@ -0,0 +1,103 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO.Compression; +using System.Threading; +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Class to handle cases where TIFF image data is compressed using Deflate compression. + /// + /// + /// Note that the 'OldDeflate' compression type is identical to the 'Deflate' compression type. + /// + internal sealed class DeflateTiffCompression : TiffBaseDecompressor + { + private readonly bool isBigEndian; + + private readonly TiffColorType colorType; + + private readonly bool isTiled; + + private readonly int tileWidth; + + private readonly int tileHeight; + + /// + /// Initializes a new instance of the class. + /// + /// The memoryAllocator to use for buffer allocations. + /// The image width. + /// The bits used per pixel. + /// The color type of the pixel data. + /// The tiff predictor used. + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + /// Flag indicates, if the image is a tiled image. + /// Number of pixels in a tile row. + /// Number of rows in a tile. + public DeflateTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffColorType colorType, TiffPredictor predictor, bool isBigEndian, bool isTiled, int tileWidth, int tileHeight) + : base(memoryAllocator, width, bitsPerPixel, predictor) + { + this.colorType = colorType; + this.isBigEndian = isBigEndian; + this.isTiled = isTiled; + this.tileWidth = tileWidth; + this.tileHeight = tileHeight; + } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) + { + long pos = stream.Position; + using (ZlibInflateStream deframeStream = new( + stream, + () => + { + int left = (int)(byteCount - (stream.Position - pos)); + return left > 0 ? left : 0; + })) + { + if (deframeStream.AllocateNewBytes(byteCount, true)) + { + DeflateStream? dataStream = deframeStream.CompressedStream; + + int totalRead = 0; + while (totalRead < buffer.Length) + { + int bytesRead = dataStream.Read(buffer, totalRead, buffer.Length - totalRead); + if (bytesRead <= 0) + { + break; + } + + totalRead += bytesRead; + } + } + } + + if (this.Predictor == TiffPredictor.Horizontal) + { + if (this.isTiled) + { + // When the image is tiled, undoing the horizontal predictor will be done for each tile row. + HorizontalPredictor.UndoTile(buffer, this.tileWidth, this.tileHeight, this.colorType, this.isBigEndian); + } + else + { + HorizontalPredictor.Undo(buffer, this.Width, this.colorType, this.isBigEndian); + } + } + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs new file mode 100644 index 0000000..3bfb2b6 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jpeg.Components; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Spectral converter for gray TIFF's which use the JPEG compression. + /// + /// The type of the pixel. + internal sealed class GrayJpegSpectralConverter : SpectralConverter + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + public GrayJpegSpectralConverter(Configuration configuration) + : base(configuration) + { + } + + /// + protected override JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverterBase.GetConverter(JpegColorSpace.Grayscale, frame.Precision); + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegCompressionUtils.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegCompressionUtils.cs new file mode 100644 index 0000000..eb5bfed --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegCompressionUtils.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + internal static class JpegCompressionUtils + { + public static void CopyImageBytesToBuffer(Configuration configuration, Span buffer, Buffer2D pixelBuffer) + { + int offset = 0; + for (int y = 0; y < pixelBuffer.Height; y++) + { + Span pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y); + PixelOperations.Instance.ToRgb24Bytes(configuration, pixelRowSpan, buffer[offset..], pixelRowSpan.Length); + offset += Unsafe.SizeOf() * pixelRowSpan.Length; + } + } + + public static void CopyImageBytesToBuffer(Configuration configuration, Span buffer, Buffer2D pixelBuffer) + { + int offset = 0; + for (int y = 0; y < pixelBuffer.Height; y++) + { + Span pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y); + PixelOperations.Instance.ToL8Bytes(configuration, pixelRowSpan, buffer[offset..], pixelRowSpan.Length); + offset += Unsafe.SizeOf() * pixelRowSpan.Length; + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs new file mode 100644 index 0000000..9b0524e --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs @@ -0,0 +1,123 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Class to handle cases where TIFF image data is compressed as a jpeg stream. + /// + internal sealed class JpegTiffCompression : TiffBaseDecompressor + { + private readonly JpegDecoderOptions options; + + private readonly byte[] jpegTables; + + private readonly TiffPhotometricInterpretation photometricInterpretation; + + private readonly ImageFrameMetadata metadata; + + /// + /// Initializes a new instance of the class. + /// + /// The specialized jpeg decoder options. + /// The memoryAllocator to use for buffer allocations. + /// The image width. + /// The bits per pixel. + /// The image frame metadata. + /// The JPEG tables containing the quantization and/or Huffman tables. + /// The photometric interpretation. + public JpegTiffCompression( + JpegDecoderOptions options, + MemoryAllocator memoryAllocator, + int width, + int bitsPerPixel, + ImageFrameMetadata metadata, + byte[] jpegTables, + TiffPhotometricInterpretation photometricInterpretation) + : base(memoryAllocator, width, bitsPerPixel) + { + this.options = options; + this.metadata = metadata; + this.jpegTables = jpegTables; + this.photometricInterpretation = photometricInterpretation; + } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) + { + if (this.jpegTables != null) + { + this.DecodeJpegData(stream, buffer, cancellationToken); + } + else + { + using Image image = Image.Load(this.options.GeneralOptions, stream); + JpegCompressionUtils.CopyImageBytesToBuffer(this.options.GeneralOptions.Configuration, buffer, image.Frames.RootFrame.PixelBuffer); + } + } + + private void DecodeJpegData(BufferedReadStream stream, Span buffer, CancellationToken cancellationToken) + { + using JpegDecoderCore jpegDecoder = new(this.options, this.metadata.IccProfile); + Configuration configuration = this.options.GeneralOptions.Configuration; + switch (this.photometricInterpretation) + { + case TiffPhotometricInterpretation.BlackIsZero: + case TiffPhotometricInterpretation.WhiteIsZero: + { + using SpectralConverter spectralConverterGray = new GrayJpegSpectralConverter(configuration); + HuffmanScanDecoder scanDecoderGray = new(stream, spectralConverterGray, cancellationToken); + + jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray); + jpegDecoder.ParseStream(stream, spectralConverterGray, cancellationToken); + + _ = this.options.GeneralOptions.TryGetIccProfileForColorConversion( + jpegDecoder.Metadata?.IccProfile, + out IccProfile? profile); + + using Buffer2D decompressedBuffer = spectralConverterGray.GetPixelBuffer(profile, cancellationToken); + JpegCompressionUtils.CopyImageBytesToBuffer(spectralConverterGray.Configuration, buffer, decompressedBuffer); + break; + } + + case TiffPhotometricInterpretation.YCbCr: + case TiffPhotometricInterpretation.Rgb: + case TiffPhotometricInterpretation.Separated: + { + using SpectralConverter spectralConverter = new TiffJpegSpectralConverter(configuration, this.photometricInterpretation); + HuffmanScanDecoder scanDecoder = new(stream, spectralConverter, cancellationToken); + + jpegDecoder.LoadTables(this.jpegTables, scanDecoder); + jpegDecoder.ParseStream(stream, spectralConverter, cancellationToken); + + _ = this.options.GeneralOptions.TryGetIccProfileForColorConversion( + jpegDecoder.Metadata?.IccProfile, + out IccProfile? profile); + + using Buffer2D decompressedBuffer = spectralConverter.GetPixelBuffer(profile, cancellationToken); + JpegCompressionUtils.CopyImageBytesToBuffer(spectralConverter.Configuration, buffer, decompressedBuffer); + break; + } + + default: + TiffThrowHelper.ThrowNotSupported($"Jpeg compressed tiff with photometric interpretation {this.photometricInterpretation} is not supported"); + break; + } + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwString.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwString.cs new file mode 100644 index 0000000..2c8d431 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwString.cs @@ -0,0 +1,103 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Represents a lzw string with a code word and a code length. + /// + public class LzwString + { + private static readonly LzwString Empty = new(0, 0, 0, null); + + private readonly LzwString previous; + private readonly byte value; + + /// + /// Initializes a new instance of the class. + /// + /// The code word. + public LzwString(byte code) + : this(code, code, 1, null) + { + } + + private LzwString(byte value, byte firstChar, int length, LzwString previous) + { + this.value = value; + this.FirstChar = firstChar; + this.Length = length; + this.previous = previous; + } + + /// + /// Gets the code length; + /// + public int Length { get; } + + /// + /// Gets the first character of the codeword. + /// + public byte FirstChar { get; } + + /// + /// Concatenates two code words. + /// + /// The code word to concatenate. + /// A concatenated lzw string. + public LzwString Concatenate(byte other) + { + if (this == Empty) + { + return new LzwString(other); + } + + return new LzwString(other, this.FirstChar, this.Length + 1, this); + } + + /// + /// Writes decoded pixel to buffer at a given position. + /// + /// The buffer to write to. + /// The position to write to. + /// The number of bytes written. + public int WriteTo(Span buffer, int offset) + { + if (this.Length == 0) + { + return 0; + } + + int available = buffer.Length - offset; + if (available <= 0) + { + return 0; + } + + int numToWrite = this.Length; + if (numToWrite > available) + { + numToWrite = available; + } + + LzwString e = this; + + // if string is too long, skip bytes at the end + int toSkip = this.Length - numToWrite; + for (int i = 0; i < toSkip; i++) + { + e = e.previous; + } + + for (int i = numToWrite - 1; i >= 0; i--) + { + buffer[offset + i] = e.value; + e = e.previous; + } + + return numToWrite; + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs new file mode 100644 index 0000000..c6f8ebf --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs @@ -0,0 +1,74 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using System; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Class to handle cases where TIFF image data is compressed using LZW compression. + /// + internal sealed class LzwTiffCompression : TiffBaseDecompressor + { + private readonly bool isBigEndian; + + private readonly TiffColorType colorType; + + private readonly bool isTiled; + + private readonly int tileWidth; + + private readonly int tileHeight; + + /// + /// Initializes a new instance of the class. + /// + /// The memoryAllocator to use for buffer allocations. + /// The image width. + /// The bits used per pixel. + /// The color type of the pixel data. + /// The tiff predictor used. + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + /// Flag indicates, if the image is a tiled image. + /// Number of pixels in a tile row. + /// Number of rows in a tile. + public LzwTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffColorType colorType, TiffPredictor predictor, bool isBigEndian, bool isTiled, int tileWidth, int tileHeight) + : base(memoryAllocator, width, bitsPerPixel, predictor) + { + this.colorType = colorType; + this.isBigEndian = isBigEndian; + this.isTiled = isTiled; + this.tileWidth = tileWidth; + this.tileHeight = tileHeight; + } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) + { + TiffLzwDecoder decoder = new(stream); + decoder.DecodePixels(buffer); + + if (this.Predictor == TiffPredictor.Horizontal) + { + if (this.isTiled) + { + // When the image is tiled, undoing the horizontal predictor will be done for each tile row. + HorizontalPredictor.UndoTile(buffer, this.tileWidth, this.tileHeight, this.colorType, this.isBigEndian); + } + else + { + HorizontalPredictor.Undo(buffer, this.Width, this.colorType, this.isBigEndian); + } + } + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanBitReader.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanBitReader.cs new file mode 100644 index 0000000..da717b3 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanBitReader.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Bit reader for data encoded with the modified huffman rle method. + /// See TIFF 6.0 specification, section 10. + /// + internal sealed class ModifiedHuffmanBitReader : T4BitReader + { + /// + /// Initializes a new instance of the class. + /// + /// The compressed input stream. + /// The logical order of bits within a byte. + /// The number of bytes to read from the stream. + public ModifiedHuffmanBitReader(BufferedReadStream input, TiffFillOrder fillOrder, int bytesToRead) + : base(input, fillOrder, bytesToRead) + { + } + + /// + public override bool HasMoreData => this.Position < (ulong)this.DataLength - 1 || (uint)(this.BitsRead - 1) < 6; + + /// + public override bool IsEndOfScanLine + { + get + { + if (this.IsWhiteRun && this.CurValueBitsRead == 12 && this.Value == 1) + { + return true; + } + + if (this.CurValueBitsRead == 11 && this.Value == 0) + { + // black run. + return true; + } + + return false; + } + } + + /// + public override void StartNewRow() + { + base.StartNewRow(); + + int remainder = Numerics.Modulo8(this.BitsRead); + if (remainder != 0) + { + // Skip padding bits, move to next byte. + this.AdvancePosition(); + } + } + + /// + /// No EOL is expected at the start of a run for the modified huffman encoding. + /// + protected override void ReadEolBeforeFirstData() + { + // Nothing to do here. + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs new file mode 100644 index 0000000..31bcfe0 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs @@ -0,0 +1,103 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using System; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Class to handle cases where TIFF image data is compressed using Modified Huffman Compression. + /// + internal sealed class ModifiedHuffmanTiffCompression : TiffBaseDecompressor + { + private readonly byte whiteValue; + + private readonly byte blackValue; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The logical order of bits within a byte. + /// The image width. + /// The number of bits per pixel. + /// The photometric interpretation. + public ModifiedHuffmanTiffCompression(MemoryAllocator allocator, TiffFillOrder fillOrder, int width, int bitsPerPixel, TiffPhotometricInterpretation photometricInterpretation) + : base(allocator, width, bitsPerPixel) + { + this.FillOrder = fillOrder; + bool isWhiteZero = photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero; + this.whiteValue = (byte)(isWhiteZero ? 0 : 1); + this.blackValue = (byte)(isWhiteZero ? 1 : 0); + } + + /// + /// Gets the logical order of bits within a byte. + /// + private TiffFillOrder FillOrder { get; } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) + { + ModifiedHuffmanBitReader bitReader = new(stream, this.FillOrder, byteCount); + + buffer.Clear(); + nint bitsWritten = 0; + nuint pixelsWritten = 0; + nint rowsWritten = 0; + while (bitReader.HasMoreData) + { + bitReader.ReadNextRun(); + + if (bitReader.RunLength > 0) + { + if (bitReader.IsWhiteRun) + { + BitWriterUtils.WriteBits(buffer, bitsWritten, (int)bitReader.RunLength, this.whiteValue); + } + else + { + BitWriterUtils.WriteBits(buffer, bitsWritten, (int)bitReader.RunLength, this.blackValue); + } + + bitsWritten += (int)bitReader.RunLength; + pixelsWritten += bitReader.RunLength; + } + + if (pixelsWritten == (ulong)this.Width) + { + rowsWritten++; + pixelsWritten = 0; + + // Write padding bits, if necessary. + nint pad = 8 - Numerics.Modulo8(bitsWritten); + if (pad != 8) + { + BitWriterUtils.WriteBits(buffer, bitsWritten, pad, 0); + bitsWritten += pad; + } + + if (rowsWritten >= stripHeight) + { + break; + } + + bitReader.StartNewRow(); + } + + if (pixelsWritten > (ulong)this.Width) + { + TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error, decoded more pixels then image width"); + } + } + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/NoneTiffCompression.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/NoneTiffCompression.cs new file mode 100644 index 0000000..6a01417 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/NoneTiffCompression.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using System; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Class to handle cases where TIFF image data is not compressed. + /// + internal sealed class NoneTiffCompression : TiffBaseDecompressor + { + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The width of the image. + /// The bits per pixel. + public NoneTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel) + : base(memoryAllocator, width, bitsPerPixel) + { + } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) + => _ = stream.Read(buffer, 0, Math.Min(buffer.Length, byteCount)); + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/OldJpegTiffCompression.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/OldJpegTiffCompression.cs new file mode 100644 index 0000000..2f00817 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/OldJpegTiffCompression.cs @@ -0,0 +1,106 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + internal sealed class OldJpegTiffCompression : TiffBaseDecompressor + { + private readonly JpegDecoderOptions options; + + private readonly uint startOfImageMarker; + + private readonly ImageFrameMetadata metadata; + + private readonly TiffPhotometricInterpretation photometricInterpretation; + + public OldJpegTiffCompression( + JpegDecoderOptions options, + MemoryAllocator memoryAllocator, + int width, + int bitsPerPixel, + ImageFrameMetadata metadata, + uint startOfImageMarker, + TiffPhotometricInterpretation photometricInterpretation) + : base(memoryAllocator, width, bitsPerPixel) + { + this.options = options; + this.startOfImageMarker = startOfImageMarker; + this.metadata = metadata; + this.photometricInterpretation = photometricInterpretation; + } + + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) + { + long stripOffset = stream.Position; + stream.Position = this.startOfImageMarker; + + this.DecodeJpegData(stream, buffer, cancellationToken); + + // Setting the stream position to the expected position. + // This is a workaround for some images having set the stripBytesCount not equal to the compressed jpeg data. + stream.Position = stripOffset + byteCount; + } + + private void DecodeJpegData(BufferedReadStream stream, Span buffer, CancellationToken cancellationToken) + { + using JpegDecoderCore jpegDecoder = new(this.options, this.metadata.IccProfile); + Configuration configuration = this.options.GeneralOptions.Configuration; + switch (this.photometricInterpretation) + { + case TiffPhotometricInterpretation.BlackIsZero: + case TiffPhotometricInterpretation.WhiteIsZero: + { + using SpectralConverter spectralConverterGray = new GrayJpegSpectralConverter(configuration); + + jpegDecoder.ParseStream(stream, spectralConverterGray, cancellationToken); + + _ = this.options.GeneralOptions.TryGetIccProfileForColorConversion( + jpegDecoder.Metadata?.IccProfile, + out IccProfile? profile); + + using Buffer2D decompressedBuffer = spectralConverterGray.GetPixelBuffer( + profile, + cancellationToken); + JpegCompressionUtils.CopyImageBytesToBuffer(spectralConverterGray.Configuration, buffer, decompressedBuffer); + break; + } + + case TiffPhotometricInterpretation.YCbCr: + case TiffPhotometricInterpretation.Rgb: + case TiffPhotometricInterpretation.Separated: + { + using SpectralConverter spectralConverter = new TiffOldJpegSpectralConverter(configuration, this.photometricInterpretation); + + jpegDecoder.ParseStream(stream, spectralConverter, cancellationToken); + + _ = this.options.GeneralOptions.TryGetIccProfileForColorConversion( + jpegDecoder.Metadata?.IccProfile, + out IccProfile? profile); + + using Buffer2D decompressedBuffer = spectralConverter.GetPixelBuffer(profile, cancellationToken); + JpegCompressionUtils.CopyImageBytesToBuffer(spectralConverter.Configuration, buffer, decompressedBuffer); + break; + } + + default: + TiffThrowHelper.ThrowNotSupported($"Jpeg compressed tiff with photometric interpretation {this.photometricInterpretation} is not supported"); + break; + } + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/PackBitsTiffCompression.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/PackBitsTiffCompression.cs new file mode 100644 index 0000000..8b3e867 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/PackBitsTiffCompression.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using System.Threading; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Class to handle cases where TIFF image data is compressed using PackBits compression. + /// + internal sealed class PackBitsTiffCompression : TiffBaseDecompressor + { + private IMemoryOwner compressedDataMemory; + + /// + /// Initializes a new instance of the class. + /// + /// The memoryAllocator to use for buffer allocations. + /// The width of the image. + /// The number of bits per pixel. + public PackBitsTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel) + : base(memoryAllocator, width, bitsPerPixel) + { + } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) + { + if (this.compressedDataMemory == null) + { + this.compressedDataMemory = this.Allocator.Allocate(byteCount); + } + else if (this.compressedDataMemory.Length() < byteCount) + { + this.compressedDataMemory.Dispose(); + this.compressedDataMemory = this.Allocator.Allocate(byteCount); + } + + Span compressedData = this.compressedDataMemory.GetSpan(); + + stream.Read(compressedData, 0, byteCount); + int compressedOffset = 0; + int decompressedOffset = 0; + + while (compressedOffset < byteCount) + { + byte headerByte = compressedData[compressedOffset]; + + if (headerByte <= 127) + { + int literalOffset = compressedOffset + 1; + int literalLength = compressedData[compressedOffset] + 1; + + if ((literalOffset + literalLength) > compressedData.Length) + { + TiffThrowHelper.ThrowImageFormatException("Tiff packbits compression error: not enough data."); + } + + compressedData.Slice(literalOffset, literalLength).CopyTo(buffer[decompressedOffset..]); + + compressedOffset += literalLength + 1; + decompressedOffset += literalLength; + } + else if (headerByte == 0x80) + { + compressedOffset += 1; + } + else + { + byte repeatData = compressedData[compressedOffset + 1]; + int repeatLength = 257 - headerByte; + + buffer.Slice(decompressedOffset, repeatLength).Fill(repeatData); + + compressedOffset += 2; + decompressedOffset += repeatLength; + } + } + } + + /// + protected override void Dispose(bool disposing) => this.compressedDataMemory?.Dispose(); + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs new file mode 100644 index 0000000..3569846 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jpeg.Components; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Spectral converter for YCbCr TIFF's which use the JPEG compression. + /// The jpeg data should be always treated as RGB color space. + /// + /// The type of the pixel. + internal sealed class RgbJpegSpectralConverter : SpectralConverter + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// This Spectral converter will always convert the pixel data to RGB color. + /// + /// The configuration. + public RgbJpegSpectralConverter(Configuration configuration) + : base(configuration) + { + } + + /// + protected override JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverterBase.GetConverter(JpegColorSpace.RGB, frame.Precision); + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs new file mode 100644 index 0000000..88a9ffe --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs @@ -0,0 +1,871 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Bitreader for reading compressed CCITT T4 1D data. + /// + internal class T4BitReader + { + /// + /// The logical order of bits within a byte. + /// + private readonly TiffFillOrder fillOrder; + + /// + /// Indicates whether its the first line of data which is read from the image. + /// + private bool isFirstScanLine; + + /// + /// Indicates whether we have found a termination code which signals the end of a run. + /// + private bool terminationCodeFound; + + /// + /// We keep track if its the start of the row, because each run is expected to start with a white run. + /// If the image row itself starts with black, a white run of zero is expected. + /// + private bool isStartOfRow; + + /// + /// Indicates, if fill bits have been added as necessary before EOL codes such that EOL always ends on a byte boundary. Defaults to false. + /// + private readonly bool eolPadding; + + /// + /// The minimum code length in bits. + /// + private const int MinCodeLength = 2; + + /// + /// The maximum code length in bits. + /// + private readonly int maxCodeLength = 13; + + private static readonly Dictionary WhiteLen4TermCodes = new() + { + { 0x7, 2 }, { 0x8, 3 }, { 0xB, 4 }, { 0xC, 5 }, { 0xE, 6 }, { 0xF, 7 } + }; + + private static readonly Dictionary WhiteLen5TermCodes = new() + { + { 0x13, 8 }, { 0x14, 9 }, { 0x7, 10 }, { 0x8, 11 } + }; + + private static readonly Dictionary WhiteLen6TermCodes = new() + { + { 0x7, 1 }, { 0x8, 12 }, { 0x3, 13 }, { 0x34, 14 }, { 0x35, 15 }, { 0x2A, 16 }, { 0x2B, 17 } + }; + + private static readonly Dictionary WhiteLen7TermCodes = new() + { + { 0x27, 18 }, { 0xC, 19 }, { 0x8, 20 }, { 0x17, 21 }, { 0x3, 22 }, { 0x4, 23 }, { 0x28, 24 }, { 0x2B, 25 }, { 0x13, 26 }, + { 0x24, 27 }, { 0x18, 28 } + }; + + private static readonly Dictionary WhiteLen8TermCodes = new() + { + { 0x35, 0 }, { 0x2, 29 }, { 0x3, 30 }, { 0x1A, 31 }, { 0x1B, 32 }, { 0x12, 33 }, { 0x13, 34 }, { 0x14, 35 }, { 0x15, 36 }, + { 0x16, 37 }, { 0x17, 38 }, { 0x28, 39 }, { 0x29, 40 }, { 0x2A, 41 }, { 0x2B, 42 }, { 0x2C, 43 }, { 0x2D, 44 }, { 0x4, 45 }, + { 0x5, 46 }, { 0xA, 47 }, { 0xB, 48 }, { 0x52, 49 }, { 0x53, 50 }, { 0x54, 51 }, { 0x55, 52 }, { 0x24, 53 }, { 0x25, 54 }, + { 0x58, 55 }, { 0x59, 56 }, { 0x5A, 57 }, { 0x5B, 58 }, { 0x4A, 59 }, { 0x4B, 60 }, { 0x32, 61 }, { 0x33, 62 }, { 0x34, 63 } + }; + + private static readonly Dictionary BlackLen2TermCodes = new() + { + { 0x3, 2 }, { 0x2, 3 } + }; + + private static readonly Dictionary BlackLen3TermCodes = new() + { + { 0x2, 1 }, { 0x3, 4 } + }; + + private static readonly Dictionary BlackLen4TermCodes = new() + { + { 0x3, 5 }, { 0x2, 6 } + }; + + private static readonly Dictionary BlackLen5TermCodes = new() + { + { 0x3, 7 } + }; + + private static readonly Dictionary BlackLen6TermCodes = new() + { + { 0x5, 8 }, { 0x4, 9 } + }; + + private static readonly Dictionary BlackLen7TermCodes = new() + { + { 0x4, 10 }, { 0x5, 11 }, { 0x7, 12 } + }; + + private static readonly Dictionary BlackLen8TermCodes = new() + { + { 0x4, 13 }, { 0x7, 14 } + }; + + private static readonly Dictionary BlackLen9TermCodes = new() + { + { 0x18, 15 } + }; + + private static readonly Dictionary BlackLen10TermCodes = new() + { + { 0x37, 0 }, { 0x17, 16 }, { 0x18, 17 }, { 0x8, 18 } + }; + + private static readonly Dictionary BlackLen11TermCodes = new() + { + { 0x67, 19 }, { 0x68, 20 }, { 0x6C, 21 }, { 0x37, 22 }, { 0x28, 23 }, { 0x17, 24 }, { 0x18, 25 } + }; + + private static readonly Dictionary BlackLen12TermCodes = new() + { + { 0xCA, 26 }, { 0xCB, 27 }, { 0xCC, 28 }, { 0xCD, 29 }, { 0x68, 30 }, { 0x69, 31 }, { 0x6A, 32 }, { 0x6B, 33 }, { 0xD2, 34 }, + { 0xD3, 35 }, { 0xD4, 36 }, { 0xD5, 37 }, { 0xD6, 38 }, { 0xD7, 39 }, { 0x6C, 40 }, { 0x6D, 41 }, { 0xDA, 42 }, { 0xDB, 43 }, + { 0x54, 44 }, { 0x55, 45 }, { 0x56, 46 }, { 0x57, 47 }, { 0x64, 48 }, { 0x65, 49 }, { 0x52, 50 }, { 0x53, 51 }, { 0x24, 52 }, + { 0x37, 53 }, { 0x38, 54 }, { 0x27, 55 }, { 0x28, 56 }, { 0x58, 57 }, { 0x59, 58 }, { 0x2B, 59 }, { 0x2C, 60 }, { 0x5A, 61 }, + { 0x66, 62 }, { 0x67, 63 } + }; + + private static readonly Dictionary WhiteLen5MakeupCodes = new() + { + { 0x1B, 64 }, { 0x12, 128 } + }; + + private static readonly Dictionary WhiteLen6MakeupCodes = new() + { + { 0x17, 192 }, { 0x18, 1664 } + }; + + private static readonly Dictionary WhiteLen8MakeupCodes = new() + { + { 0x36, 320 }, { 0x37, 384 }, { 0x64, 448 }, { 0x65, 512 }, { 0x68, 576 }, { 0x67, 640 } + }; + + private static readonly Dictionary WhiteLen7MakeupCodes = new() + { + { 0x37, 256 } + }; + + private static readonly Dictionary WhiteLen9MakeupCodes = new() + { + { 0xCC, 704 }, { 0xCD, 768 }, { 0xD2, 832 }, { 0xD3, 896 }, { 0xD4, 960 }, { 0xD5, 1024 }, { 0xD6, 1088 }, + { 0xD7, 1152 }, { 0xD8, 1216 }, { 0xD9, 1280 }, { 0xDA, 1344 }, { 0xDB, 1408 }, { 0x98, 1472 }, { 0x99, 1536 }, + { 0x9A, 1600 }, { 0x9B, 1728 } + }; + + private static readonly Dictionary WhiteLen11MakeupCodes = new() + { + { 0x8, 1792 }, { 0xC, 1856 }, { 0xD, 1920 } + }; + + private static readonly Dictionary WhiteLen12MakeupCodes = new() + { + { 0x12, 1984 }, { 0x13, 2048 }, { 0x14, 2112 }, { 0x15, 2176 }, { 0x16, 2240 }, { 0x17, 2304 }, { 0x1C, 2368 }, + { 0x1D, 2432 }, { 0x1E, 2496 }, { 0x1F, 2560 } + }; + + private static readonly Dictionary BlackLen10MakeupCodes = new() + { + { 0xF, 64 } + }; + + private static readonly Dictionary BlackLen11MakeupCodes = new() + { + { 0x8, 1792 }, { 0xC, 1856 }, { 0xD, 1920 } + }; + + private static readonly Dictionary BlackLen12MakeupCodes = new() + { + { 0xC8, 128 }, { 0xC9, 192 }, { 0x5B, 256 }, { 0x33, 320 }, { 0x34, 384 }, { 0x35, 448 }, + { 0x12, 1984 }, { 0x13, 2048 }, { 0x14, 2112 }, { 0x15, 2176 }, { 0x16, 2240 }, { 0x17, 2304 }, { 0x1C, 2368 }, + { 0x1D, 2432 }, { 0x1E, 2496 }, { 0x1F, 2560 } + }; + + private static readonly Dictionary BlackLen13MakeupCodes = new() + { + { 0x6C, 512 }, { 0x6D, 576 }, { 0x4A, 640 }, { 0x4B, 704 }, { 0x4C, 768 }, { 0x4D, 832 }, { 0x72, 896 }, + { 0x73, 960 }, { 0x74, 1024 }, { 0x75, 1088 }, { 0x76, 1152 }, { 0x77, 1216 }, { 0x52, 1280 }, { 0x53, 1344 }, + { 0x54, 1408 }, { 0x55, 1472 }, { 0x5A, 1536 }, { 0x5B, 1600 }, { 0x64, 1664 }, { 0x65, 1728 } + }; + + /// + /// The compressed input stream. + /// + private readonly BufferedReadStream stream; + + /// + /// Initializes a new instance of the class. + /// + /// The compressed input stream. + /// The logical order of bits within a byte. + /// The number of bytes to read from the stream. + /// Indicates, if fill bits have been added as necessary before EOL codes such that EOL always ends on a byte boundary. Defaults to false. + public T4BitReader(BufferedReadStream input, TiffFillOrder fillOrder, int bytesToRead, bool eolPadding = false) + { + this.stream = input; + this.fillOrder = fillOrder; + this.DataLength = bytesToRead; + this.BitsRead = 0; + this.Value = 0; + this.CurValueBitsRead = 0; + this.Position = 0; + this.IsWhiteRun = true; + this.isFirstScanLine = true; + this.isStartOfRow = true; + this.terminationCodeFound = false; + this.RunLength = 0; + this.eolPadding = eolPadding; + + this.ReadNextByte(); + + if (this.eolPadding) + { + this.maxCodeLength = 24; + } + } + + /// + /// Gets or sets the byte at the given position. + /// + private byte DataAtPosition { get; set; } + + /// + /// Gets the current value. + /// + protected uint Value { get; private set; } + + /// + /// Gets the number of bits read for the current run value. + /// + protected int CurValueBitsRead { get; private set; } + + /// + /// Gets the number of bits read. + /// + protected int BitsRead { get; private set; } + + /// + /// Gets the available data in bytes. + /// + protected int DataLength { get; } + + /// + /// Gets or sets the byte position in the buffer. + /// + protected ulong Position { get; set; } + + /// + /// Gets a value indicating whether there is more data to read left. + /// + public virtual bool HasMoreData => this.Position < (ulong)this.DataLength - 1; + + /// + /// Gets or sets a value indicating whether the current run is a white pixel run, otherwise its a black pixel run. + /// + public bool IsWhiteRun { get; protected set; } + + /// + /// Gets the number of pixels in the current run. + /// + public uint RunLength { get; private set; } + + /// + /// Gets a value indicating whether the end of a pixel row has been reached. + /// + public virtual bool IsEndOfScanLine + { + get + { + if (this.eolPadding) + { + return this.CurValueBitsRead >= 12 && this.Value == 1; + } + + return this.CurValueBitsRead == 12 && this.Value == 1; + } + } + + /// + /// Read the next run of pixels. + /// + public void ReadNextRun() + { + if (this.terminationCodeFound) + { + this.IsWhiteRun = !this.IsWhiteRun; + this.terminationCodeFound = false; + } + + // Initialize for next run. + this.Reset(); + + // We expect an EOL before the first data. + this.ReadEolBeforeFirstData(); + + // A code word must have at least 2 bits. + this.Value = this.ReadValue(MinCodeLength); + + do + { + if (this.CurValueBitsRead > this.maxCodeLength) + { + TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error: invalid code length read"); + } + + bool isMakeupCode = this.IsMakeupCode(); + if (isMakeupCode) + { + if (this.IsWhiteRun) + { + this.RunLength += this.WhiteMakeupCodeRunLength(); + } + else + { + this.RunLength += this.BlackMakeupCodeRunLength(); + } + + this.isStartOfRow = false; + this.Reset(resetRunLength: false); + continue; + } + + bool isTerminatingCode = this.IsTerminatingCode(); + if (isTerminatingCode) + { + // Each line starts with a white run. If the image starts with black, a white run with length zero is written. + if (this.isStartOfRow && this.IsWhiteRun && this.WhiteTerminatingCodeRunLength() == 0) + { + this.Reset(); + this.isStartOfRow = false; + this.terminationCodeFound = true; + this.RunLength = 0; + break; + } + + if (this.IsWhiteRun) + { + this.RunLength += this.WhiteTerminatingCodeRunLength(); + } + else + { + this.RunLength += this.BlackTerminatingCodeRunLength(); + } + + this.terminationCodeFound = true; + this.isStartOfRow = false; + break; + } + + uint currBit = this.ReadValue(1); + this.Value = (this.Value << 1) | currBit; + + if (this.IsEndOfScanLine) + { + this.StartNewRow(); + } + } + while (!this.IsEndOfScanLine); + + this.isFirstScanLine = false; + } + + /// + /// Initialization for a new row. + /// + public virtual void StartNewRow() + { + // Each new row starts with a white run. + this.IsWhiteRun = true; + this.isStartOfRow = true; + this.terminationCodeFound = false; + } + + /// + /// An EOL is expected before the first data. + /// + protected virtual void ReadEolBeforeFirstData() + { + if (this.isFirstScanLine) + { + this.Value = this.ReadValue(this.eolPadding ? 16 : 12); + + if (!this.IsEndOfScanLine) + { + TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error: expected start of data marker not found"); + } + + this.Reset(); + } + } + + /// + /// Resets the current value read and the number of bits read. + /// + /// if set to true resets also the run length. + protected void Reset(bool resetRunLength = true) + { + this.Value = 0; + this.CurValueBitsRead = 0; + + if (resetRunLength) + { + this.RunLength = 0; + } + } + + /// + /// Resets the bits read to 0. + /// + protected void ResetBitsRead() => this.BitsRead = 0; + + /// + /// Reads the next value. + /// + /// The number of bits to read. + /// The value read. + [MethodImpl(InliningOptions.ShortMethod)] + protected uint ReadValue(int nBits) + { + DebugGuard.MustBeGreaterThan(nBits, 0, nameof(nBits)); + + uint v = 0; + int shift = nBits; + while (shift-- > 0) + { + uint bit = this.GetBit(); + v |= bit << shift; + this.CurValueBitsRead++; + } + + return v; + } + + /// + /// Advances the position by one byte. + /// + /// True, if data could be advanced by one byte, otherwise false. + protected bool AdvancePosition() + { + if (this.LoadNewByte()) + { + return true; + } + + return false; + } + + private uint WhiteTerminatingCodeRunLength() + { + switch (this.CurValueBitsRead) + { + case 4: + { + return WhiteLen4TermCodes[this.Value]; + } + + case 5: + { + return WhiteLen5TermCodes[this.Value]; + } + + case 6: + { + return WhiteLen6TermCodes[this.Value]; + } + + case 7: + { + return WhiteLen7TermCodes[this.Value]; + } + + case 8: + { + return WhiteLen8TermCodes[this.Value]; + } + } + + return 0; + } + + private uint BlackTerminatingCodeRunLength() + { + switch (this.CurValueBitsRead) + { + case 2: + { + return BlackLen2TermCodes[this.Value]; + } + + case 3: + { + return BlackLen3TermCodes[this.Value]; + } + + case 4: + { + return BlackLen4TermCodes[this.Value]; + } + + case 5: + { + return BlackLen5TermCodes[this.Value]; + } + + case 6: + { + return BlackLen6TermCodes[this.Value]; + } + + case 7: + { + return BlackLen7TermCodes[this.Value]; + } + + case 8: + { + return BlackLen8TermCodes[this.Value]; + } + + case 9: + { + return BlackLen9TermCodes[this.Value]; + } + + case 10: + { + return BlackLen10TermCodes[this.Value]; + } + + case 11: + { + return BlackLen11TermCodes[this.Value]; + } + + case 12: + { + return BlackLen12TermCodes[this.Value]; + } + } + + return 0; + } + + private uint WhiteMakeupCodeRunLength() + { + switch (this.CurValueBitsRead) + { + case 5: + { + return WhiteLen5MakeupCodes[this.Value]; + } + + case 6: + { + return WhiteLen6MakeupCodes[this.Value]; + } + + case 7: + { + return WhiteLen7MakeupCodes[this.Value]; + } + + case 8: + { + return WhiteLen8MakeupCodes[this.Value]; + } + + case 9: + { + return WhiteLen9MakeupCodes[this.Value]; + } + + case 11: + { + return WhiteLen11MakeupCodes[this.Value]; + } + + case 12: + { + return WhiteLen12MakeupCodes[this.Value]; + } + } + + return 0; + } + + private uint BlackMakeupCodeRunLength() + { + switch (this.CurValueBitsRead) + { + case 10: + { + return BlackLen10MakeupCodes[this.Value]; + } + + case 11: + { + return BlackLen11MakeupCodes[this.Value]; + } + + case 12: + { + return BlackLen12MakeupCodes[this.Value]; + } + + case 13: + { + return BlackLen13MakeupCodes[this.Value]; + } + } + + return 0; + } + + private bool IsMakeupCode() + { + if (this.IsWhiteRun) + { + return this.IsWhiteMakeupCode(); + } + + return this.IsBlackMakeupCode(); + } + + private bool IsWhiteMakeupCode() + { + switch (this.CurValueBitsRead) + { + case 5: + { + return WhiteLen5MakeupCodes.ContainsKey(this.Value); + } + + case 6: + { + return WhiteLen6MakeupCodes.ContainsKey(this.Value); + } + + case 7: + { + return WhiteLen7MakeupCodes.ContainsKey(this.Value); + } + + case 8: + { + return WhiteLen8MakeupCodes.ContainsKey(this.Value); + } + + case 9: + { + return WhiteLen9MakeupCodes.ContainsKey(this.Value); + } + + case 11: + { + return WhiteLen11MakeupCodes.ContainsKey(this.Value); + } + + case 12: + { + return WhiteLen12MakeupCodes.ContainsKey(this.Value); + } + } + + return false; + } + + private bool IsBlackMakeupCode() + { + switch (this.CurValueBitsRead) + { + case 10: + { + return BlackLen10MakeupCodes.ContainsKey(this.Value); + } + + case 11: + { + return BlackLen11MakeupCodes.ContainsKey(this.Value); + } + + case 12: + { + return BlackLen12MakeupCodes.ContainsKey(this.Value); + } + + case 13: + { + return BlackLen13MakeupCodes.ContainsKey(this.Value); + } + } + + return false; + } + + private bool IsTerminatingCode() + { + if (this.IsWhiteRun) + { + return this.IsWhiteTerminatingCode(); + } + + return this.IsBlackTerminatingCode(); + } + + private bool IsWhiteTerminatingCode() + { + switch (this.CurValueBitsRead) + { + case 4: + { + return WhiteLen4TermCodes.ContainsKey(this.Value); + } + + case 5: + { + return WhiteLen5TermCodes.ContainsKey(this.Value); + } + + case 6: + { + return WhiteLen6TermCodes.ContainsKey(this.Value); + } + + case 7: + { + return WhiteLen7TermCodes.ContainsKey(this.Value); + } + + case 8: + { + return WhiteLen8TermCodes.ContainsKey(this.Value); + } + } + + return false; + } + + private bool IsBlackTerminatingCode() + { + switch (this.CurValueBitsRead) + { + case 2: + { + return BlackLen2TermCodes.ContainsKey(this.Value); + } + + case 3: + { + return BlackLen3TermCodes.ContainsKey(this.Value); + } + + case 4: + { + return BlackLen4TermCodes.ContainsKey(this.Value); + } + + case 5: + { + return BlackLen5TermCodes.ContainsKey(this.Value); + } + + case 6: + { + return BlackLen6TermCodes.ContainsKey(this.Value); + } + + case 7: + { + return BlackLen7TermCodes.ContainsKey(this.Value); + } + + case 8: + { + return BlackLen8TermCodes.ContainsKey(this.Value); + } + + case 9: + { + return BlackLen9TermCodes.ContainsKey(this.Value); + } + + case 10: + { + return BlackLen10TermCodes.ContainsKey(this.Value); + } + + case 11: + { + return BlackLen11TermCodes.ContainsKey(this.Value); + } + + case 12: + { + return BlackLen12TermCodes.ContainsKey(this.Value); + } + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private uint GetBit() + { + if (this.BitsRead >= 8) + { + this.AdvancePosition(); + } + + int shift = 8 - this.BitsRead - 1; + uint bit = (uint)((this.DataAtPosition & (1 << shift)) != 0 ? 1 : 0); + this.BitsRead++; + + return bit; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool LoadNewByte() + { + if (this.Position < (ulong)this.DataLength) + { + this.ReadNextByte(); + this.Position++; + return true; + } + + this.Position++; + this.DataAtPosition = 0; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ReadNextByte() + { + int nextByte = this.stream.ReadByte(); + if (nextByte == -1) + { + TiffThrowHelper.ThrowImageFormatException("Tiff fax compression error: not enough data."); + } + + this.ResetBitsRead(); + this.DataAtPosition = this.fillOrder == TiffFillOrder.LeastSignificantBitFirst + ? ReverseBits((byte)nextByte) + : (byte)nextByte; + } + + // http://graphics.stanford.edu/~seander/bithacks.html#ReverseByteWith64Bits + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte ReverseBits(byte b) => + (byte)((((b * 0x80200802UL) & 0x0884422110UL) * 0x0101010101UL) >> 32); + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs new file mode 100644 index 0000000..b0ad1b0 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs @@ -0,0 +1,127 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using System; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Class to handle cases where TIFF image data is compressed using CCITT T4 compression. + /// + internal sealed class T4TiffCompression : TiffBaseDecompressor + { + private readonly FaxCompressionOptions faxCompressionOptions; + + private readonly byte whiteValue; + + private readonly byte blackValue; + + private readonly int width; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The logical order of bits within a byte. + /// The image width. + /// The number of bits per pixel. + /// Fax compression options. + /// The photometric interpretation. + public T4TiffCompression( + MemoryAllocator allocator, + TiffFillOrder fillOrder, + int width, + int bitsPerPixel, + FaxCompressionOptions faxOptions, + TiffPhotometricInterpretation photometricInterpretation) + : base(allocator, width, bitsPerPixel) + { + this.faxCompressionOptions = faxOptions; + this.FillOrder = fillOrder; + this.width = width; + bool isWhiteZero = photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero; + this.whiteValue = (byte)(isWhiteZero ? 0 : 1); + this.blackValue = (byte)(isWhiteZero ? 1 : 0); + } + + /// + /// Gets the logical order of bits within a byte. + /// + private TiffFillOrder FillOrder { get; } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) + { + if (this.faxCompressionOptions.HasFlag(FaxCompressionOptions.TwoDimensionalCoding)) + { + TiffThrowHelper.ThrowNotSupported("TIFF CCITT 2D compression is not yet supported"); + } + + bool eolPadding = this.faxCompressionOptions.HasFlag(FaxCompressionOptions.EolPadding); + T4BitReader bitReader = new(stream, this.FillOrder, byteCount, eolPadding); + + buffer.Clear(); + nint bitsWritten = 0; + nuint pixelsWritten = 0; + nint rowsWritten = 0; + while (bitReader.HasMoreData) + { + bitReader.ReadNextRun(); + + if (bitReader.RunLength > 0) + { + this.WritePixelRun(buffer, bitReader, bitsWritten); + + bitsWritten += (int)bitReader.RunLength; + pixelsWritten += bitReader.RunLength; + } + + if (bitReader.IsEndOfScanLine) + { + // Write padding bytes, if necessary. + nint pad = 8 - Numerics.Modulo8(bitsWritten); + if (pad != 8) + { + BitWriterUtils.WriteBits(buffer, bitsWritten, pad, 0); + bitsWritten += pad; + } + + pixelsWritten = 0; + rowsWritten++; + + if (rowsWritten >= stripHeight) + { + break; + } + } + } + + // Edge case for when we are at the last byte, but there are still some unwritten pixels left. + if (pixelsWritten > 0 && pixelsWritten < (ulong)this.width) + { + bitReader.ReadNextRun(); + this.WritePixelRun(buffer, bitReader, bitsWritten); + } + } + + private void WritePixelRun(Span buffer, T4BitReader bitReader, nint bitsWritten) + { + if (bitReader.IsWhiteRun) + { + BitWriterUtils.WriteBits(buffer, bitsWritten, (int)bitReader.RunLength, this.whiteValue); + } + else + { + BitWriterUtils.WriteBits(buffer, bitsWritten, (int)bitReader.RunLength, this.blackValue); + } + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs new file mode 100644 index 0000000..4ca7b57 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs @@ -0,0 +1,195 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Bit reader for reading CCITT T6 compressed fax data. + /// See: Facsimile Coding Schemes and Coding Control Functions for Group 4 Facsimile Apparatus, itu-t recommendation t.6 + /// + internal sealed class T6BitReader : T4BitReader + { + private readonly int maxCodeLength = 12; + + private static readonly CcittTwoDimensionalCode None = new(0, CcittTwoDimensionalCodeType.None, 0); + + private static readonly CcittTwoDimensionalCode Len1Code1 = new(0b1, CcittTwoDimensionalCodeType.Vertical0, 1); + + private static readonly CcittTwoDimensionalCode Len3Code001 = new(0b001, CcittTwoDimensionalCodeType.Horizontal, 3); + private static readonly CcittTwoDimensionalCode Len3Code010 = new(0b010, CcittTwoDimensionalCodeType.VerticalL1, 3); + private static readonly CcittTwoDimensionalCode Len3Code011 = new(0b011, CcittTwoDimensionalCodeType.VerticalR1, 3); + + private static readonly CcittTwoDimensionalCode Len4Code0001 = new(0b0001, CcittTwoDimensionalCodeType.Pass, 4); + + private static readonly CcittTwoDimensionalCode Len6Code000011 = new(0b000011, CcittTwoDimensionalCodeType.VerticalR2, 6); + private static readonly CcittTwoDimensionalCode Len6Code000010 = new(0b000010, CcittTwoDimensionalCodeType.VerticalL2, 6); + + private static readonly CcittTwoDimensionalCode Len7Code0000011 = new(0b0000011, CcittTwoDimensionalCodeType.VerticalR3, 7); + private static readonly CcittTwoDimensionalCode Len7Code0000010 = new(0b0000010, CcittTwoDimensionalCodeType.VerticalL3, 7); + private static readonly CcittTwoDimensionalCode Len7Code0000001 = new(0b0000001, CcittTwoDimensionalCodeType.Extensions2D, 7); + private static readonly CcittTwoDimensionalCode Len7Code0000000 = new(0b0000000, CcittTwoDimensionalCodeType.Extensions1D, 7); + + /// + /// Initializes a new instance of the class. + /// + /// The compressed input stream. + /// The logical order of bits within a byte. + /// The number of bytes to read from the stream. + public T6BitReader(BufferedReadStream input, TiffFillOrder fillOrder, int bytesToRead) + : base(input, fillOrder, bytesToRead) + { + } + + /// + public override bool HasMoreData => this.Position < (ulong)this.DataLength - 1 || (uint)(this.BitsRead - 1) < (7 - 1); + + /// + /// Gets or sets the two dimensional code. + /// + public CcittTwoDimensionalCode Code { get; internal set; } + + public bool ReadNextCodeWord() + { + this.Code = None; + this.Reset(); + uint value = this.ReadValue(1); + + do + { + if (this.CurValueBitsRead > this.maxCodeLength) + { + TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error: invalid code length read"); + } + + switch (this.CurValueBitsRead) + { + case 1: + if (value == Len1Code1.Code) + { + this.Code = Len1Code1; + return false; + } + + break; + + case 3: + if (value == Len3Code001.Code) + { + this.Code = Len3Code001; + return false; + } + + if (value == Len3Code010.Code) + { + this.Code = Len3Code010; + return false; + } + + if (value == Len3Code011.Code) + { + this.Code = Len3Code011; + return false; + } + + break; + + case 4: + if (value == Len4Code0001.Code) + { + this.Code = Len4Code0001; + return false; + } + + break; + + case 6: + if (value == Len6Code000010.Code) + { + this.Code = Len6Code000010; + return false; + } + + if (value == Len6Code000011.Code) + { + this.Code = Len6Code000011; + return false; + } + + break; + + case 7: + if (value == Len7Code0000000.Code) + { + this.Code = Len7Code0000000; + + // We do not support Extensions1D codes, but some encoders (scanner from epson) write a premature EOL code, + // which at this point cannot be distinguished from the marker, because we read the data bit by bit. + // Read the next 5 bit, if its a EOL code return true, indicating its the end of the image. + if (this.ReadValue(5) == 1) + { + return true; + } + + throw new NotSupportedException("ccitt extensions 1D codes are not supported."); + } + + if (value == Len7Code0000001.Code) + { + this.Code = Len7Code0000001; + + // Same as above, we do not support Extensions2D codes, but it could be a EOL instead. + if (this.ReadValue(5) == 1) + { + return true; + } + + throw new NotSupportedException("ccitt extensions 2D codes are not supported."); + } + + if (value == Len7Code0000011.Code) + { + this.Code = Len7Code0000011; + return false; + } + + if (value == Len7Code0000010.Code) + { + this.Code = Len7Code0000010; + return false; + } + + break; + } + + uint currBit = this.ReadValue(1); + value = (value << 1) | currBit; + } + while (!this.IsEndOfScanLine); + + if (this.IsEndOfScanLine) + { + return true; + } + + return false; + } + + /// + /// No EOL is expected at the start of a run. + /// + protected override void ReadEolBeforeFirstData() + { + // Nothing to do here. + } + + /// + /// Swaps the white run to black run an vise versa. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void SwapColor() => this.IsWhiteRun = !this.IsWhiteRun; + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/T6TiffCompression.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/T6TiffCompression.cs new file mode 100644 index 0000000..b2c8051 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/T6TiffCompression.cs @@ -0,0 +1,275 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Class to handle cases where TIFF image data is compressed using CCITT T6 compression. + /// + internal sealed class T6TiffCompression : TiffBaseDecompressor + { + private readonly bool isWhiteZero; + + private readonly int width; + + private readonly byte white; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The logical order of bits within a byte. + /// The image width. + /// The number of bits per pixel. + /// The photometric interpretation. + public T6TiffCompression( + MemoryAllocator allocator, + TiffFillOrder fillOrder, + int width, + int bitsPerPixel, + TiffPhotometricInterpretation photometricInterpretation) + : base(allocator, width, bitsPerPixel) + { + this.FillOrder = fillOrder; + this.width = width; + this.isWhiteZero = photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero; + this.white = (byte)(this.isWhiteZero ? 0 : 255); + } + + /// + /// Gets the logical order of bits within a byte. + /// + private TiffFillOrder FillOrder { get; } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) + { + int height = stripHeight; + buffer.Clear(); + + using System.Buffers.IMemoryOwner scanLineBuffer = this.Allocator.Allocate(this.width * 2); + Span scanLine = scanLineBuffer.GetSpan()[..this.width]; + Span referenceScanLineSpan = scanLineBuffer.GetSpan().Slice(this.width, this.width); + + T6BitReader bitReader = new(stream, this.FillOrder, byteCount); + + CcittReferenceScanline referenceScanLine = new(this.isWhiteZero, this.width); + nint bitsWritten = 0; + for (int y = 0; y < height; y++) + { + scanLine.Clear(); + Decode2DScanline(bitReader, this.isWhiteZero, referenceScanLine, scanLine); + + bitsWritten = this.WriteScanLine(buffer, scanLine, bitsWritten); + + scanLine.CopyTo(referenceScanLineSpan); + referenceScanLine = new CcittReferenceScanline(this.isWhiteZero, referenceScanLineSpan); + } + } + + private nint WriteScanLine(Span buffer, Span scanLine, nint bitsWritten) + { + nint bitPos = Numerics.Modulo8(bitsWritten); + nint bufferPos = bitsWritten / 8; + ref byte scanLineRef = ref MemoryMarshal.GetReference(scanLine); + for (nuint i = 0; i < (uint)scanLine.Length; i++) + { + if (Unsafe.Add(ref scanLineRef, i) != this.white) + { + BitWriterUtils.WriteBit(buffer, bufferPos, bitPos); + } + + bitPos++; + bitsWritten++; + + if (bitPos >= 8) + { + bitPos = 0; + bufferPos++; + } + } + + // Write padding bytes, if necessary. + nint remainder = Numerics.Modulo8(bitsWritten); + if (remainder != 0) + { + nint padding = 8 - remainder; + BitWriterUtils.WriteBits(buffer, bitsWritten, padding, 0); + bitsWritten += padding; + } + + return bitsWritten; + } + + private static void Decode2DScanline(T6BitReader bitReader, bool whiteIsZero, CcittReferenceScanline referenceScanline, Span scanline) + { + int width = scanline.Length; + bitReader.StartNewRow(); + + // 2D Encoding variables. + int a0 = -1; + byte fillByte = whiteIsZero ? (byte)0 : (byte)255; + + // Process every code word in this scanline. + int unpacked = 0; + while (true) + { + // Read next code word and advance pass it. + bool isEol = bitReader.ReadNextCodeWord(); + + // Special case handling for EOL. + if (isEol) + { + // If a TIFF reader encounters EOFB before the expected number of lines has been extracted, + // it is appropriate to assume that the missing rows consist entirely of white pixels. + if (whiteIsZero) + { + scanline.Clear(); + } + else + { + scanline.Fill(255); + } + + break; + } + + // Update 2D Encoding variables. + int b1 = referenceScanline.FindB1(a0, fillByte); + + // Switch on the code word. + int a1; + switch (bitReader.Code.Type) + { + case CcittTwoDimensionalCodeType.None: + TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error, could not read a valid code word."); + break; + + case CcittTwoDimensionalCodeType.Pass: + int b2 = referenceScanline.FindB2(b1); + scanline[unpacked..b2].Fill(fillByte); + unpacked = b2; + a0 = b2; + break; + case CcittTwoDimensionalCodeType.Horizontal: + // Decode M(a0a1) + bitReader.ReadNextRun(); + int runLength = (int)bitReader.RunLength; + if (runLength > (uint)(scanline.Length - unpacked)) + { + TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error"); + } + + scanline.Slice(unpacked, runLength).Fill(fillByte); + unpacked += runLength; + fillByte = (byte)~fillByte; + + // Decode M(a1a2) + bitReader.ReadNextRun(); + runLength = (int)bitReader.RunLength; + if (runLength > (uint)(scanline.Length - unpacked)) + { + TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error"); + } + + scanline.Slice(unpacked, runLength).Fill(fillByte); + unpacked += runLength; + fillByte = (byte)~fillByte; + + // Prepare next a0 + a0 = unpacked; + break; + + case CcittTwoDimensionalCodeType.Vertical0: + a1 = b1; + scanline[unpacked..a1].Fill(fillByte); + unpacked = a1; + a0 = a1; + fillByte = (byte)~fillByte; + bitReader.SwapColor(); + break; + + case CcittTwoDimensionalCodeType.VerticalR1: + a1 = b1 + 1; + scanline[unpacked..a1].Fill(fillByte); + unpacked = a1; + a0 = a1; + fillByte = (byte)~fillByte; + bitReader.SwapColor(); + break; + + case CcittTwoDimensionalCodeType.VerticalR2: + a1 = b1 + 2; + scanline[unpacked..a1].Fill(fillByte); + unpacked = a1; + a0 = a1; + fillByte = (byte)~fillByte; + bitReader.SwapColor(); + break; + + case CcittTwoDimensionalCodeType.VerticalR3: + a1 = b1 + 3; + scanline[unpacked..a1].Fill(fillByte); + unpacked = a1; + a0 = a1; + fillByte = (byte)~fillByte; + bitReader.SwapColor(); + break; + + case CcittTwoDimensionalCodeType.VerticalL1: + a1 = b1 - 1; + scanline[unpacked..a1].Fill(fillByte); + unpacked = a1; + a0 = a1; + fillByte = (byte)~fillByte; + bitReader.SwapColor(); + break; + + case CcittTwoDimensionalCodeType.VerticalL2: + a1 = b1 - 2; + scanline[unpacked..a1].Fill(fillByte); + unpacked = a1; + a0 = a1; + fillByte = (byte)~fillByte; + bitReader.SwapColor(); + break; + + case CcittTwoDimensionalCodeType.VerticalL3: + a1 = b1 - 3; + scanline[unpacked..a1].Fill(fillByte); + unpacked = a1; + a0 = a1; + fillByte = (byte)~fillByte; + bitReader.SwapColor(); + break; + + default: + throw new NotSupportedException("ccitt extensions are not supported."); + } + + // This line is fully unpacked. Should exit and process next line. + if (unpacked == width) + { + break; + } + + if (unpacked > width) + { + TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error, unpacked data > width"); + } + } + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs new file mode 100644 index 0000000..52144b8 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jpeg.Components; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Spectral converter for YCbCr TIFF's which use the JPEG compression. + /// The jpeg data should be always treated as RGB color space. + /// + /// The type of the pixel. + internal sealed class TiffJpegSpectralConverter : SpectralConverter + where TPixel : unmanaged, IPixel + { + private readonly TiffPhotometricInterpretation photometricInterpretation; + + /// + /// Initializes a new instance of the class. + /// This Spectral converter will always convert the pixel data to RGB color. + /// + /// The configuration. + /// Tiff photometric interpretation. + public TiffJpegSpectralConverter(Configuration configuration, TiffPhotometricInterpretation photometricInterpretation) + : base(configuration) + => this.photometricInterpretation = photometricInterpretation; + + /// + protected override JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData) + { + JpegColorSpace colorSpace = GetJpegColorSpace(this.photometricInterpretation, jpegData); + return JpegColorConverterBase.GetConverter(colorSpace, frame.Precision); + } + + /// + /// Photometric interpretation Rgb and YCbCr will be mapped to RGB colorspace, which means the jpeg decompression will leave the data as is (no color conversion). + /// The color conversion will be done after the decompression. For Separated/CMYK/YCCK, the jpeg color converter will handle the color conversion, + /// since the jpeg color converter needs to return RGB data and cannot return 4 component data. + /// For grayscale images must be used. + /// + /// + /// The to convert to a . + /// + /// + /// The containing the color space information. + /// + /// + /// Thrown when the is not supported for JPEG encoding. + /// + private static JpegColorSpace GetJpegColorSpace(TiffPhotometricInterpretation interpretation, IRawJpegData data) => interpretation switch + { + TiffPhotometricInterpretation.Rgb => JpegColorSpace.RGB, + TiffPhotometricInterpretation.Separated => data.ColorSpace == JpegColorSpace.Ycck ? JpegColorSpace.TiffYccK : JpegColorSpace.TiffCmyk, + TiffPhotometricInterpretation.YCbCr => JpegColorSpace.RGB, // TODO: Why doesn't this use the YCbCr color space? + _ => throw new InvalidImageContentException($"Invalid TIFF photometric interpretation for JPEG encoding: {interpretation}"), + }; + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffLzwDecoder.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffLzwDecoder.cs new file mode 100644 index 0000000..38457ba --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffLzwDecoder.cs @@ -0,0 +1,256 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /* + This implementation is based on a port of a java tiff decoder by Harald Kuhr: https://github.com/haraldk/TwelveMonkeys + + Original licence: + + BSD 3-Clause License + + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + ** Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED.IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + /// + /// Decompresses and decodes data using the dynamic LZW algorithms, see TIFF spec Section 13. + /// + internal sealed class TiffLzwDecoder + { + /// + /// The stream to decode. + /// + private readonly Stream stream; + + /// + /// As soon as we use entry 4094 of the table (maxTableSize - 2), the lzw compressor write out a (12-bit) ClearCode. + /// At this point, the compressor reinitializes the string table and then writes out 9-bit codes again. + /// + private const int ClearCode = 256; + + /// + /// End of Information. + /// + private const int EoiCode = 257; + + /// + /// Minimum code length of 9 bits. + /// + private const int MinBits = 9; + + /// + /// Maximum code length of 12 bits. + /// + private const int MaxBits = 12; + + /// + /// Maximum table size of 4096. + /// + private const int TableSize = 1 << MaxBits; + + private readonly LzwString[] table; + + private int tableLength; + private int bitsPerCode; + private int oldCode = ClearCode; + private int maxCode; + private int bitMask; + private int maxString; + private bool eofReached; + private int nextData; + private int nextBits; + + /// + /// Initializes a new instance of the class + /// and sets the stream, where the compressed data should be read from. + /// + /// The stream to read from. + /// is null. + public TiffLzwDecoder(Stream stream) + { + Guard.NotNull(stream, nameof(stream)); + + this.stream = stream; + + // TODO: Investigate a manner by which we can avoid this allocation. + this.table = new LzwString[TableSize]; + for (int i = 0; i < 256; i++) + { + this.table[i] = new LzwString((byte)i); + } + + this.Init(); + } + + private void Init() + { + // Table length is 256 + 2, because of special clear code and end of information code. + this.tableLength = 258; + this.bitsPerCode = MinBits; + this.bitMask = BitmaskFor(this.bitsPerCode); + this.maxCode = this.MaxCode(); + this.maxString = 1; + } + + /// + /// Decodes and decompresses all pixel indices from the stream. + /// + /// The pixel array to decode to. + public void DecodePixels(Span pixels) + { + // Adapted from the pseudo-code example found in the TIFF 6.0 Specification, 1992. + // See Section 13: "LZW Compression"/"LZW Decoding", page 61+ + int code; + int offset = 0; + + while ((code = this.GetNextCode()) != EoiCode) + { + if (code == ClearCode) + { + this.Init(); + code = this.GetNextCode(); + + if (code == EoiCode) + { + break; + } + + if (this.table[code] == null) + { + TiffThrowHelper.ThrowImageFormatException($"Corrupted TIFF LZW: code {code} (table size: {this.tableLength})"); + } + + offset += this.table[code].WriteTo(pixels, offset); + } + else + { + if (this.table[this.oldCode] == null) + { + TiffThrowHelper.ThrowImageFormatException($"Corrupted TIFF LZW: code {this.oldCode} (table size: {this.tableLength})"); + } + + if (this.IsInTable(code)) + { + offset += this.table[code].WriteTo(pixels, offset); + + this.AddStringToTable(this.table[this.oldCode].Concatenate(this.table[code].FirstChar)); + } + else + { + LzwString outString = this.table[this.oldCode].Concatenate(this.table[this.oldCode].FirstChar); + + offset += outString.WriteTo(pixels, offset); + this.AddStringToTable(outString); + } + } + + this.oldCode = code; + + if (offset >= pixels.Length) + { + break; + } + } + } + + private void AddStringToTable(LzwString lzwString) + { + if (this.tableLength > this.table.Length) + { + TiffThrowHelper.ThrowImageFormatException($"TIFF LZW with more than {MaxBits} bits per code encountered (table overflow)"); + } + + this.table[this.tableLength++] = lzwString; + + if (this.tableLength > this.maxCode) + { + this.bitsPerCode++; + + if (this.bitsPerCode > MaxBits) + { + // Continue reading MaxBits (12 bit) length codes. + this.bitsPerCode = MaxBits; + } + + this.bitMask = BitmaskFor(this.bitsPerCode); + this.maxCode = this.MaxCode(); + } + + if (lzwString.Length > this.maxString) + { + this.maxString = lzwString.Length; + } + } + + private int GetNextCode() + { + if (this.eofReached) + { + return EoiCode; + } + + int read = this.stream.ReadByte(); + if (read < 0) + { + this.eofReached = true; + return EoiCode; + } + + this.nextData = (this.nextData << 8) | read; + this.nextBits += 8; + + if (this.nextBits < this.bitsPerCode) + { + read = this.stream.ReadByte(); + if (read < 0) + { + this.eofReached = true; + return EoiCode; + } + + this.nextData = (this.nextData << 8) | read; + this.nextBits += 8; + } + + int code = (this.nextData >> (this.nextBits - this.bitsPerCode)) & this.bitMask; + this.nextBits -= this.bitsPerCode; + + return code; + } + + private bool IsInTable(int code) => code < this.tableLength; + + private int MaxCode() => this.bitMask - 1; + + private static int BitmaskFor(int bits) => (1 << bits) - 1; + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffOldJpegSpectralConverter{TPixel}.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffOldJpegSpectralConverter{TPixel}.cs new file mode 100644 index 0000000..77ed8c0 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffOldJpegSpectralConverter{TPixel}.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jpeg.Components; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Spectral converter for YCbCr TIFF's which use the OldJPEG compression. + /// The jpeg data should be always treated as YCbCr color space. + /// + /// The type of the pixel. + internal sealed class TiffOldJpegSpectralConverter : SpectralConverter + where TPixel : unmanaged, IPixel + { + private readonly TiffPhotometricInterpretation photometricInterpretation; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// Tiff photometric interpretation. + public TiffOldJpegSpectralConverter(Configuration configuration, TiffPhotometricInterpretation photometricInterpretation) + : base(configuration) + => this.photometricInterpretation = photometricInterpretation; + + /// + protected override JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData) + { + JpegColorSpace colorSpace = GetJpegColorSpaceFromPhotometricInterpretation(this.photometricInterpretation, jpegData); + return JpegColorConverterBase.GetConverter(colorSpace, frame.Precision); + } + + private static JpegColorSpace GetJpegColorSpaceFromPhotometricInterpretation(TiffPhotometricInterpretation interpretation, IRawJpegData data) + => interpretation switch + { + // Like libtiff: Always treat the pixel data as YCbCr when the data is compressed with old jpeg compression. + TiffPhotometricInterpretation.Rgb => JpegColorSpace.YCbCr, + TiffPhotometricInterpretation.Separated => data.ColorSpace == JpegColorSpace.Ycck ? JpegColorSpace.TiffYccK : JpegColorSpace.TiffCmyk, + TiffPhotometricInterpretation.YCbCr => JpegColorSpace.YCbCr, + _ => throw new InvalidImageContentException($"Invalid tiff photometric interpretation for jpeg encoding: {interpretation}"), + }; + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs b/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs new file mode 100644 index 0000000..efe75c4 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { + /// + /// Class to handle cases where TIFF image data is compressed as a webp stream. + /// + internal class WebpTiffCompression : TiffBaseDecompressor + { + private readonly DecoderOptions options; + + /// + /// Initializes a new instance of the class. + /// + /// The general decoder options. + /// The memory allocator. + /// The width of the image. + /// The bits per pixel. + /// The predictor. + public WebpTiffCompression(DecoderOptions options, MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None) + : base(memoryAllocator, width, bitsPerPixel, predictor) + => this.options = options; + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) + { + using WebpDecoderCore decoder = new(new WebpDecoderOptions { GeneralOptions = this.options }); + using Image image = decoder.Decode(this.options.Configuration, stream, cancellationToken); + CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer); + } + + private static void CopyImageBytesToBuffer(Span buffer, Buffer2D pixelBuffer) + { + int offset = 0; + for (int y = 0; y < pixelBuffer.Height; y++) + { + Span pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y); + Span rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan); + rgbBytes.CopyTo(buffer[offset..]); + offset += rgbBytes.Length; + } + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/FaxCompressionOptions.cs b/ImageSharp/Formats/Tiff/Compression/FaxCompressionOptions.cs new file mode 100644 index 0000000..ef09be7 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/FaxCompressionOptions.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression { + /// + /// Fax compression options, see TIFF spec page 51f (T4Options). + /// + [Flags] + public enum FaxCompressionOptions : uint + { + /// + /// No options. + /// + None = 0, + + /// + /// If set, 2-dimensional coding is used (otherwise 1-dimensional is assumed). + /// + TwoDimensionalCoding = 1, + + /// + /// If set, uncompressed mode is used. + /// + UncompressedMode = 2, + + /// + /// If set, fill bits have been added as necessary before EOL codes such that + /// EOL always ends on a byte boundary, thus ensuring an EOL-sequence of 1 byte + /// preceded by a zero nibble: xxxx-0000 0000-0001. + /// + EolPadding = 4 + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs b/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs new file mode 100644 index 0000000..55a4683 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs @@ -0,0 +1,873 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression { + /// + /// Methods for undoing the horizontal prediction used in combination with deflate and LZW compressed TIFF images. + /// + internal static class HorizontalPredictor + { + /// + /// Inverts the horizontal predictor. + /// + /// Buffer with decompressed pixel data. + /// The width of the image or strip. + /// The color type of the pixel data. + /// If set to true decodes the pixel data as big endian, otherwise as little endian. + public static void Undo(Span pixelBytes, int width, TiffColorType colorType, bool isBigEndian) + { + switch (colorType) + { + case TiffColorType.BlackIsZero8: + case TiffColorType.WhiteIsZero8: + case TiffColorType.PaletteColor: + UndoGray8Bit(pixelBytes, width); + break; + case TiffColorType.BlackIsZero16: + case TiffColorType.WhiteIsZero16: + UndoGray16Bit(pixelBytes, width, isBigEndian); + break; + case TiffColorType.BlackIsZero32: + case TiffColorType.WhiteIsZero32: + UndoGray32Bit(pixelBytes, width, isBigEndian); + break; + case TiffColorType.Rgb888: + case TiffColorType.CieLab: + UndoRgb24Bit(pixelBytes, width); + break; + case TiffColorType.Rgba8888: + case TiffColorType.Cmyk: + UndoRgba32Bit(pixelBytes, width); + break; + case TiffColorType.Rgb161616: + UndoRgb48Bit(pixelBytes, width, isBigEndian); + break; + case TiffColorType.Rgba16161616: + UndoRgba64Bit(pixelBytes, width, isBigEndian); + break; + case TiffColorType.Rgb323232: + UndoRgb96Bit(pixelBytes, width, isBigEndian); + break; + case TiffColorType.Rgba32323232: + UndoRgba128Bit(pixelBytes, width, isBigEndian); + break; + } + } + + /// + /// Inverts the horizontal predictor for each tile row. + /// + /// Buffer with decompressed pixel data for a tile. + /// Tile width in pixels. + /// Tile height in pixels. + /// The color type of the pixel data. + /// If set to true decodes the pixel data as big endian, otherwise as little endian. + public static void UndoTile(Span pixelBytes, int tileWidth, int tileHeight, TiffColorType colorType, bool isBigEndian) + { + for (int y = 0; y < tileHeight; y++) + { + UndoRow(pixelBytes, tileWidth, y, colorType, isBigEndian); + } + } + + /// + /// Inverts the horizontal predictor for one row. + /// + /// Buffer with decompressed pixel data. + /// The width in pixels of the row. + /// The row index. + /// The color type of the pixel data. + /// If set to true decodes the pixel data as big endian, otherwise as little endian. + public static void UndoRow(Span pixelBytes, int width, int y, TiffColorType colorType, bool isBigEndian) + { + switch (colorType) + { + case TiffColorType.BlackIsZero8: + case TiffColorType.WhiteIsZero8: + case TiffColorType.PaletteColor: + UndoGray8BitRow(pixelBytes, width, y); + break; + + case TiffColorType.BlackIsZero16: + case TiffColorType.WhiteIsZero16: + if (isBigEndian) + { + UndoGray16BitBigEndianRow(pixelBytes, width, y); + } + else + { + UndoGray16BitLittleEndianRow(pixelBytes, width, y); + } + + break; + + case TiffColorType.BlackIsZero32: + case TiffColorType.WhiteIsZero32: + if (isBigEndian) + { + UndoGray32BitBigEndianRow(pixelBytes, width, y); + } + else + { + UndoGray32BitLittleEndianRow(pixelBytes, width, y); + } + + break; + + case TiffColorType.Rgb888: + case TiffColorType.CieLab: + UndoRgb24BitRow(pixelBytes, width, y); + break; + + case TiffColorType.Rgba8888: + case TiffColorType.Cmyk: + UndoRgba32BitRow(pixelBytes, width, y); + break; + + case TiffColorType.Rgb161616: + if (isBigEndian) + { + UndoRgb48BitBigEndianRow(pixelBytes, width, y); + } + else + { + UndoRgb48BitLittleEndianRow(pixelBytes, width, y); + } + + break; + + case TiffColorType.Rgba16161616: + if (isBigEndian) + { + UndoRgb64BitBigEndianRow(pixelBytes, width, y); + } + else + { + UndoRgb64BitLittleEndianRow(pixelBytes, width, y); + } + + break; + + case TiffColorType.Rgb323232: + if (isBigEndian) + { + UndoRgb96BitBigEndianRow(pixelBytes, width, y); + } + else + { + UndoRgb96BitLittleEndianRow(pixelBytes, width, y); + } + + break; + + case TiffColorType.Rgba32323232: + if (isBigEndian) + { + UndoRgba128BitBigEndianRow(pixelBytes, width, y); + } + else + { + UndoRgba128BitLittleEndianRow(pixelBytes, width, y); + } + + break; + } + } + + public static void ApplyHorizontalPrediction(Span rows, int width, int bitsPerPixel) + { + if (bitsPerPixel == 8) + { + ApplyHorizontalPrediction8Bit(rows, width); + } + else if (bitsPerPixel == 16) + { + // Assume rows are L16 grayscale since that's currently the only way 16 bits is supported by encoder + ApplyHorizontalPrediction16Bit(rows, width); + } + else if (bitsPerPixel == 24) + { + ApplyHorizontalPrediction24Bit(rows, width); + } + } + + /// + /// Applies a horizontal predictor to the rgb row. + /// Make use of the fact that many continuous-tone images rarely vary much in pixel value from one pixel to the next. + /// In such images, if we replace the pixel values by differences between consecutive pixels, many of the differences should be 0, plus + /// or minus 1, and so on.This reduces the apparent information content and allows LZW to encode the data more compactly. + /// + /// The rgb pixel rows. + /// The width. + [MethodImpl(InliningOptions.ShortMethod)] + private static void ApplyHorizontalPrediction24Bit(Span rows, int width) + { + DebugGuard.IsTrue(rows.Length % width == 0, "Values must be equals"); + int height = rows.Length / width; + for (int y = 0; y < height; y++) + { + Span rowSpan = rows.Slice(y * width, width); + Span rowRgb = MemoryMarshal.Cast(rowSpan); + + for (int x = rowRgb.Length - 1; x >= 1; x--) + { + byte r = (byte)(rowRgb[x].R - rowRgb[x - 1].R); + byte g = (byte)(rowRgb[x].G - rowRgb[x - 1].G); + byte b = (byte)(rowRgb[x].B - rowRgb[x - 1].B); + rowRgb[x] = new Rgb24(r, g, b); + } + } + } + + /// + /// Applies a horizontal predictor to the L16 row. + /// Make use of the fact that many continuous-tone images rarely vary much in pixel value from one pixel to the next. + /// In such images, if we replace the pixel values by differences between consecutive pixels, many of the differences should be 0, plus + /// or minus 1, and so on.This reduces the apparent information content and allows LZW to encode the data more compactly. + /// + /// The L16 pixel rows. + /// The width. + [MethodImpl(InliningOptions.ShortMethod)] + private static void ApplyHorizontalPrediction16Bit(Span rows, int width) + { + DebugGuard.IsTrue(rows.Length % width == 0, "Values must be equals"); + int height = rows.Length / width; + for (int y = 0; y < height; y++) + { + Span rowSpan = rows.Slice(y * width, width); + Span rowL16 = MemoryMarshal.Cast(rowSpan); + + for (int x = rowL16.Length - 1; x >= 1; x--) + { + rowL16[x].PackedValue = (ushort)(rowL16[x].PackedValue - rowL16[x - 1].PackedValue); + } + } + } + + /// + /// Applies a horizontal predictor to a gray pixel row. + /// + /// The gray pixel rows. + /// The width. + [MethodImpl(InliningOptions.ShortMethod)] + private static void ApplyHorizontalPrediction8Bit(Span rows, int width) + { + DebugGuard.IsTrue(rows.Length % width == 0, "Values must be equals"); + int height = rows.Length / width; + for (int y = 0; y < height; y++) + { + Span rowSpan = rows.Slice(y * width, width); + for (int x = rowSpan.Length - 1; x >= 1; x--) + { + rowSpan[x] -= rowSpan[x - 1]; + } + } + } + + private static void UndoGray8BitRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width; + int height = pixelBytes.Length / rowBytesCount; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + byte pixelValue = rowBytes[0]; + for (int x = 1; x < width; x++) + { + pixelValue += rowBytes[x]; + rowBytes[x] = pixelValue; + } + } + + private static void UndoGray8Bit(Span pixelBytes, int width) + { + int rowBytesCount = width; + int height = pixelBytes.Length / rowBytesCount; + for (int y = 0; y < height; y++) + { + UndoGray8BitRow(pixelBytes, width, y); + } + } + + private static void UndoGray16BitBigEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 2; + int height = pixelBytes.Length / rowBytesCount; + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort pixelValue = TiffUtilities.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort diff = TiffUtilities.ConvertToUShortBigEndian(rowSpan); + pixelValue += diff; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, pixelValue); + offset += 2; + } + } + + private static void UndoGray16BitLittleEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 2; + int height = pixelBytes.Length / rowBytesCount; + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort pixelValue = TiffUtilities.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort diff = TiffUtilities.ConvertToUShortLittleEndian(rowSpan); + pixelValue += diff; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, pixelValue); + offset += 2; + } + } + + private static void UndoGray16Bit(Span pixelBytes, int width, bool isBigEndian) + { + int rowBytesCount = width * 2; + int height = pixelBytes.Length / rowBytesCount; + if (isBigEndian) + { + for (int y = 0; y < height; y++) + { + UndoGray16BitBigEndianRow(pixelBytes, width, y); + } + } + else + { + for (int y = 0; y < height; y++) + { + UndoGray16BitLittleEndianRow(pixelBytes, width, y); + } + } + } + + private static void UndoGray32BitBigEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 4; + int height = pixelBytes.Length / rowBytesCount; + + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint pixelValue = TiffUtilities.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint diff = TiffUtilities.ConvertToUIntBigEndian(rowSpan); + pixelValue += diff; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, pixelValue); + offset += 4; + } + } + + private static void UndoGray32BitLittleEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 4; + int height = pixelBytes.Length / rowBytesCount; + + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint pixelValue = TiffUtilities.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint diff = TiffUtilities.ConvertToUIntLittleEndian(rowSpan); + pixelValue += diff; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, pixelValue); + offset += 4; + } + } + + private static void UndoGray32Bit(Span pixelBytes, int width, bool isBigEndian) + { + int rowBytesCount = width * 4; + int height = pixelBytes.Length / rowBytesCount; + if (isBigEndian) + { + for (int y = 0; y < height; y++) + { + UndoGray32BitBigEndianRow(pixelBytes, width, y); + } + } + else + { + for (int y = 0; y < height; y++) + { + UndoGray32BitLittleEndianRow(pixelBytes, width, y); + } + } + } + + private static void UndoRgb24BitRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 3; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + Span rowRgb = MemoryMarshal.Cast(rowBytes)[..width]; + ref Rgb24 rowRgbBase = ref MemoryMarshal.GetReference(rowRgb); + byte r = rowRgbBase.R; + byte g = rowRgbBase.G; + byte b = rowRgbBase.B; + + for (int x = 1; x < rowRgb.Length; x++) + { + ref Rgb24 pixel = ref rowRgb[x]; + r += pixel.R; + g += pixel.G; + b += pixel.B; + pixel = new Rgb24(r, g, b); + } + } + + private static void UndoRgb24Bit(Span pixelBytes, int width) + { + int rowBytesCount = width * 3; + int height = pixelBytes.Length / rowBytesCount; + for (int y = 0; y < height; y++) + { + UndoRgb24BitRow(pixelBytes, width, y); + } + } + + private static void UndoRgba32BitRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 4; + + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + Span rowRgb = MemoryMarshal.Cast(rowBytes)[..width]; + ref Rgba32 rowRgbBase = ref MemoryMarshal.GetReference(rowRgb); + byte r = rowRgbBase.R; + byte g = rowRgbBase.G; + byte b = rowRgbBase.B; + byte a = rowRgbBase.A; + + for (int x = 1; x < rowRgb.Length; x++) + { + ref Rgba32 pixel = ref rowRgb[x]; + r += pixel.R; + g += pixel.G; + b += pixel.B; + a += pixel.A; + pixel = new Rgba32(r, g, b, a); + } + } + + private static void UndoRgba32Bit(Span pixelBytes, int width) + { + int rowBytesCount = width * 4; + int height = pixelBytes.Length / rowBytesCount; + for (int y = 0; y < height; y++) + { + UndoRgba32BitRow(pixelBytes, width, y); + } + } + + private static void UndoRgb48BitBigEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 6; + int height = pixelBytes.Length / rowBytesCount; + + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort r = TiffUtilities.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort g = TiffUtilities.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort b = TiffUtilities.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort deltaR = TiffUtilities.ConvertToUShortBigEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, r); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaG = TiffUtilities.ConvertToUShortBigEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, g); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaB = TiffUtilities.ConvertToUShortBigEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, b); + offset += 2; + } + } + + private static void UndoRgb48BitLittleEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 6; + int height = pixelBytes.Length / rowBytesCount; + + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort r = TiffUtilities.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort g = TiffUtilities.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort b = TiffUtilities.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort deltaR = TiffUtilities.ConvertToUShortLittleEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, r); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaG = TiffUtilities.ConvertToUShortLittleEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, g); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaB = TiffUtilities.ConvertToUShortLittleEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, b); + offset += 2; + } + } + + private static void UndoRgb48Bit(Span pixelBytes, int width, bool isBigEndian) + { + int rowBytesCount = width * 6; + int height = pixelBytes.Length / rowBytesCount; + if (isBigEndian) + { + for (int y = 0; y < height; y++) + { + UndoRgb48BitBigEndianRow(pixelBytes, width, y); + } + } + else + { + for (int y = 0; y < height; y++) + { + UndoRgb48BitLittleEndianRow(pixelBytes, width, y); + } + } + } + + private static void UndoRgb64BitBigEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 8; + int offset = 0; + + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort r = TiffUtilities.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort g = TiffUtilities.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort b = TiffUtilities.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort a = TiffUtilities.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort deltaR = TiffUtilities.ConvertToUShortBigEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, r); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaG = TiffUtilities.ConvertToUShortBigEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, g); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaB = TiffUtilities.ConvertToUShortBigEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, b); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaA = TiffUtilities.ConvertToUShortBigEndian(rowSpan); + a += deltaA; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, a); + offset += 2; + } + } + + private static void UndoRgb64BitLittleEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 8; + int offset = 0; + + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort r = TiffUtilities.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort g = TiffUtilities.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort b = TiffUtilities.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort a = TiffUtilities.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort deltaR = TiffUtilities.ConvertToUShortLittleEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, r); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaG = TiffUtilities.ConvertToUShortLittleEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, g); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaB = TiffUtilities.ConvertToUShortLittleEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, b); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaA = TiffUtilities.ConvertToUShortLittleEndian(rowSpan); + a += deltaA; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, a); + offset += 2; + } + } + + private static void UndoRgba64Bit(Span pixelBytes, int width, bool isBigEndian) + { + int rowBytesCount = width * 8; + int height = pixelBytes.Length / rowBytesCount; + if (isBigEndian) + { + for (int y = 0; y < height; y++) + { + UndoRgb64BitBigEndianRow(pixelBytes, width, y); + } + } + else + { + for (int y = 0; y < height; y++) + { + UndoRgb64BitLittleEndianRow(pixelBytes, width, y); + } + } + } + + private static void UndoRgb96BitBigEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 12; + + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint r = TiffUtilities.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint g = TiffUtilities.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint b = TiffUtilities.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint deltaR = TiffUtilities.ConvertToUIntBigEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, r); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaG = TiffUtilities.ConvertToUIntBigEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, g); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaB = TiffUtilities.ConvertToUIntBigEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, b); + offset += 4; + } + } + + private static void UndoRgb96BitLittleEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 12; + + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint r = TiffUtilities.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint g = TiffUtilities.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint b = TiffUtilities.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint deltaR = TiffUtilities.ConvertToUIntLittleEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, r); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaG = TiffUtilities.ConvertToUIntLittleEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, g); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaB = TiffUtilities.ConvertToUIntLittleEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, b); + offset += 4; + } + } + + private static void UndoRgb96Bit(Span pixelBytes, int width, bool isBigEndian) + { + int rowBytesCount = width * 12; + int height = pixelBytes.Length / rowBytesCount; + if (isBigEndian) + { + for (int y = 0; y < height; y++) + { + UndoRgb96BitBigEndianRow(pixelBytes, width, y); + } + } + else + { + for (int y = 0; y < height; y++) + { + UndoRgb96BitLittleEndianRow(pixelBytes, width, y); + } + } + } + + private static void UndoRgba128BitBigEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 16; + + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint r = TiffUtilities.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint g = TiffUtilities.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint b = TiffUtilities.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint a = TiffUtilities.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint deltaR = TiffUtilities.ConvertToUIntBigEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, r); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaG = TiffUtilities.ConvertToUIntBigEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, g); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaB = TiffUtilities.ConvertToUIntBigEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, b); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaA = TiffUtilities.ConvertToUIntBigEndian(rowSpan); + a += deltaA; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, a); + offset += 4; + } + } + + private static void UndoRgba128BitLittleEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 16; + + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint r = TiffUtilities.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint g = TiffUtilities.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint b = TiffUtilities.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint a = TiffUtilities.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint deltaR = TiffUtilities.ConvertToUIntLittleEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, r); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaG = TiffUtilities.ConvertToUIntLittleEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, g); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaB = TiffUtilities.ConvertToUIntLittleEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, b); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaA = TiffUtilities.ConvertToUIntLittleEndian(rowSpan); + a += deltaA; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, a); + offset += 4; + } + } + + private static void UndoRgba128Bit(Span pixelBytes, int width, bool isBigEndian) + { + int rowBytesCount = width * 16; + int height = pixelBytes.Length / rowBytesCount; + if (isBigEndian) + { + for (int y = 0; y < height; y++) + { + UndoRgba128BitBigEndianRow(pixelBytes, width, y); + } + } + else + { + for (int y = 0; y < height; y++) + { + UndoRgba128BitLittleEndianRow(pixelBytes, width, y); + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/TiffBaseCompression.cs b/ImageSharp/Formats/Tiff/Compression/TiffBaseCompression.cs new file mode 100644 index 0000000..cb03ffa --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/TiffBaseCompression.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression { + internal abstract class TiffBaseCompression : IDisposable + { + private bool isDisposed; + + protected TiffBaseCompression(MemoryAllocator allocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None) + { + this.Allocator = allocator; + this.Width = width; + this.BitsPerPixel = bitsPerPixel; + this.Predictor = predictor; + this.BytesPerRow = ((width * bitsPerPixel) + 7) / 8; + } + + /// + /// Gets the image width. + /// + public int Width { get; } + + /// + /// Gets the bits per pixel. + /// + public int BitsPerPixel { get; } + + /// + /// Gets the bytes per row. + /// + public int BytesPerRow { get; } + + /// + /// Gets the predictor to use. Should only be used with deflate or lzw compression. + /// + public TiffPredictor Predictor { get; } + + /// + /// Gets the memory allocator. + /// + protected MemoryAllocator Allocator { get; } + + /// + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.isDisposed = true; + this.Dispose(true); + } + + protected abstract void Dispose(bool disposing); + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/TiffBaseCompressor.cs b/ImageSharp/Formats/Tiff/Compression/TiffBaseCompressor.cs new file mode 100644 index 0000000..34b1e79 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/TiffBaseCompressor.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression { + internal abstract class TiffBaseCompressor : TiffBaseCompression + { + /// + /// Initializes a new instance of the class. + /// + /// The output stream to write the compressed image to. + /// The memory allocator. + /// The image width. + /// Bits per pixel. + /// The predictor to use (should only be used with deflate or lzw compression). Defaults to none. + protected TiffBaseCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None) + : base(allocator, width, bitsPerPixel, predictor) + => this.Output = output; + + /// + /// Gets the compression method to use. + /// + public abstract TiffCompression Method { get; } + + /// + /// Gets the output stream to write the compressed image to. + /// + public Stream Output { get; } + + /// + /// Does any initialization required for the compression. + /// + /// The number of rows per strip. + public abstract void Initialize(int rowsPerStrip); + + /// + /// Compresses a strip of the image. + /// + /// Image rows to compress. + /// Image height. + public abstract void CompressStrip(Span rows, int height); + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs b/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs new file mode 100644 index 0000000..0bc7d6b --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using System; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression { + /// + /// The base tiff decompressor class. + /// + internal abstract class TiffBaseDecompressor : TiffBaseCompression + { + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The width of the image. + /// The bits per pixel. + /// The predictor. + protected TiffBaseDecompressor(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None) + : base(memoryAllocator, width, bitsPerPixel, predictor) + { + } + + /// + /// Decompresses image data into the supplied buffer. + /// + /// The to read image data from. + /// The data offset within the stream. + /// The number of bytes to read from the input stream. + /// The height of the strip. + /// The output buffer for uncompressed data. + /// The token to monitor cancellation. + public void Decompress(BufferedReadStream stream, ulong offset, ulong count, int stripHeight, Span buffer, CancellationToken cancellationToken) + { + DebugGuard.MustBeLessThanOrEqualTo(offset, (ulong)long.MaxValue, nameof(offset)); + DebugGuard.MustBeLessThanOrEqualTo(count, (ulong)int.MaxValue, nameof(count)); + + stream.Seek((long)offset, SeekOrigin.Begin); + this.Decompress(stream, (int)count, stripHeight, buffer, cancellationToken); + + if ((long)offset + (long)count < stream.Position) + { + TiffThrowHelper.ThrowImageFormatException("Out of range when reading a strip."); + } + } + + /// + /// Decompresses image data into the supplied buffer. + /// + /// The to read image data from. + /// The number of bytes to read from the input stream. + /// The height of the strip. + /// The output buffer for uncompressed data. + /// The token to monitor cancellation. + protected abstract void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken); + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs b/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs new file mode 100644 index 0000000..b9d5ca7 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression { + internal static class TiffCompressorFactory + { + public static TiffBaseCompressor Create( + TiffCompression method, + Stream output, + MemoryAllocator allocator, + int width, + int bitsPerPixel, + DeflateCompressionLevel compressionLevel, + TiffPredictor predictor) + { + switch (method) + { + // The following compression types are not implemented in the encoder and will default to no compression instead. + case TiffCompression.ItuTRecT43: + case TiffCompression.ItuTRecT82: + case TiffCompression.OldJpeg: + case TiffCompression.OldDeflate: + case TiffCompression.None: + DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + + return new NoCompressor(output, allocator, width, bitsPerPixel); + + case TiffCompression.Jpeg: + DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new TiffJpegCompressor(output, allocator, width, bitsPerPixel); + + case TiffCompression.PackBits: + DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new PackBitsCompressor(output, allocator, width, bitsPerPixel); + + case TiffCompression.Deflate: + return new DeflateCompressor(output, allocator, width, bitsPerPixel, predictor, compressionLevel); + + case TiffCompression.Lzw: + DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); + return new LzwCompressor(output, allocator, width, bitsPerPixel, predictor); + + case TiffCompression.CcittGroup3Fax: + DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new T4BitCompressor(output, allocator, width, bitsPerPixel, false); + + case TiffCompression.CcittGroup4Fax: + DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new T6BitCompressor(output, allocator, width, bitsPerPixel); + + case TiffCompression.Ccitt1D: + DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new T4BitCompressor(output, allocator, width, bitsPerPixel, true); + + default: + throw TiffThrowHelper.NotSupportedCompressor(method.ToString()); + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs b/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs new file mode 100644 index 0000000..c125406 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression { + /// + /// Provides enumeration of the various TIFF compression types the decoder can handle. + /// + internal enum TiffDecoderCompressionType + { + /// + /// Image data is stored uncompressed in the TIFF file. + /// + None = 0, + + /// + /// Image data is compressed using PackBits compression. + /// + PackBits = 1, + + /// + /// Image data is compressed using Deflate compression. + /// + Deflate = 2, + + /// + /// Image data is compressed using LZW compression. + /// + Lzw = 3, + + /// + /// Image data is compressed using CCITT T.4 fax compression. + /// + T4 = 4, + + /// + /// Image data is compressed using CCITT T.6 fax compression. + /// + T6 = 5, + + /// + /// Image data is compressed using modified huffman compression. + /// + HuffmanRle = 6, + + /// + /// The image data is compressed as a JPEG stream. + /// + Jpeg = 7, + + /// + /// The image data is compressed as a WEBP stream. + /// + Webp = 8, + + /// + /// The image data is compressed as a OldJPEG compressed stream. + /// + OldJpeg = 9, + } +} diff --git a/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs b/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs new file mode 100644 index 0000000..04fc9d1 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression { + internal static class TiffDecompressorsFactory + { + public static TiffBaseDecompressor Create( + DecoderOptions options, + TiffDecoderCompressionType method, + MemoryAllocator allocator, + TiffPhotometricInterpretation photometricInterpretation, + int width, + int bitsPerPixel, + ImageFrameMetadata metadata, + TiffColorType colorType, + TiffPredictor predictor, + FaxCompressionOptions faxOptions, + byte[] jpegTables, + uint oldJpegStartOfImageMarker, + TiffFillOrder fillOrder, + ByteOrder byteOrder, + bool isTiled = false, + int tileWidth = 0, + int tileHeight = 0) + { + switch (method) + { + case TiffDecoderCompressionType.None: + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected"); + return new NoneTiffCompression(allocator, width, bitsPerPixel); + + case TiffDecoderCompressionType.PackBits: + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected"); + return new PackBitsTiffCompression(allocator, width, bitsPerPixel); + + case TiffDecoderCompressionType.Deflate: + DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected"); + return new DeflateTiffCompression(allocator, width, bitsPerPixel, colorType, predictor, byteOrder == ByteOrder.BigEndian, isTiled, tileWidth, tileHeight); + + case TiffDecoderCompressionType.Lzw: + DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected"); + return new LzwTiffCompression(allocator, width, bitsPerPixel, colorType, predictor, byteOrder == ByteOrder.BigEndian, isTiled, tileWidth, tileHeight); + + case TiffDecoderCompressionType.T4: + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new T4TiffCompression(allocator, fillOrder, width, bitsPerPixel, faxOptions, photometricInterpretation); + + case TiffDecoderCompressionType.T6: + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new T6TiffCompression(allocator, fillOrder, width, bitsPerPixel, photometricInterpretation); + + case TiffDecoderCompressionType.HuffmanRle: + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new ModifiedHuffmanTiffCompression(allocator, fillOrder, width, bitsPerPixel, photometricInterpretation); + + case TiffDecoderCompressionType.Jpeg: + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new JpegTiffCompression(new JpegDecoderOptions { GeneralOptions = options }, allocator, width, bitsPerPixel, metadata, jpegTables, photometricInterpretation); + + case TiffDecoderCompressionType.OldJpeg: + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new OldJpegTiffCompression(new JpegDecoderOptions { GeneralOptions = options }, allocator, width, bitsPerPixel, metadata, oldJpegStartOfImageMarker, photometricInterpretation); + + case TiffDecoderCompressionType.Webp: + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new WebpTiffCompression(options, allocator, width, bitsPerPixel); + + default: + throw TiffThrowHelper.NotSupportedDecompressor(nameof(method)); + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs b/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs new file mode 100644 index 0000000..27b48f0 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants { + /// + /// Enumeration representing the compression formats defined by the Tiff file-format. + /// + public enum TiffCompression : ushort + { + /// + /// A invalid compression value. + /// + Invalid = 0, + + /// + /// No compression. + /// + None = 1, + + /// + /// CCITT Group 3 1-Dimensional Modified Huffman run-length encoding. + /// + Ccitt1D = 2, + + /// + /// T4-encoding: CCITT T.4 bi-level encoding (see Section 11 of the TIFF 6.0 specification). + /// + CcittGroup3Fax = 3, + + /// + /// T6-encoding: CCITT T.6 bi-level encoding (see Section 11 of the TIFF 6.0 specification). + /// + CcittGroup4Fax = 4, + + /// + /// LZW compression (see Section 13 of the TIFF 6.0 specification). + /// + Lzw = 5, + + /// + /// JPEG compression - obsolete (see Section 22 of the TIFF 6.0 specification). + /// + /// Note: The TIFF encoder does not support this compression and will default to use no compression instead, + /// if this is chosen. + /// + OldJpeg = 6, + + /// + /// JPEG compression (see TIFF Specification, supplement 2). + /// + /// Note: The TIFF encoder does not yet support this compression and will default to use no compression instead, + /// if this is chosen. + /// + Jpeg = 7, + + /// + /// Deflate compression, using zlib data format (see TIFF Specification, supplement 2). + /// + Deflate = 8, + + /// + /// ITU-T Rec. T.82 coding, applying ITU-T Rec. T.85 (JBIG) (see RFC2301). + /// + /// Note: The TIFF encoder does not yet support this compression and will default to use no compression instead, + /// if this is chosen. + /// + ItuTRecT82 = 9, + + /// + /// ITU-T Rec. T.43 representation, using ITU-T Rec. T.82 (JBIG) (see RFC2301). + /// + /// Note: The TIFF encoder does not yet support this compression and will default to use no compression instead, + /// if this is chosen. + /// + ItuTRecT43 = 10, + + /// + /// NeXT 2-bit Grey Scale compression algorithm. + /// + /// Note: The TIFF encoder does not support this compression and will default to use no compression instead, + /// if this is chosen. + /// + NeXT = 32766, + + /// + /// PackBits compression. + /// + PackBits = 32773, + + /// + /// ThunderScan 4-bit compression. + /// + /// Note: The TIFF encoder does not support this compression and will default to use no compression instead, + /// if this is chosen. + /// + ThunderScan = 32809, + + /// + /// Deflate compression - old. + /// + /// Note: The TIFF encoder does not support this compression and will default to use no compression instead, + /// if this is chosen. + /// + OldDeflate = 32946, + + /// + /// Pixel data is compressed with webp encoder. + /// + /// Note: The TIFF encoder does not support this compression and will default to use no compression instead, + /// if this is chosen. + /// + Webp = 50001, + } +} diff --git a/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs b/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs new file mode 100644 index 0000000..e9e7dc0 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs @@ -0,0 +1,122 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants { + /// + /// Defines constants defined in the TIFF specification. + /// + internal static class TiffConstants + { + /// + /// Byte order markers for indicating little endian encoding. + /// + public const byte ByteOrderLittleEndian = 0x49; + + /// + /// Byte order markers for indicating big endian encoding. + /// + public const byte ByteOrderBigEndian = 0x4D; + + /// + /// Byte order markers for indicating little endian encoding. + /// + public const ushort ByteOrderLittleEndianShort = 0x4949; + + /// + /// Byte order markers for indicating big endian encoding. + /// + public const ushort ByteOrderBigEndianShort = 0x4D4D; + + /// + /// Magic number used within the image file header to identify a TIFF format file. + /// + public const ushort HeaderMagicNumber = 42; + + /// + /// The big tiff header magic number + /// + public const ushort BigTiffHeaderMagicNumber = 43; + + /// + /// The big tiff byte size of offsets value. + /// + public const ushort BigTiffByteSize = 8; + + /// + /// RowsPerStrip default value, which is effectively infinity. + /// + public const int RowsPerStripInfinity = 2147483647; + + /// + /// Size (in bytes) of the Rational and SRational data types + /// + public const int SizeOfRational = 8; + + /// + /// The default strip size is 8k. + /// + public const int DefaultStripSize = 8 * 1024; + + /// + /// The default predictor is None. + /// + public const TiffPredictor DefaultPredictor = TiffPredictor.None; + + /// + /// The default bits per pixel is Bit24. + /// + public const TiffBitsPerPixel DefaultBitsPerPixel = TiffBitsPerPixel.Bit24; + + /// + /// The default bits per sample for color images with 8 bits for each color channel. + /// + public static readonly TiffBitsPerSample DefaultBitsPerSample = BitsPerSampleRgb8Bit; + + /// + /// The default compression is None. + /// + public const TiffCompression DefaultCompression = TiffCompression.None; + + /// + /// The default photometric interpretation is Rgb. + /// + public const TiffPhotometricInterpretation DefaultPhotometricInterpretation = TiffPhotometricInterpretation.Rgb; + + /// + /// The bits per sample for 1 bit bicolor images. + /// + public static readonly TiffBitsPerSample BitsPerSample1Bit = new(1, 0, 0); + + /// + /// The bits per sample for images with a 4 color palette. + /// + public static readonly TiffBitsPerSample BitsPerSample4Bit = new(4, 0, 0); + + /// + /// The bits per sample for 8 bit images. + /// + public static readonly TiffBitsPerSample BitsPerSample8Bit = new(8, 0, 0); + + /// + /// The bits per sample for 16-bit grayscale images. + /// + public static readonly TiffBitsPerSample BitsPerSample16Bit = new(16, 0, 0); + + /// + /// The bits per sample for color images with 8 bits for each color channel. + /// + public static readonly TiffBitsPerSample BitsPerSampleRgb8Bit = new(8, 8, 8); + + /// + /// The list of mime types that equate to a tiff. + /// + public static readonly IEnumerable MimeTypes = ["image/tiff", "image/tiff-fx"]; + + /// + /// The list of file extensions that equate to a tiff. + /// + public static readonly IEnumerable FileExtensions = ["tiff", "tif"]; + } +} diff --git a/ImageSharp/Formats/Tiff/Constants/TiffFillOrder.cs b/ImageSharp/Formats/Tiff/Constants/TiffFillOrder.cs new file mode 100644 index 0000000..d3112e6 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Constants/TiffFillOrder.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants { + /// + /// Enumeration representing the fill orders defined by the Tiff file-format. + /// + internal enum TiffFillOrder : ushort + { + /// + /// Pixels with lower column values are stored in the higher-order bits of the byte. + /// + MostSignificantBitFirst = 1, + + /// + /// Pixels with lower column values are stored in the lower-order bits of the byte. + /// + LeastSignificantBitFirst = 2 + } +} diff --git a/ImageSharp/Formats/Tiff/Constants/TiffInkSet.cs b/ImageSharp/Formats/Tiff/Constants/TiffInkSet.cs new file mode 100644 index 0000000..798380c --- /dev/null +++ b/ImageSharp/Formats/Tiff/Constants/TiffInkSet.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Metadata.Profiles.Exif; + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants { + /// + /// Enumeration representing the set of inks used in a separated () image. + /// + public enum TiffInkSet : ushort + { + /// + /// CMYK. + /// The order of the components is cyan, magenta, yellow, black. + /// Usually, a value of 0 represents 0% ink coverage and a value of 255 represents 100% ink coverage for that component, but see DotRange. + /// The field should not exist when InkSet=1. + /// + Cmyk = 1, + + /// + /// Not CMYK. + /// See the field for a description of the inks to be used. + /// + NotCmyk = 2 + } +} diff --git a/ImageSharp/Formats/Tiff/Constants/TiffNewSubfileType.cs b/ImageSharp/Formats/Tiff/Constants/TiffNewSubfileType.cs new file mode 100644 index 0000000..a8c9823 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Constants/TiffNewSubfileType.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants { + /// + /// Enumeration representing the sub-file types defined by the Tiff file-format. + /// + [Flags] + public enum TiffNewSubfileType : uint + { + /// + /// A full-resolution image. + /// + FullImage = 0, + + /// + /// Reduced-resolution version of another image in this TIFF file. + /// + Preview = 1, + + /// + /// A single page of a multi-page image. + /// + SinglePage = 2, + + /// + /// A transparency mask for another image in this TIFF file. + /// + TransparencyMask = 4, + + /// + /// Alternative reduced-resolution version of another image in this TIFF file (see DNG specification). + /// + AlternativePreview = 65536, + + /// + /// Mixed raster content (see RFC2301). + /// + MixedRasterContent = 8 + } +} diff --git a/ImageSharp/Formats/Tiff/Constants/TiffOrientation.cs b/ImageSharp/Formats/Tiff/Constants/TiffOrientation.cs new file mode 100644 index 0000000..61160d1 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Constants/TiffOrientation.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants { + /// + /// Enumeration representing the image orientations defined by the Tiff file-format. + /// + internal enum TiffOrientation + { + /// + /// The 0th row and 0th column represent the visual top and left-hand side of the image respectively. + /// + TopLeft = 1, + + /// + /// The 0th row and 0th column represent the visual top and right-hand side of the image respectively. + /// + TopRight = 2, + + /// + /// The 0th row and 0th column represent the visual bottom and right-hand side of the image respectively. + /// + BottomRight = 3, + + /// + /// The 0th row and 0th column represent the visual bottom and left-hand side of the image respectively. + /// + BottomLeft = 4, + + /// + /// The 0th row and 0th column represent the visual left-hand side and top of the image respectively. + /// + LeftTop = 5, + + /// + /// The 0th row and 0th column represent the visual right-hand side and top of the image respectively. + /// + RightTop = 6, + + /// + /// The 0th row and 0th column represent the visual right-hand side and bottom of the image respectively. + /// + RightBottom = 7, + + /// + /// The 0th row and 0th column represent the visual left-hand side and bottom of the image respectively. + /// + LeftBottom = 8 + } +} diff --git a/ImageSharp/Formats/Tiff/Constants/TiffPhotometricInterpretation.cs b/ImageSharp/Formats/Tiff/Constants/TiffPhotometricInterpretation.cs new file mode 100644 index 0000000..3e7b68c --- /dev/null +++ b/ImageSharp/Formats/Tiff/Constants/TiffPhotometricInterpretation.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants { + /// + /// Enumeration representing the photometric interpretation formats defined by the Tiff file-format. + /// + public enum TiffPhotometricInterpretation : ushort + { + /// + /// Bilevel and grayscale: 0 is imaged as white. The maximum value is imaged as black. + /// Not supported by the TiffEncoder. + /// + WhiteIsZero = 0, + + /// + /// Bilevel and grayscale: 0 is imaged as black. The maximum value is imaged as white. + /// + BlackIsZero = 1, + + /// + /// RGB image. + /// + Rgb = 2, + + /// + /// Palette Color. + /// + PaletteColor = 3, + + /// + /// A transparency mask. + /// Not supported by the TiffEncoder. + /// + TransparencyMask = 4, + + /// + /// Separated: usually CMYK (see Section 16 of the TIFF 6.0 specification). + /// Not supported by the TiffEncoder. + /// + Separated = 5, + + /// + /// YCbCr (see Section 21 of the TIFF 6.0 specification). + /// Not supported by the TiffEncoder. + /// + YCbCr = 6, + + /// + /// 1976 CIE L*a*b* (see Section 23 of the TIFF 6.0 specification). + /// Not supported by the TiffEncoder. + /// + CieLab = 8, + + /// + /// ICC L*a*b* (see TIFF Specification, supplement 1). + /// Not supported by the TiffEncoder. + /// + IccLab = 9, + + /// + /// ITU L*a*b* (see RFC2301). + /// Not supported by the TiffEncoder. + /// + ItuLab = 10, + + /// + /// Color Filter Array (see the DNG specification). + /// Not supported by the TiffEncoder. + /// + ColorFilterArray = 32803, + + /// + /// Linear Raw (see the DNG specification). + /// Not supported by the TiffEncoder. + /// + LinearRaw = 34892 + } +} diff --git a/ImageSharp/Formats/Tiff/Constants/TiffPlanarConfiguration.cs b/ImageSharp/Formats/Tiff/Constants/TiffPlanarConfiguration.cs new file mode 100644 index 0000000..0d0150c --- /dev/null +++ b/ImageSharp/Formats/Tiff/Constants/TiffPlanarConfiguration.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants { + /// + /// Enumeration representing how the components of each pixel are stored the Tiff file-format. + /// + public enum TiffPlanarConfiguration : ushort + { + /// + /// Chunky format. + /// The component values for each pixel are stored contiguously. + /// The order of the components within the pixel is specified by + /// PhotometricInterpretation. For example, for RGB data, the data is stored as RGBRGBRGB. + /// + Chunky = 1, + + /// + /// Planar format. + /// The components are stored in separate “component planes.” The + /// values in StripOffsets and StripByteCounts are then arranged as a 2-dimensional + /// array, with SamplesPerPixel rows and StripsPerImage columns. (All of the columns + /// for row 0 are stored first, followed by the columns of row 1, and so on.) + /// PhotometricInterpretation describes the type of data stored in each component + /// plane. For example, RGB data is stored with the Red components in one component + /// plane, the Green in another, and the Blue in another. + /// + Planar = 2 + } +} diff --git a/ImageSharp/Formats/Tiff/Constants/TiffPredictor.cs b/ImageSharp/Formats/Tiff/Constants/TiffPredictor.cs new file mode 100644 index 0000000..dbf3c59 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Constants/TiffPredictor.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants { + /// + /// A mathematical operator that is applied to the image data before an encoding scheme is applied. + /// + public enum TiffPredictor : ushort + { + /// + /// No prediction. + /// + None = 1, + + /// + /// Horizontal differencing. + /// + Horizontal = 2, + + /// + /// Floating point horizontal differencing. + /// + /// Note: The Tiff Encoder does not yet support this. If this is chosen, the encoder will fallback to none. + /// + FloatingPoint = 3 + } +} diff --git a/ImageSharp/Formats/Tiff/Constants/TiffSampleFormat.cs b/ImageSharp/Formats/Tiff/Constants/TiffSampleFormat.cs new file mode 100644 index 0000000..93c911d --- /dev/null +++ b/ImageSharp/Formats/Tiff/Constants/TiffSampleFormat.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants { + /// + /// Specifies how to interpret each data sample in a pixel. + /// + public enum TiffSampleFormat : ushort + { + /// + /// Unsigned integer data. Default value. + /// + UnsignedInteger = 1, + + /// + /// Signed integer data. + /// + SignedInteger = 2, + + /// + /// IEEE floating point data. + /// + Float = 3, + + /// + /// Undefined data format. + /// + Undefined = 4, + + /// + /// The complex int. + /// + ComplexInt = 5, + + /// + /// The complex float. + /// + ComplexFloat = 6 + } +} diff --git a/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs b/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs new file mode 100644 index 0000000..993bd0b --- /dev/null +++ b/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants { + /// + /// Enumeration representing the sub-file types defined by the Tiff file-format. + /// + public enum TiffSubfileType : ushort + { + /// + /// Full-resolution image data. + /// + FullImage = 1, + + /// + /// Reduced-resolution image data. + /// + Preview = 2, + + /// + /// A single page of a multi-page image. + /// + SinglePage = 3 + } +} diff --git a/ImageSharp/Formats/Tiff/Constants/TiffThresholding.cs b/ImageSharp/Formats/Tiff/Constants/TiffThresholding.cs new file mode 100644 index 0000000..9ef7b8c --- /dev/null +++ b/ImageSharp/Formats/Tiff/Constants/TiffThresholding.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants { + /// + /// Enumeration representing the thresholding applied to image data defined by the Tiff file-format. + /// + internal enum TiffThresholding + { + /// + /// No dithering or halftoning. + /// + None = 1, + + /// + /// An ordered dither or halftone technique. + /// + Ordered = 2, + + /// + /// A randomized process such as error diffusion. + /// + Random = 3 + } +} diff --git a/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs b/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs new file mode 100644 index 0000000..66b561e --- /dev/null +++ b/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs @@ -0,0 +1,120 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using System; +using System.Collections.Generic; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff { + /// + /// The TIFF IFD reader class. + /// + internal class DirectoryReader + { + private const int DirectoryMax = 65534; + + private readonly Stream stream; + + private readonly MemoryAllocator allocator; + + private ulong nextIfdOffset; + + public DirectoryReader(Stream stream, MemoryAllocator allocator) + { + this.stream = stream; + this.allocator = allocator; + } + + /// + /// Gets the byte order. + /// + public ByteOrder ByteOrder { get; private set; } + + public bool IsBigTiff { get; private set; } + + /// + /// Reads image file directories. + /// + /// Image file directories. + public IList Read() + { + this.ByteOrder = ReadByteOrder(this.stream); + HeaderReader headerReader = new(this.stream, this.ByteOrder); + headerReader.ReadFileHeader(); + + this.nextIfdOffset = headerReader.FirstIfdOffset; + this.IsBigTiff = headerReader.IsBigTiff; + + return this.ReadIfds(headerReader.IsBigTiff); + } + + private static ByteOrder ReadByteOrder(Stream stream) + { + Span headerBytes = stackalloc byte[2]; + + if (stream.Read(headerBytes) != 2) + { + throw TiffThrowHelper.ThrowInvalidHeader(); + } + + if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian) + { + return ByteOrder.LittleEndian; + } + + if (headerBytes[0] == TiffConstants.ByteOrderBigEndian && headerBytes[1] == TiffConstants.ByteOrderBigEndian) + { + return ByteOrder.BigEndian; + } + + throw TiffThrowHelper.ThrowInvalidHeader(); + } + + private List ReadIfds(bool isBigTiff) + { + List readers = []; + while (this.nextIfdOffset != 0 && this.nextIfdOffset < (ulong)this.stream.Length) + { + EntryReader reader = new(this.stream, this.ByteOrder, this.allocator); + reader.ReadTags(isBigTiff, this.nextIfdOffset); + + if (reader.BigValues.Count > 0) + { + reader.BigValues.Sort((t1, t2) => t1.Offset.CompareTo(t2.Offset)); + + // this means that most likely all elements are placed before next IFD + if (reader.BigValues[0].Offset < reader.NextIfdOffset) + { + reader.ReadBigValues(); + } + } + + if (this.nextIfdOffset >= reader.NextIfdOffset && reader.NextIfdOffset != 0) + { + TiffThrowHelper.ThrowImageFormatException("TIFF image contains circular directory offsets"); + } + + this.nextIfdOffset = reader.NextIfdOffset; + readers.Add(reader); + + if (readers.Count >= DirectoryMax) + { + TiffThrowHelper.ThrowImageFormatException("TIFF image contains too many directories"); + } + } + + List list = new(readers.Count); + foreach (EntryReader reader in readers) + { + reader.ReadBigValues(); + ExifProfile profile = new(reader.Values, reader.InvalidTags); + list.Add(profile); + } + + return list; + } + } +} diff --git a/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs b/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs new file mode 100644 index 0000000..b9b9c44 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using System.Collections.Generic; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff { + internal class EntryReader : BaseExifReader + { + public EntryReader(Stream stream, ByteOrder byteOrder, MemoryAllocator allocator) + : base(stream, allocator) => + this.IsBigEndian = byteOrder == ByteOrder.BigEndian; + + public List Values { get; } = []; + + public ulong NextIfdOffset { get; private set; } + + public void ReadTags(bool isBigTiff, ulong ifdOffset) + { + if (!isBigTiff) + { + this.ReadValues(this.Values, (uint)ifdOffset); + this.NextIfdOffset = this.ReadUInt32(); + + this.ReadSubIfd(this.Values); + } + else + { + this.ReadValues64(this.Values, ifdOffset); + this.NextIfdOffset = this.ReadUInt64(); + } + } + + public void ReadBigValues() => this.ReadBigValues(this.Values); + } + + internal class HeaderReader : BaseExifReader + { + public HeaderReader(Stream stream, ByteOrder byteOrder) + : base(stream, null) => + this.IsBigEndian = byteOrder == ByteOrder.BigEndian; + + public bool IsBigTiff { get; private set; } + + public ulong FirstIfdOffset { get; private set; } + + public void ReadFileHeader() + { + ushort magic = this.ReadUInt16(); + if (magic == TiffConstants.HeaderMagicNumber) + { + this.IsBigTiff = false; + this.FirstIfdOffset = this.ReadUInt32(); + return; + } + else if (magic == TiffConstants.BigTiffHeaderMagicNumber) + { + this.IsBigTiff = true; + + ushort byteSize = this.ReadUInt16(); + ushort reserve = this.ReadUInt16(); + if (byteSize == TiffConstants.BigTiffByteSize && reserve == 0) + { + this.FirstIfdOffset = this.ReadUInt64(); + return; + } + } + + TiffThrowHelper.ThrowInvalidHeader(); + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs new file mode 100644 index 0000000..7b0204e --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'BlackIsZero' photometric interpretation for 16-bit grayscale images. + /// + /// The type of pixel format. + internal class BlackIsZero16TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + private readonly Configuration configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public BlackIsZero16TiffColor(Configuration configuration, bool isBigEndian) + { + this.configuration = configuration; + this.isBigEndian = isBigEndian; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + L16 l16 = TiffUtilities.L16Default; + TPixel color = TPixel.FromScaledVector4(Vector4.Zero); + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ushort intensity = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2)); + offset += 2; + + pixelRow[x] = TPixel.FromL16(new L16(intensity)); + } + } + else + { + int byteCount = pixelRow.Length * 2; + PixelOperations.Instance.FromL16Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); + + offset += byteCount; + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero1TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero1TiffColor{TPixel}.cs new file mode 100644 index 0000000..dd2b014 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero1TiffColor{TPixel}.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'BlackIsZero' photometric interpretation (optimized for bilevel images). + /// + /// The pixel format. + internal class BlackIsZero1TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + nuint offset = 0; + TPixel colorBlack = TPixel.FromRgba32(Color.Black.ToPixel()); + TPixel colorWhite = TPixel.FromRgba32(Color.White.ToPixel()); + + ref byte dataRef = ref MemoryMarshal.GetReference(data); + for (nuint y = (uint)top; y < (uint)(top + height); y++) + { + Span pixelRowSpan = pixels.DangerousGetRowSpan((int)y); + ref TPixel pixelRowRef = ref MemoryMarshal.GetReference(pixelRowSpan); + for (nuint x = (uint)left; x < (uint)(left + width); x += 8) + { + byte b = Unsafe.Add(ref dataRef, offset++); + nuint maxShift = Math.Min((uint)(left + width) - x, 8); + + if (maxShift == 8) + { + int bit = (b >> 7) & 1; + ref TPixel pixel0 = ref Unsafe.Add(ref pixelRowRef, x); + pixel0 = bit == 0 ? colorBlack : colorWhite; + + bit = (b >> 6) & 1; + ref TPixel pixel1 = ref Unsafe.Add(ref pixelRowRef, x + 1); + pixel1 = bit == 0 ? colorBlack : colorWhite; + + bit = (b >> 5) & 1; + ref TPixel pixel2 = ref Unsafe.Add(ref pixelRowRef, x + 2); + pixel2 = bit == 0 ? colorBlack : colorWhite; + + bit = (b >> 4) & 1; + ref TPixel pixel3 = ref Unsafe.Add(ref pixelRowRef, x + 3); + pixel3 = bit == 0 ? colorBlack : colorWhite; + + bit = (b >> 3) & 1; + ref TPixel pixel4 = ref Unsafe.Add(ref pixelRowRef, x + 4); + pixel4 = bit == 0 ? colorBlack : colorWhite; + + bit = (b >> 2) & 1; + ref TPixel pixel5 = ref Unsafe.Add(ref pixelRowRef, x + 5); + pixel5 = bit == 0 ? colorBlack : colorWhite; + + bit = (b >> 1) & 1; + ref TPixel pixel6 = ref Unsafe.Add(ref pixelRowRef, x + 6); + pixel6 = bit == 0 ? colorBlack : colorWhite; + + bit = b & 1; + ref TPixel pixel7 = ref Unsafe.Add(ref pixelRowRef, x + 7); + pixel7 = bit == 0 ? colorBlack : colorWhite; + } + else + { + for (nuint shift = 0; shift < maxShift; shift++) + { + int bit = (b >> (7 - (int)shift)) & 1; + + ref TPixel pixel = ref Unsafe.Add(ref pixelRowRef, x + shift); + pixel = bit == 0 ? colorBlack : colorWhite; + } + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs new file mode 100644 index 0000000..992d67c --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'BlackIsZero' photometric interpretation for 24-bit grayscale images. + /// + /// The type of pixel format. + internal class BlackIsZero24TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public BlackIsZero24TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + Span buffer = stackalloc byte[4]; + int bufferStartIdx = this.isBigEndian ? 1 : 0; + + Span bufferSpan = buffer[bufferStartIdx..]; + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(bufferSpan); + uint intensity = TiffUtilities.ConvertToUIntBigEndian(buffer); + offset += 3; + + pixelRow[x] = TiffUtilities.ColorScaleTo24Bit(intensity); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(bufferSpan); + uint intensity = TiffUtilities.ConvertToUIntLittleEndian(buffer); + offset += 3; + + pixelRow[x] = TiffUtilities.ColorScaleTo24Bit(intensity); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs new file mode 100644 index 0000000..ebd9bc4 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'BlackIsZero' photometric interpretation for 32-bit float grayscale images. + /// + /// The type of pixel format. + internal class BlackIsZero32FloatTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public BlackIsZero32FloatTiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + Span buffer = stackalloc byte[4]; + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 4).CopyTo(buffer); + buffer.Reverse(); + float intensity = BitConverter.ToSingle(buffer); + offset += 4; + + pixelRow[x] = TPixel.FromScaledVector4(new Vector4(intensity, intensity, intensity, 1f)); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + float intensity = BitConverter.ToSingle(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TPixel.FromScaledVector4(new Vector4(intensity, intensity, intensity, 1f)); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs new file mode 100644 index 0000000..02e27b2 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'BlackIsZero' photometric interpretation for 32-bit grayscale images. + /// + /// The type of pixel format. + internal class BlackIsZero32TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public BlackIsZero32TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + uint intensity = TiffUtilities.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtilities.ColorScaleTo32Bit(intensity); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + uint intensity = TiffUtilities.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtilities.ColorScaleTo32Bit(intensity); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero4TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero4TiffColor{TPixel}.cs new file mode 100644 index 0000000..d4da1d8 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero4TiffColor{TPixel}.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'BlackIsZero' photometric interpretation (optimized for 4-bit grayscale images). + /// + /// The type of pixel format. + internal class BlackIsZero4TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + bool isOddWidth = (width & 1) == 1; + + for (int y = top; y < top + height; y++) + { + Span pixelRowSpan = pixels.DangerousGetRowSpan(y); + for (int x = left; x < left + width - 1;) + { + byte byteData = data[offset++]; + pixelRowSpan[x++] = TPixel.FromL8(new L8((byte)(((byteData & 0xF0) >> 4) * 17))); + pixelRowSpan[x++] = TPixel.FromL8(new L8((byte)((byteData & 0x0F) * 17))); + } + + if (isOddWidth) + { + byte byteData = data[offset++]; + pixelRowSpan[left + width - 1] = TPixel.FromL8(new L8((byte)(((byteData & 0xF0) >> 4) * 17))); + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs new file mode 100644 index 0000000..0eab90f --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'BlackIsZero' photometric interpretation (optimized for 8-bit grayscale images). + /// + internal class BlackIsZero8TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly Configuration configuration; + + public BlackIsZero8TiffColor(Configuration configuration) => this.configuration = configuration; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + int byteCount = pixelRow.Length; + PixelOperations.Instance.FromL8Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); + + offset += byteCount; + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs new file mode 100644 index 0000000..3fc5447 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'BlackIsZero' photometric interpretation (for all bit depths). + /// + /// The type of pixel format. + internal class BlackIsZeroTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly ushort bitsPerSample0; + private readonly float factor; + + public BlackIsZeroTiffColor(TiffBitsPerSample bitsPerSample) + { + this.bitsPerSample0 = bitsPerSample.Channel0; + this.factor = (1 << this.bitsPerSample0) - 1f; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + BitReader bitReader = new(data); + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + int value = bitReader.ReadBits(this.bitsPerSample0); + float intensity = value / this.factor; + pixelRow[x] = TPixel.FromScaledVector4(new Vector4(intensity, intensity, intensity, 1f)); + } + + bitReader.NextRow(); + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16PlanarTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16PlanarTiffColor{TPixel}.cs new file mode 100644 index 0000000..c7b3abe --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16PlanarTiffColor{TPixel}.cs @@ -0,0 +1,145 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements decoding pixel data with photometric interpretation of type 'CieLab' with the planar configuration. + /// Each channel is represented with 16 bits. + /// + /// The type of pixel format. + internal class CieLab16PlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly ColorProfileConverter colorProfileConverter; + private readonly Configuration configuration; + private readonly bool isBigEndian; + + // libtiff encodes 16-bit Lab as: + // L* : unsigned [0, 65535] mapping to [0, 100] + // a*, b* : signed [-32768, 32767], values are 256x the 1976 a*, b* values. + private const float Inv65535 = 1f / 65535f; + private const float Inv256 = 1f / 256f; + + public CieLab16PlanarTiffColor( + Configuration configuration, + DecoderOptions decoderOptions, + ImageFrameMetadata metadata, + MemoryAllocator allocator, + bool isBigEndian) + { + this.isBigEndian = isBigEndian; + this.configuration = configuration; + + if (decoderOptions.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) + { + ColorConversionOptions options = new() + { + SourceIccProfile = iccProfile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + MemoryAllocator = allocator + }; + + this.colorProfileConverter = new ColorProfileConverter(options); + } + else + { + ColorConversionOptions options = new() + { + MemoryAllocator = allocator + }; + + this.colorProfileConverter = new ColorProfileConverter(options); + } + } + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + Span lPlane = data[0].GetSpan(); + Span aPlane = data[1].GetSpan(); + Span bPlane = data[2].GetSpan(); + + // Allocate temporary buffers to hold the LAB -> RGB conversion. + // This should be the maximum width of a row. + using IMemoryOwner rgbBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate(width); + using IMemoryOwner vectorBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate(width); + + Span rgbRow = rgbBuffer.Memory.Span; + Span vectorRow = vectorBuffer.Memory.Span; + + // Reuse the rgbRow span for lab data since both are 3-float structs, avoiding an extra allocation. + Span cieLabRow = MemoryMarshal.Cast(rgbRow); + + int stride = width * 2; + + if (this.isBigEndian) + { + for (int y = 0; y < height; y++) + { + int rowBase = y * stride; + Span pixelRow = pixels.DangerousGetRowSpan(top + y).Slice(left, width); + + for (int x = 0; x < width; x++) + { + int i = rowBase + (x * 2); + + ushort lRaw = TiffUtilities.ConvertToUShortBigEndian(lPlane.Slice(i, 2)); + short aRaw = unchecked((short)TiffUtilities.ConvertToUShortBigEndian(aPlane.Slice(i, 2))); + short bRaw = unchecked((short)TiffUtilities.ConvertToUShortBigEndian(bPlane.Slice(i, 2))); + + float l = lRaw * 100f * Inv65535; + float a = aRaw * Inv256; + float b = bRaw * Inv256; + + cieLabRow[x] = new CieLab(l, a, b); + } + + // Convert CIE Lab -> Rgb -> Vector4 -> TPixel + this.colorProfileConverter.Convert(cieLabRow, rgbRow); + Rgb.ToScaledVector4(rgbRow, vectorRow); + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale); + } + + return; + } + + for (int y = 0; y < height; y++) + { + int rowBase = y * stride; + Span pixelRow = pixels.DangerousGetRowSpan(top + y).Slice(left, width); + + for (int x = 0; x < width; x++) + { + int i = rowBase + (x * 2); + + ushort lRaw = TiffUtilities.ConvertToUShortLittleEndian(lPlane.Slice(i, 2)); + short aRaw = unchecked((short)TiffUtilities.ConvertToUShortLittleEndian(aPlane.Slice(i, 2))); + short bRaw = unchecked((short)TiffUtilities.ConvertToUShortLittleEndian(bPlane.Slice(i, 2))); + + float l = lRaw * 100f * Inv65535; + float a = aRaw * Inv256; + float b = bRaw * Inv256; + + cieLabRow[x] = new CieLab(l, a, b); + } + + // Convert CIE Lab -> Rgb -> Vector4 -> TPixel + this.colorProfileConverter.Convert(cieLabRow, rgbRow); + Rgb.ToScaledVector4(rgbRow, vectorRow); + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale); + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16TiffColor{TPixel}.cs new file mode 100644 index 0000000..1377893 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16TiffColor{TPixel}.cs @@ -0,0 +1,141 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements decoding pixel data with photometric interpretation of type 'CieLab'. + /// Each channel is represented with 16 bits. + /// + /// The type of pixel format. + internal class CieLab16TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly ColorProfileConverter colorProfileConverter; + private readonly Configuration configuration; + private readonly bool isBigEndian; + + // libtiff encodes 16-bit Lab as: + // L* : unsigned [0, 65535] mapping to [0, 100] + // a*, b* : signed [-32768, 32767], values are 256x the 1976 a*, b* values. + private const float Inv65535 = 1f / 65535f; + private const float Inv256 = 1f / 256f; + + public CieLab16TiffColor( + Configuration configuration, + DecoderOptions decoderOptions, + ImageFrameMetadata metadata, + MemoryAllocator allocator, + bool isBigEndian) + { + this.isBigEndian = isBigEndian; + this.configuration = configuration; + + if (decoderOptions.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) + { + ColorConversionOptions options = new() + { + SourceIccProfile = iccProfile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + MemoryAllocator = allocator + }; + + this.colorProfileConverter = new ColorProfileConverter(options); + } + else + { + ColorConversionOptions options = new() + { + MemoryAllocator = allocator + }; + + this.colorProfileConverter = new ColorProfileConverter(options); + } + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + + // Allocate temporary buffers to hold the LAB -> RGB conversion. + // This should be the maximum width of a row. + using IMemoryOwner rgbBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate(width); + using IMemoryOwner vectorBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate(width); + + Span rgbRow = rgbBuffer.Memory.Span; + Span vectorRow = vectorBuffer.Memory.Span; + + // Reuse the rgbRow span for lab data since both are 3-float structs, avoiding an extra allocation. + Span cieLabRow = MemoryMarshal.Cast(rgbRow); + + if (this.isBigEndian) + { + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + for (int x = 0; x < pixelRow.Length; x++) + { + ushort lRaw = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2)); + offset += 2; + short aRaw = unchecked((short)TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2))); + offset += 2; + short bRaw = unchecked((short)TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2))); + offset += 2; + + float l = lRaw * 100f * Inv65535; + float a = aRaw * Inv256; + float b = bRaw * Inv256; + + cieLabRow[x] = new CieLab(l, a, b); + } + + // Convert CIE Lab -> Rgb -> Vector4 -> TPixel + this.colorProfileConverter.Convert(cieLabRow, rgbRow); + Rgb.ToScaledVector4(rgbRow, vectorRow); + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale); + } + + return; + } + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + for (int x = 0; x < pixelRow.Length; x++) + { + ushort lRaw = TiffUtilities.ConvertToUShortLittleEndian(data.Slice(offset, 2)); + offset += 2; + short aRaw = unchecked((short)TiffUtilities.ConvertToUShortLittleEndian(data.Slice(offset, 2))); + offset += 2; + short bRaw = unchecked((short)TiffUtilities.ConvertToUShortLittleEndian(data.Slice(offset, 2))); + offset += 2; + + float l = lRaw * 100f * Inv65535; + float a = aRaw * Inv256; + float b = bRaw * Inv256; + + cieLabRow[x] = new CieLab(l, a, b); + } + + // Convert CIE Lab -> Rgb -> Vector4 -> TPixel + this.colorProfileConverter.Convert(cieLabRow, rgbRow); + Rgb.ToScaledVector4(rgbRow, vectorRow); + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale); + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8PlanarTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8PlanarTiffColor{TPixel}.cs new file mode 100644 index 0000000..78cfca7 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8PlanarTiffColor{TPixel}.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements decoding pixel data with photometric interpretation of type 'CieLab' with the planar configuration. + /// + /// The type of pixel format. + internal class CieLab8PlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private static readonly ColorProfileConverter ColorProfileConverter = new(); + + private const float Inv255 = 1.0f / 255.0f; + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + Span b = data[2].GetSpan(); + Span a = data[1].GetSpan(); + Span l = data[0].GetSpan(); + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + CieLab lab = new((l[offset] & 0xFF) * 100f * Inv255, (sbyte)a[offset], (sbyte)b[offset]); + Rgb rgb = ColorProfileConverter.Convert(in lab); + pixelRow[x] = TPixel.FromScaledVector4(new Vector4(rgb.R, rgb.G, rgb.B, 1.0f)); + + offset++; + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8TiffColor{TPixel}.cs new file mode 100644 index 0000000..bf4eaab --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8TiffColor{TPixel}.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements decoding pixel data with photometric interpretation of type 'CieLab'. + /// Each channel is represented with 8 bits. + /// + /// The type of pixel format. + internal class CieLab8TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private static readonly ColorProfileConverter ColorProfileConverter = new(); + private const float Inv255 = 1f / 255f; + + /// + /// Initializes a new instance of the class. + /// + public CieLab8TiffColor() + { + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + for (int x = 0; x < pixelRow.Length; x++) + { + float l = (data[offset] & 0xFF) * 100f * Inv255; + CieLab lab = new(l, (sbyte)data[offset + 1], (sbyte)data[offset + 2]); + Rgb rgb = ColorProfileConverter.Convert(in lab); + pixelRow[x] = TPixel.FromScaledVector4(new Vector4(rgb.R, rgb.G, rgb.B, 1f)); + + offset += 3; + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/CmykTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/CmykTiffColor{TPixel}.cs new file mode 100644 index 0000000..d4a7ad1 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/CmykTiffColor{TPixel}.cs @@ -0,0 +1,106 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + internal class CmykTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly ColorProfileConverter colorProfileConverter; + private readonly Configuration configuration; + private const float Inv255 = 1f / 255f; + + private readonly TiffDecoderCompressionType compression; + + public CmykTiffColor( + TiffDecoderCompressionType compression, + Configuration configuration, + DecoderOptions decoderOptions, + ImageFrameMetadata metadata, + MemoryAllocator allocator) + { + this.compression = compression; + this.configuration = configuration; + + if (decoderOptions.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) + { + ColorConversionOptions options = new() + { + SourceIccProfile = iccProfile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + MemoryAllocator = allocator + }; + + this.colorProfileConverter = new ColorProfileConverter(options); + } + else + { + ColorConversionOptions options = new() + { + MemoryAllocator = allocator + }; + + this.colorProfileConverter = new ColorProfileConverter(options); + } + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + if (this.compression == TiffDecoderCompressionType.Jpeg) + { + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + pixelRow[x] = TPixel.FromVector4(new Vector4(data[offset] * Inv255, data[offset + 1] * Inv255, data[offset + 2] * Inv255, 1.0f)); + + offset += 3; + } + } + + return; + } + + // Allocate temporary buffers to hold the CMYK -> RGB conversion. + // This should be the maximum width of a row. + using IMemoryOwner rgbBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate(width); + using IMemoryOwner vectorBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate(width); + + Span rgbRow = rgbBuffer.Memory.Span; + Span vectorRow = vectorBuffer.Memory.Span; + + // Reuse the Vector4 buffer as CMYK storage since both are 4-float structs, avoiding an extra allocation. + Span cmykRow = MemoryMarshal.Cast(vectorRow); + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + // Collect CMYK pixels. + // ByteToNormalizedFloat efficiently converts packed 4-byte component data + // to normalized 0-1 floats using SIMD. + SimdUtils.ByteToNormalizedFloat(data.Slice(offset, width * 4), MemoryMarshal.Cast(cmykRow)); + offset += width * 4; + + // Convert CMYK -> RGB -> Vector4 -> TPixel + this.colorProfileConverter.Convert(cmykRow, rgbRow); + Rgb.ToScaledVector4(rgbRow, vectorRow); + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale); + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs new file mode 100644 index 0000000..8106c69 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs @@ -0,0 +1,179 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'PaletteTiffColor' photometric interpretation (for all bit depths). + /// + /// The type of pixel format. + internal class PaletteTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly ushort bitsPerSample0; + private readonly ushort bitsPerSample1; + private readonly TiffExtraSampleType? extraSamplesType; + + private readonly Vector4[] vectorPallete; + private readonly TPixel[] pixelPalette; + + private readonly float alphaScale; + private readonly bool hasAlpha; + private Color[]? paletteColors; + + private const float InvMax = 1f / 65535f; + + /// + /// Initializes a new instance of the class. + /// + /// The number of bits per sample for each pixel. + /// The RGB color lookup table to use for decoding the image. + /// The type of extra samples. + public PaletteTiffColor(TiffBitsPerSample bitsPerSample, ushort[] colorMap, TiffExtraSampleType? extraSamplesType) + { + this.bitsPerSample0 = bitsPerSample.Channel0; + this.bitsPerSample1 = bitsPerSample.Channel1; + this.extraSamplesType = extraSamplesType; + + int colorCount = 1 << this.bitsPerSample0; + + // TIFF PaletteColor uses ColorMap (tag 320 / 0x0140) which is RGB-only (no alpha). + this.vectorPallete = GenerateVectorPalette(colorMap, colorCount); + + // ExtraSamples (tag 338 / 0x0152) describes extra per-pixel samples stored in the image data stream. + // For PaletteColor, any alpha is per pixel (stored alongside the index), not per palette entry. + this.hasAlpha = + this.bitsPerSample1 > 0 + && this.extraSamplesType.HasValue + && this.extraSamplesType != TiffExtraSampleType.UnspecifiedData; + + if (this.hasAlpha) + { + ulong alphaMax = (1UL << this.bitsPerSample1) - 1; + this.alphaScale = alphaMax > 0 ? 1f / alphaMax : 1f; + this.pixelPalette = []; + } + else + { + // Pre-generate pixel palette for non-alpha case for performance. + this.pixelPalette = GeneratePixelPalette(colorMap, colorCount); + } + } + + public Color[] PaletteColors => this.paletteColors ??= GenerateColorPalette(this.vectorPallete); + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + BitReader bitReader = new(data); + + if (this.hasAlpha) + { + Color[] colors = this.paletteColors ??= GenerateColorPalette(this.vectorPallete); + + // NOTE: ExtraSamples may report "AssociatedAlphaData". For PaletteColor, the stored color sample is the + // palette index, not per-pixel RGB components, so the premultiplication concept is not representable + // in the encoded stream. We therefore treat the alpha sample as a per-pixel alpha value applied after + // palette expansion. + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + int index = bitReader.ReadBits(this.bitsPerSample0); + float alpha = bitReader.ReadBits(this.bitsPerSample1) * this.alphaScale; + + // Defensive guard against malformed streams. + if ((uint)index >= (uint)this.vectorPallete.Length) + { + index = 0; + } + + Vector4 color = this.vectorPallete[index]; + color.W = alpha; + + pixelRow[x] = TPixel.FromScaledVector4(color); + + // Best-effort palette update for downstream conversions. + // This is intentionally "last writer wins" with no per-pixel branch. + // Performance is not an issue here since the constructor performs no actual transformations. + colors[index] = Color.FromScaledVector(color); + } + + bitReader.NextRow(); + } + + return; + } + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + int index = bitReader.ReadBits(this.bitsPerSample0); + + // Defensive guard against malformed streams. + if ((uint)index >= (uint)this.pixelPalette.Length) + { + index = 0; + } + + pixelRow[x] = this.pixelPalette[index]; + } + + bitReader.NextRow(); + } + } + + private static Vector4[] GenerateVectorPalette(ushort[] colorMap, int colorCount) + { + Vector4[] palette = new Vector4[colorCount]; + + const int rOffset = 0; + int gOffset = colorCount; + int bOffset = colorCount * 2; + + for (int i = 0; i < palette.Length; i++) + { + float r = colorMap[rOffset + i] * InvMax; + float g = colorMap[gOffset + i] * InvMax; + float b = colorMap[bOffset + i] * InvMax; + palette[i] = new Vector4(r, g, b, 1f); + } + + return palette; + } + + private static TPixel[] GeneratePixelPalette(ushort[] colorMap, int colorCount) + { + TPixel[] palette = new TPixel[colorCount]; + + const int rOffset = 0; + int gOffset = colorCount; + int bOffset = colorCount * 2; + + for (int i = 0; i < palette.Length; i++) + { + float r = colorMap[rOffset + i] * InvMax; + float g = colorMap[gOffset + i] * InvMax; + float b = colorMap[bOffset + i] * InvMax; + palette[i] = TPixel.FromScaledVector4(new Vector4(r, g, b, 1f)); + } + + return palette; + } + + private static Color[] GenerateColorPalette(Vector4[] palette) + { + Color[] colors = new Color[palette.Length]; + Color.FromScaledVector(palette, colors); + return colors; + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs new file mode 100644 index 0000000..26cbec1 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with 16 bits for each channel. + /// + /// The type of pixel format. + internal class Rgb161616TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + private readonly Configuration configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgb161616TiffColor(Configuration configuration, bool isBigEndian) + { + this.configuration = configuration; + this.isBigEndian = isBigEndian; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ushort r = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2)); + offset += 2; + ushort g = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2)); + offset += 2; + ushort b = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2)); + offset += 2; + + pixelRow[x] = TPixel.FromRgb48(new Rgb48(r, g, b)); + } + } + else + { + int byteCount = pixelRow.Length * 6; + PixelOperations.Instance.FromRgb48Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); + + offset += byteCount; + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs new file mode 100644 index 0000000..5475dd1 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with 'Planar' layout for each color channel with 16 bit. + /// + /// The type of pixel format. + internal class Rgb16PlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgb16PlanarTiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + Span redData = data[0].GetSpan(); + Span greenData = data[1].GetSpan(); + Span blueData = data[2].GetSpan(); + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ushort r = TiffUtilities.ConvertToUShortBigEndian(redData.Slice(offset, 2)); + ushort g = TiffUtilities.ConvertToUShortBigEndian(greenData.Slice(offset, 2)); + ushort b = TiffUtilities.ConvertToUShortBigEndian(blueData.Slice(offset, 2)); + + offset += 2; + + pixelRow[x] = TPixel.FromRgb48(new Rgb48(r, g, b)); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + ushort r = TiffUtilities.ConvertToUShortLittleEndian(redData.Slice(offset, 2)); + ushort g = TiffUtilities.ConvertToUShortLittleEndian(greenData.Slice(offset, 2)); + ushort b = TiffUtilities.ConvertToUShortLittleEndian(blueData.Slice(offset, 2)); + + offset += 2; + + pixelRow[x] = TPixel.FromRgb48(new Rgb48(r, g, b)); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs new file mode 100644 index 0000000..f55b50a --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with 24 bits for each channel. + /// + /// The type of pixel format. + internal class Rgb242424TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgb242424TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + Span buffer = stackalloc byte[4]; + int bufferStartIdx = this.isBigEndian ? 1 : 0; + + Span bufferSpan = buffer[bufferStartIdx..]; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(bufferSpan); + uint r = TiffUtilities.ConvertToUIntBigEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + uint g = TiffUtilities.ConvertToUIntBigEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + uint b = TiffUtilities.ConvertToUIntBigEndian(buffer); + offset += 3; + + pixelRow[x] = TiffUtilities.ColorScaleTo24Bit(r, g, b); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(bufferSpan); + uint r = TiffUtilities.ConvertToUIntLittleEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + uint g = TiffUtilities.ConvertToUIntLittleEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + uint b = TiffUtilities.ConvertToUIntLittleEndian(buffer); + offset += 3; + + pixelRow[x] = TiffUtilities.ColorScaleTo24Bit(r, g, b); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs new file mode 100644 index 0000000..3bdc1d6 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with 'Planar' layout for each color channel with 24 bit. + /// + /// The type of pixel format. + internal class Rgb24PlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgb24PlanarTiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + Span buffer = stackalloc byte[4]; + int bufferStartIdx = this.isBigEndian ? 1 : 0; + + Span redData = data[0].GetSpan(); + Span greenData = data[1].GetSpan(); + Span blueData = data[2].GetSpan(); + Span bufferSpan = buffer[bufferStartIdx..]; + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + redData.Slice(offset, 3).CopyTo(bufferSpan); + uint r = TiffUtilities.ConvertToUIntBigEndian(buffer); + greenData.Slice(offset, 3).CopyTo(bufferSpan); + uint g = TiffUtilities.ConvertToUIntBigEndian(buffer); + blueData.Slice(offset, 3).CopyTo(bufferSpan); + uint b = TiffUtilities.ConvertToUIntBigEndian(buffer); + + offset += 3; + + pixelRow[x] = TiffUtilities.ColorScaleTo24Bit(r, g, b); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + redData.Slice(offset, 3).CopyTo(bufferSpan); + uint r = TiffUtilities.ConvertToUIntLittleEndian(buffer); + greenData.Slice(offset, 3).CopyTo(bufferSpan); + uint g = TiffUtilities.ConvertToUIntLittleEndian(buffer); + blueData.Slice(offset, 3).CopyTo(bufferSpan); + uint b = TiffUtilities.ConvertToUIntLittleEndian(buffer); + + offset += 3; + + pixelRow[x] = TiffUtilities.ColorScaleTo24Bit(r, g, b); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs new file mode 100644 index 0000000..9278a85 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with 32 bits for each channel. + /// + /// The type of pixel format. + internal class Rgb323232TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgb323232TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + uint r = TiffUtilities.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + uint g = TiffUtilities.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + uint b = TiffUtilities.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtilities.ColorScaleTo32Bit(r, g, b); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + uint r = TiffUtilities.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + uint g = TiffUtilities.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + uint b = TiffUtilities.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtilities.ColorScaleTo32Bit(r, g, b); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs new file mode 100644 index 0000000..b1bdfcf --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with 'Planar' layout for each color channel with 32 bit. + /// + /// The type of pixel format. + internal class Rgb32PlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgb32PlanarTiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + Span redData = data[0].GetSpan(); + Span greenData = data[1].GetSpan(); + Span blueData = data[2].GetSpan(); + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + uint r = TiffUtilities.ConvertToUIntBigEndian(redData.Slice(offset, 4)); + uint g = TiffUtilities.ConvertToUIntBigEndian(greenData.Slice(offset, 4)); + uint b = TiffUtilities.ConvertToUIntBigEndian(blueData.Slice(offset, 4)); + + offset += 4; + + pixelRow[x] = TiffUtilities.ColorScaleTo32Bit(r, g, b); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + uint r = TiffUtilities.ConvertToUIntLittleEndian(redData.Slice(offset, 4)); + uint g = TiffUtilities.ConvertToUIntLittleEndian(greenData.Slice(offset, 4)); + uint b = TiffUtilities.ConvertToUIntLittleEndian(blueData.Slice(offset, 4)); + + offset += 4; + + pixelRow[x] = TiffUtilities.ColorScaleTo32Bit(r, g, b); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb444TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb444TiffColor{TPixel}.cs new file mode 100644 index 0000000..fabb47c --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb444TiffColor{TPixel}.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation for 4 bits per color channel images. + /// + /// The type of pixel format. + internal class Rgb444TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y); + + for (int x = left; x < left + width; x += 2) + { + byte r = (byte)((data[offset] & 0xF0) >> 4); + byte g = (byte)(data[offset] & 0xF); + offset++; + byte b = (byte)((data[offset] & 0xF0) >> 4); + + Bgra4444 bgra = new() { PackedValue = ToBgraPackedValue(b, g, r) }; + pixelRow[x] = TPixel.FromScaledVector4(bgra.ToScaledVector4()); + if (x + 1 >= pixelRow.Length) + { + offset++; + break; + } + + r = (byte)(data[offset] & 0xF); + offset++; + g = (byte)((data[offset] & 0xF0) >> 4); + b = (byte)(data[offset] & 0xF); + offset++; + + bgra.PackedValue = ToBgraPackedValue(b, g, r); + pixelRow[x + 1] = TPixel.FromScaledVector4(bgra.ToScaledVector4()); + } + } + } + + private static ushort ToBgraPackedValue(byte b, byte g, byte r) => (ushort)(b | (g << 4) | (r << 8) | (0xF << 12)); + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs new file mode 100644 index 0000000..975c2ef --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation (optimized for 8-bit full color images). + /// + internal class Rgb888TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly Configuration configuration; + + public Rgb888TiffColor(Configuration configuration) => this.configuration = configuration; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + int byteCount = pixelRow.Length * 3; + PixelOperations.Instance.FromRgb24Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); + + offset += byteCount; + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs new file mode 100644 index 0000000..8abfcc0 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with 32 bits for each channel. + /// + /// The type of pixel format. + internal class RgbFloat323232TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public RgbFloat323232TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + Span buffer = stackalloc byte[4]; + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 4).CopyTo(buffer); + buffer.Reverse(); + float r = BitConverter.ToSingle(buffer); + offset += 4; + + data.Slice(offset, 4).CopyTo(buffer); + buffer.Reverse(); + float g = BitConverter.ToSingle(buffer); + offset += 4; + + data.Slice(offset, 4).CopyTo(buffer); + buffer.Reverse(); + float b = BitConverter.ToSingle(buffer); + offset += 4; + + pixelRow[x] = TPixel.FromScaledVector4(new Vector4(r, g, b, 1f)); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + float r = BitConverter.ToSingle(data.Slice(offset, 4)); + offset += 4; + + float g = BitConverter.ToSingle(data.Slice(offset, 4)); + offset += 4; + + float b = BitConverter.ToSingle(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TPixel.FromScaledVector4(new Vector4(r, g, b, 1f)); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs new file mode 100644 index 0000000..6523cab --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with 'Planar' layout (for all bit depths). + /// + /// The type of pixel format. + internal class RgbPlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly float rFactor; + + private readonly float gFactor; + + private readonly float bFactor; + + private readonly ushort bitsPerSampleR; + + private readonly ushort bitsPerSampleG; + + private readonly ushort bitsPerSampleB; + + public RgbPlanarTiffColor(TiffBitsPerSample bitsPerSample) + { + this.bitsPerSampleR = bitsPerSample.Channel0; + this.bitsPerSampleG = bitsPerSample.Channel1; + this.bitsPerSampleB = bitsPerSample.Channel2; + + this.rFactor = (1 << this.bitsPerSampleR) - 1.0f; + this.gFactor = (1 << this.bitsPerSampleG) - 1.0f; + this.bFactor = (1 << this.bitsPerSampleB) - 1.0f; + } + + /// + /// Decodes pixel data using the current photometric interpretation. + /// + /// The buffers to read image data from. + /// The image buffer to write pixels to. + /// The x-coordinate of the left-hand side of the image block. + /// The y-coordinate of the top of the image block. + /// The width of the image block. + /// The height of the image block. + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + BitReader rBitReader = new(data[0].GetSpan()); + BitReader gBitReader = new(data[1].GetSpan()); + BitReader bBitReader = new(data[2].GetSpan()); + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + float r = rBitReader.ReadBits(this.bitsPerSampleR) / this.rFactor; + float g = gBitReader.ReadBits(this.bitsPerSampleG) / this.gFactor; + float b = bBitReader.ReadBits(this.bitsPerSampleB) / this.bFactor; + + pixelRow[x] = TPixel.FromScaledVector4(new Vector4(r, g, b, 1f)); + } + + rBitReader.NextRow(); + gBitReader.NextRow(); + bBitReader.NextRow(); + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs new file mode 100644 index 0000000..34c1f87 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation (for all bit depths). + /// + /// The type of pixel format. + internal class RgbTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly float rFactor; + + private readonly float gFactor; + + private readonly float bFactor; + + private readonly ushort bitsPerSampleR; + + private readonly ushort bitsPerSampleG; + + private readonly ushort bitsPerSampleB; + + public RgbTiffColor(TiffBitsPerSample bitsPerSample) + { + this.bitsPerSampleR = bitsPerSample.Channel0; + this.bitsPerSampleG = bitsPerSample.Channel1; + this.bitsPerSampleB = bitsPerSample.Channel2; + + this.rFactor = (1 << this.bitsPerSampleR) - 1.0f; + this.gFactor = (1 << this.bitsPerSampleG) - 1.0f; + this.bFactor = (1 << this.bitsPerSampleB) - 1.0f; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + BitReader bitReader = new(data); + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + float r = bitReader.ReadBits(this.bitsPerSampleR) / this.rFactor; + float g = bitReader.ReadBits(this.bitsPerSampleG) / this.gFactor; + float b = bitReader.ReadBits(this.bitsPerSampleB) / this.bFactor; + + pixelRow[x] = TPixel.FromScaledVector4(new Vector4(r, g, b, 1f)); + } + + bitReader.NextRow(); + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16161616TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16161616TiffColor{TPixel}.cs new file mode 100644 index 0000000..091dfd0 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16161616TiffColor{TPixel}.cs @@ -0,0 +1,116 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#nullable disable + +using System; +using System.Buffers; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and with 16 bits for each channel. + /// + /// The type of pixel format. + internal class Rgba16161616TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + private readonly Configuration configuration; + + private readonly MemoryAllocator memoryAllocator; + + private readonly TiffExtraSampleType? extraSamplesType; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The memory allocator. + /// The type of the extra samples. + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgba16161616TiffColor(Configuration configuration, MemoryAllocator memoryAllocator, TiffExtraSampleType? extraSamplesType, bool isBigEndian) + { + this.configuration = configuration; + this.isBigEndian = isBigEndian; + this.memoryAllocator = memoryAllocator; + this.extraSamplesType = extraSamplesType; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + bool hasAssociatedAlpha = this.extraSamplesType.HasValue && this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData; + int offset = 0; + + using IMemoryOwner vectors = hasAssociatedAlpha ? this.memoryAllocator.Allocate(width) : null; + Span vectorsSpan = hasAssociatedAlpha ? vectors.GetSpan() : []; + + if (this.isBigEndian) + { + if (hasAssociatedAlpha) + { + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + for (int x = 0; x < pixelRow.Length; x++) + { + ushort r = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2)); + ushort g = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset + 2, 2)); + ushort b = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset + 4, 2)); + ushort a = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset + 6, 2)); + offset += 8; + + pixelRow[x] = TiffUtilities.ColorFromRgba64Premultiplied(r, g, b, a); + } + } + } + else + { + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + for (int x = 0; x < pixelRow.Length; x++) + { + ushort r = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2)); + ushort g = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset + 2, 2)); + ushort b = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset + 4, 2)); + ushort a = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset + 6, 2)); + offset += 8; + + pixelRow[x] = TPixel.FromRgba64(new Rgba64(r, g, b, a)); + } + } + } + } + else + { + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + int byteCount = pixelRow.Length * 8; + + PixelOperations.Instance.FromRgba64Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); + + if (hasAssociatedAlpha) + { + PixelOperations.Instance.ToVector4(this.configuration, pixelRow, vectorsSpan); + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorsSpan, pixelRow, PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale); + } + + offset += byteCount; + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16PlanarTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16PlanarTiffColor{TPixel}.cs new file mode 100644 index 0000000..85c7a55 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16PlanarTiffColor{TPixel}.cs @@ -0,0 +1,81 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and with 'Planar' layout for each color channel with 16 bit. + /// + /// The type of pixel format. + internal class Rgba16PlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + private readonly TiffExtraSampleType? extraSamplesType; + + /// + /// Initializes a new instance of the class. + /// + /// The extra samples type. + /// If set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgba16PlanarTiffColor(TiffExtraSampleType? extraSamplesType, bool isBigEndian) + { + this.extraSamplesType = extraSamplesType; + this.isBigEndian = isBigEndian; + } + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + Span redData = data[0].GetSpan(); + Span greenData = data[1].GetSpan(); + Span blueData = data[2].GetSpan(); + Span alphaData = data[3].GetSpan(); + + bool hasAssociatedAlpha = this.extraSamplesType.HasValue && this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData; + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ushort r = TiffUtilities.ConvertToUShortBigEndian(redData.Slice(offset, 2)); + ushort g = TiffUtilities.ConvertToUShortBigEndian(greenData.Slice(offset, 2)); + ushort b = TiffUtilities.ConvertToUShortBigEndian(blueData.Slice(offset, 2)); + ushort a = TiffUtilities.ConvertToUShortBigEndian(alphaData.Slice(offset, 2)); + + offset += 2; + + pixelRow[x] = hasAssociatedAlpha + ? TiffUtilities.ColorFromRgba64Premultiplied(r, g, b, a) + : TPixel.FromRgba64(new Rgba64(r, g, b, a)); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + ushort r = TiffUtilities.ConvertToUShortLittleEndian(redData.Slice(offset, 2)); + ushort g = TiffUtilities.ConvertToUShortLittleEndian(greenData.Slice(offset, 2)); + ushort b = TiffUtilities.ConvertToUShortLittleEndian(blueData.Slice(offset, 2)); + ushort a = TiffUtilities.ConvertToUShortLittleEndian(alphaData.Slice(offset, 2)); + + offset += 2; + + pixelRow[x] = hasAssociatedAlpha + ? TiffUtilities.ColorFromRgba64Premultiplied(r, g, b, a) + : TPixel.FromRgba64(new Rgba64(r, g, b, a)); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba24242424TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba24242424TiffColor{TPixel}.cs new file mode 100644 index 0000000..742436c --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba24242424TiffColor{TPixel}.cs @@ -0,0 +1,99 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and with 24 bits for each channel. + /// + /// The type of pixel format. + internal class Rgba24242424TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + private readonly TiffExtraSampleType? extraSamplesType; + + /// + /// Initializes a new instance of the class. + /// + /// The type of the extra samples. + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgba24242424TiffColor(TiffExtraSampleType? extraSamplesType, bool isBigEndian) + { + this.extraSamplesType = extraSamplesType; + this.isBigEndian = isBigEndian; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + bool hasAssociatedAlpha = this.extraSamplesType.HasValue && this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData; + int offset = 0; + + Span buffer = stackalloc byte[4]; + int bufferStartIdx = this.isBigEndian ? 1 : 0; + + Span bufferSpan = buffer[bufferStartIdx..]; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(bufferSpan); + uint r = TiffUtilities.ConvertToUIntBigEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + uint g = TiffUtilities.ConvertToUIntBigEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + uint b = TiffUtilities.ConvertToUIntBigEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + uint a = TiffUtilities.ConvertToUIntBigEndian(buffer); + offset += 3; + + pixelRow[x] = hasAssociatedAlpha + ? TiffUtilities.ColorScaleTo24BitPremultiplied(r, g, b, a) + : TiffUtilities.ColorScaleTo24Bit(r, g, b, a); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(bufferSpan); + uint r = TiffUtilities.ConvertToUIntLittleEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + uint g = TiffUtilities.ConvertToUIntLittleEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + uint b = TiffUtilities.ConvertToUIntLittleEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + uint a = TiffUtilities.ConvertToUIntLittleEndian(buffer); + offset += 3; + + pixelRow[x] = hasAssociatedAlpha + ? TiffUtilities.ColorScaleTo24BitPremultiplied(r, g, b, a) + : TiffUtilities.ColorScaleTo24Bit(r, g, b, a); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba24PlanarTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba24PlanarTiffColor{TPixel}.cs new file mode 100644 index 0000000..9f8c1ee --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba24PlanarTiffColor{TPixel}.cs @@ -0,0 +1,93 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and with 'Planar' layout for each color channel with 24 bit. + /// + /// The type of pixel format. + internal class Rgba24PlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + private readonly TiffExtraSampleType? extraSamplesType; + + /// + /// Initializes a new instance of the class. + /// + /// The extra samples type. + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgba24PlanarTiffColor(TiffExtraSampleType? extraSamplesType, bool isBigEndian) + { + this.extraSamplesType = extraSamplesType; + this.isBigEndian = isBigEndian; + } + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + Span buffer = stackalloc byte[4]; + int bufferStartIdx = this.isBigEndian ? 1 : 0; + + Span redData = data[0].GetSpan(); + Span greenData = data[1].GetSpan(); + Span blueData = data[2].GetSpan(); + Span alphaData = data[3].GetSpan(); + Span bufferSpan = buffer[bufferStartIdx..]; + + bool hasAssociatedAlpha = this.extraSamplesType.HasValue && this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData; + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + redData.Slice(offset, 3).CopyTo(bufferSpan); + uint r = TiffUtilities.ConvertToUIntBigEndian(buffer); + greenData.Slice(offset, 3).CopyTo(bufferSpan); + uint g = TiffUtilities.ConvertToUIntBigEndian(buffer); + blueData.Slice(offset, 3).CopyTo(bufferSpan); + uint b = TiffUtilities.ConvertToUIntBigEndian(buffer); + alphaData.Slice(offset, 3).CopyTo(bufferSpan); + uint a = TiffUtilities.ConvertToUIntBigEndian(buffer); + + offset += 3; + + pixelRow[x] = hasAssociatedAlpha + ? TiffUtilities.ColorScaleTo24BitPremultiplied(r, g, b, a) + : TiffUtilities.ColorScaleTo24Bit(r, g, b, a); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + redData.Slice(offset, 3).CopyTo(bufferSpan); + uint r = TiffUtilities.ConvertToUIntLittleEndian(buffer); + greenData.Slice(offset, 3).CopyTo(bufferSpan); + uint g = TiffUtilities.ConvertToUIntLittleEndian(buffer); + blueData.Slice(offset, 3).CopyTo(bufferSpan); + uint b = TiffUtilities.ConvertToUIntLittleEndian(buffer); + alphaData.Slice(offset, 3).CopyTo(bufferSpan); + uint a = TiffUtilities.ConvertToUIntLittleEndian(buffer); + + offset += 3; + + pixelRow[x] = hasAssociatedAlpha + ? TiffUtilities.ColorScaleTo24BitPremultiplied(r, g, b, a) + : TiffUtilities.ColorScaleTo24Bit(r, g, b, a); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32323232TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32323232TiffColor{TPixel}.cs new file mode 100644 index 0000000..6ee0c4a --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32323232TiffColor{TPixel}.cs @@ -0,0 +1,87 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and with 32 bits for each channel. + /// + /// The type of pixel format. + internal class Rgba32323232TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + private readonly TiffExtraSampleType? extraSamplesType; + + /// + /// Initializes a new instance of the class. + /// + /// The type of the extra samples. + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgba32323232TiffColor(TiffExtraSampleType? extraSamplesType, bool isBigEndian) + { + this.extraSamplesType = extraSamplesType; + this.isBigEndian = isBigEndian; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + bool hasAssociatedAlpha = this.extraSamplesType.HasValue && this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData; + int offset = 0; + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + uint r = TiffUtilities.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + uint g = TiffUtilities.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + uint b = TiffUtilities.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + uint a = TiffUtilities.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = hasAssociatedAlpha + ? TiffUtilities.ColorScaleTo32BitPremultiplied(r, g, b, a) + : TiffUtilities.ColorScaleTo32Bit(r, g, b, a); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + uint r = TiffUtilities.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + uint g = TiffUtilities.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + uint b = TiffUtilities.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + uint a = TiffUtilities.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = hasAssociatedAlpha + ? TiffUtilities.ColorScaleTo32BitPremultiplied(r, g, b, a) + : TiffUtilities.ColorScaleTo32Bit(r, g, b, a); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32PlanarTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32PlanarTiffColor{TPixel}.cs new file mode 100644 index 0000000..1095533 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32PlanarTiffColor{TPixel}.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and a 'Planar' layout for each color channel with 32 bit. + /// + /// The type of pixel format. + internal class Rgba32PlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + private readonly TiffExtraSampleType? extraSamplesType; + + /// + /// Initializes a new instance of the class. + /// + /// The extra samples type. + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgba32PlanarTiffColor(TiffExtraSampleType? extraSamplesType, bool isBigEndian) + { + this.extraSamplesType = extraSamplesType; + this.isBigEndian = isBigEndian; + } + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + Span redData = data[0].GetSpan(); + Span greenData = data[1].GetSpan(); + Span blueData = data[2].GetSpan(); + Span alphaData = data[3].GetSpan(); + + bool hasAssociatedAlpha = this.extraSamplesType.HasValue && this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData; + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + uint r = TiffUtilities.ConvertToUIntBigEndian(redData.Slice(offset, 4)); + uint g = TiffUtilities.ConvertToUIntBigEndian(greenData.Slice(offset, 4)); + uint b = TiffUtilities.ConvertToUIntBigEndian(blueData.Slice(offset, 4)); + uint a = TiffUtilities.ConvertToUIntBigEndian(alphaData.Slice(offset, 4)); + + offset += 4; + + pixelRow[x] = hasAssociatedAlpha + ? TiffUtilities.ColorScaleTo32BitPremultiplied(r, g, b, a) + : TiffUtilities.ColorScaleTo32Bit(r, g, b, a); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + uint r = TiffUtilities.ConvertToUIntLittleEndian(redData.Slice(offset, 4)); + uint g = TiffUtilities.ConvertToUIntLittleEndian(greenData.Slice(offset, 4)); + uint b = TiffUtilities.ConvertToUIntLittleEndian(blueData.Slice(offset, 4)); + uint a = TiffUtilities.ConvertToUIntLittleEndian(alphaData.Slice(offset, 4)); + + offset += 4; + + pixelRow[x] = hasAssociatedAlpha + ? TiffUtilities.ColorScaleTo32BitPremultiplied(r, g, b, a) + : TiffUtilities.ColorScaleTo32Bit(r, g, b, a); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba8888TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba8888TiffColor{TPixel}.cs new file mode 100644 index 0000000..85a216d --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba8888TiffColor{TPixel}.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using System.Numerics; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and 8 bits per channel. + /// + /// The type of pixel format. + internal class Rgba8888TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly Configuration configuration; + + private readonly MemoryAllocator memoryAllocator; + + private readonly TiffExtraSampleType? extraSamplesType; + + public Rgba8888TiffColor(Configuration configuration, MemoryAllocator memoryAllocator, TiffExtraSampleType? extraSamplesType) + { + this.configuration = configuration; + this.memoryAllocator = memoryAllocator; + this.extraSamplesType = extraSamplesType; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + bool hasAssociatedAlpha = this.extraSamplesType.HasValue && this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData; + + using IMemoryOwner vectors = hasAssociatedAlpha ? this.memoryAllocator.Allocate(width) : null; + Span vectorsSpan = hasAssociatedAlpha ? vectors.GetSpan() : []; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + int byteCount = pixelRow.Length * 4; + PixelOperations.Instance.FromRgba32Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); + + if (hasAssociatedAlpha) + { + PixelOperations.Instance.ToVector4(this.configuration, pixelRow, vectorsSpan); + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorsSpan, pixelRow, PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale); + } + + offset += byteCount; + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs new file mode 100644 index 0000000..0e1b3b5 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs @@ -0,0 +1,84 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and with 32 bits for each channel. + /// + /// The type of pixel format. + internal class RgbaFloat32323232TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public RgbaFloat32323232TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + Span buffer = stackalloc byte[4]; + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 4).CopyTo(buffer); + buffer.Reverse(); + float r = BitConverter.ToSingle(buffer); + offset += 4; + + data.Slice(offset, 4).CopyTo(buffer); + buffer.Reverse(); + float g = BitConverter.ToSingle(buffer); + offset += 4; + + data.Slice(offset, 4).CopyTo(buffer); + buffer.Reverse(); + float b = BitConverter.ToSingle(buffer); + offset += 4; + + data.Slice(offset, 4).CopyTo(buffer); + buffer.Reverse(); + float a = BitConverter.ToSingle(buffer); + offset += 4; + + pixelRow[x] = TPixel.FromScaledVector4(new Vector4(r, g, b, a)); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + float r = BitConverter.ToSingle(data.Slice(offset, 4)); + offset += 4; + + float g = BitConverter.ToSingle(data.Slice(offset, 4)); + offset += 4; + + float b = BitConverter.ToSingle(data.Slice(offset, 4)); + offset += 4; + + float a = BitConverter.ToSingle(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TPixel.FromScaledVector4(new Vector4(r, g, b, a)); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaPlanarTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaPlanarTiffColor{TPixel}.cs new file mode 100644 index 0000000..f00d0cf --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaPlanarTiffColor{TPixel}.cs @@ -0,0 +1,98 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and with 'Planar' layout (for all bit depths). + /// + /// The type of pixel format. + internal class RgbaPlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly float rFactor; + + private readonly float gFactor; + + private readonly float bFactor; + + private readonly float aFactor; + + private readonly ushort bitsPerSampleR; + + private readonly ushort bitsPerSampleG; + + private readonly ushort bitsPerSampleB; + + private readonly ushort bitsPerSampleA; + + private readonly TiffExtraSampleType? extraSampleType; + + public RgbaPlanarTiffColor(TiffExtraSampleType? extraSampleType, TiffBitsPerSample bitsPerSample) + { + this.bitsPerSampleR = bitsPerSample.Channel0; + this.bitsPerSampleG = bitsPerSample.Channel1; + this.bitsPerSampleB = bitsPerSample.Channel2; + this.bitsPerSampleA = bitsPerSample.Channel3; + + this.rFactor = (1 << this.bitsPerSampleR) - 1.0f; + this.gFactor = (1 << this.bitsPerSampleG) - 1.0f; + this.bFactor = (1 << this.bitsPerSampleB) - 1.0f; + this.aFactor = (1 << this.bitsPerSampleA) - 1.0f; + + this.extraSampleType = extraSampleType; + } + + /// + /// Decodes pixel data using the current photometric interpretation. + /// + /// The buffers to read image data from. + /// The image buffer to write pixels to. + /// The x-coordinate of the left-hand side of the image block. + /// The y-coordinate of the top of the image block. + /// The width of the image block. + /// The height of the image block. + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + bool hasAssociatedAlpha = this.extraSampleType.HasValue && this.extraSampleType == TiffExtraSampleType.AssociatedAlphaData; + + BitReader rBitReader = new(data[0].GetSpan()); + BitReader gBitReader = new(data[1].GetSpan()); + BitReader bBitReader = new(data[2].GetSpan()); + BitReader aBitReader = new(data[3].GetSpan()); + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + float r = rBitReader.ReadBits(this.bitsPerSampleR) / this.rFactor; + float g = gBitReader.ReadBits(this.bitsPerSampleG) / this.gFactor; + float b = bBitReader.ReadBits(this.bitsPerSampleB) / this.bFactor; + float a = aBitReader.ReadBits(this.bitsPerSampleA) / this.aFactor; + + Vector4 vector = new(r, g, b, a); + if (hasAssociatedAlpha) + { + pixelRow[x] = TiffUtilities.UnPremultiply(ref vector); + } + else + { + pixelRow[x] = TPixel.FromScaledVector4(vector); + } + } + + rBitReader.NextRow(); + gBitReader.NextRow(); + bBitReader.NextRow(); + aBitReader.NextRow(); + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaTiffColor{TPixel}.cs new file mode 100644 index 0000000..a0da93f --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaTiffColor{TPixel}.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'RGB' photometric interpretation with alpha channel (for all bit depths). + /// + /// The type of pixel format. + internal class RgbaTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly float rFactor; + + private readonly float gFactor; + + private readonly float bFactor; + + private readonly float aFactor; + + private readonly ushort bitsPerSampleR; + + private readonly ushort bitsPerSampleG; + + private readonly ushort bitsPerSampleB; + + private readonly ushort bitsPerSampleA; + + private readonly TiffExtraSampleType? extraSamplesType; + + public RgbaTiffColor(TiffExtraSampleType? extraSampleType, TiffBitsPerSample bitsPerSample) + { + this.bitsPerSampleR = bitsPerSample.Channel0; + this.bitsPerSampleG = bitsPerSample.Channel1; + this.bitsPerSampleB = bitsPerSample.Channel2; + this.bitsPerSampleA = bitsPerSample.Channel3; + + this.rFactor = (1 << this.bitsPerSampleR) - 1.0f; + this.gFactor = (1 << this.bitsPerSampleG) - 1.0f; + this.bFactor = (1 << this.bitsPerSampleB) - 1.0f; + this.aFactor = (1 << this.bitsPerSampleA) - 1.0f; + + this.extraSamplesType = extraSampleType; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + BitReader bitReader = new(data); + + bool hasAssociatedAlpha = this.extraSamplesType.HasValue && this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData; + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + float r = bitReader.ReadBits(this.bitsPerSampleR) / this.rFactor; + float g = bitReader.ReadBits(this.bitsPerSampleG) / this.gFactor; + float b = bitReader.ReadBits(this.bitsPerSampleB) / this.bFactor; + float a = bitReader.ReadBits(this.bitsPerSampleB) / this.aFactor; + + Vector4 vector = new(r, g, b, a); + if (hasAssociatedAlpha) + { + pixelRow[x] = TiffUtilities.UnPremultiply(ref vector); + } + else + { + pixelRow[x] = TPixel.FromScaledVector4(vector); + } + } + + bitReader.NextRow(); + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBaseColorDecoder{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBaseColorDecoder{TPixel}.cs new file mode 100644 index 0000000..6dc16b0 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBaseColorDecoder{TPixel}.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// The base class for photometric interpretation decoders. + /// + /// The pixel format. + internal abstract class TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + /// + /// Decodes source raw pixel data using the current photometric interpretation. + /// + /// The buffer to read image data from. + /// The image buffer to write pixels to. + /// The x-coordinate of the left-hand side of the image block. + /// The y-coordinate of the top of the image block. + /// The width of the image block. + /// The height of the image block. + public abstract void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height); + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBasePlanarColorDecoder{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBasePlanarColorDecoder{TPixel}.cs new file mode 100644 index 0000000..68ade19 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBasePlanarColorDecoder{TPixel}.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// The base class for planar color decoders. + /// + /// The pixel format. + internal abstract class TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + /// + /// Decodes source raw pixel data using the current photometric interpretation. + /// + /// The buffers to read image data from. + /// The image buffer to write pixels to. + /// The x-coordinate of the left-hand side of the image block. + /// The y-coordinate of the top of the image block. + /// The width of the image block. + /// The height of the image block. + public abstract void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height); + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs new file mode 100644 index 0000000..93c0217 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -0,0 +1,497 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + internal static class TiffColorDecoderFactory + where TPixel : unmanaged, IPixel + { + public static TiffBaseColorDecoder Create( + ImageFrameMetadata metadata, + DecoderOptions options, + Configuration configuration, + MemoryAllocator memoryAllocator, + TiffColorType colorType, + TiffBitsPerSample bitsPerSample, + TiffExtraSampleType? extraSampleType, + ushort[] colorMap, + Rational[] referenceBlackAndWhite, + Rational[] ycbcrCoefficients, + ushort[] ycbcrSubSampling, + TiffDecoderCompressionType compression, + ByteOrder byteOrder) + { + switch (colorType) + { + case TiffColorType.WhiteIsZero: + DebugGuard.IsTrue(bitsPerSample.Channels == 1, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZeroTiffColor(bitsPerSample); + + case TiffColorType.WhiteIsZero1: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 1, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZero1TiffColor(); + + case TiffColorType.WhiteIsZero4: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 4, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZero4TiffColor(); + + case TiffColorType.WhiteIsZero8: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 8, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZero8TiffColor(); + + case TiffColorType.WhiteIsZero16: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 16, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZero16TiffColor(byteOrder == ByteOrder.BigEndian); + + case TiffColorType.WhiteIsZero24: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 24, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZero24TiffColor(byteOrder == ByteOrder.BigEndian); + + case TiffColorType.WhiteIsZero32: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 32, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZero32TiffColor(byteOrder == ByteOrder.BigEndian); + + case TiffColorType.WhiteIsZero32Float: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 32, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZero32FloatTiffColor(byteOrder == ByteOrder.BigEndian); + + case TiffColorType.BlackIsZero: + DebugGuard.IsTrue(bitsPerSample.Channels == 1, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZeroTiffColor(bitsPerSample); + + case TiffColorType.BlackIsZero1: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 1, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZero1TiffColor(); + + case TiffColorType.BlackIsZero4: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 4, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZero4TiffColor(); + + case TiffColorType.BlackIsZero8: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 8, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZero8TiffColor(configuration); + + case TiffColorType.BlackIsZero16: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 16, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZero16TiffColor(configuration, byteOrder == ByteOrder.BigEndian); + + case TiffColorType.BlackIsZero24: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 24, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZero24TiffColor(byteOrder == ByteOrder.BigEndian); + + case TiffColorType.BlackIsZero32: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 32, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZero32TiffColor(byteOrder == ByteOrder.BigEndian); + + case TiffColorType.BlackIsZero32Float: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 32, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZero32FloatTiffColor(byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Rgb: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbTiffColor(bitsPerSample); + + case TiffColorType.Rgb222: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 2 + && bitsPerSample.Channel1 == 2 + && bitsPerSample.Channel0 == 2, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbTiffColor(bitsPerSample); + + case TiffColorType.Rgba2222: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 2 + && bitsPerSample.Channel2 == 2 + && bitsPerSample.Channel1 == 2 + && bitsPerSample.Channel0 == 2, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaTiffColor(extraSampleType, bitsPerSample); + + case TiffColorType.Rgb333: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 3 + && bitsPerSample.Channel1 == 3 + && bitsPerSample.Channel0 == 3, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbTiffColor(bitsPerSample); + + case TiffColorType.Rgba3333: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 3 + && bitsPerSample.Channel2 == 3 + && bitsPerSample.Channel1 == 3 + && bitsPerSample.Channel0 == 3, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaTiffColor(extraSampleType, bitsPerSample); + + case TiffColorType.Rgb444: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 4 + && bitsPerSample.Channel1 == 4 + && bitsPerSample.Channel0 == 4, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb444TiffColor(); + + case TiffColorType.Rgba4444: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 4 + && bitsPerSample.Channel2 == 4 + && bitsPerSample.Channel1 == 4 + && bitsPerSample.Channel0 == 4, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaTiffColor(extraSampleType, bitsPerSample); + + case TiffColorType.Rgb555: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 5 + && bitsPerSample.Channel1 == 5 + && bitsPerSample.Channel0 == 5, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbTiffColor(bitsPerSample); + + case TiffColorType.Rgba5555: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 5 + && bitsPerSample.Channel2 == 5 + && bitsPerSample.Channel1 == 5 + && bitsPerSample.Channel0 == 5, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaTiffColor(extraSampleType, bitsPerSample); + + case TiffColorType.Rgb666: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 6 + && bitsPerSample.Channel1 == 6 + && bitsPerSample.Channel0 == 6, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbTiffColor(bitsPerSample); + + case TiffColorType.Rgba6666: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 6 + && bitsPerSample.Channel2 == 6 + && bitsPerSample.Channel1 == 6 + && bitsPerSample.Channel0 == 6, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaTiffColor(extraSampleType, bitsPerSample); + + case TiffColorType.Rgb888: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 8 + && bitsPerSample.Channel1 == 8 + && bitsPerSample.Channel0 == 8, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb888TiffColor(configuration); + + case TiffColorType.Rgba8888: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 8 + && bitsPerSample.Channel2 == 8 + && bitsPerSample.Channel1 == 8 + && bitsPerSample.Channel0 == 8, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgba8888TiffColor(configuration, memoryAllocator, extraSampleType); + + case TiffColorType.Rgb101010: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 10 + && bitsPerSample.Channel1 == 10 + && bitsPerSample.Channel0 == 10, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbTiffColor(bitsPerSample); + + case TiffColorType.Rgba10101010: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 10 + && bitsPerSample.Channel2 == 10 + && bitsPerSample.Channel1 == 10 + && bitsPerSample.Channel0 == 10, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaTiffColor(extraSampleType, bitsPerSample); + + case TiffColorType.Rgb121212: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 12 + && bitsPerSample.Channel1 == 12 + && bitsPerSample.Channel0 == 12, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbTiffColor(bitsPerSample); + + case TiffColorType.Rgba12121212: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 12 + && bitsPerSample.Channel2 == 12 + && bitsPerSample.Channel1 == 12 + && bitsPerSample.Channel0 == 12, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaTiffColor(extraSampleType, bitsPerSample); + + case TiffColorType.Rgb141414: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 14 + && bitsPerSample.Channel1 == 14 + && bitsPerSample.Channel0 == 14, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbTiffColor(bitsPerSample); + + case TiffColorType.Rgba14141414: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 14 + && bitsPerSample.Channel2 == 14 + && bitsPerSample.Channel1 == 14 + && bitsPerSample.Channel0 == 14, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaTiffColor(extraSampleType, bitsPerSample); + + case TiffColorType.Rgb161616: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 16 + && bitsPerSample.Channel1 == 16 + && bitsPerSample.Channel0 == 16, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb161616TiffColor(configuration, isBigEndian: byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Rgba16161616: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 16 + && bitsPerSample.Channel2 == 16 + && bitsPerSample.Channel1 == 16 + && bitsPerSample.Channel0 == 16, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgba16161616TiffColor(configuration, memoryAllocator, extraSampleType, isBigEndian: byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Rgb242424: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 24 + && bitsPerSample.Channel1 == 24 + && bitsPerSample.Channel0 == 24, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb242424TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Rgba24242424: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 24 + && bitsPerSample.Channel2 == 24 + && bitsPerSample.Channel1 == 24 + && bitsPerSample.Channel0 == 24, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgba24242424TiffColor(extraSampleType, isBigEndian: byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Rgb323232: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 32 + && bitsPerSample.Channel1 == 32 + && bitsPerSample.Channel0 == 32, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb323232TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Rgba32323232: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 32 + && bitsPerSample.Channel2 == 32 + && bitsPerSample.Channel1 == 32 + && bitsPerSample.Channel0 == 32, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgba32323232TiffColor(extraSampleType, isBigEndian: byteOrder == ByteOrder.BigEndian); + + case TiffColorType.RgbFloat323232: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 32 + && bitsPerSample.Channel1 == 32 + && bitsPerSample.Channel0 == 32, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbFloat323232TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + + case TiffColorType.RgbaFloat32323232: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 32 + && bitsPerSample.Channel2 == 32 + && bitsPerSample.Channel1 == 32 + && bitsPerSample.Channel0 == 32, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaFloat32323232TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + + case TiffColorType.PaletteColor: + DebugGuard.NotNull(colorMap, "colorMap"); + return new PaletteTiffColor(bitsPerSample, colorMap, extraSampleType); + + case TiffColorType.YCbCr: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 8 + && bitsPerSample.Channel1 == 8 + && bitsPerSample.Channel0 == 8, + "bitsPerSample"); + return new YCbCrTiffColor(memoryAllocator, referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling); + + case TiffColorType.CieLab: + + DebugGuard.IsTrue(bitsPerSample.Channels == 3, "bitsPerSample"); + + if (bitsPerSample.Channel0 == 8) + { + return new CieLab8TiffColor(); + } + + return new CieLab16TiffColor( + configuration, + options, + metadata, + memoryAllocator, + byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Cmyk: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 8 + && bitsPerSample.Channel2 == 8 + && bitsPerSample.Channel1 == 8 + && bitsPerSample.Channel0 == 8, + "bitsPerSample"); + return new CmykTiffColor(compression, configuration, options, metadata, memoryAllocator); + + default: + throw TiffThrowHelper.InvalidColorType(colorType.ToString()); + } + } + + public static TiffBasePlanarColorDecoder CreatePlanar( + ImageFrameMetadata metadata, + DecoderOptions options, + Configuration configuration, + MemoryAllocator allocator, + TiffColorType colorType, + TiffBitsPerSample bitsPerSample, + TiffExtraSampleType? extraSampleType, + ushort[] colorMap, + Rational[] referenceBlackAndWhite, + Rational[] ycbcrCoefficients, + ushort[] ycbcrSubSampling, + ByteOrder byteOrder) + { + switch (colorType) + { + case TiffColorType.Rgb888Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbPlanarTiffColor(bitsPerSample); + + case TiffColorType.Rgba8888Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaPlanarTiffColor(extraSampleType, bitsPerSample); + + case TiffColorType.YCbCrPlanar: + return new YCbCrPlanarTiffColor(referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling); + + case TiffColorType.CieLabPlanar: + return bitsPerSample.Channel0 == 8 + ? new CieLab8PlanarTiffColor() + : new CieLab16PlanarTiffColor( + configuration, + options, + metadata, + allocator, + byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Rgb161616Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb16PlanarTiffColor(byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Rgba16161616Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgba16PlanarTiffColor(extraSampleType, byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Rgb242424Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb24PlanarTiffColor(byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Rgba24242424Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgba24PlanarTiffColor(extraSampleType, byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Rgb323232Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb32PlanarTiffColor(byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Rgba32323232Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgba32PlanarTiffColor(extraSampleType, byteOrder == ByteOrder.BigEndian); + + default: + throw TiffThrowHelper.InvalidColorType(colorType.ToString()); + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs new file mode 100644 index 0000000..0c60207 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -0,0 +1,295 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Provides enumeration of the various TIFF photometric interpretation implementation types. + /// + internal enum TiffColorType + { + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. + /// + BlackIsZero, + + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for bilevel images. + /// + BlackIsZero1, + + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for 4-bit images. + /// + BlackIsZero4, + + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for 8-bit images. + /// + BlackIsZero8, + + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for 16-bit images. + /// + BlackIsZero16, + + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for 24-bit images. + /// + BlackIsZero24, + + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for 32-bit images. + /// + BlackIsZero32, + + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Pixel data is 32-bit float. + /// + BlackIsZero32Float, + + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. + /// + WhiteIsZero, + + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for bilevel images. + /// + WhiteIsZero1, + + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for 4-bit images. + /// + WhiteIsZero4, + + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for 8-bit images. + /// + WhiteIsZero8, + + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for 16-bit images. + /// + WhiteIsZero16, + + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for 24-bit images. + /// + WhiteIsZero24, + + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for 32-bit images. + /// + WhiteIsZero32, + + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Pixel data is 32-bit float. + /// + WhiteIsZero32Float, + + /// + /// Palette-color. + /// + PaletteColor, + + /// + /// RGB Full Color. + /// + Rgb, + + /// + /// RGB color image with 2 bits for each channel. + /// + Rgb222, + + /// + /// RGBA color image with 2 bits for each channel. + /// + Rgba2222, + + /// + /// RGB color image with 3 bits for each channel. + /// + Rgb333, + + /// + /// RGBA color image with 3 bits for each channel. + /// + Rgba3333, + + /// + /// RGB color image with 4 bits for each channel. + /// + Rgb444, + + /// + /// RGBA color image with 4 bits for each channel. + /// + Rgba4444, + + /// + /// RGB color image with 5 bits for each channel. + /// + Rgb555, + + /// + /// RGBA color image with 5 bits for each channel. + /// + Rgba5555, + + /// + /// RGB color image with 6 bits for each channel. + /// + Rgb666, + + /// + /// RGBA color image with 6 bits for each channel. + /// + Rgba6666, + + /// + /// RGB Full Color. Optimized implementation for 8-bit images. + /// + Rgb888, + + /// + /// RGBA Full Color with 8-bit for each channel. + /// + Rgba8888, + + /// + /// RGB color image with 10 bits for each channel. + /// + Rgb101010, + + /// + /// RGBA color image with 10 bits for each channel. + /// + Rgba10101010, + + /// + /// RGB color image with 12 bits for each channel. + /// + Rgb121212, + + /// + /// RGBA color image with 12 bits for each channel. + /// + Rgba12121212, + + /// + /// RGB color image with 14 bits for each channel. + /// + Rgb141414, + + /// + /// RGBA color image with 14 bits for each channel. + /// + Rgba14141414, + + /// + /// RGB color image with 16 bits for each channel. + /// + Rgb161616, + + /// + /// RGBA color image with 16 bits for each channel. + /// + Rgba16161616, + + /// + /// RGB color image with 24 bits for each channel. + /// + Rgb242424, + + /// + /// RGBA color image with 24 bits for each channel. + /// + Rgba24242424, + + /// + /// RGB color image with 32 bits for each channel. + /// + Rgb323232, + + /// + /// RGBA color image with 32 bits for each channel. + /// + Rgba32323232, + + /// + /// RGB color image with 32 bits floats for each channel. + /// + RgbFloat323232, + + /// + /// RGBA color image with 32 bits floats for each channel. + /// + RgbaFloat32323232, + + /// + /// RGB Full Color. Planar configuration of data. 8 Bit per color channel. + /// + Rgb888Planar, + + /// + /// RGBA color image with an alpha channel. Planar configuration of data. 8 Bit per color channel. + /// + Rgba8888Planar, + + /// + /// RGB Full Color. Planar configuration of data. 16 Bit per color channel. + /// + Rgb161616Planar, + + /// + /// RGB Color with an alpha channel. Planar configuration of data. 16 Bit per color channel. + /// + Rgba16161616Planar, + + /// + /// RGB Full Color. Planar configuration of data. 24 Bit per color channel. + /// + Rgb242424Planar, + + /// + /// RGB Color with an alpha channel. Planar configuration of data. 24 Bit per color channel. + /// + Rgba24242424Planar, + + /// + /// RGB Full Color. Planar configuration of data. 32 Bit per color channel. + /// + Rgb323232Planar, + + /// + /// RGB Color with an alpha channel. Planar configuration of data. 32 Bit per color channel. + /// + Rgba32323232Planar, + + /// + /// The pixels are stored in YCbCr format. + /// + YCbCr, + + /// + /// The pixels are stored in YCbCr format as planar. + /// + YCbCrPlanar, + + /// + /// The pixels are stored in CieLab format. + /// + CieLab, + + /// + /// The pixels are stored in CieLab format as planar. + /// + CieLabPlanar, + + /// + /// The pixels are stored as CMYK. + /// + Cmyk, + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs new file mode 100644 index 0000000..83c9213 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'WhiteIsZero' photometric interpretation for 16-bit grayscale images. + /// + /// The type of pixel format. + internal class WhiteIsZero16TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public WhiteIsZero16TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ushort intensity = (ushort)(ushort.MaxValue - TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2))); + offset += 2; + + pixelRow[x] = TPixel.FromL16(new L16(intensity)); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + ushort intensity = (ushort)(ushort.MaxValue - TiffUtilities.ConvertToUShortLittleEndian(data.Slice(offset, 2))); + offset += 2; + + pixelRow[x] = TPixel.FromL16(new L16(intensity)); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero1TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero1TiffColor{TPixel}.cs new file mode 100644 index 0000000..45ae843 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero1TiffColor{TPixel}.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'WhiteIsZero' photometric interpretation (optimized for bilevel images). + /// + /// The type of pixel format. + internal class WhiteIsZero1TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + nuint offset = 0; + TPixel colorBlack = TPixel.FromRgba32(Color.Black.ToPixel()); + TPixel colorWhite = TPixel.FromRgba32(Color.White.ToPixel()); + + ref byte dataRef = ref MemoryMarshal.GetReference(data); + for (nuint y = (uint)top; y < (uint)(top + height); y++) + { + Span pixelRowSpan = pixels.DangerousGetRowSpan((int)y); + ref TPixel pixelRowRef = ref MemoryMarshal.GetReference(pixelRowSpan); + for (nuint x = (uint)left; x < (uint)(left + width); x += 8) + { + byte b = Unsafe.Add(ref dataRef, offset++); + nuint maxShift = Math.Min((uint)(left + width) - x, 8); + + if (maxShift == 8) + { + int bit = (b >> 7) & 1; + ref TPixel pixel0 = ref Unsafe.Add(ref pixelRowRef, x); + pixel0 = bit == 0 ? colorWhite : colorBlack; + + bit = (b >> 6) & 1; + ref TPixel pixel1 = ref Unsafe.Add(ref pixelRowRef, x + 1); + pixel1 = bit == 0 ? colorWhite : colorBlack; + + bit = (b >> 5) & 1; + ref TPixel pixel2 = ref Unsafe.Add(ref pixelRowRef, x + 2); + pixel2 = bit == 0 ? colorWhite : colorBlack; + + bit = (b >> 4) & 1; + ref TPixel pixel3 = ref Unsafe.Add(ref pixelRowRef, x + 3); + pixel3 = bit == 0 ? colorWhite : colorBlack; + + bit = (b >> 3) & 1; + ref TPixel pixel4 = ref Unsafe.Add(ref pixelRowRef, x + 4); + pixel4 = bit == 0 ? colorWhite : colorBlack; + + bit = (b >> 2) & 1; + ref TPixel pixel5 = ref Unsafe.Add(ref pixelRowRef, x + 5); + pixel5 = bit == 0 ? colorWhite : colorBlack; + + bit = (b >> 1) & 1; + ref TPixel pixel6 = ref Unsafe.Add(ref pixelRowRef, x + 6); + pixel6 = bit == 0 ? colorWhite : colorBlack; + + bit = b & 1; + ref TPixel pixel7 = ref Unsafe.Add(ref pixelRowRef, x + 7); + pixel7 = bit == 0 ? colorWhite : colorBlack; + } + else + { + for (nuint shift = 0; shift < maxShift; shift++) + { + int bit = (b >> (7 - (int)shift)) & 1; + + ref TPixel pixel = ref Unsafe.Add(ref pixelRowRef, x + shift); + pixel = bit == 0 ? colorWhite : colorBlack; + } + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs new file mode 100644 index 0000000..e1d94c6 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'WhiteIsZero' photometric interpretation for 24-bit grayscale images. + /// + /// The type of pixel format. + internal class WhiteIsZero24TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public WhiteIsZero24TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + Span buffer = stackalloc byte[4]; + int bufferStartIdx = this.isBigEndian ? 1 : 0; + const uint maxValue = 0xFFFFFF; + + Span bufferSpan = buffer[bufferStartIdx..]; + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(bufferSpan); + uint intensity = maxValue - TiffUtilities.ConvertToUIntBigEndian(buffer); + offset += 3; + + pixelRow[x] = TiffUtilities.ColorScaleTo24Bit(intensity); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(bufferSpan); + uint intensity = maxValue - TiffUtilities.ConvertToUIntLittleEndian(buffer); + offset += 3; + + pixelRow[x] = TiffUtilities.ColorScaleTo24Bit(intensity); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs new file mode 100644 index 0000000..cc39b6c --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'WhiteIsZero' photometric interpretation for 32-bit float grayscale images. + /// + /// The type of pixel format. + internal class WhiteIsZero32FloatTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public WhiteIsZero32FloatTiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + Span buffer = stackalloc byte[4]; + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 4).CopyTo(buffer); + buffer.Reverse(); + float intensity = 1.0f - BitConverter.ToSingle(buffer); + offset += 4; + + pixelRow[x] = TPixel.FromScaledVector4(new Vector4(intensity, intensity, intensity, 1f)); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + float intensity = 1.0f - BitConverter.ToSingle(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TPixel.FromScaledVector4(new Vector4(intensity, intensity, intensity, 1.0f)); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs new file mode 100644 index 0000000..8598691 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'WhiteIsZero' photometric interpretation for 32-bit grayscale images. + /// + /// The type of pixel format. + internal class WhiteIsZero32TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public WhiteIsZero32TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + const uint maxValue = 0xFFFFFFFF; + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + uint intensity = maxValue - TiffUtilities.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtilities.ColorScaleTo32Bit(intensity); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + uint intensity = maxValue - TiffUtilities.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtilities.ColorScaleTo32Bit(intensity); + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero4TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero4TiffColor{TPixel}.cs new file mode 100644 index 0000000..d9ae786 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero4TiffColor{TPixel}.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'WhiteIsZero' photometric interpretation (optimized for 4-bit grayscale images). + /// + /// The type of pixel format. + internal class WhiteIsZero4TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + bool isOddWidth = (width & 1) == 1; + + for (int y = top; y < top + height; y++) + { + Span pixelRowSpan = pixels.DangerousGetRowSpan(y); + for (int x = left; x < left + width - 1;) + { + byte byteData = data[offset++]; + pixelRowSpan[x++] = TPixel.FromL8(new L8((byte)((15 - ((byteData & 0xF0) >> 4)) * 17))); + pixelRowSpan[x++] = TPixel.FromL8(new L8((byte)((15 - (byteData & 0x0F)) * 17))); + } + + if (isOddWidth) + { + byte byteData = data[offset++]; + pixelRowSpan[left + width - 1] = TPixel.FromL8(new L8((byte)((15 - ((byteData & 0xF0) >> 4)) * 17))); + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs new file mode 100644 index 0000000..4d38910 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'WhiteIsZero' photometric interpretation (optimized for 8-bit grayscale images). + /// + /// The type of pixel format. + internal class WhiteIsZero8TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + byte intensity = (byte)(byte.MaxValue - data[offset++]); + pixelRow[x] = TPixel.FromL8(new L8(intensity)); + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs new file mode 100644 index 0000000..df16bba --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements the 'WhiteIsZero' photometric interpretation (for all bit depths). + /// + /// The type of pixel format. + internal class WhiteIsZeroTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly ushort bitsPerSample0; + private readonly float factor; + + public WhiteIsZeroTiffColor(TiffBitsPerSample bitsPerSample) + { + this.bitsPerSample0 = bitsPerSample.Channel0; + this.factor = (float)Math.Pow(2, this.bitsPerSample0) - 1.0f; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + BitReader bitReader = new(data); + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + int value = bitReader.ReadBits(this.bitsPerSample0); + float intensity = 1f - (value / this.factor); + pixelRow[x] = TPixel.FromScaledVector4(new Vector4(intensity, intensity, intensity, 1f)); + } + + bitReader.NextRow(); + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs new file mode 100644 index 0000000..8ecba6f --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs @@ -0,0 +1,120 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Converts YCbCr data to rgb data. + /// + internal class YCbCrConverter + { + private readonly CodingRangeExpander yExpander; + private readonly CodingRangeExpander cbExpander; + private readonly CodingRangeExpander crExpander; + private readonly YCbCrToRgbConverter converter; + + private static readonly Rational[] DefaultLuma = + [ + new(299, 1000), + new(587, 1000), + new(114, 1000) + ]; + + private static readonly Rational[] DefaultReferenceBlackWhite = + [ + new(0, 1), new(255, 1), + new(128, 1), new(255, 1), + new(128, 1), new(255, 1) + ]; + + public YCbCrConverter(Rational[] referenceBlackAndWhite, Rational[] coefficients) + { + referenceBlackAndWhite ??= DefaultReferenceBlackWhite; + coefficients ??= DefaultLuma; + + if (referenceBlackAndWhite.Length != 6) + { + TiffThrowHelper.ThrowImageFormatException("reference black and white array should have 6 entry's"); + } + + if (coefficients.Length != 3) + { + TiffThrowHelper.ThrowImageFormatException("luma coefficients array should have 6 entry's"); + } + + this.yExpander = new CodingRangeExpander(referenceBlackAndWhite[0], referenceBlackAndWhite[1], 255); + this.cbExpander = new CodingRangeExpander(referenceBlackAndWhite[2], referenceBlackAndWhite[3], 127); + this.crExpander = new CodingRangeExpander(referenceBlackAndWhite[4], referenceBlackAndWhite[5], 127); + this.converter = new YCbCrToRgbConverter(coefficients[0], coefficients[1], coefficients[2]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba32 ConvertToRgba32(byte y, byte cb, byte cr) + { + float yExpanded = this.yExpander.Expand(y); + float cbExpanded = this.cbExpander.Expand(cb); + float crExpanded = this.crExpander.Expand(cr); + + Rgba32 rgba = this.converter.Convert(yExpanded, cbExpanded, crExpanded); + + return rgba; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte RoundAndClampTo8Bit(float value) + { + int input = (int)MathF.Round(value); + return (byte)Numerics.Clamp(input, 0, 255); + } + + private readonly struct CodingRangeExpander + { + private readonly float f1; + private readonly float f2; + + public CodingRangeExpander(Rational referenceBlack, Rational referenceWhite, int codingRange) + { + float black = referenceBlack.ToSingle(); + float white = referenceWhite.ToSingle(); + this.f1 = codingRange / (white - black); + this.f2 = this.f1 * black; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Expand(float code) => (code * this.f1) - this.f2; + } + + private readonly struct YCbCrToRgbConverter + { + private readonly float cr2R; + private readonly float cb2B; + private readonly float y2G; + private readonly float cr2G; + private readonly float cb2G; + + public YCbCrToRgbConverter(Rational lumaRed, Rational lumaGreen, Rational lumaBlue) + { + this.cr2R = 2 - (2 * lumaRed.ToSingle()); + this.cb2B = 2 - (2 * lumaBlue.ToSingle()); + this.y2G = (1 - lumaBlue.ToSingle() - lumaRed.ToSingle()) / lumaGreen.ToSingle(); + this.cr2G = 2 * lumaRed.ToSingle() * (lumaRed.ToSingle() - 1) / lumaGreen.ToSingle(); + this.cb2G = 2 * lumaBlue.ToSingle() * (lumaBlue.ToSingle() - 1) / lumaGreen.ToSingle(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba32 Convert(float y, float cb, float cr) + { + Rgba32 pixel = default; + pixel.R = RoundAndClampTo8Bit((cr * this.cr2R) + y); + pixel.G = RoundAndClampTo8Bit((this.y2G * y) + (this.cr2G * cr) + (this.cb2G * cb)); + pixel.B = RoundAndClampTo8Bit((cb * this.cb2B) + y); + pixel.A = byte.MaxValue; + + return pixel; + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs new file mode 100644 index 0000000..84c2656 --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements decoding pixel data with photometric interpretation of type 'YCbCr' with the planar configuration. + /// + /// The type of pixel format. + internal class YCbCrPlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly YCbCrConverter converter; + private readonly ushort[] ycbcrSubSampling; + + public YCbCrPlanarTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients, ushort[] ycbcrSubSampling) + { + this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); + this.ycbcrSubSampling = ycbcrSubSampling; + } + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + Span yData = data[0].GetSpan(); + Span cbData = data[1].GetSpan(); + Span crData = data[2].GetSpan(); + + if (this.ycbcrSubSampling != null && !(this.ycbcrSubSampling[0] == 1 && this.ycbcrSubSampling[1] == 1)) + { + ReverseChromaSubSampling(width, height, this.ycbcrSubSampling[0], this.ycbcrSubSampling[1], cbData, crData); + } + + int offset = 0; + int widthPadding = 0; + if (this.ycbcrSubSampling != null) + { + // Round to the next integer multiple of horizontalSubSampling. + widthPadding = TiffUtilities.PaddingToNextInteger(width, this.ycbcrSubSampling[0]); + } + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + Rgba32 rgba = this.converter.ConvertToRgba32(yData[offset], cbData[offset], crData[offset]); + pixelRow[x] = TPixel.FromRgba32(rgba); + offset++; + } + + offset += widthPadding; + } + } + + private static void ReverseChromaSubSampling(int width, int height, int horizontalSubSampling, int verticalSubSampling, Span planarCb, Span planarCr) + { + // If width and height are not multiples of ChromaSubsampleHoriz and ChromaSubsampleVert respectively, + // then the source data will be padded. + width += TiffUtilities.PaddingToNextInteger(width, horizontalSubSampling); + height += TiffUtilities.PaddingToNextInteger(height, verticalSubSampling); + + for (int row = height - 1; row >= 0; row--) + { + for (int col = width - 1; col >= 0; col--) + { + int offset = (row * width) + col; + int subSampleOffset = (row / verticalSubSampling * (width / horizontalSubSampling)) + (col / horizontalSubSampling); + planarCb[offset] = planarCb[subSampleOffset]; + planarCr[offset] = planarCr[subSampleOffset]; + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs new file mode 100644 index 0000000..d83584e --- /dev/null +++ b/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -0,0 +1,110 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements decoding pixel data with photometric interpretation of type 'YCbCr'. + /// + /// The type of pixel format. + internal class YCbCrTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly MemoryAllocator memoryAllocator; + + private readonly YCbCrConverter converter; + + private readonly ushort[] ycbcrSubSampling; + + public YCbCrTiffColor(MemoryAllocator memoryAllocator, Rational[] referenceBlackAndWhite, Rational[] coefficients, ushort[] ycbcrSubSampling) + { + this.memoryAllocator = memoryAllocator; + this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); + this.ycbcrSubSampling = ycbcrSubSampling; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + ReadOnlySpan ycbcrData = data; + if (this.ycbcrSubSampling != null && !(this.ycbcrSubSampling[0] == 1 && this.ycbcrSubSampling[1] == 1)) + { + // 4 extra rows and columns for possible padding. + int paddedWidth = width + 4; + int paddedHeight = height + 4; + int requiredBytes = paddedWidth * paddedHeight * 3; + using IMemoryOwner tmpBuffer = this.memoryAllocator.Allocate(requiredBytes); + Span tmpBufferSpan = tmpBuffer.GetSpan(); + ReverseChromaSubSampling(width, height, this.ycbcrSubSampling[0], this.ycbcrSubSampling[1], data, tmpBufferSpan); + ycbcrData = tmpBufferSpan; + this.DecodeYCbCrData(pixels, left, top, width, height, ycbcrData); + return; + } + + this.DecodeYCbCrData(pixels, left, top, width, height, ycbcrData); + } + + private void DecodeYCbCrData(Buffer2D pixels, int left, int top, int width, int height, ReadOnlySpan ycbcrData) + { + int offset = 0; + int widthPadding = 0; + if (this.ycbcrSubSampling != null) + { + // Round to the next integer multiple of horizontalSubSampling. + widthPadding = TiffUtilities.PaddingToNextInteger(width, this.ycbcrSubSampling[0]); + } + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + Rgba32 rgba = this.converter.ConvertToRgba32(ycbcrData[offset], ycbcrData[offset + 1], ycbcrData[offset + 2]); + pixelRow[x] = TPixel.FromRgba32(rgba); + offset += 3; + } + + offset += widthPadding * 3; + } + } + + private static void ReverseChromaSubSampling(int width, int height, int horizontalSubSampling, int verticalSubSampling, ReadOnlySpan source, Span destination) + { + // If width and height are not multiples of ChromaSubsampleHoriz and ChromaSubsampleVert respectively, + // then the source data will be padded. + width += TiffUtilities.PaddingToNextInteger(width, horizontalSubSampling); + height += TiffUtilities.PaddingToNextInteger(height, verticalSubSampling); + int blockWidth = width / horizontalSubSampling; + int blockHeight = height / verticalSubSampling; + int cbCrOffsetInBlock = horizontalSubSampling * verticalSubSampling; + int blockByteCount = cbCrOffsetInBlock + 2; + + for (int blockRow = blockHeight - 1; blockRow >= 0; blockRow--) + { + for (int blockCol = blockWidth - 1; blockCol >= 0; blockCol--) + { + int blockOffset = (blockRow * blockWidth) + blockCol; + ReadOnlySpan blockData = source.Slice(blockOffset * blockByteCount, blockByteCount); + byte cr = blockData[cbCrOffsetInBlock + 1]; + byte cb = blockData[cbCrOffsetInBlock]; + + for (int row = verticalSubSampling - 1; row >= 0; row--) + { + for (int col = horizontalSubSampling - 1; col >= 0; col--) + { + int offset = 3 * ((((blockRow * verticalSubSampling) + row) * width) + (blockCol * horizontalSubSampling) + col); + destination[offset + 2] = cr; + destination[offset + 1] = cb; + destination[offset] = blockData[(row * horizontalSubSampling) + col]; + } + } + } + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/README.md b/ImageSharp/Formats/Tiff/README.md new file mode 100644 index 0000000..48cbd54 --- /dev/null +++ b/ImageSharp/Formats/Tiff/README.md @@ -0,0 +1,247 @@ +# ImageSharp TIFF codec + +## References +- TIFF + - [TIFF 6.0 Specification](http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf),(http://www.npes.org/pdf/TIFF-v6.pdf) + - [TIFF Supplement 1](http://partners.adobe.com/public/developer/en/tiff/TIFFPM6.pdf) + - [TIFF Supplement 2](http://partners.adobe.com/public/developer/en/tiff/TIFFphotoshop.pdf) + - [TIFF Supplement 3](http://chriscox.org/TIFFTN3d1.pdf) + - [TIFF-F/FX Extension (RFC2301)](http://www.ietf.org/rfc/rfc2301.txt) + - [TIFF/EP Extension (Wikipedia)](https://en.wikipedia.org/wiki/TIFF/EP) + - [Adobe TIFF Pages](http://partners.adobe.com/public/developer/tiff/index.html) + - [Unofficial TIFF FAQ](http://www.awaresystems.be/imaging/tiff/faq.html) + - [CCITT T.4 Compression](https://www.itu.int/rec/T-REC-T.4-198811-S/_page.print) + - [CCITT T.6 Compression](https://www.itu.int/rec/T-REC-T.6/en) + +- DNG + - [Adobe DNG Pages](https://helpx.adobe.com/photoshop/digital-negative.html) + +- Metadata (EXIF) + - [EXIF 2.3 Specification](http://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf) + +- Metadata (XMP) + - [Adobe XMP Pages](http://www.adobe.com/products/xmp.html) + - [Adobe XMP Developer Center](http://www.adobe.com/devnet/xmp.html) + +## Implementation Status + +- The Decoder currently only supports decoding multiframe images, which have the same dimensions. +- Some compression formats are not yet supported. See the list below. + +### Compression Formats + +| |Encoder|Decoder|Comments | +|---------------------------|:-----:|:-----:|-----------------------------------| +|None | Y | Y | | +|Ccitt1D | Y | Y | | +|PackBits | Y | Y | | +|CcittGroup3Fax | Y | Y | | +|CcittGroup4Fax | Y | Y | | +|Lzw | Y | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case. | +|Old Jpeg | | Y | Only with chunky configuration. | +|Jpeg (Technote 2) | Y | Y | | +|Deflate (Technote 2) | Y | Y | Based on PNG Deflate. | +|Old Deflate (Technote 2) | | Y | | +|Webp | | Y | | + +### Photometric Interpretation Formats + +| |Encoder|Decoder|Comments | +|---------------------------|:-----:|:-----:|------------------------------------------------| +|WhiteIsZero | Y | Y | General + 1/4/8-bit optimised implementations. | +|BlackIsZero | Y | Y | General + 1/4/8-bit optimised implementations. | +|Rgb (Chunky) | Y | Y | General + Rgb888 optimised implementation. | +|Rgb (Planar) | | Y | General implementation only. | +|PaletteColor | Y | Y | General implementation only. | +|TransparencyMask | | | | +|Separated (TIFF Extension) | | Y | | +|YCbCr (TIFF Extension) | | Y | | +|CieLab (TIFF Extension) | | Y | | +|IccLab (TechNote 1) | | | | +|CMYK | | Y | | +|Tiled Images | | Y | | + +### Baseline TIFF Tags + +| |Encoder|Decoder|Comments | +|---------------------------|:-----:|:-----:|--------------------------| +|NewSubfileType | | | | +|SubfileType | | | | +|ImageWidth | Y | Y | | +|ImageLength | Y | Y | | +|BitsPerSample | Y | Y | | +|Compression | Y | Y | | +|PhotometricInterpretation | Y | Y | | +|Thresholding | | | | +|CellWidth | | | | +|CellLength | | | | +|FillOrder | | Y | | +|ImageDescription | Y | Y | | +|Make | Y | Y | | +|Model | Y | Y | | +|StripOffsets | Y | Y | | +|Orientation | | - | Ignore. Many readers ignore this tag. | +|SamplesPerPixel | Y | - | Currently ignored, as can be inferred from count of BitsPerSample. | +|RowsPerStrip | Y | Y | | +|StripByteCounts | Y | Y | | +|MinSampleValue | | | | +|MaxSampleValue | | | | +|XResolution | Y | Y | | +|YResolution | Y | Y | | +|PlanarConfiguration | | Y | Encoding support only chunky. | +|FreeOffsets | | | | +|FreeByteCounts | | | | +|GrayResponseUnit | | | | +|GrayResponseCurve | | | | +|ResolutionUnit | Y | Y | | +|Software | Y | Y | | +|DateTime | Y | Y | | +|Artist | Y | Y | | +|HostComputer | Y | Y | | +|ColorMap | Y | Y | | +|ExtraSamples | | Y | Unspecified alpha data is not supported. | +|Copyright | Y | Y | | + +### Extension TIFF Tags + +| |Encoder|Decoder|Comments | +|---------------------------|:-----:|:-----:|--------------------------| +|NewSubfileType | | | | +|DocumentName | Y | Y | | +|PageName | | | | +|XPosition | | | | +|YPosition | | | | +|T4Options | | Y | | +|T6Options | | | | +|PageNumber | | | | +|TransferFunction | | | | +|Predictor | Y | Y | only Horizontal | +|WhitePoint | | | | +|PrimaryChromaticities | | | | +|HalftoneHints | | | | +|TileWidth | | - | | +|TileLength | | - | | +|TileOffsets | | - | | +|TileByteCounts | | - | | +|BadFaxLines | | | | +|CleanFaxData | | | | +|ConsecutiveBadFaxLines | | | | +|SubIFDs | | - | | +|InkSet | | Y | CMYK | +|InkNames | | - | | +|NumberOfInks | | - | | +|DotRange | | | | +|TargetPrinter | | | | +|SampleFormat | | - | | +|SMinSampleValue | | | | +|SMaxSampleValue | | | | +|TransferRange | | | | +|ClipPath | | | | +|XClipPathUnits | | | | +|YClipPathUnits | | | | +|Indexed | | | | +|JPEGTables | | | | +|OPIProxy | | | | +|GlobalParametersIFD | | | | +|ProfileType | | | | +|FaxProfile | | | | +|CodingMethods | | | | +|VersionYear | | | | +|ModeNumber | | | | +|Decode | | | | +|DefaultImageColor | | | | +|JPEGProc | | | | +|JPEGInterchangeFormat | | | | +|JPEGInterchangeFormatLength| | | | +|JPEGRestartInterval | | | | +|JPEGLosslessPredictors | | | | +|JPEGPointTransforms | | | | +|JPEGQTables | | | | +|JPEGDCTables | | | | +|JPEGACTables | | | | +|YCbCrCoefficients | | Y | | +|YCbCrSubSampling | | Y | | +|YCbCrPositioning | | | | +|ReferenceBlackWhite | | Y | | +|StripRowCounts | - | - | See RFC 2301 (File Format for Internet Fax). | +|XMP | Y | Y | | +|ImageID | | | | +|ImageLayer | | | | + +### Private TIFF Tags + +| |Encoder|Decoder|Comments | +|---------------------------|:-----:|:-----:|--------------------------| +|Wang Annotation | | | | +|MD FileTag | | | | +|MD ScalePixel | | | | +|MD ColorTable | | | | +|MD LabName | | | | +|MD SampleInfo | | | | +|MD PrepDate | | | | +|MD PrepTime | | | | +|MD FileUnits | | | | +|ModelPixelScaleTag | | | | +|IPTC | Y | Y | | +|INGR Packet Data Tag | | | | +|INGR Flag Registers | | | | +|IrasB Transformation Matrix| | | | +|ModelTiepointTag | | | | +|ModelTransformationTag | | | | +|Photoshop | | | | +|Exif IFD | | - | 0x8769 SubExif | +|ICC Profile | Y | Y | | +|GeoKeyDirectoryTag | | | | +|GeoDoubleParamsTag | | | | +|GeoAsciiParamsTag | | | | +|GPS IFD | | | | +|HylaFAX FaxRecvParams | | | | +|HylaFAX FaxSubAddress | | | | +|HylaFAX FaxRecvTime | | | | +|ImageSourceData | | | | +|Interoperability IFD | | | | +|GDAL_METADATA | | | | +|GDAL_NODATA | | | | +|Oce Scanjob Description | | | | +|Oce Application Selector | | | | +|Oce Identification Number | | | | +|Oce ImageLogic Characteristics| | | | +|DNGVersion | | | | +|DNGBackwardVersion | | | | +|UniqueCameraModel | | | | +|LocalizedCameraModel | | | | +|CFAPlaneColor | | | | +|CFALayout | | | | +|LinearizationTable | | | | +|BlackLevelRepeatDim | | | | +|BlackLevel | | | | +|BlackLevelDeltaH | | | | +|BlackLevelDeltaV | | | | +|WhiteLevel | | | | +|DefaultScale | | | | +|DefaultCropOrigin | | | | +|DefaultCropSize | | | | +|ColorMatrix1 | | | | +|ColorMatrix2 | | | | +|CameraCalibration1 | | | | +|CameraCalibration2 | | | | +|ReductionMatrix1 | | | | +|ReductionMatrix2 | | | | +|AnalogBalance | | | | +|AsShotNeutral | | | | +|AsShotWhiteXY | | | | +|BaselineExposure | | | | +|BaselineNoise | | | | +|BaselineSharpness | | | | +|BayerGreenSplit | | | | +|LinearResponseLimit | | | | +|CameraSerialNumber | | | | +|LensInfo | | | | +|ChromaBlurRadius | | | | +|AntiAliasStrength | | | | +|DNGPrivateData | | | | +|MakerNoteSafety | | | | +|CalibrationIlluminant1 | | | | +|CalibrationIlluminant2 | | | | +|BestQualityScale | | | | +|Alias Layer Metadata | | | | diff --git a/ImageSharp/Formats/Tiff/T-REC-T.4-198811-S!!PDF-E.pdf b/ImageSharp/Formats/Tiff/T-REC-T.4-198811-S!!PDF-E.pdf new file mode 100644 index 0000000..40724dd Binary files /dev/null and b/ImageSharp/Formats/Tiff/T-REC-T.4-198811-S!!PDF-E.pdf differ diff --git a/ImageSharp/Formats/Tiff/T-REC-T.6-198811-I!!PDF-E.pdf b/ImageSharp/Formats/Tiff/T-REC-T.6-198811-I!!PDF-E.pdf new file mode 100644 index 0000000..32fa877 Binary files /dev/null and b/ImageSharp/Formats/Tiff/T-REC-T.6-198811-I!!PDF-E.pdf differ diff --git a/ImageSharp/Formats/Tiff/TIFF-AdobeTechNote-22032002.pdf b/ImageSharp/Formats/Tiff/TIFF-AdobeTechNote-22032002.pdf new file mode 100644 index 0000000..e4822d4 Binary files /dev/null and b/ImageSharp/Formats/Tiff/TIFF-AdobeTechNote-22032002.pdf differ diff --git a/ImageSharp/Formats/Tiff/TIFF-v6.pdf b/ImageSharp/Formats/Tiff/TIFF-v6.pdf new file mode 100644 index 0000000..9911706 Binary files /dev/null and b/ImageSharp/Formats/Tiff/TIFF-v6.pdf differ diff --git a/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs b/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs new file mode 100644 index 0000000..a776ae8 --- /dev/null +++ b/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tiff { + /// + /// Enumerates the available bits per pixel for the tiff format. + /// + public enum TiffBitsPerPixel + { + /// + /// 1 bit per pixel, for bi-color image. + /// + Bit1 = 1, + + /// + /// 4 bits per pixel, for images with a color palette. + /// + Bit4 = 4, + + /// + /// 6 bits per pixel. 2 bit for each color channel. + /// Note: The TiffEncoder does not yet support 2 bits per color channel and will default to 24 bits per pixel instead. + /// + Bit6 = 6, + + /// + /// 8 bits per pixel, grayscale or color palette images. + /// + Bit8 = 8, + + /// + /// 10 bits per pixel, for gray images. + /// Note: The TiffEncoder does not yet support 10 bits per pixel and will default to 24 bits per pixel instead. + /// + Bit10 = 10, + + /// + /// 12 bits per pixel. 4 bit for each color channel. + /// Note: The TiffEncoder does not yet support 4 bits per color channel and will default to 24 bits per pixel instead. + /// + Bit12 = 12, + + /// + /// 14 bits per pixel, for gray images. + /// Note: The TiffEncoder does not yet support 14 bits per pixel images and will default to 24 bits per pixel instead. + /// + Bit14 = 14, + + /// + /// 16 bits per pixel, for gray images. + /// Note: The TiffEncoder does not yet support 16 bits per color channel and will default to 16 bits grayscale instead. + /// + Bit16 = 16, + + /// + /// 24 bits per pixel. One byte for each color channel. + /// + Bit24 = 24, + + /// + /// 30 bits per pixel. 10 bit for each color channel. + /// Note: The TiffEncoder does not yet support 10 bits per color channel and will default to 24 bits per pixel instead. + /// + Bit30 = 30, + + /// + /// 32 bits per pixel. One byte for each color channel. + /// + Bit32 = 32, + + /// + /// 36 bits per pixel. 12 bit for each color channel. + /// Note: The TiffEncoder does not yet support 12 bits per color channel and will default to 24 bits per pixel instead. + /// + Bit36 = 36, + + /// + /// 42 bits per pixel. 14 bit for each color channel. + /// Note: The TiffEncoder does not yet support 14 bits per color channel and will default to 24 bits per pixel instead. + /// + Bit42 = 42, + + /// + /// 48 bits per pixel. 16 bit for each color channel. + /// Note: The TiffEncoder does not yet support 16 bits per color channel and will default to 24 bits per pixel instead. + /// + Bit48 = 48, + + /// + /// 64 bits per pixel. 16 bit for each color channel. + /// Note: The TiffEncoder does not yet support 16 bits per color channel and will default to 32 bits per pixel instead. + /// + Bit64 = 64, + } +} diff --git a/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs b/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs new file mode 100644 index 0000000..3fd98dc --- /dev/null +++ b/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs @@ -0,0 +1,183 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff { + /// + /// The number of bits per component. + /// + public readonly struct TiffBitsPerSample : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The bits for the channel 0. + /// The bits for the channel 1. + /// The bits for the channel 2. + /// The bits for the channel 3. + public TiffBitsPerSample(ushort channel0, ushort channel1, ushort channel2, ushort channel3 = 0) + { + this.Channel0 = (ushort)Numerics.Clamp(channel0, 0, 32); + this.Channel1 = (ushort)Numerics.Clamp(channel1, 0, 32); + this.Channel2 = (ushort)Numerics.Clamp(channel2, 0, 32); + this.Channel3 = (ushort)Numerics.Clamp(channel3, 0, 32); + + this.Channels = 0; + this.Channels += (byte)(this.Channel0 != 0 ? 1 : 0); + this.Channels += (byte)(this.Channel1 != 0 ? 1 : 0); + this.Channels += (byte)(this.Channel2 != 0 ? 1 : 0); + this.Channels += (byte)(this.Channel3 != 0 ? 1 : 0); + } + + /// + /// Gets the bits for the channel 0. + /// + public readonly ushort Channel0 { get; } + + /// + /// Gets the bits for the channel 1. + /// + public readonly ushort Channel1 { get; } + + /// + /// Gets the bits for the channel 2. + /// + public readonly ushort Channel2 { get; } + + /// + /// Gets the bits for the alpha channel. + /// + public readonly ushort Channel3 { get; } + + /// + /// Gets the number of channels. + /// + public readonly byte Channels { get; } + + /// + /// Checks whether two structures are equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is equal to the parameter; + /// otherwise, false. + /// + public static bool operator ==(TiffBitsPerSample left, TiffBitsPerSample right) => left.Equals(right); + + /// + /// Checks whether two structures are not equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is not equal to the parameter; + /// otherwise, false. + /// + public static bool operator !=(TiffBitsPerSample left, TiffBitsPerSample right) => !(left == right); + + /// + /// Tries to parse a ushort array and convert it into a TiffBitsPerSample struct. + /// + /// The value to parse. + /// The tiff bits per sample. + /// True, if the value could be parsed. + public static bool TryParse(ushort[]? value, out TiffBitsPerSample sample) + { + if (value is null || value.Length == 0) + { + sample = default; + return false; + } + + ushort c3 = 0; + ushort c2; + ushort c1; + ushort c0; + switch (value.Length) + { + case 4: + c3 = value[3]; + c2 = value[2]; + c1 = value[1]; + c0 = value[0]; + break; + + case 3: + c2 = value[2]; + c1 = value[1]; + c0 = value[0]; + break; + case 2: + c2 = 0; + c1 = value[1]; + c0 = value[0]; + break; + default: + c2 = 0; + c1 = 0; + c0 = value[0]; + break; + } + + sample = new TiffBitsPerSample(c0, c1, c2, c3); + return true; + } + + /// + public override bool Equals(object? obj) + => obj is TiffBitsPerSample sample && this.Equals(sample); + + /// + public bool Equals(TiffBitsPerSample other) + => this.Channel0 == other.Channel0 + && this.Channel1 == other.Channel1 + && this.Channel2 == other.Channel2 + && this.Channel3 == other.Channel3; + + /// + public override int GetHashCode() + => HashCode.Combine(this.Channel0, this.Channel1, this.Channel2, this.Channel3); + + /// + /// Converts the bits per sample struct to an ushort array. + /// + /// Bits per sample as ushort array. + public ushort[] ToArray() + { + if (this.Channel1 == 0) + { + return [this.Channel0]; + } + + if (this.Channel2 == 0) + { + return [this.Channel0, this.Channel1]; + } + + if (this.Channel3 == 0) + { + return [this.Channel0, this.Channel1, this.Channel2]; + } + + return [this.Channel0, this.Channel1, this.Channel2, this.Channel3]; + } + + /// + /// Gets the bits per pixel for the given bits per sample. + /// + /// Bits per pixel. + public TiffBitsPerPixel BitsPerPixel() + { + int bitsPerPixel = this.Channel0 + this.Channel1 + this.Channel2 + this.Channel3; + return (TiffBitsPerPixel)bitsPerPixel; + } + + /// + public override string ToString() + => this.Channel3 is 0 ? + $"TiffBitsPerSample({this.Channel0}, {this.Channel1}, {this.Channel2})" + : $"TiffBitsPerSample({this.Channel0}, {this.Channel1}, {this.Channel2}, {this.Channel3})"; + } +} diff --git a/ImageSharp/Formats/Tiff/TiffConfigurationModule.cs b/ImageSharp/Formats/Tiff/TiffConfigurationModule.cs new file mode 100644 index 0000000..4d71c51 --- /dev/null +++ b/ImageSharp/Formats/Tiff/TiffConfigurationModule.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tiff { + /// + /// Registers the image encoders, decoders and mime type detectors for the TIFF format. + /// + public sealed class TiffConfigurationModule : IImageFormatConfigurationModule + { + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetEncoder(TiffFormat.Instance, new TiffEncoder()); + configuration.ImageFormatsManager.SetDecoder(TiffFormat.Instance, TiffDecoder.Instance); + configuration.ImageFormatsManager.AddImageFormatDetector(new TiffImageFormatDetector()); + } + } +} diff --git a/ImageSharp/Formats/Tiff/TiffDecoder.cs b/ImageSharp/Formats/Tiff/TiffDecoder.cs new file mode 100644 index 0000000..a82b1ec --- /dev/null +++ b/ImageSharp/Formats/Tiff/TiffDecoder.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Tiff { + /// + /// Image decoder for generating an image out of a TIFF stream. + /// + public class TiffDecoder : ImageDecoder + { + private TiffDecoder() + { + } + + /// + /// Gets the shared instance. + /// + public static TiffDecoder Instance { get; } = new(); + + /// + protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + return new TiffDecoderCore(options).Identify(options.Configuration, stream, cancellationToken); + } + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + TiffDecoderCore decoder = new(options); + Image image = decoder.Decode(options.Configuration, stream, cancellationToken); + + ScaleToTargetSize(options, image); + + return image; + } + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(options, stream, cancellationToken); + } +} diff --git a/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/ImageSharp/Formats/Tiff/TiffDecoderCore.cs new file mode 100644 index 0000000..4d79c9a --- /dev/null +++ b/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -0,0 +1,974 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff { + /// + /// Performs the tiff decoding operation. + /// + internal class TiffDecoderCore : ImageDecoderCore + { + /// + /// General configuration options. + /// + private readonly Configuration configuration; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// A value indicating whether the metadata should be ignored when the image is being decoded. + /// + private readonly bool skipMetadata; + + /// + /// The maximum number of frames to decode. Inclusive. + /// + private readonly uint maxFrames; + + /// + /// The stream to decode from. + /// + private BufferedReadStream inputStream; + + /// + /// Indicates the byte order of the stream. + /// + private ByteOrder byteOrder; + + /// + /// Initializes a new instance of the class. + /// + /// The decoder options. + public TiffDecoderCore(DecoderOptions options) + : base(options) + { + this.configuration = options.Configuration; + this.skipMetadata = options.SkipMetadata; + this.maxFrames = options.MaxFrames; + this.memoryAllocator = this.configuration.MemoryAllocator; + } + + /// + /// Gets or sets the bits per sample. + /// + public TiffBitsPerSample BitsPerSample { get; set; } + + /// + /// Gets or sets the bits per pixel. + /// + public int BitsPerPixel { get; set; } + + /// + /// Gets or sets the lookup table for RGB palette colored images. + /// + public ushort[] ColorMap { get; set; } + + /// + /// Gets or sets the photometric interpretation implementation to use when decoding the image. + /// + public TiffColorType ColorType { get; set; } + + /// + /// Gets or sets the reference black and white for decoding YCbCr pixel data. + /// + public Rational[] ReferenceBlackAndWhite { get; set; } + + /// + /// Gets or sets the YCbCr coefficients. + /// + public Rational[] YcbcrCoefficients { get; set; } + + /// + /// Gets or sets the YCbCr sub sampling. + /// + public ushort[] YcbcrSubSampling { get; set; } + + /// + /// Gets or sets the compression used, when the image was encoded. + /// + public TiffDecoderCompressionType CompressionType { get; set; } + + /// + /// Gets or sets the Fax specific compression options. + /// + public FaxCompressionOptions FaxCompressionOptions { get; set; } + + /// + /// Gets or sets the logical order of bits within a byte. + /// + public TiffFillOrder FillOrder { get; set; } + + /// + /// Gets or sets the extra samples type. + /// + public TiffExtraSampleType? ExtraSamplesType { get; set; } + + /// + /// Gets or sets the JPEG tables when jpeg compression is used. + /// + public byte[] JpegTables { get; set; } + + /// + /// Gets or sets the start of image marker for old Jpeg compression. + /// + public uint? OldJpegCompressionStartOfImageMarker { get; set; } + + /// + /// Gets or sets the planar configuration type to use when decoding the image. + /// + public TiffPlanarConfiguration PlanarConfiguration { get; set; } + + /// + /// Gets or sets the photometric interpretation. + /// + public TiffPhotometricInterpretation PhotometricInterpretation { get; set; } + + /// + /// Gets or sets the sample format. + /// + public TiffSampleFormat SampleFormat { get; set; } + + /// + /// Gets or sets the horizontal predictor. + /// + public TiffPredictor Predictor { get; set; } + + /// + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + { + List> frames = []; + List framesMetadata = []; + try + { + this.inputStream = stream; + DirectoryReader reader = new(stream, this.configuration.MemoryAllocator); + + IList directories = reader.Read(); + this.byteOrder = reader.ByteOrder; + + Size? size = null; + uint frameCount = 0; + foreach (ExifProfile ifd in directories) + { + cancellationToken.ThrowIfCancellationRequested(); + ImageFrame frame = this.DecodeFrame(ifd, size, cancellationToken); + + if (!size.HasValue) + { + size = frame.Size; + } + + frames.Add(frame); + framesMetadata.Add(frame.Metadata); + + if (++frameCount == this.maxFrames) + { + break; + } + } + + this.Dimensions = frames[0].Size; + ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff); + return new Image(this.configuration, metadata, frames); + } + catch + { + foreach (ImageFrame f in frames) + { + f.Dispose(); + } + + throw; + } + } + + /// + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + this.inputStream = stream; + DirectoryReader reader = new(stream, this.configuration.MemoryAllocator); + IList directories = reader.Read(); + + List framesMetadata = []; + int width = 0; + int height = 0; + + for (int i = 0; i < directories.Count; i++) + { + (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffMetadata) meta + = this.CreateFrameMetadata(directories[i]); + + framesMetadata.Add(meta.FrameMetadata); + + width = Math.Max(width, meta.TiffMetadata.EncodingWidth); + height = Math.Max(height, meta.TiffMetadata.EncodingHeight); + } + + ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff); + + return new ImageInfo(new Size(width, height), metadata, framesMetadata); + } + + /// + /// Decodes the image data from a specified IFD. + /// + /// The pixel format. + /// The IFD tags. + /// The previously determined root frame size if decoded. + /// The token to monitor cancellation. + /// The tiff frame. + private ImageFrame DecodeFrame(ExifProfile tags, Size? size, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffFrameMetadata) metadata = this.CreateFrameMetadata(tags); + bool isTiled = this.VerifyAndParse(tags, metadata.TiffFrameMetadata); + + int width = metadata.TiffFrameMetadata.EncodingWidth; + int height = metadata.TiffFrameMetadata.EncodingHeight; + + // If size has a value and the width/height off the tiff is smaller we much capture the delta. + if (size.HasValue) + { + if (size.Value.Width < width || size.Value.Height < height) + { + TiffThrowHelper.ThrowNotSupported("Images with frames of size greater than the root frame are not supported."); + } + } + else + { + size = new Size(width, height); + } + + ImageFrame frame = new(this.configuration, size.Value.Width, size.Value.Height, metadata.FrameMetadata); + + if (isTiled) + { + this.DecodeImageWithTiles(tags, frame, width, height, cancellationToken); + } + else + { + this.DecodeImageWithStrips(tags, frame, width, height, cancellationToken); + } + + // Only RGB-compatible color types can be converted here because the TPixel-based ICC profile conversion + // expects RGB-like pixel data; other photometric interpretations (YCbCr, CMYK, Lab, etc.) would require + // dedicated transforms. We do this once at the frame level to avoid duplicating conversion logic + // across all color decoders and to keep their decode paths focused on raw pixel unpacking. + if (this.ColorType is >= TiffColorType.PaletteColor and <= TiffColorType.Rgba32323232Planar) + { + _ = this.TryConvertIccProfile(frame); + } + + return frame; + } + + private (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffMetadata) CreateFrameMetadata(ExifProfile tags) + { + ImageFrameMetadata imageFrameMetaData = new(); + if (!this.skipMetadata) + { + imageFrameMetaData.ExifProfile = tags; + + // We resolve the ICC profile early so that we can use it for color conversion if needed. + if (tags.TryGetValue(ExifTag.IccProfile, out IExifValue iccProfileBytes)) + { + this.ExecuteAncillarySegmentAction( + () => + { + IccProfile profile = new(iccProfileBytes.Value); + if (profile.CheckIsValid()) + { + imageFrameMetaData.IccProfile = profile; + } + else + { + throw new InvalidIccProfileException("Invalid TIFF ICC profile."); + } + }); + } + } + + TiffFrameMetadata tiffMetadata = TiffFrameMetadata.Parse(tags); + imageFrameMetaData.SetFormatMetadata(TiffFormat.Instance, tiffMetadata); + + return (imageFrameMetaData, tiffMetadata); + } + + /// + /// Decodes the image data for Tiff's which arrange the pixel data in stripes. + /// + /// The pixel format. + /// The IFD tags. + /// The image frame to decode into. + /// The width in px units of the frame data. + /// The height in px units of the frame data. + /// The token to monitor cancellation. + private void DecodeImageWithStrips(ExifProfile tags, ImageFrame frame, int width, int height, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + int rowsPerStrip; + if (tags.TryGetValue(ExifTag.RowsPerStrip, out IExifValue value)) + { + rowsPerStrip = (int)value.Value; + } + else + { + rowsPerStrip = TiffConstants.RowsPerStripInfinity; + } + + Array stripOffsetsArray = (Array)tags.GetValueInternal(ExifTag.StripOffsets).GetValue(); + Array stripByteCountsArray = (Array)tags.GetValueInternal(ExifTag.StripByteCounts).GetValue(); + + using IMemoryOwner stripOffsetsMemory = this.ConvertNumbers(stripOffsetsArray, out Span stripOffsets); + using IMemoryOwner stripByteCountsMemory = this.ConvertNumbers(stripByteCountsArray, out Span stripByteCounts); + + if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar) + { + this.DecodeStripsPlanar( + frame, + width, + height, + rowsPerStrip, + stripOffsets, + stripByteCounts, + cancellationToken); + } + else + { + this.DecodeStripsChunky( + frame, + width, + height, + rowsPerStrip, + stripOffsets, + stripByteCounts, + cancellationToken); + } + } + + /// + /// Decodes the image data for Tiff's which arrange the pixel data in tiles. + /// + /// The pixel format. + /// The IFD tags. + /// The image frame to decode into. + /// The width in px units of the frame data. + /// The height in px units of the frame data. + /// The token to monitor cancellation. + private void DecodeImageWithTiles(ExifProfile tags, ImageFrame frame, int width, int height, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Buffer2D pixels = frame.PixelBuffer; + + if (!tags.TryGetValue(ExifTag.TileWidth, out IExifValue valueWidth)) + { + ArgumentNullException.ThrowIfNull(valueWidth); + } + + if (!tags.TryGetValue(ExifTag.TileLength, out IExifValue valueLength)) + { + ArgumentNullException.ThrowIfNull(valueLength); + } + + int tileWidth = (int)valueWidth.Value; + int tileLength = (int)valueLength.Value; + int tilesAcross = (width + tileWidth - 1) / tileWidth; + int tilesDown = (height + tileLength - 1) / tileLength; + + Array tilesOffsetsArray; + Array tilesByteCountsArray; + IExifValue tilesOffsetsExifValue = tags.GetValueInternal(ExifTag.TileOffsets); + IExifValue tilesByteCountsExifValue = tags.GetValueInternal(ExifTag.TileByteCounts); + if (tilesOffsetsExifValue is null) + { + // Note: This is against the spec, but libTiff seems to handle it this way. + // TIFF 6.0 says: "Do not use both strip- oriented and tile-oriented fields in the same TIFF file". + tilesOffsetsExifValue = tags.GetValueInternal(ExifTag.StripOffsets); + tilesByteCountsExifValue = tags.GetValueInternal(ExifTag.StripByteCounts); + tilesOffsetsArray = (Array)tilesOffsetsExifValue.GetValue(); + tilesByteCountsArray = (Array)tilesByteCountsExifValue.GetValue(); + } + else + { + tilesOffsetsArray = (Array)tilesOffsetsExifValue.GetValue(); + tilesByteCountsArray = (Array)tilesByteCountsExifValue.GetValue(); + } + + using IMemoryOwner tileOffsetsMemory = this.ConvertNumbers(tilesOffsetsArray, out Span tileOffsets); + using IMemoryOwner tileByteCountsMemory = this.ConvertNumbers(tilesByteCountsArray, out Span tileByteCounts); + + if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar) + { + this.DecodeTilesPlanar(frame, tileWidth, tileLength, tilesAcross, tilesDown, tileOffsets, tileByteCounts, cancellationToken); + } + else + { + this.DecodeTilesChunky(frame, tileWidth, tileLength, tilesAcross, tilesDown, tileOffsets, tileByteCounts, cancellationToken); + } + } + + /// + /// Decodes the image data for planar encoded pixel data. + /// + /// The pixel format. + /// The image frame to decode data into. + /// The width in px units of the frame data. + /// The height in px units of the frame data. + /// The number of rows per strip of data. + /// An array of byte offsets to each strip in the image. + /// An array of the size of each strip (in bytes). + /// The token to monitor cancellation. + private void DecodeStripsPlanar( + ImageFrame frame, + int width, + int height, + int rowsPerStrip, + Span stripOffsets, + Span stripByteCounts, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + int stripsPerPixel = this.BitsPerSample.Channels; + int stripsPerPlane = stripOffsets.Length / stripsPerPixel; + int bitsPerPixel = this.BitsPerPixel; + + Buffer2D pixels = frame.PixelBuffer; + + IMemoryOwner[] stripBuffers = new IMemoryOwner[stripsPerPixel]; + + try + { + for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++) + { + ulong uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip, stripIndex); + + if (uncompressedStripSize > int.MaxValue) + { + TiffThrowHelper.ThrowNotSupported("Strips larger than Int32.MaxValue bytes are not supported for compressed images."); + } + + stripBuffers[stripIndex] = this.memoryAllocator.Allocate((int)uncompressedStripSize); + } + + using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel, frame.Metadata); + TiffBasePlanarColorDecoder colorDecoder = this.CreatePlanarColorDecoder(frame.Metadata); + + for (int i = 0; i < stripsPerPlane; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + int stripHeight = i < stripsPerPlane - 1 || height % rowsPerStrip == 0 ? rowsPerStrip : height % rowsPerStrip; + + int stripIndex = i; + for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++) + { + decompressor.Decompress( + this.inputStream, + stripOffsets[stripIndex], + stripByteCounts[stripIndex], + stripHeight, + stripBuffers[planeIndex].GetSpan(), + cancellationToken); + + stripIndex += stripsPerPlane; + } + + colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, width, stripHeight); + } + } + finally + { + foreach (IMemoryOwner buf in stripBuffers) + { + buf?.Dispose(); + } + } + } + + /// + /// Decodes the image data for chunky encoded pixel data. + /// + /// The pixel format. + /// The image frame to decode data into. + /// The width in px units of the frame data. + /// The height in px units of the frame data. + /// The rows per strip. + /// The strip offsets. + /// The strip byte counts. + /// The token to monitor cancellation. + private void DecodeStripsChunky( + ImageFrame frame, + int width, + int height, + int rowsPerStrip, + Span stripOffsets, + Span stripByteCounts, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + // If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip. + if (rowsPerStrip == TiffConstants.RowsPerStripInfinity) + { + rowsPerStrip = height; + } + + ulong uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip); + int bitsPerPixel = this.BitsPerPixel; + + using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel, frame.Metadata); + TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(frame.Metadata); + Buffer2D pixels = frame.PixelBuffer; + + // There exists in this world TIFF files with uncompressed strips larger than Int32.MaxValue. + // We can read them, but we cannot allocate a buffer that large to hold the uncompressed data. + // In this scenario we fall back to reading and decoding one row at a time. + // + // The NoneTiffCompression decompressor can be used to read individual rows since we have + // a guarantee that each row required the same number of bytes. + if (decompressor is NoneTiffCompression none && uncompressedStripSize > int.MaxValue) + { + ulong bytesPerRowU = this.CalculateStripBufferSize(width, 1); + + // This should never happen, but we check just to be sure. + if (bytesPerRowU > int.MaxValue) + { + TiffThrowHelper.ThrowNotSupported("Strips larger than Int32.MaxValue bytes are not supported for compressed images."); + } + + int bytesPerRow = (int)bytesPerRowU; + using IMemoryOwner rowBufferOwner = this.memoryAllocator.Allocate(bytesPerRow, AllocationOptions.Clean); + Span rowBuffer = rowBufferOwner.GetSpan(); + for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) + { + cancellationToken.ThrowIfCancellationRequested(); + + int stripHeight = stripIndex < stripOffsets.Length - 1 || height % rowsPerStrip == 0 + ? rowsPerStrip + : height % rowsPerStrip; + + int top = rowsPerStrip * stripIndex; + if (top + stripHeight > height) + { + break; + } + + ulong baseOffset = stripOffsets[stripIndex]; + ulong available = stripByteCounts[stripIndex]; + ulong required = (ulong)bytesPerRow * (ulong)stripHeight; + if (available < required) + { + break; + } + + for (int r = 0; r < stripHeight; r++) + { + cancellationToken.ThrowIfCancellationRequested(); + + ulong rowOffset = baseOffset + ((ulong)r * (ulong)bytesPerRow); + + // Use the NoneTiffCompression decompressor to read exactly one row. + none.Decompress( + this.inputStream, + rowOffset, + (ulong)bytesPerRow, + 1, + rowBuffer, + cancellationToken); + + colorDecoder.Decode(rowBuffer, pixels, 0, top + r, width, 1); + } + } + + { + // If the color decoder is the palette decoder we need to capture its palette. + if (colorDecoder is PaletteTiffColor paletteDecoder) + { + TiffFrameMetadata tiffFrameMetadata = frame.Metadata.GetTiffMetadata(); + tiffFrameMetadata.LocalColorTable = paletteDecoder.PaletteColors; + } + } + + return; + } + + if (uncompressedStripSize > int.MaxValue) + { + TiffThrowHelper.ThrowNotSupported("Strips larger than Int32.MaxValue bytes are not supported for compressed images."); + } + + using IMemoryOwner stripBuffer = this.memoryAllocator.Allocate((int)uncompressedStripSize, AllocationOptions.Clean); + Span stripBufferSpan = stripBuffer.GetSpan(); + + for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) + { + cancellationToken.ThrowIfCancellationRequested(); + + int stripHeight = stripIndex < stripOffsets.Length - 1 || height % rowsPerStrip == 0 + ? rowsPerStrip + : height % rowsPerStrip; + + int top = rowsPerStrip * stripIndex; + if (top + stripHeight > height) + { + // Make sure we ignore any strips that are not needed for the image (if too many are present). + break; + } + + decompressor.Decompress( + this.inputStream, + stripOffsets[stripIndex], + stripByteCounts[stripIndex], + stripHeight, + stripBufferSpan, + cancellationToken); + + colorDecoder.Decode(stripBufferSpan, pixels, 0, top, width, stripHeight); + } + + { + // If the color decoder is the palette decoder we need to capture its palette. + if (colorDecoder is PaletteTiffColor paletteDecoder) + { + TiffFrameMetadata tiffFrameMetadata = frame.Metadata.GetTiffMetadata(); + tiffFrameMetadata.LocalColorTable = paletteDecoder.PaletteColors; + } + } + } + + /// + /// Decodes the image data for Tiff's which arrange the pixel data in tiles and the planar configuration. + /// + /// The pixel format. + /// The image frame to decode into. + /// The width in pixels of the tile. + /// The height in pixels of the tile. + /// The number of tiles horizontally. + /// The number of tiles vertically. + /// The tile offsets. + /// The tile byte counts. + /// The token to monitor cancellation. + private void DecodeTilesPlanar( + ImageFrame frame, + int tileWidth, + int tileLength, + int tilesAcross, + int tilesDown, + Span tileOffsets, + Span tileByteCounts, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Buffer2D pixels = frame.PixelBuffer; + int width = pixels.Width; + int height = pixels.Height; + int bitsPerPixel = this.BitsPerPixel; + int channels = this.BitsPerSample.Channels; + int tilesPerChannel = tileOffsets.Length / channels; + + IMemoryOwner[] tilesBuffers = new IMemoryOwner[channels]; + + try + { + int bytesPerTileRow = RoundUpToMultipleOfEight(tileWidth * bitsPerPixel); + int uncompressedTilesSize = bytesPerTileRow * tileLength; + for (int i = 0; i < tilesBuffers.Length; i++) + { + tilesBuffers[i] = this.memoryAllocator.Allocate(uncompressedTilesSize, AllocationOptions.Clean); + } + + using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel, frame.Metadata); + TiffBasePlanarColorDecoder colorDecoder = this.CreatePlanarColorDecoder(frame.Metadata); + + int tileIndex = 0; + int remainingPixelsInColumn = height; + for (int tileY = 0; tileY < tilesDown; tileY++) + { + int remainingPixelsInRow = width; + int pixelColumnOffset = tileY * tileLength; + bool isLastVerticalTile = tileY == tilesDown - 1; + for (int tileX = 0; tileX < tilesAcross; tileX++) + { + int pixelRowOffset = tileX * tileWidth; + bool isLastHorizontalTile = tileX == tilesAcross - 1; + int tileIndexForChannel = tileIndex; + for (int i = 0; i < channels; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + decompressor.Decompress( + this.inputStream, + tileOffsets[tileIndexForChannel], + tileByteCounts[tileIndexForChannel], + tileLength, + tilesBuffers[i].GetSpan(), + cancellationToken); + + tileIndexForChannel += tilesPerChannel; + } + + if (isLastHorizontalTile && remainingPixelsInRow < tileWidth) + { + // Adjust pixel data in the tile buffer to fit the smaller then usual tile width. + for (int i = 0; i < channels; i++) + { + Span tileBufferSpan = tilesBuffers[i].GetSpan(); + for (int y = 0; y < tileLength; y++) + { + int currentRowOffset = y * tileWidth; + Span adjustedRow = tileBufferSpan.Slice(y * remainingPixelsInRow, remainingPixelsInRow); + tileBufferSpan.Slice(currentRowOffset, remainingPixelsInRow).CopyTo(adjustedRow); + } + } + } + + colorDecoder.Decode( + tilesBuffers, + pixels, + pixelRowOffset, + pixelColumnOffset, + isLastHorizontalTile ? remainingPixelsInRow : tileWidth, + isLastVerticalTile ? remainingPixelsInColumn : tileLength); + + remainingPixelsInRow -= tileWidth; + tileIndex++; + } + + remainingPixelsInColumn -= tileLength; + } + } + finally + { + foreach (IMemoryOwner buf in tilesBuffers) + { + buf?.Dispose(); + } + } + } + + /// + /// Decodes the image data for TIFFs which arrange the pixel data in tiles and the chunky configuration. + /// + /// The pixel format. + /// The image frame to decode into. + /// The width in pixels of the tile. + /// The height in pixels of the tile. + /// The number of tiles horizontally. + /// The number of tiles vertically. + /// The tile offsets. + /// The tile byte counts. + /// The token to monitor cancellation. + private void DecodeTilesChunky( + ImageFrame frame, + int tileWidth, + int tileLength, + int tilesAcross, + int tilesDown, + Span tileOffsets, + Span tileByteCounts, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Buffer2D pixels = frame.PixelBuffer; + int width = pixels.Width; + int height = pixels.Height; + int bitsPerPixel = this.BitsPerPixel; + int bytesPerTileRow = RoundUpToMultipleOfEight(tileWidth * bitsPerPixel); + + using IMemoryOwner tileBuffer = this.memoryAllocator.Allocate(bytesPerTileRow * tileLength, AllocationOptions.Clean); + Span tileBufferSpan = tileBuffer.GetSpan(); + + using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel, frame.Metadata, true, tileWidth, tileLength); + TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(frame.Metadata); + + int tileIndex = 0; + for (int tileY = 0; tileY < tilesDown; tileY++) + { + int rowStartY = tileY * tileLength; + int rowEndY = Math.Min(rowStartY + tileLength, height); + + for (int tileX = 0; tileX < tilesAcross; tileX++) + { + cancellationToken.ThrowIfCancellationRequested(); + + bool isLastHorizontalTile = tileX == tilesAcross - 1; + int remainingPixelsInRow = width - (tileX * tileWidth); + + decompressor.Decompress( + this.inputStream, + tileOffsets[tileIndex], + tileByteCounts[tileIndex], + tileLength, + tileBufferSpan, + cancellationToken); + + int tileBufferOffset = 0; + int bytesToCopy = isLastHorizontalTile ? RoundUpToMultipleOfEight(bitsPerPixel * remainingPixelsInRow) : bytesPerTileRow; + int rowWidth = Math.Min(tileWidth, remainingPixelsInRow); + int left = tileX * tileWidth; + + for (int y = rowStartY; y < rowEndY; y++) + { + // Decode the tile row directly into the pixel buffer. + ReadOnlySpan tileRowSpan = tileBufferSpan.Slice(tileBufferOffset, bytesToCopy); + colorDecoder.Decode(tileRowSpan, pixels, left, y, rowWidth, 1); + tileBufferOffset += bytesPerTileRow; + } + + tileIndex++; + } + } + + // If the color decoder is the palette decoder we need to capture its palette. + if (colorDecoder is PaletteTiffColor paletteDecoder) + { + TiffFrameMetadata tiffFrameMetadata = frame.Metadata.GetTiffMetadata(); + tiffFrameMetadata.LocalColorTable = paletteDecoder.PaletteColors; + } + } + + private TiffBaseColorDecoder CreateChunkyColorDecoder(ImageFrameMetadata metadata) + where TPixel : unmanaged, IPixel => + TiffColorDecoderFactory.Create( + metadata, + this.Options, + this.configuration, + this.memoryAllocator, + this.ColorType, + this.BitsPerSample, + this.ExtraSamplesType, + this.ColorMap, + this.ReferenceBlackAndWhite, + this.YcbcrCoefficients, + this.YcbcrSubSampling, + this.CompressionType, + this.byteOrder); + + private TiffBasePlanarColorDecoder CreatePlanarColorDecoder(ImageFrameMetadata metadata) + where TPixel : unmanaged, IPixel => + TiffColorDecoderFactory.CreatePlanar( + metadata, + this.Options, + this.configuration, + this.memoryAllocator, + this.ColorType, + this.BitsPerSample, + this.ExtraSamplesType, + this.ColorMap, + this.ReferenceBlackAndWhite, + this.YcbcrCoefficients, + this.YcbcrSubSampling, + this.byteOrder); + + private TiffBaseDecompressor CreateDecompressor( + int frameWidth, + int bitsPerPixel, + ImageFrameMetadata metadata, + bool isTiled = false, + int tileWidth = 0, + int tileHeight = 0) + where TPixel : unmanaged, IPixel => + TiffDecompressorsFactory.Create( + this.Options, + this.CompressionType, + this.memoryAllocator, + this.PhotometricInterpretation, + frameWidth, + bitsPerPixel, + metadata, + this.ColorType, + this.Predictor, + this.FaxCompressionOptions, + this.JpegTables, + this.OldJpegCompressionStartOfImageMarker.GetValueOrDefault(), + this.FillOrder, + this.byteOrder, + isTiled, + tileWidth, + tileHeight); + + private IMemoryOwner ConvertNumbers(Array array, out Span span) + { + if (array is Number[] numbers) + { + IMemoryOwner memory = this.memoryAllocator.Allocate(numbers.Length); + span = memory.GetSpan(); + for (int i = 0; i < numbers.Length; i++) + { + span[i] = (uint)numbers[i]; + } + + return memory; + } + + DebugGuard.IsTrue(array is ulong[], $"Expected {nameof(UInt64)} array."); + span = (ulong[])array; + return null; + } + + /// + /// Calculates the size (in bytes) for a pixel buffer using the determined color format. + /// + /// The width for the desired pixel buffer. + /// The height for the desired pixel buffer. + /// The index of the plane for planar image configuration (or zero for chunky). + /// The size (in bytes) of the required pixel buffer. + private ulong CalculateStripBufferSize(int width, int height, int plane = -1) + { + DebugGuard.MustBeLessThanOrEqualTo(plane, 3, nameof(plane)); + + int bitsPerPixel = 0; + + if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky) + { + DebugGuard.IsTrue(plane == -1, "Expected Chunky planar."); + bitsPerPixel = this.BitsPerPixel; + } + else + { + switch (plane) + { + case 0: + bitsPerPixel = this.BitsPerSample.Channel0; + break; + case 1: + bitsPerPixel = this.BitsPerSample.Channel1; + break; + case 2: + bitsPerPixel = this.BitsPerSample.Channel2; + break; + case 3: + bitsPerPixel = this.BitsPerSample.Channel3; + break; + default: + TiffThrowHelper.ThrowNotSupported("More then 4 color channels are not supported"); + break; + } + } + + ulong bytesPerRow = (((ulong)width * (ulong)bitsPerPixel) + 7) / 8; + return bytesPerRow * (ulong)height; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int RoundUpToMultipleOfEight(int value) => (int)(((uint)value + 7) / 8); + } +} diff --git a/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs b/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs new file mode 100644 index 0000000..b4d6fb9 --- /dev/null +++ b/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs @@ -0,0 +1,137 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Iptc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SixLabors.ImageSharp.Formats.Tiff { + /// + /// The decoder metadata creator. + /// + internal static class TiffDecoderMetadataCreator + { + public static ImageMetadata Create(List frames, bool ignoreMetadata, ByteOrder byteOrder, bool isBigTiff) + { + if (frames.Count < 1) + { + TiffThrowHelper.ThrowImageFormatException("Expected at least one frame."); + } + + ImageMetadata imageMetaData = Create(byteOrder, isBigTiff, frames[0]); + + if (!ignoreMetadata) + { + for (int i = 0; i < frames.Count; i++) + { + // ICC profile data has already been resolved in the frame metadata, + // as it is required for color conversion. + ImageFrameMetadata frameMetaData = frames[i]; + if (TryGetIptc(frameMetaData.ExifProfile.Values, out byte[] iptcBytes)) + { + frameMetaData.IptcProfile = new IptcProfile(iptcBytes); + } + + if (frameMetaData.ExifProfile.TryGetValue(ExifTag.XMP, out IExifValue xmpProfileBytes)) + { + frameMetaData.XmpProfile = new XmpProfile(xmpProfileBytes.Value); + } + } + } + + return imageMetaData; + } + + private static ImageMetadata Create(ByteOrder byteOrder, bool isBigTiff, ImageFrameMetadata rootFrameMetadata) + { + ImageMetadata imageMetaData = new(); + SetResolution(imageMetaData, rootFrameMetadata.ExifProfile); + + TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata(); + tiffMetadata.ByteOrder = byteOrder; + tiffMetadata.FormatType = isBigTiff ? TiffFormatType.BigTIFF : TiffFormatType.Default; + + TiffFrameMetadata tiffFrameMetadata = rootFrameMetadata.GetTiffMetadata(); + tiffMetadata.BitsPerPixel = tiffFrameMetadata.BitsPerPixel; + tiffMetadata.BitsPerSample = tiffFrameMetadata.BitsPerSample; + tiffMetadata.Compression = tiffFrameMetadata.Compression; + tiffMetadata.PhotometricInterpretation = tiffFrameMetadata.PhotometricInterpretation; + tiffMetadata.Predictor = tiffFrameMetadata.Predictor; + + return imageMetaData; + } + + private static void SetResolution(ImageMetadata imageMetaData, ExifProfile exifProfile) + { + imageMetaData.ResolutionUnits = exifProfile != null ? UnitConverter.ExifProfileToResolutionUnit(exifProfile) : PixelResolutionUnit.PixelsPerInch; + + if (exifProfile is null) + { + return; + } + + if (exifProfile.TryGetValue(ExifTag.XResolution, out IExifValue horizontalResolution)) + { + imageMetaData.HorizontalResolution = horizontalResolution.Value.ToDouble(); + } + + if (exifProfile.TryGetValue(ExifTag.YResolution, out IExifValue verticalResolution)) + { + imageMetaData.VerticalResolution = verticalResolution.Value.ToDouble(); + } + } + + private static bool TryGetIptc(IReadOnlyList exifValues, out byte[] iptcBytes) + { + iptcBytes = null; + IExifValue iptc = exifValues.FirstOrDefault(f => f.Tag == ExifTag.IPTC); + + if (iptc != null) + { + if (iptc.DataType is ExifDataType.Byte or ExifDataType.Undefined) + { + iptcBytes = (byte[])iptc.GetValue(); + return true; + } + + // Some Encoders write the data type of IPTC as long. + if (iptc.DataType == ExifDataType.Long) + { + uint[] iptcValues = (uint[])iptc.GetValue(); + iptcBytes = new byte[iptcValues.Length * 4]; + Buffer.BlockCopy(iptcValues, 0, iptcBytes, 0, iptcValues.Length * 4); + if (iptcBytes[0] == 0x1c) + { + return true; + } + else if (iptcBytes[3] != 0x1c) + { + return false; + } + + // Probably wrong endianness, swap byte order. + Span iptcBytesSpan = iptcBytes.AsSpan(); + Span buffer = stackalloc byte[4]; + for (int i = 0; i < iptcBytes.Length; i += 4) + { + iptcBytesSpan.Slice(i, 4).CopyTo(buffer); + iptcBytes[i] = buffer[3]; + iptcBytes[i + 1] = buffer[2]; + iptcBytes[i + 2] = buffer[1]; + iptcBytes[i + 3] = buffer[0]; + } + + return true; + } + } + + return false; + } + } +} diff --git a/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs new file mode 100644 index 0000000..b505cf8 --- /dev/null +++ b/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -0,0 +1,612 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System.Linq; +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; + +namespace SixLabors.ImageSharp.Formats.Tiff { + /// + /// The decoder options parser. + /// + internal static class TiffDecoderOptionsParser + { + private const TiffPlanarConfiguration DefaultPlanarConfiguration = TiffPlanarConfiguration.Chunky; + + /// + /// Determines the TIFF compression and color types, and reads any associated parameters. + /// + /// The options. + /// The exif profile of the frame to decode. + /// The IFD entries container to read the image format information for current frame. + /// True, if the image uses tiles. Otherwise the images has strip's. + public static bool VerifyAndParse(this TiffDecoderCore options, ExifProfile exifProfile, TiffFrameMetadata frameMetadata) + { + if (exifProfile.TryGetValue(ExifTag.ExtraSamples, out IExifValue samples)) + { + // We only support a single sample pertaining to alpha data. + // Other information is discarded. + TiffExtraSampleType sampleType = (TiffExtraSampleType)samples.Value[0]; + if (sampleType is TiffExtraSampleType.CorelDrawUnassociatedAlphaData) + { + // According to libtiff, this CorelDRAW-specific value indicates unassociated alpha. + // Patch required for compatibility with malformed CorelDRAW-generated TIFFs. + // https://libtiff.gitlab.io/libtiff/releases/v3.9.0beta.html + sampleType = TiffExtraSampleType.UnassociatedAlphaData; + } + + if (sampleType is (TiffExtraSampleType.UnassociatedAlphaData or TiffExtraSampleType.AssociatedAlphaData)) + { + options.ExtraSamplesType = sampleType; + } + } + + TiffFillOrder fillOrder; + if (exifProfile.TryGetValue(ExifTag.FillOrder, out IExifValue value)) + { + fillOrder = (TiffFillOrder)value.Value; + } + else + { + fillOrder = TiffFillOrder.MostSignificantBitFirst; + } + + if (fillOrder == TiffFillOrder.LeastSignificantBitFirst && frameMetadata.BitsPerPixel != TiffBitsPerPixel.Bit1) + { + TiffThrowHelper.ThrowNotSupported("The lower-order bits of the byte FillOrder is only supported in combination with 1bit per pixel bicolor tiff's."); + } + + if (frameMetadata.Predictor == TiffPredictor.FloatingPoint) + { + TiffThrowHelper.ThrowNotSupported("TIFF images with FloatingPoint horizontal predictor are not supported."); + } + + TiffSampleFormat? sampleFormat = null; + if (exifProfile.TryGetValue(ExifTag.SampleFormat, out IExifValue formatValue)) + { + TiffSampleFormat[] sampleFormats = formatValue.Value.Select(a => (TiffSampleFormat)a).ToArray(); + sampleFormat = sampleFormats[0]; + foreach (TiffSampleFormat format in sampleFormats) + { + if (format is not TiffSampleFormat.UnsignedInteger and not TiffSampleFormat.Float) + { + TiffThrowHelper.ThrowNotSupported("ImageSharp only supports the UnsignedInteger and Float SampleFormat."); + } + } + } + + ushort[] ycbcrSubSampling = null; + if (exifProfile.TryGetValue(ExifTag.YCbCrSubsampling, out IExifValue subSamplingValue)) + { + ycbcrSubSampling = subSamplingValue.Value; + } + + if (ycbcrSubSampling != null && ycbcrSubSampling.Length != 2) + { + TiffThrowHelper.ThrowImageFormatException("Invalid YCbCrSubsampling, expected 2 values."); + } + + if (ycbcrSubSampling != null && ycbcrSubSampling[1] > ycbcrSubSampling[0]) + { + TiffThrowHelper.ThrowImageFormatException("ChromaSubsampleVert shall always be less than or equal to ChromaSubsampleHoriz."); + } + + if (exifProfile.TryGetValue(ExifTag.StripRowCounts, out _)) + { + TiffThrowHelper.ThrowNotSupported("Variable-sized strips are not supported."); + } + + if (exifProfile.TryGetValue(ExifTag.PlanarConfiguration, out IExifValue planarValue)) + { + options.PlanarConfiguration = (TiffPlanarConfiguration)planarValue.Value; + } + else + { + options.PlanarConfiguration = DefaultPlanarConfiguration; + } + + options.Predictor = frameMetadata.Predictor; + options.PhotometricInterpretation = frameMetadata.PhotometricInterpretation; + options.SampleFormat = sampleFormat ?? TiffSampleFormat.UnsignedInteger; + options.BitsPerPixel = (int)frameMetadata.BitsPerPixel; + options.BitsPerSample = frameMetadata.BitsPerSample; + + if (exifProfile.TryGetValue(ExifTag.ReferenceBlackWhite, out IExifValue blackWhiteValue)) + { + options.ReferenceBlackAndWhite = blackWhiteValue.Value; + } + + if (exifProfile.TryGetValue(ExifTag.YCbCrCoefficients, out IExifValue coefficientsValue)) + { + options.YcbcrCoefficients = coefficientsValue.Value; + } + + if (exifProfile.TryGetValue(ExifTag.YCbCrSubsampling, out IExifValue ycbrSubSamplingValue)) + { + options.YcbcrSubSampling = ycbrSubSamplingValue.Value; + } + + options.FillOrder = fillOrder; + + if (exifProfile.TryGetValue(ExifTag.JPEGTables, out IExifValue jpegTablesValue)) + { + options.JpegTables = jpegTablesValue.Value; + } + + if (exifProfile.TryGetValue(ExifTag.JPEGInterchangeFormat, out IExifValue jpegInterchangeFormatValue)) + { + options.OldJpegCompressionStartOfImageMarker = jpegInterchangeFormatValue.Value; + } + + options.ParseCompression(frameMetadata.Compression, exifProfile); + options.ParseColorType(exifProfile); + + return VerifyRequiredFieldsArePresent(exifProfile, frameMetadata, options.PlanarConfiguration); + } + + /// + /// Verifies that all required fields for decoding are present. + /// + /// The exif profile. + /// The frame metadata. + /// The planar configuration. Either planar or chunky. + /// True, if the image uses tiles. Otherwise the images has strip's. + private static bool VerifyRequiredFieldsArePresent(ExifProfile exifProfile, TiffFrameMetadata frameMetadata, TiffPlanarConfiguration planarConfiguration) + { + bool isTiled = false; + if (exifProfile.GetValueInternal(ExifTag.TileWidth) is not null || exifProfile.GetValueInternal(ExifTag.TileLength) is not null) + { + if (planarConfiguration == TiffPlanarConfiguration.Planar && exifProfile.GetValueInternal(ExifTag.TileOffsets) is null) + { + TiffThrowHelper.ThrowImageFormatException("TileOffsets are missing and are required for decoding the TIFF image!"); + } + + if (planarConfiguration == TiffPlanarConfiguration.Chunky && exifProfile.GetValueInternal(ExifTag.TileOffsets) is null && exifProfile.GetValueInternal(ExifTag.StripOffsets) is null) + { + TiffThrowHelper.ThrowImageFormatException("TileOffsets are missing and are required for decoding the TIFF image!"); + } + + if (exifProfile.GetValueInternal(ExifTag.TileWidth) is null) + { + TiffThrowHelper.ThrowImageFormatException("TileWidth are missing and are required for decoding the TIFF image!"); + } + + if (exifProfile.GetValueInternal(ExifTag.TileLength) is null) + { + TiffThrowHelper.ThrowImageFormatException("TileLength are missing and are required for decoding the TIFF image!"); + } + + isTiled = true; + } + else + { + if (exifProfile.GetValueInternal(ExifTag.StripOffsets) is null) + { + TiffThrowHelper.ThrowImageFormatException("StripOffsets are missing and are required for decoding the TIFF image!"); + } + + if (exifProfile.GetValueInternal(ExifTag.StripByteCounts) is null) + { + TiffThrowHelper.ThrowImageFormatException("StripByteCounts are missing and are required for decoding the TIFF image!"); + } + } + + return isTiled; + } + + private static void ParseColorType(this TiffDecoderCore options, ExifProfile exifProfile) + { + switch (options.PhotometricInterpretation) + { + case TiffPhotometricInterpretation.WhiteIsZero: + { + if (options.BitsPerSample.Channels != 1) + { + TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); + } + + ushort bitsPerChannel = options.BitsPerSample.Channel0; + if (bitsPerChannel > 32) + { + TiffThrowHelper.ThrowNotSupported("Bits per sample is not supported."); + } + + switch (bitsPerChannel) + { + case 32: + if (options.SampleFormat == TiffSampleFormat.Float) + { + options.ColorType = TiffColorType.WhiteIsZero32Float; + return; + } + + options.ColorType = TiffColorType.WhiteIsZero32; + break; + + case 24: + options.ColorType = TiffColorType.WhiteIsZero24; + break; + + case 16: + options.ColorType = TiffColorType.WhiteIsZero16; + break; + + case 8: + options.ColorType = TiffColorType.WhiteIsZero8; + break; + + case 4: + options.ColorType = TiffColorType.WhiteIsZero4; + break; + + case 1: + options.ColorType = TiffColorType.WhiteIsZero1; + break; + + default: + options.ColorType = TiffColorType.WhiteIsZero; + break; + } + + break; + } + + case TiffPhotometricInterpretation.BlackIsZero: + { + if (options.BitsPerSample.Channels != 1) + { + TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); + } + + ushort bitsPerChannel = options.BitsPerSample.Channel0; + if (bitsPerChannel > 32) + { + TiffThrowHelper.ThrowNotSupported("Bits per sample is not supported."); + } + + switch (bitsPerChannel) + { + case 32: + if (options.SampleFormat == TiffSampleFormat.Float) + { + options.ColorType = TiffColorType.BlackIsZero32Float; + return; + } + + options.ColorType = TiffColorType.BlackIsZero32; + break; + + case 24: + options.ColorType = TiffColorType.BlackIsZero24; + break; + + case 16: + options.ColorType = TiffColorType.BlackIsZero16; + break; + + case 8: + options.ColorType = TiffColorType.BlackIsZero8; + break; + + case 4: + options.ColorType = TiffColorType.BlackIsZero4; + break; + + case 1: + options.ColorType = TiffColorType.BlackIsZero1; + break; + + default: + options.ColorType = TiffColorType.BlackIsZero; + break; + } + + break; + } + + case TiffPhotometricInterpretation.Rgb: + { + TiffBitsPerSample bitsPerSample = options.BitsPerSample; + if (bitsPerSample.Channels is not (3 or 4)) + { + TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); + } + + if ((bitsPerSample.Channels == 3 && !(bitsPerSample.Channel0 == bitsPerSample.Channel1 && bitsPerSample.Channel1 == bitsPerSample.Channel2)) || + (bitsPerSample.Channels == 4 && !(bitsPerSample.Channel0 == bitsPerSample.Channel1 && bitsPerSample.Channel1 == bitsPerSample.Channel2 && bitsPerSample.Channel2 == bitsPerSample.Channel3))) + { + TiffThrowHelper.ThrowNotSupported("Only BitsPerSample with equal bits per channel are supported."); + } + + if (options.PlanarConfiguration == TiffPlanarConfiguration.Chunky) + { + ushort bitsPerChannel = options.BitsPerSample.Channel0; + switch (bitsPerChannel) + { + case 32: + if (options.SampleFormat == TiffSampleFormat.Float) + { + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.RgbFloat323232 : TiffColorType.RgbaFloat32323232; + return; + } + + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb323232 : TiffColorType.Rgba32323232; + break; + + case 24: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb242424 : TiffColorType.Rgba24242424; + break; + + case 16: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb161616 : TiffColorType.Rgba16161616; + break; + + case 14: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb141414 : TiffColorType.Rgba14141414; + break; + + case 12: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb121212 : TiffColorType.Rgba12121212; + break; + + case 10: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb101010 : TiffColorType.Rgba10101010; + break; + + case 8: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb888 : TiffColorType.Rgba8888; + break; + case 6: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb666 : TiffColorType.Rgba6666; + break; + case 5: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb555 : TiffColorType.Rgba5555; + break; + case 4: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb444 : TiffColorType.Rgba4444; + break; + case 3: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb333 : TiffColorType.Rgba3333; + break; + case 2: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb222 : TiffColorType.Rgba2222; + break; + default: + TiffThrowHelper.ThrowNotSupported("Bits per sample is not supported."); + break; + } + } + else + { + ushort bitsPerChannel = options.BitsPerSample.Channel0; + switch (bitsPerChannel) + { + case 32: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb323232Planar : TiffColorType.Rgba32323232Planar; + break; + case 24: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb242424Planar : TiffColorType.Rgba24242424Planar; + break; + case 16: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb161616Planar : TiffColorType.Rgba16161616Planar; + break; + default: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb888Planar : TiffColorType.Rgba8888Planar; + break; + } + } + + break; + } + + case TiffPhotometricInterpretation.PaletteColor: + { + if (exifProfile.TryGetValue(ExifTag.ColorMap, out IExifValue value)) + { + options.ColorMap = value.Value; + if (options.BitsPerSample.Channels is not 1 and not 2) + { + TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); + } + + options.ColorType = TiffColorType.PaletteColor; + } + else + { + TiffThrowHelper.ThrowNotSupported("The TIFF ColorMap entry is missing for a palette color image."); + } + + break; + } + + case TiffPhotometricInterpretation.YCbCr: + { + if (exifProfile.TryGetValue(ExifTag.ColorMap, out IExifValue value)) + { + options.ColorMap = value.Value; + } + + if (options.BitsPerSample.Channels != 3) + { + TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported for YCbCr images."); + } + + ushort bitsPerChannel = options.BitsPerSample.Channel0; + if (bitsPerChannel != 8) + { + TiffThrowHelper.ThrowNotSupported("Only 8 bits per channel is supported for YCbCr images."); + } + + options.ColorType = options.PlanarConfiguration == TiffPlanarConfiguration.Chunky ? TiffColorType.YCbCr : TiffColorType.YCbCrPlanar; + + break; + } + + case TiffPhotometricInterpretation.CieLab: + { + if (options.BitsPerSample.Channels != 3) + { + TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported for CieLab images."); + } + + options.ColorType = options.PlanarConfiguration == TiffPlanarConfiguration.Chunky + ? TiffColorType.CieLab + : TiffColorType.CieLabPlanar; + + break; + } + + case TiffPhotometricInterpretation.Separated: + { + if (options.BitsPerSample.Channels != 4) + { + TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported for CMYK images."); + } + + ushort bitsPerChannel = options.BitsPerSample.Channel0; + if (bitsPerChannel != 8) + { + TiffThrowHelper.ThrowNotSupported("Only 8 bits per channel is supported for CMYK images."); + } + + if (exifProfile.GetValueInternal(ExifTag.InkNames) is not null) + { + TiffThrowHelper.ThrowNotSupported("The custom ink name strings are not supported for CMYK images."); + } + + options.ColorType = TiffColorType.Cmyk; + break; + } + + default: + { + TiffThrowHelper.ThrowNotSupported($"The specified TIFF photometric interpretation is not supported: {options.PhotometricInterpretation}"); + } + + break; + } + } + + private static void ParseCompression(this TiffDecoderCore options, TiffCompression? compression, ExifProfile exifProfile) + { + // Default 1 (No compression) https://www.awaresystems.be/imaging/tiff/tifftags/compression.html + switch (compression ?? TiffCompression.None) + { + case TiffCompression.None: + options.CompressionType = TiffDecoderCompressionType.None; + break; + + case TiffCompression.PackBits: + options.CompressionType = TiffDecoderCompressionType.PackBits; + break; + + case TiffCompression.Deflate: + case TiffCompression.OldDeflate: + options.CompressionType = TiffDecoderCompressionType.Deflate; + break; + + case TiffCompression.Lzw: + options.CompressionType = TiffDecoderCompressionType.Lzw; + break; + + case TiffCompression.CcittGroup3Fax: + { + options.CompressionType = TiffDecoderCompressionType.T4; + + if (exifProfile.TryGetValue(ExifTag.T4Options, out IExifValue t4OptionsValue)) + { + options.FaxCompressionOptions = (FaxCompressionOptions)t4OptionsValue.Value; + } + else + { + options.FaxCompressionOptions = FaxCompressionOptions.None; + } + + // Some encoders do not set the BitsPerSample correctly, so we set those values here to the required values: + // https://github.com/SixLabors/ImageSharp/issues/2587 + options.BitsPerSample = new TiffBitsPerSample(1, 0, 0); + options.BitsPerPixel = 1; + + break; + } + + case TiffCompression.CcittGroup4Fax: + { + options.CompressionType = TiffDecoderCompressionType.T6; + if (exifProfile.TryGetValue(ExifTag.T4Options, out IExifValue t4OptionsValue)) + { + options.FaxCompressionOptions = (FaxCompressionOptions)t4OptionsValue.Value; + } + else + { + options.FaxCompressionOptions = FaxCompressionOptions.None; + } + + options.BitsPerSample = new TiffBitsPerSample(1, 0, 0); + options.BitsPerPixel = 1; + + break; + } + + case TiffCompression.Ccitt1D: + options.CompressionType = TiffDecoderCompressionType.HuffmanRle; + options.BitsPerSample = new TiffBitsPerSample(1, 0, 0); + options.BitsPerPixel = 1; + + break; + + case TiffCompression.OldJpeg: + if (!options.OldJpegCompressionStartOfImageMarker.HasValue) + { + TiffThrowHelper.ThrowNotSupported("Missing SOI marker offset for tiff with old jpeg compression"); + } + + if (options.PlanarConfiguration is TiffPlanarConfiguration.Planar) + { + TiffThrowHelper.ThrowNotSupported("Old Jpeg compression is not supported with planar configuration"); + } + + options.CompressionType = TiffDecoderCompressionType.OldJpeg; + if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr) + { + // Note: Setting PhotometricInterpretation and color type to RGB here, since the jpeg decoder will handle the conversion of the pixel data. + options.PhotometricInterpretation = TiffPhotometricInterpretation.Rgb; + options.ColorType = TiffColorType.Rgb; + } + + break; + + case TiffCompression.Jpeg: + options.CompressionType = TiffDecoderCompressionType.Jpeg; + + // Some tiff encoder set this to values different from [1, 1]. The jpeg decoder already handles this, + // so we set this always to [1, 1], see: https://github.com/SixLabors/ImageSharp/issues/2679 + if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr && options.YcbcrSubSampling != null) + { + options.YcbcrSubSampling[0] = 1; + options.YcbcrSubSampling[1] = 1; + } + + if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr && options.JpegTables is null) + { + // Note: Setting PhotometricInterpretation and color type to RGB here, since the jpeg decoder will handle the conversion of the pixel data. + options.PhotometricInterpretation = TiffPhotometricInterpretation.Rgb; + options.ColorType = TiffColorType.Rgb; + } + + break; + + case TiffCompression.Webp: + options.CompressionType = TiffDecoderCompressionType.Webp; + break; + + default: + TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format '{compression}' is not supported"); + break; + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/TiffEncoder.cs b/ImageSharp/Formats/Tiff/TiffEncoder.cs new file mode 100644 index 0000000..24ed8bc --- /dev/null +++ b/ImageSharp/Formats/Tiff/TiffEncoder.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Processing; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Tiff { + /// + /// Encoder for writing the data image to a stream in TIFF format. + /// + public class TiffEncoder : QuantizingImageEncoder + { + /// + /// Initializes a new instance of the class. + /// + public TiffEncoder() => this.Quantizer = KnownQuantizers.Hexadecatree; + + /// + /// Gets the number of bits per pixel. + /// + public TiffBitsPerPixel? BitsPerPixel { get; init; } + + /// + /// Gets the compression type to use. + /// + public TiffCompression? Compression { get; init; } + + /// + /// Gets the compression level 1-9 for the deflate compression mode. + /// Defaults to . + /// + public DeflateCompressionLevel? CompressionLevel { get; init; } + + /// + /// Gets the PhotometricInterpretation to use. Possible options are RGB, RGB with a color palette, gray or BiColor. + /// If no PhotometricInterpretation is specified or it is unsupported by the encoder, RGB will be used. + /// + public TiffPhotometricInterpretation? PhotometricInterpretation { get; init; } + + /// + /// Gets a value indicating which horizontal prediction to use. This can improve the compression ratio with deflate or lzw compression. + /// + public TiffPredictor? HorizontalPredictor { get; init; } + + /// + protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) + { + TiffEncoderCore encode = new(this, image.Configuration); + encode.Encode(image, stream, cancellationToken); + } + } +} diff --git a/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/ImageSharp/Formats/Tiff/TiffEncoderCore.cs new file mode 100644 index 0000000..ee9c67f --- /dev/null +++ b/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -0,0 +1,468 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading; +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Tiff.Writers; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Formats.Tiff { + /// + /// Performs the TIFF encoding operation. + /// + internal sealed class TiffEncoderCore + { + private static readonly ushort ByteOrderMarker = BitConverter.IsLittleEndian + ? TiffConstants.ByteOrderLittleEndianShort + : TiffConstants.ByteOrderBigEndianShort; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The global configuration. + /// + private Configuration configuration; + + /// + /// The quantizer for creating color palette images. + /// + private readonly IQuantizer quantizer; + + /// + /// The pixel sampling strategy for quantization. + /// + private readonly IPixelSamplingStrategy pixelSamplingStrategy; + + /// + /// Sets the deflate compression level. + /// + private readonly DeflateCompressionLevel compressionLevel; + + /// + /// The transparent color mode to use when encoding. + /// + private readonly TransparentColorMode transparentColorMode; + + /// + /// Whether to skip metadata during encoding. + /// + private readonly bool skipMetadata; + + private readonly List<(long, uint)> frameMarkers = []; + + /// + /// Initializes a new instance of the class. + /// + /// The options for the encoder. + /// The global configuration. + public TiffEncoderCore(TiffEncoder encoder, Configuration configuration) + { + this.configuration = configuration; + this.memoryAllocator = configuration.MemoryAllocator; + this.PhotometricInterpretation = encoder.PhotometricInterpretation; + this.quantizer = encoder.Quantizer ?? KnownQuantizers.Hexadecatree; + this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; + this.BitsPerPixel = encoder.BitsPerPixel; + this.HorizontalPredictor = encoder.HorizontalPredictor; + this.CompressionType = encoder.Compression; + this.compressionLevel = encoder.CompressionLevel ?? DeflateCompressionLevel.DefaultCompression; + this.skipMetadata = encoder.SkipMetadata; + this.transparentColorMode = encoder.TransparentColorMode; + } + + /// + /// Gets the photometric interpretation implementation to use when encoding the image. + /// + internal TiffPhotometricInterpretation? PhotometricInterpretation { get; private set; } + + /// + /// Gets or sets the compression implementation to use when encoding the image. + /// + internal TiffCompression? CompressionType { get; set; } + + /// + /// Gets or sets a value indicating which horizontal predictor to use. This can improve the compression ratio with deflate compression. + /// + internal TiffPredictor? HorizontalPredictor { get; set; } + + /// + /// Gets the bits per pixel. + /// + internal TiffBitsPerPixel? BitsPerPixel { get; private set; } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + this.configuration = image.Configuration; + + ImageFrameMetadata rootFrameMetaData = image.Frames.RootFrame.Metadata; + TiffFrameMetadata rootFrameTiffMetaData = rootFrameMetaData.GetTiffMetadata(); + + // Determine the correct values to encode with. + // EncoderOptions > Metadata > Default. + TiffBitsPerPixel bitsPerPixel = this.BitsPerPixel ?? rootFrameTiffMetaData.BitsPerPixel; + + TiffPhotometricInterpretation photometricInterpretation = this.PhotometricInterpretation ?? rootFrameTiffMetaData.PhotometricInterpretation; + + TiffPredictor predictor = this.HorizontalPredictor ?? rootFrameTiffMetaData.Predictor; + + TiffCompression compression = this.CompressionType ?? rootFrameTiffMetaData.Compression; + + // Make sure the Encoder options makes sense in combination with each other. + this.SanitizeAndSetEncoderOptions(bitsPerPixel, photometricInterpretation, compression, predictor); + + using TiffStreamWriter writer = new(stream); + Span buffer = stackalloc byte[4]; + + long ifdMarker = WriteHeader(writer, buffer); + + Image? imageMetadata = image; + + foreach (ImageFrame frame in image.Frames) + { + ImageFrame? clonedFrame = null; + try + { + cancellationToken.ThrowIfCancellationRequested(); + + // TODO: Try to avoid cloning the frame if possible. + // We should be cloning individual scanlines instead. + if (EncodingUtilities.ShouldReplaceTransparentPixels(this.transparentColorMode)) + { + clonedFrame = frame.Clone(); + EncodingUtilities.ReplaceTransparentPixels(clonedFrame); + } + + ImageFrame encodingFrame = clonedFrame ?? frame; + + ifdMarker = this.WriteFrame(writer, encodingFrame, image.Metadata, imageMetadata, this.BitsPerPixel.Value, this.CompressionType.Value, ifdMarker); + imageMetadata = null; + } + finally + { + clonedFrame?.Dispose(); + } + } + + long currentOffset = writer.BaseStream.Position; + foreach ((long, uint) marker in this.frameMarkers) + { + writer.WriteMarkerFast(marker.Item1, marker.Item2, buffer); + } + + writer.BaseStream.Seek(currentOffset, SeekOrigin.Begin); + } + + /// + /// Writes the TIFF file header. + /// + /// The to write data to. + /// Scratch buffer with minimum size of 2. + /// + /// The marker to write the first IFD offset. + /// + public static long WriteHeader(TiffStreamWriter writer, Span buffer) + { + writer.Write(ByteOrderMarker, buffer); + writer.Write(TiffConstants.HeaderMagicNumber, buffer); + return writer.PlaceMarker(buffer); + } + + /// + /// Writes all data required to define an image. + /// + /// The pixel format. + /// The to write data to. + /// The tiff frame. + /// The image metadata (resolution values for each frame). + /// The image (common metadata for root frame). + /// The bits per pixel. + /// The compression type. + /// The marker to write this IFD offset. + /// + /// The next IFD offset value. + /// + private long WriteFrame( + TiffStreamWriter writer, + ImageFrame frame, + ImageMetadata imageMetadata, + Image? image, + TiffBitsPerPixel bitsPerPixel, + TiffCompression compression, + long ifdOffset) + where TPixel : unmanaged, IPixel + { + // Get the width and height of the frame. + // This can differ from the frame bounds in-memory if the image represents only + // a subregion. + TiffFrameMetadata frameMetaData = frame.Metadata.GetTiffMetadata(); + int width = frameMetaData.EncodingWidth > 0 ? frameMetaData.EncodingWidth : frame.Width; + int height = frameMetaData.EncodingHeight > 0 ? frameMetaData.EncodingHeight : frame.Height; + + width = Math.Min(width, frame.Width); + height = Math.Min(height, frame.Height); + Size encodingSize = new(width, height); + + TiffEncoderEntriesCollector entriesCollector = new(); + using TiffBaseColorWriter colorWriter = TiffColorWriterFactory.Create( + this.PhotometricInterpretation, + frame, + encodingSize, + this.quantizer, + this.pixelSamplingStrategy, + this.memoryAllocator, + this.configuration, + entriesCollector, + (int)bitsPerPixel); + + using TiffBaseCompressor compressor = TiffCompressorFactory.Create( + compression, + writer.BaseStream, + this.memoryAllocator, + width, + colorWriter.BitsPerPixel, + this.compressionLevel, + this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None); + + int rowsPerStrip = CalcRowsPerStrip(height, colorWriter.BytesPerRow, this.CompressionType); + + colorWriter.Write(compressor, rowsPerStrip); + + if (image != null) + { + // Write the metadata for the root image + entriesCollector.ProcessMetadata(image, this.skipMetadata); + } + + // Write the metadata for the frame + entriesCollector.ProcessMetadata(frame, this.skipMetadata); + + entriesCollector.ProcessFrameInfo(frame, encodingSize, imageMetadata); + entriesCollector.ProcessImageFormat(this); + + if (writer.Position % 2 != 0) + { + // Write padding byte, because the tiff spec requires ifd offset to begin on a word boundary. + writer.Write(0); + } + + this.frameMarkers.Add((ifdOffset, (uint)writer.Position)); + + return this.WriteIfd(writer, entriesCollector.Entries); + } + + /// + /// Calculates the number of rows written per strip. + /// + /// The height of the image. + /// The number of bytes per row. + /// The compression used. + /// Number of rows per strip. + private static int CalcRowsPerStrip(int height, int bytesPerRow, TiffCompression? compression) + { + DebugGuard.MustBeGreaterThan(height, 0, nameof(height)); + DebugGuard.MustBeGreaterThan(bytesPerRow, 0, nameof(bytesPerRow)); + + // Jpeg compressed images should be written in one strip. + if (compression is TiffCompression.Jpeg) + { + return height; + } + + // If compression is used, change stripSizeInBytes heuristically to a larger value to not write to many strips. + int stripSizeInBytes = compression is TiffCompression.Deflate || compression is TiffCompression.Lzw ? TiffConstants.DefaultStripSize * 2 : TiffConstants.DefaultStripSize; + int rowsPerStrip = stripSizeInBytes / bytesPerRow; + + if (rowsPerStrip > 0) + { + if (rowsPerStrip < height) + { + return rowsPerStrip; + } + + return height; + } + + return 1; + } + + /// + /// Writes a TIFF IFD block. + /// + /// The to write data to. + /// The IFD entries to write to the file. + /// The marker to write the next IFD offset (if present). + private long WriteIfd(TiffStreamWriter writer, List entries) + { + if (entries.Count == 0) + { + TiffThrowHelper.ThrowArgumentException("There must be at least one entry per IFD."); + } + + uint dataOffset = (uint)writer.Position + (uint)(6 + (entries.Count * 12)); + List largeDataBlocks = []; + + entries.Sort((a, b) => (ushort)a.Tag - (ushort)b.Tag); + + Span buffer = stackalloc byte[4]; + + writer.Write((ushort)entries.Count, buffer); + + foreach (IExifValue entry in entries) + { + writer.Write((ushort)entry.Tag, buffer); + writer.Write((ushort)entry.DataType, buffer); + writer.Write(ExifWriter.GetNumberOfComponents(entry), buffer); + + uint length = ExifWriter.GetLength(entry); + if (length <= 4) + { + int sz = ExifWriter.WriteValue(entry, buffer, 0); + DebugGuard.IsTrue(sz == length, "Incorrect number of bytes written"); + writer.WritePadded(buffer[..sz]); + } + else + { + byte[] raw = new byte[length]; + int sz = ExifWriter.WriteValue(entry, raw, 0); + DebugGuard.IsTrue(sz == raw.Length, "Incorrect number of bytes written"); + largeDataBlocks.Add(raw); + writer.Write(dataOffset, buffer); + dataOffset += (uint)(raw.Length + (raw.Length % 2)); + } + } + + long nextIfdMarker = writer.PlaceMarker(buffer); + + foreach (byte[] dataBlock in largeDataBlocks) + { + writer.Write(dataBlock); + + if (dataBlock.Length % 2 == 1) + { + writer.Write(0); + } + } + + return nextIfdMarker; + } + + [MemberNotNull(nameof(BitsPerPixel), nameof(PhotometricInterpretation), nameof(CompressionType), nameof(HorizontalPredictor))] + private void SanitizeAndSetEncoderOptions( + TiffBitsPerPixel bitsPerPixel, + TiffPhotometricInterpretation photometricInterpretation, + TiffCompression compression, + TiffPredictor predictor) + { + // Ensure 1 Bit compression is only used with 1 bit pixel type. + // Choose a sensible default based on the bits per pixel. + if (IsOneBitCompression(compression) && bitsPerPixel != TiffBitsPerPixel.Bit1) + { + compression = bitsPerPixel switch + { + < TiffBitsPerPixel.Bit8 => TiffCompression.None, + _ => TiffCompression.Deflate, + }; + } + + // Ensure predictor is only used with compression that supports it. + predictor = HasPredictor(compression) ? predictor : TiffPredictor.None; + + // BitsPerPixel should be the primary source of truth for the encoder options. + switch (bitsPerPixel) + { + case TiffBitsPerPixel.Bit1: + if (IsOneBitCompression(compression)) + { + // The “normal” PhotometricInterpretation for bilevel CCITT compressed data is WhiteIsZero. + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.WhiteIsZero, compression, predictor); + break; + } + + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, predictor); + break; + case TiffBitsPerPixel.Bit4: + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, compression, predictor); + break; + case TiffBitsPerPixel.Bit8: + + // Allow any combination of the below for 8 bit images. + if (photometricInterpretation is TiffPhotometricInterpretation.BlackIsZero + or TiffPhotometricInterpretation.WhiteIsZero + or TiffPhotometricInterpretation.PaletteColor) + { + this.SetEncoderOptions(bitsPerPixel, photometricInterpretation, compression, predictor); + break; + } + + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, compression, predictor); + break; + case TiffBitsPerPixel.Bit16: + // Assume desire to encode as L16 grayscale + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, predictor); + break; + case TiffBitsPerPixel.Bit6: + case TiffBitsPerPixel.Bit10: + case TiffBitsPerPixel.Bit12: + case TiffBitsPerPixel.Bit14: + case TiffBitsPerPixel.Bit30: + case TiffBitsPerPixel.Bit36: + case TiffBitsPerPixel.Bit42: + case TiffBitsPerPixel.Bit48: + // Encoding not yet supported bits per pixel will default to 24 bits. + this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, predictor); + break; + case TiffBitsPerPixel.Bit64: + // Encoding not yet supported bits per pixel will default to 32 bits. + this.SetEncoderOptions(TiffBitsPerPixel.Bit32, TiffPhotometricInterpretation.Rgb, compression, predictor); + break; + default: + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.Rgb, compression, predictor); + break; + } + } + + [MemberNotNull(nameof(BitsPerPixel), nameof(PhotometricInterpretation), nameof(CompressionType), nameof(HorizontalPredictor))] + private void SetEncoderOptions( + TiffBitsPerPixel bitsPerPixel, + TiffPhotometricInterpretation photometricInterpretation, + TiffCompression compression, + TiffPredictor predictor) + { + this.BitsPerPixel = bitsPerPixel; + this.PhotometricInterpretation = photometricInterpretation; + this.CompressionType = compression; + this.HorizontalPredictor = predictor; + } + + public static bool IsOneBitCompression(TiffCompression? compression) + => compression is TiffCompression.Ccitt1D or TiffCompression.CcittGroup3Fax or TiffCompression.CcittGroup4Fax; + + public static bool HasPredictor(TiffCompression? compression) + => compression is TiffCompression.Deflate or TiffCompression.Lzw; + } +} diff --git a/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs new file mode 100644 index 0000000..fadbf35 --- /dev/null +++ b/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -0,0 +1,447 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +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.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Iptc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Tiff { + internal class TiffEncoderEntriesCollector + { + private const string SoftwareValue = "ImageSharp"; + + public List Entries { get; } = []; + + public void ProcessMetadata(Image image, bool skipMetadata) + => new MetadataProcessor(this).Process(image, skipMetadata); + + public void ProcessMetadata(ImageFrame frame, bool skipMetadata) + => new MetadataProcessor(this).Process(frame, skipMetadata); + + public void ProcessFrameInfo(ImageFrame frame, Size encodingSize, ImageMetadata imageMetadata) + => new FrameInfoProcessor(this).Process(frame, encodingSize, imageMetadata); + + public void ProcessImageFormat(TiffEncoderCore encoder) + => new ImageFormatProcessor(this).Process(encoder); + + public void AddOrReplace(IExifValue entry) + { + int index = this.Entries.FindIndex(t => t.Tag == entry.Tag); + if (index >= 0) + { + this.Entries[index] = entry; + } + else + { + this.Entries.Add(entry); + } + } + + private void Add(IExifValue entry) => this.Entries.Add(entry); + + private abstract class BaseProcessor + { + protected BaseProcessor(TiffEncoderEntriesCollector collector) => this.Collector = collector; + + protected TiffEncoderEntriesCollector Collector { get; } + } + + private class MetadataProcessor : BaseProcessor + { + public MetadataProcessor(TiffEncoderEntriesCollector collector) + : base(collector) + { + } + + public void Process(Image image, bool skipMetadata) + { + this.ProcessProfiles(image.Metadata, skipMetadata); + + if (!skipMetadata) + { + this.ProcessMetadata(image.Metadata.ExifProfile ?? new ExifProfile()); + } + + if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software)) + { + this.Collector.Add(new ExifString(ExifTagValue.Software) + { + Value = SoftwareValue + }); + } + } + + public void Process(ImageFrame frame, bool skipMetadata) + { + this.ProcessProfiles(frame.Metadata, skipMetadata); + + if (!skipMetadata) + { + this.ProcessMetadata(frame.Metadata.ExifProfile ?? new ExifProfile()); + } + + if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software)) + { + this.Collector.Add(new ExifString(ExifTagValue.Software) + { + Value = SoftwareValue + }); + } + } + + private static bool IsPureMetadata(ExifTag tag) + => (ExifTagValue)(ushort)tag switch + { + ExifTagValue.DocumentName or + ExifTagValue.ImageDescription or + ExifTagValue.Make or + ExifTagValue.Model or + ExifTagValue.Software or + ExifTagValue.DateTime or + ExifTagValue.Artist or + ExifTagValue.HostComputer or + ExifTagValue.TargetPrinter or + ExifTagValue.XMP or + ExifTagValue.Rating or + ExifTagValue.RatingPercent or + ExifTagValue.ImageID or + ExifTagValue.Copyright or + ExifTagValue.MDLabName or + ExifTagValue.MDSampleInfo or + ExifTagValue.MDPrepDate or + ExifTagValue.MDPrepTime or + ExifTagValue.MDFileUnits or + ExifTagValue.SEMInfo or + ExifTagValue.XPTitle or + ExifTagValue.XPComment or + ExifTagValue.XPAuthor or + ExifTagValue.XPKeywords or + ExifTagValue.XPSubject => true, + _ => false, + }; + + private void ProcessMetadata(ExifProfile exifProfile) + { + foreach (IExifValue entry in exifProfile.Values) + { + // todo: skip subIfd + if (entry.DataType == ExifDataType.Ifd) + { + continue; + } + + switch ((ExifTagValue)(ushort)entry.Tag) + { + case ExifTagValue.SubIFDOffset: + case ExifTagValue.GPSIFDOffset: + case ExifTagValue.SubIFDs: + case ExifTagValue.XMP: + case ExifTagValue.IPTC: + case ExifTagValue.IccProfile: + continue; + } + + switch (ExifTags.GetPart(entry.Tag)) + { + case ExifParts.ExifTags: + case ExifParts.GpsTags: + break; + + case ExifParts.IfdTags: + if (!IsPureMetadata(entry.Tag)) + { + continue; + } + + break; + } + + if (!this.Collector.Entries.Exists(t => t.Tag == entry.Tag)) + { + this.Collector.AddOrReplace(entry.DeepClone()); + } + } + } + + private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata) + { + this.ProcessExifProfile(skipMetadata, imageMetadata.ExifProfile); + this.ProcessIptcProfile(skipMetadata, imageMetadata.IptcProfile, imageMetadata.ExifProfile); + this.ProcessIccProfile(imageMetadata.IccProfile, imageMetadata.ExifProfile); + this.ProcessXmpProfile(skipMetadata, imageMetadata.XmpProfile, imageMetadata.ExifProfile); + } + + private void ProcessProfiles(ImageFrameMetadata frameMetadata, bool skipMetadata) + { + this.ProcessExifProfile(skipMetadata, frameMetadata.ExifProfile); + this.ProcessIptcProfile(skipMetadata, frameMetadata.IptcProfile, frameMetadata.ExifProfile); + this.ProcessIccProfile(frameMetadata.IccProfile, frameMetadata.ExifProfile); + this.ProcessXmpProfile(skipMetadata, frameMetadata.XmpProfile, frameMetadata.ExifProfile); + } + + private void ProcessExifProfile(bool skipMetadata, ExifProfile exifProfile) + { + if (!skipMetadata && (exifProfile != null && exifProfile.Parts != ExifParts.None)) + { + foreach (IExifValue entry in exifProfile.Values) + { + if (!this.Collector.Entries.Exists(t => t.Tag == entry.Tag) && entry.GetValue() != null) + { + ExifParts entryPart = ExifTags.GetPart(entry.Tag); + if (entryPart != ExifParts.None && exifProfile.Parts.HasFlag(entryPart)) + { + this.Collector.AddOrReplace(entry.DeepClone()); + } + } + } + } + else + { + exifProfile?.RemoveValue(ExifTag.SubIFDOffset); + } + } + + private void ProcessIptcProfile(bool skipMetadata, IptcProfile iptcProfile, ExifProfile exifProfile) + { + if (!skipMetadata && iptcProfile != null) + { + iptcProfile.UpdateData(); + ExifByteArray iptc = new(ExifTagValue.IPTC, ExifDataType.Byte) + { + Value = iptcProfile.Data + }; + + this.Collector.AddOrReplace(iptc); + } + else + { + exifProfile?.RemoveValue(ExifTag.IPTC); + } + } + + private void ProcessIccProfile(IccProfile iccProfile, ExifProfile exifProfile) + { + if (iccProfile != null) + { + ExifByteArray icc = new(ExifTagValue.IccProfile, ExifDataType.Undefined) + { + Value = iccProfile.ToByteArray() + }; + + this.Collector.AddOrReplace(icc); + } + else + { + exifProfile?.RemoveValue(ExifTag.IccProfile); + } + } + + private void ProcessXmpProfile(bool skipMetadata, XmpProfile xmpProfile, ExifProfile exifProfile) + { + if (!skipMetadata && xmpProfile != null) + { + ExifByteArray xmp = new(ExifTagValue.XMP, ExifDataType.Byte) + { + Value = xmpProfile.Data + }; + + this.Collector.AddOrReplace(xmp); + } + else + { + exifProfile?.RemoveValue(ExifTag.XMP); + } + } + } + + private class FrameInfoProcessor : BaseProcessor + { + public FrameInfoProcessor(TiffEncoderEntriesCollector collector) + : base(collector) + { + } + + public void Process(ImageFrame frame, Size encodingSize, ImageMetadata imageMetadata) + { + this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageWidth) + { + Value = (uint)encodingSize.Width + }); + + this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageLength) + { + Value = (uint)encodingSize.Height + }); + + this.ProcessResolution(imageMetadata); + } + + private void ProcessResolution(ImageMetadata imageMetadata) + { + ExifResolutionValues resolution = UnitConverter.GetExifResolutionValues( + imageMetadata.ResolutionUnits, + imageMetadata.HorizontalResolution, + imageMetadata.VerticalResolution); + + this.Collector.AddOrReplace(new ExifShort(ExifTagValue.ResolutionUnit) + { + Value = resolution.ResolutionUnit + }); + + if (resolution.VerticalResolution.HasValue && resolution.HorizontalResolution.HasValue) + { + this.Collector.AddOrReplace(new ExifRational(ExifTagValue.XResolution) + { + Value = new Rational(resolution.HorizontalResolution.Value) + }); + + this.Collector.AddOrReplace(new ExifRational(ExifTagValue.YResolution) + { + Value = new Rational(resolution.VerticalResolution.Value) + }); + } + } + } + + private class ImageFormatProcessor : BaseProcessor + { + public ImageFormatProcessor(TiffEncoderEntriesCollector collector) + : base(collector) + { + } + + public void Process(TiffEncoderCore encoder) + { + ExifShort planarConfig = new(ExifTagValue.PlanarConfiguration) + { + Value = (ushort)TiffPlanarConfiguration.Chunky + }; + + ExifShort samplesPerPixel = new(ExifTagValue.SamplesPerPixel) + { + Value = GetSamplesPerPixel(encoder) + }; + + ushort[] bitsPerSampleValue = GetBitsPerSampleValue(encoder); + ExifShortArray bitPerSample = new(ExifTagValue.BitsPerSample) + { + Value = bitsPerSampleValue + }; + + ushort compressionType = GetCompressionType(encoder); + ExifShort compression = new(ExifTagValue.Compression) + { + Value = compressionType + }; + + ExifShort photometricInterpretation = new(ExifTagValue.PhotometricInterpretation) + { + Value = (ushort)encoder.PhotometricInterpretation + }; + + this.Collector.AddOrReplace(planarConfig); + this.Collector.AddOrReplace(samplesPerPixel); + this.Collector.AddOrReplace(bitPerSample); + this.Collector.AddOrReplace(compression); + this.Collector.AddOrReplace(photometricInterpretation); + + if (encoder.HorizontalPredictor == TiffPredictor.Horizontal && + (encoder.PhotometricInterpretation is TiffPhotometricInterpretation.Rgb or + TiffPhotometricInterpretation.PaletteColor or + TiffPhotometricInterpretation.BlackIsZero)) + { + ExifShort predictor = new(ExifTagValue.Predictor) { Value = (ushort)TiffPredictor.Horizontal }; + + this.Collector.AddOrReplace(predictor); + } + } + + private static ushort GetSamplesPerPixel(TiffEncoderCore encoder) + => encoder.PhotometricInterpretation switch + { + TiffPhotometricInterpretation.PaletteColor or + TiffPhotometricInterpretation.BlackIsZero or + TiffPhotometricInterpretation.WhiteIsZero => 1, + _ => 3, + }; + + private static ushort[] GetBitsPerSampleValue(TiffEncoderCore encoder) + { + switch (encoder.PhotometricInterpretation) + { + case TiffPhotometricInterpretation.PaletteColor: + if (encoder.BitsPerPixel == TiffBitsPerPixel.Bit4) + { + return TiffConstants.BitsPerSample4Bit.ToArray(); + } + + return TiffConstants.BitsPerSample8Bit.ToArray(); + + case TiffPhotometricInterpretation.Rgb: + return TiffConstants.BitsPerSampleRgb8Bit.ToArray(); + + case TiffPhotometricInterpretation.WhiteIsZero: + return encoder.BitsPerPixel switch + { + TiffBitsPerPixel.Bit1 => TiffConstants.BitsPerSample1Bit.ToArray(), + TiffBitsPerPixel.Bit16 => TiffConstants.BitsPerSample16Bit.ToArray(), + _ => TiffConstants.BitsPerSample8Bit.ToArray() + }; + + case TiffPhotometricInterpretation.BlackIsZero: + return encoder.BitsPerPixel switch + { + TiffBitsPerPixel.Bit1 => TiffConstants.BitsPerSample1Bit.ToArray(), + TiffBitsPerPixel.Bit16 => TiffConstants.BitsPerSample16Bit.ToArray(), + _ => TiffConstants.BitsPerSample8Bit.ToArray() + }; + + default: + return TiffConstants.BitsPerSampleRgb8Bit.ToArray(); + } + } + + private static ushort GetCompressionType(TiffEncoderCore encoder) + { + switch (encoder.CompressionType) + { + case TiffCompression.Deflate: + // Deflate is allowed for all modes. + return (ushort)TiffCompression.Deflate; + case TiffCompression.PackBits: + // PackBits is allowed for all modes. + return (ushort)TiffCompression.PackBits; + case TiffCompression.Lzw: + if (encoder.PhotometricInterpretation is TiffPhotometricInterpretation.Rgb or + TiffPhotometricInterpretation.PaletteColor or + TiffPhotometricInterpretation.BlackIsZero) + { + return (ushort)TiffCompression.Lzw; + } + + break; + + case TiffCompression.CcittGroup3Fax: + return (ushort)TiffCompression.CcittGroup3Fax; + + case TiffCompression.CcittGroup4Fax: + return (ushort)TiffCompression.CcittGroup4Fax; + + case TiffCompression.Ccitt1D: + return (ushort)TiffCompression.Ccitt1D; + + case TiffCompression.Jpeg: + return (ushort)TiffCompression.Jpeg; + } + + return (ushort)TiffCompression.None; + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/TiffExtraSampleType.cs b/ImageSharp/Formats/Tiff/TiffExtraSampleType.cs new file mode 100644 index 0000000..10bed2f --- /dev/null +++ b/ImageSharp/Formats/Tiff/TiffExtraSampleType.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tiff { + /// + /// Description of extra components. + /// + internal enum TiffExtraSampleType + { + /// + /// The data is unspecified, not supported. + /// + UnspecifiedData = 0, + + /// + /// The extra data is associated alpha data (with pre-multiplied color). + /// + AssociatedAlphaData = 1, + + /// + /// The extra data is unassociated alpha data is transparency information that logically exists independent of an image; + /// it is commonly called a soft matte. + /// + UnassociatedAlphaData = 2, + + /// + /// A CorelDRAW-specific value observed in damaged files, indicating unassociated alpha. + /// Not part of the official TIFF specification; patched in ImageSharp for compatibility. + /// + CorelDrawUnassociatedAlphaData = 999, + } +} diff --git a/ImageSharp/Formats/Tiff/TiffFormat.cs b/ImageSharp/Formats/Tiff/TiffFormat.cs new file mode 100644 index 0000000..b451883 --- /dev/null +++ b/ImageSharp/Formats/Tiff/TiffFormat.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Tiff { + /// + /// Encapsulates the means to encode and decode Tiff images. + /// + public sealed class TiffFormat : IImageFormat + { + private TiffFormat() + { + } + + /// + /// Gets the shared instance. + /// + public static TiffFormat Instance { get; } = new(); + + /// + public string Name => "TIFF"; + + /// + public string DefaultMimeType => "image/tiff"; + + /// + public IEnumerable MimeTypes => TiffConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => TiffConstants.FileExtensions; + + /// + public TiffMetadata CreateDefaultFormatMetadata() => new(); + + /// + public TiffFrameMetadata CreateDefaultFormatFrameMetadata() => new(); + } +} diff --git a/ImageSharp/Formats/Tiff/TiffFormatType.cs b/ImageSharp/Formats/Tiff/TiffFormatType.cs new file mode 100644 index 0000000..c4cb165 --- /dev/null +++ b/ImageSharp/Formats/Tiff/TiffFormatType.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Tiff { + /// + /// The TIFF format type enum. + /// + public enum TiffFormatType + { + /// + /// The TIFF file format type. + /// + Default, + + /// + /// The BigTIFF format type. + /// + BigTIFF + } +} diff --git a/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs b/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs new file mode 100644 index 0000000..a562e90 --- /dev/null +++ b/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs @@ -0,0 +1,230 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff { + /// + /// Provides Tiff specific metadata information for the frame. + /// + public class TiffFrameMetadata : IFormatFrameMetadata + { + /// + /// Initializes a new instance of the class. + /// + public TiffFrameMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The other tiff frame metadata. + private TiffFrameMetadata(TiffFrameMetadata other) + { + this.BitsPerPixel = other.BitsPerPixel; + this.Compression = other.Compression; + this.PhotometricInterpretation = other.PhotometricInterpretation; + this.Predictor = other.Predictor; + this.InkSet = other.InkSet; + this.EncodingWidth = other.EncodingWidth; + this.EncodingHeight = other.EncodingHeight; + + if (other.LocalColorTable?.Length > 0) + { + this.LocalColorTable = other.LocalColorTable.Value.ToArray(); + } + } + + /// + /// Gets or sets the bits per pixel. + /// + public TiffBitsPerPixel BitsPerPixel { get; set; } = TiffConstants.DefaultBitsPerPixel; + + /// + /// Gets or sets number of bits per component. + /// + public TiffBitsPerSample BitsPerSample { get; set; } = TiffConstants.DefaultBitsPerSample; + + /// + /// Gets or sets the compression scheme used on the image data. + /// + public TiffCompression Compression { get; set; } = TiffConstants.DefaultCompression; + + /// + /// Gets or sets the color space of the image data. + /// + public TiffPhotometricInterpretation PhotometricInterpretation { get; set; } = TiffConstants.DefaultPhotometricInterpretation; + + /// + /// Gets or sets a mathematical operator that is applied to the image data before an encoding scheme is applied. + /// + public TiffPredictor Predictor { get; set; } = TiffConstants.DefaultPredictor; + + /// + /// Gets or sets the set of inks used in a separated () image. + /// + public TiffInkSet? InkSet { get; set; } + + /// + /// Gets or sets the encoding width. + /// + public int EncodingWidth { get; set; } + + /// + /// Gets or sets the encoding height. + /// + public int EncodingHeight { get; set; } + + /// + /// Gets or sets the local color table, if any. + /// + public ReadOnlyMemory? LocalColorTable { get; set; } + + /// + public static TiffFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) + { + TiffFrameMetadata frameMetadata = new(); + if (metadata.EncodingWidth.HasValue && metadata.EncodingHeight.HasValue) + { + frameMetadata.EncodingWidth = metadata.EncodingWidth.Value; + frameMetadata.EncodingHeight = metadata.EncodingHeight.Value; + } + + return frameMetadata; + } + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + => new() + { + EncodingWidth = this.EncodingWidth, + EncodingHeight = this.EncodingHeight + }; + + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + this.LocalColorTable = null; + + float ratioX = destination.Width / (float)source.Width; + float ratioY = destination.Height / (float)source.Height; + this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX); + this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY); + + // Overwrite the EXIF dimensional metadata with the encoding dimensions of the image. + destination.Metadata.ExifProfile?.SyncDimensions(this.EncodingWidth, this.EncodingHeight); + } + + private static int Scale(int value, int destination, float ratio) + { + if (value <= 0) + { + return destination; + } + + return Math.Min((int)MathF.Ceiling(value * ratio), destination); + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public TiffFrameMetadata DeepClone() => new(this); + + /// + /// Returns a new instance parsed from the given Exif profile. + /// + /// The Exif profile containing tiff frame directory tags to parse. + /// If null, a new instance is created and parsed instead. + /// The . + internal static TiffFrameMetadata Parse(ExifProfile profile) + { + TiffFrameMetadata meta = new(); + Parse(meta, profile); + return meta; + } + + /// + /// Parses the given Exif profile to populate the properties of the tiff frame meta data. + /// + /// The tiff frame meta data. + /// The Exif profile containing tiff frame directory tags. + private static void Parse(TiffFrameMetadata meta, ExifProfile profile) + { + meta.EncodingWidth = GetImageWidth(profile); + meta.EncodingHeight = GetImageHeight(profile); + + if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue? bitsPerSampleValue) + && TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample)) + { + meta.BitsPerSample = bitsPerSample; + } + + meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel(); + + if (profile.TryGetValue(ExifTag.Compression, out IExifValue? compressionValue)) + { + meta.Compression = (TiffCompression)compressionValue.Value; + } + + if (profile.TryGetValue(ExifTag.PhotometricInterpretation, out IExifValue? photometricInterpretationValue)) + { + meta.PhotometricInterpretation = (TiffPhotometricInterpretation)photometricInterpretationValue.Value; + } + + if (profile.TryGetValue(ExifTag.Predictor, out IExifValue? predictorValue)) + { + meta.Predictor = (TiffPredictor)predictorValue.Value; + } + + if (profile.TryGetValue(ExifTag.InkSet, out IExifValue? inkSetValue)) + { + meta.InkSet = (TiffInkSet)inkSetValue.Value; + } + + // Remove values, we've explicitly captured them and they could change on encode. + profile.RemoveValue(ExifTag.BitsPerSample); + profile.RemoveValue(ExifTag.Compression); + profile.RemoveValue(ExifTag.PhotometricInterpretation); + profile.RemoveValue(ExifTag.Predictor); + } + + /// + /// Gets the width of the image frame. + /// + /// The image frame exif profile. + /// The image width. + private static int GetImageWidth(ExifProfile exifProfile) + { + if (!exifProfile.TryGetValue(ExifTag.ImageWidth, out IExifValue? width)) + { + TiffThrowHelper.ThrowInvalidImageContentException("The TIFF image frame is missing the ImageWidth"); + } + + DebugGuard.MustBeLessThanOrEqualTo((ulong)width.Value, (ulong)int.MaxValue, nameof(ExifTag.ImageWidth)); + + return (int)width.Value; + } + + /// + /// Gets the height of the image frame. + /// + /// The image frame exif profile. + /// The image height. + private static int GetImageHeight(ExifProfile exifProfile) + { + if (!exifProfile.TryGetValue(ExifTag.ImageLength, out IExifValue? height)) + { + TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength"); + } + + return (int)height.Value; + } + } +} diff --git a/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs b/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs new file mode 100644 index 0000000..e896250 --- /dev/null +++ b/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Tiff { + /// + /// Detects tiff file headers + /// + public sealed class TiffImageFormatDetector : IImageFormatDetector + { + /// + public int HeaderSize => 8; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.IsSupportedFileFormat(header) ? TiffFormat.Instance : null; + return format != null; + } + + private bool IsSupportedFileFormat(ReadOnlySpan header) + { + if (header.Length >= this.HeaderSize) + { + if (header[0] == 0x49 && header[1] == 0x49) + { + // Little-endian + if (header[2] == 0x2A && header[3] == 0x00) + { + // tiff + return true; + } + else if (header[2] == 0x2B && header[3] == 0x00 + && header[4] == 8 && header[5] == 0 && header[6] == 0 && header[7] == 0) + { + // big tiff + return true; + } + } + else if (header[0] == 0x4D && header[1] == 0x4D) + { + // Big-endian + if (header[2] == 0 && header[3] == 0x2A) + { + // tiff + return true; + } + else + if (header[2] == 0 && header[3] == 0x2B + && header[4] == 0 && header[5] == 8 && header[6] == 0 && header[7] == 0) + { + // big tiff + return true; + } + } + } + + return false; + } + } +} diff --git a/ImageSharp/Formats/Tiff/TiffMetadata.cs b/ImageSharp/Formats/Tiff/TiffMetadata.cs new file mode 100644 index 0000000..8c9eba8 --- /dev/null +++ b/ImageSharp/Formats/Tiff/TiffMetadata.cs @@ -0,0 +1,195 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff { + /// + /// Provides Tiff specific metadata information for the image. + /// + public class TiffMetadata : IFormatMetadata + { + /// + /// Initializes a new instance of the class. + /// + public TiffMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private TiffMetadata(TiffMetadata other) + { + this.ByteOrder = other.ByteOrder; + this.FormatType = other.FormatType; + this.BitsPerPixel = other.BitsPerPixel; + this.BitsPerSample = other.BitsPerSample; + this.Compression = other.Compression; + this.PhotometricInterpretation = other.PhotometricInterpretation; + this.Predictor = other.Predictor; + } + + /// + /// Gets or sets the byte order. + /// + public ByteOrder ByteOrder { get; set; } + + /// + /// Gets or sets the format type. + /// + public TiffFormatType FormatType { get; set; } + + /// + /// Gets or sets the bits per pixel. Derived from the root frame. + /// + public TiffBitsPerPixel BitsPerPixel { get; set; } = TiffConstants.DefaultBitsPerPixel; + + /// + /// Gets or sets number of bits per component. Derived from the root frame. + /// + public TiffBitsPerSample BitsPerSample { get; set; } = TiffConstants.DefaultBitsPerSample; + + /// + /// Gets or sets the compression scheme used on the image data. Derived from the root frame. + /// + public TiffCompression Compression { get; set; } = TiffConstants.DefaultCompression; + + /// + /// Gets or sets the color space of the image data. Derived from the root frame. + /// + public TiffPhotometricInterpretation PhotometricInterpretation { get; set; } = TiffConstants.DefaultPhotometricInterpretation; + + /// + /// Gets or sets a mathematical operator that is applied to the image data before an encoding scheme is applied. + /// Derived from the root frame. + /// + public TiffPredictor Predictor { get; set; } = TiffConstants.DefaultPredictor; + + /// + public static TiffMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + int bpp = metadata.PixelTypeInfo.BitsPerPixel; + return bpp switch + { + 1 => new TiffMetadata + { + BitsPerPixel = TiffBitsPerPixel.Bit1, + BitsPerSample = TiffConstants.BitsPerSample1Bit, + PhotometricInterpretation = TiffPhotometricInterpretation.WhiteIsZero, + Compression = TiffCompression.CcittGroup4Fax, + Predictor = TiffPredictor.None + }, + <= 4 => new TiffMetadata + { + BitsPerPixel = TiffBitsPerPixel.Bit4, + BitsPerSample = TiffConstants.BitsPerSample4Bit, + PhotometricInterpretation = TiffPhotometricInterpretation.PaletteColor, + Compression = TiffCompression.Deflate, + Predictor = TiffPredictor.None // Best match for low bit depth + }, + 8 => new TiffMetadata + { + BitsPerPixel = TiffBitsPerPixel.Bit8, + BitsPerSample = TiffConstants.BitsPerSample8Bit, + PhotometricInterpretation = TiffPhotometricInterpretation.PaletteColor, + Compression = TiffCompression.Deflate, + Predictor = TiffPredictor.Horizontal + }, + 16 => new TiffMetadata + { + BitsPerPixel = TiffBitsPerPixel.Bit16, + BitsPerSample = TiffConstants.BitsPerSample16Bit, + PhotometricInterpretation = TiffPhotometricInterpretation.BlackIsZero, + Compression = TiffCompression.Deflate, + Predictor = TiffPredictor.Horizontal + }, + 32 or 64 => new TiffMetadata + { + BitsPerPixel = TiffBitsPerPixel.Bit32, + BitsPerSample = TiffConstants.BitsPerSampleRgb8Bit, + PhotometricInterpretation = TiffPhotometricInterpretation.Rgb, + Compression = TiffCompression.Deflate, + Predictor = TiffPredictor.Horizontal + }, + _ => new TiffMetadata + { + BitsPerPixel = TiffBitsPerPixel.Bit24, + BitsPerSample = TiffConstants.BitsPerSampleRgb8Bit, + PhotometricInterpretation = TiffPhotometricInterpretation.Rgb, + Compression = TiffCompression.Deflate, + Predictor = TiffPredictor.Horizontal + } + }; + } + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp = (int)this.BitsPerPixel; + + TiffBitsPerSample samples = this.BitsPerSample; + PixelComponentInfo info = samples.Channels switch + { + 1 => PixelComponentInfo.Create(1, bpp, bpp), + 2 => PixelComponentInfo.Create(2, bpp, bpp, samples.Channel0, samples.Channel1), + 3 => PixelComponentInfo.Create(3, bpp, samples.Channel0, samples.Channel1, samples.Channel2), + _ => PixelComponentInfo.Create(4, bpp, samples.Channel0, samples.Channel1, samples.Channel2, samples.Channel3) + }; + + PixelColorType colorType; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + switch (this.BitsPerPixel) + { + case TiffBitsPerPixel.Bit1: + colorType = PixelColorType.Binary; + break; + case TiffBitsPerPixel.Bit4: + case TiffBitsPerPixel.Bit6: + case TiffBitsPerPixel.Bit8: + colorType = PixelColorType.Indexed; + break; + case TiffBitsPerPixel.Bit16: + colorType = PixelColorType.Luminance; + break; + case TiffBitsPerPixel.Bit32: + case TiffBitsPerPixel.Bit64: + colorType = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + default: + colorType = PixelColorType.RGB; + break; + } + + return new PixelTypeInfo(bpp) + { + ColorType = colorType, + ComponentInfo = info, + AlphaRepresentation = alpha + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo() + }; + + /// + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public TiffMetadata DeepClone() => new(this); + } +} diff --git a/ImageSharp/Formats/Tiff/TiffThrowHelper.cs b/ImageSharp/Formats/Tiff/TiffThrowHelper.cs new file mode 100644 index 0000000..f4d2be5 --- /dev/null +++ b/ImageSharp/Formats/Tiff/TiffThrowHelper.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Tiff { + internal static class TiffThrowHelper + { + [DoesNotReturn] + public static Exception ThrowImageFormatException(string errorMessage) => throw new ImageFormatException(errorMessage); + + [DoesNotReturn] + public static Exception ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage); + + [DoesNotReturn] + public static Exception NotSupportedDecompressor(string compressionType) => throw new NotSupportedException($"Not supported decoder compression method: {compressionType}"); + + [DoesNotReturn] + public static Exception NotSupportedCompressor(string compressionType) => throw new NotSupportedException($"Not supported encoder compression method: {compressionType}"); + + [DoesNotReturn] + public static Exception InvalidColorType(string colorType) => throw new NotSupportedException($"Invalid color type: {colorType}"); + + [DoesNotReturn] + public static Exception ThrowInvalidHeader() => throw new ImageFormatException("Invalid TIFF file header."); + + [DoesNotReturn] + public static void ThrowNotSupported(string message) => throw new NotSupportedException(message); + + [DoesNotReturn] + public static void ThrowArgumentException(string message) => throw new ArgumentException(message); + } +} diff --git a/ImageSharp/Formats/Tiff/Utils/BitReader.cs b/ImageSharp/Formats/Tiff/Utils/BitReader.cs new file mode 100644 index 0000000..4cd4efb --- /dev/null +++ b/ImageSharp/Formats/Tiff/Utils/BitReader.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Utils { + /// + /// Utility class to read a sequence of bits from an array + /// + internal ref struct BitReader + { + private readonly ReadOnlySpan array; + private int offset; + private int bitOffset; + + /// + /// Initializes a new instance of the struct. + /// + /// The array to read data from. + public BitReader(ReadOnlySpan array) + { + this.array = array; + this.offset = 0; + this.bitOffset = 0; + } + + /// + /// Reads the specified number of bits from the array. + /// + /// The number of bits to read. + /// The value read from the array. + public int ReadBits(uint bits) + { + int value = 0; + + for (uint i = 0; i < bits; i++) + { + int bit = (this.array[this.offset] >> (7 - this.bitOffset)) & 0x01; + value = (value << 1) | bit; + + this.bitOffset++; + + if (this.bitOffset == 8) + { + this.bitOffset = 0; + this.offset++; + } + } + + return value; + } + + /// + /// Moves the reader to the next row of byte-aligned data. + /// + public void NextRow() + { + if (this.bitOffset > 0) + { + this.bitOffset = 0; + this.offset++; + } + } + } +} diff --git a/ImageSharp/Formats/Tiff/Utils/TiffUtilities.cs b/ImageSharp/Formats/Tiff/Utils/TiffUtilities.cs new file mode 100644 index 0000000..1e99a87 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Utils/TiffUtilities.cs @@ -0,0 +1,126 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Utils { + /// + /// Helper methods for TIFF decoding. + /// + internal static class TiffUtilities + { + private const float Scale24Bit = 1f / 0xFFFFFF; + private static readonly Vector4 Scale24BitVector = Vector128.Create(Scale24Bit, Scale24Bit, Scale24Bit, 1f).AsVector4(); + + private const float Scale32Bit = 1f / 0xFFFFFFFF; + private static readonly Vector4 Scale32BitVector = Vector128.Create(Scale32Bit, Scale32Bit, Scale32Bit, 1f).AsVector4(); + + public static Rgba64 Rgba64Default { get; } = new(0, 0, 0, 0); + + public static L16 L16Default { get; } = new(0); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ConvertToUShortBigEndian(ReadOnlySpan buffer) => BinaryPrimitives.ReadUInt16BigEndian(buffer); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ConvertToUShortLittleEndian(ReadOnlySpan buffer) => BinaryPrimitives.ReadUInt16LittleEndian(buffer); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ConvertToUIntBigEndian(ReadOnlySpan buffer) => BinaryPrimitives.ReadUInt32BigEndian(buffer); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ConvertToUIntLittleEndian(ReadOnlySpan buffer) => BinaryPrimitives.ReadUInt32LittleEndian(buffer); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorFromRgba64Premultiplied(ushort r, ushort g, ushort b, ushort a) + where TPixel : unmanaged, IPixel + { + if (a == 0) + { + return TPixel.FromRgba64(default); + } + + float scale = 65535f / a; + ushort ur = (ushort)Math.Min(r * scale, 65535); + ushort ug = (ushort)Math.Min(g * scale, 65535); + ushort ub = (ushort)Math.Min(b * scale, 65535); + + return TPixel.FromRgba64(new Rgba64(ur, ug, ub, a)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo24Bit(uint r, uint g, uint b) + where TPixel : unmanaged, IPixel + => TPixel.FromScaledVector4(new Vector4(r, g, b, 1f) * Scale24BitVector); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo24Bit(uint r, uint g, uint b, uint a) + where TPixel : unmanaged, IPixel + => TPixel.FromScaledVector4(new Vector4(r, g, b, a) * Scale24Bit); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo24BitPremultiplied(uint r, uint g, uint b, uint a) + where TPixel : unmanaged, IPixel + { + Vector4 colorVector = new Vector4(r, g, b, a) * Scale24Bit; + return UnPremultiply(ref colorVector); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo32Bit(uint r, uint g, uint b) + where TPixel : unmanaged, IPixel + => TPixel.FromScaledVector4(new Vector4(r, g, b, 1f) * Scale32BitVector); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo32Bit(uint r, uint g, uint b, uint a) + where TPixel : unmanaged, IPixel + => TPixel.FromScaledVector4(new Vector4(r, g, b, a) * Scale32Bit); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo32BitPremultiplied(uint r, uint g, uint b, uint a) + where TPixel : unmanaged, IPixel + { + Vector4 vector = new Vector4(r, g, b, a) * Scale32Bit; + return UnPremultiply(ref vector); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo24Bit(uint intensity) + where TPixel : unmanaged, IPixel + => TPixel.FromScaledVector4(new Vector4(intensity, intensity, intensity, 1f) * Scale24BitVector); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo32Bit(uint intensity) + where TPixel : unmanaged, IPixel + => TPixel.FromScaledVector4(new Vector4(intensity, intensity, intensity, 1f) * Scale32BitVector); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel UnPremultiply(ref Vector4 vector) + where TPixel : unmanaged, IPixel + { + Numerics.UnPremultiply(ref vector); + return TPixel.FromScaledVector4(vector); + } + + /// + /// Finds the padding needed to round 'valueToRoundUp' to the next integer multiple of subSampling value. + /// + /// The width or height to round up. + /// The sub sampling. + /// The padding. + public static int PaddingToNextInteger(int valueToRoundUp, int subSampling) + { + if (valueToRoundUp % subSampling == 0) + { + return 0; + } + + return subSampling - (valueToRoundUp % subSampling); + } + } +} diff --git a/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs b/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs new file mode 100644 index 0000000..9cf3352 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs @@ -0,0 +1,126 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers { + internal abstract class TiffBaseColorWriter : IDisposable + where TPixel : unmanaged, IPixel + { + private bool isDisposed; + + protected TiffBaseColorWriter( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) + { + this.Width = encodingSize.Width; + this.Height = encodingSize.Height; + this.Image = image; + this.MemoryAllocator = memoryAllocator; + this.Configuration = configuration; + this.EntriesCollector = entriesCollector; + } + + /// + /// Gets the bits per pixel. + /// + public abstract int BitsPerPixel { get; } + + /// + /// Gets the width of the portion of the image to be encoded. + /// + public int Width { get; } + + /// + /// Gets the height of the portion of the image to be encoded. + /// + public int Height { get; } + + /// + /// Gets the bytes per row. + /// + public int BytesPerRow => (int)(((uint)(this.Width * this.BitsPerPixel) + 7) / 8); + + protected ImageFrame Image { get; } + + protected MemoryAllocator MemoryAllocator { get; } + + protected Configuration Configuration { get; } + + protected TiffEncoderEntriesCollector EntriesCollector { get; } + + public virtual void Write(TiffBaseCompressor compressor, int rowsPerStrip) + { + DebugGuard.IsTrue(this.BytesPerRow == compressor.BytesPerRow, "bytes per row of the compressor does not match tiff color writer"); + int stripsCount = (this.Height + rowsPerStrip - 1) / rowsPerStrip; + + uint[] stripOffsets = new uint[stripsCount]; + uint[] stripByteCounts = new uint[stripsCount]; + + int stripIndex = 0; + compressor.Initialize(rowsPerStrip); + for (int y = 0; y < this.Height; y += rowsPerStrip) + { + long offset = compressor.Output.Position; + + int height = Math.Min(rowsPerStrip, this.Height - y); + this.EncodeStrip(y, height, compressor); + + long endOffset = compressor.Output.Position; + stripOffsets[stripIndex] = (uint)offset; + stripByteCounts[stripIndex] = (uint)(endOffset - offset); + stripIndex++; + } + + DebugGuard.IsTrue(stripIndex == stripsCount, "stripIndex and stripsCount should match"); + this.AddStripTags(rowsPerStrip, stripOffsets, stripByteCounts); + } + + /// + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.isDisposed = true; + this.Dispose(true); + } + + protected abstract void EncodeStrip(int y, int height, TiffBaseCompressor compressor); + + /// + /// Adds image format information to the specified IFD. + /// + /// The rows per strip. + /// The strip offsets. + /// The strip byte counts. + private void AddStripTags(int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts) + { + this.EntriesCollector.AddOrReplace(new ExifLong(ExifTagValue.RowsPerStrip) + { + Value = (uint)rowsPerStrip + }); + + this.EntriesCollector.AddOrReplace(new ExifLongArray(ExifTagValue.StripOffsets) + { + Value = stripOffsets + }); + + this.EntriesCollector.AddOrReplace(new ExifLongArray(ExifTagValue.StripByteCounts) + { + Value = stripByteCounts + }); + } + + protected abstract void Dispose(bool disposing); + } +} diff --git a/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs b/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs new file mode 100644 index 0000000..38d8e8e --- /dev/null +++ b/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs @@ -0,0 +1,118 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers { + internal sealed class TiffBiColorWriter : TiffBaseColorWriter + where TPixel : unmanaged, IPixel + { + private readonly Image imageBlackWhite; + + private IMemoryOwner pixelsAsGray; + + private IMemoryOwner bitStrip; + + public TiffBiColorWriter( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) + { + // Convert image to black and white. + this.imageBlackWhite = new Image(configuration, new ImageMetadata(), [image.Clone()]); + this.imageBlackWhite.Mutate(img => img.BinaryDither(KnownDitherings.FloydSteinberg)); + } + + /// + public override int BitsPerPixel => 1; + + /// + protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor) + { + int width = this.Width; + + if (compressor.Method is TiffCompression.CcittGroup3Fax or TiffCompression.Ccitt1D or TiffCompression.CcittGroup4Fax) + { + // Special case for T4BitCompressor. + int stripPixels = width * height; + this.pixelsAsGray ??= this.MemoryAllocator.Allocate(stripPixels); + this.imageBlackWhite.ProcessPixelRows(accessor => + { + Span pixelAsGraySpan = this.pixelsAsGray.GetSpan(); + int lastRow = y + height; + int grayRowIdx = 0; + for (int row = y; row < lastRow; row++) + { + Span pixelsBlackWhiteRow = accessor.GetRowSpan(row); + Span pixelAsGrayRow = pixelAsGraySpan.Slice(grayRowIdx * width, width); + PixelOperations.Instance.ToL8Bytes(this.Configuration, pixelsBlackWhiteRow, pixelAsGrayRow, width); + grayRowIdx++; + } + + compressor.CompressStrip(pixelAsGraySpan[..stripPixels], height); + }); + } + else + { + // Write uncompressed image. + int bytesPerStrip = this.BytesPerRow * height; + this.bitStrip ??= this.MemoryAllocator.Allocate(bytesPerStrip); + this.pixelsAsGray ??= this.MemoryAllocator.Allocate(width); + Span pixelAsGraySpan = this.pixelsAsGray.GetSpan(); + + Span rows = this.bitStrip.Slice(0, bytesPerStrip); + rows.Clear(); + Buffer2D blackWhiteBuffer = this.imageBlackWhite.Frames.RootFrame.PixelBuffer; + + int outputRowIdx = 0; + int lastRow = y + height; + for (int row = y; row < lastRow; row++) + { + int bitIndex = 0; + int byteIndex = 0; + Span outputRow = rows[(outputRowIdx * this.BytesPerRow)..]; + Span pixelsBlackWhiteRow = blackWhiteBuffer.DangerousGetRowSpan(row)[..width]; + PixelOperations.Instance.ToL8Bytes(this.Configuration, pixelsBlackWhiteRow, pixelAsGraySpan, width); + for (int x = 0; x < this.Width; x++) + { + int shift = 7 - bitIndex; + if (pixelAsGraySpan[x] == 255) + { + outputRow[byteIndex] |= (byte)(1 << shift); + } + + bitIndex++; + if (bitIndex == 8) + { + byteIndex++; + bitIndex = 0; + } + } + + outputRowIdx++; + } + + compressor.CompressStrip(rows, height); + } + } + + /// + protected override void Dispose(bool disposing) + { + this.imageBlackWhite?.Dispose(); + this.pixelsAsGray?.Dispose(); + this.bitStrip?.Dispose(); + } + } +} diff --git a/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs b/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs new file mode 100644 index 0000000..230c5a0 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers { + internal static class TiffColorWriterFactory + { + public static TiffBaseColorWriter Create( + TiffPhotometricInterpretation? photometricInterpretation, + ImageFrame image, + Size encodingSize, + IQuantizer quantizer, + IPixelSamplingStrategy pixelSamplingStrategy, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector, + int bitsPerPixel) + where TPixel : unmanaged, IPixel + => photometricInterpretation switch + { + TiffPhotometricInterpretation.PaletteColor => new TiffPaletteWriter(image, encodingSize, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel), + TiffPhotometricInterpretation.BlackIsZero or TiffPhotometricInterpretation.WhiteIsZero => bitsPerPixel switch + { + 1 => new TiffBiColorWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector), + 16 => new TiffGrayL16Writer(image, encodingSize, memoryAllocator, configuration, entriesCollector), + _ => new TiffGrayWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector) + }, + _ => new TiffRgbWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector), + }; + } +} diff --git a/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs b/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs new file mode 100644 index 0000000..a9f47a6 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers { + /// + /// The base class for composite color types: 8-bit gray, 24-bit RGB (4-bit gray, 16-bit (565/555) RGB, 32-bit RGB, CMYK, YCbCr). + /// + /// The tpe of pixel format. + internal abstract class TiffCompositeColorWriter : TiffBaseColorWriter + where TPixel : unmanaged, IPixel + { + private IMemoryOwner rowBuffer; + + protected TiffCompositeColorWriter( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) + { + } + + protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor) + { + (this.rowBuffer ??= this.MemoryAllocator.Allocate(this.BytesPerRow * height)).Clear(); + + Span outputRowSpan = this.rowBuffer.GetSpan()[..(this.BytesPerRow * height)]; + + int width = this.Width; + using IMemoryOwner stripPixelBuffer = this.MemoryAllocator.Allocate(height * width); + Span stripPixels = stripPixelBuffer.GetSpan(); + int lastRow = y + height; + int stripPixelsRowIdx = 0; + for (int row = y; row < lastRow; row++) + { + Span stripPixelsRow = this.Image.PixelBuffer.DangerousGetRowSpan(row)[..width]; + stripPixelsRow.CopyTo(stripPixels.Slice(stripPixelsRowIdx * width, width)); + stripPixelsRowIdx++; + } + + this.EncodePixels(stripPixels, outputRowSpan); + compressor.CompressStrip(outputRowSpan, height); + } + + protected abstract void EncodePixels(Span pixels, Span buffer); + + /// + protected override void Dispose(bool disposing) => this.rowBuffer?.Dispose(); + } +} diff --git a/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs b/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs new file mode 100644 index 0000000..8902e03 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers { + internal sealed class TiffGrayL16Writer : TiffCompositeColorWriter + where TPixel : unmanaged, IPixel + { + public TiffGrayL16Writer( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) + { + } + + /// + public override int BitsPerPixel => 16; + + /// + protected override void EncodePixels(Span pixels, Span buffer) + => PixelOperations.Instance.ToL16Bytes(this.Configuration, pixels, buffer, pixels.Length); + } +} diff --git a/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs b/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs new file mode 100644 index 0000000..dc40251 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers { + internal sealed class TiffGrayWriter : TiffCompositeColorWriter + where TPixel : unmanaged, IPixel + { + public TiffGrayWriter( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) + { + } + + /// + public override int BitsPerPixel => 8; + + /// + protected override void EncodePixels(Span pixels, Span buffer) + => PixelOperations.Instance.ToL8Bytes(this.Configuration, pixels, buffer, pixels.Length); + } +} diff --git a/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs b/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs new file mode 100644 index 0000000..5e4d9c9 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs @@ -0,0 +1,166 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers { + internal sealed class TiffPaletteWriter : TiffBaseColorWriter + where TPixel : unmanaged, IPixel + { + private readonly int maxColors; + private readonly int colorPaletteSize; + private readonly int colorPaletteBytes; + private readonly IndexedImageFrame quantizedFrame; + private IMemoryOwner indexedPixelsBuffer; + + public TiffPaletteWriter( + ImageFrame frame, + Size encodingSize, + IQuantizer quantizer, + IPixelSamplingStrategy pixelSamplingStrategy, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector, + int bitsPerPixel) + : base(frame, encodingSize, memoryAllocator, configuration, entriesCollector) + { + DebugGuard.NotNull(quantizer, nameof(quantizer)); + DebugGuard.NotNull(quantizer, nameof(pixelSamplingStrategy)); + DebugGuard.NotNull(configuration, nameof(configuration)); + DebugGuard.NotNull(entriesCollector, nameof(entriesCollector)); + DebugGuard.MustBeBetweenOrEqualTo(bitsPerPixel, 4, 8, nameof(bitsPerPixel)); + + this.BitsPerPixel = bitsPerPixel; + this.maxColors = this.BitsPerPixel == 4 ? 16 : 256; + this.colorPaletteSize = this.maxColors * 3; + this.colorPaletteBytes = this.colorPaletteSize * 2; + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer( + this.Configuration, + new QuantizerOptions + { + MaxColors = this.maxColors + }); + + frameQuantizer.BuildPalette(pixelSamplingStrategy, frame); + this.quantizedFrame = frameQuantizer.QuantizeFrame(frame, new Rectangle(Point.Empty, encodingSize)); + + this.AddColorMapTag(); + } + + /// + public override int BitsPerPixel { get; } + + /// + protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor) + { + int width = this.quantizedFrame.Width; + + if (this.BitsPerPixel == 4) + { + int halfWidth = width >> 1; + int excess = (width & 1) * height; // (width % 2) * height + int rows4BitBufferLength = (halfWidth * height) + excess; + this.indexedPixelsBuffer ??= this.MemoryAllocator.Allocate(rows4BitBufferLength); + Span rows4bit = this.indexedPixelsBuffer.GetSpan(); + int idx4bitRows = 0; + int lastRow = y + height; + for (int row = y; row < lastRow; row++) + { + ReadOnlySpan indexedPixelRow = this.quantizedFrame.DangerousGetRowSpan(row); + int idxPixels = 0; + for (int x = 0; x < halfWidth; x++) + { + rows4bit[idx4bitRows] = (byte)((indexedPixelRow[idxPixels] << 4) | (indexedPixelRow[idxPixels + 1] & 0xF)); + idxPixels += 2; + idx4bitRows++; + } + + // Make sure rows are byte-aligned. + if (width % 2 != 0) + { + rows4bit[idx4bitRows++] = (byte)(indexedPixelRow[idxPixels] << 4); + } + } + + compressor.CompressStrip(rows4bit[..idx4bitRows], height); + } + else + { + int stripPixels = width * height; + this.indexedPixelsBuffer ??= this.MemoryAllocator.Allocate(stripPixels); + Span indexedPixels = this.indexedPixelsBuffer.GetSpan(); + int lastRow = y + height; + int indexedPixelsRowIdx = 0; + for (int row = y; row < lastRow; row++) + { + ReadOnlySpan indexedPixelRow = this.quantizedFrame.DangerousGetRowSpan(row); + indexedPixelRow.CopyTo(indexedPixels.Slice(indexedPixelsRowIdx * width, width)); + indexedPixelsRowIdx++; + } + + compressor.CompressStrip(indexedPixels[..stripPixels], height); + } + } + + /// + protected override void Dispose(bool disposing) + { + this.quantizedFrame?.Dispose(); + this.indexedPixelsBuffer?.Dispose(); + } + + private void AddColorMapTag() + { + using IMemoryOwner colorPaletteBuffer = this.MemoryAllocator.Allocate(this.colorPaletteBytes); + Span colorPalette = colorPaletteBuffer.GetSpan(); + + ReadOnlySpan quantizedColors = this.quantizedFrame.Palette.Span; + int quantizedColorBytes = quantizedColors.Length * 3 * 2; + + // In the ColorMap, black is represented by 0, 0, 0 and white is represented by 65535, 65535, 65535. + Span quantizedColorRgb48 = MemoryMarshal.Cast(colorPalette[..quantizedColorBytes]); + PixelOperations.Instance.ToRgb48(this.Configuration, quantizedColors, quantizedColorRgb48); + + // It can happen that the quantized colors are less than the expected maximum per channel. + int diffToMaxColors = this.maxColors - quantizedColors.Length; + + // In a TIFF ColorMap, all the Red values come first, followed by the Green values, + // then the Blue values. Convert the quantized palette to this format. + ushort[] palette = new ushort[this.colorPaletteSize]; + int paletteIdx = 0; + for (int i = 0; i < quantizedColors.Length; i++) + { + palette[paletteIdx++] = quantizedColorRgb48[i].R; + } + + paletteIdx += diffToMaxColors; + + for (int i = 0; i < quantizedColors.Length; i++) + { + palette[paletteIdx++] = quantizedColorRgb48[i].G; + } + + paletteIdx += diffToMaxColors; + + for (int i = 0; i < quantizedColors.Length; i++) + { + palette[paletteIdx++] = quantizedColorRgb48[i].B; + } + + ExifShortArray colorMap = new(ExifTagValue.ColorMap) + { + Value = palette + }; + + this.EntriesCollector.AddOrReplace(colorMap); + } + } +} diff --git a/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs b/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs new file mode 100644 index 0000000..8dbdd5f --- /dev/null +++ b/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers { + internal sealed class TiffRgbWriter : TiffCompositeColorWriter + where TPixel : unmanaged, IPixel + { + public TiffRgbWriter( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) + { + } + + /// + public override int BitsPerPixel => 24; + + /// + protected override void EncodePixels(Span pixels, Span buffer) + => PixelOperations.Instance.ToRgb24Bytes(this.Configuration, pixels, buffer, pixels.Length); + } +} diff --git a/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs b/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs new file mode 100644 index 0000000..67abfe8 --- /dev/null +++ b/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs @@ -0,0 +1,145 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers { + /// + /// Utility class for writing TIFF data to a . + /// + internal sealed class TiffStreamWriter : IDisposable + { + /// + /// Initializes a new instance of the class. + /// + /// The output stream. + public TiffStreamWriter(Stream output) => this.BaseStream = output; + + /// + /// Gets a value indicating whether the architecture is little-endian. + /// + public static bool IsLittleEndian => BitConverter.IsLittleEndian; + + /// + /// Gets the current position within the stream. + /// + public long Position => this.BaseStream.Position; + + /// + /// Gets the base stream. + /// + public Stream BaseStream { get; } + + /// + /// Writes an empty four bytes to the stream, returning the offset to be written later. + /// + /// Scratch buffer with minimum size of 4. + /// The offset to be written later. + public long PlaceMarker(Span buffer) + { + long offset = this.BaseStream.Position; + this.Write(0u, buffer); + return offset; + } + + /// + /// Writes an array of bytes to the current stream. + /// + /// The bytes to write. + public void Write(byte[] value) => this.BaseStream.Write(value, 0, value.Length); + + /// + /// Writes the specified value. + /// + /// The bytes to write. + public void Write(ReadOnlySpan value) => this.BaseStream.Write(value); + + /// + /// Writes a byte to the current stream. + /// + /// The byte to write. + public void Write(byte value) => this.BaseStream.WriteByte(value); + + /// + /// Writes a two-byte unsigned integer to the current stream. + /// + /// The two-byte unsigned integer to write. + /// Scratch buffer with minimum size of 2. + public void Write(ushort value, Span buffer) + { + if (IsLittleEndian) + { + BinaryPrimitives.WriteUInt16LittleEndian(buffer, value); + } + else + { + BinaryPrimitives.WriteUInt16BigEndian(buffer, value); + } + + this.BaseStream.Write(buffer.Slice(0, 2)); + } + + /// + /// Writes a four-byte unsigned integer to the current stream. + /// + /// The four-byte unsigned integer to write. + /// Scratch buffer with minimum size of 4. + public void Write(uint value, Span buffer) + { + if (IsLittleEndian) + { + BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); + } + else + { + BinaryPrimitives.WriteUInt32BigEndian(buffer, value); + } + + this.BaseStream.Write(buffer.Slice(0, 4)); + } + + /// + /// Writes an array of bytes to the current stream, padded to four-bytes. + /// + /// The bytes to write. + public void WritePadded(Span value) + { + this.BaseStream.Write(value); + + if (value.Length % 4 != 0) + { + // No allocation occurs, refers directly to assembly's data segment. + ReadOnlySpan paddingBytes = [0x00, 0x00, 0x00, 0x00]; + paddingBytes = paddingBytes[..(4 - (value.Length % 4))]; + this.BaseStream.Write(paddingBytes); + } + } + + /// + /// Writes a four-byte unsigned integer to the specified marker in the stream. + /// + /// The offset returned when placing the marker + /// The four-byte unsigned integer to write. + /// Scratch buffer. + public void WriteMarker(long offset, uint value, Span buffer) + { + long back = this.BaseStream.Position; + this.BaseStream.Seek(offset, SeekOrigin.Begin); + this.Write(value, buffer); + this.BaseStream.Seek(back, SeekOrigin.Begin); + } + + public void WriteMarkerFast(long offset, uint value, Span buffer) + { + this.BaseStream.Seek(offset, SeekOrigin.Begin); + this.Write(value, buffer); + } + + /// + /// Disposes instance, ensuring any unwritten data is flushed. + /// + public void Dispose() => this.BaseStream.Flush(); + } +} diff --git a/ImageSharp/Formats/TransparentColorMode.cs b/ImageSharp/Formats/TransparentColorMode.cs new file mode 100644 index 0000000..e0397de --- /dev/null +++ b/ImageSharp/Formats/TransparentColorMode.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats { + /// + /// Specifies how pixels with transparent alpha components should be handled during encoding and quantization. + /// + public enum TransparentColorMode + { + /// + /// Retains the original color values of transparent pixels. + /// + Preserve = 0, + + /// + /// Converts transparent pixels with non-zero color components + /// to fully transparent pixels (all components set to zero), + /// which may improve compression. + /// + Clear = 1 + } +} diff --git a/ImageSharp/Formats/Webp/AlphaDecoder.cs b/ImageSharp/Formats/Webp/AlphaDecoder.cs new file mode 100644 index 0000000..df365d2 --- /dev/null +++ b/ImageSharp/Formats/Webp/AlphaDecoder.cs @@ -0,0 +1,492 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Webp.BitReader; +using SixLabors.ImageSharp.Formats.Webp.Lossless; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Implements decoding for lossy alpha chunks which may be compressed. + /// + internal class AlphaDecoder : IDisposable + { + private readonly MemoryAllocator memoryAllocator; + + /// + /// Initializes a new instance of the class. + /// + /// The width of the image. + /// The height of the image. + /// The (maybe compressed) alpha data. + /// The first byte of the alpha image stream contains information on how to decode the stream. + /// Used for allocating memory during decoding. + /// The configuration. + public AlphaDecoder(int width, int height, IMemoryOwner data, byte alphaChunkHeader, MemoryAllocator memoryAllocator, Configuration configuration) + { + this.Width = width; + this.Height = height; + this.Data = data; + this.memoryAllocator = memoryAllocator; + this.LastRow = 0; + int totalPixels = width * height; + + WebpAlphaCompressionMethod compression = (WebpAlphaCompressionMethod)(alphaChunkHeader & 0x03); + if (compression is not WebpAlphaCompressionMethod.NoCompression and not WebpAlphaCompressionMethod.WebpLosslessCompression) + { + WebpThrowHelper.ThrowImageFormatException($"unexpected alpha compression method {compression} found"); + } + + this.Compressed = compression == WebpAlphaCompressionMethod.WebpLosslessCompression; + + // The filtering method used. Only values between 0 and 3 are valid. + int filter = (alphaChunkHeader >> 2) & 0x03; + if (filter is < (int)WebpAlphaFilterType.None or > (int)WebpAlphaFilterType.Gradient) + { + WebpThrowHelper.ThrowImageFormatException($"unexpected alpha filter method {filter} found"); + } + + this.Alpha = memoryAllocator.Allocate(totalPixels); + this.AlphaFilterType = (WebpAlphaFilterType)filter; + this.Vp8LDec = new Vp8LDecoder(width, height, memoryAllocator); + + if (this.Compressed) + { + Vp8LBitReader bitReader = new(data); + this.LosslessDecoder = new WebpLosslessDecoder(bitReader, memoryAllocator, configuration); + this.LosslessDecoder.DecodeImageStream(this.Vp8LDec, width, height, true); + + // Special case: if alpha data uses only the color indexing transform and + // doesn't use color cache (a frequent case), we will use DecodeAlphaData() + // method that only needs allocation of 1 byte per pixel (alpha channel). + this.Use8BDecode = this.Vp8LDec.Transforms.Count is 1 + && this.Vp8LDec.Transforms[0].TransformType == Vp8LTransformType.ColorIndexingTransform + && Is8BOptimizable(this.Vp8LDec.Metadata); + } + } + + /// + /// Gets the width of the image. + /// + public int Width { get; } + + /// + /// Gets the height of the image. + /// + public int Height { get; } + + /// + /// Gets the used filter type. + /// + public WebpAlphaFilterType AlphaFilterType { get; } + + /// + /// Gets or sets the last decoded row. + /// + public int LastRow { get; set; } + + /// + /// Gets or sets the row before the last decoded row. + /// + public int PrevRow { get; set; } + + /// + /// Gets information for decoding Vp8L compressed alpha data. + /// + public Vp8LDecoder Vp8LDec { get; } + + /// + /// Gets the decoded alpha data. + /// + public IMemoryOwner Alpha { get; } + + /// + /// Gets a value indicating whether the alpha channel uses compression. + /// + [MemberNotNullWhen(true, nameof(LosslessDecoder))] + private bool Compressed { get; } + + /// + /// Gets the (maybe compressed) alpha data. + /// + private IMemoryOwner Data { get; } + + /// + /// Gets the Vp8L decoder which is used to de compress the alpha channel, if needed. + /// + private WebpLosslessDecoder? LosslessDecoder { get; } + + /// + /// Gets a value indicating whether the decoding needs 1 byte per pixel for decoding. + /// Although Alpha Channel requires only 1 byte per pixel, sometimes Vp8LDecoder may need to allocate + /// 4 bytes per pixel internally during decode. + /// + public bool Use8BDecode { get; } + + /// + /// Decodes and filters the maybe compressed alpha data. + /// + public void Decode() + { + if (!this.Compressed) + { + Span dataSpan = this.Data.Memory.Span; + int pixelCount = this.Width * this.Height; + if (dataSpan.Length < pixelCount) + { + WebpThrowHelper.ThrowImageFormatException("not enough data in the ALPH chunk"); + } + + Span alphaSpan = this.Alpha.Memory.Span; + if (this.AlphaFilterType == WebpAlphaFilterType.None) + { + dataSpan[..pixelCount].CopyTo(alphaSpan); + return; + } + + Span deltas = dataSpan; + Span dst = alphaSpan; + Span prev = default; + for (int y = 0; y < this.Height; y++) + { + switch (this.AlphaFilterType) + { + case WebpAlphaFilterType.Horizontal: + HorizontalUnfilter(prev, deltas, dst, this.Width); + break; + case WebpAlphaFilterType.Vertical: + VerticalUnfilter(prev, deltas, dst, this.Width); + break; + case WebpAlphaFilterType.Gradient: + GradientUnfilter(prev, deltas, dst, this.Width); + break; + } + + prev = dst; + deltas = deltas[this.Width..]; + dst = dst[this.Width..]; + } + } + else if (this.Use8BDecode) + { + this.LosslessDecoder.DecodeAlphaData(this); + } + else + { + this.LosslessDecoder.DecodeImageData(this.Vp8LDec, this.Vp8LDec.Pixels.Memory.Span); + this.ExtractAlphaRows(this.Vp8LDec, this.Width); + } + } + + /// + /// Applies filtering to a set of rows. + /// + /// The first row index to start filtering. + /// The last row index for filtering. + /// The destination to store the filtered data. + /// The stride to use. + public void AlphaApplyFilter(int firstRow, int lastRow, Span dst, int stride) + { + if (this.AlphaFilterType == WebpAlphaFilterType.None) + { + return; + } + + Span alphaSpan = this.Alpha.Memory.Span; + Span prev = this.PrevRow == 0 ? null : alphaSpan[(this.Width * this.PrevRow)..]; + for (int y = firstRow; y < lastRow; y++) + { + switch (this.AlphaFilterType) + { + case WebpAlphaFilterType.Horizontal: + HorizontalUnfilter(prev, dst, dst, this.Width); + break; + case WebpAlphaFilterType.Vertical: + VerticalUnfilter(prev, dst, dst, this.Width); + break; + case WebpAlphaFilterType.Gradient: + GradientUnfilter(prev, dst, dst, this.Width); + break; + } + + prev = dst; + dst = dst[stride..]; + } + + this.PrevRow = lastRow - 1; + } + + public void ExtractPalettedAlphaRows(int lastRow) + { + // For vertical and gradient filtering, we need to decode the part above the + // cropTop row, in order to have the correct spatial predictors. + int topRow = this.AlphaFilterType is WebpAlphaFilterType.None or WebpAlphaFilterType.Horizontal ? 0 : this.LastRow; + int firstRow = this.LastRow < topRow ? topRow : this.LastRow; + if (lastRow > firstRow) + { + // Special method for paletted alpha data. + Span output = this.Alpha.Memory.Span; + Span pixelData = this.Vp8LDec.Pixels.Memory.Span; + Span pixelDataAsBytes = MemoryMarshal.Cast(pixelData); + Span dst = output[(this.Width * firstRow)..]; + Span input = pixelDataAsBytes[(this.Vp8LDec.Width * firstRow)..]; + + if (this.Vp8LDec.Transforms.Count == 0 || this.Vp8LDec.Transforms[0].TransformType != Vp8LTransformType.ColorIndexingTransform) + { + WebpThrowHelper.ThrowImageFormatException("error while decoding alpha channel, expected color index transform data is missing"); + } + + Vp8LTransform transform = this.Vp8LDec.Transforms[0]; + ColorIndexInverseTransformAlpha(transform, firstRow, lastRow, input, dst); + this.AlphaApplyFilter(firstRow, lastRow, dst, this.Width); + } + + this.LastRow = lastRow; + } + + /// + /// Once the image-stream is decoded into ARGB color values, the transparency information will be extracted from the green channel of the ARGB quadruplet. + /// + /// The VP8L decoder. + /// The image width. + private void ExtractAlphaRows(Vp8LDecoder dec, int width) + { + int numRowsToProcess = dec.Height; + Span input = dec.Pixels.Memory.Span; + Span output = this.Alpha.Memory.Span; + + // Extract alpha (which is stored in the green plane). + // the final width (!= dec->width_) + int pixelCount = width * numRowsToProcess; + WebpLosslessDecoder.ApplyInverseTransforms(dec, input, this.memoryAllocator); + ExtractGreen(input, output, pixelCount); + this.AlphaApplyFilter(0, numRowsToProcess, output, width); + } + + private static void ColorIndexInverseTransformAlpha( + Vp8LTransform transform, + int yStart, + int yEnd, + Span src, + Span dst) + { + int bitsPerPixel = 8 >> transform.Bits; + int width = transform.XSize; + Span colorMap = transform.Data.Memory.Span; + if (bitsPerPixel < 8) + { + int srcOffset = 0; + int dstOffset = 0; + int pixelsPerByte = 1 << transform.Bits; + int countMask = pixelsPerByte - 1; + int bitMask = (1 << bitsPerPixel) - 1; + for (int y = yStart; y < yEnd; y++) + { + int packedPixels = 0; + for (int x = 0; x < width; x++) + { + if ((x & countMask) == 0) + { + packedPixels = src[srcOffset]; + srcOffset++; + } + + dst[dstOffset] = GetAlphaValue((int)colorMap[packedPixels & bitMask]); + dstOffset++; + packedPixels >>= bitsPerPixel; + } + } + } + else + { + MapAlpha(src, colorMap, dst, yStart, yEnd, width); + } + } + + private static void HorizontalUnfilter(Span prev, Span input, Span dst, int width) + { + if (Vector128.IsHardwareAccelerated && width >= 9) + { + dst[0] = (byte)(input[0] + (prev.IsEmpty ? 0 : prev[0])); + nuint i; + Vector128 last = Vector128.Zero.WithElement(0, dst[0]); + ref byte srcRef = ref MemoryMarshal.GetReference(input); + ref byte dstRef = ref MemoryMarshal.GetReference(dst); + + for (i = 1; i <= (uint)width - 8; i += 8) + { + Vector128 a0 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, i)), 0); + Vector128 a1 = a0.AsByte() + last.AsByte(); + Vector128 a2 = Vector128_.ShiftLeftBytesInVector(a1, 1); + Vector128 a3 = a1 + a2; + Vector128 a4 = Vector128_.ShiftLeftBytesInVector(a3, 2); + Vector128 a5 = a3 + a4; + Vector128 a6 = Vector128_.ShiftLeftBytesInVector(a5, 4); + Vector128 a7 = a5 + a6; + + ref byte outputRef = ref Unsafe.Add(ref dstRef, i); + Unsafe.As>(ref outputRef) = a7.GetLower(); + last = Vector128.ShiftRightLogical(a7.AsInt64(), 56).AsInt32(); + } + + for (; i < (uint)width; ++i) + { + dst[(int)i] = (byte)(input[(int)i] + dst[(int)i - 1]); + } + } + else + { + byte pred = (byte)(prev.IsEmpty ? 0 : prev[0]); + + for (int i = 0; i < width; i++) + { + byte val = (byte)(pred + input[i]); + pred = val; + dst[i] = val; + } + } + } + + private static void VerticalUnfilter(Span prev, Span input, Span dst, int width) + { + if (prev.IsEmpty) + { + HorizontalUnfilter(null, input, dst, width); + } + else if (Vector256.IsHardwareAccelerated) + { + ref byte inputRef = ref MemoryMarshal.GetReference(input); + ref byte prevRef = ref MemoryMarshal.GetReference(prev); + ref byte dstRef = ref MemoryMarshal.GetReference(dst); + + nuint i; + int maxPos = width & ~31; + for (i = 0; i < (uint)maxPos; i += 32) + { + Vector256 a0 = Unsafe.As>(ref Unsafe.Add(ref inputRef, i)); + Vector256 b0 = Unsafe.As>(ref Unsafe.Add(ref prevRef, i)); + Vector256 c0 = a0.AsByte() + b0.AsByte(); + ref byte outputRef = ref Unsafe.Add(ref dstRef, i); + Unsafe.As>(ref outputRef) = c0; + } + + for (; i < (uint)width; i++) + { + Unsafe.Add(ref dstRef, i) = (byte)(Unsafe.Add(ref prevRef, i) + Unsafe.Add(ref inputRef, i)); + } + } + else + { + for (int i = 0; i < width; i++) + { + dst[i] = (byte)(prev[i] + input[i]); + } + } + } + + private static void GradientUnfilter(Span prev, Span input, Span dst, int width) + { + if (prev.IsEmpty) + { + HorizontalUnfilter(null, input, dst, width); + } + else + { + byte prev0 = prev[0]; + byte topLeft = prev0; + byte left = prev0; + for (int i = 0; i < width; i++) + { + byte top = prev[i]; + left = (byte)(input[i] + GradientPredictor(left, top, topLeft)); + topLeft = top; + dst[i] = left; + } + } + } + + /// + /// Row-processing for the special case when alpha data contains only one + /// transform (color indexing), and trivial non-green literals. + /// + /// The VP8L meta data. + /// True, if alpha channel needs one byte per pixel, otherwise 4. + private static bool Is8BOptimizable(Vp8LMetadata hdr) + { + if (hdr.ColorCacheSize > 0) + { + return false; + } + + for (int i = 0; i < hdr.NumHTreeGroups; i++) + { + List htrees = hdr.HTreeGroups[i].HTrees; + if (htrees[HuffIndex.Red][0].BitsUsed > 0) + { + return false; + } + + if (htrees[HuffIndex.Blue][0].BitsUsed > 0) + { + return false; + } + + if (htrees[HuffIndex.Alpha][0].BitsUsed > 0) + { + return false; + } + } + + return true; + } + + private static void MapAlpha(Span src, Span colorMap, Span dst, int yStart, int yEnd, int width) + { + int offset = 0; + for (int y = yStart; y < yEnd; y++) + { + for (int x = 0; x < width; x++) + { + dst[offset] = GetAlphaValue((int)colorMap[src[offset]]); + offset++; + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static byte GetAlphaValue(int val) => (byte)((val >> 8) & 0xff); + + [MethodImpl(InliningOptions.ShortMethod)] + private static int GradientPredictor(byte a, byte b, byte c) + { + int g = a + b - c; + return (g & ~0xff) == 0 ? g : g < 0 ? 0 : 255; // clip to 8bit. + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void ExtractGreen(Span argb, Span alpha, int size) + { + for (int i = 0; i < size; i++) + { + alpha[i] = (byte)(argb[i] >> 8); + } + } + + /// + public void Dispose() + { + this.Vp8LDec?.Dispose(); + this.Data.Dispose(); + this.Alpha?.Dispose(); + } + } +} diff --git a/ImageSharp/Formats/Webp/AlphaEncoder.cs b/ImageSharp/Formats/Webp/AlphaEncoder.cs new file mode 100644 index 0000000..3836f69 --- /dev/null +++ b/ImageSharp/Formats/Webp/AlphaEncoder.cs @@ -0,0 +1,136 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats.Webp.Lossless; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Methods for encoding the alpha data of a VP8 image. + /// + internal static class AlphaEncoder + { + /// + /// Encodes the alpha channel data. + /// Data is either compressed as lossless webp image or uncompressed. + /// + /// The pixel format. + /// The to encode from. + /// The global configuration. + /// The memory manager. + /// Whether to skip metadata encoding. + /// Indicates, if the data should be compressed with the lossless webp compression. + /// The size in bytes of the alpha data. + /// The encoded alpha data. + public static IMemoryOwner EncodeAlpha( + Buffer2DRegion frame, + Configuration configuration, + MemoryAllocator memoryAllocator, + bool skipMetadata, + bool compress, + out int size) + where TPixel : unmanaged, IPixel + { + IMemoryOwner alphaData = ExtractAlphaChannel(frame, configuration, memoryAllocator); + + if (compress) + { + const WebpEncodingMethod effort = WebpEncodingMethod.Default; + const int quality = 8 * (int)effort; + using Vp8LEncoder lossLessEncoder = new( + memoryAllocator, + configuration, + frame.Width, + frame.Height, + quality, + skipMetadata, + effort, + TransparentColorMode.Preserve, + false, + 0); + + // The transparency information will be stored in the green channel of the ARGB quadruplet. + // The green channel is allowed extra transformation steps in the specification -- unlike the other channels, + // that can improve compression. + using ImageFrame alphaAsFrame = DispatchAlphaToGreen(configuration, frame, alphaData.GetSpan()); + + size = lossLessEncoder.EncodeAlphaImageData(alphaAsFrame.PixelBuffer.GetRegion(), alphaData); + + return alphaData; + } + + size = frame.Width * frame.Height; + return alphaData; + } + + /// + /// Store the transparency in the green channel. + /// + /// The pixel format. + /// The configuration. + /// The pixel buffer to encode from. + /// A byte sequence of length width * height, containing all the 8-bit transparency values in scan order. + /// The transparency frame. + private static ImageFrame DispatchAlphaToGreen(Configuration configuration, Buffer2DRegion frame, Span alphaData) + where TPixel : unmanaged, IPixel + { + int width = frame.Width; + int height = frame.Height; + ImageFrame alphaAsFrame = new(configuration, width, height); + + for (int y = 0; y < height; y++) + { + Memory rowBuffer = alphaAsFrame.DangerousGetPixelRowMemory(y); + Span pixelRow = rowBuffer.Span; + Span alphaRow = alphaData.Slice(y * width, width); + + // TODO: This can be probably simd optimized. + for (int x = 0; x < width; x++) + { + // Leave A/R/B channels zero'd. + pixelRow[x] = new Bgra32(0, alphaRow[x], 0, 0); + } + } + + return alphaAsFrame; + } + + /// + /// Extract the alpha data of the image. + /// + /// The pixel format. + /// The to encode from. + /// The global configuration. + /// The memory manager. + /// A byte sequence of length width * height, containing all the 8-bit transparency values in scan order. + private static IMemoryOwner ExtractAlphaChannel(Buffer2DRegion frame, Configuration configuration, MemoryAllocator memoryAllocator) + where TPixel : unmanaged, IPixel + { + int width = frame.Width; + int height = frame.Height; + + IMemoryOwner alphaDataBuffer = memoryAllocator.Allocate(width * height); + Span alphaData = alphaDataBuffer.GetSpan(); + + using IMemoryOwner rowBuffer = memoryAllocator.Allocate(width); + Span rgbaRow = rowBuffer.GetSpan(); + + for (int y = 0; y < height; y++) + { + Span rowSpan = frame.DangerousGetRowSpan(y); + PixelOperations.Instance.ToRgba32(configuration, rowSpan, rgbaRow); + int offset = y * width; + for (int x = 0; x < width; x++) + { + alphaData[offset + x] = rgbaRow[x].A; + } + } + + return alphaDataBuffer; + } + } +} diff --git a/ImageSharp/Formats/Webp/BackgroundColorHandling.cs b/ImageSharp/Formats/Webp/BackgroundColorHandling.cs new file mode 100644 index 0000000..9caab38 --- /dev/null +++ b/ImageSharp/Formats/Webp/BackgroundColorHandling.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Enum to decide how to handle the background color of the Animation chunk during decoding. + /// + public enum BackgroundColorHandling + { + /// + /// The background color of the ANIM chunk will be used to initialize the canvas to fill the unused space on the canvas around the frame. + /// Also, if AnimationDisposalMethod.Dispose is used, this color will be used to restore the canvas background. + /// + Standard = 0, + + /// + /// The background color of the ANIM chunk is ignored and instead the canvas is initialized with transparent, BGRA(0, 0, 0, 0). + /// + Ignore = 1 + } +} diff --git a/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs b/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs new file mode 100644 index 0000000..528846f --- /dev/null +++ b/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.BitReader { + /// + /// Base class for VP8 and VP8L bitreader. + /// + internal abstract class BitReaderBase : IDisposable + { + private bool isDisposed; + + protected BitReaderBase(IMemoryOwner data) + => this.Data = data; + + protected BitReaderBase(Stream inputStream, int imageDataSize, MemoryAllocator memoryAllocator) + => this.Data = ReadImageDataFromStream(inputStream, imageDataSize, memoryAllocator); + + /// + /// Gets the raw encoded image data. + /// + public IMemoryOwner Data { get; } + + /// + /// Copies the raw encoded image data from the stream into a byte array. + /// + /// The input stream. + /// Number of bytes to read as indicated from the chunk size. + /// Used for allocating memory during reading data from the stream. + protected static IMemoryOwner ReadImageDataFromStream(Stream input, int bytesToRead, MemoryAllocator memoryAllocator) + { + IMemoryOwner data = memoryAllocator.Allocate(bytesToRead, AllocationOptions.Clean); + Span dataSpan = data.Memory.Span; + input.Read(dataSpan[..bytesToRead], 0, bytesToRead); + + return data; + } + + protected virtual void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + if (disposing) + { + this.Data.Dispose(); + } + + this.isDisposed = true; + } + + /// + public void Dispose() + { + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/ImageSharp/Formats/Webp/BitReader/Vp8BitReader.cs b/ImageSharp/Formats/Webp/BitReader/Vp8BitReader.cs new file mode 100644 index 0000000..87ee6c4 --- /dev/null +++ b/ImageSharp/Formats/Webp/BitReader/Vp8BitReader.cs @@ -0,0 +1,231 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Buffers.Binary; +using System.IO; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.BitReader { + /// + /// A bit reader for VP8 streams. + /// + internal class Vp8BitReader : BitReaderBase + { + private const int BitsCount = 56; + + /// + /// Current value. + /// + private ulong value; + + /// + /// Current range minus 1. In [127, 254] interval. + /// + private uint range; + + /// + /// Number of valid bits left. + /// + private int bits; + + /// + /// Max packed-read position of the buffer. + /// + private uint bufferMax; + + private uint bufferEnd; + + /// + /// True if input is exhausted. + /// + private bool eof; + + /// + /// Byte position in buffer. + /// + private long pos; + + /// + /// Initializes a new instance of the class. + /// + /// The input stream to read from. + /// The raw image data size in bytes. + /// Used for allocating memory during reading data from the stream. + /// The partition length. + /// Start index in the data array. Defaults to 0. + public Vp8BitReader(Stream inputStream, uint imageDataSize, MemoryAllocator memoryAllocator, uint partitionLength, int startPos = 0) + : base(inputStream, (int)imageDataSize, memoryAllocator) + { + Guard.MustBeLessThan(imageDataSize, int.MaxValue, nameof(imageDataSize)); + + this.ImageDataSize = imageDataSize; + this.PartitionLength = partitionLength; + this.InitBitreader(partitionLength, startPos); + } + + /// + /// Initializes a new instance of the class. + /// + /// The raw encoded image data. + /// The partition length. + /// Start index in the data array. Defaults to 0. + public Vp8BitReader(IMemoryOwner imageData, uint partitionLength, int startPos = 0) + : base(imageData) + { + this.ImageDataSize = (uint)imageData.Memory.Length; + this.PartitionLength = partitionLength; + this.InitBitreader(partitionLength, startPos); + } + + public int Pos => (int)this.pos; + + public uint ImageDataSize { get; } + + public uint PartitionLength { get; } + + public uint Remaining { get; set; } + + [MethodImpl(InliningOptions.ShortMethod)] + public int GetBit(int prob) + { + uint range = this.range; + if (this.bits < 0) + { + this.LoadNewBytes(); + } + + int pos = this.bits; + uint split = (uint)((range * prob) >> 8); + ulong value = this.value >> pos; + bool bit = value > split; + if (bit) + { + range -= split; + this.value -= (ulong)(split + 1) << pos; + } + else + { + range = split + 1; + } + + int shift = 7 ^ BitOperations.Log2(range); + range <<= shift; + this.bits -= shift; + + this.range = range - 1; + + return bit ? 1 : 0; + } + + // Simplified version of VP8GetBit() for prob=0x80 (note shift is always 1 here) + public int GetSigned(int v) + { + if (this.bits < 0) + { + this.LoadNewBytes(); + } + + int pos = this.bits; + uint split = this.range >> 1; + ulong value = this.value >> pos; + ulong mask = (split - value) >> 31; // -1 or 0 + this.bits--; + this.range = (this.range + (uint)mask) | 1; + this.value -= ((split + 1) & mask) << pos; + + return (v ^ (int)mask) - (int)mask; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public bool ReadBool() => this.ReadValue(1) is 1; + + [MethodImpl(InliningOptions.ShortMethod)] + public uint ReadValue(int nBits) + { + DebugGuard.MustBeGreaterThan(nBits, 0, nameof(nBits)); + DebugGuard.MustBeLessThanOrEqualTo(nBits, 32, nameof(nBits)); + + uint v = 0; + while (nBits-- > 0) + { + v |= (uint)this.GetBit(0x80) << nBits; + } + + return v; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public int ReadSignedValue(int nBits) + { + DebugGuard.MustBeGreaterThan(nBits, 0, nameof(nBits)); + DebugGuard.MustBeLessThanOrEqualTo(nBits, 32, nameof(nBits)); + + int value = (int)this.ReadValue(nBits); + return this.ReadValue(1) != 0 ? -value : value; + } + + private void InitBitreader(uint size, int pos = 0) + { + long posPlusSize = pos + size; + this.range = 255 - 1; + this.value = 0; + this.bits = -8; // to load the very first 8 bits. + this.eof = false; + this.pos = pos; + this.bufferEnd = (uint)posPlusSize; + this.bufferMax = (uint)(size > 8 ? posPlusSize - 8 + 1 : pos); + + this.LoadNewBytes(); + } + + [MethodImpl(InliningOptions.ColdPath)] + private void LoadNewBytes() + { + if (this.pos < this.bufferMax) + { + ulong inBits = BinaryPrimitives.ReadUInt64LittleEndian(this.Data.Memory.Span.Slice((int)this.pos, 8)); + this.pos += BitsCount >> 3; + ulong bits = ByteSwap64(inBits); + bits >>= 64 - BitsCount; + this.value = bits | (this.value << BitsCount); + this.bits += BitsCount; + } + else + { + this.LoadFinalBytes(); + } + } + + private void LoadFinalBytes() + { + // Only read 8bits at a time. + if (this.pos < this.bufferEnd) + { + this.bits += 8; + this.value = this.Data.Memory.Span[(int)this.pos++] | (this.value << 8); + } + else if (!this.eof) + { + this.value <<= 8; + this.bits += 8; + this.eof = true; + } + else + { + this.bits = 0; // This is to avoid undefined behaviour with shifts. + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static ulong ByteSwap64(ulong x) + { + x = ((x & 0xffffffff00000000ul) >> 32) | ((x & 0x00000000fffffffful) << 32); + x = ((x & 0xffff0000ffff0000ul) >> 16) | ((x & 0x0000ffff0000fffful) << 16); + x = ((x & 0xff00ff00ff00ff00ul) >> 8) | ((x & 0x00ff00ff00ff00fful) << 8); + return x; + } + } +} diff --git a/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs b/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs new file mode 100644 index 0000000..d4b4b4b --- /dev/null +++ b/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs @@ -0,0 +1,206 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.BitReader { + /// + /// A bit reader for reading lossless webp streams. + /// + internal class Vp8LBitReader : BitReaderBase + { + /// + /// Maximum number of bits (inclusive) the bit-reader can handle. + /// + private const int Vp8LMaxNumBitRead = 24; + + /// + /// Number of bits prefetched. + /// + private const int Lbits = 64; + + /// + /// Minimum number of bytes ready after VP8LFillBitWindow. + /// + private const int Wbits = 32; + + private static readonly uint[] BitMask = + [ + 0, + 0x000001, 0x000003, 0x000007, 0x00000f, + 0x00001f, 0x00003f, 0x00007f, 0x0000ff, + 0x0001ff, 0x0003ff, 0x0007ff, 0x000fff, + 0x001fff, 0x003fff, 0x007fff, 0x00ffff, + 0x01ffff, 0x03ffff, 0x07ffff, 0x0fffff, + 0x1fffff, 0x3fffff, 0x7fffff, 0xffffff + ]; + + /// + /// Pre-fetched bits. + /// + private ulong value; + + /// + /// Buffer length. + /// + private readonly long len; + + /// + /// Byte position in buffer. + /// + private long pos; + + /// + /// Current bit-reading position in value. + /// + private int bitPos; + + /// + /// Initializes a new instance of the class. + /// + /// Lossless compressed image data. + public Vp8LBitReader(IMemoryOwner data) + : base(data) + { + this.len = data.Memory.Length; + this.value = 0; + this.bitPos = 0; + this.Eos = false; + + ulong currentValue = 0; + Span dataSpan = this.Data.Memory.Span; + for (int i = 0; i < 8; i++) + { + currentValue |= (ulong)dataSpan[i] << (8 * i); + } + + this.value = currentValue; + this.pos = 8; + } + + /// + /// Initializes a new instance of the class. + /// + /// The input stream to read from. + /// The raw image data size in bytes. + /// Used for allocating memory during reading data from the stream. + public Vp8LBitReader(Stream inputStream, uint imageDataSize, MemoryAllocator memoryAllocator) + : base(inputStream, (int)imageDataSize, memoryAllocator) + { + long length = imageDataSize; + + this.len = length; + this.value = 0; + this.bitPos = 0; + this.Eos = false; + + if (length > sizeof(long)) + { + length = sizeof(long); + } + + ulong currentValue = 0; + Span dataSpan = this.Data.Memory.Span; + for (int i = 0; i < length; i++) + { + currentValue |= (ulong)dataSpan[i] << (8 * i); + } + + this.value = currentValue; + this.pos = length; + } + + /// + /// Gets or sets a value indicating whether a bit was read past the end of buffer. + /// + public bool Eos { get; set; } + + /// + /// Reads a unsigned short value from the buffer. The bits of each byte are read in least-significant-bit-first order. + /// + /// The number of bits to read (should not exceed 16). + /// A ushort value. + [MethodImpl(InliningOptions.ShortMethod)] + public uint ReadValue(int nBits) + { + DebugGuard.MustBeGreaterThan(nBits, 0, nameof(nBits)); + + if (!this.Eos && nBits <= Vp8LMaxNumBitRead) + { + ulong val = this.PrefetchBits() & BitMask[nBits]; + this.bitPos += nBits; + this.ShiftBytes(); + return (uint)val; + } + + return 0; + } + + /// + /// Reads a single bit from the stream. + /// + /// True if the bit read was 1, false otherwise. + [MethodImpl(InliningOptions.ShortMethod)] + public bool ReadBit() + { + uint bit = this.ReadValue(1); + return bit != 0; + } + + /// + /// For jumping over a number of bits in the bit stream when accessed with PrefetchBits and FillBitWindow. + /// + /// The number of bits to advance the position. + [MethodImpl(InliningOptions.ShortMethod)] + public void AdvanceBitPosition(int numberOfBits) => this.bitPos += numberOfBits; + + /// + /// Return the pre-fetched bits, so they can be looked up. + /// + /// The pre-fetched bits. + [MethodImpl(InliningOptions.ShortMethod)] + public ulong PrefetchBits() => this.value >> (this.bitPos & (Lbits - 1)); + + /// + /// Advances the read buffer by 4 bytes to make room for reading next 32 bits. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void FillBitWindow() + { + if (this.bitPos >= Wbits) + { + this.DoFillBitWindow(); + } + } + + /// + /// Returns true if there was an attempt at reading bit past the end of the buffer. + /// + /// True, if end of buffer was reached. + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsEndOfStream() => this.Eos || (this.pos == this.len && this.bitPos > Lbits); + + [MethodImpl(InliningOptions.ShortMethod)] + private void DoFillBitWindow() => this.ShiftBytes(); + + /// + /// If not at EOS, reload up to Vp8LLbits byte-by-byte. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void ShiftBytes() + { + Span dataSpan = this.Data!.Memory.Span; + while (this.bitPos >= 8 && this.pos < this.len) + { + this.value >>= 8; + this.value |= (ulong)dataSpan[(int)this.pos] << (Lbits - 8); + ++this.pos; + this.bitPos -= 8; + } + } + } +} diff --git a/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs new file mode 100644 index 0000000..b20d209 --- /dev/null +++ b/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -0,0 +1,218 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Webp.Chunks; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Webp.BitWriter { + internal abstract class BitWriterBase + { + private const uint MaxDimension = 16777215; + + private const ulong MaxCanvasPixels = 4294967295ul; + + /// + /// Buffer to write to. + /// + private byte[] buffer; + + /// + /// Initializes a new instance of the class. + /// + /// The expected size in bytes. + protected BitWriterBase(int expectedSize) => this.buffer = new byte[expectedSize]; + + /// + /// Initializes a new instance of the class. + /// Used internally for cloning. + /// + /// The byte buffer. + private protected BitWriterBase(byte[] buffer) => this.buffer = buffer; + + public byte[] Buffer => this.buffer; + + /// + /// Gets the number of bytes of the encoded image data. + /// + /// The number of bytes of the image data. + public abstract int NumBytes { get; } + + /// + /// Writes the encoded bytes of the image to the stream. Call Finish() before this. + /// + /// The stream to write to. + public void WriteToStream(Stream stream) => stream.Write(this.Buffer.AsSpan(0, this.NumBytes)); + + /// + /// Writes the encoded bytes of the image to the given buffer. Call Finish() before this. + /// + /// The destination buffer. + public void WriteToBuffer(Span dest) => this.Buffer.AsSpan(0, this.NumBytes).CopyTo(dest); + + /// + /// Resizes the buffer to write to. + /// + /// The extra size in bytes needed. + public abstract void BitWriterResize(int extraSize); + + /// + /// Flush leftover bits. + /// + public abstract void Finish(); + + protected void ResizeBuffer(int maxBytes, int sizeRequired) + { + int newSize = (3 * maxBytes) >> 1; + if (newSize < sizeRequired) + { + newSize = sizeRequired; + } + + // Make new size multiple of 1k. + newSize = ((newSize >> 10) + 1) << 10; + Array.Resize(ref this.buffer, newSize); + } + + /// + /// Write the trunks before data trunk. + /// + /// The stream to write to. + /// The width of the image. + /// The height of the image. + /// The exif profile. + /// The XMP profile. + /// The color profile. + /// Flag indicating, if a alpha channel is present. + /// Flag indicating, if an animation parameter is present. + /// A or a default instance. + public static WebpVp8X WriteTrunksBeforeData( + Stream stream, + uint width, + uint height, + ExifProfile? exifProfile, + XmpProfile? xmpProfile, + IccProfile? iccProfile, + bool hasAlpha, + bool hasAnimation) + { + // Write file size later + RiffHelper.BeginWriteRiffFile(stream, WebpConstants.WebpFourCc); + + // Write VP8X, header if necessary. + WebpVp8X vp8x = default; + bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha || hasAnimation; + if (isVp8X) + { + vp8x = WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha, hasAnimation); + + if (iccProfile != null) + { + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Iccp, iccProfile.ToByteArray()); + } + } + + return vp8x; + } + + /// + /// Writes the encoded image to the stream. + /// + /// The stream to write to. + public abstract void WriteEncodedImageToStream(Stream stream); + + /// + /// Write the trunks after data trunk. + /// + /// The stream to write to. + /// The VP8X chunk. + /// Whether to update the chunk. + /// The initial position of the stream before encoding. + /// The EXIF profile. + /// The XMP profile. + public static void WriteTrunksAfterData( + Stream stream, + in WebpVp8X vp8x, + bool updateVp8x, + long initialPosition, + ExifProfile? exifProfile, + XmpProfile? xmpProfile) + { + if (exifProfile != null) + { + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Exif, exifProfile.ToByteArray()); + } + + if (xmpProfile != null) + { + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Xmp, xmpProfile.Data); + } + + RiffHelper.EndWriteRiffFile(stream, in vp8x, updateVp8x, initialPosition); + } + + /// + /// Writes the animation parameter() to the stream. + /// + /// The stream to write to. + /// + /// The default background color of the canvas in [Blue, Green, Red, Alpha] byte order. + /// This color MAY be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when the Disposal method is 1. + /// + /// The number of times to loop the animation. If it is 0, this means infinitely. + public static void WriteAnimationParameter(Stream stream, Color background, ushort loopCount) + { + WebpAnimationParameter chunk = new(background.ToPixel().PackedValue, loopCount); + chunk.WriteTo(stream); + } + + /// + /// Writes the alpha chunk to the stream. + /// + /// The stream to write to. + /// The alpha channel data bytes. + /// Indicates, if the alpha channel data is compressed. + public static void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDataIsCompressed) + { + long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.Alpha); + byte flags = 0; + if (alphaDataIsCompressed) + { + // TODO: Filtering and preprocessing + flags = 1; + } + + stream.WriteByte(flags); + stream.Write(dataBytes); + RiffHelper.EndWriteChunk(stream, pos); + } + + /// + /// Writes a VP8X header to the stream. + /// + /// The stream to write to. + /// An EXIF profile or null, if it does not exist. + /// An XMP profile or null, if it does not exist. + /// The color profile. + /// The width of the image. + /// The height of the image. + /// Flag indicating, if a alpha channel is present. + /// Flag indicating, if an animation parameter is present. + protected static WebpVp8X WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha, bool hasAnimation) + { + WebpVp8X chunk = new(hasAnimation, xmpProfile != null, exifProfile != null, hasAlpha, iccProfile != null, width, height); + + chunk.Validate(MaxDimension, MaxCanvasPixels); + + chunk.WriteTo(stream); + + return chunk; + } + } +} diff --git a/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs new file mode 100644 index 0000000..1beca1b --- /dev/null +++ b/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -0,0 +1,634 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.IO; +using SixLabors.ImageSharp.Formats.Webp.Lossy; + +namespace SixLabors.ImageSharp.Formats.Webp.BitWriter { + /// + /// A bit writer for writing lossy webp streams. + /// + internal class Vp8BitWriter : BitWriterBase + { +#pragma warning disable SA1310 // Field names should not contain underscore + private const int DC_PRED = 0; + private const int TM_PRED = 1; + private const int V_PRED = 2; + private const int H_PRED = 3; + + // 4x4 modes + private const int B_DC_PRED = 0; + private const int B_TM_PRED = 1; + private const int B_VE_PRED = 2; + private const int B_HE_PRED = 3; + private const int B_RD_PRED = 4; + private const int B_VR_PRED = 5; + private const int B_LD_PRED = 6; + private const int B_VL_PRED = 7; + private const int B_HD_PRED = 8; + private const int B_HU_PRED = 9; +#pragma warning restore SA1310 // Field names should not contain underscore + + private readonly Vp8Encoder enc; + + private int range; + + private int value; + + /// + /// Number of outstanding bits. + /// + private int run; + + /// + /// Number of pending bits. + /// + private int nbBits; + + private uint pos; + + private readonly int maxPos; + + /// + /// Initializes a new instance of the class. + /// + /// The expected size in bytes. + /// The Vp8Encoder. + public Vp8BitWriter(int expectedSize, Vp8Encoder enc) + : base(expectedSize) + { + this.range = 255 - 1; + this.value = 0; + this.run = 0; + this.nbBits = -8; + this.pos = 0; + this.maxPos = 0; + + this.enc = enc; + } + + /// + public override int NumBytes => (int)this.pos; + + public int PutCoeffs(int ctx, Vp8Residual residual) + { + int n = residual.First; + Vp8ProbaArray p = residual.Prob[n].Probabilities[ctx]; + if (!this.PutBit(residual.Last >= 0, p.Probabilities[0])) + { + return 0; + } + + while (n < 16) + { + int c = residual.Coeffs[n++]; + bool sign = c < 0; + int v = sign ? -c : c; + if (!this.PutBit(v != 0, p.Probabilities[1])) + { + p = residual.Prob[WebpConstants.Vp8EncBands[n]].Probabilities[0]; + continue; + } + + if (!this.PutBit(v > 1, p.Probabilities[2])) + { + p = residual.Prob[WebpConstants.Vp8EncBands[n]].Probabilities[1]; + } + else + { + if (!this.PutBit(v > 4, p.Probabilities[3])) + { + if (this.PutBit(v != 2, p.Probabilities[4])) + { + this.PutBit(v == 4, p.Probabilities[5]); + } + } + else if (!this.PutBit(v > 10, p.Probabilities[6])) + { + if (!this.PutBit(v > 6, p.Probabilities[7])) + { + this.PutBit(v == 6, 159); + } + else + { + this.PutBit(v >= 9, 165); + this.PutBit((v & 1) == 0, 145); + } + } + else + { + int mask; + byte[] tab; + if (v < 3 + (8 << 1)) + { + // VP8Cat3 (3b) + this.PutBit(0, p.Probabilities[8]); + this.PutBit(0, p.Probabilities[9]); + v -= 3 + (8 << 0); + mask = 1 << 2; + tab = WebpConstants.Cat3; + } + else if (v < 3 + (8 << 2)) + { + // VP8Cat4 (4b) + this.PutBit(0, p.Probabilities[8]); + this.PutBit(1, p.Probabilities[9]); + v -= 3 + (8 << 1); + mask = 1 << 3; + tab = WebpConstants.Cat4; + } + else if (v < 3 + (8 << 3)) + { + // VP8Cat5 (5b) + this.PutBit(1, p.Probabilities[8]); + this.PutBit(0, p.Probabilities[10]); + v -= 3 + (8 << 2); + mask = 1 << 4; + tab = WebpConstants.Cat5; + } + else + { + // VP8Cat6 (11b) + this.PutBit(1, p.Probabilities[8]); + this.PutBit(1, p.Probabilities[10]); + v -= 3 + (8 << 3); + mask = 1 << 10; + tab = WebpConstants.Cat6; + } + + int tabIdx = 0; + while (mask != 0) + { + this.PutBit(v & mask, tab[tabIdx++]); + mask >>= 1; + } + } + + p = residual.Prob[WebpConstants.Vp8EncBands[n]].Probabilities[2]; + } + + this.PutBitUniform(sign ? 1 : 0); + if (n == 16 || !this.PutBit(n <= residual.Last, p.Probabilities[0])) + { + return 1; // EOB + } + } + + return 1; + } + + /// + /// Resizes the buffer to write to. + /// + /// The extra size in bytes needed. + public override void BitWriterResize(int extraSize) + { + long neededSize = this.pos + extraSize; + if (neededSize <= this.maxPos) + { + return; + } + + this.ResizeBuffer(this.maxPos, (int)neededSize); + } + + /// + public override void Finish() + { + this.PutBits(0, 9 - this.nbBits); + this.nbBits = 0; // pad with zeroes. + this.Flush(); + } + + public void PutSegment(int s, Span p) + { + if (this.PutBit(s >= 2, p[0])) + { + p = p[1..]; + } + + this.PutBit(s & 1, p[1]); + } + + public void PutI16Mode(int mode) + { + if (this.PutBit(mode is TM_PRED or H_PRED, 156)) + { + this.PutBit(mode == TM_PRED, 128); // TM or HE + } + else + { + this.PutBit(mode == V_PRED, 163); // VE or DC + } + } + + public int PutI4Mode(int mode, Span prob) + { + if (this.PutBit(mode != B_DC_PRED, prob[0])) + { + if (this.PutBit(mode != B_TM_PRED, prob[1])) + { + if (this.PutBit(mode != B_VE_PRED, prob[2])) + { + if (!this.PutBit(mode >= B_LD_PRED, prob[3])) + { + if (this.PutBit(mode != B_HE_PRED, prob[4])) + { + this.PutBit(mode != B_RD_PRED, prob[5]); + } + } + else + { + if (this.PutBit(mode != B_LD_PRED, prob[6])) + { + if (this.PutBit(mode != B_VL_PRED, prob[7])) + { + this.PutBit(mode != B_HD_PRED, prob[8]); + } + } + } + } + } + } + + return mode; + } + + public void PutUvMode(int uvMode) + { + // DC_PRED + if (this.PutBit(uvMode != DC_PRED, 142)) + { + // V_PRED + if (this.PutBit(uvMode != V_PRED, 114)) + { + // H_PRED + this.PutBit(uvMode != H_PRED, 183); + } + } + } + + private void PutBits(uint value, int nbBits) + { + for (uint mask = 1u << (nbBits - 1); mask != 0; mask >>= 1) + { + this.PutBitUniform((int)(value & mask)); + } + } + + private bool PutBit(bool bit, int prob) => this.PutBit(bit ? 1 : 0, prob); + + private bool PutBit(int bit, int prob) + { + int split = (this.range * prob) >> 8; + if (bit != 0) + { + this.value += split + 1; + this.range -= split + 1; + } + else + { + this.range = split; + } + + if (this.range < 127) + { + // emit 'shift' bits out and renormalize. + int shift = WebpLookupTables.Norm[this.range]; + this.range = WebpLookupTables.NewRange[this.range]; + this.value <<= shift; + this.nbBits += shift; + if (this.nbBits > 0) + { + this.Flush(); + } + } + + return bit != 0; + } + + private int PutBitUniform(int bit) + { + int split = this.range >> 1; + if (bit != 0) + { + this.value += split + 1; + this.range -= split + 1; + } + else + { + this.range = split; + } + + if (this.range < 127) + { + this.range = WebpLookupTables.NewRange[this.range]; + this.value <<= 1; + this.nbBits += 1; + if (this.nbBits > 0) + { + this.Flush(); + } + } + + return bit; + } + + private void PutSignedBits(int value, int nbBits) + { + if (this.PutBitUniform(value != 0 ? 1 : 0) == 0) + { + return; + } + + if (value < 0) + { + int valueToWrite = (-value << 1) | 1; + this.PutBits((uint)valueToWrite, nbBits + 1); + } + else + { + this.PutBits((uint)(value << 1), nbBits + 1); + } + } + + private void Flush() + { + int s = 8 + this.nbBits; + int bits = this.value >> s; + this.value -= bits << s; + this.nbBits -= 8; + if ((bits & 0xff) != 0xff) + { + uint pos = this.pos; + this.BitWriterResize(this.run + 1); + + if ((bits & 0x100) != 0) + { + // overflow -> propagate carry over pending 0xff's + if (pos > 0) + { + this.Buffer[pos - 1]++; + } + } + + if (this.run > 0) + { + int value = (bits & 0x100) != 0 ? 0x00 : 0xff; + for (; this.run > 0; --this.run) + { + this.Buffer[pos++] = (byte)value; + } + } + + this.Buffer[pos++] = (byte)(bits & 0xff); + this.pos = pos; + } + else + { + this.run++; // Delay writing of bytes 0xff, pending eventual carry. + } + } + + /// + public override void WriteEncodedImageToStream(Stream stream) + { + uint numBytes = (uint)this.NumBytes; + + int mbSize = this.enc.Mbw * this.enc.Mbh; + int expectedSize = (int)((uint)mbSize * 7 / 8); + + Vp8BitWriter bitWriterPartZero = new(expectedSize, this.enc); + + // Partition #0 with header and partition sizes. + uint size0 = bitWriterPartZero.GeneratePartition0(); + + uint vp8Size = WebpConstants.Vp8FrameHeaderSize + size0; + vp8Size += numBytes; + uint pad = vp8Size & 1; + vp8Size += pad; + + // Emit header and partition #0 + this.WriteVp8Header(stream, vp8Size); + this.WriteFrameHeader(stream, size0); + + bitWriterPartZero.WriteToStream(stream); + + // Write the encoded image to the stream. + this.WriteToStream(stream); + if (pad == 1) + { + stream.WriteByte(0); + } + } + + private uint GeneratePartition0() + { + this.PutBitUniform(0); // colorspace + this.PutBitUniform(0); // clamp type + + this.WriteSegmentHeader(); + this.WriteFilterHeader(); + + this.PutBits(0, 2); + + this.WriteQuant(); + this.PutBitUniform(0); + this.WriteProbas(); + this.CodeIntraModes(); + + this.Finish(); + + return (uint)this.NumBytes; + } + + private void WriteSegmentHeader() + { + Vp8EncSegmentHeader hdr = this.enc.SegmentHeader; + Vp8EncProba proba = this.enc.Proba; + if (this.PutBitUniform(hdr.NumSegments > 1 ? 1 : 0) != 0) + { + // We always 'update' the quant and filter strength values. + int updateData = 1; + this.PutBitUniform(hdr.UpdateMap ? 1 : 0); + if (this.PutBitUniform(updateData) != 0) + { + // We always use absolute values, not relative ones. + this.PutBitUniform(1); // (segment_feature_mode = 1. Paragraph 9.3.) + for (int s = 0; s < WebpConstants.NumMbSegments; ++s) + { + this.PutSignedBits(this.enc.SegmentInfos[s].Quant, 7); + } + + for (int s = 0; s < WebpConstants.NumMbSegments; ++s) + { + this.PutSignedBits(this.enc.SegmentInfos[s].FStrength, 6); + } + } + + if (hdr.UpdateMap) + { + for (int s = 0; s < 3; ++s) + { + if (this.PutBitUniform(proba.Segments[s] != 255 ? 1 : 0) != 0) + { + this.PutBits(proba.Segments[s], 8); + } + } + } + } + } + + private void WriteFilterHeader() + { + Vp8FilterHeader hdr = this.enc.FilterHeader; + bool useLfDelta = hdr.I4x4LfDelta != 0; + this.PutBitUniform(hdr.Simple ? 1 : 0); + this.PutBits((uint)hdr.FilterLevel, 6); + this.PutBits((uint)hdr.Sharpness, 3); + if (this.PutBitUniform(useLfDelta ? 1 : 0) != 0) + { + // '0' is the default value for i4x4LfDelta at frame #0. + bool needUpdate = hdr.I4x4LfDelta != 0; + if (this.PutBitUniform(needUpdate ? 1 : 0) != 0) + { + // we don't use refLfDelta => emit four 0 bits. + this.PutBits(0, 4); + + // we use modeLfDelta for i4x4 + this.PutSignedBits(hdr.I4x4LfDelta, 6); + this.PutBits(0, 3); // all others unused. + } + } + } + + // Nominal quantization parameters + private void WriteQuant() + { + this.PutBits((uint)this.enc.BaseQuant, 7); + this.PutSignedBits(this.enc.DqY1Dc, 4); + this.PutSignedBits(this.enc.DqY2Dc, 4); + this.PutSignedBits(this.enc.DqY2Ac, 4); + this.PutSignedBits(this.enc.DqUvDc, 4); + this.PutSignedBits(this.enc.DqUvAc, 4); + } + + private void WriteProbas() + { + Vp8EncProba probas = this.enc.Proba; + for (int t = 0; t < WebpConstants.NumTypes; ++t) + { + for (int b = 0; b < WebpConstants.NumBands; ++b) + { + for (int c = 0; c < WebpConstants.NumCtx; ++c) + { + for (int p = 0; p < WebpConstants.NumProbas; ++p) + { + byte p0 = probas.Coeffs[t][b].Probabilities[c].Probabilities[p]; + bool update = p0 != WebpLookupTables.DefaultCoeffsProba[t, b, c, p]; + if (this.PutBit(update, WebpLookupTables.CoeffsUpdateProba[t, b, c, p])) + { + this.PutBits(p0, 8); + } + } + } + } + } + + if (this.PutBitUniform(probas.UseSkipProba ? 1 : 0) != 0) + { + this.PutBits(probas.SkipProba, 8); + } + } + + // Writes the partition #0 modes (that is: all intra modes) + private void CodeIntraModes() + { + Vp8EncIterator it = new(this.enc); + int predsWidth = this.enc.PredsWidth; + + do + { + Vp8MacroBlockInfo mb = it.CurrentMacroBlockInfo; + int predIdx = it.PredIdx; + Span preds = it.Preds.AsSpan(predIdx); + if (this.enc.SegmentHeader.UpdateMap) + { + this.PutSegment(mb.Segment, this.enc.Proba.Segments); + } + + if (this.enc.Proba.UseSkipProba) + { + this.PutBit(mb.Skip, this.enc.Proba.SkipProba); + } + + if (this.PutBit(mb.MacroBlockType != 0, 145)) + { + // i16x16 + this.PutI16Mode(preds[0]); + } + else + { + Span topPred = it.Preds.AsSpan(predIdx - predsWidth); + for (int y = 0; y < 4; y++) + { + int left = it.Preds[predIdx - 1]; + for (int x = 0; x < 4; x++) + { + byte[] probas = WebpLookupTables.ModesProba[topPred[x], left]; + left = this.PutI4Mode(it.Preds[predIdx + x], probas); + } + + topPred = it.Preds.AsSpan(predIdx); + predIdx += predsWidth; + } + } + + this.PutUvMode(mb.UvMode); + } + while (it.Next()); + } + + private void WriteVp8Header(Stream stream, uint size) + { + Span buf = stackalloc byte[WebpConstants.TagSize]; + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, size); + stream.Write(buf); + } + + private void WriteFrameHeader(Stream stream, uint size0) + { + uint profile = 0; + int width = this.enc.Width; + int height = this.enc.Height; + Span vp8FrameHeader = stackalloc byte[WebpConstants.Vp8FrameHeaderSize]; + + // Paragraph 9.1. + uint bits = 0 // keyframe (1b) + | (profile << 1) // profile (3b) + | (1 << 4) // visible (1b) + | (size0 << 5); // partition length (19b) + + vp8FrameHeader[0] = (byte)((bits >> 0) & 0xff); + vp8FrameHeader[1] = (byte)((bits >> 8) & 0xff); + vp8FrameHeader[2] = (byte)((bits >> 16) & 0xff); + + // signature + vp8FrameHeader[3] = WebpConstants.Vp8HeaderMagicBytes[0]; + vp8FrameHeader[4] = WebpConstants.Vp8HeaderMagicBytes[1]; + vp8FrameHeader[5] = WebpConstants.Vp8HeaderMagicBytes[2]; + + // dimensions + vp8FrameHeader[6] = (byte)(width & 0xff); + vp8FrameHeader[7] = (byte)(width >> 8); + vp8FrameHeader[8] = (byte)(height & 0xff); + vp8FrameHeader[9] = (byte)(height >> 8); + + stream.Write(vp8FrameHeader); + } + } +} diff --git a/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs new file mode 100644 index 0000000..f32bc7a --- /dev/null +++ b/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -0,0 +1,179 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.IO; +using SixLabors.ImageSharp.Formats.Webp.Lossless; + +namespace SixLabors.ImageSharp.Formats.Webp.BitWriter { + /// + /// A bit writer for writing lossless webp streams. + /// + internal class Vp8LBitWriter : BitWriterBase + { + /// + /// This is the minimum amount of size the memory buffer is guaranteed to grow when extra space is needed. + /// + private const int MinExtraSize = 32768; + + private const int WriterBytes = 4; + + private const int WriterBits = 32; + + /// + /// Bit accumulator. + /// + private ulong bits; + + /// + /// Number of bits used in accumulator. + /// + private int used; + + /// + /// Current write position. + /// + private int cur; + + /// + /// Initializes a new instance of the class. + /// + /// The expected size in bytes. + public Vp8LBitWriter(int expectedSize) + : base(expectedSize) + { + } + + /// + /// Initializes a new instance of the class. + /// Used internally for cloning. + /// + private Vp8LBitWriter(byte[] buffer, ulong bits, int used, int cur) + : base(buffer) + { + this.bits = bits; + this.used = used; + this.cur = cur; + } + + /// + public override int NumBytes => this.cur + ((this.used + 7) >> 3); + + /// + /// This function writes bits into bytes in increasing addresses (little endian), + /// and within a byte least-significant-bit first. This function can write up to 32 bits in one go. + /// + public void PutBits(uint bits, int nBits) + { + if (nBits > 0) + { + if (this.used >= 32) + { + this.PutBitsFlushBits(); + } + + this.bits |= (ulong)bits << this.used; + this.used += nBits; + } + } + + public void Reset(Vp8LBitWriter bwInit) + { + this.bits = bwInit.bits; + this.used = bwInit.used; + this.cur = bwInit.cur; + } + + public void WriteHuffmanCode(HuffmanTreeCode code, int codeIndex) + { + int depth = code.CodeLengths[codeIndex]; + int symbol = code.Codes[codeIndex]; + this.PutBits((uint)symbol, depth); + } + + public void WriteHuffmanCodeWithExtraBits(HuffmanTreeCode code, int codeIndex, int bits, int nBits) + { + int depth = code.CodeLengths[codeIndex]; + int symbol = code.Codes[codeIndex]; + this.PutBits((uint)((bits << depth) | symbol), depth + nBits); + } + + public Vp8LBitWriter Clone() + { + byte[] clonedBuffer = new byte[this.Buffer.Length]; + System.Buffer.BlockCopy(this.Buffer, 0, clonedBuffer, 0, this.cur); + return new Vp8LBitWriter(clonedBuffer, this.bits, this.used, this.cur); + } + + /// + public override void Finish() + { + this.BitWriterResize((this.used + 7) >> 3); + while (this.used > 0) + { + this.Buffer[this.cur++] = (byte)this.bits; + this.bits >>= 8; + this.used -= 8; + } + + this.used = 0; + } + + /// + public override void WriteEncodedImageToStream(Stream stream) + { + uint size = (uint)this.NumBytes + 1; // One byte extra for the VP8L signature + uint pad = size & 1; + + // Write magic bytes indicating its a lossless webp. + Span scratchBuffer = stackalloc byte[WebpConstants.TagSize]; + BinaryPrimitives.WriteUInt32BigEndian(scratchBuffer, (uint)WebpChunkType.Vp8L); + stream.Write(scratchBuffer); + + // Write Vp8 Header. + BinaryPrimitives.WriteUInt32LittleEndian(scratchBuffer, size); + stream.Write(scratchBuffer); + stream.WriteByte(WebpConstants.Vp8LHeaderMagicByte); + + // Write the encoded bytes of the image to the stream. + this.WriteToStream(stream); + if (pad == 1) + { + stream.WriteByte(0); + } + } + + /// + /// Internal function for PutBits flushing 32 bits from the written state. + /// + private void PutBitsFlushBits() + { + // If needed, make some room by flushing some bits out. + if (this.cur + WriterBytes > this.Buffer.Length) + { + int extraSize = this.Buffer.Length - this.cur + MinExtraSize; + this.BitWriterResize(extraSize); + } + + Span scratchBuffer = stackalloc byte[8]; + BinaryPrimitives.WriteUInt64LittleEndian(scratchBuffer, this.bits); + scratchBuffer[..4].CopyTo(this.Buffer.AsSpan(this.cur)); + + this.cur += WriterBytes; + this.bits >>= WriterBits; + this.used -= WriterBits; + } + + /// + /// Resizes the buffer to write to. + /// + /// The extra size in bytes needed. + public override void BitWriterResize(int extraSize) + { + int maxBytes = this.Buffer.Length + this.Buffer.Length; + int sizeRequired = this.cur + extraSize; + this.ResizeBuffer(maxBytes, sizeRequired); + } + } +} diff --git a/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs b/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs new file mode 100644 index 0000000..f5d91da --- /dev/null +++ b/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Webp.Chunks { + internal readonly struct WebpAnimationParameter + { + public WebpAnimationParameter(uint background, ushort loopCount) + { + this.Background = background; + this.LoopCount = loopCount; + } + + /// + /// Gets default background color of the canvas in [Blue, Green, Red, Alpha] byte order. + /// This color MAY be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when the Disposal method is 1. + /// + public uint Background { get; } + + /// + /// Gets number of times to loop the animation. If it is 0, this means infinitely. + /// + public ushort LoopCount { get; } + + public void WriteTo(Stream stream) + { + Span buffer = stackalloc byte[6]; + BinaryPrimitives.WriteUInt32LittleEndian(buffer[..4], this.Background); + BinaryPrimitives.WriteUInt16LittleEndian(buffer[4..], this.LoopCount); + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.AnimationParameter, buffer); + } + } +} diff --git a/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs b/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs new file mode 100644 index 0000000..3a033d8 --- /dev/null +++ b/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs @@ -0,0 +1,139 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Webp.Chunks { + internal readonly struct WebpFrameData + { + /// + /// X(3) + Y(3) + Width(3) + Height(3) + Duration(3) + 1 byte for flags. + /// + public const uint HeaderSize = 16; + + public WebpFrameData(uint dataSize, uint x, uint y, uint width, uint height, uint duration, FrameBlendMode blendingMethod, FrameDisposalMode disposalMethod) + { + this.DataSize = dataSize; + this.X = x; + this.Y = y; + this.Width = width; + this.Height = height; + this.Duration = duration; + this.DisposalMethod = disposalMethod; + this.BlendingMethod = blendingMethod; + } + + public WebpFrameData(uint dataSize, uint x, uint y, uint width, uint height, uint duration, int flags) + : this( + dataSize, + x, + y, + width, + height, + duration, + (flags & 2) == 0 ? FrameBlendMode.Over : FrameBlendMode.Source, + (flags & 1) == 1 ? FrameDisposalMode.RestoreToBackground : FrameDisposalMode.DoNotDispose) + { + } + + public WebpFrameData(uint x, uint y, uint width, uint height, uint duration, FrameBlendMode blendingMethod, FrameDisposalMode disposalMethod) + : this(0, x, y, width, height, duration, blendingMethod, disposalMethod) + { + } + + /// + /// Gets the animation chunk size. + /// + public uint DataSize { get; } + + /// + /// Gets the X coordinate of the upper left corner of the frame is Frame X * 2. + /// + public uint X { get; } + + /// + /// Gets the Y coordinate of the upper left corner of the frame is Frame Y * 2. + /// + public uint Y { get; } + + /// + /// Gets the width of the frame. + /// + public uint Width { get; } + + /// + /// Gets the height of the frame. + /// + public uint Height { get; } + + /// + /// Gets the time to wait before displaying the next frame, in 1 millisecond units. + /// Note the interpretation of frame duration of 0 (and often smaller then 10) is implementation defined. + /// + public uint Duration { get; } + + /// + /// Gets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. + /// + public FrameBlendMode BlendingMethod { get; } + + /// + /// Gets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. + /// + public FrameDisposalMode DisposalMethod { get; } + + public Rectangle Bounds => new((int)this.X, (int)this.Y, (int)this.Width, (int)this.Height); + + /// + /// Writes the animation frame() to the stream. + /// + /// The stream to write to. + public long WriteHeaderTo(Stream stream) + { + byte flags = 0; + + if (this.BlendingMethod is FrameBlendMode.Source) + { + // Set blending flag. + flags |= 2; + } + + if (this.DisposalMethod is FrameDisposalMode.RestoreToBackground) + { + // Set disposal flag. + flags |= 1; + } + + long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.FrameData); + + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, (uint)Math.Round(this.X / 2f)); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, (uint)Math.Round(this.Y / 2f)); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Width - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Height - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Duration); + stream.WriteByte(flags); + + return pos; + } + + /// + /// Reads the animation frame header. + /// + /// The stream to read from. + /// Animation frame data. + public static WebpFrameData Parse(Stream stream) + { + Span buffer = stackalloc byte[4]; + + return new WebpFrameData( + dataSize: WebpChunkParsingUtils.ReadChunkSize(stream, buffer), + x: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) * 2, + y: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) * 2, + width: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, + height: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, + duration: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), + flags: stream.ReadByte()); + } + } +} diff --git a/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs b/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs new file mode 100644 index 0000000..9abde77 --- /dev/null +++ b/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs @@ -0,0 +1,138 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Webp.Chunks { + internal readonly struct WebpVp8X : IEquatable + { + public WebpVp8X(bool hasAnimation, bool hasXmp, bool hasExif, bool hasAlpha, bool hasIcc, uint width, uint height) + { + this.HasAnimation = hasAnimation; + this.HasXmp = hasXmp; + this.HasExif = hasExif; + this.HasAlpha = hasAlpha; + this.HasIcc = hasIcc; + this.Width = width; + this.Height = height; + } + + /// + /// Gets a value indicating whether this is an animated image. Data in 'ANIM' and 'ANMF' Chunks should be used to control the animation. + /// + public bool HasAnimation { get; } + + /// + /// Gets a value indicating whether the file contains XMP metadata. + /// + public bool HasXmp { get; } + + /// + /// Gets a value indicating whether the file contains Exif metadata. + /// + public bool HasExif { get; } + + /// + /// Gets a value indicating whether any of the frames of the image contain transparency information ("alpha"). + /// + public bool HasAlpha { get; } + + /// + /// Gets a value indicating whether the file contains an 'ICCP' Chunk. + /// + public bool HasIcc { get; } + + /// + /// Gets width of the canvas in pixels. (uint24) + /// + public uint Width { get; } + + /// + /// Gets height of the canvas in pixels. (uint24) + /// + public uint Height { get; } + + public static bool operator ==(WebpVp8X left, WebpVp8X right) => left.Equals(right); + + public static bool operator !=(WebpVp8X left, WebpVp8X right) => !(left == right); + + public override bool Equals(object? obj) => obj is WebpVp8X x && this.Equals(x); + + public bool Equals(WebpVp8X other) + => this.HasAnimation == other.HasAnimation + && this.HasXmp == other.HasXmp + && this.HasExif == other.HasExif + && this.HasAlpha == other.HasAlpha + && this.HasIcc == other.HasIcc + && this.Width == other.Width + && this.Height == other.Height; + + public override int GetHashCode() + => HashCode.Combine(this.HasAnimation, this.HasXmp, this.HasExif, this.HasAlpha, this.HasIcc, this.Width, this.Height); + + public void Validate(uint maxDimension, ulong maxCanvasPixels) + { + if (this.Width > maxDimension || this.Height > maxDimension) + { + WebpThrowHelper.ThrowInvalidImageDimensions($"Image width or height exceeds maximum allowed dimension of {maxDimension}"); + } + + // The spec states that the product of Canvas Width and Canvas Height MUST be at most 2^32 - 1. + if (this.Width * this.Height > maxCanvasPixels) + { + WebpThrowHelper.ThrowInvalidImageDimensions("The product of image width and height MUST be at most 2^32 - 1"); + } + } + + public WebpVp8X WithAlpha(bool hasAlpha) + => new(this.HasAnimation, this.HasXmp, this.HasExif, hasAlpha, this.HasIcc, this.Width, this.Height); + + public void WriteTo(Stream stream) + { + byte flags = 0; + + if (this.HasAnimation) + { + // Set animated flag. + flags |= 2; + } + + if (this.HasXmp) + { + // Set xmp bit. + flags |= 4; + } + + if (this.HasExif) + { + // Set exif bit. + flags |= 8; + } + + if (this.HasAlpha) + { + // Set alpha bit. + flags |= 16; + } + + if (this.HasIcc) + { + // Set icc flag. + flags |= 32; + } + + long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.Vp8X); + + stream.WriteByte(flags); + + Span reserved = stackalloc byte[3]; + stream.Write(reserved); + + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Width - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Height - 1); + + RiffHelper.EndWriteChunk(stream, pos); + } + } +} diff --git a/ImageSharp/Formats/Webp/EntropyIx.cs b/ImageSharp/Formats/Webp/EntropyIx.cs new file mode 100644 index 0000000..5bfa453 --- /dev/null +++ b/ImageSharp/Formats/Webp/EntropyIx.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// These five modes are evaluated and their respective entropy is computed. + /// + internal enum EntropyIx : byte + { + Direct = 0, + + Spatial = 1, + + SubGreen = 2, + + SpatialSubGreen = 3, + + Palette = 4, + + PaletteAndSpatial = 5, + + NumEntropyIx = 6 + } +} diff --git a/ImageSharp/Formats/Webp/HistoIx.cs b/ImageSharp/Formats/Webp/HistoIx.cs new file mode 100644 index 0000000..91802b4 --- /dev/null +++ b/ImageSharp/Formats/Webp/HistoIx.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp { + internal enum HistoIx : byte + { + HistoAlpha = 0, + + HistoAlphaPred, + + HistoGreen, + + HistoGreenPred, + + HistoRed, + + HistoRedPred, + + HistoBlue, + + HistoBluePred, + + HistoRedSubGreen, + + HistoRedPredSubGreen, + + HistoBlueSubGreen, + + HistoBluePredSubGreen, + + HistoPalette, + + HistoTotal + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs b/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs new file mode 100644 index 0000000..f7a9179 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs @@ -0,0 +1,864 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + internal static class BackwardReferenceEncoder + { + /// + /// Maximum bit length. + /// + public const int MaxLengthBits = 12; + + private const float MaxEntropy = 1e30f; + + private const int WindowOffsetsSizeMax = 32; + + /// + /// We want the max value to be attainable and stored in MaxLengthBits bits. + /// + public const int MaxLength = (1 << MaxLengthBits) - 1; + + /// + /// Minimum number of pixels for which it is cheaper to encode a + /// distance + length instead of each pixel as a literal. + /// + private const int MinLength = 4; + + /// + /// Evaluates best possible backward references for specified quality. The input cacheBits to 'GetBackwardReferences' + /// sets the maximum cache bits to use (passing 0 implies disabling the local color cache). + /// The optimal cache bits is evaluated and set for the cacheBits parameter. + /// The return value is the pointer to the best of the two backward refs viz, refs[0] or refs[1]. + /// + public static Vp8LBackwardRefs GetBackwardReferences( + int width, + int height, + ReadOnlySpan bgra, + uint quality, + int lz77TypesToTry, + ref int cacheBits, + MemoryAllocator memoryAllocator, + Vp8LHashChain hashChain, + Vp8LBackwardRefs best, + Vp8LBackwardRefs worst) + { + int lz77TypeBest = 0; + double bitCostBest = -1; + int cacheBitsInitial = cacheBits; + Vp8LHashChain? hashChainBox = null; + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); + + ColorCache[] colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1]; + for (int lz77Type = 1; lz77TypesToTry > 0; lz77TypesToTry &= ~lz77Type, lz77Type <<= 1) + { + int cacheBitsTmp = cacheBitsInitial; + if ((lz77TypesToTry & lz77Type) == 0) + { + continue; + } + + switch ((Vp8LLz77Type)lz77Type) + { + case Vp8LLz77Type.Lz77Rle: + BackwardReferencesRle(width, height, bgra, 0, worst); + break; + case Vp8LLz77Type.Lz77Standard: + // Compute LZ77 with no cache (0 bits), as the ideal LZ77 with a color cache is not that different in practice. + BackwardReferencesLz77(width, height, bgra, 0, hashChain, worst); + break; + case Vp8LLz77Type.Lz77Box: + hashChainBox = new Vp8LHashChain(memoryAllocator, width * height); + BackwardReferencesLz77Box(width, height, bgra, 0, hashChain, hashChainBox, worst); + break; + } + + // Next, try with a color cache and update the references. + cacheBitsTmp = CalculateBestCacheSize(memoryAllocator, colorCache, bgra, quality, worst, cacheBitsTmp); + if (cacheBitsTmp > 0) + { + BackwardRefsWithLocalCache(bgra, cacheBitsTmp, worst); + } + + // Keep the best backward references. + using OwnedVp8LHistogram histo = OwnedVp8LHistogram.Create(memoryAllocator, worst, cacheBitsTmp); + double bitCost = histo.EstimateBits(stats, bitsEntropy); + + if (lz77TypeBest == 0 || bitCost < bitCostBest) + { + (best, worst) = (worst, best); + bitCostBest = bitCost; + cacheBits = cacheBitsTmp; + lz77TypeBest = lz77Type; + } + } + + // Improve on simple LZ77 but only for high quality (TraceBackwards is costly). + if ((lz77TypeBest == (int)Vp8LLz77Type.Lz77Standard || lz77TypeBest == (int)Vp8LLz77Type.Lz77Box) && quality >= 25) + { + Vp8LHashChain hashChainTmp = lz77TypeBest == (int)Vp8LLz77Type.Lz77Standard ? hashChain : hashChainBox!; + BackwardReferencesTraceBackwards(width, height, memoryAllocator, bgra, cacheBits, hashChainTmp, best, worst); + using OwnedVp8LHistogram histo = OwnedVp8LHistogram.Create(memoryAllocator, worst, cacheBits); + double bitCostTrace = histo.EstimateBits(stats, bitsEntropy); + if (bitCostTrace < bitCostBest) + { + best = worst; + } + } + + BackwardReferences2DLocality(width, best); + + hashChainBox?.Dispose(); + + return best; + } + + /// + /// Evaluate optimal cache bits for the local color cache. + /// The input bestCacheBits sets the maximum cache bits to use (passing 0 implies disabling the local color cache). + /// The local color cache is also disabled for the lower (smaller then 25) quality. + /// + /// Best cache size. + private static int CalculateBestCacheSize( + MemoryAllocator memoryAllocator, + Span colorCache, + ReadOnlySpan bgra, + uint quality, + Vp8LBackwardRefs refs, + int bestCacheBits) + { + int cacheBitsMax = quality <= 25 ? 0 : bestCacheBits; + if (cacheBitsMax == 0) + { + // Local color cache is disabled. + return 0; + } + + double entropyMin = MaxEntropy; + int pos = 0; + + using Vp8LHistogramSet histos = new(memoryAllocator, colorCache.Length, 0); + for (int i = 0; i < colorCache.Length; i++) + { + histos[i].PaletteCodeBits = i; + colorCache[i] = new ColorCache(i); + } + + // Find the cacheBits giving the lowest entropy. + foreach (PixOrCopy v in refs) + { + if (v.IsLiteral()) + { + uint pix = bgra[pos++]; + int a = (int)(pix >> 24) & 0xff; + int r = (int)(pix >> 16) & 0xff; + int g = (int)(pix >> 8) & 0xff; + int b = (int)(pix >> 0) & 0xff; + + // The keys of the caches can be derived from the longest one. + int key = ColorCache.HashPix(pix, 32 - cacheBitsMax); + + // Do not use the color cache for cacheBits = 0. + ++histos[0].Blue[b]; + ++histos[0].Literal[g]; + ++histos[0].Red[r]; + ++histos[0].Alpha[a]; + + // Deal with cacheBits > 0. + for (int i = cacheBitsMax; i >= 1; --i, key >>= 1) + { + if (colorCache[i].Lookup(key) == pix) + { + ++histos[i].Literal[WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + key]; + } + else + { + colorCache[i].Set((uint)key, pix); + ++histos[i].Blue[b]; + ++histos[i].Literal[g]; + ++histos[i].Red[r]; + ++histos[i].Alpha[a]; + } + } + } + else + { + // We should compute the contribution of the (distance, length) + // histograms but those are the same independently from the cache size. + // As those constant contributions are in the end added to the other + // histogram contributions, we can ignore them, except for the length + // prefix that is part of the literal_ histogram. + int len = v.Len; + uint bgraPrev = bgra[pos] ^ 0xffffffffu; + + int extraBits = 0, extraBitsValue = 0; + int code = LosslessUtils.PrefixEncode(len, ref extraBits, ref extraBitsValue); + for (int i = 0; i <= cacheBitsMax; i++) + { + ++histos[i].Literal[WebpConstants.NumLiteralCodes + code]; + } + + // Update the color caches. + do + { + if (bgra[pos] != bgraPrev) + { + // Efficiency: insert only if the color changes. + int key = ColorCache.HashPix(bgra[pos], 32 - cacheBitsMax); + for (int i = cacheBitsMax; i >= 1; --i, key >>= 1) + { + colorCache[i].Colors[key] = bgra[pos]; + } + + bgraPrev = bgra[pos]; + } + + pos++; + } + while (--len != 0); + } + } + + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); + for (int i = 0; i <= cacheBitsMax; i++) + { + double entropy = histos[i].EstimateBits(stats, bitsEntropy); + if (i == 0 || entropy < entropyMin) + { + entropyMin = entropy; + bestCacheBits = i; + } + } + + return bestCacheBits; + } + + private static void BackwardReferencesTraceBackwards( + int xSize, + int ySize, + MemoryAllocator memoryAllocator, + ReadOnlySpan bgra, + int cacheBits, + Vp8LHashChain hashChain, + Vp8LBackwardRefs refsSrc, + Vp8LBackwardRefs refsDst) + { + int distArraySize = xSize * ySize; + using IMemoryOwner distArrayBuffer = memoryAllocator.Allocate(distArraySize); + Span distArray = distArrayBuffer.GetSpan(); + + BackwardReferencesHashChainDistanceOnly(xSize, ySize, memoryAllocator, bgra, cacheBits, hashChain, refsSrc, distArrayBuffer); + int chosenPathSize = TraceBackwards(distArray, distArraySize); + Span chosenPath = distArray[(distArraySize - chosenPathSize)..]; + BackwardReferencesHashChainFollowChosenPath(bgra, cacheBits, chosenPath, chosenPathSize, hashChain, refsDst); + } + + private static void BackwardReferencesHashChainDistanceOnly( + int xSize, + int ySize, + MemoryAllocator memoryAllocator, + ReadOnlySpan bgra, + int cacheBits, + Vp8LHashChain hashChain, + Vp8LBackwardRefs refs, + IMemoryOwner distArrayBuffer) + { + int pixCount = xSize * ySize; + bool useColorCache = cacheBits > 0; + int literalArraySize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (cacheBits > 0 ? 1 << cacheBits : 0); + CostModel costModel = new(memoryAllocator, literalArraySize); + int offsetPrev = -1; + int lenPrev = -1; + double offsetCost = -1; + int firstOffsetIsConstant = -1; // initialized with 'impossible' value. + int reach = 0; + ColorCache? colorCache = null; + + if (useColorCache) + { + colorCache = new ColorCache(cacheBits); + } + + costModel.Build(xSize, cacheBits, refs); + using CostManager costManager = new(memoryAllocator, distArrayBuffer, pixCount, costModel); + Span costManagerCosts = costManager.Costs.GetSpan(); + Span distArray = distArrayBuffer.GetSpan(); + + // We loop one pixel at a time, but store all currently best points to non-processed locations from this point. + distArray[0] = 0; + + // Add first pixel as literal. + AddSingleLiteralWithCostModel(bgra, colorCache, costModel, 0, useColorCache, 0.0f, costManagerCosts, distArray); + + for (int i = 1; i < pixCount; i++) + { + float prevCost = costManagerCosts[i - 1]; + int offset = hashChain.FindOffset(i); + int len = hashChain.FindLength(i); + + // Try adding the pixel as a literal. + AddSingleLiteralWithCostModel(bgra, colorCache, costModel, i, useColorCache, prevCost, costManagerCosts, distArray); + + // If we are dealing with a non-literal. + if (len >= 2) + { + if (offset != offsetPrev) + { + int code = DistanceToPlaneCode(xSize, offset); + offsetCost = costModel.GetDistanceCost(code); + firstOffsetIsConstant = 1; + costManager.PushInterval(prevCost + offsetCost, i, len); + } + else + { + // Instead of considering all contributions from a pixel i by calling: + // costManager.PushInterval(prevCost + offsetCost, i, len); + // we optimize these contributions in case offsetCost stays the same + // for consecutive pixels. This describes a set of pixels similar to a + // previous set (e.g. constant color regions). + if (firstOffsetIsConstant != 0) + { + reach = i - 1 + lenPrev - 1; + firstOffsetIsConstant = 0; + } + + if (i + len - 1 > reach) + { + int lenJ = 0; + int j; + for (j = i; j <= reach; j++) + { + int offsetJ = hashChain.FindOffset(j + 1); + lenJ = hashChain.FindLength(j + 1); + if (offsetJ != offset) + { + lenJ = hashChain.FindLength(j); + break; + } + } + + // Update the cost at j - 1 and j. + costManager.UpdateCostAtIndex(j - 1, false); + costManager.UpdateCostAtIndex(j, false); + + costManager.PushInterval(costManagerCosts[j - 1] + offsetCost, j, lenJ); + reach = j + lenJ - 1; + } + } + } + + costManager.UpdateCostAtIndex(i, true); + offsetPrev = offset; + lenPrev = len; + } + } + + private static int TraceBackwards(Span distArray, int distArraySize) + { + int chosenPathSize = 0; + int pathPos = distArraySize; + int curPos = distArraySize - 1; + while (curPos >= 0) + { + ushort cur = distArray[curPos]; + pathPos--; + chosenPathSize++; + distArray[pathPos] = cur; + curPos -= cur; + } + + return chosenPathSize; + } + + private static void BackwardReferencesHashChainFollowChosenPath(ReadOnlySpan bgra, int cacheBits, Span chosenPath, int chosenPathSize, Vp8LHashChain hashChain, Vp8LBackwardRefs backwardRefs) + { + bool useColorCache = cacheBits > 0; + ColorCache? colorCache = null; + int i = 0; + + if (useColorCache) + { + colorCache = new ColorCache(cacheBits); + } + + backwardRefs.Clear(); + for (int ix = 0; ix < chosenPathSize; ix++) + { + int len = chosenPath[ix]; + if (len != 1) + { + int offset = hashChain.FindOffset(i); + backwardRefs.Add(PixOrCopy.CreateCopy((uint)offset, (ushort)len)); + + if (useColorCache) + { + for (int k = 0; k < len; k++) + { + colorCache!.Insert(bgra[i + k]); + } + } + + i += len; + } + else + { + PixOrCopy v; + int idx = useColorCache ? colorCache!.Contains(bgra[i]) : -1; + if (idx >= 0) + { + // useColorCache is true and color cache contains bgra[i] + // Push pixel as a color cache index. + v = PixOrCopy.CreateCacheIdx(idx); + } + else + { + if (useColorCache) + { + colorCache!.Insert(bgra[i]); + } + + v = PixOrCopy.CreateLiteral(bgra[i]); + } + + backwardRefs.Add(v); + i++; + } + } + } + + private static void AddSingleLiteralWithCostModel( + ReadOnlySpan bgra, + ColorCache? colorCache, + CostModel costModel, + int idx, + bool useColorCache, + float prevCost, + Span cost, + Span distArray) + { + double costVal = prevCost; + uint color = bgra[idx]; + int ix = useColorCache ? colorCache!.Contains(color) : -1; + if (ix >= 0) + { + const double mul0 = 0.68; + costVal += costModel.GetCacheCost((uint)ix) * mul0; + } + else + { + const double mul1 = 0.82; + if (useColorCache) + { + colorCache!.Insert(color); + } + + costVal += costModel.GetLiteralCost(color) * mul1; + } + + if (cost[idx] > costVal) + { + cost[idx] = (float)costVal; + distArray[idx] = 1; // only one is inserted. + } + } + + private static void BackwardReferencesLz77(int xSize, int ySize, ReadOnlySpan bgra, int cacheBits, Vp8LHashChain hashChain, Vp8LBackwardRefs refs) + { + int iLastCheck = -1; + bool useColorCache = cacheBits > 0; + int pixCount = xSize * ySize; + ColorCache? colorCache = null; + if (useColorCache) + { + colorCache = new ColorCache(cacheBits); + } + + refs.Clear(); + for (int i = 0; i < pixCount;) + { + // Alternative #1: Code the pixels starting at 'i' using backward reference. + int j; + int offset = hashChain.FindOffset(i); + int len = hashChain.FindLength(i); + if (len >= MinLength) + { + int lenIni = len; + int maxReach = 0; + int jMax = i + lenIni >= pixCount ? pixCount - 1 : i + lenIni; + + // Only start from what we have not checked already. + iLastCheck = i > iLastCheck ? i : iLastCheck; + + // We know the best match for the current pixel but we try to find the + // best matches for the current pixel AND the next one combined. + // The naive method would use the intervals: + // [i,i+len) + [i+len, length of best match at i+len) + // while we check if we can use: + // [i,j) (where j<=i+len) + [j, length of best match at j) + for (j = iLastCheck + 1; j <= jMax; j++) + { + int lenJ = hashChain.FindLength(j); + int reach = j + (lenJ >= MinLength ? lenJ : 1); // 1 for single literal. + if (reach > maxReach) + { + len = j - i; + maxReach = reach; + if (maxReach >= pixCount) + { + break; + } + } + } + } + else + { + len = 1; + } + + // Go with literal or backward reference. + if (len == 1) + { + AddSingleLiteral(bgra[i], useColorCache, colorCache, refs); + } + else + { + refs.Add(PixOrCopy.CreateCopy((uint)offset, (ushort)len)); + if (useColorCache) + { + for (j = i; j < i + len; j++) + { + colorCache!.Insert(bgra[j]); + } + } + } + + i += len; + } + } + + /// + /// Compute an LZ77 by forcing matches to happen within a given distance cost. + /// We therefore limit the algorithm to the lowest 32 values in the PlaneCode definition. + /// + private static void BackwardReferencesLz77Box(int xSize, int ySize, ReadOnlySpan bgra, int cacheBits, Vp8LHashChain hashChainBest, Vp8LHashChain hashChain, Vp8LBackwardRefs refs) + { + int pixelCount = xSize * ySize; + int[] windowOffsets = new int[WindowOffsetsSizeMax]; + int[] windowOffsetsNew = new int[WindowOffsetsSizeMax]; + int windowOffsetsSize = 0; + int windowOffsetsNewSize = 0; + short[] counts = new short[xSize * ySize]; + int bestOffsetPrev = -1; + int bestLengthPrev = -1; + + // counts[i] counts how many times a pixel is repeated starting at position i. + int i = pixelCount - 2; + int countsPos = i; + counts[countsPos + 1] = 1; + for (; i >= 0; --i, --countsPos) + { + if (bgra[i] == bgra[i + 1]) + { + // Max out the counts to MaxLength. + counts[countsPos] = counts[countsPos + 1]; + if (counts[countsPos + 1] != MaxLength) + { + counts[countsPos]++; + } + } + else + { + counts[countsPos] = 1; + } + } + + // Figure out the window offsets around a pixel. They are stored in a + // spiraling order around the pixel as defined by DistanceToPlaneCode. + for (int y = 0; y <= 6; y++) + { + for (int x = -6; x <= 6; x++) + { + int offset = (y * xSize) + x; + + // Ignore offsets that bring us after the pixel. + if (offset <= 0) + { + continue; + } + + int planeCode = DistanceToPlaneCode(xSize, offset) - 1; + if (planeCode >= WindowOffsetsSizeMax) + { + continue; + } + + windowOffsets[planeCode] = offset; + } + } + + // For narrow images, not all plane codes are reached, so remove those. + for (i = 0; i < WindowOffsetsSizeMax; i++) + { + if (windowOffsets[i] == 0) + { + continue; + } + + windowOffsets[windowOffsetsSize++] = windowOffsets[i]; + } + + // Given a pixel P, find the offsets that reach pixels unreachable from P-1 + // with any of the offsets in windowOffsets[]. + for (i = 0; i < windowOffsetsSize; i++) + { + bool isReachable = false; + for (int j = 0; j < windowOffsetsSize && !isReachable; j++) + { + isReachable |= windowOffsets[i] == windowOffsets[j] + 1; + } + + if (!isReachable) + { + windowOffsetsNew[windowOffsetsNewSize] = windowOffsets[i]; + ++windowOffsetsNewSize; + } + } + + Span hashChainOffsetLength = hashChain.OffsetLength.GetSpan(); + hashChainOffsetLength[0] = 0; + for (i = 1; i < pixelCount; i++) + { + int ind; + int bestLength = hashChainBest.FindLength(i); + int bestOffset = 0; + bool doCompute = true; + + if (bestLength >= MaxLength) + { + // Do not recompute the best match if we already have a maximal one in the window. + bestOffset = hashChainBest.FindOffset(i); + for (ind = 0; ind < windowOffsetsSize; ind++) + { + if (bestOffset == windowOffsets[ind]) + { + doCompute = false; + break; + } + } + } + + if (doCompute) + { + // Figure out if we should use the offset/length from the previous pixel + // as an initial guess and therefore only inspect the offsets in windowOffsetsNew[]. + bool usePrev = bestLengthPrev is > 1 and < MaxLength; + int numInd = usePrev ? windowOffsetsNewSize : windowOffsetsSize; + bestLength = usePrev ? bestLengthPrev - 1 : 0; + bestOffset = usePrev ? bestOffsetPrev : 0; + + // Find the longest match in a window around the pixel. + for (ind = 0; ind < numInd; ind++) + { + int currLength = 0; + int j = i; + int jOffset = usePrev ? i - windowOffsetsNew[ind] : i - windowOffsets[ind]; + if (jOffset < 0 || bgra[jOffset] != bgra[i]) + { + continue; + } + + // The longest match is the sum of how many times each pixel is repeated. + do + { + int countsJOffset = counts[jOffset]; + int countsJ = counts[j]; + if (countsJOffset != countsJ) + { + currLength += countsJOffset < countsJ ? countsJOffset : countsJ; + break; + } + + // The same color is repeated counts_pos times at jOffset and j. + currLength += countsJOffset; + jOffset += countsJOffset; + j += countsJOffset; + } + while (currLength <= MaxLength && j < pixelCount && bgra[jOffset] == bgra[j]); + + if (bestLength < currLength) + { + bestOffset = usePrev ? windowOffsetsNew[ind] : windowOffsets[ind]; + if (currLength >= MaxLength) + { + bestLength = MaxLength; + break; + } + + bestLength = currLength; + } + } + } + + if (bestLength <= MinLength) + { + hashChainOffsetLength[i] = 0; + bestOffsetPrev = 0; + bestLengthPrev = 0; + } + else + { + hashChainOffsetLength[i] = (uint)((bestOffset << MaxLengthBits) | bestLength); + bestOffsetPrev = bestOffset; + bestLengthPrev = bestLength; + } + } + + hashChainOffsetLength[0] = 0; + BackwardReferencesLz77(xSize, ySize, bgra, cacheBits, hashChain, refs); + } + + private static void BackwardReferencesRle(int xSize, int ySize, ReadOnlySpan bgra, int cacheBits, Vp8LBackwardRefs refs) + { + int pixelCount = xSize * ySize; + bool useColorCache = cacheBits > 0; + ColorCache? colorCache = null; + + if (useColorCache) + { + colorCache = new ColorCache(cacheBits); + } + + refs.Clear(); + + // Add first pixel as literal. + AddSingleLiteral(bgra[0], useColorCache, colorCache, refs); + int i = 1; + while (i < pixelCount) + { + int maxLen = LosslessUtils.MaxFindCopyLength(pixelCount - i); + int rleLen = LosslessUtils.FindMatchLength(bgra[i..], bgra[(i - 1)..], 0, maxLen); + int prevRowLen = i < xSize ? 0 : LosslessUtils.FindMatchLength(bgra[i..], bgra[(i - xSize)..], 0, maxLen); + if (rleLen >= prevRowLen && rleLen >= MinLength) + { + refs.Add(PixOrCopy.CreateCopy(1, (ushort)rleLen)); + + // We don't need to update the color cache here since it is always the + // same pixel being copied, and that does not change the color cache state. + i += rleLen; + } + else if (prevRowLen >= MinLength) + { + refs.Add(PixOrCopy.CreateCopy((uint)xSize, (ushort)prevRowLen)); + if (useColorCache) + { + for (int k = 0; k < prevRowLen; ++k) + { + colorCache!.Insert(bgra[i + k]); + } + } + + i += prevRowLen; + } + else + { + AddSingleLiteral(bgra[i], useColorCache, colorCache, refs); + i++; + } + } + } + + /// + /// Update (in-place) backward references for the specified cacheBits. + /// + private static void BackwardRefsWithLocalCache(ReadOnlySpan bgra, int cacheBits, Vp8LBackwardRefs refs) + { + int pixelIndex = 0; + ColorCache colorCache = new(cacheBits); + foreach (ref PixOrCopy v in refs) + { + if (v.IsLiteral()) + { + uint bgraLiteral = v.BgraOrDistance; + int ix = colorCache.Contains(bgraLiteral); + if (ix >= 0) + { + // Color cache contains bgraLiteral + v = PixOrCopy.CreateCacheIdx(ix); + } + else + { + colorCache.Insert(bgraLiteral); + } + + pixelIndex++; + } + else + { + // refs was created without local cache, so it can not have cache indexes. + for (int k = 0; k < v.Len; ++k) + { + colorCache.Insert(bgra[pixelIndex++]); + } + } + } + } + + private static void BackwardReferences2DLocality(int xSize, Vp8LBackwardRefs refs) + { + foreach (ref PixOrCopy v in refs) + { + if (v.IsCopy()) + { + int dist = (int)v.BgraOrDistance; + int transformedDist = DistanceToPlaneCode(xSize, dist); + v = PixOrCopy.CreateCopy((uint)transformedDist, v.Len); + } + } + } + + private static void AddSingleLiteral(uint pixel, bool useColorCache, ColorCache? colorCache, Vp8LBackwardRefs refs) + { + PixOrCopy v; + if (useColorCache) + { + int key = colorCache!.GetIndex(pixel); + if (colorCache.Lookup(key) == pixel) + { + v = PixOrCopy.CreateCacheIdx(key); + } + else + { + v = PixOrCopy.CreateLiteral(pixel); + colorCache.Set((uint)key, pixel); + } + } + else + { + v = PixOrCopy.CreateLiteral(pixel); + } + + refs.Add(v); + } + + public static int DistanceToPlaneCode(int xSize, int dist) + { + int yOffset = dist / xSize; + int xOffset = dist - (yOffset * xSize); + if (xOffset <= 8 && yOffset < 8) + { + return (int)WebpLookupTables.PlaneToCodeLut[(yOffset * 16) + 8 - xOffset] + 1; + } + else if (xOffset > xSize - 8 && yOffset < 7) + { + return (int)WebpLookupTables.PlaneToCodeLut[((yOffset + 1) * 16) + 8 + (xSize - xOffset)] + 1; + } + + return dist + 120; + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/ColorCache.cs b/ImageSharp/Formats/Webp/Lossless/ColorCache.cs new file mode 100644 index 0000000..3b5c9ac --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/ColorCache.cs @@ -0,0 +1,91 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// A small hash-addressed array to store recently used colors, to be able to recall them with shorter codes. + /// + internal class ColorCache + { + private const uint HashMul = 0x1e35a7bdu; + + /// + /// Initializes a new instance of the class. + /// + /// The hashBits determine the size of cache. It will be 1 left shifted by hashBits. + public ColorCache(int hashBits) + { + int hashSize = 1 << hashBits; + this.Colors = new uint[hashSize]; + this.HashBits = hashBits; + this.HashShift = 32 - hashBits; + } + + /// + /// Gets the color entries. + /// + public uint[] Colors { get; } + + /// + /// Gets the hash shift: 32 - hashBits. + /// + public int HashShift { get; } + + /// + /// Gets the hash bits. + /// + public int HashBits { get; } + + /// + /// Inserts a new color into the cache. + /// + /// The color to insert. + [MethodImpl(InliningOptions.ShortMethod)] + public void Insert(uint bgra) + { + int key = HashPix(bgra, this.HashShift); + this.Colors[key] = bgra; + } + + /// + /// Gets a color for a given key. + /// + /// The key to lookup. + /// The color for the key. + [MethodImpl(InliningOptions.ShortMethod)] + public uint Lookup(int key) => this.Colors[key]; + + /// + /// Returns the index of the given color. + /// + /// The color to check. + /// The index of the color in the cache or -1 if its not present. + [MethodImpl(InliningOptions.ShortMethod)] + public int Contains(uint bgra) + { + int key = HashPix(bgra, this.HashShift); + return (this.Colors[key] == bgra) ? key : -1; + } + + /// + /// Gets the index of a color. + /// + /// The color. + /// The index for the color. + [MethodImpl(InliningOptions.ShortMethod)] + public int GetIndex(uint bgra) => HashPix(bgra, this.HashShift); + + /// + /// Adds a new color to the cache. + /// + /// The key. + /// The color to add. + [MethodImpl(InliningOptions.ShortMethod)] + public void Set(uint key, uint bgra) => this.Colors[key] = bgra; + + [MethodImpl(InliningOptions.ShortMethod)] + public static int HashPix(uint argb, int shift) => (int)((argb * HashMul) >> shift); + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/ColorSpaceTransformUtils.cs b/ImageSharp/Formats/Webp/Lossless/ColorSpaceTransformUtils.cs new file mode 100644 index 0000000..3e1a9f5 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/ColorSpaceTransformUtils.cs @@ -0,0 +1,248 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + internal static class ColorSpaceTransformUtils + { + public static void CollectColorBlueTransforms(Span bgra, int stride, int tileWidth, int tileHeight, int greenToBlue, int redToBlue, Span histo) + { + if (Vector256.IsHardwareAccelerated && tileWidth >= 16) + { + const int span = 16; + Span values = stackalloc ushort[span]; + + // These shuffle masks are safe for use with Avx2.Shuffle because all indices are within their respective 128-bit lanes (015 for the low mask, 1631 for the high mask), + // and all disabled lanes are set to 0xFF to zero those bytes per the vpshufb specification. This guarantees lane-local shuffling with no cross-lane violations. + Vector256 collectColorBlueTransformsShuffleLowMask256 = Vector256.Create(255, 2, 255, 6, 255, 10, 255, 14, 255, 255, 255, 255, 255, 255, 255, 255, 255, 18, 255, 22, 255, 26, 255, 30, 255, 255, 255, 255, 255, 255, 255, 255); + Vector256 collectColorBlueTransformsShuffleHighMask256 = Vector256.Create(255, 255, 255, 255, 255, 255, 255, 255, 255, 2, 255, 6, 255, 10, 255, 14, 255, 255, 255, 255, 255, 255, 255, 255, 255, 18, 255, 22, 255, 26, 255, 30); + Vector256 collectColorBlueTransformsGreenBlueMask256 = Vector256.Create(255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0); + Vector256 collectColorBlueTransformsGreenMask256 = Vector256.Create(0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255); + Vector256 collectColorBlueTransformsBlueMask256 = Vector256.Create(255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0); + Vector256 multsr = Vector256.Create(LosslessUtils.Cst5b(redToBlue)); + Vector256 multsg = Vector256.Create(LosslessUtils.Cst5b(greenToBlue)); + for (int y = 0; y < tileHeight; y++) + { + Span srcSpan = bgra[(y * stride)..]; + ref uint inputRef = ref MemoryMarshal.GetReference(srcSpan); + for (nuint x = 0; x <= (uint)tileWidth - span; x += span) + { + nuint input0Idx = x; + nuint input1Idx = x + (span / 2); + Vector256 input0 = Unsafe.As>(ref Unsafe.Add(ref inputRef, input0Idx)).AsByte(); + Vector256 input1 = Unsafe.As>(ref Unsafe.Add(ref inputRef, input1Idx)).AsByte(); + Vector256 r0 = Vector256_.ShufflePerLane(input0, collectColorBlueTransformsShuffleLowMask256); + Vector256 r1 = Vector256_.ShufflePerLane(input1, collectColorBlueTransformsShuffleHighMask256); + Vector256 r = r0 | r1; + Vector256 gb0 = input0 & collectColorBlueTransformsGreenBlueMask256; + Vector256 gb1 = input1 & collectColorBlueTransformsGreenBlueMask256; + Vector256 gb = Vector256_.PackUnsignedSaturate(gb0.AsInt32(), gb1.AsInt32()); + Vector256 g = gb.AsByte() & collectColorBlueTransformsGreenMask256; + Vector256 a = Vector256_.MultiplyHigh(r.AsInt16(), multsr); + Vector256 b = Vector256_.MultiplyHigh(g.AsInt16(), multsg); + Vector256 c = gb.AsByte() - b.AsByte(); + Vector256 d = c - a.AsByte(); + Vector256 e = d & collectColorBlueTransformsBlueMask256; + + ref ushort outputRef = ref MemoryMarshal.GetReference(values); + Unsafe.As>(ref outputRef) = e.AsUInt16(); + + for (int i = 0; i < span; i++) + { + ++histo[values[i]]; + } + } + } + + int leftOver = tileWidth & (span - 1); + if (leftOver > 0) + { + CollectColorBlueTransformsScalar(bgra[(tileWidth - leftOver)..], stride, leftOver, tileHeight, greenToBlue, redToBlue, histo); + } + } + else if (Vector128.IsHardwareAccelerated) + { + const int span = 8; + Span values = stackalloc ushort[span]; + Vector128 collectColorBlueTransformsShuffleLowMask = Vector128.Create(255, 2, 255, 6, 255, 10, 255, 14, 255, 255, 255, 255, 255, 255, 255, 255); + Vector128 collectColorBlueTransformsShuffleHighMask = Vector128.Create(255, 255, 255, 255, 255, 255, 255, 255, 255, 2, 255, 6, 255, 10, 255, 14); + Vector128 collectColorBlueTransformsGreenBlueMask = Vector128.Create(255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0); + Vector128 collectColorBlueTransformsGreenMask = Vector128.Create(0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255); + Vector128 collectColorBlueTransformsBlueMask = Vector128.Create(255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0); + Vector128 multsr = Vector128.Create(LosslessUtils.Cst5b(redToBlue)); + Vector128 multsg = Vector128.Create(LosslessUtils.Cst5b(greenToBlue)); + for (int y = 0; y < tileHeight; y++) + { + Span srcSpan = bgra[(y * stride)..]; + ref uint inputRef = ref MemoryMarshal.GetReference(srcSpan); + for (nuint x = 0; (int)x <= tileWidth - span; x += span) + { + nuint input0Idx = x; + nuint input1Idx = x + (span / 2); + Vector128 input0 = Unsafe.As>(ref Unsafe.Add(ref inputRef, input0Idx)).AsByte(); + Vector128 input1 = Unsafe.As>(ref Unsafe.Add(ref inputRef, input1Idx)).AsByte(); + Vector128 r0 = Vector128_.ShuffleNative(input0, collectColorBlueTransformsShuffleLowMask); + Vector128 r1 = Vector128_.ShuffleNative(input1, collectColorBlueTransformsShuffleHighMask); + Vector128 r = r0 | r1; + Vector128 gb0 = input0 & collectColorBlueTransformsGreenBlueMask; + Vector128 gb1 = input1 & collectColorBlueTransformsGreenBlueMask; + Vector128 gb = Vector128_.PackUnsignedSaturate(gb0.AsInt32(), gb1.AsInt32()); + Vector128 g = gb.AsByte() & collectColorBlueTransformsGreenMask; + Vector128 a = Vector128_.MultiplyHigh(r.AsInt16(), multsr); + Vector128 b = Vector128_.MultiplyHigh(g.AsInt16(), multsg); + Vector128 c = gb.AsByte() - b.AsByte(); + Vector128 d = c - a.AsByte(); + Vector128 e = d & collectColorBlueTransformsBlueMask; + + ref ushort outputRef = ref MemoryMarshal.GetReference(values); + Unsafe.As>(ref outputRef) = e.AsUInt16(); + + for (int i = 0; i < span; i++) + { + ++histo[values[i]]; + } + } + } + + int leftOver = tileWidth & (span - 1); + if (leftOver > 0) + { + CollectColorBlueTransformsScalar(bgra[(tileWidth - leftOver)..], stride, leftOver, tileHeight, greenToBlue, redToBlue, histo); + } + } + else + { + CollectColorBlueTransformsScalar(bgra, stride, tileWidth, tileHeight, greenToBlue, redToBlue, histo); + } + } + + private static void CollectColorBlueTransformsScalar(Span bgra, int stride, int tileWidth, int tileHeight, int greenToBlue, int redToBlue, Span histo) + { + int pos = 0; + while (tileHeight-- > 0) + { + for (int x = 0; x < tileWidth; x++) + { + int idx = LosslessUtils.TransformColorBlue((sbyte)greenToBlue, (sbyte)redToBlue, bgra[pos + x]); + ++histo[idx]; + } + + pos += stride; + } + } + + public static void CollectColorRedTransforms(Span bgra, int stride, int tileWidth, int tileHeight, int greenToRed, Span histo) + { + if (Vector256.IsHardwareAccelerated && tileWidth >= 16) + { + Vector256 collectColorRedTransformsGreenMask256 = Vector256.Create(0x00ff00).AsByte(); + Vector256 collectColorRedTransformsAndMask256 = Vector256.Create((short)0xff).AsByte(); + Vector256 multsg = Vector256.Create(LosslessUtils.Cst5b(greenToRed)); + const int span = 16; + Span values = stackalloc ushort[span]; + for (int y = 0; y < tileHeight; y++) + { + Span srcSpan = bgra[(y * stride)..]; + ref uint inputRef = ref MemoryMarshal.GetReference(srcSpan); + for (nuint x = 0; x <= (uint)tileWidth - span; x += span) + { + nuint input0Idx = x; + nuint input1Idx = x + (span / 2); + Vector256 input0 = Unsafe.As>(ref Unsafe.Add(ref inputRef, input0Idx)).AsByte(); + Vector256 input1 = Unsafe.As>(ref Unsafe.Add(ref inputRef, input1Idx)).AsByte(); + Vector256 g0 = input0 & collectColorRedTransformsGreenMask256; // 0 0 | g 0 + Vector256 g1 = input1 & collectColorRedTransformsGreenMask256; + Vector256 g = Vector256_.PackUnsignedSaturate(g0.AsInt32(), g1.AsInt32()); // g 0 + Vector256 a0 = Vector256.ShiftRightLogical(input0.AsInt32(), 16); // 0 0 | x r + Vector256 a1 = Vector256.ShiftRightLogical(input1.AsInt32(), 16); + Vector256 a = Vector256_.PackUnsignedSaturate(a0, a1); // x r + Vector256 b = Vector256_.MultiplyHigh(g.AsInt16(), multsg); // x dr + Vector256 c = a.AsByte() - b.AsByte(); // x r' + Vector256 d = c & collectColorRedTransformsAndMask256; // 0 r' + + ref ushort outputRef = ref MemoryMarshal.GetReference(values); + Unsafe.As>(ref outputRef) = d.AsUInt16(); + + for (int i = 0; i < span; i++) + { + ++histo[values[i]]; + } + } + } + + int leftOver = tileWidth & (span - 1); + if (leftOver > 0) + { + CollectColorRedTransformsScalar(bgra[(tileWidth - leftOver)..], stride, leftOver, tileHeight, greenToRed, histo); + } + } + else if (Vector128.IsHardwareAccelerated) + { + Vector128 collectColorRedTransformsGreenMask = Vector128.Create(0x00ff00).AsByte(); + Vector128 collectColorRedTransformsAndMask = Vector128.Create((short)0xff).AsByte(); + Vector128 multsg = Vector128.Create(LosslessUtils.Cst5b(greenToRed)); + const int span = 8; + Span values = stackalloc ushort[span]; + for (int y = 0; y < tileHeight; y++) + { + Span srcSpan = bgra[(y * stride)..]; + ref uint inputRef = ref MemoryMarshal.GetReference(srcSpan); + for (nuint x = 0; (int)x <= tileWidth - span; x += span) + { + nuint input0Idx = x; + nuint input1Idx = x + (span / 2); + Vector128 input0 = Unsafe.As>(ref Unsafe.Add(ref inputRef, input0Idx)).AsByte(); + Vector128 input1 = Unsafe.As>(ref Unsafe.Add(ref inputRef, input1Idx)).AsByte(); + Vector128 g0 = input0 & collectColorRedTransformsGreenMask; // 0 0 | g 0 + Vector128 g1 = input1 & collectColorRedTransformsGreenMask; + Vector128 g = Vector128_.PackUnsignedSaturate(g0.AsInt32(), g1.AsInt32()); // g 0 + Vector128 a0 = Vector128.ShiftRightLogical(input0.AsInt32(), 16); // 0 0 | x r + Vector128 a1 = Vector128.ShiftRightLogical(input1.AsInt32(), 16); + Vector128 a = Vector128_.PackUnsignedSaturate(a0, a1); // x r + Vector128 b = Vector128_.MultiplyHigh(g.AsInt16(), multsg); // x dr + Vector128 c = a.AsByte() - b.AsByte(); // x r' + Vector128 d = c & collectColorRedTransformsAndMask; // 0 r' + + ref ushort outputRef = ref MemoryMarshal.GetReference(values); + Unsafe.As>(ref outputRef) = d.AsUInt16(); + + for (int i = 0; i < span; i++) + { + ++histo[values[i]]; + } + } + } + + int leftOver = tileWidth & (span - 1); + if (leftOver > 0) + { + CollectColorRedTransformsScalar(bgra[(tileWidth - leftOver)..], stride, leftOver, tileHeight, greenToRed, histo); + } + } + else + { + CollectColorRedTransformsScalar(bgra, stride, tileWidth, tileHeight, greenToRed, histo); + } + } + + private static void CollectColorRedTransformsScalar(Span bgra, int stride, int tileWidth, int tileHeight, int greenToRed, Span histo) + { + int pos = 0; + while (tileHeight-- > 0) + { + for (int x = 0; x < tileWidth; x++) + { + int idx = LosslessUtils.TransformColorRed((sbyte)greenToRed, bgra[pos + x]); + ++histo[idx]; + } + + pos += stride; + } + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/CostCacheInterval.cs b/ImageSharp/Formats/Webp/Lossless/CostCacheInterval.cs new file mode 100644 index 0000000..d53fb1c --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/CostCacheInterval.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// The GetLengthCost(costModel, k) are cached in a CostCacheInterval. + /// + [DebuggerDisplay("Start: {Start}, End: {End}, Cost: {Cost}")] + internal class CostCacheInterval + { + public double Cost { get; set; } + + public int Start { get; set; } + + public int End { get; set; } // Exclusive. + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/CostInterval.cs b/ImageSharp/Formats/Webp/Lossless/CostInterval.cs new file mode 100644 index 0000000..a3ae4fb --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/CostInterval.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// To perform backward reference every pixel at index index_ is considered and + /// the cost for the MAX_LENGTH following pixels computed. Those following pixels + /// at index index_ + k (k from 0 to MAX_LENGTH) have a cost of: + /// cost = distance cost at index + GetLengthCost(costModel, k) + /// and the minimum value is kept. GetLengthCost(costModel, k) is cached in an + /// array of size MAX_LENGTH. + /// Instead of performing MAX_LENGTH comparisons per pixel, we keep track of the + /// minimal values using intervals of constant cost. + /// An interval is defined by the index_ of the pixel that generated it and + /// is only useful in a range of indices from start to end (exclusive), i.e. + /// it contains the minimum value for pixels between start and end. + /// Intervals are stored in a linked list and ordered by start. When a new + /// interval has a better value, old intervals are split or removed. There are + /// therefore no overlapping intervals. + /// + [DebuggerDisplay("Start: {Start}, End: {End}, Cost: {Cost}")] + internal class CostInterval + { + public float Cost { get; set; } + + public int Start { get; set; } + + public int End { get; set; } + + public int Index { get; set; } + + public CostInterval? Previous { get; set; } + + public CostInterval? Next { get; set; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/CostManager.cs b/ImageSharp/Formats/Webp/Lossless/CostManager.cs new file mode 100644 index 0000000..d6d12f6 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/CostManager.cs @@ -0,0 +1,331 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Collections.Generic; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// The CostManager is in charge of managing intervals and costs. + /// It caches the different CostCacheInterval, caches the different + /// GetLengthCost(costModel, k) in costCache and the CostInterval's. + /// + internal sealed class CostManager : IDisposable + { + private CostInterval? head; + + private const int FreeIntervalsStartCount = 25; + + private readonly Stack freeIntervals = new(FreeIntervalsStartCount); + + public CostManager(MemoryAllocator memoryAllocator, IMemoryOwner distArray, int pixCount, CostModel costModel) + { + int costCacheSize = pixCount > BackwardReferenceEncoder.MaxLength ? BackwardReferenceEncoder.MaxLength : pixCount; + + this.CacheIntervals = new List(); + this.CostCache = new List(); + this.Costs = memoryAllocator.Allocate(pixCount); + this.DistArray = distArray; + this.Count = 0; + + for (int i = 0; i < FreeIntervalsStartCount; i++) + { + this.freeIntervals.Push(new CostInterval()); + } + + // Fill in the cost cache. + this.CacheIntervalsSize++; + this.CostCache.Add(costModel.GetLengthCost(0)); + for (int i = 1; i < costCacheSize; i++) + { + this.CostCache.Add(costModel.GetLengthCost(i)); + + // Get the number of bound intervals. + if (this.CostCache[i] != this.CostCache[i - 1]) + { + this.CacheIntervalsSize++; + } + } + + // Fill in the cache intervals. + CostCacheInterval cur = new() + { + Start = 0, + End = 1, + Cost = this.CostCache[0] + }; + this.CacheIntervals.Add(cur); + + for (int i = 1; i < costCacheSize; i++) + { + double costVal = this.CostCache[i]; + if (costVal != cur.Cost) + { + cur = new CostCacheInterval + { + Start = i, + Cost = costVal + }; + this.CacheIntervals.Add(cur); + } + + cur.End = i + 1; + } + + // Set the initial costs high for every pixel as we will keep the minimum. + this.Costs.GetSpan().Fill(1e38f); + } + + /// + /// Gets or sets the number of stored intervals. + /// + public int Count { get; set; } + + /// + /// Gets the costs cache. Contains the GetLengthCost(costModel, k). + /// + public List CostCache { get; } + + public int CacheIntervalsSize { get; } + + public IMemoryOwner Costs { get; } + + public IMemoryOwner DistArray { get; } + + public List CacheIntervals { get; } + + /// + /// Update the cost at index i by going over all the stored intervals that overlap with i. + /// + /// The index to update. + /// If 'doCleanIntervals' is true, intervals that end before 'i' will be popped. + public void UpdateCostAtIndex(int i, bool doCleanIntervals) + { + CostInterval? current = this.head; + while (current != null && current.Start <= i) + { + CostInterval? next = current.Next; + if (current.End <= i) + { + if (doCleanIntervals) + { + // We have an outdated interval, remove it. + this.PopInterval(current); + } + } + else + { + this.UpdateCost(i, current.Index, current.Cost); + } + + current = next; + } + } + + /// + /// Given a new cost interval defined by its start at position, its length value + /// and distanceCost, add its contributions to the previous intervals and costs. + /// If handling the interval or one of its sub-intervals becomes to heavy, its + /// contribution is added to the costs right away. + /// + public void PushInterval(double distanceCost, int position, int len) + { + // If the interval is small enough, no need to deal with the heavy + // interval logic, just serialize it right away. This constant is empirical. + int skipDistance = 10; + + Span costs = this.Costs.GetSpan(); + Span distArray = this.DistArray.GetSpan(); + if (len < skipDistance) + { + for (int j = position; j < position + len; j++) + { + int k = j - position; + float costTmp = (float)(distanceCost + this.CostCache[k]); + + if (costs[j] > costTmp) + { + costs[j] = costTmp; + distArray[j] = (ushort)(k + 1); + } + } + + return; + } + + CostInterval? interval = this.head; + for (int i = 0; i < this.CacheIntervalsSize && this.CacheIntervals[i].Start < len; i++) + { + // Define the intersection of the ith interval with the new one. + int start = position + this.CacheIntervals[i].Start; + int end = position + (this.CacheIntervals[i].End > len ? len : this.CacheIntervals[i].End); + float cost = (float)(distanceCost + this.CacheIntervals[i].Cost); + + CostInterval? intervalNext; + for (; interval != null && interval.Start < end; interval = intervalNext) + { + intervalNext = interval.Next; + + // Make sure we have some overlap. + if (start >= interval.End) + { + continue; + } + + if (cost >= interval.Cost) + { + // If we are worse than what we already have, add whatever we have so far up to interval. + int startNew = interval.End; + this.InsertInterval(interval, cost, position, start, interval.Start); + start = startNew; + if (start >= end) + { + break; + } + + continue; + } + + if (start <= interval.Start) + { + if (interval.End <= end) + { + // We can safely remove the old interval as it is fully included. + this.PopInterval(interval); + } + else + { + interval.Start = end; + break; + } + } + else + { + if (end < interval.End) + { + // We have to split the old interval as it fully contains the new one. + int endOriginal = interval.End; + interval.End = start; + this.InsertInterval(interval, interval.Cost, interval.Index, end, endOriginal); + break; + } + + interval.End = start; + } + } + + // Insert the remaining interval from start to end. + this.InsertInterval(interval, cost, position, start, end); + } + } + + /// + /// Pop an interval from the manager. + /// + /// The interval to remove. + private void PopInterval(CostInterval? interval) + { + if (interval == null) + { + return; + } + + this.ConnectIntervals(interval.Previous, interval.Next); + this.Count--; + + interval.Next = null; + interval.Previous = null; + this.freeIntervals.Push(interval); + } + + private void InsertInterval(CostInterval? intervalIn, float cost, int position, int start, int end) + { + if (start >= end) + { + return; + } + + // TODO: should we use COST_CACHE_INTERVAL_SIZE_MAX? + CostInterval intervalNew; + if (this.freeIntervals.Count > 0) + { + intervalNew = this.freeIntervals.Pop(); + intervalNew.Cost = cost; + intervalNew.Start = start; + intervalNew.End = end; + intervalNew.Index = position; + } + else + { + intervalNew = new CostInterval { Cost = cost, Start = start, End = end, Index = position }; + } + + this.PositionOrphanInterval(intervalNew, intervalIn); + this.Count++; + } + + /// + /// Given a current orphan interval and its previous interval, before + /// it was orphaned (which can be NULL), set it at the right place in the list + /// of intervals using the start_ ordering and the previous interval as a hint. + /// + private void PositionOrphanInterval(CostInterval current, CostInterval? previous) + { + previous ??= this.head; + + while (previous != null && current.Start < previous.Start) + { + previous = previous.Previous; + } + + while (previous?.Next != null && previous.Next.Start < current.Start) + { + previous = previous.Next; + } + + this.ConnectIntervals(current, previous != null ? previous.Next : this.head); + this.ConnectIntervals(previous, current); + } + + /// + /// Given two intervals, make 'prev' be the previous one of 'next' in 'manager'. + /// + private void ConnectIntervals(CostInterval? prev, CostInterval? next) + { + if (prev != null) + { + prev.Next = next; + } + else + { + this.head = next; + } + + if (next != null) + { + next.Previous = prev; + } + } + + /// + /// Given the cost and the position that define an interval, update the cost at + /// pixel 'i' if it is smaller than the previously computed value. + /// + private void UpdateCost(int i, int position, float cost) + { + Span costs = this.Costs.GetSpan(); + Span distArray = this.DistArray.GetSpan(); + int k = i - position; + if (costs[i] > cost) + { + costs[i] = cost; + distArray[i] = (ushort)(k + 1); + } + } + + /// + public void Dispose() => this.Costs.Dispose(); + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/CostModel.cs b/ImageSharp/Formats/Webp/Lossless/CostModel.cs new file mode 100644 index 0000000..32d4c80 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/CostModel.cs @@ -0,0 +1,104 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + internal class CostModel + { + private readonly MemoryAllocator memoryAllocator; + private const int ValuesInBytes = 256; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The literal array size. + public CostModel(MemoryAllocator memoryAllocator, int literalArraySize) + { + this.memoryAllocator = memoryAllocator; + this.Alpha = new double[ValuesInBytes]; + this.Red = new double[ValuesInBytes]; + this.Blue = new double[ValuesInBytes]; + this.Distance = new double[WebpConstants.NumDistanceCodes]; + this.Literal = new double[literalArraySize]; + } + + public double[] Alpha { get; } + + public double[] Red { get; } + + public double[] Blue { get; } + + public double[] Distance { get; } + + public double[] Literal { get; } + + public void Build(int xSize, int cacheBits, Vp8LBackwardRefs backwardRefs) + { + using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(this.memoryAllocator, cacheBits); + + // The following code is similar to HistogramCreate but converts the distance to plane code. + foreach (PixOrCopy v in backwardRefs) + { + histogram.AddSinglePixOrCopy(in v, true, xSize); + } + + ConvertPopulationCountTableToBitEstimates(histogram.NumCodes(), histogram.Literal, this.Literal); + ConvertPopulationCountTableToBitEstimates(ValuesInBytes, histogram.Red, this.Red); + ConvertPopulationCountTableToBitEstimates(ValuesInBytes, histogram.Blue, this.Blue); + ConvertPopulationCountTableToBitEstimates(ValuesInBytes, histogram.Alpha, this.Alpha); + ConvertPopulationCountTableToBitEstimates(WebpConstants.NumDistanceCodes, histogram.Distance, this.Distance); + } + + public double GetLengthCost(int length) + { + int extraBits = 0; + int code = LosslessUtils.PrefixEncodeBits(length, ref extraBits); + return this.Literal[ValuesInBytes + code] + extraBits; + } + + public double GetDistanceCost(int distance) + { + int extraBits = 0; + int code = LosslessUtils.PrefixEncodeBits(distance, ref extraBits); + return this.Distance[code] + extraBits; + } + + public double GetCacheCost(uint idx) + { + int literalIdx = (int)(ValuesInBytes + WebpConstants.NumLengthCodes + idx); + return this.Literal[literalIdx]; + } + + public double GetLiteralCost(uint v) => this.Alpha[v >> 24] + this.Red[(v >> 16) & 0xff] + this.Literal[(v >> 8) & 0xff] + this.Blue[v & 0xff]; + + private static void ConvertPopulationCountTableToBitEstimates(int numSymbols, Span populationCounts, double[] output) + { + uint sum = 0; + int nonzeros = 0; + for (int i = 0; i < numSymbols; i++) + { + sum += populationCounts[i]; + if (populationCounts[i] > 0) + { + nonzeros++; + } + } + + if (nonzeros <= 1) + { + output.AsSpan(0, numSymbols).Clear(); + } + else + { + double logsum = LosslessUtils.FastLog2(sum); + for (int i = 0; i < numSymbols; i++) + { + output[i] = logsum - LosslessUtils.FastLog2(populationCounts[i]); + } + } + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/CrunchConfig.cs b/ImageSharp/Formats/Webp/Lossless/CrunchConfig.cs new file mode 100644 index 0000000..79001b9 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/CrunchConfig.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + internal class CrunchConfig + { + public EntropyIx EntropyIdx { get; set; } + + public List SubConfigs { get; } = new(); + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/CrunchSubConfig.cs b/ImageSharp/Formats/Webp/Lossless/CrunchSubConfig.cs new file mode 100644 index 0000000..f433597 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/CrunchSubConfig.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + internal class CrunchSubConfig + { + public int Lz77 { get; set; } + + public bool DoNotCache { get; set; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/DominantCostRange.cs b/ImageSharp/Formats/Webp/Lossless/DominantCostRange.cs new file mode 100644 index 0000000..119c41e --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/DominantCostRange.cs @@ -0,0 +1,91 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// Data container to keep track of cost range for the three dominant entropy symbols. + /// + internal class DominantCostRange + { + /// + /// Initializes a new instance of the class. + /// + public DominantCostRange() + { + this.LiteralMax = 0.0d; + this.LiteralMin = double.MaxValue; + this.RedMax = 0.0d; + this.RedMin = double.MaxValue; + this.BlueMax = 0.0d; + this.BlueMin = double.MaxValue; + } + + public double LiteralMax { get; set; } + + public double LiteralMin { get; set; } + + public double RedMax { get; set; } + + public double RedMin { get; set; } + + public double BlueMax { get; set; } + + public double BlueMin { get; set; } + + public void UpdateDominantCostRange(Vp8LHistogram h) + { + if (this.LiteralMax < h.LiteralCost) + { + this.LiteralMax = h.LiteralCost; + } + + if (this.LiteralMin > h.LiteralCost) + { + this.LiteralMin = h.LiteralCost; + } + + if (this.RedMax < h.RedCost) + { + this.RedMax = h.RedCost; + } + + if (this.RedMin > h.RedCost) + { + this.RedMin = h.RedCost; + } + + if (this.BlueMax < h.BlueCost) + { + this.BlueMax = h.BlueCost; + } + + if (this.BlueMin > h.BlueCost) + { + this.BlueMin = h.BlueCost; + } + } + + public int GetHistoBinIndex(Vp8LHistogram h, int numPartitions) + { + int binId = GetBinIdForEntropy(this.LiteralMin, this.LiteralMax, h.LiteralCost, numPartitions); + binId = (binId * numPartitions) + GetBinIdForEntropy(this.RedMin, this.RedMax, h.RedCost, numPartitions); + binId = (binId * numPartitions) + GetBinIdForEntropy(this.BlueMin, this.BlueMax, h.BlueCost, numPartitions); + + return binId; + } + + private static int GetBinIdForEntropy(double min, double max, double val, int numPartitions) + { + double range = max - min; + if (range > 0.0d) + { + double delta = val - min; + return (int)((numPartitions - 1e-6) * delta / range); + } + else + { + return 0; + } + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/HTreeGroup.cs b/ImageSharp/Formats/Webp/Lossless/HTreeGroup.cs new file mode 100644 index 0000000..90ba0c1 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/HTreeGroup.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// Huffman table group. + /// Includes special handling for the following cases: + /// - IsTrivialLiteral: one common literal base for RED/BLUE/ALPHA (not GREEN) + /// - IsTrivialCode: only 1 code (no bit is read from the bitstream) + /// - UsePackedTable: few enough literal symbols, so all the bit codes can fit into a small look-up table PackedTable[] + /// The common literal base, if applicable, is stored in 'LiteralArb'. + /// + internal struct HTreeGroup + { + public HTreeGroup(uint packedTableSize) + { + this.HTrees = new List(WebpConstants.HuffmanCodesPerMetaCode); + this.PackedTable = new HuffmanCode[packedTableSize]; + this.IsTrivialCode = false; + this.IsTrivialLiteral = false; + this.LiteralArb = 0; + this.UsePackedTable = false; + } + + /// + /// Gets the Huffman trees. This has a maximum of (5) entry's. + /// + public List HTrees { get; } + + /// + /// Gets or sets a value indicating whether huffman trees for Red, Blue and Alpha Symbols are trivial (have a single code). + /// + public bool IsTrivialLiteral { get; set; } + + /// + /// Gets or sets a the literal argb value of the pixel. + /// If IsTrivialLiteral is true, this is the ARGB value of the pixel, with Green channel being set to zero. + /// + public uint LiteralArb { get; set; } + + /// + /// Gets or sets a value indicating whether there is only one code. + /// + public bool IsTrivialCode { get; set; } + + /// + /// Gets or sets a value indicating whether to use packed table below for short literal code. + /// + public bool UsePackedTable { get; set; } + + /// + /// Gets or sets table mapping input bits to packed values, or escape case to literal code. + /// + public HuffmanCode[] PackedTable { get; set; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/HistogramBinInfo.cs b/ImageSharp/Formats/Webp/Lossless/HistogramBinInfo.cs new file mode 100644 index 0000000..3c0c6fb --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/HistogramBinInfo.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + internal struct HistogramBinInfo + { + /// + /// Position of the histogram that accumulates all histograms with the same binId. + /// + public short First; + + /// + /// Number of combine failures per binId. + /// + public ushort NumCombineFailures; + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs b/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs new file mode 100644 index 0000000..6e3e5d3 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs @@ -0,0 +1,730 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + internal static class HistogramEncoder + { + /// + /// Number of partitions for the three dominant (literal, red and blue) symbol costs. + /// + private const int NumPartitions = 4; + + /// + /// The size of the bin-hash corresponding to the three dominant costs. + /// + private const int BinSize = NumPartitions * NumPartitions * NumPartitions; + + /// + /// Maximum number of histograms allowed in greedy combining algorithm. + /// + private const int MaxHistoGreedy = 100; + + private const uint NonTrivialSym = 0xffffffff; + + private const ushort InvalidHistogramSymbol = ushort.MaxValue; + + public static void GetHistoImageSymbols( + MemoryAllocator memoryAllocator, + int xSize, + int ySize, + Vp8LBackwardRefs refs, + uint quality, + int histoBits, + int cacheBits, + Vp8LHistogramSet imageHisto, + Vp8LHistogram tmpHisto, + Span histogramSymbols) + { + int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(xSize, histoBits) : 1; + int histoYSize = histoBits > 0 ? LosslessUtils.SubSampleSize(ySize, histoBits) : 1; + int imageHistoRawSize = histoXSize * histoYSize; + const int entropyCombineNumBins = BinSize; + + using IMemoryOwner tmp = memoryAllocator.Allocate(imageHistoRawSize * 2, AllocationOptions.Clean); + Span mapTmp = tmp.Slice(0, imageHistoRawSize); + Span clusterMappings = tmp.Slice(imageHistoRawSize, imageHistoRawSize); + + using Vp8LHistogramSet origHisto = new(memoryAllocator, imageHistoRawSize, cacheBits); + + // Construct the histograms from the backward references. + HistogramBuild(xSize, histoBits, refs, origHisto); + + // Copies the histograms and computes its bitCost. histogramSymbols is optimized. + int numUsed = HistogramCopyAndAnalyze(origHisto, imageHisto, histogramSymbols); + + bool entropyCombine = numUsed > entropyCombineNumBins * 2 && quality < 100; + if (entropyCombine) + { + int numClusters = numUsed; + double combineCostFactor = GetCombineCostFactor(imageHistoRawSize, quality); + HistogramAnalyzeEntropyBin(imageHisto, mapTmp); + + // Collapse histograms with similar entropy. + HistogramCombineEntropyBin(imageHisto, histogramSymbols, clusterMappings, tmpHisto, mapTmp, entropyCombineNumBins, combineCostFactor); + + OptimizeHistogramSymbols(clusterMappings, numClusters, mapTmp, histogramSymbols); + } + + float x = quality / 100F; + + // Cubic ramp between 1 and MaxHistoGreedy: + int thresholdSize = (int)(1 + (x * x * x * (MaxHistoGreedy - 1))); + bool doGreedy = HistogramCombineStochastic(imageHisto, thresholdSize); + if (doGreedy) + { + RemoveEmptyHistograms(imageHisto); + HistogramCombineGreedy(imageHisto); + } + + // Find the optimal map from original histograms to the final ones. + RemoveEmptyHistograms(imageHisto); + HistogramRemap(origHisto, imageHisto, histogramSymbols); + } + + private static void RemoveEmptyHistograms(Vp8LHistogramSet histograms) + { + for (int i = histograms.Count - 1; i >= 0; i--) + { + if (histograms[i] == null) + { + histograms.RemoveAt(i); + } + } + } + + /// + /// Construct the histograms from the backward references. + /// + private static void HistogramBuild( + int xSize, + int histoBits, + Vp8LBackwardRefs backwardRefs, + Vp8LHistogramSet histograms) + { + int x = 0, y = 0; + int histoXSize = LosslessUtils.SubSampleSize(xSize, histoBits); + + foreach (PixOrCopy v in backwardRefs) + { + int ix = ((y >> histoBits) * histoXSize) + (x >> histoBits); + histograms[ix].AddSinglePixOrCopy(in v, false); + x += v.Len; + while (x >= xSize) + { + x -= xSize; + y++; + } + } + } + + /// + /// Partition histograms to different entropy bins for three dominant (literal, + /// red and blue) symbol costs and compute the histogram aggregate bitCost. + /// + private static void HistogramAnalyzeEntropyBin(Vp8LHistogramSet histograms, Span binMap) + { + int histoSize = histograms.Count; + DominantCostRange costRange = new(); + + // Analyze the dominant (literal, red and blue) entropy costs. + for (int i = 0; i < histoSize; i++) + { + if (histograms[i] == null) + { + continue; + } + + costRange.UpdateDominantCostRange(histograms[i]); + } + + // bin-hash histograms on three of the dominant (literal, red and blue) + // symbol costs and store the resulting bin_id for each histogram. + for (int i = 0; i < histoSize; i++) + { + if (histograms[i] == null) + { + continue; + } + + binMap[i] = (ushort)costRange.GetHistoBinIndex(histograms[i], NumPartitions); + } + } + + private static int HistogramCopyAndAnalyze( + Vp8LHistogramSet origHistograms, + Vp8LHistogramSet histograms, + Span histogramSymbols) + { + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); + for (int clusterId = 0, i = 0; i < origHistograms.Count; i++) + { + Vp8LHistogram origHistogram = origHistograms[i]; + origHistogram.UpdateHistogramCost(stats, bitsEntropy); + + // Skip the histogram if it is completely empty, which can happen for tiles with no information (when they are skipped because of LZ77). + if (!origHistogram.IsUsed(0) && !origHistogram.IsUsed(1) && !origHistogram.IsUsed(2) && !origHistogram.IsUsed(3) && !origHistogram.IsUsed(4)) + { + origHistograms[i] = null; + histograms[i] = null; + histogramSymbols[i] = InvalidHistogramSymbol; + } + else + { + origHistogram.CopyTo(histograms[i]); + histogramSymbols[i] = (ushort)clusterId++; + } + } + + int numUsed = 0; + foreach (ushort h in histogramSymbols) + { + if (h != InvalidHistogramSymbol) + { + numUsed++; + } + } + + return numUsed; + } + + private static void HistogramCombineEntropyBin( + Vp8LHistogramSet histograms, + Span clusters, + Span clusterMappings, + Vp8LHistogram curCombo, + ReadOnlySpan binMap, + int numBins, + double combineCostFactor) + { + Span binInfo = stackalloc HistogramBinInfo[BinSize]; + for (int idx = 0; idx < numBins; idx++) + { + binInfo[idx].First = -1; + binInfo[idx].NumCombineFailures = 0; + } + + // By default, a cluster matches itself. + for (int idx = 0; idx < histograms.Count; idx++) + { + clusterMappings[idx] = (ushort)idx; + } + + List indicesToRemove = []; + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); + for (int idx = 0; idx < histograms.Count; idx++) + { + if (histograms[idx] == null) + { + continue; + } + + int binId = binMap[idx]; + int first = binInfo[binId].First; + if (first == -1) + { + binInfo[binId].First = (short)idx; + } + else + { + // Try to merge #idx into #first (both share the same binId) + double bitCost = histograms[idx].BitCost; + double bitCostThresh = -bitCost * combineCostFactor; + double currCostDiff = histograms[first].AddEval(histograms[idx], stats, bitsEntropy, bitCostThresh, curCombo); + + if (currCostDiff < bitCostThresh) + { + // Try to merge two histograms only if the combo is a trivial one or + // the two candidate histograms are already non-trivial. + // For some images, 'tryCombine' turns out to be false for a lot of + // histogram pairs. In that case, we fallback to combining + // histograms as usual to avoid increasing the header size. + bool tryCombine = curCombo.TrivialSymbol != NonTrivialSym || (histograms[idx].TrivialSymbol == NonTrivialSym && histograms[first].TrivialSymbol == NonTrivialSym); + const int maxCombineFailures = 32; + if (tryCombine || binInfo[binId].NumCombineFailures >= maxCombineFailures) + { + // Move the (better) merged histogram to its final slot. + (histograms[first], curCombo) = (curCombo, histograms[first]); + + histograms[idx] = null; + indicesToRemove.Add(idx); + clusterMappings[clusters[idx]] = clusters[first]; + } + else + { + binInfo[binId].NumCombineFailures++; + } + } + } + } + + for (int i = indicesToRemove.Count - 1; i >= 0; i--) + { + histograms.RemoveAt(indicesToRemove[i]); + } + } + + /// + /// Given a Histogram set, the mapping of clusters 'clusterMapping' and the + /// current assignment of the cells in 'symbols', merge the clusters and assign the smallest possible clusters values. + /// + private static void OptimizeHistogramSymbols(Span clusterMappings, int numClusters, Span clusterMappingsTmp, Span symbols) + { + bool doContinue = true; + + // First, assign the lowest cluster to each pixel. + while (doContinue) + { + doContinue = false; + for (int i = 0; i < numClusters; i++) + { + int k = clusterMappings[i]; + while (k != clusterMappings[k]) + { + clusterMappings[k] = clusterMappings[clusterMappings[k]]; + k = clusterMappings[k]; + } + + if (k != clusterMappings[i]) + { + doContinue = true; + clusterMappings[i] = (ushort)k; + } + } + } + + // Create a mapping from a cluster id to its minimal version. + int clusterMax = 0; + clusterMappingsTmp.Clear(); + + // Re-map the ids. + for (int i = 0; i < symbols.Length; i++) + { + if (symbols[i] == InvalidHistogramSymbol) + { + continue; + } + + int cluster = clusterMappings[symbols[i]]; + if (cluster > 0 && clusterMappingsTmp[cluster] == 0) + { + clusterMax++; + clusterMappingsTmp[cluster] = (ushort)clusterMax; + } + + symbols[i] = clusterMappingsTmp[cluster]; + } + } + + /// + /// Perform histogram aggregation using a stochastic approach. + /// + /// true if a greedy approach needs to be performed afterwards, false otherwise. + private static bool HistogramCombineStochastic(Vp8LHistogramSet histograms, int minClusterSize) + { + uint seed = 1; + int triesWithNoSuccess = 0; + int numUsed = histograms.Count(h => h != null); + int outerIters = numUsed; + int numTriesNoSuccess = (int)((uint)outerIters / 2); + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); + + if (numUsed < minClusterSize) + { + return true; + } + + // Priority list of histogram pairs. Its size impacts the quality of the compression and the speed: + // the smaller the faster but the worse for the compression. + List histoPriorityList = []; + const int maxSize = 9; + + // Fill the initial mapping. + Span mappings = histograms.Count <= 64 ? stackalloc int[histograms.Count] : new int[histograms.Count]; + for (int j = 0, i = 0; i < histograms.Count; i++) + { + if (histograms[i] == null) + { + continue; + } + + mappings[j++] = i; + } + + // Collapse similar histograms. + for (int i = 0; i < outerIters && numUsed >= minClusterSize && ++triesWithNoSuccess < numTriesNoSuccess; i++) + { + double bestCost = histoPriorityList.Count == 0 ? 0D : histoPriorityList[0].CostDiff; + int numTries = (int)((uint)numUsed / 2); + uint randRange = (uint)((numUsed - 1) * numUsed); + + // Pick random samples. + for (int j = 0; numUsed >= 2 && j < numTries; j++) + { + // Choose two different histograms at random and try to combine them. + uint tmp = MyRand(ref seed) % randRange; + int idx1 = (int)(tmp / (numUsed - 1)); + int idx2 = (int)(tmp % (numUsed - 1)); + if (idx2 >= idx1) + { + idx2++; + } + + idx1 = mappings[idx1]; + idx2 = mappings[idx2]; + + // Calculate cost reduction on combination. + double currCost = HistoPriorityListPush(histoPriorityList, maxSize, histograms, idx1, idx2, bestCost, stats, bitsEntropy); + + // Found a better pair? + if (currCost < 0) + { + bestCost = currCost; + + if (histoPriorityList.Count == maxSize) + { + break; + } + } + } + + if (histoPriorityList.Count == 0) + { + continue; + } + + // Get the best histograms. + int bestIdx1 = histoPriorityList[0].Idx1; + int bestIdx2 = histoPriorityList[0].Idx2; + + int mappingIndex = mappings.IndexOf(bestIdx2); + Span src = mappings.Slice(mappingIndex + 1, numUsed - mappingIndex - 1); + Span dst = mappings[mappingIndex..]; + src.CopyTo(dst); + + // Merge the histograms and remove bestIdx2 from the list. + HistogramAdd(histograms[bestIdx2], histograms[bestIdx1], histograms[bestIdx1]); + histograms[bestIdx1].BitCost = histoPriorityList[0].CostCombo; + histograms[bestIdx2] = null; + numUsed--; + + for (int j = 0; j < histoPriorityList.Count;) + { + HistogramPair p = histoPriorityList[j]; + bool isIdx1Best = p.Idx1 == bestIdx1 || p.Idx1 == bestIdx2; + bool isIdx2Best = p.Idx2 == bestIdx1 || p.Idx2 == bestIdx2; + bool doEval = false; + + // The front pair could have been duplicated by a random pick so + // check for it all the time nevertheless. + if (isIdx1Best && isIdx2Best) + { + histoPriorityList[j] = histoPriorityList[^1]; + histoPriorityList.RemoveAt(histoPriorityList.Count - 1); + continue; + } + + // Any pair containing one of the two best indices should only refer to + // bestIdx1. Its cost should also be updated. + if (isIdx1Best) + { + p.Idx1 = bestIdx1; + doEval = true; + } + else if (isIdx2Best) + { + p.Idx2 = bestIdx1; + doEval = true; + } + + // Make sure the index order is respected. + if (p.Idx1 > p.Idx2) + { + (p.Idx1, p.Idx2) = (p.Idx2, p.Idx1); + } + + if (doEval) + { + // Re-evaluate the cost of an updated pair. + HistoListUpdatePair(histograms[p.Idx1], histograms[p.Idx2], stats, bitsEntropy, 0D, p); + + if (p.CostDiff >= 0D) + { + histoPriorityList[j] = histoPriorityList[^1]; + histoPriorityList.RemoveAt(histoPriorityList.Count - 1); + continue; + } + } + + HistoListUpdateHead(histoPriorityList, p, j); + j++; + } + + triesWithNoSuccess = 0; + } + + return numUsed <= minClusterSize; + } + + private static void HistogramCombineGreedy(Vp8LHistogramSet histograms) + { + int histoSize = histograms.Count(h => h != null); + + // Priority list of histogram pairs. + List histoPriorityList = []; + int maxSize = histoSize * histoSize; + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); + + for (int i = 0; i < histoSize; i++) + { + if (histograms[i] == null) + { + continue; + } + + for (int j = i + 1; j < histoSize; j++) + { + if (histograms[j] == null) + { + continue; + } + + HistoPriorityListPush(histoPriorityList, maxSize, histograms, i, j, 0.0d, stats, bitsEntropy); + } + } + + while (histoPriorityList.Count > 0) + { + int idx1 = histoPriorityList[0].Idx1; + int idx2 = histoPriorityList[0].Idx2; + HistogramAdd(histograms[idx2], histograms[idx1], histograms[idx1]); + histograms[idx1].BitCost = histoPriorityList[0].CostCombo; + + // Remove merged histogram. + histograms[idx2] = null; + + // Remove pairs intersecting the just combined best pair. + for (int i = 0; i < histoPriorityList.Count;) + { + HistogramPair p = histoPriorityList[i]; + if (p.Idx1 == idx1 || p.Idx2 == idx1 || p.Idx1 == idx2 || p.Idx2 == idx2) + { + // Replace item at pos i with the last one and shrinking the list. + histoPriorityList[i] = histoPriorityList[^1]; + histoPriorityList.RemoveAt(histoPriorityList.Count - 1); + } + else + { + HistoListUpdateHead(histoPriorityList, p, i); + i++; + } + } + + // Push new pairs formed with combined histogram to the list. + for (int i = 0; i < histoSize; i++) + { + if (i == idx1 || histograms[i] == null) + { + continue; + } + + HistoPriorityListPush(histoPriorityList, maxSize, histograms, idx1, i, 0.0d, stats, bitsEntropy); + } + } + } + + private static void HistogramRemap( + Vp8LHistogramSet input, + Vp8LHistogramSet output, + Span symbols) + { + int inSize = input.Count; + int outSize = output.Count; + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); + if (outSize > 1) + { + for (int i = 0; i < inSize; i++) + { + if (input[i] == null) + { + // Arbitrarily set to the previous value if unused to help future LZ77. + symbols[i] = symbols[i - 1]; + continue; + } + + int bestOut = 0; + double bestBits = double.MaxValue; + for (int k = 0; k < outSize; k++) + { + double curBits = output[k].AddThresh(input[i], stats, bitsEntropy, bestBits); + if (k == 0 || curBits < bestBits) + { + bestBits = curBits; + bestOut = k; + } + } + + symbols[i] = (ushort)bestOut; + } + } + else + { + for (int i = 0; i < inSize; i++) + { + symbols[i] = 0; + } + } + + // Recompute each output. + int paletteCodeBits = output[0].PaletteCodeBits; + for (int i = 0; i < outSize; i++) + { + output[i].Clear(); + output[i].PaletteCodeBits = paletteCodeBits; + } + + for (int i = 0; i < inSize; i++) + { + if (input[i] == null) + { + continue; + } + + int idx = symbols[i]; + input[i].Add(output[idx], output[idx]); + } + } + + /// + /// Create a pair from indices "idx1" and "idx2" provided its cost is inferior to "threshold", a negative entropy. + /// + /// The cost of the pair, or 0 if it superior to threshold. + private static double HistoPriorityListPush( + List histoList, + int maxSize, + Vp8LHistogramSet histograms, + int idx1, + int idx2, + double threshold, + Vp8LStreaks stats, + Vp8LBitEntropy bitsEntropy) + { + HistogramPair pair = new(); + + if (histoList.Count == maxSize) + { + return 0D; + } + + if (idx1 > idx2) + { + (idx1, idx2) = (idx2, idx1); + } + + pair.Idx1 = idx1; + pair.Idx2 = idx2; + Vp8LHistogram h1 = histograms[idx1]; + Vp8LHistogram h2 = histograms[idx2]; + + HistoListUpdatePair(h1, h2, stats, bitsEntropy, threshold, pair); + + // Do not even consider the pair if it does not improve the entropy. + if (pair.CostDiff >= threshold) + { + return 0.0d; + } + + histoList.Add(pair); + + HistoListUpdateHead(histoList, pair, histoList.Count - 1); + + return pair.CostDiff; + } + + /// + /// Update the cost diff and combo of a pair of histograms. This needs to be called when the histograms have been + /// merged with a third one. + /// + private static void HistoListUpdatePair( + Vp8LHistogram h1, + Vp8LHistogram h2, + Vp8LStreaks stats, + Vp8LBitEntropy bitsEntropy, + double threshold, + HistogramPair pair) + { + double sumCost = h1.BitCost + h2.BitCost; + pair.CostCombo = 0.0d; + h1.GetCombinedHistogramEntropy(h2, stats, bitsEntropy, sumCost + threshold, costInitial: pair.CostCombo, out double cost); + pair.CostCombo = cost; + pair.CostDiff = pair.CostCombo - sumCost; + } + + /// + /// Check whether a pair in the list should be updated as head or not. + /// + private static void HistoListUpdateHead(List histoList, HistogramPair pair, int idx) + { + if (pair.CostDiff < histoList[0].CostDiff) + { + histoList[idx] = histoList[0]; + histoList[0] = pair; + } + } + + private static void HistogramAdd(Vp8LHistogram a, Vp8LHistogram b, Vp8LHistogram output) + { + a.Add(b, output); + output.TrivialSymbol = a.TrivialSymbol == b.TrivialSymbol ? a.TrivialSymbol : NonTrivialSym; + } + + private static double GetCombineCostFactor(int histoSize, uint quality) + { + double combineCostFactor = 0.16d; + if (quality < 90) + { + if (histoSize > 256) + { + combineCostFactor /= 2.0d; + } + + if (histoSize > 512) + { + combineCostFactor /= 2.0d; + } + + if (histoSize > 1024) + { + combineCostFactor /= 2.0d; + } + + if (quality <= 50) + { + combineCostFactor /= 2.0d; + } + } + + return combineCostFactor; + } + + // Implement a Lehmer random number generator with a multiplicative constant of 48271 and a modulo constant of 2^31 - 1. + [MethodImpl(InliningOptions.ShortMethod)] + private static uint MyRand(ref uint seed) + { + seed = (uint)(((ulong)seed * 48271u) % 2147483647u); + return seed; + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/HistogramPair.cs b/ImageSharp/Formats/Webp/Lossless/HistogramPair.cs new file mode 100644 index 0000000..e04dfa3 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/HistogramPair.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// Pair of histograms. Negative Idx1 value means that pair is out-of-date. + /// + [DebuggerDisplay("Idx1: {Idx1}, Idx2: {Idx2}, CostDiff: {CostDiff}, CostCombo: {CostCombo}")] + internal class HistogramPair + { + public int Idx1 { get; set; } + + public int Idx2 { get; set; } + + public double CostDiff { get; set; } + + public double CostCombo { get; set; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/HuffIndex.cs b/ImageSharp/Formats/Webp/Lossless/HuffIndex.cs new file mode 100644 index 0000000..0a6fc40 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/HuffIndex.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// Five Huffman codes are used at each meta code. + /// + internal static class HuffIndex + { + /// + /// Green + length prefix codes + color cache codes. + /// + public const int Green = 0; + + /// + /// Red. + /// + public const int Red = 1; + + /// + /// Blue. + /// + public const int Blue = 2; + + /// + /// Alpha. + /// + public const int Alpha = 3; + + /// + /// Distance prefix codes. + /// + public const int Dist = 4; + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/HuffmanCode.cs b/ImageSharp/Formats/Webp/Lossless/HuffmanCode.cs new file mode 100644 index 0000000..2b6c97c --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/HuffmanCode.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// A classic way to do entropy coding where a smaller number of bits are used for more frequent codes. + /// + [DebuggerDisplay("BitsUsed: {BitsUsed}, Value: {Value}")] + internal struct HuffmanCode + { + /// + /// Gets or sets the number of bits used for this symbol. + /// + public int BitsUsed { get; set; } + + /// + /// Gets or sets the symbol value or table offset. + /// + public uint Value { get; set; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/HuffmanTree.cs b/ImageSharp/Formats/Webp/Lossless/HuffmanTree.cs new file mode 100644 index 0000000..e50b60d --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/HuffmanTree.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// Represents the Huffman tree. + /// + [DebuggerDisplay("TotalCount = {TotalCount}, Value = {Value}, Left = {PoolIndexLeft}, Right = {PoolIndexRight}")] + internal struct HuffmanTree + { + /// + /// Initializes a new instance of the struct. + /// + /// The HuffmanTree to create an instance from. + private HuffmanTree(HuffmanTree other) + { + this.TotalCount = other.TotalCount; + this.Value = other.Value; + this.PoolIndexLeft = other.PoolIndexLeft; + this.PoolIndexRight = other.PoolIndexRight; + } + + /// + /// Gets or sets the symbol frequency. + /// + public int TotalCount { get; set; } + + /// + /// Gets or sets the symbol value. + /// + public int Value { get; set; } + + /// + /// Gets or sets the index for the left sub-tree. + /// + public int PoolIndexLeft { get; set; } + + /// + /// Gets or sets the index for the right sub-tree. + /// + public int PoolIndexRight { get; set; } + + public static int Compare(HuffmanTree t1, HuffmanTree t2) + { + if (t1.TotalCount > t2.TotalCount) + { + return -1; + } + + if (t1.TotalCount < t2.TotalCount) + { + return 1; + } + + return t1.Value < t2.Value ? -1 : 1; + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/HuffmanTreeCode.cs b/ImageSharp/Formats/Webp/Lossless/HuffmanTreeCode.cs new file mode 100644 index 0000000..f7d723e --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/HuffmanTreeCode.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// Represents the tree codes (depth and bits array). + /// + internal struct HuffmanTreeCode + { + /// + /// Gets or sets the number of symbols. + /// + public int NumSymbols { get; set; } + + /// + /// Gets or sets the code lengths of the symbols. + /// + public byte[] CodeLengths { get; set; } + + /// + /// Gets or sets the symbol Codes. + /// + public short[] Codes { get; set; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/HuffmanTreeToken.cs b/ImageSharp/Formats/Webp/Lossless/HuffmanTreeToken.cs new file mode 100644 index 0000000..5b59a29 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/HuffmanTreeToken.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// Holds the tree header in coded form. + /// + [DebuggerDisplay("Code = {Code}, ExtraBits = {ExtraBits}")] + internal class HuffmanTreeToken + { + /// + /// Gets or sets the code. Value (0..15) or escape code (16, 17, 18). + /// + public byte Code { get; set; } + + /// + /// Gets or sets the extra bits for escape codes. + /// + public byte ExtraBits { get; set; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs b/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs new file mode 100644 index 0000000..c6b2089 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs @@ -0,0 +1,655 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// Utility functions related to creating the huffman tables. + /// + internal static class HuffmanUtils + { + public const int HuffmanTableBits = 8; + + public const int HuffmanPackedBits = 6; + + public const int HuffmanTableMask = (1 << HuffmanTableBits) - 1; + + public const uint HuffmanPackedTableSize = 1u << HuffmanPackedBits; + + // Pre-reversed 4-bit values. + private static readonly byte[] ReversedBits = + [ + 0x0, 0x8, 0x4, 0xc, 0x2, 0xa, 0x6, 0xe, + 0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf + ]; + + public static void CreateHuffmanTree(Span histogram, int treeDepthLimit, bool[] bufRle, Span huffTree, HuffmanTreeCode huffCode) + { + int numSymbols = huffCode.NumSymbols; + bufRle.AsSpan().Clear(); + OptimizeHuffmanForRle(numSymbols, bufRle, histogram); + GenerateOptimalTree(huffTree, histogram, numSymbols, treeDepthLimit, huffCode.CodeLengths); + + // Create the actual bit codes for the bit lengths. + ConvertBitDepthsToSymbols(huffCode); + } + + /// + /// Change the population counts in a way that the consequent + /// Huffman tree compression, especially its RLE-part, give smaller output. + /// + public static void OptimizeHuffmanForRle(int length, bool[] goodForRle, Span counts) + { + // 1) Let's make the Huffman code more compatible with rle encoding. + for (; length >= 0; --length) + { + if (length == 0) + { + return; // All zeros. + } + + if (counts[length - 1] != 0) + { + // Now counts[0..length - 1] does not have trailing zeros. + break; + } + } + + // 2) Let's mark all population counts that already can be encoded with an rle code. + // Let's not spoil any of the existing good rle codes. + // Mark any seq of 0's that is longer as 5 as a goodForRle. + // Mark any seq of non-0's that is longer as 7 as a goodForRle. + uint symbol = counts[0]; + int stride = 0; + for (int i = 0; i < length + 1; i++) + { + if (i == length || counts[i] != symbol) + { + if ((symbol == 0 && stride >= 5) || (symbol != 0 && stride >= 7)) + { + for (int k = 0; k < stride; k++) + { + goodForRle[i - k - 1] = true; + } + } + + stride = 1; + if (i != length) + { + symbol = counts[i]; + } + } + else + { + ++stride; + } + } + + // 3) Let's replace those population counts that lead to more rle codes. + stride = 0; + uint limit = counts[0]; + uint sum = 0; + for (int i = 0; i < length + 1; i++) + { + if (i == length || goodForRle[i] || (i != 0 && goodForRle[i - 1]) || !ValuesShouldBeCollapsedToStrideAverage((int)counts[i], (int)limit)) + { + if (stride >= 4 || (stride >= 3 && sum == 0)) + { + uint k; + + // The stride must end, collapse what we have, if we have enough (4). + uint count = (sum + ((uint)stride / 2)) / (uint)stride; + if (count < 1) + { + count = 1; + } + + if (sum == 0) + { + // Don't make an all zeros stride to be upgraded to ones. + count = 0; + } + + for (k = 0; k < stride; k++) + { + // We don't want to change value at counts[i], + // that is already belonging to the next stride. Thus - 1. + counts[(int)(i - k - 1)] = count; + } + } + + stride = 0; + sum = 0; + if (i < length - 3) + { + // All interesting strides have a count of at least 4, at least when non-zeros. + limit = (counts[i] + counts[i + 1] + + counts[i + 2] + counts[i + 3] + 2) / 4; + } + else if (i < length) + { + limit = counts[i]; + } + else + { + limit = 0; + } + } + + ++stride; + if (i != length) + { + sum += counts[i]; + if (stride >= 4) + { + limit = (sum + ((uint)stride / 2)) / (uint)stride; + } + } + } + } + + /// + /// Create an optimal Huffman tree. + /// + /// + /// The huffman tree. + /// The histogram. + /// The size of the histogram. + /// The tree depth limit. + /// How many bits are used for the symbol. + public static void GenerateOptimalTree(Span tree, Span histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths) + { + uint countMin; + int treeSizeOrig = 0; + + for (int i = 0; i < histogramSize; i++) + { + if (histogram[i] != 0) + { + ++treeSizeOrig; + } + } + + if (treeSizeOrig == 0) + { + return; + } + + Span treePool = tree[treeSizeOrig..]; + + // For block sizes with less than 64k symbols we never need to do a + // second iteration of this loop. + for (countMin = 1; ; countMin *= 2) + { + int treeSize = treeSizeOrig; + + // We need to pack the Huffman tree in treeDepthLimit bits. + // So, we try by faking histogram entries to be at least 'countMin'. + int idx = 0; + for (int j = 0; j < histogramSize; j++) + { + if (histogram[j] != 0) + { + uint count = histogram[j] < countMin ? countMin : histogram[j]; + tree[idx].TotalCount = (int)count; + tree[idx].Value = j; + tree[idx].PoolIndexLeft = -1; + tree[idx].PoolIndexRight = -1; + idx++; + } + } + + // Build the Huffman tree. + Span treeSlice = tree[..treeSize]; + treeSlice.Sort(HuffmanTree.Compare); + + if (treeSize > 1) + { + // Normal case. + int treePoolSize = 0; + while (treeSize > 1) + { + // Finish when we have only one root. + treePool[treePoolSize++] = tree[treeSize - 1]; + treePool[treePoolSize++] = tree[treeSize - 2]; + int count = treePool[treePoolSize - 1].TotalCount + treePool[treePoolSize - 2].TotalCount; + treeSize -= 2; + + // Search for the insertion point. + int k; + for (k = 0; k < treeSize; k++) + { + if (tree[k].TotalCount <= count) + { + break; + } + } + + int endIdx = k + 1; + int num = treeSize - k; + int startIdx = endIdx + num - 1; + for (int i = startIdx; i >= endIdx; i--) + { + tree[i] = tree[i - 1]; + } + + tree[k].TotalCount = count; + tree[k].Value = -1; + tree[k].PoolIndexLeft = treePoolSize - 1; + tree[k].PoolIndexRight = treePoolSize - 2; + treeSize++; + } + + SetBitDepths(tree, treePool, bitDepths, 0); + } + else if (treeSize == 1) + { + // Trivial case: only one element. + bitDepths[tree[0].Value] = 1; + } + + // Test if this Huffman tree satisfies our 'treeDepthLimit' criteria. + int maxDepth = bitDepths[0]; + for (int j = 1; j < histogramSize; j++) + { + if (maxDepth < bitDepths[j]) + { + maxDepth = bitDepths[j]; + } + } + + if (maxDepth <= treeDepthLimit) + { + break; + } + } + } + + public static int CreateCompressedHuffmanTree(HuffmanTreeCode tree, HuffmanTreeToken[] tokensArray) + { + int depthSize = tree.NumSymbols; + int prevValue = 8; // 8 is the initial value for rle. + int i = 0; + int tokenPos = 0; + while (i < depthSize) + { + int value = tree.CodeLengths[i]; + int k = i + 1; + while (k < depthSize && tree.CodeLengths[k] == value) + { + k++; + } + + int runs = k - i; + if (value == 0) + { + tokenPos += CodeRepeatedZeros(runs, tokensArray.AsSpan(tokenPos)); + } + else + { + tokenPos += CodeRepeatedValues(runs, tokensArray.AsSpan(tokenPos), value, prevValue); + prevValue = value; + } + + i += runs; + } + + return tokenPos; + } + + public static int BuildHuffmanTable(Span table, int rootBits, int[] codeLengths, int codeLengthsSize) + { + DebugGuard.MustBeGreaterThan(rootBits, 0, nameof(rootBits)); + DebugGuard.NotNull(codeLengths, nameof(codeLengths)); + DebugGuard.MustBeGreaterThan(codeLengthsSize, 0, nameof(codeLengthsSize)); + + // sorted[codeLengthsSize] is a pre-allocated array for sorting symbols by code length. + Span sorted = codeLengthsSize <= 64 ? stackalloc int[codeLengthsSize] : new int[codeLengthsSize]; + int totalSize = 1 << rootBits; // total size root table + 2nd level table. + int len; // current code length. + int symbol; // symbol index in original or sorted table. + Span counts = stackalloc int[WebpConstants.MaxAllowedCodeLength + 1]; // number of codes of each length. + Span offsets = stackalloc int[WebpConstants.MaxAllowedCodeLength + 1]; // offsets in sorted table for each length. + + // Build histogram of code lengths. + for (symbol = 0; symbol < codeLengthsSize; ++symbol) + { + int codeLengthOfSymbol = codeLengths[symbol]; + if (codeLengthOfSymbol > WebpConstants.MaxAllowedCodeLength) + { + return 0; + } + + counts[codeLengthOfSymbol]++; + } + + // Error, all code lengths are zeros. + if (counts[0] == codeLengthsSize) + { + return 0; + } + + // Generate offsets into sorted symbol table by code length. + offsets[1] = 0; + for (len = 1; len < WebpConstants.MaxAllowedCodeLength; ++len) + { + int codesOfLength = counts[len]; + if (codesOfLength > 1 << len) + { + return 0; + } + + offsets[len + 1] = offsets[len] + codesOfLength; + } + + // Sort symbols by length, by symbol order within each length. + for (symbol = 0; symbol < codeLengthsSize; ++symbol) + { + int symbolCodeLength = codeLengths[symbol]; + if (symbolCodeLength > 0) + { + sorted[offsets[symbolCodeLength]++] = symbol; + } + } + + // Special case code with only one value. + if (offsets[WebpConstants.MaxAllowedCodeLength] == 1) + { + HuffmanCode huffmanCode = new() + { + BitsUsed = 0, + Value = (uint)sorted[0] + }; + ReplicateValue(table, 1, totalSize, huffmanCode); + return totalSize; + } + + int step; // step size to replicate values in current table + int low = -1; // low bits for current root entry + int mask = totalSize - 1; // mask for low bits + int key = 0; // reversed prefix code + int numNodes = 1; // number of Huffman tree nodes + int numOpen = 1; // number of open branches in current tree level + int tableBits = rootBits; // key length of current table + int tableSize = 1 << tableBits; // size of current table + symbol = 0; + + // Fill in root table. + for (len = 1, step = 2; len <= rootBits; ++len, step <<= 1) + { + int countsLen = counts[len]; + numOpen <<= 1; + numNodes += numOpen; + numOpen -= counts[len]; + if (numOpen < 0) + { + return 0; + } + + for (; countsLen > 0; countsLen--) + { + HuffmanCode huffmanCode = new() + { + BitsUsed = len, + Value = (uint)sorted[symbol++] + }; + ReplicateValue(table[key..], step, tableSize, huffmanCode); + key = GetNextKey(key, len); + } + + counts[len] = countsLen; + } + + // Fill in 2nd level tables and add pointers to root table. + Span tableSpan = table; + int tablePos = 0; + for (len = rootBits + 1, step = 2; len <= WebpConstants.MaxAllowedCodeLength; ++len, step <<= 1) + { + numOpen <<= 1; + numNodes += numOpen; + numOpen -= counts[len]; + if (numOpen < 0) + { + return 0; + } + + for (; counts[len] > 0; --counts[len]) + { + if ((key & mask) != low) + { + tableSpan = tableSpan[tableSize..]; + tablePos += tableSize; + tableBits = NextTableBitSize(counts, len, rootBits); + tableSize = 1 << tableBits; + totalSize += tableSize; + low = key & mask; + table[low] = new HuffmanCode + { + BitsUsed = tableBits + rootBits, + Value = (uint)(tablePos - low) + }; + } + + HuffmanCode huffmanCode = new() + { + BitsUsed = len - rootBits, + Value = (uint)sorted[symbol++] + }; + ReplicateValue(tableSpan[(key >> rootBits)..], step, tableSize, huffmanCode); + key = GetNextKey(key, len); + } + } + + return totalSize; + } + + private static int CodeRepeatedZeros(int repetitions, Span tokens) + { + int pos = 0; + while (repetitions >= 1) + { + if (repetitions < 3) + { + for (int i = 0; i < repetitions; i++) + { + tokens[pos].Code = 0; // 0-value + tokens[pos].ExtraBits = 0; + pos++; + } + + break; + } + + if (repetitions < 11) + { + tokens[pos].Code = 17; + tokens[pos].ExtraBits = (byte)(repetitions - 3); + pos++; + break; + } + + if (repetitions < 139) + { + tokens[pos].Code = 18; + tokens[pos].ExtraBits = (byte)(repetitions - 11); + pos++; + break; + } + + tokens[pos].Code = 18; + tokens[pos].ExtraBits = 0x7f; // 138 repeated 0s + pos++; + repetitions -= 138; + } + + return pos; + } + + private static int CodeRepeatedValues(int repetitions, Span tokens, int value, int prevValue) + { + int pos = 0; + + if (value != prevValue) + { + tokens[pos].Code = (byte)value; + tokens[pos].ExtraBits = 0; + pos++; + repetitions--; + } + + while (repetitions >= 1) + { + if (repetitions < 3) + { + int i; + for (i = 0; i < repetitions; i++) + { + tokens[pos].Code = (byte)value; + tokens[pos].ExtraBits = 0; + pos++; + } + + break; + } + + if (repetitions < 7) + { + tokens[pos].Code = 16; + tokens[pos].ExtraBits = (byte)(repetitions - 3); + pos++; + break; + } + + tokens[pos].Code = 16; + tokens[pos].ExtraBits = 3; + pos++; + repetitions -= 6; + } + + return pos; + } + + /// + /// Get the actual bit values for a tree of bit depths. + /// + /// The huffman tree. + private static void ConvertBitDepthsToSymbols(HuffmanTreeCode tree) + { + // 0 bit-depth means that the symbol does not exist. + Span nextCode = stackalloc uint[WebpConstants.MaxAllowedCodeLength + 1]; + Span depthCount = stackalloc int[WebpConstants.MaxAllowedCodeLength + 1]; + + int len = tree.NumSymbols; + for (int i = 0; i < len; i++) + { + int codeLength = tree.CodeLengths[i]; + depthCount[codeLength]++; + } + + depthCount[0] = 0; // ignore unused symbol. + nextCode[0] = 0; + + uint code = 0; + for (int i = 1; i <= WebpConstants.MaxAllowedCodeLength; i++) + { + code = (uint)((code + depthCount[i - 1]) << 1); + nextCode[i] = code; + } + + for (int i = 0; i < len; i++) + { + int codeLength = tree.CodeLengths[i]; + tree.Codes[i] = (short)ReverseBits(codeLength, nextCode[codeLength]++); + } + } + + private static void SetBitDepths(Span tree, Span pool, byte[] bitDepths, int level) + { + if (tree[0].PoolIndexLeft >= 0) + { + SetBitDepths(pool[tree[0].PoolIndexLeft..], pool, bitDepths, level + 1); + SetBitDepths(pool[tree[0].PoolIndexRight..], pool, bitDepths, level + 1); + } + else + { + bitDepths[tree[0].Value] = (byte)level; + } + } + + private static uint ReverseBits(int numBits, uint bits) + { + uint retval = 0; + int i = 0; + while (i < numBits) + { + i += 4; + retval |= (uint)(ReversedBits[bits & 0xf] << (WebpConstants.MaxAllowedCodeLength + 1 - i)); + bits >>= 4; + } + + retval >>= WebpConstants.MaxAllowedCodeLength + 1 - numBits; + return retval; + } + + /// + /// Returns the table width of the next 2nd level table. count is the histogram of bit lengths for the remaining symbols, + /// len is the code length of the next processed symbol. + /// + private static int NextTableBitSize(ReadOnlySpan count, int len, int rootBits) + { + int left = 1 << (len - rootBits); + while (len < WebpConstants.MaxAllowedCodeLength) + { + left -= count[len]; + if (left <= 0) + { + break; + } + + ++len; + left <<= 1; + } + + return len - rootBits; + } + + /// + /// Stores code in table[0], table[step], table[2*step], ..., table[end-step]. + /// Assumes that end is an integer multiple of step. + /// + private static void ReplicateValue(Span table, int step, int end, HuffmanCode code) + { + DebugGuard.IsTrue(end % step == 0, nameof(end), "end must be a multiple of step"); + + do + { + end -= step; + table[end] = code; + } + while (end > 0); + } + + /// + /// Returns reverse(reverse(key, len) + 1, len), where reverse(key, len) is the + /// bit-wise reversal of the len least significant bits of key. + /// + private static int GetNextKey(int key, int len) + { + int step = 1 << (len - 1); + while ((key & step) != 0) + { + step >>= 1; + } + + return step != 0 ? (key & (step - 1)) + step : key; + } + + /// + /// Heuristics for selecting the stride ranges to collapse. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static bool ValuesShouldBeCollapsedToStrideAverage(int a, int b) => Math.Abs(a - b) < 4; + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs b/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs new file mode 100644 index 0000000..fa6c01d --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs @@ -0,0 +1,1496 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// Utility functions for the lossless decoder. + /// + internal static unsafe class LosslessUtils + { + private const int PrefixLookupIdxMax = 512; + + private const int LogLookupIdxMax = 256; + + private const int ApproxLogMax = 4096; + + private const int ApproxLogWithCorrectionMax = 65536; + + private const double Log2Reciprocal = 1.44269504088896338700465094007086; + + /// + /// Returns the exact index where array1 and array2 are different. For an index + /// inferior or equal to bestLenMatch, the return value just has to be strictly + /// inferior to bestLenMatch match. The current behavior is to return 0 if this index + /// is bestLenMatch, and the index itself otherwise. + /// If no two elements are the same, it returns maxLimit. + /// + public static int FindMatchLength(ReadOnlySpan array1, ReadOnlySpan array2, int bestLenMatch, int maxLimit) + { + // Before 'expensive' linear match, check if the two arrays match at the + // current best length index. + if (array1[bestLenMatch] != array2[bestLenMatch]) + { + return 0; + } + + return VectorMismatch(array1, array2, maxLimit); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static int VectorMismatch(ReadOnlySpan array1, ReadOnlySpan array2, int length) + { + int matchLen = 0; + ref uint array1Ref = ref MemoryMarshal.GetReference(array1); + ref uint array2Ref = ref MemoryMarshal.GetReference(array2); + + while (matchLen < length && Unsafe.Add(ref array1Ref, (uint)matchLen) == Unsafe.Add(ref array2Ref, (uint)matchLen)) + { + matchLen++; + } + + return matchLen; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static int MaxFindCopyLength(int len) => len < BackwardReferenceEncoder.MaxLength ? len : BackwardReferenceEncoder.MaxLength; + + public static int PrefixEncodeBits(int distance, ref int extraBits) + { + if (distance < PrefixLookupIdxMax) + { + (int code, int bits) = WebpLookupTables.PrefixEncodeCode[distance]; + extraBits = bits; + return code; + } + + return PrefixEncodeBitsNoLut(distance, ref extraBits); + } + + public static int PrefixEncode(int distance, ref int extraBits, ref int extraBitsValue) + { + if (distance < PrefixLookupIdxMax) + { + (int code, int bits) = WebpLookupTables.PrefixEncodeCode[distance]; + extraBits = bits; + extraBitsValue = WebpLookupTables.PrefixEncodeExtraBitsValue[distance]; + + return code; + } + + return PrefixEncodeNoLut(distance, ref extraBits, ref extraBitsValue); + } + + /// + /// Add green to blue and red channels (i.e. perform the inverse transform of 'subtract green'). + /// + /// The pixel data to apply the transformation. + public static void AddGreenToBlueAndRed(Span pixelData) + { + if (Vector256.IsHardwareAccelerated && pixelData.Length >= 8) + { + // The `255` values disable the write for alpha (A), since 0x80 is set in the control byte (high bit set). + // Each byte index is within its respective 128-bit lane (015 and 1631), so this is safe for per-lane shuffle. + // The high bits are not set for the index bytes, and the values are always < 16 per lane, satisfying AVX2 lane rules. + Vector256 addGreenToBlueAndRedMask = Vector256.Create(1, 255, 1, 255, 5, 255, 5, 255, 9, 255, 9, 255, 13, 255, 13, 255, 17, 255, 17, 255, 21, 255, 21, 255, 25, 255, 25, 255, 29, 255, 29, 255); + nuint numPixels = (uint)pixelData.Length; + nuint i = 0; + do + { + ref uint pos = ref Unsafe.Add(ref MemoryMarshal.GetReference(pixelData), i); + Vector256 input = Unsafe.As>(ref pos).AsByte(); + Vector256 in0g0g = Vector256_.ShufflePerLane(input, addGreenToBlueAndRedMask); + Vector256 output = input + in0g0g; + Unsafe.As>(ref pos) = output.AsUInt32(); + i += 8; + } + while (i <= numPixels - 8); + + if (i != numPixels) + { + AddGreenToBlueAndRedScalar(pixelData[(int)i..]); + } + } + else if (Vector128.IsHardwareAccelerated && pixelData.Length >= 4) + { + Vector128 addGreenToBlueAndRedMask = Vector128.Create(1, 255, 1, 255, 5, 255, 5, 255, 9, 255, 9, 255, 13, 255, 13, 255); + nuint numPixels = (uint)pixelData.Length; + nuint i = 0; + do + { + ref uint pos = ref Unsafe.Add(ref MemoryMarshal.GetReference(pixelData), i); + Vector128 input = Unsafe.As>(ref pos).AsByte(); + Vector128 in0g0g = Vector128_.ShuffleNative(input, addGreenToBlueAndRedMask); + Vector128 output = input + in0g0g; + Unsafe.As>(ref pos) = output.AsUInt32(); + i += 4; + } + while (i <= numPixels - 4); + + if (i != numPixels) + { + AddGreenToBlueAndRedScalar(pixelData[(int)i..]); + } + } + else + { + AddGreenToBlueAndRedScalar(pixelData); + } + } + + private static void AddGreenToBlueAndRedScalar(Span pixelData) + { + int numPixels = pixelData.Length; + for (int i = 0; i < numPixels; i++) + { + uint argb = pixelData[i]; + uint green = (argb >> 8) & 0xff; + uint redBlue = argb & 0x00ff00ffu; + redBlue += (green << 16) | green; + redBlue &= 0x00ff00ffu; + pixelData[i] = (argb & 0xff00ff00u) | redBlue; + } + } + + public static void SubtractGreenFromBlueAndRed(Span pixelData) + { + if (Vector256.IsHardwareAccelerated && pixelData.Length >= 8) + { + Vector256 subtractGreenFromBlueAndRedMask = Vector256.Create(1, 255, 1, 255, 5, 255, 5, 255, 9, 255, 9, 255, 13, 255, 13, 255, 17, 255, 17, 255, 21, 255, 21, 255, 25, 255, 25, 255, 29, 255, 29, 255); + nuint numPixels = (uint)pixelData.Length; + nuint i = 0; + do + { + ref uint pos = ref Unsafe.Add(ref MemoryMarshal.GetReference(pixelData), i); + Vector256 input = Unsafe.As>(ref pos).AsByte(); + Vector256 in0g0g = Vector256_.ShufflePerLane(input, subtractGreenFromBlueAndRedMask); + Vector256 output = input - in0g0g; + Unsafe.As>(ref pos) = output.AsUInt32(); + i += 8; + } + while (i <= numPixels - 8); + + if (i != numPixels) + { + SubtractGreenFromBlueAndRedScalar(pixelData[(int)i..]); + } + } + else if (Vector128.IsHardwareAccelerated && pixelData.Length >= 4) + { + Vector128 subtractGreenFromBlueAndRedMask = Vector128.Create(1, 255, 1, 255, 5, 255, 5, 255, 9, 255, 9, 255, 13, 255, 13, 255); + nuint numPixels = (uint)pixelData.Length; + nuint i = 0; + do + { + ref uint pos = ref Unsafe.Add(ref MemoryMarshal.GetReference(pixelData), i); + Vector128 input = Unsafe.As>(ref pos).AsByte(); + Vector128 in0g0g = Vector128_.ShuffleNative(input, subtractGreenFromBlueAndRedMask); + Vector128 output = input - in0g0g; + Unsafe.As>(ref pos) = output.AsUInt32(); + i += 4; + } + while (i <= numPixels - 4); + + if (i != numPixels) + { + SubtractGreenFromBlueAndRedScalar(pixelData[(int)i..]); + } + } + else + { + SubtractGreenFromBlueAndRedScalar(pixelData); + } + } + + private static void SubtractGreenFromBlueAndRedScalar(Span pixelData) + { + int numPixels = pixelData.Length; + for (int i = 0; i < numPixels; i++) + { + uint argb = pixelData[i]; + uint green = (argb >> 8) & 0xff; + uint newR = (((argb >> 16) & 0xff) - green) & 0xff; + uint newB = (((argb >> 0) & 0xff) - green) & 0xff; + pixelData[i] = (argb & 0xff00ff00u) | (newR << 16) | newB; + } + } + + /// + /// If there are not many unique pixel values, it is more efficient to create a color index array and replace the pixel values by the array's indices. + /// This will reverse the color index transform. + /// + /// The transform data contains color table size and the entries in the color table. + /// The pixel data to apply the reverse transform on. + /// The resulting pixel data with the reversed transformation data. + public static void ColorIndexInverseTransform( + Vp8LTransform transform, + Span pixelData, + Span outputSpan) + { + int bitsPerPixel = 8 >> transform.Bits; + int width = transform.XSize; + int height = transform.YSize; + Span colorMap = transform.Data.GetSpan(); + int decodedPixels = 0; + if (bitsPerPixel < 8) + { + int pixelsPerByte = 1 << transform.Bits; + int countMask = pixelsPerByte - 1; + int bitMask = (1 << bitsPerPixel) - 1; + + int pixelDataPos = 0; + for (int y = 0; y < height; y++) + { + uint packedPixels = 0; + for (int x = 0; x < width; x++) + { + // We need to load fresh 'packed_pixels' once every + // 'pixelsPerByte' increments of x. Fortunately, pixelsPerByte + // is a power of 2, so we can just use a mask for that, instead of + // decrementing a counter. + if ((x & countMask) == 0) + { + packedPixels = GetArgbIndex(pixelData[pixelDataPos++]); + } + + outputSpan[decodedPixels++] = colorMap[(int)(packedPixels & bitMask)]; + packedPixels >>= bitsPerPixel; + } + } + + outputSpan.CopyTo(pixelData); + } + else + { + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + uint colorMapIndex = GetArgbIndex(pixelData[decodedPixels]); + pixelData[decodedPixels] = colorMap[(int)colorMapIndex]; + decodedPixels++; + } + } + } + } + + /// + /// The goal of the color transform is to de-correlate the R, G and B values of each pixel. + /// Color transform keeps the green (G) value as it is, transforms red (R) based on green and transforms blue (B) based on green and then based on red. + /// + /// The transform data. + /// The pixel data to apply the inverse transform on. + public static void ColorSpaceInverseTransform(Vp8LTransform transform, Span pixelData) + { + int width = transform.XSize; + int yEnd = transform.YSize; + int tileWidth = 1 << transform.Bits; + int mask = tileWidth - 1; + int safeWidth = width & ~mask; + int remainingWidth = width - safeWidth; + int tilesPerRow = SubSampleSize(width, transform.Bits); + int y = 0; + int predRowIdxStart = (y >> transform.Bits) * tilesPerRow; + Span transformData = transform.Data.GetSpan(); + + int pixelPos = 0; + while (y < yEnd) + { + int predRowIdx = predRowIdxStart; + Vp8LMultipliers m = default; + int srcSafeEnd = pixelPos + safeWidth; + int srcEnd = pixelPos + width; + while (pixelPos < srcSafeEnd) + { + uint colorCode = transformData[predRowIdx++]; + ColorCodeToMultipliers(colorCode, ref m); + TransformColorInverse(m, pixelData.Slice(pixelPos, tileWidth)); + pixelPos += tileWidth; + } + + if (pixelPos < srcEnd) + { + uint colorCode = transformData[predRowIdx]; + ColorCodeToMultipliers(colorCode, ref m); + TransformColorInverse(m, pixelData.Slice(pixelPos, remainingWidth)); + pixelPos += remainingWidth; + } + + y++; + if ((y & mask) == 0) + { + predRowIdxStart += tilesPerRow; + } + } + } + + /// + /// Color transform keeps the green (G) value as it is, transforms red (R) based on green and transforms blue (B) based on green and then based on red. + /// + /// The Vp8LMultipliers. + /// The pixel data to transform. + /// The number of pixels to process. + public static void TransformColor(Vp8LMultipliers m, Span pixelData, int numPixels) + { + if (Avx2.IsSupported && numPixels >= 8) + { + Vector256 transformColorAlphaGreenMask256 = Vector256.Create(0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255); + Vector256 transformColorRedBlueMask256 = Vector256.Create(255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0); + Vector256 multsrb = MkCst32(Cst5b(m.GreenToRed), Cst5b(m.GreenToBlue)); + Vector256 multsb2 = MkCst32(Cst5b(m.RedToBlue), 0); + + nuint idx = 0; + do + { + ref uint pos = ref Unsafe.Add(ref MemoryMarshal.GetReference(pixelData), idx); + Vector256 input = Unsafe.As>(ref pos); + Vector256 a = Avx2.And(input.AsByte(), transformColorAlphaGreenMask256); + Vector256 b = Avx2.ShuffleLow(a.AsInt16(), SimdUtils.Shuffle.MMShuffle2200); + Vector256 c = Avx2.ShuffleHigh(b.AsInt16(), SimdUtils.Shuffle.MMShuffle2200); + Vector256 d = Avx2.MultiplyHigh(c.AsInt16(), multsrb.AsInt16()); + Vector256 e = Avx2.ShiftLeftLogical(input.AsInt16(), 8); + Vector256 f = Avx2.MultiplyHigh(e.AsInt16(), multsb2.AsInt16()); + Vector256 g = Avx2.ShiftRightLogical(f.AsInt32(), 16); + Vector256 h = Avx2.Add(g.AsByte(), d.AsByte()); + Vector256 i = Avx2.And(h, transformColorRedBlueMask256); + Vector256 output = Avx2.Subtract(input.AsByte(), i); + Unsafe.As>(ref pos) = output.AsUInt32(); + idx += 8; + } + while (idx <= (uint)numPixels - 8); + + if (idx != (uint)numPixels) + { + TransformColorScalar(m, pixelData[(int)idx..], numPixels - (int)idx); + } + } + else if (Vector128.IsHardwareAccelerated && numPixels >= 4) + { + Vector128 transformColorAlphaGreenMask = Vector128.Create(0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255); + Vector128 transformColorRedBlueMask = Vector128.Create(255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0); + Vector128 multsrb = MkCst16(Cst5b(m.GreenToRed), Cst5b(m.GreenToBlue)); + Vector128 multsb2 = MkCst16(Cst5b(m.RedToBlue), 0); + nuint idx = 0; + do + { + ref uint pos = ref Unsafe.Add(ref MemoryMarshal.GetReference(pixelData), idx); + Vector128 input = Unsafe.As>(ref pos); + Vector128 a = input.AsByte() & transformColorAlphaGreenMask; + Vector128 b = Vector128_.ShuffleLow(a.AsInt16(), SimdUtils.Shuffle.MMShuffle2200); + Vector128 c = Vector128_.ShuffleHigh(b.AsInt16(), SimdUtils.Shuffle.MMShuffle2200); + Vector128 d = Vector128_.MultiplyHigh(c.AsInt16(), multsrb.AsInt16()); + Vector128 e = Vector128_.ShiftLeftLogical(input.AsInt16(), 8); + Vector128 f = Vector128_.MultiplyHigh(e.AsInt16(), multsb2.AsInt16()); + Vector128 g = Vector128.ShiftRightLogical(f.AsInt32(), 16); + Vector128 h = g.AsByte() + d.AsByte(); + Vector128 i = h & transformColorRedBlueMask; + Vector128 output = input.AsByte() - i; + Unsafe.As>(ref pos) = output.AsUInt32(); + idx += 4; + } + while ((int)idx <= numPixels - 4); + + if ((int)idx != numPixels) + { + TransformColorScalar(m, pixelData[(int)idx..], numPixels - (int)idx); + } + } + else + { + TransformColorScalar(m, pixelData, numPixels); + } + } + + private static void TransformColorScalar(Vp8LMultipliers m, Span data, int numPixels) + { + for (int i = 0; i < numPixels; i++) + { + uint argb = data[i]; + sbyte green = U32ToS8(argb >> 8); + sbyte red = U32ToS8(argb >> 16); + int newRed = red & 0xff; + int newBlue = (int)(argb & 0xff); + newRed -= ColorTransformDelta((sbyte)m.GreenToRed, green); + newRed &= 0xff; + newBlue -= ColorTransformDelta((sbyte)m.GreenToBlue, green); + newBlue -= ColorTransformDelta((sbyte)m.RedToBlue, red); + newBlue &= 0xff; + data[i] = (argb & 0xff00ff00u) | ((uint)newRed << 16) | (uint)newBlue; + } + } + + /// + /// Reverses the color space transform. + /// + /// The color transform element. + /// The pixel data to apply the inverse transform on. + public static void TransformColorInverse(Vp8LMultipliers m, Span pixelData) + { + if (Avx2.IsSupported && pixelData.Length >= 8) + { + Vector256 transformColorInverseAlphaGreenMask256 = Vector256.Create(0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255); + Vector256 multsrb = MkCst32(Cst5b(m.GreenToRed), Cst5b(m.GreenToBlue)); + Vector256 multsb2 = MkCst32(Cst5b(m.RedToBlue), 0); + nuint idx; + for (idx = 0; idx <= (uint)pixelData.Length - 8; idx += 8) + { + ref uint pos = ref Unsafe.Add(ref MemoryMarshal.GetReference(pixelData), idx); + Vector256 input = Unsafe.As>(ref pos); + Vector256 a = Avx2.And(input.AsByte(), transformColorInverseAlphaGreenMask256); + Vector256 b = Avx2.ShuffleLow(a.AsInt16(), SimdUtils.Shuffle.MMShuffle2200); + Vector256 c = Avx2.ShuffleHigh(b.AsInt16(), SimdUtils.Shuffle.MMShuffle2200); + Vector256 d = Avx2.MultiplyHigh(c.AsInt16(), multsrb.AsInt16()); + Vector256 e = Avx2.Add(input.AsByte(), d.AsByte()); + Vector256 f = Avx2.ShiftLeftLogical(e.AsInt16(), 8); + Vector256 g = Avx2.MultiplyHigh(f, multsb2.AsInt16()); + Vector256 h = Avx2.ShiftRightLogical(g.AsInt32(), 8); + Vector256 i = Avx2.Add(h.AsByte(), f.AsByte()); + Vector256 j = Avx2.ShiftRightLogical(i.AsInt16(), 8); + Vector256 output = Avx2.Or(j.AsByte(), a); + Unsafe.As>(ref pos) = output.AsUInt32(); + } + + if (idx != (uint)pixelData.Length) + { + TransformColorInverseScalar(m, pixelData[(int)idx..]); + } + } + else if (Vector128.IsHardwareAccelerated && pixelData.Length >= 4) + { + Vector128 transformColorInverseAlphaGreenMask = Vector128.Create(0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255); + Vector128 multsrb = MkCst16(Cst5b(m.GreenToRed), Cst5b(m.GreenToBlue)); + Vector128 multsb2 = MkCst16(Cst5b(m.RedToBlue), 0); + + nuint idx; + for (idx = 0; idx <= (uint)pixelData.Length - 4; idx += 4) + { + ref uint pos = ref Unsafe.Add(ref MemoryMarshal.GetReference(pixelData), idx); + Vector128 input = Unsafe.As>(ref pos); + Vector128 a = input.AsByte() & transformColorInverseAlphaGreenMask; + Vector128 b = Vector128_.ShuffleLow(a.AsInt16(), SimdUtils.Shuffle.MMShuffle2200); + Vector128 c = Vector128_.ShuffleHigh(b.AsInt16(), SimdUtils.Shuffle.MMShuffle2200); + Vector128 d = Vector128_.MultiplyHigh(c.AsInt16(), multsrb.AsInt16()); + Vector128 e = input.AsByte() + d.AsByte(); + Vector128 f = Vector128_.ShiftLeftLogical(e.AsInt16(), 8); + Vector128 g = Vector128_.MultiplyHigh(f, multsb2.AsInt16()); + Vector128 h = Vector128.ShiftRightLogical(g.AsInt32(), 8); + Vector128 i = h.AsByte() + f.AsByte(); + Vector128 j = Vector128.ShiftRightLogical(i.AsInt16(), 8); + Vector128 output = j.AsByte() | a; + Unsafe.As>(ref pos) = output.AsUInt32(); + } + + if (idx != (uint)pixelData.Length) + { + TransformColorInverseScalar(m, pixelData[(int)idx..]); + } + } + else + { + TransformColorInverseScalar(m, pixelData); + } + } + + private static void TransformColorInverseScalar(Vp8LMultipliers m, Span pixelData) + { + for (int i = 0; i < pixelData.Length; i++) + { + uint argb = pixelData[i]; + sbyte green = (sbyte)(argb >> 8); + uint red = argb >> 16; + int newRed = (int)(red & 0xff); + int newBlue = (int)argb & 0xff; + newRed += ColorTransformDelta((sbyte)m.GreenToRed, green); + newRed &= 0xff; + newBlue += ColorTransformDelta((sbyte)m.GreenToBlue, green); + newBlue += ColorTransformDelta((sbyte)m.RedToBlue, (sbyte)newRed); + newBlue &= 0xff; + + pixelData[i] = (argb & 0xff00ff00u) | ((uint)newRed << 16) | (uint)newBlue; + } + } + + /// + /// This will reverse the predictor transform. + /// The predictor transform can be used to reduce entropy by exploiting the fact that neighboring pixels are often correlated. + /// In the predictor transform, the current pixel value is predicted from the pixels already decoded (in scan-line order) and only the residual value (actual - predicted) is encoded. + /// The prediction mode determines the type of prediction to use. The image is divided into squares and all the pixels in a square use same prediction mode. + /// + /// The transform data. + /// The pixel data to apply the inverse transform. + /// The resulting pixel data with the reversed transformation data. + public static void PredictorInverseTransform( + Vp8LTransform transform, + Span pixelData, + Span outputSpan) + { + fixed (uint* inputFixed = pixelData) + { + fixed (uint* outputFixed = outputSpan) + { + uint* input = inputFixed; + uint* output = outputFixed; + + int width = transform.XSize; + Span transformData = transform.Data.GetSpan(); + + // First Row follows the L (mode=1) mode. + PredictorAdd0(input, 1, output); + PredictorAdd1(input + 1, width - 1, output + 1); + input += width; + output += width; + + int y = 1; + int yEnd = transform.YSize; + int tileWidth = 1 << transform.Bits; + int mask = tileWidth - 1; + int tilesPerRow = SubSampleSize(width, transform.Bits); + int predictorModeIdxBase = (y >> transform.Bits) * tilesPerRow; + Span scratch = stackalloc short[8]; + while (y < yEnd) + { + int predictorModeIdx = predictorModeIdxBase; + int x = 1; + + // First pixel follows the T (mode=2) mode. + PredictorAdd2(input, output - width, 1, output); + + // .. the rest: + while (x < width) + { + uint predictorMode = (transformData[predictorModeIdx++] >> 8) & 0xf; + int xEnd = (x & ~mask) + tileWidth; + if (xEnd > width) + { + xEnd = width; + } + + // There are 14 different prediction modes. + // In each prediction mode, the current pixel value is predicted from one + // or more neighboring pixels whose values are already known. + switch (predictorMode) + { + case 0: + PredictorAdd0(input + x, xEnd - x, output + x); + break; + case 1: + PredictorAdd1(input + x, xEnd - x, output + x); + break; + case 2: + PredictorAdd2(input + x, output + x - width, xEnd - x, output + x); + break; + case 3: + PredictorAdd3(input + x, output + x - width, xEnd - x, output + x); + break; + case 4: + PredictorAdd4(input + x, output + x - width, xEnd - x, output + x); + break; + case 5: + PredictorAdd5(input + x, output + x - width, xEnd - x, output + x); + break; + case 6: + PredictorAdd6(input + x, output + x - width, xEnd - x, output + x); + break; + case 7: + PredictorAdd7(input + x, output + x - width, xEnd - x, output + x); + break; + case 8: + PredictorAdd8(input + x, output + x - width, xEnd - x, output + x); + break; + case 9: + PredictorAdd9(input + x, output + x - width, xEnd - x, output + x); + break; + case 10: + PredictorAdd10(input + x, output + x - width, xEnd - x, output + x); + break; + case 11: + PredictorAdd11(input + x, output + x - width, xEnd - x, output + x, scratch); + break; + case 12: + PredictorAdd12(input + x, output + x - width, xEnd - x, output + x); + break; + case 13: + PredictorAdd13(input + x, output + x - width, xEnd - x, output + x); + break; + } + + x = xEnd; + } + + input += width; + output += width; + y++; + + if ((y & mask) == 0) + { + // Use the same mask, since tiles are squares. + predictorModeIdxBase += tilesPerRow; + } + } + } + } + + outputSpan.CopyTo(pixelData); + } + + public static void ExpandColorMap(int numColors, Span transformData, Span newColorMap) + { + newColorMap[0] = transformData[0]; + Span data = MemoryMarshal.Cast(transformData); + Span newData = MemoryMarshal.Cast(newColorMap); + int numColorsX4 = 4 * numColors; + int i; + for (i = 4; i < numColorsX4; i++) + { + // Equivalent to AddPixelEq(), on a byte-basis. + newData[i] = (byte)((data[i] + newData[i - 4]) & 0xff); + } + + int colorMapLength4 = 4 * newColorMap.Length; + for (; i < colorMapLength4; i++) + { + newData[i] = 0; // black tail. + } + } + + /// + /// Difference of each component, mod 256. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public static uint SubPixels(uint a, uint b) + { + uint alphaAndGreen = 0x00ff00ffu + (a & 0xff00ff00u) - (b & 0xff00ff00u); + uint redAndBlue = 0xff00ff00u + (a & 0x00ff00ffu) - (b & 0x00ff00ffu); + return (alphaAndGreen & 0xff00ff00u) | (redAndBlue & 0x00ff00ffu); + } + + /// + /// Bundles multiple (1, 2, 4 or 8) pixels into a single pixel. + /// + public static void BundleColorMap(Span row, int width, int xBits, Span dst) + { + int x; + if (xBits > 0) + { + int bitDepth = 1 << (3 - xBits); + int mask = (1 << xBits) - 1; + uint code = 0xff000000; + for (x = 0; x < width; x++) + { + int xsub = x & mask; + if (xsub == 0) + { + code = 0xff000000; + } + + code |= (uint)(row[x] << (8 + (bitDepth * xsub))); + dst[x >> xBits] = code; + } + } + else + { + for (x = 0; x < width; x++) + { + dst[x] = (uint)(0xff000000 | (row[x] << 8)); + } + } + } + + /// + /// Compute the combined Shanon's entropy for distribution {X} and {X+Y}. + /// + /// Shanon entropy. + public static float CombinedShannonEntropy(Span x, Span y) + { + if (Avx2.IsSupported) + { + double retVal = 0.0d; + Vector256 tmp = Vector256.Zero; // has the size of the scratch space of sizeof(int) * 8 + ref int xRef = ref MemoryMarshal.GetReference(x); + ref int yRef = ref MemoryMarshal.GetReference(y); + Vector256 sumXY256 = Vector256.Zero; + Vector256 sumX256 = Vector256.Zero; + ref int tmpRef = ref Unsafe.As, int>(ref tmp); + for (nuint i = 0; i < 256; i += 8) + { + Vector256 xVec = Unsafe.As>(ref Unsafe.Add(ref xRef, i)); + Vector256 yVec = Unsafe.As>(ref Unsafe.Add(ref yRef, i)); + + // Check if any X is non-zero: this actually provides a speedup as X is usually sparse. + int mask = Avx2.MoveMask(Avx2.CompareEqual(xVec, Vector256.Zero).AsByte()); + if (mask != -1) + { + Vector256 xy256 = Avx2.Add(xVec, yVec); + sumXY256 = Avx2.Add(sumXY256, xy256); + sumX256 = Avx2.Add(sumX256, xVec); + + // Analyze the different X + Y. + Unsafe.As>(ref tmpRef) = xy256; + if (tmpRef != 0) + { + retVal -= FastSLog2((uint)tmpRef); + if (Unsafe.Add(ref xRef, i) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref xRef, i)); + } + } + + if (Unsafe.Add(ref tmpRef, 1) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref tmpRef, 1)); + if (Unsafe.Add(ref xRef, i + 1) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref xRef, i + 1)); + } + } + + if (Unsafe.Add(ref tmpRef, 2) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref tmpRef, 2)); + if (Unsafe.Add(ref xRef, i + 2) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref xRef, i + 2)); + } + } + + if (Unsafe.Add(ref tmpRef, 3) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref tmpRef, 3)); + if (Unsafe.Add(ref xRef, i + 3) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref xRef, i + 3)); + } + } + + if (Unsafe.Add(ref tmpRef, 4) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref tmpRef, 4)); + if (Unsafe.Add(ref xRef, i + 4) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref xRef, i + 4)); + } + } + + if (Unsafe.Add(ref tmpRef, 5) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref tmpRef, 5)); + if (Unsafe.Add(ref xRef, i + 5) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref xRef, i + 5)); + } + } + + if (Unsafe.Add(ref tmpRef, 6) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref tmpRef, 6)); + if (Unsafe.Add(ref xRef, i + 6) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref xRef, i + 6)); + } + } + + if (Unsafe.Add(ref tmpRef, 7) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref tmpRef, 7)); + if (Unsafe.Add(ref xRef, i + 7) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref xRef, i + 7)); + } + } + } + else + { + // X is fully 0, so only deal with Y. + sumXY256 = Avx2.Add(sumXY256, yVec); + + if (Unsafe.Add(ref yRef, i) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref yRef, i)); + } + + if (Unsafe.Add(ref yRef, i + 1) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref yRef, i + 1)); + } + + if (Unsafe.Add(ref yRef, i + 2) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref yRef, i + 2)); + } + + if (Unsafe.Add(ref yRef, i + 3) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref yRef, i + 3)); + } + + if (Unsafe.Add(ref yRef, i + 4) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref yRef, i + 4)); + } + + if (Unsafe.Add(ref yRef, i + 5) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref yRef, i + 5)); + } + + if (Unsafe.Add(ref yRef, i + 6) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref yRef, i + 6)); + } + + if (Unsafe.Add(ref yRef, i + 7) != 0) + { + retVal -= FastSLog2((uint)Unsafe.Add(ref yRef, i + 7)); + } + } + } + + // Sum up sumX256 to get sumX and sum up sumXY256 to get sumXY. + int sumX = Numerics.ReduceSum(sumX256); + int sumXY = Numerics.ReduceSum(sumXY256); + + retVal += FastSLog2((uint)sumX) + FastSLog2((uint)sumXY); + + return (float)retVal; + } + else + { + double retVal = 0.0d; + uint sumX = 0, sumXY = 0; + for (int i = 0; i < 256; i++) + { + uint xi = (uint)x[i]; + if (xi != 0) + { + uint xy = xi + (uint)y[i]; + sumX += xi; + retVal -= FastSLog2(xi); + sumXY += xy; + retVal -= FastSLog2(xy); + } + else if (y[i] != 0) + { + sumXY += (uint)y[i]; + retVal -= FastSLog2((uint)y[i]); + } + } + + retVal += FastSLog2(sumX) + FastSLog2(sumXY); + return (float)retVal; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static byte TransformColorRed(sbyte greenToRed, uint argb) + { + sbyte green = U32ToS8(argb >> 8); + int newRed = (int)(argb >> 16); + newRed -= ColorTransformDelta(greenToRed, green); + return (byte)(newRed & 0xff); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static byte TransformColorBlue(sbyte greenToBlue, sbyte redToBlue, uint argb) + { + sbyte green = U32ToS8(argb >> 8); + sbyte red = U32ToS8(argb >> 16); + int newBlue = (int)(argb & 0xff); + newBlue -= ColorTransformDelta(greenToBlue, green); + newBlue -= ColorTransformDelta(redToBlue, red); + return (byte)(newBlue & 0xff); + } + + /// + /// Fast calculation of log2(v) for integer input. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public static float FastLog2(uint v) => v < LogLookupIdxMax ? WebpLookupTables.Log2Table[v] : FastLog2Slow(v); + + /// + /// Fast calculation of v * log2(v) for integer input. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public static float FastSLog2(uint v) => v < LogLookupIdxMax ? WebpLookupTables.SLog2Table[v] : FastSLog2Slow(v); + + [MethodImpl(InliningOptions.ShortMethod)] + public static void ColorCodeToMultipliers(uint colorCode, ref Vp8LMultipliers m) + { + m.GreenToRed = (byte)(colorCode & 0xff); + m.GreenToBlue = (byte)((colorCode >> 8) & 0xff); + m.RedToBlue = (byte)((colorCode >> 16) & 0xff); + } + + // Converts near lossless quality into max number of bits shaved off. + // 100 -> 0 + // 80..99 -> 1 + // 60..79 -> 2 + // 40..59 -> 3 + // 20..39 -> 4 + // 0..19 -> 5 + [MethodImpl(InliningOptions.ShortMethod)] + public static int NearLosslessBits(int nearLosslessQuality) => 5 - (nearLosslessQuality / 20); + + private static float FastSLog2Slow(uint v) + { + DebugGuard.MustBeGreaterThanOrEqualTo(v, LogLookupIdxMax, nameof(v)); + + if (v < ApproxLogWithCorrectionMax) + { + int logCnt = 0; + uint y = 1; + float vF = v; + uint origV = v; + do + { + ++logCnt; + v >>= 1; + y <<= 1; + } + while (v >= LogLookupIdxMax); + + // vf = (2^log_cnt) * Xf; where y = 2^log_cnt and Xf < 256 + // Xf = floor(Xf) * (1 + (v % y) / v) + // log2(Xf) = log2(floor(Xf)) + log2(1 + (v % y) / v) + // The correction factor: log(1 + d) ~ d; for very small d values, so + // log2(1 + (v % y) / v) ~ LOG_2_RECIPROCAL * (v % y)/v + // LOG_2_RECIPROCAL ~ 23/16 + int correction = (int)((23 * (origV & (y - 1))) >> 4); + return (vF * (WebpLookupTables.Log2Table[v] + logCnt)) + correction; + } + + return (float)(Log2Reciprocal * v * Math.Log(v)); + } + + private static float FastLog2Slow(uint v) + { + DebugGuard.MustBeGreaterThanOrEqualTo(v, LogLookupIdxMax, nameof(v)); + + if (v < ApproxLogWithCorrectionMax) + { + int logCnt = 0; + uint y = 1; + uint origV = v; + do + { + ++logCnt; + v >>= 1; + y <<= 1; + } + while (v >= LogLookupIdxMax); + + double log2 = WebpLookupTables.Log2Table[v] + logCnt; + if (origV >= ApproxLogMax) + { + // Since the division is still expensive, add this correction factor only + // for large values of 'v'. + int correction = (int)(23 * (origV & (y - 1))) >> 4; + log2 += (double)correction / origV; + } + + return (float)log2; + } + + return (float)(Log2Reciprocal * Math.Log(v)); + } + + /// + /// Splitting of distance and length codes into prefixes and + /// extra bits. The prefixes are encoded with an entropy code + /// while the extra bits are stored just as normal bits. + /// + private static int PrefixEncodeBitsNoLut(int distance, ref int extraBits) + { + int highestBit = BitOperations.Log2((uint)--distance); + int secondHighestBit = (distance >> (highestBit - 1)) & 1; + extraBits = highestBit - 1; + int code = (2 * highestBit) + secondHighestBit; + return code; + } + + private static int PrefixEncodeNoLut(int distance, ref int extraBits, ref int extraBitsValue) + { + int highestBit = BitOperations.Log2((uint)--distance); + int secondHighestBit = (distance >> (highestBit - 1)) & 1; + extraBits = highestBit - 1; + extraBitsValue = distance & ((1 << extraBits) - 1); + int code = (2 * highestBit) + secondHighestBit; + return code; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd0(uint* input, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + output[x] = AddPixels(input[x], WebpConstants.ArgbBlack); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd1(uint* input, int numberOfPixels, uint* output) + { + uint left = output[-1]; + for (int x = 0; x < numberOfPixels; x++) + { + output[x] = left = AddPixels(input[x], left); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd2(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor2(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd3(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor3(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd4(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor4(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd5(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor5(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd6(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor6(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd7(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor7(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd8(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor8(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd9(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor9(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd10(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor10(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd11(uint* input, uint* upper, int numberOfPixels, uint* output, Span scratch) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor11(output[x - 1], upper + x, scratch); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd12(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor12(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd13(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor13(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor2(uint left, uint* top) => top[0]; + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor3(uint left, uint* top) => top[1]; + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor4(uint left, uint* top) => top[-1]; + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor5(uint left, uint* top) => Average3(left, top[0], top[1]); + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor6(uint left, uint* top) => Average2(left, top[-1]); + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor7(uint left, uint* top) => Average2(left, top[0]); + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor8(uint left, uint* top) => Average2(top[-1], top[0]); + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor9(uint left, uint* top) => Average2(top[0], top[1]); + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor10(uint left, uint* top) => Average4(left, top[-1], top[0], top[1]); + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor11(uint left, uint* top, Span scratch) => Select(top[0], left, top[-1], scratch); + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor12(uint left, uint* top) => ClampedAddSubtractFull(left, top[0], top[-1]); + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor13(uint left, uint* top) => ClampedAddSubtractHalf(left, top[0], top[-1]); + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub0(uint* input, int numPixels, uint* output) + { + for (int i = 0; i < numPixels; i++) + { + output[i] = SubPixels(input[i], WebpConstants.ArgbBlack); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub1(uint* input, int numPixels, uint* output) + { + for (int i = 0; i < numPixels; i++) + { + output[i] = SubPixels(input[i], input[i - 1]); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub2(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor2(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub3(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor3(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub4(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor4(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub5(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor5(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub6(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor6(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub7(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor7(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub8(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor8(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub9(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor9(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub10(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor10(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub11(uint* input, uint* upper, int numPixels, uint* output, Span scratch) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor11(input[x - 1], upper + x, scratch); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub12(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor12(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub13(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor13(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + /// + /// Computes sampled size of 'size' when sampling using 'sampling bits'. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public static int SubSampleSize(int size, int samplingBits) => (size + (1 << samplingBits) - 1) >> samplingBits; + + /// + /// Sum of each component, mod 256. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public static uint AddPixels(uint a, uint b) + { + uint alphaAndGreen = (a & 0xff00ff00u) + (b & 0xff00ff00u); + uint redAndBlue = (a & 0x00ff00ffu) + (b & 0x00ff00ffu); + return (alphaAndGreen & 0xff00ff00u) | (redAndBlue & 0x00ff00ffu); + } + + // For sign-extended multiplying constants, pre-shifted by 5: + [MethodImpl(InliningOptions.ShortMethod)] + public static short Cst5b(int x) => (short)(((short)(x << 8)) >> 5); + + private static uint ClampedAddSubtractFull(uint c0, uint c1, uint c2) + { + if (Vector128.IsHardwareAccelerated) + { + Vector128 c0Vec = Vector128_.UnpackLow(Vector128.CreateScalar(c0).AsByte(), Vector128.Zero); + Vector128 c1Vec = Vector128_.UnpackLow(Vector128.CreateScalar(c1).AsByte(), Vector128.Zero); + Vector128 c2Vec = Vector128_.UnpackLow(Vector128.CreateScalar(c2).AsByte(), Vector128.Zero); + Vector128 v1 = c0Vec.AsInt16() + c1Vec.AsInt16(); + Vector128 v2 = v1 - c2Vec.AsInt16(); + Vector128 b = Vector128_.PackUnsignedSaturate(v2, v2); + return b.AsUInt32().ToScalar(); + } + + { + int a = AddSubtractComponentFull( + (int)(c0 >> 24), + (int)(c1 >> 24), + (int)(c2 >> 24)); + int r = AddSubtractComponentFull( + (int)((c0 >> 16) & 0xff), + (int)((c1 >> 16) & 0xff), + (int)((c2 >> 16) & 0xff)); + int g = AddSubtractComponentFull( + (int)((c0 >> 8) & 0xff), + (int)((c1 >> 8) & 0xff), + (int)((c2 >> 8) & 0xff)); + int b = AddSubtractComponentFull((int)(c0 & 0xff), (int)(c1 & 0xff), (int)(c2 & 0xff)); + return ((uint)a << 24) | ((uint)r << 16) | ((uint)g << 8) | (uint)b; + } + } + + private static uint ClampedAddSubtractHalf(uint c0, uint c1, uint c2) + { + if (Vector128.IsHardwareAccelerated) + { + Vector128 c0Vec = Vector128_.UnpackLow(Vector128.CreateScalar(c0).AsByte(), Vector128.Zero); + Vector128 c1Vec = Vector128_.UnpackLow(Vector128.CreateScalar(c1).AsByte(), Vector128.Zero); + Vector128 b0 = Vector128_.UnpackLow(Vector128.CreateScalar(c2).AsByte(), Vector128.Zero); + Vector128 avg = c1Vec.AsInt16() + c0Vec.AsInt16(); + Vector128 a0 = Vector128.ShiftRightLogical(avg, 1); + Vector128 a1 = a0 - b0.AsInt16(); + Vector128 bgta = Vector128.GreaterThan(b0.AsInt16(), a0.AsInt16()); + Vector128 a2 = a1 - bgta; + Vector128 a3 = Vector128.ShiftRightArithmetic(a2, 1); + Vector128 a4 = (a0 + a3).AsInt16(); + Vector128 a5 = Vector128_.PackUnsignedSaturate(a4, a4); + return a5.AsUInt32().ToScalar(); + } + + { + uint ave = Average2(c0, c1); + int a = AddSubtractComponentHalf((int)(ave >> 24), (int)(c2 >> 24)); + int r = AddSubtractComponentHalf((int)((ave >> 16) & 0xff), (int)((c2 >> 16) & 0xff)); + int g = AddSubtractComponentHalf((int)((ave >> 8) & 0xff), (int)((c2 >> 8) & 0xff)); + int b = AddSubtractComponentHalf((int)(ave & 0xff), (int)(c2 & 0xff)); + return ((uint)a << 24) | ((uint)r << 16) | ((uint)g << 8) | (uint)b; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int AddSubtractComponentHalf(int a, int b) => (int)Clip255((uint)(a + ((a - b) / 2))); + + [MethodImpl(InliningOptions.ShortMethod)] + private static int AddSubtractComponentFull(int a, int b, int c) => (int)Clip255((uint)(a + b - c)); + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint Clip255(uint a) => a < 256 ? a : ~a >> 24; + + [MethodImpl(InliningOptions.ShortMethod)] + private static Vector128 MkCst16(int hi, int lo) => Vector128.Create((hi << 16) | (lo & 0xffff)); + + [MethodImpl(InliningOptions.ShortMethod)] + private static Vector256 MkCst32(int hi, int lo) => Vector256.Create((hi << 16) | (lo & 0xffff)); + + private static uint Select(uint a, uint b, uint c, Span scratch) + { + if (Vector128.IsHardwareAccelerated) + { + fixed (short* ptr = &MemoryMarshal.GetReference(scratch)) + { + Vector128 a0 = Vector128.CreateScalar(a).AsByte(); + Vector128 b0 = Vector128.CreateScalar(b).AsByte(); + Vector128 c0 = Vector128.CreateScalar(c).AsByte(); + Vector128 ac0 = Vector128_.SubtractSaturate(a0, c0); + Vector128 ca0 = Vector128_.SubtractSaturate(c0, a0); + Vector128 bc0 = Vector128_.SubtractSaturate(b0, c0); + Vector128 cb0 = Vector128_.SubtractSaturate(c0, b0); + Vector128 ac = ac0 | ca0; + Vector128 bc = bc0 | cb0; + Vector128 pa = Vector128_.UnpackLow(ac, Vector128.Zero); // |a - c| + Vector128 pb = Vector128_.UnpackLow(bc, Vector128.Zero); // |b - c| + Vector128 diff = pb.AsUInt16() - pa.AsUInt16(); + diff.Store((ushort*)ptr); + int paMinusPb = ptr[3] + ptr[2] + ptr[1] + ptr[0]; + return (paMinusPb <= 0) ? a : b; + } + } + else + { + int paMinusPb = + Sub3((int)(a >> 24), (int)(b >> 24), (int)(c >> 24)) + + Sub3((int)((a >> 16) & 0xff), (int)((b >> 16) & 0xff), (int)((c >> 16) & 0xff)) + + Sub3((int)((a >> 8) & 0xff), (int)((b >> 8) & 0xff), (int)((c >> 8) & 0xff)) + + Sub3((int)(a & 0xff), (int)(b & 0xff), (int)(c & 0xff)); + return paMinusPb <= 0 ? a : b; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Sub3(int a, int b, int c) + { + int pb = b - c; + int pa = a - c; + return Math.Abs(pb) - Math.Abs(pa); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint Average2(uint a0, uint a1) => (((a0 ^ a1) & 0xfefefefeu) >> 1) + (a0 & a1); + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint Average3(uint a0, uint a1, uint a2) => Average2(Average2(a0, a2), a1); + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint Average4(uint a0, uint a1, uint a2, uint a3) => Average2(Average2(a0, a1), Average2(a2, a3)); + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint GetArgbIndex(uint idx) => (idx >> 8) & 0xff; + + [MethodImpl(InliningOptions.ShortMethod)] + private static int ColorTransformDelta(sbyte colorPred, sbyte color) => (colorPred * color) >> 5; + + [MethodImpl(InliningOptions.ShortMethod)] + private static sbyte U32ToS8(uint v) => (sbyte)(v & 0xff); + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/NearLosslessEnc.cs b/ImageSharp/Formats/Webp/Lossless/NearLosslessEnc.cs new file mode 100644 index 0000000..35bc0da --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/NearLosslessEnc.cs @@ -0,0 +1,124 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// Near-lossless image preprocessing adjusts pixel values to help compressibility with a guarantee + /// of maximum deviation between original and resulting pixel values. + /// + internal static class NearLosslessEnc + { + private const int MinDimForNearLossless = 64; + + public static void ApplyNearLossless(int xSize, int ySize, int quality, Span argbSrc, Span argbDst, int stride) + { + uint[] copyBuffer = new uint[xSize * 3]; + int limitBits = LosslessUtils.NearLosslessBits(quality); + + // For small icon images, don't attempt to apply near-lossless compression. + if ((xSize < MinDimForNearLossless && ySize < MinDimForNearLossless) || ySize < 3) + { + for (int i = 0; i < ySize; i++) + { + argbSrc.Slice(i * stride, xSize).CopyTo(argbDst.Slice(i * xSize, xSize)); + } + + return; + } + + NearLossless(xSize, ySize, argbSrc, stride, limitBits, copyBuffer, argbDst); + for (int i = limitBits - 1; i != 0; i--) + { + NearLossless(xSize, ySize, argbDst, xSize, i, copyBuffer, argbDst); + } + } + + // Adjusts pixel values of image with given maximum error. + private static void NearLossless(int xSize, int ySize, Span argbSrc, int stride, int limitBits, Span copyBuffer, Span argbDst) + { + int y; + int limit = 1 << limitBits; + Span prevRow = copyBuffer; + Span currRow = copyBuffer.Slice(xSize, xSize); + Span nextRow = copyBuffer.Slice(xSize * 2, xSize); + argbSrc[..xSize].CopyTo(currRow); + argbSrc.Slice(xSize, xSize).CopyTo(nextRow); + + int srcOffset = 0; + int dstOffset = 0; + for (y = 0; y < ySize; y++) + { + if (y == 0 || y == ySize - 1) + { + argbSrc.Slice(srcOffset, xSize).CopyTo(argbDst.Slice(dstOffset, xSize)); + } + else + { + argbSrc.Slice(srcOffset + stride, xSize).CopyTo(nextRow); + argbDst[dstOffset] = argbSrc[srcOffset]; + argbDst[dstOffset + xSize - 1] = argbSrc[srcOffset + xSize - 1]; + for (int x = 1; x < xSize - 1; x++) + { + if (IsSmooth(prevRow, currRow, nextRow, x, limit)) + { + argbDst[dstOffset + x] = currRow[x]; + } + else + { + argbDst[dstOffset + x] = ClosestDiscretizedArgb(currRow[x], limitBits); + } + } + } + + Span temp = prevRow; + prevRow = currRow; + currRow = nextRow; + nextRow = temp; + srcOffset += stride; + dstOffset += xSize; + } + } + + // Applies FindClosestDiscretized to all channels of pixel. + private static uint ClosestDiscretizedArgb(uint a, int bits) => + (FindClosestDiscretized(a >> 24, bits) << 24) | + (FindClosestDiscretized((a >> 16) & 0xff, bits) << 16) | + (FindClosestDiscretized((a >> 8) & 0xff, bits) << 8) | + FindClosestDiscretized(a & 0xff, bits); + + private static uint FindClosestDiscretized(uint a, int bits) + { + uint mask = (1u << bits) - 1; + uint biased = a + (mask >> 1) + ((a >> bits) & 1); + if (biased > 0xff) + { + return 0xff; + } + + return biased & ~mask; + } + + private static bool IsSmooth(Span prevRow, Span currRow, Span nextRow, int ix, int limit) => + IsNear(currRow[ix], currRow[ix - 1], limit) && // Check that all pixels in 4-connected neighborhood are smooth. + IsNear(currRow[ix], currRow[ix + 1], limit) && + IsNear(currRow[ix], prevRow[ix], limit) && + IsNear(currRow[ix], nextRow[ix], limit); + + // Checks if distance between corresponding channel values of pixels a and b is within the given limit. + private static bool IsNear(uint a, uint b, int limit) + { + for (int k = 0; k < 4; ++k) + { + int delta = (int)((a >> (k * 8)) & 0xff) - (int)((b >> (k * 8)) & 0xff); + if (delta >= limit || delta <= -limit) + { + return false; + } + } + + return true; + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs b/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs new file mode 100644 index 0000000..56ec5c0 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + [DebuggerDisplay("Mode: {Mode}, Len: {Len}, BgraOrDistance: {BgraOrDistance}")] + internal readonly struct PixOrCopy + { + public readonly PixOrCopyMode Mode; + public readonly ushort Len; + public readonly uint BgraOrDistance; + + private PixOrCopy(PixOrCopyMode mode, ushort len, uint bgraOrDistance) + { + this.Mode = mode; + this.Len = len; + this.BgraOrDistance = bgraOrDistance; + } + + public static PixOrCopy CreateCacheIdx(int idx) => new(PixOrCopyMode.CacheIdx, 1, (uint)idx); + + public static PixOrCopy CreateLiteral(uint bgra) => new(PixOrCopyMode.Literal, 1, bgra); + + public static PixOrCopy CreateCopy(uint distance, ushort len) => new(PixOrCopyMode.Copy, len, distance); + + public int Literal(int component) => (int)(this.BgraOrDistance >> (component * 8)) & 0xFF; + + public uint CacheIdx() => this.BgraOrDistance; + + public ushort Length() => this.Len; + + public uint Distance() => this.BgraOrDistance; + + public bool IsLiteral() => this.Mode == PixOrCopyMode.Literal; + + public bool IsCacheIdx() => this.Mode == PixOrCopyMode.CacheIdx; + + public bool IsCopy() => this.Mode == PixOrCopyMode.Copy; + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/PixOrCopyMode.cs b/ImageSharp/Formats/Webp/Lossless/PixOrCopyMode.cs new file mode 100644 index 0000000..4c297fc --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/PixOrCopyMode.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + internal enum PixOrCopyMode : byte + { + Literal, + + CacheIdx, + + Copy, + + None + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs b/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs new file mode 100644 index 0000000..1356f59 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs @@ -0,0 +1,1087 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// Image transform methods for the lossless webp encoder. + /// + internal static unsafe class PredictorEncoder + { + private static readonly sbyte[][] Offset = + [ + [0, -1], [0, 1], [-1, 0], [1, 0], [-1, -1], [-1, 1], [1, -1], [1, 1] + ]; + + private const int GreenRedToBlueNumAxis = 8; + + private const int GreenRedToBlueMaxIters = 7; + + private const float MaxDiffCost = 1e30f; + + private const uint MaskAlpha = 0xff000000; + + private const float SpatialPredictorBias = 15.0f; + + private const int PredLowEffort = 11; + + // This uses C#'s compiler optimization to refer to assembly's static data directly. + private static ReadOnlySpan DeltaLut => [16, 16, 8, 4, 2, 2, 2]; + + /// + /// Finds the best predictor for each tile, and converts the image to residuals + /// with respect to predictions. If nearLosslessQuality < 100, applies + /// near lossless processing, shaving off more bits of residuals for lower qualities. + /// + public static void ResidualImage( + int width, + int height, + int bits, + Span bgra, + Span bgraScratch, + Span image, + int[][] histoArgb, + int[][] bestHisto, + bool nearLossless, + int nearLosslessQuality, + TransparentColorMode transparentColorMode, + bool usedSubtractGreen, + bool lowEffort) + { + int tilesPerRow = LosslessUtils.SubSampleSize(width, bits); + int tilesPerCol = LosslessUtils.SubSampleSize(height, bits); + int maxQuantization = 1 << LosslessUtils.NearLosslessBits(nearLosslessQuality); + Span scratch = stackalloc short[8]; + + // TODO: Can we optimize this? + int[][] histo = + [ + new int[256], + new int[256], + new int[256], + new int[256] + ]; + + if (lowEffort) + { + for (int i = 0; i < tilesPerRow * tilesPerCol; i++) + { + image[i] = WebpConstants.ArgbBlack | (PredLowEffort << 8); + } + } + else + { + for (int tileY = 0; tileY < tilesPerCol; tileY++) + { + for (int tileX = 0; tileX < tilesPerRow; tileX++) + { + int pred = GetBestPredictorForTile( + width, + height, + tileX, + tileY, + bits, + histo, + bgraScratch, + bgra, + histoArgb, + bestHisto, + maxQuantization, + transparentColorMode, + usedSubtractGreen, + nearLossless, + image, + scratch); + + image[(tileY * tilesPerRow) + tileX] = (uint)(WebpConstants.ArgbBlack | (pred << 8)); + } + } + } + + CopyImageWithPrediction( + width, + height, + bits, + image, + bgraScratch, + bgra, + maxQuantization, + transparentColorMode, + usedSubtractGreen, + nearLossless, + lowEffort); + } + + public static void ColorSpaceTransform(int width, int height, int bits, uint quality, Span bgra, Span image, Span scratch) + { + int maxTileSize = 1 << bits; + int tileXSize = LosslessUtils.SubSampleSize(width, bits); + int tileYSize = LosslessUtils.SubSampleSize(height, bits); + int[] accumulatedRedHisto = new int[256]; + int[] accumulatedBlueHisto = new int[256]; + Vp8LMultipliers prevX = default(Vp8LMultipliers); + Vp8LMultipliers prevY = default(Vp8LMultipliers); + for (int tileY = 0; tileY < tileYSize; tileY++) + { + for (int tileX = 0; tileX < tileXSize; tileX++) + { + int tileXOffset = tileX * maxTileSize; + int tileYOffset = tileY * maxTileSize; + int allXMax = GetMin(tileXOffset + maxTileSize, width); + int allYMax = GetMin(tileYOffset + maxTileSize, height); + int offset = (tileY * tileXSize) + tileX; + if (tileY != 0) + { + LosslessUtils.ColorCodeToMultipliers(image[offset - tileXSize], ref prevY); + } + + prevX = GetBestColorTransformForTile( + tileX, + tileY, + bits, + prevX, + prevY, + quality, + width, + height, + accumulatedRedHisto, + accumulatedBlueHisto, + bgra, + scratch); + + image[offset] = MultipliersToColorCode(prevX); + CopyTileWithColorTransform(width, height, tileXOffset, tileYOffset, maxTileSize, prevX, bgra); + + // Gather accumulated histogram data. + for (int y = tileYOffset; y < allYMax; y++) + { + int ix = (y * width) + tileXOffset; + int ixEnd = ix + allXMax - tileXOffset; + + for (; ix < ixEnd; ix++) + { + uint pix = bgra[ix]; + if (ix >= 2 && pix == bgra[ix - 2] && pix == bgra[ix - 1]) + { + continue; // Repeated pixels are handled by backward references. + } + + if (ix >= width + 2 && bgra[ix - 2] == bgra[ix - width - 2] && bgra[ix - 1] == bgra[ix - width - 1] && pix == bgra[ix - width]) + { + continue; // Repeated pixels are handled by backward references. + } + + accumulatedRedHisto[(pix >> 16) & 0xff]++; + accumulatedBlueHisto[(pix >> 0) & 0xff]++; + } + } + } + } + } + + /// + /// Returns best predictor and updates the accumulated histogram. + /// If maxQuantization > 1, assumes that near lossless processing will be + /// applied, quantizing residuals to multiples of quantization levels up to + /// maxQuantization (the actual quantization level depends on smoothness near + /// the given pixel). + /// + /// Best predictor. + private static int GetBestPredictorForTile( + int width, + int height, + int tileX, + int tileY, + int bits, + int[][] accumulated, + Span argbScratch, + Span argb, + int[][] histoArgb, + int[][] bestHisto, + int maxQuantization, + TransparentColorMode transparentColorMode, + bool usedSubtractGreen, + bool nearLossless, + Span modes, + Span scratch) + { + const int numPredModes = 14; + int startX = tileX << bits; + int startY = tileY << bits; + int tileSize = 1 << bits; + int maxY = GetMin(tileSize, height - startY); + int maxX = GetMin(tileSize, width - startX); + + // Whether there exist columns just outside the tile. + int haveLeft = startX > 0 ? 1 : 0; + + // Position and size of the strip covering the tile and adjacent columns if they exist. + int contextStartX = startX - haveLeft; + int contextWidth = maxX + haveLeft + (maxX < width ? 1 : 0) - startX; + int tilesPerRow = LosslessUtils.SubSampleSize(width, bits); + + // Prediction modes of the left and above neighbor tiles. + int leftMode = (int)(tileX > 0 ? (modes[(tileY * tilesPerRow) + tileX - 1] >> 8) & 0xff : 0xff); + int aboveMode = (int)(tileY > 0 ? (modes[((tileY - 1) * tilesPerRow) + tileX] >> 8) & 0xff : 0xff); + + // The width of upper_row and current_row is one pixel larger than image width + // to allow the top right pixel to point to the leftmost pixel of the next row + // when at the right edge. + Span upperRow = argbScratch; + Span currentRow = upperRow[(width + 1)..]; + Span maxDiffs = MemoryMarshal.Cast(currentRow[(width + 1)..]); + float bestDiff = MaxDiffCost; + int bestMode = 0; + Span residuals = stackalloc uint[1 << WebpConstants.MaxTransformBits]; // 256 bytes + for (int i = 0; i < 4; i++) + { + histoArgb[i].AsSpan().Clear(); + bestHisto[i].AsSpan().Clear(); + } + + for (int mode = 0; mode < numPredModes; mode++) + { + if (startY > 0) + { + // Read the row above the tile which will become the first upper_row. + // Include a pixel to the left if it exists; include a pixel to the right + // in all cases (wrapping to the leftmost pixel of the next row if it does + // not exist). + Span src = argb.Slice(((startY - 1) * width) + contextStartX, maxX + haveLeft + 1); + Span dst = currentRow[contextStartX..]; + src.CopyTo(dst); + } + + for (int relativeY = 0; relativeY < maxY; relativeY++) + { + int y = startY + relativeY; + Span tmp = upperRow; + upperRow = currentRow; + currentRow = tmp; + + // Read currentRow. Include a pixel to the left if it exists; include a + // pixel to the right in all cases except at the bottom right corner of + // the image (wrapping to the leftmost pixel of the next row if it does + // not exist in the currentRow). + int offset = (y * width) + contextStartX; + Span src = argb.Slice(offset, maxX + haveLeft + (y + 1 < height ? 1 : 0)); + Span dst = currentRow[contextStartX..]; + src.CopyTo(dst); + + if (nearLossless) + { + if (maxQuantization > 1 && y >= 1 && y + 1 < height) + { + MaxDiffsForRow(contextWidth, width, argb, offset, maxDiffs[contextStartX..], usedSubtractGreen); + } + } + + GetResidual(width, height, upperRow, currentRow, maxDiffs, mode, startX, startX + maxX, y, maxQuantization, transparentColorMode, usedSubtractGreen, nearLossless, residuals, scratch); + for (int relativeX = 0; relativeX < maxX; ++relativeX) + { + UpdateHisto(histoArgb, residuals[relativeX]); + } + } + + float curDiff = PredictionCostSpatialHistogram(accumulated, histoArgb); + + // Favor keeping the areas locally similar. + if (mode == leftMode) + { + curDiff -= SpatialPredictorBias; + } + + if (mode == aboveMode) + { + curDiff -= SpatialPredictorBias; + } + + if (curDiff < bestDiff) + { + (bestHisto, histoArgb) = (histoArgb, bestHisto); + bestDiff = curDiff; + bestMode = mode; + } + + for (int i = 0; i < 4; i++) + { + histoArgb[i].AsSpan().Clear(); + } + } + + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 256; j++) + { + accumulated[i][j] += bestHisto[i][j]; + } + } + + return bestMode; + } + + /// + /// Stores the difference between the pixel and its prediction in "output". + /// In case of a lossy encoding, updates the source image to avoid propagating + /// the deviation further to pixels which depend on the current pixel for their + /// predictions. + /// + private static void GetResidual( + int width, + int height, + Span upperRowSpan, + Span currentRowSpan, + Span maxDiffs, + int mode, + int xStart, + int xEnd, + int y, + int maxQuantization, + TransparentColorMode transparentColorMode, + bool usedSubtractGreen, + bool nearLossless, + Span output, + Span scratch) + { + if (transparentColorMode == TransparentColorMode.Preserve) + { + PredictBatch(mode, xStart, y, xEnd - xStart, currentRowSpan, upperRowSpan, output, scratch); + } + else + { +#pragma warning disable SA1503 // Braces should not be omitted +#pragma warning disable RCS1001 // Add braces (when expression spans over multiple lines) + fixed (uint* currentRow = currentRowSpan) + fixed (uint* upperRow = upperRowSpan) + { + for (int x = xStart; x < xEnd; x++) + { + uint predict = 0; + uint residual; + if (y == 0) + { + predict = x == 0 ? WebpConstants.ArgbBlack : currentRow[x - 1]; // Left. + } + else if (x == 0) + { + predict = upperRow[x]; // Top. + } + else + { + switch (mode) + { + case 0: + predict = WebpConstants.ArgbBlack; + break; + case 1: + predict = currentRow[x - 1]; + break; + case 2: + predict = LosslessUtils.Predictor2(currentRow[x - 1], upperRow + x); + break; + case 3: + predict = LosslessUtils.Predictor3(currentRow[x - 1], upperRow + x); + break; + case 4: + predict = LosslessUtils.Predictor4(currentRow[x - 1], upperRow + x); + break; + case 5: + predict = LosslessUtils.Predictor5(currentRow[x - 1], upperRow + x); + break; + case 6: + predict = LosslessUtils.Predictor6(currentRow[x - 1], upperRow + x); + break; + case 7: + predict = LosslessUtils.Predictor7(currentRow[x - 1], upperRow + x); + break; + case 8: + predict = LosslessUtils.Predictor8(currentRow[x - 1], upperRow + x); + break; + case 9: + predict = LosslessUtils.Predictor9(currentRow[x - 1], upperRow + x); + break; + case 10: + predict = LosslessUtils.Predictor10(currentRow[x - 1], upperRow + x); + break; + case 11: + predict = LosslessUtils.Predictor11(currentRow[x - 1], upperRow + x, scratch); + break; + case 12: + predict = LosslessUtils.Predictor12(currentRow[x - 1], upperRow + x); + break; + case 13: + predict = LosslessUtils.Predictor13(currentRow[x - 1], upperRow + x); + break; + } + } + + if (nearLossless) + { + if (maxQuantization == 1 || mode == 0 || y == 0 || y == height - 1 || x == 0 || x == width - 1) + { + residual = LosslessUtils.SubPixels(currentRow[x], predict); + } + else + { + residual = NearLossless(currentRow[x], predict, maxQuantization, maxDiffs[x], usedSubtractGreen); + + // Update the source image. + currentRow[x] = LosslessUtils.AddPixels(predict, residual); + + // x is never 0 here so we do not need to update upperRow like below. + } + } + else + { + residual = LosslessUtils.SubPixels(currentRow[x], predict); + } + + if ((currentRow[x] & MaskAlpha) == 0) + { + // If alpha is 0, cleanup RGB. We can choose the RGB values of the + // residual for best compression. The prediction of alpha itself can be + // non-zero and must be kept though. We choose RGB of the residual to be + // 0. + residual &= MaskAlpha; + + // Update the source image. + currentRow[x] = predict & ~MaskAlpha; + + // The prediction for the rightmost pixel in a row uses the leftmost + // pixel + // in that row as its top-right context pixel. Hence if we change the + // leftmost pixel of current_row, the corresponding change must be + // applied + // to upperRow as well where top-right context is being read from. + if (x == 0 && y != 0) + { + upperRow[width] = currentRow[0]; + } + } + + output[x - xStart] = residual; + } + } + } + } +#pragma warning restore RCS1001 // Add braces (when expression spans over multiple lines) +#pragma warning restore SA1503 // Braces should not be omitted + + /// + /// Quantize every component of the difference between the actual pixel value and + /// its prediction to a multiple of a quantization (a power of 2, not larger than + /// maxQuantization which is a power of 2, smaller than maxDiff). Take care if + /// value and predict have undergone subtract green, which means that red and + /// blue are represented as offsets from green. + /// + private static uint NearLossless(uint value, uint predict, int maxQuantization, int maxDiff, bool usedSubtractGreen) + { + byte newGreen = 0; + byte greenDiff = 0; + byte a; + if (maxDiff <= 2) + { + return LosslessUtils.SubPixels(value, predict); + } + + int quantization = maxQuantization; + while (quantization >= maxDiff) + { + quantization >>= 1; + } + + if (value >> 24 is 0 or 0xff) + { + // Preserve transparency of fully transparent or fully opaque pixels. + a = NearLosslessDiff((byte)((value >> 24) & 0xff), (byte)((predict >> 24) & 0xff)); + } + else + { + a = NearLosslessComponent((byte)(value >> 24), (byte)(predict >> 24), 0xff, quantization); + } + + byte g = NearLosslessComponent((byte)((value >> 8) & 0xff), (byte)((predict >> 8) & 0xff), 0xff, quantization); + + if (usedSubtractGreen) + { + // The green offset will be added to red and blue components during decoding + // to obtain the actual red and blue values. + newGreen = (byte)(((predict >> 8) + g) & 0xff); + + // The amount by which green has been adjusted during quantization. It is + // subtracted from red and blue for compensation, to avoid accumulating two + // quantization errors in them. + greenDiff = NearLosslessDiff(newGreen, (byte)((value >> 8) & 0xff)); + } + + byte r = NearLosslessComponent(NearLosslessDiff((byte)((value >> 16) & 0xff), greenDiff), (byte)((predict >> 16) & 0xff), (byte)(0xff - newGreen), quantization); + byte b = NearLosslessComponent(NearLosslessDiff((byte)(value & 0xff), greenDiff), (byte)(predict & 0xff), (byte)(0xff - newGreen), quantization); + + return ((uint)a << 24) | ((uint)r << 16) | ((uint)g << 8) | b; + } + + /// + /// Quantize the difference between the actual component value and its prediction + /// to a multiple of quantization, working modulo 256, taking care not to cross + /// a boundary (inclusive upper limit). + /// + private static byte NearLosslessComponent(byte value, byte predict, byte boundary, int quantization) + { + int residual = (value - predict) & 0xff; + int boundaryResidual = (boundary - predict) & 0xff; + int lower = residual & ~(quantization - 1); + int upper = lower + quantization; + + // Resolve ties towards a value closer to the prediction (i.e. towards lower + // if value comes after prediction and towards upper otherwise). + int bias = ((boundary - value) & 0xff) < boundaryResidual ? 1 : 0; + + if (residual - lower < upper - residual + bias) + { + // lower is closer to residual than upper. + if (residual > boundaryResidual && lower <= boundaryResidual) + { + // Halve quantization step to avoid crossing boundary. This midpoint is + // on the same side of boundary as residual because midpoint >= residual + // (since lower is closer than upper) and residual is above the boundary. + return (byte)(lower + (quantization >> 1)); + } + + return (byte)lower; + } + + // upper is closer to residual than lower. + if (residual <= boundaryResidual && upper > boundaryResidual) + { + // Halve quantization step to avoid crossing boundary. This midpoint is + // on the same side of boundary as residual because midpoint <= residual + // (since upper is closer than lower) and residual is below the boundary. + return (byte)(lower + (quantization >> 1)); + } + + return (byte)upper; + } + + /// + /// Converts pixels of the image to residuals with respect to predictions. + /// If max_quantization > 1, applies near lossless processing, quantizing + /// residuals to multiples of quantization levels up to max_quantization + /// (the actual quantization level depends on smoothness near the given pixel). + /// + private static void CopyImageWithPrediction( + int width, + int height, + int bits, + Span modes, + Span argbScratch, + Span argb, + int maxQuantization, + TransparentColorMode transparentColorMode, + bool usedSubtractGreen, + bool nearLossless, + bool lowEffort) + { + int tilesPerRow = LosslessUtils.SubSampleSize(width, bits); + + // The width of upperRow and currentRow is one pixel larger than image width + // to allow the top right pixel to point to the leftmost pixel of the next row + // when at the right edge. + Span upperRow = argbScratch; + Span currentRow = upperRow[(width + 1)..]; + Span currentMaxDiffs = MemoryMarshal.Cast(currentRow[(width + 1)..]); + + Span lowerMaxDiffs = currentMaxDiffs[width..]; + Span scratch = stackalloc short[8]; + for (int y = 0; y < height; y++) + { + Span tmp32 = upperRow; + upperRow = currentRow; + currentRow = tmp32; + Span src = argb.Slice(y * width, width + (y + 1 < height ? 1 : 0)); + src.CopyTo(currentRow); + + if (lowEffort) + { + PredictBatch(PredLowEffort, 0, y, width, currentRow, upperRow, argb[(y * width)..], scratch); + } + else + { + if (nearLossless && maxQuantization > 1) + { + // Compute maxDiffs for the lower row now, because that needs the + // contents of bgra for the current row, which we will overwrite with + // residuals before proceeding with the next row. + Span tmp8 = currentMaxDiffs; + currentMaxDiffs = lowerMaxDiffs; + lowerMaxDiffs = tmp8; + if (y + 2 < height) + { + MaxDiffsForRow(width, width, argb, (y + 1) * width, lowerMaxDiffs, usedSubtractGreen); + } + } + + for (int x = 0; x < width;) + { + int mode = (int)((modes[((y >> bits) * tilesPerRow) + (x >> bits)] >> 8) & 0xff); + int xEnd = x + (1 << bits); + if (xEnd > width) + { + xEnd = width; + } + + GetResidual( + width, + height, + upperRow, + currentRow, + currentMaxDiffs, + mode, + x, + xEnd, + y, + maxQuantization, + transparentColorMode, + usedSubtractGreen, + nearLossless, + argb[((y * width) + x)..], + scratch); + + x = xEnd; + } + } + } + } + + private static void PredictBatch( + int mode, + int xStart, + int y, + int numPixels, + Span currentSpan, + Span upperSpan, + Span outputSpan, + Span scratch) + { +#pragma warning disable SA1503 // Braces should not be omitted + fixed (uint* current = currentSpan) + fixed (uint* upper = upperSpan) + fixed (uint* outputFixed = outputSpan) + { + uint* output = outputFixed; + if (xStart == 0) + { + if (y == 0) + { + // ARGB_BLACK. + LosslessUtils.PredictorSub0(current, 1, output); + } + else + { + // Top one. + LosslessUtils.PredictorSub2(current, upper, 1, output); + } + + ++xStart; + ++output; + --numPixels; + } + + if (y == 0) + { + // Left one. + LosslessUtils.PredictorSub1(current + xStart, numPixels, output); + } + else + { + switch (mode) + { + case 0: + LosslessUtils.PredictorSub0(current + xStart, numPixels, output); + break; + case 1: + LosslessUtils.PredictorSub1(current + xStart, numPixels, output); + break; + case 2: + LosslessUtils.PredictorSub2(current + xStart, upper + xStart, numPixels, output); + break; + case 3: + LosslessUtils.PredictorSub3(current + xStart, upper + xStart, numPixels, output); + break; + case 4: + LosslessUtils.PredictorSub4(current + xStart, upper + xStart, numPixels, output); + break; + case 5: + LosslessUtils.PredictorSub5(current + xStart, upper + xStart, numPixels, output); + break; + case 6: + LosslessUtils.PredictorSub6(current + xStart, upper + xStart, numPixels, output); + break; + case 7: + LosslessUtils.PredictorSub7(current + xStart, upper + xStart, numPixels, output); + break; + case 8: + LosslessUtils.PredictorSub8(current + xStart, upper + xStart, numPixels, output); + break; + case 9: + LosslessUtils.PredictorSub9(current + xStart, upper + xStart, numPixels, output); + break; + case 10: + LosslessUtils.PredictorSub10(current + xStart, upper + xStart, numPixels, output); + break; + case 11: + LosslessUtils.PredictorSub11(current + xStart, upper + xStart, numPixels, output, scratch); + break; + case 12: + LosslessUtils.PredictorSub12(current + xStart, upper + xStart, numPixels, output); + break; + case 13: + LosslessUtils.PredictorSub13(current + xStart, upper + xStart, numPixels, output); + break; + } + } + } + } +#pragma warning restore SA1503 // Braces should not be omitted + + private static void MaxDiffsForRow(int width, int stride, Span argb, int offset, Span maxDiffs, bool usedSubtractGreen) + { + if (width <= 2) + { + return; + } + + uint current = argb[offset]; + uint right = argb[offset + 1]; + if (usedSubtractGreen) + { + current = AddGreenToBlueAndRed(current); + right = AddGreenToBlueAndRed(right); + } + + for (int x = 1; x < width - 1; x++) + { + uint up = argb[offset - stride + x]; + uint down = argb[offset + stride + x]; + uint left = current; + current = right; + right = argb[offset + x + 1]; + if (usedSubtractGreen) + { + up = AddGreenToBlueAndRed(up); + down = AddGreenToBlueAndRed(down); + right = AddGreenToBlueAndRed(right); + } + + maxDiffs[x] = (byte)MaxDiffAroundPixel(current, up, down, left, right); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int MaxDiffBetweenPixels(uint p1, uint p2) + { + int diffA = Math.Abs((int)(p1 >> 24) - (int)(p2 >> 24)); + int diffR = Math.Abs((int)((p1 >> 16) & 0xff) - (int)((p2 >> 16) & 0xff)); + int diffG = Math.Abs((int)((p1 >> 8) & 0xff) - (int)((p2 >> 8) & 0xff)); + int diffB = Math.Abs((int)(p1 & 0xff) - (int)(p2 & 0xff)); + return GetMax(GetMax(diffA, diffR), GetMax(diffG, diffB)); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int MaxDiffAroundPixel(uint current, uint up, uint down, uint left, uint right) + { + int diffUp = MaxDiffBetweenPixels(current, up); + int diffDown = MaxDiffBetweenPixels(current, down); + int diffLeft = MaxDiffBetweenPixels(current, left); + int diffRight = MaxDiffBetweenPixels(current, right); + return GetMax(GetMax(diffUp, diffDown), GetMax(diffLeft, diffRight)); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void UpdateHisto(int[][] histoArgb, uint argb) + { + ++histoArgb[0][argb >> 24]; + ++histoArgb[1][(argb >> 16) & 0xff]; + ++histoArgb[2][(argb >> 8) & 0xff]; + ++histoArgb[3][argb & 0xff]; + } + + private static uint AddGreenToBlueAndRed(uint argb) + { + uint green = (argb >> 8) & 0xff; + uint redBlue = argb & 0x00ff00ffu; + redBlue += (green << 16) | green; + redBlue &= 0x00ff00ffu; + return (argb & 0xff00ff00u) | redBlue; + } + + private static void CopyTileWithColorTransform(int xSize, int ySize, int tileX, int tileY, int maxTileSize, Vp8LMultipliers colorTransform, Span argb) + { + int xScan = GetMin(maxTileSize, xSize - tileX); + int yScan = GetMin(maxTileSize, ySize - tileY); + argb = argb[((tileY * xSize) + tileX)..]; + while (yScan-- > 0) + { + LosslessUtils.TransformColor(colorTransform, argb, xScan); + + if (argb.Length > xSize) + { + argb = argb[xSize..]; + } + } + } + + private static Vp8LMultipliers GetBestColorTransformForTile( + int tileX, + int tileY, + int bits, + Vp8LMultipliers prevX, + Vp8LMultipliers prevY, + uint quality, + int xSize, + int ySize, + int[] accumulatedRedHisto, + int[] accumulatedBlueHisto, + Span argb, + Span scratch) + { + int maxTileSize = 1 << bits; + int tileYOffset = tileY * maxTileSize; + int tileXOffset = tileX * maxTileSize; + int allXMax = GetMin(tileXOffset + maxTileSize, xSize); + int allYMax = GetMin(tileYOffset + maxTileSize, ySize); + int tileWidth = allXMax - tileXOffset; + int tileHeight = allYMax - tileYOffset; + Span tileArgb = argb[((tileYOffset * xSize) + tileXOffset)..]; + + Vp8LMultipliers bestTx = default(Vp8LMultipliers); + + GetBestGreenToRed(tileArgb, xSize, scratch, tileWidth, tileHeight, prevX, prevY, quality, accumulatedRedHisto, ref bestTx); + + GetBestGreenRedToBlue(tileArgb, xSize, scratch, tileWidth, tileHeight, prevX, prevY, quality, accumulatedBlueHisto, ref bestTx); + + return bestTx; + } + + private static void GetBestGreenToRed( + Span argb, + int stride, + Span scratch, + int tileWidth, + int tileHeight, + Vp8LMultipliers prevX, + Vp8LMultipliers prevY, + uint quality, + int[] accumulatedRedHisto, + ref Vp8LMultipliers bestTx) + { + uint maxIters = 4 + ((7 * quality) / 256); // in range [4..6] + int greenToRedBest = 0; + double bestDiff = GetPredictionCostCrossColorRed(argb, stride, scratch, tileWidth, tileHeight, prevX, prevY, greenToRedBest, accumulatedRedHisto); + for (int iter = 0; iter < (int)maxIters; iter++) + { + // ColorTransformDelta is a 3.5 bit fixed point, so 32 is equal to + // one in color computation. Having initial delta here as 1 is sufficient + // to explore the range of (-2, 2). + int delta = 32 >> iter; + + // Try a negative and a positive delta from the best known value. + for (int offset = -delta; offset <= delta; offset += 2 * delta) + { + int greenToRedCur = offset + greenToRedBest; + double curDiff = GetPredictionCostCrossColorRed(argb, stride, scratch, tileWidth, tileHeight, prevX, prevY, greenToRedCur, accumulatedRedHisto); + if (curDiff < bestDiff) + { + bestDiff = curDiff; + greenToRedBest = greenToRedCur; + } + } + } + + bestTx.GreenToRed = (byte)(greenToRedBest & 0xff); + } + + private static void GetBestGreenRedToBlue(Span argb, int stride, Span scratch, int tileWidth, int tileHeight, Vp8LMultipliers prevX, Vp8LMultipliers prevY, uint quality, int[] accumulatedBlueHisto, ref Vp8LMultipliers bestTx) + { + int iters = (quality < 25) ? 1 : (quality > 50) ? GreenRedToBlueMaxIters : 4; + int greenToBlueBest = 0; + int redToBlueBest = 0; + + // Initial value at origin: + double bestDiff = GetPredictionCostCrossColorBlue(argb, stride, scratch, tileWidth, tileHeight, prevX, prevY, greenToBlueBest, redToBlueBest, accumulatedBlueHisto); + for (int iter = 0; iter < iters; iter++) + { + int delta = DeltaLut[iter]; + for (int axis = 0; axis < GreenRedToBlueNumAxis; axis++) + { + int greenToBlueCur = (Offset[axis][0] * delta) + greenToBlueBest; + int redToBlueCur = (Offset[axis][1] * delta) + redToBlueBest; + double curDiff = GetPredictionCostCrossColorBlue(argb, stride, scratch, tileWidth, tileHeight, prevX, prevY, greenToBlueCur, redToBlueCur, accumulatedBlueHisto); + if (curDiff < bestDiff) + { + bestDiff = curDiff; + greenToBlueBest = greenToBlueCur; + redToBlueBest = redToBlueCur; + } + + if (quality < 25 && iter == 4) + { + // Only axis aligned diffs for lower quality. + break; // next iter. + } + } + + if (delta == 2 && greenToBlueBest == 0 && redToBlueBest == 0) + { + // Further iterations would not help. + break; // out of iter-loop. + } + } + + bestTx.GreenToBlue = (byte)(greenToBlueBest & 0xff); + bestTx.RedToBlue = (byte)(redToBlueBest & 0xff); + } + + private static double GetPredictionCostCrossColorRed( + Span argb, + int stride, + Span scratch, + int tileWidth, + int tileHeight, + Vp8LMultipliers prevX, + Vp8LMultipliers prevY, + int greenToRed, + int[] accumulatedRedHisto) + { + Span histo = scratch[..256]; + histo.Clear(); + + ColorSpaceTransformUtils.CollectColorRedTransforms(argb, stride, tileWidth, tileHeight, greenToRed, histo); + double curDiff = PredictionCostCrossColor(accumulatedRedHisto, histo); + + if ((byte)greenToRed == prevX.GreenToRed) + { + // Favor keeping the areas locally similar. + curDiff -= 3; + } + + if ((byte)greenToRed == prevY.GreenToRed) + { + // Favor keeping the areas locally similar. + curDiff -= 3; + } + + if (greenToRed == 0) + { + curDiff -= 3; + } + + return curDiff; + } + + private static double GetPredictionCostCrossColorBlue( + Span argb, + int stride, + Span scratch, + int tileWidth, + int tileHeight, + Vp8LMultipliers prevX, + Vp8LMultipliers prevY, + int greenToBlue, + int redToBlue, + int[] accumulatedBlueHisto) + { + Span histo = scratch[..256]; + histo.Clear(); + + ColorSpaceTransformUtils.CollectColorBlueTransforms(argb, stride, tileWidth, tileHeight, greenToBlue, redToBlue, histo); + double curDiff = PredictionCostCrossColor(accumulatedBlueHisto, histo); + if ((byte)greenToBlue == prevX.GreenToBlue) + { + // Favor keeping the areas locally similar. + curDiff -= 3; + } + + if ((byte)greenToBlue == prevY.GreenToBlue) + { + // Favor keeping the areas locally similar. + curDiff -= 3; + } + + if ((byte)redToBlue == prevX.RedToBlue) + { + // Favor keeping the areas locally similar. + curDiff -= 3; + } + + if ((byte)redToBlue == prevY.RedToBlue) + { + // Favor keeping the areas locally similar. + curDiff -= 3; + } + + if (greenToBlue == 0) + { + curDiff -= 3; + } + + if (redToBlue == 0) + { + curDiff -= 3; + } + + return curDiff; + } + + private static float PredictionCostSpatialHistogram(int[][] accumulated, int[][] tile) + { + double retVal = 0.0d; + for (int i = 0; i < 4; i++) + { + double kExpValue = 0.94; + retVal += PredictionCostSpatial(tile[i], 1, kExpValue); + retVal += LosslessUtils.CombinedShannonEntropy(tile[i], accumulated[i]); + } + + return (float)retVal; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static double PredictionCostCrossColor(int[] accumulated, Span counts) + { + // Favor low entropy, locally and globally. + // Favor small absolute values for PredictionCostSpatial. + const double expValue = 2.4d; + return LosslessUtils.CombinedShannonEntropy(counts, accumulated) + PredictionCostSpatial(counts, 3, expValue); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static float PredictionCostSpatial(Span counts, int weight0, double expVal) + { + int significantSymbols = 256 >> 4; + double expDecayFactor = 0.6; + double bits = weight0 * counts[0]; + for (int i = 1; i < significantSymbols; i++) + { + bits += expVal * (counts[i] + counts[256 - i]); + expVal *= expDecayFactor; + } + + return (float)(-0.1 * bits); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static byte NearLosslessDiff(byte a, byte b) => (byte)((a - b) & 0xff); + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint MultipliersToColorCode(Vp8LMultipliers m) => 0xff000000u | ((uint)m.RedToBlue << 16) | ((uint)m.GreenToBlue << 8) | m.GreenToRed; + + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetMin(int a, int b) => a > b ? b : a; + + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetMax(int a, int b) => (a < b) ? b : a; + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs b/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs new file mode 100644 index 0000000..34e259a --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + internal class Vp8LBackwardRefs : IDisposable + { + private readonly IMemoryOwner refs; + private int count; + + public Vp8LBackwardRefs(MemoryAllocator memoryAllocator, int pixels) + { + this.refs = memoryAllocator.Allocate(pixels); + this.count = 0; + } + + public void Add(PixOrCopy pixOrCopy) => this.refs.Memory.Span[this.count++] = pixOrCopy; + + public void Clear() => this.count = 0; + + public Span.Enumerator GetEnumerator() => this.refs.Slice(0, this.count).GetEnumerator(); + + /// + public void Dispose() => this.refs.Dispose(); + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs b/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs new file mode 100644 index 0000000..2096934 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs @@ -0,0 +1,220 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// Holds bit entropy results and entropy-related functions. + /// + internal class Vp8LBitEntropy + { + /// + /// Not a trivial literal symbol. + /// + private const uint NonTrivialSym = 0xffffffff; + + /// + /// Initializes a new instance of the class. + /// + public Vp8LBitEntropy() + { + this.Entropy = 0.0d; + this.Sum = 0; + this.NoneZeros = 0; + this.MaxVal = 0; + this.NoneZeroCode = NonTrivialSym; + } + + /// + /// Gets or sets the entropy. + /// + public double Entropy { get; set; } + + /// + /// Gets or sets the sum of the population. + /// + public uint Sum { get; set; } + + /// + /// Gets or sets the number of non-zero elements in the population. + /// + public int NoneZeros { get; set; } + + /// + /// Gets or sets the maximum value in the population. + /// + public uint MaxVal { get; set; } + + /// + /// Gets or sets the index of the last non-zero in the population. + /// + public uint NoneZeroCode { get; set; } + + public void Init() + { + this.Entropy = 0.0d; + this.Sum = 0; + this.NoneZeros = 0; + this.MaxVal = 0; + this.NoneZeroCode = NonTrivialSym; + } + + public double BitsEntropyRefine() + { + double mix; + if (this.NoneZeros < 5) + { + if (this.NoneZeros <= 1) + { + return 0; + } + + // Two symbols, they will be 0 and 1 in a Huffman code. + // Let's mix in a bit of entropy to favor good clustering when + // distributions of these are combined. + if (this.NoneZeros == 2) + { + return (0.99 * this.Sum) + (0.01 * this.Entropy); + } + + // No matter what the entropy says, we cannot be better than minLimit + // with Huffman coding. I am mixing a bit of entropy into the + // minLimit since it produces much better (~0.5 %) compression results + // perhaps because of better entropy clustering. + if (this.NoneZeros == 3) + { + mix = 0.95; + } + else + { + mix = 0.7; // nonzeros == 4. + } + } + else + { + mix = 0.627; + } + + double minLimit = (2 * this.Sum) - this.MaxVal; + minLimit = (mix * minLimit) + ((1.0 - mix) * this.Entropy); + return this.Entropy < minLimit ? minLimit : this.Entropy; + } + + public void BitsEntropyUnrefined(Span array, int n) + { + this.Init(); + + for (int i = 0; i < n; i++) + { + if (array[i] != 0) + { + this.Sum += array[i]; + this.NoneZeroCode = (uint)i; + this.NoneZeros++; + this.Entropy -= LosslessUtils.FastSLog2(array[i]); + if (this.MaxVal < array[i]) + { + this.MaxVal = array[i]; + } + } + } + + this.Entropy += LosslessUtils.FastSLog2(this.Sum); + } + + /// + /// Get the entropy for the distribution 'X'. + /// + public void BitsEntropyUnrefined(Span x, int length, Vp8LStreaks stats) + { + int i; + int iPrev = 0; + uint xPrev = x[0]; + + this.Init(); + + for (i = 1; i < length; i++) + { + uint xi = x[i]; + if (xi != xPrev) + { + this.GetEntropyUnrefined(xi, i, ref xPrev, ref iPrev, stats); + } + } + + this.GetEntropyUnrefined(0, i, ref xPrev, ref iPrev, stats); + + this.Entropy += LosslessUtils.FastSLog2(this.Sum); + } + + public void GetCombinedEntropyUnrefined(Span x, Span y, int length, Vp8LStreaks stats) + { + int i; + int iPrev = 0; + uint xyPrev = x[0] + y[0]; + + this.Init(); + + for (i = 1; i < length; i++) + { + uint xy = x[i] + y[i]; + if (xy != xyPrev) + { + this.GetEntropyUnrefined(xy, i, ref xyPrev, ref iPrev, stats); + } + } + + this.GetEntropyUnrefined(0, i, ref xyPrev, ref iPrev, stats); + + this.Entropy += LosslessUtils.FastSLog2(this.Sum); + } + + public void GetEntropyUnrefined(Span x, int length, Vp8LStreaks stats) + { + int i; + int iPrev = 0; + uint xPrev = x[0]; + + this.Init(); + + for (i = 1; i < length; i++) + { + uint xi = x[i]; + if (xi != xPrev) + { + this.GetEntropyUnrefined(xi, i, ref xPrev, ref iPrev, stats); + } + } + + this.GetEntropyUnrefined(0, i, ref xPrev, ref iPrev, stats); + + this.Entropy += LosslessUtils.FastSLog2(this.Sum); + } + + private void GetEntropyUnrefined(uint val, int i, ref uint valPrev, ref int iPrev, Vp8LStreaks stats) + { + int streak = i - iPrev; + + // Gather info for the bit entropy. + if (valPrev != 0) + { + this.Sum += (uint)(valPrev * streak); + this.NoneZeros += streak; + this.NoneZeroCode = (uint)iPrev; + this.Entropy -= LosslessUtils.FastSLog2(valPrev) * streak; + if (this.MaxVal < valPrev) + { + this.MaxVal = valPrev; + } + } + + // Gather info for the Huffman cost. + stats.Counts[valPrev != 0 ? 1 : 0] += streak > 3 ? 1 : 0; + stats.Streaks[valPrev != 0 ? 1 : 0][streak > 3 ? 1 : 0] += streak; + + valPrev = val; + iPrev = i; + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/Vp8LDecoder.cs b/ImageSharp/Formats/Webp/Lossless/Vp8LDecoder.cs new file mode 100644 index 0000000..2cf3dd0 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/Vp8LDecoder.cs @@ -0,0 +1,70 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using System.Collections.Generic; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// Holds information for decoding a lossless webp image. + /// + internal class Vp8LDecoder : IDisposable + { + /// + /// Initializes a new instance of the class. + /// + /// The width of the image. + /// The height of the image. + /// Used for allocating memory for the pixel data output. + public Vp8LDecoder(int width, int height, MemoryAllocator memoryAllocator) + { + this.Width = width; + this.Height = height; + this.Metadata = new Vp8LMetadata(); + this.Pixels = memoryAllocator.Allocate(width * height, AllocationOptions.Clean); + } + + /// + /// Gets or sets the width of the image to decode. + /// + public int Width { get; set; } + + /// + /// Gets or sets the height of the image to decode. + /// + public int Height { get; set; } + + /// + /// Gets or sets the necessary VP8L metadata (like huffman tables) to decode the image. + /// + public Vp8LMetadata Metadata { get; set; } + + /// + /// Gets or sets the transformations which needs to be reversed. + /// + public List Transforms { get; set; } + + /// + /// Gets the pixel data. + /// + public IMemoryOwner Pixels { get; } + + /// + public void Dispose() + { + this.Pixels.Dispose(); + this.Metadata?.HuffmanImage?.Dispose(); + + if (this.Transforms != null) + { + foreach (Vp8LTransform transform in this.Transforms) + { + transform.Data?.Dispose(); + } + } + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs new file mode 100644 index 0000000..9b9c5d8 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -0,0 +1,1932 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Webp.BitWriter; +using SixLabors.ImageSharp.Formats.Webp.Chunks; +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.Webp.Lossless { + /// + /// Encoder for lossless webp images. + /// + internal class Vp8LEncoder : IDisposable + { + /// + /// Scratch buffer to reduce allocations. + /// + private ScratchBuffer scratch; // mutable struct, don't make readonly + + private readonly int[][] histoArgb = [new int[256], new int[256], new int[256], new int[256]]; + + private readonly int[][] bestHisto = [new int[256], new int[256], new int[256], new int[256]]; + + /// + /// The to use for buffer allocations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// Maximum number of reference blocks the image will be segmented into. + /// + private const int MaxRefsBlockPerImage = 16; + + /// + /// A bit writer for writing lossless webp streams. + /// + private Vp8LBitWriter bitWriter; + + /// + /// The quality, that will be used to encode the image. + /// + private readonly uint quality; + + /// + /// Quality/speed trade-off (0=fast, 6=slower-better). + /// + private readonly WebpEncodingMethod method; + + /// + /// Flag indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible + /// RGB information for better compression. + /// + private readonly TransparentColorMode transparentColorMode; + + /// + /// Whether to skip metadata during encoding. + /// + private readonly bool skipMetadata; + + /// + /// Indicating whether near lossless mode should be used. + /// + private readonly bool nearLossless; + + /// + /// The near lossless quality. The range is 0 (maximum preprocessing) to 100 (no preprocessing, the default). + /// + private readonly int nearLosslessQuality; + + private const int ApplyPaletteGreedyMax = 4; + + private const int PaletteInvSizeBits = 11; + + private const int PaletteInvSize = 1 << PaletteInvSizeBits; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The global configuration. + /// The width of the input image. + /// The height of the input image. + /// The encoding quality. + /// Whether to skip metadata encoding. + /// Quality/speed trade-off (0=fast, 6=slower-better). + /// Flag indicating whether to preserve the exact RGB values under transparent area. + /// Otherwise, discard this invisible RGB information for better compression. + /// Indicating whether near lossless mode should be used. + /// The near lossless quality. The range is 0 (maximum preprocessing) to 100 (no preprocessing, the default). + public Vp8LEncoder( + MemoryAllocator memoryAllocator, + Configuration configuration, + int width, + int height, + uint quality, + bool skipMetadata, + WebpEncodingMethod method, + TransparentColorMode transparentColorMode, + bool nearLossless, + int nearLosslessQuality) + { + int pixelCount = width * height; + int initialSize = pixelCount * 2; + + this.memoryAllocator = memoryAllocator; + this.configuration = configuration; + this.quality = Math.Min(quality, 100u); + this.skipMetadata = skipMetadata; + this.method = method; + this.transparentColorMode = transparentColorMode; + this.nearLossless = nearLossless; + this.nearLosslessQuality = Numerics.Clamp(nearLosslessQuality, 0, 100); + this.bitWriter = new Vp8LBitWriter(initialSize); + this.Bgra = memoryAllocator.Allocate(pixelCount); + this.EncodedData = memoryAllocator.Allocate(pixelCount); + this.Palette = memoryAllocator.Allocate(WebpConstants.MaxPaletteSize); + this.Refs = new Vp8LBackwardRefs[3]; + this.HashChain = new Vp8LHashChain(memoryAllocator, pixelCount); + + for (int i = 0; i < this.Refs.Length; i++) + { + this.Refs[i] = new Vp8LBackwardRefs(memoryAllocator, pixelCount); + } + } + + // RFC 1951 will calm you down if you are worried about this funny sequence. + // This sequence is tuned from that, but more weighted for lower symbol count, + // and more spiking histograms. + // This uses C#'s compiler optimization to refer to assembly's static data directly. + private static ReadOnlySpan StorageOrder => [17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + + // This uses C#'s compiler optimization to refer to assembly's static data directly. + private static ReadOnlySpan Order => [1, 2, 0, 3]; + + /// + /// Gets the memory for the image data as packed bgra values. + /// + public IMemoryOwner Bgra { get; } + + /// + /// Gets the memory for the encoded output image data. + /// + public IMemoryOwner EncodedData { get; } + + /// + /// Gets or sets the scratch memory for bgra rows used for predictions. + /// + public IMemoryOwner BgraScratch { get; set; } + + /// + /// Gets or sets the packed image width. + /// + public int CurrentWidth { get; set; } + + /// + /// Gets or sets the huffman image bits. + /// + public int HistoBits { get; set; } + + /// + /// Gets or sets the bits used for the transformation. + /// + public int TransformBits { get; set; } + + /// + /// Gets or sets the transform data. + /// + public IMemoryOwner TransformData { get; set; } + + /// + /// Gets or sets the cache bits. If equal to 0, don't use color cache. + /// + public int CacheBits { get; set; } + + /// + /// Gets or sets a value indicating whether to use the cross color transform. + /// + public bool UseCrossColorTransform { get; set; } + + /// + /// Gets or sets a value indicating whether to use the subtract green transform. + /// + public bool UseSubtractGreenTransform { get; set; } + + /// + /// Gets or sets a value indicating whether to use the predictor transform. + /// + public bool UsePredictorTransform { get; set; } + + /// + /// Gets or sets a value indicating whether to use color indexing transform. + /// + public bool UsePalette { get; set; } + + /// + /// Gets or sets the palette size. + /// + public int PaletteSize { get; set; } + + /// + /// Gets the palette. + /// + public IMemoryOwner Palette { get; } + + /// + /// Gets the backward references. + /// + public Vp8LBackwardRefs[] Refs { get; } + + /// + /// Gets the hash chain. + /// + public Vp8LHashChain HashChain { get; } + + public WebpVp8X EncodeHeader(Image image, Stream stream, bool hasAnimation, ushort? repeatCount) + where TPixel : unmanaged, IPixel + { + // Write bytes from the bit-writer buffer to the stream. + ImageMetadata metadata = image.Metadata; + ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; + XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + + // The alpha flag is updated following encoding. + WebpVp8X vp8x = BitWriterBase.WriteTrunksBeforeData( + stream, + (uint)image.Width, + (uint)image.Height, + exifProfile, + xmpProfile, + metadata.IccProfile, + false, + hasAnimation); + + if (hasAnimation) + { + WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata(); + BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, repeatCount ?? webpMetadata.RepeatCount); + } + + return vp8x; + } + + public void EncodeFooter(Image image, in WebpVp8X vp8x, bool hasAlpha, Stream stream, long initialPosition) + where TPixel : unmanaged, IPixel + { + // Write bytes from the bit-writer buffer to the stream. + ImageMetadata metadata = image.Metadata; + + ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; + XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + + bool updateVp8x = hasAlpha && vp8x != default; + WebpVp8X updated = updateVp8x ? vp8x.WithAlpha(true) : vp8x; + BitWriterBase.WriteTrunksAfterData(stream, in updated, updateVp8x, initialPosition, exifProfile, xmpProfile); + } + + /// + /// Encodes the image as lossless webp to the specified stream. + /// + /// The pixel format. + /// The image frame to encode from. + /// The region of interest within the frame to encode. + /// The frame metadata. + /// The to encode the image data to. + /// Flag indicating, if an animation parameter is present. + /// A indicating whether the frame contains an alpha channel. + public bool Encode(ImageFrame frame, Rectangle bounds, WebpFrameMetadata frameMetadata, Stream stream, bool hasAnimation) + where TPixel : unmanaged, IPixel + { + // Convert image pixels to bgra array. + bool hasAlpha = this.ConvertPixelsToBgra(frame.PixelBuffer.GetRegion(bounds)); + + // Write the image size. + this.WriteImageSize(bounds.Width, bounds.Height); + + // Write the non-trivial Alpha flag and lossless version. + this.WriteAlphaAndVersion(hasAlpha); + + // Encode the main image stream. + this.EncodeStream(bounds.Width, bounds.Height); + + this.bitWriter.Finish(); + + long prevPosition = 0; + + if (hasAnimation) + { + prevPosition = new WebpFrameData( + (uint)bounds.Left, + (uint)bounds.Top, + (uint)bounds.Width, + (uint)bounds.Height, + frameMetadata.FrameDelay, + frameMetadata.BlendMode, + frameMetadata.DisposalMode) + .WriteHeaderTo(stream); + } + + // Write bytes from the bit-writer buffer to the stream. + this.bitWriter.WriteEncodedImageToStream(stream); + + if (hasAnimation) + { + RiffHelper.EndWriteChunk(stream, prevPosition); + } + + return hasAlpha; + } + + /// + /// Encodes the alpha image data using the webp lossless compression. + /// + /// The type of the pixel. + /// The alpha-pixel data to encode from. + /// The destination buffer to write the encoded alpha data to. + /// The size of the compressed data in bytes. + /// If the size of the data is the same as the pixel count, the compression would not yield in smaller data and is left uncompressed. + /// + public int EncodeAlphaImageData(Buffer2DRegion frame, IMemoryOwner alphaData) + where TPixel : unmanaged, IPixel + { + int width = frame.Width; + int height = frame.Height; + int pixelCount = width * height; + + // Convert image pixels to bgra array. + this.ConvertPixelsToBgra(frame); + + // The image-stream will NOT contain any headers describing the image dimension, the dimension is already known. + this.EncodeStream(width, height); + this.bitWriter.Finish(); + int size = this.bitWriter.NumBytes; + if (size >= pixelCount) + { + // Compressing would not yield in smaller data -> leave the data uncompressed. + return pixelCount; + } + + this.bitWriter.WriteToBuffer(alphaData.GetSpan()); + return size; + } + + /// + /// Writes the image size to the bit writer buffer. + /// + /// The input image width. + /// The input image height. + private void WriteImageSize(int inputImgWidth, int inputImgHeight) + { + uint width = (uint)inputImgWidth - 1; + uint height = (uint)inputImgHeight - 1; + + this.bitWriter.PutBits(width, WebpConstants.Vp8LImageSizeBits); + this.bitWriter.PutBits(height, WebpConstants.Vp8LImageSizeBits); + } + + /// + /// Writes a flag indicating if alpha channel is used and the VP8L version to the bit-writer buffer. + /// + /// Indicates if a alpha channel is present. + private void WriteAlphaAndVersion(bool hasAlpha) + { + this.bitWriter.PutBits(hasAlpha ? 1U : 0, 1); + this.bitWriter.PutBits(WebpConstants.Vp8LVersion, WebpConstants.Vp8LVersionBits); + } + + /// + /// Encodes the image stream using lossless webp format. + /// + /// The image frame width. + /// The image frame height. + private void EncodeStream(int width, int height) + { + Span bgra = this.Bgra.GetSpan(); + Span encodedData = this.EncodedData.GetSpan(); + bool lowEffort = this.method == 0; + + // Analyze image (entropy, numPalettes etc). + CrunchConfig[] crunchConfigs = this.EncoderAnalyze(bgra, width, height, out bool redAndBlueAlwaysZero); + + int bestSize = 0; + Vp8LBitWriter bitWriterInit = this.bitWriter; + Vp8LBitWriter bitWriterBest = this.bitWriter.Clone(); + bool isFirstConfig = true; + foreach (CrunchConfig crunchConfig in crunchConfigs) + { + bgra.CopyTo(encodedData); + const bool useCache = true; + this.UsePalette = crunchConfig.EntropyIdx is EntropyIx.Palette or EntropyIx.PaletteAndSpatial; + this.UseSubtractGreenTransform = crunchConfig.EntropyIdx is EntropyIx.SubGreen or EntropyIx.SpatialSubGreen; + this.UsePredictorTransform = crunchConfig.EntropyIdx is EntropyIx.Spatial or EntropyIx.SpatialSubGreen; + if (lowEffort) + { + this.UseCrossColorTransform = false; + } + else + { + this.UseCrossColorTransform = !redAndBlueAlwaysZero && this.UsePredictorTransform; + } + + this.AllocateTransformBuffer(width, height); + + // Reset any parameter in the encoder that is set in the previous iteration. + this.CacheBits = 0; + this.ClearRefs(); + + if (this.nearLossless) + { + // Apply near-lossless preprocessing. + bool useNearLossless = this.nearLosslessQuality < 100 && !this.UsePalette && !this.UsePredictorTransform; + if (useNearLossless) + { + this.AllocateTransformBuffer(width, height); + NearLosslessEnc.ApplyNearLossless(width, height, this.nearLosslessQuality, bgra, bgra, width); + } + } + + // Encode palette. + if (this.UsePalette) + { + this.EncodePalette(lowEffort); + this.MapImageFromPalette(width, height); + + // If using a color cache, do not have it bigger than the number of colors. + if (useCache && this.PaletteSize < 1 << WebpConstants.MaxColorCacheBits) + { + this.CacheBits = BitOperations.Log2((uint)this.PaletteSize) + 1; + } + } + + // Apply transforms and write transform data. + if (this.UseSubtractGreenTransform) + { + this.ApplySubtractGreen(); + } + + if (this.UsePredictorTransform) + { + this.ApplyPredictFilter(this.CurrentWidth, height, lowEffort); + } + + if (this.UseCrossColorTransform) + { + this.ApplyCrossColorFilter(this.CurrentWidth, height, lowEffort); + } + + this.bitWriter.PutBits(0, 1); // No more transforms. + + // Encode and write the transformed image. + this.EncodeImage( + this.CurrentWidth, + height, + useCache, + crunchConfig, + this.CacheBits, + lowEffort); + + // If we are better than what we already have. + if (isFirstConfig || this.bitWriter.NumBytes < bestSize) + { + bestSize = this.bitWriter.NumBytes; + BitWriterSwap(ref this.bitWriter, ref bitWriterBest); + } + + // Reset the bit writer for the following iteration if any. + if (crunchConfigs.Length > 1) + { + this.bitWriter.Reset(bitWriterInit); + } + + isFirstConfig = false; + } + + BitWriterSwap(ref bitWriterBest, ref this.bitWriter); + } + + /// + /// Converts the pixels of the image to bgra. + /// + /// The type of the pixels. + /// The frame pixel buffer to convert. + /// true, if the image is non opaque. + public bool ConvertPixelsToBgra(Buffer2DRegion pixels) + where TPixel : unmanaged, IPixel + { + bool nonOpaque = false; + Span bgra = this.Bgra.GetSpan(); + Span bgraBytes = MemoryMarshal.Cast(bgra); + int widthBytes = pixels.Width * 4; + for (int y = 0; y < pixels.Height; y++) + { + Span rowSpan = pixels.DangerousGetRowSpan(y); + Span rowBytes = bgraBytes.Slice(y * widthBytes, widthBytes); + PixelOperations.Instance.ToBgra32Bytes(this.configuration, rowSpan, rowBytes, pixels.Width); + if (!nonOpaque) + { + Span rowBgra = MemoryMarshal.Cast(rowBytes); + nonOpaque = WebpCommonUtils.CheckNonOpaque(rowBgra); + } + } + + return nonOpaque; + } + + /// + /// Analyzes the image and decides which transforms should be used. + /// + /// The image as packed bgra values. + /// The image width. + /// The image height. + /// Indicates if red and blue are always zero. + private CrunchConfig[] EncoderAnalyze(ReadOnlySpan bgra, int width, int height, out bool redAndBlueAlwaysZero) + { + // Check if we only deal with a small number of colors and should use a palette. + bool usePalette = this.AnalyzeAndCreatePalette(bgra, width, height); + + // Empirical bit sizes. + this.HistoBits = GetHistoBits(this.method, usePalette, width, height); + this.TransformBits = GetTransformBits(this.method, this.HistoBits); + + // Try out multiple LZ77 on images with few colors. + int nlz77s = this.PaletteSize is > 0 and <= 16 ? 2 : 1; + EntropyIx entropyIdx = this.AnalyzeEntropy(bgra, width, height, usePalette, this.PaletteSize, this.TransformBits, out redAndBlueAlwaysZero); + + bool doNotCache = false; + List crunchConfigs = []; + + if (this.method == WebpEncodingMethod.BestQuality && this.quality == 100) + { + doNotCache = true; + + // Go brute force on all transforms. + foreach (EntropyIx entropyIx in Enum.GetValues()) + { + // We can only apply kPalette or kPaletteAndSpatial if we can indeed use a palette. + if ((entropyIx != EntropyIx.Palette && entropyIx != EntropyIx.PaletteAndSpatial) || usePalette) + { + crunchConfigs.Add(new CrunchConfig { EntropyIdx = entropyIx }); + } + } + } + else + { + // Only choose the guessed best transform. + crunchConfigs.Add(new CrunchConfig { EntropyIdx = entropyIdx }); + if (this.quality >= 75 && this.method == WebpEncodingMethod.Level5) + { + // Test with and without color cache. + doNotCache = true; + + // If we have a palette, also check in combination with spatial. + if (entropyIdx == EntropyIx.Palette) + { + crunchConfigs.Add(new CrunchConfig { EntropyIdx = EntropyIx.PaletteAndSpatial }); + } + } + } + + // Fill in the different LZ77s. + foreach (CrunchConfig crunchConfig in crunchConfigs) + { + for (int j = 0; j < nlz77s; j++) + { + crunchConfig.SubConfigs.Add(new CrunchSubConfig + { + Lz77 = j == 0 ? (int)Vp8LLz77Type.Lz77Standard | (int)Vp8LLz77Type.Lz77Rle : (int)Vp8LLz77Type.Lz77Box, + DoNotCache = doNotCache + }); + } + } + + return [.. crunchConfigs]; + } + + private void EncodeImage(int width, int height, bool useCache, CrunchConfig config, int cacheBits, bool lowEffort) + { + // bgra data with transformations applied. + Span bgra = this.EncodedData.GetSpan(); + int histogramImageXySize = LosslessUtils.SubSampleSize(width, this.HistoBits) * LosslessUtils.SubSampleSize(height, this.HistoBits); + Span histogramSymbols = histogramImageXySize <= 64 ? stackalloc ushort[histogramImageXySize] : new ushort[histogramImageXySize]; + Span huffTree = stackalloc HuffmanTree[3 * WebpConstants.CodeLengthCodes]; + + if (useCache) + { + if (cacheBits == 0) + { + cacheBits = WebpConstants.MaxColorCacheBits; + } + } + else + { + cacheBits = 0; + } + + // Calculate backward references from BGRA image. + this.HashChain.Fill(bgra, this.quality, width, height, lowEffort); + + Vp8LBitWriter bitWriterBest = config.SubConfigs.Count > 1 ? this.bitWriter.Clone() : this.bitWriter; + Vp8LBitWriter bwInit = this.bitWriter; + bool isFirstIteration = true; + foreach (CrunchSubConfig subConfig in config.SubConfigs) + { + Vp8LBackwardRefs refsBest = BackwardReferenceEncoder.GetBackwardReferences( + width, + height, + bgra, + this.quality, + subConfig.Lz77, + ref cacheBits, + this.memoryAllocator, + this.HashChain, + this.Refs[0], + this.Refs[1]); + + // Keep the best references aside and use the other element from the first + // two as a temporary for later usage. + Vp8LBackwardRefs refsTmp = this.Refs[refsBest.Equals(this.Refs[0]) ? 1 : 0]; + + this.bitWriter.Reset(bwInit); + using OwnedVp8LHistogram tmpHisto = OwnedVp8LHistogram.Create(this.memoryAllocator, cacheBits); + using Vp8LHistogramSet histogramImage = new(this.memoryAllocator, histogramImageXySize, cacheBits); + + // Build histogram image and symbols from backward references. + HistogramEncoder.GetHistoImageSymbols( + this.memoryAllocator, + width, + height, + refsBest, + this.quality, + this.HistoBits, + cacheBits, + histogramImage, + tmpHisto, + histogramSymbols); + + // Create Huffman bit lengths and codes for each histogram image. + int histogramImageSize = histogramImage.Count; + int bitArraySize = 5 * histogramImageSize; + HuffmanTreeCode[] huffmanCodes = new HuffmanTreeCode[bitArraySize]; + + GetHuffBitLengthsAndCodes(histogramImage, huffmanCodes); + + // Color Cache parameters. + if (cacheBits > 0) + { + this.bitWriter.PutBits(1, 1); + this.bitWriter.PutBits((uint)cacheBits, 4); + } + else + { + this.bitWriter.PutBits(0, 1); + } + + // Huffman image + meta huffman. + bool writeHistogramImage = histogramImageSize > 1; + this.bitWriter.PutBits((uint)(writeHistogramImage ? 1 : 0), 1); + if (writeHistogramImage) + { + using IMemoryOwner histogramBgraBuffer = this.memoryAllocator.Allocate(histogramImageXySize); + Span histogramBgra = histogramBgraBuffer.GetSpan(); + int maxIndex = 0; + for (int i = 0; i < histogramImageXySize; i++) + { + int symbolIndex = histogramSymbols[i] & 0xffff; + histogramBgra[i] = (uint)(symbolIndex << 8); + if (symbolIndex >= maxIndex) + { + maxIndex = symbolIndex + 1; + } + } + + histogramImageSize = maxIndex; + + this.bitWriter.PutBits((uint)(this.HistoBits - 2), 3); + this.EncodeImageNoHuffman( + histogramBgra, + this.HashChain, + refsTmp, + this.Refs[2], + LosslessUtils.SubSampleSize(width, this.HistoBits), + LosslessUtils.SubSampleSize(height, this.HistoBits), + this.quality, + lowEffort); + } + + // Store Huffman codes. + // Find maximum number of symbols for the huffman tree-set. + int maxTokens = 0; + for (int i = 0; i < 5 * histogramImageSize; i++) + { + HuffmanTreeCode codes = huffmanCodes[i]; + if (maxTokens < codes.NumSymbols) + { + maxTokens = codes.NumSymbols; + } + } + + HuffmanTreeToken[] tokens = new HuffmanTreeToken[maxTokens]; + for (int i = 0; i < tokens.Length; i++) + { + tokens[i] = new HuffmanTreeToken(); + } + + for (int i = 0; i < 5 * histogramImageSize; i++) + { + HuffmanTreeCode codes = huffmanCodes[i]; + this.StoreHuffmanCode(huffTree, tokens, codes); + ClearHuffmanTreeIfOnlyOneSymbol(codes); + } + + // Store actual literals. + this.StoreImageToBitMask(width, this.HistoBits, refsBest, histogramSymbols, huffmanCodes); + + // Keep track of the smallest image so far. + if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes < bitWriterBest.NumBytes)) + { + (bitWriterBest, this.bitWriter) = (this.bitWriter, bitWriterBest); + } + + isFirstIteration = false; + } + + this.bitWriter = bitWriterBest; + } + + /// + /// Save the palette to the bitstream. + /// + private void EncodePalette(bool lowEffort) + { + Span tmpPalette = stackalloc uint[WebpConstants.MaxPaletteSize]; + int paletteSize = this.PaletteSize; + Span palette = this.Palette.Memory.Span; + this.bitWriter.PutBits(WebpConstants.TransformPresent, 1); + this.bitWriter.PutBits((uint)Vp8LTransformType.ColorIndexingTransform, 2); + this.bitWriter.PutBits((uint)paletteSize - 1, 8); + for (int i = paletteSize - 1; i >= 1; i--) + { + tmpPalette[i] = LosslessUtils.SubPixels(palette[i], palette[i - 1]); + } + + tmpPalette[0] = palette[0]; + this.EncodeImageNoHuffman(tmpPalette, this.HashChain, this.Refs[0], this.Refs[1], width: paletteSize, height: 1, quality: 20, lowEffort); + } + + /// + /// Applies the subtract green transformation to the pixel data of the image. + /// + private void ApplySubtractGreen() + { + this.bitWriter.PutBits(WebpConstants.TransformPresent, 1); + this.bitWriter.PutBits((uint)Vp8LTransformType.SubtractGreen, 2); + LosslessUtils.SubtractGreenFromBlueAndRed(this.EncodedData.GetSpan()); + } + + private void ApplyPredictFilter(int width, int height, bool lowEffort) + { + // We disable near-lossless quantization if palette is used. + int nearLosslessStrength = this.UsePalette ? 100 : this.nearLosslessQuality; + int predBits = this.TransformBits; + int transformWidth = LosslessUtils.SubSampleSize(width, predBits); + int transformHeight = LosslessUtils.SubSampleSize(height, predBits); + + PredictorEncoder.ResidualImage( + width, + height, + predBits, + this.EncodedData.GetSpan(), + this.BgraScratch.GetSpan(), + this.TransformData.GetSpan(), + this.histoArgb, + this.bestHisto, + this.nearLossless, + nearLosslessStrength, + this.transparentColorMode, + this.UseSubtractGreenTransform, + lowEffort); + + this.bitWriter.PutBits(WebpConstants.TransformPresent, 1); + this.bitWriter.PutBits((uint)Vp8LTransformType.PredictorTransform, 2); + this.bitWriter.PutBits((uint)(predBits - 2), 3); + + this.EncodeImageNoHuffman(this.TransformData.GetSpan(), this.HashChain, this.Refs[0], this.Refs[1], transformWidth, transformHeight, this.quality, lowEffort); + } + + private void ApplyCrossColorFilter(int width, int height, bool lowEffort) + { + int colorTransformBits = this.TransformBits; + int transformWidth = LosslessUtils.SubSampleSize(width, colorTransformBits); + int transformHeight = LosslessUtils.SubSampleSize(height, colorTransformBits); + + PredictorEncoder.ColorSpaceTransform(width, height, colorTransformBits, this.quality, this.EncodedData.GetSpan(), this.TransformData.GetSpan(), this.scratch.Span); + + this.bitWriter.PutBits(WebpConstants.TransformPresent, 1); + this.bitWriter.PutBits((uint)Vp8LTransformType.CrossColorTransform, 2); + this.bitWriter.PutBits((uint)(colorTransformBits - 2), 3); + + this.EncodeImageNoHuffman(this.TransformData.GetSpan(), this.HashChain, this.Refs[0], this.Refs[1], transformWidth, transformHeight, this.quality, lowEffort); + } + + private void EncodeImageNoHuffman(Span bgra, Vp8LHashChain hashChain, Vp8LBackwardRefs refsTmp1, Vp8LBackwardRefs refsTmp2, int width, int height, uint quality, bool lowEffort) + { + int cacheBits = 0; + ushort[] histogramSymbols = new ushort[1]; // Only one tree, one symbol. + + HuffmanTreeCode[] huffmanCodes = new HuffmanTreeCode[5]; + Span huffTree = stackalloc HuffmanTree[3 * WebpConstants.CodeLengthCodes]; + + // Calculate backward references from the image pixels. + hashChain.Fill(bgra, quality, width, height, lowEffort); + + Vp8LBackwardRefs refs = BackwardReferenceEncoder.GetBackwardReferences( + width, + height, + bgra, + quality, + (int)Vp8LLz77Type.Lz77Standard | (int)Vp8LLz77Type.Lz77Rle, + ref cacheBits, + this.memoryAllocator, + hashChain, + refsTmp1, + refsTmp2); + + // Build histogram image and symbols from backward references. + using Vp8LHistogramSet histogramImage = new(this.memoryAllocator, refs, 1, cacheBits); + + // Create Huffman bit lengths and codes for each histogram image. + GetHuffBitLengthsAndCodes(histogramImage, huffmanCodes); + + // No color cache, no Huffman image. + this.bitWriter.PutBits(0, 1); + + // Find maximum number of symbols for the huffman tree-set. + int maxTokens = 0; + for (int i = 0; i < 5; i++) + { + HuffmanTreeCode codes = huffmanCodes[i]; + if (maxTokens < codes.NumSymbols) + { + maxTokens = codes.NumSymbols; + } + } + + HuffmanTreeToken[] tokens = new HuffmanTreeToken[maxTokens]; + for (int i = 0; i < tokens.Length; i++) + { + tokens[i] = new HuffmanTreeToken(); + } + + // Store Huffman codes. + for (int i = 0; i < 5; i++) + { + HuffmanTreeCode codes = huffmanCodes[i]; + this.StoreHuffmanCode(huffTree, tokens, codes); + ClearHuffmanTreeIfOnlyOneSymbol(codes); + } + + // Store actual literals. + this.StoreImageToBitMask(width, 0, refs, histogramSymbols, huffmanCodes); + } + + private void StoreHuffmanCode(Span huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode huffmanCode) + { + int count = 0; + Span symbols = this.scratch.Span[..2]; + symbols.Clear(); + const int maxBits = 8; + const int maxSymbol = 1 << maxBits; + + // Check whether it's a small tree. + for (int i = 0; i < huffmanCode.NumSymbols && count < 3; i++) + { + if (huffmanCode.CodeLengths[i] != 0) + { + if (count < 2) + { + symbols[count] = i; + } + + count++; + } + } + + if (count == 0) + { + // Emit minimal tree for empty cases. + // bits: small tree marker: 1, count-1: 0, large 8-bit code: 0, code: 0 + this.bitWriter.PutBits(0x01, 4); + } + else if (count <= 2 && symbols[0] < maxSymbol && symbols[1] < maxSymbol) + { + this.bitWriter.PutBits(1, 1); // Small tree marker to encode 1 or 2 symbols. + this.bitWriter.PutBits((uint)(count - 1), 1); + if (symbols[0] <= 1) + { + this.bitWriter.PutBits(0, 1); // Code bit for small (1 bit) symbol value. + this.bitWriter.PutBits((uint)symbols[0], 1); + } + else + { + this.bitWriter.PutBits(1, 1); + this.bitWriter.PutBits((uint)symbols[0], 8); + } + + if (count == 2) + { + this.bitWriter.PutBits((uint)symbols[1], 8); + } + } + else + { + this.StoreFullHuffmanCode(huffTree, tokens, huffmanCode); + } + } + + private void StoreFullHuffmanCode(Span huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode tree) + { + // TODO: Allocations. This method is called in a loop. + int i; + byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes]; + short[] codeLengthBitDepthSymbols = new short[WebpConstants.CodeLengthCodes]; + HuffmanTreeCode huffmanCode = new() + { + NumSymbols = WebpConstants.CodeLengthCodes, + CodeLengths = codeLengthBitDepth, + Codes = codeLengthBitDepthSymbols + }; + + this.bitWriter.PutBits(0, 1); + int numTokens = HuffmanUtils.CreateCompressedHuffmanTree(tree, tokens); + uint[] histogram = new uint[WebpConstants.CodeLengthCodes + 1]; + bool[] bufRle = new bool[WebpConstants.CodeLengthCodes + 1]; + for (i = 0; i < numTokens; i++) + { + histogram[tokens[i].Code]++; + } + + HuffmanUtils.CreateHuffmanTree(histogram, 7, bufRle, huffTree, huffmanCode); + this.StoreHuffmanTreeOfHuffmanTreeToBitMask(codeLengthBitDepth); + ClearHuffmanTreeIfOnlyOneSymbol(huffmanCode); + + int trailingZeroBits = 0; + int trimmedLength = numTokens; + i = numTokens; + while (i-- > 0) + { + int ix = tokens[i].Code; + if (ix is 0 or 17 or 18) + { + trimmedLength--; // Discount trailing zeros. + trailingZeroBits += codeLengthBitDepth[ix]; + if (ix == 17) + { + trailingZeroBits += 3; + } + else if (ix == 18) + { + trailingZeroBits += 7; + } + } + else + { + break; + } + } + + bool writeTrimmedLength = trimmedLength > 1 && trailingZeroBits > 12; + int length = writeTrimmedLength ? trimmedLength : numTokens; + this.bitWriter.PutBits((uint)(writeTrimmedLength ? 1 : 0), 1); + if (writeTrimmedLength) + { + if (trimmedLength == 2) + { + this.bitWriter.PutBits(0, 3 + 2); // nbitpairs=1, trimmedLength=2 + } + else + { + int nBits = BitOperations.Log2((uint)trimmedLength - 2); + int nBitPairs = (int)(((uint)nBits / 2) + 1); + this.bitWriter.PutBits((uint)nBitPairs - 1, 3); + this.bitWriter.PutBits((uint)trimmedLength - 2, nBitPairs * 2); + } + } + + this.StoreHuffmanTreeToBitMask(tokens, length, huffmanCode); + } + + private void StoreHuffmanTreeToBitMask(HuffmanTreeToken[] tokens, int numTokens, HuffmanTreeCode huffmanCode) + { + for (int i = 0; i < numTokens; i++) + { + int ix = tokens[i].Code; + int extraBits = tokens[i].ExtraBits; + this.bitWriter.PutBits((uint)huffmanCode.Codes[ix], huffmanCode.CodeLengths[ix]); + switch (ix) + { + case 16: + this.bitWriter.PutBits((uint)extraBits, 2); + break; + case 17: + this.bitWriter.PutBits((uint)extraBits, 3); + break; + case 18: + this.bitWriter.PutBits((uint)extraBits, 7); + break; + } + } + } + + private void StoreHuffmanTreeOfHuffmanTreeToBitMask(byte[] codeLengthBitDepth) + { + // Throw away trailing zeros: + int codesToStore = WebpConstants.CodeLengthCodes; + for (; codesToStore > 4; codesToStore--) + { + if (codeLengthBitDepth[StorageOrder[codesToStore - 1]] != 0) + { + break; + } + } + + this.bitWriter.PutBits((uint)codesToStore - 4, 4); + for (int i = 0; i < codesToStore; i++) + { + this.bitWriter.PutBits(codeLengthBitDepth[StorageOrder[i]], 3); + } + } + + private void StoreImageToBitMask( + int width, + int histoBits, + Vp8LBackwardRefs backwardRefs, + Span histogramSymbols, + HuffmanTreeCode[] huffmanCodes) + { + int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(width, histoBits) : 1; + int tileMask = histoBits == 0 ? 0 : -(1 << histoBits); + + // x and y trace the position in the image. + int x = 0; + int y = 0; + int tileX = x & tileMask; + int tileY = y & tileMask; + int histogramIx = histogramSymbols[0]; + Span codes = huffmanCodes.AsSpan(5 * histogramIx); + + foreach (PixOrCopy v in backwardRefs) + { + if (tileX != (x & tileMask) || tileY != (y & tileMask)) + { + tileX = x & tileMask; + tileY = y & tileMask; + histogramIx = histogramSymbols[((y >> histoBits) * histoXSize) + (x >> histoBits)]; + codes = huffmanCodes.AsSpan(5 * histogramIx); + } + + if (v.IsLiteral()) + { + for (int k = 0; k < 4; k++) + { + int code = v.Literal(Order[k]); + this.bitWriter.WriteHuffmanCode(codes[k], code); + } + } + else if (v.IsCacheIdx()) + { + int code = (int)v.CacheIdx(); + int literalIx = 256 + WebpConstants.NumLengthCodes + code; + this.bitWriter.WriteHuffmanCode(codes[0], literalIx); + } + else + { + int bits = 0; + int nBits = 0; + int distance = (int)v.Distance(); + int code = LosslessUtils.PrefixEncode(v.Len, ref nBits, ref bits); + this.bitWriter.WriteHuffmanCodeWithExtraBits(codes[0], 256 + code, bits, nBits); + + // Don't write the distance with the extra bits code since + // the distance can be up to 18 bits of extra bits, and the prefix + // 15 bits, totaling to 33, and our PutBits only supports up to 32 bits. + code = LosslessUtils.PrefixEncode(distance, ref nBits, ref bits); + this.bitWriter.WriteHuffmanCode(codes[4], code); + this.bitWriter.PutBits((uint)bits, nBits); + } + + x += v.Length(); + while (x >= width) + { + x -= width; + y++; + } + } + } + + /// + /// Analyzes the entropy of the input image to determine which transforms to use during encoding the image. + /// + /// The image to analyze as a bgra span. + /// The image width. + /// The image height. + /// Indicates whether a palette should be used. + /// The palette size. + /// The transformation bits. + /// Indicates if red and blue are always zero. + /// The entropy mode to use. + private EntropyIx AnalyzeEntropy(ReadOnlySpan bgra, int width, int height, bool usePalette, int paletteSize, int transformBits, out bool redAndBlueAlwaysZero) + { + if (usePalette && paletteSize <= 16) + { + // In the case of small palettes, we pack 2, 4 or 8 pixels together. In + // practice, small palettes are better than any other transform. + redAndBlueAlwaysZero = true; + return EntropyIx.Palette; + } + + using IMemoryOwner histoBuffer = this.memoryAllocator.Allocate((int)HistoIx.HistoTotal * 256, AllocationOptions.Clean); + Span histo = histoBuffer.Memory.Span; + uint pixPrev = bgra[0]; // Skip the first pixel. + ReadOnlySpan prevRow = null; + for (int y = 0; y < height; y++) + { + ReadOnlySpan currentRow = bgra.Slice(y * width, width); + for (int x = 0; x < width; x++) + { + uint pix = currentRow[x]; + uint pixDiff = LosslessUtils.SubPixels(pix, pixPrev); + pixPrev = pix; + if (pixDiff == 0 || (prevRow.Length > 0 && pix == prevRow[x])) + { + continue; + } + + AddSingle( + pix, + histo[..], + histo[((int)HistoIx.HistoRed * 256)..], + histo[((int)HistoIx.HistoGreen * 256)..], + histo[((int)HistoIx.HistoBlue * 256)..]); + AddSingle( + pixDiff, + histo[((int)HistoIx.HistoAlphaPred * 256)..], + histo[((int)HistoIx.HistoRedPred * 256)..], + histo[((int)HistoIx.HistoGreenPred * 256)..], + histo[((int)HistoIx.HistoBluePred * 256)..]); + AddSingleSubGreen( + pix, + histo[((int)HistoIx.HistoRedSubGreen * 256)..], + histo[((int)HistoIx.HistoBlueSubGreen * 256)..]); + AddSingleSubGreen( + pixDiff, + histo[((int)HistoIx.HistoRedPredSubGreen * 256)..], + histo[((int)HistoIx.HistoBluePredSubGreen * 256)..]); + + // Approximate the palette by the entropy of the multiplicative hash. + uint hash = HashPix(pix); + histo[((int)HistoIx.HistoPalette * 256) + (int)hash]++; + } + + prevRow = currentRow; + } + + Span entropyComp = stackalloc double[(int)HistoIx.HistoTotal]; + Span entropy = stackalloc double[(int)EntropyIx.NumEntropyIx]; + int lastModeToAnalyze = usePalette ? (int)EntropyIx.Palette : (int)EntropyIx.SpatialSubGreen; + + // Let's add one zero to the predicted histograms. The zeros are removed + // too efficiently by the pixDiff == 0 comparison, at least one of the + // zeros is likely to exist. + histo[(int)HistoIx.HistoRedPredSubGreen * 256]++; + histo[(int)HistoIx.HistoBluePredSubGreen * 256]++; + histo[(int)HistoIx.HistoRedPred * 256]++; + histo[(int)HistoIx.HistoGreenPred * 256]++; + histo[(int)HistoIx.HistoBluePred * 256]++; + histo[(int)HistoIx.HistoAlphaPred * 256]++; + + Vp8LBitEntropy bitEntropy = new(); + for (int j = 0; j < (int)HistoIx.HistoTotal; j++) + { + bitEntropy.Init(); + Span curHisto = histo.Slice(j * 256, 256); + bitEntropy.BitsEntropyUnrefined(curHisto, 256); + entropyComp[j] = bitEntropy.BitsEntropyRefine(); + } + + entropy[(int)EntropyIx.Direct] = + entropyComp[(int)HistoIx.HistoAlpha] + + entropyComp[(int)HistoIx.HistoRed] + + entropyComp[(int)HistoIx.HistoGreen] + + entropyComp[(int)HistoIx.HistoBlue]; + entropy[(int)EntropyIx.Spatial] = + entropyComp[(int)HistoIx.HistoAlphaPred] + + entropyComp[(int)HistoIx.HistoRedPred] + + entropyComp[(int)HistoIx.HistoGreenPred] + + entropyComp[(int)HistoIx.HistoBluePred]; + entropy[(int)EntropyIx.SubGreen] = + entropyComp[(int)HistoIx.HistoAlpha] + + entropyComp[(int)HistoIx.HistoRedSubGreen] + + entropyComp[(int)HistoIx.HistoGreen] + + entropyComp[(int)HistoIx.HistoBlueSubGreen]; + entropy[(int)EntropyIx.SpatialSubGreen] = + entropyComp[(int)HistoIx.HistoAlphaPred] + + entropyComp[(int)HistoIx.HistoRedPredSubGreen] + + entropyComp[(int)HistoIx.HistoGreenPred] + + entropyComp[(int)HistoIx.HistoBluePredSubGreen]; + entropy[(int)EntropyIx.Palette] = entropyComp[(int)HistoIx.HistoPalette]; + + // When including transforms, there is an overhead in bits from + // storing them. This overhead is small but matters for small images. + // For spatial, there are 14 transformations. + entropy[(int)EntropyIx.Spatial] += + LosslessUtils.SubSampleSize(width, transformBits) * + LosslessUtils.SubSampleSize(height, transformBits) * + LosslessUtils.FastLog2(14); + + // For color transforms: 24 as only 3 channels are considered in a ColorTransformElement. + entropy[(int)EntropyIx.SpatialSubGreen] += + LosslessUtils.SubSampleSize(width, transformBits) * + LosslessUtils.SubSampleSize(height, transformBits) * + LosslessUtils.FastLog2(24); + + // For palettes, add the cost of storing the palette. + // We empirically estimate the cost of a compressed entry as 8 bits. + // The palette is differential-coded when compressed hence a much + // lower cost than sizeof(uint32_t)*8. + entropy[(int)EntropyIx.Palette] += paletteSize * 8; + + EntropyIx minEntropyIx = EntropyIx.Direct; + for (int k = (int)EntropyIx.Direct + 1; k <= lastModeToAnalyze; k++) + { + if (entropy[(int)minEntropyIx] > entropy[k]) + { + minEntropyIx = (EntropyIx)k; + } + } + + redAndBlueAlwaysZero = true; + + // Let's check if the histogram of the chosen entropy mode has + // non-zero red and blue values. If all are zero, we can later skip + // the cross color optimization. + byte[][] histoPairs = + [ + [(byte)HistoIx.HistoRed, (byte)HistoIx.HistoBlue], + [(byte)HistoIx.HistoRedPred, (byte)HistoIx.HistoBluePred], + [(byte)HistoIx.HistoRedSubGreen, (byte)HistoIx.HistoBlueSubGreen], + [(byte)HistoIx.HistoRedPredSubGreen, (byte)HistoIx.HistoBluePredSubGreen], + [(byte)HistoIx.HistoRed, (byte)HistoIx.HistoBlue] + ]; + Span redHisto = histo[(256 * histoPairs[(int)minEntropyIx][0])..]; + Span blueHisto = histo[(256 * histoPairs[(int)minEntropyIx][1])..]; + for (int i = 1; i < 256; i++) + { + if ((redHisto[i] | blueHisto[i]) != 0) + { + redAndBlueAlwaysZero = false; + break; + } + } + + return minEntropyIx; + } + + /// + /// If number of colors in the image is less than or equal to MaxPaletteSize, + /// creates a palette and returns true, else returns false. + /// + /// The image as packed bgra values. + /// The image width. + /// The image height. + /// true, if a palette should be used. + private bool AnalyzeAndCreatePalette(ReadOnlySpan bgra, int width, int height) + { + Span palette = this.Palette.Memory.Span; + this.PaletteSize = GetColorPalette(bgra, width, height, palette); + if (this.PaletteSize > WebpConstants.MaxPaletteSize) + { + this.PaletteSize = 0; + return false; + } + + Span paletteSlice = palette[..this.PaletteSize]; + paletteSlice.Sort(); + + if (PaletteHasNonMonotonousDeltas(palette, this.PaletteSize)) + { + GreedyMinimizeDeltas(palette, this.PaletteSize); + } + + return true; + } + + /// + /// Gets the color palette. + /// + /// The image to get the palette from as packed bgra values. + /// The image width. + /// The image height. + /// The span to store the palette into. + /// The number of palette entries. + private static int GetColorPalette(ReadOnlySpan bgra, int width, int height, Span palette) + { + HashSet colors = []; + for (int y = 0; y < height; y++) + { + ReadOnlySpan bgraRow = bgra.Slice(y * width, width); + for (int x = 0; x < width; x++) + { + colors.Add(bgraRow[x]); + if (colors.Count > WebpConstants.MaxPaletteSize) + { + // Exact count is not needed, because a palette will not be used then anyway. + return WebpConstants.MaxPaletteSize + 1; + } + } + } + + // Fill the colors into the palette. + using HashSet.Enumerator colorEnumerator = colors.GetEnumerator(); + int idx = 0; + while (colorEnumerator.MoveNext()) + { + palette[idx++] = colorEnumerator.Current; + } + + return colors.Count; + } + + private void MapImageFromPalette(int width, int height) + { + Span src = this.EncodedData.GetSpan(); + int srcStride = this.CurrentWidth; + Span dst = this.EncodedData.GetSpan(); // Applying the palette will be done in place. + Span palette = this.Palette.GetSpan(); + int paletteSize = this.PaletteSize; + int xBits; + + // Replace each input pixel by corresponding palette index. + // This is done line by line. + if (paletteSize <= 4) + { + xBits = paletteSize <= 2 ? 3 : 2; + } + else + { + xBits = paletteSize <= 16 ? 1 : 0; + } + + this.CurrentWidth = LosslessUtils.SubSampleSize(width, xBits); + this.ApplyPalette(src, srcStride, dst, this.CurrentWidth, palette, paletteSize, width, height, xBits); + } + + /// + /// Remap bgra values in src[] to packed palettes entries in dst[] + /// using 'row' as a temporary buffer of size 'width'. + /// We assume that all src[] values have a corresponding entry in the palette. + /// Note: src[] can be the same as dst[] + /// + private void ApplyPalette(Span src, int srcStride, Span dst, int dstStride, Span palette, int paletteSize, int width, int height, int xBits) + { + using IMemoryOwner tmpRowBuffer = this.memoryAllocator.Allocate(width); + Span tmpRow = tmpRowBuffer.GetSpan(); + + if (paletteSize < ApplyPaletteGreedyMax) + { + uint prevPix = palette[0]; + uint prevIdx = 0; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + uint pix = src[x]; + if (pix != prevPix) + { + prevIdx = SearchColorGreedy(palette, pix); + prevPix = pix; + } + + tmpRow[x] = (byte)prevIdx; + } + + BundleColorMap(tmpRow, width, xBits, dst); + src = src[srcStride..]; + dst = dst[dstStride..]; + } + } + else + { + uint[] buffer = new uint[PaletteInvSize]; + + // Try to find a perfect hash function able to go from a color to an index + // within 1 << PaletteInvSize in order to build a hash map to go from color to index in palette. + int i; + for (i = 0; i < 3; i++) + { + bool useLut = true; + + // Set each element in buffer to max value. + buffer.AsSpan().Fill(uint.MaxValue); + + for (int j = 0; j < paletteSize; j++) + { + uint ind = 0; + switch (i) + { + case 0: + ind = ApplyPaletteHash0(palette[j]); + break; + case 1: + ind = ApplyPaletteHash1(palette[j]); + break; + case 2: + ind = ApplyPaletteHash2(palette[j]); + break; + } + + if (buffer[ind] != uint.MaxValue) + { + useLut = false; + break; + } + + buffer[ind] = (uint)j; + } + + if (useLut) + { + break; + } + } + + if (i is 0 or 1 or 2) + { + ApplyPaletteFor(width, height, palette, i, src, srcStride, dst, dstStride, tmpRow, buffer, xBits); + } + else + { + uint[] idxMap = new uint[paletteSize]; + uint[] paletteSorted = new uint[paletteSize]; + PrepareMapToPalette(palette, paletteSize, paletteSorted, idxMap); + ApplyPaletteForWithIdxMap(width, height, palette, src, srcStride, dst, dstStride, tmpRow, idxMap, xBits, paletteSorted, paletteSize); + } + } + } + + private static void ApplyPaletteFor(int width, int height, Span palette, int hashIdx, Span src, int srcStride, Span dst, int dstStride, Span tmpRow, uint[] buffer, int xBits) + { + uint prevPix = palette[0]; + uint prevIdx = 0; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + uint pix = src[x]; + if (pix != prevPix) + { + switch (hashIdx) + { + case 0: + prevIdx = buffer[ApplyPaletteHash0(pix)]; + break; + case 1: + prevIdx = buffer[ApplyPaletteHash1(pix)]; + break; + case 2: + prevIdx = buffer[ApplyPaletteHash2(pix)]; + break; + } + + prevPix = pix; + } + + tmpRow[x] = (byte)prevIdx; + } + + LosslessUtils.BundleColorMap(tmpRow, width, xBits, dst); + + src = src[srcStride..]; + dst = dst[dstStride..]; + } + } + + private static void ApplyPaletteForWithIdxMap(int width, int height, Span palette, Span src, int srcStride, Span dst, int dstStride, Span tmpRow, uint[] idxMap, int xBits, uint[] paletteSorted, int paletteSize) + { + uint prevPix = palette[0]; + uint prevIdx = 0; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + uint pix = src[x]; + if (pix != prevPix) + { + prevIdx = idxMap[SearchColorNoIdx(paletteSorted, pix, paletteSize)]; + prevPix = pix; + } + + tmpRow[x] = (byte)prevIdx; + } + + LosslessUtils.BundleColorMap(tmpRow, width, xBits, dst); + + src = src[srcStride..]; + dst = dst[dstStride..]; + } + } + + /// + /// Sort palette in increasing order and prepare an inverse mapping array. + /// + private static void PrepareMapToPalette(Span palette, int numColors, uint[] sorted, uint[] idxMap) + { + palette[..numColors].CopyTo(sorted); + Array.Sort(sorted, PaletteCompareColorsForSort); + for (int i = 0; i < numColors; i++) + { + idxMap[SearchColorNoIdx(sorted, palette[i], numColors)] = (uint)i; + } + } + + private static int SearchColorNoIdx(uint[] sorted, uint color, int hi) + { + int low = 0; + if (sorted[low] == color) + { + return low; // loop invariant: sorted[low] != color + } + + while (true) + { + int mid = (low + hi) >> 1; + if (sorted[mid] == color) + { + return mid; + } + + if (sorted[mid] < color) + { + low = mid; + } + else + { + hi = mid; + } + } + } + + private static void ClearHuffmanTreeIfOnlyOneSymbol(HuffmanTreeCode huffmanCode) + { + int count = 0; + for (int k = 0; k < huffmanCode.NumSymbols; k++) + { + if (huffmanCode.CodeLengths[k] != 0) + { + count++; + if (count > 1) + { + return; + } + } + } + + for (int k = 0; k < huffmanCode.NumSymbols; k++) + { + huffmanCode.CodeLengths[k] = 0; + huffmanCode.Codes[k] = 0; + } + } + + /// + /// The palette has been sorted by alpha. This function checks if the other components of the palette + /// have a monotonic development with regards to position in the palette. + /// If all have monotonic development, there is no benefit to re-organize them greedily. A monotonic development + /// would be spotted in green-only situations (like lossy alpha) or gray-scale images. + /// + /// The palette. + /// Number of colors in the palette. + /// True, if the palette has no monotonous deltas. + private static bool PaletteHasNonMonotonousDeltas(Span palette, int numColors) + { + const uint predict = 0x000000; + byte signFound = 0x00; + for (int i = 0; i < numColors; i++) + { + uint diff = LosslessUtils.SubPixels(palette[i], predict); + byte rd = (byte)((diff >> 16) & 0xff); + byte gd = (byte)((diff >> 8) & 0xff); + byte bd = (byte)((diff >> 0) & 0xff); + if (rd != 0x00) + { + signFound |= (byte)(rd < 0x80 ? 1 : 2); + } + + if (gd != 0x00) + { + signFound |= (byte)(gd < 0x80 ? 8 : 16); + } + + if (bd != 0x00) + { + signFound |= (byte)(bd < 0x80 ? 64 : 128); + } + } + + return (signFound & (signFound << 1)) != 0; // two consequent signs. + } + + /// + /// Find greedily always the closest color of the predicted color to minimize + /// deltas in the palette. This reduces storage needs since the palette is stored with delta encoding. + /// + /// The palette. + /// The number of colors in the palette. + private static void GreedyMinimizeDeltas(Span palette, int numColors) + { + uint predict = 0x00000000; + for (int i = 0; i < numColors; i++) + { + int bestIdx = i; + uint bestScore = ~0U; + for (int k = i; k < numColors; k++) + { + uint curScore = PaletteColorDistance(palette[k], predict); + if (bestScore > curScore) + { + bestScore = curScore; + bestIdx = k; + } + } + + // Swap color(palette[bestIdx], palette[i]); + (palette[i], palette[bestIdx]) = (palette[bestIdx], palette[i]); + predict = palette[i]; + } + } + + private static void GetHuffBitLengthsAndCodes(Vp8LHistogramSet histogramImage, HuffmanTreeCode[] huffmanCodes) + { + int maxNumSymbols = 0; + + // Iterate over all histograms and get the aggregate number of codes used. + for (int i = 0; i < histogramImage.Count; i++) + { + Vp8LHistogram histo = histogramImage[i]; + int startIdx = 5 * i; + for (int k = 0; k < 5; k++) + { + int numSymbols; + if (k == 0) + { + numSymbols = histo.NumCodes(); + } + else if (k == 4) + { + numSymbols = WebpConstants.NumDistanceCodes; + } + else + { + numSymbols = 256; + } + + huffmanCodes[startIdx + k].NumSymbols = numSymbols; + } + } + + // TODO: Allocations. + int end = 5 * histogramImage.Count; + for (int i = 0; i < end; i++) + { + int bitLength = huffmanCodes[i].NumSymbols; + huffmanCodes[i].Codes = new short[bitLength]; + huffmanCodes[i].CodeLengths = new byte[bitLength]; + if (maxNumSymbols < bitLength) + { + maxNumSymbols = bitLength; + } + } + + // Create Huffman trees. + // TODO: Allocations. + bool[] bufRle = new bool[maxNumSymbols]; + HuffmanTree[] huffTree = new HuffmanTree[3 * maxNumSymbols]; + + for (int i = 0; i < histogramImage.Count; i++) + { + int codesStartIdx = 5 * i; + Vp8LHistogram histo = histogramImage[i]; + HuffmanUtils.CreateHuffmanTree(histo.Literal, 15, bufRle, huffTree, huffmanCodes[codesStartIdx]); + HuffmanUtils.CreateHuffmanTree(histo.Red, 15, bufRle, huffTree, huffmanCodes[codesStartIdx + 1]); + HuffmanUtils.CreateHuffmanTree(histo.Blue, 15, bufRle, huffTree, huffmanCodes[codesStartIdx + 2]); + HuffmanUtils.CreateHuffmanTree(histo.Alpha, 15, bufRle, huffTree, huffmanCodes[codesStartIdx + 3]); + HuffmanUtils.CreateHuffmanTree(histo.Distance, 15, bufRle, huffTree, huffmanCodes[codesStartIdx + 4]); + } + } + + /// + /// Computes a value that is related to the entropy created by the palette entry diff. + /// + /// First color. + /// Second color. + /// The color distance. + [MethodImpl(InliningOptions.ShortMethod)] + private static uint PaletteColorDistance(uint col1, uint col2) + { + uint diff = LosslessUtils.SubPixels(col1, col2); + const uint moreWeightForRGBThanForAlpha = 9; + uint score = PaletteComponentDistance((diff >> 0) & 0xff); + score += PaletteComponentDistance((diff >> 8) & 0xff); + score += PaletteComponentDistance((diff >> 16) & 0xff); + score *= moreWeightForRGBThanForAlpha; + score += PaletteComponentDistance((diff >> 24) & 0xff); + + return score; + } + + /// + /// Calculates the huffman image bits. + /// + private static int GetHistoBits(WebpEncodingMethod method, bool usePalette, int width, int height) + { + // Make tile size a function of encoding method (Range: 0 to 6). + int histoBits = (usePalette ? 9 : 7) - (int)method; + while (true) + { + int huffImageSize = LosslessUtils.SubSampleSize(width, histoBits) * LosslessUtils.SubSampleSize(height, histoBits); + if (huffImageSize <= WebpConstants.MaxHuffImageSize) + { + break; + } + + histoBits++; + } + + if (histoBits < WebpConstants.MinHuffmanBits) + { + return WebpConstants.MinHuffmanBits; + } + else if (histoBits > WebpConstants.MaxHuffmanBits) + { + return WebpConstants.MaxHuffmanBits; + } + else + { + return histoBits; + } + } + + /// + /// Bundles multiple (1, 2, 4 or 8) pixels into a single pixel. + /// + private static void BundleColorMap(Span row, int width, int xBits, Span dst) + { + int x; + if (xBits > 0) + { + int bitDepth = 1 << (3 - xBits); + int mask = (1 << xBits) - 1; + uint code = 0xff000000; + for (x = 0; x < width; x++) + { + int xSub = x & mask; + if (xSub == 0) + { + code = 0xff000000; + } + + code |= (uint)(row[x] << (8 + (bitDepth * xSub))); + dst[x >> xBits] = code; + } + } + else + { + for (x = 0; x < width; x++) + { + dst[x] = (uint)(0xff000000 | (row[x] << 8)); + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void BitWriterSwap(ref Vp8LBitWriter src, ref Vp8LBitWriter dst) + => (dst, src) = (src, dst); + + /// + /// Calculates the bits used for the transformation. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetTransformBits(WebpEncodingMethod method, int histoBits) + { + int maxTransformBits; + if ((int)method < 4) + { + maxTransformBits = 6; + } + else if (method > WebpEncodingMethod.Level4) + { + maxTransformBits = 4; + } + else + { + maxTransformBits = 5; + } + + return histoBits > maxTransformBits ? maxTransformBits : histoBits; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void AddSingle(uint p, Span a, Span r, Span g, Span b) + { + a[(int)(p >> 24) & 0xff]++; + r[(int)(p >> 16) & 0xff]++; + g[(int)(p >> 8) & 0xff]++; + b[(int)(p >> 0) & 0xff]++; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void AddSingleSubGreen(uint p, Span r, Span b) + { + int green = (int)p >> 8; // The upper bits are masked away later. + r[(int)((p >> 16) - green) & 0xff]++; + b[(int)((p >> 0) - green) & 0xff]++; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint SearchColorGreedy(Span palette, uint color) + { + if (color == palette[0]) + { + return 0; + } + + if (color == palette[1]) + { + return 1; + } + + if (color == palette[2]) + { + return 2; + } + + return 3; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint ApplyPaletteHash0(uint color) => (color >> 8) & 0xff; // Focus on the green color. + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint ApplyPaletteHash1(uint color) => (uint)((color & 0x00ffffffu) * 4222244071ul) >> (32 - PaletteInvSizeBits); // Forget about alpha. + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint ApplyPaletteHash2(uint color) => (uint)((color & 0x00ffffffu) * ((1ul << 31) - 1)) >> (32 - PaletteInvSizeBits); // Forget about alpha. + + // Note that masking with 0xffffffffu is for preventing an + // 'unsigned int overflow' warning. Doesn't impact the compiled code. + [MethodImpl(InliningOptions.ShortMethod)] + private static uint HashPix(uint pix) => (uint)((((long)pix + (pix >> 19)) * 0x39c5fba7L) & 0xffffffffu) >> 24; + + [MethodImpl(InliningOptions.ShortMethod)] + private static int PaletteCompareColorsForSort(uint p1, uint p2) => p1 < p2 ? -1 : 1; + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint PaletteComponentDistance(uint v) => (v <= 128) ? v : (256 - v); + + public void AllocateTransformBuffer(int width, int height) + { + // VP8LResidualImage needs room for 2 scanlines of uint32 pixels with an extra + // pixel in each, plus 2 regular scanlines of bytes. + int bgraScratchSize = this.UsePredictorTransform ? ((width + 1) * 2) + (((width * 2) + 4 - 1) / 4) : 0; + int transformDataSize = this.UsePredictorTransform || this.UseCrossColorTransform ? LosslessUtils.SubSampleSize(width, this.TransformBits) * LosslessUtils.SubSampleSize(height, this.TransformBits) : 0; + + this.BgraScratch = this.memoryAllocator.Allocate(bgraScratchSize); + this.TransformData = this.memoryAllocator.Allocate(transformDataSize); + this.CurrentWidth = width; + } + + /// + /// Clears the backward references. + /// + public void ClearRefs() + { + foreach (Vp8LBackwardRefs refs in this.Refs) + { + refs.Clear(); + } + } + + /// + public void Dispose() + { + this.Bgra.Dispose(); + this.EncodedData.Dispose(); + this.BgraScratch?.Dispose(); + this.Palette.Dispose(); + this.TransformData?.Dispose(); + + foreach (Vp8LBackwardRefs refs in this.Refs) + { + refs.Dispose(); + } + + this.HashChain.Dispose(); + } + + /// + /// Scratch buffer to reduce allocations. + /// + private unsafe struct ScratchBuffer + { + private const int Size = 256; + private fixed int scratch[Size]; + + public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/Vp8LHashChain.cs b/ImageSharp/Formats/Webp/Lossless/Vp8LHashChain.cs new file mode 100644 index 0000000..1d6cac0 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/Vp8LHashChain.cs @@ -0,0 +1,291 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + internal sealed class Vp8LHashChain : IDisposable + { + private const uint HashMultiplierHi = 0xc6a4a793u; + + private const uint HashMultiplierLo = 0x5bd1e996u; + + private const int HashBits = 18; + + private const int HashSize = 1 << HashBits; + + /// + /// The number of bits for the window size. + /// + private const int WindowSizeBits = 20; + + /// + /// 1M window (4M bytes) minus 120 special codes for short distances. + /// + private const int WindowSize = (1 << WindowSizeBits) - 120; + + private readonly MemoryAllocator memoryAllocator; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The size off the chain. + public Vp8LHashChain(MemoryAllocator memoryAllocator, int size) + { + this.memoryAllocator = memoryAllocator; + this.OffsetLength = this.memoryAllocator.Allocate(size, AllocationOptions.Clean); + this.Size = size; + } + + /// + /// Gets the offset length. + /// The 20 most significant bits contain the offset at which the best match is found. + /// These 20 bits are the limit defined by GetWindowSizeForHashChain (through WindowSize = 1 << 20). + /// The lower 12 bits contain the length of the match. + /// + public IMemoryOwner OffsetLength { get; } + + /// + /// Gets the size of the hash chain. + /// This is the maximum size of the hashchain that can be constructed. + /// Typically this is the pixel count (width x height) for a given image. + /// + public int Size { get; } + + public void Fill(ReadOnlySpan bgra, uint quality, int xSize, int ySize, bool lowEffort) + { + int size = xSize * ySize; + int iterMax = GetMaxItersForQuality(quality); + int windowSize = GetWindowSizeForHashChain(quality, xSize); + int pos; + + if (size <= 2) + { + this.OffsetLength.GetSpan()[0] = 0; + return; + } + + using IMemoryOwner hashToFirstIndexBuffer = this.memoryAllocator.Allocate(HashSize); + using IMemoryOwner chainBuffer = this.memoryAllocator.Allocate(size, AllocationOptions.Clean); + Span hashToFirstIndex = hashToFirstIndexBuffer.GetSpan(); + Span chain = chainBuffer.GetSpan(); + + // Initialize hashToFirstIndex array to -1. + hashToFirstIndex.Fill(-1); + + // Fill the chain linking pixels with the same hash. + bool bgraComp = bgra.Length > 1 && bgra[0] == bgra[1]; + Span tmp = stackalloc uint[2]; + for (pos = 0; pos < size - 2;) + { + uint hashCode; + bool bgraCompNext = bgra[pos + 1] == bgra[pos + 2]; + if (bgraComp && bgraCompNext) + { + // Consecutive pixels with the same color will share the same hash. + // We therefore use a different hash: the color and its repetition length. + tmp.Clear(); + uint len = 1; + tmp[0] = bgra[pos]; + + // Figure out how far the pixels are the same. The last pixel has a different 64 bit hash, + // as its next pixel does not have the same color, so we just need to get to + // the last pixel equal to its follower. + while (pos + (int)len + 2 < size && bgra[(int)(pos + len + 2)] == bgra[pos]) + { + ++len; + } + + if (len > BackwardReferenceEncoder.MaxLength) + { + // Skip the pixels that match for distance=1 and length>MaxLength + // because they are linked to their predecessor and we automatically + // check that in the main for loop below. Skipping means setting no + // predecessor in the chain, hence -1. + pos += (int)(len - BackwardReferenceEncoder.MaxLength); + len = BackwardReferenceEncoder.MaxLength; + } + + // Process the rest of the hash chain. + while (len > 0) + { + tmp[1] = len--; + hashCode = GetPixPairHash64(tmp); + chain[pos] = hashToFirstIndex[(int)hashCode]; + hashToFirstIndex[(int)hashCode] = pos++; + } + + bgraComp = false; + } + else + { + // Just move one pixel forward. + hashCode = GetPixPairHash64(bgra[pos..]); + chain[pos] = hashToFirstIndex[(int)hashCode]; + hashToFirstIndex[(int)hashCode] = pos++; + bgraComp = bgraCompNext; + } + } + + // Process the penultimate pixel. + chain[pos] = hashToFirstIndex[(int)GetPixPairHash64(bgra[pos..])]; + + // Find the best match interval at each pixel, defined by an offset to the + // pixel and a length. The right-most pixel cannot match anything to the right + // (hence a best length of 0) and the left-most pixel nothing to the left (hence an offset of 0). + Span offsetLength = this.OffsetLength.GetSpan(); + offsetLength[0] = offsetLength[size - 1] = 0; + for (int basePosition = size - 2; basePosition > 0;) + { + int maxLen = LosslessUtils.MaxFindCopyLength(size - 1 - basePosition); + int bgraStart = basePosition; + int iter = iterMax; + int bestLength = 0; + uint bestDistance = 0; + int minPos = basePosition > windowSize ? basePosition - windowSize : 0; + int lengthMax = maxLen < 256 ? maxLen : 256; + pos = chain[basePosition]; + int currLength; + + if (!lowEffort) + { + // Heuristic: use the comparison with the above line as an initialization. + if (basePosition >= (uint)xSize) + { + currLength = LosslessUtils.FindMatchLength(bgra[(bgraStart - xSize)..], bgra[bgraStart..], bestLength, maxLen); + if (currLength > bestLength) + { + bestLength = currLength; + bestDistance = (uint)xSize; + } + + iter--; + } + + // Heuristic: compare to the previous pixel. + currLength = LosslessUtils.FindMatchLength(bgra[(bgraStart - 1)..], bgra[bgraStart..], bestLength, maxLen); + if (currLength > bestLength) + { + bestLength = currLength; + bestDistance = 1; + } + + iter--; + + // Skip the for loop if we already have the maximum. + if (bestLength == BackwardReferenceEncoder.MaxLength) + { + pos = minPos - 1; + } + } + + uint bestBgra = bgra[bgraStart..][bestLength]; + + for (; pos >= minPos && (--iter > 0); pos = chain[pos]) + { + if (bgra[pos + bestLength] != bestBgra) + { + continue; + } + + currLength = LosslessUtils.VectorMismatch(bgra[pos..], bgra[bgraStart..], maxLen); + if (bestLength < currLength) + { + bestLength = currLength; + bestDistance = (uint)(basePosition - pos); + bestBgra = bgra[bgraStart..][bestLength]; + + // Stop if we have reached a good enough length. + if (bestLength >= lengthMax) + { + break; + } + } + } + + // We have the best match but in case the two intervals continue matching + // to the left, we have the best matches for the left-extended pixels. + uint maxBasePosition = (uint)basePosition; + while (true) + { + offsetLength[basePosition] = (bestDistance << BackwardReferenceEncoder.MaxLengthBits) | (uint)bestLength; + --basePosition; + + // Stop if we don't have a match or if we are out of bounds. + if (bestDistance == 0 || basePosition == 0) + { + break; + } + + // Stop if we cannot extend the matching intervals to the left. + if (basePosition < bestDistance || bgra[(int)(basePosition - bestDistance)] != bgra[basePosition]) + { + break; + } + + // Stop if we are matching at its limit because there could be a closer + // matching interval with the same maximum length. Then again, if the + // matching interval is as close as possible (best_distance == 1), we will + // never find anything better so let's continue. + if (bestLength == BackwardReferenceEncoder.MaxLength && bestDistance != 1 && basePosition + BackwardReferenceEncoder.MaxLength < maxBasePosition) + { + break; + } + + if (bestLength < BackwardReferenceEncoder.MaxLength) + { + bestLength++; + maxBasePosition = (uint)basePosition; + } + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public int FindLength(int basePosition) => (int)(this.OffsetLength.GetSpan()[basePosition] & ((1U << BackwardReferenceEncoder.MaxLengthBits) - 1)); + + [MethodImpl(InliningOptions.ShortMethod)] + public int FindOffset(int basePosition) => (int)(this.OffsetLength.GetSpan()[basePosition] >> BackwardReferenceEncoder.MaxLengthBits); + + /// + /// Calculates the hash for a pixel pair. + /// + /// An Span with two pixels. + /// The hash. + [MethodImpl(InliningOptions.ShortMethod)] + private static uint GetPixPairHash64(ReadOnlySpan bgra) + { + uint key = bgra[1] * HashMultiplierHi; + key += bgra[0] * HashMultiplierLo; + key >>= 32 - HashBits; + return key; + } + + /// + /// Returns the maximum number of hash chain lookups to do for a + /// given compression quality. Return value in range [8, 86]. + /// + /// The quality. + /// Number of hash chain lookups. + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetMaxItersForQuality(uint quality) => (int)(8 + (quality * quality / 128)); + + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetWindowSizeForHashChain(uint quality, int xSize) + { + int maxWindowSize = quality > 75u ? WindowSize + : quality > 50u ? xSize << 8 + : quality > 25u ? xSize << 6 + : xSize << 4; + + return maxWindowSize > WindowSize ? WindowSize : maxWindowSize; + } + + /// + public void Dispose() => this.OffsetLength.Dispose(); + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs b/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs new file mode 100644 index 0000000..51ac293 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs @@ -0,0 +1,642 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + internal abstract unsafe class Vp8LHistogram + { + private const uint NonTrivialSym = 0xffffffff; + private readonly uint* red; + private readonly uint* blue; + private readonly uint* alpha; + private readonly uint* distance; + private readonly uint* literal; + private readonly uint* isUsed; + + private const int RedSize = WebpConstants.NumLiteralCodes; + private const int BlueSize = WebpConstants.NumLiteralCodes; + private const int AlphaSize = WebpConstants.NumLiteralCodes; + private const int DistanceSize = WebpConstants.NumDistanceCodes; + public const int LiteralSize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (1 << WebpConstants.MaxColorCacheBits) + 1; + private const int UsedSize = 5; // 5 for literal, red, blue, alpha, distance + public const int BufferSize = RedSize + BlueSize + AlphaSize + DistanceSize + LiteralSize + UsedSize; + + /// + /// Initializes a new instance of the class. + /// + /// The base pointer to the backing memory. + /// The backward references to initialize the histogram with. + /// The palette code bits. + protected Vp8LHistogram(uint* basePointer, Vp8LBackwardRefs refs, int paletteCodeBits) + : this(basePointer, paletteCodeBits) => this.StoreRefs(refs); + + /// + /// Initializes a new instance of the class. + /// + /// The base pointer to the backing memory. + /// The palette code bits. + protected Vp8LHistogram(uint* basePointer, int paletteCodeBits) + { + this.PaletteCodeBits = paletteCodeBits; + this.red = basePointer; + this.blue = this.red + RedSize; + this.alpha = this.blue + BlueSize; + this.distance = this.alpha + AlphaSize; + this.literal = this.distance + DistanceSize; + this.isUsed = this.literal + LiteralSize; + } + + /// + /// Gets or sets the palette code bits. + /// + public int PaletteCodeBits { get; set; } + + /// + /// Gets or sets the cached value of bit cost. + /// + public double BitCost { get; set; } + + /// + /// Gets or sets the cached value of literal entropy costs. + /// + public double LiteralCost { get; set; } + + /// + /// Gets or sets the cached value of red entropy costs. + /// + public double RedCost { get; set; } + + /// + /// Gets or sets the cached value of blue entropy costs. + /// + public double BlueCost { get; set; } + + public Span Red => new(this.red, RedSize); + + public Span Blue => new(this.blue, BlueSize); + + public Span Alpha => new(this.alpha, AlphaSize); + + public Span Distance => new(this.distance, DistanceSize); + + public Span Literal => new(this.literal, LiteralSize); + + public uint TrivialSymbol { get; set; } + + private Span IsUsedSpan => new(this.isUsed, UsedSize); + + private Span TotalSpan => new(this.red, BufferSize); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsUsed(int index) => this.IsUsedSpan[index] == 1u; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void IsUsed(int index, bool value) => this.IsUsedSpan[index] = value ? 1u : 0; + + /// + /// Creates a copy of the given class. + /// + /// The histogram to copy to. + public void CopyTo(Vp8LHistogram other) + { + this.Red.CopyTo(other.Red); + this.Blue.CopyTo(other.Blue); + this.Alpha.CopyTo(other.Alpha); + this.Literal.CopyTo(other.Literal); + this.Distance.CopyTo(other.Distance); + this.IsUsedSpan.CopyTo(other.IsUsedSpan); + + other.LiteralCost = this.LiteralCost; + other.RedCost = this.RedCost; + other.BlueCost = this.BlueCost; + other.BitCost = this.BitCost; + other.TrivialSymbol = this.TrivialSymbol; + other.PaletteCodeBits = this.PaletteCodeBits; + } + + public void Clear() + { + this.TotalSpan.Clear(); + this.PaletteCodeBits = 0; + this.BitCost = 0; + this.LiteralCost = 0; + this.RedCost = 0; + this.BlueCost = 0; + this.TrivialSymbol = 0; + } + + /// + /// Collect all the references into a histogram (without reset). + /// + /// The backward references. + public void StoreRefs(Vp8LBackwardRefs refs) + { + foreach (PixOrCopy v in refs) + { + this.AddSinglePixOrCopy(in v, false); + } + } + + /// + /// Accumulate a token 'v' into a histogram. + /// + /// The token to add. + /// Indicates whether to use the distance modifier. + /// xSize is only used when useDistanceModifier is true. + public void AddSinglePixOrCopy(in PixOrCopy v, bool useDistanceModifier, int xSize = 0) + { + if (v.IsLiteral()) + { + this.Alpha[v.Literal(3)]++; + this.Red[v.Literal(2)]++; + this.Literal[v.Literal(1)]++; + this.Blue[v.Literal(0)]++; + } + else if (v.IsCacheIdx()) + { + int literalIx = (int)(WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + v.CacheIdx()); + this.Literal[literalIx]++; + } + else + { + int extraBits = 0; + int code = LosslessUtils.PrefixEncodeBits(v.Length(), ref extraBits); + this.Literal[WebpConstants.NumLiteralCodes + code]++; + if (!useDistanceModifier) + { + code = LosslessUtils.PrefixEncodeBits((int)v.Distance(), ref extraBits); + } + else + { + code = LosslessUtils.PrefixEncodeBits(BackwardReferenceEncoder.DistanceToPlaneCode(xSize, (int)v.Distance()), ref extraBits); + } + + this.Distance[code]++; + } + } + + public int NumCodes() => WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (this.PaletteCodeBits > 0 ? 1 << this.PaletteCodeBits : 0); + + /// + /// Estimate how many bits the combined entropy of literals and distance approximately maps to. + /// + /// Estimated bits. + public double EstimateBits(Vp8LStreaks stats, Vp8LBitEntropy bitsEntropy) + { + uint notUsed = 0; + return + this.PopulationCost(this.Literal, this.NumCodes(), ref notUsed, 0, stats, bitsEntropy) + + this.PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref notUsed, 1, stats, bitsEntropy) + + this.PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref notUsed, 2, stats, bitsEntropy) + + this.PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref notUsed, 3, stats, bitsEntropy) + + this.PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, 4, stats, bitsEntropy) + + ExtraCost(this.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes) + + ExtraCost(this.Distance, WebpConstants.NumDistanceCodes); + } + + public void UpdateHistogramCost(Vp8LStreaks stats, Vp8LBitEntropy bitsEntropy) + { + uint alphaSym = 0, redSym = 0, blueSym = 0; + uint notUsed = 0; + + double alphaCost = this.PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref alphaSym, 3, stats, bitsEntropy); + double distanceCost = this.PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, 4, stats, bitsEntropy) + ExtraCost(this.Distance, WebpConstants.NumDistanceCodes); + int numCodes = this.NumCodes(); + this.LiteralCost = this.PopulationCost(this.Literal, numCodes, ref notUsed, 0, stats, bitsEntropy) + ExtraCost(this.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes); + this.RedCost = this.PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref redSym, 1, stats, bitsEntropy); + this.BlueCost = this.PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref blueSym, 2, stats, bitsEntropy); + this.BitCost = this.LiteralCost + this.RedCost + this.BlueCost + alphaCost + distanceCost; + if ((alphaSym | redSym | blueSym) == NonTrivialSym) + { + this.TrivialSymbol = NonTrivialSym; + } + else + { + this.TrivialSymbol = (alphaSym << 24) | (redSym << 16) | (blueSym << 0); + } + } + + /// + /// Performs output = a + b, computing the cost C(a+b) - C(a) - C(b) while comparing + /// to the threshold value 'costThreshold'. The score returned is + /// Score = C(a+b) - C(a) - C(b), where C(a) + C(b) is known and fixed. + /// Since the previous score passed is 'costThreshold', we only need to compare + /// the partial cost against 'costThreshold + C(a) + C(b)' to possibly bail-out early. + /// + public double AddEval(Vp8LHistogram b, Vp8LStreaks stats, Vp8LBitEntropy bitsEntropy, double costThreshold, Vp8LHistogram output) + { + double sumCost = this.BitCost + b.BitCost; + costThreshold += sumCost; + if (this.GetCombinedHistogramEntropy(b, stats, bitsEntropy, costThreshold, costInitial: 0, out double cost)) + { + this.Add(b, output); + output.BitCost = cost; + output.PaletteCodeBits = this.PaletteCodeBits; + } + + return cost - sumCost; + } + + public double AddThresh(Vp8LHistogram b, Vp8LStreaks stats, Vp8LBitEntropy bitsEntropy, double costThreshold) + { + double costInitial = -this.BitCost; + this.GetCombinedHistogramEntropy(b, stats, bitsEntropy, costThreshold, costInitial, out double cost); + return cost; + } + + public void Add(Vp8LHistogram b, Vp8LHistogram output) + { + int literalSize = this.NumCodes(); + + this.AddLiteral(b, output, literalSize); + this.AddRed(b, output, WebpConstants.NumLiteralCodes); + this.AddBlue(b, output, WebpConstants.NumLiteralCodes); + this.AddAlpha(b, output, WebpConstants.NumLiteralCodes); + this.AddDistance(b, output, WebpConstants.NumDistanceCodes); + + for (int i = 0; i < 5; i++) + { + output.IsUsed(i, this.IsUsed(i) | b.IsUsed(i)); + } + + output.TrivialSymbol = this.TrivialSymbol == b.TrivialSymbol + ? this.TrivialSymbol + : NonTrivialSym; + } + + public bool GetCombinedHistogramEntropy(Vp8LHistogram b, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy, double costThreshold, double costInitial, out double cost) + { + bool trivialAtEnd = false; + cost = costInitial; + + cost += GetCombinedEntropy(this.Literal, b.Literal, this.NumCodes(), this.IsUsed(0), b.IsUsed(0), false, stats, bitEntropy); + + cost += ExtraCostCombined(this.Literal[WebpConstants.NumLiteralCodes..], b.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes); + + if (cost > costThreshold) + { + return false; + } + + if (this.TrivialSymbol != NonTrivialSym && this.TrivialSymbol == b.TrivialSymbol) + { + // A, R and B are all 0 or 0xff. + uint colorA = (this.TrivialSymbol >> 24) & 0xff; + uint colorR = (this.TrivialSymbol >> 16) & 0xff; + uint colorB = (this.TrivialSymbol >> 0) & 0xff; + if ((colorA == 0 || colorA == 0xff) && + (colorR == 0 || colorR == 0xff) && + (colorB == 0 || colorB == 0xff)) + { + trivialAtEnd = true; + } + } + + cost += GetCombinedEntropy(this.Red, b.Red, WebpConstants.NumLiteralCodes, this.IsUsed(1), b.IsUsed(1), trivialAtEnd, stats, bitEntropy); + if (cost > costThreshold) + { + return false; + } + + cost += GetCombinedEntropy(this.Blue, b.Blue, WebpConstants.NumLiteralCodes, this.IsUsed(2), b.IsUsed(2), trivialAtEnd, stats, bitEntropy); + if (cost > costThreshold) + { + return false; + } + + cost += GetCombinedEntropy(this.Alpha, b.Alpha, WebpConstants.NumLiteralCodes, this.IsUsed(3), b.IsUsed(3), trivialAtEnd, stats, bitEntropy); + if (cost > costThreshold) + { + return false; + } + + cost += GetCombinedEntropy(this.Distance, b.Distance, WebpConstants.NumDistanceCodes, this.IsUsed(4), b.IsUsed(4), false, stats, bitEntropy); + if (cost > costThreshold) + { + return false; + } + + cost += ExtraCostCombined(this.Distance, b.Distance, WebpConstants.NumDistanceCodes); + return cost <= costThreshold; + } + + private void AddLiteral(Vp8LHistogram b, Vp8LHistogram output, int literalSize) + { + if (this.IsUsed(0)) + { + if (b.IsUsed(0)) + { + AddVector(this.Literal, b.Literal, output.Literal, literalSize); + } + else + { + this.Literal[..literalSize].CopyTo(output.Literal); + } + } + else if (b.IsUsed(0)) + { + b.Literal[..literalSize].CopyTo(output.Literal); + } + else + { + output.Literal[..literalSize].Clear(); + } + } + + private void AddRed(Vp8LHistogram b, Vp8LHistogram output, int size) + { + if (this.IsUsed(1)) + { + if (b.IsUsed(1)) + { + AddVector(this.Red, b.Red, output.Red, size); + } + else + { + this.Red[..size].CopyTo(output.Red); + } + } + else if (b.IsUsed(1)) + { + b.Red[..size].CopyTo(output.Red); + } + else + { + output.Red[..size].Clear(); + } + } + + private void AddBlue(Vp8LHistogram b, Vp8LHistogram output, int size) + { + if (this.IsUsed(2)) + { + if (b.IsUsed(2)) + { + AddVector(this.Blue, b.Blue, output.Blue, size); + } + else + { + this.Blue[..size].CopyTo(output.Blue); + } + } + else if (b.IsUsed(2)) + { + b.Blue[..size].CopyTo(output.Blue); + } + else + { + output.Blue[..size].Clear(); + } + } + + private void AddAlpha(Vp8LHistogram b, Vp8LHistogram output, int size) + { + if (this.IsUsed(3)) + { + if (b.IsUsed(3)) + { + AddVector(this.Alpha, b.Alpha, output.Alpha, size); + } + else + { + this.Alpha[..size].CopyTo(output.Alpha); + } + } + else if (b.IsUsed(3)) + { + b.Alpha[..size].CopyTo(output.Alpha); + } + else + { + output.Alpha[..size].Clear(); + } + } + + private void AddDistance(Vp8LHistogram b, Vp8LHistogram output, int size) + { + if (this.IsUsed(4)) + { + if (b.IsUsed(4)) + { + AddVector(this.Distance, b.Distance, output.Distance, size); + } + else + { + this.Distance[..size].CopyTo(output.Distance); + } + } + else if (b.IsUsed(4)) + { + b.Distance[..size].CopyTo(output.Distance); + } + else + { + output.Distance[..size].Clear(); + } + } + + private static double GetCombinedEntropy( + Span x, + Span y, + int length, + bool isXUsed, + bool isYUsed, + bool trivialAtEnd, + Vp8LStreaks stats, + Vp8LBitEntropy bitEntropy) + { + stats.Clear(); + bitEntropy.Init(); + if (trivialAtEnd) + { + // This configuration is due to palettization that transforms an indexed + // pixel into 0xff000000 | (pixel << 8) in BundleColorMap. + // BitsEntropyRefine is 0 for histograms with only one non-zero value. + // Only FinalHuffmanCost needs to be evaluated. + + // Deal with the non-zero value at index 0 or length-1. + stats.Streaks[1][0] = 1; + + // Deal with the following/previous zero streak. + stats.Counts[0] = 1; + stats.Streaks[0][1] = length - 1; + + return stats.FinalHuffmanCost(); + } + + if (isXUsed) + { + if (isYUsed) + { + bitEntropy.GetCombinedEntropyUnrefined(x, y, length, stats); + } + else + { + bitEntropy.GetEntropyUnrefined(x, length, stats); + } + } + else if (isYUsed) + { + bitEntropy.GetEntropyUnrefined(y, length, stats); + } + else + { + stats.Counts[0] = 1; + stats.Streaks[0][length > 3 ? 1 : 0] = length; + bitEntropy.Init(); + } + + return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost(); + } + + private static double ExtraCostCombined(Span x, Span y, int length) + { + double cost = 0.0d; + for (int i = 2; i < length - 2; i++) + { + int xy = (int)(x[i + 2] + y[i + 2]); + cost += (i >> 1) * xy; + } + + return cost; + } + + /// + /// Get the symbol entropy for the distribution 'population'. + /// + private double PopulationCost(Span population, int length, ref uint trivialSym, int isUsedIndex, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy) + { + bitEntropy.Init(); + stats.Clear(); + bitEntropy.BitsEntropyUnrefined(population, length, stats); + + trivialSym = (bitEntropy.NoneZeros == 1) ? bitEntropy.NoneZeroCode : NonTrivialSym; + + // The histogram is used if there is at least one non-zero streak. + this.IsUsed(isUsedIndex, stats.Streaks[1][0] != 0 || stats.Streaks[1][1] != 0); + + return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost(); + } + + private static double ExtraCost(Span population, int length) + { + double cost = 0.0d; + for (int i = 2; i < length - 2; i++) + { + cost += (i >> 1) * population[i + 2]; + } + + return cost; + } + + private static void AddVector(Span a, Span b, Span output, int count) + { + DebugGuard.MustBeGreaterThanOrEqualTo(a.Length, count, nameof(a.Length)); + DebugGuard.MustBeGreaterThanOrEqualTo(b.Length, count, nameof(b.Length)); + DebugGuard.MustBeGreaterThanOrEqualTo(output.Length, count, nameof(output.Length)); + + if (Avx2.IsSupported && count >= 32) + { + ref uint aRef = ref MemoryMarshal.GetReference(a); + ref uint bRef = ref MemoryMarshal.GetReference(b); + ref uint outputRef = ref MemoryMarshal.GetReference(output); + + nuint idx = 0; + do + { + // Load values. + Vector256 a0 = Unsafe.As>(ref Unsafe.Add(ref aRef, idx + 0)); + Vector256 a1 = Unsafe.As>(ref Unsafe.Add(ref aRef, idx + 8)); + Vector256 a2 = Unsafe.As>(ref Unsafe.Add(ref aRef, idx + 16)); + Vector256 a3 = Unsafe.As>(ref Unsafe.Add(ref aRef, idx + 24)); + Vector256 b0 = Unsafe.As>(ref Unsafe.Add(ref bRef, idx + 0)); + Vector256 b1 = Unsafe.As>(ref Unsafe.Add(ref bRef, idx + 8)); + Vector256 b2 = Unsafe.As>(ref Unsafe.Add(ref bRef, idx + 16)); + Vector256 b3 = Unsafe.As>(ref Unsafe.Add(ref bRef, idx + 24)); + + // Note we are adding uint32_t's as *signed* int32's (using _mm_add_epi32). But + // that's ok since the histogram values are less than 1<<28 (max picture count). + Unsafe.As>(ref Unsafe.Add(ref outputRef, idx + 0)) = Avx2.Add(a0, b0); + Unsafe.As>(ref Unsafe.Add(ref outputRef, idx + 8)) = Avx2.Add(a1, b1); + Unsafe.As>(ref Unsafe.Add(ref outputRef, idx + 16)) = Avx2.Add(a2, b2); + Unsafe.As>(ref Unsafe.Add(ref outputRef, idx + 24)) = Avx2.Add(a3, b3); + idx += 32; + } + while (idx <= (uint)count - 32); + + int i = (int)idx; + for (; i < count; i++) + { + output[i] = a[i] + b[i]; + } + } + else + { + for (int i = 0; i < count; i++) + { + output[i] = a[i] + b[i]; + } + } + } + } + + internal sealed unsafe class OwnedVp8LHistogram : Vp8LHistogram, IDisposable + { + private readonly IMemoryOwner bufferOwner; + private MemoryHandle bufferHandle; + private bool isDisposed; + + private OwnedVp8LHistogram( + IMemoryOwner bufferOwner, + ref MemoryHandle bufferHandle, + uint* basePointer, + int paletteCodeBits) + : base(basePointer, paletteCodeBits) + { + this.bufferOwner = bufferOwner; + this.bufferHandle = bufferHandle; + } + + /// + /// Creates an that is not a member of a . + /// + /// The memory allocator. + /// The palette code bits. + public static OwnedVp8LHistogram Create(MemoryAllocator memoryAllocator, int paletteCodeBits) + { + IMemoryOwner bufferOwner = memoryAllocator.Allocate(BufferSize, AllocationOptions.Clean); + MemoryHandle bufferHandle = bufferOwner.Memory.Pin(); + return new OwnedVp8LHistogram(bufferOwner, ref bufferHandle, (uint*)bufferHandle.Pointer, paletteCodeBits); + } + + /// + /// Creates an that is not a member of a . + /// + /// The memory allocator. + /// The backward references to initialize the histogram with. + /// The palette code bits. + public static OwnedVp8LHistogram Create(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int paletteCodeBits) + { + OwnedVp8LHistogram histogram = Create(memoryAllocator, paletteCodeBits); + histogram.StoreRefs(refs); + return histogram; + } + + public void Dispose() + { + if (!this.isDisposed) + { + this.bufferHandle.Dispose(); + this.bufferOwner.Dispose(); + this.isDisposed = true; + } + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs b/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs new file mode 100644 index 0000000..46715d2 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs @@ -0,0 +1,112 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#nullable disable + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable + { + private readonly IMemoryOwner buffer; + private MemoryHandle bufferHandle; + private readonly List items; + private bool isDisposed; + + public Vp8LHistogramSet(MemoryAllocator memoryAllocator, int capacity, int cacheBits) + { + this.buffer = memoryAllocator.Allocate(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean); + this.bufferHandle = this.buffer.Memory.Pin(); + + unsafe + { + uint* basePointer = (uint*)this.bufferHandle.Pointer; + this.items = new List(capacity); + for (int i = 0; i < capacity; i++) + { + this.items.Add(new MemberVp8LHistogram(basePointer + (Vp8LHistogram.BufferSize * i), cacheBits)); + } + } + } + + public Vp8LHistogramSet(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int capacity, int cacheBits) + { + this.buffer = memoryAllocator.Allocate(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean); + this.bufferHandle = this.buffer.Memory.Pin(); + + unsafe + { + uint* basePointer = (uint*)this.bufferHandle.Pointer; + this.items = new List(capacity); + for (int i = 0; i < capacity; i++) + { + this.items.Add(new MemberVp8LHistogram(basePointer + (Vp8LHistogram.BufferSize * i), refs, cacheBits)); + } + } + } + + public Vp8LHistogramSet(int capacity) => this.items = new List(capacity); + + public Vp8LHistogramSet() => this.items = new List(); + + public int Count => this.items.Count; + + public Vp8LHistogram this[int index] + { + get => this.items[index]; + set => this.items[index] = value; + } + + public void RemoveAt(int index) + { + this.CheckDisposed(); + this.items.RemoveAt(index); + } + + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.buffer.Dispose(); + this.bufferHandle.Dispose(); + this.items.Clear(); + this.isDisposed = true; + } + + public IEnumerator GetEnumerator() => ((IEnumerable)this.items).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this.items).GetEnumerator(); + + [Conditional("DEBUG")] + private void CheckDisposed() + { + if (this.isDisposed) + { + ThrowDisposed(); + } + } + + private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(Vp8LHistogramSet)); + + private sealed unsafe class MemberVp8LHistogram : Vp8LHistogram + { + public MemberVp8LHistogram(uint* basePointer, int paletteCodeBits) + : base(basePointer, paletteCodeBits) + { + } + + public MemberVp8LHistogram(uint* basePointer, Vp8LBackwardRefs refs, int paletteCodeBits) + : base(basePointer, refs, paletteCodeBits) + { + } + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/Vp8LLz77Type.cs b/ImageSharp/Formats/Webp/Lossless/Vp8LLz77Type.cs new file mode 100644 index 0000000..582ce80 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/Vp8LLz77Type.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + internal enum Vp8LLz77Type + { + Lz77Standard = 1, + + Lz77Rle = 2, + + Lz77Box = 4 + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/Vp8LMetadata.cs b/ImageSharp/Formats/Webp/Lossless/Vp8LMetadata.cs new file mode 100644 index 0000000..ee8f6c3 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/Vp8LMetadata.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System.Buffers; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + internal class Vp8LMetadata + { + public int ColorCacheSize { get; set; } + + public ColorCache ColorCache { get; set; } + + public int HuffmanMask { get; set; } + + public int HuffmanSubSampleBits { get; set; } + + public int HuffmanXSize { get; set; } + + public IMemoryOwner HuffmanImage { get; set; } + + public int NumHTreeGroups { get; set; } + + public HTreeGroup[] HTreeGroups { get; set; } + + public HuffmanCode[] HuffmanTables { get; set; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/Vp8LMultipliers.cs b/ImageSharp/Formats/Webp/Lossless/Vp8LMultipliers.cs new file mode 100644 index 0000000..099a502 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/Vp8LMultipliers.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + internal struct Vp8LMultipliers + { + public byte GreenToRed; + + public byte GreenToBlue; + + public byte RedToBlue; + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/Vp8LStreaks.cs b/ImageSharp/Formats/Webp/Lossless/Vp8LStreaks.cs new file mode 100644 index 0000000..d16051c --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/Vp8LStreaks.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + internal class Vp8LStreaks + { + /// + /// Initializes a new instance of the class. + /// + public Vp8LStreaks() + { + this.Counts = new int[2]; + this.Streaks = new int[2][]; + this.Streaks[0] = new int[2]; + this.Streaks[1] = new int[2]; + } + + /// + /// Gets the streak count. + /// index: 0=zero streak, 1=non-zero streak. + /// + public int[] Counts { get; } + + /// + /// Gets the streaks. + /// [zero/non-zero][streak < 3 / streak >= 3]. + /// + public int[][] Streaks { get; } + + public void Clear() + { + this.Counts.AsSpan().Clear(); + this.Streaks[0].AsSpan().Clear(); + this.Streaks[1].AsSpan().Clear(); + } + + public double FinalHuffmanCost() + { + // The constants in this function are experimental and got rounded from + // their original values in 1/8 when switched to 1/1024. + double retval = InitialHuffmanCost(); + + // Second coefficient: Many zeros in the histogram are covered efficiently + // by a run-length encode. Originally 2/8. + retval += (this.Counts[0] * 1.5625) + (0.234375 * this.Streaks[0][1]); + + // Second coefficient: Constant values are encoded less efficiently, but still + // RLE'ed. Originally 6/8. + retval += (this.Counts[1] * 2.578125) + (0.703125 * this.Streaks[1][1]); + + // 0s are usually encoded more efficiently than non-0s. + // Originally 15/8. + retval += 1.796875 * this.Streaks[0][0]; + + // Originally 26/8. + retval += 3.28125 * this.Streaks[1][0]; + + return retval; + } + + private static double InitialHuffmanCost() + { + // Small bias because Huffman code length is typically not stored in full length. + int huffmanCodeOfHuffmanCodeSize = WebpConstants.CodeLengthCodes * 3; + double smallBias = 9.1; + return huffmanCodeOfHuffmanCodeSize - smallBias; + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/Vp8LTransform.cs b/ImageSharp/Formats/Webp/Lossless/Vp8LTransform.cs new file mode 100644 index 0000000..3ed8ffe --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/Vp8LTransform.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System.Buffers; +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// Data associated with a VP8L transformation to reduce the entropy. + /// + [DebuggerDisplay("Transformtype: {" + nameof(TransformType) + "}")] + internal class Vp8LTransform + { + public Vp8LTransform(Vp8LTransformType transformType, int xSize, int ySize) + { + this.TransformType = transformType; + this.XSize = xSize; + this.YSize = ySize; + } + + /// + /// Gets the transform type. + /// + public Vp8LTransformType TransformType { get; } + + /// + /// Gets or sets the subsampling bits defining the transform window. + /// + public int Bits { get; set; } + + /// + /// Gets or sets the transform window X index. + /// + public int XSize { get; set; } + + /// + /// Gets the transform window Y index. + /// + public int YSize { get; } + + /// + /// Gets or sets the transform data. + /// + public IMemoryOwner Data { get; set; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/Vp8LTransformType.cs b/ImageSharp/Formats/Webp/Lossless/Vp8LTransformType.cs new file mode 100644 index 0000000..a266efa --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/Vp8LTransformType.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// Enum for the different transform types. Transformations are reversible manipulations of the image data + /// that can reduce the remaining symbolic entropy by modeling spatial and color correlations. + /// Transformations can make the final compression more dense. + /// + internal enum Vp8LTransformType : uint + { + /// + /// The predictor transform can be used to reduce entropy by exploiting the fact that neighboring pixels are often correlated. + /// + PredictorTransform = 0, + + /// + /// The goal of the color transform is to de-correlate the R, G and B values of each pixel. + /// Color transform keeps the green (G) value as it is, transforms red (R) based on green and transforms blue (B) based on green and then based on red. + /// + CrossColorTransform = 1, + + /// + /// The subtract green transform subtracts green values from red and blue values of each pixel. + /// When this transform is present, the decoder needs to add the green value to both red and blue. + /// There is no data associated with this transform. + /// + SubtractGreen = 2, + + /// + /// If there are not many unique pixel values, it may be more efficient to create a color index array and replace the pixel values by the array's indices. + /// The color indexing transform achieves this. + /// + ColorIndexingTransform = 3, + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs b/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs new file mode 100644 index 0000000..01f9735 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs @@ -0,0 +1,1015 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Webp.BitReader; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless { + /// + /// Decoder for lossless webp images. This code is a port of libwebp, which can be found here: https://chromium.googlesource.com/webm/libwebp + /// + /// + /// The lossless specification can be found here: + /// https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification + /// + internal sealed class WebpLosslessDecoder + { + /// + /// A bit reader for reading lossless webp streams. + /// + private readonly Vp8LBitReader bitReader; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + private const int BitsSpecialMarker = 0x100; + + private const uint PackedNonLiteralCode = 0; + + private static readonly int CodeToPlaneCodes = WebpLookupTables.CodeToPlane.Length; + + // Memory needed for lookup tables of one Huffman tree group. Red, blue, alpha and distance alphabets are constant (256 for red, blue and alpha, 40 for + // distance) and lookup table sizes for them in worst case are 630 and 410 respectively. Size of green alphabet depends on color cache size and is equal + // to 256 (green component values) + 24 (length prefix values) + color_cache_size (between 0 and 2048). + // All values computed for 8-bit first level lookup with Mark Adler's tool: + // http://www.hdfgroup.org/ftp/lib-external/zlib/zlib-1.2.5/examples/enough.c + private const int FixedTableSize = (630 * 3) + 410; + + private static readonly int[] TableSize = + [ + FixedTableSize + 654, + FixedTableSize + 656, + FixedTableSize + 658, + FixedTableSize + 662, + FixedTableSize + 670, + FixedTableSize + 686, + FixedTableSize + 718, + FixedTableSize + 782, + FixedTableSize + 912, + FixedTableSize + 1168, + FixedTableSize + 1680, + FixedTableSize + 2704 + ]; + + private static readonly int NumCodeLengthCodes = CodeLengthCodeOrder.Length; + + /// + /// Initializes a new instance of the class. + /// + /// Bitreader to read from the stream. + /// Used for allocating memory during processing operations. + /// The configuration. + public WebpLosslessDecoder(Vp8LBitReader bitReader, MemoryAllocator memoryAllocator, Configuration configuration) + { + this.bitReader = bitReader; + this.memoryAllocator = memoryAllocator; + this.configuration = configuration; + } + + // This uses C#'s compiler optimization to refer to assembly's static data directly. + private static ReadOnlySpan CodeLengthCodeOrder => [17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 + ]; + + // This uses C#'s compiler optimization to refer to assembly's static data directly. + private static ReadOnlySpan LiteralMap => [0, 1, 1, 1, 0]; + + /// + /// Decodes the lossless webp image from the stream. + /// + /// The pixel format. + /// The pixel buffer to store the decoded data. + /// The width of the image. + /// The height of the image. + public void Decode(Buffer2D pixels, int width, int height) + where TPixel : unmanaged, IPixel + { + using Vp8LDecoder decoder = new(width, height, this.memoryAllocator); + this.DecodeImageStream(decoder, width, height, true); + this.DecodeImageData(decoder, decoder.Pixels.Memory.Span); + this.DecodePixelValues(decoder, pixels, width, height); + } + + public IMemoryOwner DecodeImageStream(Vp8LDecoder decoder, int xSize, int ySize, bool isLevel0) + { + int transformXSize = xSize; + int transformYSize = ySize; + int numberOfTransformsPresent = 0; + if (isLevel0) + { + decoder.Transforms = new List(WebpConstants.MaxNumberOfTransforms); + + // Next bit indicates, if a transformation is present. + while (this.bitReader.ReadBit()) + { + if (numberOfTransformsPresent > WebpConstants.MaxNumberOfTransforms) + { + WebpThrowHelper.ThrowImageFormatException($"The maximum number of transforms of {WebpConstants.MaxNumberOfTransforms} was exceeded"); + } + + this.ReadTransformation(transformXSize, transformYSize, decoder); + if (decoder.Transforms[numberOfTransformsPresent].TransformType == Vp8LTransformType.ColorIndexingTransform) + { + transformXSize = LosslessUtils.SubSampleSize(transformXSize, decoder.Transforms[numberOfTransformsPresent].Bits); + } + + numberOfTransformsPresent++; + } + } + else + { + decoder.Metadata = new Vp8LMetadata(); + } + + // Color cache. + bool isColorCachePresent = this.bitReader.ReadBit(); + int colorCacheBits = 0; + int colorCacheSize = 0; + if (isColorCachePresent) + { + colorCacheBits = (int)this.bitReader.ReadValue(4); + + // Note: According to webpinfo color cache bits of 11 are valid, even though 10 is defined in the source code as maximum. + // That is why 11 bits is also considered valid here. + bool colorCacheBitsIsValid = colorCacheBits is >= 1 and <= WebpConstants.MaxColorCacheBits + 1; + if (!colorCacheBitsIsValid) + { + WebpThrowHelper.ThrowImageFormatException("Invalid color cache bits found"); + } + } + + // Read the Huffman codes (may recurse). + this.ReadHuffmanCodes(decoder, transformXSize, transformYSize, colorCacheBits, isLevel0); + decoder.Metadata.ColorCacheSize = colorCacheSize; + + // Finish setting up the color-cache. + if (isColorCachePresent) + { + decoder.Metadata.ColorCache = new ColorCache(colorCacheBits); + colorCacheSize = 1 << colorCacheBits; + decoder.Metadata.ColorCacheSize = colorCacheSize; + } + else + { + decoder.Metadata.ColorCacheSize = 0; + } + + UpdateDecoder(decoder, transformXSize, transformYSize); + if (isLevel0) + { + // level 0 complete. + return null; + } + + // Use the Huffman trees to decode the LZ77 encoded data. + IMemoryOwner pixelData = this.memoryAllocator.Allocate(decoder.Width * decoder.Height, AllocationOptions.Clean); + this.DecodeImageData(decoder, pixelData.GetSpan()); + + return pixelData; + } + + private void DecodePixelValues(Vp8LDecoder decoder, Buffer2D pixels, int width, int height) + where TPixel : unmanaged, IPixel + { + Span pixelData = decoder.Pixels.GetSpan(); + + // Apply reverse transformations, if any are present. + ApplyInverseTransforms(decoder, pixelData, this.memoryAllocator); + + Span pixelDataAsBytes = MemoryMarshal.Cast(pixelData); + int bytesPerRow = width * 4; + for (int y = 0; y < height; y++) + { + Span rowAsBytes = pixelDataAsBytes.Slice(y * bytesPerRow, bytesPerRow); + Span pixelRow = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.FromBgra32Bytes( + this.configuration, + rowAsBytes[..bytesPerRow], + pixelRow[..width], + width); + } + } + + public void DecodeImageData(Vp8LDecoder decoder, Span pixelData) + { + const int lastPixel = 0; + int width = decoder.Width; + int height = decoder.Height; + int row = lastPixel / width; + int col = lastPixel % width; + const int lenCodeLimit = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes; + int colorCacheSize = decoder.Metadata.ColorCacheSize; + ColorCache colorCache = decoder.Metadata.ColorCache; + int colorCacheLimit = lenCodeLimit + colorCacheSize; + int mask = decoder.Metadata.HuffmanMask; + Span hTreeGroup = GetHTreeGroupForPos(decoder.Metadata, col, row); + + int totalPixels = width * height; + int decodedPixels = 0; + int lastCached = decodedPixels; + while (decodedPixels < totalPixels) + { + int code; + if ((col & mask) == 0) + { + hTreeGroup = GetHTreeGroupForPos(decoder.Metadata, col, row); + } + + if (hTreeGroup[0].IsTrivialCode) + { + pixelData[decodedPixels] = hTreeGroup[0].LiteralArb; + AdvanceByOne(ref col, ref row, width, colorCache, ref decodedPixels, pixelData, ref lastCached); + continue; + } + + this.bitReader.FillBitWindow(); + if (hTreeGroup[0].UsePackedTable) + { + code = (int)this.ReadPackedSymbols(hTreeGroup, pixelData, decodedPixels); + if (this.bitReader.IsEndOfStream()) + { + break; + } + + if (code == PackedNonLiteralCode) + { + AdvanceByOne(ref col, ref row, width, colorCache, ref decodedPixels, pixelData, ref lastCached); + continue; + } + } + else + { + code = (int)this.ReadSymbol(hTreeGroup[0].HTrees[HuffIndex.Green]); + } + + if (this.bitReader.IsEndOfStream()) + { + break; + } + + // Literal + if (code < WebpConstants.NumLiteralCodes) + { + if (hTreeGroup[0].IsTrivialLiteral) + { + pixelData[decodedPixels] = hTreeGroup[0].LiteralArb | ((uint)code << 8); + } + else + { + uint red = this.ReadSymbol(hTreeGroup[0].HTrees[HuffIndex.Red]); + this.bitReader.FillBitWindow(); + uint blue = this.ReadSymbol(hTreeGroup[0].HTrees[HuffIndex.Blue]); + uint alpha = this.ReadSymbol(hTreeGroup[0].HTrees[HuffIndex.Alpha]); + if (this.bitReader.IsEndOfStream()) + { + break; + } + + pixelData[decodedPixels] = (uint)(((byte)alpha << 24) | ((byte)red << 16) | ((byte)code << 8) | (byte)blue); + } + + AdvanceByOne(ref col, ref row, width, colorCache, ref decodedPixels, pixelData, ref lastCached); + } + else if (code < lenCodeLimit) + { + // Backward reference is used. + int lengthSym = code - WebpConstants.NumLiteralCodes; + int length = this.GetCopyLength(lengthSym); + uint distSymbol = this.ReadSymbol(hTreeGroup[0].HTrees[HuffIndex.Dist]); + this.bitReader.FillBitWindow(); + int distCode = this.GetCopyDistance((int)distSymbol); + int dist = PlaneCodeToDistance(width, distCode); + if (this.bitReader.IsEndOfStream()) + { + break; + } + + CopyBlock(pixelData, decodedPixels, dist, length); + decodedPixels += length; + col += length; + while (col >= width) + { + col -= width; + row++; + } + + if ((col & mask) != 0) + { + hTreeGroup = GetHTreeGroupForPos(decoder.Metadata, col, row); + } + + if (colorCache != null) + { + while (lastCached < decodedPixels) + { + colorCache.Insert(pixelData[lastCached]); + lastCached++; + } + } + } + else if (code < colorCacheLimit) + { + // Color cache should be used. + int key = code - lenCodeLimit; + while (lastCached < decodedPixels) + { + colorCache.Insert(pixelData[lastCached]); + lastCached++; + } + + pixelData[decodedPixels] = colorCache.Lookup(key); + AdvanceByOne(ref col, ref row, width, colorCache, ref decodedPixels, pixelData, ref lastCached); + } + else + { + WebpThrowHelper.ThrowImageFormatException("Webp parsing error"); + } + } + } + + private static void AdvanceByOne(ref int col, ref int row, int width, ColorCache colorCache, ref int decodedPixels, Span pixelData, ref int lastCached) + { + col++; + decodedPixels++; + if (col >= width) + { + col = 0; + row++; + + if (colorCache != null) + { + while (lastCached < decodedPixels) + { + colorCache.Insert(pixelData[lastCached]); + lastCached++; + } + } + } + } + + private void ReadHuffmanCodes(Vp8LDecoder decoder, int xSize, int ySize, int colorCacheBits, bool allowRecursion) + { + int maxAlphabetSize = 0; + int numHTreeGroups = 1; + int numHTreeGroupsMax = 1; + + // If the next bit is zero, there is only one meta Huffman code used everywhere in the image. No more data is stored. + // If this bit is one, the image uses multiple meta Huffman codes. These meta Huffman codes are stored as an entropy image. + if (allowRecursion && this.bitReader.ReadBit()) + { + // Use meta Huffman codes. + int huffmanPrecision = (int)(this.bitReader.ReadValue(3) + 2); + int huffmanXSize = LosslessUtils.SubSampleSize(xSize, huffmanPrecision); + int huffmanYSize = LosslessUtils.SubSampleSize(ySize, huffmanPrecision); + int huffmanPixels = huffmanXSize * huffmanYSize; + + IMemoryOwner huffmanImage = this.DecodeImageStream(decoder, huffmanXSize, huffmanYSize, false); + Span huffmanImageSpan = huffmanImage.GetSpan(); + decoder.Metadata.HuffmanSubSampleBits = huffmanPrecision; + + // TODO: Isn't huffmanPixels the length of the span? + for (int i = 0; i < huffmanPixels; i++) + { + // The huffman data is stored in red and green bytes. + uint group = (huffmanImageSpan[i] >> 8) & 0xffff; + huffmanImageSpan[i] = group; + if (group >= numHTreeGroupsMax) + { + numHTreeGroupsMax = (int)group + 1; + } + } + + numHTreeGroups = numHTreeGroupsMax; + decoder.Metadata.HuffmanImage = huffmanImage; + } + + // Find maximum alphabet size for the hTree group. + for (int j = 0; j < WebpConstants.HuffmanCodesPerMetaCode; j++) + { + int alphabetSize = WebpConstants.AlphabetSize[j]; + if (j == 0 && colorCacheBits > 0) + { + alphabetSize += 1 << colorCacheBits; + } + + if (maxAlphabetSize < alphabetSize) + { + maxAlphabetSize = alphabetSize; + } + } + + int tableSize = TableSize[colorCacheBits]; + HuffmanCode[] huffmanTables = new HuffmanCode[numHTreeGroups * tableSize]; + HTreeGroup[] hTreeGroups = new HTreeGroup[numHTreeGroups]; + Span huffmanTable = huffmanTables.AsSpan(); + int[] codeLengths = new int[maxAlphabetSize]; + for (int i = 0; i < numHTreeGroupsMax; i++) + { + hTreeGroups[i] = new HTreeGroup(HuffmanUtils.HuffmanPackedTableSize); + HTreeGroup hTreeGroup = hTreeGroups[i]; + int totalSize = 0; + bool isTrivialLiteral = true; + int maxBits = 0; + codeLengths.AsSpan().Clear(); + for (int j = 0; j < WebpConstants.HuffmanCodesPerMetaCode; j++) + { + int alphabetSize = WebpConstants.AlphabetSize[j]; + if (j == 0 && colorCacheBits > 0) + { + alphabetSize += 1 << colorCacheBits; + } + + int size = this.ReadHuffmanCode(alphabetSize, codeLengths, huffmanTable); + if (size == 0) + { + WebpThrowHelper.ThrowImageFormatException("Huffman table size is zero"); + } + + // TODO: Avoid allocation. + hTreeGroup.HTrees.Add(huffmanTable[..size].ToArray()); + + HuffmanCode huffTableZero = huffmanTable[0]; + if (isTrivialLiteral && LiteralMap[j] == 1) + { + isTrivialLiteral = huffTableZero.BitsUsed == 0; + } + + totalSize += huffTableZero.BitsUsed; + huffmanTable = huffmanTable[size..]; + + if (j <= HuffIndex.Alpha) + { + int localMaxBits = codeLengths[0]; + int k; + for (k = 1; k < alphabetSize; ++k) + { + int codeLengthK = codeLengths[k]; + if (codeLengthK > localMaxBits) + { + localMaxBits = codeLengthK; + } + } + + maxBits += localMaxBits; + } + } + + hTreeGroup.IsTrivialLiteral = isTrivialLiteral; + hTreeGroup.IsTrivialCode = false; + if (isTrivialLiteral) + { + uint red = hTreeGroup.HTrees[HuffIndex.Red][0].Value; + uint blue = hTreeGroup.HTrees[HuffIndex.Blue][0].Value; + uint green = hTreeGroup.HTrees[HuffIndex.Green][0].Value; + uint alpha = hTreeGroup.HTrees[HuffIndex.Alpha][0].Value; + hTreeGroup.LiteralArb = (alpha << 24) | (red << 16) | blue; + if (totalSize == 0 && green < WebpConstants.NumLiteralCodes) + { + hTreeGroup.IsTrivialCode = true; + hTreeGroup.LiteralArb |= green << 8; + } + } + + hTreeGroup.UsePackedTable = !hTreeGroup.IsTrivialCode && maxBits < HuffmanUtils.HuffmanPackedBits; + if (hTreeGroup.UsePackedTable) + { + BuildPackedTable(hTreeGroup); + } + } + + decoder.Metadata.NumHTreeGroups = numHTreeGroups; + decoder.Metadata.HTreeGroups = hTreeGroups; + decoder.Metadata.HuffmanTables = huffmanTables; + } + + private int ReadHuffmanCode(int alphabetSize, int[] codeLengths, Span table) + { + bool simpleCode = this.bitReader.ReadBit(); + codeLengths.AsSpan(0, alphabetSize).Clear(); + + if (simpleCode) + { + // (i) Simple Code Length Code. + // This variant is used in the special case when only 1 or 2 Huffman code lengths are non-zero, + // and are in the range of[0, 255]. All other Huffman code lengths are implicitly zeros. + + // Read symbols, codes & code lengths directly. + uint numSymbols = this.bitReader.ReadValue(1) + 1; + uint firstSymbolLenCode = this.bitReader.ReadValue(1); + + // The first code is either 1 bit or 8 bit code. + uint symbol = this.bitReader.ReadValue(firstSymbolLenCode == 0 ? 1 : 8); + codeLengths[symbol] = 1; + + // The second code (if present), is always 8 bit long. + if (numSymbols == 2) + { + symbol = this.bitReader.ReadValue(8); + codeLengths[symbol] = 1; + } + } + else + { + // (ii) Normal Code Length Code: + // The code lengths of a Huffman code are read as follows: num_code_lengths specifies the number of code lengths; + // the rest of the code lengths (according to the order in kCodeLengthCodeOrder) are zeros. + int[] codeLengthCodeLengths = new int[NumCodeLengthCodes]; + uint numCodes = this.bitReader.ReadValue(4) + 4; + if (numCodes > NumCodeLengthCodes) + { + WebpThrowHelper.ThrowImageFormatException("Bitstream error, numCodes has an invalid value"); + } + + for (int i = 0; i < numCodes; i++) + { + codeLengthCodeLengths[CodeLengthCodeOrder[i]] = (int)this.bitReader.ReadValue(3); + } + + this.ReadHuffmanCodeLengths(table, codeLengthCodeLengths, alphabetSize, codeLengths); + } + + return HuffmanUtils.BuildHuffmanTable(table, HuffmanUtils.HuffmanTableBits, codeLengths, alphabetSize); + } + + private void ReadHuffmanCodeLengths(Span table, int[] codeLengthCodeLengths, int numSymbols, int[] codeLengths) + { + int maxSymbol; + int symbol = 0; + int prevCodeLen = WebpConstants.DefaultCodeLength; + int size = HuffmanUtils.BuildHuffmanTable(table, WebpConstants.LengthTableBits, codeLengthCodeLengths, NumCodeLengthCodes); + if (size == 0) + { + WebpThrowHelper.ThrowImageFormatException("Error building huffman table"); + } + + if (this.bitReader.ReadBit()) + { + int lengthNBits = 2 + (2 * (int)this.bitReader.ReadValue(3)); + maxSymbol = 2 + (int)this.bitReader.ReadValue(lengthNBits); + } + else + { + maxSymbol = numSymbols; + } + + while (symbol < numSymbols) + { + if (maxSymbol-- == 0) + { + break; + } + + this.bitReader.FillBitWindow(); + ulong prefetchBits = this.bitReader.PrefetchBits(); + int idx = (int)(prefetchBits & 127); + HuffmanCode huffmanCode = table[idx]; + this.bitReader.AdvanceBitPosition(huffmanCode.BitsUsed); + uint codeLen = huffmanCode.Value; + if (codeLen < WebpConstants.CodeLengthLiterals) + { + codeLengths[symbol++] = (int)codeLen; + if (codeLen != 0) + { + prevCodeLen = (int)codeLen; + } + } + else + { + bool usePrev = codeLen == WebpConstants.CodeLengthRepeatCode; + uint slot = codeLen - WebpConstants.CodeLengthLiterals; + int extraBits = WebpConstants.CodeLengthExtraBits[slot]; + int repeatOffset = WebpConstants.CodeLengthRepeatOffsets[slot]; + int repeat = (int)(this.bitReader.ReadValue(extraBits) + repeatOffset); + if (symbol + repeat > numSymbols) + { + return; + } + + int length = usePrev ? prevCodeLen : 0; + while (repeat-- > 0) + { + codeLengths[symbol++] = length; + } + } + } + } + + /// + /// Reads the transformations, if any are present. + /// + /// The width of the image. + /// The height of the image. + /// Vp8LDecoder where the transformations will be stored. + private void ReadTransformation(int xSize, int ySize, Vp8LDecoder decoder) + { + Vp8LTransformType transformType = (Vp8LTransformType)this.bitReader.ReadValue(2); + Vp8LTransform transform = new(transformType, xSize, ySize); + + // Each transform is allowed to be used only once. + if (decoder.Transforms.Any(decoderTransform => decoderTransform.TransformType == transform.TransformType)) + { + WebpThrowHelper.ThrowImageFormatException("Each transform can only be present once"); + } + + switch (transformType) + { + case Vp8LTransformType.SubtractGreen: + // There is no data associated with this transform. + break; + case Vp8LTransformType.ColorIndexingTransform: + // The transform data contains color table size and the entries in the color table. + // 8 bit value for color table size. + uint numColors = this.bitReader.ReadValue(8) + 1; + if (numColors > 16) + { + transform.Bits = 0; + } + else if (numColors > 4) + { + transform.Bits = 1; + } + else if (numColors > 2) + { + transform.Bits = 2; + } + else + { + transform.Bits = 3; + } + + using (IMemoryOwner colorMap = this.DecodeImageStream(decoder, (int)numColors, 1, false)) + { + int finalNumColors = 1 << (8 >> transform.Bits); + IMemoryOwner newColorMap = this.memoryAllocator.Allocate(finalNumColors, AllocationOptions.Clean); + LosslessUtils.ExpandColorMap((int)numColors, colorMap.GetSpan(), newColorMap.GetSpan()); + transform.Data = newColorMap; + } + + break; + + case Vp8LTransformType.PredictorTransform: + case Vp8LTransformType.CrossColorTransform: + + // The first 3 bits of prediction data define the block width and height in number of bits. + transform.Bits = (int)this.bitReader.ReadValue(3) + 2; + int blockWidth = LosslessUtils.SubSampleSize(transform.XSize, transform.Bits); + int blockHeight = LosslessUtils.SubSampleSize(transform.YSize, transform.Bits); + transform.Data = this.DecodeImageStream(decoder, blockWidth, blockHeight, false); + break; + } + + decoder.Transforms.Add(transform); + } + + /// + /// A Webp lossless image can go through four different types of transformation before being entropy encoded. + /// This will reverse the transformations, if any are present. + /// + /// The decoder holding the transformation infos. + /// The pixel data to apply the transformation. + /// The memory allocator is needed to allocate memory during the predictor transform. + public static void ApplyInverseTransforms(Vp8LDecoder decoder, Span pixelData, MemoryAllocator memoryAllocator) + { + List transforms = decoder.Transforms; + for (int i = transforms.Count - 1; i >= 0; i--) + { + // TODO: Review these 1D allocations. They could conceivably exceed limits. + Vp8LTransform transform = transforms[i]; + switch (transform.TransformType) + { + case Vp8LTransformType.PredictorTransform: + using (IMemoryOwner output = memoryAllocator.Allocate(pixelData.Length, AllocationOptions.Clean)) + { + LosslessUtils.PredictorInverseTransform(transform, pixelData, output.GetSpan()); + } + + break; + case Vp8LTransformType.SubtractGreen: + LosslessUtils.AddGreenToBlueAndRed(pixelData); + break; + case Vp8LTransformType.CrossColorTransform: + LosslessUtils.ColorSpaceInverseTransform(transform, pixelData); + break; + case Vp8LTransformType.ColorIndexingTransform: + using (IMemoryOwner output = memoryAllocator.Allocate(transform.XSize * transform.YSize, AllocationOptions.Clean)) + { + LosslessUtils.ColorIndexInverseTransform(transform, pixelData, output.GetSpan()); + } + + break; + } + } + } + + /// + /// The alpha channel of a lossy webp image can be compressed using the lossless webp compression. + /// This method will undo the compression. + /// + /// The alpha decoder. + public void DecodeAlphaData(AlphaDecoder dec) + { + Span pixelData = dec.Vp8LDec.Pixels.Memory.Span; + Span data = MemoryMarshal.Cast(pixelData); + int row = 0; + int col = 0; + Vp8LDecoder vp8LDec = dec.Vp8LDec; + int width = vp8LDec.Width; + int height = vp8LDec.Height; + Vp8LMetadata hdr = vp8LDec.Metadata; + int pos = 0; // Current position. + int end = width * height; // End of data. + int last = end; // Last pixel to decode. + int lastRow = height; + const int lenCodeLimit = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes; + int mask = hdr.HuffmanMask; + Span htreeGroup = pos < last ? GetHTreeGroupForPos(hdr, col, row) : null; + while (!this.bitReader.Eos && pos < last) + { + // Only update when changing tile. + if ((col & mask) == 0) + { + htreeGroup = GetHTreeGroupForPos(hdr, col, row); + } + + this.bitReader.FillBitWindow(); + int code = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Green]); + switch (code) + { + case < WebpConstants.NumLiteralCodes: + { + // Literal + data[pos] = (byte)code; + ++pos; + ++col; + + if (col >= width) + { + col = 0; + ++row; + if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0) + { + dec.ExtractPalettedAlphaRows(row); + } + } + + break; + } + + case < lenCodeLimit: + { + // Backward reference + int lengthSym = code - WebpConstants.NumLiteralCodes; + int length = this.GetCopyLength(lengthSym); + int distSymbol = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Dist]); + this.bitReader.FillBitWindow(); + int distCode = this.GetCopyDistance(distSymbol); + int dist = PlaneCodeToDistance(width, distCode); + if (pos >= dist && end - pos >= length) + { + CopyBlock8B(data, pos, dist, length); + } + else + { + WebpThrowHelper.ThrowImageFormatException("error while decoding alpha data"); + } + + pos += length; + col += length; + while (col >= width) + { + col -= width; + ++row; + if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0) + { + dec.ExtractPalettedAlphaRows(row); + } + } + + if (pos < last && (col & mask) > 0) + { + htreeGroup = GetHTreeGroupForPos(hdr, col, row); + } + + break; + } + + default: + WebpThrowHelper.ThrowImageFormatException("bitstream error while parsing alpha data"); + break; + } + + this.bitReader.Eos = this.bitReader.IsEndOfStream(); + } + + // Process the remaining rows corresponding to last row-block. + dec.ExtractPalettedAlphaRows(row > lastRow ? lastRow : row); + } + + private static void UpdateDecoder(Vp8LDecoder decoder, int width, int height) + { + int numBits = decoder.Metadata.HuffmanSubSampleBits; + decoder.Width = width; + decoder.Height = height; + decoder.Metadata.HuffmanXSize = LosslessUtils.SubSampleSize(width, numBits); + decoder.Metadata.HuffmanMask = numBits == 0 ? ~0 : (1 << numBits) - 1; + } + + private uint ReadPackedSymbols(Span group, Span pixelData, int decodedPixels) + { + uint val = (uint)(this.bitReader.PrefetchBits() & (HuffmanUtils.HuffmanPackedTableSize - 1)); + HuffmanCode code = group[0].PackedTable[val]; + if (code.BitsUsed < BitsSpecialMarker) + { + this.bitReader.AdvanceBitPosition(code.BitsUsed); + pixelData[decodedPixels] = code.Value; + return PackedNonLiteralCode; + } + + this.bitReader.AdvanceBitPosition(code.BitsUsed - BitsSpecialMarker); + + return code.Value; + } + + private static void BuildPackedTable(HTreeGroup hTreeGroup) + { + for (uint code = 0; code < HuffmanUtils.HuffmanPackedTableSize; code++) + { + uint bits = code; + ref HuffmanCode huff = ref hTreeGroup.PackedTable[bits]; + HuffmanCode hCode = hTreeGroup.HTrees[HuffIndex.Green][bits]; + if (hCode.Value >= WebpConstants.NumLiteralCodes) + { + huff.BitsUsed = hCode.BitsUsed + BitsSpecialMarker; + huff.Value = hCode.Value; + } + else + { + huff.BitsUsed = 0; + huff.Value = 0; + bits >>= AccumulateHCode(hCode, 8, ref huff); + bits >>= AccumulateHCode(hTreeGroup.HTrees[HuffIndex.Red][bits], 16, ref huff); + bits >>= AccumulateHCode(hTreeGroup.HTrees[HuffIndex.Blue][bits], 0, ref huff); + bits >>= AccumulateHCode(hTreeGroup.HTrees[HuffIndex.Alpha][bits], 24, ref huff); + } + } + } + + /// + /// Decodes the next Huffman code from the bit-stream. + /// FillBitWindow() needs to be called at minimum every second call to ReadSymbol, in order to pre-fetch enough bits. + /// + /// The Huffman table. + private uint ReadSymbol(Span table) + { + uint val = (uint)this.bitReader.PrefetchBits(); + Span tableSpan = table[(int)(val & HuffmanUtils.HuffmanTableMask)..]; + int nBits = tableSpan[0].BitsUsed - HuffmanUtils.HuffmanTableBits; + if (nBits > 0) + { + this.bitReader.AdvanceBitPosition(HuffmanUtils.HuffmanTableBits); + val = (uint)this.bitReader.PrefetchBits(); + tableSpan = tableSpan[(int)tableSpan[0].Value..]; + tableSpan = tableSpan[((int)val & ((1 << nBits) - 1))..]; + } + + this.bitReader.AdvanceBitPosition(tableSpan[0].BitsUsed); + + return tableSpan[0].Value; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private int GetCopyLength(int lengthSymbol) => + this.GetCopyDistance(lengthSymbol); // Length and distance prefixes are encoded the same way. + + private int GetCopyDistance(int distanceSymbol) + { + if (distanceSymbol < 4) + { + return distanceSymbol + 1; + } + + int extraBits = (distanceSymbol - 2) >> 1; + int offset = (2 + (distanceSymbol & 1)) << extraBits; + + return (int)(offset + this.bitReader.ReadValue(extraBits) + 1); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static Span GetHTreeGroupForPos(Vp8LMetadata metadata, int x, int y) + { + uint metaIndex = GetMetaIndex(metadata.HuffmanImage, metadata.HuffmanXSize, metadata.HuffmanSubSampleBits, x, y); + return metadata.HTreeGroups.AsSpan((int)metaIndex); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint GetMetaIndex(IMemoryOwner huffmanImage, int xSize, int bits, int x, int y) + { + if (bits is 0) + { + return 0; + } + + Span huffmanImageSpan = huffmanImage.GetSpan(); + return huffmanImageSpan[(xSize * (y >> bits)) + (x >> bits)]; + } + + private static int PlaneCodeToDistance(int xSize, int planeCode) + { + if (planeCode > CodeToPlaneCodes) + { + return planeCode - CodeToPlaneCodes; + } + + int distCode = WebpLookupTables.CodeToPlane[planeCode - 1]; + int yOffset = distCode >> 4; + int xOffset = 8 - (distCode & 0xf); + int dist = (yOffset * xSize) + xOffset; + + // dist < 1 can happen if xSize is very small. + return dist >= 1 ? dist : 1; + } + + /// + /// Copies pixels when a backward reference is used. + /// Copy 'length' number of pixels (in scan-line order) from the sequence of pixels prior to them by 'dist' pixels. + /// + /// The pixel data. + /// The number of so far decoded pixels. + /// The backward reference distance prior to the current decoded pixel. + /// The number of pixels to copy. + private static void CopyBlock(Span pixelData, int decodedPixels, int dist, int length) + { + int start = decodedPixels - dist; + if (start < 0) + { + WebpThrowHelper.ThrowImageFormatException("webp image data seems to be invalid"); + } + + if (dist >= length) + { + // no overlap. + Span src = pixelData.Slice(start, length); + Span dest = pixelData[decodedPixels..]; + src.CopyTo(dest); + } + else + { + // There is overlap between the backward reference distance and the pixels to copy. + Span src = pixelData[start..]; + Span dest = pixelData[decodedPixels..]; + for (int i = 0; i < length; i++) + { + dest[i] = src[i]; + } + } + } + + /// + /// Copies alpha values when a backward reference is used. + /// Copy 'length' number of alpha values from the sequence of alpha values prior to them by 'dist'. + /// + /// The alpha values. + /// The position of the so far decoded pixels. + /// The backward reference distance prior to the current decoded pixel. + /// The number of pixels to copy. + private static void CopyBlock8B(Span data, int pos, int dist, int length) + { + if (dist >= length) + { + // no overlap. + data.Slice(pos - dist, length).CopyTo(data[pos..]); + } + else + { + Span dst = data[pos..]; + Span src = data[(pos - dist)..]; + for (int i = 0; i < length; i++) + { + dst[i] = src[i]; + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int AccumulateHCode(HuffmanCode hCode, int shift, ref HuffmanCode huff) + { + huff.BitsUsed += hCode.BitsUsed; + huff.Value |= hCode.Value << shift; + return hCode.BitsUsed; + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossless/Webp_Lossless_Bitstream_Specification.pdf b/ImageSharp/Formats/Webp/Lossless/Webp_Lossless_Bitstream_Specification.pdf new file mode 100644 index 0000000..4b5ddd5 Binary files /dev/null and b/ImageSharp/Formats/Webp/Lossless/Webp_Lossless_Bitstream_Specification.pdf differ diff --git a/ImageSharp/Formats/Webp/Lossy/IntraPredictionMode.cs b/ImageSharp/Formats/Webp/Lossy/IntraPredictionMode.cs new file mode 100644 index 0000000..c076a66 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/IntraPredictionMode.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal enum IntraPredictionMode + { + /// + /// Predict DC using row above and column to the left. + /// + DcPrediction = 0, + + /// + /// Propagate second differences a la "True Motion". + /// + TrueMotion = 1, + + /// + /// Predict rows using row above. + /// + VPrediction = 2, + + /// + /// Predict columns using column to the left. + /// + HPrediction = 3, + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/LoopFilter.cs b/ImageSharp/Formats/Webp/Lossy/LoopFilter.cs new file mode 100644 index 0000000..0abdc20 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/LoopFilter.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// Enum for the different loop filters used. VP8 supports two types of loop filters. + /// + internal enum LoopFilter + { + /// + /// No filter is used. + /// + None = 0, + + /// + /// Simple loop filter. + /// + Simple = 1, + + /// + /// Complex loop filter. + /// + Complex = 2, + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/LossyUtils.cs b/ImageSharp/Formats/Webp/Lossy/LossyUtils.cs new file mode 100644 index 0000000..ba9672f --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/LossyUtils.cs @@ -0,0 +1,2408 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal static class LossyUtils + { + // Note: method name in libwebp reference implementation is called VP8SSE16x16. + [MethodImpl(InliningOptions.ShortMethod)] + public static int Vp8_Sse16x16(Span a, Span b) + { + if (Vector256.IsHardwareAccelerated) + { + return Vp8_Sse16xN_Vector256(a, b, 4); + } + + if (Vector128.IsHardwareAccelerated) + { + return Vp8_16xN_Vector128(a, b, 8); + } + + return Vp8_SseNxN(a, b, 16, 16); + } + + // Note: method name in libwebp reference implementation is called VP8SSE16x8. + [MethodImpl(InliningOptions.ShortMethod)] + public static int Vp8_Sse16x8(Span a, Span b) + { + if (Vector256.IsHardwareAccelerated) + { + return Vp8_Sse16xN_Vector256(a, b, 2); + } + + if (Vector128.IsHardwareAccelerated) + { + return Vp8_16xN_Vector128(a, b, 4); + } + + return Vp8_SseNxN(a, b, 16, 8); + } + + // Note: method name in libwebp reference implementation is called VP8SSE4x4. + [MethodImpl(InliningOptions.ShortMethod)] + public static int Vp8_Sse4x4(Span a, Span b) + { + if (Vector256.IsHardwareAccelerated) + { + // Load values. + ref byte aRef = ref MemoryMarshal.GetReference(a); + ref byte bRef = ref MemoryMarshal.GetReference(b); + Vector256 a0 = Vector256.Create( + Unsafe.As>(ref aRef), + Unsafe.As>(ref Unsafe.Add(ref aRef, WebpConstants.Bps))); + Vector256 a1 = Vector256.Create( + Unsafe.As>(ref Unsafe.Add(ref aRef, WebpConstants.Bps * 2)), + Unsafe.As>(ref Unsafe.Add(ref aRef, WebpConstants.Bps * 3))); + Vector256 b0 = Vector256.Create( + Unsafe.As>(ref bRef), + Unsafe.As>(ref Unsafe.Add(ref bRef, WebpConstants.Bps))); + Vector256 b1 = Vector256.Create( + Unsafe.As>(ref Unsafe.Add(ref bRef, WebpConstants.Bps * 2)), + Unsafe.As>(ref Unsafe.Add(ref bRef, WebpConstants.Bps * 3))); + + // Combine pair of lines. + Vector256 a01 = Vector256_.UnpackLow(a0.AsInt32(), a1.AsInt32()); + Vector256 b01 = Vector256_.UnpackLow(b0.AsInt32(), b1.AsInt32()); + + // Convert to 16b. + Vector256 a01s = Vector256_.UnpackLow(a01.AsByte(), Vector256.Zero); + Vector256 b01s = Vector256_.UnpackLow(b01.AsByte(), Vector256.Zero); + + // subtract, square and accumulate. + Vector256 d0 = Vector256_.SubtractSaturate(a01s.AsInt16(), b01s.AsInt16()); + Vector256 e0 = Vector256_.MultiplyAddAdjacent(d0, d0); + + return ReduceSumVector256(e0); + } + + if (Vector128.IsHardwareAccelerated) + { + // Load values. + ref byte aRef = ref MemoryMarshal.GetReference(a); + ref byte bRef = ref MemoryMarshal.GetReference(b); + Vector128 a0 = Unsafe.As>(ref aRef); + Vector128 a1 = Unsafe.As>(ref Unsafe.Add(ref aRef, WebpConstants.Bps)); + Vector128 a2 = Unsafe.As>(ref Unsafe.Add(ref aRef, WebpConstants.Bps * 2)); + Vector128 a3 = Unsafe.As>(ref Unsafe.Add(ref aRef, WebpConstants.Bps * 3)); + Vector128 b0 = Unsafe.As>(ref bRef); + Vector128 b1 = Unsafe.As>(ref Unsafe.Add(ref bRef, WebpConstants.Bps)); + Vector128 b2 = Unsafe.As>(ref Unsafe.Add(ref bRef, WebpConstants.Bps * 2)); + Vector128 b3 = Unsafe.As>(ref Unsafe.Add(ref bRef, WebpConstants.Bps * 3)); + + // Combine pair of lines. + Vector128 a01 = Vector128_.UnpackLow(a0.AsInt32(), a1.AsInt32()); + Vector128 a23 = Vector128_.UnpackLow(a2.AsInt32(), a3.AsInt32()); + Vector128 b01 = Vector128_.UnpackLow(b0.AsInt32(), b1.AsInt32()); + Vector128 b23 = Vector128_.UnpackLow(b2.AsInt32(), b3.AsInt32()); + + // Convert to 16b. + Vector128 a01s = Vector128_.UnpackLow(a01.AsByte(), Vector128.Zero); + Vector128 a23s = Vector128_.UnpackLow(a23.AsByte(), Vector128.Zero); + Vector128 b01s = Vector128_.UnpackLow(b01.AsByte(), Vector128.Zero); + Vector128 b23s = Vector128_.UnpackLow(b23.AsByte(), Vector128.Zero); + + // subtract, square and accumulate. + Vector128 d0 = Vector128_.SubtractSaturate(a01s.AsInt16(), b01s.AsInt16()); + Vector128 d1 = Vector128_.SubtractSaturate(a23s.AsInt16(), b23s.AsInt16()); + Vector128 e0 = Vector128_.MultiplyAddAdjacent(d0, d0); + Vector128 e1 = Vector128_.MultiplyAddAdjacent(d1, d1); + Vector128 sum = e0 + e1; + + return ReduceSumVector128(sum); + } + + return Vp8_SseNxN(a, b, 4, 4); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static int Vp8_SseNxN(Span a, Span b, int w, int h) + { + int count = 0; + int offset = 0; + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) + { + int diff = a[offset + x] - b[offset + x]; + count += diff * diff; + } + + offset += WebpConstants.Bps; + } + + return count; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Vp8_16xN_Vector128(Span a, Span b, int numPairs) + { + Vector128 sum = Vector128.Zero; + nuint offset = 0; + ref byte aRef = ref MemoryMarshal.GetReference(a); + ref byte bRef = ref MemoryMarshal.GetReference(b); + for (int i = 0; i < numPairs; i++) + { + // Load values. + Vector128 a0 = Unsafe.As>(ref Unsafe.Add(ref aRef, offset)); + Vector128 b0 = Unsafe.As>(ref Unsafe.Add(ref bRef, offset)); + Vector128 a1 = Unsafe.As>(ref Unsafe.Add(ref aRef, offset + WebpConstants.Bps)); + Vector128 b1 = Unsafe.As>(ref Unsafe.Add(ref bRef, offset + WebpConstants.Bps)); + + Vector128 sum1 = SubtractAndAccumulateVector128(a0, b0); + Vector128 sum2 = SubtractAndAccumulateVector128(a1, b1); + sum += sum1 + sum2; + + offset += 2 * WebpConstants.Bps; + } + + return ReduceSumVector128(sum); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Vp8_Sse16xN_Vector256(Span a, Span b, int numPairs) + { + Vector256 sum = Vector256.Zero; + nuint offset = 0; + ref byte aRef = ref MemoryMarshal.GetReference(a); + ref byte bRef = ref MemoryMarshal.GetReference(b); + for (int i = 0; i < numPairs; i++) + { + // Load values. + Vector256 a0 = Vector256.Create( + Unsafe.As>(ref Unsafe.Add(ref aRef, offset)), + Unsafe.As>(ref Unsafe.Add(ref aRef, offset + WebpConstants.Bps))); + Vector256 b0 = Vector256.Create( + Unsafe.As>(ref Unsafe.Add(ref bRef, offset)), + Unsafe.As>(ref Unsafe.Add(ref bRef, offset + WebpConstants.Bps))); + Vector256 a1 = Vector256.Create( + Unsafe.As>(ref Unsafe.Add(ref aRef, offset + (2 * WebpConstants.Bps))), + Unsafe.As>(ref Unsafe.Add(ref aRef, offset + (3 * WebpConstants.Bps)))); + Vector256 b1 = Vector256.Create( + Unsafe.As>(ref Unsafe.Add(ref bRef, offset + (2 * WebpConstants.Bps))), + Unsafe.As>(ref Unsafe.Add(ref bRef, offset + (3 * WebpConstants.Bps)))); + + Vector256 sum1 = SubtractAndAccumulateVector256(a0, b0); + Vector256 sum2 = SubtractAndAccumulateVector256(a1, b1); + sum += sum1 + sum2; + + offset += 4 * WebpConstants.Bps; + } + + return ReduceSumVector256(sum); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static Vector128 SubtractAndAccumulateVector128(Vector128 a, Vector128 b) + { + // Take abs(a-b) in 8b. + Vector128 ab = Vector128_.SubtractSaturate(a, b); + Vector128 ba = Vector128_.SubtractSaturate(b, a); + Vector128 absAb = ab | ba; + + // Zero-extend to 16b. + Vector128 c0 = Vector128_.UnpackLow(absAb, Vector128.Zero); + Vector128 c1 = Vector128_.UnpackHigh(absAb, Vector128.Zero); + + // Multiply with self. + Vector128 sum1 = Vector128_.MultiplyAddAdjacent(c0.AsInt16(), c0.AsInt16()); + Vector128 sum2 = Vector128_.MultiplyAddAdjacent(c1.AsInt16(), c1.AsInt16()); + + return sum1 + sum2; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static Vector256 SubtractAndAccumulateVector256(Vector256 a, Vector256 b) + { + // Take abs(a-b) in 8b. + Vector256 ab = Vector256_.SubtractSaturate(a, b); + Vector256 ba = Vector256_.SubtractSaturate(b, a); + Vector256 absAb = ab | ba; + + // Zero-extend to 16b. + Vector256 c0 = Vector256_.UnpackLow(absAb, Vector256.Zero); + Vector256 c1 = Vector256_.UnpackHigh(absAb, Vector256.Zero); + + // Multiply with self. + Vector256 sum1 = Vector256_.MultiplyAddAdjacent(c0.AsInt16(), c0.AsInt16()); + Vector256 sum2 = Vector256_.MultiplyAddAdjacent(c1.AsInt16(), c1.AsInt16()); + + return sum1 + sum2; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void Vp8Copy4X4(Span src, Span dst) => Copy(src, dst, 4, 4); + + [MethodImpl(InliningOptions.ShortMethod)] + public static void Vp8Copy16X8(Span src, Span dst) => Copy(src, dst, 16, 8); + + [MethodImpl(InliningOptions.ShortMethod)] + public static void Copy(Span src, Span dst, int w, int h) + { + int offset = 0; + for (int y = 0; y < h; y++) + { + src.Slice(offset, w).CopyTo(dst.Slice(offset, w)); + offset += WebpConstants.Bps; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static int Vp8Disto16X16(Span a, Span b, Span w, Span scratch) + { + int d = 0; + const int dataSize = (4 * WebpConstants.Bps) - 16; + for (int y = 0; y < 16 * WebpConstants.Bps; y += 4 * WebpConstants.Bps) + { + for (int x = 0; x < 16; x += 4) + { + d += Vp8Disto4X4(a.Slice(x + y, dataSize), b.Slice(x + y, dataSize), w, scratch); + } + } + + return d; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static int Vp8Disto4X4(Span a, Span b, Span w, Span scratch) + { + if (Vector128.IsHardwareAccelerated) + { + int diffSum = TTransformVector128(a, b, w); + return Math.Abs(diffSum) >> 5; + } + + int sum1 = TTransform(a, w, scratch); + int sum2 = TTransform(b, w, scratch); + + return Math.Abs(sum2 - sum1) >> 5; + } + + public static void DC16(Span dst, Span yuv, int offset) + { + int offsetMinus1 = offset - 1; + int offsetMinusBps = offset - WebpConstants.Bps; + int dc = 16; + for (int j = 0; j < 16; j++) + { + // DC += dst[-1 + j * BPS] + dst[j - BPS]; + dc += yuv[offsetMinus1 + (j * WebpConstants.Bps)] + yuv[offsetMinusBps + j]; + } + + Put16(dc >> 5, dst); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void TM16(Span dst, Span yuv, int offset) => TrueMotion(dst, yuv, offset, 16); + + public static void VE16(Span dst, Span yuv, int offset) + { + // vertical + Span src = yuv.Slice(offset - WebpConstants.Bps, 16); + for (int j = 0; j < 16; j++) + { + // memcpy(dst + j * BPS, dst - BPS, 16); + src.CopyTo(dst[(j * WebpConstants.Bps)..]); + } + } + + public static void HE16(Span dst, Span yuv, int offset) + { + // horizontal + offset--; + for (int j = 16; j > 0; j--) + { + // memset(dst, dst[-1], 16); + byte v = yuv[offset]; + Memset(dst, v, 0, 16); + offset += WebpConstants.Bps; + dst = dst[WebpConstants.Bps..]; + } + } + + public static void DC16NoTop(Span dst, Span yuv, int offset) + { + // DC with top samples not available. + int dc = 8; + for (int j = 0; j < 16; j++) + { + // DC += dst[-1 + j * BPS]; + dc += yuv[-1 + (j * WebpConstants.Bps) + offset]; + } + + Put16(dc >> 4, dst); + } + + public static void DC16NoLeft(Span dst, Span yuv, int offset) + { + // DC with left samples not available. + int dc = 8; + for (int i = 0; i < 16; i++) + { + // DC += dst[i - BPS]; + dc += yuv[i - WebpConstants.Bps + offset]; + } + + Put16(dc >> 4, dst); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void DC16NoTopLeft(Span dst) => + Put16(0x80, dst); // DC with no top and left samples. + + public static void DC8uv(Span dst, Span yuv, int offset) + { + int dc0 = 8; + int offsetMinus1 = offset - 1; + int offsetMinusBps = offset - WebpConstants.Bps; + for (int i = 0; i < 8; i++) + { + // dc0 += dst[i - BPS] + dst[-1 + i * BPS]; + dc0 += yuv[offsetMinusBps + i] + yuv[offsetMinus1 + (i * WebpConstants.Bps)]; + } + + Put8x8uv((byte)(dc0 >> 4), dst); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void TM8uv(Span dst, Span yuv, int offset) => + TrueMotion(dst, yuv, offset, 8); // TrueMotion + + public static void VE8uv(Span dst, Span yuv, int offset) + { + // vertical + Span src = yuv.Slice(offset - WebpConstants.Bps, 8); + + const int endIdx = 8 * WebpConstants.Bps; + for (int j = 0; j < endIdx; j += WebpConstants.Bps) + { + // memcpy(dst + j * BPS, dst - BPS, 8); + src.CopyTo(dst[j..]); + } + } + + public static void HE8uv(Span dst, Span yuv, int offset) + { + // horizontal + offset--; + for (int j = 0; j < 8; j++) + { + // memset(dst, dst[-1], 8); + // dst += BPS; + byte v = yuv[offset]; + Memset(dst, v, 0, 8); + dst = dst[WebpConstants.Bps..]; + offset += WebpConstants.Bps; + } + } + + public static void DC8uvNoTop(Span dst, Span yuv, int offset) + { + // DC with no top samples. + int dc0 = 4; + int offsetMinusOne = offset - 1; + const int endIdx = 8 * WebpConstants.Bps; + for (int i = 0; i < endIdx; i += WebpConstants.Bps) + { + // dc0 += dst[-1 + i * BPS]; + dc0 += yuv[offsetMinusOne + i]; + } + + Put8x8uv((byte)(dc0 >> 3), dst); + } + + public static void DC8uvNoLeft(Span dst, Span yuv, int offset) + { + // DC with no left samples. + int offsetMinusBps = offset - WebpConstants.Bps; + int dc0 = 4; + for (int i = 0; i < 8; i++) + { + // dc0 += dst[i - BPS]; + dc0 += yuv[offsetMinusBps + i]; + } + + Put8x8uv((byte)(dc0 >> 3), dst); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void DC8uvNoTopLeft(Span dst) => + Put8x8uv(0x80, dst); // DC with nothing. + + public static void DC4(Span dst, Span yuv, int offset) + { + int dc = 4; + int offsetMinusBps = offset - WebpConstants.Bps; + int offsetMinusOne = offset - 1; + for (int i = 0; i < 4; i++) + { + dc += yuv[offsetMinusBps + i] + yuv[offsetMinusOne + (i * WebpConstants.Bps)]; + } + + dc >>= 3; + const int endIndx = 4 * WebpConstants.Bps; + for (int i = 0; i < endIndx; i += WebpConstants.Bps) + { + Memset(dst, (byte)dc, i, 4); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void TM4(Span dst, Span yuv, int offset) => TrueMotion(dst, yuv, offset, 4); + + public static void VE4(Span dst, Span yuv, int offset, Span vals) + { + // vertical + int topOffset = offset - WebpConstants.Bps; + vals[0] = Avg3(yuv[topOffset - 1], yuv[topOffset], yuv[topOffset + 1]); + vals[1] = Avg3(yuv[topOffset], yuv[topOffset + 1], yuv[topOffset + 2]); + vals[2] = Avg3(yuv[topOffset + 1], yuv[topOffset + 2], yuv[topOffset + 3]); + vals[3] = Avg3(yuv[topOffset + 2], yuv[topOffset + 3], yuv[topOffset + 4]); + const int endIdx = 4 * WebpConstants.Bps; + for (int i = 0; i < endIdx; i += WebpConstants.Bps) + { + vals.CopyTo(dst[i..]); + } + } + + public static void HE4(Span dst, Span yuv, int offset) + { + // horizontal + int offsetMinusOne = offset - 1; + byte a = yuv[offsetMinusOne - WebpConstants.Bps]; + byte b = yuv[offsetMinusOne]; + byte c = yuv[offsetMinusOne + WebpConstants.Bps]; + byte d = yuv[offsetMinusOne + (2 * WebpConstants.Bps)]; + byte e = yuv[offsetMinusOne + (3 * WebpConstants.Bps)]; + uint val = 0x01010101U * Avg3(a, b, c); + BinaryPrimitives.WriteUInt32BigEndian(dst, val); + val = 0x01010101U * Avg3(b, c, d); + BinaryPrimitives.WriteUInt32BigEndian(dst[WebpConstants.Bps..], val); + val = 0x01010101U * Avg3(c, d, e); + BinaryPrimitives.WriteUInt32BigEndian(dst[(2 * WebpConstants.Bps)..], val); + val = 0x01010101U * Avg3(d, e, e); + BinaryPrimitives.WriteUInt32BigEndian(dst[(3 * WebpConstants.Bps)..], val); + } + + public static void RD4(Span dst, Span yuv, int offset) + { + // Down-right + int offsetMinusOne = offset - 1; + byte i = yuv[offsetMinusOne]; + byte j = yuv[offsetMinusOne + (1 * WebpConstants.Bps)]; + byte k = yuv[offsetMinusOne + (2 * WebpConstants.Bps)]; + byte l = yuv[offsetMinusOne + (3 * WebpConstants.Bps)]; + byte x = yuv[offsetMinusOne - WebpConstants.Bps]; + byte a = yuv[offset - WebpConstants.Bps]; + byte b = yuv[offset + 1 - WebpConstants.Bps]; + byte c = yuv[offset + 2 - WebpConstants.Bps]; + byte d = yuv[offset + 3 - WebpConstants.Bps]; + + Dst(dst, 0, 3, Avg3(j, k, l)); + byte ijk = Avg3(i, j, k); + Dst(dst, 1, 3, ijk); + Dst(dst, 0, 2, ijk); + byte xij = Avg3(x, i, j); + Dst(dst, 2, 3, xij); + Dst(dst, 1, 2, xij); + Dst(dst, 0, 1, xij); + byte axi = Avg3(a, x, i); + Dst(dst, 3, 3, axi); + Dst(dst, 2, 2, axi); + Dst(dst, 1, 1, axi); + Dst(dst, 0, 0, axi); + byte bax = Avg3(b, a, x); + Dst(dst, 3, 2, bax); + Dst(dst, 2, 1, bax); + Dst(dst, 1, 0, bax); + byte cba = Avg3(c, b, a); + Dst(dst, 3, 1, cba); + Dst(dst, 2, 0, cba); + Dst(dst, 3, 0, Avg3(d, c, b)); + } + + public static void VR4(Span dst, Span yuv, int offset) + { + // Vertical-Right + int offsetMinusOne = offset - 1; + byte i = yuv[offsetMinusOne]; + byte j = yuv[offsetMinusOne + (1 * WebpConstants.Bps)]; + byte k = yuv[offsetMinusOne + (2 * WebpConstants.Bps)]; + byte x = yuv[offsetMinusOne - WebpConstants.Bps]; + byte a = yuv[offset - WebpConstants.Bps]; + byte b = yuv[offset + 1 - WebpConstants.Bps]; + byte c = yuv[offset + 2 - WebpConstants.Bps]; + byte d = yuv[offset + 3 - WebpConstants.Bps]; + + byte xa = Avg2(x, a); + Dst(dst, 0, 0, xa); + Dst(dst, 1, 2, xa); + byte ab = Avg2(a, b); + Dst(dst, 1, 0, ab); + Dst(dst, 2, 2, ab); + byte bc = Avg2(b, c); + Dst(dst, 2, 0, bc); + Dst(dst, 3, 2, bc); + Dst(dst, 3, 0, Avg2(c, d)); + Dst(dst, 0, 3, Avg3(k, j, i)); + Dst(dst, 0, 2, Avg3(j, i, x)); + byte ixa = Avg3(i, x, a); + Dst(dst, 0, 1, ixa); + Dst(dst, 1, 3, ixa); + byte xab = Avg3(x, a, b); + Dst(dst, 1, 1, xab); + Dst(dst, 2, 3, xab); + byte abc = Avg3(a, b, c); + Dst(dst, 2, 1, abc); + Dst(dst, 3, 3, abc); + Dst(dst, 3, 1, Avg3(b, c, d)); + } + + public static void LD4(Span dst, Span yuv, int offset) + { + // Down-Left + byte a = yuv[offset - WebpConstants.Bps]; + byte b = yuv[offset + 1 - WebpConstants.Bps]; + byte c = yuv[offset + 2 - WebpConstants.Bps]; + byte d = yuv[offset + 3 - WebpConstants.Bps]; + byte e = yuv[offset + 4 - WebpConstants.Bps]; + byte f = yuv[offset + 5 - WebpConstants.Bps]; + byte g = yuv[offset + 6 - WebpConstants.Bps]; + byte h = yuv[offset + 7 - WebpConstants.Bps]; + + Dst(dst, 0, 0, Avg3(a, b, c)); + byte bcd = Avg3(b, c, d); + Dst(dst, 1, 0, bcd); + Dst(dst, 0, 1, bcd); + byte cde = Avg3(c, d, e); + Dst(dst, 2, 0, cde); + Dst(dst, 1, 1, cde); + Dst(dst, 0, 2, cde); + byte def = Avg3(d, e, f); + Dst(dst, 3, 0, def); + Dst(dst, 2, 1, def); + Dst(dst, 1, 2, def); + Dst(dst, 0, 3, def); + byte efg = Avg3(e, f, g); + Dst(dst, 3, 1, efg); + Dst(dst, 2, 2, efg); + Dst(dst, 1, 3, efg); + byte fgh = Avg3(f, g, h); + Dst(dst, 3, 2, fgh); + Dst(dst, 2, 3, fgh); + Dst(dst, 3, 3, Avg3(g, h, h)); + } + + public static void VL4(Span dst, Span yuv, int offset) + { + // Vertical-Left + byte a = yuv[offset - WebpConstants.Bps]; + byte b = yuv[offset + 1 - WebpConstants.Bps]; + byte c = yuv[offset + 2 - WebpConstants.Bps]; + byte d = yuv[offset + 3 - WebpConstants.Bps]; + byte e = yuv[offset + 4 - WebpConstants.Bps]; + byte f = yuv[offset + 5 - WebpConstants.Bps]; + byte g = yuv[offset + 6 - WebpConstants.Bps]; + byte h = yuv[offset + 7 - WebpConstants.Bps]; + + Dst(dst, 0, 0, Avg2(a, b)); + byte bc = Avg2(b, c); + Dst(dst, 1, 0, bc); + Dst(dst, 0, 2, bc); + byte cd = Avg2(c, d); + Dst(dst, 2, 0, cd); + Dst(dst, 1, 2, cd); + byte de = Avg2(d, e); + Dst(dst, 3, 0, de); + Dst(dst, 2, 2, de); + Dst(dst, 0, 1, Avg3(a, b, c)); + byte bcd = Avg3(b, c, d); + Dst(dst, 1, 1, bcd); + Dst(dst, 0, 3, bcd); + byte cde = Avg3(c, d, e); + Dst(dst, 2, 1, cde); + Dst(dst, 1, 3, cde); + byte def = Avg3(d, e, f); + Dst(dst, 3, 1, def); + Dst(dst, 2, 3, def); + Dst(dst, 3, 2, Avg3(e, f, g)); + Dst(dst, 3, 3, Avg3(f, g, h)); + } + + public static void HD4(Span dst, Span yuv, int offset) + { + // Horizontal-Down + byte i = yuv[offset - 1]; + byte j = yuv[offset - 1 + (1 * WebpConstants.Bps)]; + byte k = yuv[offset - 1 + (2 * WebpConstants.Bps)]; + byte l = yuv[offset - 1 + (3 * WebpConstants.Bps)]; + byte x = yuv[offset - 1 - WebpConstants.Bps]; + byte a = yuv[offset - WebpConstants.Bps]; + byte b = yuv[offset + 1 - WebpConstants.Bps]; + byte c = yuv[offset + 2 - WebpConstants.Bps]; + + byte ix = Avg2(i, x); + Dst(dst, 0, 0, ix); + Dst(dst, 2, 1, ix); + byte ji = Avg2(j, i); + Dst(dst, 0, 1, ji); + Dst(dst, 2, 2, ji); + byte kj = Avg2(k, j); + Dst(dst, 0, 2, kj); + Dst(dst, 2, 3, kj); + Dst(dst, 0, 3, Avg2(l, k)); + Dst(dst, 3, 0, Avg3(a, b, c)); + Dst(dst, 2, 0, Avg3(x, a, b)); + byte ixa = Avg3(i, x, a); + Dst(dst, 1, 0, ixa); + Dst(dst, 3, 1, ixa); + byte jix = Avg3(j, i, x); + Dst(dst, 1, 1, jix); + Dst(dst, 3, 2, jix); + byte kji = Avg3(k, j, i); + Dst(dst, 1, 2, kji); + Dst(dst, 3, 3, kji); + Dst(dst, 1, 3, Avg3(l, k, j)); + } + + public static void HU4(Span dst, Span yuv, int offset) + { + // Horizontal-Up + byte i = yuv[offset - 1]; + byte j = yuv[offset - 1 + (1 * WebpConstants.Bps)]; + byte k = yuv[offset - 1 + (2 * WebpConstants.Bps)]; + byte l = yuv[offset - 1 + (3 * WebpConstants.Bps)]; + + Dst(dst, 0, 0, Avg2(i, j)); + byte jk = Avg2(j, k); + Dst(dst, 2, 0, jk); + Dst(dst, 0, 1, jk); + byte kl = Avg2(k, l); + Dst(dst, 2, 1, kl); + Dst(dst, 0, 2, kl); + Dst(dst, 1, 0, Avg3(i, j, k)); + byte jkl = Avg3(j, k, l); + Dst(dst, 3, 0, jkl); + Dst(dst, 1, 1, jkl); + byte kll = Avg3(k, l, l); + Dst(dst, 3, 1, kll); + Dst(dst, 1, 2, kll); + Dst(dst, 3, 2, l); + Dst(dst, 2, 2, l); + Dst(dst, 0, 3, l); + Dst(dst, 1, 3, l); + Dst(dst, 2, 3, l); + Dst(dst, 3, 3, l); + } + + /// + /// Paragraph 14.3: Implementation of the Walsh-Hadamard transform inversion. + /// + public static void TransformWht(Span input, Span output, Span scratch) + { + Span tmp = scratch[..16]; + tmp.Clear(); + for (int i = 0; i < 4; i++) + { + int iPlus4 = 4 + i; + int iPlus8 = 8 + i; + int iPlus12 = 12 + i; + int a0 = input[i] + input[iPlus12]; + int a1 = input[iPlus4] + input[iPlus8]; + int a2 = input[iPlus4] - input[iPlus8]; + int a3 = input[i] - input[iPlus12]; + tmp[i] = a0 + a1; + tmp[iPlus8] = a0 - a1; + tmp[iPlus4] = a3 + a2; + tmp[iPlus12] = a3 - a2; + } + + int outputOffset = 0; + for (int i = 0; i < 4; i++) + { + int imul4 = i * 4; + int dc = tmp[0 + imul4] + 3; + int a0 = dc + tmp[3 + imul4]; + int a1 = tmp[1 + imul4] + tmp[2 + imul4]; + int a2 = tmp[1 + imul4] - tmp[2 + imul4]; + int a3 = dc - tmp[3 + imul4]; + output[outputOffset + 0] = (short)((a0 + a1) >> 3); + output[outputOffset + 16] = (short)((a3 + a2) >> 3); + output[outputOffset + 32] = (short)((a0 - a1) >> 3); + output[outputOffset + 48] = (short)((a3 - a2) >> 3); + outputOffset += 64; + } + } + + /// + /// Hadamard transform + /// Returns the weighted sum of the absolute value of transformed coefficients. + /// w[] contains a row-major 4 by 4 symmetric matrix. + /// + public static int TTransform(Span input, Span w, Span scratch) + { + int sum = 0; + Span tmp = scratch[..16]; + tmp.Clear(); + + // horizontal pass. + int inputOffset = 0; + for (int i = 0; i < 4; i++) + { + int inputOffsetPlusOne = inputOffset + 1; + int inputOffsetPlusTwo = inputOffset + 2; + int inputOffsetPlusThree = inputOffset + 3; + int a0 = input[inputOffset] + input[inputOffsetPlusTwo]; + int a1 = input[inputOffsetPlusOne] + input[inputOffsetPlusThree]; + int a2 = input[inputOffsetPlusOne] - input[inputOffsetPlusThree]; + int a3 = input[inputOffset] - input[inputOffsetPlusTwo]; + tmp[0 + (i * 4)] = a0 + a1; + tmp[1 + (i * 4)] = a3 + a2; + tmp[2 + (i * 4)] = a3 - a2; + tmp[3 + (i * 4)] = a0 - a1; + + inputOffset += WebpConstants.Bps; + } + + // vertical pass + for (int i = 0; i < 4; i++) + { + int a0 = tmp[0 + i] + tmp[8 + i]; + int a1 = tmp[4 + i] + tmp[12 + i]; + int a2 = tmp[4 + i] - tmp[12 + i]; + int a3 = tmp[0 + i] - tmp[8 + i]; + int b0 = a0 + a1; + int b1 = a3 + a2; + int b2 = a3 - a2; + int b3 = a0 - a1; + + sum += w[0] * Math.Abs(b0); + sum += w[4] * Math.Abs(b1); + sum += w[8] * Math.Abs(b2); + sum += w[12] * Math.Abs(b3); + + w = w[1..]; + } + + return sum; + } + + /// + /// Hadamard transform + /// Returns the weighted sum of the absolute value of transformed coefficients. + /// w[] contains a row-major 4 by 4 symmetric matrix. + /// + public static int TTransformVector128(Span inputA, Span inputB, Span w) + { + // Load and combine inputs. + Vector128 ina0 = Unsafe.As>(ref MemoryMarshal.GetReference(inputA)); + Vector128 ina1 = Unsafe.As>(ref MemoryMarshal.GetReference(inputA.Slice(WebpConstants.Bps, 16))); + Vector128 ina2 = Unsafe.As>(ref MemoryMarshal.GetReference(inputA.Slice(WebpConstants.Bps * 2, 16))); + Vector128 ina3 = Unsafe.As>(ref MemoryMarshal.GetReference(inputA.Slice(WebpConstants.Bps * 3, 16))).AsInt64(); + Vector128 inb0 = Unsafe.As>(ref MemoryMarshal.GetReference(inputB)); + Vector128 inb1 = Unsafe.As>(ref MemoryMarshal.GetReference(inputB.Slice(WebpConstants.Bps, 16))); + Vector128 inb2 = Unsafe.As>(ref MemoryMarshal.GetReference(inputB.Slice(WebpConstants.Bps * 2, 16))); + Vector128 inb3 = Unsafe.As>(ref MemoryMarshal.GetReference(inputB.Slice(WebpConstants.Bps * 3, 16))).AsInt64(); + + // Combine inA and inB (we'll do two transforms in parallel). + Vector128 inab0 = Vector128_.UnpackLow(ina0.AsInt32(), inb0.AsInt32()); + Vector128 inab1 = Vector128_.UnpackLow(ina1.AsInt32(), inb1.AsInt32()); + Vector128 inab2 = Vector128_.UnpackLow(ina2.AsInt32(), inb2.AsInt32()); + Vector128 inab3 = Vector128_.UnpackLow(ina3.AsInt32(), inb3.AsInt32()); + Vector128 tmp0 = Vector128.WidenLower(inab0.AsByte()).AsInt16(); + Vector128 tmp1 = Vector128.WidenLower(inab1.AsByte()).AsInt16(); + Vector128 tmp2 = Vector128.WidenLower(inab2.AsByte()).AsInt16(); + Vector128 tmp3 = Vector128.WidenLower(inab3.AsByte()).AsInt16(); + + // a00 a01 a02 a03 b00 b01 b02 b03 + // a10 a11 a12 a13 b10 b11 b12 b13 + // a20 a21 a22 a23 b20 b21 b22 b23 + // a30 a31 a32 a33 b30 b31 b32 b33 + // Vertical pass first to avoid a transpose (vertical and horizontal passes + // are commutative because w/kWeightY is symmetric) and subsequent transpose. + // Calculate a and b (two 4x4 at once). + Vector128 a0 = tmp0 + tmp2; + Vector128 a1 = tmp1 + tmp3; + Vector128 a2 = tmp1 - tmp3; + Vector128 a3 = tmp0 - tmp2; + Vector128 b0 = a0 + a1; + Vector128 b1 = a3 + a2; + Vector128 b2 = a3 - a2; + Vector128 b3 = a0 - a1; + + // a00 a01 a02 a03 b00 b01 b02 b03 + // a10 a11 a12 a13 b10 b11 b12 b13 + // a20 a21 a22 a23 b20 b21 b22 b23 + // a30 a31 a32 a33 b30 b31 b32 b33 + // Transpose the two 4x4. + Vp8Transpose_2_4x4_16bVector128(b0, b1, b2, b3, out Vector128 output0, out Vector128 output1, out Vector128 output2, out Vector128 output3); + + // a00 a10 a20 a30 b00 b10 b20 b30 + // a01 a11 a21 a31 b01 b11 b21 b31 + // a02 a12 a22 a32 b02 b12 b22 b32 + // a03 a13 a23 a33 b03 b13 b23 b33 + // Horizontal pass and difference of weighted sums. + Vector128 w0 = Unsafe.As>(ref MemoryMarshal.GetReference(w)); + Vector128 w8 = Unsafe.As>(ref MemoryMarshal.GetReference(w.Slice(8, 8))); + + // Calculate a and b (two 4x4 at once). + a0 = output0.AsInt16() + output2.AsInt16(); + a1 = output1.AsInt16() + output3.AsInt16(); + a2 = output1.AsInt16() - output3.AsInt16(); + a3 = output0.AsInt16() - output2.AsInt16(); + b0 = a0 + a1; + b1 = a3 + a2; + b2 = a3 - a2; + b3 = a0 - a1; + + // Separate the transforms of inA and inB. + Vector128 ab0 = Vector128_.UnpackLow(b0.AsInt64(), b1.AsInt64()); + Vector128 ab2 = Vector128_.UnpackLow(b2.AsInt64(), b3.AsInt64()); + Vector128 bb0 = Vector128_.UnpackHigh(b0.AsInt64(), b1.AsInt64()); + Vector128 bb2 = Vector128_.UnpackHigh(b2.AsInt64(), b3.AsInt64()); + + Vector128 ab0Abs = Vector128.Abs(ab0.AsInt16()); + Vector128 ab2Abs = Vector128.Abs(ab2.AsInt16()); + Vector128 b0Abs = Vector128.Abs(bb0.AsInt16()); + Vector128 bb2Abs = Vector128.Abs(bb2.AsInt16()); + + // weighted sums. + Vector128 ab0mulw0 = Vector128_.MultiplyAddAdjacent(ab0Abs, w0.AsInt16()); + Vector128 ab2mulw8 = Vector128_.MultiplyAddAdjacent(ab2Abs, w8.AsInt16()); + Vector128 b0mulw0 = Vector128_.MultiplyAddAdjacent(b0Abs, w0.AsInt16()); + Vector128 bb2mulw8 = Vector128_.MultiplyAddAdjacent(bb2Abs, w8.AsInt16()); + Vector128 ab0ab2Sum = ab0mulw0 + ab2mulw8; + Vector128 b0w0bb2w8Sum = b0mulw0 + bb2mulw8; + + // difference of weighted sums. + Vector128 result = ab0ab2Sum - b0w0bb2w8Sum; + + return ReduceSumVector128(result); + } + + // Transpose two 4x4 16b matrices horizontally stored in registers. + [MethodImpl(InliningOptions.ShortMethod)] + public static void Vp8Transpose_2_4x4_16bVector128(Vector128 b0, Vector128 b1, Vector128 b2, Vector128 b3, out Vector128 output0, out Vector128 output1, out Vector128 output2, out Vector128 output3) + { + // Transpose the two 4x4. + // a00 a01 a02 a03 b00 b01 b02 b03 + // a10 a11 a12 a13 b10 b11 b12 b13 + // a20 a21 a22 a23 b20 b21 b22 b23 + // a30 a31 a32 a33 b30 b31 b32 b33 + Vector128 transpose00 = Vector128_.UnpackLow(b0, b1); + Vector128 transpose01 = Vector128_.UnpackLow(b2, b3); + Vector128 transpose02 = Vector128_.UnpackHigh(b0, b1); + Vector128 transpose03 = Vector128_.UnpackHigh(b2, b3); + + // a00 a10 a01 a11 a02 a12 a03 a13 + // a20 a30 a21 a31 a22 a32 a23 a33 + // b00 b10 b01 b11 b02 b12 b03 b13 + // b20 b30 b21 b31 b22 b32 b23 b33 + Vector128 transpose10 = Vector128_.UnpackLow(transpose00.AsInt32(), transpose01.AsInt32()); + Vector128 transpose11 = Vector128_.UnpackLow(transpose02.AsInt32(), transpose03.AsInt32()); + Vector128 transpose12 = Vector128_.UnpackHigh(transpose00.AsInt32(), transpose01.AsInt32()); + Vector128 transpose13 = Vector128_.UnpackHigh(transpose02.AsInt32(), transpose03.AsInt32()); + + // a00 a10 a20 a30 a01 a11 a21 a31 + // b00 b10 b20 b30 b01 b11 b21 b31 + // a02 a12 a22 a32 a03 a13 a23 a33 + // b02 b12 a22 b32 b03 b13 b23 b33 + output0 = Vector128_.UnpackLow(transpose10.AsInt64(), transpose11.AsInt64()); + output1 = Vector128_.UnpackHigh(transpose10.AsInt64(), transpose11.AsInt64()); + output2 = Vector128_.UnpackLow(transpose12.AsInt64(), transpose13.AsInt64()); + output3 = Vector128_.UnpackHigh(transpose12.AsInt64(), transpose13.AsInt64()); + + // a00 a10 a20 a30 b00 b10 b20 b30 + // a01 a11 a21 a31 b01 b11 b21 b31 + // a02 a12 a22 a32 b02 b12 b22 b32 + // a03 a13 a23 a33 b03 b13 b23 b33 + } + + // Transforms (Paragraph 14.4). + // Does two transforms. + public static void TransformTwo(Span src, Span dst, Span scratch) + { + if (Vector128.IsHardwareAccelerated) + { + // This implementation makes use of 16-bit fixed point versions of two + // multiply constants: + // K1 = sqrt(2) * cos (pi/8) ~= 85627 / 2^16 + // K2 = sqrt(2) * sin (pi/8) ~= 35468 / 2^16 + // + // To be able to use signed 16-bit integers, we use the following trick to + // have constants within range: + // - Associated constants are obtained by subtracting the 16-bit fixed point + // version of one: + // k = K - (1 << 16) => K = k + (1 << 16) + // K1 = 85267 => k1 = 20091 + // K2 = 35468 => k2 = -30068 + // - The multiplication of a variable by a constant become the sum of the + // variable and the multiplication of that variable by the associated + // constant: + // (x * K) >> 16 = (x * (k + (1 << 16))) >> 16 = ((x * k ) >> 16) + x + + // Load and concatenate the transform coefficients (we'll do two transforms + // in parallel). + ref short srcRef = ref MemoryMarshal.GetReference(src); + Vector128 in0 = Vector128.Create(Unsafe.As(ref srcRef), 0); + Vector128 in1 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, 4)), 0); + Vector128 in2 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, 8)), 0); + Vector128 in3 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, 12)), 0); + + // a00 a10 a20 a30 x x x x + // a01 a11 a21 a31 x x x x + // a02 a12 a22 a32 x x x x + // a03 a13 a23 a33 x x x x + Vector128 inb0 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, 16)), 0); + Vector128 inb1 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, 20)), 0); + Vector128 inb2 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, 24)), 0); + Vector128 inb3 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, 28)), 0); + + in0 = Vector128_.UnpackLow(in0, inb0); + in1 = Vector128_.UnpackLow(in1, inb1); + in2 = Vector128_.UnpackLow(in2, inb2); + in3 = Vector128_.UnpackLow(in3, inb3); + + // a00 a10 a20 a30 b00 b10 b20 b30 + // a01 a11 a21 a31 b01 b11 b21 b31 + // a02 a12 a22 a32 b02 b12 b22 b32 + // a03 a13 a23 a33 b03 b13 b23 b33 + + // Vertical pass and subsequent transpose. + // First pass, c and d calculations are longer because of the "trick" multiplications. + Vector128 a = in0.AsInt16() + in2.AsInt16(); + Vector128 b = in0.AsInt16() - in2.AsInt16(); + + Vector128 k1 = Vector128.Create((short)20091); + Vector128 k2 = Vector128.Create((short)-30068); + + // c = MUL(in1, K2) - MUL(in3, K1) = MUL(in1, k2) - MUL(in3, k1) + in1 - in3 + Vector128 c1 = Vector128_.MultiplyHigh(in1.AsInt16(), k2); + Vector128 c2 = Vector128_.MultiplyHigh(in3.AsInt16(), k1); + Vector128 c3 = in1.AsInt16() - in3.AsInt16(); + Vector128 c4 = c1 - c2; + Vector128 c = c3.AsInt16() + c4; + + // d = MUL(in1, K1) + MUL(in3, K2) = MUL(in1, k1) + MUL(in3, k2) + in1 + in3 + Vector128 d1 = Vector128_.MultiplyHigh(in1.AsInt16(), k1); + Vector128 d2 = Vector128_.MultiplyHigh(in3.AsInt16(), k2); + Vector128 d3 = in1.AsInt16() + in3.AsInt16(); + Vector128 d4 = d1 + d2; + Vector128 d = d3 + d4; + + // Second pass. + Vector128 tmp0 = a.AsInt16() + d; + Vector128 tmp1 = b.AsInt16() + c; + Vector128 tmp2 = b.AsInt16() - c; + Vector128 tmp3 = a.AsInt16() - d; + + // Transpose the two 4x4. + Vp8Transpose_2_4x4_16bVector128(tmp0, tmp1, tmp2, tmp3, out Vector128 t0, out Vector128 t1, out Vector128 t2, out Vector128 t3); + + // Horizontal pass and subsequent transpose. + // First pass, c and d calculations are longer because of the "trick" multiplications. + Vector128 dc = t0.AsInt16() + Vector128.Create((short)4); + a = dc + t2.AsInt16(); + b = dc - t2.AsInt16(); + + // c = MUL(T1, K2) - MUL(T3, K1) = MUL(T1, k2) - MUL(T3, k1) + T1 - T3 + c1 = Vector128_.MultiplyHigh(t1.AsInt16(), k2); + c2 = Vector128_.MultiplyHigh(t3.AsInt16(), k1); + c3 = t1.AsInt16() - t3.AsInt16(); + c4 = c1 - c2; + c = c3 + c4; + + // d = MUL(T1, K1) + MUL(T3, K2) = MUL(T1, k1) + MUL(T3, k2) + T1 + T3 + d1 = Vector128_.MultiplyHigh(t1.AsInt16(), k1); + d2 = Vector128_.MultiplyHigh(t3.AsInt16(), k2); + d3 = t1.AsInt16() + t3.AsInt16(); + d4 = d1 + d2; + d = d3 + d4; + + // Second pass. + tmp0 = a + d; + tmp1 = b + c; + tmp2 = b - c; + tmp3 = a - d; + Vector128 shifted0 = Vector128.ShiftRightArithmetic(tmp0, 3); + Vector128 shifted1 = Vector128.ShiftRightArithmetic(tmp1, 3); + Vector128 shifted2 = Vector128.ShiftRightArithmetic(tmp2, 3); + Vector128 shifted3 = Vector128.ShiftRightArithmetic(tmp3, 3); + + // Transpose the two 4x4. + Vp8Transpose_2_4x4_16bVector128(shifted0, shifted1, shifted2, shifted3, out t0, out t1, out t2, out t3); + + // Add inverse transform to 'dst' and store. + // Load the reference(s). + // Load eight bytes/pixels per line. + ref byte dstRef = ref MemoryMarshal.GetReference(dst); + Vector128 dst0 = Vector128.Create(Unsafe.As(ref dstRef), 0).AsByte(); + Vector128 dst1 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref dstRef, WebpConstants.Bps)), 0).AsByte(); + Vector128 dst2 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref dstRef, WebpConstants.Bps * 2)), 0).AsByte(); + Vector128 dst3 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref dstRef, WebpConstants.Bps * 3)), 0).AsByte(); + + // Convert to 16b. + dst0 = Vector128_.UnpackLow(dst0, Vector128.Zero); + dst1 = Vector128_.UnpackLow(dst1, Vector128.Zero); + dst2 = Vector128_.UnpackLow(dst2, Vector128.Zero); + dst3 = Vector128_.UnpackLow(dst3, Vector128.Zero); + + // Add the inverse transform(s). + dst0 = (dst0.AsInt16() + t0.AsInt16()).AsByte(); + dst1 = (dst1.AsInt16() + t1.AsInt16()).AsByte(); + dst2 = (dst2.AsInt16() + t2.AsInt16()).AsByte(); + dst3 = (dst3.AsInt16() + t3.AsInt16()).AsByte(); + + // Unsigned saturate to 8b. + dst0 = Vector128_.PackUnsignedSaturate(dst0.AsInt16(), dst0.AsInt16()); + dst1 = Vector128_.PackUnsignedSaturate(dst1.AsInt16(), dst1.AsInt16()); + dst2 = Vector128_.PackUnsignedSaturate(dst2.AsInt16(), dst2.AsInt16()); + dst3 = Vector128_.PackUnsignedSaturate(dst3.AsInt16(), dst3.AsInt16()); + + // Store the results. + // Store eight bytes/pixels per line. + ref byte outputRef = ref MemoryMarshal.GetReference(dst); + Unsafe.As>(ref outputRef) = dst0.GetLower(); + Unsafe.As>(ref Unsafe.Add(ref outputRef, WebpConstants.Bps)) = dst1.GetLower(); + Unsafe.As>(ref Unsafe.Add(ref outputRef, WebpConstants.Bps * 2)) = dst2.GetLower(); + Unsafe.As>(ref Unsafe.Add(ref outputRef, WebpConstants.Bps * 3)) = dst3.GetLower(); + } + else + { + TransformOne(src, dst, scratch); + TransformOne(src[16..], dst[4..], scratch); + } + } + + public static void TransformOne(Span src, Span dst, Span scratch) + { + if (Vector128.IsHardwareAccelerated) + { + // Load and concatenate the transform coefficients. + ref short srcRef = ref MemoryMarshal.GetReference(src); + Vector128 in0 = Vector128.Create(Unsafe.As(ref srcRef), 0); + Vector128 in1 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, 4)), 0); + Vector128 in2 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, 8)), 0); + Vector128 in3 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, 12)), 0); + + // a00 a10 a20 a30 x x x x + // a01 a11 a21 a31 x x x x + // a02 a12 a22 a32 x x x x + // a03 a13 a23 a33 x x x x + + // Vertical pass and subsequent transpose. + // First pass, c and d calculations are longer because of the "trick" multiplications. + Vector128 a = in0.AsInt16() + in2.AsInt16(); + Vector128 b = in0.AsInt16() - in2.AsInt16(); + + Vector128 k1 = Vector128.Create((short)20091); + Vector128 k2 = Vector128.Create((short)-30068); + + // c = MUL(in1, K2) - MUL(in3, K1) = MUL(in1, k2) - MUL(in3, k1) + in1 - in3 + Vector128 c1 = Vector128_.MultiplyHigh(in1.AsInt16(), k2); + Vector128 c2 = Vector128_.MultiplyHigh(in3.AsInt16(), k1); + Vector128 c3 = in1.AsInt16() - in3.AsInt16(); + Vector128 c4 = c1 - c2; + Vector128 c = c3.AsInt16() + c4; + + // d = MUL(in1, K1) + MUL(in3, K2) = MUL(in1, k1) + MUL(in3, k2) + in1 + in3 + Vector128 d1 = Vector128_.MultiplyHigh(in1.AsInt16(), k1); + Vector128 d2 = Vector128_.MultiplyHigh(in3.AsInt16(), k2); + Vector128 d3 = in1.AsInt16() + in3.AsInt16(); + Vector128 d4 = d1 + d2; + Vector128 d = d3 + d4; + + // Second pass. + Vector128 tmp0 = a.AsInt16() + d; + Vector128 tmp1 = b.AsInt16() + c; + Vector128 tmp2 = b.AsInt16() - c; + Vector128 tmp3 = a.AsInt16() - d; + + // Transpose the two 4x4. + Vp8Transpose_2_4x4_16bVector128(tmp0, tmp1, tmp2, tmp3, out Vector128 t0, out Vector128 t1, out Vector128 t2, out Vector128 t3); + + // Horizontal pass and subsequent transpose. + // First pass, c and d calculations are longer because of the "trick" multiplications. + Vector128 dc = t0.AsInt16() + Vector128.Create((short)4); + a = dc + t2.AsInt16(); + b = dc - t2.AsInt16(); + + // c = MUL(T1, K2) - MUL(T3, K1) = MUL(T1, k2) - MUL(T3, k1) + T1 - T3 + c1 = Vector128_.MultiplyHigh(t1.AsInt16(), k2); + c2 = Vector128_.MultiplyHigh(t3.AsInt16(), k1); + c3 = t1.AsInt16() - t3.AsInt16(); + c4 = c1 - c2; + c = c3 + c4; + + // d = MUL(T1, K1) + MUL(T3, K2) = MUL(T1, k1) + MUL(T3, k2) + T1 + T3 + d1 = Vector128_.MultiplyHigh(t1.AsInt16(), k1); + d2 = Vector128_.MultiplyHigh(t3.AsInt16(), k2); + d3 = t1.AsInt16() + t3.AsInt16(); + d4 = d1 + d2; + d = d3 + d4; + + // Second pass. + tmp0 = a + d; + tmp1 = b + c; + tmp2 = b - c; + tmp3 = a - d; + Vector128 shifted0 = Vector128.ShiftRightArithmetic(tmp0, 3); + Vector128 shifted1 = Vector128.ShiftRightArithmetic(tmp1, 3); + Vector128 shifted2 = Vector128.ShiftRightArithmetic(tmp2, 3); + Vector128 shifted3 = Vector128.ShiftRightArithmetic(tmp3, 3); + + // Transpose the two 4x4. + Vp8Transpose_2_4x4_16bVector128(shifted0, shifted1, shifted2, shifted3, out t0, out t1, out t2, out t3); + + // Add inverse transform to 'dst' and store. + // Load the reference(s). + // Load four bytes/pixels per line. + ref byte dstRef = ref MemoryMarshal.GetReference(dst); + Vector128 dst0 = Vector128.CreateScalar(Unsafe.As(ref dstRef)).AsByte(); + Vector128 dst1 = Vector128.CreateScalar(Unsafe.As(ref Unsafe.Add(ref dstRef, WebpConstants.Bps))).AsByte(); + Vector128 dst2 = Vector128.CreateScalar(Unsafe.As(ref Unsafe.Add(ref dstRef, WebpConstants.Bps * 2))).AsByte(); + Vector128 dst3 = Vector128.CreateScalar(Unsafe.As(ref Unsafe.Add(ref dstRef, WebpConstants.Bps * 3))).AsByte(); + + // Convert to 16b. + dst0 = Vector128_.UnpackLow(dst0, Vector128.Zero); + dst1 = Vector128_.UnpackLow(dst1, Vector128.Zero); + dst2 = Vector128_.UnpackLow(dst2, Vector128.Zero); + dst3 = Vector128_.UnpackLow(dst3, Vector128.Zero); + + // Add the inverse transform(s). + dst0 = (dst0.AsInt16() + t0.AsInt16()).AsByte(); + dst1 = (dst1.AsInt16() + t1.AsInt16()).AsByte(); + dst2 = (dst2.AsInt16() + t2.AsInt16()).AsByte(); + dst3 = (dst3.AsInt16() + t3.AsInt16()).AsByte(); + + // Unsigned saturate to 8b. + dst0 = Vector128_.PackUnsignedSaturate(dst0.AsInt16(), dst0.AsInt16()); + dst1 = Vector128_.PackUnsignedSaturate(dst1.AsInt16(), dst1.AsInt16()); + dst2 = Vector128_.PackUnsignedSaturate(dst2.AsInt16(), dst2.AsInt16()); + dst3 = Vector128_.PackUnsignedSaturate(dst3.AsInt16(), dst3.AsInt16()); + + // Store the results. + // Store four bytes/pixels per line. + ref byte outputRef = ref MemoryMarshal.GetReference(dst); + int output0 = dst0.AsInt32().ToScalar(); + int output1 = dst1.AsInt32().ToScalar(); + int output2 = dst2.AsInt32().ToScalar(); + int output3 = dst3.AsInt32().ToScalar(); + Unsafe.As(ref outputRef) = output0; + Unsafe.As(ref Unsafe.Add(ref outputRef, WebpConstants.Bps)) = output1; + Unsafe.As(ref Unsafe.Add(ref outputRef, WebpConstants.Bps * 2)) = output2; + Unsafe.As(ref Unsafe.Add(ref outputRef, WebpConstants.Bps * 3)) = output3; + } + else + { + Span tmp = scratch[..16]; + int tmpOffset = 0; + for (int srcOffset = 0; srcOffset < 4; srcOffset++) + { + // vertical pass + int srcOffsetPlus4 = srcOffset + 4; + int srcOffsetPlus8 = srcOffset + 8; + int srcOffsetPlus12 = srcOffset + 12; + int a = src[srcOffset] + src[srcOffsetPlus8]; + int b = src[srcOffset] - src[srcOffsetPlus8]; + int c = Mul2(src[srcOffsetPlus4]) - Mul1(src[srcOffsetPlus12]); + int d = Mul1(src[srcOffsetPlus4]) + Mul2(src[srcOffsetPlus12]); + tmp[tmpOffset++] = a + d; + tmp[tmpOffset++] = b + c; + tmp[tmpOffset++] = b - c; + tmp[tmpOffset++] = a - d; + } + + // Each pass is expanding the dynamic range by ~3.85 (upper bound). + // The exact value is (2. + (20091 + 35468) / 65536). + // After the second pass, maximum interval is [-3794, 3794], assuming + // an input in [-2048, 2047] interval. We then need to add a dst value in the [0, 255] range. + // In the worst case scenario, the input to clip_8b() can be as large as [-60713, 60968]. + tmpOffset = 0; + int dstOffset = 0; + for (int i = 0; i < 4; i++) + { + // horizontal pass + int tmpOffsetPlus4 = tmpOffset + 4; + int tmpOffsetPlus8 = tmpOffset + 8; + int tmpOffsetPlus12 = tmpOffset + 12; + int dc = tmp[tmpOffset] + 4; + int a = dc + tmp[tmpOffsetPlus8]; + int b = dc - tmp[tmpOffsetPlus8]; + int c = Mul2(tmp[tmpOffsetPlus4]) - Mul1(tmp[tmpOffsetPlus12]); + int d = Mul1(tmp[tmpOffsetPlus4]) + Mul2(tmp[tmpOffsetPlus12]); + Store(dst[dstOffset..], 0, 0, a + d); + Store(dst[dstOffset..], 1, 0, b + c); + Store(dst[dstOffset..], 2, 0, b - c); + Store(dst[dstOffset..], 3, 0, a - d); + tmpOffset++; + + dstOffset += WebpConstants.Bps; + } + } + } + + public static void TransformDc(Span src, Span dst) + { + int dc = src[0] + 4; + for (int j = 0; j < 4; j++) + { + for (int i = 0; i < 4; i++) + { + Store(dst, i, j, dc); + } + } + } + + // Simplified transform when only src[0], src[1] and src[4] are non-zero + public static void TransformAc3(Span src, Span dst) + { + int a = src[0] + 4; + int c4 = Mul2(src[4]); + int d4 = Mul1(src[4]); + int c1 = Mul2(src[1]); + int d1 = Mul1(src[1]); + Store2(dst, 0, a + d4, d1, c1); + Store2(dst, 1, a + c4, d1, c1); + Store2(dst, 2, a - c4, d1, c1); + Store2(dst, 3, a - d4, d1, c1); + } + + public static void TransformUv(Span src, Span dst, Span scratch) + { + TransformTwo(src[..], dst, scratch); + TransformTwo(src[(2 * 16)..], dst[(4 * WebpConstants.Bps)..], scratch); + } + + public static void TransformDcuv(Span src, Span dst) + { + if (src[0 * 16] != 0) + { + TransformDc(src[..], dst); + } + + if (src[1 * 16] != 0) + { + TransformDc(src[(1 * 16)..], dst[4..]); + } + + if (src[2 * 16] != 0) + { + TransformDc(src[(2 * 16)..], dst[(4 * WebpConstants.Bps)..]); + } + + if (src[3 * 16] != 0) + { + TransformDc(src[(3 * 16)..], dst[((4 * WebpConstants.Bps) + 4)..]); + } + } + + // Simple In-loop filtering (Paragraph 15.2) + public static void SimpleVFilter16(Span p, int offset, int stride, int thresh) + { + if (Vector128.IsHardwareAccelerated) + { + // Load. + ref byte pRef = ref Unsafe.Add(ref MemoryMarshal.GetReference(p), (uint)offset); + + Vector128 p1 = Unsafe.As>(ref Unsafe.Subtract(ref pRef, 2 * stride)); + Vector128 p0 = Unsafe.As>(ref Unsafe.Subtract(ref pRef, stride)); + Vector128 q0 = Unsafe.As>(ref pRef); + Vector128 q1 = Unsafe.As>(ref Unsafe.Add(ref pRef, (uint)stride)); + + DoFilter2Vector128(ref p1, ref p0, ref q0, ref q1, thresh); + + // Store. + ref byte outputRef = ref Unsafe.Add(ref MemoryMarshal.GetReference(p), (uint)offset); + Unsafe.As>(ref Unsafe.Subtract(ref outputRef, stride)) = p0.AsSByte(); + Unsafe.As>(ref outputRef) = q0.AsSByte(); + } + else + { + int thresh2 = (2 * thresh) + 1; + int end = 16 + offset; + for (int i = offset; i < end; i++) + { + if (NeedsFilter(p, i, stride, thresh2)) + { + DoFilter2(p, i, stride); + } + } + } + } + + public static void SimpleHFilter16(Span p, int offset, int stride, int thresh) + { + if (Vector128.IsHardwareAccelerated) + { + // Beginning of p1 + ref byte pRef = ref Unsafe.Add(ref MemoryMarshal.GetReference(p), (uint)(offset - 2)); + + Load16x4Vector128(ref pRef, ref Unsafe.Add(ref pRef, 8 * (uint)stride), stride, out Vector128 p1, out Vector128 p0, out Vector128 q0, out Vector128 q1); + DoFilter2Vector128(ref p1, ref p0, ref q0, ref q1, thresh); + Store16x4Vector128(p1, p0, q0, q1, ref pRef, ref Unsafe.Add(ref pRef, 8 * (uint)stride), stride); + } + else + { + int thresh2 = (2 * thresh) + 1; + int end = offset + (16 * stride); + for (int i = offset; i < end; i += stride) + { + if (NeedsFilter(p, i, 1, thresh2)) + { + DoFilter2(p, i, 1); + } + } + } + } + + public static void SimpleVFilter16i(Span p, int offset, int stride, int thresh) + { + if (Vector128.IsHardwareAccelerated) + { + for (int k = 3; k > 0; k--) + { + offset += 4 * stride; + SimpleVFilter16(p, offset, stride, thresh); + } + } + else + { + for (int k = 3; k > 0; k--) + { + offset += 4 * stride; + SimpleVFilter16(p, offset, stride, thresh); + } + } + } + + public static void SimpleHFilter16i(Span p, int offset, int stride, int thresh) + { + if (Vector128.IsHardwareAccelerated) + { + for (int k = 3; k > 0; k--) + { + offset += 4; + SimpleHFilter16(p, offset, stride, thresh); + } + } + else + { + for (int k = 3; k > 0; k--) + { + offset += 4; + SimpleHFilter16(p, offset, stride, thresh); + } + } + } + + // On macroblock edges. + [MethodImpl(InliningOptions.ShortMethod)] + public static void VFilter16(Span p, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + if (Vector128.IsHardwareAccelerated) + { + ref byte pRef = ref MemoryMarshal.GetReference(p); + Vector128 t1 = Unsafe.As>(ref Unsafe.Add(ref pRef, (uint)(offset - (4 * stride)))); + Vector128 p2 = Unsafe.As>(ref Unsafe.Add(ref pRef, (uint)(offset - (3 * stride)))); + Vector128 p1 = Unsafe.As>(ref Unsafe.Add(ref pRef, (uint)(offset - (2 * stride)))); + Vector128 p0 = Unsafe.As>(ref Unsafe.Add(ref pRef, (uint)(offset - stride))); + + Vector128 mask = AbsVector128(p1, p0); + mask = Vector128.Max(mask, AbsVector128(t1, p2)); + mask = Vector128.Max(mask, AbsVector128(p2, p1)); + + Vector128 q0 = Unsafe.As>(ref Unsafe.Add(ref pRef, (uint)offset)); + Vector128 q1 = Unsafe.As>(ref Unsafe.Add(ref pRef, (uint)(offset + stride))); + Vector128 q2 = Unsafe.As>(ref Unsafe.Add(ref pRef, (uint)(offset + (2 * stride)))); + t1 = Unsafe.As>(ref Unsafe.Add(ref pRef, (uint)(offset + (3 * stride)))); + + mask = Vector128.Max(mask, AbsVector128(q1, q0)); + mask = Vector128.Max(mask, AbsVector128(t1, q2)); + mask = Vector128.Max(mask, AbsVector128(q2, q1)); + + ComplexMaskVector128(p1, p0, q0, q1, thresh, ithresh, ref mask); + DoFilter6Vector128(ref p2, ref p1, ref p0, ref q0, ref q1, ref q2, mask, hevThresh); + + // Store. + ref byte outputRef = ref MemoryMarshal.GetReference(p); + Unsafe.As>(ref Unsafe.Add(ref outputRef, (uint)(offset - (3 * stride)))) = p2.AsInt32(); + Unsafe.As>(ref Unsafe.Add(ref outputRef, (uint)(offset - (2 * stride)))) = p1.AsInt32(); + Unsafe.As>(ref Unsafe.Add(ref outputRef, (uint)(offset - stride))) = p0.AsInt32(); + Unsafe.As>(ref Unsafe.Add(ref outputRef, (uint)offset)) = q0.AsInt32(); + Unsafe.As>(ref Unsafe.Add(ref outputRef, (uint)(offset + stride))) = q1.AsInt32(); + Unsafe.As>(ref Unsafe.Add(ref outputRef, (uint)(offset + (2 * stride)))) = q2.AsInt32(); + } + else + { + FilterLoop26(p, offset, stride, 1, 16, thresh, ithresh, hevThresh); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void HFilter16(Span p, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + if (Vector128.IsHardwareAccelerated) + { + ref byte pRef = ref MemoryMarshal.GetReference(p); + ref byte bRef = ref Unsafe.Add(ref pRef, (uint)offset - 4); + Load16x4Vector128(ref bRef, ref Unsafe.Add(ref bRef, 8 * (uint)stride), stride, out Vector128 p3, out Vector128 p2, out Vector128 p1, out Vector128 p0); + + Vector128 mask = AbsVector128(p1, p0); + mask = Vector128.Max(mask, AbsVector128(p3, p2)); + mask = Vector128.Max(mask, AbsVector128(p2, p1)); + + Load16x4Vector128(ref Unsafe.Add(ref pRef, (uint)offset), ref Unsafe.Add(ref pRef, (uint)(offset + (8 * stride))), stride, out Vector128 q0, out Vector128 q1, out Vector128 q2, out Vector128 q3); + + mask = Vector128.Max(mask, AbsVector128(q1, q0)); + mask = Vector128.Max(mask, AbsVector128(q3, q2)); + mask = Vector128.Max(mask, AbsVector128(q2, q1)); + + ComplexMaskVector128(p1, p0, q0, q1, thresh, ithresh, ref mask); + DoFilter6Vector128(ref p2, ref p1, ref p0, ref q0, ref q1, ref q2, mask, hevThresh); + + Store16x4Vector128(p3, p2, p1, p0, ref bRef, ref Unsafe.Add(ref bRef, 8 * (uint)stride), stride); + Store16x4Vector128(q0, q1, q2, q3, ref Unsafe.Add(ref pRef, (uint)offset), ref Unsafe.Add(ref pRef, (uint)(offset + (8 * stride))), stride); + } + else + { + FilterLoop26(p, offset, 1, stride, 16, thresh, ithresh, hevThresh); + } + } + + public static void VFilter16i(Span p, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + if (Vector128.IsHardwareAccelerated) + { + ref byte pRef = ref MemoryMarshal.GetReference(p); + Vector128 p3 = Unsafe.As>(ref Unsafe.Add(ref pRef, (uint)offset)); + Vector128 p2 = Unsafe.As>(ref Unsafe.Add(ref pRef, (uint)(offset + stride))); + Vector128 p1 = Unsafe.As>(ref Unsafe.Add(ref pRef, (uint)(offset + (2 * stride)))); + Vector128 p0 = Unsafe.As>(ref Unsafe.Add(ref pRef, (uint)(offset + (3 * stride)))); + + for (int k = 3; k > 0; k--) + { + // Beginning of p1. + Span b = p[(offset + (2 * stride))..]; + offset += 4 * stride; + + Vector128 mask = AbsVector128(p0, p1); + mask = Vector128.Max(mask, AbsVector128(p3, p2)); + mask = Vector128.Max(mask, AbsVector128(p2, p1)); + + p3 = Unsafe.As>(ref Unsafe.Add(ref pRef, (uint)offset)); + p2 = Unsafe.As>(ref Unsafe.Add(ref pRef, (uint)(offset + stride))); + Vector128 tmp1 = Unsafe.As>(ref Unsafe.Add(ref pRef, (uint)(offset + (2 * stride)))); + Vector128 tmp2 = Unsafe.As>(ref Unsafe.Add(ref pRef, (uint)(offset + (3 * stride)))); + + mask = Vector128.Max(mask, AbsVector128(tmp1, tmp2)); + mask = Vector128.Max(mask, AbsVector128(p3, p2)); + mask = Vector128.Max(mask, AbsVector128(p2, tmp1)); + + // p3 and p2 are not just temporary variables here: they will be + // re-used for next span. And q2/q3 will become p1/p0 accordingly. + ComplexMaskVector128(p1, p0, p3, p2, thresh, ithresh, ref mask); + DoFilter4Vector128(ref p1, ref p0, ref p3, ref p2, mask, hevThresh); + + // Store. + ref byte outputRef = ref MemoryMarshal.GetReference(b); + Unsafe.As>(ref outputRef) = p1.AsInt32(); + Unsafe.As>(ref Unsafe.Add(ref outputRef, (uint)stride)) = p0.AsInt32(); + Unsafe.As>(ref Unsafe.Add(ref outputRef, (uint)(stride * 2))) = p3.AsInt32(); + Unsafe.As>(ref Unsafe.Add(ref outputRef, (uint)(stride * 3))) = p2.AsInt32(); + + // Rotate samples. + p1 = tmp1; + p0 = tmp2; + } + } + else + { + for (int k = 3; k > 0; k--) + { + offset += 4 * stride; + FilterLoop24(p, offset, stride, 1, 16, thresh, ithresh, hevThresh); + } + } + } + + public static void HFilter16i(Span p, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + if (Vector128.IsHardwareAccelerated) + { + ref byte pRef = ref MemoryMarshal.GetReference(p); + Load16x4Vector128(ref Unsafe.Add(ref pRef, (uint)offset), ref Unsafe.Add(ref pRef, (uint)(offset + (8 * stride))), stride, out Vector128 p3, out Vector128 p2, out Vector128 p1, out Vector128 p0); + + Vector128 mask; + for (int k = 3; k > 0; k--) + { + // Beginning of p1. + ref byte bRef = ref Unsafe.Add(ref pRef, (uint)offset + 2); + + // Beginning of q0 (and next span). + offset += 4; + + // Compute partial mask. + mask = AbsVector128(p1, p0); + mask = Vector128.Max(mask, AbsVector128(p3, p2)); + mask = Vector128.Max(mask, AbsVector128(p2, p1)); + + Load16x4Vector128(ref Unsafe.Add(ref pRef, (uint)offset), ref Unsafe.Add(ref pRef, (uint)(offset + (8 * stride))), stride, out p3, out p2, out Vector128 tmp1, out Vector128 tmp2); + + mask = Vector128.Max(mask, AbsVector128(tmp1, tmp2)); + mask = Vector128.Max(mask, AbsVector128(p3, p2)); + mask = Vector128.Max(mask, AbsVector128(p2, tmp1)); + + ComplexMaskVector128(p1, p0, p3, p2, thresh, ithresh, ref mask); + DoFilter4Vector128(ref p1, ref p0, ref p3, ref p2, mask, hevThresh); + + Store16x4Vector128(p1, p0, p3, p2, ref bRef, ref Unsafe.Add(ref bRef, 8 * (uint)stride), stride); + + // Rotate samples. + p1 = tmp1; + p0 = tmp2; + } + } + else + { + for (int k = 3; k > 0; k--) + { + offset += 4; + FilterLoop24(p, offset, 1, stride, 16, thresh, ithresh, hevThresh); + } + } + } + + // 8-pixels wide variant, for chroma filtering. + [MethodImpl(InliningOptions.ShortMethod)] + public static void VFilter8(Span u, Span v, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + if (Vector128.IsHardwareAccelerated) + { + // Load uv h-edges. + ref byte uRef = ref MemoryMarshal.GetReference(u); + ref byte vRef = ref MemoryMarshal.GetReference(v); + Vector128 t1 = LoadUvEdgeVector128(ref uRef, ref vRef, offset - (4 * stride)); + Vector128 p2 = LoadUvEdgeVector128(ref uRef, ref vRef, offset - (3 * stride)); + Vector128 p1 = LoadUvEdgeVector128(ref uRef, ref vRef, offset - (2 * stride)); + Vector128 p0 = LoadUvEdgeVector128(ref uRef, ref vRef, offset - stride); + + Vector128 mask = AbsVector128(p1, p0); + mask = Vector128.Max(mask, AbsVector128(t1, p2)); + mask = Vector128.Max(mask, AbsVector128(p2, p1)); + + Vector128 q0 = LoadUvEdgeVector128(ref uRef, ref vRef, offset); + Vector128 q1 = LoadUvEdgeVector128(ref uRef, ref vRef, offset + stride); + Vector128 q2 = LoadUvEdgeVector128(ref uRef, ref vRef, offset + (2 * stride)); + t1 = LoadUvEdgeVector128(ref uRef, ref vRef, offset + (3 * stride)); + + mask = Vector128.Max(mask, AbsVector128(q1, q0)); + mask = Vector128.Max(mask, AbsVector128(t1, q2)); + mask = Vector128.Max(mask, AbsVector128(q2, q1)); + + ComplexMaskVector128(p1, p0, q0, q1, thresh, ithresh, ref mask); + DoFilter6Vector128(ref p2, ref p1, ref p0, ref q0, ref q1, ref q2, mask, hevThresh); + + // Store. + StoreUvVector128(p2, ref uRef, ref vRef, offset - (3 * stride)); + StoreUvVector128(p1, ref uRef, ref vRef, offset - (2 * stride)); + StoreUvVector128(p0, ref uRef, ref vRef, offset - stride); + StoreUvVector128(q0, ref uRef, ref vRef, offset); + StoreUvVector128(q1, ref uRef, ref vRef, offset + (1 * stride)); + StoreUvVector128(q2, ref uRef, ref vRef, offset + (2 * stride)); + } + else + { + FilterLoop26(u, offset, stride, 1, 8, thresh, ithresh, hevThresh); + FilterLoop26(v, offset, stride, 1, 8, thresh, ithresh, hevThresh); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void HFilter8(Span u, Span v, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + if (Vector128.IsHardwareAccelerated) + { + ref byte uRef = ref MemoryMarshal.GetReference(u); + ref byte vRef = ref MemoryMarshal.GetReference(v); + Load16x4Vector128(ref Unsafe.Add(ref uRef, (uint)offset - 4), ref Unsafe.Add(ref vRef, (uint)offset - 4), stride, out Vector128 p3, out Vector128 p2, out Vector128 p1, out Vector128 p0); + + Vector128 mask = AbsVector128(p1, p0); + mask = Vector128.Max(mask, AbsVector128(p3, p2)); + mask = Vector128.Max(mask, AbsVector128(p2, p1)); + + Load16x4Vector128(ref Unsafe.Add(ref uRef, (uint)offset), ref Unsafe.Add(ref vRef, (uint)offset), stride, out Vector128 q0, out Vector128 q1, out Vector128 q2, out Vector128 q3); + + mask = Vector128.Max(mask, AbsVector128(q1, q0)); + mask = Vector128.Max(mask, AbsVector128(q3, q2)); + mask = Vector128.Max(mask, AbsVector128(q2, q1)); + + ComplexMaskVector128(p1, p0, q0, q1, thresh, ithresh, ref mask); + DoFilter6Vector128(ref p2, ref p1, ref p0, ref q0, ref q1, ref q2, mask, hevThresh); + + Store16x4Vector128(p3, p2, p1, p0, ref Unsafe.Add(ref uRef, (uint)offset - 4), ref Unsafe.Add(ref vRef, (uint)offset - 4), stride); + Store16x4Vector128(q0, q1, q2, q3, ref Unsafe.Add(ref uRef, (uint)offset), ref Unsafe.Add(ref vRef, (uint)offset), stride); + } + else + { + FilterLoop26(u, offset, 1, stride, 8, thresh, ithresh, hevThresh); + FilterLoop26(v, offset, 1, stride, 8, thresh, ithresh, hevThresh); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void VFilter8i(Span u, Span v, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + if (Vector128.IsHardwareAccelerated) + { + // Load uv h-edges. + ref byte uRef = ref MemoryMarshal.GetReference(u); + ref byte vRef = ref MemoryMarshal.GetReference(v); + Vector128 t2 = LoadUvEdgeVector128(ref uRef, ref vRef, offset); + Vector128 t1 = LoadUvEdgeVector128(ref uRef, ref vRef, offset + stride); + Vector128 p1 = LoadUvEdgeVector128(ref uRef, ref vRef, offset + (stride * 2)); + Vector128 p0 = LoadUvEdgeVector128(ref uRef, ref vRef, offset + (stride * 3)); + + Vector128 mask = AbsVector128(p1, p0); + mask = Vector128.Max(mask, AbsVector128(t2, t1)); + mask = Vector128.Max(mask, AbsVector128(t1, p1)); + + offset += 4 * stride; + + Vector128 q0 = LoadUvEdgeVector128(ref uRef, ref vRef, offset); + Vector128 q1 = LoadUvEdgeVector128(ref uRef, ref vRef, offset + stride); + t1 = LoadUvEdgeVector128(ref uRef, ref vRef, offset + (stride * 2)); + t2 = LoadUvEdgeVector128(ref uRef, ref vRef, offset + (stride * 3)); + + mask = Vector128.Max(mask, AbsVector128(q1, q0)); + mask = Vector128.Max(mask, AbsVector128(t2, t1)); + mask = Vector128.Max(mask, AbsVector128(t1, q1)); + + ComplexMaskVector128(p1, p0, q0, q1, thresh, ithresh, ref mask); + DoFilter4Vector128(ref p1, ref p0, ref q0, ref q1, mask, hevThresh); + + // Store. + StoreUvVector128(p1, ref uRef, ref vRef, offset + (-2 * stride)); + StoreUvVector128(p0, ref uRef, ref vRef, offset + (-1 * stride)); + StoreUvVector128(q0, ref uRef, ref vRef, offset); + StoreUvVector128(q1, ref uRef, ref vRef, offset + stride); + } + else + { + int offset4mulstride = offset + (4 * stride); + FilterLoop24(u, offset4mulstride, stride, 1, 8, thresh, ithresh, hevThresh); + FilterLoop24(v, offset4mulstride, stride, 1, 8, thresh, ithresh, hevThresh); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void HFilter8i(Span u, Span v, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + if (Vector128.IsHardwareAccelerated) + { + ref byte uRef = ref MemoryMarshal.GetReference(u); + ref byte vRef = ref MemoryMarshal.GetReference(v); + Load16x4Vector128(ref Unsafe.Add(ref uRef, (uint)offset), ref Unsafe.Add(ref vRef, (uint)offset), stride, out Vector128 t2, out Vector128 t1, out Vector128 p1, out Vector128 p0); + + Vector128 mask = AbsVector128(p1, p0); + mask = Vector128.Max(mask, AbsVector128(t2, t1)); + mask = Vector128.Max(mask, AbsVector128(t1, p1)); + + // Beginning of q0. + offset += 4; + + Load16x4Vector128(ref Unsafe.Add(ref uRef, (uint)offset), ref Unsafe.Add(ref vRef, (uint)offset), stride, out Vector128 q0, out Vector128 q1, out t1, out t2); + + mask = Vector128.Max(mask, AbsVector128(q1, q0)); + mask = Vector128.Max(mask, AbsVector128(t2, t1)); + mask = Vector128.Max(mask, AbsVector128(t1, q1)); + + ComplexMaskVector128(p1, p0, q0, q1, thresh, ithresh, ref mask); + DoFilter4Vector128(ref p1, ref p0, ref q0, ref q1, mask, hevThresh); + + // Beginning of p1. + offset -= 2; + Store16x4Vector128(p1, p0, q0, q1, ref Unsafe.Add(ref uRef, (uint)offset), ref Unsafe.Add(ref vRef, (uint)offset), stride); + } + else + { + int offsetPlus4 = offset + 4; + FilterLoop24(u, offsetPlus4, 1, stride, 8, thresh, ithresh, hevThresh); + FilterLoop24(v, offsetPlus4, 1, stride, 8, thresh, ithresh, hevThresh); + } + } + + public static void Mean16x4(Span input, Span dc) + { + if (Vector128.IsHardwareAccelerated) + { + Vector128 mean16x4Mask = Vector128.Create((short)0x00ff).AsByte(); + + Vector128 a0 = Unsafe.As>(ref MemoryMarshal.GetReference(input)); + Vector128 a1 = Unsafe.As>(ref MemoryMarshal.GetReference(input.Slice(WebpConstants.Bps, 16))); + Vector128 a2 = Unsafe.As>(ref MemoryMarshal.GetReference(input.Slice(WebpConstants.Bps * 2, 16))); + Vector128 a3 = Unsafe.As>(ref MemoryMarshal.GetReference(input.Slice(WebpConstants.Bps * 3, 16))); + Vector128 b0 = Vector128.ShiftRightLogical(a0.AsInt16(), 8); // hi byte + Vector128 b1 = Vector128.ShiftRightLogical(a1.AsInt16(), 8); + Vector128 b2 = Vector128.ShiftRightLogical(a2.AsInt16(), 8); + Vector128 b3 = Vector128.ShiftRightLogical(a3.AsInt16(), 8); + Vector128 c0 = a0 & mean16x4Mask; // lo byte + Vector128 c1 = a1 & mean16x4Mask; + Vector128 c2 = a2 & mean16x4Mask; + Vector128 c3 = a3 & mean16x4Mask; + Vector128 d0 = b0.AsInt32() + c0.AsInt32(); + Vector128 d1 = b1.AsInt32() + c1.AsInt32(); + Vector128 d2 = b2.AsInt32() + c2.AsInt32(); + Vector128 d3 = b3.AsInt32() + c3.AsInt32(); + Vector128 e0 = d0 + d1; + Vector128 e1 = d2 + d3; + Vector128 f0 = e0 + e1; + Vector128 hadd = Vector128_.HorizontalAdd(f0.AsInt16(), f0.AsInt16()); + Vector128 wide = Vector128_.UnpackLow(hadd, Vector128.Zero).AsUInt32(); + + ref uint outputRef = ref MemoryMarshal.GetReference(dc); + Unsafe.As>(ref outputRef) = wide; + } + else + { + for (int k = 0; k < 4; k++) + { + uint avg = 0; + for (int y = 0; y < 4; y++) + { + for (int x = 0; x < 4; x++) + { + avg += input[x + (y * WebpConstants.Bps)]; + } + } + + dc[k] = avg; + input = input[4..]; // go to next 4x4 block. + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static byte Avg2(byte a, byte b) => (byte)((a + b + 1) >> 1); + + [MethodImpl(InliningOptions.ShortMethod)] + public static byte Avg3(byte a, byte b, byte c) => (byte)((a + (2 * b) + c + 2) >> 2); + + [MethodImpl(InliningOptions.ShortMethod)] + public static void Dst(Span dst, int x, int y, byte v) => dst[x + (y * WebpConstants.Bps)] = v; + + [MethodImpl(InliningOptions.ShortMethod)] + public static byte Clip8B(int v) => (byte)((v & ~0xff) == 0 ? v : v < 0 ? 0 : 255); + + // Cost of coding one event with probability 'proba'. + public static int Vp8BitCost(int bit, byte proba) => bit == 0 ? WebpLookupTables.Vp8EntropyCost[proba] : WebpLookupTables.Vp8EntropyCost[255 - proba]; + + /// + /// Reduces elements of the vector into one sum. + /// + /// The accumulator to reduce. + /// The sum of all elements. + [MethodImpl(InliningOptions.ShortMethod)] + public static int ReduceSumVector256(Vector256 accumulator) + { + // Add upper lane to lower lane. + Vector128 vsum = accumulator.GetLower() + accumulator.GetUpper(); + + // Add odd to even. + vsum += Vector128_.ShuffleNative(vsum, 0b_11_11_01_01); + + // Add high to low. + vsum += Vector128_.ShuffleNative(vsum, 0b_11_10_11_10); + + return vsum.ToScalar(); + } + + /// + /// Reduces elements of the vector into one sum. + /// + /// The accumulator to reduce. + /// The sum of all elements. + [MethodImpl(InliningOptions.ShortMethod)] + private static int ReduceSumVector128(Vector128 accumulator) + { + // Add odd to even. + Vector128 vsum = accumulator + Vector128_.ShuffleNative(accumulator, 0b_11_11_01_01); + + // Add high to low. + vsum += Vector128_.ShuffleNative(vsum, 0b_11_10_11_10); + + return vsum.ToScalar(); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void Put16(int v, Span dst) + { + for (int j = 0; j < 16; j++) + { + Memset(dst[(j * WebpConstants.Bps)..], (byte)v, 0, 16); + } + } + + private static void TrueMotion(Span dst, Span yuv, int offset, int size) + { + // For information about how true motion works, see rfc6386, page 52. ff and section 20.14. + int topOffset = offset - WebpConstants.Bps; + Span top = yuv[topOffset..]; + byte p = yuv[topOffset - 1]; + int leftOffset = offset - 1; + byte left = yuv[leftOffset]; + for (int y = 0; y < size; y++) + { + for (int x = 0; x < size; x++) + { + dst[x] = (byte)Clamp255(left + top[x] - p); + } + + leftOffset += WebpConstants.Bps; + left = yuv[leftOffset]; + dst = dst[WebpConstants.Bps..]; + } + } + + // Complex In-loop filtering (Paragraph 15.3) + private static void FilterLoop24( + Span p, + int offset, + int hStride, + int vStride, + int size, + int thresh, + int ithresh, + int hevThresh) + { + int thresh2 = (2 * thresh) + 1; + while (size-- > 0) + { + if (NeedsFilter2(p, offset, hStride, thresh2, ithresh)) + { + if (Hev(p, offset, hStride, hevThresh)) + { + DoFilter2(p, offset, hStride); + } + else + { + DoFilter4(p, offset, hStride); + } + } + + offset += vStride; + } + } + + private static void FilterLoop26( + Span p, + int offset, + int hStride, + int vStride, + int size, + int thresh, + int ithresh, + int hevThresh) + { + int thresh2 = (2 * thresh) + 1; + while (size-- > 0) + { + if (NeedsFilter2(p, offset, hStride, thresh2, ithresh)) + { + if (Hev(p, offset, hStride, hevThresh)) + { + DoFilter2(p, offset, hStride); + } + else + { + DoFilter6(p, offset, hStride); + } + } + + offset += vStride; + } + } + + // Applies filter on 2 pixels (p0 and q0) + private static void DoFilter2(Span p, int offset, int step) + { + // 4 pixels in, 2 pixels out. + int p1 = p[offset - (2 * step)]; + int p0 = p[offset - step]; + int q0 = p[offset]; + int q1 = p[offset + step]; + int a = (3 * (q0 - p0)) + WebpLookupTables.Sclip1(p1 - q1); + int a1 = WebpLookupTables.Sclip2((a + 4) >> 3); + int a2 = WebpLookupTables.Sclip2((a + 3) >> 3); + p[offset - step] = WebpLookupTables.Clip1(p0 + a2); + p[offset] = WebpLookupTables.Clip1(q0 - a1); + } + + // Applies filter on 2 pixels (p0 and q0) + private static void DoFilter2Vector128(ref Vector128 p1, ref Vector128 p0, ref Vector128 q0, ref Vector128 q1, int thresh) + { + Vector128 signBit = Vector128.Create((byte)0x80); + + // Convert p1/q1 to byte (for GetBaseDelta). + Vector128 p1s = p1 ^ signBit; + Vector128 q1s = q1 ^ signBit; + Vector128 mask = NeedsFilterVector128(p1, p0, q0, q1, thresh); + + // Flip sign. + p0 ^= signBit; + q0 ^= signBit; + + Vector128 a = GetBaseDeltaVector128(p1s.AsSByte(), p0.AsSByte(), q0.AsSByte(), q1s.AsSByte()).AsByte(); + + // Mask filter values we don't care about. + a &= mask; + + DoSimpleFilterVector128(ref p0, ref q0, a); + + // Flip sign. + p0 ^= signBit; + q0 ^= signBit; + } + + // Applies filter on 4 pixels (p1, p0, q0 and q1) + private static void DoFilter4Vector128(ref Vector128 p1, ref Vector128 p0, ref Vector128 q0, ref Vector128 q1, Vector128 mask, int tresh) + { + // Compute hev mask. + Vector128 notHev = GetNotHevVector128(ref p1, ref p0, ref q0, ref q1, tresh); + + Vector128 signBit = Vector128.Create((byte)0x80); + + // Convert to signed values. + p1 ^= signBit; + p0 ^= signBit; + q0 ^= signBit; + q1 ^= signBit; + + Vector128 t1 = Vector128_.SubtractSaturate(p1.AsSByte(), q1.AsSByte()); // p1 - q1 + t1 = (~notHev & t1.AsByte()).AsSByte(); // hev(p1 - q1) + Vector128 t2 = Vector128_.SubtractSaturate(q0.AsSByte(), p0.AsSByte()); // q0 - p0 + t1 = Vector128_.AddSaturate(t1, t2); // hev(p1 - q1) + 1 * (q0 - p0) + t1 = Vector128_.AddSaturate(t1, t2); // hev(p1 - q1) + 2 * (q0 - p0) + t1 = Vector128_.AddSaturate(t1, t2); // hev(p1 - q1) + 3 * (q0 - p0) + t1 = (t1.AsByte() & mask).AsSByte(); // mask filter values we don't care about. + + t2 = Vector128_.AddSaturate(t1, Vector128.Create((byte)3).AsSByte()); // 3 * (q0 - p0) + hev(p1 - q1) + 3 + Vector128 t3 = Vector128_.AddSaturate(t1, Vector128.Create((byte)4).AsSByte()); // 3 * (q0 - p0) + hev(p1 - q1) + 4 + t2 = SignedShift8bVector128(t2.AsByte()); // (3 * (q0 - p0) + hev(p1 - q1) + 3) >> 3 + t3 = SignedShift8bVector128(t3.AsByte()); // (3 * (q0 - p0) + hev(p1 - q1) + 4) >> 3 + p0 = Vector128_.AddSaturate(p0.AsSByte(), t2).AsByte(); // p0 += t2 + q0 = Vector128_.SubtractSaturate(q0.AsSByte(), t3).AsByte(); // q0 -= t3 + p0 ^= signBit; + q0 ^= signBit; + + // This is equivalent to signed (a + 1) >> 1 calculation. + t2 = t3 + signBit.AsSByte(); + t3 = Vector128_.Average(t2.AsByte(), Vector128.Zero).AsSByte(); + t3 -= Vector128.Create((sbyte)64); + + t3 = (notHev & t3.AsByte()).AsSByte(); // if !hev + q1 = Vector128_.SubtractSaturate(q1.AsSByte(), t3).AsByte(); // q1 -= t3 + p1 = Vector128_.AddSaturate(p1.AsSByte(), t3).AsByte(); // p1 += t3 + p1 = p1.AsByte() ^ signBit; + q1 = q1.AsByte() ^ signBit; + } + + // Applies filter on 6 pixels (p2, p1, p0, q0, q1 and q2) + private static void DoFilter6Vector128(ref Vector128 p2, ref Vector128 p1, ref Vector128 p0, ref Vector128 q0, ref Vector128 q1, ref Vector128 q2, Vector128 mask, int tresh) + { + // Compute hev mask. + Vector128 notHev = GetNotHevVector128(ref p1, ref p0, ref q0, ref q1, tresh); + + // Convert to signed values. + Vector128 signBit = Vector128.Create((byte)0x80); + p1 ^= signBit; + p0 ^= signBit; + q0 ^= signBit; + q1 ^= signBit; + p2 ^= signBit; + q2 ^= signBit; + + Vector128 a = GetBaseDeltaVector128(p1.AsSByte(), p0.AsSByte(), q0.AsSByte(), q1.AsSByte()); + + // Do simple filter on pixels with hev. + Vector128 m = ~notHev & mask; + Vector128 f = a.AsByte() & m; + DoSimpleFilterVector128(ref p0, ref q0, f); + + // Do strong filter on pixels with not hev. + m = notHev & mask; + f = a.AsByte() & m; + Vector128 flow = Vector128_.UnpackLow(Vector128.Zero, f); + Vector128 fhigh = Vector128_.UnpackHigh(Vector128.Zero, f); + + Vector128 nine = Vector128.Create((short)0x0900); + Vector128 f9Low = Vector128_.MultiplyHigh(flow.AsInt16(), nine); // Filter (lo) * 9 + Vector128 f9High = Vector128_.MultiplyHigh(fhigh.AsInt16(), nine); // Filter (hi) * 9 + + Vector128 sixtyThree = Vector128.Create((short)63); + Vector128 a2Low = f9Low + sixtyThree; // Filter * 9 + 63 + Vector128 a2High = f9High + sixtyThree; // Filter * 9 + 63 + + Vector128 a1Low = a2Low + f9Low; // Filter * 18 + 63 + Vector128 a1High = a2High + f9High; // // Filter * 18 + 63 + + Vector128 a0Low = a1Low + f9Low; // Filter * 27 + 63 + Vector128 a0High = a1High + f9High; // Filter * 27 + 63 + + Update2PixelsVector128(ref p2, ref q2, a2Low, a2High); + Update2PixelsVector128(ref p1, ref q1, a1Low, a1High); + Update2PixelsVector128(ref p0, ref q0, a0Low, a0High); + } + + private static void DoSimpleFilterVector128(ref Vector128 p0, ref Vector128 q0, Vector128 fl) + { + Vector128 v3 = Vector128_.AddSaturate(fl.AsSByte(), Vector128.Create((byte)3).AsSByte()); + Vector128 v4 = Vector128_.AddSaturate(fl.AsSByte(), Vector128.Create((byte)4).AsSByte()); + + v4 = SignedShift8bVector128(v4.AsByte()).AsSByte(); // v4 >> 3 + v3 = SignedShift8bVector128(v3.AsByte()).AsSByte(); // v3 >> 3 + q0 = Vector128_.SubtractSaturate(q0.AsSByte(), v4).AsByte(); // q0 -= v4 + p0 = Vector128_.AddSaturate(p0.AsSByte(), v3).AsByte(); // p0 += v3 + } + + private static Vector128 GetNotHevVector128(ref Vector128 p1, ref Vector128 p0, ref Vector128 q0, ref Vector128 q1, int hevThresh) + { + Vector128 t1 = AbsVector128(p1, p0); + Vector128 t2 = AbsVector128(q1, q0); + + Vector128 h = Vector128.Create((byte)hevThresh); + Vector128 tMax = Vector128.Max(t1, t2); + + Vector128 tMaxH = Vector128_.SubtractSaturate(tMax, h); + + // not_hev <= t1 && not_hev <= t2 + return Vector128.Equals(tMaxH, Vector128.Zero); + } + + // Applies filter on 4 pixels (p1, p0, q0 and q1) + private static void DoFilter4(Span p, int offset, int step) + { + // 4 pixels in, 4 pixels out. + int offsetMinus2Step = offset - (2 * step); + int p1 = p[offsetMinus2Step]; + int p0 = p[offset - step]; + int q0 = p[offset]; + int q1 = p[offset + step]; + int a = 3 * (q0 - p0); + int a1 = WebpLookupTables.Sclip2((a + 4) >> 3); + int a2 = WebpLookupTables.Sclip2((a + 3) >> 3); + int a3 = (a1 + 1) >> 1; + p[offsetMinus2Step] = WebpLookupTables.Clip1(p1 + a3); + p[offset - step] = WebpLookupTables.Clip1(p0 + a2); + p[offset] = WebpLookupTables.Clip1(q0 - a1); + p[offset + step] = WebpLookupTables.Clip1(q1 - a3); + } + + // Applies filter on 6 pixels (p2, p1, p0, q0, q1 and q2) + private static void DoFilter6(Span p, int offset, int step) + { + // 6 pixels in, 6 pixels out. + int step2 = 2 * step; + int step3 = 3 * step; + int offsetMinusStep = offset - step; + int p2 = p[offset - step3]; + int p1 = p[offset - step2]; + int p0 = p[offsetMinusStep]; + int q0 = p[offset]; + int q1 = p[offset + step]; + int q2 = p[offset + step2]; + int a = WebpLookupTables.Sclip1((3 * (q0 - p0)) + WebpLookupTables.Sclip1(p1 - q1)); + + // a is in [-128,127], a1 in [-27,27], a2 in [-18,18] and a3 in [-9,9] + int a1 = ((27 * a) + 63) >> 7; // eq. to ((3 * a + 7) * 9) >> 7 + int a2 = ((18 * a) + 63) >> 7; // eq. to ((2 * a + 7) * 9) >> 7 + int a3 = ((9 * a) + 63) >> 7; // eq. to ((1 * a + 7) * 9) >> 7 + p[offset - step3] = WebpLookupTables.Clip1(p2 + a3); + p[offset - step2] = WebpLookupTables.Clip1(p1 + a2); + p[offsetMinusStep] = WebpLookupTables.Clip1(p0 + a1); + p[offset] = WebpLookupTables.Clip1(q0 - a1); + p[offset + step] = WebpLookupTables.Clip1(q1 - a2); + p[offset + step2] = WebpLookupTables.Clip1(q2 - a3); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static bool NeedsFilter(Span p, int offset, int step, int t) + { + int p1 = p[offset + (-2 * step)]; + int p0 = p[offset - step]; + int q0 = p[offset]; + int q1 = p[offset + step]; + return (4 * WebpLookupTables.Abs0(p0 - q0)) + WebpLookupTables.Abs0(p1 - q1) <= t; + } + + private static bool NeedsFilter2(Span p, int offset, int step, int t, int it) + { + int step2 = 2 * step; + int step3 = 3 * step; + int p3 = p[offset - (4 * step)]; + int p2 = p[offset - step3]; + int p1 = p[offset - step2]; + int p0 = p[offset - step]; + int q0 = p[offset]; + int q1 = p[offset + step]; + int q2 = p[offset + step2]; + int q3 = p[offset + step3]; + if ((4 * WebpLookupTables.Abs0(p0 - q0)) + WebpLookupTables.Abs0(p1 - q1) > t) + { + return false; + } + + return WebpLookupTables.Abs0(p3 - p2) <= it && WebpLookupTables.Abs0(p2 - p1) <= it && + WebpLookupTables.Abs0(p1 - p0) <= it && WebpLookupTables.Abs0(q3 - q2) <= it && + WebpLookupTables.Abs0(q2 - q1) <= it && WebpLookupTables.Abs0(q1 - q0) <= it; + } + + private static Vector128 NeedsFilterVector128(Vector128 p1, Vector128 p0, Vector128 q0, Vector128 q1, int thresh) + { + Vector128 mthresh = Vector128.Create((byte)thresh); + Vector128 t1 = AbsVector128(p1, q1); // abs(p1 - q1) + Vector128 fe = Vector128.Create((byte)0xFE); + Vector128 t2 = t1 & fe; // set lsb of each byte to zero. + Vector128 t3 = Vector128.ShiftRightLogical(t2.AsInt16(), 1); // abs(p1 - q1) / 2 + + Vector128 t4 = AbsVector128(p0, q0); // abs(p0 - q0) + Vector128 t5 = Vector128_.AddSaturate(t4, t4); // abs(p0 - q0) * 2 + Vector128 t6 = Vector128_.AddSaturate(t5.AsByte(), t3.AsByte()); // abs(p0-q0)*2 + abs(p1-q1)/2 + + Vector128 t7 = Vector128_.SubtractSaturate(t6, mthresh.AsByte()); // mask <= m_thresh + + return Vector128.Equals(t7, Vector128.Zero); + } + + private static void Load16x4Vector128(ref byte r0, ref byte r8, int stride, out Vector128 p1, out Vector128 p0, out Vector128 q0, out Vector128 q1) + { + // Assume the pixels around the edge (|) are numbered as follows + // 00 01 | 02 03 + // 10 11 | 12 13 + // ... | ... + // e0 e1 | e2 e3 + // f0 f1 | f2 f3 + // + // r0 is pointing to the 0th row (00) + // r8 is pointing to the 8th row (80) + + // Load + // p1 = 71 61 51 41 31 21 11 01 70 60 50 40 30 20 10 00 + // q0 = 73 63 53 43 33 23 13 03 72 62 52 42 32 22 12 02 + // p0 = f1 e1 d1 c1 b1 a1 91 81 f0 e0 d0 c0 b0 a0 90 80 + // q1 = f3 e3 d3 c3 b3 a3 93 83 f2 e2 d2 c2 b2 a2 92 82 + Load8x4Vector128(ref r0, (uint)stride, out Vector128 t1, out Vector128 t2); + Load8x4Vector128(ref r8, (uint)stride, out p0, out q1); + + // p1 = f0 e0 d0 c0 b0 a0 90 80 70 60 50 40 30 20 10 00 + // p0 = f1 e1 d1 c1 b1 a1 91 81 71 61 51 41 31 21 11 01 + // q0 = f2 e2 d2 c2 b2 a2 92 82 72 62 52 42 32 22 12 02 + // q1 = f3 e3 d3 c3 b3 a3 93 83 73 63 53 43 33 23 13 03 + p1 = Vector128_.UnpackLow(t1.AsInt64(), p0.AsInt64()).AsByte(); + p0 = Vector128_.UnpackHigh(t1.AsInt64(), p0.AsInt64()).AsByte(); + q0 = Vector128_.UnpackLow(t2.AsInt64(), q1.AsInt64()).AsByte(); + q1 = Vector128_.UnpackHigh(t2.AsInt64(), q1.AsInt64()).AsByte(); + } + + // Reads 8 rows across a vertical edge. + private static void Load8x4Vector128(ref byte bRef, nuint stride, out Vector128 p, out Vector128 q) + { + // A0 = 63 62 61 60 23 22 21 20 43 42 41 40 03 02 01 00 + // A1 = 73 72 71 70 33 32 31 30 53 52 51 50 13 12 11 10 + uint a00 = Unsafe.As(ref Unsafe.Add(ref bRef, 6 * stride)); + uint a01 = Unsafe.As(ref Unsafe.Add(ref bRef, 2 * stride)); + uint a02 = Unsafe.As(ref Unsafe.Add(ref bRef, 4 * stride)); + uint a03 = Unsafe.As(ref Unsafe.Add(ref bRef, 0 * stride)); + Vector128 a0 = Vector128.Create(a03, a02, a01, a00).AsByte(); + uint a10 = Unsafe.As(ref Unsafe.Add(ref bRef, 7 * stride)); + uint a11 = Unsafe.As(ref Unsafe.Add(ref bRef, 3 * stride)); + uint a12 = Unsafe.As(ref Unsafe.Add(ref bRef, 5 * stride)); + uint a13 = Unsafe.As(ref Unsafe.Add(ref bRef, 1 * stride)); + Vector128 a1 = Vector128.Create(a13, a12, a11, a10).AsByte(); + + // B0 = 53 43 52 42 51 41 50 40 13 03 12 02 11 01 10 00 + // B1 = 73 63 72 62 71 61 70 60 33 23 32 22 31 21 30 20 + Vector128 b0 = Vector128_.UnpackLow(a0.AsSByte(), a1.AsSByte()); + Vector128 b1 = Vector128_.UnpackHigh(a0.AsSByte(), a1.AsSByte()); + + // C0 = 33 23 13 03 32 22 12 02 31 21 11 01 30 20 10 00 + // C1 = 73 63 53 43 72 62 52 42 71 61 51 41 70 60 50 40 + Vector128 c0 = Vector128_.UnpackLow(b0.AsInt16(), b1.AsInt16()); + Vector128 c1 = Vector128_.UnpackHigh(b0.AsInt16(), b1.AsInt16()); + + // *p = 71 61 51 41 31 21 11 01 70 60 50 40 30 20 10 00 + // *q = 73 63 53 43 33 23 13 03 72 62 52 42 32 22 12 02 + p = Vector128_.UnpackLow(c0.AsInt32(), c1.AsInt32()).AsByte(); + q = Vector128_.UnpackHigh(c0.AsInt32(), c1.AsInt32()).AsByte(); + } + + // Transpose back and store + private static void Store16x4Vector128(Vector128 p1, Vector128 p0, Vector128 q0, Vector128 q1, ref byte r0Ref, ref byte r8Ref, int stride) + { + // p0 = 71 70 61 60 51 50 41 40 31 30 21 20 11 10 01 00 + // p1 = f1 f0 e1 e0 d1 d0 c1 c0 b1 b0 a1 a0 91 90 81 80 + Vector128 p0s = Vector128_.UnpackLow(p1, p0); + Vector128 p1s = Vector128_.UnpackHigh(p1, p0); + + // q0 = 73 72 63 62 53 52 43 42 33 32 23 22 13 12 03 02 + // q1 = f3 f2 e3 e2 d3 d2 c3 c2 b3 b2 a3 a2 93 92 83 82 + Vector128 q0s = Vector128_.UnpackLow(q0, q1); + Vector128 q1s = Vector128_.UnpackHigh(q0, q1); + + // p0 = 33 32 31 30 23 22 21 20 13 12 11 10 03 02 01 00 + // q0 = 73 72 71 70 63 62 61 60 53 52 51 50 43 42 41 40 + Vector128 t1 = p0s; + p0s = Vector128_.UnpackLow(t1.AsInt16(), q0s.AsInt16()).AsByte(); + q0s = Vector128_.UnpackHigh(t1.AsInt16(), q0s.AsInt16()).AsByte(); + + // p1 = b3 b2 b1 b0 a3 a2 a1 a0 93 92 91 90 83 82 81 80 + // q1 = f3 f2 f1 f0 e3 e2 e1 e0 d3 d2 d1 d0 c3 c2 c1 c0 + t1 = p1s; + p1s = Vector128_.UnpackLow(t1.AsInt16(), q1s.AsInt16()).AsByte(); + q1s = Vector128_.UnpackHigh(t1.AsInt16(), q1s.AsInt16()).AsByte(); + + Store4x4Vector128(p0s, ref r0Ref, stride); + Store4x4Vector128(q0s, ref Unsafe.Add(ref r0Ref, 4 * (uint)stride), stride); + + Store4x4Vector128(p1s, ref r8Ref, stride); + Store4x4Vector128(q1s, ref Unsafe.Add(ref r8Ref, 4 * (uint)stride), stride); + } + + private static void Store4x4Vector128(Vector128 x, ref byte dstRef, int stride) + { + int offset = 0; + for (int i = 0; i < 4; i++) + { + Unsafe.As(ref Unsafe.Add(ref dstRef, (uint)offset)) = x.AsInt32().ToScalar(); + x = Vector128_.ShiftRightBytesInVector(x, 4); + offset += stride; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static Vector128 GetBaseDeltaVector128(Vector128 p1, Vector128 p0, Vector128 q0, Vector128 q1) + { + // Beware of addition order, for saturation! + Vector128 p1q1 = Vector128_.SubtractSaturate(p1, q1); // p1 - q1 + Vector128 q0p0 = Vector128_.SubtractSaturate(q0, p0); // q0 - p0 + Vector128 s1 = Vector128_.AddSaturate(p1q1, q0p0); // p1 - q1 + 1 * (q0 - p0) + Vector128 s2 = Vector128_.AddSaturate(q0p0, s1); // p1 - q1 + 2 * (q0 - p0) + return Vector128_.AddSaturate(q0p0, s2); // p1 - q1 + 3 * (q0 - p0) + } + + // Shift each byte of "x" by 3 bits while preserving by the sign bit. + [MethodImpl(InliningOptions.ShortMethod)] + private static Vector128 SignedShift8bVector128(Vector128 x) + { + Vector128 low0 = Vector128_.UnpackLow(Vector128.Zero, x); + Vector128 high0 = Vector128_.UnpackHigh(Vector128.Zero, x); + Vector128 low1 = Vector128.ShiftRightArithmetic(low0.AsInt16(), 3 + 8); + Vector128 high1 = Vector128.ShiftRightArithmetic(high0.AsInt16(), 3 + 8); + + return Vector128_.PackSignedSaturate(low1, high1); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void ComplexMaskVector128(Vector128 p1, Vector128 p0, Vector128 q0, Vector128 q1, int thresh, int ithresh, ref Vector128 mask) + { + Vector128 it = Vector128.Create((byte)ithresh); + Vector128 diff = Vector128_.SubtractSaturate(mask, it); + Vector128 threshMask = Vector128.Equals(diff, Vector128.Zero); + Vector128 filterMask = NeedsFilterVector128(p1, p0, q0, q1, thresh); + + mask = threshMask & filterMask; + } + + // Updates values of 2 pixels at MB edge during complex filtering. + // Update operations: + // q = q - delta and p = p + delta; where delta = [(a_hi >> 7), (a_lo >> 7)] + // Pixels 'pi' and 'qi' are int8_t on input, uint8_t on output (sign flip). + private static void Update2PixelsVector128(ref Vector128 pi, ref Vector128 qi, Vector128 a0Low, Vector128 a0High) + { + Vector128 signBit = Vector128.Create((byte)0x80); + Vector128 a1Low = Vector128.ShiftRightArithmetic(a0Low, 7); + Vector128 a1High = Vector128.ShiftRightArithmetic(a0High, 7); + Vector128 delta = Vector128_.PackSignedSaturate(a1Low, a1High); + pi = Vector128_.AddSaturate(pi.AsSByte(), delta).AsByte(); + qi = Vector128_.SubtractSaturate(qi.AsSByte(), delta).AsByte(); + pi ^= signBit.AsByte(); + qi ^= signBit.AsByte(); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static Vector128 LoadUvEdgeVector128(ref byte uRef, ref byte vRef, int offset) + { + Vector128 uVec = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref uRef, (uint)offset)), 0); + Vector128 vVec = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref vRef, (uint)offset)), 0); + return Vector128_.UnpackLow(uVec, vVec).AsByte(); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void StoreUvVector128(Vector128 x, ref byte uRef, ref byte vRef, int offset) + { + Unsafe.As>(ref Unsafe.Add(ref uRef, (uint)offset)) = x.GetLower(); + Unsafe.As>(ref Unsafe.Add(ref vRef, (uint)offset)) = x.GetUpper(); + } + + // Compute abs(p - q) = subs(p - q) OR subs(q - p) + [MethodImpl(InliningOptions.ShortMethod)] + private static Vector128 AbsVector128(Vector128 p, Vector128 q) + => Vector128_.SubtractSaturate(q, p) | Vector128_.SubtractSaturate(p, q); + + [MethodImpl(InliningOptions.ShortMethod)] + private static bool Hev(Span p, int offset, int step, int thresh) + { + int p1 = p[offset - (2 * step)]; + int p0 = p[offset - step]; + int q0 = p[offset]; + int q1 = p[offset + step]; + return WebpLookupTables.Abs0(p1 - p0) > thresh || WebpLookupTables.Abs0(q1 - q0) > thresh; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void Store(Span dst, int x, int y, int v) + { + int index = x + (y * WebpConstants.Bps); + dst[index] = Clip8B(dst[index] + (v >> 3)); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void Store2(Span dst, int y, int dc, int d, int c) + { + Store(dst, 0, y, dc + d); + Store(dst, 1, y, dc + c); + Store(dst, 2, y, dc - c); + Store(dst, 3, y, dc - d); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Mul1(int a) => ((a * 20091) >> 16) + a; + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Mul2(int a) => (a * 35468) >> 16; + + [MethodImpl(InliningOptions.ShortMethod)] + private static void Put8x8uv(byte value, Span dst) + { + const int end = 8 * WebpConstants.Bps; + for (int j = 0; j < end; j += WebpConstants.Bps) + { + // memset(dst + j * BPS, value, 8); + Memset(dst, value, j, 8); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void Memset(Span dst, byte value, int startIdx, int count) => dst.Slice(startIdx, count).Fill(value); + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Clamp255(int x) => Numerics.Clamp(x, 0, 255); + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/PassStats.cs b/ImageSharp/Formats/Webp/Lossy/PassStats.cs new file mode 100644 index 0000000..56716cc --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/PassStats.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// Class for organizing convergence in either size or PSNR. + /// + internal class PassStats + { + public PassStats(long targetSize, float targetPsnr, int qMin, int qMax, uint quality) + { + bool doSizeSearch = targetSize != 0; + + this.IsFirst = true; + this.Dq = 10.0f; + this.Qmin = qMin; + this.Qmax = qMax; + this.Q = Numerics.Clamp(quality, qMin, qMax); + this.LastQ = this.Q; + this.Target = doSizeSearch ? targetSize + : targetPsnr > 0.0f ? targetPsnr + : 40.0f; // default, just in case + this.Value = 0.0f; + this.LastValue = 0.0f; + this.DoSizeSearch = doSizeSearch; + } + + public bool IsFirst { get; set; } + + public float Dq { get; set; } + + public float Q { get; set; } + + public float LastQ { get; set; } + + public float Qmin { get; } + + public float Qmax { get; } + + public double Value { get; set; } // PSNR or size + + public double LastValue { get; set; } + + public double Target { get; } + + public bool DoSizeSearch { get; } + + public float ComputeNextQ() + { + float dq; + if (this.IsFirst) + { + dq = this.Value > this.Target ? -this.Dq : this.Dq; + this.IsFirst = false; + } + else if (this.Value != this.LastValue) + { + double slope = (this.Target - this.Value) / (this.LastValue - this.Value); + dq = (float)(slope * (this.LastQ - this.Q)); + } + else + { + dq = 0.0f; // we're done?! + } + + // Limit variable to avoid large swings. + this.Dq = Numerics.Clamp(dq, -30.0f, 30.0f); + this.LastQ = this.Q; + this.LastValue = this.Value; + this.Q = Numerics.Clamp(this.Q + this.Dq, this.Qmin, this.Qmax); + + return this.Q; + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs b/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs new file mode 100644 index 0000000..d8a95cb --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs @@ -0,0 +1,817 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// Quantization methods. + /// + internal static unsafe class QuantEnc + { + private static readonly ushort[] WeightY = [38, 32, 20, 9, 32, 28, 17, 7, 20, 17, 10, 4, 9, 7, 4, 2]; + + private const int MaxLevel = 2047; + + // Diffusion weights. We under-correct a bit (15/16th of the error is actually + // diffused) to avoid 'rainbow' chessboard pattern of blocks at q~=0. + private const int C1 = 7; // fraction of error sent to the 4x4 block below + private const int C2 = 8; // fraction of error sent to the 4x4 block on the right + private const int DSHIFT = 4; + private const int DSCALE = 1; // storage descaling, needed to make the error fit byte + + // This uses C#'s optimization to refer to the static data segment of the assembly, no allocation occurs. + private static ReadOnlySpan Zigzag => [0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15]; + + public static void PickBestIntra16(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8SegmentInfo[] segmentInfos, Vp8EncProba proba) + { + const int numBlocks = 16; + Vp8SegmentInfo dqm = segmentInfos[it.CurrentMacroBlockInfo.Segment]; + int lambda = dqm.LambdaI16; + int tlambda = dqm.TLambda; + Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); + Span scratch = it.Scratch3; + Vp8ModeScore rdTmp = new(); + Vp8Residual res = new(); + Vp8ModeScore rdCur = rdTmp; + Vp8ModeScore rdBest = rd; + int mode; + bool isFlat = IsFlatSource16(src); + rd.ModeI16 = -1; + for (mode = 0; mode < WebpConstants.NumPredModes; ++mode) + { + // Scratch buffer. + Span tmpDst = it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc); + rdCur.ModeI16 = mode; + + // Reconstruct. + rdCur.Nz = (uint)ReconstructIntra16(it, dqm, rdCur, tmpDst, mode); + + // Measure RD-score. + rdCur.D = LossyUtils.Vp8_Sse16x16(src, tmpDst); + rdCur.SD = tlambda != 0 ? Mult8B(tlambda, LossyUtils.Vp8Disto16X16(src, tmpDst, WeightY, scratch)) : 0; + rdCur.H = WebpConstants.Vp8FixedCostsI16[mode]; + rdCur.R = it.GetCostLuma16(rdCur, proba, res); + + if (isFlat) + { + // Refine the first impression (which was in pixel space). + isFlat = IsFlat(rdCur.YAcLevels, numBlocks, WebpConstants.FlatnessLimitI16); + if (isFlat) + { + // Block is very flat. We put emphasis on the distortion being very low! + rdCur.D *= 2; + rdCur.SD *= 2; + } + } + + // Since we always examine Intra16 first, we can overwrite *rd directly. + rdCur.SetRdScore(lambda); + + if (mode == 0 || rdCur.Score < rdBest.Score) + { + RuntimeUtility.Swap(ref rdBest, ref rdCur); + it.SwapOut(); + } + } + + if (rdBest != rd) + { + rd = rdBest; + } + + // Finalize score for mode decision. + rd.SetRdScore(dqm.LambdaMode); + it.SetIntra16Mode(rd.ModeI16); + + // We have a blocky macroblock (only DCs are non-zero) with fairly high + // distortion, record max delta so we can later adjust the minimal filtering + // strength needed to smooth these blocks out. + if ((rd.Nz & 0x100ffff) == 0x1000000 && rd.D > dqm.MinDisto) + { + dqm.StoreMaxDelta(rd.YDcLevels); + } + } + + public static bool PickBestIntra4(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8SegmentInfo[] segmentInfos, Vp8EncProba proba, int maxI4HeaderBits) + { + Vp8SegmentInfo dqm = segmentInfos[it.CurrentMacroBlockInfo.Segment]; + int lambda = dqm.LambdaI4; + int tlambda = dqm.TLambda; + Span src0 = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); + Span bestBlocks = it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc); + Span scratch = it.Scratch3; + int totalHeaderBits = 0; + Vp8ModeScore rdBest = new(); + + if (maxI4HeaderBits == 0) + { + return false; + } + + rdBest.InitScore(); + rdBest.H = 211; // '211' is the value of VP8BitCost(0, 145) + rdBest.SetRdScore(dqm.LambdaMode); + it.StartI4(); + Vp8ModeScore rdi4 = new(); + Vp8ModeScore rdTmp = new(); + Vp8Residual res = new(); + Span tmpLevels = stackalloc short[16]; + do + { + const int numBlocks = 1; + rdi4.Clear(); + int mode; + int bestMode = -1; + Span src = src0[WebpLookupTables.Vp8Scan[it.I4]..]; + short[] modeCosts = it.GetCostModeI4(rd.ModesI4); + Span bestBlock = bestBlocks[WebpLookupTables.Vp8Scan[it.I4]..]; + Span tmpDst = it.Scratch.AsSpan(); + tmpDst.Clear(); + + rdi4.InitScore(); + it.MakeIntra4Preds(); + for (mode = 0; mode < WebpConstants.NumBModes; ++mode) + { + rdTmp.Clear(); + tmpLevels.Clear(); + + // Reconstruct. + rdTmp.Nz = (uint)ReconstructIntra4(it, dqm, tmpLevels, src, tmpDst, mode); + + // Compute RD-score. + rdTmp.D = LossyUtils.Vp8_Sse4x4(src, tmpDst); + rdTmp.SD = tlambda != 0 ? Mult8B(tlambda, LossyUtils.Vp8Disto4X4(src, tmpDst, WeightY, scratch)) : 0; + rdTmp.H = modeCosts[mode]; + + // Add flatness penalty, to avoid flat area to be mispredicted by a complex mode. + if (mode > 0 && IsFlat(tmpLevels, numBlocks, WebpConstants.FlatnessLimitI4)) + { + rdTmp.R = WebpConstants.FlatnessPenality * numBlocks; + } + else + { + rdTmp.R = 0; + } + + // Early-out check. + rdTmp.SetRdScore(lambda); + if (bestMode >= 0 && rdTmp.Score >= rdi4.Score) + { + continue; + } + + // Finish computing score. + rdTmp.R += it.GetCostLuma4(tmpLevels, proba, res); + rdTmp.SetRdScore(lambda); + + if (bestMode < 0 || rdTmp.Score < rdi4.Score) + { + rdi4.CopyScore(rdTmp); + bestMode = mode; + + RuntimeUtility.Swap(ref tmpDst, ref bestBlock); + tmpLevels.CopyTo(rdBest.YAcLevels.AsSpan(it.I4 * 16, 16)); + } + } + + rdi4.SetRdScore(dqm.LambdaMode); + rdBest.AddScore(rdi4); + if (rdBest.Score >= rd.Score) + { + return false; + } + + totalHeaderBits += (int)rdi4.H; // <- equal to modeCosts[bestMode]; + if (totalHeaderBits > maxI4HeaderBits) + { + return false; + } + + // Copy selected samples to the right place. + LossyUtils.Vp8Copy4X4(bestBlock, bestBlocks[WebpLookupTables.Vp8Scan[it.I4]..]); + + rd.ModesI4[it.I4] = (byte)bestMode; + it.TopNz[it.I4 & 3] = it.LeftNz[it.I4 >> 2] = rdi4.Nz != 0 ? 1 : 0; + } + while (it.RotateI4(bestBlocks)); + + // Finalize state. + rd.CopyScore(rdBest); + it.SetIntra4Mode(rd.ModesI4); + it.SwapOut(); + rdBest.YAcLevels.AsSpan().CopyTo(rd.YAcLevels); + + // Select intra4x4 over intra16x16. + return true; + } + + public static void PickBestUv(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8SegmentInfo[] segmentInfos, Vp8EncProba proba) + { + const int numBlocks = 8; + Vp8SegmentInfo dqm = segmentInfos[it.CurrentMacroBlockInfo.Segment]; + int lambda = dqm.LambdaUv; + Span src = it.YuvIn.AsSpan(Vp8EncIterator.UOffEnc); + Span tmpDst = it.YuvOut2.AsSpan(Vp8EncIterator.UOffEnc); + Span dst0 = it.YuvOut.AsSpan(Vp8EncIterator.UOffEnc); + Span dst = dst0; + Vp8ModeScore rdBest = new(); + Vp8ModeScore rdUv = new(); + Vp8Residual res = new(); + int mode; + + rd.ModeUv = -1; + rdBest.InitScore(); + for (mode = 0; mode < WebpConstants.NumPredModes; ++mode) + { + rdUv.Clear(); + + // Reconstruct + rdUv.Nz = (uint)ReconstructUv(it, dqm, rdUv, tmpDst, mode); + + // Compute RD-score + rdUv.D = LossyUtils.Vp8_Sse16x8(src, tmpDst); + rdUv.SD = 0; // not calling TDisto here: it tends to flatten areas. + rdUv.H = WebpConstants.Vp8FixedCostsUv[mode]; + rdUv.R = it.GetCostUv(rdUv, proba, res); + if (mode > 0 && IsFlat(rdUv.UvLevels, numBlocks, WebpConstants.FlatnessLimitIUv)) + { + rdUv.R += WebpConstants.FlatnessPenality * numBlocks; + } + + rdUv.SetRdScore(lambda); + if (mode == 0 || rdUv.Score < rdBest.Score) + { + rdBest.CopyScore(rdUv); + rd.ModeUv = mode; + rdUv.UvLevels.CopyTo(rd.UvLevels.AsSpan()); + for (int i = 0; i < 2; i++) + { + rd.Derr[i, 0] = rdUv.Derr[i, 0]; + rd.Derr[i, 1] = rdUv.Derr[i, 1]; + rd.Derr[i, 2] = rdUv.Derr[i, 2]; + } + + RuntimeUtility.Swap(ref tmpDst, ref dst); + } + } + + it.SetIntraUvMode(rd.ModeUv); + rd.AddScore(rdBest); + if (dst != dst0) + { + // copy 16x8 block if needed. + LossyUtils.Vp8Copy16X8(dst, dst0); + } + + // Store diffusion errors for next block. + it.StoreDiffusionErrors(rd); + } + + public static int ReconstructIntra16(Vp8EncIterator it, Vp8SegmentInfo dqm, Vp8ModeScore rd, Span yuvOut, int mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I16ModeOffsets[mode]); + Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); + int nz = 0; + int n; + Span shortScratchSpan = it.Scratch2.AsSpan(); + Span scratch = it.Scratch3.AsSpan(0, 16); + shortScratchSpan.Clear(); + scratch.Clear(); + Span dcTmp = shortScratchSpan[..16]; + Span tmp = shortScratchSpan.Slice(16, 16 * 16); + + for (n = 0; n < 16; n += 2) + { + Vp8Encoding.FTransform2( + src[WebpLookupTables.Vp8Scan[n]..], + reference[WebpLookupTables.Vp8Scan[n]..], + tmp.Slice(n * 16, 16), + tmp.Slice((n + 1) * 16, 16), + scratch); + } + + Vp8Encoding.FTransformWht(tmp, dcTmp, scratch); + nz |= QuantizeBlock(dcTmp, rd.YDcLevels, ref dqm.Y2) << 24; + + for (n = 0; n < 16; n += 2) + { + // Zero-out the first coeff, so that: a) nz is correct below, and + // b) finding 'last' non-zero coeffs in SetResidualCoeffs() is simplified. + tmp[n * 16] = tmp[(n + 1) * 16] = 0; + nz |= Quantize2Blocks(tmp.Slice(n * 16, 32), rd.YAcLevels.AsSpan(n * 16, 32), ref dqm.Y1) << n; + } + + // Transform back. + LossyUtils.TransformWht(dcTmp, tmp, scratch); + for (n = 0; n < 16; n += 2) + { + Vp8Encoding.ITransformTwo(reference[WebpLookupTables.Vp8Scan[n]..], tmp.Slice(n * 16, 32), yuvOut[WebpLookupTables.Vp8Scan[n]..], scratch); + } + + return nz; + } + + public static int ReconstructIntra4(Vp8EncIterator it, Vp8SegmentInfo dqm, Span levels, Span src, Span yuvOut, int mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I4ModeOffsets[mode]); + Span tmp = it.Scratch2.AsSpan(0, 16); + Span scratch = it.Scratch3.AsSpan(0, 16); + Vp8Encoding.FTransform(src, reference, tmp, scratch); + int nz = QuantizeBlock(tmp, levels, ref dqm.Y1); + Vp8Encoding.ITransformOne(reference, tmp, yuvOut, scratch); + + return nz; + } + + public static int ReconstructUv(Vp8EncIterator it, Vp8SegmentInfo dqm, Vp8ModeScore rd, Span yuvOut, int mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8UvModeOffsets[mode]); + Span src = it.YuvIn.AsSpan(Vp8EncIterator.UOffEnc); + int nz = 0; + int n; + Span tmp = it.Scratch2.AsSpan(0, 8 * 16); + Span scratch = it.Scratch3.AsSpan(0, 16); + + for (n = 0; n < 8; n += 2) + { + Vp8Encoding.FTransform2( + src[WebpLookupTables.Vp8ScanUv[n]..], + reference[WebpLookupTables.Vp8ScanUv[n]..], + tmp.Slice(n * 16, 16), + tmp.Slice((n + 1) * 16, 16), + scratch); + } + + CorrectDcValues(it, ref dqm.Uv, tmp, rd); + + for (n = 0; n < 8; n += 2) + { + nz |= Quantize2Blocks(tmp.Slice(n * 16, 32), rd.UvLevels.AsSpan(n * 16, 32), ref dqm.Uv) << n; + } + + for (n = 0; n < 8; n += 2) + { + Vp8Encoding.ITransformTwo(reference[WebpLookupTables.Vp8ScanUv[n]..], tmp.Slice(n * 16, 32), yuvOut[WebpLookupTables.Vp8ScanUv[n]..], scratch); + } + + return nz << 16; + } + + // Refine intra16/intra4 sub-modes based on distortion only (not rate). + public static void RefineUsingDistortion(Vp8EncIterator it, Vp8SegmentInfo[] segmentInfos, Vp8ModeScore rd, bool tryBothModes, bool refineUvMode, int mbHeaderLimit) + { + long bestScore = Vp8ModeScore.MaxCost; + int nz = 0; + int mode; + bool isI16 = tryBothModes || it.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16; + Vp8SegmentInfo dqm = segmentInfos[it.CurrentMacroBlockInfo.Segment]; + + // Some empiric constants, of approximate order of magnitude. + const int lambdaDi16 = 106; + const int lambdaDi4 = 11; + const int lambdaDuv = 120; + long scoreI4 = dqm.I4Penalty; + long i4BitSum = 0; + long bitLimit = tryBothModes + ? mbHeaderLimit + : Vp8ModeScore.MaxCost; // no early-out allowed. + + if (isI16) + { + int bestMode = -1; + Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); + for (mode = 0; mode < WebpConstants.NumPredModes; ++mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I16ModeOffsets[mode]); + long score = (LossyUtils.Vp8_Sse16x16(src, reference) * WebpConstants.RdDistoMult) + (WebpConstants.Vp8FixedCostsI16[mode] * lambdaDi16); + + if (mode > 0 && WebpConstants.Vp8FixedCostsI16[mode] > bitLimit) + { + continue; + } + + if (score < bestScore) + { + bestMode = mode; + bestScore = score; + } + } + + if (it.X == 0 || it.Y == 0) + { + // Avoid starting a checkerboard resonance from the border. See bug #432 of libwebp. + if (IsFlatSource16(src)) + { + bestMode = it.X == 0 ? 0 : 2; + tryBothModes = false; // Stick to i16. + } + } + + it.SetIntra16Mode(bestMode); + + // We'll reconstruct later, if i16 mode actually gets selected. + } + + // Next, evaluate Intra4. + if (tryBothModes || !isI16) + { + // We don't evaluate the rate here, but just account for it through a + // constant penalty (i4 mode usually needs more bits compared to i16). + isI16 = false; + it.StartI4(); + do + { + int bestI4Mode = -1; + long bestI4Score = Vp8ModeScore.MaxCost; + Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc + WebpLookupTables.Vp8Scan[it.I4]); + short[] modeCosts = it.GetCostModeI4(rd.ModesI4); + + it.MakeIntra4Preds(); + for (mode = 0; mode < WebpConstants.NumBModes; ++mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I4ModeOffsets[mode]); + long score = (LossyUtils.Vp8_Sse4x4(src, reference) * WebpConstants.RdDistoMult) + (modeCosts[mode] * lambdaDi4); + if (score < bestI4Score) + { + bestI4Mode = mode; + bestI4Score = score; + } + } + + i4BitSum += modeCosts[bestI4Mode]; + rd.ModesI4[it.I4] = (byte)bestI4Mode; + scoreI4 += bestI4Score; + if (scoreI4 >= bestScore || i4BitSum > bitLimit) + { + // Intra4 won't be better than Intra16. Bail out and pick Intra16. + isI16 = true; + break; + } + else + { + // Reconstruct partial block inside YuvOut2 buffer + Span tmpDst = it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc + WebpLookupTables.Vp8Scan[it.I4]); + nz |= ReconstructIntra4(it, dqm, rd.YAcLevels.AsSpan(it.I4 * 16, 16), src, tmpDst, bestI4Mode) << it.I4; + } + } + while (it.RotateI4(it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc))); + } + + // Final reconstruction, depending on which mode is selected. + if (!isI16) + { + it.SetIntra4Mode(rd.ModesI4); + it.SwapOut(); + bestScore = scoreI4; + } + else + { + int intra16Mode = it.Preds[it.PredIdx]; + nz = ReconstructIntra16(it, dqm, rd, it.YuvOut.AsSpan(Vp8EncIterator.YOffEnc), intra16Mode); + } + + // ... and UV! + if (refineUvMode) + { + int bestMode = -1; + long bestUvScore = Vp8ModeScore.MaxCost; + Span src = it.YuvIn.AsSpan(Vp8EncIterator.UOffEnc); + for (mode = 0; mode < WebpConstants.NumPredModes; ++mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8UvModeOffsets[mode]); + long score = (LossyUtils.Vp8_Sse16x8(src, reference) * WebpConstants.RdDistoMult) + (WebpConstants.Vp8FixedCostsUv[mode] * lambdaDuv); + if (score < bestUvScore) + { + bestMode = mode; + bestUvScore = score; + } + } + + it.SetIntraUvMode(bestMode); + } + + nz |= ReconstructUv(it, dqm, rd, it.YuvOut.AsSpan(Vp8EncIterator.UOffEnc), it.CurrentMacroBlockInfo.UvMode); + + rd.Nz = (uint)nz; + rd.Score = bestScore; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static int Quantize2Blocks(Span input, Span output, ref Vp8Matrix mtx) + { + int nz = QuantizeBlock(input[..16], output[..16], ref mtx) << 0; + nz |= QuantizeBlock(input.Slice(1 * 16, 16), output.Slice(1 * 16, 16), ref mtx) << 1; + return nz; + } + + public static int QuantizeBlock(Span input, Span output, ref Vp8Matrix mtx) + { + if (Avx2.IsSupported) + { + // Load all inputs. + Vector256 input0 = Unsafe.As>(ref MemoryMarshal.GetReference(input)); + Vector256 iq0 = Unsafe.As>(ref mtx.IQ[0]); + Vector256 q0 = Unsafe.As>(ref mtx.Q[0]); + + // coeff = abs(in) + Vector256 coeff0 = Avx2.Abs(input0); + + // coeff = abs(in) + sharpen + Vector256 sharpen0 = Unsafe.As>(ref mtx.Sharpen[0]); + Avx2.Add(coeff0.AsInt16(), sharpen0); + + // out = (coeff * iQ + B) >> QFIX + // doing calculations with 32b precision (QFIX=17) + // out = (coeff * iQ) + Vector256 coeffiQ0H = Avx2.MultiplyHigh(coeff0, iq0); + Vector256 coeffiQ0L = Avx2.MultiplyLow(coeff0, iq0); + Vector256 out00 = Avx2.UnpackLow(coeffiQ0L, coeffiQ0H); + Vector256 out08 = Avx2.UnpackHigh(coeffiQ0L, coeffiQ0H); + + // out = (coeff * iQ + B) + Vector256 bias00 = Unsafe.As>(ref mtx.Bias[0]); + Vector256 bias08 = Unsafe.As>(ref mtx.Bias[8]); + out00 = Avx2.Add(out00.AsInt32(), bias00.AsInt32()).AsUInt16(); + out08 = Avx2.Add(out08.AsInt32(), bias08.AsInt32()).AsUInt16(); + + // out = QUANTDIV(coeff, iQ, B, QFIX) + out00 = Avx2.ShiftRightArithmetic(out00.AsInt32(), WebpConstants.QFix).AsUInt16(); + out08 = Avx2.ShiftRightArithmetic(out08.AsInt32(), WebpConstants.QFix).AsUInt16(); + + // Pack result as 16b. + Vector256 out0 = Avx2.PackSignedSaturate(out00.AsInt32(), out08.AsInt32()); + + // if (coeff > 2047) coeff = 2047 + out0 = Avx2.Min(out0, Vector256.Create((short)MaxLevel)); + + // Put the sign back. + out0 = Avx2.Sign(out0, input0); + + // in = out * Q + input0 = Avx2.MultiplyLow(out0, q0.AsInt16()); + ref short inputRef = ref MemoryMarshal.GetReference(input); + Unsafe.As>(ref inputRef) = input0; + + // zigzag the output before storing it. + Vector256 tmp256 = Avx2.Shuffle(out0.AsByte(), Vector256.Create(0, 1, 2, 3, 8, 9, 254, 255, 10, 11, 4, 5, 6, 7, 12, 13, 2, 3, 8, 9, 10, 11, 4, 5, 254, 255, 6, 7, 12, 13, 14, 15)); // Cst256 + Vector256 tmp78 = Avx2.Shuffle(out0.AsByte(), Vector256.Create(254, 255, 254, 255, 254, 255, 254, 255, 14, 15, 254, 255, 254, 255, 254, 255, 254, 255, 254, 255, 254, 255, 0, 1, 254, 255, 254, 255, 254, 255, 254, 255)); // Cst78 + + // Reverse the order of the 16-byte lanes. + Vector256 tmp87 = Avx2.Permute2x128(tmp78, tmp78, 1); + Vector256 outZ = Avx2.Or(tmp256, tmp87).AsInt16(); + + ref short outputRef = ref MemoryMarshal.GetReference(output); + Unsafe.As>(ref outputRef) = outZ; + + Vector256 packedOutput = Avx2.PackSignedSaturate(outZ, outZ); + + // Detect if all 'out' values are zeros or not. + Vector256 cmpeq = Avx2.CompareEqual(packedOutput, Vector256.Zero); + return Avx2.MoveMask(cmpeq) != -1 ? 1 : 0; + } + else if (Sse41.IsSupported) + { + // Load all inputs. + Vector128 input0 = Unsafe.As>(ref MemoryMarshal.GetReference(input)); + Vector128 input8 = Unsafe.As>(ref MemoryMarshal.GetReference(input.Slice(8, 8))); + Vector128 iq0 = Unsafe.As>(ref mtx.IQ[0]); + Vector128 iq8 = Unsafe.As>(ref mtx.IQ[8]); + Vector128 q0 = Unsafe.As>(ref mtx.Q[0]); + Vector128 q8 = Unsafe.As>(ref mtx.Q[8]); + + // coeff = abs(in) + Vector128 coeff0 = Ssse3.Abs(input0); + Vector128 coeff8 = Ssse3.Abs(input8); + + // coeff = abs(in) + sharpen + Vector128 sharpen0 = Unsafe.As>(ref mtx.Sharpen[0]); + Vector128 sharpen8 = Unsafe.As>(ref mtx.Sharpen[8]); + Sse2.Add(coeff0.AsInt16(), sharpen0); + Sse2.Add(coeff8.AsInt16(), sharpen8); + + // out = (coeff * iQ + B) >> QFIX + // doing calculations with 32b precision (QFIX=17) + // out = (coeff * iQ) + Vector128 coeffiQ0H = Sse2.MultiplyHigh(coeff0, iq0); + Vector128 coeffiQ0L = Sse2.MultiplyLow(coeff0, iq0); + Vector128 coeffiQ8H = Sse2.MultiplyHigh(coeff8, iq8); + Vector128 coeffiQ8L = Sse2.MultiplyLow(coeff8, iq8); + Vector128 out00 = Sse2.UnpackLow(coeffiQ0L, coeffiQ0H); + Vector128 out04 = Sse2.UnpackHigh(coeffiQ0L, coeffiQ0H); + Vector128 out08 = Sse2.UnpackLow(coeffiQ8L, coeffiQ8H); + Vector128 out12 = Sse2.UnpackHigh(coeffiQ8L, coeffiQ8H); + + // out = (coeff * iQ + B) + Vector128 bias00 = Unsafe.As>(ref mtx.Bias[0]); + Vector128 bias04 = Unsafe.As>(ref mtx.Bias[4]); + Vector128 bias08 = Unsafe.As>(ref mtx.Bias[8]); + Vector128 bias12 = Unsafe.As>(ref mtx.Bias[12]); + out00 = Sse2.Add(out00.AsInt32(), bias00.AsInt32()).AsUInt16(); + out04 = Sse2.Add(out04.AsInt32(), bias04.AsInt32()).AsUInt16(); + out08 = Sse2.Add(out08.AsInt32(), bias08.AsInt32()).AsUInt16(); + out12 = Sse2.Add(out12.AsInt32(), bias12.AsInt32()).AsUInt16(); + + // out = QUANTDIV(coeff, iQ, B, QFIX) + out00 = Sse2.ShiftRightArithmetic(out00.AsInt32(), WebpConstants.QFix).AsUInt16(); + out04 = Sse2.ShiftRightArithmetic(out04.AsInt32(), WebpConstants.QFix).AsUInt16(); + out08 = Sse2.ShiftRightArithmetic(out08.AsInt32(), WebpConstants.QFix).AsUInt16(); + out12 = Sse2.ShiftRightArithmetic(out12.AsInt32(), WebpConstants.QFix).AsUInt16(); + + // Pack result as 16b. + Vector128 out0 = Sse2.PackSignedSaturate(out00.AsInt32(), out04.AsInt32()); + Vector128 out8 = Sse2.PackSignedSaturate(out08.AsInt32(), out12.AsInt32()); + + // if (coeff > 2047) coeff = 2047 + Vector128 maxCoeff2047 = Vector128.Create((short)MaxLevel); + out0 = Sse2.Min(out0, maxCoeff2047); + out8 = Sse2.Min(out8, maxCoeff2047); + + // Put the sign back. + out0 = Ssse3.Sign(out0, input0); + out8 = Ssse3.Sign(out8, input8); + + // in = out * Q + input0 = Sse2.MultiplyLow(out0, q0.AsInt16()); + input8 = Sse2.MultiplyLow(out8, q8.AsInt16()); + + // in = out * Q + ref short inputRef = ref MemoryMarshal.GetReference(input); + Unsafe.As>(ref inputRef) = input0; + Unsafe.As>(ref Unsafe.Add(ref inputRef, 8)) = input8; + + // zigzag the output before storing it. The re-ordering is: + // 0 1 2 3 4 5 6 7 | 8 9 10 11 12 13 14 15 + // -> 0 1 4[8]5 2 3 6 | 9 12 13 10 [7]11 14 15 + // There's only two misplaced entries ([8] and [7]) that are crossing the + // reg's boundaries. + // We use pshufb instead of pshuflo/pshufhi. + Vector128 tmpLo = Ssse3.Shuffle(out0.AsByte(), Vector128.Create(0, 1, 2, 3, 8, 9, 254, 255, 10, 11, 4, 5, 6, 7, 12, 13)); + Vector128 tmp7 = Ssse3.Shuffle(out0.AsByte(), Vector128.Create(254, 255, 254, 255, 254, 255, 254, 255, 14, 15, 254, 255, 254, 255, 254, 255)); // extract #7 + Vector128 tmpHi = Ssse3.Shuffle(out8.AsByte(), Vector128.Create(2, 3, 8, 9, 10, 11, 4, 5, 254, 255, 6, 7, 12, 13, 14, 15)); + Vector128 tmp8 = Ssse3.Shuffle(out8.AsByte(), Vector128.Create(254, 255, 254, 255, 254, 255, 0, 1, 254, 255, 254, 255, 254, 255, 254, 255)); // extract #8 + Vector128 outZ0 = Sse2.Or(tmpLo, tmp8); + Vector128 outZ8 = Sse2.Or(tmpHi, tmp7); + + ref short outputRef = ref MemoryMarshal.GetReference(output); + Unsafe.As>(ref outputRef) = outZ0.AsInt16(); + Unsafe.As>(ref Unsafe.Add(ref outputRef, 8)) = outZ8.AsInt16(); + + Vector128 packedOutput = Sse2.PackSignedSaturate(outZ0.AsInt16(), outZ8.AsInt16()); + + // Detect if all 'out' values are zeros or not. + Vector128 cmpeq = Sse2.CompareEqual(packedOutput, Vector128.Zero); + return Sse2.MoveMask(cmpeq) != 0xffff ? 1 : 0; + } + else + { + int last = -1; + int n; + for (n = 0; n < 16; ++n) + { + int j = Zigzag[n]; + bool sign = input[j] < 0; + uint coeff = (uint)((sign ? -input[j] : input[j]) + mtx.Sharpen[j]); + if (coeff > mtx.ZThresh[j]) + { + uint q = mtx.Q[j]; + uint iQ = mtx.IQ[j]; + uint b = mtx.Bias[j]; + int level = QuantDiv(coeff, iQ, b); + if (level > MaxLevel) + { + level = MaxLevel; + } + + if (sign) + { + level = -level; + } + + input[j] = (short)(level * (int)q); + output[n] = (short)level; + if (level != 0) + { + last = n; + } + } + else + { + output[n] = 0; + input[j] = 0; + } + } + + return last >= 0 ? 1 : 0; + } + } + + // Quantize as usual, but also compute and return the quantization error. + // Error is already divided by DSHIFT. + public static int QuantizeSingle(Span v, ref Vp8Matrix mtx) + { + int v0 = v[0]; + bool sign = v0 < 0; + if (sign) + { + v0 = -v0; + } + + if (v0 > (int)mtx.ZThresh[0]) + { + int qV = QuantDiv((uint)v0, mtx.IQ[0], mtx.Bias[0]) * mtx.Q[0]; + int err = v0 - qV; + v[0] = (short)(sign ? -qV : qV); + return (sign ? -err : err) >> DSCALE; + } + + v[0] = 0; + return (sign ? -v0 : v0) >> DSCALE; + } + + public static void CorrectDcValues(Vp8EncIterator it, ref Vp8Matrix mtx, Span tmp, Vp8ModeScore rd) + { +#pragma warning disable SA1005 // Single line comments should begin with single space + // | top[0] | top[1] + // --------+--------+--------- + // left[0] | tmp[0] tmp[1] <-> err0 err1 + // left[1] | tmp[2] tmp[3] err2 err3 + // + // Final errors {err1,err2,err3} are preserved and later restored + // as top[]/left[] on the next block. +#pragma warning restore SA1005 // Single line comments should begin with single space + for (int ch = 0; ch <= 1; ++ch) + { + Span top = it.TopDerr.AsSpan((it.X * 4) + ch, 2); + Span left = it.LeftDerr.AsSpan(ch, 2); + Span c = tmp.Slice(ch * 4 * 16, 4 * 16); + c[0] += (short)(((C1 * top[0]) + (C2 * left[0])) >> (DSHIFT - DSCALE)); + int err0 = QuantizeSingle(c, ref mtx); + c[1 * 16] += (short)(((C1 * top[1]) + (C2 * err0)) >> (DSHIFT - DSCALE)); + int err1 = QuantizeSingle(c[(1 * 16)..], ref mtx); + c[2 * 16] += (short)(((C1 * err0) + (C2 * left[1])) >> (DSHIFT - DSCALE)); + int err2 = QuantizeSingle(c[(2 * 16)..], ref mtx); + c[3 * 16] += (short)(((C1 * err1) + (C2 * err2)) >> (DSHIFT - DSCALE)); + int err3 = QuantizeSingle(c[(3 * 16)..], ref mtx); + + rd.Derr[ch, 0] = err1; + rd.Derr[ch, 1] = err2; + rd.Derr[ch, 2] = err3; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static bool IsFlatSource16(Span src) + { + uint v = src[0] * 0x01010101u; + Span vSpan = BitConverter.GetBytes(v).AsSpan(); + for (nuint i = 0; i < 16; i++) + { + if (!src[..4].SequenceEqual(vSpan) || !src.Slice(4, 4).SequenceEqual(vSpan) || + !src.Slice(8, 4).SequenceEqual(vSpan) || !src.Slice(12, 4).SequenceEqual(vSpan)) + { + return false; + } + + src = src[WebpConstants.Bps..]; + } + + return true; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static bool IsFlat(Span levels, int numBlocks, int thresh) + { + int score = 0; + ref short levelsRef = ref MemoryMarshal.GetReference(levels); + nuint offset = 0; + while (numBlocks-- > 0) + { + for (nuint i = 1; i < 16; i++) + { + // omit DC, we're only interested in AC + score += Unsafe.Add(ref levelsRef, offset) != 0 ? 1 : 0; + if (score > thresh) + { + return false; + } + } + + offset += 16; + } + + return true; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Mult8B(int a, int b) => ((a * b) + 128) >> 8; + + [MethodImpl(InliningOptions.ShortMethod)] + private static int QuantDiv(uint n, uint iQ, uint b) => (int)(((n * iQ) + b) >> WebpConstants.QFix); + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8BandProbas.cs b/ImageSharp/Formats/Webp/Lossy/Vp8BandProbas.cs new file mode 100644 index 0000000..8716d3f --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8BandProbas.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// All the probabilities associated to one band. + /// + internal class Vp8BandProbas + { + /// + /// Initializes a new instance of the class. + /// + public Vp8BandProbas() + { + this.Probabilities = new Vp8ProbaArray[WebpConstants.NumCtx]; + for (int i = 0; i < WebpConstants.NumCtx; i++) + { + this.Probabilities[i] = new Vp8ProbaArray(); + } + } + + /// + /// Gets the Probabilities. + /// + public Vp8ProbaArray[] Probabilities { get; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8CostArray.cs b/ImageSharp/Formats/Webp/Lossy/Vp8CostArray.cs new file mode 100644 index 0000000..e4a74a0 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8CostArray.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal class Vp8CostArray + { + /// + /// Initializes a new instance of the class. + /// + public Vp8CostArray() => this.Costs = new ushort[67 + 1]; + + public ushort[] Costs { get; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8Costs.cs b/ImageSharp/Formats/Webp/Lossy/Vp8Costs.cs new file mode 100644 index 0000000..657fdb6 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8Costs.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal class Vp8Costs + { + /// + /// Initializes a new instance of the class. + /// + public Vp8Costs() + { + this.Costs = new Vp8CostArray[WebpConstants.NumCtx]; + for (int i = 0; i < WebpConstants.NumCtx; i++) + { + this.Costs[i] = new Vp8CostArray(); + } + } + + /// + /// Gets the Costs. + /// + public Vp8CostArray[] Costs { get; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs b/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs new file mode 100644 index 0000000..23a3d34 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs @@ -0,0 +1,345 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Webp.BitReader; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// Holds information for decoding a lossy webp image. + /// + internal class Vp8Decoder : IDisposable + { + private Vp8MacroBlock leftMacroBlock; + + /// + /// Initializes a new instance of the class. + /// + /// The frame header. + /// The picture header. + /// The segment header. + /// The probabilities. + /// Used for allocating memory for the pixel data output and the temporary buffers. + public Vp8Decoder(Vp8FrameHeader frameHeader, Vp8PictureHeader pictureHeader, Vp8SegmentHeader segmentHeader, Vp8Proba probabilities, MemoryAllocator memoryAllocator) + { + this.FilterHeader = new Vp8FilterHeader(); + this.FrameHeader = frameHeader; + this.PictureHeader = pictureHeader; + this.SegmentHeader = segmentHeader; + this.Probabilities = probabilities; + this.IntraL = new byte[4]; + this.MbWidth = (int)((this.PictureHeader.Width + 15) >> 4); + this.MbHeight = (int)((this.PictureHeader.Height + 15) >> 4); + this.CacheYStride = 16 * this.MbWidth; + this.CacheUvStride = 8 * this.MbWidth; + this.MacroBlockInfo = new Vp8MacroBlock[this.MbWidth + 1]; + this.MacroBlockData = new Vp8MacroBlockData[this.MbWidth]; + this.YuvTopSamples = new Vp8TopSamples[this.MbWidth]; + this.FilterInfo = new Vp8FilterInfo[this.MbWidth]; + for (int i = 0; i < this.MbWidth; i++) + { + this.MacroBlockInfo[i] = new Vp8MacroBlock(); + this.MacroBlockData[i] = new Vp8MacroBlockData(); + this.YuvTopSamples[i] = new Vp8TopSamples(); + this.FilterInfo[i] = new Vp8FilterInfo(); + } + + this.MacroBlockInfo[this.MbWidth] = new Vp8MacroBlock(); + + this.DeQuantMatrices = new Vp8QuantMatrix[WebpConstants.NumMbSegments]; + this.FilterStrength = new Vp8FilterInfo[WebpConstants.NumMbSegments, 2]; + for (int i = 0; i < WebpConstants.NumMbSegments; i++) + { + this.DeQuantMatrices[i] = new Vp8QuantMatrix(); + for (int j = 0; j < 2; j++) + { + this.FilterStrength[i, j] = new Vp8FilterInfo(); + } + } + + uint width = pictureHeader.Width; + uint height = pictureHeader.Height; + + int extraRows = WebpConstants.FilterExtraRows[(int)LoopFilter.Complex]; // assuming worst case: complex filter + int extraY = extraRows * this.CacheYStride; + int extraUv = extraRows / 2 * this.CacheUvStride; + this.YuvBuffer = memoryAllocator.Allocate((WebpConstants.Bps * 17) + (WebpConstants.Bps * 9) + extraY); + this.CacheY = memoryAllocator.Allocate((16 * this.CacheYStride) + extraY, AllocationOptions.Clean); + int cacheUvSize = (16 * this.CacheUvStride) + extraUv; + this.CacheU = memoryAllocator.Allocate(cacheUvSize); + this.CacheV = memoryAllocator.Allocate(cacheUvSize); + this.TmpYBuffer = memoryAllocator.Allocate((int)width); + this.TmpUBuffer = memoryAllocator.Allocate((int)width); + this.TmpVBuffer = memoryAllocator.Allocate((int)width); + this.Pixels = memoryAllocator.Allocate((int)(width * height * 4), AllocationOptions.Clean); + +#if DEBUG + // Filling those buffers with 205, is only useful for debugging, + // so the default values are the same as the reference libwebp implementation. + this.YuvBuffer.Memory.Span.Fill(205); + this.CacheY.Memory.Span.Fill(205); + this.CacheU.Memory.Span.Fill(205); + this.CacheV.Memory.Span.Fill(205); +#endif + + this.Vp8BitReaders = new Vp8BitReader[WebpConstants.MaxNumPartitions]; + } + + /// + /// Gets the frame header. + /// + public Vp8FrameHeader FrameHeader { get; } + + /// + /// Gets the picture header. + /// + public Vp8PictureHeader PictureHeader { get; } + + /// + /// Gets the filter header. + /// + public Vp8FilterHeader FilterHeader { get; } + + /// + /// Gets the segment header. + /// + public Vp8SegmentHeader SegmentHeader { get; } + + /// + /// Gets or sets the number of partitions minus one. + /// + public int NumPartsMinusOne { get; set; } + + /// + /// Gets the per-partition boolean decoders. + /// + public Vp8BitReader[] Vp8BitReaders { get; } + + /// + /// Gets the dequantization matrices (one set of DC/AC dequant factor per segment). + /// + public Vp8QuantMatrix[] DeQuantMatrices { get; } + + /// + /// Gets or sets a value indicating whether to use the skip probabilities. + /// + public bool UseSkipProbability { get; set; } + + /// + /// Gets or sets the skip probability. + /// + public byte SkipProbability { get; set; } + + /// + /// Gets or sets the Probabilities. + /// + public Vp8Proba Probabilities { get; set; } + + /// + /// Gets or sets the top intra modes values: 4 * MbWidth. + /// + public byte[] IntraT { get; set; } + + /// + /// Gets the left intra modes values. + /// + public byte[] IntraL { get; } + + /// + /// Gets the width in macroblock units. + /// + public int MbWidth { get; } + + /// + /// Gets the height in macroblock units. + /// + public int MbHeight { get; } + + /// + /// Gets or sets the top-left x index of the macroblock that must be in-loop filtered. + /// + public int TopLeftMbX { get; set; } + + /// + /// Gets or sets the top-left y index of the macroblock that must be in-loop filtered. + /// + public int TopLeftMbY { get; set; } + + /// + /// Gets or sets the last bottom-right x index of the macroblock that must be decoded. + /// + public int BottomRightMbX { get; set; } + + /// + /// Gets or sets the last bottom-right y index of the macroblock that must be decoded. + /// + public int BottomRightMbY { get; set; } + + /// + /// Gets or sets the current x position in macroblock units. + /// + public int MbX { get; set; } + + /// + /// Gets or sets the current y position in macroblock units. + /// + public int MbY { get; set; } + + /// + /// Gets the parsed reconstruction data. + /// + public Vp8MacroBlockData[] MacroBlockData { get; } + + /// + /// Gets the contextual macroblock info. + /// + public Vp8MacroBlock[] MacroBlockInfo { get; } + + /// + /// Gets or sets the loop filter used. The purpose of the loop filter is to eliminate (or at least reduce) + /// visually objectionable artifacts. + /// + public LoopFilter Filter { get; set; } + + /// + /// Gets the pre-calculated per-segment filter strengths. + /// + public Vp8FilterInfo[,] FilterStrength { get; } + + public IMemoryOwner YuvBuffer { get; } + + public Vp8TopSamples[] YuvTopSamples { get; } + + public IMemoryOwner CacheY { get; } + + public IMemoryOwner CacheU { get; } + + public IMemoryOwner CacheV { get; } + + public int CacheYOffset { get; set; } + + public int CacheUvOffset { get; set; } + + public int CacheYStride { get; } + + public int CacheUvStride { get; } + + public IMemoryOwner TmpYBuffer { get; } + + public IMemoryOwner TmpUBuffer { get; } + + public IMemoryOwner TmpVBuffer { get; } + + /// + /// Gets the pixel buffer where the decoded pixel data will be stored. + /// + public IMemoryOwner Pixels { get; } + + /// + /// Gets or sets filter info. + /// + public Vp8FilterInfo[] FilterInfo { get; set; } + + public Vp8MacroBlock CurrentMacroBlock => this.MacroBlockInfo[this.MbX]; + + public Vp8MacroBlock LeftMacroBlock => this.leftMacroBlock ??= new Vp8MacroBlock(); + + public Vp8MacroBlockData CurrentBlockData => this.MacroBlockData[this.MbX]; + + public void PrecomputeFilterStrengths() + { + if (this.Filter == LoopFilter.None) + { + return; + } + + Vp8FilterHeader hdr = this.FilterHeader; + for (int s = 0; s < WebpConstants.NumMbSegments; ++s) + { + int baseLevel; + + // First, compute the initial level. + if (this.SegmentHeader.UseSegment) + { + baseLevel = this.SegmentHeader.FilterStrength[s]; + if (!this.SegmentHeader.Delta) + { + baseLevel += hdr.FilterLevel; + } + } + else + { + baseLevel = hdr.FilterLevel; + } + + for (int i4x4 = 0; i4x4 <= 1; i4x4++) + { + Vp8FilterInfo info = this.FilterStrength[s, i4x4]; + int level = baseLevel; + if (hdr.UseLfDelta) + { + level += hdr.RefLfDelta[0]; + if (i4x4 > 0) + { + level += hdr.ModeLfDelta[0]; + } + } + + level = level < 0 ? 0 : level > 63 ? 63 : level; + if (level > 0) + { + int iLevel = level; + if (hdr.Sharpness > 0) + { + if (hdr.Sharpness > 4) + { + iLevel >>= 2; + } + else + { + iLevel >>= 1; + } + + int iLevelCap = 9 - hdr.Sharpness; + if (iLevel > iLevelCap) + { + iLevel = iLevelCap; + } + } + + if (iLevel < 1) + { + iLevel = 1; + } + + info.InnerLevel = (byte)iLevel; + info.Limit = (byte)((2 * level) + iLevel); + info.HighEdgeVarianceThreshold = (byte)(level >= 40 ? 2 : level >= 15 ? 1 : 0); + } + else + { + info.Limit = 0; // no filtering. + } + + info.UseInnerFiltering = i4x4 == 1; + } + } + } + + /// + public void Dispose() + { + this.YuvBuffer.Dispose(); + this.CacheY.Dispose(); + this.CacheU.Dispose(); + this.CacheV.Dispose(); + this.TmpYBuffer.Dispose(); + this.TmpUBuffer.Dispose(); + this.TmpVBuffer.Dispose(); + this.Pixels.Dispose(); + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs b/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs new file mode 100644 index 0000000..d497dc5 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs @@ -0,0 +1,945 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// Iterator structure to iterate through macroblocks, pointing to the + /// right neighbouring data (samples, predictions, contexts, ...) + /// + internal class Vp8EncIterator + { + public const int YOffEnc = 0; + + public const int UOffEnc = 16; + + public const int VOffEnc = 16 + 8; + + private const int MaxIntra16Mode = 2; + + private const int MaxIntra4Mode = 2; + + private const int MaxUvMode = 2; + + private const int DefaultAlpha = -1; + + private readonly int mbw; + + private readonly int mbh; + + /// + /// Stride of the prediction plane(=4*mbw + 1). + /// + private readonly int predsWidth; + + /// + /// Array to record the position of the top sample to pass to the prediction functions. + /// + private readonly byte[] vp8TopLeftI4 = + [ + 17, 21, 25, 29, + 13, 17, 21, 25, + 9, 13, 17, 21, + 5, 9, 13, 17 + ]; + + private int currentMbIdx; + + private int nzIdx; + private int yTopIdx; + + private int uvTopIdx; + + public Vp8EncIterator(Vp8Encoder enc) + : this(enc.YTop, enc.UvTop, enc.Nz, enc.MbInfo, enc.Preds, enc.TopDerr, enc.Mbw, enc.Mbh) + { + } + + public Vp8EncIterator(byte[] yTop, byte[] uvTop, uint[] nz, Vp8MacroBlockInfo[] mb, byte[] preds, sbyte[] topDerr, int mbw, int mbh) + { + this.YTop = yTop; + this.UvTop = uvTop; + this.Nz = nz; + this.Mb = mb; + this.Preds = preds; + this.TopDerr = topDerr; + this.LeftDerr = new sbyte[2 * 2]; + this.mbw = mbw; + this.mbh = mbh; + this.currentMbIdx = 0; + this.nzIdx = 1; + this.yTopIdx = 0; + this.uvTopIdx = 0; + this.predsWidth = (4 * mbw) + 1; + this.PredIdx = this.predsWidth; + this.YuvIn = new byte[WebpConstants.Bps * 16]; + this.YuvOut = new byte[WebpConstants.Bps * 16]; + this.YuvOut2 = new byte[WebpConstants.Bps * 16]; + this.YuvP = new byte[(32 * WebpConstants.Bps) + (16 * WebpConstants.Bps) + (8 * WebpConstants.Bps)]; // I16+Chroma+I4 preds + this.YLeft = new byte[32]; + this.UvLeft = new byte[32]; + this.TopNz = new int[9]; + this.LeftNz = new int[9]; + this.I4Boundary = new byte[37]; + this.BitCount = new long[4, 3]; + this.Scratch = new byte[WebpConstants.Bps * 16]; + this.Scratch2 = new short[17 * 16]; + this.Scratch3 = new int[16]; + + // To match the C initial values of the reference implementation, initialize all with 204. + const byte defaultInitVal = 204; + this.YuvIn.AsSpan().Fill(defaultInitVal); + this.YuvOut.AsSpan().Fill(defaultInitVal); + this.YuvOut2.AsSpan().Fill(defaultInitVal); + this.YuvP.AsSpan().Fill(defaultInitVal); + this.YLeft.AsSpan().Fill(defaultInitVal); + this.UvLeft.AsSpan().Fill(defaultInitVal); + this.Scratch.AsSpan().Fill(defaultInitVal); + + this.Reset(); + } + + /// + /// Gets or sets the current macroblock X value. + /// + public int X { get; set; } + + /// + /// Gets or sets the current macroblock Y. + /// + public int Y { get; set; } + + /// + /// Gets the input samples. + /// + public byte[] YuvIn { get; } + + /// + /// Gets or sets the output samples. + /// + public byte[] YuvOut { get; set; } + + /// + /// Gets or sets the secondary buffer swapped with YuvOut. + /// + public byte[] YuvOut2 { get; set; } + + /// + /// Gets the scratch buffer for prediction. + /// + public byte[] YuvP { get; } + + /// + /// Gets the left luma samples. + /// + public byte[] YLeft { get; } + + /// + /// Gets the left uv samples. + /// + public byte[] UvLeft { get; } + + /// + /// Gets the left error diffusion (u/v). + /// + public sbyte[] LeftDerr { get; } + + /// + /// Gets the top luma samples at position 'X'. + /// + public byte[] YTop { get; } + + /// + /// Gets the top u/v samples at position 'X', packed as 16 bytes. + /// + public byte[] UvTop { get; } + + /// + /// Gets the intra mode predictors (4x4 blocks). + /// + public byte[] Preds { get; } + + /// + /// Gets the current start index of the intra mode predictors. + /// + public int PredIdx { get; private set; } + + /// + /// Gets the non-zero pattern. + /// + public uint[] Nz { get; } + + /// + /// Gets the top diffusion error. + /// + public sbyte[] TopDerr { get; } + + /// + /// Gets 32+5 boundary samples needed by intra4x4. + /// + public byte[] I4Boundary { get; } + + /// + /// Gets or sets the index to the current top boundary sample. + /// + public int I4BoundaryIdx { get; set; } + + /// + /// Gets or sets the current intra4x4 mode being tested. + /// + public int I4 { get; set; } + + /// + /// Gets the top-non-zero context. + /// + public int[] TopNz { get; } + + /// + /// Gets the left-non-zero. leftNz[8] is independent. + /// + public int[] LeftNz { get; } + + /// + /// Gets or sets the macroblock bit-cost for luma. + /// + public long LumaBits { get; set; } + + /// + /// Gets the bit counters for coded levels. + /// + public long[,] BitCount { get; } + + /// + /// Gets or sets the macroblock bit-cost for chroma. + /// + public long UvBits { get; set; } + + /// + /// Gets or sets the number of mb still to be processed. + /// + public int CountDown { get; set; } + + /// + /// Gets the byte scratch buffer. + /// + public byte[] Scratch { get; } + + /// + /// Gets the short scratch buffer. + /// + public short[] Scratch2 { get; } + + /// + /// Gets the int scratch buffer. + /// + public int[] Scratch3 { get; } + + public Vp8MacroBlockInfo CurrentMacroBlockInfo => this.Mb[this.currentMbIdx]; + + private Vp8MacroBlockInfo[] Mb { get; } + + public void Init() => this.Reset(); + + public static void InitFilter() + { + // TODO: add support for autofilter + } + + public void StartI4() + { + int i; + this.I4 = 0; // first 4x4 sub-block. + this.I4BoundaryIdx = this.vp8TopLeftI4[0]; + + // Import the boundary samples. + for (i = 0; i < 17; i++) + { + // left + this.I4Boundary[i] = this.YLeft[15 - i + 1]; + } + + Span yTop = this.YTop.AsSpan(this.yTopIdx); + for (i = 0; i < 16; i++) + { + // top + this.I4Boundary[17 + i] = yTop[i]; + } + + // top-right samples have a special case on the far right of the picture. + if (this.X < this.mbw - 1) + { + for (i = 16; i < 16 + 4; i++) + { + this.I4Boundary[17 + i] = yTop[i]; + } + } + else + { + // else, replicate the last valid pixel four times + for (i = 16; i < 16 + 4; i++) + { + this.I4Boundary[17 + i] = this.I4Boundary[17 + 15]; + } + } + + this.NzToBytes(); // import the non-zero context. + } + + // Import uncompressed samples from source. + public void Import(Span y, Span u, Span v, int yStride, int uvStride, int width, int height, bool importBoundarySamples) + { + int yStartIdx = ((this.Y * yStride) + this.X) * 16; + int uvStartIdx = ((this.Y * uvStride) + this.X) * 8; + Span ySrc = y[yStartIdx..]; + Span uSrc = u[uvStartIdx..]; + Span vSrc = v[uvStartIdx..]; + int w = Math.Min(width - (this.X * 16), 16); + int h = Math.Min(height - (this.Y * 16), 16); + int uvw = (w + 1) >> 1; + int uvh = (h + 1) >> 1; + + Span yuvIn = this.YuvIn.AsSpan(YOffEnc); + Span uIn = this.YuvIn.AsSpan(UOffEnc); + Span vIn = this.YuvIn.AsSpan(VOffEnc); + ImportBlock(ySrc, yStride, yuvIn, w, h, 16); + ImportBlock(uSrc, uvStride, uIn, uvw, uvh, 8); + ImportBlock(vSrc, uvStride, vIn, uvw, uvh, 8); + + if (!importBoundarySamples) + { + return; + } + + // Import source (uncompressed) samples into boundary. + if (this.X == 0) + { + this.InitLeft(); + } + else + { + Span yLeft = this.YLeft.AsSpan(); + Span uLeft = this.UvLeft.AsSpan(0, 16); + Span vLeft = this.UvLeft.AsSpan(16, 16); + if (this.Y == 0) + { + yLeft[0] = 127; + uLeft[0] = 127; + vLeft[0] = 127; + } + else + { + yLeft[0] = y[yStartIdx - 1 - yStride]; + uLeft[0] = u[uvStartIdx - 1 - uvStride]; + vLeft[0] = v[uvStartIdx - 1 - uvStride]; + } + + ImportLine(y[(yStartIdx - 1)..], yStride, yLeft[1..], h, 16); + ImportLine(u[(uvStartIdx - 1)..], uvStride, uLeft[1..], uvh, 8); + ImportLine(v[(uvStartIdx - 1)..], uvStride, vLeft[1..], uvh, 8); + } + + Span yTop = this.YTop.AsSpan(this.yTopIdx, 16); + if (this.Y == 0) + { + yTop.Fill(127); + this.UvTop.AsSpan(this.uvTopIdx, 16).Fill(127); + } + else + { + ImportLine(y[(yStartIdx - yStride)..], 1, yTop, w, 16); + ImportLine(u[(uvStartIdx - uvStride)..], 1, this.UvTop.AsSpan(this.uvTopIdx, 8), uvw, 8); + ImportLine(v[(uvStartIdx - uvStride)..], 1, this.UvTop.AsSpan(this.uvTopIdx + 8, 8), uvw, 8); + } + } + + public int FastMbAnalyze(uint quality) + { + // Empirical cut-off value, should be around 16 (~=block size). We use the + // [8-17] range and favor intra4 at high quality, intra16 for low quality. + uint q = quality; + uint kThreshold = 8 + ((17 - 8) * q / 100); + int k; + Span dc = stackalloc uint[16]; + uint m; + uint m2; + for (k = 0; k < 16; k += 4) + { + LossyUtils.Mean16x4(this.YuvIn.AsSpan(YOffEnc + (k * WebpConstants.Bps)), dc.Slice(k, 4)); + } + + for (m = 0, m2 = 0, k = 0; k < 16; k++) + { + m += dc[k]; + m2 += dc[k] * dc[k]; + } + + if (kThreshold * m2 < m * m) + { + this.SetIntra16Mode(0); // DC16 + } + else + { + Span modes = stackalloc byte[16]; // DC4 + this.SetIntra4Mode(modes); + } + + return 0; + } + + public int MbAnalyzeBestIntra16Mode() + { + const int maxMode = MaxIntra16Mode; + int mode; + int bestAlpha = DefaultAlpha; + int bestMode = 0; + + this.MakeLuma16Preds(); + for (mode = 0; mode < maxMode; mode++) + { + Vp8Histogram histo = new(); + histo.CollectHistogram(this.YuvIn.AsSpan(YOffEnc), this.YuvP.AsSpan(Vp8Encoding.Vp8I16ModeOffsets[mode]), 0, 16); + int alpha = histo.GetAlpha(); + if (alpha > bestAlpha) + { + bestAlpha = alpha; + bestMode = mode; + } + } + + this.SetIntra16Mode(bestMode); + return bestAlpha; + } + + public int MbAnalyzeBestIntra4Mode(int bestAlpha) + { + Span modes = stackalloc byte[16]; + const int maxMode = MaxIntra4Mode; + Vp8Histogram totalHisto = new(); + int curHisto = 0; + this.StartI4(); + do + { + int mode; + int bestModeAlpha = DefaultAlpha; + Vp8Histogram[] histos = new Vp8Histogram[2]; + Span src = this.YuvIn.AsSpan(YOffEnc + WebpLookupTables.Vp8Scan[this.I4]); + + this.MakeIntra4Preds(); + for (mode = 0; mode < maxMode; ++mode) + { + histos[curHisto] = new Vp8Histogram(); + histos[curHisto].CollectHistogram(src, this.YuvP.AsSpan(Vp8Encoding.Vp8I4ModeOffsets[mode]), 0, 1); + + int alpha = histos[curHisto].GetAlpha(); + if (alpha > bestModeAlpha) + { + bestModeAlpha = alpha; + modes[this.I4] = (byte)mode; + + // Keep track of best histo so far. + curHisto ^= 1; + } + } + + // Accumulate best histogram. + histos[curHisto ^ 1].Merge(totalHisto); + } + while (this.RotateI4(this.YuvIn.AsSpan(YOffEnc))); // Note: we reuse the original samples for predictors. + + int i4Alpha = totalHisto.GetAlpha(); + if (i4Alpha > bestAlpha) + { + this.SetIntra4Mode(modes); + bestAlpha = i4Alpha; + } + + return bestAlpha; + } + + public int MbAnalyzeBestUvMode() + { + int bestAlpha = DefaultAlpha; + int smallestAlpha = 0; + int bestMode = 0; + const int maxMode = MaxUvMode; + int mode; + + this.MakeChroma8Preds(); + for (mode = 0; mode < maxMode; ++mode) + { + Vp8Histogram histo = new(); + histo.CollectHistogram(this.YuvIn.AsSpan(UOffEnc), this.YuvP.AsSpan(Vp8Encoding.Vp8UvModeOffsets[mode]), 16, 16 + 4 + 4); + int alpha = histo.GetAlpha(); + if (alpha > bestAlpha) + { + bestAlpha = alpha; + } + + // The best prediction mode tends to be the one with the smallest alpha. + if (mode == 0 || alpha < smallestAlpha) + { + smallestAlpha = alpha; + bestMode = mode; + } + } + + this.SetIntraUvMode(bestMode); + return bestAlpha; + } + + public void SetIntra16Mode(int mode) + { + Span preds = this.Preds.AsSpan(this.PredIdx); + for (int y = 0; y < 4; y++) + { + preds[..4].Fill((byte)mode); + preds = preds[this.predsWidth..]; + } + + this.CurrentMacroBlockInfo.MacroBlockType = Vp8MacroBlockType.I16X16; + } + + public void SetIntra4Mode(ReadOnlySpan modes) + { + int modesIdx = 0; + int predIdx = this.PredIdx; + for (int y = 4; y > 0; y--) + { + modes.Slice(modesIdx, 4).CopyTo(this.Preds.AsSpan(predIdx)); + predIdx += this.predsWidth; + modesIdx += 4; + } + + this.CurrentMacroBlockInfo.MacroBlockType = Vp8MacroBlockType.I4X4; + } + + public int GetCostLuma16(Vp8ModeScore rd, Vp8EncProba proba, Vp8Residual res) + { + int r = 0; + + // re-import the non-zero context. + this.NzToBytes(); + + // DC + res.Init(0, 1, proba); + res.SetCoeffs(rd.YDcLevels); + r += res.GetResidualCost(this.TopNz[8] + this.LeftNz[8]); + + // AC + res.Init(1, 0, proba); + for (int y = 0; y < 4; y++) + { + for (int x = 0; x < 4; x++) + { + int ctx = this.TopNz[x] + this.LeftNz[y]; + res.SetCoeffs(rd.YAcLevels.AsSpan((x + (y * 4)) * 16, 16)); + r += res.GetResidualCost(ctx); + this.TopNz[x] = this.LeftNz[y] = res.Last >= 0 ? 1 : 0; + } + } + + return r; + } + + public short[] GetCostModeI4(byte[] modes) + { + int predsWidth = this.predsWidth; + int predIdx = this.PredIdx; + int x = this.I4 & 3; + int y = this.I4 >> 2; + int left = x == 0 ? this.Preds[predIdx + (y * predsWidth) - 1] : modes[this.I4 - 1]; + int top = y == 0 ? this.Preds[predIdx - predsWidth + x] : modes[this.I4 - 4]; + return WebpLookupTables.Vp8FixedCostsI4[top, left]; + } + + public int GetCostLuma4(Span levels, Vp8EncProba proba, Vp8Residual res) + { + int x = this.I4 & 3; + int y = this.I4 >> 2; + int r = 0; + + res.Init(0, 3, proba); + int ctx = this.TopNz[x] + this.LeftNz[y]; + res.SetCoeffs(levels); + r += res.GetResidualCost(ctx); + return r; + } + + public int GetCostUv(Vp8ModeScore rd, Vp8EncProba proba, Vp8Residual res) + { + int r = 0; + + // re-import the non-zero context. + this.NzToBytes(); + + res.Init(0, 2, proba); + for (int ch = 0; ch <= 2; ch += 2) + { + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 2; x++) + { + int ctx = this.TopNz[4 + ch + x] + this.LeftNz[4 + ch + y]; + res.SetCoeffs(rd.UvLevels.AsSpan(((ch * 2) + x + (y * 2)) * 16, 16)); + r += res.GetResidualCost(ctx); + this.TopNz[4 + ch + x] = this.LeftNz[4 + ch + y] = res.Last >= 0 ? 1 : 0; + } + } + } + + return r; + } + + public void SetIntraUvMode(int mode) => this.CurrentMacroBlockInfo.UvMode = mode; + + public void SetSkip(bool skip) => this.CurrentMacroBlockInfo.Skip = skip; + + public void SetSegment(int segment) => this.CurrentMacroBlockInfo.Segment = segment; + + public void StoreDiffusionErrors(Vp8ModeScore rd) + { + for (int ch = 0; ch <= 1; ++ch) + { + Span top = this.TopDerr.AsSpan((this.X * 4) + ch, 2); + Span left = this.LeftDerr.AsSpan(ch, 2); + + // restore err1 + left[0] = (sbyte)rd.Derr[ch, 0]; + + // 3/4th of err3 + left[1] = (sbyte)((3 * rd.Derr[ch, 2]) >> 2); + + // err2 + top[0] = (sbyte)rd.Derr[ch, 1]; + + // 1/4th of err3. + top[1] = (sbyte)(rd.Derr[ch, 2] - left[1]); + } + } + + /// + /// Returns true if iteration is finished. + /// + /// True if iterator is finished. + public bool IsDone() => this.CountDown <= 0; + + /// + /// Go to next macroblock. + /// + /// Returns false if not finished. + public bool Next() + { + if (++this.X == this.mbw) + { + this.SetRow(++this.Y); + } + else + { + this.currentMbIdx++; + this.nzIdx++; + this.PredIdx += 4; + this.yTopIdx += 16; + this.uvTopIdx += 16; + } + + return --this.CountDown > 0; + } + + public void SaveBoundary() + { + int x = this.X; + int y = this.Y; + Span ySrc = this.YuvOut.AsSpan(YOffEnc); + Span uvSrc = this.YuvOut.AsSpan(UOffEnc); + if (x < this.mbw - 1) + { + // left + for (int i = 0; i < 16; i++) + { + this.YLeft[i + 1] = ySrc[15 + (i * WebpConstants.Bps)]; + } + + for (int i = 0; i < 8; i++) + { + this.UvLeft[i + 1] = uvSrc[7 + (i * WebpConstants.Bps)]; + this.UvLeft[i + 16 + 1] = uvSrc[15 + (i * WebpConstants.Bps)]; + } + + // top-left (before 'top'!) + this.YLeft[0] = this.YTop[this.yTopIdx + 15]; + this.UvLeft[0] = this.UvTop[this.uvTopIdx + 0 + 7]; + this.UvLeft[16] = this.UvTop[this.uvTopIdx + 8 + 7]; + } + + if (y < this.mbh - 1) + { + // top + ySrc.Slice(15 * WebpConstants.Bps, 16).CopyTo(this.YTop.AsSpan(this.yTopIdx)); + uvSrc.Slice(7 * WebpConstants.Bps, 8 + 8).CopyTo(this.UvTop.AsSpan(this.uvTopIdx)); + } + } + + public bool RotateI4(Span yuvOut) + { + Span blk = yuvOut[WebpLookupTables.Vp8Scan[this.I4]..]; + Span top = this.I4Boundary.AsSpan(); + int topOffset = this.I4BoundaryIdx; + int i; + + // Update the cache with 7 fresh samples. + for (i = 0; i <= 3; i++) + { + top[topOffset - 4 + i] = blk[i + (3 * WebpConstants.Bps)]; // Store future top samples. + } + + if ((this.I4 & 3) != 3) + { + // if not on the right sub-blocks #3, #7, #11, #15 + for (i = 0; i <= 2; i++) + { + // store future left samples + top[topOffset + i] = blk[3 + ((2 - i) * WebpConstants.Bps)]; + } + } + else + { + // else replicate top-right samples, as says the specs. + for (i = 0; i <= 3; i++) + { + top[topOffset + i] = top[topOffset + i + 4]; + } + } + + // move pointers to next sub-block + ++this.I4; + if (this.I4 == 16) + { + // we're done + return false; + } + + this.I4BoundaryIdx = this.vp8TopLeftI4[this.I4]; + + return true; + } + + public void ResetAfterSkip() + { + if (this.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16) + { + // Reset all predictors. + this.Nz[this.nzIdx] = 0; + this.LeftNz[8] = 0; + } + else + { + // Preserve the dc_nz bit. + this.Nz[this.nzIdx] &= 1 << 24; + } + } + + public void MakeLuma16Preds() + { + Span left = this.X != 0 ? this.YLeft.AsSpan() : null; + Span top = this.Y != 0 ? this.YTop.AsSpan(this.yTopIdx) : null; + Vp8Encoding.EncPredLuma16(this.YuvP, left, top); + } + + public void MakeChroma8Preds() + { + Span left = this.X != 0 ? this.UvLeft.AsSpan() : null; + Span top = this.Y != 0 ? this.UvTop.AsSpan(this.uvTopIdx) : null; + Vp8Encoding.EncPredChroma8(this.YuvP, left, top); + } + + public void MakeIntra4Preds() => Vp8Encoding.EncPredLuma4(this.YuvP, this.I4Boundary, this.I4BoundaryIdx, this.Scratch.AsSpan(0, 4)); + + public void SwapOut() + { + // Tuple swap uses 2 more IL bytes +#pragma warning disable IDE0180 // Use tuple to swap values + byte[] tmp = this.YuvOut; + this.YuvOut = this.YuvOut2; + this.YuvOut2 = tmp; +#pragma warning restore IDE0180 // Use tuple to swap values + } + + public void NzToBytes() + { + Span nz = this.Nz.AsSpan(); + + uint lnz = nz[this.nzIdx - 1]; + uint tnz = nz[this.nzIdx]; + Span topNz = this.TopNz; + Span leftNz = this.LeftNz; + + // Top-Y + topNz[0] = Bit(tnz, 12); + topNz[1] = Bit(tnz, 13); + topNz[2] = Bit(tnz, 14); + topNz[3] = Bit(tnz, 15); + + // Top-U + topNz[4] = Bit(tnz, 18); + topNz[5] = Bit(tnz, 19); + + // Top-V + topNz[6] = Bit(tnz, 22); + topNz[7] = Bit(tnz, 23); + + // DC + topNz[8] = Bit(tnz, 24); + + // left-Y + leftNz[0] = Bit(lnz, 3); + leftNz[1] = Bit(lnz, 7); + leftNz[2] = Bit(lnz, 11); + leftNz[3] = Bit(lnz, 15); + + // left-U + leftNz[4] = Bit(lnz, 17); + leftNz[5] = Bit(lnz, 19); + + // left-V + leftNz[6] = Bit(lnz, 21); + leftNz[7] = Bit(lnz, 23); + + // left-DC is special, iterated separately. + } + + public void BytesToNz() + { + uint nz = 0; + int[] topNz = this.TopNz; + int[] leftNz = this.LeftNz; + + // top + nz |= (uint)((topNz[0] << 12) | (topNz[1] << 13)); + nz |= (uint)((topNz[2] << 14) | (topNz[3] << 15)); + nz |= (uint)((topNz[4] << 18) | (topNz[5] << 19)); + nz |= (uint)((topNz[6] << 22) | (topNz[7] << 23)); + nz |= (uint)(topNz[8] << 24); // we propagate the top bit, esp. for intra4 + + // left + nz |= (uint)((leftNz[0] << 3) | (leftNz[1] << 7)); + nz |= (uint)(leftNz[2] << 11); + nz |= (uint)((leftNz[4] << 17) | (leftNz[6] << 21)); + + this.Nz[this.nzIdx] = nz; + } + + private static void ImportBlock(Span src, int srcStride, Span dst, int w, int h, int size) + { + int dstIdx = 0; + int srcIdx = 0; + for (int i = 0; i < h; i++) + { + // memcpy(dst, src, w); + src.Slice(srcIdx, w).CopyTo(dst[dstIdx..]); + if (w < size) + { + // memset(dst + w, dst[w - 1], size - w); + dst.Slice(dstIdx + w, size - w).Fill(dst[dstIdx + w - 1]); + } + + dstIdx += WebpConstants.Bps; + srcIdx += srcStride; + } + + for (int i = h; i < size; i++) + { + // memcpy(dst, dst - BPS, size); + dst.Slice(dstIdx - WebpConstants.Bps, size).CopyTo(dst[dstIdx..]); + dstIdx += WebpConstants.Bps; + } + } + + private static void ImportLine(Span src, int srcStride, Span dst, int len, int totalLen) + { + int i; + int srcIdx = 0; + for (i = 0; i < len; i++) + { + dst[i] = src[srcIdx]; + srcIdx += srcStride; + } + + for (; i < totalLen; i++) + { + dst[i] = dst[len - 1]; + } + } + + /// + /// Restart a scan. + /// + private void Reset() + { + this.SetRow(0); + this.SetCountDown(this.mbw * this.mbh); + this.InitTop(); + + Array.Clear(this.BitCount); + } + + /// + /// Reset iterator position to row 'y'. + /// + /// The y position. + private void SetRow(int y) + { + this.X = 0; + this.Y = y; + this.currentMbIdx = y * this.mbw; + this.nzIdx = 1; // note: in reference source nz starts at -1. + this.yTopIdx = 0; + this.uvTopIdx = 0; + this.PredIdx = this.predsWidth + (y * 4 * this.predsWidth); + + this.InitLeft(); + } + + private void InitLeft() + { + Span yLeft = this.YLeft.AsSpan(); + Span uLeft = this.UvLeft.AsSpan(0, 16); + Span vLeft = this.UvLeft.AsSpan(16, 16); + byte val = (byte)(this.Y > 0 ? 129 : 127); + yLeft[0] = val; + uLeft[0] = val; + vLeft[0] = val; + + yLeft.Slice(1, 16).Fill(129); + uLeft.Slice(1, 8).Fill(129); + vLeft.Slice(1, 8).Fill(129); + + this.LeftNz[8] = 0; + + this.LeftDerr.AsSpan().Clear(); + } + + private void InitTop() + { + int topSize = this.mbw * 16; + this.YTop.AsSpan(0, topSize).Fill(127); + this.UvTop.AsSpan().Fill(127); + this.Nz.AsSpan().Clear(); + + int predsW = (4 * this.mbw) + 1; + int predsH = (4 * this.mbh) + 1; + int predsSize = predsW * predsH; + this.Preds.AsSpan(predsSize + this.predsWidth, this.mbw).Clear(); + + this.TopDerr.AsSpan().Clear(); + } + + private static int Bit(uint nz, int n) => (nz & (1 << n)) != 0 ? 1 : 0; + + /// + /// Set count down. + /// + /// Number of iterations to go. + private void SetCountDown(int countDown) => this.CountDown = countDown; + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8EncProba.cs b/ImageSharp/Formats/Webp/Lossy/Vp8EncProba.cs new file mode 100644 index 0000000..603cdd7 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8EncProba.cs @@ -0,0 +1,264 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal class Vp8EncProba + { + /// + /// Last (inclusive) level with variable cost. + /// + private const int MaxVariableLevel = 67; + + /// + /// Value below which using skipProba is OK. + /// + private const int SkipProbaThreshold = 250; + + /// + /// Initializes a new instance of the class. + /// + public Vp8EncProba() + { + this.Dirty = true; + this.UseSkipProba = false; + this.Segments = new byte[3]; + this.Coeffs = new Vp8BandProbas[WebpConstants.NumTypes][]; + for (int i = 0; i < this.Coeffs.Length; i++) + { + this.Coeffs[i] = new Vp8BandProbas[WebpConstants.NumBands]; + for (int j = 0; j < this.Coeffs[i].Length; j++) + { + this.Coeffs[i][j] = new Vp8BandProbas(); + } + } + + this.Stats = new Vp8Stats[WebpConstants.NumTypes][]; + for (int i = 0; i < this.Coeffs.Length; i++) + { + this.Stats[i] = new Vp8Stats[WebpConstants.NumBands]; + for (int j = 0; j < this.Stats[i].Length; j++) + { + this.Stats[i][j] = new Vp8Stats(); + } + } + + this.LevelCost = new Vp8Costs[WebpConstants.NumTypes][]; + for (int i = 0; i < this.LevelCost.Length; i++) + { + this.LevelCost[i] = new Vp8Costs[WebpConstants.NumBands]; + for (int j = 0; j < this.LevelCost[i].Length; j++) + { + this.LevelCost[i][j] = new Vp8Costs(); + } + } + + this.RemappedCosts = new Vp8Costs[WebpConstants.NumTypes][]; + for (int i = 0; i < this.RemappedCosts.Length; i++) + { + this.RemappedCosts[i] = new Vp8Costs[16]; + for (int j = 0; j < this.RemappedCosts[i].Length; j++) + { + this.RemappedCosts[i][j] = new Vp8Costs(); + } + } + + // Initialize with default probabilities. + this.Segments.AsSpan().Fill(255); + for (int t = 0; t < WebpConstants.NumTypes; ++t) + { + for (int b = 0; b < WebpConstants.NumBands; ++b) + { + for (int c = 0; c < WebpConstants.NumCtx; ++c) + { + Vp8ProbaArray dst = this.Coeffs[t][b].Probabilities[c]; + for (int p = 0; p < WebpConstants.NumProbas; ++p) + { + dst.Probabilities[p] = WebpLookupTables.DefaultCoeffsProba[t, b, c, p]; + } + } + } + } + } + + /// + /// Gets the probabilities for segment tree. + /// + public byte[] Segments { get; } + + /// + /// Gets or sets the final probability of being skipped. + /// + public byte SkipProba { get; set; } + + /// + /// Gets or sets a value indicating whether to use the skip probability. + /// + public bool UseSkipProba { get; set; } + + public Vp8BandProbas[][] Coeffs { get; } + + public Vp8Stats[][] Stats { get; } + + public Vp8Costs[][] LevelCost { get; } + + public Vp8Costs[][] RemappedCosts { get; } + + /// + /// Gets or sets the number of skipped blocks. + /// + public int NbSkip { get; set; } + + /// + /// Gets or sets a value indicating whether CalculateLevelCosts() needs to be called. + /// + public bool Dirty { get; set; } + + public void CalculateLevelCosts() + { + if (!this.Dirty) + { + return; // Nothing to do. + } + + for (int ctype = 0; ctype < WebpConstants.NumTypes; ++ctype) + { + for (int band = 0; band < WebpConstants.NumBands; ++band) + { + for (int ctx = 0; ctx < WebpConstants.NumCtx; ++ctx) + { + Vp8ProbaArray p = this.Coeffs[ctype][band].Probabilities[ctx]; + Vp8CostArray table = this.LevelCost[ctype][band].Costs[ctx]; + int cost0 = ctx > 0 ? LossyUtils.Vp8BitCost(1, p.Probabilities[0]) : 0; + int costBase = LossyUtils.Vp8BitCost(1, p.Probabilities[1]) + cost0; + int v; + table.Costs[0] = (ushort)(LossyUtils.Vp8BitCost(0, p.Probabilities[1]) + cost0); + for (v = 1; v <= MaxVariableLevel; ++v) + { + table.Costs[v] = (ushort)(costBase + VariableLevelCost(v, p.Probabilities)); + } + + // Starting at level 67 and up, the variable part of the cost is actually constant + } + } + + for (int n = 0; n < 16; ++n) + { + for (int ctx = 0; ctx < WebpConstants.NumCtx; ++ctx) + { + Vp8CostArray dst = this.RemappedCosts[ctype][n].Costs[ctx]; + Vp8CostArray src = this.LevelCost[ctype][WebpConstants.Vp8EncBands[n]].Costs[ctx]; + src.Costs.CopyTo(dst.Costs.AsSpan()); + } + } + } + + this.Dirty = false; + } + + public int FinalizeTokenProbas() + { + bool hasChanged = false; + int size = 0; + for (int t = 0; t < WebpConstants.NumTypes; ++t) + { + for (int b = 0; b < WebpConstants.NumBands; ++b) + { + for (int c = 0; c < WebpConstants.NumCtx; ++c) + { + for (int p = 0; p < WebpConstants.NumProbas; ++p) + { + uint stats = this.Stats[t][b].Stats[c].Stats[p]; + int nb = (int)((stats >> 0) & 0xffff); + int total = (int)((stats >> 16) & 0xffff); + int updateProba = WebpLookupTables.CoeffsUpdateProba[t, b, c, p]; + int oldP = WebpLookupTables.DefaultCoeffsProba[t, b, c, p]; + int newP = CalcTokenProba(nb, total); + int oldCost = BranchCost(nb, total, oldP) + LossyUtils.Vp8BitCost(0, (byte)updateProba); + int newCost = BranchCost(nb, total, newP) + LossyUtils.Vp8BitCost(1, (byte)updateProba) + (8 * 256); + bool useNewP = oldCost > newCost; + size += LossyUtils.Vp8BitCost(useNewP ? 1 : 0, (byte)updateProba); + if (useNewP) + { + // Only use proba that seem meaningful enough. + this.Coeffs[t][b].Probabilities[c].Probabilities[p] = (byte)newP; + hasChanged |= newP != oldP; + size += 8 * 256; + } + else + { + this.Coeffs[t][b].Probabilities[c].Probabilities[p] = (byte)oldP; + } + } + } + } + } + + this.Dirty = hasChanged; + return size; + } + + public int FinalizeSkipProba(int mbw, int mbh) + { + int nbMbs = mbw * mbh; + int nbEvents = this.NbSkip; + this.SkipProba = (byte)CalcSkipProba(nbEvents, nbMbs); + this.UseSkipProba = this.SkipProba < SkipProbaThreshold; + + int size = 256; + if (this.UseSkipProba) + { + size += (nbEvents * LossyUtils.Vp8BitCost(1, this.SkipProba)) + ((nbMbs - nbEvents) * LossyUtils.Vp8BitCost(0, this.SkipProba)); + size += 8 * 256; // cost of signaling the skipProba itself. + } + + return size; + } + + public void ResetTokenStats() + { + for (int t = 0; t < WebpConstants.NumTypes; ++t) + { + for (int b = 0; b < WebpConstants.NumBands; ++b) + { + for (int c = 0; c < WebpConstants.NumCtx; ++c) + { + for (int p = 0; p < WebpConstants.NumProbas; ++p) + { + this.Stats[t][b].Stats[c].Stats[p] = 0; + } + } + } + } + } + + private static int CalcSkipProba(long nb, long total) => (int)(total != 0 ? (total - nb) * 255 / total : 255); + + private static int VariableLevelCost(int level, Span probas) + { + int pattern = WebpLookupTables.Vp8LevelCodes[level - 1][0]; + int bits = WebpLookupTables.Vp8LevelCodes[level - 1][1]; + int cost = 0; + for (int i = 2; pattern != 0; i++) + { + if ((pattern & 1) != 0) + { + cost += LossyUtils.Vp8BitCost(bits & 1, probas[i]); + } + + bits >>= 1; + pattern >>= 1; + } + + return cost; + } + + // Collect statistics and deduce probabilities for next coding pass. + // Return the total bit-cost for coding the probability updates. + private static int CalcTokenProba(int nb, int total) => nb != 0 ? (255 - (nb * 255 / total)) : 255; + + // Cost of coding 'nb' 1's and 'total-nb' 0's using 'proba' probability. + private static int BranchCost(int nb, int total, int proba) => (nb * LossyUtils.Vp8BitCost(1, (byte)proba)) + ((total - nb) * LossyUtils.Vp8BitCost(0, (byte)proba)); + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8EncSegmentHeader.cs b/ImageSharp/Formats/Webp/Lossy/Vp8EncSegmentHeader.cs new file mode 100644 index 0000000..5c6f1ea --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8EncSegmentHeader.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal class Vp8EncSegmentHeader + { + /// + /// Initializes a new instance of the class. + /// + /// Number of segments. + public Vp8EncSegmentHeader(int numSegments) + { + this.NumSegments = numSegments; + this.UpdateMap = this.NumSegments > 1; + this.Size = 0; + } + + /// + /// Gets the actual number of segments. 1 segment only = unused. + /// + public int NumSegments { get; } + + /// + /// Gets or sets a value indicating whether to update the segment map or not. Must be false if there's only 1 segment. + /// + public bool UpdateMap { get; set; } + + /// + /// Gets or sets the bit-cost for transmitting the segment map. + /// + public int Size { get; set; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs new file mode 100644 index 0000000..0d19f61 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -0,0 +1,1296 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Webp.BitWriter; +using SixLabors.ImageSharp.Formats.Webp.Chunks; +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.Webp.Lossy { + /// + /// Encoder for lossy webp images. + /// + internal class Vp8Encoder : IDisposable + { + /// + /// The to use for buffer allocations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// The quality, that will be used to encode the image. + /// + private readonly uint quality; + + /// + /// Quality/speed trade-off (0=fast, 6=slower-better). + /// + private readonly WebpEncodingMethod method; + + /// + /// Number of entropy-analysis passes (in [1..10]). + /// + private readonly int entropyPasses; + + /// + /// Specify the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering). A value of 0 will turn off any filtering. + /// + private readonly int filterStrength; + + /// + /// The spatial noise shaping. 0=off, 100=maximum. + /// + private readonly int spatialNoiseShaping; + + /// + /// A bit writer for writing lossy webp streams. + /// + private Vp8BitWriter bitWriter; + + /// + /// Whether to skip metadata during encoding. + /// + private readonly bool skipMetadata; + + private readonly Vp8RdLevel rdOptLevel; + + private int maxI4HeaderBits; + + /// + /// Global susceptibility. + /// + private int alpha; + + /// + /// U/V quantization susceptibility. + /// + private int uvAlpha; + + private readonly bool alphaCompression; + + private const int NumMbSegments = 4; + + private const int MaxItersKMeans = 6; + + // Convergence is considered reached if dq < DqLimit + private const float DqLimit = 0.4f; + + private const ulong Partition0SizeLimit = (WebpConstants.Vp8MaxPartition0Size - 2048UL) << 11; + + private const long HeaderSizeEstimate = + WebpConstants.RiffHeaderSize + WebpConstants.ChunkHeaderSize + WebpConstants.Vp8FrameHeaderSize; + + private const int QMin = 0; + + private const int QMax = 100; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The global configuration. + /// The width of the input image. + /// The height of the input image. + /// The encoding quality. + /// Whether to skip metadata encoding. + /// Quality/speed trade-off (0=fast, 6=slower-better). + /// Number of entropy-analysis passes (in [1..10]). + /// The filter the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering). + /// The spatial noise shaping. 0=off, 100=maximum. + /// If true, the alpha channel will be compressed with the lossless compression. + public Vp8Encoder( + MemoryAllocator memoryAllocator, + Configuration configuration, + int width, + int height, + uint quality, + bool skipMetadata, + WebpEncodingMethod method, + int entropyPasses, + int filterStrength, + int spatialNoiseShaping, + bool alphaCompression) + { + this.memoryAllocator = memoryAllocator; + this.configuration = configuration; + this.Width = width; + this.Height = height; + this.quality = Math.Min(quality, 100); + this.skipMetadata = skipMetadata; + this.method = method; + this.entropyPasses = Numerics.Clamp(entropyPasses, 1, 10); + this.filterStrength = Numerics.Clamp(filterStrength, 0, 100); + this.spatialNoiseShaping = Numerics.Clamp(spatialNoiseShaping, 0, 100); + this.alphaCompression = alphaCompression; + if (method is WebpEncodingMethod.BestQuality) + { + this.rdOptLevel = Vp8RdLevel.RdOptTrellisAll; + } + else if (method >= WebpEncodingMethod.Level5) + { + this.rdOptLevel = Vp8RdLevel.RdOptTrellis; + } + else if (method >= WebpEncodingMethod.Level3) + { + this.rdOptLevel = Vp8RdLevel.RdOptBasic; + } + else + { + this.rdOptLevel = Vp8RdLevel.RdOptNone; + } + + int pixelCount = width * height; + this.Mbw = (width + 15) >> 4; + this.Mbh = (height + 15) >> 4; + int uvSize = ((width + 1) >> 1) * ((height + 1) >> 1); + this.Y = this.memoryAllocator.Allocate(pixelCount); + this.U = this.memoryAllocator.Allocate(uvSize); + this.V = this.memoryAllocator.Allocate(uvSize); + this.YTop = new byte[this.Mbw * 16]; + this.UvTop = new byte[this.Mbw * 16 * 2]; + this.Nz = new uint[this.Mbw + 1]; + this.MbHeaderLimit = 256 * 510 * 8 * 1024 / (this.Mbw * this.Mbh); + this.TopDerr = new sbyte[this.Mbw * 4]; + + // TODO: make partition_limit configurable? + const int limit = 100; // original code: limit = 100 - config->partition_limit; + this.maxI4HeaderBits = + 256 * 16 * 16 * limit * limit / (100 * 100); // ... modulated with a quadratic curve. + + this.MbInfo = new Vp8MacroBlockInfo[this.Mbw * this.Mbh]; + for (int i = 0; i < this.MbInfo.Length; i++) + { + this.MbInfo[i] = new Vp8MacroBlockInfo(); + } + + this.SegmentInfos = new Vp8SegmentInfo[4]; + for (int i = 0; i < 4; i++) + { + this.SegmentInfos[i] = new Vp8SegmentInfo(); + } + + this.FilterHeader = new Vp8FilterHeader(); + int predSize = (((4 * this.Mbw) + 1) * ((4 * this.Mbh) + 1)) + this.PredsWidth + 1; + this.PredsWidth = (4 * this.Mbw) + 1; + this.Proba = new Vp8EncProba(); + this.Preds = new byte[predSize + this.PredsWidth + this.Mbw]; + + // Initialize with default values, which the reference c implementation uses, + // to be able to compare to the original and spot differences. + this.Preds.AsSpan().Fill(205); + this.Nz.AsSpan().Fill(3452816845); + + this.ResetBoundaryPredictions(); + } + + // This uses C#'s optimization to refer to the static data segment of the assembly, no allocation occurs. + private static ReadOnlySpan AverageBytesPerMb => [50, 24, 16, 9, 7, 5, 3, 2]; + + public int BaseQuant { get; set; } + + /// + /// Gets the probabilities. + /// + public Vp8EncProba Proba { get; } + + /// + /// Gets the segment features. + /// + public Vp8EncSegmentHeader SegmentHeader { get; private set; } + + /// + /// Gets the segment infos. + /// + public Vp8SegmentInfo[] SegmentInfos { get; } + + /// + /// Gets the macro block info's. + /// + public Vp8MacroBlockInfo[] MbInfo { get; } + + /// + /// Gets the filter header. + /// + public Vp8FilterHeader FilterHeader { get; } + + /// + /// Gets or sets the global susceptibility. + /// + public int Alpha { get; set; } + + /// + /// Gets the width of the image. + /// + public int Width { get; } + + /// + /// Gets the height of the image. + /// + public int Height { get; } + + /// + /// Gets the stride of the prediction plane (=4*mb_w + 1) + /// + public int PredsWidth { get; } + + /// + /// Gets the macroblock width. + /// + public int Mbw { get; } + + /// + /// Gets the macroblock height. + /// + public int Mbh { get; } + + public int DqY1Dc { get; private set; } + + public int DqY2Ac { get; private set; } + + public int DqY2Dc { get; private set; } + + public int DqUvAc { get; private set; } + + public int DqUvDc { get; private set; } + + /// + /// Gets the luma component. + /// + private IMemoryOwner Y { get; } + + /// + /// Gets the chroma U component. + /// + private IMemoryOwner U { get; } + + /// + /// Gets the chroma U component. + /// + private IMemoryOwner V { get; } + + /// + /// Gets the top luma samples. + /// + public byte[] YTop { get; } + + /// + /// Gets the top u/v samples. U and V are packed into 16 bytes (8 U + 8 V). + /// + public byte[] UvTop { get; } + + /// + /// Gets the non-zero pattern. + /// + public uint[] Nz { get; } + + /// + /// Gets the prediction modes: (4*mbw+1) * (4*mbh+1). + /// + public byte[] Preds { get; } + + /// + /// Gets the diffusion error. + /// + public sbyte[] TopDerr { get; } + + /// + /// Gets a rough limit for header bits per MB. + /// + private int MbHeaderLimit { get; } + + public WebpVp8X EncodeHeader(Image image, Stream stream, bool hasAlpha, bool hasAnimation) + where TPixel : unmanaged, IPixel + { + // Write bytes from the bitwriter buffer to the stream. + ImageMetadata metadata = image.Metadata; + ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; + XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + + WebpVp8X vp8x = BitWriterBase.WriteTrunksBeforeData( + stream, + (uint)image.Width, + (uint)image.Height, + exifProfile, + xmpProfile, + metadata.IccProfile, + hasAlpha, + hasAnimation); + + if (hasAnimation) + { + WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata(); + BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount); + } + + return vp8x; + } + + public void EncodeFooter(Image image, in WebpVp8X vp8x, bool hasAlpha, Stream stream, long initialPosition) + where TPixel : unmanaged, IPixel + { + // Write bytes from the bitwriter buffer to the stream. + ImageMetadata metadata = image.Metadata; + + ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; + XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + + bool updateVp8x = hasAlpha && vp8x != default; + WebpVp8X updated = updateVp8x ? vp8x.WithAlpha(true) : vp8x; + BitWriterBase.WriteTrunksAfterData(stream, in updated, updateVp8x, initialPosition, exifProfile, xmpProfile); + } + + /// + /// Encodes the animated image frame to the specified stream. + /// + /// The pixel format. + /// The image frame to encode from. + /// The stream to encode the image data to. + /// The region of interest within the frame to encode. + /// The frame metadata. + /// A indicating whether the frame contains an alpha channel. + public bool EncodeAnimation(ImageFrame frame, Stream stream, Rectangle bounds, WebpFrameMetadata frameMetadata) + where TPixel : unmanaged, IPixel + => this.Encode(stream, frame, bounds, frameMetadata, true, null); + + /// + /// Encodes the static image frame to the specified stream. + /// + /// The pixel format. + /// The stream to encode the image data to. + /// The image to encode from. + public void EncodeStatic(Stream stream, Image image) + where TPixel : unmanaged, IPixel + { + ImageFrame frame = image.Frames.RootFrame; + this.Encode(stream, frame, image.Bounds, frame.Metadata.GetWebpMetadata(), false, image); + } + + /// + /// Encodes the image to the specified stream. + /// + /// The pixel format. + /// The stream to encode the image data to. + /// The image frame to encode from. + /// The region of interest within the frame to encode. + /// The frame metadata. + /// Flag indicating, if an animation parameter is present. + /// The image to encode from. + /// A indicating whether the frame contains an alpha channel. + private bool Encode( + Stream stream, + ImageFrame frame, + Rectangle bounds, + WebpFrameMetadata frameMetadata, + bool hasAnimation, + Image image) + where TPixel : unmanaged, IPixel + { + int width = bounds.Width; + int height = bounds.Height; + + int pixelCount = width * height; + Span y = this.Y.GetSpan(); + Span u = this.U.GetSpan(); + Span v = this.V.GetSpan(); + + Buffer2DRegion pixels = frame.PixelBuffer.GetRegion(bounds); + bool hasAlpha = YuvConversion.ConvertRgbToYuv(pixels, this.configuration, this.memoryAllocator, y, u, v); + + if (!hasAnimation) + { + this.EncodeHeader(image, stream, hasAlpha, false); + } + + int yStride = width; + int uvStride = (yStride + 1) >> 1; + + Vp8EncIterator it = new(this); + Span alphas = stackalloc int[WebpConstants.MaxAlpha + 1]; + this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha); + int totalMb = this.Mbw * this.Mbw; + this.alpha /= totalMb; + this.uvAlpha /= totalMb; + + // Analysis is done, proceed to actual encoding. + this.SegmentHeader = new Vp8EncSegmentHeader(4); + this.AssignSegments(alphas); + this.SetLoopParams(this.quality); + + // Initialize the bitwriter. + int averageBytesPerMacroBlock = AverageBytesPerMb[this.BaseQuant >> 4]; + int expectedSize = this.Mbw * this.Mbh * averageBytesPerMacroBlock; + this.bitWriter = new Vp8BitWriter(expectedSize, this); + + // Stats-collection loop. + this.StatLoop(width, height, yStride, uvStride); + it.Init(); + Vp8EncIterator.InitFilter(); + Vp8ModeScore info = new(); + Vp8Residual residual = new(); + do + { + bool dontUseSkip = !this.Proba.UseSkipProba; + info.Clear(); + it.Import(y, u, v, yStride, uvStride, width, height, false); + + // Warning! order is important: first call VP8Decimate() and + // *then* decide how to code the skip decision if there's one. + if (!this.Decimate(it, ref info, this.rdOptLevel) || dontUseSkip) + { + this.CodeResiduals(it, info, residual); + } + else + { + it.ResetAfterSkip(); + } + + it.SaveBoundary(); + } + while (it.Next()); + + // Store filter stats. + this.AdjustFilterStrength(); + + // Extract and encode alpha channel data, if present. + int alphaDataSize = 0; + bool alphaCompressionSucceeded = false; + Span alphaData = []; + IMemoryOwner encodedAlphaData = null; + try + { + if (hasAlpha) + { + // TODO: This can potentially run in an separate task. + encodedAlphaData = AlphaEncoder.EncodeAlpha( + pixels, + this.configuration, + this.memoryAllocator, + this.skipMetadata, + this.alphaCompression, + out alphaDataSize); + + alphaData = encodedAlphaData.GetSpan(); + if (alphaDataSize < pixelCount) + { + // Only use compressed data, if the compressed data is actually smaller then the uncompressed data. + alphaCompressionSucceeded = true; + } + } + + this.bitWriter.Finish(); + + long prevPosition = 0; + + if (hasAnimation) + { + prevPosition = new WebpFrameData( + (uint)bounds.X, + (uint)bounds.Y, + (uint)bounds.Width, + (uint)bounds.Height, + frameMetadata.FrameDelay, + frameMetadata.BlendMode, + frameMetadata.DisposalMode) + .WriteHeaderTo(stream); + } + + if (hasAlpha) + { + Span data = alphaData[..alphaDataSize]; + bool alphaDataIsCompressed = this.alphaCompression && alphaCompressionSucceeded; + BitWriterBase.WriteAlphaChunk(stream, data, alphaDataIsCompressed); + } + + this.bitWriter.WriteEncodedImageToStream(stream); + + if (hasAnimation) + { + RiffHelper.EndWriteChunk(stream, prevPosition); + } + } + finally + { + encodedAlphaData?.Dispose(); + } + + return hasAlpha; + } + + /// + public void Dispose() + { + this.Y.Dispose(); + this.U.Dispose(); + this.V.Dispose(); + } + + /// + /// Only collect statistics(number of skips, token usage, ...). + /// This is used for deciding optimal probabilities. It also modifies the + /// quantizer value if some target (size, PSNR) was specified. + /// + /// The image width. + /// The image height. + /// The y-luminance stride. + /// The uv stride. + private void StatLoop(int width, int height, int yStride, int uvStride) + { + const int targetSize = 0; // TODO: target size is hardcoded. + const float targetPsnr = 0.0f; // TODO: targetPsnr is hardcoded. + const bool doSearch = targetSize > 0 || targetPsnr > 0; + bool fastProbe = (this.method == 0 || this.method == WebpEncodingMethod.Level3) && !doSearch; + int numPassLeft = this.entropyPasses; + Vp8RdLevel rdOpt = this.method >= WebpEncodingMethod.Level3 || doSearch ? Vp8RdLevel.RdOptBasic : Vp8RdLevel.RdOptNone; + int nbMbs = this.Mbw * this.Mbh; + + PassStats stats = new(targetSize, targetPsnr, QMin, QMax, this.quality); + this.Proba.ResetTokenStats(); + + // Fast mode: quick analysis pass over few mbs. Better than nothing. + if (fastProbe) + { + if (this.method == WebpEncodingMethod.Level3) + { + // We need more stats for method 3 to be reliable. + nbMbs = nbMbs > 200 ? nbMbs >> 1 : 100; + } + else + { + nbMbs = nbMbs > 200 ? nbMbs >> 2 : 50; + } + } + + while (numPassLeft-- > 0) + { + bool isLastPass = (MathF.Abs(stats.Dq) <= DqLimit) || (numPassLeft == 0) || (this.maxI4HeaderBits == 0); + long sizeP0 = this.OneStatPass(width, height, yStride, uvStride, rdOpt, nbMbs, stats); + if (sizeP0 == 0) + { + return; + } + + if (this.maxI4HeaderBits > 0 && sizeP0 > (long)Partition0SizeLimit) + { + ++numPassLeft; + this.maxI4HeaderBits >>= 1; // strengthen header bit limitation... + continue; // ...and start over + } + + if (isLastPass) + { + break; + } + + // If no target size: just do several pass without changing 'q' + if (doSearch) + { + // Unreachable due to hardcoding above. +#pragma warning disable CS0162 // Unreachable code detected + stats.ComputeNextQ(); +#pragma warning restore CS0162 // Unreachable code detected + if (MathF.Abs(stats.Dq) <= DqLimit) + { + break; + } + } + } + + if (!doSearch || !stats.DoSizeSearch) + { + // Need to finalize probas now, since it wasn't done during the search. + this.Proba.FinalizeSkipProba(this.Mbw, this.Mbh); + this.Proba.FinalizeTokenProbas(); + } + + // Finalize costs. + this.Proba.CalculateLevelCosts(); + } + + private long OneStatPass(int width, int height, int yStride, int uvStride, Vp8RdLevel rdOpt, int nbMbs, PassStats stats) + { + Span y = this.Y.GetSpan(); + Span u = this.U.GetSpan(); + Span v = this.V.GetSpan(); + Vp8EncIterator it = new(this); + long size = 0; + long sizeP0 = 0; + long distortion = 0; + long pixelCount = nbMbs * 384; + + it.Init(); + this.SetLoopParams(stats.Q); + Vp8ModeScore info = new(); + do + { + info.Clear(); + it.Import(y, u, v, yStride, uvStride, width, height, false); + if (this.Decimate(it, ref info, rdOpt)) + { + // Just record the number of skips and act like skipProba is not used. + ++this.Proba.NbSkip; + } + + this.RecordResiduals(it, info); + size += info.R + info.H; + sizeP0 += info.H; + distortion += info.D; + + it.SaveBoundary(); + } + while (it.Next() && --nbMbs > 0); + + sizeP0 += this.SegmentHeader.Size; + if (stats.DoSizeSearch) + { + size += this.Proba.FinalizeSkipProba(this.Mbw, this.Mbh); + size += this.Proba.FinalizeTokenProbas(); + size = ((size + sizeP0 + 1024) >> 11) + HeaderSizeEstimate; + stats.Value = size; + } + else + { + stats.Value = GetPsnr(distortion, pixelCount); + } + + return sizeP0; + } + + private void SetLoopParams(float q) + { + // Setup segment quantizations and filters. + this.SetSegmentParams(q); + + // Compute segment probabilities. + this.SetSegmentProbas(); + + this.ResetStats(); + } + + private unsafe void AdjustFilterStrength() + { + if (this.filterStrength > 0) + { + int maxLevel = 0; + for (int s = 0; s < WebpConstants.NumMbSegments; s++) + { + Vp8SegmentInfo dqm = this.SegmentInfos[s]; + + // this '>> 3' accounts for some inverse WHT scaling + int delta = (dqm.MaxEdge * dqm.Y2.Q[1]) >> 3; + int level = FilterStrengthFromDelta(this.FilterHeader.Sharpness, delta); + if (level > dqm.FStrength) + { + dqm.FStrength = level; + } + + if (maxLevel < dqm.FStrength) + { + maxLevel = dqm.FStrength; + } + } + + this.FilterHeader.FilterLevel = maxLevel; + } + } + + private void ResetBoundaryPredictions() + { + Span top = this.Preds.AsSpan(); // original source top starts at: enc->preds_ - enc->preds_w_ + Span left = this.Preds.AsSpan(this.PredsWidth - 1); + for (int i = 0; i < 4 * this.Mbw; i++) + { + top[i] = (int)IntraPredictionMode.DcPrediction; + } + + for (int i = 0; i < 4 * this.Mbh; i++) + { + left[i * this.PredsWidth] = (int)IntraPredictionMode.DcPrediction; + } + + int predsW = (4 * this.Mbw) + 1; + int predsH = (4 * this.Mbh) + 1; + int predsSize = predsW * predsH; + this.Preds.AsSpan(predsSize + this.PredsWidth - 4, 4).Clear(); + + this.Nz[0] = 0; // constant + } + + // Simplified k-Means, to assign Nb segments based on alpha-histogram. + private void AssignSegments(ReadOnlySpan alphas) + { + int nb = this.SegmentHeader.NumSegments < NumMbSegments ? this.SegmentHeader.NumSegments : NumMbSegments; + Span centers = stackalloc int[NumMbSegments]; + int weightedAverage = 0; + Span map = stackalloc int[WebpConstants.MaxAlpha + 1]; + int n, k; + Span accum = stackalloc int[NumMbSegments]; + Span distAccum = stackalloc int[NumMbSegments]; + + // Bracket the input. + for (n = 0; n <= WebpConstants.MaxAlpha && alphas[n] == 0; ++n) + { + } + + int minA = n; + for (n = WebpConstants.MaxAlpha; n > minA && alphas[n] == 0; --n) + { + } + + int maxA = n; + int rangeA = maxA - minA; + + // Spread initial centers evenly. + for (k = 0, n = 1; k < nb; ++k, n += 2) + { + centers[k] = minA + (n * rangeA / (2 * nb)); + } + + for (k = 0; k < MaxItersKMeans; ++k) + { + // Reset stats. + for (n = 0; n < nb; ++n) + { + accum[n] = 0; + distAccum[n] = 0; + } + + // Assign nearest center for each 'a' + n = 0; // track the nearest center for current 'a' + int a; + for (a = minA; a <= maxA; ++a) + { + if (alphas[a] != 0) + { + while (n + 1 < nb && Math.Abs(a - centers[n + 1]) < Math.Abs(a - centers[n])) + { + n++; + } + + map[a] = n; + + // Accumulate contribution into best centroid. + distAccum[n] += a * alphas[a]; + accum[n] += alphas[a]; + } + } + + // All point are classified. Move the centroids to the center of their respective cloud. + int displaced = 0; + weightedAverage = 0; + int totalWeight = 0; + for (n = 0; n < nb; ++n) + { + if (accum[n] != 0) + { + int newCenter = (distAccum[n] + (accum[n] >> 1)) / accum[n]; // >> 1 is bit-hack for / 2 + displaced += Math.Abs(centers[n] - newCenter); + centers[n] = newCenter; + weightedAverage += newCenter * accum[n]; + totalWeight += accum[n]; + } + } + + weightedAverage = (weightedAverage + (totalWeight >> 1)) / totalWeight; // >> 1 is bit-hack for / 2 + if (displaced < 5) + { + break; // no need to keep on looping... + } + } + + // Map each original value to the closest centroid + for (n = 0; n < this.Mbw * this.Mbh; ++n) + { + Vp8MacroBlockInfo mb = this.MbInfo[n]; + int alpha = mb.Alpha; + mb.Segment = map[alpha]; + mb.Alpha = centers[map[alpha]]; + } + + // TODO: add possibility for SmoothSegmentMap + this.SetSegmentAlphas(centers, weightedAverage); + } + + private void SetSegmentAlphas(ReadOnlySpan centers, int mid) + { + int nb = this.SegmentHeader.NumSegments; + Vp8SegmentInfo[] dqm = this.SegmentInfos; + int min = centers[0], max = centers[0]; + int n; + + if (nb > 1) + { + for (n = 0; n < nb; ++n) + { + if (min > centers[n]) + { + min = centers[n]; + } + + if (max < centers[n]) + { + max = centers[n]; + } + } + } + + if (max == min) + { + max = min + 1; + } + + for (n = 0; n < nb; ++n) + { + int alpha = 255 * (centers[n] - mid) / (max - min); + int beta = 255 * (centers[n] - min) / (max - min); + dqm[n].Alpha = Numerics.Clamp(alpha, -127, 127); + dqm[n].Beta = Numerics.Clamp(beta, 0, 255); + } + } + + private void SetSegmentParams(float quality) + { + int nb = this.SegmentHeader.NumSegments; + Vp8SegmentInfo[] dqm = this.SegmentInfos; + double amp = WebpConstants.SnsToDq * this.spatialNoiseShaping / 100.0d / 128.0d; + double cBase = QualityToCompression(quality / 100.0d); + for (int i = 0; i < nb; i++) + { + // We modulate the base coefficient to accommodate for the quantization + // susceptibility and allow denser segments to be quantized more. + double expn = 1.0d - (amp * dqm[i].Alpha); + double c = Math.Pow(cBase, expn); + int q = (int)(127.0d * (1.0d - c)); + dqm[i].Quant = Numerics.Clamp(q, 0, 127); + } + + // Purely indicative in the bitstream (except for the 1-segment case). + this.BaseQuant = dqm[0].Quant; + + // uvAlpha is normally spread around ~60. The useful range is + // typically ~30 (quite bad) to ~100 (ok to decimate UV more). + // We map it to the safe maximal range of MAX/MIN_DQ_UV for dq_uv. + this.DqUvAc = (this.uvAlpha - WebpConstants.QuantEncMidAlpha) * (WebpConstants.QuantEncMaxDqUv - WebpConstants.QuantEncMinDqUv) / (WebpConstants.QuantEncMaxAlpha - WebpConstants.QuantEncMinAlpha); + + // We rescale by the user-defined strength of adaptation. + this.DqUvAc = this.DqUvAc * this.spatialNoiseShaping / 100; + + // and make it safe. + this.DqUvAc = Numerics.Clamp(this.DqUvAc, WebpConstants.QuantEncMinDqUv, WebpConstants.QuantEncMaxDqUv); + + // We also boost the dc-uv-quant a little, based on sns-strength, since + // U/V channels are quite more reactive to high quants (flat DC-blocks tend to appear, and are unpleasant). + this.DqUvDc = -4 * this.spatialNoiseShaping / 100; + this.DqUvDc = Numerics.Clamp(this.DqUvDc, -15, 15); // 4bit-signed max allowed. + + this.DqY1Dc = 0; + this.DqY2Dc = 0; + this.DqY2Ac = 0; + + // Initialize segments' filtering. + this.SetupFilterStrength(); + + this.SetupMatrices(dqm); + } + + private void SetupFilterStrength() + { + const int filterSharpness = 0; // TODO: filterSharpness is hardcoded + const int filterType = 1; // TODO: filterType is hardcoded + + // level0 is in [0..500]. Using '-f 50' as filter_strength is mid-filtering. + int level0 = 5 * this.filterStrength; + for (int i = 0; i < WebpConstants.NumMbSegments; i++) + { + Vp8SegmentInfo m = this.SegmentInfos[i]; + + // We focus on the quantization of AC coeffs. + int qstep = WebpLookupTables.AcTable[Numerics.Clamp(m.Quant, 0, 127)] >> 2; + int baseStrength = FilterStrengthFromDelta(this.FilterHeader.Sharpness, qstep); + + // Segments with lower complexity ('beta') will be less filtered. + int f = baseStrength * level0 / (256 + m.Beta); + if (f < WebpConstants.FilterStrengthCutoff) + { + m.FStrength = 0; + } + else if (f > 63) + { + m.FStrength = 63; + } + else + { + m.FStrength = f; + } + } + + // We record the initial strength (mainly for the case of 1-segment only). + this.FilterHeader.FilterLevel = this.SegmentInfos[0].FStrength; + this.FilterHeader.Simple = filterType == 0; + this.FilterHeader.Sharpness = filterSharpness; + } + + private void SetSegmentProbas() + { + Span p = stackalloc int[NumMbSegments]; + int n; + + for (n = 0; n < this.Mbw * this.Mbh; ++n) + { + Vp8MacroBlockInfo mb = this.MbInfo[n]; + ++p[mb.Segment]; + } + + if (this.SegmentHeader.NumSegments > 1) + { + byte[] probas = this.Proba.Segments; + probas[0] = (byte)GetProba(p[0] + p[1], p[2] + p[3]); + probas[1] = (byte)GetProba(p[0], p[1]); + probas[2] = (byte)GetProba(p[2], p[3]); + + this.SegmentHeader.UpdateMap = probas[0] != 255 || probas[1] != 255 || probas[2] != 255; + if (!this.SegmentHeader.UpdateMap) + { + this.ResetSegments(); + } + + this.SegmentHeader.Size = + (p[0] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(0, probas[1]))) + + (p[1] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(1, probas[1]))) + + (p[2] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(0, probas[2]))) + + (p[3] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(1, probas[2]))); + } + else + { + this.SegmentHeader.UpdateMap = false; + this.SegmentHeader.Size = 0; + } + } + + private void ResetSegments() + { + int n; + for (n = 0; n < this.Mbw * this.Mbh; ++n) + { + this.MbInfo[n].Segment = 0; + } + } + + private void ResetStats() + { + Vp8EncProba proba = this.Proba; + proba.CalculateLevelCosts(); + proba.NbSkip = 0; + } + + private unsafe void SetupMatrices(Vp8SegmentInfo[] dqm) + { + int tlambdaScale = this.method >= WebpEncodingMethod.Default ? this.spatialNoiseShaping : 0; + for (int i = 0; i < dqm.Length; i++) + { + Vp8SegmentInfo m = dqm[i]; + int q = m.Quant; + + m.Y1.Q[0] = WebpLookupTables.DcTable[Numerics.Clamp(q + this.DqY1Dc, 0, 127)]; + m.Y1.Q[1] = WebpLookupTables.AcTable[Numerics.Clamp(q, 0, 127)]; + + m.Y2.Q[0] = (ushort)(WebpLookupTables.DcTable[Numerics.Clamp(q + this.DqY2Dc, 0, 127)] * 2); + m.Y2.Q[1] = WebpLookupTables.AcTable2[Numerics.Clamp(q + this.DqY2Ac, 0, 127)]; + + m.Uv.Q[0] = WebpLookupTables.DcTable[Numerics.Clamp(q + this.DqUvDc, 0, 117)]; + m.Uv.Q[1] = WebpLookupTables.AcTable[Numerics.Clamp(q + this.DqUvAc, 0, 127)]; + + int qi4 = m.Y1.Expand(0); + int qi16 = m.Y2.Expand(1); + int quv = m.Uv.Expand(2); + + m.LambdaI16 = 3 * qi16 * qi16; + m.LambdaI4 = (3 * qi4 * qi4) >> 7; + m.LambdaUv = (3 * quv * quv) >> 6; + m.LambdaMode = (1 * qi4 * qi4) >> 7; + m.TLambda = (tlambdaScale * qi4) >> 5; + + // none of these constants should be < 1. + m.LambdaI16 = m.LambdaI16 < 1 ? 1 : m.LambdaI16; + m.LambdaI4 = m.LambdaI4 < 1 ? 1 : m.LambdaI4; + m.LambdaUv = m.LambdaUv < 1 ? 1 : m.LambdaUv; + m.LambdaMode = m.LambdaMode < 1 ? 1 : m.LambdaMode; + m.TLambda = m.TLambda < 1 ? 1 : m.TLambda; + + m.MinDisto = 20 * m.Y1.Q[0]; + m.MaxEdge = 0; + + m.I4Penalty = 1000 * qi4 * qi4; + } + } + + private int MacroBlockAnalysis(int width, int height, Vp8EncIterator it, Span y, Span u, Span v, int yStride, int uvStride, Span alphas, out int uvAlpha) + { + int alpha = 0; + uvAlpha = 0; + if (!it.IsDone()) + { + do + { + it.Import(y, u, v, yStride, uvStride, width, height, true); + int bestAlpha = this.MbAnalyze(it, alphas, out int bestUvAlpha); + + // Accumulate for later complexity analysis. + alpha += bestAlpha; + uvAlpha += bestUvAlpha; + } + while (it.Next()); + } + + return alpha; + } + + private int MbAnalyze(Vp8EncIterator it, Span alphas, out int bestUvAlpha) + { + it.SetIntra16Mode(0); // default: Intra16, DC_PRED + it.SetSkip(false); // not skipped. + it.SetSegment(0); // default segment, spec-wise. + + int bestAlpha; + if (this.method <= WebpEncodingMethod.Level1) + { + bestAlpha = it.FastMbAnalyze(this.quality); + } + else + { + bestAlpha = it.MbAnalyzeBestIntra16Mode(); + if (this.method >= WebpEncodingMethod.Level5) + { + // We go and make a fast decision for intra4/intra16. + // It's usually not a good and definitive pick, but helps seeding the stats about level bit-cost. + bestAlpha = it.MbAnalyzeBestIntra4Mode(bestAlpha); + } + } + + bestUvAlpha = it.MbAnalyzeBestUvMode(); + + // Final susceptibility mix. + bestAlpha = ((3 * bestAlpha) + bestUvAlpha + 2) >> 2; + bestAlpha = FinalAlphaValue(bestAlpha); + alphas[bestAlpha]++; + it.CurrentMacroBlockInfo.Alpha = bestAlpha; // For later remapping. + + return bestAlpha; // Mixed susceptibility (not just luma). + } + + private bool Decimate(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8RdLevel rdOpt) + { + rd.InitScore(); + + // We can perform predictions for Luma16x16 and Chroma8x8 already. + // Luma4x4 predictions needs to be done as-we-go. + it.MakeLuma16Preds(); + it.MakeChroma8Preds(); + + if (rdOpt > Vp8RdLevel.RdOptNone) + { + QuantEnc.PickBestIntra16(it, ref rd, this.SegmentInfos, this.Proba); + if (this.method >= WebpEncodingMethod.Level2) + { + QuantEnc.PickBestIntra4(it, ref rd, this.SegmentInfos, this.Proba, this.maxI4HeaderBits); + } + + QuantEnc.PickBestUv(it, ref rd, this.SegmentInfos, this.Proba); + } + else + { + // At this point we have heuristically decided intra16 / intra4. + // For method >= 2, pick the best intra4/intra16 based on SSE (~tad slower). + // For method <= 1, we don't re-examine the decision but just go ahead with + // quantization/reconstruction. + QuantEnc.RefineUsingDistortion(it, this.SegmentInfos, rd, this.method >= WebpEncodingMethod.Level2, this.method >= WebpEncodingMethod.Level1, this.MbHeaderLimit); + } + + bool isSkipped = rd.Nz == 0; + it.SetSkip(isSkipped); + + return isSkipped; + } + + private void CodeResiduals(Vp8EncIterator it, Vp8ModeScore rd, Vp8Residual residual) + { + int x, y, ch; + bool i16 = it.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16; + int segment = it.CurrentMacroBlockInfo.Segment; + + it.NzToBytes(); + + int pos1 = this.bitWriter.NumBytes; + if (i16) + { + residual.Init(0, 1, this.Proba); + residual.SetCoeffs(rd.YDcLevels); + int res = this.bitWriter.PutCoeffs(it.TopNz[8] + it.LeftNz[8], residual); + it.TopNz[8] = it.LeftNz[8] = res; + residual.Init(1, 0, this.Proba); + } + else + { + residual.Init(0, 3, this.Proba); + } + + // luma-AC + for (y = 0; y < 4; y++) + { + for (x = 0; x < 4; x++) + { + int ctx = it.TopNz[x] + it.LeftNz[y]; + Span coeffs = rd.YAcLevels.AsSpan(16 * (x + (y * 4)), 16); + residual.SetCoeffs(coeffs); + int res = this.bitWriter.PutCoeffs(ctx, residual); + it.TopNz[x] = it.LeftNz[y] = res; + } + } + + int pos2 = this.bitWriter.NumBytes; + + // U/V + residual.Init(0, 2, this.Proba); + for (ch = 0; ch <= 2; ch += 2) + { + for (y = 0; y < 2; y++) + { + for (x = 0; x < 2; x++) + { + int ctx = it.TopNz[4 + ch + x] + it.LeftNz[4 + ch + y]; + residual.SetCoeffs(rd.UvLevels.AsSpan(16 * ((ch * 2) + x + (y * 2)), 16)); + int res = this.bitWriter.PutCoeffs(ctx, residual); + it.TopNz[4 + ch + x] = it.LeftNz[4 + ch + y] = res; + } + } + } + + int pos3 = this.bitWriter.NumBytes; + it.LumaBits = pos2 - pos1; + it.UvBits = pos3 - pos2; + it.BitCount[segment, i16 ? 1 : 0] += it.LumaBits; + it.BitCount[segment, 2] += it.UvBits; + it.BytesToNz(); + } + + /// + /// Same as CodeResiduals, but doesn't actually write anything. + /// Instead, it just records the event distribution. + /// + /// The iterator. + /// The score accumulator. + private void RecordResiduals(Vp8EncIterator it, Vp8ModeScore rd) + { + int x, y, ch; + Vp8Residual residual = new(); + bool i16 = it.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16; + + it.NzToBytes(); + + if (i16) + { + // i16x16 + residual.Init(0, 1, this.Proba); + residual.SetCoeffs(rd.YDcLevels); + int res = residual.RecordCoeffs(it.TopNz[8] + it.LeftNz[8]); + it.TopNz[8] = res; + it.LeftNz[8] = res; + residual.Init(1, 0, this.Proba); + } + else + { + residual.Init(0, 3, this.Proba); + } + + // luma-AC + for (y = 0; y < 4; y++) + { + for (x = 0; x < 4; x++) + { + int ctx = it.TopNz[x] + it.LeftNz[y]; + Span coeffs = rd.YAcLevels.AsSpan(16 * (x + (y * 4)), 16); + residual.SetCoeffs(coeffs); + int res = residual.RecordCoeffs(ctx); + it.TopNz[x] = res; + it.LeftNz[y] = res; + } + } + + // U/V + residual.Init(0, 2, this.Proba); + for (ch = 0; ch <= 2; ch += 2) + { + for (y = 0; y < 2; y++) + { + for (x = 0; x < 2; x++) + { + int ctx = it.TopNz[4 + ch + x] + it.LeftNz[4 + ch + y]; + residual.SetCoeffs(rd.UvLevels.AsSpan(16 * ((ch * 2) + x + (y * 2)), 16)); + int res = residual.RecordCoeffs(ctx); + it.TopNz[4 + ch + x] = res; + it.LeftNz[4 + ch + y] = res; + } + } + } + + it.BytesToNz(); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int FinalAlphaValue(int alpha) + { + alpha = WebpConstants.MaxAlpha - alpha; + return Numerics.Clamp(alpha, 0, WebpConstants.MaxAlpha); + } + + /// + /// We want to emulate jpeg-like behaviour where the expected "good" quality + /// is around q=75. Internally, our "good" middle is around c=50. So we + /// map accordingly using linear piece-wise function + /// + /// The compression level. + [MethodImpl(InliningOptions.ShortMethod)] + private static double QualityToCompression(double c) + { + double linearC = c < 0.75 ? c * (2.0d / 3.0d) : (2.0d * c) - 1.0d; + + // The file size roughly scales as pow(quantizer, 3.). Actually, the + // exponent is somewhere between 2.8 and 3.2, but we're mostly interested + // in the mid-quant range. So we scale the compressibility inversely to + // this power-law: quant ~= compression ^ 1/3. This law holds well for + // low quant. Finer modeling for high-quant would make use of AcTable[] + // more explicitly. + return (double)Math.Pow(linearC, 1 / 3.0d); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int FilterStrengthFromDelta(int sharpness, int delta) + { + int pos = delta < WebpConstants.MaxDelzaSize ? delta : WebpConstants.MaxDelzaSize - 1; + return WebpLookupTables.LevelsFromDelta[sharpness, pos]; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static double GetPsnr(long mse, long size) => mse > 0 && size > 0 ? 10.0f * Math.Log10(255.0f * 255.0f * size / mse) : 99; + + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetProba(int a, int b) + { + int total = a + b; + return total == 0 ? 255 // that's the default probability. + : ((255 * a) + (total >> 1)) / total; // rounded proba + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs b/ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs new file mode 100644 index 0000000..1e42e48 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs @@ -0,0 +1,1096 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// Methods for encoding a VP8 frame. + /// + internal static unsafe class Vp8Encoding + { + private const int KC1 = 20091 + (1 << 16); + + private const int KC2 = 35468; + + private static readonly byte[] Clip1 = GetClip1(); // clips [-255,510] to [0,255] + + private const int I16DC16 = 0 * 16 * WebpConstants.Bps; + + private const int I16TM16 = I16DC16 + 16; + + private const int I16VE16 = 1 * 16 * WebpConstants.Bps; + + private const int I16HE16 = I16VE16 + 16; + + private const int C8DC8 = 2 * 16 * WebpConstants.Bps; + + private const int C8TM8 = C8DC8 + (1 * 16); + + private const int C8VE8 = (2 * 16 * WebpConstants.Bps) + (8 * WebpConstants.Bps); + + private const int C8HE8 = C8VE8 + (1 * 16); + + public static readonly int[] Vp8I16ModeOffsets = [I16DC16, I16TM16, I16VE16, I16HE16]; + + public static readonly int[] Vp8UvModeOffsets = [C8DC8, C8TM8, C8VE8, C8HE8]; + + private const int I4DC4 = (3 * 16 * WebpConstants.Bps) + 0; + + private const int I4TM4 = I4DC4 + 4; + + private const int I4VE4 = I4DC4 + 8; + + private const int I4HE4 = I4DC4 + 12; + + private const int I4RD4 = I4DC4 + 16; + + private const int I4VR4 = I4DC4 + 20; + + private const int I4LD4 = I4DC4 + 24; + + private const int I4VL4 = I4DC4 + 28; + + private const int I4HD4 = (3 * 16 * WebpConstants.Bps) + (4 * WebpConstants.Bps); + + private const int I4HU4 = I4HD4 + 4; + + public static readonly int[] Vp8I4ModeOffsets = [I4DC4, I4TM4, I4VE4, I4HE4, I4RD4, I4VR4, I4LD4, I4VL4, I4HD4, I4HU4 + ]; + + private static byte[] GetClip1() + { + byte[] clip1 = new byte[255 + 510 + 1]; + + for (int i = -255; i <= 255 + 255; i++) + { + clip1[255 + i] = Clip8b(i); + } + + return clip1; + } + + // Transforms (Paragraph 14.4) + // Does two inverse transforms. + public static void ITransformTwo(Span reference, Span input, Span dst, Span scratch) + { + if (Vector128.IsHardwareAccelerated) + { + // This implementation makes use of 16-bit fixed point versions of two + // multiply constants: + // K1 = sqrt(2) * cos (pi/8) ~= 85627 / 2^16 + // K2 = sqrt(2) * sin (pi/8) ~= 35468 / 2^16 + // + // To be able to use signed 16-bit integers, we use the following trick to + // have constants within range: + // - Associated constants are obtained by subtracting the 16-bit fixed point + // version of one: + // k = K - (1 << 16) => K = k + (1 << 16) + // K1 = 85267 => k1 = 20091 + // K2 = 35468 => k2 = -30068 + // - The multiplication of a variable by a constant become the sum of the + // variable and the multiplication of that variable by the associated + // constant: + // (x * K) >> 16 = (x * (k + (1 << 16))) >> 16 = ((x * k ) >> 16) + x + + // Load and concatenate the transform coefficients (we'll do two inverse + // transforms in parallel). In the case of only one inverse transform, the + // second half of the vectors will just contain random value we'll never + // use nor store. + ref short inputRef = ref MemoryMarshal.GetReference(input); + Vector128 in0 = Vector128.Create(Unsafe.As(ref inputRef), 0); + Vector128 in1 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref inputRef, 4)), 0); + Vector128 in2 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref inputRef, 8)), 0); + Vector128 in3 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref inputRef, 12)), 0); + + // a00 a10 a20 a30 x x x x + // a01 a11 a21 a31 x x x x + // a02 a12 a22 a32 x x x x + // a03 a13 a23 a33 x x x x + Vector128 inb0 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref inputRef, 16)), 0); + Vector128 inb1 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref inputRef, 20)), 0); + Vector128 inb2 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref inputRef, 24)), 0); + Vector128 inb3 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref inputRef, 28)), 0); + + in0 = Vector128_.UnpackLow(in0, inb0); + in1 = Vector128_.UnpackLow(in1, inb1); + in2 = Vector128_.UnpackLow(in2, inb2); + in3 = Vector128_.UnpackLow(in3, inb3); + + // a00 a10 a20 a30 b00 b10 b20 b30 + // a01 a11 a21 a31 b01 b11 b21 b31 + // a02 a12 a22 a32 b02 b12 b22 b32 + // a03 a13 a23 a33 b03 b13 b23 b33 + + // Vertical pass and subsequent transpose. + // First pass, c and d calculations are longer because of the "trick" multiplications. + InverseTransformVerticalPassVector128(in0, in2, in1, in3, out Vector128 tmp0, out Vector128 tmp1, out Vector128 tmp2, out Vector128 tmp3); + + // Transpose the two 4x4. + LossyUtils.Vp8Transpose_2_4x4_16bVector128(tmp0, tmp1, tmp2, tmp3, out Vector128 t0, out Vector128 t1, out Vector128 t2, out Vector128 t3); + + // Horizontal pass and subsequent transpose. + // First pass, c and d calculations are longer because of the "trick" multiplications. + InverseTransformHorizontalPassVector128(t0, t2, t1, t3, out Vector128 shifted0, out Vector128 shifted1, out Vector128 shifted2, out Vector128 shifted3); + + // Transpose the two 4x4. + LossyUtils.Vp8Transpose_2_4x4_16bVector128(shifted0, shifted1, shifted2, shifted3, out t0, out t1, out t2, out t3); + + // Add inverse transform to 'ref' and store. + // Load the reference(s). + ref byte referenceRef = ref MemoryMarshal.GetReference(reference); + + // Load eight bytes/pixels per line. + Vector128 ref0 = Vector128.Create(Unsafe.As(ref referenceRef), 0).AsByte(); + Vector128 ref1 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref referenceRef, WebpConstants.Bps)), 0).AsByte(); + Vector128 ref2 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref referenceRef, WebpConstants.Bps * 2)), 0).AsByte(); + Vector128 ref3 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref referenceRef, WebpConstants.Bps * 3)), 0).AsByte(); + + // Convert to 16b. + ref0 = Vector128_.UnpackLow(ref0, Vector128.Zero); + ref1 = Vector128_.UnpackLow(ref1, Vector128.Zero); + ref2 = Vector128_.UnpackLow(ref2, Vector128.Zero); + ref3 = Vector128_.UnpackLow(ref3, Vector128.Zero); + + // Add the inverse transform(s). + Vector128 ref0InvAdded = ref0.AsInt16() + t0.AsInt16(); + Vector128 ref1InvAdded = ref1.AsInt16() + t1.AsInt16(); + Vector128 ref2InvAdded = ref2.AsInt16() + t2.AsInt16(); + Vector128 ref3InvAdded = ref3.AsInt16() + t3.AsInt16(); + + // Unsigned saturate to 8b. + ref0 = Vector128_.PackUnsignedSaturate(ref0InvAdded, ref0InvAdded); + ref1 = Vector128_.PackUnsignedSaturate(ref1InvAdded, ref1InvAdded); + ref2 = Vector128_.PackUnsignedSaturate(ref2InvAdded, ref2InvAdded); + ref3 = Vector128_.PackUnsignedSaturate(ref3InvAdded, ref3InvAdded); + + // Store eight bytes/pixels per line. + ref byte outputRef = ref MemoryMarshal.GetReference(dst); + Unsafe.As>(ref outputRef) = ref0.GetLower(); + Unsafe.As>(ref Unsafe.Add(ref outputRef, WebpConstants.Bps)) = ref1.GetLower(); + Unsafe.As>(ref Unsafe.Add(ref outputRef, WebpConstants.Bps * 2)) = ref2.GetLower(); + Unsafe.As>(ref Unsafe.Add(ref outputRef, WebpConstants.Bps * 3)) = ref3.GetLower(); + } + else + { + ITransformOne(reference, input, dst, scratch); + ITransformOne(reference[4..], input[16..], dst[4..], scratch); + } + } + + public static void ITransformOne(Span reference, Span input, Span dst, Span scratch) + { + if (Vector128.IsHardwareAccelerated) + { + // Load and concatenate the transform coefficients (we'll do two inverse + // transforms in parallel). In the case of only one inverse transform, the + // second half of the vectors will just contain random value we'll never + // use nor store. + ref short inputRef = ref MemoryMarshal.GetReference(input); + Vector128 in0 = Vector128.Create(Unsafe.As(ref inputRef), 0); + Vector128 in1 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref inputRef, 4)), 0); + Vector128 in2 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref inputRef, 8)), 0); + Vector128 in3 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref inputRef, 12)), 0); + + // a00 a10 a20 a30 x x x x + // a01 a11 a21 a31 x x x x + // a02 a12 a22 a32 x x x x + // a03 a13 a23 a33 x x x x + + // Vertical pass and subsequent transpose. + // First pass, c and d calculations are longer because of the "trick" multiplications. + InverseTransformVerticalPassVector128(in0, in2, in1, in3, out Vector128 tmp0, out Vector128 tmp1, out Vector128 tmp2, out Vector128 tmp3); + + // Transpose the two 4x4. + LossyUtils.Vp8Transpose_2_4x4_16bVector128(tmp0, tmp1, tmp2, tmp3, out Vector128 t0, out Vector128 t1, out Vector128 t2, out Vector128 t3); + + // Horizontal pass and subsequent transpose. + // First pass, c and d calculations are longer because of the "trick" multiplications. + InverseTransformHorizontalPassVector128(t0, t2, t1, t3, out Vector128 shifted0, out Vector128 shifted1, out Vector128 shifted2, out Vector128 shifted3); + + // Transpose the two 4x4. + LossyUtils.Vp8Transpose_2_4x4_16bVector128(shifted0, shifted1, shifted2, shifted3, out t0, out t1, out t2, out t3); + + // Add inverse transform to 'ref' and store. + // Load the reference(s). + ref byte referenceRef = ref MemoryMarshal.GetReference(reference); + + // Load four bytes/pixels per line. + Vector128 ref0 = Vector128.CreateScalar(Unsafe.ReadUnaligned(ref referenceRef)).AsByte(); + Vector128 ref1 = Vector128.CreateScalar(Unsafe.ReadUnaligned(ref Unsafe.Add(ref referenceRef, WebpConstants.Bps))).AsByte(); + Vector128 ref2 = Vector128.CreateScalar(Unsafe.ReadUnaligned(ref Unsafe.Add(ref referenceRef, WebpConstants.Bps * 2))).AsByte(); + Vector128 ref3 = Vector128.CreateScalar(Unsafe.ReadUnaligned(ref Unsafe.Add(ref referenceRef, WebpConstants.Bps * 3))).AsByte(); + + // Convert to 16b. + ref0 = Vector128_.UnpackLow(ref0, Vector128.Zero); + ref1 = Vector128_.UnpackLow(ref1, Vector128.Zero); + ref2 = Vector128_.UnpackLow(ref2, Vector128.Zero); + ref3 = Vector128_.UnpackLow(ref3, Vector128.Zero); + + // Add the inverse transform(s). + Vector128 ref0InvAdded = ref0.AsInt16() + t0.AsInt16(); + Vector128 ref1InvAdded = ref1.AsInt16() + t1.AsInt16(); + Vector128 ref2InvAdded = ref2.AsInt16() + t2.AsInt16(); + Vector128 ref3InvAdded = ref3.AsInt16() + t3.AsInt16(); + + // Unsigned saturate to 8b. + ref0 = Vector128_.PackUnsignedSaturate(ref0InvAdded, ref0InvAdded); + ref1 = Vector128_.PackUnsignedSaturate(ref1InvAdded, ref1InvAdded); + ref2 = Vector128_.PackUnsignedSaturate(ref2InvAdded, ref2InvAdded); + ref3 = Vector128_.PackUnsignedSaturate(ref3InvAdded, ref3InvAdded); + + // Unsigned saturate to 8b. + ref byte outputRef = ref MemoryMarshal.GetReference(dst); + + // Store four bytes/pixels per line. + int output0 = ref0.AsInt32().ToScalar(); + int output1 = ref1.AsInt32().ToScalar(); + int output2 = ref2.AsInt32().ToScalar(); + int output3 = ref3.AsInt32().ToScalar(); + + Unsafe.WriteUnaligned(ref outputRef, output0); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref outputRef, WebpConstants.Bps), output1); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref outputRef, WebpConstants.Bps * 2), output2); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref outputRef, WebpConstants.Bps * 3), output3); + } + else + { + int i; + Span tmp = scratch[..16]; + for (i = 0; i < 4; i++) + { + // vertical pass. + int a = input[0] + input[8]; + int b = input[0] - input[8]; + int c = Mul(input[4], KC2) - Mul(input[12], KC1); + int d = Mul(input[4], KC1) + Mul(input[12], KC2); + tmp[0] = a + d; + tmp[1] = b + c; + tmp[2] = b - c; + tmp[3] = a - d; + tmp = tmp[4..]; + input = input[1..]; + } + + tmp = scratch; + for (i = 0; i < 4; i++) + { + // horizontal pass. + int dc = tmp[0] + 4; + int a = dc + tmp[8]; + int b = dc - tmp[8]; + int c = Mul(tmp[4], KC2) - Mul(tmp[12], KC1); + int d = Mul(tmp[4], KC1) + Mul(tmp[12], KC2); + Store(dst, reference, 0, i, a + d); + Store(dst, reference, 1, i, b + c); + Store(dst, reference, 2, i, b - c); + Store(dst, reference, 3, i, a - d); + tmp = tmp[1..]; + } + } + } + + private static void InverseTransformVerticalPassVector128(Vector128 in0, Vector128 in2, Vector128 in1, Vector128 in3, out Vector128 tmp0, out Vector128 tmp1, out Vector128 tmp2, out Vector128 tmp3) + { + Vector128 a = in0.AsInt16() + in2.AsInt16(); + Vector128 b = in0.AsInt16() - in2.AsInt16(); + + Vector128 k1 = Vector128.Create((short)20091).AsInt16(); + Vector128 k2 = Vector128.Create((short)-30068).AsInt16(); + + // c = MUL(in1, K2) - MUL(in3, K1) = MUL(in1, k2) - MUL(in3, k1) + in1 - in3 + Vector128 c1 = Vector128_.MultiplyHigh(in1.AsInt16(), k2); + Vector128 c2 = Vector128_.MultiplyHigh(in3.AsInt16(), k1); + Vector128 c3 = in1.AsInt16() - in3.AsInt16(); + Vector128 c4 = c1 - c2; + Vector128 c = c3 + c4; + + // d = MUL(in1, K1) + MUL(in3, K2) = MUL(in1, k1) + MUL(in3, k2) + in1 + in3 + Vector128 d1 = Vector128_.MultiplyHigh(in1.AsInt16(), k1); + Vector128 d2 = Vector128_.MultiplyHigh(in3.AsInt16(), k2); + Vector128 d3 = in1.AsInt16() + in3.AsInt16(); + Vector128 d4 = d1 + d2; + Vector128 d = d3 + d4; + + // Second pass. + tmp0 = a + d; + tmp1 = b + c; + tmp2 = b - c; + tmp3 = a - d; + } + + private static void InverseTransformHorizontalPassVector128(Vector128 t0, Vector128 t2, Vector128 t1, Vector128 t3, out Vector128 shifted0, out Vector128 shifted1, out Vector128 shifted2, out Vector128 shifted3) + { + Vector128 dc = t0.AsInt16() + Vector128.Create((short)4); + Vector128 a = dc + t2.AsInt16(); + Vector128 b = dc - t2.AsInt16(); + + Vector128 k1 = Vector128.Create((short)20091).AsInt16(); + Vector128 k2 = Vector128.Create((short)-30068).AsInt16(); + + // c = MUL(T1, K2) - MUL(T3, K1) = MUL(T1, k2) - MUL(T3, k1) + T1 - T3 + Vector128 c1 = Vector128_.MultiplyHigh(t1.AsInt16(), k2); + Vector128 c2 = Vector128_.MultiplyHigh(t3.AsInt16(), k1); + Vector128 c3 = t1.AsInt16() - t3.AsInt16(); + Vector128 c4 = c1 - c2; + Vector128 c = c3 + c4; + + // d = MUL(T1, K1) + MUL(T3, K2) = MUL(T1, k1) + MUL(T3, k2) + T1 + T3 + Vector128 d1 = Vector128_.MultiplyHigh(t1.AsInt16(), k1); + Vector128 d2 = Vector128_.MultiplyHigh(t3.AsInt16(), k2); + Vector128 d3 = t1.AsInt16() + t3.AsInt16(); + Vector128 d4 = d1 + d2; + Vector128 d = d3 + d4; + + // Second pass. + Vector128 tmp0 = a + d; + Vector128 tmp1 = b + c; + Vector128 tmp2 = b - c; + Vector128 tmp3 = a - d; + shifted0 = Vector128.ShiftRightArithmetic(tmp0, 3); + shifted1 = Vector128.ShiftRightArithmetic(tmp1, 3); + shifted2 = Vector128.ShiftRightArithmetic(tmp2, 3); + shifted3 = Vector128.ShiftRightArithmetic(tmp3, 3); + } + + public static void FTransform2(Span src, Span reference, Span output, Span output2, Span scratch) + { + if (Vector128.IsHardwareAccelerated) + { + ref byte srcRef = ref MemoryMarshal.GetReference(src); + ref byte referenceRef = ref MemoryMarshal.GetReference(reference); + + // Load src. + Vector128 src0 = Vector128.Create(Unsafe.As(ref srcRef), 0); + Vector128 src1 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, WebpConstants.Bps)), 0); + Vector128 src2 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, WebpConstants.Bps * 2)), 0); + Vector128 src3 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, WebpConstants.Bps * 3)), 0); + + // Load ref. + Vector128 ref0 = Vector128.Create(Unsafe.As(ref referenceRef), 0); + Vector128 ref1 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref referenceRef, WebpConstants.Bps)), 0); + Vector128 ref2 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref referenceRef, WebpConstants.Bps * 2)), 0); + Vector128 ref3 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref referenceRef, WebpConstants.Bps * 3)), 0); + + // Convert both to 16 bit. + Vector128 srcLow0 = Vector128_.UnpackLow(src0.AsByte(), Vector128.Zero); + Vector128 srcLow1 = Vector128_.UnpackLow(src1.AsByte(), Vector128.Zero); + Vector128 srcLow2 = Vector128_.UnpackLow(src2.AsByte(), Vector128.Zero); + Vector128 srcLow3 = Vector128_.UnpackLow(src3.AsByte(), Vector128.Zero); + Vector128 refLow0 = Vector128_.UnpackLow(ref0.AsByte(), Vector128.Zero); + Vector128 refLow1 = Vector128_.UnpackLow(ref1.AsByte(), Vector128.Zero); + Vector128 refLow2 = Vector128_.UnpackLow(ref2.AsByte(), Vector128.Zero); + Vector128 refLow3 = Vector128_.UnpackLow(ref3.AsByte(), Vector128.Zero); + + // Compute difference. -> 00 01 02 03 00' 01' 02' 03' + Vector128 diff0 = srcLow0.AsInt16() - refLow0.AsInt16(); + Vector128 diff1 = srcLow1.AsInt16() - refLow1.AsInt16(); + Vector128 diff2 = srcLow2.AsInt16() - refLow2.AsInt16(); + Vector128 diff3 = srcLow3.AsInt16() - refLow3.AsInt16(); + + // Unpack and shuffle. + // 00 01 02 03 0 0 0 0 + // 10 11 12 13 0 0 0 0 + // 20 21 22 23 0 0 0 0 + // 30 31 32 33 0 0 0 0 + Vector128 shuf01l = Vector128_.UnpackLow(diff0.AsInt32(), diff1.AsInt32()); + Vector128 shuf23l = Vector128_.UnpackLow(diff2.AsInt32(), diff3.AsInt32()); + Vector128 shuf01h = Vector128_.UnpackHigh(diff0.AsInt32(), diff1.AsInt32()); + Vector128 shuf23h = Vector128_.UnpackHigh(diff2.AsInt32(), diff3.AsInt32()); + + // First pass. + FTransformPass1Vector128(shuf01l.AsInt16(), shuf23l.AsInt16(), out Vector128 v01l, out Vector128 v32l); + FTransformPass1Vector128(shuf01h.AsInt16(), shuf23h.AsInt16(), out Vector128 v01h, out Vector128 v32h); + + // Second pass. + FTransformPass2Vector128(v01l, v32l, output); + FTransformPass2Vector128(v01h, v32h, output2); + } + else + { + FTransform(src, reference, output, scratch); + FTransform(src[4..], reference[4..], output2, scratch); + } + } + + public static void FTransform(Span src, Span reference, Span output, Span scratch) + { + if (Vector128.IsHardwareAccelerated) + { + ref byte srcRef = ref MemoryMarshal.GetReference(src); + ref byte referenceRef = ref MemoryMarshal.GetReference(reference); + + // Load src. + Vector128 src0 = Vector128.Create(Unsafe.As(ref srcRef), 0); + Vector128 src1 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, WebpConstants.Bps)), 0); + Vector128 src2 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, WebpConstants.Bps * 2)), 0); + Vector128 src3 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, WebpConstants.Bps * 3)), 0); + + // Load ref. + Vector128 ref0 = Vector128.Create(Unsafe.As(ref referenceRef), 0); + Vector128 ref1 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref referenceRef, WebpConstants.Bps)), 0); + Vector128 ref2 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref referenceRef, WebpConstants.Bps * 2)), 0); + Vector128 ref3 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref referenceRef, WebpConstants.Bps * 3)), 0); + + // 00 01 02 03 * + // 10 11 12 13 * + // 20 21 22 23 * + // 30 31 32 33 * + // Shuffle. + Vector128 srcLow0 = Vector128_.UnpackLow(src0.AsInt16(), src1.AsInt16()); + Vector128 srcLow1 = Vector128_.UnpackLow(src2.AsInt16(), src3.AsInt16()); + Vector128 refLow0 = Vector128_.UnpackLow(ref0.AsInt16(), ref1.AsInt16()); + Vector128 refLow1 = Vector128_.UnpackLow(ref2.AsInt16(), ref3.AsInt16()); + + // 00 01 10 11 02 03 12 13 * * ... + // 20 21 30 31 22 22 32 33 * * ... + + // Convert both to 16 bit. + Vector128 src0_16b = Vector128_.UnpackLow(srcLow0.AsByte(), Vector128.Zero); + Vector128 src1_16b = Vector128_.UnpackLow(srcLow1.AsByte(), Vector128.Zero); + Vector128 ref0_16b = Vector128_.UnpackLow(refLow0.AsByte(), Vector128.Zero); + Vector128 ref1_16b = Vector128_.UnpackLow(refLow1.AsByte(), Vector128.Zero); + + // Compute the difference. + Vector128 row01 = src0_16b.AsInt16() - ref0_16b.AsInt16(); + Vector128 row23 = src1_16b.AsInt16() - ref1_16b.AsInt16(); + + // First pass. + FTransformPass1Vector128(row01, row23, out Vector128 v01, out Vector128 v32); + + // Second pass. + FTransformPass2Vector128(v01, v32, output); + } + else + { + int i; + Span tmp = scratch[..16]; + + int srcIdx = 0; + int refIdx = 0; + for (i = 0; i < 4; i++) + { + int d3 = src[srcIdx + 3] - reference[refIdx + 3]; + int d2 = src[srcIdx + 2] - reference[refIdx + 2]; + int d1 = src[srcIdx + 1] - reference[refIdx + 1]; + int d0 = src[srcIdx] - reference[refIdx]; // 9bit dynamic range ([-255,255]) + int a0 = d0 + d3; // 10b [-510,510] + int a1 = d1 + d2; + int a2 = d1 - d2; + int a3 = d0 - d3; + tmp[3 + (i * 4)] = ((a3 * 2217) - (a2 * 5352) + 937) >> 9; + tmp[2 + (i * 4)] = (a0 - a1) * 8; + tmp[1 + (i * 4)] = ((a2 * 2217) + (a3 * 5352) + 1812) >> 9; // [-7536,7542] + tmp[0 + (i * 4)] = (a0 + a1) * 8; // 14b [-8160,8160] + + srcIdx += WebpConstants.Bps; + refIdx += WebpConstants.Bps; + } + + for (i = 0; i < 4; i++) + { + int t12 = tmp[12 + i]; // 15b + int t8 = tmp[8 + i]; + + int a1 = tmp[4 + i] + t8; + int a2 = tmp[4 + i] - t8; + int a0 = tmp[0 + i] + t12; // 15b + int a3 = tmp[0 + i] - t12; + + output[12 + i] = (short)(((a3 * 2217) - (a2 * 5352) + 51000) >> 16); + output[8 + i] = (short)((a0 - a1 + 7) >> 4); + output[4 + i] = (short)((((a2 * 2217) + (a3 * 5352) + 12000) >> 16) + (a3 != 0 ? 1 : 0)); + output[0 + i] = (short)((a0 + a1 + 7) >> 4); // 12b + } + } + } + + public static void FTransformPass1Vector128(Vector128 row01, Vector128 row23, out Vector128 out01, out Vector128 out32) + { + // *in01 = 00 01 10 11 02 03 12 13 + // *in23 = 20 21 30 31 22 23 32 33 + Vector128 shuf01_p = Vector128_.ShuffleHigh(row01, SimdUtils.Shuffle.MMShuffle2301); + Vector128 shuf32_p = Vector128_.ShuffleHigh(row23, SimdUtils.Shuffle.MMShuffle2301); + + // 00 01 10 11 03 02 13 12 + // 20 21 30 31 23 22 33 32 + Vector128 s01 = Vector128_.UnpackLow(shuf01_p.AsInt64(), shuf32_p.AsInt64()); + Vector128 s32 = Vector128_.UnpackHigh(shuf01_p.AsInt64(), shuf32_p.AsInt64()); + + // 00 01 10 11 20 21 30 31 + // 03 02 13 12 23 22 33 32 + Vector128 a01 = s01.AsInt16() + s32.AsInt16(); + Vector128 a32 = s01.AsInt16() - s32.AsInt16(); + + // [d0 + d3 | d1 + d2 | ...] = [a0 a1 | a0' a1' | ... ] + // [d0 - d3 | d1 - d2 | ...] = [a3 a2 | a3' a2' | ... ] + + // [ (a0 + a1) << 3, ... ] + Vector128 tmp0 = Vector128_.MultiplyAddAdjacent(a01, Vector128.Create(8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0).AsInt16()); // K88p + + // [ (a0 - a1) << 3, ... ] + Vector128 tmp2 = Vector128_.MultiplyAddAdjacent(a01, Vector128.Create(8, 0, 248, 255, 8, 0, 248, 255, 8, 0, 248, 255, 8, 0, 248, 255).AsInt16()); // K88m + Vector128 tmp11 = Vector128_.MultiplyAddAdjacent(a32, Vector128.Create(232, 20, 169, 8, 232, 20, 169, 8, 232, 20, 169, 8, 232, 20, 169, 8).AsInt16()); // K5352_2217p + Vector128 tmp31 = Vector128_.MultiplyAddAdjacent(a32, Vector128.Create(169, 8, 24, 235, 169, 8, 24, 235, 169, 8, 24, 235, 169, 8, 24, 235).AsInt16()); // K5352_2217m + Vector128 tmp12 = tmp11 + Vector128.Create(1812); + Vector128 tmp32 = tmp31 + Vector128.Create(937); + Vector128 tmp1 = Vector128.ShiftRightArithmetic(tmp12, 9); + Vector128 tmp3 = Vector128.ShiftRightArithmetic(tmp32, 9); + Vector128 s03 = Vector128_.PackSignedSaturate(tmp0, tmp2); + Vector128 s12 = Vector128_.PackSignedSaturate(tmp1, tmp3); + Vector128 slo = Vector128_.UnpackLow(s03, s12); // 0 1 0 1 0 1... + Vector128 shi = Vector128_.UnpackHigh(s03, s12); // 2 3 2 3 2 3 + Vector128 v23 = Vector128_.UnpackHigh(slo.AsInt32(), shi.AsInt32()); + out01 = Vector128_.UnpackLow(slo.AsInt32(), shi.AsInt32()); + out32 = Vector128_.ShuffleNative(v23, SimdUtils.Shuffle.MMShuffle1032); + } + + public static void FTransformPass2Vector128(Vector128 v01, Vector128 v32, Span output) + { + // Same operations are done on the (0,3) and (1,2) pairs. + // a3 = v0 - v3 + // a2 = v1 - v2 + Vector128 a32 = v01.AsInt16() - v32.AsInt16(); + Vector128 a22 = Vector128_.UnpackHigh(a32.AsInt64(), a32.AsInt64()); + + Vector128 b23 = Vector128_.UnpackLow(a22.AsInt16(), a32.AsInt16()); + Vector128 c1 = Vector128_.MultiplyAddAdjacent(b23, Vector128.Create(169, 8, 232, 20, 169, 8, 232, 20, 169, 8, 232, 20, 169, 8, 232, 20).AsInt16()); // K5352_2217 + Vector128 c3 = Vector128_.MultiplyAddAdjacent(b23, Vector128.Create(24, 235, 169, 8, 24, 235, 169, 8, 24, 235, 169, 8, 24, 235, 169, 8).AsInt16()); // K2217_5352 + Vector128 d1 = c1 + Vector128.Create(12000 + (1 << 16)); // K12000PlusOne + Vector128 d3 = c3 + Vector128.Create(51000); + Vector128 e1 = Vector128.ShiftRightArithmetic(d1, 16); + Vector128 e3 = Vector128.ShiftRightArithmetic(d3, 16); + + // f1 = ((b3 * 5352 + b2 * 2217 + 12000) >> 16) + // f3 = ((b3 * 2217 - b2 * 5352 + 51000) >> 16) + Vector128 f1 = Vector128_.PackSignedSaturate(e1, e1); + Vector128 f3 = Vector128_.PackSignedSaturate(e3, e3); + + // g1 = f1 + (a3 != 0); + // The compare will return (0xffff, 0) for (==0, !=0). To turn that into the + // desired (0, 1), we add one earlier through k12000_plus_one. + // -> g1 = f1 + 1 - (a3 == 0) + Vector128 g1 = f1 + Vector128.Equals(a32, Vector128.Zero); + + // a0 = v0 + v3 + // a1 = v1 + v2 + Vector128 a01 = v01.AsInt16() + v32.AsInt16(); + Vector128 a01Plus7 = a01.AsInt16() + Vector128.Create((short)7); + Vector128 a11 = Vector128_.UnpackHigh(a01.AsInt64(), a01.AsInt64()).AsInt16(); + Vector128 c0 = a01Plus7 + a11; + Vector128 c2 = a01Plus7 - a11; + + // d0 = (a0 + a1 + 7) >> 4; + // d2 = (a0 - a1 + 7) >> 4; + Vector128 d0 = Vector128.ShiftRightArithmetic(c0, 4); + Vector128 d2 = Vector128.ShiftRightArithmetic(c2, 4); + + Vector128 d0g1 = Vector128_.UnpackLow(d0.AsInt64(), g1.AsInt64()); + Vector128 d2f3 = Vector128_.UnpackLow(d2.AsInt64(), f3.AsInt64()); + + ref short outputRef = ref MemoryMarshal.GetReference(output); + Unsafe.As>(ref outputRef) = d0g1.AsInt16(); + Unsafe.As>(ref Unsafe.Add(ref outputRef, 8)) = d2f3.AsInt16(); + } + + public static void FTransformWht(Span input, Span output, Span scratch) + { + Span tmp = scratch[..16]; + + int i; + int inputIdx = 0; + for (i = 0; i < 4; i++) + { + int a1 = input[inputIdx + (1 * 16)] + input[inputIdx + (3 * 16)]; + int a2 = input[inputIdx + (1 * 16)] - input[inputIdx + (3 * 16)]; + int a0 = input[inputIdx + (0 * 16)] + input[inputIdx + (2 * 16)]; // 13b + int a3 = input[inputIdx + (0 * 16)] - input[inputIdx + (2 * 16)]; + tmp[3 + (i * 4)] = a0 - a1; + tmp[2 + (i * 4)] = a3 - a2; + tmp[1 + (i * 4)] = a3 + a2; + tmp[0 + (i * 4)] = a0 + a1; // 14b + + inputIdx += 64; + } + + for (i = 0; i < 4; i++) + { + int t12 = tmp[12 + i]; + int t8 = tmp[8 + i]; + + int a1 = tmp[4 + i] + t12; + int a2 = tmp[4 + i] - t12; + int a0 = tmp[0 + i] + t8; // 15b + int a3 = tmp[0 + i] - t8; + + int b0 = a0 + a1; // 16b + int b1 = a3 + a2; + int b2 = a3 - a2; + int b3 = a0 - a1; + + output[12 + i] = (short)(b3 >> 1); + output[8 + i] = (short)(b2 >> 1); + output[4 + i] = (short)(b1 >> 1); + output[0 + i] = (short)(b0 >> 1); // 15b + } + } + + // luma 16x16 prediction (paragraph 12.3). + public static void EncPredLuma16(Span dst, Span left, Span top) + { + DcMode(dst, left, top, 16, 16, 5); + VerticalPred(dst[I16VE16..], top, 16); + HorizontalPred(dst[I16HE16..], left, 16); + TrueMotion(dst[I16TM16..], left, top, 16); + } + + // Chroma 8x8 prediction (paragraph 12.2). + public static void EncPredChroma8(Span dst, Span left, Span top) + { + // U block. + DcMode(dst[C8DC8..], left, top, 8, 8, 4); + VerticalPred(dst[C8VE8..], top, 8); + HorizontalPred(dst[C8HE8..], left, 8); + TrueMotion(dst[C8TM8..], left, top, 8); + + // V block. + dst = dst[8..]; + if (!top.IsEmpty) + { + top = top[8..]; + } + + if (!left.IsEmpty) + { + left = left[16..]; + } + + DcMode(dst[C8DC8..], left, top, 8, 8, 4); + VerticalPred(dst[C8VE8..], top, 8); + HorizontalPred(dst[C8HE8..], left, 8); + TrueMotion(dst[C8TM8..], left, top, 8); + } + + // Left samples are top[-5 .. -2], top_left is top[-1], top are + // located at top[0..3], and top right is top[4..7] + public static void EncPredLuma4(Span dst, Span top, int topOffset, Span vals) + { + Dc4(dst[I4DC4..], top, topOffset); + Tm4(dst[I4TM4..], top, topOffset); + Ve4(dst[I4VE4..], top, topOffset, vals); + He4(dst[I4HE4..], top, topOffset); + Rd4(dst[I4RD4..], top, topOffset); + Vr4(dst[I4VR4..], top, topOffset); + Ld4(dst[I4LD4..], top, topOffset); + Vl4(dst[I4VL4..], top, topOffset); + Hd4(dst[I4HD4..], top, topOffset); + Hu4(dst[I4HU4..], top, topOffset); + } + + private static void VerticalPred(Span dst, Span top, int size) + { + if (!top.IsEmpty) + { + for (int j = 0; j < size; j++) + { + top[..size].CopyTo(dst[(j * WebpConstants.Bps)..]); + } + } + else + { + Fill(dst, 127, size); + } + } + + public static void HorizontalPred(Span dst, Span left, int size) + { + if (!left.IsEmpty) + { + left = left[1..]; // in the reference implementation, left starts at - 1. + for (int j = 0; j < size; j++) + { + dst.Slice(j * WebpConstants.Bps, size).Fill(left[j]); + } + } + else + { + Fill(dst, 129, size); + } + } + + public static void TrueMotion(Span dst, Span left, Span top, int size) + { + if (!left.IsEmpty) + { + if (!top.IsEmpty) + { + Span clip = Clip1.AsSpan(255 - left[0]); // left [0] instead of left[-1], original left starts at -1 + for (int y = 0; y < size; y++) + { + Span clipTable = clip[left[y + 1]..]; // left[y] + for (int x = 0; x < size; x++) + { + dst[x] = clipTable[top[x]]; + } + + dst = dst[WebpConstants.Bps..]; + } + } + else + { + HorizontalPred(dst, left, size); + } + } + else + { + // true motion without left samples (hence: with default 129 value) + // is equivalent to VE prediction where you just copy the top samples. + // Note that if top samples are not available, the default value is + // then 129, and not 127 as in the VerticalPred case. + if (!top.IsEmpty) + { + VerticalPred(dst, top, size); + } + else + { + Fill(dst, 129, size); + } + } + } + + private static void DcMode(Span dst, Span left, Span top, int size, int round, int shift) + { + int dc = 0; + int j; + if (!top.IsEmpty) + { + for (j = 0; j < size; j++) + { + dc += top[j]; + } + + if (!left.IsEmpty) + { + // top and left present. + left = left[1..]; // in the reference implementation, left starts at -1. + for (j = 0; j < size; j++) + { + dc += left[j]; + } + } + else + { + // top, but no left. + dc += dc; + } + + dc = (dc + round) >> shift; + } + else if (!left.IsEmpty) + { + // left but no top. + left = left[1..]; // in the reference implementation, left starts at -1. + for (j = 0; j < size; j++) + { + dc += left[j]; + } + + dc += dc; + dc = (dc + round) >> shift; + } + else + { + // no top, no left, nothing. + dc = 0x80; + } + + Fill(dst, dc, size); + } + + private static void Dc4(Span dst, Span top, int topOffset) + { + uint dc = 4; + int i; + for (i = 0; i < 4; i++) + { + dc += (uint)(top[topOffset + i] + top[topOffset - 5 + i]); + } + + Fill(dst, (int)(dc >> 3), 4); + } + + private static void Tm4(Span dst, Span top, int topOffset) + { + Span clip = Clip1.AsSpan(255 - top[topOffset - 1]); + for (int y = 0; y < 4; y++) + { + Span clipTable = clip[top[topOffset - 2 - y]..]; + for (int x = 0; x < 4; x++) + { + dst[x] = clipTable[top[topOffset + x]]; + } + + dst = dst[WebpConstants.Bps..]; + } + } + + private static void Ve4(Span dst, Span top, int topOffset, Span vals) + { + // vertical + vals[0] = LossyUtils.Avg3(top[topOffset - 1], top[topOffset], top[topOffset + 1]); + vals[1] = LossyUtils.Avg3(top[topOffset], top[topOffset + 1], top[topOffset + 2]); + vals[2] = LossyUtils.Avg3(top[topOffset + 1], top[topOffset + 2], top[topOffset + 3]); + vals[3] = LossyUtils.Avg3(top[topOffset + 2], top[topOffset + 3], top[topOffset + 4]); + for (int i = 0; i < 4; i++) + { + vals.CopyTo(dst[(i * WebpConstants.Bps)..]); + } + } + + private static void He4(Span dst, Span top, int topOffset) + { + // horizontal + byte x = top[topOffset - 1]; + byte i = top[topOffset - 2]; + byte j = top[topOffset - 3]; + byte k = top[topOffset - 4]; + byte l = top[topOffset - 5]; + + uint val = 0x01010101U * LossyUtils.Avg3(x, i, j); + BinaryPrimitives.WriteUInt32BigEndian(dst, val); + val = 0x01010101U * LossyUtils.Avg3(i, j, k); + BinaryPrimitives.WriteUInt32BigEndian(dst[(1 * WebpConstants.Bps)..], val); + val = 0x01010101U * LossyUtils.Avg3(j, k, l); + BinaryPrimitives.WriteUInt32BigEndian(dst[(2 * WebpConstants.Bps)..], val); + val = 0x01010101U * LossyUtils.Avg3(k, l, l); + BinaryPrimitives.WriteUInt32BigEndian(dst[(3 * WebpConstants.Bps)..], val); + } + + private static void Rd4(Span dst, Span top, int topOffset) + { + byte x = top[topOffset - 1]; + byte i = top[topOffset - 2]; + byte j = top[topOffset - 3]; + byte k = top[topOffset - 4]; + byte l = top[topOffset - 5]; + byte a = top[topOffset]; + byte b = top[topOffset + 1]; + byte c = top[topOffset + 2]; + byte d = top[topOffset + 3]; + + LossyUtils.Dst(dst, 0, 3, LossyUtils.Avg3(j, k, l)); + byte ijk = LossyUtils.Avg3(i, j, k); + LossyUtils.Dst(dst, 0, 2, ijk); + LossyUtils.Dst(dst, 1, 3, ijk); + byte xij = LossyUtils.Avg3(x, i, j); + LossyUtils.Dst(dst, 0, 1, xij); + LossyUtils.Dst(dst, 1, 2, xij); + LossyUtils.Dst(dst, 2, 3, xij); + byte axi = LossyUtils.Avg3(a, x, i); + LossyUtils.Dst(dst, 0, 0, axi); + LossyUtils.Dst(dst, 1, 1, axi); + LossyUtils.Dst(dst, 2, 2, axi); + LossyUtils.Dst(dst, 3, 3, axi); + byte bax = LossyUtils.Avg3(b, a, x); + LossyUtils.Dst(dst, 1, 0, bax); + LossyUtils.Dst(dst, 2, 1, bax); + LossyUtils.Dst(dst, 3, 2, bax); + byte cba = LossyUtils.Avg3(c, b, a); + LossyUtils.Dst(dst, 2, 0, cba); + LossyUtils.Dst(dst, 3, 1, cba); + LossyUtils.Dst(dst, 3, 0, LossyUtils.Avg3(d, c, b)); + } + + private static void Vr4(Span dst, Span top, int topOffset) + { + byte x = top[topOffset - 1]; + byte i = top[topOffset - 2]; + byte j = top[topOffset - 3]; + byte k = top[topOffset - 4]; + byte a = top[topOffset]; + byte b = top[topOffset + 1]; + byte c = top[topOffset + 2]; + byte d = top[topOffset + 3]; + + byte xa = LossyUtils.Avg2(x, a); + LossyUtils.Dst(dst, 0, 0, xa); + LossyUtils.Dst(dst, 1, 2, xa); + byte ab = LossyUtils.Avg2(a, b); + LossyUtils.Dst(dst, 1, 0, ab); + LossyUtils.Dst(dst, 2, 2, ab); + byte bc = LossyUtils.Avg2(b, c); + LossyUtils.Dst(dst, 2, 0, bc); + LossyUtils.Dst(dst, 3, 2, bc); + LossyUtils.Dst(dst, 3, 0, LossyUtils.Avg2(c, d)); + LossyUtils.Dst(dst, 0, 3, LossyUtils.Avg3(k, j, i)); + LossyUtils.Dst(dst, 0, 2, LossyUtils.Avg3(j, i, x)); + byte ixa = LossyUtils.Avg3(i, x, a); + LossyUtils.Dst(dst, 0, 1, ixa); + LossyUtils.Dst(dst, 1, 3, ixa); + byte xab = LossyUtils.Avg3(x, a, b); + LossyUtils.Dst(dst, 1, 1, xab); + LossyUtils.Dst(dst, 2, 3, xab); + byte abc = LossyUtils.Avg3(a, b, c); + LossyUtils.Dst(dst, 2, 1, abc); + LossyUtils.Dst(dst, 3, 3, abc); + LossyUtils.Dst(dst, 3, 1, LossyUtils.Avg3(b, c, d)); + } + + private static void Ld4(Span dst, Span top, int topOffset) + { + byte a = top[topOffset + 0]; + byte b = top[topOffset + 1]; + byte c = top[topOffset + 2]; + byte d = top[topOffset + 3]; + byte e = top[topOffset + 4]; + byte f = top[topOffset + 5]; + byte g = top[topOffset + 6]; + byte h = top[topOffset + 7]; + + LossyUtils.Dst(dst, 0, 0, LossyUtils.Avg3(a, b, c)); + byte bcd = LossyUtils.Avg3(b, c, d); + LossyUtils.Dst(dst, 1, 0, bcd); + LossyUtils.Dst(dst, 0, 1, bcd); + byte cde = LossyUtils.Avg3(c, d, e); + LossyUtils.Dst(dst, 2, 0, cde); + LossyUtils.Dst(dst, 1, 1, cde); + LossyUtils.Dst(dst, 0, 2, cde); + byte def = LossyUtils.Avg3(d, e, f); + LossyUtils.Dst(dst, 3, 0, def); + LossyUtils.Dst(dst, 2, 1, def); + LossyUtils.Dst(dst, 1, 2, def); + LossyUtils.Dst(dst, 0, 3, def); + byte efg = LossyUtils.Avg3(e, f, g); + LossyUtils.Dst(dst, 3, 1, efg); + LossyUtils.Dst(dst, 2, 2, efg); + LossyUtils.Dst(dst, 1, 3, efg); + byte fgh = LossyUtils.Avg3(f, g, h); + LossyUtils.Dst(dst, 3, 2, fgh); + LossyUtils.Dst(dst, 2, 3, fgh); + LossyUtils.Dst(dst, 3, 3, LossyUtils.Avg3(g, h, h)); + } + + private static void Vl4(Span dst, Span top, int topOffset) + { + byte a = top[topOffset + 0]; + byte b = top[topOffset + 1]; + byte c = top[topOffset + 2]; + byte d = top[topOffset + 3]; + byte e = top[topOffset + 4]; + byte f = top[topOffset + 5]; + byte g = top[topOffset + 6]; + byte h = top[topOffset + 7]; + + LossyUtils.Dst(dst, 0, 0, LossyUtils.Avg2(a, b)); + byte bc = LossyUtils.Avg2(b, c); + LossyUtils.Dst(dst, 1, 0, bc); + LossyUtils.Dst(dst, 0, 2, bc); + byte cd = LossyUtils.Avg2(c, d); + LossyUtils.Dst(dst, 2, 0, cd); + LossyUtils.Dst(dst, 1, 2, cd); + byte de = LossyUtils.Avg2(d, e); + LossyUtils.Dst(dst, 3, 0, de); + LossyUtils.Dst(dst, 2, 2, de); + LossyUtils.Dst(dst, 0, 1, LossyUtils.Avg3(a, b, c)); + byte bcd = LossyUtils.Avg3(b, c, d); + LossyUtils.Dst(dst, 1, 1, bcd); + LossyUtils.Dst(dst, 0, 3, bcd); + byte cde = LossyUtils.Avg3(c, d, e); + LossyUtils.Dst(dst, 2, 1, cde); + LossyUtils.Dst(dst, 1, 3, cde); + byte def = LossyUtils.Avg3(d, e, f); + LossyUtils.Dst(dst, 3, 1, def); + LossyUtils.Dst(dst, 2, 3, def); + LossyUtils.Dst(dst, 3, 2, LossyUtils.Avg3(e, f, g)); + LossyUtils.Dst(dst, 3, 3, LossyUtils.Avg3(f, g, h)); + } + + private static void Hd4(Span dst, Span top, int topOffset) + { + byte x = top[topOffset - 1]; + byte i = top[topOffset - 2]; + byte j = top[topOffset - 3]; + byte k = top[topOffset - 4]; + byte l = top[topOffset - 5]; + byte a = top[topOffset]; + byte b = top[topOffset + 1]; + byte c = top[topOffset + 2]; + + byte ix = LossyUtils.Avg2(i, x); + LossyUtils.Dst(dst, 0, 0, ix); + LossyUtils.Dst(dst, 2, 1, ix); + byte ji = LossyUtils.Avg2(j, i); + LossyUtils.Dst(dst, 0, 1, ji); + LossyUtils.Dst(dst, 2, 2, ji); + byte kj = LossyUtils.Avg2(k, j); + LossyUtils.Dst(dst, 0, 2, kj); + LossyUtils.Dst(dst, 2, 3, kj); + LossyUtils.Dst(dst, 0, 3, LossyUtils.Avg2(l, k)); + LossyUtils.Dst(dst, 3, 0, LossyUtils.Avg3(a, b, c)); + LossyUtils.Dst(dst, 2, 0, LossyUtils.Avg3(x, a, b)); + byte ixa = LossyUtils.Avg3(i, x, a); + LossyUtils.Dst(dst, 1, 0, ixa); + LossyUtils.Dst(dst, 3, 1, ixa); + byte jix = LossyUtils.Avg3(j, i, x); + LossyUtils.Dst(dst, 1, 1, jix); + LossyUtils.Dst(dst, 3, 2, jix); + byte kji = LossyUtils.Avg3(k, j, i); + LossyUtils.Dst(dst, 1, 2, kji); + LossyUtils.Dst(dst, 3, 3, kji); + LossyUtils.Dst(dst, 1, 3, LossyUtils.Avg3(l, k, j)); + } + + private static void Hu4(Span dst, Span top, int topOffset) + { + byte i = top[topOffset - 2]; + byte j = top[topOffset - 3]; + byte k = top[topOffset - 4]; + byte l = top[topOffset - 5]; + + LossyUtils.Dst(dst, 0, 0, LossyUtils.Avg2(i, j)); + byte jk = LossyUtils.Avg2(j, k); + LossyUtils.Dst(dst, 2, 0, jk); + LossyUtils.Dst(dst, 0, 1, jk); + byte kl = LossyUtils.Avg2(k, l); + LossyUtils.Dst(dst, 2, 1, kl); + LossyUtils.Dst(dst, 0, 2, kl); + LossyUtils.Dst(dst, 1, 0, LossyUtils.Avg3(i, j, k)); + byte jkl = LossyUtils.Avg3(j, k, l); + LossyUtils.Dst(dst, 3, 0, jkl); + LossyUtils.Dst(dst, 1, 1, jkl); + byte kll = LossyUtils.Avg3(k, l, l); + LossyUtils.Dst(dst, 3, 1, kll); + LossyUtils.Dst(dst, 1, 2, kll); + LossyUtils.Dst(dst, 3, 2, l); + LossyUtils.Dst(dst, 2, 2, l); + LossyUtils.Dst(dst, 0, 3, l); + LossyUtils.Dst(dst, 1, 3, l); + LossyUtils.Dst(dst, 2, 3, l); + LossyUtils.Dst(dst, 3, 3, l); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void Fill(Span dst, int value, int size) + { + for (int j = 0; j < size; j++) + { + dst.Slice(j * WebpConstants.Bps, size).Fill((byte)value); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static byte Clip8b(int v) => (v & ~0xff) == 0 ? (byte)v : v < 0 ? (byte)0 : (byte)255; + + [MethodImpl(InliningOptions.ShortMethod)] + private static void Store(Span dst, Span reference, int x, int y, int v) => dst[x + (y * WebpConstants.Bps)] = LossyUtils.Clip8B(reference[x + (y * WebpConstants.Bps)] + (v >> 3)); + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Mul(int a, int b) => (a * b) >> 16; + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8FilterHeader.cs b/ImageSharp/Formats/Webp/Lossy/Vp8FilterHeader.cs new file mode 100644 index 0000000..55bf69f --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8FilterHeader.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal class Vp8FilterHeader + { + private const int NumRefLfDeltas = 4; + + private const int NumModeLfDeltas = 4; + + private int filterLevel; + + private int sharpness; + + /// + /// Initializes a new instance of the class. + /// + public Vp8FilterHeader() + { + this.RefLfDelta = new int[NumRefLfDeltas]; + this.ModeLfDelta = new int[NumModeLfDeltas]; + } + + /// + /// Gets or sets the loop filter. + /// + public LoopFilter LoopFilter { get; set; } + + /// + /// Gets or sets the filter level. Valid values are [0..63]. + /// + public int FilterLevel + { + get => this.filterLevel; + set + { + Guard.MustBeBetweenOrEqualTo(value, 0, 63, nameof(this.FilterLevel)); + this.filterLevel = value; + } + } + + /// + /// Gets or sets the filter sharpness. Valid values are [0..7]. + /// + public int Sharpness + { + get => this.sharpness; + set + { + Guard.MustBeBetweenOrEqualTo(value, 0, 7, nameof(this.Sharpness)); + this.sharpness = value; + } + } + + /// + /// Gets or sets a value indicating whether the filtering type is: 0=complex, 1=simple. + /// + public bool Simple { get; set; } + + /// + /// Gets or sets delta filter level for i4x4 relative to i16x16. + /// + public int I4x4LfDelta { get; set; } + + public bool UseLfDelta { get; set; } + + public int[] RefLfDelta { get; } + + public int[] ModeLfDelta { get; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8FilterInfo.cs b/ImageSharp/Formats/Webp/Lossy/Vp8FilterInfo.cs new file mode 100644 index 0000000..cd1b3f3 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8FilterInfo.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// Filter information. + /// + internal class Vp8FilterInfo : IDeepCloneable + { + private byte limit; + + private byte innerLevel; + + private byte highEdgeVarianceThreshold; + + /// + /// Initializes a new instance of the class. + /// + public Vp8FilterInfo() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The filter info to create a copy from. + public Vp8FilterInfo(Vp8FilterInfo other) + { + this.Limit = other.Limit; + this.HighEdgeVarianceThreshold = other.HighEdgeVarianceThreshold; + this.InnerLevel = other.InnerLevel; + this.UseInnerFiltering = other.UseInnerFiltering; + } + + /// + /// Gets or sets the filter limit in [3..189], or 0 if no filtering. + /// + public byte Limit + { + get => this.limit; + set + { + Guard.MustBeBetweenOrEqualTo(value, (byte)0, (byte)189, nameof(this.Limit)); + this.limit = value; + } + } + + /// + /// Gets or sets the inner limit in [1..63], or 0 if no filtering. + /// + public byte InnerLevel + { + get => this.innerLevel; + set + { + Guard.MustBeBetweenOrEqualTo(value, (byte)0, (byte)63, nameof(this.InnerLevel)); + this.innerLevel = value; + } + } + + /// + /// Gets or sets a value indicating whether to do inner filtering. + /// + public bool UseInnerFiltering { get; set; } + + /// + /// Gets or sets the high edge variance threshold in [0..2]. + /// + public byte HighEdgeVarianceThreshold + { + get => this.highEdgeVarianceThreshold; + set + { + Guard.MustBeBetweenOrEqualTo(value, (byte)0, (byte)2, nameof(this.HighEdgeVarianceThreshold)); + this.highEdgeVarianceThreshold = value; + } + } + + /// + public IDeepCloneable DeepClone() => new Vp8FilterInfo(this); + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8FrameHeader.cs b/ImageSharp/Formats/Webp/Lossy/Vp8FrameHeader.cs new file mode 100644 index 0000000..f11eda2 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8FrameHeader.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// Vp8 frame header information. + /// + internal class Vp8FrameHeader + { + /// + /// Gets or sets a value indicating whether this is a key frame. + /// + public bool KeyFrame { get; set; } + + /// + /// Gets or sets Vp8 profile [0..3]. + /// + public sbyte Profile { get; set; } + + /// + /// Gets or sets the partition length. + /// + public uint PartitionLength { get; set; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs b/ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs new file mode 100644 index 0000000..07cc4c1 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs @@ -0,0 +1,128 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal sealed class Vp8Histogram + { + /// + /// Size of histogram used by CollectHistogram. + /// + private const int MaxCoeffThresh = 31; + + private int maxValue; + + private int lastNonZero; + + /// + /// Initializes a new instance of the class. + /// + public Vp8Histogram() + { + this.maxValue = 0; + this.lastNonZero = 1; + } + + public int GetAlpha() + { + // 'alpha' will later be clipped to [0..MAX_ALPHA] range, clamping outer + // values which happen to be mostly noise. This leaves the maximum precision + // for handling the useful small values which contribute most. + int maxValue = this.maxValue; + int lastNonZero = this.lastNonZero; + int alpha = maxValue > 1 ? WebpConstants.AlphaScale * lastNonZero / maxValue : 0; + return alpha; + } + + public void CollectHistogram(Span reference, Span pred, int startBlock, int endBlock) + { + Span scratch = stackalloc int[16]; + Span output = stackalloc short[16]; + Span distribution = stackalloc int[MaxCoeffThresh + 1]; + + int j; + for (j = startBlock; j < endBlock; j++) + { + Vp8Encoding.FTransform(reference[WebpLookupTables.Vp8DspScan[j]..], pred[WebpLookupTables.Vp8DspScan[j]..], output, scratch); + + // Convert coefficients to bin. + if (Avx2.IsSupported) + { + // Load. + ref short outputRef = ref MemoryMarshal.GetReference(output); + Vector256 out0 = Unsafe.As>(ref outputRef); + + // v = abs(out) >> 3 + Vector256 abs0 = Avx2.Abs(out0.AsInt16()); + Vector256 v0 = Avx2.ShiftRightArithmetic(abs0.AsInt16(), 3); + + // bin = min(v, MAX_COEFF_THRESH) + Vector256 min0 = Avx2.Min(v0, Vector256.Create((short)MaxCoeffThresh)); + + // Store. + Unsafe.As>(ref outputRef) = min0; + + // Convert coefficients to bin. + for (int k = 0; k < 16; ++k) + { + ++distribution[output[k]]; + } + } + else + { + for (int k = 0; k < 16; ++k) + { + int v = Math.Abs(output[k]) >> 3; + int clippedValue = ClipMax(v, MaxCoeffThresh); + ++distribution[clippedValue]; + } + } + } + + this.SetHistogramData(distribution); + } + + public void Merge(Vp8Histogram other) + { + if (this.maxValue > other.maxValue) + { + other.maxValue = this.maxValue; + } + + if (this.lastNonZero > other.lastNonZero) + { + other.lastNonZero = this.lastNonZero; + } + } + + private void SetHistogramData(ReadOnlySpan distribution) + { + int maxValue = 0; + int lastNonZero = 1; + for (int k = 0; k <= MaxCoeffThresh; ++k) + { + int value = distribution[k]; + if (value > 0) + { + if (value > maxValue) + { + maxValue = value; + } + + lastNonZero = k; + } + } + + this.maxValue = maxValue; + this.lastNonZero = lastNonZero; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int ClipMax(int v, int max) => v > max ? max : v; + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8Io.cs b/ImageSharp/Formats/Webp/Lossy/Vp8Io.cs new file mode 100644 index 0000000..34f0d58 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8Io.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal ref struct Vp8Io + { + /// + /// Gets or sets the picture width in pixels (invariable). + /// The actual area passed to put() is stored in /> field. + /// + public int Width { get; set; } + + /// + /// Gets or sets the picture height in pixels (invariable). + /// The actual area passed to put() is stored in /> field. + /// + public int Height { get; set; } + + /// + /// Gets or sets the y-position of the current macroblock. + /// + public int MbY { get; set; } + + /// + /// Gets or sets number of columns in the sample. + /// + public int MbW { get; set; } + + /// + /// Gets or sets number of rows in the sample. + /// + public int MbH { get; set; } + + /// + /// Gets or sets the luma component. + /// + public Span Y { get; set; } + + /// + /// Gets or sets the U chroma component. + /// + public Span U { get; set; } + + /// + /// Gets or sets the V chroma component. + /// + public Span V { get; set; } + + /// + /// Gets or sets the row stride for luma. + /// + public int YStride { get; set; } + + /// + /// Gets or sets the row stride for chroma. + /// + public int UvStride { get; set; } + + public bool UseScaling { get; set; } + + public int ScaledWidth { get; set; } + + public int ScaledHeight { get; set; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlock.cs b/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlock.cs new file mode 100644 index 0000000..4959ecb --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlock.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// Contextual macroblock information. + /// + internal class Vp8MacroBlock + { + /// + /// Gets or sets non-zero AC/DC coeffs (4bit for luma + 4bit for chroma). + /// + public uint NoneZeroAcDcCoeffs { get; set; } + + /// + /// Gets or sets non-zero DC coeff (1bit). + /// + public uint NoneZeroDcCoeffs { get; set; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockData.cs b/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockData.cs new file mode 100644 index 0000000..00ebec4 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockData.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// Data needed to reconstruct a macroblock. + /// + internal class Vp8MacroBlockData + { + /// + /// Initializes a new instance of the class. + /// + public Vp8MacroBlockData() + { + this.Modes = new byte[16]; + this.Coeffs = new short[384]; + } + + /// + /// Gets or sets the coefficients. 384 coeffs = (16+4+4) * 4*4. + /// + public short[] Coeffs { get; set; } + + /// + /// Gets or sets a value indicating whether its intra4x4. + /// + public bool IsI4x4 { get; set; } + + /// + /// Gets the modes. One 16x16 mode (#0) or sixteen 4x4 modes. + /// + public byte[] Modes { get; } + + /// + /// Gets or sets the chroma prediction mode. + /// + public byte UvMode { get; set; } + + /// + /// Gets or sets bit-wise info about the content of each sub-4x4 blocks (in decoding order). + /// Each of the 4x4 blocks for y/u/v is associated with a 2b code according to: + /// code=0 -> no coefficient + /// code=1 -> only DC + /// code=2 -> first three coefficients are non-zero + /// code=3 -> more than three coefficients are non-zero + /// This allows to call specialized transform functions. + /// + public uint NonZeroY { get; set; } + + /// + /// Gets or sets bit-wise info about the content of each sub-4x4 blocks (in decoding order). + /// Each of the 4x4 blocks for y/u/v is associated with a 2b code according to: + /// code=0 -> no coefficient + /// code=1 -> only DC + /// code=2 -> first three coefficients are non-zero + /// code=3 -> more than three coefficients are non-zero + /// This allows to call specialized transform functions. + /// + public uint NonZeroUv { get; set; } + + public bool Skip { get; set; } + + public byte Segment { get; set; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockInfo.cs b/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockInfo.cs new file mode 100644 index 0000000..887d50b --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockInfo.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + [DebuggerDisplay("Type: {MacroBlockType}, Alpha: {Alpha}, UvMode: {UvMode}")] + internal class Vp8MacroBlockInfo + { + public Vp8MacroBlockType MacroBlockType { get; set; } + + public int UvMode { get; set; } + + public bool Skip { get; set; } + + public int Segment { get; set; } + + public int Alpha { get; set; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockType.cs b/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockType.cs new file mode 100644 index 0000000..50a4c10 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockType.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal enum Vp8MacroBlockType + { + I4X4 = 0, + + I16X16 = 1 + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs b/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs new file mode 100644 index 0000000..9864ca6 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal unsafe struct Vp8Matrix + { + // [luma-ac,luma-dc,chroma][dc,ac] + private static readonly int[][] BiasMatrices = + [ + [96, 110], + [96, 108], + [110, 115] + ]; + + /// + /// Number of descaling bits for sharpening bias. + /// + private const int SharpenBits = 11; + + /// + /// The quantizer steps. + /// + public fixed ushort Q[16]; + + /// + /// The reciprocals, fixed point. + /// + public fixed ushort IQ[16]; + + /// + /// The rounding bias. + /// + public fixed uint Bias[16]; + + /// + /// The value below which a coefficient is zeroed. + /// + public fixed uint ZThresh[16]; + + /// + /// The frequency boosters for slight sharpening. + /// + public fixed short Sharpen[16]; + + // Sharpening by (slightly) raising the hi-frequency coeffs. + // Hack-ish but helpful for mid-bitrate range. Use with care. + // This uses C#'s optimization to refer to the static data segment of the assembly, no allocation occurs. + private static ReadOnlySpan FreqSharpening => [0, 30, 60, 90, 30, 60, 90, 90, 60, 90, 90, 90, 90, 90, 90, 90]; + + /// + /// Returns the average quantizer. + /// + /// The average quantizer. + public int Expand(int type) + { + int sum; + int i; + for (i = 0; i < 2; i++) + { + int isAcCoeff = i > 0 ? 1 : 0; + int bias = BiasMatrices[type][isAcCoeff]; + this.IQ[i] = (ushort)((1 << WebpConstants.QFix) / this.Q[i]); + this.Bias[i] = (uint)BIAS(bias); + + // zthresh is the exact value such that QUANTDIV(coeff, iQ, B) is: + // * zero if coeff <= zthresh + // * non-zero if coeff > zthresh + this.ZThresh[i] = ((1 << WebpConstants.QFix) - 1 - this.Bias[i]) / this.IQ[i]; + } + + for (i = 2; i < 16; i++) + { + this.Q[i] = this.Q[1]; + this.IQ[i] = this.IQ[1]; + this.Bias[i] = this.Bias[1]; + this.ZThresh[i] = this.ZThresh[1]; + } + + for (sum = 0, i = 0; i < 16; i++) + { + if (type == 0) + { + // We only use sharpening for AC luma coeffs. + this.Sharpen[i] = (short)((FreqSharpening[i] * this.Q[i]) >> SharpenBits); + } + else + { + this.Sharpen[i] = 0; + } + + sum += this.Q[i]; + } + + return (sum + 8) >> 4; + } + + private static int BIAS(int b) => b << (WebpConstants.QFix - 8); + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8ModeScore.cs b/ImageSharp/Formats/Webp/Lossy/Vp8ModeScore.cs new file mode 100644 index 0000000..7e78b71 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8ModeScore.cs @@ -0,0 +1,138 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// Class to accumulate score and info during RD-optimization and mode evaluation. + /// + internal class Vp8ModeScore + { + public const long MaxCost = 0x7fffffffffffffL; + + /// + /// Distortion multiplier (equivalent of lambda). + /// + private const int RdDistoMult = 256; + + /// + /// Initializes a new instance of the class. + /// + public Vp8ModeScore() + { + this.YDcLevels = new short[16]; + this.YAcLevels = new short[16 * 16]; + this.UvLevels = new short[(4 + 4) * 16]; + + this.ModesI4 = new byte[16]; + this.Derr = new int[2, 3]; + } + + /// + /// Gets or sets the distortion. + /// + public long D { get; set; } + + /// + /// Gets or sets the spectral distortion. + /// + public long SD { get; set; } + + /// + /// Gets or sets the header bits. + /// + public long H { get; set; } + + /// + /// Gets or sets the rate. + /// + public long R { get; set; } + + /// + /// Gets or sets the score. + /// + public long Score { get; set; } + + /// + /// Gets the quantized levels for luma-DC. + /// + public short[] YDcLevels { get; } + + /// + /// Gets the quantized levels for luma-AC. + /// + public short[] YAcLevels { get; } + + /// + /// Gets the quantized levels for chroma. + /// + public short[] UvLevels { get; } + + /// + /// Gets or sets the mode number for intra16 prediction. + /// + public int ModeI16 { get; set; } + + /// + /// Gets the mode numbers for intra4 predictions. + /// + public byte[] ModesI4 { get; } + + /// + /// Gets or sets the mode number of chroma prediction. + /// + public int ModeUv { get; set; } + + /// + /// Gets or sets the Non-zero blocks. + /// + public uint Nz { get; set; } + + /// + /// Gets the diffusion errors. + /// + public int[,] Derr { get; } + + public void Clear() + { + Array.Clear(this.YDcLevels); + Array.Clear(this.YAcLevels); + Array.Clear(this.UvLevels); + Array.Clear(this.ModesI4); + Array.Clear(this.Derr); + } + + public void InitScore() + { + this.D = 0; + this.SD = 0; + this.R = 0; + this.H = 0; + this.Nz = 0; + this.Score = MaxCost; + } + + public void CopyScore(Vp8ModeScore other) + { + this.D = other.D; + this.SD = other.SD; + this.R = other.R; + this.H = other.H; + this.Nz = other.Nz; // note that nz is not accumulated, but just copied. + this.Score = other.Score; + } + + public void AddScore(Vp8ModeScore other) + { + this.D += other.D; + this.SD += other.SD; + this.R += other.R; + this.H += other.H; + this.Nz |= other.Nz; // here, new nz bits are accumulated. + this.Score += other.Score; + } + + public void SetRdScore(int lambda) => this.Score = ((this.R + this.H) * lambda) + (RdDistoMult * (this.D + this.SD)); + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8PictureHeader.cs b/ImageSharp/Formats/Webp/Lossy/Vp8PictureHeader.cs new file mode 100644 index 0000000..3a62048 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8PictureHeader.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal class Vp8PictureHeader + { + /// + /// Gets or sets the width of the image. + /// + public uint Width { get; set; } + + /// + /// Gets or sets the Height of the image. + /// + public uint Height { get; set; } + + /// + /// Gets or sets the horizontal scale. + /// + public sbyte XScale { get; set; } + + /// + /// Gets or sets the vertical scale. + /// + public sbyte YScale { get; set; } + + /// + /// Gets or sets the colorspace. + /// 0 - YUV color space similar to the YCrCb color space defined in. + /// 1 - Reserved for future use. + /// + public sbyte ColorSpace { get; set; } + + /// + /// Gets or sets the clamp type. + /// 0 - Decoders are required to clamp the reconstructed pixel values to between 0 and 255 (inclusive). + /// 1 - Reconstructed pixel values are guaranteed to be between 0 and 255; no clamping is necessary. + /// + public sbyte ClampType { get; set; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8Proba.cs b/ImageSharp/Formats/Webp/Lossy/Vp8Proba.cs new file mode 100644 index 0000000..38b7441 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8Proba.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// Data for all frame-persistent probabilities. + /// + internal class Vp8Proba + { + private const int MbFeatureTreeProbs = 3; + + /// + /// Initializes a new instance of the class. + /// + public Vp8Proba() + { + this.Segments = new uint[MbFeatureTreeProbs]; + this.Bands = new Vp8BandProbas[WebpConstants.NumTypes, WebpConstants.NumBands]; + this.BandsPtr = new Vp8BandProbas[WebpConstants.NumTypes][]; + + for (int i = 0; i < WebpConstants.NumTypes; i++) + { + for (int j = 0; j < WebpConstants.NumBands; j++) + { + this.Bands[i, j] = new Vp8BandProbas(); + } + } + + for (int i = 0; i < WebpConstants.NumTypes; i++) + { + this.BandsPtr[i] = new Vp8BandProbas[16 + 1]; + } + } + + public uint[] Segments { get; } + + public Vp8BandProbas[,] Bands { get; } + + public Vp8BandProbas[][] BandsPtr { get; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8ProbaArray.cs b/ImageSharp/Formats/Webp/Lossy/Vp8ProbaArray.cs new file mode 100644 index 0000000..9fa7e39 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8ProbaArray.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// Probabilities associated to one of the contexts. + /// + internal class Vp8ProbaArray + { + /// + /// Initializes a new instance of the class. + /// + public Vp8ProbaArray() => this.Probabilities = new byte[WebpConstants.NumProbas]; + + /// + /// Gets the probabilities. + /// + public byte[] Probabilities { get; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8QuantMatrix.cs b/ImageSharp/Formats/Webp/Lossy/Vp8QuantMatrix.cs new file mode 100644 index 0000000..e1f0a28 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8QuantMatrix.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal class Vp8QuantMatrix + { + private int dither; + + public int[] Y1Mat { get; } = new int[2]; + + public int[] Y2Mat { get; } = new int[2]; + + public int[] UvMat { get; } = new int[2]; + + /// + /// Gets or sets the U/V quantizer value. + /// + public int UvQuant { get; set; } + + /// + /// Gets or sets the dithering amplitude (0 = off, max=255). + /// + public int Dither + { + get => this.dither; + set + { + Guard.MustBeBetweenOrEqualTo(value, 0, 255, nameof(this.Dither)); + this.dither = value; + } + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8RDLevel.cs b/ImageSharp/Formats/Webp/Lossy/Vp8RDLevel.cs new file mode 100644 index 0000000..b59abf7 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8RDLevel.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// Rate-distortion optimization levels + /// + internal enum Vp8RdLevel + { + /// + /// No rd-opt. + /// + RdOptNone = 0, + + /// + /// Basic scoring (no trellis). + /// + RdOptBasic = 1, + + /// + /// Perform trellis-quant on the final decision only. + /// + RdOptTrellis = 2, + + /// + /// Trellis-quant for every scoring (much slower). + /// + RdOptTrellisAll = 3 + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs b/ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs new file mode 100644 index 0000000..76f4185 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs @@ -0,0 +1,256 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// On-the-fly info about the current set of residuals. + /// + internal class Vp8Residual + { + public int First { get; set; } + + public int Last { get; set; } + + public int CoeffType { get; set; } + + public short[] Coeffs { get; } = new short[16]; + + public Vp8BandProbas[] Prob { get; set; } + + public Vp8Stats[] Stats { get; set; } + + public Vp8Costs[] Costs { get; set; } + + public void Init(int first, int coeffType, Vp8EncProba prob) + { + this.First = first; + this.CoeffType = coeffType; + this.Prob = prob.Coeffs[this.CoeffType]; + this.Stats = prob.Stats[this.CoeffType]; + this.Costs = prob.RemappedCosts[this.CoeffType]; + this.Coeffs.AsSpan().Clear(); + } + + public void SetCoeffs(Span coeffs) + { + if (Sse2.IsSupported) + { + ref short coeffsRef = ref MemoryMarshal.GetReference(coeffs); + Vector128 c0 = Unsafe.As>(ref coeffsRef); + Vector128 c1 = Unsafe.As>(ref Unsafe.Add(ref coeffsRef, 8)); + + // Use SSE2 to compare 16 values with a single instruction. + Vector128 m0 = Sse2.PackSignedSaturate(c0.AsInt16(), c1.AsInt16()); + Vector128 m1 = Sse2.CompareEqual(m0, Vector128.Zero); + + // Get the comparison results as a bitmask into 16bits. Negate the mask to get + // the position of entries that are not equal to zero. We don't need to mask + // out least significant bits according to res->first, since coeffs[0] is 0 + // if res->first > 0. + uint mask = 0x0000ffffu ^ (uint)Sse2.MoveMask(m1); + + // The position of the most significant non-zero bit indicates the position of + // the last non-zero value. + this.Last = mask != 0 ? BitOperations.Log2(mask) : -1; + } + else + { + int n; + this.Last = -1; + for (n = 15; n >= 0; --n) + { + if (coeffs[n] != 0) + { + this.Last = n; + break; + } + } + } + + coeffs[..16].CopyTo(this.Coeffs); + } + + // Simulate block coding, but only record statistics. + // Note: no need to record the fixed probas. + public int RecordCoeffs(int ctx) + { + int n = this.First; + Vp8StatsArray s = this.Stats[n].Stats[ctx]; + if (this.Last < 0) + { + RecordStats(0, s, 0); + return 0; + } + + while (n <= this.Last) + { + int v; + RecordStats(1, s, 0); // order of record doesn't matter + while ((v = this.Coeffs[n++]) == 0) + { + RecordStats(0, s, 1); + s = this.Stats[WebpConstants.Vp8EncBands[n]].Stats[0]; + } + + RecordStats(1, s, 1); + bool bit = (uint)(v + 1) > 2u; + if (RecordStats(bit ? 1 : 0, s, 2) == 0) + { + // v = -1 or 1 + s = this.Stats[WebpConstants.Vp8EncBands[n]].Stats[1]; + } + else + { + v = Math.Abs(v); + if (v > WebpConstants.MaxVariableLevel) + { + v = WebpConstants.MaxVariableLevel; + } + + int bits = WebpLookupTables.Vp8LevelCodes[v - 1][1]; + int pattern = WebpLookupTables.Vp8LevelCodes[v - 1][0]; + int i; + for (i = 0; (pattern >>= 1) != 0; i++) + { + int mask = 2 << i; + if ((pattern & 1) != 0) + { + RecordStats((bits & mask) != 0 ? 1 : 0, s, 3 + i); + } + } + + s = this.Stats[WebpConstants.Vp8EncBands[n]].Stats[2]; + } + } + + if (n < 16) + { + RecordStats(0, s, 0); + } + + return 1; + } + + public int GetResidualCost(int ctx0) + { + int n = this.First; + int p0 = this.Prob[n].Probabilities[ctx0].Probabilities[0]; + Vp8Costs[] costs = this.Costs; + Vp8CostArray t = costs[n].Costs[ctx0]; + + // bitCost(1, p0) is already incorporated in t[] tables, but only if ctx != 0 + // (as required by the syntax). For ctx0 == 0, we need to add it here or it'll + // be missing during the loop. + int cost = ctx0 == 0 ? LossyUtils.Vp8BitCost(1, (byte)p0) : 0; + + if (this.Last < 0) + { + return LossyUtils.Vp8BitCost(0, (byte)p0); + } + + if (Sse2.IsSupported) + { + Span scratch = stackalloc byte[32]; + Span ctxs = scratch.Slice(0, 16); + Span levels = scratch.Slice(16); + Span absLevels = stackalloc ushort[16]; + + // Precompute clamped levels and contexts, packed to 8b. + ref short outputRef = ref MemoryMarshal.GetReference(this.Coeffs); + Vector128 c0 = Unsafe.As>(ref outputRef).AsInt16(); + Vector128 c1 = Unsafe.As>(ref Unsafe.Add(ref outputRef, 8)).AsInt16(); + Vector128 d0 = Sse2.Subtract(Vector128.Zero, c0); + Vector128 d1 = Sse2.Subtract(Vector128.Zero, c1); + Vector128 e0 = Sse2.Max(c0, d0); // abs(v), 16b + Vector128 e1 = Sse2.Max(c1, d1); + Vector128 f = Sse2.PackSignedSaturate(e0, e1); + Vector128 g = Sse2.Min(f.AsByte(), Vector128.Create((byte)2)); // context = 0, 1, 2 + Vector128 h = Sse2.Min(f.AsByte(), Vector128.Create((byte)67)); // clampLevel in [0..67] + + ref byte ctxsRef = ref MemoryMarshal.GetReference(ctxs); + ref byte levelsRef = ref MemoryMarshal.GetReference(levels); + ref ushort absLevelsRef = ref MemoryMarshal.GetReference(absLevels); + Unsafe.As>(ref ctxsRef) = g; + Unsafe.As>(ref levelsRef) = h; + Unsafe.As>(ref absLevelsRef) = e0.AsUInt16(); + Unsafe.As>(ref Unsafe.Add(ref absLevelsRef, 8)) = e1.AsUInt16(); + + int level; + int flevel; + for (; n < this.Last; ++n) + { + int ctx = ctxs[n]; + level = levels[n]; + flevel = absLevels[n]; + cost += WebpLookupTables.Vp8LevelFixedCosts[flevel] + t.Costs[level]; + t = costs[n + 1].Costs[ctx]; + } + + // Last coefficient is always non-zero. + level = levels[n]; + flevel = absLevels[n]; + cost += WebpLookupTables.Vp8LevelFixedCosts[flevel] + t.Costs[level]; + if (n < 15) + { + int b = WebpConstants.Vp8EncBands[n + 1]; + int ctx = ctxs[n]; + int lastP0 = this.Prob[b].Probabilities[ctx].Probabilities[0]; + cost += LossyUtils.Vp8BitCost(0, (byte)lastP0); + } + + return cost; + } + + { + int v; + for (; n < this.Last; ++n) + { + v = Math.Abs(this.Coeffs[n]); + int ctx = v >= 2 ? 2 : v; + cost += LevelCost(t.Costs, v); + t = costs[n + 1].Costs[ctx]; + } + + // Last coefficient is always non-zero + v = Math.Abs(this.Coeffs[n]); + cost += LevelCost(t.Costs, v); + if (n < 15) + { + int b = WebpConstants.Vp8EncBands[n + 1]; + int ctx = v == 1 ? 1 : 2; + int lastP0 = this.Prob[b].Probabilities[ctx].Probabilities[0]; + cost += LossyUtils.Vp8BitCost(0, (byte)lastP0); + } + + return cost; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int LevelCost(Span table, int level) + => WebpLookupTables.Vp8LevelFixedCosts[level] + table[level > WebpConstants.MaxVariableLevel ? WebpConstants.MaxVariableLevel : level]; + + private static int RecordStats(int bit, Vp8StatsArray statsArr, int idx) + { + // An overflow is inbound. Note we handle this at 0xfffe0000u instead of + // 0xffff0000u to make sure p + 1u does not overflow. + if (statsArr.Stats[idx] >= 0xfffe0000u) + { + statsArr.Stats[idx] = ((statsArr.Stats[idx] + 1u) >> 1) & 0x7fff7fffu; // -> divide the stats by 2. + } + + // Record bit count (lower 16 bits) and increment total count (upper 16 bits). + statsArr.Stats[idx] += 0x00010000u + (uint)bit; + + return bit; + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8SegmentHeader.cs b/ImageSharp/Formats/Webp/Lossy/Vp8SegmentHeader.cs new file mode 100644 index 0000000..6265189 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8SegmentHeader.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// Segment features. + /// + internal class Vp8SegmentHeader + { + private const int NumMbSegments = 4; + + /// + /// Initializes a new instance of the class. + /// + public Vp8SegmentHeader() + { + this.Quantizer = new byte[NumMbSegments]; + this.FilterStrength = new byte[NumMbSegments]; + } + + public bool UseSegment { get; set; } + + /// + /// Gets or sets a value indicating whether to update the segment map or not. + /// + public bool UpdateMap { get; set; } + + /// + /// Gets or sets a value indicating whether to use delta values for quantizer and filter. + /// If this value is false, absolute values are used. + /// + public bool Delta { get; set; } + + /// + /// Gets quantization changes. + /// + public byte[] Quantizer { get; } + + /// + /// Gets the filter strength for segments. + /// + public byte[] FilterStrength { get; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8SegmentInfo.cs b/ImageSharp/Formats/Webp/Lossy/Vp8SegmentInfo.cs new file mode 100644 index 0000000..8b3339f --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8SegmentInfo.cs @@ -0,0 +1,86 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal class Vp8SegmentInfo + { + /// + /// Gets the quantization matrix y1. + /// +#pragma warning disable SA1401 // Fields should be private + public Vp8Matrix Y1; + + /// + /// Gets the quantization matrix y2. + /// + public Vp8Matrix Y2; + + /// + /// Gets the quantization matrix uv. + /// + public Vp8Matrix Uv; +#pragma warning restore SA1401 // Fields should be private + + /// + /// Gets or sets the quant-susceptibility, range [-127,127]. Zero is neutral. Lower values indicate a lower risk of blurriness. + /// + public int Alpha { get; set; } + + /// + /// Gets or sets the filter-susceptibility, range [0,255]. + /// + public int Beta { get; set; } + + /// + /// Gets or sets the final segment quantizer. + /// + public int Quant { get; set; } + + /// + /// Gets or sets the final in-loop filtering strength. + /// + public int FStrength { get; set; } + + /// + /// Gets or sets the max edge delta (for filtering strength). + /// + public int MaxEdge { get; set; } + + /// + /// Gets or sets the penalty for using Intra4. + /// + public long I4Penalty { get; set; } + + /// + /// Gets or sets the minimum distortion required to trigger filtering record. + /// + public int MinDisto { get; set; } + + public int LambdaI16 { get; set; } + + public int LambdaI4 { get; set; } + + public int TLambda { get; set; } + + public int LambdaUv { get; set; } + + public int LambdaMode { get; set; } + + public void StoreMaxDelta(Span dcs) + { + // We look at the first three AC coefficients to determine what is the average + // delta between each sub-4x4 block. + int v0 = Math.Abs(dcs[1]); + int v1 = Math.Abs(dcs[2]); + int v2 = Math.Abs(dcs[4]); + int maxV = v1 > v0 ? v1 : v0; + maxV = v2 > maxV ? v2 : maxV; + if (maxV > this.MaxEdge) + { + this.MaxEdge = maxV; + } + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8Stats.cs b/ImageSharp/Formats/Webp/Lossy/Vp8Stats.cs new file mode 100644 index 0000000..810cac9 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8Stats.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal class Vp8Stats + { + /// + /// Initializes a new instance of the class. + /// + public Vp8Stats() + { + this.Stats = new Vp8StatsArray[WebpConstants.NumCtx]; + for (int i = 0; i < WebpConstants.NumCtx; i++) + { + this.Stats[i] = new Vp8StatsArray(); + } + } + + public Vp8StatsArray[] Stats { get; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8StatsArray.cs b/ImageSharp/Formats/Webp/Lossy/Vp8StatsArray.cs new file mode 100644 index 0000000..5af2165 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8StatsArray.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal class Vp8StatsArray + { + /// + /// Initializes a new instance of the class. + /// + public Vp8StatsArray() => this.Stats = new uint[WebpConstants.NumProbas]; + + public uint[] Stats { get; } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/Vp8TopSamples.cs b/ImageSharp/Formats/Webp/Lossy/Vp8TopSamples.cs new file mode 100644 index 0000000..eb485d2 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/Vp8TopSamples.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal class Vp8TopSamples + { + public byte[] Y { get; } = new byte[16]; + + public byte[] U { get; } = new byte[8]; + + public byte[] V { get; } = new byte[8]; + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs b/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs new file mode 100644 index 0000000..2ccf8ea --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs @@ -0,0 +1,1372 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Webp.BitReader; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + /// + /// Decoder for lossy webp images. This code is a port of libwebp, which can be found here: https://chromium.googlesource.com/webm/libwebp + /// + /// + /// The lossy specification can be found here: https://tools.ietf.org/html/rfc6386 + /// + internal sealed class WebpLossyDecoder + { + /// + /// A bit reader for reading lossy webp streams. + /// + private readonly Vp8BitReader bitReader; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// Initializes a new instance of the class. + /// + /// Bitreader to read from the stream. + /// Used for allocating memory during processing operations. + /// The configuration. + public WebpLossyDecoder(Vp8BitReader bitReader, MemoryAllocator memoryAllocator, Configuration configuration) + { + this.bitReader = bitReader; + this.memoryAllocator = memoryAllocator; + this.configuration = configuration; + } + + /// + /// Decodes the lossless webp image from the stream. + /// + /// The pixel format. + /// The pixel buffer to store the decoded data. + /// The width of the image. + /// The height of the image. + /// Information about the image. + /// The ALPH chunk data. + public void Decode(Buffer2D pixels, int width, int height, WebpImageInfo info, IMemoryOwner alphaData) + where TPixel : unmanaged, IPixel + { + // Paragraph 9.2: color space and clamp type follow. + sbyte colorSpace = (sbyte)this.bitReader.ReadValue(1); + sbyte clampType = (sbyte)this.bitReader.ReadValue(1); + Vp8PictureHeader pictureHeader = new() + { + Width = (uint)width, + Height = (uint)height, + XScale = info.XScale, + YScale = info.YScale, + ColorSpace = colorSpace, + ClampType = clampType + }; + + // Paragraph 9.3: Parse the segment header. + Vp8Proba proba = new(); + Vp8SegmentHeader vp8SegmentHeader = this.ParseSegmentHeader(proba); + + using Vp8Decoder decoder = new( + info.Vp8FrameHeader, + pictureHeader, + vp8SegmentHeader, + proba, + this.memoryAllocator); + Vp8Io io = InitializeVp8Io(decoder, pictureHeader); + + // Paragraph 9.4: Parse the filter specs. + this.ParseFilterHeader(decoder); + decoder.PrecomputeFilterStrengths(); + + // Paragraph 9.5: Parse partitions. + this.ParsePartitions(decoder); + + // Paragraph 9.6: Dequantization Indices. + this.ParseDequantizationIndices(decoder); + + // Ignore the value of update probabilities. + this.bitReader.ReadBool(); + + // Paragraph 13.4: Parse probabilities. + this.ParseProbabilities(decoder); + + // Decode image data. + this.ParseFrame(decoder, io); + + if (info.Features?.Alpha == true) + { + using AlphaDecoder alphaDecoder = new( + width, + height, + alphaData, + info.Features.AlphaChunkHeader, + this.memoryAllocator, + this.configuration); + alphaDecoder.Decode(); + DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha); + } + else + { + this.DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels); + } + } + + private void DecodePixelValues(int width, int height, Span pixelData, Buffer2D decodedPixels) + where TPixel : unmanaged, IPixel + { + int widthMul3 = width * 3; + for (int y = 0; y < height; y++) + { + Span row = pixelData.Slice(y * widthMul3, widthMul3); + Span decodedPixelRow = decodedPixels.DangerousGetRowSpan(y); + PixelOperations.Instance.FromBgr24Bytes( + this.configuration, + row, + decodedPixelRow, + width); + } + } + + private static void DecodePixelValues(int width, int height, Span pixelData, Buffer2D decodedPixels, IMemoryOwner alpha) + where TPixel : unmanaged, IPixel + { + Span alphaSpan = alpha.Memory.Span; + Span pixelsBgr = MemoryMarshal.Cast(pixelData); + for (int y = 0; y < height; y++) + { + int yMulWidth = y * width; + Span decodedPixelRow = decodedPixels.DangerousGetRowSpan(y); + for (int x = 0; x < width; x++) + { + int offset = yMulWidth + x; + Bgr24 bgr = pixelsBgr[offset]; + decodedPixelRow[x] = TPixel.FromBgra32(new Bgra32(bgr.R, bgr.G, bgr.B, alphaSpan[offset])); + } + } + } + + private void ParseFrame(Vp8Decoder dec, Vp8Io io) + { + for (dec.MbY = 0; dec.MbY < dec.BottomRightMbY; ++dec.MbY) + { + // Parse bitstream for this row. + long bitreaderIdx = dec.MbY & dec.NumPartsMinusOne; + Vp8BitReader bitreader = dec.Vp8BitReaders[bitreaderIdx]; + + // Parse intra mode mode row. + for (int mbX = 0; mbX < dec.MbWidth; ++mbX) + { + this.ParseIntraMode(dec, mbX); + } + + while (dec.MbX < dec.MbWidth) + { + this.DecodeMacroBlock(dec, bitreader); + ++dec.MbX; + } + + // Prepare for next scanline. + InitScanline(dec); + + // Reconstruct, filter and emit the row. + this.ProcessRow(dec, io); + } + } + + private void ParseIntraMode(Vp8Decoder dec, int mbX) + { + Vp8MacroBlockData block = dec.MacroBlockData[mbX]; + Span top = dec.IntraT.AsSpan(4 * mbX, 4); + byte[] left = dec.IntraL; + + if (dec.SegmentHeader.UpdateMap) + { + // Hardcoded tree parsing. + block.Segment = this.bitReader.GetBit((int)dec.Probabilities.Segments[0]) == 0 + ? (byte)this.bitReader.GetBit((int)dec.Probabilities.Segments[1]) + : (byte)(this.bitReader.GetBit((int)dec.Probabilities.Segments[2]) + 2); + } + else + { + // default for intra + block.Segment = 0; + } + + if (dec.UseSkipProbability) + { + block.Skip = this.bitReader.GetBit(dec.SkipProbability) == 1; + } + + block.IsI4x4 = this.bitReader.GetBit(145) == 0; + if (!block.IsI4x4) + { + // Hardcoded 16x16 intra-mode decision tree. + int yMode; + if (this.bitReader.GetBit(156) != 0) + { + if (this.bitReader.GetBit(128) != 0) + { + yMode = (int)IntraPredictionMode.TrueMotion; + } + else + { + yMode = (int)IntraPredictionMode.HPrediction; + } + } + else if (this.bitReader.GetBit(163) != 0) + { + yMode = (int)IntraPredictionMode.VPrediction; + } + else + { + yMode = (int)IntraPredictionMode.DcPrediction; + } + + block.Modes[0] = (byte)yMode; + for (int i = 0; i < left.Length; i++) + { + left[i] = (byte)yMode; + top[i] = (byte)yMode; + } + } + else + { + Span modes = block.Modes.AsSpan(); + for (int y = 0; y < 4; y++) + { + int yMode = left[y]; + for (int x = 0; x < 4; x++) + { + byte[] prob = WebpLookupTables.ModesProba[top[x], yMode]; + int i = WebpConstants.YModesIntra4[this.bitReader.GetBit(prob[0])]; + while (i > 0) + { + i = WebpConstants.YModesIntra4[(2 * i) + this.bitReader.GetBit(prob[i])]; + } + + yMode = -i; + top[x] = (byte)yMode; + } + + top.CopyTo(modes); + modes = modes[4..]; + left[y] = (byte)yMode; + } + } + + // Hardcoded UVMode decision tree. + if (this.bitReader.GetBit(142) == 0) + { + // Hardcoded UVMode decision tree. + block.UvMode = 0; + } + else if (this.bitReader.GetBit(114) == 0) + { + // Hardcoded UVMode decision tree. + block.UvMode = 2; + } + else if (this.bitReader.GetBit(183) != 0) + { + // Hardcoded UVMode decision tree. + block.UvMode = 1; + } + else + { + // Hardcoded UVMode decision tree. + block.UvMode = 3; + } + } + + private static void InitScanline(Vp8Decoder dec) + { + Vp8MacroBlock left = dec.LeftMacroBlock; + left.NoneZeroAcDcCoeffs = 0; + left.NoneZeroDcCoeffs = 0; + for (int i = 0; i < dec.IntraL.Length; i++) + { + dec.IntraL[i] = 0; + } + + dec.MbX = 0; + } + + private void ProcessRow(Vp8Decoder dec, Vp8Io io) + { + this.ReconstructRow(dec); + FinishRow(dec, io); + } + + private void ReconstructRow(Vp8Decoder dec) + { + int mby = dec.MbY; + const int yOff = (WebpConstants.Bps * 1) + 8; + const int uOff = yOff + (WebpConstants.Bps * 16) + WebpConstants.Bps; + const int vOff = uOff + 16; + + Span yuv = dec.YuvBuffer.Memory.Span; + Span yDst = yuv[yOff..]; + Span uDst = yuv[uOff..]; + Span vDst = yuv[vOff..]; + + // Initialize left-most block. + int end = 16 * WebpConstants.Bps; + for (int i = 0; i < end; i += WebpConstants.Bps) + { + yuv[i - 1 + yOff] = 129; + } + + end = 8 * WebpConstants.Bps; + for (int i = 0; i < end; i += WebpConstants.Bps) + { + yuv[i - 1 + uOff] = 129; + yuv[i - 1 + vOff] = 129; + } + + // Init top-left sample on left column too. + if (mby > 0) + { + yuv[yOff - 1 - WebpConstants.Bps] = yuv[uOff - 1 - WebpConstants.Bps] = yuv[vOff - 1 - WebpConstants.Bps] = 129; + } + else + { + // We only need to do this init once at block (0,0). + // Afterward, it remains valid for the whole topmost row. + Span tmp = yuv.Slice(yOff - WebpConstants.Bps - 1, 16 + 4 + 1); + for (int i = 0; i < tmp.Length; i++) + { + tmp[i] = 127; + } + + tmp = yuv.Slice(uOff - WebpConstants.Bps - 1, 8 + 1); + for (int i = 0; i < tmp.Length; i++) + { + tmp[i] = 127; + } + + tmp = yuv.Slice(vOff - WebpConstants.Bps - 1, 8 + 1); + for (int i = 0; i < tmp.Length; i++) + { + tmp[i] = 127; + } + } + + Span scratch = stackalloc int[16]; + Span scratchBytes = stackalloc byte[4]; + + // Reconstruct one row. + for (int mbx = 0; mbx < dec.MbWidth; mbx++) + { + Vp8MacroBlockData block = dec.MacroBlockData[mbx]; + + // Rotate in the left samples from previously decoded block. We move four + // pixels at a time for alignment reason, and because of in-loop filter. + if (mbx > 0) + { + for (int i = -1; i < 16; i++) + { + int srcIdx = (i * WebpConstants.Bps) + 12 + yOff; + int dstIdx = (i * WebpConstants.Bps) - 4 + yOff; + yuv.Slice(srcIdx, 4).CopyTo(yuv[dstIdx..]); + } + + for (int i = -1; i < 8; i++) + { + int srcIdx = (i * WebpConstants.Bps) + 4 + uOff; + int dstIdx = (i * WebpConstants.Bps) - 4 + uOff; + yuv.Slice(srcIdx, 4).CopyTo(yuv[dstIdx..]); + srcIdx = (i * WebpConstants.Bps) + 4 + vOff; + dstIdx = (i * WebpConstants.Bps) - 4 + vOff; + yuv.Slice(srcIdx, 4).CopyTo(yuv[dstIdx..]); + } + } + + // Bring top samples into the cache. + Vp8TopSamples topYuv = dec.YuvTopSamples[mbx]; + short[] coeffs = block.Coeffs; + uint bits = block.NonZeroY; + if (mby > 0) + { + topYuv.Y.CopyTo(yuv[(yOff - WebpConstants.Bps)..]); + topYuv.U.CopyTo(yuv[(uOff - WebpConstants.Bps)..]); + topYuv.V.CopyTo(yuv[(vOff - WebpConstants.Bps)..]); + } + + // Predict and add residuals. + if (block.IsI4x4) + { + Span topRight = yuv[(yOff - WebpConstants.Bps + 16)..]; + if (mby > 0) + { + if (mbx >= dec.MbWidth - 1) + { + // On rightmost border. + byte topYuv15 = topYuv.Y[15]; + topRight[0] = topYuv15; + topRight[1] = topYuv15; + topRight[2] = topYuv15; + topRight[3] = topYuv15; + } + else + { + dec.YuvTopSamples[mbx + 1].Y.AsSpan(0, 4).CopyTo(topRight); + } + } + + // Replicate the top-right pixels below. + Span topRightUint = MemoryMarshal.Cast(yuv[(yOff - WebpConstants.Bps + 16)..]); + topRightUint[WebpConstants.Bps] = topRightUint[2 * WebpConstants.Bps] = topRightUint[3 * WebpConstants.Bps] = topRightUint[0]; + + // Predict and add residuals for all 4x4 blocks in turn. + for (int n = 0; n < 16; ++n, bits <<= 2) + { + int offset = yOff + WebpConstants.Scan[n]; + Span dst = yuv[offset..]; + switch (block.Modes[n]) + { + case 0: + LossyUtils.DC4(dst, yuv, offset); + break; + case 1: + LossyUtils.TM4(dst, yuv, offset); + break; + case 2: + LossyUtils.VE4(dst, yuv, offset, scratchBytes); + break; + case 3: + LossyUtils.HE4(dst, yuv, offset); + break; + case 4: + LossyUtils.RD4(dst, yuv, offset); + break; + case 5: + LossyUtils.VR4(dst, yuv, offset); + break; + case 6: + LossyUtils.LD4(dst, yuv, offset); + break; + case 7: + LossyUtils.VL4(dst, yuv, offset); + break; + case 8: + LossyUtils.HD4(dst, yuv, offset); + break; + case 9: + LossyUtils.HU4(dst, yuv, offset); + break; + } + + DoTransform(bits, coeffs.AsSpan(n * 16), dst, scratch); + } + } + else + { + // 16x16 + switch (CheckMode(mbx, mby, block.Modes[0])) + { + case 0: + LossyUtils.DC16(yDst, yuv, yOff); + break; + case 1: + LossyUtils.TM16(yDst, yuv, yOff); + break; + case 2: + LossyUtils.VE16(yDst, yuv, yOff); + break; + case 3: + LossyUtils.HE16(yDst, yuv, yOff); + break; + case 4: + LossyUtils.DC16NoTop(yDst, yuv, yOff); + break; + case 5: + LossyUtils.DC16NoLeft(yDst, yuv, yOff); + break; + case 6: + LossyUtils.DC16NoTopLeft(yDst); + break; + } + + if (bits != 0) + { + for (int n = 0; n < 16; ++n, bits <<= 2) + { + DoTransform(bits, coeffs.AsSpan(n * 16), yDst[WebpConstants.Scan[n]..], scratch); + } + } + } + + // Chroma + uint bitsUv = block.NonZeroUv; + switch (CheckMode(mbx, mby, block.UvMode)) + { + case 0: + LossyUtils.DC8uv(uDst, yuv, uOff); + LossyUtils.DC8uv(vDst, yuv, vOff); + break; + case 1: + LossyUtils.TM8uv(uDst, yuv, uOff); + LossyUtils.TM8uv(vDst, yuv, vOff); + break; + case 2: + LossyUtils.VE8uv(uDst, yuv, uOff); + LossyUtils.VE8uv(vDst, yuv, vOff); + break; + case 3: + LossyUtils.HE8uv(uDst, yuv, uOff); + LossyUtils.HE8uv(vDst, yuv, vOff); + break; + case 4: + LossyUtils.DC8uvNoTop(uDst, yuv, uOff); + LossyUtils.DC8uvNoTop(vDst, yuv, vOff); + break; + case 5: + LossyUtils.DC8uvNoLeft(uDst, yuv, uOff); + LossyUtils.DC8uvNoLeft(vDst, yuv, vOff); + break; + case 6: + LossyUtils.DC8uvNoTopLeft(uDst); + LossyUtils.DC8uvNoTopLeft(vDst); + break; + } + + DoUVTransform(bitsUv, coeffs.AsSpan(16 * 16), uDst, scratch); + DoUVTransform(bitsUv >> 8, coeffs.AsSpan(20 * 16), vDst, scratch); + + // Stash away top samples for next block. + if (mby < dec.MbHeight - 1) + { + yDst.Slice(15 * WebpConstants.Bps, 16).CopyTo(topYuv.Y); + uDst.Slice(7 * WebpConstants.Bps, 8).CopyTo(topYuv.U); + vDst.Slice(7 * WebpConstants.Bps, 8).CopyTo(topYuv.V); + } + + // Transfer reconstructed samples from yuv_buffer cache to final destination. + Span yOut = dec.CacheY.Memory.Span[(dec.CacheYOffset + (mbx * 16))..]; + Span uOut = dec.CacheU.Memory.Span[(dec.CacheUvOffset + (mbx * 8))..]; + Span vOut = dec.CacheV.Memory.Span[(dec.CacheUvOffset + (mbx * 8))..]; + for (int j = 0; j < 16; j++) + { + yDst.Slice(j * WebpConstants.Bps, Math.Min(16, yOut.Length)).CopyTo(yOut[(j * dec.CacheYStride)..]); + } + + for (int j = 0; j < 8; j++) + { + int jUvStride = j * dec.CacheUvStride; + uDst.Slice(j * WebpConstants.Bps, Math.Min(8, uOut.Length)).CopyTo(uOut[jUvStride..]); + vDst.Slice(j * WebpConstants.Bps, Math.Min(8, vOut.Length)).CopyTo(vOut[jUvStride..]); + } + } + } + + private static void FilterRow(Vp8Decoder dec) + { + int mby = dec.MbY; + for (int mbx = dec.TopLeftMbX; mbx < dec.BottomRightMbX; ++mbx) + { + DoFilter(dec, mbx, mby); + } + } + + private static void DoFilter(Vp8Decoder dec, int mbx, int mby) + { + int yBps = dec.CacheYStride; + Vp8FilterInfo filterInfo = dec.FilterInfo[mbx]; + int iLevel = filterInfo.InnerLevel; + int limit = filterInfo.Limit; + + if (limit == 0) + { + return; + } + + switch (dec.Filter) + { + case LoopFilter.Simple: + { + int offset = dec.CacheYOffset + (mbx * 16); + if (mbx > 0) + { + LossyUtils.SimpleHFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); + } + + if (filterInfo.UseInnerFiltering) + { + LossyUtils.SimpleHFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); + } + + if (mby > 0) + { + LossyUtils.SimpleVFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); + } + + if (filterInfo.UseInnerFiltering) + { + LossyUtils.SimpleVFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); + } + + break; + } + + case LoopFilter.Complex: + { + int uvBps = dec.CacheUvStride; + int yOffset = dec.CacheYOffset + (mbx * 16); + int uvOffset = dec.CacheUvOffset + (mbx * 8); + int hevThresh = filterInfo.HighEdgeVarianceThreshold; + if (mbx > 0) + { + LossyUtils.HFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh); + LossyUtils.HFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh); + } + + if (filterInfo.UseInnerFiltering) + { + LossyUtils.HFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh); + LossyUtils.HFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh); + } + + if (mby > 0) + { + LossyUtils.VFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh); + LossyUtils.VFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh); + } + + if (filterInfo.UseInnerFiltering) + { + LossyUtils.VFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh); + LossyUtils.VFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh); + } + + break; + } + } + } + + private static void FinishRow(Vp8Decoder dec, Vp8Io io) + { + int extraYRows = WebpConstants.FilterExtraRows[(int)dec.Filter]; + int ySize = extraYRows * dec.CacheYStride; + int uvSize = extraYRows / 2 * dec.CacheUvStride; + Span yDst = dec.CacheY.Memory.Span; + Span uDst = dec.CacheU.Memory.Span; + Span vDst = dec.CacheV.Memory.Span; + int mby = dec.MbY; + bool isFirstRow = mby == 0; + bool isLastRow = mby >= dec.BottomRightMbY - 1; + bool filterRow = dec.Filter != LoopFilter.None && dec.MbY >= dec.TopLeftMbY && dec.MbY <= dec.BottomRightMbY; + + if (filterRow) + { + FilterRow(dec); + } + + int yStart = mby * 16; + int yEnd = (mby + 1) * 16; + if (!isFirstRow) + { + yStart -= extraYRows; + io.Y = yDst; + io.U = uDst; + io.V = vDst; + } + else + { + io.Y = dec.CacheY.Memory.Span[dec.CacheYOffset..]; + io.U = dec.CacheU.Memory.Span[dec.CacheUvOffset..]; + io.V = dec.CacheV.Memory.Span[dec.CacheUvOffset..]; + } + + if (!isLastRow) + { + yEnd -= extraYRows; + } + + if (yEnd > io.Height) + { + yEnd = io.Height; // make sure we don't overflow on last row. + } + + if (yStart < yEnd) + { + io.MbY = yStart; + io.MbW = io.Width; + io.MbH = yEnd - yStart; + EmitRgb(dec, io); + } + + // Rotate top samples if needed. + if (!isLastRow) + { + yDst.Slice(16 * dec.CacheYStride, ySize).CopyTo(dec.CacheY.Memory.Span); + uDst.Slice(8 * dec.CacheUvStride, uvSize).CopyTo(dec.CacheU.Memory.Span); + vDst.Slice(8 * dec.CacheUvStride, uvSize).CopyTo(dec.CacheV.Memory.Span); + } + } + + private static int EmitRgb(Vp8Decoder dec, Vp8Io io) + { + Span buf = dec.Pixels.Memory.Span; + int numLinesOut = io.MbH; // a priori guess. + Span curY = io.Y; + Span curU = io.U; + Span curV = io.V; + Span tmpYBuffer = dec.TmpYBuffer.Memory.Span; + Span tmpUBuffer = dec.TmpUBuffer.Memory.Span; + Span tmpVBuffer = dec.TmpVBuffer.Memory.Span; + Span topU = tmpUBuffer; + Span topV = tmpVBuffer; + const int bpp = 3; + int bufferStride = bpp * io.Width; + int dstStartIdx = io.MbY * bufferStride; + Span dst = buf[dstStartIdx..]; + int yEnd = io.MbY + io.MbH; + int mbw = io.MbW; + int uvw = (mbw + 1) >> 1; // >> 1 is bit-hack for / 2 + int y = io.MbY; + byte[] uvBuffer = new byte[(14 * 32) + 15]; + + if (y == 0) + { + // First line is special cased. We mirror the u/v samples at boundary. + YuvConversion.UpSample(curY, default, curU, curV, curU, curV, dst, default, mbw, uvBuffer); + } + else + { + // We can finish the left-over line from previous call. + YuvConversion.UpSample(tmpYBuffer, curY, topU, topV, curU, curV, buf[(dstStartIdx - bufferStride)..], dst, mbw, uvBuffer); + numLinesOut++; + } + + // Loop over each output pairs of row. + int bufferStride2 = 2 * bufferStride; + int ioStride2 = 2 * io.YStride; + for (; y + 2 < yEnd; y += 2) + { + topU = curU; + topV = curV; + curU = curU[io.UvStride..]; + curV = curV[io.UvStride..]; + YuvConversion.UpSample(curY[io.YStride..], curY[ioStride2..], topU, topV, curU, curV, dst[bufferStride..], dst[bufferStride2..], mbw, uvBuffer); + curY = curY[ioStride2..]; + dst = dst[bufferStride2..]; + } + + // Move to last row. + curY = curY[io.YStride..]; + if (yEnd < io.Height) + { + // Save the unfinished samples for next call (as we're not done yet). + curY[..mbw].CopyTo(tmpYBuffer); + curU[..uvw].CopyTo(tmpUBuffer); + curV[..uvw].CopyTo(tmpVBuffer); + + // The upsampler leaves a row unfinished behind (except for the very last row). + numLinesOut--; + } + else + { + // Process the very last row of even-sized picture. + if ((yEnd & 1) == 0) + { + YuvConversion.UpSample(curY, default, curU, curV, curU, curV, dst[bufferStride..], default, mbw, uvBuffer); + } + } + + return numLinesOut; + } + + private static void DoTransform(uint bits, Span src, Span dst, Span scratch) + { + switch (bits >> 30) + { + case 3: + LossyUtils.TransformOne(src, dst, scratch); + break; + case 2: + LossyUtils.TransformAc3(src, dst); + break; + case 1: + LossyUtils.TransformDc(src, dst); + break; + } + } + + private static void DoUVTransform(uint bits, Span src, Span dst, Span scratch) + { + // any non-zero coeff at all? + if ((bits & 0xff) > 0) + { + // any non-zero AC coefficient? + if ((bits & 0xaa) > 0) + { + LossyUtils.TransformUv(src, dst, scratch); // note we don't use the AC3 variant for U/V. + } + else + { + LossyUtils.TransformDcuv(src, dst); + } + } + } + + private void DecodeMacroBlock(Vp8Decoder dec, Vp8BitReader bitreader) + { + Vp8MacroBlock left = dec.LeftMacroBlock; + Vp8MacroBlock macroBlock = dec.CurrentMacroBlock; + Vp8MacroBlockData blockData = dec.CurrentBlockData; + bool skip = dec.UseSkipProbability && blockData.Skip; + + if (!skip) + { + skip = this.ParseResiduals(dec, bitreader, macroBlock); + } + else + { + left.NoneZeroAcDcCoeffs = macroBlock.NoneZeroAcDcCoeffs = 0; + if (!blockData.IsI4x4) + { + left.NoneZeroDcCoeffs = macroBlock.NoneZeroDcCoeffs = 0; + } + + blockData.NonZeroY = 0; + blockData.NonZeroUv = 0; + } + + // Store filter info. + if (dec.Filter != LoopFilter.None) + { + Vp8FilterInfo precomputedFilterInfo = dec.FilterStrength[blockData.Segment, blockData.IsI4x4 ? 1 : 0]; + dec.FilterInfo[dec.MbX] = (Vp8FilterInfo)precomputedFilterInfo.DeepClone(); + dec.FilterInfo[dec.MbX].UseInnerFiltering |= !skip; + } + } + + private bool ParseResiduals(Vp8Decoder dec, Vp8BitReader br, Vp8MacroBlock mb) + { + uint nonZeroY = 0; + uint nonZeroUv = 0; + int first; + int dstOffset = 0; + Vp8MacroBlockData block = dec.CurrentBlockData; + Vp8QuantMatrix q = dec.DeQuantMatrices[block.Segment]; + Vp8BandProbas[][] bands = dec.Probabilities.BandsPtr; + Vp8BandProbas[] acProba; + Vp8MacroBlock leftMb = dec.LeftMacroBlock; + short[] dst = block.Coeffs; + for (int i = 0; i < dst.Length; i++) + { + dst[i] = 0; + } + + if (block.IsI4x4) + { + first = 0; + acProba = bands[3]; + } + else + { + // Parse DC + Span dc = stackalloc short[16]; + int ctx = (int)(mb.NoneZeroDcCoeffs + leftMb.NoneZeroDcCoeffs); + int nz = GetCoeffs(br, bands[1], ctx, q.Y2Mat, 0, dc); + mb.NoneZeroDcCoeffs = leftMb.NoneZeroDcCoeffs = (uint)(nz > 0 ? 1 : 0); + if (nz > 1) + { + // More than just the DC -> perform the full transform. + LossyUtils.TransformWht(dc, dst, stackalloc int[16]); + } + else + { + // Only DC is non-zero -> inlined simplified transform. + int dc0 = (dc[0] + 3) >> 3; + for (int i = 0; i < 16 * 16; i += 16) + { + dst[i] = (short)dc0; + } + } + + first = 1; + acProba = bands[0]; + } + + byte tnz = (byte)(mb.NoneZeroAcDcCoeffs & 0x0f); + byte lnz = (byte)(leftMb.NoneZeroAcDcCoeffs & 0x0f); + + for (int y = 0; y < 4; y++) + { + int l = lnz & 1; + uint nzCoeffs = 0; + for (int x = 0; x < 4; x++) + { + int ctx = l + (tnz & 1); + int nz = GetCoeffs(br, acProba, ctx, q.Y1Mat, first, dst.AsSpan(dstOffset)); + l = nz > first ? 1 : 0; + tnz = (byte)((tnz >> 1) | (l << 7)); + nzCoeffs = NzCodeBits(nzCoeffs, nz, dst[dstOffset] != 0 ? 1 : 0); + dstOffset += 16; + } + + tnz >>= 4; + lnz = (byte)((lnz >> 1) | (l << 7)); + nonZeroY = (nonZeroY << 8) | nzCoeffs; + } + + uint outTnz = tnz; + uint outLnz = (uint)(lnz >> 4); + + for (int ch = 0; ch < 4; ch += 2) + { + uint nzCoeffs = 0; + int chPlus4 = 4 + ch; + tnz = (byte)(mb.NoneZeroAcDcCoeffs >> chPlus4); + lnz = (byte)(leftMb.NoneZeroAcDcCoeffs >> chPlus4); + for (int y = 0; y < 2; y++) + { + int l = lnz & 1; + for (int x = 0; x < 2; x++) + { + int ctx = l + (tnz & 1); + int nz = GetCoeffs(br, bands[2], ctx, q.UvMat, 0, dst.AsSpan(dstOffset)); + l = nz > 0 ? 1 : 0; + tnz = (byte)((tnz >> 1) | (l << 3)); + nzCoeffs = NzCodeBits(nzCoeffs, nz, dst[dstOffset] != 0 ? 1 : 0); + dstOffset += 16; + } + + tnz >>= 2; + lnz = (byte)((lnz >> 1) | (l << 5)); + } + + // Note: we don't really need the per-4x4 details for U/V blocks. + nonZeroUv |= nzCoeffs << (4 * ch); + outTnz |= (uint)(tnz << 4 << ch); + outLnz |= (uint)((lnz & 0xf0) << ch); + } + + mb.NoneZeroAcDcCoeffs = outTnz; + leftMb.NoneZeroAcDcCoeffs = outLnz; + + block.NonZeroY = nonZeroY; + block.NonZeroUv = nonZeroUv; + + return (nonZeroY | nonZeroUv) == 0; + } + + private static int GetCoeffs(Vp8BitReader br, Vp8BandProbas[] prob, int ctx, int[] dq, int n, Span coeffs) + { + // Returns the position of the last non-zero coeff plus one. + Vp8ProbaArray p = prob[n].Probabilities[ctx]; + for (; n < 16; ++n) + { + if (br.GetBit(p.Probabilities[0]) == 0) + { + // Previous coeff was last non-zero coeff. + return n; + } + + // Sequence of zero coeffs. + while (br.GetBit(p.Probabilities[1]) == 0) + { + p = prob[++n].Probabilities[0]; + if (n == 16) + { + return 16; + } + } + + // Non zero coeffs. + int v; + if (br.GetBit(p.Probabilities[2]) == 0) + { + v = 1; + p = prob[n + 1].Probabilities[1]; + } + else + { + v = GetLargeValue(br, p.Probabilities); + p = prob[n + 1].Probabilities[2]; + } + + int idx = n > 0 ? 1 : 0; + coeffs[WebpConstants.Zigzag[n]] = (short)(br.GetSigned(v) * dq[idx]); + } + + return 16; + } + + private static int GetLargeValue(Vp8BitReader br, byte[] p) + { + // See section 13 - 2: http://tools.ietf.org/html/rfc6386#section-13.2 + int v; + if (br.GetBit(p[3]) == 0) + { + if (br.GetBit(p[4]) == 0) + { + v = 2; + } + else + { + v = 3 + br.GetBit(p[5]); + } + } + else if (br.GetBit(p[6]) == 0) + { + if (br.GetBit(p[7]) == 0) + { + v = 5 + br.GetBit(159); + } + else + { + v = 7 + (2 * br.GetBit(165)); + v += br.GetBit(145); + } + } + else + { + int bit1 = br.GetBit(p[8]); + int bit0 = br.GetBit(p[9 + bit1]); + int cat = (2 * bit1) + bit0; + v = 0; + byte[] tab = null; + switch (cat) + { + case 0: + tab = WebpConstants.Cat3; + break; + case 1: + tab = WebpConstants.Cat4; + break; + case 2: + tab = WebpConstants.Cat5; + break; + case 3: + tab = WebpConstants.Cat6; + break; + default: + WebpThrowHelper.ThrowImageFormatException("VP8 parsing error"); + break; + } + + for (int i = 0; i < tab.Length; i++) + { + v += v + br.GetBit(tab[i]); + } + + v += 3 + (8 << cat); + } + + return v; + } + + private Vp8SegmentHeader ParseSegmentHeader(Vp8Proba proba) + { + Vp8SegmentHeader vp8SegmentHeader = new() + { + UseSegment = this.bitReader.ReadBool() + }; + if (vp8SegmentHeader.UseSegment) + { + vp8SegmentHeader.UpdateMap = this.bitReader.ReadBool(); + bool updateData = this.bitReader.ReadBool(); + if (updateData) + { + vp8SegmentHeader.Delta = this.bitReader.ReadBool(); + bool hasValue; + for (int i = 0; i < vp8SegmentHeader.Quantizer.Length; i++) + { + hasValue = this.bitReader.ReadBool(); + vp8SegmentHeader.Quantizer[i] = (byte)(hasValue ? this.bitReader.ReadSignedValue(7) : 0); + } + + for (int i = 0; i < vp8SegmentHeader.FilterStrength.Length; i++) + { + hasValue = this.bitReader.ReadBool(); + vp8SegmentHeader.FilterStrength[i] = (byte)(hasValue ? this.bitReader.ReadSignedValue(6) : 0); + } + + if (vp8SegmentHeader.UpdateMap) + { + for (int s = 0; s < proba.Segments.Length; ++s) + { + hasValue = this.bitReader.ReadBool(); + proba.Segments[s] = hasValue ? this.bitReader.ReadValue(8) : 255; + } + } + } + } + else + { + vp8SegmentHeader.UpdateMap = false; + } + + return vp8SegmentHeader; + } + + private void ParseFilterHeader(Vp8Decoder dec) + { + Vp8FilterHeader vp8FilterHeader = dec.FilterHeader; + vp8FilterHeader.LoopFilter = this.bitReader.ReadBool() ? LoopFilter.Simple : LoopFilter.Complex; + vp8FilterHeader.FilterLevel = (int)this.bitReader.ReadValue(6); + vp8FilterHeader.Sharpness = (int)this.bitReader.ReadValue(3); + vp8FilterHeader.UseLfDelta = this.bitReader.ReadBool(); + + dec.Filter = vp8FilterHeader.FilterLevel == 0 ? LoopFilter.None : vp8FilterHeader.LoopFilter; + if (vp8FilterHeader.UseLfDelta) + { + // Update lf-delta? + if (this.bitReader.ReadBool()) + { + bool hasValue; + for (int i = 0; i < vp8FilterHeader.RefLfDelta.Length; i++) + { + hasValue = this.bitReader.ReadBool(); + if (hasValue) + { + vp8FilterHeader.RefLfDelta[i] = this.bitReader.ReadSignedValue(6); + } + } + + for (int i = 0; i < vp8FilterHeader.ModeLfDelta.Length; i++) + { + hasValue = this.bitReader.ReadBool(); + if (hasValue) + { + vp8FilterHeader.ModeLfDelta[i] = this.bitReader.ReadSignedValue(6); + } + } + } + } + + int extraRows = WebpConstants.FilterExtraRows[(int)dec.Filter]; + int extraY = extraRows * dec.CacheYStride; + int extraUv = extraRows / 2 * dec.CacheUvStride; + dec.CacheYOffset = extraY; + dec.CacheUvOffset = extraUv; + } + + private void ParsePartitions(Vp8Decoder dec) + { + uint size = this.bitReader.Remaining - this.bitReader.PartitionLength; + int startIdx = (int)this.bitReader.PartitionLength; + Span sz = this.bitReader.Data.Slice(startIdx); + int sizeLeft = (int)size; + dec.NumPartsMinusOne = (1 << (int)this.bitReader.ReadValue(2)) - 1; + int lastPart = dec.NumPartsMinusOne; + + int lastPartMul3 = lastPart * 3; + int partStart = startIdx + lastPartMul3; + sizeLeft -= lastPartMul3; + for (int p = 0; p < lastPart; ++p) + { + int pSize = sz[0] | (sz[1] << 8) | (sz[2] << 16); + if (pSize > sizeLeft) + { + pSize = sizeLeft; + } + + dec.Vp8BitReaders[p] = new Vp8BitReader(this.bitReader.Data, (uint)pSize, partStart); + partStart += pSize; + sizeLeft -= pSize; + sz = sz[3..]; + } + + dec.Vp8BitReaders[lastPart] = new Vp8BitReader(this.bitReader.Data, (uint)sizeLeft, partStart); + } + + private void ParseDequantizationIndices(Vp8Decoder decoder) + { + Vp8SegmentHeader vp8SegmentHeader = decoder.SegmentHeader; + + int baseQ0 = (int)this.bitReader.ReadValue(7); + bool hasValue = this.bitReader.ReadBool(); + int dqy1Dc = hasValue ? this.bitReader.ReadSignedValue(4) : 0; + hasValue = this.bitReader.ReadBool(); + int dqy2Dc = hasValue ? this.bitReader.ReadSignedValue(4) : 0; + hasValue = this.bitReader.ReadBool(); + int dqy2Ac = hasValue ? this.bitReader.ReadSignedValue(4) : 0; + hasValue = this.bitReader.ReadBool(); + int dquvDc = hasValue ? this.bitReader.ReadSignedValue(4) : 0; + hasValue = this.bitReader.ReadBool(); + int dquvAc = hasValue ? this.bitReader.ReadSignedValue(4) : 0; + for (int i = 0; i < WebpConstants.NumMbSegments; i++) + { + int q; + if (vp8SegmentHeader.UseSegment) + { + q = vp8SegmentHeader.Quantizer[i]; + if (!vp8SegmentHeader.Delta) + { + q += baseQ0; + } + } + else + { + if (i > 0) + { + decoder.DeQuantMatrices[i] = decoder.DeQuantMatrices[0]; + continue; + } + + q = baseQ0; + } + + Vp8QuantMatrix m = decoder.DeQuantMatrices[i]; + m.Y1Mat[0] = WebpLookupTables.DcTable[Clip(q + dqy1Dc, 127)]; + m.Y1Mat[1] = WebpLookupTables.AcTable[Clip(q + 0, 127)]; + m.Y2Mat[0] = WebpLookupTables.DcTable[Clip(q + dqy2Dc, 127)] * 2; + + // For all x in [0..284], x*155/100 is bitwise equal to (x*101581) >> 16. + // The smallest precision for that is '(x*6349) >> 12' but 16 is a good word size. + m.Y2Mat[1] = (WebpLookupTables.AcTable[Clip(q + dqy2Ac, 127)] * 101581) >> 16; + if (m.Y2Mat[1] < 8) + { + m.Y2Mat[1] = 8; + } + + m.UvMat[0] = WebpLookupTables.DcTable[Clip(q + dquvDc, 117)]; + m.UvMat[1] = WebpLookupTables.AcTable[Clip(q + dquvAc, 127)]; + + // For dithering strength evaluation. + m.UvQuant = q + dquvAc; + } + } + + private void ParseProbabilities(Vp8Decoder dec) + { + Vp8Proba proba = dec.Probabilities; + + for (int t = 0; t < WebpConstants.NumTypes; ++t) + { + for (int b = 0; b < WebpConstants.NumBands; ++b) + { + for (int c = 0; c < WebpConstants.NumCtx; ++c) + { + for (int p = 0; p < WebpConstants.NumProbas; ++p) + { + byte prob = WebpLookupTables.CoeffsUpdateProba[t, b, c, p]; + proba.Bands[t, b].Probabilities[c].Probabilities[p] = (byte)(this.bitReader.GetBit(prob) != 0 + ? this.bitReader.ReadValue(8) + : WebpLookupTables.DefaultCoeffsProba[t, b, c, p]); + } + } + } + + for (int b = 0; b < 16 + 1; ++b) + { + proba.BandsPtr[t][b] = proba.Bands[t, WebpConstants.Vp8EncBands[b]]; + } + } + + dec.UseSkipProbability = this.bitReader.ReadBool(); + if (dec.UseSkipProbability) + { + dec.SkipProbability = (byte)this.bitReader.ReadValue(8); + } + } + + private static Vp8Io InitializeVp8Io(Vp8Decoder dec, Vp8PictureHeader pictureHeader) + { + Vp8Io io = default; + io.Width = (int)pictureHeader.Width; + io.Height = (int)pictureHeader.Height; + io.UseScaling = false; + io.ScaledWidth = io.Width; + io.ScaledHeight = io.ScaledHeight; + io.MbW = io.Width; + io.MbH = io.Height; + uint strideLength = (pictureHeader.Width + 15) >> 4; + io.YStride = (int)(16 * strideLength); + io.UvStride = (int)(8 * strideLength); + + int intraPredModeSize = 4 * dec.MbWidth; + dec.IntraT = new byte[intraPredModeSize]; + + int extraPixels = WebpConstants.FilterExtraRows[(int)dec.Filter]; + if (dec.Filter == LoopFilter.Complex) + { + // For complex filter, we need to preserve the dependency chain. + dec.TopLeftMbX = 0; + dec.TopLeftMbY = 0; + } + else + { + // For simple filter, we include 'extraPixels' on the other side of the boundary, + // since vertical or horizontal filtering of the previous macroblock can modify some abutting pixels. + int extraShift4 = -extraPixels >> 4; + dec.TopLeftMbX = extraShift4; + dec.TopLeftMbY = extraShift4; + if (dec.TopLeftMbX < 0) + { + dec.TopLeftMbX = 0; + } + + if (dec.TopLeftMbY < 0) + { + dec.TopLeftMbY = 0; + } + } + + // We need some 'extra' pixels on the right/bottom. + dec.BottomRightMbY = (io.Height + 15 + extraPixels) >> 4; + dec.BottomRightMbX = (io.Width + 15 + extraPixels) >> 4; + if (dec.BottomRightMbX > dec.MbWidth) + { + dec.BottomRightMbX = dec.MbWidth; + } + + if (dec.BottomRightMbY > dec.MbHeight) + { + dec.BottomRightMbY = dec.MbHeight; + } + + return io; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint NzCodeBits(uint nzCoeffs, int nz, int dcNz) + { + nzCoeffs <<= 2; + nzCoeffs |= nz switch + { + > 3 => 3, + > 1 => 2, + _ => (uint)dcNz + }; + + return nzCoeffs; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int CheckMode(int mbx, int mby, int mode) + { + // B_DC_PRED + if (mode == 0) + { + if (mbx == 0) + { + return mby == 0 + ? 6 // B_DC_PRED_NOTOPLEFT + : 5; // B_DC_PRED_NOLEFT + } + + return mby == 0 + ? 4 // B_DC_PRED_NOTOP + : 0; // B_DC_PRED + } + + return mode; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Clip(int value, int max) => Math.Clamp(value, 0, max); + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs b/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs new file mode 100644 index 0000000..11099e5 --- /dev/null +++ b/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs @@ -0,0 +1,757 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy { + internal static class YuvConversion + { + /// + /// Fixed-point precision for RGB->YUV. + /// + private const int YuvFix = 16; + + private const int YuvHalf = 1 << (YuvFix - 1); + + // UpSample from YUV to RGB. + // Given samples laid out in a square as: + // [a b] + // [c d] + // we interpolate u/v as: + // ([9*a + 3*b + 3*c + d 3*a + 9*b + 3*c + d] + [8 8]) / 16 + // ([3*a + b + 9*c + 3*d a + 3*b + 3*c + 9*d] [8 8]) / 16 + public static void UpSample(Span topY, Span bottomY, Span topU, Span topV, Span curU, Span curV, Span topDst, Span bottomDst, int len, byte[] uvBuffer) + { + if (Vector128.IsHardwareAccelerated) + { + UpSampleVector128(topY, bottomY, topU, topV, curU, curV, topDst, bottomDst, len, uvBuffer); + } + else + { + UpSampleScalar(topY, bottomY, topU, topV, curU, curV, topDst, bottomDst, len); + } + } + + private static void UpSampleScalar(Span topY, Span bottomY, Span topU, Span topV, Span curU, Span curV, Span topDst, Span bottomDst, int len) + { + const int xStep = 3; + int lastPixelPair = (len - 1) >> 1; + uint tluv = LoadUv(topU[0], topV[0]); // top-left sample + uint luv = LoadUv(curU[0], curV[0]); // left-sample + uint uv0 = ((3 * tluv) + luv + 0x00020002u) >> 2; + YuvToBgr(topY[0], (int)(uv0 & 0xff), (int)(uv0 >> 16), topDst); + + if (!bottomY.IsEmpty) + { + uv0 = ((3 * luv) + tluv + 0x00020002u) >> 2; + YuvToBgr(bottomY[0], (int)uv0 & 0xff, (int)(uv0 >> 16), bottomDst); + } + + for (int x = 1; x <= lastPixelPair; x++) + { + uint tuv = LoadUv(topU[x], topV[x]); // top sample + uint uv = LoadUv(curU[x], curV[x]); // sample + + // Precompute invariant values associated with first and second diagonals. + uint avg = tluv + tuv + luv + uv + 0x00080008u; + uint diag12 = (avg + (2 * (tuv + luv))) >> 3; + uint diag03 = (avg + (2 * (tluv + uv))) >> 3; + uv0 = (diag12 + tluv) >> 1; + uint uv1 = (diag03 + tuv) >> 1; + int xMul2 = x * 2; + YuvToBgr(topY[xMul2 - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), topDst[((xMul2 - 1) * xStep)..]); + YuvToBgr(topY[xMul2 - 0], (int)(uv1 & 0xff), (int)(uv1 >> 16), topDst[((xMul2 - 0) * xStep)..]); + + if (!bottomY.IsEmpty) + { + uv0 = (diag03 + luv) >> 1; + uv1 = (diag12 + uv) >> 1; + YuvToBgr(bottomY[xMul2 - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), bottomDst[((xMul2 - 1) * xStep)..]); + YuvToBgr(bottomY[xMul2 + 0], (int)(uv1 & 0xff), (int)(uv1 >> 16), bottomDst[((xMul2 + 0) * xStep)..]); + } + + tluv = tuv; + luv = uv; + } + + if ((len & 1) == 0) + { + uv0 = ((3 * tluv) + luv + 0x00020002u) >> 2; + YuvToBgr(topY[len - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), topDst[((len - 1) * xStep)..]); + if (!bottomY.IsEmpty) + { + uv0 = ((3 * luv) + tluv + 0x00020002u) >> 2; + YuvToBgr(bottomY[len - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), bottomDst[((len - 1) * xStep)..]); + } + } + } + + // We compute (9*a + 3*b + 3*c + d + 8) / 16 as follows + // u = (9*a + 3*b + 3*c + d + 8) / 16 + // = (a + (a + 3*b + 3*c + d) / 8 + 1) / 2 + // = (a + m + 1) / 2 + // where m = (a + 3*b + 3*c + d) / 8 + // = ((a + b + c + d) / 2 + b + c) / 4 + // + // Let's say k = (a + b + c + d) / 4. + // We can compute k as + // k = (s + t + 1) / 2 - ((a^d) | (b^c) | (s^t)) & 1 + // where s = (a + d + 1) / 2 and t = (b + c + 1) / 2 + // + // Then m can be written as + // m = (k + t + 1) / 2 - (((b^c) & (s^t)) | (k^t)) & 1 + private static void UpSampleVector128(Span topY, Span bottomY, Span topU, Span topV, Span curU, Span curV, Span topDst, Span bottomDst, int len, byte[] uvBuffer) + { + const int xStep = 3; + Array.Clear(uvBuffer); + Span ru = uvBuffer.AsSpan(15); + Span rv = ru[32..]; + + // Treat the first pixel in regular way. + int uDiag = ((topU[0] + curU[0]) >> 1) + 1; + int vDiag = ((topV[0] + curV[0]) >> 1) + 1; + int u0t = (topU[0] + uDiag) >> 1; + int v0t = (topV[0] + vDiag) >> 1; + YuvToBgr(topY[0], u0t, v0t, topDst); + if (!bottomY.IsEmpty) + { + int u0b = (curU[0] + uDiag) >> 1; + int v0b = (curV[0] + vDiag) >> 1; + YuvToBgr(bottomY[0], u0b, v0b, bottomDst); + } + + // For UpSample32Pixels, 17 u/v values must be read-able for each block. + int pos; + int uvPos; + ref byte topURef = ref MemoryMarshal.GetReference(topU); + ref byte topVRef = ref MemoryMarshal.GetReference(topV); + ref byte curURef = ref MemoryMarshal.GetReference(curU); + ref byte curVRef = ref MemoryMarshal.GetReference(curV); + if (!bottomY.IsEmpty) + { + for (pos = 1, uvPos = 0; pos + 32 + 1 <= len; pos += 32, uvPos += 16) + { + UpSample32PixelsVector128(ref Unsafe.Add(ref topURef, (uint)uvPos), ref Unsafe.Add(ref curURef, (uint)uvPos), ru); + UpSample32PixelsVector128(ref Unsafe.Add(ref topVRef, (uint)uvPos), ref Unsafe.Add(ref curVRef, (uint)uvPos), rv); + ConvertYuvToBgrWithBottomYVector128(topY, bottomY, topDst, bottomDst, ru, rv, pos, xStep); + } + } + else + { + for (pos = 1, uvPos = 0; pos + 32 + 1 <= len; pos += 32, uvPos += 16) + { + UpSample32PixelsVector128(ref Unsafe.Add(ref topURef, (uint)uvPos), ref Unsafe.Add(ref curURef, (uint)uvPos), ru); + UpSample32PixelsVector128(ref Unsafe.Add(ref topVRef, (uint)uvPos), ref Unsafe.Add(ref curVRef, (uint)uvPos), rv); + ConvertYuvToBgrVector128(topY, topDst, ru, rv, pos, xStep); + } + } + + // Process last block. + if (len > 1) + { + int leftOver = ((len + 1) >> 1) - (pos >> 1); + Span tmpTopDst = ru[(4 * 32)..]; + Span tmpBottomDst = tmpTopDst[(4 * 32)..]; + Span tmpTop = tmpBottomDst[(4 * 32)..]; + Span tmpBottom = bottomY.IsEmpty ? null : tmpTop[32..]; + UpSampleLastBlockVector128(topU[uvPos..], curU[uvPos..], leftOver, ru); + UpSampleLastBlockVector128(topV[uvPos..], curV[uvPos..], leftOver, rv); + + topY[pos..len].CopyTo(tmpTop); + if (!bottomY.IsEmpty) + { + bottomY[pos..len].CopyTo(tmpBottom); + ConvertYuvToBgrWithBottomYVector128(tmpTop, tmpBottom, tmpTopDst, tmpBottomDst, ru, rv, 0, xStep); + } + else + { + ConvertYuvToBgrVector128(tmpTop, tmpTopDst, ru, rv, 0, xStep); + } + + tmpTopDst[..((len - pos) * xStep)].CopyTo(topDst[(pos * xStep)..]); + if (!bottomY.IsEmpty) + { + tmpBottomDst[..((len - pos) * xStep)].CopyTo(bottomDst[(pos * xStep)..]); + } + } + } + + // Loads 17 pixels each from rows r1 and r2 and generates 32 pixels. + private static void UpSample32PixelsVector128(ref byte r1, ref byte r2, Span output) + { + // Load inputs. + Vector128 a = Unsafe.As>(ref r1); + Vector128 b = Unsafe.As>(ref Unsafe.Add(ref r1, 1)); + Vector128 c = Unsafe.As>(ref r2); + Vector128 d = Unsafe.As>(ref Unsafe.Add(ref r2, 1)); + + Vector128 s = Vector128_.Average(a, d); // s = (a + d + 1) / 2 + Vector128 t = Vector128_.Average(b, c); // t = (b + c + 1) / 2 + Vector128 st = s ^ t; // st = s^t + + Vector128 ad = a ^ d; // ad = a^d + Vector128 bc = b ^ c; // bc = b^c + + Vector128 t1 = ad | bc; // (a^d) | (b^c) + Vector128 t2 = t1 | st; // (a^d) | (b^c) | (s^t) + Vector128 t3 = t2 & Vector128.Create((byte)1); // (a^d) | (b^c) | (s^t) & 1 + Vector128 t4 = Vector128_.Average(s, t); + Vector128 k = t4 - t3; // k = (a + b + c + d) / 4 + + Vector128 diag1 = GetMVector128(k, st, bc, t); + Vector128 diag2 = GetMVector128(k, st, ad, s); + + // Pack the alternate pixels. + PackAndStoreVector128(a, b, diag1, diag2, output); // store top. + PackAndStoreVector128(c, d, diag2, diag1, output[(2 * 32)..]); + } + + private static void UpSampleLastBlockVector128(Span tb, Span bb, int numPixels, Span output) + { + Span r1 = stackalloc byte[17]; + Span r2 = stackalloc byte[17]; + tb[..numPixels].CopyTo(r1); + bb[..numPixels].CopyTo(r2); + + // Replicate last byte. + int length = 17 - numPixels; + if (length > 0) + { + r1.Slice(numPixels, length).Fill(r1[numPixels - 1]); + r2.Slice(numPixels, length).Fill(r2[numPixels - 1]); + } + + ref byte r1Ref = ref MemoryMarshal.GetReference(r1); + ref byte r2Ref = ref MemoryMarshal.GetReference(r2); + UpSample32PixelsVector128(ref r1Ref, ref r2Ref, output); + } + + // Computes out = (k + in + 1) / 2 - ((ij & (s^t)) | (k^in)) & 1 + private static Vector128 GetMVector128(Vector128 k, Vector128 st, Vector128 ij, Vector128 input) + { + Vector128 tmp0 = Vector128_.Average(k, input); // (k + in + 1) / 2 + Vector128 tmp1 = ij & st; // (ij) & (s^t) + Vector128 tmp2 = k ^ input; // (k^in) + Vector128 tmp3 = tmp1 | tmp2; // ((ij) & (s^t)) | (k^in) + Vector128 tmp4 = tmp3 & Vector128.Create((byte)1); // & 1 -> lsb_correction + + return tmp0 - tmp4; // (k + in + 1) / 2 - lsb_correction + } + + private static void PackAndStoreVector128(Vector128 a, Vector128 b, Vector128 da, Vector128 db, Span output) + { + Vector128 ta = Vector128_.Average(a, da); // (9a + 3b + 3c + d + 8) / 16 + Vector128 tb = Vector128_.Average(b, db); // (3a + 9b + c + 3d + 8) / 16 + Vector128 t1 = Vector128_.UnpackLow(ta, tb); + Vector128 t2 = Vector128_.UnpackHigh(ta, tb); + + ref byte output0Ref = ref MemoryMarshal.GetReference(output); + ref byte output1Ref = ref Unsafe.Add(ref output0Ref, 16); + Unsafe.As>(ref output0Ref) = t1; + Unsafe.As>(ref output1Ref) = t2; + } + + /// + /// Converts the pixel values of the image to YUV. + /// + /// The pixel type of the image. + /// The frame to convert. + /// The global configuration. + /// The memory allocator. + /// Span to store the luma component of the image. + /// Span to store the u component of the image. + /// Span to store the v component of the image. + /// true, if the image contains alpha data. + public static bool ConvertRgbToYuv(Buffer2DRegion frame, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v) + where TPixel : unmanaged, IPixel + { + int width = frame.Width; + int height = frame.Height; + int uvWidth = (width + 1) >> 1; + + // Temporary storage for accumulated R/G/B values during conversion to U/V. + using IMemoryOwner tmpRgb = memoryAllocator.Allocate(4 * uvWidth); + using IMemoryOwner bgraRow0Buffer = memoryAllocator.Allocate(width); + using IMemoryOwner bgraRow1Buffer = memoryAllocator.Allocate(width); + Span tmpRgbSpan = tmpRgb.GetSpan(); + Span bgraRow0 = bgraRow0Buffer.GetSpan(); + Span bgraRow1 = bgraRow1Buffer.GetSpan(); + int uvRowIndex = 0; + int rowIndex; + bool hasAlpha = false; + for (rowIndex = 0; rowIndex < height - 1; rowIndex += 2) + { + Span rowSpan = frame.DangerousGetRowSpan(rowIndex); + Span nextRowSpan = frame.DangerousGetRowSpan(rowIndex + 1); + PixelOperations.Instance.ToBgra32(configuration, rowSpan, bgraRow0); + PixelOperations.Instance.ToBgra32(configuration, nextRowSpan, bgraRow1); + + bool rowsHaveAlpha = WebpCommonUtils.CheckNonOpaque(bgraRow0) && WebpCommonUtils.CheckNonOpaque(bgraRow1); + if (rowsHaveAlpha) + { + hasAlpha = true; + } + + // Downsample U/V planes, two rows at a time. + if (!rowsHaveAlpha) + { + AccumulateRgb(bgraRow0, bgraRow1, tmpRgbSpan, width); + } + else + { + AccumulateRgba(bgraRow0, bgraRow1, tmpRgbSpan, width); + } + + ConvertRgbaToUv(tmpRgbSpan, u[(uvRowIndex * uvWidth)..], v[(uvRowIndex * uvWidth)..], uvWidth); + uvRowIndex++; + + ConvertRgbaToY(bgraRow0, y[(rowIndex * width)..], width); + ConvertRgbaToY(bgraRow1, y[((rowIndex + 1) * width)..], width); + } + + // Extra last row. + if ((height & 1) != 0) + { + Span rowSpan = frame.DangerousGetRowSpan(rowIndex); + PixelOperations.Instance.ToBgra32(configuration, rowSpan, bgraRow0); + ConvertRgbaToY(bgraRow0, y[(rowIndex * width)..], width); + + if (!WebpCommonUtils.CheckNonOpaque(bgraRow0)) + { + AccumulateRgb(bgraRow0, bgraRow0, tmpRgbSpan, width); + } + else + { + AccumulateRgba(bgraRow0, bgraRow0, tmpRgbSpan, width); + hasAlpha = true; + } + + ConvertRgbaToUv(tmpRgbSpan, u[(uvRowIndex * uvWidth)..], v[(uvRowIndex * uvWidth)..], uvWidth); + } + + return hasAlpha; + } + + /// + /// Converts a rgba pixel row to Y. + /// + /// The row span to convert. + /// The destination span for y. + /// The width. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ConvertRgbaToY(Span rowSpan, Span y, int width) + { + for (int x = 0; x < width; x++) + { + y[x] = (byte)RgbToY(rowSpan[x].R, rowSpan[x].G, rowSpan[x].B, YuvHalf); + } + } + + /// + /// Converts a rgb row of pixels to UV. + /// + /// The RGB pixel row. + /// The destination span for u. + /// The destination span for v. + /// The width. + public static void ConvertRgbaToUv(Span rgb, Span u, Span v, int width) + { + int rgbIdx = 0; + for (int i = 0; i < width; i += 1, rgbIdx += 4) + { + int r = rgb[rgbIdx], g = rgb[rgbIdx + 1], b = rgb[rgbIdx + 2]; + u[i] = (byte)RgbToU(r, g, b, YuvHalf << 2); + v[i] = (byte)RgbToV(r, g, b, YuvHalf << 2); + } + } + + public static void AccumulateRgb(Span rowSpan, Span nextRowSpan, Span dst, int width) + { + Bgra32 bgra0; + Bgra32 bgra1; + int i, j; + int dstIdx = 0; + for (i = 0, j = 0; i < (width >> 1); i += 1, j += 2, dstIdx += 4) + { + bgra0 = rowSpan[j]; + bgra1 = rowSpan[j + 1]; + Bgra32 bgra2 = nextRowSpan[j]; + Bgra32 bgra3 = nextRowSpan[j + 1]; + + dst[dstIdx] = (ushort)LinearToGamma( + GammaToLinear(bgra0.R) + + GammaToLinear(bgra1.R) + + GammaToLinear(bgra2.R) + + GammaToLinear(bgra3.R), + 0); + dst[dstIdx + 1] = (ushort)LinearToGamma( + GammaToLinear(bgra0.G) + + GammaToLinear(bgra1.G) + + GammaToLinear(bgra2.G) + + GammaToLinear(bgra3.G), + 0); + dst[dstIdx + 2] = (ushort)LinearToGamma( + GammaToLinear(bgra0.B) + + GammaToLinear(bgra1.B) + + GammaToLinear(bgra2.B) + + GammaToLinear(bgra3.B), + 0); + } + + if ((width & 1) != 0) + { + bgra0 = rowSpan[j]; + bgra1 = nextRowSpan[j]; + + dst[dstIdx] = (ushort)LinearToGamma(GammaToLinear(bgra0.R) + GammaToLinear(bgra1.R), 1); + dst[dstIdx + 1] = (ushort)LinearToGamma(GammaToLinear(bgra0.G) + GammaToLinear(bgra1.G), 1); + dst[dstIdx + 2] = (ushort)LinearToGamma(GammaToLinear(bgra0.B) + GammaToLinear(bgra1.B), 1); + } + } + + public static void AccumulateRgba(Span rowSpan, Span nextRowSpan, Span dst, int width) + { + Bgra32 bgra0; + Bgra32 bgra1; + int i, j; + int dstIdx = 0; + for (i = 0, j = 0; i < width >> 1; i += 1, j += 2, dstIdx += 4) + { + bgra0 = rowSpan[j]; + bgra1 = rowSpan[j + 1]; + Bgra32 bgra2 = nextRowSpan[j]; + Bgra32 bgra3 = nextRowSpan[j + 1]; + uint a = (uint)(bgra0.A + bgra1.A + bgra2.A + bgra3.A); + int r, g, b; + if (a is 4 * 0xff or 0) + { + r = (ushort)LinearToGamma( + GammaToLinear(bgra0.R) + + GammaToLinear(bgra1.R) + + GammaToLinear(bgra2.R) + + GammaToLinear(bgra3.R), + 0); + g = (ushort)LinearToGamma( + GammaToLinear(bgra0.G) + + GammaToLinear(bgra1.G) + + GammaToLinear(bgra2.G) + + GammaToLinear(bgra3.G), + 0); + b = (ushort)LinearToGamma( + GammaToLinear(bgra0.B) + + GammaToLinear(bgra1.B) + + GammaToLinear(bgra2.B) + + GammaToLinear(bgra3.B), + 0); + } + else + { + r = LinearToGammaWeighted(bgra0.R, bgra1.R, bgra2.R, bgra3.R, bgra0.A, bgra1.A, bgra2.A, bgra3.A, a); + g = LinearToGammaWeighted(bgra0.G, bgra1.G, bgra2.G, bgra3.G, bgra0.A, bgra1.A, bgra2.A, bgra3.A, a); + b = LinearToGammaWeighted(bgra0.B, bgra1.B, bgra2.B, bgra3.B, bgra0.A, bgra1.A, bgra2.A, bgra3.A, a); + } + + dst[dstIdx] = (ushort)r; + dst[dstIdx + 1] = (ushort)g; + dst[dstIdx + 2] = (ushort)b; + dst[dstIdx + 3] = (ushort)a; + } + + if ((width & 1) != 0) + { + bgra0 = rowSpan[j]; + bgra1 = nextRowSpan[j]; + uint a = (uint)(2u * (bgra0.A + bgra1.A)); + int r, g, b; + if (a is 4 * 0xff or 0) + { + r = (ushort)LinearToGamma(GammaToLinear(bgra0.R) + GammaToLinear(bgra1.R), 1); + g = (ushort)LinearToGamma(GammaToLinear(bgra0.G) + GammaToLinear(bgra1.G), 1); + b = (ushort)LinearToGamma(GammaToLinear(bgra0.B) + GammaToLinear(bgra1.B), 1); + } + else + { + r = LinearToGammaWeighted(bgra0.R, bgra1.R, bgra0.R, bgra1.R, bgra0.A, bgra1.A, bgra0.A, bgra1.A, a); + g = LinearToGammaWeighted(bgra0.G, bgra1.G, bgra0.G, bgra1.G, bgra0.A, bgra1.A, bgra0.A, bgra1.A, a); + b = LinearToGammaWeighted(bgra0.B, bgra1.B, bgra0.B, bgra1.B, bgra0.A, bgra1.A, bgra0.A, bgra1.A, a); + } + + dst[dstIdx] = (ushort)r; + dst[dstIdx + 1] = (ushort)g; + dst[dstIdx + 2] = (ushort)b; + dst[dstIdx + 3] = (ushort)a; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int LinearToGammaWeighted(byte rgb0, byte rgb1, byte rgb2, byte rgb3, byte a0, byte a1, byte a2, byte a3, uint totalA) + { + uint sum = (a0 * GammaToLinear(rgb0)) + (a1 * GammaToLinear(rgb1)) + (a2 * GammaToLinear(rgb2)) + (a3 * GammaToLinear(rgb3)); + return LinearToGamma((sum * WebpLookupTables.InvAlpha[totalA]) >> (WebpConstants.AlphaFix - 2), 0); + } + + // Convert a linear value 'v' to YUV_FIX+2 fixed-point precision + // U/V value, suitable for RGBToU/V calls. + [MethodImpl(InliningOptions.ShortMethod)] + private static int LinearToGamma(uint baseValue, int shift) + { + int y = Interpolate((int)(baseValue << shift)); // Final uplifted value. + return (y + WebpConstants.GammaTabRounder) >> WebpConstants.GammaTabFix; // Descale. + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint GammaToLinear(byte v) => WebpLookupTables.GammaToLinearTab[v]; + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Interpolate(int v) + { + int tabPos = v >> (WebpConstants.GammaTabFix + 2); // integer part. + int x = v & ((WebpConstants.GammaTabScale << 2) - 1); // fractional part. + int v0 = WebpLookupTables.LinearToGammaTab[tabPos]; + int v1 = WebpLookupTables.LinearToGammaTab[tabPos + 1]; + int y = (v1 * x) + (v0 * ((WebpConstants.GammaTabScale << 2) - x)); // interpolate + + return y; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int RgbToY(byte r, byte g, byte b, int rounding) + { + int luma = (16839 * r) + (33059 * g) + (6420 * b); + return (luma + rounding + (16 << YuvFix)) >> YuvFix; // No need to clip. + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int RgbToU(int r, int g, int b, int rounding) + { + int u = (-9719 * r) - (19081 * g) + (28800 * b); + return ClipUv(u, rounding); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int RgbToV(int r, int g, int b, int rounding) + { + int v = (+28800 * r) - (24116 * g) - (4684 * b); + return ClipUv(v, rounding); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int ClipUv(int uv, int rounding) + { + uv = (uv + rounding + (128 << (YuvFix + 2))) >> (YuvFix + 2); + return (uv & ~0xff) == 0 ? uv : uv < 0 ? 0 : 255; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint LoadUv(byte u, byte v) => + (uint)(u | (v << 16)); // We process u and v together stashed into 32bit(16bit each). + + [MethodImpl(InliningOptions.ShortMethod)] + public static void YuvToBgr(int y, int u, int v, Span bgr) + { + bgr[2] = (byte)YuvToR(y, v); + bgr[1] = (byte)YuvToG(y, u, v); + bgr[0] = (byte)YuvToB(y, u); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void ConvertYuvToBgrVector128(Span topY, Span topDst, Span ru, Span rv, int curX, int step) + => YuvToBgrVector128(topY[curX..], ru, rv, topDst[(curX * step)..]); + + [MethodImpl(InliningOptions.ShortMethod)] + private static void ConvertYuvToBgrWithBottomYVector128(Span topY, Span bottomY, Span topDst, Span bottomDst, Span ru, Span rv, int curX, int step) + { + YuvToBgrVector128(topY[curX..], ru, rv, topDst[(curX * step)..]); + YuvToBgrVector128(bottomY[curX..], ru[64..], rv[64..], bottomDst[(curX * step)..]); + } + + private static void YuvToBgrVector128(Span y, Span u, Span v, Span dst) + { + ref byte yRef = ref MemoryMarshal.GetReference(y); + ref byte uRef = ref MemoryMarshal.GetReference(u); + ref byte vRef = ref MemoryMarshal.GetReference(v); + ConvertYuv444ToBgrVector128(ref yRef, ref uRef, ref vRef, out Vector128 r0, out Vector128 g0, out Vector128 b0); + ConvertYuv444ToBgrVector128(ref Unsafe.Add(ref yRef, 8), ref Unsafe.Add(ref uRef, 8), ref Unsafe.Add(ref vRef, 8), out Vector128 r1, out Vector128 g1, out Vector128 b1); + ConvertYuv444ToBgrVector128(ref Unsafe.Add(ref yRef, 16), ref Unsafe.Add(ref uRef, 16), ref Unsafe.Add(ref vRef, 16), out Vector128 r2, out Vector128 g2, out Vector128 b2); + ConvertYuv444ToBgrVector128(ref Unsafe.Add(ref yRef, 24), ref Unsafe.Add(ref uRef, 24), ref Unsafe.Add(ref vRef, 24), out Vector128 r3, out Vector128 g3, out Vector128 b3); + + // Cast to 8b and store as BBBBGGGGRRRR. + Vector128 bgr0 = Vector128_.PackUnsignedSaturate(b0, b1); + Vector128 bgr1 = Vector128_.PackUnsignedSaturate(b2, b3); + Vector128 bgr2 = Vector128_.PackUnsignedSaturate(g0, g1); + Vector128 bgr3 = Vector128_.PackUnsignedSaturate(g2, g3); + Vector128 bgr4 = Vector128_.PackUnsignedSaturate(r0, r1); + Vector128 bgr5 = Vector128_.PackUnsignedSaturate(r2, r3); + + // Pack as BGRBGRBGRBGR. + PlanarTo24bVector128(bgr0, bgr1, bgr2, bgr3, bgr4, bgr5, dst); + } + + // Pack the planar buffers + // rrrr... rrrr... gggg... gggg... bbbb... bbbb.... + // triplet by triplet in the output buffer rgb as rgbrgbrgbrgb ... + private static void PlanarTo24bVector128(Vector128 input0, Vector128 input1, Vector128 input2, Vector128 input3, Vector128 input4, Vector128 input5, Span rgb) + { + // The input is 6 registers of sixteen 8b but for the sake of explanation, + // let's take 6 registers of four 8b values. + // To pack, we will keep taking one every two 8b integer and move it + // around as follows: + // Input: + // r0r1r2r3 | r4r5r6r7 | g0g1g2g3 | g4g5g6g7 | b0b1b2b3 | b4b5b6b7 + // Split the 6 registers in two sets of 3 registers: the first set as the even + // 8b bytes, the second the odd ones: + // r0r2r4r6 | g0g2g4g6 | b0b2b4b6 | r1r3r5r7 | g1g3g5g7 | b1b3b5b7 + // Repeat the same permutations twice more: + // r0r4g0g4 | b0b4r1r5 | g1g5b1b5 | r2r6g2g6 | b2b6r3r7 | g3g7b3b7 + // r0g0b0r1 | g1b1r2g2 | b2r3g3b3 | r4g4b4r5 | g5b5r6g6 | b6r7g7b7 + + // Process R. + ChannelMixingVector128( + input0, + input1, + Vector128.Create(0, 255, 255, 1, 255, 255, 2, 255, 255, 3, 255, 255, 4, 255, 255, 5), // PlanarTo24Shuffle0 + Vector128.Create(255, 255, 6, 255, 255, 7, 255, 255, 8, 255, 255, 9, 255, 255, 10, 255), // PlanarTo24Shuffle1 + Vector128.Create(255, 11, 255, 255, 12, 255, 255, 13, 255, 255, 14, 255, 255, 15, 255, 255), // PlanarTo24Shuffle2 + out Vector128 r0, + out Vector128 r1, + out Vector128 r2, + out Vector128 r3, + out Vector128 r4, + out Vector128 r5); + + // Process G. + // Same as before, just shifted to the left by one and including the right padding. + ChannelMixingVector128( + input2, + input3, + Vector128.Create(255, 0, 255, 255, 1, 255, 255, 2, 255, 255, 3, 255, 255, 4, 255, 255), // PlanarTo24Shuffle3 + Vector128.Create(5, 255, 255, 6, 255, 255, 7, 255, 255, 8, 255, 255, 9, 255, 255, 10), // PlanarTo24Shuffle4 + Vector128.Create(255, 255, 11, 255, 255, 12, 255, 255, 13, 255, 255, 14, 255, 255, 15, 255), // PlanarTo24Shuffle5 + out Vector128 g0, + out Vector128 g1, + out Vector128 g2, + out Vector128 g3, + out Vector128 g4, + out Vector128 g5); + + // Process B. + ChannelMixingVector128( + input4, + input5, + Vector128.Create(255, 255, 0, 255, 255, 1, 255, 255, 2, 255, 255, 3, 255, 255, 4, 255), // PlanarTo24Shuffle6 + Vector128.Create(255, 5, 255, 255, 6, 255, 255, 7, 255, 255, 8, 255, 255, 9, 255, 255), // PlanarTo24Shuffle7 + Vector128.Create(10, 255, 255, 11, 255, 255, 12, 255, 255, 13, 255, 255, 14, 255, 255, 15), // PlanarTo24Shuffle8 + out Vector128 b0, + out Vector128 b1, + out Vector128 b2, + out Vector128 b3, + out Vector128 b4, + out Vector128 b5); + + // OR the different channels. + Vector128 rg0 = r0 | g0; + Vector128 rg1 = r1 | g1; + Vector128 rg2 = r2 | g2; + Vector128 rg3 = r3 | g3; + Vector128 rg4 = r4 | g4; + Vector128 rg5 = r5 | g5; + + ref byte outputRef = ref MemoryMarshal.GetReference(rgb); + Unsafe.As>(ref outputRef) = rg0 | b0; + Unsafe.As>(ref Unsafe.Add(ref outputRef, 16)) = rg1 | b1; + Unsafe.As>(ref Unsafe.Add(ref outputRef, 32)) = rg2 | b2; + Unsafe.As>(ref Unsafe.Add(ref outputRef, 48)) = rg3 | b3; + Unsafe.As>(ref Unsafe.Add(ref outputRef, 64)) = rg4 | b4; + Unsafe.As>(ref Unsafe.Add(ref outputRef, 80)) = rg5 | b5; + } + + // Shuffles the input buffer as A0 0 0 A1 0 0 A2 + private static void ChannelMixingVector128( + Vector128 input0, + Vector128 input1, + Vector128 shuffle0, + Vector128 shuffle1, + Vector128 shuffle2, + out Vector128 output0, + out Vector128 output1, + out Vector128 output2, + out Vector128 output3, + out Vector128 output4, + out Vector128 output5) + { + output0 = Vector128_.ShuffleNative(input0, shuffle0); + output1 = Vector128_.ShuffleNative(input0, shuffle1); + output2 = Vector128_.ShuffleNative(input0, shuffle2); + output3 = Vector128_.ShuffleNative(input1, shuffle0); + output4 = Vector128_.ShuffleNative(input1, shuffle1); + output5 = Vector128_.ShuffleNative(input1, shuffle2); + } + + // Convert 32 samples of YUV444 to B/G/R + private static void ConvertYuv444ToBgrVector128(ref byte y, ref byte u, ref byte v, out Vector128 r, out Vector128 g, out Vector128 b) + { + // Load the bytes into the *upper* part of 16b words. That's "<< 8", basically. + Vector128 y0 = Unsafe.As>(ref y); + Vector128 u0 = Unsafe.As>(ref u); + Vector128 v0 = Unsafe.As>(ref v); + y0 = Vector128_.UnpackLow(Vector128.Zero, y0); + u0 = Vector128_.UnpackLow(Vector128.Zero, u0); + v0 = Vector128_.UnpackLow(Vector128.Zero, v0); + + // These constants are 14b fixed-point version of ITU-R BT.601 constants. + // R = (19077 * y + 26149 * v - 14234) >> 6 + // G = (19077 * y - 6419 * u - 13320 * v + 8708) >> 6 + // B = (19077 * y + 33050 * u - 17685) >> 6 + Vector128 k19077 = Vector128.Create((ushort)19077); + Vector128 k26149 = Vector128.Create((ushort)26149); + Vector128 k14234 = Vector128.Create((ushort)14234); + + Vector128 y1 = Vector128_.MultiplyHigh(y0.AsUInt16(), k19077); + Vector128 r0 = Vector128_.MultiplyHigh(v0.AsUInt16(), k26149); + Vector128 g0 = Vector128_.MultiplyHigh(u0.AsUInt16(), Vector128.Create((ushort)6419)); + Vector128 g1 = Vector128_.MultiplyHigh(v0.AsUInt16(), Vector128.Create((ushort)13320)); + + Vector128 r1 = y1.AsUInt16() - k14234; + Vector128 r2 = r1 + r0; + + Vector128 g2 = y1.AsUInt16() + Vector128.Create((ushort)8708); + Vector128 g3 = g0 + g1; + Vector128 g4 = g2 - g3; + + Vector128 b0 = Vector128_.MultiplyHigh(u0.AsUInt16(), Vector128.Create(26, 129, 26, 129, 26, 129, 26, 129, 26, 129, 26, 129, 26, 129, 26, 129).AsUInt16()); + Vector128 b1 = Vector128_.AddSaturate(b0, y1); + Vector128 b2 = Vector128_.SubtractSaturate(b1, Vector128.Create((ushort)17685)); + + // Use logical shift for B2, which can be larger than 32767. + r = Vector128.ShiftRightArithmetic(r2.AsInt16(), 6); // range: [-14234, 30815] + g = Vector128.ShiftRightArithmetic(g4.AsInt16(), 6); // range: [-10953, 27710] + b = Vector128.ShiftRightLogical(b2.AsInt16(), 6); // range: [0, 34238] + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static int YuvToB(int y, int u) => Clip8(MultHi(y, 19077) + MultHi(u, 33050) - 17685); + + [MethodImpl(InliningOptions.ShortMethod)] + public static int YuvToG(int y, int u, int v) => Clip8(MultHi(y, 19077) - MultHi(u, 6419) - MultHi(v, 13320) + 8708); + + [MethodImpl(InliningOptions.ShortMethod)] + public static int YuvToR(int y, int v) => Clip8(MultHi(y, 19077) + MultHi(v, 26149) - 14234); + + [MethodImpl(InliningOptions.ShortMethod)] + private static int MultHi(int v, int coeff) => (v * coeff) >> 8; + + [MethodImpl(InliningOptions.ShortMethod)] + private static byte Clip8(int v) + { + const int yuvMask = (256 << 6) - 1; + return (byte)((v & ~yuvMask) == 0 ? v >> 6 : v < 0 ? 0 : 255); + } + } +} diff --git a/ImageSharp/Formats/Webp/Lossy/rfc6386_lossy_specification.pdf b/ImageSharp/Formats/Webp/Lossy/rfc6386_lossy_specification.pdf new file mode 100644 index 0000000..d421b34 Binary files /dev/null and b/ImageSharp/Formats/Webp/Lossy/rfc6386_lossy_specification.pdf differ diff --git a/ImageSharp/Formats/Webp/Readme.md b/ImageSharp/Formats/Webp/Readme.md new file mode 100644 index 0000000..38c1cad --- /dev/null +++ b/ImageSharp/Formats/Webp/Readme.md @@ -0,0 +1,10 @@ +# Webp Format + +Reference implementation, specification and stuff like that: + +- [google webp introduction](https://developers.google.com/speed/webp) +- [Webp Spec 1.0.3](https://chromium.googlesource.com/webm/libwebp/+/v1.0.3/doc/webp-container-spec.txt) +- [Webp VP8 Spec, Lossy](http://tools.ietf.org/html/rfc6386) +- [Webp VP8L Spec, Lossless](https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification) +- [Webp filefront](https://wiki.fileformat.com/image/webp/) +- [Webp test data](https://github.com/webmproject/libwebp-test-data/) diff --git a/ImageSharp/Formats/Webp/RiffHelper.cs b/ImageSharp/Formats/Webp/RiffHelper.cs new file mode 100644 index 0000000..40d5439 --- /dev/null +++ b/ImageSharp/Formats/Webp/RiffHelper.cs @@ -0,0 +1,141 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.IO; +using System.Text; +using SixLabors.ImageSharp.Formats.Webp.Chunks; + +namespace SixLabors.ImageSharp.Formats.Webp { + internal static class RiffHelper + { + /// + /// The header bytes identifying RIFF file. + /// + private const uint RiffFourCc = 0x52_49_46_46; + + public static void WriteRiffFile(Stream stream, string formType, Action func) => + WriteChunk(stream, RiffFourCc, s => + { + s.Write(Encoding.ASCII.GetBytes(formType)); + func(s); + }); + + public static void WriteChunk(Stream stream, uint fourCc, Action func) + { + Span buffer = stackalloc byte[4]; + + // write the fourCC + BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); + stream.Write(buffer); + + long sizePosition = stream.Position; + stream.Position += 4; + + func(stream); + + long position = stream.Position; + + uint dataSize = (uint)(position - sizePosition - 4); + + // padding + if (dataSize % 2 == 1) + { + stream.WriteByte(0); + position++; + } + + BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); + stream.Position = sizePosition; + stream.Write(buffer); + stream.Position = position; + } + + public static void WriteChunk(Stream stream, uint fourCc, ReadOnlySpan data) + { + Span buffer = stackalloc byte[4]; + + // write the fourCC + BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); + stream.Write(buffer); + uint size = (uint)data.Length; + BinaryPrimitives.WriteUInt32LittleEndian(buffer, size); + stream.Write(buffer); + stream.Write(data); + + // padding + if (size % 2 is 1) + { + stream.WriteByte(0); + } + } + + public static unsafe void WriteChunk(Stream stream, uint fourCc, in TStruct chunk) + where TStruct : unmanaged + { + fixed (TStruct* ptr = &chunk) + { + WriteChunk(stream, fourCc, new Span(ptr, sizeof(TStruct))); + } + } + + public static long BeginWriteChunk(Stream stream, uint fourCc) + { + Span buffer = stackalloc byte[4]; + + // write the fourCC + BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); + stream.Write(buffer); + + long sizePosition = stream.Position; + stream.Position += 4; + + return sizePosition; + } + + public static void EndWriteChunk(Stream stream, long sizePosition) + { + Span buffer = stackalloc byte[4]; + + long position = stream.Position; + + uint dataSize = (uint)(position - sizePosition - 4); + + // padding + if (dataSize % 2 is 1) + { + stream.WriteByte(0); + position++; + } + + // Add the size of the encoded file to the Riff header. + BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); + stream.Position = sizePosition; + stream.Write(buffer); + stream.Position = position; + } + + public static long BeginWriteRiffFile(Stream stream, string formType) + { + long sizePosition = BeginWriteChunk(stream, RiffFourCc); + stream.Write(Encoding.ASCII.GetBytes(formType)); + return sizePosition; + } + + public static void EndWriteRiffFile(Stream stream, in WebpVp8X vp8x, bool updateVp8x, long sizePosition) + { + EndWriteChunk(stream, sizePosition + 4); + + // Write the VP8X chunk if necessary. + if (updateVp8x) + { + long position = stream.Position; + + stream.Position = sizePosition + 12; + vp8x.WriteTo(stream); + stream.Position = position; + } + } + } +} diff --git a/ImageSharp/Formats/Webp/WebpAlphaCompressionMethod.cs b/ImageSharp/Formats/Webp/WebpAlphaCompressionMethod.cs new file mode 100644 index 0000000..2c8ced0 --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpAlphaCompressionMethod.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp { + internal enum WebpAlphaCompressionMethod + { + /// + /// No compression. + /// + NoCompression = 0, + + /// + /// Compressed using the Webp lossless format. + /// + WebpLosslessCompression = 1 + } +} diff --git a/ImageSharp/Formats/Webp/WebpAlphaFilterType.cs b/ImageSharp/Formats/Webp/WebpAlphaFilterType.cs new file mode 100644 index 0000000..d7935a6 --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpAlphaFilterType.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Enum for the different alpha filter types. + /// + internal enum WebpAlphaFilterType + { + /// + /// No filtering. + /// + None = 0, + + /// + /// Horizontal filter. + /// + Horizontal = 1, + + /// + /// Vertical filter. + /// + Vertical = 2, + + /// + /// Gradient filter. + /// + Gradient = 3, + } +} diff --git a/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs new file mode 100644 index 0000000..e7738ab --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -0,0 +1,529 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Webp.Chunks; +using SixLabors.ImageSharp.Formats.Webp.Lossless; +using SixLabors.ImageSharp.Formats.Webp.Lossy; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Decoder for animated webp images. + /// + internal class WebpAnimationDecoder : IDisposable + { + /// + /// Used for allocating memory during the decoding operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// The maximum number of frames to decode. Inclusive. + /// + private readonly uint maxFrames; + + /// + /// Whether to skip metadata. + /// + private readonly bool skipMetadata; + + /// + /// The area to restore. + /// + private Rectangle? restoreArea; + + /// + /// The abstract metadata. + /// + private ImageMetadata? metadata; + + /// + /// The gif specific metadata. + /// + private WebpMetadata? webpMetadata; + + /// + /// The alpha data, if an ALPH chunk is present. + /// + private IMemoryOwner? alphaData; + + /// + /// The flag to decide how to handle the background color in the Animation Chunk. + /// + private readonly BackgroundColorHandling backgroundColorHandling; + + /// + /// Executes a known ancillary segment parsing action using the configured integrity policy. + /// + private readonly Action executeAncillarySegmentAction; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The global configuration. + /// The maximum number of frames to decode. Inclusive. + /// Whether to skip metadata. + /// The flag to decide how to handle the background color in the Animation Chunk. + /// Executes a known ancillary segment parsing action using the configured integrity policy. + public WebpAnimationDecoder( + MemoryAllocator memoryAllocator, + Configuration configuration, + uint maxFrames, + bool skipMetadata, + BackgroundColorHandling backgroundColorHandling, + Action executeAncillarySegmentAction) + { + this.memoryAllocator = memoryAllocator; + this.configuration = configuration; + this.maxFrames = maxFrames; + this.skipMetadata = skipMetadata; + this.backgroundColorHandling = backgroundColorHandling; + this.executeAncillarySegmentAction = executeAncillarySegmentAction; + } + + /// + /// Reads the animated webp image information from the specified stream. + /// + /// The stream, where the image should be decoded from. Cannot be null. + /// The webp features. + /// The width of the image. + /// The height of the image. + /// The size of the image data in bytes. + public ImageInfo Identify( + BufferedReadStream stream, + WebpFeatures features, + uint width, + uint height, + uint completeDataSize) + { + List framesMetadata = []; + this.metadata = new ImageMetadata(); + this.webpMetadata = this.metadata.GetWebpMetadata(); + this.webpMetadata.RepeatCount = features.AnimationLoopCount; + + this.webpMetadata.BackgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore + ? Color.Transparent + : features.AnimationBackgroundColor!.Value; + + bool ignoreMetadata = this.skipMetadata; + Span buffer = stackalloc byte[4]; + uint frameCount = 0; + int remainingBytes = (int)completeDataSize; + while (remainingBytes > 0) + { + WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); + remainingBytes -= 4; + switch (chunkType) + { + case WebpChunkType.FrameData: + + ImageFrameMetadata frameMetadata = new(); + uint dataSize = ReadFrameInfo(stream, ref frameMetadata); + framesMetadata.Add(frameMetadata); + + remainingBytes -= (int)dataSize; + break; + case WebpChunkType.Iccp: + case WebpChunkType.Xmp: + case WebpChunkType.Exif: + this.ReadOptionalChunk(stream, chunkType, this.metadata, ignoreMetadata); + break; + default: + + // Specification explicitly states to ignore unknown chunks. + // We do not support writing these chunks at present. + break; + } + + if (stream.Position == stream.Length || ++frameCount == this.maxFrames) + { + break; + } + } + + return new ImageInfo(new Size((int)width, (int)height), this.metadata, framesMetadata); + } + + /// + /// Decodes the animated webp image from the specified stream. + /// + /// The pixel format. + /// The stream, where the image should be decoded from. Cannot be null. + /// The webp features. + /// The width of the image. + /// The height of the image. + /// The size of the image data in bytes. + public Image Decode( + BufferedReadStream stream, + WebpFeatures features, + uint width, + uint height, + uint completeDataSize) + where TPixel : unmanaged, IPixel + { + Image? image = null; + ImageFrame? previousFrame = null; + WebpFrameData? prevFrameData = null; + + this.metadata = new ImageMetadata(); + this.webpMetadata = this.metadata.GetWebpMetadata(); + this.webpMetadata.RepeatCount = features.AnimationLoopCount; + + Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore + ? Color.Transparent + : features.AnimationBackgroundColor!.Value; + + this.webpMetadata.BackgroundColor = backgroundColor; + TPixel backgroundPixel = backgroundColor.ToPixel(); + + bool ignoreMetadata = this.skipMetadata; + Span buffer = stackalloc byte[4]; + uint frameCount = 0; + int remainingBytes = (int)completeDataSize; + + while (remainingBytes > 0) + { + WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); + remainingBytes -= 4; + switch (chunkType) + { + case WebpChunkType.FrameData: + + uint dataSize = this.ReadFrame( + stream, + ref image, + ref previousFrame, + ref prevFrameData, + width, + height, + backgroundPixel); + + remainingBytes -= (int)dataSize; + break; + case WebpChunkType.Iccp: + case WebpChunkType.Xmp: + case WebpChunkType.Exif: + this.ReadOptionalChunk(stream, chunkType, image!.Metadata, ignoreMetadata); + break; + default: + + // Specification explicitly states to ignore unknown chunks. + // We do not support writing these chunks at present. + break; + } + + if (stream.Position == stream.Length || ++frameCount == this.maxFrames) + { + break; + } + } + + return image!; + } + + /// + /// Reads frame information from the specified stream and updates the provided frame metadata. + /// + /// The stream from which to read the frame information. Must support reading and seeking. + /// A reference to the structure that will be updated with the parsed frame metadata. + /// The number of bytes read from the stream while parsing the frame information. + private static uint ReadFrameInfo(BufferedReadStream stream, ref ImageFrameMetadata frameMetadata) + { + WebpFrameData frameData = WebpFrameData.Parse(stream); + SetFrameMetadata(frameMetadata, frameData); + + // Size of the frame header chunk. + const int chunkHeaderSize = 16; + + uint remaining = frameData.DataSize - chunkHeaderSize; + stream.Skip((int)remaining); + + return remaining; + } + + /// + /// Reads an individual webp frame. + /// + /// The pixel format. + /// The stream, where the image should be decoded from. Cannot be null. + /// The image to decode the information to. + /// The previous frame. + /// The previous frame data. + /// The width of the image. + /// The height of the image. + /// The default background color of the canvas in. + /// The number of bytes read from the stream while parsing the frame information. + private uint ReadFrame( + BufferedReadStream stream, + ref Image? image, + ref ImageFrame? previousFrame, + ref WebpFrameData? prevFrameData, + uint width, + uint height, + TPixel backgroundColor) + where TPixel : unmanaged, IPixel + { + WebpFrameData frameData = WebpFrameData.Parse(stream); + long streamStartPosition = stream.Position; + Span buffer = stackalloc byte[4]; + + WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); + bool hasAlpha = false; + byte alphaChunkHeader = 0; + if (chunkType is WebpChunkType.Alpha) + { + alphaChunkHeader = this.ReadAlphaData(stream); + hasAlpha = true; + chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); + } + + WebpImageInfo? webpInfo = null; + WebpFeatures features = new(); + switch (chunkType) + { + case WebpChunkType.Vp8: + webpInfo = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); + features.Alpha = hasAlpha; + features.AlphaChunkHeader = alphaChunkHeader; + break; + case WebpChunkType.Vp8L: + if (hasAlpha) + { + WebpThrowHelper.ThrowNotSupportedException("Alpha channel is not supported for lossless webp images."); + } + + webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); + break; + default: + WebpThrowHelper.ThrowImageFormatException("Read unexpected chunk type, should be VP8 or VP8L"); + break; + } + + ImageFrame currentFrame; + if (previousFrame is null) + { + image = new Image(this.configuration, (int)width, (int)height, backgroundColor, this.metadata); + + currentFrame = image.Frames.RootFrame; + SetFrameMetadata(currentFrame.Metadata, frameData); + } + else + { + // If the frame is a key frame we do not need to clone the frame or clear it. + bool isKeyFrame = prevFrameData?.DisposalMethod is FrameDisposalMode.RestoreToBackground + && this.restoreArea == image!.Bounds; + + if (isKeyFrame) + { + currentFrame = image!.Frames.CreateFrame(backgroundColor); + } + else + { + // This clones the frame and adds it the collection. + currentFrame = image!.Frames.AddFrame(previousFrame); + if (prevFrameData?.DisposalMethod is FrameDisposalMode.RestoreToBackground) + { + this.RestoreToBackground(currentFrame, backgroundColor); + } + } + + SetFrameMetadata(currentFrame.Metadata, frameData); + } + + Rectangle interest = frameData.Bounds; + bool blend = previousFrame != null && frameData.BlendingMethod == FrameBlendMode.Over; + using Buffer2D pixelData = this.DecodeImageFrameData(frameData, webpInfo); + DrawDecodedImageFrameOnCanvas(pixelData, currentFrame, interest, blend); + + webpInfo?.Dispose(); + previousFrame = currentFrame; + prevFrameData = frameData; + + if (frameData.DisposalMethod is FrameDisposalMode.RestoreToBackground) + { + this.restoreArea = interest; + } + + return (uint)(stream.Position - streamStartPosition); + } + + /// + /// Sets the frames metadata. + /// + /// The metadata. + /// The frame data. + private static void SetFrameMetadata(ImageFrameMetadata meta, WebpFrameData frameData) + { + WebpFrameMetadata frameMetadata = meta.GetWebpMetadata(); + frameMetadata.FrameDelay = frameData.Duration; + frameMetadata.BlendMode = frameData.BlendingMethod; + frameMetadata.DisposalMode = frameData.DisposalMethod; + } + + private void ReadOptionalChunk( + BufferedReadStream stream, + WebpChunkType chunkType, + ImageMetadata imageMetadata, + bool ignoreMetadata) + { + switch (chunkType) + { + case WebpChunkType.Iccp: + + // While ICC profiles are optional, an invalid ICC profile cannot be ignored because it must + // precede the frame data, and we cannot safely skip it without successfully reading its size. + WebpChunkParsingUtils.ReadIccProfile(stream, imageMetadata, ignoreMetadata); + break; + case WebpChunkType.Exif: + this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, imageMetadata, ignoreMetadata)); + break; + case WebpChunkType.Xmp: + this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, imageMetadata, ignoreMetadata)); + break; + } + } + + /// + /// Reads the ALPH chunk data. + /// + /// The stream to read from. + private byte ReadAlphaData(BufferedReadStream stream) + { + this.alphaData?.Dispose(); + + uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, stackalloc byte[4]); + int alphaDataSize = (int)(alphaChunkSize - 1); + this.alphaData = this.memoryAllocator.Allocate(alphaDataSize); + + byte alphaChunkHeader = (byte)stream.ReadByte(); + Span alphaData = this.alphaData.GetSpan(); + _ = stream.Read(alphaData, 0, alphaDataSize); + + return alphaChunkHeader; + } + + /// + /// Decodes the either lossy or lossless webp image data. + /// + /// The pixel format. + /// The frame data. + /// The webp information. + /// A decoded image. + private Buffer2D DecodeImageFrameData(WebpFrameData frameData, WebpImageInfo webpInfo) + where TPixel : unmanaged, IPixel + { + ImageFrame decodedFrame = new(this.configuration, (int)frameData.Width, (int)frameData.Height); + + try + { + Buffer2D decodeBuffer = decodedFrame.PixelBuffer; + if (webpInfo.IsLossless) + { + WebpLosslessDecoder losslessDecoder = new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); + losslessDecoder.Decode(decodeBuffer, (int)webpInfo.Width, (int)webpInfo.Height); + } + else + { + WebpLossyDecoder lossyDecoder = + new(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); + lossyDecoder.Decode(decodeBuffer, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.alphaData); + } + + return decodeBuffer; + } + catch + { + decodedFrame?.Dispose(); + throw; + } + } + + /// + /// Draws the decoded image on canvas. The decoded image can be smaller the canvas. + /// + /// The type of the pixel. + /// The decoded image. + /// The image frame to draw into. + /// The area of the frame. + /// Whether to blend the decoded frame data onto the target frame. + private static void DrawDecodedImageFrameOnCanvas( + Buffer2D decodedImageFrame, + ImageFrame imageFrame, + Rectangle restoreArea, + bool blend) + where TPixel : unmanaged, IPixel + { + // Trim the destination frame to match the restore area. The source frame is already trimmed. + Buffer2DRegion imageFramePixels = imageFrame.PixelBuffer.GetRegion(restoreArea); + if (blend) + { + // The destination frame has already been prepopulated with the pixel data from the previous frame + // so blending will leave the desired result which takes into consideration restoration to the + // background color within the restore area. + PixelBlender blender = PixelOperations.Instance.GetPixelBlender( + PixelColorBlendingMode.Normal, + PixelAlphaCompositionMode.SrcOver); + + // By using a dedicated vector span we can avoid per-row pool allocations in PixelBlender.Blend + // We need 3 Vector4 values per pixel to store the background, foreground, and result pixels for blending. + using IMemoryOwner workingBufferOwner = imageFrame.Configuration.MemoryAllocator.Allocate(restoreArea.Width * 3); + Span workingBuffer = workingBufferOwner.GetSpan(); + + for (int y = 0; y < restoreArea.Height; y++) + { + Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); + Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width]; + + blender.Blend(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f, workingBuffer); + } + + return; + } + + for (int y = 0; y < restoreArea.Height; y++) + { + Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); + Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width]; + decodedPixelRow.CopyTo(framePixelRow); + } + } + + /// + /// Dispose to background color. Fill the rectangle on the canvas covered by the current frame + /// with background color specified in the ANIM chunk. + /// + /// The pixel format. + /// The image frame. + /// Color of the background. + private void RestoreToBackground(ImageFrame imageFrame, TPixel backgroundColor) + where TPixel : unmanaged, IPixel + { + if (!this.restoreArea.HasValue) + { + return; + } + + Rectangle interest = Rectangle.Intersect(imageFrame.Bounds, this.restoreArea.Value); + Buffer2DRegion pixelRegion = imageFrame.PixelBuffer.GetRegion(interest); + pixelRegion.Fill(backgroundColor); + + this.restoreArea = null; + } + + /// + public void Dispose() => this.alphaData?.Dispose(); + } +} diff --git a/ImageSharp/Formats/Webp/WebpBitsPerPixel.cs b/ImageSharp/Formats/Webp/WebpBitsPerPixel.cs new file mode 100644 index 0000000..e8b3351 --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpBitsPerPixel.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Enumerates the available bits per pixel the webp image uses. + /// + public enum WebpBitsPerPixel : short + { + /// + /// 24 bits per pixel. Each pixel consists of 3 bytes. + /// + Bit24 = 24, + + /// + /// 32 bits per pixel. Each pixel consists of 4 bytes (an alpha channel is present). + /// + Bit32 = 32 + } +} diff --git a/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs new file mode 100644 index 0000000..f56863b --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -0,0 +1,481 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.IO; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Webp.BitReader; +using SixLabors.ImageSharp.Formats.Webp.Lossy; +using SixLabors.ImageSharp.IO; +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; + +namespace SixLabors.ImageSharp.Formats.Webp { + internal static class WebpChunkParsingUtils + { + /// + /// Reads the header of a lossy webp image. + /// + /// The memory allocator. + /// The buffered read stream. + /// The scratch buffer to use while reading. + /// The webp features to parse. + /// Information about this webp image. + public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, BufferedReadStream stream, Span buffer, WebpFeatures features) + { + // VP8 data size (not including this 4 bytes). + int bytesRead = stream.Read(buffer, 0, 4); + if (bytesRead != 4) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 header"); + } + + uint dataSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); + + // Remaining counts the available image data payload. + uint remaining = dataSize; + + // Paragraph 9.1 https://tools.ietf.org/html/rfc6386#page-30 + // Frame tag that contains four fields: + // - A 1-bit frame type (0 for key frames, 1 for interframes). + // - A 3-bit version number. + // - A 1-bit show_frame flag. + // - A 19-bit field containing the size of the first data partition in bytes. + bytesRead = stream.Read(buffer, 0, 3); + if (bytesRead != 3) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 header"); + } + + uint frameTag = (uint)(buffer[0] | (buffer[1] << 8) | (buffer[2] << 16)); + remaining -= 3; + bool isNoKeyFrame = (frameTag & 0x1) == 1; + if (isNoKeyFrame) + { + WebpThrowHelper.ThrowImageFormatException("VP8 header indicates the image is not a key frame"); + } + + uint version = (frameTag >> 1) & 0x7; + if (version > 3) + { + WebpThrowHelper.ThrowImageFormatException($"VP8 header indicates unknown profile {version}"); + } + + bool invisibleFrame = ((frameTag >> 4) & 0x1) == 0; + if (invisibleFrame) + { + WebpThrowHelper.ThrowImageFormatException("VP8 header indicates that the first frame is invisible"); + } + + uint partitionLength = frameTag >> 5; + if (partitionLength > dataSize) + { + WebpThrowHelper.ThrowImageFormatException("VP8 header contains inconsistent size information"); + } + + // Check for VP8 magic bytes. + bytesRead = stream.Read(buffer, 0, 3); + if (bytesRead != 3) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes"); + } + + if (!buffer[..3].SequenceEqual(WebpConstants.Vp8HeaderMagicBytes)) + { + WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found"); + } + + bytesRead = stream.Read(buffer, 0, 4); + if (bytesRead != 4) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 header, could not read width and height"); + } + + uint tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer); + uint width = tmp & 0x3fff; + sbyte xScale = (sbyte)(tmp >> 6); + tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..]); + uint height = tmp & 0x3fff; + sbyte yScale = (sbyte)(tmp >> 6); + remaining -= 7; + if (width == 0 || height == 0) + { + WebpThrowHelper.ThrowImageFormatException("width or height can not be zero"); + } + + if (partitionLength > remaining) + { + WebpThrowHelper.ThrowImageFormatException("bad partition length"); + } + + Vp8FrameHeader vp8FrameHeader = new() + { + KeyFrame = true, + Profile = (sbyte)version, + PartitionLength = partitionLength + }; + + Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; + + return new WebpImageInfo + { + DataSize = dataSize, + Width = width, + Height = height, + XScale = xScale, + YScale = yScale, + + // Vp8 header can be parsed during the processing of the Vp8X header. + BitsPerPixel = features?.Alpha == true ? WebpBitsPerPixel.Bit32 : WebpBitsPerPixel.Bit24, + IsLossless = false, + Features = features, + Vp8Profile = (sbyte)version, + Vp8FrameHeader = vp8FrameHeader, + Vp8BitReader = bitReader + }; + } + + /// + /// Reads the header of a lossless webp image. + /// + /// The memory allocator. + /// The buffered read stream. + /// The scratch buffer to use while reading. + /// The webp features to parse. + public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, BufferedReadStream stream, Span buffer, WebpFeatures features) + { + // VP8 data size. + uint imageDataSize = ReadChunkSize(stream, buffer); + + Vp8LBitReader bitReader = new(stream, imageDataSize, memoryAllocator); + + // One byte signature, should be 0x2f. + uint signature = bitReader.ReadValue(8); + if (signature != WebpConstants.Vp8LHeaderMagicByte) + { + WebpThrowHelper.ThrowImageFormatException("Invalid VP8L signature"); + } + + // The first 28 bits of the bitstream specify the width and height of the image. + uint width = bitReader.ReadValue(WebpConstants.Vp8LImageSizeBits) + 1; + uint height = bitReader.ReadValue(WebpConstants.Vp8LImageSizeBits) + 1; + if (width == 0 || height == 0) + { + WebpThrowHelper.ThrowImageFormatException("invalid width or height read"); + } + + // The alphaIsUsed flag should be set to 0 when all alpha values are 255 in the picture, and 1 otherwise. + // Alpha may have already been set by the VP8X chunk. + features.Alpha |= bitReader.ReadBit(); + + // The next 3 bits are the version. The version number is a 3 bit code that must be set to 0. + // Any other value should be treated as an error. + uint version = bitReader.ReadValue(WebpConstants.Vp8LVersionBits); + if (version != 0) + { + WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header"); + } + + return new WebpImageInfo + { + DataSize = imageDataSize, + Width = width, + Height = height, + BitsPerPixel = features.Alpha ? WebpBitsPerPixel.Bit32 : WebpBitsPerPixel.Bit24, + IsLossless = true, + Features = features, + Vp8LBitReader = bitReader + }; + } + + /// + /// 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. + /// + /// The buffered read stream. + /// The scratch buffer to use while reading. + /// The webp features to parse. + /// Information about this webp image. + public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span buffer, WebpFeatures features) + { + uint fileSize = ReadChunkSize(stream, buffer); + + // The first byte contains information about the image features used. + byte imageFeatures = (byte)stream.ReadByte(); + + // The first two bit of it are reserved and should be 0. + if (imageFeatures >> 6 != 0) + { + WebpThrowHelper.ThrowImageFormatException("first two bits of the VP8X header are expected to be zero"); + } + + // If bit 3 is set, a ICC Profile Chunk should be present. + features.IccProfile = (imageFeatures & (1 << 5)) != 0; + + // If bit 4 is set, any of the frames of the image contain transparency information ("alpha" chunk). + features.Alpha = (imageFeatures & (1 << 4)) != 0; + + // If bit 5 is set, a EXIF metadata should be present. + features.ExifProfile = (imageFeatures & (1 << 3)) != 0; + + // If bit 6 is set, XMP metadata should be present. + features.XmpMetaData = (imageFeatures & (1 << 2)) != 0; + + // If bit 7 is set, animation should be present. + features.Animation = (imageFeatures & (1 << 1)) != 0; + + // 3 reserved bytes should follow which are supposed to be zero. + // No other decoder actually checks this though. + stream.Read(buffer, 0, 3); + + // 3 bytes for the width. + uint width = ReadUInt24LittleEndian(stream, buffer) + 1; + + // 3 bytes for the height. + uint height = ReadUInt24LittleEndian(stream, buffer) + 1; + + // Read all the chunks in the order they occur. + return new WebpImageInfo + { + Width = width, + Height = height, + Features = features + + // Additional properties are set during the parsing of the VP8 or VP8L headers. + }; + } + + /// + /// Reads a unsigned 24 bit integer. + /// + /// The stream to read from. + /// The buffer to store the read data into. + /// A unsigned 24 bit integer. + /// + /// Thrown if the input stream is not valid. + /// + public static uint ReadUInt24LittleEndian(Stream stream, Span buffer) + { + if (stream.Read(buffer, 0, 3) == 3) + { + buffer[3] = 0; + return BinaryPrimitives.ReadUInt32LittleEndian(buffer); + } + + throw new ImageFormatException("Invalid Webp data, could not read unsigned 24 bit integer."); + } + + /// + /// Writes a unsigned 24 bit integer. + /// + /// The stream to write to. + /// The uint24 data to write. + /// + /// Thrown if the data is not a valid unsigned 24 bit integer. + /// + public static unsafe void WriteUInt24LittleEndian(Stream stream, uint data) + { + if (data >= 1 << 24) + { + throw new InvalidDataException($"Invalid data, {data} is not a unsigned 24 bit integer."); + } + + uint* ptr = &data; + byte* b = (byte*)ptr; + + // Write the data in little endian. + stream.WriteByte(b[0]); + stream.WriteByte(b[1]); + stream.WriteByte(b[2]); + } + + /// + /// Reads the chunk size. If Chunk Size is odd, a single padding byte will be added to the payload, + /// so the chunk size will be increased by 1 in those cases. + /// + /// The stream to read the data from. + /// Buffer to store the data read from the stream. + /// If true, the chunk size is required to be read, otherwise it can be skipped. + /// The chunk size in bytes. + /// Thrown if the input stream is not valid. + public static uint ReadChunkSize(Stream stream, Span buffer, bool required = true) + { + if (stream.Read(buffer) is 4) + { + uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); + return chunkSize % 2 is 0 ? chunkSize : chunkSize + 1; + } + + if (required) + { + throw new ImageFormatException("Invalid Webp data, could not read chunk size."); + } + + // Return the size of the remaining data in the stream. + return (uint)(stream.Length - stream.Position); + } + + /// + /// Identifies the chunk type from the chunk. + /// + /// The stream to read the data from. + /// Buffer to store the data read from the stream. + /// + /// Thrown if the input stream is not valid. + /// + public static WebpChunkType ReadChunkType(BufferedReadStream stream, Span buffer) + { + if (stream.Read(buffer) == 4) + { + return (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer); + } + + // While we ignore unknown chunks we still need a to be a ble to read a chunk type + // known or otherwise from the stream. + throw new ImageFormatException("Invalid Webp data, could not read chunk type."); + } + + /// + /// Reads the ICCP chunk from the stream. + /// + /// The stream to decode from. + /// The image metadata. + /// If true, metadata will be ignored. + public static void ReadIccProfile( + BufferedReadStream stream, + ImageMetadata metadata, + bool ignoreMetadata) + { + Span buffer = stackalloc byte[4]; + uint iccpChunkSize = ReadChunkSize(stream, buffer); + if (ignoreMetadata || metadata.IccProfile != null) + { + stream.Skip((int)iccpChunkSize); + } + else + { + byte[] iccpData = new byte[iccpChunkSize]; + int bytesRead = stream.Read(iccpData, 0, (int)iccpChunkSize); + if (bytesRead != iccpChunkSize) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk"); + } + + IccProfile profile = new(iccpData); + if (!profile.CheckIsValid()) + { + throw new InvalidIccProfileException("Invalid ICC profile."); + } + + metadata.IccProfile = profile; + } + } + + /// + /// Reads the EXIF profile from the stream. + /// + /// The stream to decode from. + /// The image metadata. + /// If true, metadata will be ignored. + public static void ReadExifProfile( + BufferedReadStream stream, + ImageMetadata metadata, + bool ignoreMetadata) + { + Span buffer = stackalloc byte[4]; + uint exifChunkSize = ReadChunkSize(stream, buffer); + if (ignoreMetadata || metadata.ExifProfile != null) + { + stream.Skip((int)exifChunkSize); + } + else + { + byte[] exifData = new byte[exifChunkSize]; + int bytesRead = stream.Read(exifData, 0, (int)exifChunkSize); + if (bytesRead != exifChunkSize) + { + WebpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the EXIF profile"); + } + + ExifProfile exifProfile = new(exifData); + + // Set the resolution from the metadata. + double horizontalValue = GetExifResolutionValue(exifProfile, ExifTag.XResolution); + double verticalValue = GetExifResolutionValue(exifProfile, ExifTag.YResolution); + + if (horizontalValue > 0 && verticalValue > 0) + { + metadata.HorizontalResolution = horizontalValue; + metadata.VerticalResolution = verticalValue; + metadata.ResolutionUnits = UnitConverter.ExifProfileToResolutionUnit(exifProfile); + } + + metadata.ExifProfile = exifProfile; + } + } + + /// + /// Reads the XMP profile the stream. + /// + /// The stream to decode from. + /// The image metadata. + /// If true, metadata will be ignored. + public static void ReadXmpProfile( + BufferedReadStream stream, + ImageMetadata metadata, + bool ignoreMetadata) + { + Span buffer = stackalloc byte[4]; + uint xmpChunkSize = ReadChunkSize(stream, buffer); + if (ignoreMetadata || metadata.XmpProfile != null) + { + stream.Skip((int)xmpChunkSize); + } + else + { + byte[] xmpData = new byte[xmpChunkSize]; + int bytesRead = stream.Read(xmpData, 0, (int)xmpChunkSize); + if (bytesRead != xmpChunkSize) + { + WebpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the XMP profile"); + } + + metadata.XmpProfile = new XmpProfile(xmpData); + } + } + + private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag tag) + { + if (exifProfile.TryGetValue(tag, out IExifValue? resolution)) + { + return resolution.Value.ToDouble(); + } + + return 0; + } + + /// + /// Determines if the chunk type is an optional VP8X chunk. + /// + /// The chunk type. + /// True, if its an optional chunk type. + public static bool IsOptionalVp8XChunk(WebpChunkType chunkType) => chunkType switch + { + WebpChunkType.Alpha => true, + WebpChunkType.AnimationParameter => true, + WebpChunkType.Exif => true, + WebpChunkType.Iccp => true, + WebpChunkType.Xmp => true, + _ => false + }; + } +} diff --git a/ImageSharp/Formats/Webp/WebpChunkType.cs b/ImageSharp/Formats/Webp/WebpChunkType.cs new file mode 100644 index 0000000..5a99bb6 --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpChunkType.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Contains a list of different webp chunk types. + /// + /// See Webp Container Specification for more details: https://developers.google.com/speed/webp/docs/riff_container + internal enum WebpChunkType : uint + { + /// + /// Header signaling the use of the VP8 format. + /// + /// VP8 (Single) + Vp8 = 0x56503820U, + + /// + /// Header signaling the image uses lossless encoding. + /// + /// VP8L (Single) + Vp8L = 0x5650384CU, + + /// + /// Header for a extended-VP8 chunk. + /// + /// VP8X (Single) + Vp8X = 0x56503858U, + + /// + /// Chunk contains information about the alpha channel. + /// + /// ALPH (Single) + Alpha = 0x414C5048U, + + /// + /// Chunk which contains a color profile. + /// + /// ICCP (Single) + Iccp = 0x49434350U, + + /// + /// Chunk which contains EXIF metadata about the image. + /// + /// EXIF (Single) + Exif = 0x45584946U, + + /// + /// Chunk contains XMP metadata about the image. + /// + /// XMP (Single) + Xmp = 0x584D5020U, + + /// + /// For an animated image, this chunk contains the global parameters of the animation. + /// + /// ANIM (Single) + AnimationParameter = 0x414E494D, + + /// + /// For animated images, this chunk contains information about a single frame. If the Animation flag is not set, then this chunk SHOULD NOT be present. + /// + /// ANMF (Multiple) + FrameData = 0x414E4D46, + } +} diff --git a/ImageSharp/Formats/Webp/WebpColorType.cs b/ImageSharp/Formats/Webp/WebpColorType.cs new file mode 100644 index 0000000..c8cc334 --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpColorType.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Provides enumeration of the various webp color types. + /// + public enum WebpColorType + { + /// + /// Yuv (luminance, blue chroma, red chroma) as defined in the ITU-R Rec. BT.709 specification. + /// + Yuv, + + /// + /// Rgb color space. + /// + Rgb, + + /// + /// Rgba color space. + /// + Rgba + } +} diff --git a/ImageSharp/Formats/Webp/WebpCommonUtils.cs b/ImageSharp/Formats/Webp/WebpCommonUtils.cs new file mode 100644 index 0000000..a663458 --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpCommonUtils.cs @@ -0,0 +1,160 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Utility methods for lossy and lossless webp format. + /// + internal static class WebpCommonUtils + { + /// + /// Checks if the pixel row is not opaque. + /// + /// The row to check. + /// Returns true if alpha has non-0xff values. + public static unsafe bool CheckNonOpaque(ReadOnlySpan row) + { + if (Vector256.IsHardwareAccelerated) + { + ReadOnlySpan rowBytes = MemoryMarshal.AsBytes(row); + int i = 0; + int length = (row.Length * 4) - 3; + fixed (byte* src = rowBytes) + { + Vector256 alphaMaskVector256 = Vector256.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255); + Vector256 all0x80Vector256 = Vector256.Create((byte)0x80).AsByte(); + + for (; i + 128 <= length; i += 128) + { + Vector256 a0 = Vector256.Load(src + i).AsByte(); + Vector256 a1 = Vector256.Load(src + i + 32).AsByte(); + Vector256 a2 = Vector256.Load(src + i + 64).AsByte(); + Vector256 a3 = Vector256.Load(src + i + 96).AsByte(); + Vector256 b0 = (a0 & alphaMaskVector256).AsInt32(); + Vector256 b1 = (a1 & alphaMaskVector256).AsInt32(); + Vector256 b2 = (a2 & alphaMaskVector256).AsInt32(); + Vector256 b3 = (a3 & alphaMaskVector256).AsInt32(); + Vector256 c0 = Vector256_.PackSignedSaturate(b0, b1).AsInt16(); + Vector256 c1 = Vector256_.PackSignedSaturate(b2, b3).AsInt16(); + Vector256 d = Vector256_.PackSignedSaturate(c0, c1).AsByte(); + Vector256 bits = Vector256.Equals(d, all0x80Vector256); + uint mask = bits.ExtractMostSignificantBits(); + if (mask != 0xFFFF_FFFF) + { + return true; + } + } + + for (; i + 64 <= length; i += 64) + { + if (IsNoneOpaque64BytesVector128(src, i)) + { + return true; + } + } + + for (; i + 32 <= length; i += 32) + { + if (IsNonOpaque32BytesVector128(src, i)) + { + return true; + } + } + + for (; i <= length; i += 4) + { + if (src[i + 3] != 0xFF) + { + return true; + } + } + } + } + else if (Vector128.IsHardwareAccelerated) + { + ReadOnlySpan rowBytes = MemoryMarshal.AsBytes(row); + int i = 0; + int length = (row.Length * 4) - 3; + fixed (byte* src = rowBytes) + { + for (; i + 64 <= length; i += 64) + { + if (IsNoneOpaque64BytesVector128(src, i)) + { + return true; + } + } + + for (; i + 32 <= length; i += 32) + { + if (IsNonOpaque32BytesVector128(src, i)) + { + return true; + } + } + + for (; i <= length; i += 4) + { + if (src[i + 3] != 0xFF) + { + return true; + } + } + } + } + else + { + for (int x = 0; x < row.Length; x++) + { + if (row[x].A != 0xFF) + { + return true; + } + } + } + + return false; + } + + private static unsafe bool IsNoneOpaque64BytesVector128(byte* src, int i) + { + Vector128 alphaMask = Vector128.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255); + + Vector128 a0 = Vector128.Load(src + i).AsByte(); + Vector128 a1 = Vector128.Load(src + i + 16).AsByte(); + Vector128 a2 = Vector128.Load(src + i + 32).AsByte(); + Vector128 a3 = Vector128.Load(src + i + 48).AsByte(); + Vector128 b0 = (a0 & alphaMask).AsInt32(); + Vector128 b1 = (a1 & alphaMask).AsInt32(); + Vector128 b2 = (a2 & alphaMask).AsInt32(); + Vector128 b3 = (a3 & alphaMask).AsInt32(); + Vector128 c0 = Vector128_.PackSignedSaturate(b0, b1).AsInt16(); + Vector128 c1 = Vector128_.PackSignedSaturate(b2, b3).AsInt16(); + Vector128 d = Vector128_.PackSignedSaturate(c0, c1).AsByte(); + Vector128 bits = Vector128.Equals(d, Vector128.Create((byte)0x80).AsByte()); + uint mask = bits.ExtractMostSignificantBits(); + return mask != 0xFFFF; + } + + private static unsafe bool IsNonOpaque32BytesVector128(byte* src, int i) + { + Vector128 alphaMask = Vector128.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255); + + Vector128 a0 = Vector128.Load(src + i).AsByte(); + Vector128 a1 = Vector128.Load(src + i + 16).AsByte(); + Vector128 b0 = (a0 & alphaMask).AsInt32(); + Vector128 b1 = (a1 & alphaMask).AsInt32(); + Vector128 c = Vector128_.PackSignedSaturate(b0, b1).AsInt16(); + Vector128 d = Vector128_.PackSignedSaturate(c, c).AsByte(); + Vector128 bits = Vector128.Equals(d, Vector128.Create((byte)0x80).AsByte()); + uint mask = bits.ExtractMostSignificantBits(); + return mask != 0xFFFF; + } + } +} diff --git a/ImageSharp/Formats/Webp/WebpConfigurationModule.cs b/ImageSharp/Formats/Webp/WebpConfigurationModule.cs new file mode 100644 index 0000000..c8f81b7 --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpConfigurationModule.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Registers the image encoders, decoders and mime type detectors for the webp format. + /// + public sealed class WebpConfigurationModule : IImageFormatConfigurationModule + { + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetDecoder(WebpFormat.Instance, WebpDecoder.Instance); + configuration.ImageFormatsManager.SetEncoder(WebpFormat.Instance, new WebpEncoder()); + configuration.ImageFormatsManager.AddImageFormatDetector(new WebpImageFormatDetector()); + } + } +} diff --git a/ImageSharp/Formats/Webp/WebpConstants.cs b/ImageSharp/Formats/Webp/WebpConstants.cs new file mode 100644 index 0000000..abe75ec --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpConstants.cs @@ -0,0 +1,324 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Constants used for encoding and decoding VP8 and VP8L bitstreams. + /// + internal static class WebpConstants + { + /// + /// The list of file extensions that equate to Webp. + /// + public static readonly IEnumerable FileExtensions = ["webp"]; + + /// + /// The list of mimetypes that equate to a jpeg. + /// + public static readonly IEnumerable MimeTypes = ["image/webp"]; + + /// + /// Signature which identifies a VP8 header. + /// + public static readonly byte[] Vp8HeaderMagicBytes = + [ + 0x9D, + 0x01, + 0x2A + ]; + + /// + /// Signature byte which identifies a VP8L header. + /// + public const byte Vp8LHeaderMagicByte = 0x2F; + + /// + /// The header bytes identifying RIFF file. + /// + public static readonly byte[] RiffFourCc = + [ + 0x52, // R + 0x49, // I + 0x46, // F + 0x46 // F + ]; + + /// + /// The header bytes identifying a Webp. + /// + public static readonly byte[] WebpHeader = + [ + 0x57, // W + 0x45, // E + 0x42, // B + 0x50 // P + ]; + + /// + /// The header bytes identifying a Webp. + /// + public const string WebpFourCc = "WEBP"; + + /// + /// 3 bits reserved for version. + /// + public const int Vp8LVersionBits = 3; + + /// + /// Bits for width and height infos of a VPL8 image. + /// + public const int Vp8LImageSizeBits = 14; + + /// + /// Size of the frame header within VP8 data. + /// + public const int Vp8FrameHeaderSize = 10; + + /// + /// Size of a chunk header. + /// + public const int ChunkHeaderSize = 8; + + /// + /// Size of the RIFF header ("RIFFnnnnWEBP"). + /// + public const int RiffHeaderSize = 12; + + /// + /// Size of a chunk tag (e.g. "VP8L"). + /// + public const int TagSize = 4; + + /// + /// The Vp8L version 0. + /// + public const int Vp8LVersion = 0; + + /// + /// Maximum number of histogram images (sub-blocks). + /// + public const int MaxHuffImageSize = 2600; + + /// + /// Minimum number of Huffman bits. + /// + public const int MinHuffmanBits = 2; + + /// + /// Maximum number of Huffman bits. + /// + public const int MaxHuffmanBits = 9; + + /// + /// The maximum number of colors for a paletted images. + /// + public const int MaxPaletteSize = 256; + + /// + /// Maximum number of color cache bits is 10. + /// + public const int MaxColorCacheBits = 10; + + /// + /// The maximum number of allowed transforms in a VP8L bitstream. + /// + public const int MaxNumberOfTransforms = 4; + + /// + /// Maximum value of transformBits in VP8LEncoder. + /// + public const int MaxTransformBits = 6; + + /// + /// The bit to be written when next data to be read is a transform. + /// + public const int TransformPresent = 1; + + /// + /// The maximum allowed width or height of a webp image. + /// + public const int MaxDimension = 16383; + + public const int MaxAllowedCodeLength = 15; + + public const int DefaultCodeLength = 8; + + public const int HuffmanCodesPerMetaCode = 5; + + public const uint ArgbBlack = 0xff000000; + + public const int NumArgbCacheRows = 16; + + public const int NumLiteralCodes = 256; + + public const int NumLengthCodes = 24; + + public const int NumDistanceCodes = 40; + + public const int CodeLengthCodes = 19; + + public const int LengthTableBits = 7; + + public const uint CodeLengthLiterals = 16; + + public const int CodeLengthRepeatCode = 16; + + public static readonly int[] CodeLengthExtraBits = [2, 3, 7]; + + public static readonly int[] CodeLengthRepeatOffsets = [3, 3, 11]; + + public static readonly int[] AlphabetSize = + [ + NumLiteralCodes + NumLengthCodes, + NumLiteralCodes, NumLiteralCodes, NumLiteralCodes, + NumDistanceCodes + ]; + + public const int NumMbSegments = 4; + + public const int MaxNumPartitions = 8; + + public const int NumTypes = 4; + + public const int NumBands = 8; + + public const int NumProbas = 11; + + public const int NumPredModes = 4; + + public const int NumBModes = 10; + + public const int NumCtx = 3; + + public const int MaxVariableLevel = 67; + + public const int FlatnessLimitI16 = 0; + + public const int FlatnessLimitIUv = 2; + + public const int FlatnessLimitI4 = 3; + + public const int FlatnessPenality = 140; + + // This is the common stride for enc/dec. + public const int Bps = 32; + + // gamma-compensates loss of resolution during chroma subsampling. + public const double Gamma = 0.80d; + + public const int GammaFix = 12; // Fixed-point precision for linear values. + + public const int GammaScale = (1 << GammaFix) - 1; + + public const int GammaTabFix = 7; // Fixed-point fractional bits precision. + + public const int GammaTabSize = 1 << (GammaFix - GammaTabFix); + + public const int GammaTabScale = 1 << GammaTabFix; + + public const int GammaTabRounder = GammaTabScale >> 1; + + public const int AlphaFix = 19; + + /// + /// 8b of precision for susceptibilities. + /// + public const int MaxAlpha = 255; + + /// + /// Scaling factor for alpha. + /// + public const int AlphaScale = 2 * MaxAlpha; + + /// + /// Neutral value for susceptibility. + /// + public const int QuantEncMidAlpha = 64; + + /// + /// Lowest usable value for susceptibility. + /// + public const int QuantEncMinAlpha = 30; + + /// + /// Higher meaningful value for susceptibility. + /// + public const int QuantEncMaxAlpha = 100; + + /// + /// Scaling constant between the sns (Spatial Noise Shaping) value and the QP power-law modulation. Must be strictly less than 1. + /// + public const double SnsToDq = 0.9; + + public const int QuantEncMaxDqUv = 6; + + public const int QuantEncMinDqUv = -4; + + public const int QFix = 17; + + public const int MaxDelzaSize = 64; + + /// + /// Very small filter-strength values have close to no visual effect. So we can + /// save a little decoding-CPU by turning filtering off for these. + /// + public const int FilterStrengthCutoff = 2; + + /// + /// Max size of mode partition. + /// + public const int Vp8MaxPartition0Size = 1 << 19; + + public static readonly short[] Vp8FixedCostsUv = [302, 984, 439, 642]; + + public static readonly short[] Vp8FixedCostsI16 = [663, 919, 872, 919]; + + /// + /// Distortion multiplier (equivalent of lambda). + /// + public const int RdDistoMult = 256; + + /// + /// How many extra lines are needed on the MB boundary for caching, given a filtering level. + /// Simple filter(1): up to 2 luma samples are read and 1 is written. + /// Complex filter(2): up to 4 luma samples are read and 3 are written. Same for U/V, so it's 8 samples total (because of the 2x upsampling). + /// + public static readonly byte[] FilterExtraRows = [0, 2, 8]; + + // Paragraph 9.9 + public static readonly int[] Vp8EncBands = + [ + 0, 1, 2, 3, 6, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 0 + ]; + + public static readonly short[] Scan = + [ + 0 + (0 * Bps), 4 + (0 * Bps), 8 + (0 * Bps), 12 + (0 * Bps), + 0 + (4 * Bps), 4 + (4 * Bps), 8 + (4 * Bps), 12 + (4 * Bps), + 0 + (8 * Bps), 4 + (8 * Bps), 8 + (8 * Bps), 12 + (8 * Bps), + 0 + (12 * Bps), 4 + (12 * Bps), 8 + (12 * Bps), 12 + (12 * Bps) + ]; + + // Residual decoding (Paragraph 13.2 / 13.3) + public static readonly byte[] Cat3 = [173, 148, 140]; + public static readonly byte[] Cat4 = [176, 155, 140, 135]; + public static readonly byte[] Cat5 = [180, 157, 141, 134, 130]; + public static readonly byte[] Cat6 = [254, 254, 243, 230, 196, 177, 153, 140, 133, 130, 129]; + public static readonly byte[] Zigzag = [0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15]; + + public static readonly sbyte[] YModesIntra4 = + [ + -0, 1, + -1, 2, + -2, 3, + 4, 6, + -3, 5, + -4, -5, + -6, 7, + -7, 8, + -8, -9 + ]; + } +} diff --git a/ImageSharp/Formats/Webp/WebpDecoder.cs b/ImageSharp/Formats/Webp/WebpDecoder.cs new file mode 100644 index 0000000..e7ca3e6 --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpDecoder.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Image decoder for generating an image out of a webp stream. + /// + public sealed class WebpDecoder : SpecializedImageDecoder + { + private WebpDecoder() + { + } + + /// + /// Gets the shared instance. + /// + public static WebpDecoder Instance { get; } = new(); + + /// + protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + using WebpDecoderCore decoder = new(new WebpDecoderOptions { GeneralOptions = options }); + return decoder.Identify(options.Configuration, stream, cancellationToken); + } + + /// + protected override Image Decode(WebpDecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + using WebpDecoderCore decoder = new(options); + Image image = decoder.Decode(options.GeneralOptions.Configuration, stream, cancellationToken); + + ScaleToTargetSize(options.GeneralOptions, image); + + return image; + } + + /// + protected override Image Decode(WebpDecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(options, stream, cancellationToken); + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(options, stream, cancellationToken); + + /// + protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new() { GeneralOptions = options }; + } +} diff --git a/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/ImageSharp/Formats/Webp/WebpDecoderCore.cs new file mode 100644 index 0000000..621b892 --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -0,0 +1,405 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Threading; +using SixLabors.ImageSharp.Formats.Webp.Lossless; +using SixLabors.ImageSharp.Formats.Webp.Lossy; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Performs the webp decoding operation. + /// + internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable + { + /// + /// General configuration options. + /// + private readonly Configuration configuration; + + /// + /// A value indicating whether the metadata should be ignored when the image is being decoded. + /// + private readonly bool skipMetadata; + + /// + /// The maximum number of frames to decode. Inclusive. + /// + private readonly uint maxFrames; + + /// + /// Gets or sets the alpha data, if an ALPH chunk is present. + /// + private IMemoryOwner? alphaData; + + /// + /// Used for allocating memory during the decoding operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// Information about the webp image. + /// + private WebpImageInfo? webImageInfo; + + /// + /// The flag to decide how to handle the background color in the Animation Chunk. + /// + private readonly BackgroundColorHandling backgroundColorHandling; + + /// + /// Initializes a new instance of the class. + /// + /// The decoder options. + public WebpDecoderCore(WebpDecoderOptions options) + : base(options.GeneralOptions) + { + this.backgroundColorHandling = options.BackgroundColorHandling; + this.configuration = options.GeneralOptions.Configuration; + this.skipMetadata = options.GeneralOptions.SkipMetadata; + this.maxFrames = options.GeneralOptions.MaxFrames; + this.memoryAllocator = this.configuration.MemoryAllocator; + } + + /// + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + { + Image? image = null; + try + { + ImageMetadata metadata = new(); + Span buffer = stackalloc byte[4]; + + uint fileSize = ReadImageHeader(stream, buffer); + + using (this.webImageInfo = this.ReadVp8Info(stream, metadata)) + { + if (this.webImageInfo.Features is { Animation: true }) + { + using WebpAnimationDecoder animationDecoder = new( + this.memoryAllocator, + this.configuration, + this.maxFrames, + this.skipMetadata, + this.backgroundColorHandling, + this.ExecuteAncillarySegmentAction); + + return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); + } + + image = new Image(this.configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, metadata); + Buffer2D pixels = image.GetRootFramePixelBuffer(); + if (this.webImageInfo.IsLossless) + { + WebpLosslessDecoder losslessDecoder = new( + this.webImageInfo.Vp8LBitReader, + this.memoryAllocator, + this.configuration); + + losslessDecoder.Decode(pixels, image.Width, image.Height); + } + else + { + WebpLossyDecoder lossyDecoder = new( + this.webImageInfo.Vp8BitReader, + this.memoryAllocator, + this.configuration); + + lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData); + } + + // There can be optional chunks after the image data, like EXIF and XMP. + if (this.webImageInfo.Features != null) + { + this.ParseOptionalChunks(stream, metadata, this.webImageInfo.Features, buffer); + } + + _ = this.TryConvertIccProfile(image); + return image; + } + } + catch + { + image?.Dispose(); + throw; + } + } + + /// + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + uint fileSize = ReadImageHeader(stream, stackalloc byte[4]); + ImageMetadata metadata = new(); + + using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true)) + { + if (this.webImageInfo.Features is { Animation: true }) + { + using WebpAnimationDecoder animationDecoder = new( + this.memoryAllocator, + this.configuration, + this.maxFrames, + this.skipMetadata, + this.backgroundColorHandling, + this.ExecuteAncillarySegmentAction); + + return animationDecoder.Identify( + stream, + this.webImageInfo.Features, + this.webImageInfo.Width, + this.webImageInfo.Height, + fileSize); + } + + return new ImageInfo( + new Size((int)this.webImageInfo.Width, (int)this.webImageInfo.Height), + metadata); + } + } + + /// + /// Reads and skips over the image header. + /// + /// The stream to decode from. + /// Temporary buffer. + /// The file size in bytes. + private static uint ReadImageHeader(BufferedReadStream stream, Span buffer) + { + // Skip FourCC header, we already know its a RIFF file at this point. + stream.Skip(4); + + // Read file size. + // The size of the file in bytes starting at offset 8. + // The file size in the header is the total size of the chunks that follow plus 4 bytes for the ‘WEBP’ FourCC. + uint fileSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer); + + // Skip 'WEBP' from the header. + stream.Skip(4); + + return fileSize; + } + + /// + /// Reads information present in the image header, about the image content and how to decode the image. + /// + /// The stream to decode from. + /// The image metadata. + /// For identify, the alpha data should not be read. + /// Information about the webp image. + private WebpImageInfo ReadVp8Info(BufferedReadStream stream, ImageMetadata metadata, bool ignoreAlpha = false) + { + WebpMetadata webpMetadata = metadata.GetFormatMetadata(WebpFormat.Instance); + + Span buffer = stackalloc byte[4]; + WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); + + WebpImageInfo? info = null; + WebpFeatures features = new(); + switch (chunkType) + { + case WebpChunkType.Vp8: + info = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); + webpMetadata.FileFormat = WebpFileFormatType.Lossy; + webpMetadata.ColorType = WebpColorType.Yuv; + return info; + case WebpChunkType.Vp8L: + info = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); + webpMetadata.FileFormat = WebpFileFormatType.Lossless; + webpMetadata.ColorType = info.Features?.Alpha == true ? WebpColorType.Rgba : WebpColorType.Rgb; + return info; + case WebpChunkType.Vp8X: + info = WebpChunkParsingUtils.ReadVp8XHeader(stream, buffer, features); + while (stream.Position < stream.Length) + { + chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); + if (chunkType == WebpChunkType.Vp8) + { + info = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); + webpMetadata.FileFormat = WebpFileFormatType.Lossy; + webpMetadata.ColorType = info.Features?.Alpha == true ? WebpColorType.Rgba : WebpColorType.Rgb; + } + else if (chunkType == WebpChunkType.Vp8L) + { + info = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); + webpMetadata.FileFormat = WebpFileFormatType.Lossless; + webpMetadata.ColorType = info.Features?.Alpha == true ? WebpColorType.Rgba : WebpColorType.Rgb; + } + else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType)) + { + // ANIM chunks appear before EXIF and XMP chunks. + // Return after parsing an ANIM chunk - The animated decoder will handle the rest. + bool isAnimationChunk = this.ParseOptionalExtendedChunks(stream, metadata, chunkType, features, ignoreAlpha, buffer); + if (isAnimationChunk) + { + return info; + } + } + else + { + // Ignore unknown chunks. + // These must always fall after the image data so we are safe to always skip them. + uint chunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer, false); + stream.Skip((int)chunkSize); + } + } + + return info; + default: + WebpThrowHelper.ThrowImageFormatException("Unrecognized VP8 header"); + return + new WebpImageInfo(); // this return will never be reached, because throw helper will throw an exception. + } + } + + /// + /// Parses optional VP8X chunks, which can be ICCP, XMP, ANIM or ALPH chunks. + /// + /// The stream to decode from. + /// The image metadata. + /// The chunk type. + /// The webp image features. + /// For identify, the alpha data should not be read. + /// Temporary buffer. + /// true, if its a alpha chunk. + private bool ParseOptionalExtendedChunks( + BufferedReadStream stream, + ImageMetadata metadata, + WebpChunkType chunkType, + WebpFeatures features, + bool ignoreAlpha, + Span buffer) + { + bool ignoreMetadata = this.skipMetadata; + switch (chunkType) + { + case WebpChunkType.Iccp: + + // While ICC profiles are optional, an invalid ICC profile cannot be ignored because it must + // precede the image data, and we cannot safely skip it without successfully reading its size. + WebpChunkParsingUtils.ReadIccProfile(stream, metadata, ignoreMetadata); + break; + + case WebpChunkType.Exif: + this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata)); + break; + + case WebpChunkType.Xmp: + this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata)); + break; + + case WebpChunkType.AnimationParameter: + ReadAnimationParameters(stream, features, buffer); + return true; + + case WebpChunkType.Alpha: + this.ReadAlphaData(stream, features, ignoreAlpha, buffer); + break; + default: + + // Specification explicitly states to ignore unknown chunks. + // We do not support writing these chunks at present. + break; + } + + return false; + } + + /// + /// Reads the optional metadata EXIF of XMP profiles, which can follow the image data. + /// + /// The stream to decode from. + /// The image metadata. + /// The webp features. + /// Temporary buffer. + private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metadata, WebpFeatures features, Span buffer) + { + bool ignoreMetadata = this.skipMetadata; + + if (ignoreMetadata || (!features.ExifProfile && !features.XmpMetaData)) + { + return; + } + + long streamLength = stream.Length; + while (stream.Position < streamLength) + { + // Read chunk header. + WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); + if (chunkType == WebpChunkType.Exif && metadata.ExifProfile == null) + { + this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata)); + } + else if (chunkType == WebpChunkType.Xmp && metadata.XmpProfile == null) + { + this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata)); + } + else + { + // Skip duplicate XMP or EXIF chunk. + uint chunkLength = WebpChunkParsingUtils.ReadChunkSize(stream, buffer, false); + stream.Skip((int)chunkLength); + } + } + } + + /// + /// Reads the animation parameters chunk from the stream. + /// + /// The stream to decode from. + /// The webp features. + /// Temporary buffer. + private static void ReadAnimationParameters(BufferedReadStream stream, WebpFeatures features, Span buffer) + { + features.Animation = true; + uint animationChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer); + byte blue = (byte)stream.ReadByte(); + byte green = (byte)stream.ReadByte(); + byte red = (byte)stream.ReadByte(); + byte alpha = (byte)stream.ReadByte(); + features.AnimationBackgroundColor = Color.FromPixel(new Rgba32(red, green, blue, alpha)); + int bytesRead = stream.Read(buffer, 0, 2); + if (bytesRead != 2) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the animation loop count"); + } + + features.AnimationLoopCount = BinaryPrimitives.ReadUInt16LittleEndian(buffer); + } + + /// + /// Reads the alpha data chunk data from the stream. + /// + /// The stream to decode from. + /// The features. + /// if set to true, skips the chunk data. + /// Temporary buffer. + private void ReadAlphaData(BufferedReadStream stream, WebpFeatures features, bool ignoreAlpha, Span buffer) + { + uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer); + if (ignoreAlpha) + { + stream.Skip((int)alphaChunkSize); + return; + } + + features.AlphaChunkHeader = (byte)stream.ReadByte(); + int alphaDataSize = (int)(alphaChunkSize - 1); + this.alphaData = this.memoryAllocator.Allocate(alphaDataSize); + Span alphaData = this.alphaData.GetSpan(); + int bytesRead = stream.Read(alphaData, 0, alphaDataSize); + if (bytesRead != alphaDataSize) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the alpha data from the stream"); + } + } + + /// + public void Dispose() => this.alphaData?.Dispose(); + } +} diff --git a/ImageSharp/Formats/Webp/WebpDecoderOptions.cs b/ImageSharp/Formats/Webp/WebpDecoderOptions.cs new file mode 100644 index 0000000..4bd4ba7 --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpDecoderOptions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Configuration options for decoding webp images. + /// + public sealed class WebpDecoderOptions : ISpecializedDecoderOptions + { + /// + public DecoderOptions GeneralOptions { get; init; } = new(); + + /// + /// Gets the flag to decide how to handle the background color Animation Chunk. + /// The specification is vague on how to handle the background color of the animation chunk. + /// This option let's the user choose how to deal with it. + /// + /// + public BackgroundColorHandling BackgroundColorHandling { get; init; } = BackgroundColorHandling.Standard; + } +} diff --git a/ImageSharp/Formats/Webp/WebpEncoder.cs b/ImageSharp/Formats/Webp/WebpEncoder.cs new file mode 100644 index 0000000..1151940 --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpEncoder.cs @@ -0,0 +1,90 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Image encoder for writing an image to a stream in the Webp format. + /// + public sealed class WebpEncoder : AnimatedImageEncoder + { + /// + /// Initializes a new instance of the class. + /// + public WebpEncoder() + + // Match the default behavior of the native reference encoder. + => this.TransparentColorMode = TransparentColorMode.Clear; + + /// + /// Gets the webp file format used. Either lossless or lossy. + /// Defaults to lossy. + /// + public WebpFileFormatType? FileFormat { get; init; } + + /// + /// Gets the compression quality. Between 0 and 100. + /// For lossy, 0 gives the smallest size and 100 the largest. For lossless, + /// this parameter is the amount of effort put into the compression: 0 is the fastest but gives larger + /// files compared to the slowest, but best, 100. + /// Defaults to 75. + /// + public int Quality { get; init; } = 75; + + /// + /// Gets the encoding method to use. Its a quality/speed trade-off (0=fast, 6=slower-better). + /// Defaults to 4. + /// + public WebpEncodingMethod Method { get; init; } = WebpEncodingMethod.Default; + + /// + /// Gets a value indicating whether the alpha plane should be compressed with Webp lossless format. + /// Defaults to true. + /// + public bool UseAlphaCompression { get; init; } = true; + + /// + /// Gets the number of entropy-analysis passes (in [1..10]). + /// Defaults to 1. + /// + public int EntropyPasses { get; init; } = 1; + + /// + /// Gets the amplitude of the spatial noise shaping. Spatial noise shaping (or sns for short) refers to a general collection of built-in algorithms + /// used to decide which area of the picture should use relatively less bits, and where else to better transfer these bits. + /// The possible range goes from 0 (algorithm is off) to 100 (the maximal effect). + /// Defaults to 50. + /// + public int SpatialNoiseShaping { get; init; } = 50; + + /// + /// Gets the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering). + /// A value of 0 will turn off any filtering. Higher value will increase the strength of the filtering process applied after decoding the picture. + /// The higher the value the smoother the picture will appear. + /// Typical values are usually in the range of 20 to 50. + /// Defaults to 60. + /// + public int FilterStrength { get; init; } = 60; + + /// + /// Gets a value indicating whether near lossless mode should be used. + /// This option adjusts pixel values to help compressibility, but has minimal impact on the visual quality. + /// + public bool NearLossless { get; init; } + + /// + /// Gets the quality of near-lossless image preprocessing. The range is 0 (maximum preprocessing) to 100 (no preprocessing, the default). + /// The typical value is around 60. Note that lossy with -q 100 can at times yield better results. + /// + public int NearLosslessQuality { get; init; } = 100; + + /// + protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) + { + WebpEncoderCore encoder = new(this, image.Configuration); + encoder.Encode(image, stream, cancellationToken); + } + } +} diff --git a/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/ImageSharp/Formats/Webp/WebpEncoderCore.cs new file mode 100644 index 0000000..f5eb1de --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -0,0 +1,323 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Webp.Chunks; +using SixLabors.ImageSharp.Formats.Webp.Lossless; +using SixLabors.ImageSharp.Formats.Webp.Lossy; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Image encoder for writing an image to a stream in the Webp format. + /// + internal sealed class WebpEncoderCore + { + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// Indicating whether the alpha plane should be compressed with Webp lossless format. + /// Defaults to true. + /// + private readonly bool alphaCompression; + + /// + /// Compression quality. Between 0 and 100. + /// + private readonly uint quality; + + /// + /// Quality/speed trade-off (0=fast, 6=slower-better). + /// + private readonly WebpEncodingMethod method; + + /// + /// The number of entropy-analysis passes (in [1..10]). + /// + private readonly int entropyPasses; + + /// + /// Spatial Noise Shaping. 0=off, 100=maximum. + /// + private readonly int spatialNoiseShaping; + + /// + /// The filter the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering). + /// + private readonly int filterStrength; + + /// + /// Flag indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible + /// RGB information for better compression. + /// + private readonly TransparentColorMode transparentColorMode; + + /// + /// Whether to skip metadata during encoding. + /// + private readonly bool skipMetadata; + + /// + /// Indicating whether near lossless mode should be used. + /// + private readonly bool nearLossless; + + /// + /// The near lossless quality. The range is 0 (maximum preprocessing) to 100 (no preprocessing, the default). + /// + private readonly int nearLosslessQuality; + + /// + /// Indicating what file format compression should be used. + /// Defaults to lossy. + /// + private readonly WebpFileFormatType? fileFormat; + + /// + /// The default background color of the canvas when animating. + /// This color may be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when a frame disposal mode is . + /// + private readonly Color? backgroundColor; + + /// + /// The number of times any animation is repeated. + /// + private readonly ushort? repeatCount; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The encoder with options. + /// The global configuration. + public WebpEncoderCore(WebpEncoder encoder, Configuration configuration) + { + this.configuration = configuration; + this.memoryAllocator = configuration.MemoryAllocator; + this.alphaCompression = encoder.UseAlphaCompression; + this.fileFormat = encoder.FileFormat; + this.quality = (uint)encoder.Quality; + this.method = encoder.Method; + this.entropyPasses = encoder.EntropyPasses; + this.spatialNoiseShaping = encoder.SpatialNoiseShaping; + this.filterStrength = encoder.FilterStrength; + this.transparentColorMode = encoder.TransparentColorMode; + this.skipMetadata = encoder.SkipMetadata; + this.nearLossless = encoder.NearLossless; + this.nearLosslessQuality = encoder.NearLosslessQuality; + this.backgroundColor = encoder.BackgroundColor; + this.repeatCount = encoder.RepeatCount; + } + + /// + /// Encodes the image as webp to the specified stream. + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to monitor for cancellation requests. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + if (image.Width > WebpConstants.MaxDimension || image.Height > WebpConstants.MaxDimension) + { + WebpThrowHelper.ThrowDimensionsTooLarge(image.Width, image.Height); + } + + bool lossless; + if (this.fileFormat is not null) + { + lossless = this.fileFormat == WebpFileFormatType.Lossless; + } + else + { + WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata(); + lossless = webpMetadata.FileFormat == WebpFileFormatType.Lossless; + } + + if (lossless) + { + bool hasAnimation = image.Frames.Count > 1; + + using Vp8LEncoder encoder = new( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.transparentColorMode, + this.nearLossless, + this.nearLosslessQuality); + + long initialPosition = stream.Position; + bool hasAlpha = false; + WebpVp8X vp8x = encoder.EncodeHeader(image, stream, hasAnimation, this.repeatCount); + + // Encode the first frame. + ImageFrame previousFrame = image.Frames.RootFrame; + WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata(); + + cancellationToken.ThrowIfCancellationRequested(); + + hasAlpha |= encoder.Encode(previousFrame, previousFrame.Bounds, frameMetadata, stream, hasAnimation); + + if (hasAnimation) + { + FrameDisposalMode previousDisposal = frameMetadata.DisposalMode; + + // Encode additional frames + // This frame is reused to store de-duplicated pixel buffers. + using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size); + + for (int i = 1; i < image.Frames.Count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; + ImageFrame currentFrame = image.Frames[i]; + ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; + + frameMetadata = currentFrame.Metadata.GetWebpMetadata(); + bool blend = frameMetadata.BlendMode == FrameBlendMode.Over; + Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground + ? this.backgroundColor ?? Color.Transparent + : Color.Transparent; + + (bool difference, Rectangle bounds) = + AnimationUtilities.DeDuplicatePixels( + image.Configuration, + prev, + currentFrame, + nextFrame, + encodingFrame, + background, + blend, + ClampingMode.Even); + + using Vp8LEncoder animatedEncoder = new( + this.memoryAllocator, + this.configuration, + bounds.Width, + bounds.Height, + this.quality, + this.skipMetadata, + this.method, + this.transparentColorMode, + this.nearLossless, + this.nearLosslessQuality); + + hasAlpha |= animatedEncoder.Encode(encodingFrame, bounds, frameMetadata, stream, hasAnimation); + + previousFrame = currentFrame; + previousDisposal = frameMetadata.DisposalMode; + } + } + + encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition); + } + else + { + using Vp8Encoder encoder = new( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.entropyPasses, + this.filterStrength, + this.spatialNoiseShaping, + this.alphaCompression); + + long initialPosition = stream.Position; + bool hasAlpha = false; + WebpVp8X vp8x = default; + if (image.Frames.Count > 1) + { + // The alpha flag is updated following encoding. + vp8x = encoder.EncodeHeader(image, stream, false, true); + + // Encode the first frame. + ImageFrame previousFrame = image.Frames.RootFrame; + WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata(); + FrameDisposalMode previousDisposal = frameMetadata.DisposalMode; + + hasAlpha |= encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds, frameMetadata); + + // Encode additional frames + // This frame is reused to store de-duplicated pixel buffers. + using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size); + + for (int i = 1; i < image.Frames.Count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; + ImageFrame currentFrame = image.Frames[i]; + ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; + + frameMetadata = currentFrame.Metadata.GetWebpMetadata(); + bool blend = frameMetadata.BlendMode == FrameBlendMode.Over; + Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground + ? this.backgroundColor ?? Color.Transparent + : Color.Transparent; + + (bool difference, Rectangle bounds) = + AnimationUtilities.DeDuplicatePixels( + image.Configuration, + prev, + currentFrame, + nextFrame, + encodingFrame, + background, + blend, + ClampingMode.Even); + + using Vp8Encoder animatedEncoder = new( + this.memoryAllocator, + this.configuration, + bounds.Width, + bounds.Height, + this.quality, + this.skipMetadata, + this.method, + this.entropyPasses, + this.filterStrength, + this.spatialNoiseShaping, + this.alphaCompression); + + hasAlpha |= animatedEncoder.EncodeAnimation(encodingFrame, stream, bounds, frameMetadata); + + previousFrame = currentFrame; + previousDisposal = frameMetadata.DisposalMode; + } + + encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition); + } + else + { + cancellationToken.ThrowIfCancellationRequested(); + encoder.EncodeStatic(stream, image); + encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition); + } + } + } + } +} diff --git a/ImageSharp/Formats/Webp/WebpEncodingMethod.cs b/ImageSharp/Formats/Webp/WebpEncodingMethod.cs new file mode 100644 index 0000000..4b27b87 --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpEncodingMethod.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Quality/speed trade-off for the encoding process (0=fast, 6=slower-better). + /// + public enum WebpEncodingMethod + { + /// + /// Fastest, but quality compromise. Equivalent to . + /// + Level0 = 0, + + /// + /// Fastest, but quality compromise. + /// + Fastest = Level0, + + /// + /// Level1. + /// + Level1 = 1, + + /// + /// Level 2. + /// + Level2 = 2, + + /// + /// Level 3. + /// + Level3 = 3, + + /// + /// Level 4. Equivalent to . + /// + Level4 = 4, + + /// + /// BestQuality trade off between speed and quality. + /// + Default = Level4, + + /// + /// Level 5. + /// + Level5 = 5, + + /// + /// Slowest option, but best quality. Equivalent to . + /// + Level6 = 6, + + /// + /// Slowest option, but best quality. + /// + BestQuality = Level6 + } +} diff --git a/ImageSharp/Formats/Webp/WebpFeatures.cs b/ImageSharp/Formats/Webp/WebpFeatures.cs new file mode 100644 index 0000000..c3e37e3 --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpFeatures.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Image features of a VP8X image. + /// + internal class WebpFeatures + { + /// + /// Gets or sets a value indicating whether this image has an ICC Profile. + /// + public bool IccProfile { get; set; } + + /// + /// Gets or sets a value indicating whether this image has an alpha channel. + /// + public bool Alpha { get; set; } + + /// + /// Gets or sets the alpha chunk header. + /// + public byte AlphaChunkHeader { get; set; } + + /// + /// Gets or sets a value indicating whether this image has an EXIF Profile. + /// + public bool ExifProfile { get; set; } + + /// + /// Gets or sets a value indicating whether this image has XMP Metadata. + /// + public bool XmpMetaData { get; set; } + + /// + /// Gets or sets a value indicating whether this image is an animation. + /// + public bool Animation { get; set; } + + /// + /// Gets or sets the animation loop count. 0 means infinitely. + /// + public ushort AnimationLoopCount { get; set; } + + /// + /// Gets or sets default background color of the animation frame canvas. + /// This color MAY be used to fill the unused space on the canvas around the frames, as well as the transparent pixels of the first frame.. + /// + public Color? AnimationBackgroundColor { get; set; } + } +} diff --git a/ImageSharp/Formats/Webp/WebpFileFormatType.cs b/ImageSharp/Formats/Webp/WebpFileFormatType.cs new file mode 100644 index 0000000..fcbc67d --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpFileFormatType.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Info about the webp file format used. + /// + public enum WebpFileFormatType + { + /// + /// The lossless Webp format, which compresses data without any loss of information. + /// + Lossless, + + /// + /// The lossy Webp format, which compresses data by discarding some of it. + /// + Lossy, + } +} diff --git a/ImageSharp/Formats/Webp/WebpFormat.cs b/ImageSharp/Formats/Webp/WebpFormat.cs new file mode 100644 index 0000000..af727a9 --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpFormat.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Registers the image encoders, decoders and mime type detectors for the Webp format. + /// + public sealed class WebpFormat : IImageFormat + { + private WebpFormat() + { + } + + /// + /// Gets the shared instance. + /// + public static WebpFormat Instance { get; } = new(); + + /// + public string Name => "WEBP"; + + /// + public string DefaultMimeType => "image/webp"; + + /// + public IEnumerable MimeTypes => WebpConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => WebpConstants.FileExtensions; + + /// + public WebpMetadata CreateDefaultFormatMetadata() => new(); + + /// + public WebpFrameMetadata CreateDefaultFormatFrameMetadata() => new(); + } +} diff --git a/ImageSharp/Formats/Webp/WebpFrameMetadata.cs b/ImageSharp/Formats/Webp/WebpFrameMetadata.cs new file mode 100644 index 0000000..9f16ecc --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpFrameMetadata.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Provides webp specific metadata information for the image frame. + /// + public class WebpFrameMetadata : IFormatFrameMetadata + { + /// + /// Initializes a new instance of the class. + /// + public WebpFrameMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private WebpFrameMetadata(WebpFrameMetadata other) + { + this.FrameDelay = other.FrameDelay; + this.DisposalMode = other.DisposalMode; + this.BlendMode = other.BlendMode; + } + + /// + /// Gets or sets how transparent pixels of the current frame are to be blended with corresponding pixels + /// of the previous canvas. + /// + public FrameBlendMode BlendMode { get; set; } + + /// + /// Gets or sets how the current frame is to be treated after it has been displayed + /// (before rendering the next frame) on the canvas. + /// + public FrameDisposalMode DisposalMode { get; set; } + + /// + /// Gets or sets the frame duration. The time to wait before displaying the next frame, + /// in 1 millisecond units. Note the interpretation of frame duration of 0 (and often smaller and equal to 10) is implementation defined. + /// + public uint FrameDelay { get; set; } + + /// + public static WebpFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) + => new() + { + FrameDelay = (uint)metadata.Duration.TotalMilliseconds, + BlendMode = metadata.BlendMode, + DisposalMode = GetMode(metadata.DisposalMode) + }; + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + => new() + { + ColorTableMode = FrameColorTableMode.Global, + Duration = TimeSpan.FromMilliseconds(this.FrameDelay), + DisposalMode = this.DisposalMode, + BlendMode = this.BlendMode, + }; + + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public WebpFrameMetadata DeepClone() => new(this); + + private static FrameDisposalMode GetMode(FrameDisposalMode mode) => mode switch + { + FrameDisposalMode.RestoreToBackground => FrameDisposalMode.RestoreToBackground, + FrameDisposalMode.DoNotDispose => FrameDisposalMode.DoNotDispose, + _ => FrameDisposalMode.DoNotDispose, + }; + } +} diff --git a/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs b/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs new file mode 100644 index 0000000..a384679 --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Detects Webp file headers. + /// + public sealed class WebpImageFormatDetector : IImageFormatDetector + { + /// + public int HeaderSize => 12; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.IsSupportedFileFormat(header) ? WebpFormat.Instance : null; + return format != null; + } + + private bool IsSupportedFileFormat(ReadOnlySpan header) + => header.Length >= this.HeaderSize && IsRiffContainer(header) && IsWebpFile(header); + + /// + /// Checks, if the header starts with a valid RIFF FourCC. + /// + /// The header bytes. + /// True, if its a valid RIFF FourCC. + private static bool IsRiffContainer(ReadOnlySpan header) + => header[..4].SequenceEqual(WebpConstants.RiffFourCc); + + /// + /// Checks if 'WEBP' is present in the header. + /// + /// The header bytes. + /// True, if its a webp file. + private static bool IsWebpFile(ReadOnlySpan header) + => header.Slice(8, 4).SequenceEqual(WebpConstants.WebpHeader); + } +} diff --git a/ImageSharp/Formats/Webp/WebpImageInfo.cs b/ImageSharp/Formats/Webp/WebpImageInfo.cs new file mode 100644 index 0000000..01d5903 --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpImageInfo.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Webp.BitReader; +using SixLabors.ImageSharp.Formats.Webp.Lossy; +using System; + +namespace SixLabors.ImageSharp.Formats.Webp { + internal class WebpImageInfo : IDisposable + { + /// + /// Gets or sets the size of the encoded image data in bytes. + /// + public uint DataSize { get; set; } + + /// + /// Gets or sets the bitmap width in pixels. + /// + public uint Width { get; set; } + + /// + /// Gets or sets the bitmap height in pixels. + /// + public uint Height { get; set; } + + /// + /// Gets or sets the horizontal scale. + /// + public sbyte XScale { get; set; } + + /// + /// Gets or sets the vertical scale. + /// + public sbyte YScale { get; set; } + + /// + /// Gets or sets the bits per pixel. + /// + public WebpBitsPerPixel BitsPerPixel { get; set; } + + /// + /// Gets or sets a value indicating whether this image uses lossless compression. + /// + public bool IsLossless { get; set; } + + /// + /// Gets or sets additional features present in a VP8X image. + /// + public WebpFeatures? Features { get; set; } + + /// + /// Gets or sets the VP8 profile / version. Valid values are between 0 and 3. Default value will be the invalid value -1. + /// + public int Vp8Profile { get; set; } = -1; + + /// + /// Gets or sets the VP8 frame header. + /// + public Vp8FrameHeader? Vp8FrameHeader { get; set; } + + /// + /// Gets or sets the VP8L bitreader. Will be , if its not a lossless image. + /// + public Vp8LBitReader? Vp8LBitReader { get; set; } + + /// + /// Gets or sets the VP8 bitreader. Will be , if its not a lossy image. + /// + public Vp8BitReader? Vp8BitReader { get; set; } + + /// + public void Dispose() + { + this.Vp8BitReader?.Dispose(); + this.Vp8LBitReader?.Dispose(); + } + } +} diff --git a/ImageSharp/Formats/Webp/WebpLookupTables.cs b/ImageSharp/Formats/Webp/WebpLookupTables.cs new file mode 100644 index 0000000..5f9e27d --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpLookupTables.cs @@ -0,0 +1,1674 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Webp { +#pragma warning disable SA1201 // Elements should appear in the correct order + internal static class WebpLookupTables + { + public static readonly byte[,][] ModesProba = new byte[10, 10][]; + + public static readonly ushort[] GammaToLinearTab = new ushort[256]; + + public static readonly int[] LinearToGammaTab = new int[WebpConstants.GammaTabSize + 1]; + + public static readonly short[,][] Vp8FixedCostsI4 = new short[10, 10][]; + + // Compute susceptibility based on DCT-coeff histograms: + // the higher, the "easier" the macroblock is to compress. + public static readonly int[] Vp8DspScan = + [ + + // Luma + 0 + (0 * WebpConstants.Bps), 4 + (0 * WebpConstants.Bps), 8 + (0 * WebpConstants.Bps), 12 + (0 * WebpConstants.Bps), + 0 + (4 * WebpConstants.Bps), 4 + (4 * WebpConstants.Bps), 8 + (4 * WebpConstants.Bps), 12 + (4 * WebpConstants.Bps), + 0 + (8 * WebpConstants.Bps), 4 + (8 * WebpConstants.Bps), 8 + (8 * WebpConstants.Bps), 12 + (8 * WebpConstants.Bps), + 0 + (12 * WebpConstants.Bps), 4 + (12 * WebpConstants.Bps), 8 + (12 * WebpConstants.Bps), 12 + (12 * WebpConstants.Bps), + + 0 + (0 * WebpConstants.Bps), 4 + (0 * WebpConstants.Bps), 0 + (4 * WebpConstants.Bps), 4 + (4 * WebpConstants.Bps), // U + 8 + (0 * WebpConstants.Bps), 12 + (0 * WebpConstants.Bps), 8 + (4 * WebpConstants.Bps), 12 + (4 * WebpConstants.Bps) // V + ]; + + public static readonly short[] Vp8Scan = + [ + + // Luma + 0 + (0 * WebpConstants.Bps), 4 + (0 * WebpConstants.Bps), 8 + (0 * WebpConstants.Bps), 12 + (0 * WebpConstants.Bps), + 0 + (4 * WebpConstants.Bps), 4 + (4 * WebpConstants.Bps), 8 + (4 * WebpConstants.Bps), 12 + (4 * WebpConstants.Bps), + 0 + (8 * WebpConstants.Bps), 4 + (8 * WebpConstants.Bps), 8 + (8 * WebpConstants.Bps), 12 + (8 * WebpConstants.Bps), + 0 + (12 * WebpConstants.Bps), 4 + (12 * WebpConstants.Bps), 8 + (12 * WebpConstants.Bps), 12 + (12 * WebpConstants.Bps) + ]; + + public static readonly short[] Vp8ScanUv = + [ + 0 + (0 * WebpConstants.Bps), 4 + (0 * WebpConstants.Bps), 0 + (4 * WebpConstants.Bps), 4 + (4 * WebpConstants.Bps), // U + 8 + (0 * WebpConstants.Bps), 12 + (0 * WebpConstants.Bps), 8 + (4 * WebpConstants.Bps), 12 + (4 * WebpConstants.Bps) // V + ]; + + [MethodImpl(InliningOptions.ShortMethod)] + public static byte Abs0(int x) => Abs0Table[x + 255]; + + [MethodImpl(InliningOptions.ShortMethod)] + public static sbyte Sclip1(int x) => Sclip1Table[x + 1020]; + + [MethodImpl(InliningOptions.ShortMethod)] + public static sbyte Sclip2(int x) => Sclip2Table[x + 112]; + + [MethodImpl(InliningOptions.ShortMethod)] + public static byte Clip1(int x) => Clip1Table[x + 255]; + + // fixed costs for coding levels, deduce from the coding tree. + // This is only the part that doesn't depend on the probability state. + public static readonly short[] Vp8LevelFixedCosts = + [ + 0, 256, 256, 256, 256, 432, 618, 630, 731, 640, 640, 828, 901, 948, 1021, 1101, 1174, 1221, 1294, 1042, + 1085, 1115, 1158, 1202, 1245, 1275, 1318, 1337, 1380, 1410, 1453, 1497, 1540, 1570, 1613, 1280, 1295, + 1317, 1332, 1358, 1373, 1395, 1410, 1454, 1469, 1491, 1506, 1532, 1547, 1569, 1584, 1601, 1616, 1638, + 1653, 1679, 1694, 1716, 1731, 1775, 1790, 1812, 1827, 1853, 1868, 1890, 1905, 1727, 1733, 1742, 1748, + 1759, 1765, 1774, 1780, 1800, 1806, 1815, 1821, 1832, 1838, 1847, 1853, 1878, 1884, 1893, 1899, 1910, + 1916, 1925, 1931, 1951, 1957, 1966, 1972, 1983, 1989, 1998, 2004, 2027, 2033, 2042, 2048, 2059, 2065, + 2074, 2080, 2100, 2106, 2115, 2121, 2132, 2138, 2147, 2153, 2178, 2184, 2193, 2199, 2210, 2216, 2225, + 2231, 2251, 2257, 2266, 2272, 2283, 2289, 2298, 2304, 2168, 2174, 2183, 2189, 2200, 2206, 2215, 2221, + 2241, 2247, 2256, 2262, 2273, 2279, 2288, 2294, 2319, 2325, 2334, 2340, 2351, 2357, 2366, 2372, 2392, + 2398, 2407, 2413, 2424, 2430, 2439, 2445, 2468, 2474, 2483, 2489, 2500, 2506, 2515, 2521, 2541, 2547, + 2556, 2562, 2573, 2579, 2588, 2594, 2619, 2625, 2634, 2640, 2651, 2657, 2666, 2672, 2692, 2698, 2707, + 2713, 2724, 2730, 2739, 2745, 2540, 2546, 2555, 2561, 2572, 2578, 2587, 2593, 2613, 2619, 2628, 2634, + 2645, 2651, 2660, 2666, 2691, 2697, 2706, 2712, 2723, 2729, 2738, 2744, 2764, 2770, 2779, 2785, 2796, + 2802, 2811, 2817, 2840, 2846, 2855, 2861, 2872, 2878, 2887, 2893, 2913, 2919, 2928, 2934, 2945, 2951, + 2960, 2966, 2991, 2997, 3006, 3012, 3023, 3029, 3038, 3044, 3064, 3070, 3079, 3085, 3096, 3102, 3111, + 3117, 2981, 2987, 2996, 3002, 3013, 3019, 3028, 3034, 3054, 3060, 3069, 3075, 3086, 3092, 3101, 3107, + 3132, 3138, 3147, 3153, 3164, 3170, 3179, 3185, 3205, 3211, 3220, 3226, 3237, 3243, 3252, 3258, 3281, + 3287, 3296, 3302, 3313, 3319, 3328, 3334, 3354, 3360, 3369, 3375, 3386, 3392, 3401, 3407, 3432, 3438, + 3447, 3453, 3464, 3470, 3479, 3485, 3505, 3511, 3520, 3526, 3537, 3543, 3552, 3558, 2816, 2822, 2831, + 2837, 2848, 2854, 2863, 2869, 2889, 2895, 2904, 2910, 2921, 2927, 2936, 2942, 2967, 2973, 2982, 2988, + 2999, 3005, 3014, 3020, 3040, 3046, 3055, 3061, 3072, 3078, 3087, 3093, 3116, 3122, 3131, 3137, 3148, + 3154, 3163, 3169, 3189, 3195, 3204, 3210, 3221, 3227, 3236, 3242, 3267, 3273, 3282, 3288, 3299, 3305, + 3314, 3320, 3340, 3346, 3355, 3361, 3372, 3378, 3387, 3393, 3257, 3263, 3272, 3278, 3289, 3295, 3304, + 3310, 3330, 3336, 3345, 3351, 3362, 3368, 3377, 3383, 3408, 3414, 3423, 3429, 3440, 3446, 3455, 3461, + 3481, 3487, 3496, 3502, 3513, 3519, 3528, 3534, 3557, 3563, 3572, 3578, 3589, 3595, 3604, 3610, 3630, + 3636, 3645, 3651, 3662, 3668, 3677, 3683, 3708, 3714, 3723, 3729, 3740, 3746, 3755, 3761, 3781, 3787, + 3796, 3802, 3813, 3819, 3828, 3834, 3629, 3635, 3644, 3650, 3661, 3667, 3676, 3682, 3702, 3708, 3717, + 3723, 3734, 3740, 3749, 3755, 3780, 3786, 3795, 3801, 3812, 3818, 3827, 3833, 3853, 3859, 3868, 3874, + 3885, 3891, 3900, 3906, 3929, 3935, 3944, 3950, 3961, 3967, 3976, 3982, 4002, 4008, 4017, 4023, 4034, + 4040, 4049, 4055, 4080, 4086, 4095, 4101, 4112, 4118, 4127, 4133, 4153, 4159, 4168, 4174, 4185, 4191, + 4200, 4206, 4070, 4076, 4085, 4091, 4102, 4108, 4117, 4123, 4143, 4149, 4158, 4164, 4175, 4181, 4190, + 4196, 4221, 4227, 4236, 4242, 4253, 4259, 4268, 4274, 4294, 4300, 4309, 4315, 4326, 4332, 4341, 4347, + 4370, 4376, 4385, 4391, 4402, 4408, 4417, 4423, 4443, 4449, 4458, 4464, 4475, 4481, 4490, 4496, 4521, + 4527, 4536, 4542, 4553, 4559, 4568, 4574, 4594, 4600, 4609, 4615, 4626, 4632, 4641, 4647, 3515, 3521, + 3530, 3536, 3547, 3553, 3562, 3568, 3588, 3594, 3603, 3609, 3620, 3626, 3635, 3641, 3666, 3672, 3681, + 3687, 3698, 3704, 3713, 3719, 3739, 3745, 3754, 3760, 3771, 3777, 3786, 3792, 3815, 3821, 3830, 3836, + 3847, 3853, 3862, 3868, 3888, 3894, 3903, 3909, 3920, 3926, 3935, 3941, 3966, 3972, 3981, 3987, 3998, + 4004, 4013, 4019, 4039, 4045, 4054, 4060, 4071, 4077, 4086, 4092, 3956, 3962, 3971, 3977, 3988, 3994, + 4003, 4009, 4029, 4035, 4044, 4050, 4061, 4067, 4076, 4082, 4107, 4113, 4122, 4128, 4139, 4145, 4154, + 4160, 4180, 4186, 4195, 4201, 4212, 4218, 4227, 4233, 4256, 4262, 4271, 4277, 4288, 4294, 4303, 4309, + 4329, 4335, 4344, 4350, 4361, 4367, 4376, 4382, 4407, 4413, 4422, 4428, 4439, 4445, 4454, 4460, 4480, + 4486, 4495, 4501, 4512, 4518, 4527, 4533, 4328, 4334, 4343, 4349, 4360, 4366, 4375, 4381, 4401, 4407, + 4416, 4422, 4433, 4439, 4448, 4454, 4479, 4485, 4494, 4500, 4511, 4517, 4526, 4532, 4552, 4558, 4567, + 4573, 4584, 4590, 4599, 4605, 4628, 4634, 4643, 4649, 4660, 4666, 4675, 4681, 4701, 4707, 4716, 4722, + 4733, 4739, 4748, 4754, 4779, 4785, 4794, 4800, 4811, 4817, 4826, 4832, 4852, 4858, 4867, 4873, 4884, + 4890, 4899, 4905, 4769, 4775, 4784, 4790, 4801, 4807, 4816, 4822, 4842, 4848, 4857, 4863, 4874, 4880, + 4889, 4895, 4920, 4926, 4935, 4941, 4952, 4958, 4967, 4973, 4993, 4999, 5008, 5014, 5025, 5031, 5040, + 5046, 5069, 5075, 5084, 5090, 5101, 5107, 5116, 5122, 5142, 5148, 5157, 5163, 5174, 5180, 5189, 5195, + 5220, 5226, 5235, 5241, 5252, 5258, 5267, 5273, 5293, 5299, 5308, 5314, 5325, 5331, 5340, 5346, 4604, + 4610, 4619, 4625, 4636, 4642, 4651, 4657, 4677, 4683, 4692, 4698, 4709, 4715, 4724, 4730, 4755, 4761, + 4770, 4776, 4787, 4793, 4802, 4808, 4828, 4834, 4843, 4849, 4860, 4866, 4875, 4881, 4904, 4910, 4919, + 4925, 4936, 4942, 4951, 4957, 4977, 4983, 4992, 4998, 5009, 5015, 5024, 5030, 5055, 5061, 5070, 5076, + 5087, 5093, 5102, 5108, 5128, 5134, 5143, 5149, 5160, 5166, 5175, 5181, 5045, 5051, 5060, 5066, 5077, + 5083, 5092, 5098, 5118, 5124, 5133, 5139, 5150, 5156, 5165, 5171, 5196, 5202, 5211, 5217, 5228, 5234, + 5243, 5249, 5269, 5275, 5284, 5290, 5301, 5307, 5316, 5322, 5345, 5351, 5360, 5366, 5377, 5383, 5392, + 5398, 5418, 5424, 5433, 5439, 5450, 5456, 5465, 5471, 5496, 5502, 5511, 5517, 5528, 5534, 5543, 5549, + 5569, 5575, 5584, 5590, 5601, 5607, 5616, 5622, 5417, 5423, 5432, 5438, 5449, 5455, 5464, 5470, 5490, + 5496, 5505, 5511, 5522, 5528, 5537, 5543, 5568, 5574, 5583, 5589, 5600, 5606, 5615, 5621, 5641, 5647, + 5656, 5662, 5673, 5679, 5688, 5694, 5717, 5723, 5732, 5738, 5749, 5755, 5764, 5770, 5790, 5796, 5805, + 5811, 5822, 5828, 5837, 5843, 5868, 5874, 5883, 5889, 5900, 5906, 5915, 5921, 5941, 5947, 5956, 5962, + 5973, 5979, 5988, 5994, 5858, 5864, 5873, 5879, 5890, 5896, 5905, 5911, 5931, 5937, 5946, 5952, 5963, + 5969, 5978, 5984, 6009, 6015, 6024, 6030, 6041, 6047, 6056, 6062, 6082, 6088, 6097, 6103, 6114, 6120, + 6129, 6135, 6158, 6164, 6173, 6179, 6190, 6196, 6205, 6211, 6231, 6237, 6246, 6252, 6263, 6269, 6278, + 6284, 6309, 6315, 6324, 6330, 6341, 6347, 6356, 6362, 6382, 6388, 6397, 6403, 6414, 6420, 6429, 6435, + 3515, 3521, 3530, 3536, 3547, 3553, 3562, 3568, 3588, 3594, 3603, 3609, 3620, 3626, 3635, 3641, 3666, + 3672, 3681, 3687, 3698, 3704, 3713, 3719, 3739, 3745, 3754, 3760, 3771, 3777, 3786, 3792, 3815, 3821, + 3830, 3836, 3847, 3853, 3862, 3868, 3888, 3894, 3903, 3909, 3920, 3926, 3935, 3941, 3966, 3972, 3981, + 3987, 3998, 4004, 4013, 4019, 4039, 4045, 4054, 4060, 4071, 4077, 4086, 4092, 3956, 3962, 3971, 3977, + 3988, 3994, 4003, 4009, 4029, 4035, 4044, 4050, 4061, 4067, 4076, 4082, 4107, 4113, 4122, 4128, 4139, + 4145, 4154, 4160, 4180, 4186, 4195, 4201, 4212, 4218, 4227, 4233, 4256, 4262, 4271, 4277, 4288, 4294, + 4303, 4309, 4329, 4335, 4344, 4350, 4361, 4367, 4376, 4382, 4407, 4413, 4422, 4428, 4439, 4445, 4454, + 4460, 4480, 4486, 4495, 4501, 4512, 4518, 4527, 4533, 4328, 4334, 4343, 4349, 4360, 4366, 4375, 4381, + 4401, 4407, 4416, 4422, 4433, 4439, 4448, 4454, 4479, 4485, 4494, 4500, 4511, 4517, 4526, 4532, 4552, + 4558, 4567, 4573, 4584, 4590, 4599, 4605, 4628, 4634, 4643, 4649, 4660, 4666, 4675, 4681, 4701, 4707, + 4716, 4722, 4733, 4739, 4748, 4754, 4779, 4785, 4794, 4800, 4811, 4817, 4826, 4832, 4852, 4858, 4867, + 4873, 4884, 4890, 4899, 4905, 4769, 4775, 4784, 4790, 4801, 4807, 4816, 4822, 4842, 4848, 4857, 4863, + 4874, 4880, 4889, 4895, 4920, 4926, 4935, 4941, 4952, 4958, 4967, 4973, 4993, 4999, 5008, 5014, 5025, + 5031, 5040, 5046, 5069, 5075, 5084, 5090, 5101, 5107, 5116, 5122, 5142, 5148, 5157, 5163, 5174, 5180, + 5189, 5195, 5220, 5226, 5235, 5241, 5252, 5258, 5267, 5273, 5293, 5299, 5308, 5314, 5325, 5331, 5340, + 5346, 4604, 4610, 4619, 4625, 4636, 4642, 4651, 4657, 4677, 4683, 4692, 4698, 4709, 4715, 4724, 4730, + 4755, 4761, 4770, 4776, 4787, 4793, 4802, 4808, 4828, 4834, 4843, 4849, 4860, 4866, 4875, 4881, 4904, + 4910, 4919, 4925, 4936, 4942, 4951, 4957, 4977, 4983, 4992, 4998, 5009, 5015, 5024, 5030, 5055, 5061, + 5070, 5076, 5087, 5093, 5102, 5108, 5128, 5134, 5143, 5149, 5160, 5166, 5175, 5181, 5045, 5051, 5060, + 5066, 5077, 5083, 5092, 5098, 5118, 5124, 5133, 5139, 5150, 5156, 5165, 5171, 5196, 5202, 5211, 5217, + 5228, 5234, 5243, 5249, 5269, 5275, 5284, 5290, 5301, 5307, 5316, 5322, 5345, 5351, 5360, 5366, 5377, + 5383, 5392, 5398, 5418, 5424, 5433, 5439, 5450, 5456, 5465, 5471, 5496, 5502, 5511, 5517, 5528, 5534, + 5543, 5549, 5569, 5575, 5584, 5590, 5601, 5607, 5616, 5622, 5417, 5423, 5432, 5438, 5449, 5455, 5464, + 5470, 5490, 5496, 5505, 5511, 5522, 5528, 5537, 5543, 5568, 5574, 5583, 5589, 5600, 5606, 5615, 5621, + 5641, 5647, 5656, 5662, 5673, 5679, 5688, 5694, 5717, 5723, 5732, 5738, 5749, 5755, 5764, 5770, 5790, + 5796, 5805, 5811, 5822, 5828, 5837, 5843, 5868, 5874, 5883, 5889, 5900, 5906, 5915, 5921, 5941, 5947, + 5956, 5962, 5973, 5979, 5988, 5994, 5858, 5864, 5873, 5879, 5890, 5896, 5905, 5911, 5931, 5937, 5946, + 5952, 5963, 5969, 5978, 5984, 6009, 6015, 6024, 6030, 6041, 6047, 6056, 6062, 6082, 6088, 6097, 6103, + 6114, 6120, 6129, 6135, 6158, 6164, 6173, 6179, 6190, 6196, 6205, 6211, 6231, 6237, 6246, 6252, 6263, + 6269, 6278, 6284, 6309, 6315, 6324, 6330, 6341, 6347, 6356, 6362, 6382, 6388, 6397, 6403, 6414, 6420, + 6429, 6435, 5303, 5309, 5318, 5324, 5335, 5341, 5350, 5356, 5376, 5382, 5391, 5397, 5408, 5414, 5423, + 5429, 5454, 5460, 5469, 5475, 5486, 5492, 5501, 5507, 5527, 5533, 5542, 5548, 5559, 5565, 5574, 5580, + 5603, 5609, 5618, 5624, 5635, 5641, 5650, 5656, 5676, 5682, 5691, 5697, 5708, 5714, 5723, 5729, 5754, + 5760, 5769, 5775, 5786, 5792, 5801, 5807, 5827, 5833, 5842, 5848, 5859, 5865, 5874, 5880, 5744, 5750, + 5759, 5765, 5776, 5782, 5791, 5797, 5817, 5823, 5832, 5838, 5849, 5855, 5864, 5870, 5895, 5901, 5910, + 5916, 5927, 5933, 5942, 5948, 5968, 5974, 5983, 5989, 6000, 6006, 6015, 6021, 6044, 6050, 6059, 6065, + 6076, 6082, 6091, 6097, 6117, 6123, 6132, 6138, 6149, 6155, 6164, 6170, 6195, 6201, 6210, 6216, 6227, + 6233, 6242, 6248, 6268, 6274, 6283, 6289, 6300, 6306, 6315, 6321, 6116, 6122, 6131, 6137, 6148, 6154, + 6163, 6169, 6189, 6195, 6204, 6210, 6221, 6227, 6236, 6242, 6267, 6273, 6282, 6288, 6299, 6305, 6314, + 6320, 6340, 6346, 6355, 6361, 6372, 6378, 6387, 6393, 6416, 6422, 6431, 6437, 6448, 6454, 6463, 6469, + 6489, 6495, 6504, 6510, 6521, 6527, 6536, 6542, 6567, 6573, 6582, 6588, 6599, 6605, 6614, 6620, 6640, + 6646, 6655, 6661, 6672, 6678, 6687, 6693, 6557, 6563, 6572, 6578, 6589, 6595, 6604, 6610, 6630, 6636, + 6645, 6651, 6662, 6668, 6677, 6683, 6708, 6714, 6723, 6729, 6740, 6746, 6755, 6761, 6781, 6787, 6796, + 6802, 6813, 6819, 6828, 6834, 6857, 6863, 6872, 6878, 6889, 6895, 6904, 6910, 6930, 6936, 6945, 6951, + 6962, 6968, 6977, 6983, 7008, 7014, 7023, 7029, 7040, 7046, 7055, 7061, 7081, 7087, 7096, 7102, 7113, + 7119, 7128, 7134, 6392, 6398, 6407, 6413, 6424, 6430, 6439, 6445, 6465, 6471, 6480, 6486, 6497, 6503, + 6512, 6518, 6543, 6549, 6558, 6564, 6575, 6581, 6590, 6596, 6616, 6622, 6631, 6637, 6648, 6654, 6663, + 6669, 6692, 6698, 6707, 6713, 6724, 6730, 6739, 6745, 6765, 6771, 6780, 6786, 6797, 6803, 6812, 6818, + 6843, 6849, 6858, 6864, 6875, 6881, 6890, 6896, 6916, 6922, 6931, 6937, 6948, 6954, 6963, 6969, 6833, + 6839, 6848, 6854, 6865, 6871, 6880, 6886, 6906, 6912, 6921, 6927, 6938, 6944, 6953, 6959, 6984, 6990, + 6999, 7005, 7016, 7022, 7031, 7037, 7057, 7063, 7072, 7078, 7089, 7095, 7104, 7110, 7133, 7139, 7148, + 7154, 7165, 7171, 7180, 7186, 7206, 7212, 7221, 7227, 7238, 7244, 7253, 7259, 7284, 7290, 7299, 7305, + 7316, 7322, 7331, 7337, 7357, 7363, 7372, 7378, 7389, 7395, 7404, 7410, 7205, 7211, 7220, 7226, 7237, + 7243, 7252, 7258, 7278, 7284, 7293, 7299, 7310, 7316, 7325, 7331, 7356, 7362, 7371, 7377, 7388, 7394, + 7403, 7409, 7429, 7435, 7444, 7450, 7461, 7467, 7476, 7482, 7505, 7511, 7520, 7526, 7537, 7543, 7552, + 7558, 7578, 7584, 7593, 7599, 7610, 7616, 7625, 7631, 7656, 7662, 7671, 7677, 7688, 7694, 7703, 7709, + 7729, 7735, 7744, 7750, 7761 + ]; + + // This table gives, for a given sharpness, the filtering strength to be + // used (at least) in order to filter a given edge step delta. + public static readonly byte[,] LevelsFromDelta = + { + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63 + }, + { + 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 17, 18, + 20, 21, 23, 24, 26, 27, 29, 30, 32, 33, 35, 36, 38, 39, 41, 42, + 44, 45, 47, 48, 50, 51, 53, 54, 56, 57, 59, 60, 62, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 16, 17, 19, + 20, 22, 23, 25, 26, 28, 29, 31, 32, 34, 35, 37, 38, 40, 41, 43, + 44, 46, 47, 49, 50, 52, 53, 55, 56, 58, 59, 61, 62, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 15, 16, 18, 19, + 21, 22, 24, 25, 27, 28, 30, 31, 33, 34, 36, 37, 39, 40, 42, 43, + 45, 46, 48, 49, 51, 52, 54, 55, 57, 58, 60, 61, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 14, 15, 17, 18, 20, + 21, 23, 24, 26, 27, 29, 30, 32, 33, 35, 36, 38, 39, 41, 42, 44, + 45, 47, 48, 50, 51, 53, 54, 56, 57, 59, 60, 62, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 4, 5, 7, 8, 9, 11, 12, 13, 15, 16, 17, 19, 20, + 22, 23, 25, 26, 28, 29, 31, 32, 34, 35, 37, 38, 40, 41, 43, 44, + 46, 47, 49, 50, 52, 53, 55, 56, 58, 59, 61, 62, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 4, 5, 7, 8, 9, 11, 12, 13, 15, 16, 18, 19, 21, + 22, 24, 25, 27, 28, 30, 31, 33, 34, 36, 37, 39, 40, 42, 43, 45, + 46, 48, 49, 51, 52, 54, 55, 57, 58, 60, 61, 63, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 4, 5, 7, 8, 9, 11, 12, 14, 15, 17, 18, 20, 21, + 23, 24, 26, 27, 29, 30, 32, 33, 35, 36, 38, 39, 41, 42, 44, 45, + 47, 48, 50, 51, 53, 54, 56, 57, 59, 60, 62, 63, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + } + }; + + // This uses C#'s compiler optimization to refer to assembly's static data directly. + public static ReadOnlySpan Norm => + [ + + // renorm_sizes[i] = 8 - log2(i) + 7, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 0 + ]; + + // This uses C#'s compiler optimization to refer to assembly's static data directly. + public static ReadOnlySpan NewRange => + [ + + // range = ((range + 1) << kVP8Log2Range[range]) - 1 + 127, 127, 191, 127, 159, 191, 223, 127, 143, 159, 175, 191, 207, 223, 239, + 127, 135, 143, 151, 159, 167, 175, 183, 191, 199, 207, 215, 223, 231, 239, + 247, 127, 131, 135, 139, 143, 147, 151, 155, 159, 163, 167, 171, 175, 179, + 183, 187, 191, 195, 199, 203, 207, 211, 215, 219, 223, 227, 231, 235, 239, + 243, 247, 251, 127, 129, 131, 133, 135, 137, 139, 141, 143, 145, 147, 149, + 151, 153, 155, 157, 159, 161, 163, 165, 167, 169, 171, 173, 175, 177, 179, + 181, 183, 185, 187, 189, 191, 193, 195, 197, 199, 201, 203, 205, 207, 209, + 211, 213, 215, 217, 219, 221, 223, 225, 227, 229, 231, 233, 235, 237, 239, + 241, 243, 245, 247, 249, 251, 253, 127 + ]; + + public static readonly ushort[] Vp8EntropyCost = + [ + 1792, 1792, 1792, 1536, 1536, 1408, 1366, 1280, 1280, 1216, + 1178, 1152, 1110, 1076, 1061, 1024, 1024, 992, 968, 951, + 939, 911, 896, 878, 871, 854, 838, 820, 811, 794, + 786, 768, 768, 752, 740, 732, 720, 709, 704, 690, + 683, 672, 666, 655, 647, 640, 631, 622, 615, 607, + 598, 592, 586, 576, 572, 564, 559, 555, 547, 541, + 534, 528, 522, 512, 512, 504, 500, 494, 488, 483, + 477, 473, 467, 461, 458, 452, 448, 443, 438, 434, + 427, 424, 419, 415, 410, 406, 403, 399, 394, 390, + 384, 384, 377, 374, 370, 366, 362, 359, 355, 351, + 347, 342, 342, 336, 333, 330, 326, 323, 320, 316, + 312, 308, 305, 302, 299, 296, 293, 288, 287, 283, + 280, 277, 274, 272, 268, 266, 262, 256, 256, 256, + 251, 248, 245, 242, 240, 237, 234, 232, 228, 226, + 223, 221, 218, 216, 214, 211, 208, 205, 203, 201, + 198, 196, 192, 191, 188, 187, 183, 181, 179, 176, + 175, 171, 171, 168, 165, 163, 160, 159, 156, 154, + 152, 150, 148, 146, 144, 142, 139, 138, 135, 133, + 131, 128, 128, 125, 123, 121, 119, 117, 115, 113, + 111, 110, 107, 105, 103, 102, 100, 98, 96, 94, + 92, 91, 89, 86, 86, 83, 82, 80, 77, 76, + 74, 73, 71, 69, 67, 66, 64, 63, 61, 59, + 57, 55, 54, 52, 51, 49, 47, 46, 44, 43, + 41, 40, 38, 36, 35, 33, 32, 30, 29, 27, + 25, 24, 22, 21, 19, 18, 16, 15, 13, 12, + 10, 9, 7, 6, 4, 3 + ]; + + public static readonly ushort[][] Vp8LevelCodes = + [ + [0x001, 0x000], [0x007, 0x001], [0x00f, 0x005], + [0x00f, 0x00d], [0x033, 0x003], [0x033, 0x003], [0x033, 0x023], + [0x033, 0x023], [0x033, 0x023], [0x033, 0x023], [0x0d3, 0x013], + [0x0d3, 0x013], [0x0d3, 0x013], [0x0d3, 0x013], [0x0d3, 0x013], + [0x0d3, 0x013], [0x0d3, 0x013], [0x0d3, 0x013], [0x0d3, 0x093], + [0x0d3, 0x093], [0x0d3, 0x093], [0x0d3, 0x093], [0x0d3, 0x093], + [0x0d3, 0x093], [0x0d3, 0x093], [0x0d3, 0x093], [0x0d3, 0x093], + [0x0d3, 0x093], [0x0d3, 0x093], [0x0d3, 0x093], [0x0d3, 0x093], + [0x0d3, 0x093], [0x0d3, 0x093], [0x0d3, 0x093], [0x153, 0x053], + [0x153, 0x053], [0x153, 0x053], [0x153, 0x053], [0x153, 0x053], + [0x153, 0x053], [0x153, 0x053], [0x153, 0x053], [0x153, 0x053], + [0x153, 0x053], [0x153, 0x053], [0x153, 0x053], [0x153, 0x053], + [0x153, 0x053], [0x153, 0x053], [0x153, 0x053], [0x153, 0x053], + [0x153, 0x053], [0x153, 0x053], [0x153, 0x053], [0x153, 0x053], + [0x153, 0x053], [0x153, 0x053], [0x153, 0x053], [0x153, 0x053], + [0x153, 0x053], [0x153, 0x053], [0x153, 0x053], [0x153, 0x053], + [0x153, 0x053], [0x153, 0x053], [0x153, 0x053], [0x153, 0x153] + ]; + + /// + /// Lookup table for small values of log2(int). + /// + public static readonly float[] Log2Table = + [ + 0.0000000000000000f, 0.0000000000000000f, + 1.0000000000000000f, 1.5849625007211560f, + 2.0000000000000000f, 2.3219280948873621f, + 2.5849625007211560f, 2.8073549220576041f, + 3.0000000000000000f, 3.1699250014423121f, + 3.3219280948873621f, 3.4594316186372973f, + 3.5849625007211560f, 3.7004397181410921f, + 3.8073549220576041f, 3.9068905956085187f, + 4.0000000000000000f, 4.0874628412503390f, + 4.1699250014423121f, 4.2479275134435852f, + 4.3219280948873626f, 4.3923174227787606f, + 4.4594316186372973f, 4.5235619560570130f, + 4.5849625007211560f, 4.6438561897747243f, + 4.7004397181410917f, 4.7548875021634682f, + 4.8073549220576037f, 4.8579809951275718f, + 4.9068905956085187f, 4.9541963103868749f, + 5.0000000000000000f, 5.0443941193584533f, + 5.0874628412503390f, 5.1292830169449663f, + 5.1699250014423121f, 5.2094533656289501f, + 5.2479275134435852f, 5.2854022188622487f, + 5.3219280948873626f, 5.3575520046180837f, + 5.3923174227787606f, 5.4262647547020979f, + 5.4594316186372973f, 5.4918530963296747f, + 5.5235619560570130f, 5.5545888516776376f, + 5.5849625007211560f, 5.6147098441152083f, + 5.6438561897747243f, 5.6724253419714951f, + 5.7004397181410917f, 5.7279204545631987f, + 5.7548875021634682f, 5.7813597135246599f, + 5.8073549220576037f, 5.8328900141647412f, + 5.8579809951275718f, 5.8826430493618415f, + 5.9068905956085187f, 5.9307373375628866f, + 5.9541963103868749f, 5.9772799234999167f, + 6.0000000000000000f, 6.0223678130284543f, + 6.0443941193584533f, 6.0660891904577720f, + 6.0874628412503390f, 6.1085244567781691f, + 6.1292830169449663f, 6.1497471195046822f, + 6.1699250014423121f, 6.1898245588800175f, + 6.2094533656289501f, 6.2288186904958804f, + 6.2479275134435852f, 6.2667865406949010f, + 6.2854022188622487f, 6.3037807481771030f, + 6.3219280948873626f, 6.3398500028846243f, + 6.3575520046180837f, 6.3750394313469245f, + 6.3923174227787606f, 6.4093909361377017f, + 6.4262647547020979f, 6.4429434958487279f, + 6.4594316186372973f, 6.4757334309663976f, + 6.4918530963296747f, 6.5077946401986963f, + 6.5235619560570130f, 6.5391588111080309f, + 6.5545888516776376f, 6.5698556083309478f, + 6.5849625007211560f, 6.5999128421871278f, + 6.6147098441152083f, 6.6293566200796094f, + 6.6438561897747243f, 6.6582114827517946f, + 6.6724253419714951f, 6.6865005271832185f, + 6.7004397181410917f, 6.7142455176661224f, + 6.7279204545631987f, 6.7414669864011464f, + 6.7548875021634682f, 6.7681843247769259f, + 6.7813597135246599f, 6.7944158663501061f, + 6.8073549220576037f, 6.8201789624151878f, + 6.8328900141647412f, 6.8454900509443747f, + 6.8579809951275718f, 6.8703647195834047f, + 6.8826430493618415f, 6.8948177633079437f, + 6.9068905956085187f, 6.9188632372745946f, + 6.9307373375628866f, 6.9425145053392398f, + 6.9541963103868749f, 6.9657842846620869f, + 6.9772799234999167f, 6.9886846867721654f, + 7.0000000000000000f, 7.0112272554232539f, + 7.0223678130284543f, 7.0334230015374501f, + 7.0443941193584533f, 7.0552824355011898f, + 7.0660891904577720f, 7.0768155970508308f, + 7.0874628412503390f, 7.0980320829605263f, + 7.1085244567781691f, 7.1189410727235076f, + 7.1292830169449663f, 7.1395513523987936f, + 7.1497471195046822f, 7.1598713367783890f, + 7.1699250014423121f, 7.1799090900149344f, + 7.1898245588800175f, 7.1996723448363644f, + 7.2094533656289501f, 7.2191685204621611f, + 7.2288186904958804f, 7.2384047393250785f, + 7.2479275134435852f, 7.2573878426926521f, + 7.2667865406949010f, 7.2761244052742375f, + 7.2854022188622487f, 7.2946207488916270f, + 7.3037807481771030f, 7.3128829552843557f, + 7.3219280948873626f, 7.3309168781146167f, + 7.3398500028846243f, 7.3487281542310771f, + 7.3575520046180837f, 7.3663222142458160f, + 7.3750394313469245f, 7.3837042924740519f, + 7.3923174227787606f, 7.4008794362821843f, + 7.4093909361377017f, 7.4178525148858982f, + 7.4262647547020979f, 7.4346282276367245f, + 7.4429434958487279f, 7.4512111118323289f, + 7.4594316186372973f, 7.4676055500829976f, + 7.4757334309663976f, 7.4838157772642563f, + 7.4918530963296747f, 7.4998458870832056f, + 7.5077946401986963f, 7.5156998382840427f, + 7.5235619560570130f, 7.5313814605163118f, + 7.5391588111080309f, 7.5468944598876364f, + 7.5545888516776376f, 7.5622424242210728f, + 7.5698556083309478f, 7.5774288280357486f, + 7.5849625007211560f, 7.5924570372680806f, + 7.5999128421871278f, 7.6073303137496104f, + 7.6147098441152083f, 7.6220518194563764f, + 7.6293566200796094f, 7.6366246205436487f, + 7.6438561897747243f, 7.6510516911789281f, + 7.6582114827517946f, 7.6653359171851764f, + 7.6724253419714951f, 7.6794800995054464f, + 7.6865005271832185f, 7.6934869574993252f, + 7.7004397181410917f, 7.7073591320808825f, + 7.7142455176661224f, 7.7210991887071855f, + 7.7279204545631987f, 7.7347096202258383f, + 7.7414669864011464f, 7.7481928495894605f, + 7.7548875021634682f, 7.7615512324444795f, + 7.7681843247769259f, 7.7747870596011736f, + 7.7813597135246599f, 7.7879025593914317f, + 7.7944158663501061f, 7.8008998999203047f, + 7.8073549220576037f, 7.8137811912170374f, + 7.8201789624151878f, 7.8265484872909150f, + 7.8328900141647412f, 7.8392037880969436f, + 7.8454900509443747f, 7.8517490414160571f, + 7.8579809951275718f, 7.8641861446542797f, + 7.8703647195834047f, 7.8765169465649993f, + 7.8826430493618415f, 7.8887432488982591f, + 7.8948177633079437f, 7.9008668079807486f, + 7.9068905956085187f, 7.9128893362299619f, + 7.9188632372745946f, 7.9248125036057812f, + 7.9307373375628866f, 7.9366379390025709f, + 7.9425145053392398f, 7.9483672315846778f, + 7.9541963103868749f, 7.9600019320680805f, + 7.9657842846620869f, 7.9715435539507719f, + 7.9772799234999167f, 7.9829935746943103f, + 7.9886846867721654f, 7.9943534368588577f + ]; + + public static readonly float[] SLog2Table = + [ + 0.00000000f, 0.00000000f, 2.00000000f, 4.75488750f, + 8.00000000f, 11.60964047f, 15.50977500f, 19.65148445f, + 24.00000000f, 28.52932501f, 33.21928095f, 38.05374781f, + 43.01955001f, 48.10571634f, 53.30296891f, 58.60335893f, + 64.00000000f, 69.48686830f, 75.05865003f, 80.71062276f, + 86.43856190f, 92.23866588f, 98.10749561f, 104.04192499f, + 110.03910002f, 116.09640474f, 122.21143267f, 128.38196256f, + 134.60593782f, 140.88144886f, 147.20671787f, 153.58008562f, + 160.00000000f, 166.46500594f, 172.97373660f, 179.52490559f, + 186.11730005f, 192.74977453f, 199.42124551f, 206.13068654f, + 212.87712380f, 219.65963219f, 226.47733176f, 233.32938445f, + 240.21499122f, 247.13338933f, 254.08384998f, 261.06567603f, + 268.07820003f, 275.12078236f, 282.19280949f, 289.29369244f, + 296.42286534f, 303.57978409f, 310.76392512f, 317.97478424f, + 325.21187564f, 332.47473081f, 339.76289772f, 347.07593991f, + 354.41343574f, 361.77497759f, 369.16017124f, 376.56863518f, + 384.00000000f, 391.45390785f, 398.93001188f, 406.42797576f, + 413.94747321f, 421.48818752f, 429.04981119f, 436.63204548f, + 444.23460010f, 451.85719280f, 459.49954906f, 467.16140179f, + 474.84249102f, 482.54256363f, 490.26137307f, 497.99867911f, + 505.75424759f, 513.52785023f, 521.31926438f, 529.12827280f, + 536.95466351f, 544.79822957f, 552.65876890f, 560.53608414f, + 568.42998244f, 576.34027536f, 584.26677867f, 592.20931226f, + 600.16769996f, 608.14176943f, 616.13135206f, 624.13628279f, + 632.15640007f, 640.19154569f, 648.24156472f, 656.30630539f, + 664.38561898f, 672.47935976f, 680.58738488f, 688.70955430f, + 696.84573069f, 704.99577935f, 713.15956818f, 721.33696754f, + 729.52785023f, 737.73209140f, 745.94956849f, 754.18016116f, + 762.42375127f, 770.68022275f, 778.94946161f, 787.23135586f, + 795.52579543f, 803.83267219f, 812.15187982f, 820.48331383f, + 828.82687147f, 837.18245171f, 845.54995518f, 853.92928416f, + 862.32034249f, 870.72303558f, 879.13727036f, 887.56295522f, + 896.00000000f, 904.44831595f, 912.90781569f, 921.37841320f, + 929.86002376f, 938.35256392f, 946.85595152f, 955.37010560f, + 963.89494641f, 972.43039537f, 980.97637504f, 989.53280911f, + 998.09962237f, 1006.67674069f, 1015.26409097f, 1023.86160116f, + 1032.46920021f, 1041.08681805f, 1049.71438560f, 1058.35183469f, + 1066.99909811f, 1075.65610955f, 1084.32280357f, 1092.99911564f, + 1101.68498204f, 1110.38033993f, 1119.08512727f, 1127.79928282f, + 1136.52274614f, 1145.25545758f, 1153.99735821f, 1162.74838989f, + 1171.50849518f, 1180.27761738f, 1189.05570047f, 1197.84268914f, + 1206.63852876f, 1215.44316535f, 1224.25654560f, 1233.07861684f, + 1241.90932703f, 1250.74862473f, 1259.59645914f, 1268.45278005f, + 1277.31753781f, 1286.19068338f, 1295.07216828f, 1303.96194457f, + 1312.85996488f, 1321.76618236f, 1330.68055071f, 1339.60302413f, + 1348.53355734f, 1357.47210556f, 1366.41862452f, 1375.37307041f, + 1384.33539991f, 1393.30557020f, 1402.28353887f, 1411.26926400f, + 1420.26270412f, 1429.26381818f, 1438.27256558f, 1447.28890615f, + 1456.31280014f, 1465.34420819f, 1474.38309138f, 1483.42941118f, + 1492.48312945f, 1501.54420843f, 1510.61261078f, 1519.68829949f, + 1528.77123795f, 1537.86138993f, 1546.95871952f, 1556.06319119f, + 1565.17476976f, 1574.29342040f, 1583.41910860f, 1592.55180020f, + 1601.69146137f, 1610.83805860f, 1619.99155871f, 1629.15192882f, + 1638.31913637f, 1647.49314911f, 1656.67393509f, 1665.86146266f, + 1675.05570047f, 1684.25661744f, 1693.46418280f, 1702.67836605f, + 1711.89913698f, 1721.12646563f, 1730.36032233f, 1739.60067768f, + 1748.84750254f, 1758.10076802f, 1767.36044551f, 1776.62650662f, + 1785.89892323f, 1795.17766747f, 1804.46271172f, 1813.75402857f, + 1823.05159087f, 1832.35537170f, 1841.66534438f, 1850.98148244f, + 1860.30375965f, 1869.63214999f, 1878.96662767f, 1888.30716711f, + 1897.65374295f, 1907.00633003f, 1916.36490342f, 1925.72943838f, + 1935.09991037f, 1944.47629506f, 1953.85856831f, 1963.24670620f, + 1972.64068498f, 1982.04048108f, 1991.44607117f, 2000.85743204f, + 2010.27454072f, 2019.69737440f, 2029.12591044f, 2038.56012640f + ]; + + public static readonly int[] CodeToPlane = + [ + 0x18, 0x07, 0x17, 0x19, 0x28, 0x06, 0x27, 0x29, 0x16, 0x1a, + 0x26, 0x2a, 0x38, 0x05, 0x37, 0x39, 0x15, 0x1b, 0x36, 0x3a, + 0x25, 0x2b, 0x48, 0x04, 0x47, 0x49, 0x14, 0x1c, 0x35, 0x3b, + 0x46, 0x4a, 0x24, 0x2c, 0x58, 0x45, 0x4b, 0x34, 0x3c, 0x03, + 0x57, 0x59, 0x13, 0x1d, 0x56, 0x5a, 0x23, 0x2d, 0x44, 0x4c, + 0x55, 0x5b, 0x33, 0x3d, 0x68, 0x02, 0x67, 0x69, 0x12, 0x1e, + 0x66, 0x6a, 0x22, 0x2e, 0x54, 0x5c, 0x43, 0x4d, 0x65, 0x6b, + 0x32, 0x3e, 0x78, 0x01, 0x77, 0x79, 0x53, 0x5d, 0x11, 0x1f, + 0x64, 0x6c, 0x42, 0x4e, 0x76, 0x7a, 0x21, 0x2f, 0x75, 0x7b, + 0x31, 0x3f, 0x63, 0x6d, 0x52, 0x5e, 0x00, 0x74, 0x7c, 0x41, + 0x4f, 0x10, 0x20, 0x62, 0x6e, 0x30, 0x73, 0x7d, 0x51, 0x5f, + 0x40, 0x72, 0x7e, 0x61, 0x6f, 0x50, 0x71, 0x7f, 0x60, 0x70 + ]; + + public static readonly uint[] PlaneToCodeLut = + [ + 96, 73, 55, 39, 23, 13, 5, 1, 255, 255, 255, 255, 255, 255, 255, 255, + 101, 78, 58, 42, 26, 16, 8, 2, 0, 3, 9, 17, 27, 43, 59, 79, + 102, 86, 62, 46, 32, 20, 10, 6, 4, 7, 11, 21, 33, 47, 63, 87, + 105, 90, 70, 52, 37, 28, 18, 14, 12, 15, 19, 29, 38, 53, 71, 91, + 110, 99, 82, 66, 48, 35, 30, 24, 22, 25, 31, 36, 49, 67, 83, 100, + 115, 108, 94, 76, 64, 50, 44, 40, 34, 41, 45, 51, 65, 77, 95, 109, + 118, 113, 103, 92, 80, 68, 60, 56, 54, 57, 61, 69, 81, 93, 104, 114, + 119, 116, 111, 106, 97, 88, 84, 74, 72, 75, 85, 89, 98, 107, 112, 117 + ]; + + // 31 ^ clz(i) + public static ReadOnlySpan LogTable8Bit => + [ + 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 + ]; + + // Paragraph 14.1 + // This uses C#'s compiler optimization to refer to assembly's static data directly. + public static ReadOnlySpan DcTable => + [ + 4, 5, 6, 7, 8, 9, 10, 10, + 11, 12, 13, 14, 15, 16, 17, 17, + 18, 19, 20, 20, 21, 21, 22, 22, + 23, 23, 24, 25, 25, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, 36, + 37, 37, 38, 39, 40, 41, 42, 43, + 44, 45, 46, 46, 47, 48, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 58, + 59, 60, 61, 62, 63, 64, 65, 66, + 67, 68, 69, 70, 71, 72, 73, 74, + 75, 76, 76, 77, 78, 79, 80, 81, + 82, 83, 84, 85, 86, 87, 88, 89, + 91, 93, 95, 96, 98, 100, 101, 102, + 104, 106, 108, 110, 112, 114, 116, 118, + 122, 124, 126, 128, 130, 132, 134, 136, + 138, 140, 143, 145, 148, 151, 154, 157 + ]; + + // Paragraph 14.1 + public static readonly ushort[] AcTable = + [ + 4, 5, 6, 7, 8, 9, 10, 11, + 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, + 28, 29, 30, 31, 32, 33, 34, 35, + 36, 37, 38, 39, 40, 41, 42, 43, + 44, 45, 46, 47, 48, 49, 50, 51, + 52, 53, 54, 55, 56, 57, 58, 60, + 62, 64, 66, 68, 70, 72, 74, 76, + 78, 80, 82, 84, 86, 88, 90, 92, + 94, 96, 98, 100, 102, 104, 106, 108, + 110, 112, 114, 116, 119, 122, 125, 128, + 131, 134, 137, 140, 143, 146, 149, 152, + 155, 158, 161, 164, 167, 170, 173, 177, + 181, 185, 189, 193, 197, 201, 205, 209, + 213, 217, 221, 225, 229, 234, 239, 245, + 249, 254, 259, 264, 269, 274, 279, 284 + ]; + + public static readonly ushort[] AcTable2 = + [ + 8, 8, 9, 10, 12, 13, 15, 17, + 18, 20, 21, 23, 24, 26, 27, 29, + 31, 32, 34, 35, 37, 38, 40, 41, + 43, 44, 46, 48, 49, 51, 52, 54, + 55, 57, 58, 60, 62, 63, 65, 66, + 68, 69, 71, 72, 74, 75, 77, 79, + 80, 82, 83, 85, 86, 88, 89, 93, + 96, 99, 102, 105, 108, 111, 114, 117, + 120, 124, 127, 130, 133, 136, 139, 142, + 145, 148, 151, 155, 158, 161, 164, 167, + 170, 173, 176, 179, 184, 189, 193, 198, + 203, 207, 212, 217, 221, 226, 230, 235, + 240, 244, 249, 254, 258, 263, 268, 274, + 280, 286, 292, 299, 305, 311, 317, 323, + 330, 336, 342, 348, 354, 362, 370, 379, + 385, 393, 401, 409, 416, 424, 432, 440 + ]; + + // Paragraph 13 + public static readonly byte[,,,] CoeffsUpdateProba = + { + { + { + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 176, 246, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 223, 241, 252, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 249, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 244, 252, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 234, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 246, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 239, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 254, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 248, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 251, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 251, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 254, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 254, 253, 255, 254, 255, 255, 255, 255, 255, 255 }, + { 250, 255, 254, 255, 254, 255, 255, 255, 255, 255, 255 }, + { 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + } + }, + { + { + { 217, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 225, 252, 241, 253, 255, 255, 254, 255, 255, 255, 255 }, + { 234, 250, 241, 250, 253, 255, 253, 254, 255, 255, 255 } + }, + { + { 255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 223, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 238, 253, 254, 254, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 248, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 249, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 253, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 247, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 252, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 254, 253, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + } + }, + { + { + { 186, 251, 250, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 234, 251, 244, 254, 255, 255, 255, 255, 255, 255, 255 }, + { 251, 251, 243, 253, 254, 255, 254, 255, 255, 255, 255 } + }, + { + { 255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 236, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 251, 253, 253, 254, 254, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 254, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + } + }, + { + { + { 248, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 250, 254, 252, 254, 255, 255, 255, 255, 255, 255, 255 }, + { 248, 254, 249, 253, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 246, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 252, 254, 251, 254, 254, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 254, 252, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 248, 254, 253, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 253, 255, 254, 254, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 251, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 245, 251, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 253, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 251, 253, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 252, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 252, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 249, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 255, 253, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + } + } + }; + + // Paragraph 13.5: Default Token Probability Table. + public static readonly byte[,,,] DefaultCoeffsProba = + { + { + { + { 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128 } + }, + { + { 253, 136, 254, 255, 228, 219, 128, 128, 128, 128, 128 }, + { 189, 129, 242, 255, 227, 213, 255, 219, 128, 128, 128 }, + { 106, 126, 227, 252, 214, 209, 255, 255, 128, 128, 128 } + }, + { + { 1, 98, 248, 255, 236, 226, 255, 255, 128, 128, 128 }, + { 181, 133, 238, 254, 221, 234, 255, 154, 128, 128, 128 }, + { 78, 134, 202, 247, 198, 180, 255, 219, 128, 128, 128 }, + }, + { + { 1, 185, 249, 255, 243, 255, 128, 128, 128, 128, 128 }, + { 184, 150, 247, 255, 236, 224, 128, 128, 128, 128, 128 }, + { 77, 110, 216, 255, 236, 230, 128, 128, 128, 128, 128 }, + }, + { + { 1, 101, 251, 255, 241, 255, 128, 128, 128, 128, 128 }, + { 170, 139, 241, 252, 236, 209, 255, 255, 128, 128, 128 }, + { 37, 116, 196, 243, 228, 255, 255, 255, 128, 128, 128 } + }, + { + { 1, 204, 254, 255, 245, 255, 128, 128, 128, 128, 128 }, + { 207, 160, 250, 255, 238, 128, 128, 128, 128, 128, 128 }, + { 102, 103, 231, 255, 211, 171, 128, 128, 128, 128, 128 } + }, + { + { 1, 152, 252, 255, 240, 255, 128, 128, 128, 128, 128 }, + { 177, 135, 243, 255, 234, 225, 128, 128, 128, 128, 128 }, + { 80, 129, 211, 255, 194, 224, 128, 128, 128, 128, 128 } + }, + { + { 1, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 246, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 255, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128 } + } + }, + { + { + { 198, 35, 237, 223, 193, 187, 162, 160, 145, 155, 62 }, + { 131, 45, 198, 221, 172, 176, 220, 157, 252, 221, 1 }, + { 68, 47, 146, 208, 149, 167, 221, 162, 255, 223, 128 } + }, + { + { 1, 149, 241, 255, 221, 224, 255, 255, 128, 128, 128 }, + { 184, 141, 234, 253, 222, 220, 255, 199, 128, 128, 128 }, + { 81, 99, 181, 242, 176, 190, 249, 202, 255, 255, 128 } + }, + { + { 1, 129, 232, 253, 214, 197, 242, 196, 255, 255, 128 }, + { 99, 121, 210, 250, 201, 198, 255, 202, 128, 128, 128 }, + { 23, 91, 163, 242, 170, 187, 247, 210, 255, 255, 128 } + }, + { + { 1, 200, 246, 255, 234, 255, 128, 128, 128, 128, 128 }, + { 109, 178, 241, 255, 231, 245, 255, 255, 128, 128, 128 }, + { 44, 130, 201, 253, 205, 192, 255, 255, 128, 128, 128 } + }, + { + { 1, 132, 239, 251, 219, 209, 255, 165, 128, 128, 128 }, + { 94, 136, 225, 251, 218, 190, 255, 255, 128, 128, 128 }, + { 22, 100, 174, 245, 186, 161, 255, 199, 128, 128, 128 } + }, + { + { 1, 182, 249, 255, 232, 235, 128, 128, 128, 128, 128 }, + { 124, 143, 241, 255, 227, 234, 128, 128, 128, 128, 128 }, + { 35, 77, 181, 251, 193, 211, 255, 205, 128, 128, 128 } + }, + { + { 1, 157, 247, 255, 236, 231, 255, 255, 128, 128, 128 }, + { 121, 141, 235, 255, 225, 227, 255, 255, 128, 128, 128 }, + { 45, 99, 188, 251, 195, 217, 255, 224, 128, 128, 128 } + }, + { + { 1, 1, 251, 255, 213, 255, 128, 128, 128, 128, 128 }, + { 203, 1, 248, 255, 255, 128, 128, 128, 128, 128, 128 }, + { 137, 1, 177, 255, 224, 255, 128, 128, 128, 128, 128 } + } + }, + { + { + { 253, 9, 248, 251, 207, 208, 255, 192, 128, 128, 128 }, + { 175, 13, 224, 243, 193, 185, 249, 198, 255, 255, 128 }, + { 73, 17, 171, 221, 161, 179, 236, 167, 255, 234, 128 } + }, + { + { 1, 95, 247, 253, 212, 183, 255, 255, 128, 128, 128 }, + { 239, 90, 244, 250, 211, 209, 255, 255, 128, 128, 128 }, + { 155, 77, 195, 248, 188, 195, 255, 255, 128, 128, 128 } + }, + { + { 1, 24, 239, 251, 218, 219, 255, 205, 128, 128, 128 }, + { 201, 51, 219, 255, 196, 186, 128, 128, 128, 128, 128 }, + { 69, 46, 190, 239, 201, 218, 255, 228, 128, 128, 128 } + }, + { + { 1, 191, 251, 255, 255, 128, 128, 128, 128, 128, 128 }, + { 223, 165, 249, 255, 213, 255, 128, 128, 128, 128, 128 }, + { 141, 124, 248, 255, 255, 128, 128, 128, 128, 128, 128 } + }, + { + { 1, 16, 248, 255, 255, 128, 128, 128, 128, 128, 128 }, + { 190, 36, 230, 255, 236, 255, 128, 128, 128, 128, 128 }, + { 149, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128 } + }, + { + { 1, 226, 255, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 247, 192, 255, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 240, 128, 255, 128, 128, 128, 128, 128, 128, 128, 128 } + }, + { + { 1, 134, 252, 255, 255, 128, 128, 128, 128, 128, 128 }, + { 213, 62, 250, 255, 255, 128, 128, 128, 128, 128, 128 }, + { 55, 93, 255, 128, 128, 128, 128, 128, 128, 128, 128 } + }, + { + { 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128 } + } + }, + { + { + { 202, 24, 213, 235, 186, 191, 220, 160, 240, 175, 255 }, + { 126, 38, 182, 232, 169, 184, 228, 174, 255, 187, 128 }, + { 61, 46, 138, 219, 151, 178, 240, 170, 255, 216, 128 } + }, + { + { 1, 112, 230, 250, 199, 191, 247, 159, 255, 255, 128 }, + { 166, 109, 228, 252, 211, 215, 255, 174, 128, 128, 128 }, + { 39, 77, 162, 232, 172, 180, 245, 178, 255, 255, 128 } + }, + { + { 1, 52, 220, 246, 198, 199, 249, 220, 255, 255, 128 }, + { 124, 74, 191, 243, 183, 193, 250, 221, 255, 255, 128 }, + { 24, 71, 130, 219, 154, 170, 243, 182, 255, 255, 128 } + }, + { + { 1, 182, 225, 249, 219, 240, 255, 224, 128, 128, 128 }, + { 149, 150, 226, 252, 216, 205, 255, 171, 128, 128, 128 }, + { 28, 108, 170, 242, 183, 194, 254, 223, 255, 255, 128 } + }, + { + { 1, 81, 230, 252, 204, 203, 255, 192, 128, 128, 128 }, + { 123, 102, 209, 247, 188, 196, 255, 233, 128, 128, 128 }, + { 20, 95, 153, 243, 164, 173, 255, 203, 128, 128, 128 } + }, + { + { 1, 222, 248, 255, 216, 213, 128, 128, 128, 128, 128 }, + { 168, 175, 246, 252, 235, 205, 255, 255, 128, 128, 128 }, + { 47, 116, 215, 255, 211, 212, 255, 255, 128, 128, 128 } + }, + { + { 1, 121, 236, 253, 212, 214, 255, 255, 128, 128, 128 }, + { 141, 84, 213, 252, 201, 202, 255, 219, 128, 128, 128 }, + { 42, 80, 160, 240, 162, 185, 255, 205, 128, 128, 128 } + }, + { + { 1, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 244, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 238, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128 } + } + } + }; + + public static readonly (int Code, int ExtraBits)[] PrefixEncodeCode = + [ + (0, 0), (0, 0), (1, 0), (2, 0), (3, 0), (4, 1), (4, 1), (5, 1), + (5, 1), (6, 2), (6, 2), (6, 2), (6, 2), (7, 2), (7, 2), (7, 2), + (7, 2), (8, 3), (8, 3), (8, 3), (8, 3), (8, 3), (8, 3), (8, 3), + (8, 3), (9, 3), (9, 3), (9, 3), (9, 3), (9, 3), (9, 3), (9, 3), + (9, 3), (10, 4), (10, 4), (10, 4), (10, 4), (10, 4), (10, 4), (10, 4), + (10, 4), (10, 4), (10, 4), (10, 4), (10, 4), (10, 4), (10, 4), (10, 4), + (10, 4), (11, 4), (11, 4), (11, 4), (11, 4), (11, 4), (11, 4), (11, 4), + (11, 4), (11, 4), (11, 4), (11, 4), (11, 4), (11, 4), (11, 4), (11, 4), + (11, 4), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), + (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), + (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), + (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), + (12, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), + (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), + (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), + (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), + (13, 5), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), + (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), + (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), + (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), + (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), + (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), + (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), + (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), + (14, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), + (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), + (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), + (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), + (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), + (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), + (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), + (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), + (15, 6), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7) + ]; + + // This uses C#'s compiler optimization to refer to assembly's static data directly. + public static ReadOnlySpan PrefixEncodeExtraBitsValue => + [ + 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 2, 3, 0, 1, 2, 3, + 0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, + 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, + 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, + 123, 124, 125, 126, 127, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, + 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, + 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, + 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, + 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, + 118, 119, 120, 121, 122, 123, 124, 125, 126 + ]; + + // Following table is (1 << AlphaFix) / a. The (v * InvAlpha[a]) >> AlphaFix + // formula is then equal to v / a in most (99.6%) cases. Note that this table + // and constant are adjusted very tightly to fit 32b arithmetic. + // In particular, they use the fact that the operands for 'v / a' are actually + // derived as v = (a0.p0 + a1.p1 + a2.p2 + a3.p3) and a = a0 + a1 + a2 + a3 + // with ai in [0..255] and pi in [0..1< Abs0Table => + [ + 0xff, 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8, 0xf7, 0xf6, 0xf5, 0xf4, 0xf3, 0xf2, 0xf1, 0xf0, 0xef, + 0xee, 0xed, 0xec, 0xeb, 0xea, 0xe9, 0xe8, 0xe7, 0xe6, 0xe5, 0xe4, 0xe3, 0xe2, 0xe1, 0xe0, 0xdf, 0xde, + 0xdd, 0xdc, 0xdb, 0xda, 0xd9, 0xd8, 0xd7, 0xd6, 0xd5, 0xd4, 0xd3, 0xd2, 0xd1, 0xd0, 0xcf, 0xce, 0xcd, + 0xcc, 0xcb, 0xca, 0xc9, 0xc8, 0xc7, 0xc6, 0xc5, 0xc4, 0xc3, 0xc2, 0xc1, 0xc0, 0xbf, 0xbe, 0xbd, 0xbc, + 0xbb, 0xba, 0xb9, 0xb8, 0xb7, 0xb6, 0xb5, 0xb4, 0xb3, 0xb2, 0xb1, 0xb0, 0xaf, 0xae, 0xad, 0xac, 0xab, + 0xaa, 0xa9, 0xa8, 0xa7, 0xa6, 0xa5, 0xa4, 0xa3, 0xa2, 0xa1, 0xa0, 0x9f, 0x9e, 0x9d, 0x9c, 0x9b, 0x9a, + 0x99, 0x98, 0x97, 0x96, 0x95, 0x94, 0x93, 0x92, 0x91, 0x90, 0x8f, 0x8e, 0x8d, 0x8c, 0x8b, 0x8a, 0x89, + 0x88, 0x87, 0x86, 0x85, 0x84, 0x83, 0x82, 0x81, 0x80, 0x7f, 0x7e, 0x7d, 0x7c, 0x7b, 0x7a, 0x79, 0x78, + 0x77, 0x76, 0x75, 0x74, 0x73, 0x72, 0x71, 0x70, 0x6f, 0x6e, 0x6d, 0x6c, 0x6b, 0x6a, 0x69, 0x68, 0x67, + 0x66, 0x65, 0x64, 0x63, 0x62, 0x61, 0x60, 0x5f, 0x5e, 0x5d, 0x5c, 0x5b, 0x5a, 0x59, 0x58, 0x57, 0x56, + 0x55, 0x54, 0x53, 0x52, 0x51, 0x50, 0x4f, 0x4e, 0x4d, 0x4c, 0x4b, 0x4a, 0x49, 0x48, 0x47, 0x46, 0x45, + 0x44, 0x43, 0x42, 0x41, 0x40, 0x3f, 0x3e, 0x3d, 0x3c, 0x3b, 0x3a, 0x39, 0x38, 0x37, 0x36, 0x35, 0x34, + 0x33, 0x32, 0x31, 0x30, 0x2f, 0x2e, 0x2d, 0x2c, 0x2b, 0x2a, 0x29, 0x28, 0x27, 0x26, 0x25, 0x24, 0x23, + 0x22, 0x21, 0x20, 0x1f, 0x1e, 0x1d, 0x1c, 0x1b, 0x1a, 0x19, 0x18, 0x17, 0x16, 0x15, 0x14, 0x13, 0x12, + 0x11, 0x10, 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, + 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, + 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, + 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, + 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, + 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, + 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, + 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, + 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, + 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, + 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, + 0xcc, 0xcd, 0xce, 0xcf, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, + 0xdd, 0xde, 0xdf, 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, + 0xee, 0xef, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, + 0xff + ]; + + // This uses C#'s compiler optimization to refer to assembly's static data directly. + private static ReadOnlySpan Clip1Table => + [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, + 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, + 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, + 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, + 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, + 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, + 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, + 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, + 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, + 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, + 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, + 0xcc, 0xcd, 0xce, 0xcf, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, + 0xdd, 0xde, 0xdf, 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, + 0xee, 0xef, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff + ]; + + // This uses C#'s compiler optimization to refer to assembly's static data directly. + private static ReadOnlySpan Sclip1Table => + [ + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -127, -126, -125, -124, -123, -122, -121, -120, + -119, -118, -117, -116, -115, -114, -113, -112, -111, -110, -109, -108, -107, -106, -105, -104, -103, + -102, -101, -100, -99, -98, -97, -96, -95, -94, -93, -92, -91, -90, -89, -88, -87, -86, -85, -84, -83, + -82, -81, -80, -79, -78, -77, -76, -75, -74, -73, -72, -71, -70, -69, -68, -67, -66, -65, -64, -63, -62, + -61, -60, -59, -58, -57, -56, -55, -54, -53, -52, -51, -50, -49, -48, -47, -46, -45, -44, -43, -42, -41, + -40, -39, -38, -37, -36, -35, -34, -33, -32, -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, + -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, + 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, + 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, + 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, + 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, + 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127 + ]; + + // This uses C#'s compiler optimization to refer to assembly's static data directly. + private static ReadOnlySpan Sclip2Table => + [ + -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, + -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, + -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, + -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, + -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -15, -14, -13, -12, -11, -10, -9, -8, + -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15 + ]; + + private static void InitializeModesProbabilities() + { + // Paragraph 11.5 + ModesProba[0, 0] = [231, 120, 48, 89, 115, 113, 120, 152, 112]; + ModesProba[0, 1] = [152, 179, 64, 126, 170, 118, 46, 70, 95]; + ModesProba[0, 2] = [175, 69, 143, 80, 85, 82, 72, 155, 103]; + ModesProba[0, 3] = [56, 58, 10, 171, 218, 189, 17, 13, 152]; + ModesProba[0, 4] = [114, 26, 17, 163, 44, 195, 21, 10, 173]; + ModesProba[0, 5] = [121, 24, 80, 195, 26, 62, 44, 64, 85]; + ModesProba[0, 6] = [144, 71, 10, 38, 171, 213, 144, 34, 26]; + ModesProba[0, 7] = [170, 46, 55, 19, 136, 160, 33, 206, 71]; + ModesProba[0, 8] = [63, 20, 8, 114, 114, 208, 12, 9, 226]; + ModesProba[0, 9] = [81, 40, 11, 96, 182, 84, 29, 16, 36]; + ModesProba[1, 0] = [134, 183, 89, 137, 98, 101, 106, 165, 148]; + ModesProba[1, 1] = [72, 187, 100, 130, 157, 111, 32, 75, 80]; + ModesProba[1, 2] = [66, 102, 167, 99, 74, 62, 40, 234, 128]; + ModesProba[1, 3] = [41, 53, 9, 178, 241, 141, 26, 8, 107]; + ModesProba[1, 4] = [74, 43, 26, 146, 73, 166, 49, 23, 157]; + ModesProba[1, 5] = [65, 38, 105, 160, 51, 52, 31, 115, 128]; + ModesProba[1, 6] = [104, 79, 12, 27, 217, 255, 87, 17, 7]; + ModesProba[1, 7] = [87, 68, 71, 44, 114, 51, 15, 186, 23]; + ModesProba[1, 8] = [47, 41, 14, 110, 182, 183, 21, 17, 194]; + ModesProba[1, 9] = [66, 45, 25, 102, 197, 189, 23, 18, 22]; + ModesProba[2, 0] = [88, 88, 147, 150, 42, 46, 45, 196, 205]; + ModesProba[2, 1] = [43, 97, 183, 117, 85, 38, 35, 179, 61]; + ModesProba[2, 2] = [39, 53, 200, 87, 26, 21, 43, 232, 171]; + ModesProba[2, 3] = [56, 34, 51, 104, 114, 102, 29, 93, 77]; + ModesProba[2, 4] = [39, 28, 85, 171, 58, 165, 90, 98, 64]; + ModesProba[2, 5] = [34, 22, 116, 206, 23, 34, 43, 166, 73]; + ModesProba[2, 6] = [107, 54, 32, 26, 51, 1, 81, 43, 31]; + ModesProba[2, 7] = [68, 25, 106, 22, 64, 171, 36, 225, 114]; + ModesProba[2, 8] = [34, 19, 21, 102, 132, 188, 16, 76, 124]; + ModesProba[2, 9] = [62, 18, 78, 95, 85, 57, 50, 48, 51]; + ModesProba[3, 0] = [193, 101, 35, 159, 215, 111, 89, 46, 111]; + ModesProba[3, 1] = [60, 148, 31, 172, 219, 228, 21, 18, 111]; + ModesProba[3, 2] = [112, 113, 77, 85, 179, 255, 38, 120, 114]; + ModesProba[3, 3] = [40, 42, 1, 196, 245, 209, 10, 25, 109]; + ModesProba[3, 4] = [88, 43, 29, 140, 166, 213, 37, 43, 154]; + ModesProba[3, 5] = [61, 63, 30, 155, 67, 45, 68, 1, 209]; + ModesProba[3, 6] = [100, 80, 8, 43, 154, 1, 51, 26, 71]; + ModesProba[3, 7] = [142, 78, 78, 16, 255, 128, 34, 197, 171]; + ModesProba[3, 8] = [41, 40, 5, 102, 211, 183, 4, 1, 221]; + ModesProba[3, 9] = [51, 50, 17, 168, 209, 192, 23, 25, 82]; + ModesProba[4, 0] = [138, 31, 36, 171, 27, 166, 38, 44, 229]; + ModesProba[4, 1] = [67, 87, 58, 169, 82, 115, 26, 59, 179]; + ModesProba[4, 2] = [63, 59, 90, 180, 59, 166, 93, 73, 154]; + ModesProba[4, 3] = [40, 40, 21, 116, 143, 209, 34, 39, 175]; + ModesProba[4, 4] = [47, 15, 16, 183, 34, 223, 49, 45, 183]; + ModesProba[4, 5] = [46, 17, 33, 183, 6, 98, 15, 32, 183]; + ModesProba[4, 6] = [57, 46, 22, 24, 128, 1, 54, 17, 37]; + ModesProba[4, 7] = [65, 32, 73, 115, 28, 128, 23, 128, 205]; + ModesProba[4, 8] = [40, 3, 9, 115, 51, 192, 18, 6, 223]; + ModesProba[4, 9] = [87, 37, 9, 115, 59, 77, 64, 21, 47]; + ModesProba[5, 0] = [104, 55, 44, 218, 9, 54, 53, 130, 226]; + ModesProba[5, 1] = [64, 90, 70, 205, 40, 41, 23, 26, 57]; + ModesProba[5, 2] = [54, 57, 112, 184, 5, 41, 38, 166, 213]; + ModesProba[5, 3] = [30, 34, 26, 133, 152, 116, 10, 32, 134]; + ModesProba[5, 4] = [39, 19, 53, 221, 26, 114, 32, 73, 255]; + ModesProba[5, 5] = [31, 9, 65, 234, 2, 15, 1, 118, 73]; + ModesProba[5, 6] = [75, 32, 12, 51, 192, 255, 160, 43, 51]; + ModesProba[5, 7] = [88, 31, 35, 67, 102, 85, 55, 186, 85]; + ModesProba[5, 8] = [56, 21, 23, 111, 59, 205, 45, 37, 192]; + ModesProba[5, 9] = [55, 38, 70, 124, 73, 102, 1, 34, 98]; + ModesProba[6, 0] = [125, 98, 42, 88, 104, 85, 117, 175, 82]; + ModesProba[6, 1] = [95, 84, 53, 89, 128, 100, 113, 101, 45]; + ModesProba[6, 2] = [75, 79, 123, 47, 51, 128, 81, 171, 1]; + ModesProba[6, 3] = [57, 17, 5, 71, 102, 57, 53, 41, 49]; + ModesProba[6, 4] = [38, 33, 13, 121, 57, 73, 26, 1, 85]; + ModesProba[6, 5] = [41, 10, 67, 138, 77, 110, 90, 47, 114]; + ModesProba[6, 6] = [115, 21, 2, 10, 102, 255, 166, 23, 6]; + ModesProba[6, 7] = [101, 29, 16, 10, 85, 128, 101, 196, 26]; + ModesProba[6, 8] = [57, 18, 10, 102, 102, 213, 34, 20, 43]; + ModesProba[6, 9] = [117, 20, 15, 36, 163, 128, 68, 1, 26]; + ModesProba[7, 0] = [102, 61, 71, 37, 34, 53, 31, 243, 192]; + ModesProba[7, 1] = [69, 60, 71, 38, 73, 119, 28, 222, 37]; + ModesProba[7, 2] = [68, 45, 128, 34, 1, 47, 11, 245, 171]; + ModesProba[7, 3] = [62, 17, 19, 70, 146, 85, 55, 62, 70]; + ModesProba[7, 4] = [37, 43, 37, 154, 100, 163, 85, 160, 1]; + ModesProba[7, 5] = [63, 9, 92, 136, 28, 64, 32, 201, 85]; + ModesProba[7, 6] = [75, 15, 9, 9, 64, 255, 184, 119, 16]; + ModesProba[7, 7] = [86, 6, 28, 5, 64, 255, 25, 248, 1]; + ModesProba[7, 8] = [56, 8, 17, 132, 137, 255, 55, 116, 128]; + ModesProba[7, 9] = [58, 15, 20, 82, 135, 57, 26, 121, 40]; + ModesProba[8, 0] = [164, 50, 31, 137, 154, 133, 25, 35, 218]; + ModesProba[8, 1] = [51, 103, 44, 131, 131, 123, 31, 6, 158]; + ModesProba[8, 2] = [86, 40, 64, 135, 148, 224, 45, 183, 128]; + ModesProba[8, 3] = [22, 26, 17, 131, 240, 154, 14, 1, 209]; + ModesProba[8, 4] = [45, 16, 21, 91, 64, 222, 7, 1, 197]; + ModesProba[8, 5] = [56, 21, 39, 155, 60, 138, 23, 102, 213]; + ModesProba[8, 6] = [83, 12, 13, 54, 192, 255, 68, 47, 28]; + ModesProba[8, 7] = [85, 26, 85, 85, 128, 128, 32, 146, 171]; + ModesProba[8, 8] = [18, 11, 7, 63, 144, 171, 4, 4, 246]; + ModesProba[8, 9] = [35, 27, 10, 146, 174, 171, 12, 26, 128]; + ModesProba[9, 0] = [190, 80, 35, 99, 180, 80, 126, 54, 45]; + ModesProba[9, 1] = [85, 126, 47, 87, 176, 51, 41, 20, 32]; + ModesProba[9, 2] = [101, 75, 128, 139, 118, 146, 116, 128, 85]; + ModesProba[9, 3] = [56, 41, 15, 176, 236, 85, 37, 9, 62]; + ModesProba[9, 4] = [71, 30, 17, 119, 118, 255, 17, 18, 138]; + ModesProba[9, 5] = [101, 38, 60, 138, 55, 70, 43, 26, 142]; + ModesProba[9, 6] = [146, 36, 19, 30, 171, 255, 97, 27, 20]; + ModesProba[9, 7] = [138, 45, 61, 62, 219, 1, 81, 188, 64]; + ModesProba[9, 8] = [32, 41, 20, 117, 151, 142, 20, 21, 163]; + ModesProba[9, 9] = [112, 19, 12, 61, 195, 128, 48, 4, 24]; + } + + private static void InitializeFixedCostsI4() + { + Vp8FixedCostsI4[0, 0] = [40, 1151, 1723, 1874, 2103, 2019, 1628, 1777, 2226, 2137]; + Vp8FixedCostsI4[0, 1] = [192, 469, 1296, 1308, 1849, 1794, 1781, 1703, 1713, 1522]; + Vp8FixedCostsI4[0, 2] = [142, 910, 762, 1684, 1849, 1576, 1460, 1305, 1801, 1657]; + Vp8FixedCostsI4[0, 3] = [559, 641, 1370, 421, 1182, 1569, 1612, 1725, 863, 1007]; + Vp8FixedCostsI4[0, 4] = [299, 1059, 1256, 1108, 636, 1068, 1581, 1883, 869, 1142]; + Vp8FixedCostsI4[0, 5] = [277, 1111, 707, 1362, 1089, 672, 1603, 1541, 1545, 1291]; + Vp8FixedCostsI4[0, 6] = [214, 781, 1609, 1303, 1632, 2229, 726, 1560, 1713, 918]; + Vp8FixedCostsI4[0, 7] = [152, 1037, 1046, 1759, 1983, 2174, 1358, 742, 1740, 1390]; + Vp8FixedCostsI4[0, 8] = [512, 1046, 1420, 753, 752, 1297, 1486, 1613, 460, 1207]; + Vp8FixedCostsI4[0, 9] = [424, 827, 1362, 719, 1462, 1202, 1199, 1476, 1199, 538]; + Vp8FixedCostsI4[1, 0] = [240, 402, 1134, 1491, 1659, 1505, 1517, 1555, 1979, 2099]; + Vp8FixedCostsI4[1, 1] = [467, 242, 960, 1232, 1714, 1620, 1834, 1570, 1676, 1391]; + Vp8FixedCostsI4[1, 2] = [500, 455, 463, 1507, 1699, 1282, 1564, 982, 2114, 2114]; + Vp8FixedCostsI4[1, 3] = [672, 643, 1372, 331, 1589, 1667, 1453, 1938, 996, 876]; + Vp8FixedCostsI4[1, 4] = [458, 783, 1037, 911, 738, 968, 1165, 1518, 859, 1033]; + Vp8FixedCostsI4[1, 5] = [504, 815, 504, 1139, 1219, 719, 1506, 1085, 1268, 1268]; + Vp8FixedCostsI4[1, 6] = [333, 630, 1445, 1239, 1883, 3672, 799, 1548, 1865, 598]; + Vp8FixedCostsI4[1, 7] = [399, 644, 746, 1342, 1856, 1350, 1493, 613, 1855, 1015]; + Vp8FixedCostsI4[1, 8] = [622, 749, 1205, 608, 1066, 1408, 1290, 1406, 546, 971]; + Vp8FixedCostsI4[1, 9] = [500, 753, 1041, 668, 1230, 1617, 1297, 1425, 1383, 523]; + Vp8FixedCostsI4[2, 0] = [394, 553, 523, 1502, 1536, 981, 1608, 1142, 1666, 2181]; + Vp8FixedCostsI4[2, 1] = [655, 430, 375, 1411, 1861, 1220, 1677, 1135, 1978, 1553]; + Vp8FixedCostsI4[2, 2] = [690, 640, 245, 1954, 2070, 1194, 1528, 982, 1972, 2232]; + Vp8FixedCostsI4[2, 3] = [559, 834, 741, 867, 1131, 980, 1225, 852, 1092, 784]; + Vp8FixedCostsI4[2, 4] = [690, 875, 516, 959, 673, 894, 1056, 1190, 1528, 1126]; + Vp8FixedCostsI4[2, 5] = [740, 951, 384, 1277, 1177, 492, 1579, 1155, 1846, 1513]; + Vp8FixedCostsI4[2, 6] = [323, 775, 1062, 1776, 3062, 1274, 813, 1188, 1372, 655]; + Vp8FixedCostsI4[2, 7] = [488, 971, 484, 1767, 1515, 1775, 1115, 503, 1539, 1461]; + Vp8FixedCostsI4[2, 8] = [740, 1006, 998, 709, 851, 1230, 1337, 788, 741, 721]; + Vp8FixedCostsI4[2, 9] = [522, 1073, 573, 1045, 1346, 887, 1046, 1146, 1203, 697]; + Vp8FixedCostsI4[3, 0] = [105, 864, 1442, 1009, 1934, 1840, 1519, 1920, 1673, 1579]; + Vp8FixedCostsI4[3, 1] = [534, 305, 1193, 683, 1388, 2164, 1802, 1894, 1264, 1170]; + Vp8FixedCostsI4[3, 2] = [305, 518, 877, 1108, 1426, 3215, 1425, 1064, 1320, 1242]; + Vp8FixedCostsI4[3, 3] = [683, 732, 1927, 257, 1493, 2048, 1858, 1552, 1055, 947]; + Vp8FixedCostsI4[3, 4] = [394, 814, 1024, 660, 959, 1556, 1282, 1289, 893, 1047]; + Vp8FixedCostsI4[3, 5] = [528, 615, 996, 940, 1201, 635, 1094, 2515, 803, 1358]; + Vp8FixedCostsI4[3, 6] = [347, 614, 1609, 1187, 3133, 1345, 1007, 1339, 1017, 667]; + Vp8FixedCostsI4[3, 7] = [218, 740, 878, 1605, 3650, 3650, 1345, 758, 1357, 1617]; + Vp8FixedCostsI4[3, 8] = [672, 750, 1541, 558, 1257, 1599, 1870, 2135, 402, 1087]; + Vp8FixedCostsI4[3, 9] = [592, 684, 1161, 430, 1092, 1497, 1475, 1489, 1095, 822]; + Vp8FixedCostsI4[4, 0] = [228, 1056, 1059, 1368, 752, 982, 1512, 1518, 987, 1782]; + Vp8FixedCostsI4[4, 1] = [494, 514, 818, 942, 965, 892, 1610, 1356, 1048, 1363]; + Vp8FixedCostsI4[4, 2] = [512, 648, 591, 1042, 761, 991, 1196, 1454, 1309, 1463]; + Vp8FixedCostsI4[4, 3] = [683, 749, 1043, 676, 841, 1396, 1133, 1138, 654, 939]; + Vp8FixedCostsI4[4, 4] = [622, 1101, 1126, 994, 361, 1077, 1203, 1318, 877, 1219]; + Vp8FixedCostsI4[4, 5] = [631, 1068, 857, 1650, 651, 477, 1650, 1419, 828, 1170]; + Vp8FixedCostsI4[4, 6] = [555, 727, 1068, 1335, 3127, 1339, 820, 1331, 1077, 429]; + Vp8FixedCostsI4[4, 7] = [504, 879, 624, 1398, 889, 889, 1392, 808, 891, 1406]; + Vp8FixedCostsI4[4, 8] = [683, 1602, 1289, 977, 578, 983, 1280, 1708, 406, 1122]; + Vp8FixedCostsI4[4, 9] = [399, 865, 1433, 1070, 1072, 764, 968, 1477, 1223, 678]; + Vp8FixedCostsI4[5, 0] = [333, 760, 935, 1638, 1010, 529, 1646, 1410, 1472, 2219]; + Vp8FixedCostsI4[5, 1] = [512, 494, 750, 1160, 1215, 610, 1870, 1868, 1628, 1169]; + Vp8FixedCostsI4[5, 2] = [572, 646, 492, 1934, 1208, 603, 1580, 1099, 1398, 1995]; + Vp8FixedCostsI4[5, 3] = [786, 789, 942, 581, 1018, 951, 1599, 1207, 731, 768]; + Vp8FixedCostsI4[5, 4] = [690, 1015, 672, 1078, 582, 504, 1693, 1438, 1108, 2897]; + Vp8FixedCostsI4[5, 5] = [768, 1267, 571, 2005, 1243, 244, 2881, 1380, 1786, 1453]; + Vp8FixedCostsI4[5, 6] = [452, 899, 1293, 903, 1311, 3100, 465, 1311, 1319, 813]; + Vp8FixedCostsI4[5, 7] = [394, 927, 942, 1103, 1358, 1104, 946, 593, 1363, 1109]; + Vp8FixedCostsI4[5, 8] = [559, 1005, 1007, 1016, 658, 1173, 1021, 1164, 623, 1028]; + Vp8FixedCostsI4[5, 9] = [564, 796, 632, 1005, 1014, 863, 2316, 1268, 938, 764]; + Vp8FixedCostsI4[6, 0] = [266, 606, 1098, 1228, 1497, 1243, 948, 1030, 1734, 1461]; + Vp8FixedCostsI4[6, 1] = [366, 585, 901, 1060, 1407, 1247, 876, 1134, 1620, 1054]; + Vp8FixedCostsI4[6, 2] = [452, 565, 542, 1729, 1479, 1479, 1016, 886, 2938, 1150]; + Vp8FixedCostsI4[6, 3] = [555, 1088, 1533, 950, 1354, 895, 834, 1019, 1021, 496]; + Vp8FixedCostsI4[6, 4] = [704, 815, 1193, 971, 973, 640, 1217, 2214, 832, 578]; + Vp8FixedCostsI4[6, 5] = [672, 1245, 579, 871, 875, 774, 872, 1273, 1027, 949]; + Vp8FixedCostsI4[6, 6] = [296, 1134, 2050, 1784, 1636, 3425, 442, 1550, 2076, 722]; + Vp8FixedCostsI4[6, 7] = [342, 982, 1259, 1846, 1848, 1848, 622, 568, 1847, 1052]; + Vp8FixedCostsI4[6, 8] = [555, 1064, 1304, 828, 746, 1343, 1075, 1329, 1078, 494]; + Vp8FixedCostsI4[6, 9] = [288, 1167, 1285, 1174, 1639, 1639, 833, 2254, 1304, 509]; + Vp8FixedCostsI4[7, 0] = [342, 719, 767, 1866, 1757, 1270, 1246, 550, 1746, 2151]; + Vp8FixedCostsI4[7, 1] = [483, 653, 694, 1509, 1459, 1410, 1218, 507, 1914, 1266]; + Vp8FixedCostsI4[7, 2] = [488, 757, 447, 2979, 1813, 1268, 1654, 539, 1849, 2109]; + Vp8FixedCostsI4[7, 3] = [522, 1097, 1085, 851, 1365, 1111, 851, 901, 961, 605]; + Vp8FixedCostsI4[7, 4] = [709, 716, 841, 728, 736, 945, 941, 862, 2845, 1057]; + Vp8FixedCostsI4[7, 5] = [512, 1323, 500, 1336, 1083, 681, 1342, 717, 1604, 1350]; + Vp8FixedCostsI4[7, 6] = [452, 1155, 1372, 1900, 1501, 3290, 311, 944, 1919, 922]; + Vp8FixedCostsI4[7, 7] = [403, 1520, 977, 2132, 1733, 3522, 1076, 276, 3335, 1547]; + Vp8FixedCostsI4[7, 8] = [559, 1374, 1101, 615, 673, 2462, 974, 795, 984, 984]; + Vp8FixedCostsI4[7, 9] = [547, 1122, 1062, 812, 1410, 951, 1140, 622, 1268, 651]; + Vp8FixedCostsI4[8, 0] = [165, 982, 1235, 938, 1334, 1366, 1659, 1578, 964, 1612]; + Vp8FixedCostsI4[8, 1] = [592, 422, 925, 847, 1139, 1112, 1387, 2036, 861, 1041]; + Vp8FixedCostsI4[8, 2] = [403, 837, 732, 770, 941, 1658, 1250, 809, 1407, 1407]; + Vp8FixedCostsI4[8, 3] = [896, 874, 1071, 381, 1568, 1722, 1437, 2192, 480, 1035]; + Vp8FixedCostsI4[8, 4] = [640, 1098, 1012, 1032, 684, 1382, 1581, 2106, 416, 865]; + Vp8FixedCostsI4[8, 5] = [559, 1005, 819, 914, 710, 770, 1418, 920, 838, 1435]; + Vp8FixedCostsI4[8, 6] = [415, 1258, 1245, 870, 1278, 3067, 770, 1021, 1287, 522]; + Vp8FixedCostsI4[8, 7] = [406, 990, 601, 1009, 1265, 1265, 1267, 759, 1017, 1277]; + Vp8FixedCostsI4[8, 8] = [968, 1182, 1329, 788, 1032, 1292, 1705, 1714, 203, 1403]; + Vp8FixedCostsI4[8, 9] = [732, 877, 1279, 471, 901, 1161, 1545, 1294, 755, 755]; + Vp8FixedCostsI4[9, 0] = [111, 931, 1378, 1185, 1933, 1648, 1148, 1714, 1873, 1307]; + Vp8FixedCostsI4[9, 1] = [406, 414, 1030, 1023, 1910, 1404, 1313, 1647, 1509, 793]; + Vp8FixedCostsI4[9, 2] = [342, 640, 575, 1088, 1241, 1349, 1161, 1350, 1756, 1502]; + Vp8FixedCostsI4[9, 3] = [559, 766, 1185, 357, 1682, 1428, 1329, 1897, 1219, 802]; + Vp8FixedCostsI4[9, 4] = [473, 909, 1164, 771, 719, 2508, 1427, 1432, 722, 782]; + Vp8FixedCostsI4[9, 5] = [342, 892, 785, 1145, 1150, 794, 1296, 1550, 973, 1057]; + Vp8FixedCostsI4[9, 6] = [208, 1036, 1326, 1343, 1606, 3395, 815, 1455, 1618, 712]; + Vp8FixedCostsI4[9, 7] = [228, 928, 890, 1046, 3499, 1711, 994, 829, 1720, 1318]; + Vp8FixedCostsI4[9, 8] = [768, 724, 1058, 636, 991, 1075, 1319, 1324, 616, 825]; + Vp8FixedCostsI4[9, 9] = [305, 1167, 1358, 899, 1587, 1587, 987, 1988, 1332, 501]; + } + } +} diff --git a/ImageSharp/Formats/Webp/WebpMetadata.cs b/ImageSharp/Formats/Webp/WebpMetadata.cs new file mode 100644 index 0000000..bd356db --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpMetadata.cs @@ -0,0 +1,160 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp { + /// + /// Provides Webp specific metadata information for the image. + /// + public class WebpMetadata : IFormatMetadata + { + /// + /// Initializes a new instance of the class. + /// + public WebpMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private WebpMetadata(WebpMetadata other) + { + this.BitsPerPixel = other.BitsPerPixel; + this.ColorType = other.ColorType; + this.FileFormat = other.FileFormat; + this.RepeatCount = other.RepeatCount; + this.BackgroundColor = other.BackgroundColor; + } + + /// + /// Gets or sets the number of bits per pixel. + /// + public WebpBitsPerPixel BitsPerPixel { get; set; } = WebpBitsPerPixel.Bit32; + + /// + /// Gets or sets the color type. + /// + public WebpColorType ColorType { get; set; } = WebpColorType.Rgba; + + /// + /// Gets or sets the webp file format used. Either lossless or lossy. + /// + public WebpFileFormatType FileFormat { get; set; } = WebpFileFormatType.Lossy; + + /// + /// Gets or sets the loop count. The number of times to loop the animation. 0 means infinitely. + /// + public ushort RepeatCount { get; set; } = 1; + + /// + /// Gets or sets the default background color of the canvas when animating. + /// This color may be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when the Disposal method is . + /// + public Color BackgroundColor { get; set; } + + /// + public static WebpMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + WebpBitsPerPixel bitsPerPixel; + WebpColorType color; + PixelColorType colorType = metadata.PixelTypeInfo.ColorType; + switch (colorType) + { + case PixelColorType.RGB: + case PixelColorType.BGR: + color = WebpColorType.Rgb; + bitsPerPixel = WebpBitsPerPixel.Bit24; + break; + case PixelColorType.YCbCr: + color = WebpColorType.Yuv; + bitsPerPixel = WebpBitsPerPixel.Bit24; + break; + default: + if (colorType.HasFlag(PixelColorType.Alpha)) + { + color = WebpColorType.Rgba; + bitsPerPixel = WebpBitsPerPixel.Bit32; + break; + } + + color = WebpColorType.Rgb; + bitsPerPixel = WebpBitsPerPixel.Bit24; + break; + } + + return new WebpMetadata + { + BitsPerPixel = bitsPerPixel, + ColorType = color, + BackgroundColor = metadata.BackgroundColor, + RepeatCount = metadata.RepeatCount, + FileFormat = metadata.EncodingType == EncodingType.Lossless ? WebpFileFormatType.Lossless : WebpFileFormatType.Lossy + }; + } + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp; + PixelColorType colorType; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + PixelComponentInfo info; + switch (this.ColorType) + { + case WebpColorType.Yuv: + bpp = 24; + colorType = PixelColorType.YCbCr; + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + break; + case WebpColorType.Rgb: + bpp = 24; + colorType = PixelColorType.RGB; + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + break; + case WebpColorType.Rgba: + default: + bpp = 32; + colorType = PixelColorType.RGB | PixelColorType.Alpha; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ColorType = colorType, + ComponentInfo = info, + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + EncodingType = this.FileFormat == WebpFileFormatType.Lossless ? EncodingType.Lossless : EncodingType.Lossy, + PixelTypeInfo = this.GetPixelTypeInfo(), + ColorTableMode = FrameColorTableMode.Global, + RepeatCount = this.RepeatCount, + BackgroundColor = this.BackgroundColor + }; + + /// + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public WebpMetadata DeepClone() => new(this); + } +} diff --git a/ImageSharp/Formats/Webp/WebpThrowHelper.cs b/ImageSharp/Formats/Webp/WebpThrowHelper.cs new file mode 100644 index 0000000..7008d07 --- /dev/null +++ b/ImageSharp/Formats/Webp/WebpThrowHelper.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Webp { + internal static class WebpThrowHelper + { + [DoesNotReturn] + public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage); + + [DoesNotReturn] + public static void ThrowImageFormatException(string errorMessage) => throw new ImageFormatException(errorMessage); + + [DoesNotReturn] + public static void ThrowNotSupportedException(string errorMessage) => throw new NotSupportedException(errorMessage); + + [DoesNotReturn] + public static void ThrowInvalidImageDimensions(string errorMessage) => throw new InvalidImageContentException(errorMessage); + + [DoesNotReturn] + public static void ThrowDimensionsTooLarge(int width, int height) => throw new ImageFormatException($"Image is too large to encode at {width}x{height} for WEBP format."); + } +} diff --git a/ImageSharp/Formats/Webp/Webp_Container_Specification.pdf b/ImageSharp/Formats/Webp/Webp_Container_Specification.pdf new file mode 100644 index 0000000..e237cb3 Binary files /dev/null and b/ImageSharp/Formats/Webp/Webp_Container_Specification.pdf differ diff --git a/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs b/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs new file mode 100644 index 0000000..7c3f164 --- /dev/null +++ b/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs @@ -0,0 +1,1253 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Cur; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Ico; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Pbm; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Qoi; +using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Formats.Exr; +using System.Threading.Tasks; +using System.Threading; +using System.IO; + +namespace SixLabors.ImageSharp { + + + /// + /// Extension methods for the type. + /// + public static partial class ImageExtensions { + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsBmp(this Image source, string path) => SaveAsBmp(source, path, default); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, string path) => SaveAsBmpAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsBmpAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsBmp(this Image source, string path, BmpEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance)); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, string path, BmpEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsBmp(this Image source, Stream stream) + => SaveAsBmp(source, stream, default); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsBmpAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsBmp(this Image source, Stream stream, BmpEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance)); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, Stream stream, BmpEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsCur(this Image source, string path) => SaveAsCur(source, path, default); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsCurAsync(this Image source, string path) => SaveAsCurAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsCurAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsCurAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsCur(this Image source, string path, CurEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(CurFormat.Instance)); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsCurAsync(this Image source, string path, CurEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(CurFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsCur(this Image source, Stream stream) + => SaveAsCur(source, stream, default); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsCurAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsCurAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsCur(this Image source, Stream stream, CurEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(CurFormat.Instance)); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsCurAsync(this Image source, Stream stream, CurEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(CurFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsGif(this Image source, string path) => SaveAsGif(source, path, default); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, string path) => SaveAsGifAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsGifAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsGif(this Image source, string path, GifEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance)); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, string path, GifEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsGif(this Image source, Stream stream) + => SaveAsGif(source, stream, default); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsGifAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsGif(this Image source, Stream stream, GifEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance)); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, Stream stream, GifEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsIco(this Image source, string path) => SaveAsIco(source, path, default); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsIcoAsync(this Image source, string path) => SaveAsIcoAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsIcoAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsIcoAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsIco(this Image source, string path, IcoEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(IcoFormat.Instance)); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsIcoAsync(this Image source, string path, IcoEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(IcoFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsIco(this Image source, Stream stream) + => SaveAsIco(source, stream, default); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsIcoAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsIcoAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsIco(this Image source, Stream stream, IcoEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(IcoFormat.Instance)); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsIcoAsync(this Image source, Stream stream, IcoEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(IcoFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsJpeg(this Image source, string path) => SaveAsJpeg(source, path, default); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, string path) => SaveAsJpegAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsJpegAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsJpeg(this Image source, string path, JpegEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(JpegFormat.Instance)); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, string path, JpegEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(JpegFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsJpeg(this Image source, Stream stream) + => SaveAsJpeg(source, stream, default); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsJpegAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsJpeg(this Image source, Stream stream, JpegEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(JpegFormat.Instance)); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, Stream stream, JpegEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(JpegFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Pbm format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsPbm(this Image source, string path) => SaveAsPbm(source, path, default); + + /// + /// Saves the image to the given stream with the Pbm format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsPbmAsync(this Image source, string path) => SaveAsPbmAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Pbm format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsPbmAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsPbmAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Pbm format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsPbm(this Image source, string path, PbmEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PbmFormat.Instance)); + + /// + /// Saves the image to the given stream with the Pbm format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsPbmAsync(this Image source, string path, PbmEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PbmFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Pbm format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsPbm(this Image source, Stream stream) + => SaveAsPbm(source, stream, default); + + /// + /// Saves the image to the given stream with the Pbm format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsPbmAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsPbmAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Pbm format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsPbm(this Image source, Stream stream, PbmEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PbmFormat.Instance)); + + /// + /// Saves the image to the given stream with the Pbm format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsPbmAsync(this Image source, Stream stream, PbmEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PbmFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsPng(this Image source, string path) => SaveAsPng(source, path, default); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, string path) => SaveAsPngAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsPngAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsPng(this Image source, string path, PngEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PngFormat.Instance)); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, string path, PngEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PngFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsPng(this Image source, Stream stream) + => SaveAsPng(source, stream, default); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsPngAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsPng(this Image source, Stream stream, PngEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PngFormat.Instance)); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, Stream stream, PngEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PngFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Qoi format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsQoi(this Image source, string path) => SaveAsQoi(source, path, default); + + /// + /// Saves the image to the given stream with the Qoi format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsQoiAsync(this Image source, string path) => SaveAsQoiAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Qoi format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsQoiAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsQoiAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Qoi format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsQoi(this Image source, string path, QoiEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(QoiFormat.Instance)); + + /// + /// Saves the image to the given stream with the Qoi format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsQoiAsync(this Image source, string path, QoiEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(QoiFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Qoi format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsQoi(this Image source, Stream stream) + => SaveAsQoi(source, stream, default); + + /// + /// Saves the image to the given stream with the Qoi format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsQoiAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsQoiAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Qoi format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsQoi(this Image source, Stream stream, QoiEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(QoiFormat.Instance)); + + /// + /// Saves the image to the given stream with the Qoi format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsQoiAsync(this Image source, Stream stream, QoiEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(QoiFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsTga(this Image source, string path) => SaveAsTga(source, path, default); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, string path) => SaveAsTgaAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsTgaAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsTga(this Image source, string path, TgaEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TgaFormat.Instance)); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, string path, TgaEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TgaFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsTga(this Image source, Stream stream) + => SaveAsTga(source, stream, default); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsTgaAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TgaFormat.Instance)); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, Stream stream, TgaEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TgaFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsTiff(this Image source, string path) => SaveAsTiff(source, path, default); + + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsTiffAsync(this Image source, string path) => SaveAsTiffAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsTiffAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsTiffAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsTiff(this Image source, string path, TiffEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TiffFormat.Instance)); + + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsTiffAsync(this Image source, string path, TiffEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TiffFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsTiff(this Image source, Stream stream) + => SaveAsTiff(source, stream, default); + + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsTiffAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsTiffAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsTiff(this Image source, Stream stream, TiffEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TiffFormat.Instance)); + + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsTiffAsync(this Image source, Stream stream, TiffEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TiffFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsWebp(this Image source, string path) => SaveAsWebp(source, path, default); + + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsWebpAsync(this Image source, string path) => SaveAsWebpAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsWebpAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsWebpAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsWebp(this Image source, string path, WebpEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance)); + + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsWebpAsync(this Image source, string path, WebpEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsWebp(this Image source, Stream stream) + => SaveAsWebp(source, stream, default); + + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsWebpAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsWebpAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsWebp(this Image source, Stream stream, WebpEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance)); + + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsWebpAsync(this Image source, Stream stream, WebpEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsExr(this Image source, string path) => SaveAsExr(source, path, default); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, string path) => SaveAsExrAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsExrAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsExr(this Image source, string path, ExrEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance)); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, string path, ExrEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsExr(this Image source, Stream stream) + => SaveAsExr(source, stream, default); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsExrAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsExr(this Image source, Stream stream, ExrEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance)); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, Stream stream, ExrEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance), + cancellationToken); + + } +} \ No newline at end of file diff --git a/ImageSharp/Formats/_Generated/ImageExtensions.Save.tt b/ImageSharp/Formats/_Generated/ImageExtensions.Save.tt new file mode 100644 index 0000000..27078a5 --- /dev/null +++ b/ImageSharp/Formats/_Generated/ImageExtensions.Save.tt @@ -0,0 +1,131 @@ +<#@include file="_Formats.ttinclude" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +// +<# + foreach (string fmt in formats) + { +#> +using SixLabors.ImageSharp.Formats.<#= fmt #>; +<# + + } +#> + +namespace SixLabors.ImageSharp; + +/// +/// Extension methods for the type. +/// +public static partial class ImageExtensions +{ +<# + foreach (string fmt in formats) + { +#> + /// + /// Saves the image to the given stream with the <#= fmt #> format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAs<#= fmt #>(this Image source, string path) => SaveAs<#= fmt #>(source, path, default); + + /// + /// Saves the image to the given stream with the <#= fmt #> format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAs<#= fmt #>Async(this Image source, string path) => SaveAs<#= fmt #>Async(source, path, default); + + /// + /// Saves the image to the given stream with the <#= fmt #> format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAs<#= fmt #>Async(this Image source, string path, CancellationToken cancellationToken) + => SaveAs<#= fmt #>Async(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the <#= fmt #> format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAs<#= fmt #>(this Image source, string path, <#= fmt #>Encoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance)); + + /// + /// Saves the image to the given stream with the <#= fmt #> format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAs<#= fmt #>Async(this Image source, string path, <#= fmt #>Encoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the <#= fmt #> format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAs<#= fmt #>(this Image source, Stream stream) + => SaveAs<#= fmt #>(source, stream, default); + + /// + /// Saves the image to the given stream with the <#= fmt #> format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAs<#= fmt #>Async(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAs<#= fmt #>Async(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the <#= fmt #> format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAs<#= fmt #>(this Image source, Stream stream, <#= fmt #>Encoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance)); + + /// + /// Saves the image to the given stream with the <#= fmt #> format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAs<#= fmt #>Async(this Image source, Stream stream, <#= fmt #>Encoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance), + cancellationToken); + +<# +} +#> +} diff --git a/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs b/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs new file mode 100644 index 0000000..9dcbcc2 --- /dev/null +++ b/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs @@ -0,0 +1,387 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Cur; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Ico; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Pbm; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Qoi; +using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Formats.Exr; + +namespace SixLabors.ImageSharp { + + + /// + /// Extension methods for the and types. + /// + public static class ImageMetadataExtensions { + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static BmpMetadata GetBmpMetadata(this ImageMetadata source) => source.GetFormatMetadata(BmpFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static BmpMetadata CloneBmpMetadata(this ImageMetadata source) => source.CloneFormatMetadata(BmpFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static CurMetadata GetCurMetadata(this ImageMetadata source) => source.GetFormatMetadata(CurFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static CurMetadata CloneCurMetadata(this ImageMetadata source) => source.CloneFormatMetadata(CurFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static GifMetadata GetGifMetadata(this ImageMetadata source) => source.GetFormatMetadata(GifFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static GifMetadata CloneGifMetadata(this ImageMetadata source) => source.CloneFormatMetadata(GifFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static IcoMetadata GetIcoMetadata(this ImageMetadata source) => source.GetFormatMetadata(IcoFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static IcoMetadata CloneIcoMetadata(this ImageMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static JpegMetadata GetJpegMetadata(this ImageMetadata source) => source.GetFormatMetadata(JpegFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static JpegMetadata CloneJpegMetadata(this ImageMetadata source) => source.CloneFormatMetadata(JpegFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static PbmMetadata GetPbmMetadata(this ImageMetadata source) => source.GetFormatMetadata(PbmFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static PbmMetadata ClonePbmMetadata(this ImageMetadata source) => source.CloneFormatMetadata(PbmFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static PngMetadata GetPngMetadata(this ImageMetadata source) => source.GetFormatMetadata(PngFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static PngMetadata ClonePngMetadata(this ImageMetadata source) => source.CloneFormatMetadata(PngFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static QoiMetadata GetQoiMetadata(this ImageMetadata source) => source.GetFormatMetadata(QoiFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static QoiMetadata CloneQoiMetadata(this ImageMetadata source) => source.CloneFormatMetadata(QoiFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static TgaMetadata GetTgaMetadata(this ImageMetadata source) => source.GetFormatMetadata(TgaFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static TgaMetadata CloneTgaMetadata(this ImageMetadata source) => source.CloneFormatMetadata(TgaFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static TiffMetadata GetTiffMetadata(this ImageMetadata source) => source.GetFormatMetadata(TiffFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static TiffMetadata CloneTiffMetadata(this ImageMetadata source) => source.CloneFormatMetadata(TiffFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static WebpMetadata GetWebpMetadata(this ImageMetadata source) => source.GetFormatMetadata(WebpFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static WebpMetadata CloneWebpMetadata(this ImageMetadata source) => source.CloneFormatMetadata(WebpFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static ExrMetadata GetExrMetadata(this ImageMetadata source) => source.GetFormatMetadata(ExrFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static ExrMetadata CloneExrMetadata(this ImageMetadata source) => source.CloneFormatMetadata(ExrFormat.Instance); + + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static CurFrameMetadata GetCurMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(CurFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static CurFrameMetadata CloneCurMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(CurFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static IcoFrameMetadata GetIcoMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(IcoFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static IcoFrameMetadata CloneIcoMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static GifFrameMetadata GetGifMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(GifFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static GifFrameMetadata CloneGifMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(GifFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static PngFrameMetadata GetPngMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static PngFrameMetadata ClonePngMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(PngFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static TiffFrameMetadata GetTiffMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(TiffFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static TiffFrameMetadata CloneTiffMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(TiffFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static WebpFrameMetadata GetWebpMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(WebpFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static WebpFrameMetadata CloneWebpMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(WebpFormat.Instance); + } +} \ No newline at end of file diff --git a/ImageSharp/Formats/_Generated/ImageMetadataExtensions.tt b/ImageSharp/Formats/_Generated/ImageMetadataExtensions.tt new file mode 100644 index 0000000..982cfb4 --- /dev/null +++ b/ImageSharp/Formats/_Generated/ImageMetadataExtensions.tt @@ -0,0 +1,77 @@ +<#@include file="_Formats.ttinclude" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +// +using SixLabors.ImageSharp.Metadata; +<# + foreach (string fmt in formats) + { +#> +using SixLabors.ImageSharp.Formats.<#= fmt #>; +<# + + } +#> + +namespace SixLabors.ImageSharp; + +/// +/// Extension methods for the and types. +/// +public static class ImageMetadataExtensions +{ +<# + foreach (string fmt in formats) + { +#> + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static <#= fmt #>Metadata Get<#= fmt #>Metadata(this ImageMetadata source) => source.GetFormatMetadata(<#= fmt #>Format.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static <#= fmt #>Metadata Clone<#= fmt #>Metadata(this ImageMetadata source) => source.CloneFormatMetadata(<#= fmt #>Format.Instance); + +<# + } +#> +<# + foreach (string fmt in frameFormats) + { +#> + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static <#= fmt #>FrameMetadata Get<#= fmt #>Metadata(this ImageFrameMetadata source) => source.GetFormatMetadata(<#= fmt #>Format.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static <#= fmt #>FrameMetadata Clone<#= fmt #>Metadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(<#= fmt #>Format.Instance); +<# + } +#> +} diff --git a/ImageSharp/Formats/_Generated/_Formats.ttinclude b/ImageSharp/Formats/_Generated/_Formats.ttinclude new file mode 100644 index 0000000..3f669f8 --- /dev/null +++ b/ImageSharp/Formats/_Generated/_Formats.ttinclude @@ -0,0 +1,29 @@ +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +<#+ + private static readonly string[] formats = [ + "Bmp", + "Cur", + "Gif", + "Ico", + "Jpeg", + "Pbm", + "Png", + "Qoi", + "Tga", + "Tiff", + "Webp", + "Exr" + ]; + + private static readonly string[] frameFormats = [ + "Cur", + "Ico", + "Gif", + "Png", + "Tiff", + "Webp" + ]; +#> diff --git a/ImageSharp/GeometryUtilities.cs b/ImageSharp/GeometryUtilities.cs new file mode 100644 index 0000000..21925b0 --- /dev/null +++ b/ImageSharp/GeometryUtilities.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp { + /// + /// Utility class for common geometric functions. + /// + public static class GeometryUtilities + { + /// + /// Converts a degree (360-periodic) angle to a radian (2*Pi-periodic) angle. + /// + /// The angle in degrees. + /// + /// The representing the degree as radians. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DegreeToRadian(float degree) => degree * (MathF.PI / 180F); + + /// + /// Converts a radian (2*Pi-periodic) angle to a degree (360-periodic) angle. + /// + /// The angle in radians. + /// + /// The representing the degree as radians. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float RadianToDegree(float radian) => radian / (MathF.PI / 180F); + } +} diff --git a/ImageSharp/GraphicOptionsDefaultsExtensions.cs b/ImageSharp/GraphicOptionsDefaultsExtensions.cs new file mode 100644 index 0000000..391b6cc --- /dev/null +++ b/ImageSharp/GraphicOptionsDefaultsExtensions.cs @@ -0,0 +1,97 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing; +using System; + +namespace SixLabors.ImageSharp { + /// + /// Adds extensions that allow the processing of images to the type. + /// + public static class GraphicOptionsDefaultsExtensions + { + /// + /// Sets the default options against the image processing context. + /// + /// The image processing context to store default against. + /// The action to update instance of the default options used. + /// The passed in to allow chaining. + public static IImageProcessingContext SetGraphicsOptions(this IImageProcessingContext context, Action optionsBuilder) + { + GraphicsOptions cloned = context.GetGraphicsOptions().DeepClone(); + optionsBuilder(cloned); + context.Properties[typeof(GraphicsOptions)] = cloned; + return context; + } + + /// + /// Sets the default options against the configuration. + /// + /// The configuration to store default against. + /// The default options to use. + public static void SetGraphicsOptions(this Configuration configuration, Action optionsBuilder) + { + GraphicsOptions cloned = configuration.GetGraphicsOptions().DeepClone(); + optionsBuilder(cloned); + configuration.Properties[typeof(GraphicsOptions)] = cloned; + } + + /// + /// Sets the default options against the image processing context. + /// + /// The image processing context to store default against. + /// The default options to use. + /// The passed in to allow chaining. + public static IImageProcessingContext SetGraphicsOptions(this IImageProcessingContext context, GraphicsOptions options) + { + context.Properties[typeof(GraphicsOptions)] = options; + return context; + } + + /// + /// Sets the default options against the configuration. + /// + /// The configuration to store default against. + /// The default options to use. + public static void SetGraphicsOptions(this Configuration configuration, GraphicsOptions options) + { + configuration.Properties[typeof(GraphicsOptions)] = options; + } + + /// + /// Gets the default options against the image processing context. + /// + /// The image processing context to retrieve defaults from. + /// The globaly configued default options. + public static GraphicsOptions GetGraphicsOptions(this IImageProcessingContext context) + { + if (context.Properties.TryGetValue(typeof(GraphicsOptions), out object? options) && options is GraphicsOptions go) + { + return go; + } + + // do not cache the fall back to config into the the processing context + // in case someone want to change the value on the config and expects it re trflow thru + return context.Configuration.GetGraphicsOptions(); + } + + /// + /// Gets the default options against the image processing context. + /// + /// The configuration to retrieve defaults from. + /// The globaly configued default options. + public static GraphicsOptions GetGraphicsOptions(this Configuration configuration) + { + if (configuration.Properties.TryGetValue(typeof(GraphicsOptions), out object? options) && options is GraphicsOptions go) + { + return go; + } + + GraphicsOptions configOptions = new(); + + // capture the fallback so the same instance will always be returned in case its mutated + configuration.Properties[typeof(GraphicsOptions)] = configOptions; + return configOptions; + } + } +} diff --git a/ImageSharp/GraphicsOptions.cs b/ImageSharp/GraphicsOptions.cs new file mode 100644 index 0000000..5a2a3fb --- /dev/null +++ b/ImageSharp/GraphicsOptions.cs @@ -0,0 +1,90 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp { + /// + /// Provides configuration for controlling how graphics operations are rendered, + /// including antialiasing, pixel blending, alpha composition, and coverage thresholding. + /// + public class GraphicsOptions : IDeepCloneable + { + private float antialiasThreshold = .5F; + private float blendPercentage = 1F; + + /// + /// Initializes a new instance of the class. + /// + public GraphicsOptions() + { + } + + private GraphicsOptions(GraphicsOptions source) + { + this.AlphaCompositionMode = source.AlphaCompositionMode; + this.Antialias = source.Antialias; + this.AntialiasThreshold = source.AntialiasThreshold; + this.BlendPercentage = source.BlendPercentage; + this.ColorBlendingMode = source.ColorBlendingMode; + } + + /// + /// Gets or sets a value indicating whether antialiasing should be applied. + /// When , edges are rendered with smooth sub-pixel coverage. + /// When , coverage is snapped to binary (fully opaque or fully transparent) + /// using as the cutoff. + /// Defaults to . + /// + public bool Antialias { get; set; } = true; + + /// + /// Gets or sets the coverage threshold used when is . + /// Pixels with antialiased coverage above this value are rendered as fully opaque; + /// pixels below are discarded. Valid range is 0 to 1. Lower values preserve more + /// thin features at small sizes. Defaults to 0.5F. + /// + public float AntialiasThreshold + { + get => this.antialiasThreshold; + + set + { + Guard.MustBeBetweenOrEqualTo(value, 0F, 1F, nameof(this.AntialiasThreshold)); + this.antialiasThreshold = value; + } + } + + /// + /// Gets or sets the blending percentage applied to the drawing operation. + /// A value of 1.0 applies the operation at full strength; 0.0 makes it invisible. + /// Valid range is 0 to 1. Defaults to 1.0F. + /// + public float BlendPercentage + { + get => this.blendPercentage; + + set + { + Guard.MustBeBetweenOrEqualTo(value, 0F, 1F, nameof(this.BlendPercentage)); + this.blendPercentage = value; + } + } + + /// + /// Gets or sets the color blending mode used to combine source and destination pixel colors. + /// Defaults to . + /// + public PixelColorBlendingMode ColorBlendingMode { get; set; } = PixelColorBlendingMode.Normal; + + /// + /// Gets or sets the alpha composition mode that determines how source and destination alpha + /// channels are combined using Porter-Duff operators. + /// Defaults to . + /// + public PixelAlphaCompositionMode AlphaCompositionMode { get; set; } = PixelAlphaCompositionMode.SrcOver; + + /// + public GraphicsOptions DeepClone() => new(this); + } +} diff --git a/ImageSharp/IDeepCloneable.cs b/ImageSharp/IDeepCloneable.cs new file mode 100644 index 0000000..e5a3f04 --- /dev/null +++ b/ImageSharp/IDeepCloneable.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp { + /// + /// A generic interface for a deeply cloneable type. + /// + /// The type of object to clone. + public interface IDeepCloneable + where T : class + { + /// + /// Creates a new that is a deep copy of the current instance. + /// + /// The . + public T DeepClone(); + } + + /// + /// An interface for objects that can be cloned. This creates a deep copy of the object. + /// + public interface IDeepCloneable + { + /// + /// Creates a new object that is a deep copy of the current instance. + /// + /// The . + public IDeepCloneable DeepClone(); + } +} diff --git a/ImageSharp/IO/BufferedReadStream.cs b/ImageSharp/IO/BufferedReadStream.cs new file mode 100644 index 0000000..d2ce86c --- /dev/null +++ b/ImageSharp/IO/BufferedReadStream.cs @@ -0,0 +1,433 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace SixLabors.ImageSharp.IO { + /// + /// A readonly stream that add a secondary level buffer in addition to native stream + /// buffered reading to reduce the overhead of small incremental reads. + /// + internal sealed class BufferedReadStream : Stream + { + private readonly CancellationToken cancellationToken; + + private readonly int maxBufferIndex; + + private readonly byte[] readBuffer; + + private MemoryHandle readBufferHandle; + + private readonly unsafe byte* pinnedReadBuffer; + + // Index within our buffer, not reader position. + private int readBufferIndex; + + // Matches what the stream position would be without buffering + private long readerPosition; + + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The input stream. + /// The optional stream-level cancellation token to detect cancellation in synchronous methods. + public BufferedReadStream(Configuration configuration, Stream stream, CancellationToken cancellationToken = default) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.IsTrue(stream.CanRead, nameof(stream), "Stream must be readable."); + Guard.IsTrue(stream.CanSeek, nameof(stream), "Stream must be seekable."); + + this.cancellationToken = cancellationToken; + + // Ensure all underlying buffers have been flushed before we attempt to read the stream. + // User streams may have opted to throw from Flush if CanWrite is false + // (although the abstract Stream does not do so). + if (stream.CanWrite) + { + stream.Flush(); + } + + this.BaseStream = stream; + this.Length = stream.Length; + this.readerPosition = stream.Position; + this.BufferSize = configuration.StreamProcessingBufferSize; + this.maxBufferIndex = this.BufferSize - 1; + this.readBuffer = ArrayPool.Shared.Rent(this.BufferSize); + this.readBufferHandle = new Memory(this.readBuffer).Pin(); + unsafe + { + this.pinnedReadBuffer = (byte*)this.readBufferHandle.Pointer; + } + + // This triggers a full read on first attempt. + this.readBufferIndex = int.MinValue; + } + + /// + /// Gets the number indicating the EOF hits occurred while reading from this instance. + /// + public int EofHitCount { get; private set; } + + /// + /// Gets the size, in bytes, of the underlying buffer. + /// + public int BufferSize + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + /// + public override long Length { get; } + + /// + public override long Position + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.readerPosition; + + [MethodImpl(MethodImplOptions.NoInlining)] + set + { + Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.Position)); + this.cancellationToken.ThrowIfCancellationRequested(); + + // Only reset readBufferIndex if we are out of bounds of our working buffer + // otherwise we should simply move the value by the diff. + if (this.IsInReadBuffer(value, out long index)) + { + this.readBufferIndex = (int)index; + this.readerPosition = value; + } + else + { + // Base stream seek will throw for us if invalid. + this.BaseStream.Seek(value, SeekOrigin.Begin); + this.readerPosition = value; + this.readBufferIndex = int.MinValue; + } + } + } + + /// + public override bool CanRead { get; } = true; + + /// + public override bool CanSeek { get; } = true; + + /// + public override bool CanWrite { get; } + + /// + /// Gets remaining byte count available to read. + /// + public long RemainingBytes + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.Length - this.Position; + } + + /// + /// Gets the underlying stream. + /// + public Stream BaseStream + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int ReadByte() + { + if (this.readerPosition >= this.Length) + { + this.EofHitCount++; + return -1; + } + + // Our buffer has been read. + // We need to refill and start again. + if ((uint)this.readBufferIndex > (uint)this.maxBufferIndex) + { + this.FillReadBuffer(); + } + + this.readerPosition++; + + unsafe + { + return this.pinnedReadBuffer[this.readBufferIndex++]; + } + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int Read(byte[] buffer, int offset, int count) + => this.Read(buffer.AsSpan(offset, count)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int Read(Span buffer) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + // Too big for our buffer. Read directly from the stream. + int count = buffer.Length; + if (count > this.BufferSize) + { + return this.ReadToBufferDirectSlow(buffer); + } + + // Too big for remaining buffer but less than entire buffer length + // Copy to buffer then read from there. + if ((uint)this.readBufferIndex > (uint)(this.BufferSize - count)) + { + return this.ReadToBufferViaCopySlow(buffer); + } + + return this.ReadToBufferViaCopyFast(buffer); + } + + /// + public override void Flush() + { + // Reset the stream position to match reader position. + Stream baseStream = this.BaseStream; + if (this.readerPosition != baseStream.Position) + { + baseStream.Seek(this.readerPosition, SeekOrigin.Begin); + this.readerPosition = baseStream.Position; + } + + // Reset to trigger full read on next attempt. + this.readBufferIndex = int.MinValue; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override long Seek(long offset, SeekOrigin origin) + { + this.Position = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => this.Position + offset, + SeekOrigin.End => this.Length + offset, + _ => throw new ArgumentOutOfRangeException(nameof(offset)), + }; + + return this.readerPosition; + } + + /// + /// + /// This operation is not supported in . + /// + public override void SetLength(long value) + => throw new NotSupportedException(); + + /// + /// + /// This operation is not supported in . + /// + public override void Write(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + + /// + protected override void Dispose(bool disposing) + { + if (!this.isDisposed) + { + this.isDisposed = true; + this.readBufferHandle.Dispose(); + ArrayPool.Shared.Return(this.readBuffer); + this.Flush(); + + base.Dispose(true); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsInReadBuffer(long newPosition, out long index) + { + index = newPosition - this.readerPosition + this.readBufferIndex; + return index > -1 && index < this.BufferSize; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void FillReadBuffer() + { + this.cancellationToken.ThrowIfCancellationRequested(); + Stream baseStream = this.BaseStream; + if (this.readerPosition != baseStream.Position) + { + baseStream.Seek(this.readerPosition, SeekOrigin.Begin); + } + + // Read doesn't always guarantee the full returned length so read a byte + // at a time until we get either our count or hit the end of the stream. + int n = 0; + int i; + do + { + i = baseStream.Read(this.readBuffer, n, this.BufferSize - n); + n += i; + } + while (n < this.BufferSize && i > 0); + + this.readBufferIndex = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReadToBufferViaCopyFast(Span buffer) + { + int n = this.GetCopyCount(buffer.Length); + + // Just straight copy. MemoryStream does the same so should be fast enough. + this.readBuffer.AsSpan(this.readBufferIndex, n).CopyTo(buffer); + + this.readerPosition += n; + this.readBufferIndex += n; + this.CheckEof(n); + return n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReadToBufferViaCopyFast(byte[] buffer, int offset, int count) + { + int n = this.GetCopyCount(count); + this.CopyBytes(buffer, offset, n); + + this.readerPosition += n; + this.readBufferIndex += n; + + return n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReadToBufferViaCopySlow(Span buffer) + { + // Refill our buffer then copy. + this.FillReadBuffer(); + + return this.ReadToBufferViaCopyFast(buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReadToBufferViaCopySlow(byte[] buffer, int offset, int count) + { + // Refill our buffer then copy. + this.FillReadBuffer(); + + return this.ReadToBufferViaCopyFast(buffer, offset, count); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private int ReadToBufferDirectSlow(Span buffer) + { + // Read to target but don't copy to our read buffer. + Stream baseStream = this.BaseStream; + if (this.readerPosition != baseStream.Position) + { + baseStream.Seek(this.readerPosition, SeekOrigin.Begin); + } + + // Read doesn't always guarantee the full returned length so read a byte + // at a time until we get either our count or hit the end of the stream. + int count = buffer.Length; + int n = 0; + int i; + do + { + i = baseStream.Read(buffer[n..count]); + n += i; + } + while (n < count && i > 0); + + this.Position += n; + + this.CheckEof(n); + return n; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private int ReadToBufferDirectSlow(byte[] buffer, int offset, int count) + { + // Read to target but don't copy to our read buffer. + Stream baseStream = this.BaseStream; + if (this.readerPosition != baseStream.Position) + { + baseStream.Seek(this.readerPosition, SeekOrigin.Begin); + } + + // Read doesn't always guarantee the full returned length so read a byte + // at a time until we get either our count or hit the end of the stream. + int n = 0; + int i; + do + { + i = baseStream.Read(buffer, n + offset, count - n); + n += i; + } + while (n < count && i > 0); + + this.Position += n; + + return n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetCopyCount(int count) + { + long n = this.Length - this.readerPosition; + if (n > count) + { + return count; + } + + if (n < 0) + { + return 0; + } + + return (int)n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void CopyBytes(byte[] buffer, int offset, int count) + { + // Same as MemoryStream. + if (count < 9) + { + int byteCount = count; + int read = this.readBufferIndex; + byte* pinned = this.pinnedReadBuffer; + + while (--byteCount > -1) + { + buffer[offset + byteCount] = pinned[read + byteCount]; + } + } + else + { + Buffer.BlockCopy(this.readBuffer, this.readBufferIndex, buffer, offset, count); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CheckEof(int read) + { + if (read == 0) + { + this.EofHitCount++; + } + } + } +} diff --git a/ImageSharp/IO/ChunkedMemoryStream.cs b/ImageSharp/IO/ChunkedMemoryStream.cs new file mode 100644 index 0000000..71ec7b5 --- /dev/null +++ b/ImageSharp/IO/ChunkedMemoryStream.cs @@ -0,0 +1,470 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.IO { + /// + /// Provides an in-memory stream composed of non-contiguous chunks that doesn't need to be resized. + /// Chunks are allocated by the assigned via the constructor + /// and is designed to take advantage of buffer pooling when available. + /// + internal sealed class ChunkedMemoryStream : Stream + { + private readonly MemoryChunkBuffer memoryChunkBuffer; + private long length; + private long position; + private int bufferIndex; + private int chunkIndex; + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + public ChunkedMemoryStream(MemoryAllocator allocator) + => this.memoryChunkBuffer = new MemoryChunkBuffer(allocator); + + /// + public override bool CanRead => !this.isDisposed; + + /// + public override bool CanSeek => !this.isDisposed; + + /// + public override bool CanWrite => !this.isDisposed; + + /// + public override long Length + { + get + { + this.EnsureNotDisposed(); + return this.length; + } + } + + /// + public override long Position + { + get + { + this.EnsureNotDisposed(); + return this.position; + } + + set + { + this.EnsureNotDisposed(); + this.SetPosition(value); + } + } + + /// + public override void Flush() + { + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + this.EnsureNotDisposed(); + + this.Position = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => this.Position + offset, + SeekOrigin.End => this.Length + offset, + _ => throw new ArgumentOutOfRangeException(nameof(offset)), + }; + + return this.position; + } + + /// + public override void SetLength(long value) + => throw new NotSupportedException(); + + /// + public override int ReadByte() + { + Unsafe.SkipInit(out byte b); + return this.Read(MemoryMarshal.CreateSpan(ref b, 1)) == 1 ? b : -1; + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + Guard.NotNull(buffer, nameof(buffer)); + Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset)); + Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count)); + + const string bufferMessage = "Offset subtracted from the buffer length is less than count."; + Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); + + return this.Read(buffer.AsSpan(offset, count)); + } + + /// + public override int Read(Span buffer) + { + this.EnsureNotDisposed(); + + int offset = 0; + int count = buffer.Length; + + long remaining = this.length - this.position; + if (remaining <= 0) + { + // Already at the end of the stream, nothing to read + return 0; + } + + if (remaining > count) + { + remaining = count; + } + + // 'remaining' can be less than the provided buffer length. + int bytesToRead = (int)remaining; + int bytesRead = 0; + while (bytesToRead > 0 && this.bufferIndex != this.memoryChunkBuffer.Length) + { + bool moveToNextChunk = false; + MemoryChunk chunk = this.memoryChunkBuffer[this.bufferIndex]; + int n = bytesToRead; + int remainingBytesInCurrentChunk = chunk.Length - this.chunkIndex; + if (n >= remainingBytesInCurrentChunk) + { + n = remainingBytesInCurrentChunk; + moveToNextChunk = true; + } + + // Read n bytes from the current chunk + chunk.Buffer.Memory.Span.Slice(this.chunkIndex, n).CopyTo(buffer.Slice(offset, n)); + bytesToRead -= n; + offset += n; + bytesRead += n; + + if (moveToNextChunk) + { + this.chunkIndex = 0; + this.bufferIndex++; + } + else + { + this.chunkIndex += n; + } + } + + this.position += bytesRead; + return bytesRead; + } + + /// + public override void WriteByte(byte value) + => this.Write(MemoryMarshal.CreateSpan(ref value, 1)); + + /// + public override void Write(byte[] buffer, int offset, int count) + { + Guard.NotNull(buffer, nameof(buffer)); + Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset)); + Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count)); + + const string bufferMessage = "Offset subtracted from the buffer length is less than count."; + Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); + + this.Write(buffer.AsSpan(offset, count)); + } + + /// + public override void Write(ReadOnlySpan buffer) + { + this.EnsureNotDisposed(); + + int offset = 0; + int count = buffer.Length; + + long remaining = this.memoryChunkBuffer.Length - this.position; + + // Ensure we have enough capacity to write the data. + while (remaining < count) + { + this.memoryChunkBuffer.Expand(); + remaining = this.memoryChunkBuffer.Length - this.position; + } + + int bytesToWrite = count; + int bytesWritten = 0; + while (bytesToWrite > 0 && this.bufferIndex != this.memoryChunkBuffer.Length) + { + bool moveToNextChunk = false; + MemoryChunk chunk = this.memoryChunkBuffer[this.bufferIndex]; + int n = bytesToWrite; + int remainingBytesInCurrentChunk = chunk.Length - this.chunkIndex; + if (n >= remainingBytesInCurrentChunk) + { + n = remainingBytesInCurrentChunk; + moveToNextChunk = true; + } + + // Write n bytes to the current chunk + buffer.Slice(offset, n).CopyTo(chunk.Buffer.Slice(this.chunkIndex, n)); + bytesToWrite -= n; + offset += n; + bytesWritten += n; + + if (moveToNextChunk) + { + this.chunkIndex = 0; + this.bufferIndex++; + } + else + { + this.chunkIndex += n; + } + } + + this.position += bytesWritten; + this.length += bytesWritten; + } + + /// + /// Writes the entire contents of this memory stream to another stream. + /// + /// The stream to write this memory stream to. + /// is . + /// The current or target stream is closed. + public void WriteTo(Stream stream) + { + Guard.NotNull(stream, nameof(stream)); + this.EnsureNotDisposed(); + + this.Position = 0; + + long remaining = this.length - this.position; + if (remaining <= 0) + { + // Already at the end of the stream, nothing to read + return; + } + + int bytesToRead = (int)remaining; + int bytesRead = 0; + while (bytesToRead > 0 && this.bufferIndex != this.memoryChunkBuffer.Length) + { + bool moveToNextChunk = false; + MemoryChunk chunk = this.memoryChunkBuffer[this.bufferIndex]; + int n = bytesToRead; + int remainingBytesInCurrentChunk = chunk.Length - this.chunkIndex; + if (n >= remainingBytesInCurrentChunk) + { + n = remainingBytesInCurrentChunk; + moveToNextChunk = true; + } + + // Read n bytes from the current chunk + stream.Write(chunk.Buffer.Memory.Span.Slice(this.chunkIndex, n)); + bytesToRead -= n; + bytesRead += n; + + if (moveToNextChunk) + { + this.chunkIndex = 0; + this.bufferIndex++; + } + else + { + this.chunkIndex += n; + } + } + + this.position += bytesRead; + } + + /// + /// Writes the stream contents to a byte array, regardless of the property. + /// + /// A new . + public byte[] ToArray() + { + this.EnsureNotDisposed(); + long position = this.position; + byte[] copy = new byte[this.length]; + + this.Position = 0; + _ = this.Read(copy, 0, copy.Length); + this.Position = position; + return copy; + } + + /// + protected override void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + try + { + this.isDisposed = true; + if (disposing) + { + this.memoryChunkBuffer.Dispose(); + } + + this.bufferIndex = 0; + this.chunkIndex = 0; + this.position = 0; + this.length = 0; + } + finally + { + base.Dispose(disposing); + } + } + + private void SetPosition(long value) + { + long newPosition = value; + if (newPosition < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + this.position = newPosition; + + // Find the current chunk & current chunk index + int currentChunkIndex = 0; + long offset = newPosition; + + // If the new position is greater than the length of the stream, set the position to the end of the stream + if (offset > 0 && offset >= this.memoryChunkBuffer.Length) + { + this.bufferIndex = this.memoryChunkBuffer.ChunkCount - 1; + this.chunkIndex = this.memoryChunkBuffer[this.bufferIndex].Length - 1; + return; + } + + // Loop through the current chunks, as we increment the chunk index, we subtract the length of the chunk + // from the offset. Once the offset is less than the length of the chunk, we have found the correct chunk. + while (offset != 0) + { + int chunkLength = this.memoryChunkBuffer[currentChunkIndex].Length; + if (offset < chunkLength) + { + // Found the correct chunk and the corresponding index + break; + } + + offset -= chunkLength; + currentChunkIndex++; + } + + this.bufferIndex = currentChunkIndex; + + // Safe to cast here as we know the offset is less than the chunk length. + this.chunkIndex = (int)offset; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureNotDisposed() + { + if (this.isDisposed) + { + ThrowDisposed(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(ChunkedMemoryStream), "The stream is closed."); + + private sealed class MemoryChunkBuffer : IDisposable + { + private readonly List memoryChunks = []; + private readonly MemoryAllocator allocator; + private readonly int allocatorCapacity; + private bool isDisposed; + + public MemoryChunkBuffer(MemoryAllocator allocator) + { + this.allocatorCapacity = allocator.GetBufferCapacityInBytes(); + this.allocator = allocator; + } + + public int ChunkCount => this.memoryChunks.Count; + + public long Length { get; private set; } + + public MemoryChunk this[int index] => this.memoryChunks[index]; + + public void Expand() + { + IMemoryOwner buffer = + this.allocator.Allocate(Math.Min(this.allocatorCapacity, GetChunkSize(this.ChunkCount))); + + MemoryChunk chunk = new(buffer) + { + Length = buffer.Length() + }; + + this.memoryChunks.Add(chunk); + this.Length += chunk.Length; + } + + public void Dispose() + { + if (!this.isDisposed) + { + foreach (MemoryChunk chunk in this.memoryChunks) + { + chunk.Dispose(); + } + + this.memoryChunks.Clear(); + this.Length = 0; + this.isDisposed = true; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetChunkSize(int i) + { + // Increment chunks sizes with moderate speed, but without using too many buffers from the + // same ArrayPool bucket of the default MemoryAllocator. + // https://github.com/SixLabors/ImageSharp/pull/2006#issuecomment-1066244720 + const int b128K = 1 << 17; + const int b4M = 1 << 22; + return i < 16 ? b128K * (1 << (int)((uint)i / 4)) : b4M; + } + } + + private sealed class MemoryChunk : IDisposable + { + private bool isDisposed; + + public MemoryChunk(IMemoryOwner buffer) => this.Buffer = buffer; + + public IMemoryOwner Buffer { get; } + + public int Length { get; init; } + + public void Dispose() + { + if (!this.isDisposed) + { + this.Buffer.Dispose(); + this.isDisposed = true; + } + } + } + } +} diff --git a/ImageSharp/IO/IFileSystem.cs b/ImageSharp/IO/IFileSystem.cs new file mode 100644 index 0000000..5f1c63f --- /dev/null +++ b/ImageSharp/IO/IFileSystem.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.ImageSharp.IO { + /// + /// A simple interface representing the filesystem. + /// + internal interface IFileSystem + { + /// + /// Opens a file as defined by the path and returns it as a readable stream. + /// + /// Path to the file to open. + /// A stream representing the opened file. + Stream OpenRead(string path); + + /// + /// Opens a file as defined by the path and returns it as a readable stream + /// that can be used for asynchronous reading. + /// + /// Path to the file to open. + /// A stream representing the opened file. + Stream OpenReadAsynchronous(string path); + + /// + /// Creates or opens a file as defined by the path and returns it as a writable stream. + /// + /// Path to the file to open. + /// A stream representing the opened file. + Stream Create(string path); + + /// + /// Creates or opens a file as defined by the path and returns it as a writable stream + /// that can be used for asynchronous reading and writing. + /// + /// Path to the file to open. + /// A stream representing the opened file. + Stream CreateAsynchronous(string path); + } +} diff --git a/ImageSharp/IO/LocalFileSystem.cs b/ImageSharp/IO/LocalFileSystem.cs new file mode 100644 index 0000000..011cfd5 --- /dev/null +++ b/ImageSharp/IO/LocalFileSystem.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.ImageSharp.IO { + /// + /// A wrapper around the local File apis. + /// + internal sealed class LocalFileSystem : IFileSystem + { + /// + public Stream OpenRead(string path) => File.OpenRead(path); + + /// + public Stream OpenReadAsynchronous(string path) => File.Open(path, new FileStreamOptions + { + Mode = FileMode.Open, + Access = FileAccess.Read, + Share = FileShare.Read, + Options = FileOptions.Asynchronous, + }); + + /// + public Stream Create(string path) => File.Create(path); + + /// + public Stream CreateAsynchronous(string path) => File.Open(path, new FileStreamOptions + { + Mode = FileMode.Create, + Access = FileAccess.ReadWrite, + Share = FileShare.None, + Options = FileOptions.Asynchronous, + }); + } +} diff --git a/ImageSharp/Image.Decode.cs b/ImageSharp/Image.Decode.cs new file mode 100644 index 0000000..6dd652d --- /dev/null +++ b/ImageSharp/Image.Decode.cs @@ -0,0 +1,245 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp { + /// + /// Adds static methods allowing the decoding of new images. + /// + public abstract partial class Image + { + /// + /// Creates an instance backed by an uninitialized memory buffer. + /// This is an optimized creation method intended to be used by decoders. + /// The image might be filled with memory garbage. + /// + /// The pixel type + /// The + /// The width of the image + /// The height of the image + /// The + /// The result + internal static Image CreateUninitialized( + Configuration configuration, + int width, + int height, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Buffer2D uninitializedMemoryBuffer = configuration.MemoryAllocator.Allocate2D( + width, + height, + configuration.PreferContiguousImageBuffers); + return new Image(configuration, uninitializedMemoryBuffer.FastMemoryGroup, width, height, metadata); + } + + /// + /// By reading the header on the provided stream this calculates the images format. + /// + /// The general configuration. + /// The image stream to read the header from. + /// The mime type or null if none found. + /// The input format is not recognized. + private static IImageFormat InternalDetectFormat(Configuration configuration, Stream stream) + { + // We take a minimum of the stream length vs the max header size and always check below + // to ensure that only formats that headers fit within the given buffer length are tested. + int headerSize = (int)Math.Min(configuration.MaxHeaderSize, stream.Length); + if (headerSize <= 0) + { + ImageFormatManager.ThrowInvalidDecoder(configuration.ImageFormatsManager); + } + + // Header sizes are so small, that headersBuffer will be always stackalloc-ed in practice, + // and heap allocation will never happen, there is no need for the usual try-finally ArrayPool dance. + // The array case is only a safety mechanism following stackalloc best practices. + Span headersBuffer = headerSize > 512 ? new byte[headerSize] : stackalloc byte[headerSize]; + long startPosition = stream.Position; + + // Read doesn't always guarantee the full returned length so read a byte + // at a time until we get either our count or hit the end of the stream. + int n = 0; + int i; + do + { + i = stream.Read(headersBuffer[n..headerSize]); + n += i; + } + while (n < headerSize && i > 0); + + stream.Position = startPosition; + + return InternalDetectFormat(configuration, headersBuffer[..n]); + } + + /// + /// By reading the header on the provided stream this calculates the images format. + /// + /// The general configuration. + /// The image stream to read the header from. + /// The token to monitor for cancellation requests. + /// The mime type or null if none found. + /// The input format is not recognized. + private static async ValueTask InternalDetectFormatAsync( + Configuration configuration, + Stream stream, + CancellationToken cancellationToken) + { + // We take a minimum of the stream length vs the max header size and always check below + // to ensure that only formats that headers fit within the given buffer length are tested. + int headerSize = (int)Math.Min(configuration.MaxHeaderSize, stream.Length); + if (headerSize <= 0) + { + ImageFormatManager.ThrowInvalidDecoder(configuration.ImageFormatsManager); + } + + using (IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(headerSize)) + { + Memory headersBuffer = memoryOwner.Memory; + long startPosition = stream.Position; + + // Read doesn't always guarantee the full returned length so read a byte + // at a time until we get either our count or hit the end of the stream. + int n = 0; + int i; + do + { + i = await stream.ReadAsync(headersBuffer[n..headerSize], cancellationToken); + n += i; + } + while (n < headerSize && i > 0); + + stream.Position = startPosition; + + return InternalDetectFormat(configuration, headersBuffer.Span[..n]); + } + } + + private static IImageFormat InternalDetectFormat( + Configuration configuration, + ReadOnlySpan headersBuffer) + { + // Does the given stream contain enough data to fit in the header for the format + // and does that data match the format specification? + // Individual formats should still check since they are public. + foreach (IImageFormatDetector formatDetector in configuration.ImageFormatsManager.FormatDetectors) + { + if (formatDetector.HeaderSize <= headersBuffer.Length && formatDetector.TryDetectFormat(headersBuffer, out IImageFormat? attemptFormat)) + { + return attemptFormat; + } + } + + ImageFormatManager.ThrowInvalidDecoder(configuration.ImageFormatsManager); + + // Need to write this otherwise compiler is not happy + return null; + } + + /// + /// By reading the header on the provided stream this calculates the images format. + /// + /// The general decoder options. + /// The image stream to read the header from. + /// The . + private static IImageDecoder DiscoverDecoder(DecoderOptions options, Stream stream) + { + IImageFormat format = InternalDetectFormat(options.Configuration, stream); + return options.Configuration.ImageFormatsManager.GetDecoder(format); + } + + /// + /// By reading the header on the provided stream this calculates the images format. + /// + /// The general decoder options. + /// The image stream to read the header from. + /// The token to monitor for cancellation requests. + /// The . + private static async ValueTask DiscoverDecoderAsync( + DecoderOptions options, + Stream stream, + CancellationToken cancellationToken) + { + IImageFormat format = await InternalDetectFormatAsync(options.Configuration, stream, cancellationToken); + return options.Configuration.ImageFormatsManager.GetDecoder(format); + } + + /// + /// Decodes the image stream to the current image. + /// + /// The general decoder options. + /// The stream. + /// The pixel format. + /// + /// A new . + /// + private static Image Decode(DecoderOptions options, Stream stream) + where TPixel : unmanaged, IPixel + { + IImageDecoder decoder = DiscoverDecoder(options, stream); + return decoder.Decode(options, stream); + } + + private static async Task> DecodeAsync( + DecoderOptions options, + Stream stream, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + IImageDecoder decoder = await DiscoverDecoderAsync(options, stream, cancellationToken); + return await decoder.DecodeAsync(options, stream, cancellationToken); + } + + private static Image Decode(DecoderOptions options, Stream stream) + { + IImageDecoder decoder = DiscoverDecoder(options, stream); + return decoder.Decode(options, stream); + } + + private static async Task DecodeAsync( + DecoderOptions options, + Stream stream, + CancellationToken cancellationToken) + { + IImageDecoder decoder = await DiscoverDecoderAsync(options, stream, cancellationToken); + return await decoder.DecodeAsync(options, stream, cancellationToken); + } + + /// + /// Reads the raw image information from the specified stream. + /// + /// The general decoder options. + /// The stream. + /// The . + private static ImageInfo InternalIdentify(DecoderOptions options, Stream stream) + { + IImageDecoder decoder = DiscoverDecoder(options, stream); + return decoder.Identify(options, stream); + } + + /// + /// Reads the raw image information from the specified stream. + /// + /// The general decoder options. + /// The stream. + /// The token to monitor for cancellation requests. + /// The . + private static async Task InternalIdentifyAsync( + DecoderOptions options, + Stream stream, + CancellationToken cancellationToken) + { + IImageDecoder decoder = await DiscoverDecoderAsync(options, stream, cancellationToken); + return await decoder.IdentifyAsync(options, stream, cancellationToken); + } + } +} diff --git a/ImageSharp/Image.FromBytes.cs b/ImageSharp/Image.FromBytes.cs new file mode 100644 index 0000000..8fbe8a0 --- /dev/null +++ b/ImageSharp/Image.FromBytes.cs @@ -0,0 +1,170 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.IO; + +namespace SixLabors.ImageSharp { + /// + /// Adds static methods allowing the creation of new image from a byte span. + /// + public abstract partial class Image + { + /// + /// By reading the header on the provided byte span this calculates the images format. + /// + /// The byte span containing encoded image data to read the header from. + /// The . + /// The image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static IImageFormat DetectFormat(ReadOnlySpan buffer) + => DetectFormat(DecoderOptions.Default, buffer); + + /// + /// By reading the header on the provided byte span this calculates the images format. + /// + /// The general decoder options. + /// The byte span containing encoded image data to read the header from. + /// The . + /// The options are null. + /// The image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static unsafe IImageFormat DetectFormat(DecoderOptions options, ReadOnlySpan buffer) + { + Guard.NotNull(options, nameof(options)); + + if (buffer.IsEmpty) + { + throw new UnknownImageFormatException("Cannot detect image format from empty data."); + } + + fixed (byte* ptr = buffer) + { + using UnmanagedMemoryStream stream = new(ptr, buffer.Length); + return DetectFormat(options, stream); + } + } + + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The byte array containing encoded image data to read the header from. + /// The . + /// The image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static ImageInfo Identify(ReadOnlySpan buffer) + => Identify(DecoderOptions.Default, buffer); + + /// + /// Reads the raw image information from the specified span of bytes without fully decoding it. + /// + /// The general decoder options. + /// The byte span containing encoded image data to read the header from. + /// The . + /// The options are null. + /// The image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static unsafe ImageInfo Identify(DecoderOptions options, ReadOnlySpan buffer) + { + Guard.NotNull(options, nameof(options)); + + if (buffer.IsEmpty) + { + throw new UnknownImageFormatException("Cannot identify image format from empty data."); + } + + fixed (byte* ptr = buffer) + { + using UnmanagedMemoryStream stream = new(ptr, buffer.Length); + return Identify(options, stream); + } + } + + /// + /// Creates a new instance of the class from the given byte span. + /// The pixel format is automatically determined by the decoder. + /// + /// The byte span containing encoded image data. + /// . + /// The image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + /// The . + public static Image Load(ReadOnlySpan buffer) + => Load(DecoderOptions.Default, buffer); + + /// + /// Creates a new instance of the class from the given byte span. + /// The pixel format is automatically determined by the decoder. + /// + /// The general decoder options. + /// The byte span containing encoded image data. + /// . + /// The options are null. + /// The image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static unsafe Image Load(DecoderOptions options, ReadOnlySpan buffer) + { + Guard.NotNull(options, nameof(options)); + + if (buffer.IsEmpty) + { + throw new UnknownImageFormatException("Cannot load image from empty data."); + } + + fixed (byte* ptr = buffer) + { + using UnmanagedMemoryStream stream = new(ptr, buffer.Length); + return Load(options, stream); + } + } + + /// + /// Creates a new instance of the class from the given byte span. + /// + /// The pixel format. + /// The byte span containing encoded image data. + /// . + /// The image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Image Load(ReadOnlySpan data) + where TPixel : unmanaged, IPixel + => Load(DecoderOptions.Default, data); + + /// + /// Creates a new instance of the class from the given byte span. + /// + /// The pixel format. + /// The general decoder options. + /// The byte span containing encoded image data. + /// . + /// The options are null. + /// The image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static unsafe Image Load(DecoderOptions options, ReadOnlySpan data) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(options, nameof(options)); + + if (data.IsEmpty) + { + throw new UnknownImageFormatException("Cannot load image from empty data."); + } + + fixed (byte* ptr = data) + { + using UnmanagedMemoryStream stream = new(ptr, data.Length); + return Load(options, stream); + } + } + } +} diff --git a/ImageSharp/Image.FromFile.cs b/ImageSharp/Image.FromFile.cs new file mode 100644 index 0000000..634a812 --- /dev/null +++ b/ImageSharp/Image.FromFile.cs @@ -0,0 +1,300 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.PixelFormats; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SixLabors.ImageSharp { + /// + /// Adds static methods allowing the creation of new image from a given file. + /// + public abstract partial class Image + { + /// + /// Detects the encoded image format type from the specified file. + /// + /// The image file to open and to read the header from. + /// The . + /// The path is null. + /// The file stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static IImageFormat DetectFormat(string path) + => DetectFormat(DecoderOptions.Default, path); + + /// + /// Detects the encoded image format type from the specified file. + /// + /// The general decoder options. + /// The image file to open and to read the header from. + /// The . + /// The options are null. + /// The path is null. + /// The file stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static IImageFormat DetectFormat(DecoderOptions options, string path) + { + Guard.NotNull(options, nameof(options)); + + using Stream file = options.Configuration.FileSystem.OpenRead(path); + return DetectFormat(options, file); + } + + /// + /// Detects the encoded image format type from the specified file. + /// + /// The image file to open and to read the header from. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + public static Task DetectFormatAsync( + string path, + CancellationToken cancellationToken = default) + => DetectFormatAsync(DecoderOptions.Default, path, cancellationToken); + + /// + /// Detects the encoded image format type from the specified file. + /// + /// The general decoder options. + /// The image file to open and to read the header from. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// The options are null. + /// The path is null. + /// The file stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static async Task DetectFormatAsync( + DecoderOptions options, + string path, + CancellationToken cancellationToken = default) + { + Guard.NotNull(options, nameof(options)); + + await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path); + return await DetectFormatAsync(options, stream, cancellationToken).ConfigureAwait(false); + } + + /// + /// Reads the raw image information from the specified file path without fully decoding it. + /// A return value indicates whether the operation succeeded. + /// + /// The image file to open and to read the header from. + /// The . + /// The path is null. + /// The file stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static ImageInfo Identify(string path) + => Identify(DecoderOptions.Default, path); + + /// + /// Reads the raw image information from the specified file path without fully decoding it. + /// + /// The general decoder options. + /// The image file to open and to read the header from. + /// The . + /// The options are null. + /// The path is null. + /// The file stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static ImageInfo Identify(DecoderOptions options, string path) + { + Guard.NotNull(options, nameof(options)); + + using Stream stream = options.Configuration.FileSystem.OpenRead(path); + return Identify(options, stream); + } + + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The image file to open and to read the header from. + /// The token to monitor for cancellation requests. + /// The options are null. + /// + /// The representing the asynchronous operation. + /// + /// The path is null. + /// The file stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Task IdentifyAsync(string path, CancellationToken cancellationToken = default) + => IdentifyAsync(DecoderOptions.Default, path, cancellationToken); + + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The general decoder options. + /// The image file to open and to read the header from. + /// The token to monitor for cancellation requests. + /// + /// The representing the asynchronous operation. + /// + /// The options are null. + /// The path is null. + /// The file stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static async Task IdentifyAsync( + DecoderOptions options, + string path, + CancellationToken cancellationToken = default) + { + Guard.NotNull(options, nameof(options)); + await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path); + return await IdentifyAsync(options, stream, cancellationToken).ConfigureAwait(false); + } + + /// + /// Creates a new instance of the class from the given file path. + /// The pixel format is automatically determined by the decoder. + /// + /// The file path to the image. + /// . + /// The path is null. + /// The file stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Image Load(string path) + => Load(DecoderOptions.Default, path); + + /// + /// Creates a new instance of the class from the given file path. + /// The pixel format is automatically determined by the decoder. + /// + /// The general decoder options. + /// The file path to the image. + /// . + /// The options are null. + /// The path is null. + /// The file stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Image Load(DecoderOptions options, string path) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(path, nameof(path)); + + using Stream stream = options.Configuration.FileSystem.OpenRead(path); + return Load(options, stream); + } + + /// + /// Creates a new instance of the class from the given file path. + /// The pixel format is automatically determined by the decoder. + /// + /// The file path to the image. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// The path is null. + /// The file stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Task LoadAsync(string path, CancellationToken cancellationToken = default) + => LoadAsync(DecoderOptions.Default, path, cancellationToken); + + /// + /// Creates a new instance of the class from the given file path. + /// The pixel format is automatically determined by the decoder. + /// + /// The general decoder options. + /// The file path to the image. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// The options are null. + /// The path is null. + /// The file stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static async Task LoadAsync( + DecoderOptions options, + string path, + CancellationToken cancellationToken = default) + { + await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path); + return await LoadAsync(options, stream, cancellationToken).ConfigureAwait(false); + } + + /// + /// Creates a new instance of the class from the given file path. + /// + /// The pixel format. + /// The file path to the image. + /// . + /// The path is null. + /// The file stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Image Load(string path) + where TPixel : unmanaged, IPixel + => Load(DecoderOptions.Default, path); + + /// + /// Creates a new instance of the class from the given file path. + /// + /// The pixel format. + /// The general decoder options. + /// The file path to the image. + /// . + /// The options are null. + /// The path is null. + /// The file stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Image Load(DecoderOptions options, string path) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(path, nameof(path)); + + using Stream stream = options.Configuration.FileSystem.OpenRead(path); + return Load(options, stream); + } + + /// + /// Creates a new instance of the class from the given file path. + /// + /// The pixel format. + /// The file path to the image. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// The path is null. + /// The file stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Task> LoadAsync(string path, CancellationToken cancellationToken = default) + where TPixel : unmanaged, IPixel + => LoadAsync(DecoderOptions.Default, path, cancellationToken); + + /// + /// Creates a new instance of the class from the given file path. + /// + /// The pixel format. + /// The general decoder options. + /// The file path to the image. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// The options are null. + /// The path is null. + /// The file stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static async Task> LoadAsync( + DecoderOptions options, + string path, + CancellationToken cancellationToken = default) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(path, nameof(path)); + + await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path); + return await LoadAsync(options, stream, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/ImageSharp/Image.FromStream.cs b/ImageSharp/Image.FromStream.cs new file mode 100644 index 0000000..29710fe --- /dev/null +++ b/ImageSharp/Image.FromStream.cs @@ -0,0 +1,356 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SixLabors.ImageSharp { + /// + /// Adds static methods allowing the creation of new image from a given stream. + /// + public abstract partial class Image + { + /// + /// Detects the encoded image format type from the specified stream. + /// + /// The image stream to read the header from. + /// The . + /// The stream is null. + /// The stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static IImageFormat DetectFormat(Stream stream) + => DetectFormat(DecoderOptions.Default, stream); + + /// + /// Detects the encoded image format type from the specified stream. + /// + /// The general decoder options. + /// The image stream to read the header from. + /// The . + /// The options are null. + /// The stream is null. + /// The stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static IImageFormat DetectFormat(DecoderOptions options, Stream stream) + => WithSeekableStream(options, stream, s => InternalDetectFormat(options.Configuration, s)); + + /// + /// Detects the encoded image format type from the specified stream. + /// + /// The image stream to read the header from. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// The stream is null. + /// The stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Task DetectFormatAsync( + Stream stream, + CancellationToken cancellationToken = default) + => DetectFormatAsync(DecoderOptions.Default, stream, cancellationToken); + + /// + /// Detects the encoded image format type from the specified stream. + /// + /// The general decoder options. + /// The image stream to read the header from. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// The options are null. + /// The stream is null. + /// The stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Task DetectFormatAsync( + DecoderOptions options, + Stream stream, + CancellationToken cancellationToken = default) + => WithSeekableStreamAsync( + options, + stream, + async (s, ct) => await InternalDetectFormatAsync(options.Configuration, s, ct).ConfigureAwait(false), + cancellationToken); + + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The image stream to read the header from. + /// The . + /// The stream is null. + /// The stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static ImageInfo Identify(Stream stream) + => Identify(DecoderOptions.Default, stream); + + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The general decoder options. + /// The image stream to read the information from. + /// The . + /// The options are null. + /// The stream is null. + /// The stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static ImageInfo Identify(DecoderOptions options, Stream stream) + => WithSeekableStream(options, stream, s => InternalIdentify(options, s)); + + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The image stream to read the information from. + /// The token to monitor for cancellation requests. + /// + /// The representing the asynchronous operation. + /// + /// The stream is null. + /// The stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Task IdentifyAsync( + Stream stream, + CancellationToken cancellationToken = default) + => IdentifyAsync(DecoderOptions.Default, stream, cancellationToken); + + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The general decoder options. + /// The image stream to read the information from. + /// The token to monitor for cancellation requests. + /// + /// The representing the asynchronous operation. + /// + /// The options are null. + /// The stream is null. + /// The stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Task IdentifyAsync( + DecoderOptions options, + Stream stream, + CancellationToken cancellationToken = default) + => WithSeekableStreamAsync( + options, + stream, + (s, ct) => InternalIdentifyAsync(options, s, ct), + cancellationToken); + + /// + /// Creates a new instance of the class from the given stream. + /// The pixel format is automatically determined by the decoder. + /// + /// The stream containing image information. + /// . + /// The stream is null. + /// The stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Image Load(Stream stream) + => Load(DecoderOptions.Default, stream); + + /// + /// Creates a new instance of the class from the given stream. + /// The pixel format is automatically determined by the decoder. + /// + /// The general decoder options. + /// The stream containing image information. + /// . + /// The options are null. + /// The stream is null. + /// The stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Image Load(DecoderOptions options, Stream stream) + => WithSeekableStream(options, stream, s => Decode(options, s)); + + /// + /// Creates a new instance of the class from the given stream. + /// The pixel format is automatically determined by the decoder. + /// + /// The stream containing image information. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// The stream is null. + /// The stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Task LoadAsync(Stream stream, CancellationToken cancellationToken = default) + => LoadAsync(DecoderOptions.Default, stream, cancellationToken); + + /// + /// Creates a new instance of the class from the given stream. + /// The pixel format is automatically determined by the decoder. + /// + /// The general decoder options. + /// The stream containing image information. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// The options are null. + /// The stream is null. + /// The stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Task LoadAsync( + DecoderOptions options, + Stream stream, + CancellationToken cancellationToken = default) + => WithSeekableStreamAsync(options, stream, (s, ct) => DecodeAsync(options, s, ct), cancellationToken); + + /// + /// Creates a new instance of the class from the given stream. + /// + /// The pixel format. + /// The stream containing image information. + /// . + /// The stream is null. + /// The stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Image Load(Stream stream) + where TPixel : unmanaged, IPixel + => Load(DecoderOptions.Default, stream); + + /// + /// Creates a new instance of the class from the given stream. + /// + /// The pixel format. + /// The general decoder options. + /// The stream containing image information. + /// . + /// The options are null. + /// The stream is null. + /// The stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Image Load(DecoderOptions options, Stream stream) + where TPixel : unmanaged, IPixel + => WithSeekableStream(options, stream, s => Decode(options, s)); + + /// + /// Creates a new instance of the class from the given stream. + /// + /// The pixel format. + /// The stream containing image information. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// The stream is null. + /// The stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Task> LoadAsync(Stream stream, CancellationToken cancellationToken = default) + where TPixel : unmanaged, IPixel + => LoadAsync(DecoderOptions.Default, stream, cancellationToken); + + /// + /// Creates a new instance of the class from the given stream. + /// + /// The pixel format. + /// The general decoder options. + /// The stream containing image information. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// The options are null. + /// The stream is null. + /// The stream is not readable or the image format is not supported. + /// The encoded image contains invalid content. + /// The encoded image format is unknown. + public static Task> LoadAsync( + DecoderOptions options, + Stream stream, + CancellationToken cancellationToken = default) + where TPixel : unmanaged, IPixel + => WithSeekableStreamAsync(options, stream, (s, ct) => DecodeAsync(options, s, ct), cancellationToken); + + /// + /// Performs the given action against the stream ensuring that it is seekable. + /// + /// The type of object returned from the action. + /// The general decoder options. + /// The input stream. + /// The action to perform. + /// The . + /// Cannot read from the stream. + internal static T WithSeekableStream( + DecoderOptions options, + Stream stream, + Func action) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + if (!stream.CanRead) + { + throw new NotSupportedException("Cannot read from the stream."); + } + + Configuration configuration = options.Configuration; + if (stream.CanSeek) + { + if (configuration.ReadOrigin == ReadOrigin.Begin) + { + stream.Position = 0; + } + + return action(stream); + } + + using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize); + memoryStream.Position = 0; + + return action(memoryStream); + } + + /// + /// Performs the given action asynchronously against the stream ensuring that it is seekable. + /// + /// The type of object returned from the action. + /// The general decoder options. + /// The input stream. + /// The action to perform. + /// The cancellation token. + /// The . + /// Cannot read from the stream. + internal static async Task WithSeekableStreamAsync( + DecoderOptions options, + Stream stream, + Func> action, + CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + if (!stream.CanRead) + { + throw new NotSupportedException("Cannot read from the stream."); + } + + Configuration configuration = options.Configuration; + if (stream.CanSeek) + { + if (configuration.ReadOrigin == ReadOrigin.Begin) + { + stream.Position = 0; + } + + return await action(stream, cancellationToken).ConfigureAwait(false); + } + + using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); + memoryStream.Position = 0; + + return await action(memoryStream, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/ImageSharp/Image.LoadPixelData.cs b/ImageSharp/Image.LoadPixelData.cs new file mode 100644 index 0000000..f90176e --- /dev/null +++ b/ImageSharp/Image.LoadPixelData.cs @@ -0,0 +1,185 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp { + /// + /// Adds static methods allowing the creation of new image from raw pixel data. + /// + public abstract partial class Image + { + /// + /// Create a new instance of the class from the raw data. + /// + /// The readonly span of bytes containing image data. + /// The width of the final image. + /// The height of the final image. + /// The pixel format. + /// The data length is incorrect. + /// A new . + public static Image LoadPixelData(ReadOnlySpan data, int width, int height) + where TPixel : unmanaged, IPixel + => LoadPixelData(Configuration.Default, data, width, height); + + /// + /// Create a new instance of the class from raw data + /// using pixels between source row starts. + /// + /// The readonly span containing image data. + /// The width of the final image. + /// The height of the final image. + /// The number of pixels between row starts in . + /// The pixel format. + /// + /// or is not positive, + /// or is less than . + /// + /// + /// is smaller than ((height - 1) * rowStride) + width. + /// + /// A new . + public static Image LoadPixelData(ReadOnlySpan data, int width, int height, int rowStride) + where TPixel : unmanaged, IPixel + => LoadPixelData(Configuration.Default, data, width, height, rowStride); + + /// + /// Create a new instance of the class from the given readonly span of bytes in format. + /// + /// The readonly span of bytes containing image data. + /// The width of the final image. + /// The height of the final image. + /// The pixel format. + /// The data length is incorrect. + /// A new . + public static Image LoadPixelData(ReadOnlySpan data, int width, int height) + where TPixel : unmanaged, IPixel + => LoadPixelData(Configuration.Default, data, width, height); + + /// + /// Create a new instance of the class from a readonly span of bytes in + /// format using bytes between source row starts. + /// + /// The readonly span containing image data. + /// The width of the final image. + /// The height of the final image. + /// The number of bytes between row starts in . + /// The pixel format. + /// + /// or is not positive, + /// or resolves to fewer than pixels. + /// + /// + /// is not divisible by the pixel size, + /// or is smaller than the required strided image length. + /// + /// A new . + public static Image LoadPixelData(ReadOnlySpan data, int width, int height, int rowStrideInBytes) + where TPixel : unmanaged, IPixel + => LoadPixelData(Configuration.Default, data, width, height, rowStrideInBytes); + + /// + /// Create a new instance of the class from the given readonly span of bytes in format. + /// + /// The configuration for the decoder. + /// The readonly span of bytes containing image data. + /// The width of the final image. + /// The height of the final image. + /// The pixel format. + /// The configuration is null. + /// The data length is incorrect. + /// A new . + public static Image LoadPixelData(Configuration configuration, ReadOnlySpan data, int width, int height) + where TPixel : unmanaged, IPixel + => LoadPixelData(configuration, MemoryMarshal.Cast(data), width, height); + + /// + /// Create a new instance of the class from a readonly span of bytes in + /// format using bytes between source row starts. + /// + /// The configuration for the decoder. + /// The readonly span containing image data. + /// The width of the final image. + /// The height of the final image. + /// The number of bytes between row starts in . + /// The pixel format. + /// The configuration is null. + /// + /// or is not positive, + /// or resolves to fewer than pixels. + /// + /// + /// is not divisible by the pixel size, + /// or is smaller than the required strided image length. + /// + /// A new . + public static Image LoadPixelData( + Configuration configuration, + ReadOnlySpan data, + int width, + int height, + int rowStrideInBytes) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + + int rowStride = GetPixelRowStrideFromByteStride(width, rowStrideInBytes, nameof(rowStrideInBytes)); + return LoadPixelData(configuration, MemoryMarshal.Cast(data), width, height, rowStride); + } + + /// + /// Create a new instance of the class from the raw data. + /// + /// The configuration for the decoder. + /// The readonly span containing the image pixel data. + /// The width of the final image. + /// The height of the final image. + /// The configuration is null. + /// The data length is incorrect. + /// The pixel format. + /// A new . + public static Image LoadPixelData(Configuration configuration, ReadOnlySpan data, int width, int height) + where TPixel : unmanaged, IPixel + => LoadPixelData(configuration, data, width, height, width); + + /// + /// Create a new instance of the class from raw data + /// using pixels between source row starts. + /// + /// The configuration for the decoder. + /// The readonly span containing the image pixel data. + /// The width of the final image. + /// The height of the final image. + /// The number of pixels between row starts in . + /// The configuration is null. + /// + /// or is not positive, + /// or is less than . + /// + /// + /// is smaller than ((height - 1) * rowStride) + width. + /// + /// The pixel format. + /// A new . + public static Image LoadPixelData( + Configuration configuration, + ReadOnlySpan data, + int width, + int height, + int rowStride) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + ValidateWrapMemoryStride(width, height, rowStride, nameof(rowStride)); + long requiredLength = GetRequiredLength(width, height, rowStride); + Guard.MustBeGreaterThanOrEqualTo(data.Length, requiredLength, nameof(data)); + + Image image = new(configuration, width, height); + image.Frames.RootFrame.PixelBuffer.CopyFrom(data, rowStride); + + return image; + } + } +} diff --git a/ImageSharp/Image.WrapMemory.cs b/ImageSharp/Image.WrapMemory.cs new file mode 100644 index 0000000..1b0260d --- /dev/null +++ b/ImageSharp/Image.WrapMemory.cs @@ -0,0 +1,694 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp { + /// + /// Adds static methods allowing wrapping an existing memory area as an image. + /// + public abstract partial class Image + { + /// + /// + /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as + /// an instance. + /// + /// + /// Please note: using this method does not transfer the ownership of the underlying buffer of the input + /// to the new instance. This means that consumers of this method must ensure that the input buffer + /// is either self-contained, (for example, a instance wrapping a new array that was + /// created), or that the owning object is not disposed until the returned is disposed. + /// + /// + /// If the input instance is one retrieved from an instance + /// rented from a memory pool (such as ), and that owning instance is disposed while the image is still + /// in use, this will lead to undefined behavior and possibly runtime crashes (as the same buffer might then be modified by other + /// consumers while the returned image is still working on it). Make sure to control the lifetime of the input buffers appropriately. + /// + /// + /// The pixel type + /// The + /// The pixel memory. + /// The width of the memory image. + /// The height of the memory image. + /// The . + /// The configuration is null. + /// The metadata is null. + /// An instance + public static Image WrapMemory( + Configuration configuration, + Memory pixelMemory, + int width, + int height, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + Guard.IsTrue(pixelMemory.Length >= (long)width * height, nameof(pixelMemory), "The length of the input memory is less than the specified image size"); + + MemoryGroup memorySource = MemoryGroup.Wrap(pixelMemory); + return new Image(configuration, memorySource, width, height, metadata); + } + + /// + /// + /// Wraps an existing memory area allowing viewing/manipulation as an + /// with pixels between row starts. + /// + /// + /// Please note: using this method does not transfer the ownership of the underlying buffer of the input + /// to the new instance. Consumers must ensure that + /// the input buffer remains valid for the full lifetime of the returned image. + /// + /// + /// The pixel type. + /// The . + /// The source pixel memory. + /// The width of the memory image in pixels. + /// The height of the memory image in pixels. + /// The number of pixels between row starts in . + /// The . + /// The configuration is null. + /// The metadata is null. + /// + /// or is not positive, + /// or is less than . + /// + /// + /// The length of is less than + /// ((height - 1) * rowStride) + width. + /// + /// An instance. + public static Image WrapMemory( + Configuration configuration, + Memory pixelMemory, + int width, + int height, + int rowStride, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + + ValidateWrapMemoryStride(width, height, rowStride, nameof(rowStride)); + + long requiredLength = GetRequiredLength(width, height, rowStride); + Guard.IsTrue(pixelMemory.Length >= requiredLength, nameof(pixelMemory), "The length of the input memory is less than the specified image size"); + + MemoryGroup memorySource = MemoryGroup.Wrap(pixelMemory); + return new Image(configuration, memorySource, width, height, rowStride, metadata); + } + + /// + public static Image WrapMemory( + Configuration configuration, + Memory pixelMemory, + int width, + int height) + where TPixel : unmanaged, IPixel + => WrapMemory(configuration, pixelMemory, width, height, new ImageMetadata()); + + /// + public static Image WrapMemory( + Configuration configuration, + Memory pixelMemory, + int width, + int height, + int rowStride) + where TPixel : unmanaged, IPixel + => WrapMemory(configuration, pixelMemory, width, height, rowStride, new ImageMetadata()); + + /// + public static Image WrapMemory( + Memory pixelMemory, + int width, + int height) + where TPixel : unmanaged, IPixel + => WrapMemory(Configuration.Default, pixelMemory, width, height); + + /// + public static Image WrapMemory( + Memory pixelMemory, + int width, + int height, + int rowStride) + where TPixel : unmanaged, IPixel + => WrapMemory(Configuration.Default, pixelMemory, width, height, rowStride); + + /// + /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels, + /// allowing to view/manipulate it as an instance. + /// The ownership of the is being transferred to the new instance, + /// meaning that the caller is not allowed to dispose . + /// It will be disposed together with the result image. + /// + /// The pixel type + /// The + /// The that is being transferred to the image + /// The width of the memory image. + /// The height of the memory image. + /// The + /// The configuration is null. + /// The metadata is null. + /// An instance + public static Image WrapMemory( + Configuration configuration, + IMemoryOwner pixelMemoryOwner, + int width, + int height, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + Guard.IsTrue(pixelMemoryOwner.Memory.Length >= (long)width * height, nameof(pixelMemoryOwner), "The length of the input memory is less than the specified image size"); + + MemoryGroup memorySource = MemoryGroup.Wrap(pixelMemoryOwner); + return new Image(configuration, memorySource, width, height, metadata); + } + + /// + /// + /// Wraps an existing memory owner allowing viewing/manipulation as an + /// with pixels between row starts. + /// + /// + /// Ownership of is transferred to the returned image. The caller + /// must not dispose the owner manually. + /// + /// + /// The pixel type. + /// The . + /// The pixel memory owner transferred to the image. + /// The width of the memory image in pixels. + /// The height of the memory image in pixels. + /// The number of pixels between row starts in the source memory. + /// The . + /// The configuration is null. + /// The metadata is null. + /// + /// or is not positive, + /// or is less than . + /// + /// + /// The length of is less than + /// ((height - 1) * rowStride) + width. + /// + /// An instance. + public static Image WrapMemory( + Configuration configuration, + IMemoryOwner pixelMemoryOwner, + int width, + int height, + int rowStride, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + + ValidateWrapMemoryStride(width, height, rowStride, nameof(rowStride)); + + long requiredLength = GetRequiredLength(width, height, rowStride); + Guard.IsTrue(pixelMemoryOwner.Memory.Length >= requiredLength, nameof(pixelMemoryOwner), "The length of the input memory is less than the specified image size"); + + MemoryGroup memorySource = MemoryGroup.Wrap(pixelMemoryOwner); + return new Image(configuration, memorySource, width, height, rowStride, metadata); + } + + /// + public static Image WrapMemory( + Configuration configuration, + IMemoryOwner pixelMemoryOwner, + int width, + int height) + where TPixel : unmanaged, IPixel + => WrapMemory(configuration, pixelMemoryOwner, width, height, new ImageMetadata()); + + /// + public static Image WrapMemory( + Configuration configuration, + IMemoryOwner pixelMemoryOwner, + int width, + int height, + int rowStride) + where TPixel : unmanaged, IPixel + => WrapMemory(configuration, pixelMemoryOwner, width, height, rowStride, new ImageMetadata()); + + /// + public static Image WrapMemory( + IMemoryOwner pixelMemoryOwner, + int width, + int height) + where TPixel : unmanaged, IPixel + => WrapMemory(Configuration.Default, pixelMemoryOwner, width, height); + + /// + public static Image WrapMemory( + IMemoryOwner pixelMemoryOwner, + int width, + int height, + int rowStride) + where TPixel : unmanaged, IPixel + => WrapMemory(Configuration.Default, pixelMemoryOwner, width, height, rowStride); + + /// + /// + /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as + /// an instance. + /// + /// + /// Please note: using this method does not transfer the ownership of the underlying buffer of the input + /// to the new instance. This means that consumers of this method must ensure that the input buffer + /// is either self-contained, (for example, a instance wrapping a new array that was + /// created), or that the owning object is not disposed until the returned is disposed. + /// + /// + /// If the input instance is one retrieved from an instance + /// rented from a memory pool (such as ), and that owning instance is disposed while the image is still + /// in use, this will lead to undefined behavior and possibly runtime crashes (as the same buffer might then be modified by other + /// consumers while the returned image is still working on it). Make sure to control the lifetime of the input buffers appropriately. + /// + /// + /// The pixel type + /// The + /// The byte memory representing the pixel data. + /// The width of the memory image. + /// The height of the memory image. + /// The . + /// The configuration is null. + /// The metadata is null. + /// An instance + public static Image WrapMemory( + Configuration configuration, + Memory byteMemory, + int width, + int height, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + + ByteMemoryManager memoryManager = new(byteMemory); + + Guard.IsTrue(memoryManager.Memory.Length >= (long)width * height, nameof(byteMemory), "The length of the input memory is less than the specified image size"); + + MemoryGroup memorySource = MemoryGroup.Wrap(memoryManager.Memory); + return new Image(configuration, memorySource, width, height, metadata); + } + + /// + /// + /// Wraps an existing byte memory area allowing viewing/manipulation as an + /// with bytes between row starts. + /// + /// + /// Please note: using this method does not transfer the ownership of the underlying buffer of the input + /// to the new instance. Consumers must ensure that + /// the input buffer remains valid for the full lifetime of the returned image. + /// + /// + /// The pixel type. + /// The . + /// The source byte memory. + /// The width of the memory image in pixels. + /// The height of the memory image in pixels. + /// The number of bytes between row starts in . + /// The . + /// The configuration is null. + /// The metadata is null. + /// + /// or is not positive, + /// or resolves to less than pixels. + /// + /// + /// is not divisible by the size of , + /// or is smaller than the required strided image length. + /// + /// An instance. + public static Image WrapMemory( + Configuration configuration, + Memory byteMemory, + int width, + int height, + int rowStrideInBytes, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + + int rowStride = GetPixelRowStrideFromByteStride(width, rowStrideInBytes, nameof(rowStrideInBytes)); + long requiredLength = GetRequiredLength(width, height, rowStride); + + ByteMemoryManager memoryManager = new(byteMemory); + Guard.IsTrue(memoryManager.Memory.Length >= requiredLength, nameof(byteMemory), "The length of the input memory is less than the specified image size"); + + MemoryGroup memorySource = MemoryGroup.Wrap(memoryManager.Memory); + return new Image(configuration, memorySource, width, height, rowStride, metadata); + } + + /// + public static Image WrapMemory( + Configuration configuration, + Memory byteMemory, + int width, + int height) + where TPixel : unmanaged, IPixel + => WrapMemory(configuration, byteMemory, width, height, new ImageMetadata()); + + /// + public static Image WrapMemory( + Configuration configuration, + Memory byteMemory, + int width, + int height, + int rowStrideInBytes) + where TPixel : unmanaged, IPixel + => WrapMemory(configuration, byteMemory, width, height, rowStrideInBytes, new ImageMetadata()); + + /// + public static Image WrapMemory( + Memory byteMemory, + int width, + int height) + where TPixel : unmanaged, IPixel + => WrapMemory(Configuration.Default, byteMemory, width, height); + + /// + public static Image WrapMemory( + Memory byteMemory, + int width, + int height, + int rowStrideInBytes) + where TPixel : unmanaged, IPixel + => WrapMemory(Configuration.Default, byteMemory, width, height, rowStrideInBytes); + + /// + /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels, + /// allowing to view/manipulate it as an instance. + /// The ownership of the is being transferred to the new instance, + /// meaning that the caller is not allowed to dispose . + /// It will be disposed together with the result image. + /// + /// The pixel type + /// The + /// The that is being transferred to the image + /// The width of the memory image. + /// The height of the memory image. + /// The + /// The configuration is null. + /// The metadata is null. + /// An instance + public static Image WrapMemory( + Configuration configuration, + IMemoryOwner byteMemoryOwner, + int width, + int height, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + + ByteMemoryOwner pixelMemoryOwner = new(byteMemoryOwner); + + Guard.IsTrue(pixelMemoryOwner.Memory.Length >= (long)width * height, nameof(pixelMemoryOwner), "The length of the input memory is less than the specified image size"); + + MemoryGroup memorySource = MemoryGroup.Wrap(pixelMemoryOwner); + return new Image(configuration, memorySource, width, height, metadata); + } + + /// + /// + /// Wraps an existing byte memory owner allowing viewing/manipulation as an + /// with bytes between row starts. + /// + /// + /// Ownership of is transferred to the returned image. The caller + /// must not dispose the owner manually. + /// + /// + /// The pixel type. + /// The . + /// The byte memory owner transferred to the image. + /// The width of the memory image in pixels. + /// The height of the memory image in pixels. + /// The number of bytes between row starts in the source memory. + /// The . + /// The configuration is null. + /// The metadata is null. + /// + /// or is not positive, + /// or resolves to less than pixels. + /// + /// + /// is not divisible by the size of , + /// or is smaller than the required strided image length. + /// + /// An instance. + public static Image WrapMemory( + Configuration configuration, + IMemoryOwner byteMemoryOwner, + int width, + int height, + int rowStrideInBytes, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + + int rowStride = GetPixelRowStrideFromByteStride(width, rowStrideInBytes, nameof(rowStrideInBytes)); + + ByteMemoryOwner pixelMemoryOwner = new(byteMemoryOwner); + long requiredLength = GetRequiredLength(width, height, rowStride); + Guard.IsTrue(pixelMemoryOwner.Memory.Length >= requiredLength, nameof(byteMemoryOwner), "The length of the input memory is less than the specified image size"); + + MemoryGroup memorySource = MemoryGroup.Wrap(pixelMemoryOwner); + return new Image(configuration, memorySource, width, height, rowStride, metadata); + } + + /// + public static Image WrapMemory( + Configuration configuration, + IMemoryOwner byteMemoryOwner, + int width, + int height) + where TPixel : unmanaged, IPixel + => WrapMemory(configuration, byteMemoryOwner, width, height, new ImageMetadata()); + + /// + public static Image WrapMemory( + Configuration configuration, + IMemoryOwner byteMemoryOwner, + int width, + int height, + int rowStrideInBytes) + where TPixel : unmanaged, IPixel + => WrapMemory(configuration, byteMemoryOwner, width, height, rowStrideInBytes, new ImageMetadata()); + + /// + public static Image WrapMemory( + IMemoryOwner byteMemoryOwner, + int width, + int height) + where TPixel : unmanaged, IPixel + => WrapMemory(Configuration.Default, byteMemoryOwner, width, height); + + /// + public static Image WrapMemory( + IMemoryOwner byteMemoryOwner, + int width, + int height, + int rowStrideInBytes) + where TPixel : unmanaged, IPixel + => WrapMemory(Configuration.Default, byteMemoryOwner, width, height, rowStrideInBytes); + + /// + /// + /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as + /// an instance. + /// + /// + /// Please note: this method relies on callers to carefully manage the target memory area being referenced by the + /// pointer and that the lifetime of such a memory area is at least equal to that of the returned + /// instance. For example, if the input pointer references an unmanaged memory area, + /// callers must ensure that the memory area is not freed as long as the returned is + /// in use and not disposed. The same applies if the input memory area points to a pinned managed object, as callers + /// must ensure that objects will remain pinned as long as the instance is in use. + /// Failing to do so constitutes undefined behavior and will likely lead to memory corruption and runtime crashes. + /// + /// + /// Note also that if you have a or an array (which can be cast to ) of + /// either or values, it is highly recommended to use one of the other + /// available overloads of this method instead (such as + /// or , to make the resulting code less error + /// prone and avoid having to pin the underlying memory buffer in use. This method is primarily meant to be used when + /// doing interop or working with buffers that are located in unmanaged memory. + /// + /// + /// The pixel type + /// The + /// The pointer to the target memory buffer to wrap. + /// The byte length of the memory allocated. + /// The width of the memory image. + /// The height of the memory image. + /// The . + /// The configuration is null. + /// The metadata is null. + /// An instance + public static unsafe Image WrapMemory( + Configuration configuration, + void* pointer, + int bufferSizeInBytes, + int width, + int height, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Guard.IsFalse(pointer == null, nameof(pointer), "Pointer must be not null"); + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + Guard.MustBeLessThanOrEqualTo(height * (long)width, int.MaxValue, "Total amount of pixels exceeds int.MaxValue"); + + UnmanagedMemoryManager memoryManager = new(pointer, width * height); + + Guard.MustBeGreaterThanOrEqualTo(bufferSizeInBytes / sizeof(TPixel), memoryManager.Memory.Span.Length, nameof(bufferSizeInBytes)); + + MemoryGroup memorySource = MemoryGroup.Wrap(memoryManager.Memory); + return new Image(configuration, memorySource, width, height, metadata); + } + + /// + /// + /// Wraps an unmanaged memory area allowing viewing/manipulation as an + /// with bytes between row starts. + /// + /// + /// Callers must ensure the memory referenced by remains valid for the full + /// lifetime of the returned image. + /// + /// + /// The pixel type. + /// The . + /// The pointer to the source memory. + /// The byte length of the source memory. + /// The width of the memory image in pixels. + /// The height of the memory image in pixels. + /// The number of bytes between row starts in the source memory. + /// The . + /// The configuration is null. + /// The metadata is null. + /// + /// is null, + /// is not divisible by the size of , + /// or is smaller than the required strided image length. + /// + /// + /// or is not positive, + /// or resolves to less than pixels. + /// + /// An instance. + public static unsafe Image WrapMemory( + Configuration configuration, + void* pointer, + int bufferSizeInBytes, + int width, + int height, + int rowStrideInBytes, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Guard.IsFalse(pointer == null, nameof(pointer), "Pointer must not be null"); + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + + int rowStride = GetPixelRowStrideFromByteStride(width, rowStrideInBytes, nameof(rowStrideInBytes)); + long requiredLength = GetRequiredLength(width, height, rowStride); + + Guard.MustBeLessThanOrEqualTo(requiredLength, int.MaxValue, nameof(requiredLength)); + Guard.MustBeGreaterThanOrEqualTo(bufferSizeInBytes / Unsafe.SizeOf(), requiredLength, nameof(bufferSizeInBytes)); + + UnmanagedMemoryManager memoryManager = new(pointer, (int)requiredLength); + MemoryGroup memorySource = MemoryGroup.Wrap(memoryManager.Memory); + return new Image(configuration, memorySource, width, height, rowStride, metadata); + } + + /// + public static unsafe Image WrapMemory( + Configuration configuration, + void* pointer, + int bufferSizeInBytes, + int width, + int height) + where TPixel : unmanaged, IPixel + => WrapMemory(configuration, pointer, bufferSizeInBytes, width, height, new ImageMetadata()); + + /// + public static unsafe Image WrapMemory( + Configuration configuration, + void* pointer, + int bufferSizeInBytes, + int width, + int height, + int rowStrideInBytes) + where TPixel : unmanaged, IPixel + => WrapMemory(configuration, pointer, bufferSizeInBytes, width, height, rowStrideInBytes, new ImageMetadata()); + + /// + public static unsafe Image WrapMemory( + void* pointer, + int bufferSizeInBytes, + int width, + int height) + where TPixel : unmanaged, IPixel + => WrapMemory(Configuration.Default, pointer, bufferSizeInBytes, width, height); + + /// + public static unsafe Image WrapMemory( + void* pointer, + int bufferSizeInBytes, + int width, + int height, + int rowStrideInBytes) + where TPixel : unmanaged, IPixel + => WrapMemory(Configuration.Default, pointer, bufferSizeInBytes, width, height, rowStrideInBytes); + + private static void ValidateWrapMemoryStride(int width, int height, int rowStride, string rowStrideParamName) + { + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + Guard.MustBeGreaterThanOrEqualTo(rowStride, width, rowStrideParamName); + } + + private static int GetPixelRowStrideFromByteStride(int width, int rowStrideInBytes, string rowStrideParamName) + where TPixel : unmanaged, IPixel + { + int pixelSizeInBytes = Unsafe.SizeOf(); + + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(rowStrideInBytes, 0, rowStrideParamName); + Guard.IsTrue( + rowStrideInBytes % pixelSizeInBytes == 0, + rowStrideParamName, + "The row stride in bytes must be divisible by the pixel size."); + + int rowStride = rowStrideInBytes / pixelSizeInBytes; + Guard.MustBeGreaterThanOrEqualTo(rowStride, width, rowStrideParamName); + return rowStride; + } + + private static long GetRequiredLength(int width, int height, int rowStride) + => checked(((long)(height - 1) * rowStride) + width); + } +} diff --git a/ImageSharp/Image.cs b/ImageSharp/Image.cs new file mode 100644 index 0000000..11742b8 --- /dev/null +++ b/ImageSharp/Image.cs @@ -0,0 +1,253 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp { + /// + /// Encapsulates an image, which consists of the pixel data for a graphics image and its attributes. + /// For the non-generic type, the pixel type is only known at runtime. + /// is always implemented by a pixel-specific instance. + /// + public abstract partial class Image : IDisposable, IConfigurationProvider + { + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The global configuration.. + /// The pixel type information. + /// The image metadata. + /// The size in px units. + protected Image(Configuration configuration, PixelTypeInfo pixelType, ImageMetadata metadata, Size size) + { + this.Configuration = configuration; + this.PixelType = pixelType; + this.Size = size; + this.Metadata = metadata; + } + + /// + /// Initializes a new instance of the class. + /// + /// The global configuration. + /// The . + /// The . + /// The width in px units. + /// The height in px units. + internal Image( + Configuration configuration, + PixelTypeInfo pixelType, + ImageMetadata metadata, + int width, + int height) + : this(configuration, pixelType, metadata, new Size(width, height)) + { + } + + /// + public Configuration Configuration { get; } + + /// + /// Gets information about the image pixels. + /// + public PixelTypeInfo PixelType { get; } + + /// + /// Gets the image width in px units. + /// + public int Width => this.Size.Width; + + /// + /// Gets the image height in px units. + /// + public int Height => this.Size.Height; + + /// + /// Gets any metadata associated with the image. + /// + public ImageMetadata Metadata { get; private set; } + + /// + /// Gets the size of the image in px units. + /// + public Size Size { get; private set; } + + /// + /// Gets the bounds of the image. + /// + public Rectangle Bounds => new(0, 0, this.Width, this.Height); + + /// + /// Gets the implementing the public property. + /// + protected abstract ImageFrameCollection NonGenericFrameCollection { get; } + + /// + /// Gets the frames of the image as (non-generic) . + /// + public ImageFrameCollection Frames => this.NonGenericFrameCollection; + + /// + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.Dispose(true); + GC.SuppressFinalize(this); + + this.isDisposed = true; + } + + /// + /// Saves the image to the given stream using the given image encoder. + /// + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream or encoder is null. + public void Save(Stream stream, IImageEncoder encoder) + { + Guard.NotNull(stream, nameof(stream)); + Guard.NotNull(encoder, nameof(encoder)); + this.EnsureNotDisposed(); + + this.AcceptVisitor(new EncodeVisitor(encoder, stream)); + } + + /// + /// Saves the image to the given stream using the given image encoder. + /// + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream or encoder is null. + /// A representing the asynchronous operation. + public Task SaveAsync(Stream stream, IImageEncoder encoder, CancellationToken cancellationToken = default) + { + Guard.NotNull(stream, nameof(stream)); + Guard.NotNull(encoder, nameof(encoder)); + this.EnsureNotDisposed(); + + return this.AcceptVisitorAsync(new EncodeVisitor(encoder, stream), cancellationToken); + } + + /// + /// Returns a copy of the image in the given pixel format. + /// + /// The pixel format. + /// The + public Image CloneAs() + where TPixel2 : unmanaged, IPixel => this.CloneAs(this.Configuration); + + /// + /// Returns a copy of the image in the given pixel format. + /// + /// The pixel format. + /// The configuration providing initialization code which allows extending the library. + /// The . + public abstract Image CloneAs(Configuration configuration) + where TPixel2 : unmanaged, IPixel; + + /// + /// Synchronizes any embedded metadata profiles with the current image properties. + /// + public void SynchronizeMetadata() + { + this.Metadata.SynchronizeProfiles(); + foreach (ImageFrame frame in this.Frames) + { + frame.Metadata.SynchronizeProfiles(); + } + } + + /// + /// Synchronizes any embedded metadata profiles with the current image properties. + /// + /// A synchronization action to run in addition to the default process. + public void SynchronizeMetadata(Action action) + { + this.SynchronizeMetadata(); + action(this); + } + + /// + /// Update the size of the image after mutation. + /// + /// The . + protected void UpdateSize(Size size) => this.Size = size; + + /// + /// Updates the metadata of the image after mutation. + /// + /// The . + protected void UpdateMetadata(ImageMetadata metadata) => this.Metadata = metadata; + + /// + /// Disposes the object and frees resources for the Garbage Collector. + /// + /// Whether to dispose of managed and unmanaged objects. + protected abstract void Dispose(bool disposing); + + /// + /// Throws if the image is disposed. + /// + internal void EnsureNotDisposed() + { + if (this.isDisposed) + { + ThrowObjectDisposedException(this.GetType()); + } + } + + /// + /// Accepts a . + /// Implemented by invoking + /// with the pixel type of the image. + /// + /// The visitor. + internal abstract void Accept(IImageVisitor visitor); + + /// + /// Accepts a . + /// Implemented by invoking + /// with the pixel type of the image. + /// + /// The visitor. + /// The token to monitor for cancellation requests. + internal abstract Task AcceptAsync(IImageVisitorAsync visitor, CancellationToken cancellationToken); + + [MethodImpl(InliningOptions.ColdPath)] + private static void ThrowObjectDisposedException(Type type) => throw new ObjectDisposedException(type.Name); + + private class EncodeVisitor : IImageVisitor, IImageVisitorAsync + { + private readonly IImageEncoder encoder; + + private readonly Stream stream; + + public EncodeVisitor(IImageEncoder encoder, Stream stream) + { + this.encoder = encoder; + this.stream = stream; + } + + public void Visit(Image image) + where TPixel : unmanaged, IPixel => this.encoder.Encode(image, this.stream); + + public Task VisitAsync(Image image, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel => this.encoder.EncodeAsync(image, this.stream, cancellationToken); + } + } +} diff --git a/ImageSharp/ImageExtensions.Internal.cs b/ImageSharp/ImageExtensions.Internal.cs new file mode 100644 index 0000000..c577933 --- /dev/null +++ b/ImageSharp/ImageExtensions.Internal.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp { + /// + /// Contains internal extensions for + /// + public static partial class ImageExtensions + { + /// + /// Provides access to the image pixels. + /// + /// It is imperative that the accessor is correctly disposed of after use. + /// + /// + /// The type of the pixel. + /// The image. + /// + /// The + /// + internal static Buffer2D GetRootFramePixelBuffer(this Image image) + where TPixel : unmanaged, IPixel + => image.Frames.RootFrame.PixelBuffer; + } +} diff --git a/ImageSharp/ImageExtensions.cs b/ImageSharp/ImageExtensions.cs new file mode 100644 index 0000000..2a08e23 --- /dev/null +++ b/ImageSharp/ImageExtensions.cs @@ -0,0 +1,189 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats; + +namespace SixLabors.ImageSharp { + /// + /// Extension methods for the type. + /// + public static partial class ImageExtensions + { + /// + /// Writes the image to the given file path using an encoder detected from the path. + /// + /// The source image. + /// The file path to save the image to. + /// The path is null. + /// No encoder available for provided path. + public static void Save(this Image source, string path) + => source.Save(path, source.DetectEncoder(path)); + + /// + /// Writes the image to the given file path using an encoder detected from the path. + /// + /// The source image. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// The path is null. + /// No encoder available for provided path. + /// A representing the asynchronous operation. + public static Task SaveAsync(this Image source, string path, CancellationToken cancellationToken = default) + => source.SaveAsync(path, source.DetectEncoder(path), cancellationToken); + + /// + /// Writes the image to the given file path using the given image encoder. + /// + /// The source image. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The path is null. + /// The encoder is null. + public static void Save(this Image source, string path, IImageEncoder encoder) + { + Guard.NotNull(path, nameof(path)); + Guard.NotNull(encoder, nameof(encoder)); + using Stream fs = source.Configuration.FileSystem.Create(path); + source.Save(fs, encoder); + } + + /// + /// Writes the image to the given file path using the given image encoder. + /// + /// The source image. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// The path is null. + /// The encoder is null. + /// A representing the asynchronous operation. + public static async Task SaveAsync( + this Image source, + string path, + IImageEncoder encoder, + CancellationToken cancellationToken = default) + { + Guard.NotNull(path, nameof(path)); + Guard.NotNull(encoder, nameof(encoder)); + + await using Stream fs = source.Configuration.FileSystem.CreateAsynchronous(path); + await source.SaveAsync(fs, encoder, cancellationToken).ConfigureAwait(false); + } + + /// + /// Writes the image to the given stream using the given image format. + /// + /// The source image. + /// The stream to save the image to. + /// The format to save the image in. + /// The stream is null. + /// The format is null. + /// The stream is not writable. + /// No encoder available for provided format. + public static void Save(this Image source, Stream stream, IImageFormat format) + { + Guard.NotNull(stream, nameof(stream)); + Guard.NotNull(format, nameof(format)); + + if (!stream.CanWrite) + { + throw new NotSupportedException("Cannot write to the stream."); + } + + IImageEncoder encoder = source.Configuration.ImageFormatsManager.GetEncoder(format); + + if (encoder is null) + { + StringBuilder sb = new(); + sb.AppendLine("No encoder was found for the provided mime type. Registered encoders include:"); + + foreach (KeyValuePair val in source.Configuration.ImageFormatsManager.ImageEncoders) + { + sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine); + } + + throw new NotSupportedException(sb.ToString()); + } + + source.Save(stream, encoder); + } + + /// + /// Writes the image to the given stream using the given image format. + /// + /// The source image. + /// The stream to save the image to. + /// The format to save the image in. + /// The token to monitor for cancellation requests. + /// The stream is null. + /// The format is null. + /// The stream is not writable. + /// No encoder available for provided format. + /// A representing the asynchronous operation. + public static Task SaveAsync( + this Image source, + Stream stream, + IImageFormat format, + CancellationToken cancellationToken = default) + { + Guard.NotNull(stream, nameof(stream)); + Guard.NotNull(format, nameof(format)); + + if (!stream.CanWrite) + { + throw new NotSupportedException("Cannot write to the stream."); + } + + IImageEncoder encoder = source.Configuration.ImageFormatsManager.GetEncoder(format); + + if (encoder is null) + { + StringBuilder sb = new(); + sb.AppendLine("No encoder was found for the provided mime type. Registered encoders include:"); + + foreach (KeyValuePair val in source.Configuration.ImageFormatsManager.ImageEncoders) + { + sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine); + } + + throw new NotSupportedException(sb.ToString()); + } + + return source.SaveAsync(stream, encoder, cancellationToken); + } + + /// + /// Returns a Base64 encoded string from the given image. + /// The result is prepended with a Data URI + /// + /// + /// For example: + /// + /// + /// + /// + /// The source image + /// The format. + /// The format is null. + /// The + public static string ToBase64String(this Image source, IImageFormat format) + { + Guard.NotNull(format, nameof(format)); + + using MemoryStream stream = new(); + source.Save(stream, format); + + // Always available. + stream.TryGetBuffer(out ArraySegment buffer); + return $"data:{format.DefaultMimeType};base64,{Convert.ToBase64String(buffer.Array ?? [], 0, (int)stream.Length)}"; + } + } +} diff --git a/ImageSharp/ImageFrame.LoadPixelData.cs b/ImageSharp/ImageFrame.LoadPixelData.cs new file mode 100644 index 0000000..7b3e812 --- /dev/null +++ b/ImageSharp/ImageFrame.LoadPixelData.cs @@ -0,0 +1,104 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp { + /// + /// Contains methods for loading raw pixel data. + /// + public partial class ImageFrame + { + /// + /// Create a new instance of the class from the given byte array in format. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The byte array containing image data. + /// The width of the final image. + /// The height of the final image. + /// The pixel format. + /// A new . + internal static ImageFrame LoadPixelData(Configuration configuration, ReadOnlySpan data, int width, int height) + where TPixel : unmanaged, IPixel + => LoadPixelData(configuration, MemoryMarshal.Cast(data), width, height); + + /// + /// Create a new instance of the class from the given byte array in format. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The byte array containing image data. + /// The width of the final image. + /// The height of the final image. + /// The number of bytes between row starts in . + /// The pixel format. + /// A new . + internal static ImageFrame LoadPixelData( + Configuration configuration, + ReadOnlySpan data, + int width, + int height, + int rowStrideInBytes) + where TPixel : unmanaged, IPixel + { + int pixelSizeInBytes = Unsafe.SizeOf(); + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(rowStrideInBytes, 0, nameof(rowStrideInBytes)); + Guard.IsTrue( + rowStrideInBytes % pixelSizeInBytes == 0, + nameof(rowStrideInBytes), + "The row stride in bytes must be divisible by the pixel size."); + + int rowStride = rowStrideInBytes / pixelSizeInBytes; + return LoadPixelData(configuration, MemoryMarshal.Cast(data), width, height, rowStride); + } + + /// + /// Create a new instance of the class from the raw data. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The Span containing the image Pixel data. + /// The width of the final image. + /// The height of the final image. + /// The pixel format. + /// A new . + internal static ImageFrame LoadPixelData(Configuration configuration, ReadOnlySpan data, int width, int height) + where TPixel : unmanaged, IPixel + => LoadPixelData(configuration, data, width, height, width); + + /// + /// Create a new instance of the class from raw data + /// using pixels between source row starts. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The span containing the image pixel data. + /// The width of the final image. + /// The height of the final image. + /// The number of pixels between row starts in . + /// The pixel format. + /// A new . + internal static ImageFrame LoadPixelData( + Configuration configuration, + ReadOnlySpan data, + int width, + int height, + int rowStride) + where TPixel : unmanaged, IPixel + { + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + Guard.MustBeGreaterThanOrEqualTo(rowStride, width, nameof(rowStride)); + + long requiredLength = checked(((long)(height - 1) * rowStride) + width); + Guard.MustBeGreaterThanOrEqualTo(data.Length, requiredLength, nameof(data)); + + ImageFrame image = new(configuration, width, height); + image.PixelBuffer.CopyFrom(data, rowStride); + + return image; + } + } +} diff --git a/ImageSharp/ImageFrame.cs b/ImageSharp/ImageFrame.cs new file mode 100644 index 0000000..f72221b --- /dev/null +++ b/ImageSharp/ImageFrame.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp { + /// + /// Represents a pixel-agnostic image frame containing all pixel data and . + /// In case of animated formats like gif, it contains the single frame in a animation. + /// In all other cases it is the only frame of the image. + /// + public abstract partial class ImageFrame : IConfigurationProvider, IDisposable + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The frame width. + /// The frame height. + /// The . + protected ImageFrame(Configuration configuration, int width, int height, ImageFrameMetadata metadata) + { + this.Configuration = configuration; + this.Size = new Size(width, height); + this.Metadata = metadata; + } + + /// + /// Gets the frame width in px units. + /// + public int Width => this.Size.Width; + + /// + /// Gets the frame height in px units. + /// + public int Height => this.Size.Height; + + /// + /// Gets the metadata of the frame. + /// + public ImageFrameMetadata Metadata { get; private set; } + + /// + public Configuration Configuration { get; } + + /// + /// Gets the size of the frame. + /// + public Size Size { get; private set; } + + /// + /// Gets the bounds of the frame. + /// + /// The + public Rectangle Bounds => new(0, 0, this.Width, this.Height); + + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the object and frees resources for the Garbage Collector. + /// + /// Whether to dispose of managed and unmanaged objects. + protected abstract void Dispose(bool disposing); + + /// + /// Accepts a . + /// Implemented by invoking + /// with the pixel type of the image. + /// + /// The visitor. + internal abstract void Accept(IImageFrameVisitor visitor); + + /// + /// Copies the pixel data of the image frame to a of a specific pixel type. + /// + /// The pixel type of the destination buffer. + /// The buffer to copy the pixel data to. + internal abstract void CopyPixelsTo(Buffer2D destination) + where TDestinationPixel : unmanaged, IPixel; + + /// + /// Updates the size of the image frame after mutation. + /// + /// The . + protected void UpdateSize(Size size) => this.Size = size; + + /// + /// Updates the metadata of the image frame after mutation. + /// + /// The . + protected void UpdateMetadata(ImageFrameMetadata metadata) => this.Metadata = metadata; + } +} diff --git a/ImageSharp/ImageFrameCollection.cs b/ImageSharp/ImageFrameCollection.cs new file mode 100644 index 0000000..bef768b --- /dev/null +++ b/ImageSharp/ImageFrameCollection.cs @@ -0,0 +1,269 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp { + /// + /// Encapsulates a pixel-agnostic collection of instances + /// that make up an . + /// + public abstract class ImageFrameCollection : IDisposable, IEnumerable + { + private bool isDisposed; + + /// + /// Gets the number of frames. + /// + public abstract int Count { get; } + + /// + /// Gets the root frame. + /// + public ImageFrame RootFrame + { + get + { + this.EnsureNotDisposed(); + + return this.NonGenericRootFrame; + } + } + + /// + /// Gets the root frame. (Implements .) + /// + protected abstract ImageFrame NonGenericRootFrame { get; } + + /// + /// Gets the at the specified index. + /// + /// + /// The . + /// + /// The index. + /// The at the specified index. + public ImageFrame this[int index] + { + get + { + this.EnsureNotDisposed(); + + return this.NonGenericGetFrame(index); + } + } + + /// + /// Determines the index of a specific in the . + /// + /// The to locate in the . + /// The index of item if found in the list; otherwise, -1. + public abstract int IndexOf(ImageFrame frame); + + /// + /// Clones and inserts the into the at the specified . + /// + /// The zero-based index to insert the frame at. + /// The to clone and insert into the . + /// Frame must have the same dimensions as the image. + /// The cloned . + public ImageFrame InsertFrame(int index, ImageFrame source) + { + this.EnsureNotDisposed(); + + return this.NonGenericInsertFrame(index, source); + } + + /// + /// Clones the frame and appends the clone to the end of the collection. + /// + /// The raw pixel data to generate the from. + /// The cloned . + public ImageFrame AddFrame(ImageFrame source) + { + this.EnsureNotDisposed(); + + return this.NonGenericAddFrame(source); + } + + /// + /// Removes the frame at the specified index and frees all freeable resources associated with it. + /// + /// The zero-based index of the frame to remove. + /// Cannot remove last frame. + public abstract void RemoveFrame(int index); + + /// + /// Determines whether the contains the . + /// + /// The frame. + /// + /// true if the contains the specified frame; otherwise, false. + /// + public abstract bool Contains(ImageFrame frame); + + /// + /// Moves an from to . + /// + /// The zero-based index of the frame to move. + /// The index to move the frame to. + public abstract void MoveFrame(int sourceIndex, int destinationIndex); + + /// + /// Removes the frame at the specified index and creates a new image with only the removed frame + /// with the same metadata as the original image. + /// + /// The zero-based index of the frame to export. + /// Cannot remove last frame. + /// The new with the specified frame. + public Image ExportFrame(int index) + { + this.EnsureNotDisposed(); + + return this.NonGenericExportFrame(index); + } + + /// + /// Creates an with only the frame at the specified index + /// with the same metadata as the original image. + /// + /// The zero-based index of the frame to clone. + /// The new with the specified frame. + public Image CloneFrame(int index) + { + this.EnsureNotDisposed(); + + return this.NonGenericCloneFrame(index); + } + + /// + /// Creates a new and appends it to the end of the collection. + /// + /// + /// The new . + /// + public ImageFrame CreateFrame() + { + this.EnsureNotDisposed(); + + return this.NonGenericCreateFrame(); + } + + /// + /// Creates a new and appends it to the end of the collection. + /// + /// The background color to initialize the pixels with. + /// + /// The new . + /// + public ImageFrame CreateFrame(Color backgroundColor) + { + this.EnsureNotDisposed(); + + return this.NonGenericCreateFrame(backgroundColor); + } + + /// + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.Dispose(true); + GC.SuppressFinalize(this); + + this.isDisposed = true; + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + this.EnsureNotDisposed(); + + return this.NonGenericGetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); + + /// + /// Throws if the image frame is disposed. + /// + protected void EnsureNotDisposed() + { + if (this.isDisposed) + { + ThrowObjectDisposedException(this.GetType()); + } + } + + /// + /// Disposes the object and frees resources for the Garbage Collector. + /// + /// Whether to dispose of managed and unmanaged objects. + protected abstract void Dispose(bool disposing); + + /// + /// Implements . + /// + /// The enumerator. + protected abstract IEnumerator NonGenericGetEnumerator(); + + /// + /// Implements the getter of the indexer. + /// + /// The index. + /// The frame. + protected abstract ImageFrame NonGenericGetFrame(int index); + + /// + /// Implements . + /// + /// The index. + /// The frame. + /// The new frame. + protected abstract ImageFrame NonGenericInsertFrame(int index, ImageFrame source); + + /// + /// Implements . + /// + /// The frame. + /// The new frame. + protected abstract ImageFrame NonGenericAddFrame(ImageFrame source); + + /// + /// Implements . + /// + /// The index. + /// The new image. + protected abstract Image NonGenericExportFrame(int index); + + /// + /// Implements . + /// + /// The index. + /// The new image. + protected abstract Image NonGenericCloneFrame(int index); + + /// + /// Implements . + /// + /// The new frame. + protected abstract ImageFrame NonGenericCreateFrame(); + + /// + /// Implements . + /// + /// The background color. + /// The new frame. + protected abstract ImageFrame NonGenericCreateFrame(Color backgroundColor); + + [MethodImpl(InliningOptions.ColdPath)] + private static void ThrowObjectDisposedException(Type type) => throw new ObjectDisposedException(type.Name); + } +} diff --git a/ImageSharp/ImageFrameCollectionExtensions.cs b/ImageSharp/ImageFrameCollectionExtensions.cs new file mode 100644 index 0000000..efc0f5a --- /dev/null +++ b/ImageSharp/ImageFrameCollectionExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SixLabors.ImageSharp { + /// + /// Extension methods for . + /// + public static class ImageFrameCollectionExtensions + { + /// + public static IEnumerable> AsEnumerable(this ImageFrameCollection source) + where TPixel : unmanaged, IPixel + => source; + + /// + public static IEnumerable Select(this ImageFrameCollection source, Func, TResult> selector) + where TPixel : unmanaged, IPixel => source.AsEnumerable().Select(selector); + } +} diff --git a/ImageSharp/ImageFrameCollection{TPixel}.cs b/ImageSharp/ImageFrameCollection{TPixel}.cs new file mode 100644 index 0000000..369409f --- /dev/null +++ b/ImageSharp/ImageFrameCollection{TPixel}.cs @@ -0,0 +1,429 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections; +using System.Collections.Generic; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp { + /// + /// Encapsulates a pixel-specific collection of instances + /// that make up an . + /// + /// The type of the pixel. + public sealed class ImageFrameCollection : ImageFrameCollection, IEnumerable> + where TPixel : unmanaged, IPixel + { + private readonly IList> frames = new List>(); + private readonly Image parent; + + internal ImageFrameCollection(Image parent, int width, int height, TPixel backgroundColor) + { + this.parent = parent ?? throw new ArgumentNullException(nameof(parent)); + + // Frames are already cloned within the caller + this.frames.Add(new ImageFrame(parent.Configuration, width, height, backgroundColor)); + } + + internal ImageFrameCollection(Image parent, int width, int height, MemoryGroup memorySource) + : this(parent, width, height, width, memorySource) + { + } + + internal ImageFrameCollection(Image parent, int width, int height, int rowStride, MemoryGroup memorySource) + { + this.parent = parent ?? throw new ArgumentNullException(nameof(parent)); + + // Frames are already cloned within the caller + this.frames.Add(new ImageFrame(parent.Configuration, width, height, rowStride, memorySource)); + } + + internal ImageFrameCollection(Image parent, IEnumerable> frames) + { + Guard.NotNull(parent, nameof(parent)); + Guard.NotNull(frames, nameof(frames)); + + this.parent = parent; + + // Frames are already cloned by the caller + foreach (ImageFrame f in frames) + { + this.ValidateFrame(f); + this.frames.Add(f); + } + + // Ensure at least 1 frame was added to the frames collection + if (this.frames.Count == 0) + { + throw new ArgumentException("Must not be empty.", nameof(frames)); + } + } + + /// + /// Gets the number of frames. + /// + public override int Count => this.frames.Count; + + /// + /// Gets the root frame. + /// + public new ImageFrame RootFrame + { + get + { + this.EnsureNotDisposed(); + + // frame collection would always contain at least 1 frame + // the only exception is when collection is disposed what is checked via EnsureNotDisposed() call + return this.frames[0]; + } + } + + /// + /// Gets root frame accessor in unsafe manner without any checks. + /// + /// + /// This property is most likely to be called from for indexing pixels. + /// already checks if it was disposed before querying for root frame. + /// + internal ImageFrame RootFrameUnsafe => this.frames[0]; + + /// + protected override ImageFrame NonGenericRootFrame => this.RootFrame; + + /// + /// Gets the at the specified index. + /// + /// + /// The . + /// + /// The index. + /// The at the specified index. + public new ImageFrame this[int index] + { + get + { + this.EnsureNotDisposed(); + + return this.frames[index]; + } + } + + /// + public override int IndexOf(ImageFrame frame) + { + this.EnsureNotDisposed(); + + return frame is ImageFrame specific ? this.frames.IndexOf(specific) : -1; + } + + /// + /// Determines the index of a specific in the . + /// + /// The to locate in the . + /// The index of item if found in the list; otherwise, -1. + public int IndexOf(ImageFrame frame) + { + this.EnsureNotDisposed(); + + return this.frames.IndexOf(frame); + } + + /// + /// Clones and inserts the into the at the specified . + /// + /// The zero-based index to insert the frame at. + /// The to clone and insert into the . + /// Frame must have the same dimensions as the image. + /// The cloned . + public ImageFrame InsertFrame(int index, ImageFrame source) + { + this.EnsureNotDisposed(); + + this.ValidateFrame(source); + ImageFrame clonedFrame = source.Clone(this.parent.Configuration); + this.frames.Insert(index, clonedFrame); + return clonedFrame; + } + + /// + /// Clones the frame and appends the clone to the end of the collection. + /// + /// The raw pixel data to generate the from. + /// The cloned . + public ImageFrame AddFrame(ImageFrame source) + { + this.EnsureNotDisposed(); + + this.ValidateFrame(source); + ImageFrame clonedFrame = source.Clone(this.parent.Configuration); + this.frames.Add(clonedFrame); + return clonedFrame; + } + + /// + /// Creates a new frame from the pixel data with the same dimensions as the other frames and inserts the + /// new frame at the end of the collection. + /// + /// The raw pixel data to generate the from. + /// The new . + public ImageFrame AddFrame(ReadOnlySpan source) + { + this.EnsureNotDisposed(); + + ImageFrame frame = ImageFrame.LoadPixelData( + this.parent.Configuration, + source, + this.RootFrame.Width, + this.RootFrame.Height); + this.frames.Add(frame); + return frame; + } + + /// + /// Creates a new frame from the pixel data with the same dimensions as the other frames and inserts the + /// new frame at the end of the collection. + /// + /// The raw pixel data to generate the from. + /// The new . + public ImageFrame AddFrame(TPixel[] source) + { + Guard.NotNull(source, nameof(source)); + + return this.AddFrame(source.AsSpan()); + } + + /// + /// Removes the frame at the specified index and frees all freeable resources associated with it. + /// + /// The zero-based index of the frame to remove. + /// Cannot remove last frame. + public override void RemoveFrame(int index) + { + this.EnsureNotDisposed(); + + if (index == 0 && this.Count == 1) + { + throw new InvalidOperationException("Cannot remove last frame."); + } + + ImageFrame frame = this.frames[index]; + this.frames.RemoveAt(index); + frame.Dispose(); + } + + /// + public override bool Contains(ImageFrame frame) + { + this.EnsureNotDisposed(); + + return frame is ImageFrame specific && this.frames.Contains(specific); + } + + /// + /// Determines whether the contains the . + /// + /// The frame. + /// + /// true if the contains the specified frame; otherwise, false. + /// + public bool Contains(ImageFrame frame) + { + this.EnsureNotDisposed(); + + return this.frames.Contains(frame); + } + + /// + /// Moves an from to . + /// + /// The zero-based index of the frame to move. + /// The index to move the frame to. + public override void MoveFrame(int sourceIndex, int destinationIndex) + { + this.EnsureNotDisposed(); + + if (sourceIndex == destinationIndex) + { + return; + } + + ImageFrame frameAtIndex = this.frames[sourceIndex]; + this.frames.RemoveAt(sourceIndex); + this.frames.Insert(destinationIndex, frameAtIndex); + } + + /// + /// Removes the frame at the specified index and creates a new image with only the removed frame + /// with the same metadata as the original image. + /// + /// The zero-based index of the frame to export. + /// Cannot remove last frame. + /// The new with the specified frame. + public new Image ExportFrame(int index) + { + this.EnsureNotDisposed(); + + ImageFrame frame = this[index]; + + if (this.Count == 1 && this.frames.Contains(frame)) + { + throw new InvalidOperationException("Cannot remove last frame."); + } + + this.frames.Remove(frame); + + return new Image(this.parent.Configuration, this.parent.Metadata.DeepClone(), new[] { frame }); + } + + /// + /// Creates an with only the frame at the specified index + /// with the same metadata as the original image. + /// + /// The zero-based index of the frame to clone. + /// The new with the specified frame. + public new Image CloneFrame(int index) + { + this.EnsureNotDisposed(); + + ImageFrame frame = this[index]; + ImageFrame clonedFrame = frame.Clone(); + return new Image(this.parent.Configuration, this.parent.Metadata.DeepClone(), new[] { clonedFrame }); + } + + /// + /// Creates a new and appends it to the end of the collection. + /// + /// + /// The new . + /// + public new ImageFrame CreateFrame() + { + this.EnsureNotDisposed(); + + ImageFrame frame = new( + this.parent.Configuration, + this.RootFrame.Width, + this.RootFrame.Height); + this.frames.Add(frame); + return frame; + } + + /// + protected override IEnumerator NonGenericGetEnumerator() => this.frames.GetEnumerator(); + + /// + protected override ImageFrame NonGenericGetFrame(int index) => this[index]; + + /// + protected override ImageFrame NonGenericInsertFrame(int index, ImageFrame source) + { + Guard.NotNull(source, nameof(source)); + + if (source is ImageFrame compatibleSource) + { + return this.InsertFrame(index, compatibleSource); + } + + ImageFrame result = this.CopyNonCompatibleFrame(source); + this.frames.Insert(index, result); + return result; + } + + /// + protected override ImageFrame NonGenericAddFrame(ImageFrame source) + { + Guard.NotNull(source, nameof(source)); + + if (source is ImageFrame compatibleSource) + { + return this.AddFrame(compatibleSource); + } + + ImageFrame result = this.CopyNonCompatibleFrame(source); + this.frames.Add(result); + return result; + } + + /// + protected override Image NonGenericExportFrame(int index) => this.ExportFrame(index); + + /// + protected override Image NonGenericCloneFrame(int index) => this.CloneFrame(index); + + /// + protected override ImageFrame NonGenericCreateFrame(Color backgroundColor) => + this.CreateFrame(backgroundColor.ToPixel()); + + /// + protected override ImageFrame NonGenericCreateFrame() => this.CreateFrame(); + + /// + /// Creates a new and appends it to the end of the collection. + /// + /// The background color to initialize the pixels with. + /// + /// The new . + /// + public ImageFrame CreateFrame(TPixel backgroundColor) + { + ImageFrame frame = new( + this.parent.Configuration, + this.RootFrame.Width, + this.RootFrame.Height, + backgroundColor); + this.frames.Add(frame); + return frame; + } + + /// + public IEnumerator> GetEnumerator() + { + this.EnsureNotDisposed(); + + return this.frames.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private void ValidateFrame(ImageFrame frame) + { + Guard.NotNull(frame, nameof(frame)); + + if (this.Count != 0) + { + if (this.RootFrame.Width != frame.Width || this.RootFrame.Height != frame.Height) + { + throw new ArgumentException("Frame must have the same dimensions as the image.", nameof(frame)); + } + } + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + foreach (ImageFrame f in this.frames) + { + f.Dispose(); + } + + this.frames.Clear(); + } + } + + private ImageFrame CopyNonCompatibleFrame(ImageFrame source) + { + ImageFrame result = new( + this.parent.Configuration, + source.Size, + source.Metadata.DeepClone()); + source.CopyPixelsTo(result.PixelBuffer); + return result; + } + } +} diff --git a/ImageSharp/ImageFrame{TPixel}.cs b/ImageSharp/ImageFrame{TPixel}.cs new file mode 100644 index 0000000..7dd63e9 --- /dev/null +++ b/ImageSharp/ImageFrame{TPixel}.cs @@ -0,0 +1,536 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp { + /// + /// Represents a pixel-specific image frame containing all pixel data and . + /// In case of animated formats like gif, it contains the single frame in a animation. + /// In all other cases it is the only frame of the image. + /// + /// The pixel format. + public sealed class ImageFrame : ImageFrame, IPixelSource + where TPixel : unmanaged, IPixel + { + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The of the frame. + internal ImageFrame(Configuration configuration, Size size) + : this(configuration, size.Width, size.Height, new ImageFrameMetadata()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The width of the image in pixels. + /// The height of the image in pixels. + internal ImageFrame(Configuration configuration, int width, int height) + : this(configuration, width, height, new ImageFrameMetadata()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The of the frame. + /// The metadata. + internal ImageFrame(Configuration configuration, Size size, ImageFrameMetadata metadata) + : this(configuration, size.Width, size.Height, metadata) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The metadata. + internal ImageFrame(Configuration configuration, int width, int height, ImageFrameMetadata metadata) + : base(configuration, width, height, metadata) + { + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + + this.PixelBuffer = this.Configuration.MemoryAllocator.Allocate2D( + width, + height, + configuration.PreferContiguousImageBuffers, + AllocationOptions.Clean); + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The color to clear the image with. + internal ImageFrame(Configuration configuration, int width, int height, TPixel backgroundColor) + : this(configuration, width, height, backgroundColor, new ImageFrameMetadata()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The color to clear the image with. + /// The metadata. + internal ImageFrame(Configuration configuration, int width, int height, TPixel backgroundColor, ImageFrameMetadata metadata) + : base(configuration, width, height, metadata) + { + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + + this.PixelBuffer = this.Configuration.MemoryAllocator.Allocate2D( + width, + height, + configuration.PreferContiguousImageBuffers); + this.Clear(backgroundColor); + } + + /// + /// Initializes a new instance of the class wrapping an existing buffer. + /// + /// The configuration providing initialization code which allows extending the library. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The memory source. + internal ImageFrame(Configuration configuration, int width, int height, MemoryGroup memorySource) + : this(configuration, width, height, width, memorySource, new ImageFrameMetadata()) + { + } + + /// + /// Initializes a new instance of the class wrapping an existing buffer. + /// + /// The configuration providing initialization code which allows extending the library. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The number of elements between row starts. + /// The memory source. + internal ImageFrame(Configuration configuration, int width, int height, int rowStride, MemoryGroup memorySource) + : this(configuration, width, height, rowStride, memorySource, new ImageFrameMetadata()) + { + } + + /// + /// Initializes a new instance of the class wrapping an existing buffer. + /// + /// The configuration providing initialization code which allows extending the library. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The memory source. + /// The metadata. + internal ImageFrame(Configuration configuration, int width, int height, MemoryGroup memorySource, ImageFrameMetadata metadata) + : this(configuration, width, height, width, memorySource, metadata) + { + } + + /// + /// Initializes a new instance of the class wrapping an existing buffer. + /// + /// The configuration providing initialization code which allows extending the library. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The number of elements between row starts. + /// The memory source. + /// The metadata. + internal ImageFrame(Configuration configuration, int width, int height, int rowStride, MemoryGroup memorySource, ImageFrameMetadata metadata) + : base(configuration, width, height, metadata) + { + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + + this.PixelBuffer = new Buffer2D(memorySource, width, height, rowStride); + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The source. + internal ImageFrame(Configuration configuration, ImageFrame source) + : base(configuration, source.Width, source.Height, source.Metadata.DeepClone()) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(source, nameof(source)); + + this.PixelBuffer = this.Configuration.MemoryAllocator.Allocate2D( + source.PixelBuffer.Width, + source.PixelBuffer.Height, + configuration.PreferContiguousImageBuffers); + source.PixelBuffer.CopyTo(this.PixelBuffer); + } + + /// + public Buffer2D PixelBuffer { get; } + + /// + /// Gets or sets the pixel at the specified position. + /// + /// The x-coordinate of the pixel. Must be greater than or equal to zero and less than the width of the image. + /// The y-coordinate of the pixel. Must be greater than or equal to zero and less than the height of the image. + /// The at the specified position. + /// Thrown when the provided (x,y) coordinates are outside the image boundary. + public TPixel this[int x, int y] + { + [MethodImpl(InliningOptions.ShortMethod)] + get + { + this.VerifyCoords(x, y); + return this.PixelBuffer.GetElementUnsafe(x, y); + } + + [MethodImpl(InliningOptions.ShortMethod)] + set + { + this.VerifyCoords(x, y); + this.PixelBuffer.GetElementUnsafe(x, y) = value; + } + } + + /// + /// Execute to process image pixels in a safe and efficient manner. + /// + /// The defining the pixel operations. + public void ProcessPixelRows(PixelAccessorAction processPixels) + { + Guard.NotNull(processPixels, nameof(processPixels)); + + this.PixelBuffer.FastMemoryGroup.IncreaseRefCounts(); + + try + { + PixelAccessor accessor = new(this.PixelBuffer); + processPixels(accessor); + } + finally + { + this.PixelBuffer.FastMemoryGroup.DecreaseRefCounts(); + } + } + + /// + /// Execute to process pixels of multiple image frames in a safe and efficient manner. + /// + /// The second image frame. + /// The defining the pixel operations. + /// The pixel type of the second image frame. + public void ProcessPixelRows( + ImageFrame frame2, + PixelAccessorAction processPixels) + where TPixel2 : unmanaged, IPixel + { + Guard.NotNull(frame2, nameof(frame2)); + Guard.NotNull(processPixels, nameof(processPixels)); + + this.PixelBuffer.FastMemoryGroup.IncreaseRefCounts(); + frame2.PixelBuffer.FastMemoryGroup.IncreaseRefCounts(); + + try + { + PixelAccessor accessor1 = new(this.PixelBuffer); + PixelAccessor accessor2 = new(frame2.PixelBuffer); + processPixels(accessor1, accessor2); + } + finally + { + frame2.PixelBuffer.FastMemoryGroup.DecreaseRefCounts(); + this.PixelBuffer.FastMemoryGroup.DecreaseRefCounts(); + } + } + + /// + /// Execute to process pixels of multiple image frames in a safe and efficient manner. + /// + /// The second image frame. + /// The third image frame. + /// The defining the pixel operations. + /// The pixel type of the second image frame. + /// The pixel type of the third image frame. + public void ProcessPixelRows( + ImageFrame frame2, + ImageFrame frame3, + PixelAccessorAction processPixels) + where TPixel2 : unmanaged, IPixel + where TPixel3 : unmanaged, IPixel + { + Guard.NotNull(frame2, nameof(frame2)); + Guard.NotNull(frame3, nameof(frame3)); + Guard.NotNull(processPixels, nameof(processPixels)); + + this.PixelBuffer.FastMemoryGroup.IncreaseRefCounts(); + frame2.PixelBuffer.FastMemoryGroup.IncreaseRefCounts(); + frame3.PixelBuffer.FastMemoryGroup.IncreaseRefCounts(); + + try + { + PixelAccessor accessor1 = new(this.PixelBuffer); + PixelAccessor accessor2 = new(frame2.PixelBuffer); + PixelAccessor accessor3 = new(frame3.PixelBuffer); + processPixels(accessor1, accessor2, accessor3); + } + finally + { + frame3.PixelBuffer.FastMemoryGroup.DecreaseRefCounts(); + frame2.PixelBuffer.FastMemoryGroup.DecreaseRefCounts(); + this.PixelBuffer.FastMemoryGroup.DecreaseRefCounts(); + } + } + + /// + /// Copy image pixels to using the backing row layout. + /// + /// + /// Destination length must be at least ((Height - 1) * PixelBuffer.RowStride) + Width. + /// + /// The to copy image pixels to. + public void CopyPixelDataTo(Span destination) => this.PixelBuffer.CopyTo(destination); + + /// + /// Copy image pixels to using the backing row layout. + /// + /// + /// Destination length must be at least + /// (((Height - 1) * PixelBuffer.RowStride) + Width) * sizeof(TPixel) bytes. + /// + /// The of to copy image pixels to. + public void CopyPixelDataTo(Span destination) => this.PixelBuffer.CopyTo(MemoryMarshal.Cast(destination)); + + /// + /// Gets the representation of the pixels as a in the source image's pixel format + /// stored in row major order, if the backing buffer is contiguous. + /// + /// To ensure the memory is contiguous, should be set + /// to true, preferably on a non-global configuration instance (not ). + /// + /// WARNING: Disposing or leaking the underlying image while still working with the 's + /// might lead to memory corruption. + /// + /// The referencing the image buffer. + /// The indicating the success. + public bool DangerousTryGetSinglePixelMemory(out Memory memory) + => this.PixelBuffer.DangerousTryGetSingleMemory(out memory); + + /// + /// Gets a reference to the pixel at the specified position. + /// + /// The x-coordinate of the pixel. Must be greater than or equal to zero and less than the width of the image. + /// The y-coordinate of the pixel. Must be greater than or equal to zero and less than the height of the image. + /// The at the specified position. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ref TPixel GetPixelReference(int x, int y) => ref this.PixelBuffer[x, y]; + + /// + internal override void Accept(IImageFrameVisitor visitor) + => visitor.Visit(this); + + /// + /// Copies the pixels to a of the same size. + /// + /// The target pixel buffer accessor. + /// ImageFrame{TPixel}.CopyTo(): target must be of the same size! + internal void CopyTo(Buffer2D target) + { + if (this.Size != target.Size) + { + throw new ArgumentException("ImageFrame.CopyTo(): target must be of the same size!", nameof(target)); + } + + this.PixelBuffer.CopyTo(target); + } + + /// + /// Switches the buffers used by the image and the pixel source meaning that the Image will "own" the buffer + /// from the pixelSource and the pixel source will now own the Image buffer. + /// + /// The pixel source. + internal void SwapOrCopyPixelsBufferFrom(ImageFrame source) + { + Guard.NotNull(source, nameof(source)); + + _ = Buffer2D.SwapOrCopyContent(this.PixelBuffer, source.PixelBuffer); + this.UpdateSize(this.PixelBuffer.Size); + } + + /// + /// Copies the metadata from the source image. + /// + /// The metadata source. + internal void CopyMetadataFrom(ImageFrame source) + { + Guard.NotNull(source, nameof(source)); + + this.UpdateMetadata(source.Metadata); + } + + /// + protected override void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + if (disposing) + { + this.PixelBuffer.Dispose(); + } + + this.isDisposed = true; + } + + internal override void CopyPixelsTo(Buffer2D destination) + { + Guard.NotNull(destination, nameof(destination)); + Guard.IsTrue( + destination.Width == this.Width && destination.Height == this.Height, + nameof(destination), + "Destination buffer must have the same dimensions as the source frame."); + + if (typeof(TPixel) == typeof(TDestinationPixel)) + { + for (int y = 0; y < this.Height; y++) + { + Span sourceRow = this.PixelBuffer.DangerousGetRowSpan(y); + Span destinationRow = destination.DangerousGetRowSpan(y); + sourceRow.CopyTo(MemoryMarshal.Cast(destinationRow)); + } + + return; + } + + for (int y = 0; y < this.Height; y++) + { + Span sourceRow = this.PixelBuffer.DangerousGetRowSpan(y); + Span destinationRow = destination.DangerousGetRowSpan(y); + PixelOperations.Instance.To(this.Configuration, sourceRow, destinationRow); + } + } + + /// + public override string ToString() => $"ImageFrame<{typeof(TPixel).Name}>({this.Width}x{this.Height})"; + + /// + /// Clones the current instance. + /// + /// The + internal ImageFrame Clone() => this.Clone(this.Configuration); + + /// + /// Clones the current instance. + /// + /// The configuration providing initialization code which allows extending the library. + /// The + internal ImageFrame Clone(Configuration configuration) => new(configuration, this); + + /// + /// Returns a copy of the image frame in the given pixel format. + /// + /// The pixel format. + /// The + internal ImageFrame? CloneAs() + where TPixel2 : unmanaged, IPixel => this.CloneAs(this.Configuration); + + /// + /// Returns a copy of the image frame in the given pixel format. + /// + /// The pixel format. + /// The configuration providing initialization code which allows extending the library. + /// The + internal ImageFrame CloneAs(Configuration configuration) + where TPixel2 : unmanaged, IPixel + { + if (typeof(TPixel2) == typeof(TPixel)) + { + return (this.Clone(configuration) as ImageFrame)!; + } + + ImageFrame target = new(configuration, this.Width, this.Height, this.Metadata.DeepClone()); + RowIntervalOperation operation = new(this.PixelBuffer, target.PixelBuffer, configuration); + + ParallelRowIterator.IterateRowIntervals( + configuration, + this.Bounds, + in operation); + + return target; + } + + /// + /// Clears the bitmap. + /// + /// The value to initialize the bitmap with. + internal void Clear(TPixel value) => this.PixelBuffer.Clear(value); + + [MethodImpl(InliningOptions.ShortMethod)] + private void VerifyCoords(int x, int y) + { + if ((uint)x >= (uint)this.Width) + { + ThrowArgumentOutOfRangeException(nameof(x)); + } + + if ((uint)y >= (uint)this.Height) + { + ThrowArgumentOutOfRangeException(nameof(y)); + } + } + + [MethodImpl(InliningOptions.ColdPath)] + private static void ThrowArgumentOutOfRangeException(string paramName) => throw new ArgumentOutOfRangeException(paramName); + + /// + /// A implementing the clone logic for . + /// + /// The type of the target pixel format. + private readonly struct RowIntervalOperation : IRowIntervalOperation + where TPixel2 : unmanaged, IPixel + { + private readonly Buffer2D source; + private readonly Buffer2D target; + private readonly Configuration configuration; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowIntervalOperation( + Buffer2D source, + Buffer2D target, + Configuration configuration) + { + this.source = source; + this.target = target; + this.configuration = configuration; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(in RowInterval rows) + { + for (int y = rows.Min; y < rows.Max; y++) + { + Span sourceRow = this.source.DangerousGetRowSpan(y); + Span targetRow = this.target.DangerousGetRowSpan(y); + PixelOperations.Instance.To(this.configuration, sourceRow, targetRow); + } + } + } + } +} diff --git a/ImageSharp/ImageInfo.cs b/ImageSharp/ImageInfo.cs new file mode 100644 index 0000000..3e9e45a --- /dev/null +++ b/ImageSharp/ImageInfo.cs @@ -0,0 +1,118 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp { + /// + /// Contains information about the image including dimensions, pixel type information and additional metadata + /// + public class ImageInfo + { + /// + /// Initializes a new instance of the class. + /// + /// The size of the image in px units. + /// The image metadata. + public ImageInfo( + Size size, + ImageMetadata metadata) + : this(size, metadata, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The size of the image in px units. + /// The image metadata. + /// The collection of image frame metadata. + public ImageInfo( + Size size, + ImageMetadata metadata, + IReadOnlyList? frameMetadataCollection) + { + this.Size = size; + this.Metadata = metadata; + + // PixelTpe is normally set following decoding + // See ImageDecoder.SetDecoderFormat(Configuration configuration, ImageInfo info). + if (metadata.DecodedImageFormat is not null) + { + this.PixelType = metadata.GetDecodedPixelTypeInfo(); + } + + this.FrameMetadataCollection = frameMetadataCollection ?? []; + } + + /// + /// Gets information about the image pixels. + /// + public PixelTypeInfo PixelType { get; internal set; } + + /// + /// Gets the image width in px units. + /// + public int Width => this.Size.Width; + + /// + /// Gets the image height in px units. + /// + public int Height => this.Size.Height; + + /// + /// Gets the number of frame metadata entries available for the image. + /// + /// + /// This value is the same as count and may be 0 when frame + /// metadata was not populated by the decoder. + /// + public int FrameCount => this.FrameMetadataCollection.Count; + + /// + /// Gets any metadata associated with the image. + /// + public ImageMetadata Metadata { get; } + + /// + /// Gets the metadata associated with the decoded image frames, if available. + /// + /// + /// For multi-frame formats, decoders populate one entry per decoded frame. For single-frame formats, this + /// collection is typically empty. + /// + public IReadOnlyList FrameMetadataCollection { get; } + + /// + /// Gets the size of the image in px units. + /// + public Size Size { get; } + + /// + /// Gets the bounds of the image. + /// + public Rectangle Bounds => new(Point.Empty, this.Size); + + /// + /// Gets the total number of bytes required to store the image pixels in memory. + /// + /// + /// This reports the in-memory size of the pixel data represented by this , not the + /// encoded size of the image file. The value is computed from the image dimensions and + /// . When contains decoded frame metadata, the + /// per-frame size is multiplied by that count. Otherwise, the value is the in-memory size of the single + /// image frame represented by this . + /// + /// The total number of bytes required to store the image pixels in memory. + public long GetPixelMemorySize() + { + int count = this.FrameMetadataCollection.Count > 0 + ? this.FrameMetadataCollection.Count + : 1; + + return (long)this.Size.Width * this.Size.Height * (this.PixelType.BitsPerPixel / 8) * count; + } + } +} diff --git a/ImageSharp/ImageSharp.csproj b/ImageSharp/ImageSharp.csproj new file mode 100644 index 0000000..3c0b315 --- /dev/null +++ b/ImageSharp/ImageSharp.csproj @@ -0,0 +1,224 @@ + + + + + net10.0 + SixLabors.ImageSharp + SixLabors.ImageSharp + SixLabors.ImageSharp + SixLabors.ImageSharp + sixlabors.imagesharp.128.png + LICENSE + https://github.com/SixLabors/ImageSharp/ + $(RepositoryUrl) + Image Resize Crop Gif Jpg Jpeg Bitmap Pbm Png Tga Tiff WebP NetCore + A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET + Debug;Release + true + + + + + enable + Nullable + + + + + 4.0 + true + + + + + + + + + + + + + + + True + True + InlineArray.tt + + + True + True + ImageExtensions.Save.tt + + + True + True + ImageMetadataExtensions.tt + + + True + True + Abgr32.PixelOperations.Generated.tt + + + True + True + PixelOperations{TPixel}.Generated.tt + + + True + True + Argb32.PixelOperations.Generated.tt + + + True + True + Bgr24.PixelOperations.Generated.tt + + + True + True + Bgra32.PixelOperations.Generated.tt + + + True + True + Bgra5551.PixelOperations.Generated.tt + + + True + True + L16.PixelOperations.Generated.tt + + + True + True + L8.PixelOperations.Generated.tt + + + True + True + La16.PixelOperations.Generated.tt + + + True + True + La32.PixelOperations.Generated.tt + + + True + True + Rgb24.PixelOperations.Generated.tt + + + True + True + Rgb48.PixelOperations.Generated.tt + + + True + True + Rgba32.PixelOperations.Generated.tt + + + True + True + Rgba64.PixelOperations.Generated.tt + + + True + True + DefaultPixelBlenders.Generated.tt + + + True + True + PorterDuffFunctions.Generated.tt + + + + + + TextTemplatingFileGenerator + InlineArray.cs + + + ImageMetadataExtensions.cs + TextTemplatingFileGenerator + + + TextTemplatingFileGenerator + Abgr32.PixelOperations.Generated.cs + + + TextTemplatingFileGenerator + PixelOperations{TPixel}.Generated.cs + + + TextTemplatingFileGenerator + Argb32.PixelOperations.Generated.cs + + + TextTemplatingFileGenerator + Bgr24.PixelOperations.Generated.cs + + + TextTemplatingFileGenerator + Bgra32.PixelOperations.Generated.cs + + + TextTemplatingFileGenerator + Bgra5551.PixelOperations.Generated.cs + + + TextTemplatingFileGenerator + L8.PixelOperations.Generated.cs + + + TextTemplatingFileGenerator + L16.PixelOperations.Generated.cs + + + TextTemplatingFileGenerator + La16.PixelOperations.Generated.cs + + + TextTemplatingFileGenerator + La32.PixelOperations.Generated.cs + + + TextTemplatingFileGenerator + Rgb24.PixelOperations.Generated.cs + + + TextTemplatingFileGenerator + Rgba32.PixelOperations.Generated.cs + + + TextTemplatingFileGenerator + Rgb48.PixelOperations.Generated.cs + + + TextTemplatingFileGenerator + Rgba64.PixelOperations.Generated.cs + + + PorterDuffFunctions.Generated.cs + TextTemplatingFileGenerator + + + DefaultPixelBlenders.Generated.cs + TextTemplatingFileGenerator + + + TextTemplatingFileGenerator + ImageExtensions.Save.cs + + + + + + + + + diff --git a/ImageSharp/Image{TPixel}.cs b/ImageSharp/Image{TPixel}.cs new file mode 100644 index 0000000..88b406a --- /dev/null +++ b/ImageSharp/Image{TPixel}.cs @@ -0,0 +1,491 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp { + /// + /// Encapsulates an image, which consists of the pixel data for a graphics image and its attributes. + /// For generic -s the pixel type is known at compile time. + /// + /// The pixel format. + public sealed class Image : Image + where TPixel : unmanaged, IPixel + { + private readonly ImageFrameCollection frames; + + /// + /// Initializes a new instance of the class + /// with the height and the width of the image. + /// + /// The configuration providing initialization code which allows extending the library. + /// The width of the image in pixels. + /// The height of the image in pixels. + public Image(Configuration configuration, int width, int height) + : this(configuration, width, height, new ImageMetadata()) + { + } + + /// + /// Initializes a new instance of the class + /// with the height and the width of the image. + /// + /// The configuration providing initialization code which allows extending the library. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The color to initialize the pixels with. + public Image(Configuration configuration, int width, int height, TPixel backgroundColor) + : this(configuration, width, height, backgroundColor, new ImageMetadata()) + { + } + + /// + /// Initializes a new instance of the class + /// with the height and the width of the image. + /// + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The color to initialize the pixels with. + public Image(int width, int height, TPixel backgroundColor) + : this(Configuration.Default, width, height, backgroundColor, new ImageMetadata()) + { + } + + /// + /// Initializes a new instance of the class + /// with the height and the width of the image. + /// + /// The width of the image in pixels. + /// The height of the image in pixels. + public Image(int width, int height) + : this(Configuration.Default, width, height) + { + } + + /// + /// Initializes a new instance of the class + /// with the height and the width of the image. + /// + /// The configuration providing initialization code which allows extending the library. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The images metadata. + internal Image(Configuration configuration, int width, int height, ImageMetadata? metadata) + : base(configuration, TPixel.GetPixelTypeInfo(), metadata ?? new ImageMetadata(), width, height) + => this.frames = new ImageFrameCollection(this, width, height, default(TPixel)); + + /// + /// Initializes a new instance of the class + /// wrapping an external pixel buffer. + /// + /// The configuration providing initialization code which allows extending the library. + /// Pixel buffer. + /// The images metadata. + internal Image( + Configuration configuration, + Buffer2D pixelBuffer, + ImageMetadata metadata) + : this(configuration, pixelBuffer.FastMemoryGroup, pixelBuffer.Width, pixelBuffer.Height, pixelBuffer.RowStride, metadata) + { + } + + /// + /// Initializes a new instance of the class + /// wrapping an external . + /// + /// The configuration providing initialization code which allows extending the library. + /// The memory source. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The images metadata. + internal Image( + Configuration configuration, + MemoryGroup memoryGroup, + int width, + int height, + ImageMetadata metadata) + : this(configuration, memoryGroup, width, height, width, metadata) + { + } + + /// + /// Initializes a new instance of the class + /// wrapping an external . + /// + /// The configuration providing initialization code which allows extending the library. + /// The memory source. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The number of elements between row starts. + /// The images metadata. + internal Image( + Configuration configuration, + MemoryGroup memoryGroup, + int width, + int height, + int rowStride, + ImageMetadata metadata) + : base(configuration, TPixel.GetPixelTypeInfo(), metadata, width, height) + => this.frames = new ImageFrameCollection(this, width, height, rowStride, memoryGroup); + + /// + /// Initializes a new instance of the class + /// with the height and the width of the image. + /// + /// The configuration providing initialization code which allows extending the library. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The color to initialize the pixels with. + /// The images metadata. + internal Image( + Configuration configuration, + int width, + int height, + TPixel backgroundColor, + ImageMetadata? metadata) + : base(configuration, TPixel.GetPixelTypeInfo(), metadata ?? new ImageMetadata(), width, height) + => this.frames = new ImageFrameCollection(this, width, height, backgroundColor); + + /// + /// Initializes a new instance of the class + /// with the height and the width of the image. + /// + /// The configuration providing initialization code which allows extending the library. + /// The images metadata. + /// The frames that will be owned by this image instance. + internal Image(Configuration configuration, ImageMetadata metadata, IEnumerable> frames) + : base(configuration, TPixel.GetPixelTypeInfo(), metadata, ValidateFramesAndGetSize(frames)) + => this.frames = new ImageFrameCollection(this, frames); + + /// + protected override ImageFrameCollection NonGenericFrameCollection => this.Frames; + + /// + /// Gets the collection of image frames. + /// + public new ImageFrameCollection Frames + { + get + { + this.EnsureNotDisposed(); + return this.frames; + } + } + + /// + /// Gets the root frame. + /// + private ImageFrame PixelSourceUnsafe => this.frames.RootFrameUnsafe; + + /// + /// Gets or sets the pixel at the specified position. + /// + /// The x-coordinate of the pixel. Must be greater than or equal to zero and less than the width of the image. + /// The y-coordinate of the pixel. Must be greater than or equal to zero and less than the height of the image. + /// The at the specified position. + /// Thrown when the provided (x,y) coordinates are outside the image boundary. + public TPixel this[int x, int y] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + this.EnsureNotDisposed(); + + this.VerifyCoords(x, y); + return this.PixelSourceUnsafe.PixelBuffer.GetElementUnsafe(x, y); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + this.EnsureNotDisposed(); + + this.VerifyCoords(x, y); + this.PixelSourceUnsafe.PixelBuffer.GetElementUnsafe(x, y) = value; + } + } + + /// + /// Execute to process image pixels in a safe and efficient manner. + /// + /// The defining the pixel operations. + public void ProcessPixelRows(PixelAccessorAction processPixels) + { + Guard.NotNull(processPixels, nameof(processPixels)); + Buffer2D buffer = this.Frames.RootFrame.PixelBuffer; + buffer.FastMemoryGroup.IncreaseRefCounts(); + + try + { + PixelAccessor accessor = new(buffer); + processPixels(accessor); + } + finally + { + buffer.FastMemoryGroup.DecreaseRefCounts(); + } + } + + /// + /// Execute to process pixels of multiple images in a safe and efficient manner. + /// + /// The second image. + /// The defining the pixel operations. + /// The pixel type of the second image. + public void ProcessPixelRows( + Image image2, + PixelAccessorAction processPixels) + where TPixel2 : unmanaged, IPixel + { + Guard.NotNull(image2, nameof(image2)); + Guard.NotNull(processPixels, nameof(processPixels)); + + Buffer2D buffer1 = this.Frames.RootFrame.PixelBuffer; + Buffer2D buffer2 = image2.Frames.RootFrame.PixelBuffer; + + buffer1.FastMemoryGroup.IncreaseRefCounts(); + buffer2.FastMemoryGroup.IncreaseRefCounts(); + + try + { + PixelAccessor accessor1 = new(buffer1); + PixelAccessor accessor2 = new(buffer2); + processPixels(accessor1, accessor2); + } + finally + { + buffer2.FastMemoryGroup.DecreaseRefCounts(); + buffer1.FastMemoryGroup.DecreaseRefCounts(); + } + } + + /// + /// Execute to process pixels of multiple images in a safe and efficient manner. + /// + /// The second image. + /// The third image. + /// The defining the pixel operations. + /// The pixel type of the second image. + /// The pixel type of the third image. + public void ProcessPixelRows( + Image image2, + Image image3, + PixelAccessorAction processPixels) + where TPixel2 : unmanaged, IPixel + where TPixel3 : unmanaged, IPixel + { + Guard.NotNull(image2, nameof(image2)); + Guard.NotNull(image3, nameof(image3)); + Guard.NotNull(processPixels, nameof(processPixels)); + + Buffer2D buffer1 = this.Frames.RootFrame.PixelBuffer; + Buffer2D buffer2 = image2.Frames.RootFrame.PixelBuffer; + Buffer2D buffer3 = image3.Frames.RootFrame.PixelBuffer; + + buffer1.FastMemoryGroup.IncreaseRefCounts(); + buffer2.FastMemoryGroup.IncreaseRefCounts(); + buffer3.FastMemoryGroup.IncreaseRefCounts(); + + try + { + PixelAccessor accessor1 = new(buffer1); + PixelAccessor accessor2 = new(buffer2); + PixelAccessor accessor3 = new(buffer3); + processPixels(accessor1, accessor2, accessor3); + } + finally + { + buffer3.FastMemoryGroup.DecreaseRefCounts(); + buffer2.FastMemoryGroup.DecreaseRefCounts(); + buffer1.FastMemoryGroup.DecreaseRefCounts(); + } + } + + /// + /// Copy image pixels to using the root frame backing row layout. + /// + /// + /// Destination length must be at least + /// ((Height - 1) * Frames.RootFrame.PixelBuffer.RowStride) + Width. + /// + /// The to copy image pixels to. + public void CopyPixelDataTo(Span destination) => this.Frames.RootFrame.CopyPixelDataTo(destination); + + /// + /// Copy image pixels to using the root frame backing row layout. + /// + /// + /// Destination length must be at least + /// (((Height - 1) * Frames.RootFrame.PixelBuffer.RowStride) + Width) * sizeof(TPixel) bytes. + /// + /// The of to copy image pixels to. + public void CopyPixelDataTo(Span destination) => this.Frames.RootFrame.CopyPixelDataTo(destination); + + /// + /// Gets the representation of the pixels as a in the source image's pixel format + /// stored in row major order, if the backing buffer is contiguous. + /// + /// To ensure the memory is contiguous, should be set + /// to true, preferably on a non-global configuration instance (not ). + /// + /// WARNING: Disposing or leaking the underlying image while still working with the 's + /// might lead to memory corruption. + /// + /// The referencing the image buffer. + /// The indicating the success. + public bool DangerousTryGetSinglePixelMemory(out Memory memory) + => this.Frames.RootFrame.DangerousTryGetSinglePixelMemory(out memory); + + /// + /// Clones the current image. + /// + /// Returns a new image with all the same metadata as the original. + public Image Clone() => this.Clone(this.Configuration); + + /// + /// Clones the current image with the given configuration. + /// + /// The configuration providing initialization code which allows extending the library. + /// Returns a new with all the same pixel data as the original. + public Image Clone(Configuration configuration) + { + this.EnsureNotDisposed(); + + ImageFrame[] clonedFrames = new ImageFrame[this.frames.Count]; + for (int i = 0; i < clonedFrames.Length; i++) + { + clonedFrames[i] = this.frames[i].Clone(configuration); + } + + return new Image(configuration, this.Metadata.DeepClone(), clonedFrames); + } + + /// + /// Returns a copy of the image in the given pixel format. + /// + /// The pixel format. + /// The configuration providing initialization code which allows extending the library. + /// The . + public override Image CloneAs(Configuration configuration) + { + this.EnsureNotDisposed(); + + ImageFrame[] clonedFrames = new ImageFrame[this.frames.Count]; + for (int i = 0; i < clonedFrames.Length; i++) + { + clonedFrames[i] = this.frames[i].CloneAs(configuration); + } + + return new Image(configuration, this.Metadata.DeepClone(), clonedFrames); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.frames.Dispose(); + } + } + + /// + public override string ToString() => $"Image<{typeof(TPixel).Name}>: {this.Width}x{this.Height}"; + + /// + internal override void Accept(IImageVisitor visitor) + { + this.EnsureNotDisposed(); + + visitor.Visit(this); + } + + /// + internal override Task AcceptAsync(IImageVisitorAsync visitor, CancellationToken cancellationToken) + { + this.EnsureNotDisposed(); + + return visitor.VisitAsync(this, cancellationToken); + } + + /// + /// Switches the buffers used by the image and the pixel source meaning that the Image will + /// "own" the buffer from the pixelSource and the pixel source will now own the Image buffer. + /// + /// The pixel source. + internal void SwapOrCopyPixelsBuffersFrom(Image source) + { + Guard.NotNull(source, nameof(source)); + + this.EnsureNotDisposed(); + + ImageFrameCollection sourceFrames = source.Frames; + for (int i = 0; i < this.frames.Count; i++) + { + this.frames[i].SwapOrCopyPixelsBufferFrom(sourceFrames[i]); + } + + this.UpdateSize(source.Size); + } + + /// + /// Copies the metadata from the source image. + /// + /// The metadata source. + internal void CopyMetadataFrom(Image source) + { + Guard.NotNull(source, nameof(source)); + + this.EnsureNotDisposed(); + + ImageFrameCollection sourceFrames = source.Frames; + for (int i = 0; i < this.frames.Count; i++) + { + this.frames[i].CopyMetadataFrom(sourceFrames[i]); + } + + this.UpdateMetadata(source.Metadata); + } + + private static Size ValidateFramesAndGetSize(IEnumerable> frames) + { + Guard.NotNull(frames, nameof(frames)); + + ImageFrame? rootFrame = frames.FirstOrDefault() ?? throw new ArgumentException("Must not be empty.", nameof(frames)); + + Size rootSize = rootFrame.Size; + + if (frames.Any(f => f.Size != rootSize)) + { + throw new ArgumentException("The provided frames must be of the same size.", nameof(frames)); + } + + return rootSize; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void VerifyCoords(int x, int y) + { + if ((uint)x >= (uint)this.Width) + { + ThrowArgumentOutOfRangeException(nameof(x)); + } + + if ((uint)y >= (uint)this.Height) + { + ThrowArgumentOutOfRangeException(nameof(y)); + } + } + + private static void ThrowArgumentOutOfRangeException(string paramName) + => throw new ArgumentOutOfRangeException(paramName); + } +} diff --git a/ImageSharp/IndexedImageFrame{TPixel}.cs b/ImageSharp/IndexedImageFrame{TPixel}.cs new file mode 100644 index 0000000..7669e89 --- /dev/null +++ b/ImageSharp/IndexedImageFrame{TPixel}.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp { + /// + /// A pixel-specific image frame where each pixel buffer value represents an index in a color palette. + /// + /// The pixel format. + public sealed class IndexedImageFrame : IPixelSource, IDisposable + where TPixel : unmanaged, IPixel + { + private readonly Buffer2D pixelBuffer; + private readonly IMemoryOwner paletteOwner; + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The configuration which allows altering default behavior or extending the library. + /// + /// The frame width. + /// The frame height. + /// The color palette. + public IndexedImageFrame(Configuration configuration, int width, int height, ReadOnlyMemory palette) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.MustBeLessThanOrEqualTo(palette.Length, QuantizerConstants.MaxColors, nameof(palette)); + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + + this.Configuration = configuration; + this.Width = width; + this.Height = height; + this.pixelBuffer = configuration.MemoryAllocator.Allocate2D(width, height); + + // Copy the palette over. We want the lifetime of this frame to be independent of any palette source. + this.paletteOwner = configuration.MemoryAllocator.Allocate(palette.Length); + palette.Span.CopyTo(this.paletteOwner.GetSpan()); + this.Palette = this.paletteOwner.Memory[..palette.Length]; + } + + /// + /// Gets the configuration which allows altering default behavior or extending the library. + /// + public Configuration Configuration { get; } + + /// + /// Gets the width of this . + /// + public int Width { get; } + + /// + /// Gets the height of this . + /// + public int Height { get; } + + /// + /// Gets the color palette of this . + /// + public ReadOnlyMemory Palette { get; } + + /// + Buffer2D IPixelSource.PixelBuffer => this.pixelBuffer; + + /// + /// Gets the representation of the pixels as a of contiguous memory + /// at row beginning from the first pixel on that row. + /// + /// WARNING: Disposing or leaking the underlying while still working with it's + /// might lead to memory corruption. + /// + /// The row index in the pixel buffer. + /// The pixel row as a . + [MethodImpl(InliningOptions.ShortMethod)] + public ReadOnlySpan DangerousGetRowSpan(int rowIndex) + => this.GetWritablePixelRowSpanUnsafe(rowIndex); + + /// + /// + /// Gets the representation of the pixels as a of contiguous memory + /// at row beginning from the first pixel on that row. + /// + /// + /// Note: Values written to this span are not sanitized against the palette length. + /// Care should be taken during assignment to prevent out-of-bounds errors. + /// + /// + /// The row index in the pixel buffer. + /// The pixel row as a . + [MethodImpl(InliningOptions.ShortMethod)] + public Span GetWritablePixelRowSpanUnsafe(int rowIndex) + => this.pixelBuffer.DangerousGetRowSpan(rowIndex); + + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.isDisposed = true; + this.pixelBuffer.Dispose(); + this.paletteOwner.Dispose(); + } + } + } +} diff --git a/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs b/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs new file mode 100644 index 0000000..22a77f3 --- /dev/null +++ b/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; + +namespace SixLabors.ImageSharp.Memory { + /// + /// Provides the tracked memory-owner contract required by . + /// + /// The element type. + /// + /// Custom allocators implement + /// and return a derived type. The base allocator attaches allocation tracking after the owner has been + /// created so custom implementations cannot forget, duplicate, or mismatch the reservation lifecycle. + /// + public abstract class AllocationTrackedMemoryManager : MemoryManager + where T : struct + { + private AllocationTrackingState allocationTracking; + + /// + /// Releases resources held by the concrete tracked owner. + /// + /// + /// when the owner is being disposed deterministically; + /// otherwise, . + /// + /// + /// Implementations release their own resources here. Allocation tracking is released by the sealed base + /// dispose path after this method returns. + /// + protected abstract void DisposeCore(bool disposing); + + /// + protected sealed override void Dispose(bool disposing) + { + try + { + this.DisposeCore(disposing); + } + finally + { + this.ReleaseAllocationTracking(); + } + } + + /// + /// Attaches allocation tracking to this owner after allocation has succeeded. + /// + /// The allocator that owns the reservation for this instance. + /// The reserved allocation size, in bytes. + /// + /// calls this exactly once after AllocateCore returns. + /// Derived allocators should not call it themselves; they only construct the concrete owner. + /// + protected internal virtual void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes) + => this.allocationTracking.Attach(allocator, lengthInBytes); + + /// + /// Releases any tracked allocation bytes associated with this instance. + /// + /// + /// Calling this more than once is safe; only the first call after tracking has been attached releases bytes. + /// + private void ReleaseAllocationTracking() => this.allocationTracking.Release(); + } +} diff --git a/ImageSharp/Memory/AllocationTrackingState.cs b/ImageSharp/Memory/AllocationTrackingState.cs new file mode 100644 index 0000000..fd4c4a9 --- /dev/null +++ b/ImageSharp/Memory/AllocationTrackingState.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Threading; + +namespace SixLabors.ImageSharp.Memory { + /// + /// Tracks a single allocator reservation and releases it exactly once. + /// + /// + /// This type is intended to live as a mutable field on the owning object. It should not be copied + /// after tracking has been attached, because the owner relies on a single shared release state. + /// + internal struct AllocationTrackingState + { + private MemoryAllocator? allocator; + private long lengthInBytes; + private int released; + + /// + /// Attaches allocator reservation tracking to the current owner. + /// + /// The allocator that owns the reservation. + /// The reserved allocation size, in bytes. + /// + /// Must complete-before the owning object's reference is observable to any other thread. + /// guarantees this by attaching synchronously on the allocating + /// thread before returning the owner; reference publication then provides the release fence + /// that makes these field writes visible to a subsequent on another thread. + /// + internal void Attach(MemoryAllocator allocator, long lengthInBytes) + { + this.allocator = allocator; + this.lengthInBytes = lengthInBytes; + } + + /// + /// Releases the attached allocator reservation once. + /// + internal void Release() + { + if (Interlocked.Exchange(ref this.released, 1) == 0 && this.allocator != null) + { + this.allocator.ReleaseAccumulatedBytes(this.lengthInBytes); + this.allocator = null; + } + } + } +} diff --git a/ImageSharp/Memory/Allocators/AllocationOptions.cs b/ImageSharp/Memory/Allocators/AllocationOptions.cs new file mode 100644 index 0000000..ccb1c8a --- /dev/null +++ b/ImageSharp/Memory/Allocators/AllocationOptions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Memory { + /// + /// Options for allocating buffers. + /// + [Flags] + public enum AllocationOptions + { + /// + /// Indicates that the buffer should just be allocated. + /// + None = 0, + + /// + /// Indicates that the allocated buffer should be cleaned following allocation. + /// + Clean = 1 + } +} diff --git a/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs b/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs new file mode 100644 index 0000000..5272e7e --- /dev/null +++ b/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Memory { + /// + /// Provides helper methods for working with . + /// + internal static class AllocationOptionsExtensions + { + /// + /// Returns a value indicating whether the specified flag is set on the allocation options. + /// + /// The allocation options to inspect. + /// The flag to test for. + /// if is set; otherwise, . + public static bool Has(this AllocationOptions options, AllocationOptions flag) + => (options & flag) == flag; + } +} diff --git a/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs b/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs new file mode 100644 index 0000000..eb79ee4 --- /dev/null +++ b/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; + +namespace SixLabors.ImageSharp.Memory.Internals { + /// + /// Wraps an array as an instance. + /// + /// + internal class BasicArrayBuffer : ManagedBufferBase + where T : struct + { + /// + /// Initializes a new instance of the class. + /// + /// The array. + /// The length of the buffer. + public BasicArrayBuffer(T[] array, int length) + { + DebugGuard.MustBeLessThanOrEqualTo(length, array.Length, nameof(length)); + this.Array = array; + this.Length = length; + } + + /// + /// Initializes a new instance of the class. + /// + /// The array. + public BasicArrayBuffer(T[] array) + : this(array, array.Length) + { + } + + /// + /// Gets the array. + /// + public T[] Array { get; } + + /// + /// Gets the length. + /// + public int Length { get; } + + /// + public override Span GetSpan() => this.Array.AsSpan(0, this.Length); + + /// + protected override void DisposeCore(bool disposing) + { + } + + /// + protected override object GetPinnableObject() + { + return this.Array; + } + } +} diff --git a/ImageSharp/Memory/Allocators/Internals/Gen2GcCallback.cs b/ImageSharp/Memory/Allocators/Internals/Gen2GcCallback.cs new file mode 100644 index 0000000..034a971 --- /dev/null +++ b/ImageSharp/Memory/Allocators/Internals/Gen2GcCallback.cs @@ -0,0 +1,110 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// Port of BCL internal utility: +// https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6bf7424a79ce3af9d6657/src/libraries/System.Private.CoreLib/src/System/Gen2GcCallback.cs +using System; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Memory.Internals { + /// + /// Schedules a callback roughly every gen 2 GC (you may see a Gen 0 an Gen 1 but only once) + /// (We can fix this by capturing the Gen 2 count at startup and testing, but I mostly don't care) + /// + internal sealed class Gen2GcCallback : CriticalFinalizerObject + { + private readonly Func? callback0; + private readonly Func? callback1; + private GCHandle weakTargetObj; + + private Gen2GcCallback(Func callback) => this.callback0 = callback; + + private Gen2GcCallback(Func callback, object targetObj) + { + this.callback1 = callback; + this.weakTargetObj = GCHandle.Alloc(targetObj, GCHandleType.Weak); + } + + ~Gen2GcCallback() + { + if (this.weakTargetObj.IsAllocated) + { + // Check to see if the target object is still alive. + object? targetObj = this.weakTargetObj.Target; + if (targetObj == null) + { + // The target object is dead, so this callback object is no longer needed. + this.weakTargetObj.Free(); + return; + } + + // Execute the callback method. + try + { + if (!this.callback1!(targetObj)) + { + // If the callback returns false, this callback object is no longer needed. + this.weakTargetObj.Free(); + return; + } + } + catch + { + // Ensure that we still get a chance to resurrect this object, even if the callback throws an exception. +#if DEBUG + // Except in DEBUG, as we really shouldn't be hitting any exceptions here. + throw; +#endif + } + } + else + { + // Execute the callback method. + try + { + if (!this.callback0!()) + { + // If the callback returns false, this callback object is no longer needed. + return; + } + } + catch + { + // Ensure that we still get a chance to resurrect this object, even if the callback throws an exception. +#if DEBUG + // Except in DEBUG, as we really shouldn't be hitting any exceptions here. + throw; +#endif + } + } + + // Resurrect ourselves by re-registering for finalization. + GC.ReRegisterForFinalize(this); + } + + /// + /// Schedule 'callback' to be called in the next GC. If the callback returns true it is + /// rescheduled for the next Gen 2 GC. Otherwise the callbacks stop. + /// + public static void Register(Func callback) => + + // Create a unreachable object that remembers the callback function and target object. + _ = new Gen2GcCallback(callback); + + /// + /// + /// Schedule 'callback' to be called in the next GC. If the callback returns true it is + /// rescheduled for the next Gen 2 GC. Otherwise the callbacks stop. + /// + /// + /// NOTE: This callback will be kept alive until either the callback function returns false, + /// or the target object dies. + /// + /// + public static void Register(Func callback, object targetObj) => + + // Create a unreachable object that remembers the callback function and target object. + _ = new Gen2GcCallback(callback, targetObj); + } +} diff --git a/ImageSharp/Memory/Allocators/Internals/IRefCounted.cs b/ImageSharp/Memory/Allocators/Internals/IRefCounted.cs new file mode 100644 index 0000000..537b9c3 --- /dev/null +++ b/ImageSharp/Memory/Allocators/Internals/IRefCounted.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Memory.Internals { + /// + /// Defines an common interface for ref-counted objects. + /// + internal interface IRefCounted + { + /// + /// Increments the reference counter. + /// + void AddRef(); + + /// + /// Decrements the reference counter. + /// + void ReleaseRef(); + } +} diff --git a/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs b/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs new file mode 100644 index 0000000..972d5e5 --- /dev/null +++ b/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Memory.Internals { + /// + /// Provides a base class for implementations by implementing pinning logic for adaption. + /// + /// The element type. + internal abstract class ManagedBufferBase : AllocationTrackedMemoryManager + where T : struct + { + private GCHandle pinHandle; + + /// + public override unsafe MemoryHandle Pin(int elementIndex = 0) + { + if (!this.pinHandle.IsAllocated) + { + this.pinHandle = GCHandle.Alloc(this.GetPinnableObject(), GCHandleType.Pinned); + } + + void* ptr = Unsafe.Add((void*)this.pinHandle.AddrOfPinnedObject(), elementIndex); + + // We should only pass pinnable:this, when GCHandle lifetime is managed by the MemoryManager instance. + return new MemoryHandle(ptr, pinnable: this); + } + + /// + public override void Unpin() + { + if (this.pinHandle.IsAllocated) + { + this.pinHandle.Free(); + } + } + + /// + /// Gets the object that should be pinned. + /// + /// The pinnable . + protected abstract object GetPinnableObject(); + } +} diff --git a/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs b/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs new file mode 100644 index 0000000..53faf1e --- /dev/null +++ b/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs @@ -0,0 +1,90 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Diagnostics; +using System; +using System.Threading; + +namespace SixLabors.ImageSharp.Memory.Internals { + /// + /// Implements reference counting lifetime guard mechanism for memory resources + /// and maintains the value of . + /// + internal abstract class RefCountedMemoryLifetimeGuard : IDisposable + { + private AllocationTrackingState allocationTracking; + private int refCount = 1; + private int disposed; + private int released; + private string? allocationStackTrace; + + protected RefCountedMemoryLifetimeGuard() + { + if (MemoryDiagnostics.UndisposedAllocationSubscribed) + { + this.allocationStackTrace = Environment.StackTrace; + } + + MemoryDiagnostics.IncrementTotalUndisposedAllocationCount(); + } + + ~RefCountedMemoryLifetimeGuard() + { + Interlocked.Exchange(ref this.disposed, 1); + this.ReleaseRef(true); + } + + public bool IsDisposed => this.disposed == 1; + + public void AddRef() => Interlocked.Increment(ref this.refCount); + + public void ReleaseRef() => this.ReleaseRef(false); + + /// + /// Attaches allocator reservation tracking to this lifetime guard. + /// + /// The allocator that owns the reservation. + /// The reserved allocation size, in bytes. + public void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes) + => this.allocationTracking.Attach(allocator, lengthInBytes); + + public void Dispose() + { + int wasDisposed = Interlocked.Exchange(ref this.disposed, 1); + if (wasDisposed == 0) + { + this.ReleaseRef(); + GC.SuppressFinalize(this); + } + } + + protected abstract void Release(); + + private void ReleaseRef(bool finalizing) + { + Interlocked.Decrement(ref this.refCount); + if (this.refCount == 0) + { + int wasReleased = Interlocked.Exchange(ref this.released, 1); + + if (wasReleased == 0) + { + if (!finalizing) + { + MemoryDiagnostics.DecrementTotalUndisposedAllocationCount(); + } + else if (this.allocationStackTrace != null) + { + MemoryDiagnostics.RaiseUndisposedMemoryResource(this.allocationStackTrace); + } + + this.Release(); + + // Guard-backed resources can be recovered by finalization, so their allocator + // reservation must follow the guard's actual release point instead of the owner object. + this.allocationTracking.Release(); + } + } + } + } +} diff --git a/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs b/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs new file mode 100644 index 0000000..3ed277b --- /dev/null +++ b/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Memory.Internals { + internal class SharedArrayPoolBuffer : ManagedBufferBase, IRefCounted + where T : struct + { + private readonly int lengthInBytes; + private readonly LifetimeGuard lifetimeGuard; + + public SharedArrayPoolBuffer(int lengthInElements) + { + this.lengthInBytes = lengthInElements * Unsafe.SizeOf(); + this.Array = ArrayPool.Shared.Rent(this.lengthInBytes); + this.lifetimeGuard = new LifetimeGuard(this.Array); + } + + public byte[]? Array { get; private set; } + + protected internal override void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes) + => this.lifetimeGuard.AttachAllocationTracking(allocator, lengthInBytes); + + protected override void DisposeCore(bool disposing) + { + if (this.Array == null) + { + return; + } + + this.lifetimeGuard.Dispose(); + this.Array = null; + } + + public override Span GetSpan() + { + this.CheckDisposed(); + return MemoryMarshal.Cast(this.Array.AsSpan(0, this.lengthInBytes)); + } + + protected override object GetPinnableObject() + { + this.CheckDisposed(); + return this.Array; + } + + public void AddRef() + { + this.CheckDisposed(); + this.lifetimeGuard.AddRef(); + } + + public void ReleaseRef() => this.lifetimeGuard.ReleaseRef(); + + [Conditional("DEBUG")] + [MemberNotNull(nameof(Array))] + private void CheckDisposed() => ObjectDisposedException.ThrowIf(this.Array == null, this.Array); + + private sealed class LifetimeGuard : RefCountedMemoryLifetimeGuard + { + private byte[]? array; + + public LifetimeGuard(byte[] array) => this.array = array; + + protected override void Release() + { + // If this is called by a finalizer, we will end storing the first array of this bucket + // on the thread local storage of the finalizer thread. + // This is not ideal, but subsequent leaks will end up returning arrays to per-cpu buckets, + // meaning likely a different bucket than it was rented from, + // but this is PROBABLY better than not returning the arrays at all. + ArrayPool.Shared.Return(this.array!); + this.array = null; + } + } + } +} diff --git a/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs b/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs new file mode 100644 index 0000000..2fd8787 --- /dev/null +++ b/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Memory.Internals { + internal partial class UniformUnmanagedMemoryPool + { + public UnmanagedBuffer CreateGuardedBuffer( + UnmanagedMemoryHandle handle, + int lengthInElements, + bool clear) + where T : struct + { + UnmanagedBuffer buffer = new(lengthInElements, new ReturnToPoolBufferLifetimeGuard(this, handle)); + if (clear) + { + buffer.Clear(); + } + + return buffer; + } + + public RefCountedMemoryLifetimeGuard CreateGroupLifetimeGuard(UnmanagedMemoryHandle[] handles) => new GroupLifetimeGuard(this, handles); + + private sealed class GroupLifetimeGuard : RefCountedMemoryLifetimeGuard + { + private readonly UniformUnmanagedMemoryPool pool; + private readonly UnmanagedMemoryHandle[] handles; + + public GroupLifetimeGuard(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle[] handles) + { + this.pool = pool; + this.handles = handles; + } + + protected override void Release() + { + if (!this.pool.Return(this.handles)) + { + foreach (UnmanagedMemoryHandle handle in this.handles) + { + handle.Free(); + } + } + } + } + + private sealed class ReturnToPoolBufferLifetimeGuard : UnmanagedBufferLifetimeGuard + { + private readonly UniformUnmanagedMemoryPool pool; + + public ReturnToPoolBufferLifetimeGuard(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle handle) + : base(handle) => + this.pool = pool; + + protected override void Release() + { + if (!this.pool.Return(this.Handle)) + { + this.Handle.Free(); + } + } + } + } +} diff --git a/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.cs b/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.cs new file mode 100644 index 0000000..b4ed4a9 --- /dev/null +++ b/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.cs @@ -0,0 +1,345 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; + +namespace SixLabors.ImageSharp.Memory.Internals { + // CriticalFinalizerObject: + // In case UniformUnmanagedMemoryPool is finalized, we prefer to run its finalizer after the guard finalizers, + // but we should not rely on this. + internal partial class UniformUnmanagedMemoryPool : System.Runtime.ConstrainedExecution.CriticalFinalizerObject + { + private static int minTrimPeriodMilliseconds = int.MaxValue; + private static readonly List> AllPools = []; + private static Timer? trimTimer; + + private static readonly Stopwatch Stopwatch = Stopwatch.StartNew(); + + private readonly TrimSettings trimSettings; + private readonly UnmanagedMemoryHandle[] buffers; + private int index; + private long lastTrimTimestamp; + private int finalized; + + public UniformUnmanagedMemoryPool(int bufferLength, int capacity) + : this(bufferLength, capacity, TrimSettings.Default) + { + } + + public UniformUnmanagedMemoryPool(int bufferLength, int capacity, TrimSettings trimSettings) + { + this.trimSettings = trimSettings; + this.Capacity = capacity; + this.BufferLength = bufferLength; + this.buffers = new UnmanagedMemoryHandle[capacity]; + + if (trimSettings.Enabled) + { + UpdateTimer(trimSettings, this); + Gen2GcCallback.Register(s => ((UniformUnmanagedMemoryPool)s).Trim(), this); + this.lastTrimTimestamp = Stopwatch.ElapsedMilliseconds; + } + } + + // We don't want UniformUnmanagedMemoryPool and MemoryAllocator to be IDisposable, + // since the types don't really match Disposable semantics. + // If a user wants to drop a MemoryAllocator after they finished using it, they should call allocator.ReleaseRetainedResources(), + // which normally should free the already returned (!) buffers. + // However in case if this doesn't happen, we need the retained memory to be freed by the finalizer. + ~UniformUnmanagedMemoryPool() + { + Interlocked.Exchange(ref this.finalized, 1); + this.TrimAll(this.buffers); + } + + public int BufferLength { get; } + + public int Capacity { get; } + + private bool Finalized => this.finalized == 1; + + /// + /// Rent a single buffer. If the pool is full, return . + /// + public UnmanagedMemoryHandle Rent() + { + UnmanagedMemoryHandle[] buffersLocal = this.buffers; + + // Avoid taking the lock if the pool is is over it's limit: + if (this.index == buffersLocal.Length || this.Finalized) + { + return UnmanagedMemoryHandle.NullHandle; + } + + UnmanagedMemoryHandle buffer; + lock (buffersLocal) + { + // Check again after taking the lock: + if (this.index == buffersLocal.Length || this.Finalized) + { + return UnmanagedMemoryHandle.NullHandle; + } + + buffer = buffersLocal[this.index]; + buffersLocal[this.index++] = default; + } + + if (buffer.IsInvalid) + { + buffer = UnmanagedMemoryHandle.Allocate(this.BufferLength); + } + + return buffer; + } + + /// + /// Rent buffers or return 'null' if the pool is full. + /// + public UnmanagedMemoryHandle[]? Rent(int bufferCount) + { + UnmanagedMemoryHandle[] buffersLocal = this.buffers; + + // Avoid taking the lock if the pool is is over it's limit: + if (this.index + bufferCount >= buffersLocal.Length + 1 || this.Finalized) + { + return null; + } + + UnmanagedMemoryHandle[] result; + lock (buffersLocal) + { + // Check again after taking the lock: + if (this.index + bufferCount >= buffersLocal.Length + 1 || this.Finalized) + { + return null; + } + + result = new UnmanagedMemoryHandle[bufferCount]; + for (int i = 0; i < bufferCount; i++) + { + result[i] = buffersLocal[this.index]; + buffersLocal[this.index++] = UnmanagedMemoryHandle.NullHandle; + } + } + + for (int i = 0; i < result.Length; i++) + { + if (result[i].IsInvalid) + { + result[i] = UnmanagedMemoryHandle.Allocate(this.BufferLength); + } + } + + return result; + } + + // The Return methods return false if and only if: + // (1) More buffers are returned than rented OR + // (2) The pool has been finalized. + // This is defensive programming, since neither of the cases should happen normally + // (case 1 would be a programming mistake in the library, case 2 should be prevented by the CriticalFinalizerObject contract), + // so we throw in Debug instead of returning false. + // In Release, the caller should Free() the handles if false is returned to avoid memory leaks. + public bool Return(UnmanagedMemoryHandle bufferHandle) + { + Guard.IsTrue(bufferHandle.IsValid, nameof(bufferHandle), "Returning NullHandle to the pool is not allowed."); + lock (this.buffers) + { + if (this.Finalized || this.index == 0) + { + this.DebugThrowInvalidReturn(); + return false; + } + + this.buffers[--this.index] = bufferHandle; + } + + return true; + } + + public bool Return(Span bufferHandles) + { + lock (this.buffers) + { + if (this.Finalized || this.index - bufferHandles.Length + 1 <= 0) + { + this.DebugThrowInvalidReturn(); + return false; + } + + for (int i = bufferHandles.Length - 1; i >= 0; i--) + { + ref UnmanagedMemoryHandle h = ref bufferHandles[i]; + Guard.IsTrue(h.IsValid, nameof(bufferHandles), "Returning NullHandle to the pool is not allowed."); + this.buffers[--this.index] = h; + } + } + + return true; + } + + public void Release() + { + lock (this.buffers) + { + for (int i = this.index; i < this.buffers.Length; i++) + { + ref UnmanagedMemoryHandle buffer = ref this.buffers[i]; + if (buffer.IsInvalid) + { + break; + } + + buffer.Free(); + } + } + } + + [Conditional("DEBUG")] + private void DebugThrowInvalidReturn() + { + if (this.Finalized) + { + throw new ObjectDisposedException( + nameof(UniformUnmanagedMemoryPool), + "Invalid handle return to the pool! The pool has been finalized."); + } + + throw new InvalidOperationException( + "Invalid handle return to the pool! Returning more buffers than rented."); + } + + private static void UpdateTimer(TrimSettings settings, UniformUnmanagedMemoryPool pool) + { + lock (AllPools) + { + AllPools.Add(new WeakReference(pool)); + + // Invoke the timer callback more frequently, than trimSettings.TrimPeriodMilliseconds. + // We are checking in the callback if enough time passed since the last trimming. If not, we do nothing. + int period = settings.TrimPeriodMilliseconds / 4; + if (trimTimer == null) + { + trimTimer = new Timer(_ => TimerCallback(), null, period, period); + } + else if (settings.TrimPeriodMilliseconds < minTrimPeriodMilliseconds) + { + trimTimer.Change(period, period); + } + + minTrimPeriodMilliseconds = Math.Min(minTrimPeriodMilliseconds, settings.TrimPeriodMilliseconds); + } + } + + private static void TimerCallback() + { + lock (AllPools) + { + // Remove lost references from the list: + for (int i = AllPools.Count - 1; i >= 0; i--) + { + if (!AllPools[i].TryGetTarget(out _)) + { + AllPools.RemoveAt(i); + } + } + + foreach (WeakReference weakPoolRef in AllPools) + { + if (weakPoolRef.TryGetTarget(out UniformUnmanagedMemoryPool? pool)) + { + pool.Trim(); + } + } + } + } + + private bool Trim() + { + if (this.Finalized) + { + return false; + } + + UnmanagedMemoryHandle[] buffersLocal = this.buffers; + + bool isHighPressure = this.IsHighMemoryPressure(); + + if (isHighPressure) + { + this.TrimAll(buffersLocal); + return true; + } + + long millisecondsSinceLastTrim = Stopwatch.ElapsedMilliseconds - this.lastTrimTimestamp; + if (millisecondsSinceLastTrim > this.trimSettings.TrimPeriodMilliseconds) + { + return this.TrimLowPressure(buffersLocal); + } + + return true; + } + + private void TrimAll(UnmanagedMemoryHandle[] buffersLocal) + { + lock (buffersLocal) + { + // Trim all: + for (int i = this.index; i < buffersLocal.Length && buffersLocal[i].IsValid; i++) + { + buffersLocal[i].Free(); + } + } + } + + private bool TrimLowPressure(UnmanagedMemoryHandle[] buffersLocal) + { + lock (buffersLocal) + { + // Count the buffers in the pool: + int retainedCount = 0; + for (int i = this.index; i < buffersLocal.Length && buffersLocal[i].IsValid; i++) + { + retainedCount++; + } + + // Trim 'trimRate' of 'retainedCount': + int trimCount = (int)Math.Ceiling(retainedCount * this.trimSettings.Rate); + int trimStart = this.index + retainedCount - 1; + int trimStop = this.index + retainedCount - trimCount; + for (int i = trimStart; i >= trimStop; i--) + { + buffersLocal[i].Free(); + } + + this.lastTrimTimestamp = Stopwatch.ElapsedMilliseconds; + } + + return true; + } + + private bool IsHighMemoryPressure() + { + GCMemoryInfo memoryInfo = GC.GetGCMemoryInfo(); + return memoryInfo.MemoryLoadBytes >= memoryInfo.HighMemoryLoadThresholdBytes * this.trimSettings.HighPressureThresholdRate; + } + + public class TrimSettings + { + // Trim half of the retained pool buffers every minute. + public int TrimPeriodMilliseconds { get; set; } = 60_000; + + public float Rate { get; set; } = 0.5f; + + // Be more strict about high pressure on 32 bit. + public unsafe float HighPressureThresholdRate { get; set; } = sizeof(IntPtr) == 8 ? 0.9f : 0.6f; + + public bool Enabled => this.Rate > 0; + + public static TrimSettings Default => new(); + } + } +} diff --git a/ImageSharp/Memory/Allocators/Internals/UnmanagedBufferLifetimeGuard.cs b/ImageSharp/Memory/Allocators/Internals/UnmanagedBufferLifetimeGuard.cs new file mode 100644 index 0000000..ab89721 --- /dev/null +++ b/ImageSharp/Memory/Allocators/Internals/UnmanagedBufferLifetimeGuard.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Memory.Internals { + /// + /// Defines a strategy for managing unmanaged memory ownership. + /// + internal abstract class UnmanagedBufferLifetimeGuard : RefCountedMemoryLifetimeGuard + { + private UnmanagedMemoryHandle handle; + + protected UnmanagedBufferLifetimeGuard(UnmanagedMemoryHandle handle) => this.handle = handle; + + public ref UnmanagedMemoryHandle Handle => ref this.handle; + + public sealed class FreeHandle : UnmanagedBufferLifetimeGuard + { + public FreeHandle(UnmanagedMemoryHandle handle) + : base(handle) + { + } + + protected override void Release() => this.Handle.Free(); + } + } +} diff --git a/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs b/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs new file mode 100644 index 0000000..86ce570 --- /dev/null +++ b/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace SixLabors.ImageSharp.Memory.Internals { + /// + /// Allocates and provides an implementation giving + /// access to unmanaged buffers allocated by . + /// + /// The element type. + internal sealed unsafe class UnmanagedBuffer : AllocationTrackedMemoryManager, IRefCounted + where T : struct + { + private readonly int lengthInElements; + + private readonly UnmanagedBufferLifetimeGuard lifetimeGuard; + + private int disposed; + + public UnmanagedBuffer(int lengthInElements, UnmanagedBufferLifetimeGuard lifetimeGuard) + { + DebugGuard.NotNull(lifetimeGuard, nameof(lifetimeGuard)); + + this.lengthInElements = lengthInElements; + this.lifetimeGuard = lifetimeGuard; + } + + public void* Pointer => this.lifetimeGuard.Handle.Pointer; + + protected internal override void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes) + => this.lifetimeGuard.AttachAllocationTracking(allocator, lengthInBytes); + + public override Span GetSpan() + { + DebugGuard.NotDisposed(this.disposed == 1, this.GetType().Name); + DebugGuard.NotDisposed(this.lifetimeGuard.IsDisposed, this.lifetimeGuard.GetType().Name); + return new Span(this.Pointer, this.lengthInElements); + } + + /// + public override MemoryHandle Pin(int elementIndex = 0) + { + DebugGuard.NotDisposed(this.disposed == 1, this.GetType().Name); + DebugGuard.NotDisposed(this.lifetimeGuard.IsDisposed, this.lifetimeGuard.GetType().Name); + + // Will be released in Unpin + this.lifetimeGuard.AddRef(); + + void* pbData = Unsafe.Add(this.Pointer, elementIndex); + return new MemoryHandle(pbData, pinnable: this); + } + + /// + protected override void DisposeCore(bool disposing) + { + DebugGuard.IsTrue(disposing, nameof(disposing), "Unmanaged buffers should not have finalizer!"); + + if (Interlocked.Exchange(ref this.disposed, 1) == 1) + { + // Already disposed + return; + } + + this.lifetimeGuard.Dispose(); + } + + /// + public override void Unpin() => this.lifetimeGuard.ReleaseRef(); + + public void AddRef() => this.lifetimeGuard.AddRef(); + + public void ReleaseRef() => this.lifetimeGuard.ReleaseRef(); + + public static UnmanagedBuffer Allocate(int lengthInElements) => + new(lengthInElements, new UnmanagedBufferLifetimeGuard.FreeHandle(UnmanagedMemoryHandle.Allocate(lengthInElements * Unsafe.SizeOf()))); + } +} diff --git a/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs b/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs new file mode 100644 index 0000000..dbae064 --- /dev/null +++ b/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs @@ -0,0 +1,131 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.InteropServices; +using System.Threading; + +namespace SixLabors.ImageSharp.Memory.Internals { + /// + /// Encapsulates the functionality around allocating and releasing unmanaged memory. NOT a . + /// + internal struct UnmanagedMemoryHandle : IEquatable + { + // Number of allocation re-attempts when detecting OutOfMemoryException. + private const int MaxAllocationAttempts = 10; + + // Track allocations for testing purposes: + private static int totalOutstandingHandles; + + private static long totalOomRetries; + + // A Monitor to wait/signal when we are low on memory. + private static object? lowMemoryMonitor; + + public static readonly UnmanagedMemoryHandle NullHandle; + + private IntPtr handle; + private int lengthInBytes; + + private UnmanagedMemoryHandle(IntPtr handle, int lengthInBytes) + { + this.handle = handle; + this.lengthInBytes = lengthInBytes; + + if (lengthInBytes > 0) + { + GC.AddMemoryPressure(lengthInBytes); + } + + Interlocked.Increment(ref totalOutstandingHandles); + } + + public readonly IntPtr Handle => this.handle; + + public readonly bool IsInvalid => this.Handle == IntPtr.Zero; + + public readonly bool IsValid => this.Handle != IntPtr.Zero; + + public readonly unsafe void* Pointer => (void*)this.Handle; + + /// + /// Gets the total outstanding handle allocations for testing purposes. + /// + internal static int TotalOutstandingHandles => totalOutstandingHandles; + + /// + /// Gets the total number -s retried. + /// + internal static long TotalOomRetries => totalOomRetries; + + public static bool operator ==(UnmanagedMemoryHandle a, UnmanagedMemoryHandle b) => a.Equals(b); + + public static bool operator !=(UnmanagedMemoryHandle a, UnmanagedMemoryHandle b) => !a.Equals(b); + + public static UnmanagedMemoryHandle Allocate(int lengthInBytes) + { + IntPtr handle = AllocateHandle(lengthInBytes); + return new UnmanagedMemoryHandle(handle, lengthInBytes); + } + + private static IntPtr AllocateHandle(int lengthInBytes) + { + int counter = 0; + IntPtr handle = IntPtr.Zero; + while (handle == IntPtr.Zero) + { + try + { + handle = Marshal.AllocHGlobal(lengthInBytes); + } + catch (OutOfMemoryException) when (counter < MaxAllocationAttempts) + { + // We are low on memory, but expect some memory to be freed soon. + // Block the thread & retry to avoid OOM. + counter++; + Interlocked.Increment(ref totalOomRetries); + + Interlocked.CompareExchange(ref lowMemoryMonitor, new object(), null); + Monitor.Enter(lowMemoryMonitor); + Monitor.Wait(lowMemoryMonitor, millisecondsTimeout: 1); + Monitor.Exit(lowMemoryMonitor); + } + } + + return handle; + } + + public void Free() + { + IntPtr h = Interlocked.Exchange(ref this.handle, IntPtr.Zero); + + if (h == IntPtr.Zero) + { + return; + } + + Marshal.FreeHGlobal(h); + Interlocked.Decrement(ref totalOutstandingHandles); + if (this.lengthInBytes > 0) + { + GC.RemoveMemoryPressure(this.lengthInBytes); + } + + if (Volatile.Read(ref lowMemoryMonitor) != null) + { + // We are low on memory. Signal all threads waiting in AllocateHandle(). + Monitor.Enter(lowMemoryMonitor!); + Monitor.PulseAll(lowMemoryMonitor!); + Monitor.Exit(lowMemoryMonitor!); + } + + this.lengthInBytes = 0; + } + + public readonly bool Equals(UnmanagedMemoryHandle other) => this.handle.Equals(other.handle); + + public override readonly bool Equals(object? obj) => obj is UnmanagedMemoryHandle other && this.Equals(other); + + public override readonly int GetHashCode() => this.handle.GetHashCode(); + } +} diff --git a/ImageSharp/Memory/Allocators/MemoryAllocator.cs b/ImageSharp/Memory/Allocators/MemoryAllocator.cs new file mode 100644 index 0000000..2e21738 --- /dev/null +++ b/ImageSharp/Memory/Allocators/MemoryAllocator.cs @@ -0,0 +1,296 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace SixLabors.ImageSharp.Memory { + /// + /// Memory managers are used to allocate memory for image processing operations. + /// + public abstract class MemoryAllocator + { + private const int OneGigabyte = 1 << 30; + private long accumulativeAllocatedBytes; + + /// + /// Gets the default platform-specific global instance that + /// serves as the default value for . + /// + /// This is a get-only property, + /// you should set 's + /// to change the default allocator used by and it's operations. + /// + public static MemoryAllocator Default { get; } = Create(); + + /// + /// Gets the maximum number of bytes that can be allocated by a memory group. + /// + /// + /// The allocation limit is determined by the process architecture: 4 GB for 64-bit processes and + /// 1 GB for 32-bit processes. + /// + internal long MemoryGroupAllocationLimitBytes { get; private protected set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte; + + /// + /// Gets the maximum accumulative size, in bytes, of all active allocations made through this allocator instance. + /// + /// + /// Defaults to , effectively imposing no limit on the accumulative total. + /// When set, this provides a safeguard against excessive memory consumption by capping the combined size of + /// outstanding allocations issued by this instance.
+ /// When the accumulative size of active allocations exceeds this limit, an will be thrown to + /// prevent further allocations and signal that the limit has been breached. + ///
+ internal long AccumulativeAllocationLimitBytes { get; private protected set; } = long.MaxValue; + + /// + /// Gets the maximum size, in bytes, that can be allocated for a single buffer. + /// + /// + /// The single buffer allocation limit is set to 1 GB by default. + /// + internal int SingleBufferAllocationLimitBytes { get; private protected set; } = OneGigabyte; + + /// + /// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes. + /// + /// The length of the largest contiguous buffer that can be handled by this allocator instance. + protected internal abstract int GetBufferCapacityInBytes(); + + /// + /// Creates a default instance of a optimized for the executing platform. + /// + /// The . + public static MemoryAllocator Create() => Create(default); + + /// + /// Creates the default using the provided options. + /// + /// The . + /// The . + public static MemoryAllocator Create(MemoryAllocatorOptions options) + { + UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(options.MaximumPoolSizeMegabytes); + allocator.ApplyOptions(options); + return allocator; + } + + /// + /// Applies the supplied to this instance. + /// + /// The options to apply. Properties left as are ignored. + private protected void ApplyOptions(MemoryAllocatorOptions options) + { + if (options.AllocationLimitMegabytes.HasValue) + { + this.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024L * 1024L; + this.SingleBufferAllocationLimitBytes = (int)Math.Min(this.SingleBufferAllocationLimitBytes, this.MemoryGroupAllocationLimitBytes); + } + + if (options.AccumulativeAllocationLimitMegabytes.HasValue) + { + this.AccumulativeAllocationLimitBytes = options.AccumulativeAllocationLimitMegabytes.Value * 1024L * 1024L; + } + } + + /// + /// Allocates an , holding a of length . + /// + /// Type of the data stored in the buffer. + /// Size of the buffer to allocate. + /// The allocation options. + /// A buffer of values of type . + /// When length is negative or over the capacity of the allocator. + public IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) + where T : struct + { + long lengthInBytes = this.GetValidatedAllocationLengthInBytes(length); + bool shouldTrack = this.AccumulativeAllocationLimitBytes != long.MaxValue && lengthInBytes != 0; + if (shouldTrack) + { + this.ReserveAllocation(lengthInBytes); + } + + try + { + AllocationTrackedMemoryManager owner = this.AllocateCore(length, options); + if (shouldTrack) + { + owner.AttachAllocationTracking(this, lengthInBytes); + } + + return owner; + } + catch + { + if (shouldTrack) + { + this.ReleaseAccumulatedBytes(lengthInBytes); + } + + throw; + } + } + + /// + /// Allocates a tracked memory owner for . + /// + /// Type of the data stored in the buffer. + /// Size of the buffer to allocate. + /// The allocation options. + /// A tracked memory owner of values of type . + /// + /// Implementations should only allocate and initialize the concrete owner. The base allocator + /// reserves bytes, attaches tracking to the returned owner, and releases the reservation if allocation fails. + /// + protected abstract AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None) + where T : struct; + + /// + /// Releases all retained resources not being in use. + /// Eg: by resetting array pools and letting GC to free the arrays. + /// + /// + /// This does not dispose active allocations; callers are responsible for disposing all + /// instances to release memory. + /// + public virtual void ReleaseRetainedResources() + { + } + + /// + /// Allocates a . + /// + /// The type of element to allocate. + /// The total length of the buffer. + /// The expected alignment (eg. to make sure image rows fit into single buffers). + /// The . + /// A new . + /// Thrown when 'blockAlignment' converted to bytes is greater than the buffer capacity of the allocator. + internal MemoryGroup AllocateGroup( + long totalLength, + int bufferAlignment, + AllocationOptions options = AllocationOptions.None) + where T : struct + { + if (totalLength < 0) + { + InvalidMemoryOperationException.ThrowNegativeAllocationException(totalLength); + } + + ulong totalLengthInBytes = (ulong)totalLength * (ulong)Unsafe.SizeOf(); + if (totalLengthInBytes > (ulong)this.MemoryGroupAllocationLimitBytes) + { + InvalidMemoryOperationException.ThrowAllocationOverLimitException(totalLengthInBytes, this.MemoryGroupAllocationLimitBytes); + } + + long totalLengthInBytesLong = (long)totalLengthInBytes; + bool shouldTrack = this.AccumulativeAllocationLimitBytes != long.MaxValue && totalLengthInBytesLong != 0; + if (shouldTrack) + { + this.ReserveAllocation(totalLengthInBytesLong); + } + + try + { + MemoryGroup group = this.AllocateGroupCore(totalLength, totalLengthInBytesLong, bufferAlignment, options); + if (shouldTrack) + { + group.AttachAllocationTracking(this, totalLengthInBytesLong); + } + + return group; + } + catch + { + if (shouldTrack) + { + this.ReleaseAccumulatedBytes(totalLengthInBytesLong); + } + + throw; + } + } + + internal virtual MemoryGroup AllocateGroupCore(long totalLengthInElements, long totalLengthInBytes, int bufferAlignment, AllocationOptions options) + where T : struct + => MemoryGroup.Allocate(this, totalLengthInElements, bufferAlignment, options); + + /// + /// Allocates a single segment for construction. + /// + /// Type of the data stored in the buffer. + /// Size of the segment to allocate. + /// The allocation options. + /// A segment owner for the requested buffer length. + /// + /// The default implementation validates the segment size then calls + /// directly so group construction can reserve and release the total allocation once. + /// + internal virtual IMemoryOwner AllocateGroupBuffer(int length, AllocationOptions options = AllocationOptions.None) + where T : struct + { + _ = this.GetValidatedAllocationLengthInBytes(length); + return this.AllocateCore(length, options); + } + + /// + /// Returns the validated allocation length in bytes. + /// + /// Type of the data stored in the buffer. + /// Size of the buffer to allocate. + /// The allocation length in bytes. + private long GetValidatedAllocationLengthInBytes(int length) + where T : struct + { + if (length < 0) + { + InvalidMemoryOperationException.ThrowNegativeAllocationException(length); + } + + ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf(); + if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes) + { + InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes); + } + + return (long)lengthInBytes; + } + + /// + /// Reserves accumulative allocation bytes before creating the underlying buffer. + /// + /// The number of bytes to reserve. + private void ReserveAllocation(long lengthInBytes) + { + if (lengthInBytes <= 0) + { + return; + } + + long total = Interlocked.Add(ref this.accumulativeAllocatedBytes, lengthInBytes); + if (total > this.AccumulativeAllocationLimitBytes) + { + _ = Interlocked.Add(ref this.accumulativeAllocatedBytes, -lengthInBytes); + InvalidMemoryOperationException.ThrowAccumulativeAllocationOverLimitException(lengthInBytes, total, this.AccumulativeAllocationLimitBytes); + } + } + + /// + /// Releases accumulative allocation bytes previously tracked by this allocator. + /// + /// The number of bytes to release. + internal void ReleaseAccumulatedBytes(long lengthInBytes) + { + if (lengthInBytes <= 0) + { + return; + } + + _ = Interlocked.Add(ref this.accumulativeAllocatedBytes, -lengthInBytes); + } + } +} diff --git a/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs b/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs new file mode 100644 index 0000000..5ad31dc --- /dev/null +++ b/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Memory { + /// + /// Defines options for creating the default . + /// + public struct MemoryAllocatorOptions + { + private int? maximumPoolSizeMegabytes; + private int? allocationLimitMegabytes; + private int? accumulativeAllocationLimitMegabytes; + + /// + /// Gets or sets a value defining the maximum size of the 's internal memory pool + /// in Megabytes. means platform default. + /// + public int? MaximumPoolSizeMegabytes + { + readonly get => this.maximumPoolSizeMegabytes; + set + { + if (value.HasValue) + { + Guard.MustBeGreaterThanOrEqualTo(value.Value, 0, nameof(this.MaximumPoolSizeMegabytes)); + } + + this.maximumPoolSizeMegabytes = value; + } + } + + /// + /// Gets or sets a value defining the maximum (discontiguous) buffer size that can be allocated by the allocator in Megabytes. + /// means platform default: 1GB on 32-bit processes, 4GB on 64-bit processes. + /// + public int? AllocationLimitMegabytes + { + readonly get => this.allocationLimitMegabytes; + set + { + if (value.HasValue) + { + Guard.MustBeGreaterThan(value.Value, 0, nameof(this.AllocationLimitMegabytes)); + if (this.AccumulativeAllocationLimitMegabytes.HasValue) + { + Guard.MustBeLessThanOrEqualTo( + value.Value, + this.AccumulativeAllocationLimitMegabytes.Value, + nameof(this.AllocationLimitMegabytes)); + } + } + + this.allocationLimitMegabytes = value; + } + } + + /// + /// Gets or sets a value defining the maximum accumulative size, in Megabytes, of all active allocations made + /// through the created instance. + /// (the default) imposes no limit on the accumulative total. + /// + public int? AccumulativeAllocationLimitMegabytes + { + readonly get => this.accumulativeAllocationLimitMegabytes; + set + { + if (value.HasValue) + { + Guard.MustBeGreaterThan(value.Value, 0, nameof(this.AccumulativeAllocationLimitMegabytes)); + if (this.AllocationLimitMegabytes.HasValue) + { + Guard.MustBeGreaterThanOrEqualTo( + value.Value, + this.AllocationLimitMegabytes.Value, + nameof(this.AccumulativeAllocationLimitMegabytes)); + } + } + + this.accumulativeAllocationLimitMegabytes = value; + } + } + } +} diff --git a/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs b/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs new file mode 100644 index 0000000..90f5e12 --- /dev/null +++ b/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory.Internals; + +namespace SixLabors.ImageSharp.Memory { + /// + /// Implements by newing up managed arrays on every allocation request. + /// + public sealed class SimpleGcMemoryAllocator : MemoryAllocator + { + /// + /// Initializes a new instance of the class with default limits. + /// + public SimpleGcMemoryAllocator() + : this(default) + { + } + + /// + /// Initializes a new instance of the class with custom limits. + /// + /// The to apply. + public SimpleGcMemoryAllocator(MemoryAllocatorOptions options) => this.ApplyOptions(options); + + /// + protected internal override int GetBufferCapacityInBytes() => int.MaxValue; + + /// + protected override AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None) + => new BasicArrayBuffer(new T[length]); + } +} diff --git a/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs b/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs new file mode 100644 index 0000000..a438a42 --- /dev/null +++ b/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs @@ -0,0 +1,162 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory.Internals; + +namespace SixLabors.ImageSharp.Memory { + internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocator + { + private const int OneMegabyte = 1 << 20; + + // 4 MB seemed to perform slightly better in benchmarks than 2MB or higher values: + private const int DefaultContiguousPoolBlockSizeBytes = 4 * OneMegabyte; + private const int DefaultNonPoolBlockSizeBytes = 32 * OneMegabyte; + private readonly int sharedArrayPoolThresholdInBytes; + private readonly int poolBufferSizeInBytes; + private readonly int poolCapacity; + private readonly UniformUnmanagedMemoryPool.TrimSettings trimSettings; + + private readonly UniformUnmanagedMemoryPool pool; + private readonly UnmanagedMemoryAllocator nonPoolAllocator; + + public UniformUnmanagedMemoryPoolMemoryAllocator(int? maxPoolSizeMegabytes) + : this( + DefaultContiguousPoolBlockSizeBytes, + maxPoolSizeMegabytes.HasValue ? (long)maxPoolSizeMegabytes.Value * OneMegabyte : GetDefaultMaxPoolSizeBytes(), + DefaultNonPoolBlockSizeBytes) + { + } + + public UniformUnmanagedMemoryPoolMemoryAllocator( + int poolBufferSizeInBytes, + long maxPoolSizeInBytes, + int unmanagedBufferSizeInBytes) + : this( + OneMegabyte, + poolBufferSizeInBytes, + maxPoolSizeInBytes, + unmanagedBufferSizeInBytes) + { + } + + internal UniformUnmanagedMemoryPoolMemoryAllocator( + int sharedArrayPoolThresholdInBytes, + int poolBufferSizeInBytes, + long maxPoolSizeInBytes, + int unmanagedBufferSizeInBytes) + : this( + sharedArrayPoolThresholdInBytes, + poolBufferSizeInBytes, + maxPoolSizeInBytes, + unmanagedBufferSizeInBytes, + UniformUnmanagedMemoryPool.TrimSettings.Default) + { + } + + internal UniformUnmanagedMemoryPoolMemoryAllocator( + int sharedArrayPoolThresholdInBytes, + int poolBufferSizeInBytes, + long maxPoolSizeInBytes, + int unmanagedBufferSizeInBytes, + UniformUnmanagedMemoryPool.TrimSettings trimSettings) + { + this.sharedArrayPoolThresholdInBytes = sharedArrayPoolThresholdInBytes; + this.poolBufferSizeInBytes = poolBufferSizeInBytes; + this.poolCapacity = (int)(maxPoolSizeInBytes / poolBufferSizeInBytes); + this.trimSettings = trimSettings; + this.pool = new UniformUnmanagedMemoryPool(this.poolBufferSizeInBytes, this.poolCapacity, this.trimSettings); + this.nonPoolAllocator = new UnmanagedMemoryAllocator(unmanagedBufferSizeInBytes); + } + + internal UniformUnmanagedMemoryPoolMemoryAllocator( + int sharedArrayPoolThresholdInBytes, + int poolBufferSizeInBytes, + long maxPoolSizeInBytes, + int unmanagedBufferSizeInBytes, + MemoryAllocatorOptions options) + : this(sharedArrayPoolThresholdInBytes, poolBufferSizeInBytes, maxPoolSizeInBytes, unmanagedBufferSizeInBytes) + => this.ApplyOptions(options); + + /// + protected internal override int GetBufferCapacityInBytes() => this.poolBufferSizeInBytes; + + /// + protected override AllocationTrackedMemoryManager AllocateCore( + int length, + AllocationOptions options = AllocationOptions.None) + { + int lengthInBytes = length * Unsafe.SizeOf(); + if (lengthInBytes <= this.sharedArrayPoolThresholdInBytes) + { + SharedArrayPoolBuffer buffer = new(length); + if (options.Has(AllocationOptions.Clean)) + { + buffer.GetSpan().Clear(); + } + + return buffer; + } + + if (lengthInBytes <= this.poolBufferSizeInBytes) + { + UnmanagedMemoryHandle mem = this.pool.Rent(); + if (mem.IsValid) + { + return this.pool.CreateGuardedBuffer(mem, length, options.Has(AllocationOptions.Clean)); + } + } + + return UnmanagedMemoryAllocator.AllocateBuffer(length, options); + } + + /// + internal override MemoryGroup AllocateGroupCore( + long totalLengthInElements, + long totalLengthInBytes, + int bufferAlignment, + AllocationOptions options = AllocationOptions.None) + { + if (totalLengthInBytes <= this.sharedArrayPoolThresholdInBytes) + { + SharedArrayPoolBuffer buffer = new((int)totalLengthInElements); + return MemoryGroup.CreateContiguous(buffer, options.Has(AllocationOptions.Clean)); + } + + if (totalLengthInBytes <= this.poolBufferSizeInBytes) + { + // Optimized path renting single array from the pool + UnmanagedMemoryHandle mem = this.pool.Rent(); + if (mem.IsValid) + { + UnmanagedBuffer buffer = this.pool.CreateGuardedBuffer(mem, (int)totalLengthInElements, options.Has(AllocationOptions.Clean)); + return MemoryGroup.CreateContiguous(buffer, options.Has(AllocationOptions.Clean)); + } + } + + // Attempt to rent the whole group from the pool, allocate a group of unmanaged buffers if the attempt fails: + if (MemoryGroup.TryAllocate(this.pool, totalLengthInElements, bufferAlignment, options, out MemoryGroup? poolGroup)) + { + return poolGroup; + } + + return MemoryGroup.Allocate(this.nonPoolAllocator, totalLengthInElements, bufferAlignment, options); + } + + public override void ReleaseRetainedResources() => this.pool.Release(); + + private static long GetDefaultMaxPoolSizeBytes() + { + if (Environment.Is64BitProcess) + { + // On 64 bit set the pool size to a portion of the total available memory. + GCMemoryInfo info = GC.GetGCMemoryInfo(); + return info.TotalAvailableMemoryBytes / 8; + } + + // Stick to a conservative value of 128 Megabytes on 32 bit. + return 128 * OneMegabyte; + } + } +} diff --git a/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs b/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs new file mode 100644 index 0000000..d8418b2 --- /dev/null +++ b/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory.Internals; + +namespace SixLabors.ImageSharp.Memory { + /// + /// A implementation that allocates memory on the unmanaged heap + /// without any pooling. + /// + internal class UnmanagedMemoryAllocator : MemoryAllocator + { + private readonly int bufferCapacityInBytes; + + public UnmanagedMemoryAllocator(int bufferCapacityInBytes) => this.bufferCapacityInBytes = bufferCapacityInBytes; + + protected internal override int GetBufferCapacityInBytes() => this.bufferCapacityInBytes; + + protected override AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None) + where T : struct + => AllocateBuffer(length, options); + + // The pooled allocator uses this internal entry point when it needs a raw unmanaged owner without + // nesting another allocator-level reservation cycle around the fallback allocation. + internal static UnmanagedBuffer AllocateBuffer(int length, AllocationOptions options = AllocationOptions.None) + where T : struct + { + UnmanagedBuffer buffer = UnmanagedBuffer.Allocate(length); + if (options.Has(AllocationOptions.Clean)) + { + buffer.GetSpan().Clear(); + } + + return buffer; + } + } +} diff --git a/ImageSharp/Memory/Buffer2DExtensions.cs b/ImageSharp/Memory/Buffer2DExtensions.cs new file mode 100644 index 0000000..4ced29c --- /dev/null +++ b/ImageSharp/Memory/Buffer2DExtensions.cs @@ -0,0 +1,157 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Memory { + /// + /// Defines extension methods for . + /// + public static class Buffer2DExtensions + { + /// + /// Gets the backing . + /// + /// The buffer. + /// The element type. + /// The MemoryGroup. + public static IMemoryGroup GetMemoryGroup(this Buffer2D buffer) + where T : struct + { + Guard.NotNull(buffer, nameof(buffer)); + return buffer.FastMemoryGroup.View; + } + + /// + /// Performs a deep clone of the buffer covering the specified . + /// + /// The element type. + /// The source buffer. + /// The configuration. + /// The rectangle to clone. + /// The . + internal static Buffer2D CloneRegion(this Buffer2D source, Configuration configuration, Rectangle rectangle) + where T : unmanaged + { + Buffer2D buffer = configuration.MemoryAllocator.Allocate2D( + rectangle.Width, + rectangle.Height, + configuration.PreferContiguousImageBuffers); + + // Optimization for when the size of the area is the same as the buffer size. + Buffer2DRegion sourceRegion = source.GetRegion(rectangle); + if (sourceRegion.IsFullBufferArea) + { + sourceRegion.Buffer.CopyTo(buffer); + } + else + { + for (int y = 0; y < rectangle.Height; y++) + { + sourceRegion.DangerousGetRowSpan(y).CopyTo(buffer.DangerousGetRowSpan(y)); + } + } + + return buffer; + } + + /// + /// TODO: Does not work with multi-buffer groups, should be specific to Resize. + /// Copy columns of in-place, + /// from positions starting at to positions at . + /// + /// The element type. + /// The . + /// The source column index. + /// The destination column index. + /// The number of columns to copy. + internal static unsafe void DangerousCopyColumns( + this Buffer2D buffer, + int sourceIndex, + int destinationIndex, + int columnCount) + where T : struct + { + DebugGuard.NotNull(buffer, nameof(buffer)); + DebugGuard.MustBeGreaterThanOrEqualTo(sourceIndex, 0, nameof(sourceIndex)); + DebugGuard.MustBeGreaterThanOrEqualTo(destinationIndex, 0, nameof(sourceIndex)); + CheckColumnRegionsDoNotOverlap(buffer, sourceIndex, destinationIndex, columnCount); + + int elementSize = Unsafe.SizeOf(); + int rowByteStride = buffer.RowStride * elementSize; + int sOffset = sourceIndex * elementSize; + int dOffset = destinationIndex * elementSize; + long count = columnCount * elementSize; + + Span span = MemoryMarshal.AsBytes(buffer.DangerousGetSingleMemory().Span); + + fixed (byte* ptr = span) + { + byte* basePtr = ptr; + for (int y = 0; y < buffer.Height; y++) + { + byte* sPtr = basePtr + sOffset; + byte* dPtr = basePtr + dOffset; + + Buffer.MemoryCopy(sPtr, dPtr, count, count); + + basePtr += rowByteStride; + } + } + } + + /// + /// Return a to the subregion represented by . + /// + /// The element type + /// The + /// The rectangle subregion + /// The + public static Buffer2DRegion GetRegion(this Buffer2D buffer, Rectangle rectangle) + where T : unmanaged => + new(buffer, rectangle); + + /// + /// Return a to the specified area of . + /// + /// The element type. + /// The . + /// The X coordinate of the region. + /// The Y coordinate of the region. + /// The region width. + /// The region height. + /// The . + public static Buffer2DRegion GetRegion(this Buffer2D buffer, int x, int y, int width, int height) + where T : unmanaged => + new(buffer, new Rectangle(x, y, width, height)); + + /// + /// Return a to the whole area of . + /// + /// The element type + /// The + /// The + public static Buffer2DRegion GetRegion(this Buffer2D buffer) + where T : unmanaged => + new(buffer); + + [Conditional("DEBUG")] + private static void CheckColumnRegionsDoNotOverlap( + Buffer2D buffer, + int sourceIndex, + int destIndex, + int columnCount) + where T : struct + { + int minIndex = Math.Min(sourceIndex, destIndex); + int maxIndex = Math.Max(sourceIndex, destIndex); + if (maxIndex < minIndex + columnCount || maxIndex > buffer.Width - columnCount) + { + throw new InvalidOperationException("Column regions should not overlap!"); + } + } + } +} diff --git a/ImageSharp/Memory/Buffer2DRegion{T}.cs b/ImageSharp/Memory/Buffer2DRegion{T}.cs new file mode 100644 index 0000000..6a2d0fe --- /dev/null +++ b/ImageSharp/Memory/Buffer2DRegion{T}.cs @@ -0,0 +1,182 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Memory { + /// + /// Represents a rectangular region inside a 2D memory buffer (). + /// + /// The element type. + public readonly struct Buffer2DRegion + where T : unmanaged + { + /// + /// Initializes a new instance of the struct. + /// + /// The . + /// The defining a rectangular area within the buffer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Buffer2DRegion(Buffer2D buffer, Rectangle bounds) + { + DebugGuard.MustBeGreaterThanOrEqualTo(bounds.X, 0, nameof(bounds)); + DebugGuard.MustBeGreaterThanOrEqualTo(bounds.Y, 0, nameof(bounds)); + DebugGuard.MustBeLessThanOrEqualTo(bounds.Width, buffer.Width, nameof(bounds)); + DebugGuard.MustBeLessThanOrEqualTo(bounds.Height, buffer.Height, nameof(bounds)); + + this.Buffer = buffer; + this.Bounds = bounds; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Buffer2DRegion(Buffer2D buffer) + : this(buffer, buffer.Bounds) + { + } + + /// + /// Gets the being pointed by this instance. + /// + public Buffer2D Buffer { get; } + + /// + /// Gets the width + /// + public int Width => this.Bounds.Width; + + /// + /// Gets the height + /// + public int Height => this.Bounds.Height; + + /// + /// Gets the number of elements between row starts in . + /// + public int Stride => this.Buffer.RowStride; + + /// + /// Gets the size of the area. + /// + public Size Size => this.Bounds.Size; + + /// + /// Gets the rectangle specifying the boundaries of the area in . + /// + public Rectangle Bounds { get; } + + /// + /// Gets a value indicating whether the area refers to the entire + /// + internal bool IsFullBufferArea => this.Size == this.Buffer.Size; + + /// + /// Gets or sets a value at the given index. + /// + /// The position inside a row + /// The row index + /// The reference to the value + internal ref T this[int x, int y] => ref this.Buffer[x + this.Bounds.X, y + this.Bounds.Y]; + + /// + /// Gets a span to row 'y' inside this area. + /// + /// The row index + /// The span + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span DangerousGetRowSpan(int y) + { + int yy = this.Bounds.Y + y; + int xx = this.Bounds.X; + int width = this.Bounds.Width; + + return this.Buffer.DangerousGetRowSpan(yy).Slice(xx, width); + } + + /// + /// Returns a subregion as . (Similar to .) + /// + /// The x index at the subregion origin. + /// The y index at the subregion origin. + /// The desired width of the subregion. + /// The desired height of the subregion. + /// The subregion + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Buffer2DRegion GetSubRegion(int x, int y, int width, int height) + { + Rectangle rectangle = new(x, y, width, height); + return this.GetSubRegion(rectangle); + } + + /// + /// Returns a subregion as . (Similar to .) + /// + /// The specifying the boundaries of the subregion + /// The subregion + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Buffer2DRegion GetSubRegion(Rectangle rectangle) + { + DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, this.Bounds.Width, nameof(rectangle)); + DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, this.Bounds.Height, nameof(rectangle)); + + int x = this.Bounds.X + rectangle.X; + int y = this.Bounds.Y + rectangle.Y; + rectangle = new Rectangle(x, y, rectangle.Width, rectangle.Height); + return new Buffer2DRegion(this.Buffer, rectangle); + } + + /// + /// Gets a reference to the [0,0] element. + /// + /// The reference to the [0,0] element + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ref T GetReferenceToOrigin() + { + int y = this.Bounds.Y; + int x = this.Bounds.X; + return ref this.Buffer.DangerousGetRowSpan(y)[x]; + } + + /// + /// Clears the contents of this . + /// + internal void Clear() + { + // Optimization for when the size of the area is the same as the buffer size. + if (this.IsFullBufferArea && this.Buffer.RowStride == this.Buffer.Width) + { + this.Buffer.Clear(default); + return; + } + + for (int y = 0; y < this.Bounds.Height; y++) + { + Span row = this.DangerousGetRowSpan(y); + row.Clear(); + } + } + + /// + /// Fills the elements of this with the specified value. + /// + /// The value to assign to each element of the region. + internal void Fill(T value) + { + // Optimization for when the size of the area is the same as the buffer size. + if (this.IsFullBufferArea && this.Buffer.RowStride == this.Buffer.Width) + { + this.Buffer.Clear(value); + return; + } + + for (int y = 0; y < this.Bounds.Height; y++) + { + Span row = this.DangerousGetRowSpan(y); + row.Fill(value); + } + } + } +} diff --git a/ImageSharp/Memory/Buffer2D{T}.cs b/ImageSharp/Memory/Buffer2D{T}.cs new file mode 100644 index 0000000..226dd64 --- /dev/null +++ b/ImageSharp/Memory/Buffer2D{T}.cs @@ -0,0 +1,429 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Memory { + /// + /// Represents a buffer of value type objects + /// interpreted as a 2D region of x elements. + /// + /// The value type. + public sealed class Buffer2D : IDisposable + where T : struct + { + /// + /// Initializes a new instance of the class. + /// + /// The to wrap. + /// The number of elements in a row. + /// The number of rows. + internal Buffer2D(MemoryGroup memoryGroup, int width, int height) + : this(memoryGroup, width, height, width) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The to wrap. + /// The number of elements in a row. + /// The number of rows. + /// The number of elements between row starts. + internal Buffer2D(MemoryGroup memoryGroup, int width, int height, int rowStride) + { + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + Guard.MustBeGreaterThanOrEqualTo(rowStride, width, nameof(rowStride)); + + this.FastMemoryGroup = memoryGroup; + this.Size = new Size(width, height); + this.RowStride = rowStride; + } + + /// + /// Gets the width. + /// + public int Width => this.Size.Width; + + /// + /// Gets the height. + /// + public int Height => this.Size.Height; + + /// + /// Gets the size of the buffer. + /// + public Size Size { get; private set; } + + /// + /// Gets the bounds of the buffer. + /// + /// The + public Rectangle Bounds => new(0, 0, this.Width, this.Height); + + /// + /// Gets the number of elements between row starts in the backing memory. + /// + public int RowStride { get; private set; } + + /// + /// Gets the backing . + /// + /// The MemoryGroup. + public IMemoryGroup MemoryGroup => this.FastMemoryGroup.View; + + /// + /// Gets the backing without the view abstraction. + /// + /// + /// This property has been kept internal intentionally. + /// It's public counterpart is , + /// which only exposes the view of the MemoryGroup. + /// + internal MemoryGroup FastMemoryGroup { get; private set; } + + internal bool IsDisposed { get; private set; } + + /// + /// Gets a reference to the element at the specified position. + /// + /// The x coordinate (row) + /// The y coordinate (position at row) + /// A reference to the element. + /// When index is out of range of the buffer. + public ref T this[int x, int y] + { + [MethodImpl(InliningOptions.ShortMethod)] + get + { + DebugGuard.MustBeGreaterThanOrEqualTo(x, 0, nameof(x)); + DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y)); + DebugGuard.MustBeLessThan(x, this.Width, nameof(x)); + DebugGuard.MustBeLessThan(y, this.Height, nameof(y)); + + return ref this.DangerousGetRowSpan(y)[x]; + } + } + + /// + /// Wraps an existing memory area as a with tightly packed rows. + /// + /// + /// This method does not transfer ownership of to the returned . + /// The caller is responsible for ensuring that the memory remains valid for the entire lifetime of the returned buffer. + /// If originates from an (for example from ), + /// do not dispose that owner while the returned buffer is still in use. + /// + /// The source memory. + /// The number of elements in each row. + /// The number of rows. + /// The wrapped instance. + /// Thrown when or is not positive. + /// Thrown when is shorter than width * height. +#pragma warning disable CA1000 // Do not declare static members on generic types + public static Buffer2D WrapMemory(Memory memory, int width, int height) +#pragma warning restore CA1000 // Do not declare static members on generic types + => WrapMemory(memory, width, height, width); + + /// + /// Wraps an existing memory area as a using the specified row stride. + /// + /// + /// This method does not transfer ownership of to the returned . + /// The caller is responsible for ensuring that the memory remains valid for the entire lifetime of the returned buffer. + /// If originates from an (for example from ), + /// do not dispose that owner while the returned buffer is still in use. + /// The minimum required length is ((height - 1) * stride) + width elements. + /// + /// The source memory. + /// The number of elements in each row. + /// The number of rows. + /// The number of elements between row starts in the source memory. + /// The wrapped instance. + /// + /// Thrown when or is not positive, + /// or when is less than . + /// + /// Thrown when is shorter than the required buffer size. +#pragma warning disable CA1000 // Do not declare static members on generic types + public static Buffer2D WrapMemory(Memory memory, int width, int height, int stride) +#pragma warning restore CA1000 // Do not declare static members on generic types + { + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + Guard.MustBeGreaterThanOrEqualTo(stride, width, nameof(stride)); + + long requiredLength = checked(((long)(height - 1) * stride) + width); + Guard.IsTrue(memory.Length >= requiredLength, nameof(memory), "The length of the input memory is less than the specified buffer size"); + + MemoryGroup memorySource = MemoryGroup.Wrap(memory); + return new Buffer2D(memorySource, width, height, stride); + } + + /// + /// Gets the representation of the values as a single contiguous + /// when the backing group is a single tightly packed segment. + /// + /// The referencing the buffer. + /// + /// when the buffer can be copied as one contiguous block + /// without per-row handling; otherwise . + /// + public bool DangerousTryGetSingleMemory(out Memory memory) + { + if (this.MemoryGroup.Count > 1 || this.RowStride != this.Width) + { + memory = default; + return false; + } + + int logicalLength = checked((int)((long)this.Width * this.Height)); + memory = this.MemoryGroup[0][..logicalLength]; + return true; + } + + /// + /// Copies this buffer into using the source logical row layout. + /// + /// + /// When dimensions are equal, destination stride is respected. + /// When dimensions differ, source stride is used to copy the source logical layout into destination memory. + /// + /// The destination buffer. + internal void CopyTo(Buffer2D destination) + { + Guard.NotNull(destination, nameof(destination)); + + bool sameDimensions = this.Width == destination.Width && this.Height == destination.Height; + int destinationStride = sameDimensions ? destination.RowStride : this.RowStride; + + // Different dimensions use source logical layout. This supports SwapOrCopyContent, + // where metadata is swapped after data copy. + this.FastMemoryGroup.CopyTo( + this.RowStride, + destination.FastMemoryGroup, + destinationStride, + this.Width, + this.Height); + } + + /// + /// Copies this buffer into using the source row stride as destination layout. + /// + /// The destination span. + internal void CopyTo(Span destination) + { + long requiredLength = checked(((long)(this.Height - 1) * this.RowStride) + this.Width); + Guard.MustBeGreaterThanOrEqualTo(destination.Length, requiredLength, nameof(destination)); + + this.FastMemoryGroup.CopyTo( + this.RowStride, + destination, + this.RowStride, + this.Width, + this.Height); + } + + /// + /// Copies tightly packed row-major data from into this buffer. + /// + /// The source data. + internal void CopyFrom(ReadOnlySpan source) => this.CopyFrom(source, this.Width); + + /// + /// Copies row-major data from into this buffer using + /// elements between source row starts. + /// + /// The source data. + /// The number of elements between source row starts. + internal void CopyFrom(ReadOnlySpan source, int sourceStride) + { + Guard.MustBeGreaterThanOrEqualTo(sourceStride, this.Width, nameof(sourceStride)); + + long requiredLength = checked(((long)(this.Height - 1) * sourceStride) + this.Width); + Guard.MustBeGreaterThanOrEqualTo(source.Length, requiredLength, nameof(source)); + + // Copy row by row so padded source rows map correctly into the destination logical rows. + int sourceOffset = 0; + for (int y = 0; y < this.Height; y++) + { + source.Slice(sourceOffset, this.Width).CopyTo(this.DangerousGetRowSpan(y)); + sourceOffset += sourceStride; + } + } + + /// + /// Clears this buffer when is default; otherwise fills it with . + /// + /// The fill value. + internal void Clear(T value) + { + if (value.Equals(default)) + { + this.FastMemoryGroup.Clear(); + return; + } + + this.FastMemoryGroup.Fill(value); + } + + /// + /// Disposes the instance + /// + public void Dispose() + { + this.FastMemoryGroup.Dispose(); + this.IsDisposed = true; + } + + /// + /// Gets a to the row 'y' beginning from the pixel at the first pixel on that row. + /// + /// + /// This method does not validate the y argument for performance reason, + /// is being propagated from lower levels. + /// + /// The row index. + /// The of the pixels in the row. + /// Thrown when row index is out of range. + [MethodImpl(InliningOptions.ShortMethod)] + public Span DangerousGetRowSpan(int y) + { + if ((uint)y >= (uint)this.Height) + { + this.ThrowYOutOfRangeException(y); + } + + if (this.RowStride == this.Width) + { + return this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width); + } + + int rowStart = checked(y * this.RowStride); + return this.FastMemoryGroup[0].Span.Slice(rowStart, this.Width); + } + + internal bool DangerousTryGetPaddedRowSpan(int y, int padding, out Span paddedSpan) + { + DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y)); + DebugGuard.MustBeLessThan(y, this.Height, nameof(y)); + + int stride = this.Width + padding; + long rowStart = y * (long)this.RowStride; + Span slice = this.RowStride == this.Width + ? this.FastMemoryGroup.GetRemainingSliceOfBuffer(rowStart) + : this.FastMemoryGroup[0].Span[checked((int)rowStart)..]; + + if (slice.Length < stride) + { + paddedSpan = default; + return false; + } + + paddedSpan = slice[..stride]; + return true; + } + + [MethodImpl(InliningOptions.ShortMethod)] + internal ref T GetElementUnsafe(int x, int y) + { + Span span = this.RowStride == this.Width + ? this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width) + : this.FastMemoryGroup[0].Span.Slice(checked(y * this.RowStride), this.Width); + + return ref span[x]; + } + + /// + /// Gets a to the row 'y' beginning from the pixel at the first pixel on that row. + /// + /// The y (row) coordinate. + /// The . + [MethodImpl(InliningOptions.ShortMethod)] + internal Memory GetSafeRowMemory(int y) + { + DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y)); + DebugGuard.MustBeLessThan(y, this.Height, nameof(y)); + + if (this.RowStride != this.Width) + { + int rowStart = checked(y * this.RowStride); + return this.FastMemoryGroup[0].Slice(rowStart, this.Width); + } + + return this.FastMemoryGroup.View.GetBoundedMemorySlice(y * (long)this.Width, this.Width); + } + + /// + /// Gets a to the backing data if the backing group consists of a single contiguous memory buffer. + /// Throws otherwise. + /// + /// The referencing the memory area. + /// + /// Thrown when the backing group is discontiguous. + /// + [MethodImpl(InliningOptions.ShortMethod)] + internal Span DangerousGetSingleSpan() => this.FastMemoryGroup.Single().Span; + + /// + /// Gets a to the backing data of if the backing group consists of a single contiguous memory buffer. + /// Throws otherwise. + /// + /// The . + /// + /// Thrown when the backing group is discontiguous. + /// + [MethodImpl(InliningOptions.ShortMethod)] + internal Memory DangerousGetSingleMemory() => this.FastMemoryGroup.Single(); + + /// + /// Swaps the contents of 'destination' with 'source' if the buffers are owned (1), + /// copies the contents of 'source' to 'destination' otherwise (2). Buffers should be of same size in case 2! + /// + /// The destination buffer. + /// The source buffer. + /// Attempt to copy/swap incompatible buffers. + internal static bool SwapOrCopyContent(Buffer2D destination, Buffer2D source) + { + bool swapped = false; + if (MemoryGroup.CanSwapContent(destination.FastMemoryGroup, source.FastMemoryGroup)) + { + (destination.FastMemoryGroup, source.FastMemoryGroup) = (source.FastMemoryGroup, destination.FastMemoryGroup); + destination.FastMemoryGroup.RecreateViewAfterSwap(); + source.FastMemoryGroup.RecreateViewAfterSwap(); + swapped = true; + } + else + { + long sourceLayoutLength = GetRequiredLength(source.Width, source.Height, source.RowStride); + long destinationLayoutLength = GetRequiredLength(destination.Width, destination.Height, destination.RowStride); + + bool destinationCanRepresentSource = destination.FastMemoryGroup.TotalLength >= sourceLayoutLength; + bool sourceCanRepresentDestination = source.FastMemoryGroup.TotalLength >= destinationLayoutLength; + if (!destinationCanRepresentSource || !sourceCanRepresentDestination) + { + throw new InvalidMemoryOperationException( + "Trying to copy/swap incompatible buffers. This is most likely caused by applying an unsupported processor to wrapped-memory images."); + } + + source.CopyTo(destination); + } + + (destination.Size, source.Size) = (source.Size, destination.Size); + (destination.RowStride, source.RowStride) = (source.RowStride, destination.RowStride); + return swapped; + } + + [MethodImpl(InliningOptions.ColdPath)] + private void ThrowYOutOfRangeException(int y) + => throw new ArgumentOutOfRangeException($"DangerousGetRowSpan({y}). Y was out of range. Height={this.Height}"); + + [MethodImpl(InliningOptions.ShortMethod)] + private static long GetRequiredLength(int width, int height, int stride) + => checked(((long)(height - 1) * stride) + width); + } +} diff --git a/ImageSharp/Memory/ByteMemoryManager{T}.cs b/ImageSharp/Memory/ByteMemoryManager{T}.cs new file mode 100644 index 0000000..97bc21e --- /dev/null +++ b/ImageSharp/Memory/ByteMemoryManager{T}.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Memory { + /// + /// A custom that can wrap of instances + /// and cast them to be for any arbitrary unmanaged value type. + /// + /// The value type to use when casting the wrapped instance. + internal sealed class ByteMemoryManager : MemoryManager + where T : unmanaged + { + /// + /// The wrapped of instance. + /// + private readonly Memory memory; + + /// + /// Initializes a new instance of the class. + /// + /// The of instance to wrap. + public ByteMemoryManager(Memory memory) + { + this.memory = memory; + } + + /// + protected override void Dispose(bool disposing) + { + } + + /// + public override Span GetSpan() + { + return MemoryMarshal.Cast(this.memory.Span); + } + + /// + public override MemoryHandle Pin(int elementIndex = 0) + { + // We need to adjust the offset into the wrapped byte segment, + // as the input index refers to the target-cast memory of T. + // We just have to shift this index by the byte size of T. + return this.memory[(elementIndex * Unsafe.SizeOf())..].Pin(); + } + + /// + public override void Unpin() + { + } + } +} diff --git a/ImageSharp/Memory/ByteMemoryOwner{T}.cs b/ImageSharp/Memory/ByteMemoryOwner{T}.cs new file mode 100644 index 0000000..d9e9f0d --- /dev/null +++ b/ImageSharp/Memory/ByteMemoryOwner{T}.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; + +namespace SixLabors.ImageSharp.Memory { + /// + /// A custom that can wrap of instances + /// and cast them to be for any arbitrary unmanaged value type. + /// + /// The value type to use when casting the wrapped instance. + internal sealed class ByteMemoryOwner : IMemoryOwner + where T : unmanaged + { + private readonly IMemoryOwner memoryOwner; + private readonly ByteMemoryManager memoryManager; + private bool disposedValue; + + /// + /// Initializes a new instance of the class. + /// + /// The of instance to wrap. + public ByteMemoryOwner(IMemoryOwner memoryOwner) + { + this.memoryOwner = memoryOwner; + this.memoryManager = new ByteMemoryManager(memoryOwner.Memory); + } + + /// + public Memory Memory => this.memoryManager.Memory; + + private void Dispose(bool disposing) + { + if (!this.disposedValue) + { + if (disposing) + { + this.memoryOwner.Dispose(); + } + + this.disposedValue = true; + } + } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + this.Dispose(disposing: true); + } + } +} diff --git a/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs b/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs new file mode 100644 index 0000000..21d47c3 --- /dev/null +++ b/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Memory { + /// + /// Represents discontiguous group of multiple uniformly-sized memory segments. + /// The last segment can be smaller than the preceding ones. + /// + /// The element type. + public interface IMemoryGroup : IReadOnlyList> + where T : struct + { + /// + /// Gets the number of elements per contiguous sub-buffer preceding the last buffer. + /// The last buffer is allowed to be smaller. + /// + public int BufferLength { get; } + + /// + /// Gets the aggregate number of elements in the group. + /// + public long TotalLength { get; } + + /// + /// Gets a value indicating whether the group has been invalidated. + /// + /// + /// Invalidation usually occurs when an image processor capable to alter the image dimensions replaces + /// the image buffers internally. + /// + public bool IsValid { get; } + + /// + /// Returns a value-type implementing an allocation-free enumerator of the memory groups in the current + /// instance. The return type shouldn't be used directly: just use a block on + /// the instance in use and the C# compiler will automatically invoke this + /// method behind the scenes. This method takes precedence over the + /// implementation, which is still available when casting to one of the underlying interfaces. + /// + /// A new instance mapping the current values in use. + public new MemoryGroupEnumerator GetEnumerator(); + } +} diff --git a/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupEnumerator{T}.cs b/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupEnumerator{T}.cs new file mode 100644 index 0000000..4726f63 --- /dev/null +++ b/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupEnumerator{T}.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Memory { + /// + /// A value-type enumerator for instances. + /// + /// The element type. + [EditorBrowsable(EditorBrowsableState.Never)] + public ref struct MemoryGroupEnumerator + where T : struct + { + private readonly IMemoryGroup memoryGroup; + private readonly int count; + private int index; + + [MethodImpl(InliningOptions.ShortMethod)] + internal MemoryGroupEnumerator(MemoryGroup.Owned memoryGroup) + { + this.memoryGroup = memoryGroup; + this.count = memoryGroup.Count; + this.index = -1; + } + + [MethodImpl(InliningOptions.ShortMethod)] + internal MemoryGroupEnumerator(MemoryGroup.Consumed memoryGroup) + { + this.memoryGroup = memoryGroup; + this.count = memoryGroup.Count; + this.index = -1; + } + + [MethodImpl(InliningOptions.ShortMethod)] + internal MemoryGroupEnumerator(MemoryGroupView memoryGroup) + { + this.memoryGroup = memoryGroup; + this.count = memoryGroup.Count; + this.index = -1; + } + + /// + public Memory Current + { + [MethodImpl(InliningOptions.ShortMethod)] + get => this.memoryGroup[this.index]; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool MoveNext() + { + int index = this.index + 1; + + if (index < this.count) + { + this.index = index; + + return true; + } + + return false; + } + } +} diff --git a/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs b/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs new file mode 100644 index 0000000..c1fffc9 --- /dev/null +++ b/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs @@ -0,0 +1,280 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Memory { + internal static class MemoryGroupExtensions + { + /// + /// Fills the elements of this with the specified value. + /// + /// The type of element. + /// The group to fill. + /// The value to assign to each element of the group. + internal static void Fill(this IMemoryGroup group, T value) + where T : struct + { + foreach (Memory memory in group) + { + memory.Span.Fill(value); + } + } + + /// + /// Clears the contents of this . + /// + /// The type of element. + /// The group to clear. + internal static void Clear(this IMemoryGroup group) + where T : struct + { + foreach (Memory memory in group) + { + memory.Span.Clear(); + } + } + + /// + /// Returns a slice that is expected to be within the bounds of a single buffer. + /// + /// The type of element. + /// The group. + /// The start index of the slice. + /// The length of the slice. + /// Slice is out of bounds. + /// The slice. + internal static Memory GetBoundedMemorySlice(this IMemoryGroup group, long start, int length) + where T : struct + { + Guard.NotNull(group, nameof(group)); + Guard.IsTrue(group.IsValid, nameof(group), "Group must be valid!"); + Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length)); + Guard.MustBeLessThan(start, group.TotalLength, nameof(start)); + + int bufferIdx = (int)Math.DivRem(start, group.BufferLength, out long bufferStartLong); + int bufferStart = (int)bufferStartLong; + + // if (bufferIdx < 0 || bufferIdx >= group.Count) + if ((uint)bufferIdx >= group.Count) + { + throw new ArgumentOutOfRangeException(nameof(start)); + } + + int bufferEnd = bufferStart + length; + Memory memory = group[bufferIdx]; + + if (bufferEnd > memory.Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return memory.Slice(bufferStart, length); + } + + /// + /// Copies a 2D logical region from into + /// using the provided source and target strides. + /// + /// The element type. + /// The source memory group. + /// Elements between source row starts. + /// The destination span. + /// Elements between destination row starts. + /// The logical row width to copy. + /// The number of rows to copy. + internal static void CopyTo( + this IMemoryGroup source, + int sourceStride, + Span target, + int targetStride, + int width, + int height) + where T : struct + { + Guard.NotNull(source, nameof(source)); + Guard.MustBeGreaterThanOrEqualTo(width, 0, nameof(width)); + Guard.MustBeGreaterThanOrEqualTo(height, 0, nameof(height)); + Guard.MustBeGreaterThanOrEqualTo(sourceStride, width, nameof(sourceStride)); + Guard.MustBeGreaterThanOrEqualTo(targetStride, width, nameof(targetStride)); + + long sourceRequired = height == 0 ? 0 : checked(((long)(height - 1) * sourceStride) + width); + long targetRequired = height == 0 ? 0 : checked(((long)(height - 1) * targetStride) + width); + Guard.MustBeGreaterThanOrEqualTo(source.TotalLength, sourceRequired, nameof(source)); + Guard.MustBeGreaterThanOrEqualTo(target.Length, targetRequired, nameof(target)); + + if (width == 0 || height == 0) + { + return; + } + + MemoryGroupCursor sourceCursor = new(source); + int sourceSkip = sourceStride - width; + + for (int y = 0; y < height; y++) + { + int rowStart = checked(y * targetStride); + Span destinationRow = target.Slice(rowStart, width); + CopyFromCursorToSpan(ref sourceCursor, destinationRow); + + // Trailing padding after the last row is optional, so only skip between rows. + if (y < height - 1) + { + ForwardCursor(ref sourceCursor, sourceSkip); + } + } + } + + /// + /// Copies a 2D logical region from into + /// using the provided source and target strides. + /// + /// The element type. + /// The source memory group. + /// Elements between source row starts. + /// The destination memory group. + /// Elements between destination row starts. + /// The logical row width to copy. + /// The number of rows to copy. + internal static void CopyTo( + this IMemoryGroup source, + int sourceStride, + IMemoryGroup target, + int targetStride, + int width, + int height) + where T : struct + { + Guard.NotNull(source, nameof(source)); + Guard.NotNull(target, nameof(target)); + Guard.IsTrue(source.IsValid, nameof(source), "Source group must be valid."); + Guard.IsTrue(target.IsValid, nameof(target), "Target group must be valid."); + Guard.MustBeGreaterThanOrEqualTo(width, 0, nameof(width)); + Guard.MustBeGreaterThanOrEqualTo(height, 0, nameof(height)); + Guard.MustBeGreaterThanOrEqualTo(sourceStride, width, nameof(sourceStride)); + Guard.MustBeGreaterThanOrEqualTo(targetStride, width, nameof(targetStride)); + + long sourceRequired = height == 0 ? 0 : checked(((long)(height - 1) * sourceStride) + width); + long targetRequired = height == 0 ? 0 : checked(((long)(height - 1) * targetStride) + width); + Guard.MustBeGreaterThanOrEqualTo(source.TotalLength, sourceRequired, nameof(source)); + Guard.MustBeGreaterThanOrEqualTo(target.TotalLength, targetRequired, nameof(target)); + + if (width == 0 || height == 0) + { + return; + } + + MemoryGroupCursor sourceCursor = new(source); + MemoryGroupCursor targetCursor = new(target); + int sourceSkip = sourceStride - width; + int targetSkip = targetStride - width; + + for (int y = 0; y < height; y++) + { + CopyFromCursorToCursor(ref sourceCursor, ref targetCursor, width); + + // Trailing padding after the last row is optional, so only skip between rows. + if (y < height - 1) + { + ForwardCursor(ref sourceCursor, sourceSkip); + ForwardCursor(ref targetCursor, targetSkip); + } + } + } + + private static void CopyFromCursorToCursor( + ref MemoryGroupCursor source, + ref MemoryGroupCursor target, + int count) + where T : struct + { + int remaining = count; + while (remaining > 0) + { + int fwd = Math.Min(remaining, Math.Min(source.LookAhead(), target.LookAhead())); + source.GetSpan(fwd).CopyTo(target.GetSpan(fwd)); + source.Forward(fwd); + target.Forward(fwd); + remaining -= fwd; + } + } + + private static void CopyFromCursorToSpan(ref MemoryGroupCursor source, Span target) + where T : struct + { + int remaining = target.Length; + while (remaining > 0) + { + int copied = target.Length - remaining; + int fwd = Math.Min(remaining, source.LookAhead()); + source.GetSpan(fwd).CopyTo(target[copied..]); + source.Forward(fwd); + remaining -= fwd; + } + } + + private static void ForwardCursor(ref MemoryGroupCursor cursor, int steps) + where T : struct + { + int remaining = steps; + while (remaining > 0) + { + int fwd = Math.Min(remaining, cursor.LookAhead()); + cursor.Forward(fwd); + remaining -= fwd; + } + } + + private struct MemoryGroupCursor + where T : struct + { + private readonly IMemoryGroup memoryGroup; + + private int bufferIndex; + + private int elementIndex; + + public MemoryGroupCursor(IMemoryGroup memoryGroup) + { + this.memoryGroup = memoryGroup; + this.bufferIndex = 0; + this.elementIndex = 0; + } + + private bool IsAtLastBuffer => this.bufferIndex == this.memoryGroup.Count - 1; + + private int CurrentBufferLength => this.memoryGroup[this.bufferIndex].Length; + + public Span GetSpan(int length) + { + return this.memoryGroup[this.bufferIndex].Span.Slice(this.elementIndex, length); + } + + public int LookAhead() + { + return this.CurrentBufferLength - this.elementIndex; + } + + public void Forward(int steps) + { + int nextIdx = this.elementIndex + steps; + int currentBufferLength = this.CurrentBufferLength; + + if (nextIdx < currentBufferLength) + { + this.elementIndex = nextIdx; + } + else if (nextIdx == currentBufferLength) + { + this.bufferIndex++; + this.elementIndex = 0; + } + else + { + // If we get here, it indicates a bug in CopyTo: + throw new ArgumentException("Can't forward multiple buffers!", nameof(steps)); + } + } + } + } +} diff --git a/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupSpanCache.cs b/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupSpanCache.cs new file mode 100644 index 0000000..399037e --- /dev/null +++ b/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupSpanCache.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory.Internals; + +namespace SixLabors.ImageSharp.Memory { + /// + /// Cached pointer or array data enabling fast access from + /// known implementations. + /// + internal unsafe struct MemoryGroupSpanCache + { + public SpanCacheMode Mode; + public byte[]? SingleArray; + public void* SinglePointer; + public void*[] MultiPointer; + + public static MemoryGroupSpanCache Create(IMemoryOwner[] memoryOwners) + where T : struct + { + IMemoryOwner owner0 = memoryOwners[0]; + MemoryGroupSpanCache memoryGroupSpanCache = default; + if (memoryOwners.Length == 1) + { + if (owner0 is SharedArrayPoolBuffer sharedPoolBuffer) + { + memoryGroupSpanCache.Mode = SpanCacheMode.SingleArray; + memoryGroupSpanCache.SingleArray = sharedPoolBuffer.Array; + } + else if (owner0 is UnmanagedBuffer unmanagedBuffer) + { + memoryGroupSpanCache.Mode = SpanCacheMode.SinglePointer; + memoryGroupSpanCache.SinglePointer = unmanagedBuffer.Pointer; + } + } + else if (owner0 is UnmanagedBuffer) + { + memoryGroupSpanCache.Mode = SpanCacheMode.MultiPointer; + memoryGroupSpanCache.MultiPointer = new void*[memoryOwners.Length]; + for (int i = 0; i < memoryOwners.Length; i++) + { + memoryGroupSpanCache.MultiPointer[i] = ((UnmanagedBuffer)memoryOwners[i]).Pointer; + } + } + + return memoryGroupSpanCache; + } + } +} diff --git a/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupView{T}.cs b/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupView{T}.cs new file mode 100644 index 0000000..baa8cd3 --- /dev/null +++ b/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupView{T}.cs @@ -0,0 +1,147 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Memory { + /// + /// Implements , defining a view for + /// rather than owning the segments. + /// + /// + /// This type provides an indirection, protecting the users of publicly exposed memory API-s + /// from internal memory-swaps. Whenever an internal swap happens, the + /// instance becomes invalid, throwing an exception on all operations. + /// + /// The element type. + internal class MemoryGroupView : IMemoryGroup + where T : struct + { + private MemoryGroup? owner; + private readonly MemoryOwnerWrapper[] memoryWrappers; + + public MemoryGroupView(MemoryGroup owner) + { + this.owner = owner; + this.memoryWrappers = new MemoryOwnerWrapper[owner.Count]; + + for (int i = 0; i < owner.Count; i++) + { + this.memoryWrappers[i] = new MemoryOwnerWrapper(this, i); + } + } + + public int Count + { + [MethodImpl(InliningOptions.ShortMethod)] + get + { + this.EnsureIsValid(); + return this.owner.Count; + } + } + + public int BufferLength + { + get + { + this.EnsureIsValid(); + return this.owner.BufferLength; + } + } + + public long TotalLength + { + get + { + this.EnsureIsValid(); + return this.owner.TotalLength; + } + } + + [MemberNotNullWhen(true, nameof(owner))] + public bool IsValid => this.owner != null; + + public Memory this[int index] + { + get + { + this.EnsureIsValid(); + return this.memoryWrappers[index].Memory; + } + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public MemoryGroupEnumerator GetEnumerator() + { + return new MemoryGroupEnumerator(this); + } + + /// + IEnumerator> IEnumerable>.GetEnumerator() + { + this.EnsureIsValid(); + for (int i = 0; i < this.Count; i++) + { + yield return this.memoryWrappers[i].Memory; + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable>)this).GetEnumerator(); + + internal void Invalidate() + { + this.owner = null; + } + + [MemberNotNull(nameof(owner))] + private void EnsureIsValid() + { + if (!this.IsValid) + { + throw new InvalidMemoryOperationException("Can not access an invalidated MemoryGroupView!"); + } + } + + private class MemoryOwnerWrapper : MemoryManager + { + private readonly MemoryGroupView view; + + private readonly int index; + + public MemoryOwnerWrapper(MemoryGroupView view, int index) + { + this.view = view; + this.index = index; + } + + protected override void Dispose(bool disposing) + { + } + + public override Span GetSpan() + { + this.view.EnsureIsValid(); + return this.view.owner[this.index].Span; + } + + public override MemoryHandle Pin(int elementIndex = 0) + { + this.view.EnsureIsValid(); + return this.view.owner[this.index].Pin(); + } + + public override void Unpin() + { + throw new NotSupportedException(); + } + } + } +} diff --git a/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs b/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs new file mode 100644 index 0000000..84ac422 --- /dev/null +++ b/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Memory { + internal abstract partial class MemoryGroup + { + /// + /// A implementation that consumes the underlying memory buffers. + /// + public sealed class Consumed : MemoryGroup, IEnumerable> + { + private readonly Memory[] source; + + public Consumed(Memory[] source, int bufferLength, long totalLength) + : base(bufferLength, totalLength) + { + this.source = source; + this.View = new MemoryGroupView(this); + } + + public override int Count + { + [MethodImpl(InliningOptions.ShortMethod)] + get => this.source.Length; + } + + public override Memory this[int index] => this.source[index]; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public override MemoryGroupEnumerator GetEnumerator() => new(this); + + /// + IEnumerator> IEnumerable>.GetEnumerator() + + /* The runtime sees the Array class as if it implemented the + * type-generic collection interfaces explicitly, so here we + * can just cast the source array to IList> (or to + * an equivalent type), and invoke the generic GetEnumerator + * method directly from that interface reference. This saves + * having to create our own iterator block here. */ + => ((IList>)this.source).GetEnumerator(); + + public override void Dispose() + { + this.View.Invalidate(); + this.ReleaseAllocationTracking(); + } + } + } +} diff --git a/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs b/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs new file mode 100644 index 0000000..a64ded7 --- /dev/null +++ b/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs @@ -0,0 +1,279 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Linq; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory.Internals; + +namespace SixLabors.ImageSharp.Memory { + internal abstract partial class MemoryGroup + { + /// + /// A implementation that owns the underlying memory buffers. + /// + public sealed class Owned : MemoryGroup, IEnumerable> + { + private IMemoryOwner[]? memoryOwners; + private RefCountedMemoryLifetimeGuard? groupLifetimeGuard; + + public Owned(IMemoryOwner[] memoryOwners, int bufferLength, long totalLength, bool swappable) + : base(bufferLength, totalLength) + { + this.memoryOwners = memoryOwners; + this.Swappable = swappable; + this.View = new MemoryGroupView(this); + this.memoryGroupSpanCache = MemoryGroupSpanCache.Create(memoryOwners); + } + + public Owned( + UniformUnmanagedMemoryPool pool, + UnmanagedMemoryHandle[] pooledHandles, + int bufferLength, + long totalLength, + int sizeOfLastBuffer, + AllocationOptions options) + : this(CreateBuffers(pooledHandles, bufferLength, sizeOfLastBuffer, options), bufferLength, totalLength, true) => + this.groupLifetimeGuard = pool.CreateGroupLifetimeGuard(pooledHandles); + + public bool Swappable { get; } + + private bool IsDisposed => this.memoryOwners == null; + + public override int Count + { + [MethodImpl(InliningOptions.ShortMethod)] + get + { + this.EnsureNotDisposed(); + return this.memoryOwners.Length; + } + } + + public override Memory this[int index] + { + get + { + this.EnsureNotDisposed(); + return this.memoryOwners[index].Memory; + } + } + + internal override void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes) + { + if (this.groupLifetimeGuard != null) + { + // Pool-owned multi-buffer groups recover leaked handles through the group guard finalizer. + this.groupLifetimeGuard.AttachAllocationTracking(allocator, lengthInBytes); + return; + } + + IMemoryOwner[]? memoryOwners = this.memoryOwners; + if (memoryOwners?.Length == 1 && memoryOwners[0] is AllocationTrackedMemoryManager trackedOwner) + { + // Single-buffer groups should release tracking with the buffer owner when that owner has + // a more precise lifetime, such as an existing pooled-resource finalizer. + trackedOwner.AttachAllocationTracking(allocator, lengthInBytes); + return; + } + + if (memoryOwners?.Length > 1) + { + foreach (IMemoryOwner memoryOwner in memoryOwners) + { + if (memoryOwner is not AllocationTrackedMemoryManager) + { + // Splitting is only valid when every segment can own its reservation. A single + // untracked segment makes the whole group ineligible, and this preflight has + // not attached anything yet, so the entire group can fall back immediately. + base.AttachAllocationTracking(allocator, lengthInBytes); + return; + } + } + + // Non-pool multi-buffer groups have no group-level finalizer, so each segment carries + // its own share of the reservation through the segment owner or its lifetime guard. + long remainingLengthInBytes = lengthInBytes; + int lastOwnerIndex = memoryOwners.Length - 1; + for (int i = 0; i < lastOwnerIndex; i++) + { + trackedOwner = (AllocationTrackedMemoryManager)memoryOwners[i]; + long ownerLengthInBytes = (long)trackedOwner.Memory.Length * Unsafe.SizeOf(); + trackedOwner.AttachAllocationTracking(allocator, ownerLengthInBytes); + remainingLengthInBytes -= ownerLengthInBytes; + } + + trackedOwner = (AllocationTrackedMemoryManager)memoryOwners[lastOwnerIndex]; + trackedOwner.AttachAllocationTracking(allocator, remainingLengthInBytes); + return; + } + + base.AttachAllocationTracking(allocator, lengthInBytes); + } + + private static IMemoryOwner[] CreateBuffers( + UnmanagedMemoryHandle[] pooledBuffers, + int bufferLength, + int sizeOfLastBuffer, + AllocationOptions options) + { + IMemoryOwner[] result = new IMemoryOwner[pooledBuffers.Length]; + for (int i = 0; i < pooledBuffers.Length - 1; i++) + { + ObservedBuffer currentBuffer = ObservedBuffer.Create(pooledBuffers[i], bufferLength, options); + result[i] = currentBuffer; + } + + ObservedBuffer lastBuffer = ObservedBuffer.Create(pooledBuffers[^1], sizeOfLastBuffer, options); + result[^1] = lastBuffer; + return result; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public override MemoryGroupEnumerator GetEnumerator() => new(this); + + public override void IncreaseRefCounts() + { + this.EnsureNotDisposed(); + + if (this.groupLifetimeGuard != null) + { + this.groupLifetimeGuard.AddRef(); + } + else + { + foreach (IMemoryOwner memoryOwner in this.memoryOwners) + { + if (memoryOwner is IRefCounted unmanagedBuffer) + { + unmanagedBuffer.AddRef(); + } + } + } + } + + public override void DecreaseRefCounts() + { + this.EnsureNotDisposed(); + if (this.groupLifetimeGuard != null) + { + this.groupLifetimeGuard.ReleaseRef(); + } + else + { + foreach (IMemoryOwner memoryOwner in this.memoryOwners) + { + if (memoryOwner is IRefCounted unmanagedBuffer) + { + unmanagedBuffer.ReleaseRef(); + } + } + } + } + + public override void RecreateViewAfterSwap() + { + this.View.Invalidate(); + this.View = new MemoryGroupView(this); + } + + /// + IEnumerator> IEnumerable>.GetEnumerator() + { + this.EnsureNotDisposed(); + return this.memoryOwners.Select(mo => mo.Memory).GetEnumerator(); + } + + public override void Dispose() + { + if (this.IsDisposed) + { + return; + } + + this.View.Invalidate(); + + if (this.groupLifetimeGuard != null) + { + this.groupLifetimeGuard.Dispose(); + } + else + { + foreach (IMemoryOwner memoryOwner in this.memoryOwners!) + { + memoryOwner.Dispose(); + } + } + + this.ReleaseAllocationTracking(); + this.memoryOwners = null; + this.IsValid = false; + this.groupLifetimeGuard = null; + } + + [MethodImpl(InliningOptions.ShortMethod)] + [MemberNotNull(nameof(memoryOwners))] + private void EnsureNotDisposed() + { + if (this.memoryOwners is null) + { + ThrowObjectDisposedException(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + [DoesNotReturn] + private static void ThrowObjectDisposedException() => throw new ObjectDisposedException(nameof(MemoryGroup)); + + // When the MemoryGroup points to multiple buffers via `groupLifetimeGuard`, + // the lifetime of the individual buffers is managed by the guard. + // Group buffer IMemoryOwner-s d not manage ownership. + private sealed class ObservedBuffer : MemoryManager + { + private readonly UnmanagedMemoryHandle handle; + private readonly int lengthInElements; + + private ObservedBuffer(UnmanagedMemoryHandle handle, int lengthInElements) + { + this.handle = handle; + this.lengthInElements = lengthInElements; + } + + public static ObservedBuffer Create( + UnmanagedMemoryHandle handle, + int lengthInElements, + AllocationOptions options) + { + ObservedBuffer buffer = new(handle, lengthInElements); + if (options.Has(AllocationOptions.Clean)) + { + buffer.GetSpan().Clear(); + } + + return buffer; + } + + protected override void Dispose(bool disposing) + { + // No-op. + } + + public override unsafe Span GetSpan() => new(this.handle.Pointer, this.lengthInElements); + + public override unsafe MemoryHandle Pin(int elementIndex = 0) + { + void* pbData = Unsafe.Add(this.handle.Pointer, elementIndex); + return new MemoryHandle(pbData); + } + + public override void Unpin() + { + } + } + } + } +} diff --git a/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs b/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs new file mode 100644 index 0000000..e22d2ce --- /dev/null +++ b/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs @@ -0,0 +1,337 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory.Internals; + +namespace SixLabors.ImageSharp.Memory { + /// + /// Represents discontinuous group of multiple uniformly-sized memory segments. + /// The underlying buffers may change with time, therefore it's not safe to expose them directly on + /// and . + /// + /// The element type. + internal abstract partial class MemoryGroup : IMemoryGroup, IDisposable + where T : struct + { + private static readonly int ElementSize = Unsafe.SizeOf(); + + private AllocationTrackingState allocationTracking; + private MemoryGroupSpanCache memoryGroupSpanCache; + + private MemoryGroup(int bufferLength, long totalLength) + { + this.BufferLength = bufferLength; + this.TotalLength = totalLength; + } + + /// + public abstract int Count { get; } + + /// + public int BufferLength { get; } + + /// + public long TotalLength { get; } + + /// + public bool IsValid { get; private set; } = true; + + public MemoryGroupView View { get; private set; } = null!; + + /// + public abstract Memory this[int index] { get; } + + /// + public abstract void Dispose(); + + /// + public abstract MemoryGroupEnumerator GetEnumerator(); + + /// + /// Attaches allocation tracking by specifying the allocator and the length, in bytes, to be tracked. + /// + /// The memory allocator to use for tracking allocations. + /// The length, in bytes, of the memory region to track. Must be greater than or equal to zero. + /// + /// Intended for one-time initialization after the group has been created; callers should avoid changing + /// tracking state concurrently with disposal. + /// + internal virtual void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes) => + this.allocationTracking.Attach(allocator, lengthInBytes); + + /// + /// Releases any resources or tracking information associated with allocation tracking for this instance. + /// + /// + /// This method is intended to be called when allocation tracking is no longer needed. It is safe + /// to call multiple times; subsequent calls after the first have no effect, even when called concurrently. + /// + internal void ReleaseAllocationTracking() => this.allocationTracking.Release(); + + /// + IEnumerator> IEnumerable>.GetEnumerator() + + /* This method is implemented in each derived class. + * Implementing the method here as non-abstract and throwing, + * then reimplementing it explicitly in each derived class, is + * a workaround for the lack of support for abstract explicit + * interface method implementations in C#. */ + => throw new NotImplementedException($"The type {this.GetType()} needs to override IEnumerable>.GetEnumerator()"); + + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable>)this).GetEnumerator(); + + /// + /// Creates a new memory group, allocating it's buffers with the provided allocator. + /// + /// The to use. + /// The total length of the buffer. + /// The expected alignment (eg. to make sure image rows fit into single buffers). + /// The . + /// A new . + /// Thrown when 'blockAlignment' converted to bytes is greater than the buffer capacity of the allocator. + public static MemoryGroup Allocate( + MemoryAllocator allocator, + long totalLengthInElements, + int bufferAlignmentInElements, + AllocationOptions options = AllocationOptions.None) + { + Guard.NotNull(allocator, nameof(allocator)); + int bufferCapacityInBytes = allocator.GetBufferCapacityInBytes(); + + if (totalLengthInElements < 0) + { + InvalidMemoryOperationException.ThrowNegativeAllocationException(totalLengthInElements); + } + + int blockCapacityInElements = bufferCapacityInBytes / ElementSize; + if (bufferAlignmentInElements < 0 || bufferAlignmentInElements > blockCapacityInElements) + { + InvalidMemoryOperationException.ThrowInvalidAlignmentException(bufferAlignmentInElements); + } + + if (totalLengthInElements == 0) + { + IMemoryOwner[] emptyBuffer = [allocator.AllocateGroupBuffer(0, options)]; + return new Owned(emptyBuffer, 0, 0, true); + } + + int numberOfAlignedSegments = blockCapacityInElements / bufferAlignmentInElements; + int bufferLength = numberOfAlignedSegments * bufferAlignmentInElements; + if (totalLengthInElements > 0 && totalLengthInElements < bufferLength) + { + bufferLength = (int)totalLengthInElements; + } + + int sizeOfLastBuffer = (int)(totalLengthInElements % bufferLength); + long bufferCount = totalLengthInElements / bufferLength; + + if (sizeOfLastBuffer == 0) + { + sizeOfLastBuffer = bufferLength; + } + else + { + bufferCount++; + } + + IMemoryOwner[] buffers = new IMemoryOwner[bufferCount]; + for (int i = 0; i < buffers.Length - 1; i++) + { + buffers[i] = allocator.AllocateGroupBuffer(bufferLength, options); + } + + if (bufferCount > 0) + { + buffers[^1] = allocator.AllocateGroupBuffer(sizeOfLastBuffer, options); + } + + return new Owned(buffers, bufferLength, totalLengthInElements, true); + } + + public static MemoryGroup CreateContiguous(IMemoryOwner buffer, bool clear) + { + if (clear) + { + buffer.GetSpan().Clear(); + } + + int length = buffer.Memory.Length; + IMemoryOwner[] buffers = [buffer]; + return new Owned(buffers, length, length, true); + } + + public static bool TryAllocate( + UniformUnmanagedMemoryPool pool, + long totalLengthInElements, + int bufferAlignmentInElements, + AllocationOptions options, + [NotNullWhen(true)] out MemoryGroup? memoryGroup) + { + Guard.NotNull(pool, nameof(pool)); + Guard.MustBeGreaterThanOrEqualTo(totalLengthInElements, 0, nameof(totalLengthInElements)); + Guard.MustBeGreaterThanOrEqualTo(bufferAlignmentInElements, 0, nameof(bufferAlignmentInElements)); + + int blockCapacityInElements = pool.BufferLength / ElementSize; + + if (bufferAlignmentInElements > blockCapacityInElements) + { + memoryGroup = null; + return false; + } + + if (totalLengthInElements == 0) + { + throw new InvalidMemoryOperationException("Allocating 0 length buffer from UniformByteArrayPool is disallowed"); + } + + int numberOfAlignedSegments = blockCapacityInElements / bufferAlignmentInElements; + int bufferLength = numberOfAlignedSegments * bufferAlignmentInElements; + if (totalLengthInElements > 0 && totalLengthInElements < bufferLength) + { + bufferLength = (int)totalLengthInElements; + } + + int sizeOfLastBuffer = (int)(totalLengthInElements % bufferLength); + int bufferCount = (int)(totalLengthInElements / bufferLength); + + if (sizeOfLastBuffer == 0) + { + sizeOfLastBuffer = bufferLength; + } + else + { + bufferCount++; + } + + UnmanagedMemoryHandle[]? arrays = pool.Rent(bufferCount); + + if (arrays == null) + { + // Pool is full + memoryGroup = null; + return false; + } + + memoryGroup = new Owned(pool, arrays, bufferLength, totalLengthInElements, sizeOfLastBuffer, options); + return true; + } + + public static MemoryGroup Wrap(params Memory[] source) + { + int bufferLength = source.Length > 0 ? source[0].Length : 0; + for (int i = 1; i < source.Length - 1; i++) + { + if (source[i].Length != bufferLength) + { + throw new InvalidMemoryOperationException("Wrap: buffers should be uniformly sized!"); + } + } + + if (source.Length > 0 && source[^1].Length > bufferLength) + { + throw new InvalidMemoryOperationException("Wrap: the last buffer is too large!"); + } + + long totalLength = bufferLength > 0 ? ((long)bufferLength * (source.Length - 1)) + source[^1].Length : 0; + + return new Consumed(source, bufferLength, totalLength); + } + + public static MemoryGroup Wrap(params IMemoryOwner[] source) + { + int bufferLength = source.Length > 0 ? source[0].Memory.Length : 0; + for (int i = 1; i < source.Length - 1; i++) + { + if (source[i].Memory.Length != bufferLength) + { + throw new InvalidMemoryOperationException("Wrap: buffers should be uniformly sized!"); + } + } + + if (source.Length > 0 && source[^1].Memory.Length > bufferLength) + { + throw new InvalidMemoryOperationException("Wrap: the last buffer is too large!"); + } + + long totalLength = bufferLength > 0 ? ((long)bufferLength * (source.Length - 1)) + source[^1].Memory.Length : 0; + + return new Owned(source, bufferLength, totalLength, false); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public unsafe Span GetRowSpanCoreUnsafe(int y, int width) + { + switch (this.memoryGroupSpanCache.Mode) + { + case SpanCacheMode.SingleArray: + { + ref byte b0 = ref MemoryMarshal.GetReference(this.memoryGroupSpanCache.SingleArray); + ref T e0 = ref Unsafe.As(ref b0); + e0 = ref Unsafe.Add(ref e0, (uint)(y * width)); + return MemoryMarshal.CreateSpan(ref e0, width); + } + + case SpanCacheMode.SinglePointer: + { + void* start = Unsafe.Add(this.memoryGroupSpanCache.SinglePointer, y * width); + return new Span(start, width); + } + + case SpanCacheMode.MultiPointer: + { + this.GetMultiBufferPosition(y, width, out int bufferIdx, out int bufferStart); + void* start = Unsafe.Add(this.memoryGroupSpanCache.MultiPointer[bufferIdx], bufferStart); + return new Span(start, width); + } + + default: + { + this.GetMultiBufferPosition(y, width, out int bufferIdx, out int bufferStart); + return this[bufferIdx].Span.Slice(bufferStart, width); + } + } + } + + /// + /// Returns the slice of the buffer starting at global index that goes until the end of the buffer. + /// + public Span GetRemainingSliceOfBuffer(long start) + { + long bufferIdx = Math.DivRem(start, this.BufferLength, out long bufferStart); + Memory memory = this[(int)bufferIdx]; + return memory.Span[(int)bufferStart..]; + } + + public static bool CanSwapContent(MemoryGroup target, MemoryGroup source) => + source is Owned { Swappable: true } && target is Owned { Swappable: true }; + + public virtual void RecreateViewAfterSwap() + { + } + + public virtual void IncreaseRefCounts() + { + } + + public virtual void DecreaseRefCounts() + { + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void GetMultiBufferPosition(int y, int width, out int bufferIdx, out int bufferStart) + { + long start = y * (long)width; + long bufferIdxLong = Math.DivRem(start, this.BufferLength, out long bufferStartLong); + bufferIdx = (int)bufferIdxLong; + bufferStart = (int)bufferStartLong; + } + } +} diff --git a/ImageSharp/Memory/DiscontiguousBuffers/SpanCacheMode.cs b/ImageSharp/Memory/DiscontiguousBuffers/SpanCacheMode.cs new file mode 100644 index 0000000..d1fa250 --- /dev/null +++ b/ImageSharp/Memory/DiscontiguousBuffers/SpanCacheMode.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Memory { + /// + /// Selects active values in . + /// + internal enum SpanCacheMode + { + Default = default, + SingleArray, + SinglePointer, + MultiPointer + } +} diff --git a/ImageSharp/Memory/InvalidMemoryOperationException.cs b/ImageSharp/Memory/InvalidMemoryOperationException.cs new file mode 100644 index 0000000..b6e22bf --- /dev/null +++ b/ImageSharp/Memory/InvalidMemoryOperationException.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Memory { + /// + /// Exception thrown when the library detects an invalid memory allocation request, + /// or an attempt has been made to use an invalidated . + /// + public class InvalidMemoryOperationException : InvalidOperationException + { + /// + /// Initializes a new instance of the class. + /// + /// The exception message text. + public InvalidMemoryOperationException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + public InvalidMemoryOperationException() + { + } + + [DoesNotReturn] + internal static void ThrowNegativeAllocationException(long length) => + throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of negative length={length}."); + + [DoesNotReturn] + internal static void ThrowInvalidAlignmentException(long alignment) => + throw new InvalidMemoryOperationException( + $"The buffer capacity of the provided MemoryAllocator is insufficient for the requested buffer alignment: {alignment}."); + + [DoesNotReturn] + internal static void ThrowAllocationOverLimitException(ulong length, long limit) => + throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of length={length} that exceeded the limit {limit}."); + + [DoesNotReturn] + internal static void ThrowAccumulativeAllocationOverLimitException(long requestedLength, long totalLength, long limit) => + throw new InvalidMemoryOperationException( + $"Attempted to allocate a buffer of length={requestedLength} that would increase the accumulative allocation size to {totalLength}, exceeding the limit {limit}."); + } +} diff --git a/ImageSharp/Memory/MemoryAllocatorExtensions.cs b/ImageSharp/Memory/MemoryAllocatorExtensions.cs new file mode 100644 index 0000000..cab2bba --- /dev/null +++ b/ImageSharp/Memory/MemoryAllocatorExtensions.cs @@ -0,0 +1,138 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; + +namespace SixLabors.ImageSharp.Memory { + /// + /// Extension methods for . + /// + public static class MemoryAllocatorExtensions + { + /// + /// Allocates a buffer of value type objects interpreted as a 2D region + /// of x elements. + /// + /// The type of buffer items to allocate. + /// The memory allocator. + /// The buffer width. + /// The buffer height. + /// A value indicating whether the allocated buffer should be contiguous, unless bigger than . + /// The allocation options. + /// The . + public static Buffer2D Allocate2D( + this MemoryAllocator memoryAllocator, + int width, + int height, + bool preferContiguosImageBuffers, + AllocationOptions options = AllocationOptions.None) + where T : struct + { + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + + long groupLength = (long)width * height; + MemoryGroup memoryGroup; + if (preferContiguosImageBuffers && groupLength < int.MaxValue) + { + IMemoryOwner buffer = memoryAllocator.Allocate((int)groupLength, options); + memoryGroup = MemoryGroup.CreateContiguous(buffer, false); + } + else + { + memoryGroup = memoryAllocator.AllocateGroup(groupLength, width, options); + } + + return new Buffer2D(memoryGroup, width, height); + } + + /// + /// Allocates a buffer of value type objects interpreted as a 2D region + /// of x elements. + /// + /// The type of buffer items to allocate. + /// The memory allocator. + /// The buffer width. + /// The buffer height. + /// The allocation options. + /// The . + public static Buffer2D Allocate2D( + this MemoryAllocator memoryAllocator, + int width, + int height, + AllocationOptions options = AllocationOptions.None) + where T : struct => + Allocate2D(memoryAllocator, width, height, false, options); + + /// + /// Allocates a buffer of value type objects interpreted as a 2D region + /// of width x height elements. + /// + /// The type of buffer items to allocate. + /// The memory allocator. + /// The buffer size. + /// A value indicating whether the allocated buffer should be contiguous, unless bigger than . + /// The allocation options. + /// The . + public static Buffer2D Allocate2D( + this MemoryAllocator memoryAllocator, + Size size, + bool preferContiguosImageBuffers, + AllocationOptions options = AllocationOptions.None) + where T : struct => + Allocate2D(memoryAllocator, size.Width, size.Height, preferContiguosImageBuffers, options); + + /// + /// Allocates a buffer of value type objects interpreted as a 2D region + /// of width x height elements. + /// + /// The type of buffer items to allocate. + /// The memory allocator. + /// The buffer size. + /// The allocation options. + /// The . + public static Buffer2D Allocate2D( + this MemoryAllocator memoryAllocator, + Size size, + AllocationOptions options = AllocationOptions.None) + where T : struct => + Allocate2D(memoryAllocator, size.Width, size.Height, false, options); + + internal static Buffer2D Allocate2DOveraligned( + this MemoryAllocator memoryAllocator, + int width, + int height, + int alignmentMultiplier, + AllocationOptions options = AllocationOptions.None) + where T : struct + { + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + + long groupLength = (long)width * height; + MemoryGroup memoryGroup = memoryAllocator.AllocateGroup( + groupLength, + width * alignmentMultiplier, + options); + return new Buffer2D(memoryGroup, width, height); + } + + /// + /// Allocates padded buffers. Generally used by encoder/decoders. + /// + /// The . + /// Pixel count in the row + /// The pixel size in bytes, eg. 3 for RGB. + /// The padding. + /// A . + internal static IMemoryOwner AllocatePaddedPixelRowBuffer( + this MemoryAllocator memoryAllocator, + int width, + int pixelSizeInBytes, + int paddingInBytes) + { + int length = (width * pixelSizeInBytes) + paddingInBytes; + return memoryAllocator.Allocate(length); + } + } +} diff --git a/ImageSharp/Memory/MemoryOwnerExtensions.cs b/ImageSharp/Memory/MemoryOwnerExtensions.cs new file mode 100644 index 0000000..afc89f2 --- /dev/null +++ b/ImageSharp/Memory/MemoryOwnerExtensions.cs @@ -0,0 +1,84 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Memory { + /// + /// Extension methods for + /// + internal static class MemoryOwnerExtensions + { + /// + /// Gets a from an instance. + /// + /// The buffer + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span GetSpan(this IMemoryOwner buffer) + { + return buffer.Memory.Span; + } + + /// + /// Gets the length of an internal buffer. + /// + /// The buffer + /// The length of the buffer + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Length(this IMemoryOwner buffer) + { + return buffer.Memory.Length; + } + + /// + /// Gets a to an offsetted position inside the buffer. + /// + /// The buffer + /// The start + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span Slice(this IMemoryOwner buffer, int start) + { + return buffer.GetSpan()[start..]; + } + + /// + /// Gets a to an offsetted position inside the buffer. + /// + /// The buffer + /// The start + /// The length of the slice + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span Slice(this IMemoryOwner buffer, int start, int length) + { + return buffer.GetSpan().Slice(start, length); + } + + /// + /// Clears the contents of this buffer. + /// + /// The buffer + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Clear(this IMemoryOwner buffer) + { + buffer.GetSpan().Clear(); + } + + /// + /// Gets a reference to the first item in the internal buffer for an instance. + /// + /// The buffer + /// A reference to the first item within the memory wrapped by + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T GetReference(this IMemoryOwner buffer) + where T : struct + { + return ref MemoryMarshal.GetReference(buffer.GetSpan()); + } + } +} diff --git a/ImageSharp/Memory/RowInterval.cs b/ImageSharp/Memory/RowInterval.cs new file mode 100644 index 0000000..4ef403c --- /dev/null +++ b/ImageSharp/Memory/RowInterval.cs @@ -0,0 +1,87 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Memory { + /// + /// Represents an interval of rows in a and/or + /// + /// + /// Before RC1, this class might be target of API changes, use it on your own risk! + /// + public readonly struct RowInterval : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The inclusive minimum row. + /// The exclusive maximum row. + public RowInterval(int min, int max) + { + Guard.MustBeLessThan(min, max, nameof(min)); + + this.Min = min; + this.Max = max; + } + + /// + /// Gets the inclusive minimum row. + /// + public int Min { get; } + + /// + /// Gets the exclusive maximum row. + /// + public int Max { get; } + + /// + /// Gets the difference ( - ). + /// + public int Height => this.Max - this.Min; + + /// + /// Returns a boolean indicating whether the given two -s are equal. + /// + /// The first to compare. + /// The second to compare. + /// True if the given -s are equal; False otherwise. + public static bool operator ==(RowInterval left, RowInterval right) + { + return left.Equals(right); + } + + /// + /// Returns a boolean indicating whether the given two -s are not equal. + /// + /// The first to compare. + /// The second to compare. + /// True if the given -s are not equal; False otherwise. + public static bool operator !=(RowInterval left, RowInterval right) + { + return !left.Equals(right); + } + + /// + public bool Equals(RowInterval other) + { + return this.Min == other.Min && this.Max == other.Max; + } + + /// + public override bool Equals(object? obj) + { + return !ReferenceEquals(null, obj) && obj is RowInterval other && this.Equals(other); + } + + /// + public override int GetHashCode() => HashCode.Combine(this.Min, this.Max); + + /// + public override string ToString() => $"RowInterval [{this.Min}->{this.Max}]"; + + internal RowInterval Slice(int start) => new(this.Min + start, this.Max); + + internal RowInterval Slice(int start, int length) => new(this.Min + start, this.Min + start + length); + } +} diff --git a/ImageSharp/Memory/UnmanagedMemoryManager{T}.cs b/ImageSharp/Memory/UnmanagedMemoryManager{T}.cs new file mode 100644 index 0000000..cdf7c38 --- /dev/null +++ b/ImageSharp/Memory/UnmanagedMemoryManager{T}.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; + +namespace SixLabors.ImageSharp.Memory { + /// + /// A custom that can wrap a rawpointer to a buffer of a specified type. + /// + /// The value type to use when casting the wrapped instance. + /// This manager doesn't own the memory buffer that it points to. + internal sealed unsafe class UnmanagedMemoryManager : MemoryManager + where T : unmanaged + { + /// + /// The pointer to the memory buffer. + /// + private readonly void* pointer; + + /// + /// The length of the memory area. + /// + private readonly int length; + + /// + /// Initializes a new instance of the class. + /// + /// The pointer to the memory buffer. + /// The length of the memory area. + public UnmanagedMemoryManager(void* pointer, int length) + { + this.pointer = pointer; + this.length = length; + } + + /// + protected override void Dispose(bool disposing) + { + } + + /// + public override Span GetSpan() + { + return new Span(this.pointer, this.length); + } + + /// + public override MemoryHandle Pin(int elementIndex = 0) + { + return new MemoryHandle(((T*)this.pointer) + elementIndex, pinnable: this); + } + + /// + public override void Unpin() + { + } + } +} diff --git a/ImageSharp/Metadata/ImageFrameMetadata.cs b/ImageSharp/Metadata/ImageFrameMetadata.cs new file mode 100644 index 0000000..7af8131 --- /dev/null +++ b/ImageSharp/Metadata/ImageFrameMetadata.cs @@ -0,0 +1,180 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Numerics; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +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.Metadata { + /// + /// Encapsulates the metadata of an image frame. + /// + public sealed class ImageFrameMetadata : IDeepCloneable + { + private readonly Dictionary formatMetadata = []; + + /// + /// Initializes a new instance of the class. + /// + internal ImageFrameMetadata() + { + } + + /// + /// Initializes a new instance of the class + /// by making a copy from other metadata. + /// + /// + /// The other to create this instance from. + /// + internal ImageFrameMetadata(ImageFrameMetadata other) + { + DebugGuard.NotNull(other, nameof(other)); + + foreach (KeyValuePair meta in other.formatMetadata) + { + this.formatMetadata.Add(meta.Key, (IFormatFrameMetadata)meta.Value.DeepClone()); + } + + this.ExifProfile = other.ExifProfile?.DeepClone(); + this.IccProfile = other.IccProfile?.DeepClone(); + this.IptcProfile = other.IptcProfile?.DeepClone(); + this.XmpProfile = other.XmpProfile?.DeepClone(); + this.CicpProfile = other.CicpProfile?.DeepClone(); + + // NOTE: This clone is actually shallow but we share the same format + // instances for all images in the configuration. + this.DecodedImageFormat = other.DecodedImageFormat; + } + + /// + /// Gets or sets the Exif profile. + /// + public ExifProfile? ExifProfile { get; set; } + + /// + /// Gets or sets the XMP profile. + /// + public XmpProfile? XmpProfile { get; set; } + + /// + /// Gets or sets the ICC profile. + /// + public IccProfile? IccProfile { get; set; } + + /// + /// Gets or sets the iptc profile. + /// + public IptcProfile? IptcProfile { get; set; } + + /// + /// Gets or sets the CICP profile + /// + public CicpProfile? CicpProfile { get; set; } + + /// + /// Gets the original format, if any, the image was decode from. + /// + public IImageFormat? DecodedImageFormat { get; internal set; } + + /// + public ImageFrameMetadata DeepClone() => new(this); + + /// + /// Gets the metadata value associated with the specified key.
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The type of format metadata. + /// The type of format frame metadata. + /// The key of the value to get. + /// + /// The . + /// + public TFormatFrameMetadata GetFormatMetadata(IImageFormat key) + where TFormatMetadata : class + where TFormatFrameMetadata : class, IFormatFrameMetadata + { + if (this.formatMetadata.TryGetValue(key, out IFormatFrameMetadata? meta)) + { + return (TFormatFrameMetadata)meta; + } + + // None found. Check if we have a decoded format to convert from. + if (this.DecodedImageFormat is not null + && this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatFrameMetadata? decodedMetadata)) + { + TFormatFrameMetadata derivedMeta = TFormatFrameMetadata.FromFormatConnectingFrameMetadata(decodedMetadata.ToFormatConnectingFrameMetadata()); + this.SetFormatMetadata(key, derivedMeta); + return derivedMeta; + } + + TFormatFrameMetadata newMeta = key.CreateDefaultFormatFrameMetadata(); + this.SetFormatMetadata(key, newMeta); + return newMeta; + } + + /// + /// Sets the metadata value associated with the specified key. + /// + /// The type of format metadata. + /// The type of format frame metadata. + /// The key of the value to set. + /// The value to set. + public void SetFormatMetadata(IImageFormat key, TFormatFrameMetadata value) + where TFormatMetadata : class + where TFormatFrameMetadata : class, IFormatFrameMetadata + => this.formatMetadata[key] = value; + + /// + /// Creates a new instance the metadata value associated with the specified key. + /// The instance is created from a clone generated via . + /// + /// The type of metadata. + /// The type of format frame metadata. + /// The key of the value to get. + /// + /// The . + /// + public TFormatFrameMetadata CloneFormatMetadata(IImageFormat key) + where TFormatMetadata : class + where TFormatFrameMetadata : class, IFormatFrameMetadata + => ((IDeepCloneable)this.GetFormatMetadata(key)).DeepClone(); + + /// + /// Synchronizes the profiles with the current metadata. + /// + internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this); + + /// + /// This method is called after a process has been applied to the image frame. + /// + /// The type of pixel format. + /// The source image frame. + /// The destination image frame. + /// The transformation matrix applied to the frame. + internal void AfterFrameApply( + ImageFrame source, + ImageFrame destination, + Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + // Always updated using the full frame dimensions. + // Individual format frame metadata will update with sub region dimensions if appropriate. + this.ExifProfile?.SyncDimensions(destination.Width, destination.Height); + this.ExifProfile?.SyncSubject(destination.Width, destination.Height, matrix); + + foreach (KeyValuePair meta in this.formatMetadata) + { + meta.Value.AfterFrameApply(source, destination, matrix); + } + } + } +} diff --git a/ImageSharp/Metadata/ImageMetadata.cs b/ImageSharp/Metadata/ImageMetadata.cs new file mode 100644 index 0000000..7924003 --- /dev/null +++ b/ImageSharp/Metadata/ImageMetadata.cs @@ -0,0 +1,265 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Numerics; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +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.Metadata { + /// + /// Encapsulates the metadata of an image. + /// + public sealed class ImageMetadata : IDeepCloneable + { + /// + /// The default horizontal resolution value (dots per inch) in x direction. + /// The default value is 96 . + /// + public const double DefaultHorizontalResolution = 96; + + /// + /// The default vertical resolution value (dots per inch) in y direction. + /// The default value is 96 . + /// + public const double DefaultVerticalResolution = 96; + + /// + /// The default pixel resolution units. + /// The default value is . + /// + public const PixelResolutionUnit DefaultPixelResolutionUnits = PixelResolutionUnit.PixelsPerInch; + + private readonly Dictionary formatMetadata = []; + private double horizontalResolution; + private double verticalResolution; + + /// + /// Initializes a new instance of the class. + /// + public ImageMetadata() + { + this.horizontalResolution = DefaultHorizontalResolution; + this.verticalResolution = DefaultVerticalResolution; + this.ResolutionUnits = DefaultPixelResolutionUnits; + } + + /// + /// Initializes a new instance of the class + /// by making a copy from other metadata. + /// + /// + /// The other to create this instance from. + /// + private ImageMetadata(ImageMetadata other) + { + this.HorizontalResolution = other.HorizontalResolution; + this.VerticalResolution = other.VerticalResolution; + this.ResolutionUnits = other.ResolutionUnits; + + foreach (KeyValuePair meta in other.formatMetadata) + { + this.formatMetadata.Add(meta.Key, (IFormatMetadata)meta.Value.DeepClone()); + } + + this.ExifProfile = other.ExifProfile?.DeepClone(); + this.IccProfile = other.IccProfile?.DeepClone(); + this.IptcProfile = other.IptcProfile?.DeepClone(); + this.XmpProfile = other.XmpProfile?.DeepClone(); + this.CicpProfile = other.CicpProfile?.DeepClone(); + + // NOTE: This clone is actually shallow but we share the same format + // instances for all images in the configuration. + this.DecodedImageFormat = other.DecodedImageFormat; + } + + /// + /// Gets or sets the resolution of the image in x- direction. + /// It is defined as the number of dots per and should be an positive value. + /// + /// The density of the image in x- direction. + public double HorizontalResolution + { + get => this.horizontalResolution; + + set + { + if (value > 0) + { + this.horizontalResolution = value; + } + } + } + + /// + /// Gets or sets the resolution of the image in y- direction. + /// It is defined as the number of dots per and should be an positive value. + /// + /// The density of the image in y- direction. + public double VerticalResolution + { + get => this.verticalResolution; + + set + { + if (value > 0) + { + this.verticalResolution = value; + } + } + } + + /// + /// Gets or sets unit of measure used when reporting resolution. + /// + /// + /// Value + /// Unit + /// + /// + /// AspectRatio (00) + /// No units; width:height pixel aspect ratio = Ydensity:Xdensity + /// + /// + /// PixelsPerInch (01) + /// Pixels per inch (2.54 cm) + /// + /// + /// PixelsPerCentimeter (02) + /// Pixels per centimeter + /// + /// + /// PixelsPerMeter (03) + /// Pixels per meter (100 cm) + /// + /// + /// + public PixelResolutionUnit ResolutionUnits { get; set; } + + /// + /// Gets or sets the Exif profile. + /// + public ExifProfile? ExifProfile { get; set; } + + /// + /// Gets or sets the XMP profile. + /// + public XmpProfile? XmpProfile { get; set; } + + /// + /// Gets or sets the ICC profile. + /// + public IccProfile? IccProfile { get; set; } + + /// + /// Gets or sets the IPTC profile. + /// + public IptcProfile? IptcProfile { get; set; } + + /// + /// Gets or sets the CICP profile. + /// + public CicpProfile? CicpProfile { get; set; } + + /// + /// Gets the original format, if any, from which the image was decoded. + /// + public IImageFormat? DecodedImageFormat { get; internal set; } + + /// + /// Gets the metadata value associated with the specified key.
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The type of metadata. + /// The key of the value to get. + /// + /// The . + /// + public TFormatMetadata GetFormatMetadata(IImageFormat key) + where TFormatMetadata : class, IFormatMetadata + { + // Check for existing metadata. + if (this.formatMetadata.TryGetValue(key, out IFormatMetadata? meta)) + { + return (TFormatMetadata)meta; + } + + // None found. Check if we have a decoded format to convert from. + if (this.DecodedImageFormat is not null + && this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatMetadata? decodedMetadata)) + { + TFormatMetadata derivedMeta = TFormatMetadata.FromFormatConnectingMetadata(decodedMetadata.ToFormatConnectingMetadata()); + this.formatMetadata[key] = derivedMeta; + return derivedMeta; + } + + // Fall back to a default instance. + TFormatMetadata newMeta = key.CreateDefaultFormatMetadata(); + this.formatMetadata[key] = newMeta; + return newMeta; + } + + /// + /// Creates a new instance the metadata value associated with the specified key. + /// The instance is created from a clone generated via . + /// + /// The type of metadata. + /// The key of the value to get. + /// + /// The . + /// + public TFormatMetadata CloneFormatMetadata(IImageFormat key) + where TFormatMetadata : class, IFormatMetadata + => ((IDeepCloneable)this.GetFormatMetadata(key)).DeepClone(); + + internal void SetFormatMetadata(IImageFormat key, TFormatMetadata value) + where TFormatMetadata : class, IFormatMetadata + => this.formatMetadata[key] = value; + + /// + public ImageMetadata DeepClone() => new(this); + + /// + /// Synchronizes the profiles with the current metadata. + /// + internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this); + + /// + /// This method is called after a process has been applied to the image. + /// + /// The type of pixel format. + /// The destination image. + /// The transformation matrix applied to the image. + internal void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + this.ExifProfile?.SyncDimensions(destination.Width, destination.Height); + this.ExifProfile?.SyncSubject(destination.Width, destination.Height, matrix); + + foreach (KeyValuePair meta in this.formatMetadata) + { + meta.Value.AfterImageApply(destination, matrix); + } + } + + internal PixelTypeInfo GetDecodedPixelTypeInfo() + { + // None found. Check if we have a decoded format to convert from. + if (this.DecodedImageFormat is not null + && this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatMetadata? decodedMetadata)) + { + return decodedMetadata.GetPixelTypeInfo(); + } + + // This should never happen. + return default; + } + } +} diff --git a/ImageSharp/Metadata/PixelResolutionUnit.cs b/ImageSharp/Metadata/PixelResolutionUnit.cs new file mode 100644 index 0000000..835201e --- /dev/null +++ b/ImageSharp/Metadata/PixelResolutionUnit.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata { + /// + /// Provides enumeration of available pixel density units. + /// + public enum PixelResolutionUnit : byte + { + /// + /// No units; width:height pixel aspect ratio. + /// + AspectRatio = 0, + + /// + /// Pixels per inch (2.54 cm). + /// + PixelsPerInch = 1, + + /// + /// Pixels per centimeter. + /// + PixelsPerCentimeter = 2, + + /// + /// Pixels per meter (100 cm). + /// + PixelsPerMeter = 3 + } +} diff --git a/ImageSharp/Metadata/Profiles/CICP/CicpProfile.cs b/ImageSharp/Metadata/Profiles/CICP/CicpProfile.cs new file mode 100644 index 0000000..4d13c66 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/CICP/CicpProfile.cs @@ -0,0 +1,74 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Cicp { + /// + /// Represents a Cicp profile as per ITU-T H.273 / ISO/IEC 23091-2_2019 providing access to color space information + /// + public sealed class CicpProfile : IDeepCloneable + { + /// + /// Initializes a new instance of the class. + /// + public CicpProfile() + : this(2, 2, 2, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The color primaries as number according to ITU-T H.273 / ISO/IEC 23091-2_2019. + /// The transfer characteristics as number according to ITU-T H.273 / ISO/IEC 23091-2_2019. + /// The matrix coefficients as number according to ITU-T H.273 / ISO/IEC 23091-2_2019. + /// The full range flag, or null if unknown. + public CicpProfile(byte colorPrimaries, byte transferCharacteristics, byte matrixCoefficients, bool? fullRange) + { + this.ColorPrimaries = Enum.IsDefined(typeof(CicpColorPrimaries), colorPrimaries) ? (CicpColorPrimaries)colorPrimaries : CicpColorPrimaries.Unspecified; + this.TransferCharacteristics = Enum.IsDefined(typeof(CicpTransferCharacteristics), transferCharacteristics) ? (CicpTransferCharacteristics)transferCharacteristics : CicpTransferCharacteristics.Unspecified; + this.MatrixCoefficients = Enum.IsDefined(typeof(CicpMatrixCoefficients), matrixCoefficients) ? (CicpMatrixCoefficients)matrixCoefficients : CicpMatrixCoefficients.Unspecified; + this.FullRange = fullRange ?? (this.MatrixCoefficients == CicpMatrixCoefficients.Identity); + } + + /// + /// Initializes a new instance of the class + /// by making a copy from another CICP profile. + /// + /// The other CICP profile, where the clone should be made from. + /// is null.> + private CicpProfile(CicpProfile other) + { + Guard.NotNull(other, nameof(other)); + + this.ColorPrimaries = other.ColorPrimaries; + this.TransferCharacteristics = other.TransferCharacteristics; + this.MatrixCoefficients = other.MatrixCoefficients; + this.FullRange = other.FullRange; + } + + /// + /// Gets or sets the color primaries + /// + public CicpColorPrimaries ColorPrimaries { get; set; } + + /// + /// Gets or sets the transfer characteristics + /// + public CicpTransferCharacteristics TransferCharacteristics { get; set; } + + /// + /// Gets or sets the matrix coefficients + /// + public CicpMatrixCoefficients MatrixCoefficients { get; set; } + + /// + /// Gets or sets a value indicating whether the colors use the full numeric range + /// + public bool FullRange { get; set; } + + /// + public CicpProfile DeepClone() => new(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/CICP/Enums/CicpColorPrimaries.cs b/ImageSharp/Metadata/Profiles/CICP/Enums/CicpColorPrimaries.cs new file mode 100644 index 0000000..4a694ec --- /dev/null +++ b/ImageSharp/Metadata/Profiles/CICP/Enums/CicpColorPrimaries.cs @@ -0,0 +1,86 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Cicp { +#pragma warning disable CA1707 // Underscores in enum members + + /// + /// Color primaries according to ITU-T H.273 / ISO/IEC 23091-2_2019 subclause 8.1 + /// + public enum CicpColorPrimaries : byte + { + /// + /// Rec. ITU-R BT.709-6 + /// IEC 61966-2-1 sRGB or sYCC + /// IEC 61966-2-4 + /// SMPTE RP 177 (1993) Annex B + /// + ItuRBt709_6 = 1, + + /// + /// Image characteristics are unknown or are determined by the application. + /// + Unspecified = 2, + + /// + /// Rec. ITU-R BT.470-6 System M (historical) + /// + ItuRBt470_6M = 4, + + /// + /// Rec. ITU-R BT.601-7 625 + /// Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM + /// + ItuRBt601_7_625 = 5, + + /// + /// Rec. ITU-R BT.601-7 525 + /// Rec. ITU-R BT.1700-0 NTSC + /// SMPTE ST 170 (2004) + /// (functionally the same as the value 7) + /// + ItuRBt601_7_525 = 6, + + /// + /// SMPTE ST 240 (1999) + /// (functionally the same as the value 6) + /// + SmpteSt240 = 7, + + /// + /// Generic film (colour filters using Illuminant C) + /// + GenericFilm = 8, + + /// + /// Rec. ITU-R BT.2020-2 + /// Rec. ITU-R BT.2100-2 + /// + ItuRBt2020_2 = 9, + + /// + /// SMPTE ST 428-1 (2019) + /// (CIE 1931 XYZ as in ISO 11664-1) + /// + SmpteSt428_1 = 10, + + /// + /// SMPTE RP 431-2 (2011) + /// DCI P3 + /// + SmpteRp431_2 = 11, + + /// + /// SMPTE ST 432-1 (2010) + /// P3 D65 / Display P3 + /// + SmpteEg432_1 = 12, + + /// + /// EBU Tech.3213-E + /// + EbuTech3213E = 22, + } + +#pragma warning restore CA1707 // Underscores in enum members +} diff --git a/ImageSharp/Metadata/Profiles/CICP/Enums/CicpMatrixCoefficients.cs b/ImageSharp/Metadata/Profiles/CICP/Enums/CicpMatrixCoefficients.cs new file mode 100644 index 0000000..b68e00a --- /dev/null +++ b/ImageSharp/Metadata/Profiles/CICP/Enums/CicpMatrixCoefficients.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Cicp { +#pragma warning disable CA1707 // Underscores in enum members + + /// + /// Matrix coefficients according to ITU-T H.273 / ISO/IEC 23091-2_2019 subclause 8.3 + /// + public enum CicpMatrixCoefficients : byte + { + /// + /// The identity matrix. + /// IEC 61966-2-1 sRGB + /// SMPTE ST 428-1 (2019) + /// + Identity = 0, + + /// + /// Rec. ITU-R BT.709-6 + /// IEC 61966-2-4 xvYCC709 + /// SMPTE RP 177 (1993) Annex B + /// + ItuRBt709_6 = 1, + + /// + /// Image characteristics are unknown or are determined by the application. + /// + Unspecified = 2, + + /// + /// FCC Title 47 Code of Federal Regulations 73.682 (a) (20) + /// + Fcc47 = 4, + + /// + /// Rec. ITU-R BT.601-7 625 + /// Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM + /// IEC 61966-2-1 sYCC + /// IEC 61966-2-4 xvYCC601 + /// (functionally the same as the value 6) + /// + ItuRBt601_7_625 = 5, + + /// + /// Rec. ITU-R BT.601-7 525 + /// Rec. ITU-R BT.1700-0 NTSC + /// SMPTE ST 170 (2004) + /// (functionally the same as the value 5) + /// + ItuRBt601_7_525 = 6, + + /// + /// SMPTE ST 240 (1999) + /// + SmpteSt240 = 7, + + /// + /// YCgCo + /// + YCgCo = 8, + + /// + /// Rec. ITU-R BT.2020-2 (non-constant luminance) + /// Rec. ITU-R BT.2100-2 Y′CbCr + /// + ItuRBt2020_2_Ncl = 9, + + /// + /// Rec. ITU-R BT.2020-2 (constant luminance) + /// + ItuRBt2020_2_Cl = 10, + + /// + /// SMPTE ST 2085 (2015) + /// + SmpteSt2085 = 11, + + /// + /// Chromaticity-derived non-constant luminance system + /// + ChromaDerivedNcl = 12, + + /// + /// Chromaticity-derived constant luminance system + /// + ChromaDerivedCl = 13, + + /// + /// Rec. ITU-R BT.2100-2 ICtCp + /// + ICtCp = 14, + } + +#pragma warning restore CA1707 // Underscores in enum members +} diff --git a/ImageSharp/Metadata/Profiles/CICP/Enums/CicpTransferCharacteristics.cs b/ImageSharp/Metadata/Profiles/CICP/Enums/CicpTransferCharacteristics.cs new file mode 100644 index 0000000..295de96 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/CICP/Enums/CicpTransferCharacteristics.cs @@ -0,0 +1,109 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Cicp { +#pragma warning disable CA1707 // Underscores in enum values + + /// + /// Transfer characteristics according to ITU-T H.273 / ISO/IEC 23091-2_2019 subclause 8.2 + /// /// + public enum CicpTransferCharacteristics : byte + { + /// + /// Rec. ITU-R BT.709-6 + /// (functionally the same as the values 6, 14 and 15) + /// + ItuRBt709_6 = 1, + + /// + /// Image characteristics are unknown or are determined by the application. + /// + Unspecified = 2, + + /// + /// Assumed display gamma 2.2 + /// Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM + /// + Gamma2_2 = 4, + + /// + /// Assumed display gamma 2.8 + /// Rec. ITU-R BT.470-6 System B, G (historical) + /// + Gamma2_8 = 5, + + /// + /// Rec. ITU-R BT.601-7 525 or 625 + /// Rec. ITU-R BT.1700-0 NTSC + /// SMPTE ST 170 (2004) + /// (functionally the same as the values 1, 14 and 15) + /// + ItuRBt601_7 = 6, + + /// + /// SMPTE ST 240 (1999) + /// + SmpteSt240 = 7, + + /// + /// Linear transfer characteristics + /// + Linear = 8, + + /// + /// Logarithmic transfer characteristic (100:1 range) + /// + Log100 = 9, + + /// + /// Logarithmic transfer characteristic (100 * Sqrt( 10 ) : 1 range) + /// + Log100Sqrt = 10, + + /// + /// IEC 61966-2-4 + /// + Iec61966_2_4 = 11, + + /// + /// Rec. ITU-R BT.1361-0 extended colour gamut system (historical) + /// + ItuRBt1361_0 = 12, + + /// + /// IEC 61966-2-1 sRGB or sYCC / Display P3 + /// + Iec61966_2_1 = 13, + + /// + /// Rec. ITU-R BT.2020-2 (10-bit system) + /// (functionally the same as the values 1, 6 and 15) + /// + ItuRBt2020_2_10bit = 14, + + /// + /// Rec. ITU-R BT.2020-2 (12-bit system) + /// (functionally the same as the values 1, 6 and 14) + /// /// + ItuRBt2020_2_12bit = 15, + + /// + /// SMPTE ST 2084 (2014) for 10-, 12-, 14- and 16-bit systems + /// Rec. ITU-R BT.2100-2 perceptual quantization (PQ) system + /// + SmpteSt2084 = 16, + + /// + /// SMPTE ST 428-1 (2019) + /// + SmpteSt428_1 = 17, + + /// + /// ARIB STD-B67 (2015) + /// Rec. ITU-R BT.2100-2 hybrid log-gamma (HLG) system + /// + AribStdB67 = 18, + } + +#pragma warning restore CA1707 // Underscores in enum members +} diff --git a/ImageSharp/Metadata/Profiles/CICP/T-REC-H.273-202107-S!!PDF-E.pdf b/ImageSharp/Metadata/Profiles/CICP/T-REC-H.273-202107-S!!PDF-E.pdf new file mode 100644 index 0000000..12086dd Binary files /dev/null and b/ImageSharp/Metadata/Profiles/CICP/T-REC-H.273-202107-S!!PDF-E.pdf differ diff --git a/ImageSharp/Metadata/Profiles/Exif/DC-X008-Translation-2019-E.pdf b/ImageSharp/Metadata/Profiles/Exif/DC-X008-Translation-2019-E.pdf new file mode 100644 index 0000000..22a1058 Binary files /dev/null and b/ImageSharp/Metadata/Profiles/Exif/DC-X008-Translation-2019-E.pdf differ diff --git a/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs b/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs new file mode 100644 index 0000000..54a2131 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Text; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal static class ExifConstants + { + public static ReadOnlySpan LittleEndianByteOrderMarker => + [ + (byte)'I', + (byte)'I', + 0x2A, + 0x00 + ]; + + public static ReadOnlySpan BigEndianByteOrderMarker => + [ + (byte)'M', + (byte)'M', + 0x00, + 0x2A + ]; + + // UTF-8 is better than ASCII, UTF-8 encodes the ASCII codes the same way + public static Encoding DefaultEncoding => Encoding.UTF8; + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs b/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs new file mode 100644 index 0000000..6d9968b --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs @@ -0,0 +1,99 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + /// Specifies Exif data types. + /// + public enum ExifDataType + { + /// + /// Unknown + /// + Unknown = 0, + + /// + /// An 8-bit unsigned integer. + /// + Byte = 1, + + /// + /// An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL. + /// + /// Although the standard defines ASCII this has commonly been ignored as + /// ASCII cannot properly encode text in many languages. + /// + /// + Ascii = 2, + + /// + /// A 16-bit (2-byte) unsigned integer. + /// + Short = 3, + + /// + /// A 32-bit (4-byte) unsigned integer. + /// + Long = 4, + + /// + /// Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator. + /// + Rational = 5, + + /// + /// An 8-bit signed integer. + /// + SignedByte = 6, + + /// + /// An 8-bit byte that can take any value depending on the field definition. + /// + Undefined = 7, + + /// + /// A 16-bit (2-byte) signed integer. + /// + SignedShort = 8, + + /// + /// A 32-bit (4-byte) signed integer (2's complement notation). + /// + SignedLong = 9, + + /// + /// Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator. + /// + SignedRational = 10, + + /// + /// A 32-bit single precision floating point value. + /// + SingleFloat = 11, + + /// + /// A 64-bit double precision floating point value. + /// + DoubleFloat = 12, + + /// + /// Reference to an IFD (32-bit (4-byte) unsigned integer). + /// + Ifd = 13, + + /// + /// A 64-bit (8-byte) unsigned integer. + /// + Long8 = 16, + + /// + /// A 64-bit (8-byte) signed integer (2's complement notation). + /// + SignedLong8 = 17, + + /// + /// Reference to an IFD (64-bit (8-byte) unsigned integer). + /// + Ifd8 = 18, + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs b/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs new file mode 100644 index 0000000..57c4988 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal static class ExifDataTypes + { + /// + /// Gets the size in bytes of the given data type. + /// + /// The data type. + /// + /// The . + /// + /// + /// Thrown if the type is unsupported. + /// + public static uint GetSize(ExifDataType dataType) + { + switch (dataType) + { + case ExifDataType.Ascii: + case ExifDataType.Byte: + case ExifDataType.SignedByte: + case ExifDataType.Undefined: + return 1; + case ExifDataType.Short: + case ExifDataType.SignedShort: + return 2; + case ExifDataType.Long: + case ExifDataType.SignedLong: + case ExifDataType.SingleFloat: + case ExifDataType.Ifd: + return 4; + case ExifDataType.DoubleFloat: + case ExifDataType.Rational: + case ExifDataType.SignedRational: + case ExifDataType.Long8: + case ExifDataType.SignedLong8: + case ExifDataType.Ifd8: + return 8; + default: + throw new NotSupportedException(dataType.ToString()); + } + } + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs b/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs new file mode 100644 index 0000000..f24c020 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs @@ -0,0 +1,142 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.Text; +using static SixLabors.ImageSharp.Metadata.Profiles.Exif.EncodedString; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal static class ExifEncodedStringHelpers + { + public const int CharacterCodeBytesLength = 8; + + private const ulong AsciiCode = 0x_00_00_00_49_49_43_53_41; + private const ulong JISCode = 0x_00_00_00_00_00_53_49_4A; + private const ulong UnicodeCode = 0x_45_44_4F_43_49_4E_55; + private const ulong UndefinedCode = 0x_00_00_00_00_00_00_00_00; + + private static ReadOnlySpan AsciiCodeBytes => [0x41, 0x53, 0x43, 0x49, 0x49, 0, 0, 0]; + + private static ReadOnlySpan JISCodeBytes => [0x4A, 0x49, 0x53, 0, 0, 0, 0, 0]; + + private static ReadOnlySpan UnicodeCodeBytes => [0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0]; + + private static ReadOnlySpan UndefinedCodeBytes => [0, 0, 0, 0, 0, 0, 0, 0]; + + // 20932 EUC-JP Japanese (JIS 0208-1990 and 0212-1990) + // https://docs.microsoft.com/en-us/dotnet/api/system.text.encoding?view=net-6.0 + private static Encoding JIS0208Encoding + { + get + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + return Encoding.GetEncoding(20932); + } + } + + public static bool IsEncodedString(ExifTagValue tag) => tag switch + { + ExifTagValue.UserComment or ExifTagValue.GPSProcessingMethod or ExifTagValue.GPSAreaInformation => true, + _ => false + }; + + public static ReadOnlySpan GetCodeBytes(CharacterCode code) => code switch + { + CharacterCode.ASCII => AsciiCodeBytes, + CharacterCode.JIS => JISCodeBytes, + CharacterCode.Unicode => UnicodeCodeBytes, + CharacterCode.Undefined => UndefinedCodeBytes, + _ => UndefinedCodeBytes + }; + + public static Encoding GetEncoding(CharacterCode code, ByteOrder order) => code switch + { + CharacterCode.ASCII => Encoding.ASCII, + CharacterCode.JIS => JIS0208Encoding, + CharacterCode.Unicode => order is ByteOrder.BigEndian ? Encoding.BigEndianUnicode : Encoding.Unicode, + CharacterCode.Undefined => Encoding.UTF8, + _ => Encoding.UTF8 + }; + + public static bool TryParse(ReadOnlySpan buffer, ByteOrder order, out EncodedString encodedString) + { + if (TryDetect(buffer, out CharacterCode code)) + { + ReadOnlySpan textBuffer = buffer[CharacterCodeBytesLength..]; + if (code == CharacterCode.Unicode && textBuffer.Length >= 2) + { + // Check BOM + if (textBuffer.StartsWith((ReadOnlySpan)[0xFF, 0xFE])) + { + // Little-endian BOM + string text = Encoding.Unicode.GetString(textBuffer[2..]); + encodedString = new EncodedString(code, text); + return true; + } + + if (textBuffer.StartsWith((ReadOnlySpan)[0xFE, 0xFF])) + { + // Big-endian BOM + string text = Encoding.BigEndianUnicode.GetString(textBuffer[2..]); + encodedString = new EncodedString(code, text); + return true; + } + } + + { + string text = GetEncoding(code, order).GetString(textBuffer); + encodedString = new EncodedString(code, text); + return true; + } + } + + encodedString = default; + return false; + } + + public static uint GetDataLength(EncodedString encodedString) => + (uint)GetEncoding(encodedString.Code, ByteOrder.LittleEndian).GetByteCount(encodedString.Text) + CharacterCodeBytesLength; + + public static int Write(EncodedString encodedString, Span destination) + { + GetCodeBytes(encodedString.Code).CopyTo(destination); + + string text = encodedString.Text; + int count = Write(GetEncoding(encodedString.Code, ByteOrder.LittleEndian), text, destination[CharacterCodeBytesLength..]); + + return CharacterCodeBytesLength + count; + } + + public static unsafe int Write(Encoding encoding, string value, Span destination) + => encoding.GetBytes(value.AsSpan(), destination); + + private static bool TryDetect(ReadOnlySpan buffer, out CharacterCode code) + { + if (buffer.Length >= CharacterCodeBytesLength) + { + switch (BinaryPrimitives.ReadUInt64LittleEndian(buffer)) + { + case AsciiCode: + code = CharacterCode.ASCII; + return true; + case JISCode: + code = CharacterCode.JIS; + return true; + case UnicodeCode: + code = CharacterCode.Unicode; + return true; + case UndefinedCode: + code = CharacterCode.Undefined; + return true; + default: + code = default; + return false; + } + } + + code = default; + return false; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/ExifParts.cs b/ImageSharp/Metadata/Profiles/Exif/ExifParts.cs new file mode 100644 index 0000000..3855d59 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/ExifParts.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + /// Specifies which parts will be written when the profile is added to an image. + /// + [Flags] + public enum ExifParts + { + /// + /// None + /// + None = 0, + + /// + /// IfdTags + /// + IfdTags = 1, + + /// + /// ExifTags + /// + ExifTags = 2, + + /// + /// GPSTags + /// + GpsTags = 4, + + /// + /// All + /// + All = IfdTags | ExifTags | GpsTags + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs b/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs new file mode 100644 index 0000000..83b86d0 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs @@ -0,0 +1,419 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + /// Represents an EXIF profile providing access to the collection of values. + /// + public sealed class ExifProfile : IDeepCloneable + { + /// + /// The byte array to read the EXIF profile from. + /// + private readonly byte[]? data; + + /// + /// The collection of EXIF values + /// + private List? values; + + /// + /// The thumbnail offset position in the byte stream + /// + private int thumbnailOffset; + + /// + /// The thumbnail length in the byte stream + /// + private int thumbnailLength; + + /// + /// Initializes a new instance of the class. + /// + public ExifProfile() + : this((byte[]?)null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The byte array to read the EXIF profile from. + public ExifProfile(byte[]? data) + { + this.Parts = ExifParts.All; + this.data = data; + this.InvalidTags = []; + } + + /// + /// Initializes a new instance of the class. + /// + /// The values. + /// The invalid tags. + internal ExifProfile(List values, IReadOnlyList invalidTags) + { + this.Parts = ExifParts.All; + this.values = values; + this.InvalidTags = invalidTags; + } + + /// + /// Initializes a new instance of the class + /// by making a copy from another EXIF profile. + /// + /// The other EXIF profile, where the clone should be made from. + /// is null.> + private ExifProfile(ExifProfile other) + { + Guard.NotNull(other, nameof(other)); + + this.Parts = other.Parts; + this.thumbnailLength = other.thumbnailLength; + this.thumbnailOffset = other.thumbnailOffset; + + this.InvalidTags = other.InvalidTags.Count > 0 + ? new List(other.InvalidTags) + : Array.Empty(); + + if (other.values != null) + { + this.values = new List(other.Values.Count); + + foreach (IExifValue value in other.Values) + { + this.values.Add(value.DeepClone()); + } + } + + if (other.data != null) + { + this.data = new byte[other.data.Length]; + other.data.AsSpan().CopyTo(this.data); + } + } + + /// + /// Gets or sets which parts will be written when the profile is added to an image. + /// + public ExifParts Parts { get; set; } + + /// + /// Gets the tags that where found but contained an invalid value. + /// + public IReadOnlyList InvalidTags { get; private set; } + + /// + /// Gets the values of this EXIF profile. + /// + [MemberNotNull(nameof(values))] + public IReadOnlyList Values + { + get + { + this.InitializeValues(); + return this.values; + } + } + + /// + /// Returns the thumbnail in the EXIF profile when available. + /// + /// The thumbnail + /// + /// True, if there is a thumbnail otherwise false. + /// + public bool TryCreateThumbnail([NotNullWhen(true)] out Image? image) + { + if (this.TryCreateThumbnail(out Image? innerimage)) + { + image = innerimage; + return true; + } + + image = null; + return false; + } + + /// + /// Returns the thumbnail in the EXIF profile when available. + /// + /// The pixel format. + /// The thumbnail. + /// True, if there is a thumbnail otherwise false. + public bool TryCreateThumbnail([NotNullWhen(true)] out Image? image) + where TPixel : unmanaged, IPixel + { + this.InitializeValues(); + image = null; + if (this.thumbnailOffset == 0 || this.thumbnailLength == 0) + { + return false; + } + + if (this.data is null || this.data.Length < (this.thumbnailOffset + this.thumbnailLength)) + { + return false; + } + + using MemoryStream memStream = new(this.data, this.thumbnailOffset, this.thumbnailLength); + image = Image.Load(memStream); + return true; + } + + /// + /// Returns the value with the specified tag. + /// + /// The tag of the Exif value. + /// The value with the specified tag. + /// True when found, otherwise false + /// The data type of the tag. + public bool TryGetValue(ExifTag tag, [NotNullWhen(true)] out IExifValue? exifValue) + { + IExifValue? value = this.GetValueInternal(tag); + + if (value is null) + { + exifValue = null; + return false; + } + + exifValue = (IExifValue)value; + return true; + } + + /// + /// Removes the value with the specified tag. + /// + /// The tag of the EXIF value. + /// + /// True, if the value was removed, otherwise false. + /// + public bool RemoveValue(ExifTag tag) + { + this.InitializeValues(); + + for (int i = 0; i < this.values.Count; i++) + { + if (this.values[i].Tag == tag) + { + this.values.RemoveAt(i); + return true; + } + } + + return false; + } + + /// + /// Sets the value of the specified tag. + /// + /// The tag of the Exif value. + /// The value. + /// The data type of the tag. + public void SetValue(ExifTag tag, TValueType value) + => this.SetValueInternal(tag, value); + + /// + /// Converts this instance to a byte array. + /// + /// The + public byte[]? ToByteArray() + { + if (this.values is null) + { + return this.data; + } + + if (this.values.Count == 0) + { + return []; + } + + ExifWriter writer = new(this.values, this.Parts); + return writer.GetData(); + } + + /// + public ExifProfile DeepClone() => new(this); + + /// + /// Returns the value with the specified tag. + /// + /// The tag of the Exif value. + /// The value with the specified tag. + internal IExifValue? GetValueInternal(ExifTag tag) + { + foreach (IExifValue exifValue in this.Values) + { + if (exifValue.Tag == tag) + { + return exifValue; + } + } + + return null; + } + + /// + /// Sets the value of the specified tag. + /// + /// The tag of the Exif value. + /// The value. + /// The newly created value is null. + internal void SetValueInternal(ExifTag tag, object? value) + { + foreach (IExifValue exifValue in this.Values) + { + if (exifValue.Tag == tag) + { + exifValue.TrySetValue(value); + return; + } + } + + ExifValue? newExifValue = ExifValues.Create(tag) ?? throw new NotSupportedException($"Newly created value for tag {tag} is null."); + + newExifValue.TrySetValue(value); + this.values.Add(newExifValue); + } + + /// + /// Synchronizes the profiles with the specified metadata. + /// + /// The metadata. + internal void Sync(ImageMetadata metadata) + { + this.SyncResolution(ExifTag.XResolution, metadata.HorizontalResolution); + this.SyncResolution(ExifTag.YResolution, metadata.VerticalResolution); + } + + internal void SyncDimensions(int width, int height) + { + if (this.TryGetValue(ExifTag.PixelXDimension, out _)) + { + this.SetValue(ExifTag.PixelXDimension, width); + } + + if (this.TryGetValue(ExifTag.PixelYDimension, out _)) + { + this.SetValue(ExifTag.PixelYDimension, height); + } + } + + internal void SyncSubject(int width, int height, Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + return; + } + + if (this.TryGetValue(ExifTag.SubjectLocation, out IExifValue? location)) + { + if (location.Value?.Length == 2) + { + Vector2 point = TransformUtilities.ProjectiveTransform2D(location.Value[0], location.Value[1], matrix); + + // Ensure the point is within the image dimensions. + point = Vector2.Clamp(point, Vector2.Zero, new Vector2(width - 1, height - 1)); + + // Floor the point to the nearest pixel. + location.Value[0] = (ushort)Math.Floor(point.X); + location.Value[1] = (ushort)Math.Floor(point.Y); + + this.SetValue(ExifTag.SubjectLocation, location.Value); + } + else + { + this.RemoveValue(ExifTag.SubjectLocation); + } + } + + if (this.TryGetValue(ExifTag.SubjectArea, out IExifValue? area)) + { + if (area.Value?.Length == 4) + { + RectangleF rectangle = new(area.Value[0], area.Value[1], area.Value[2], area.Value[3]); + if (!TransformUtilities.TryGetTransformedRectangle(rectangle, matrix, out RectangleF bounds)) + { + return; + } + + // Ensure the bounds are within the image dimensions. + bounds = RectangleF.Intersect(bounds, new Rectangle(0, 0, width, height)); + + area.Value[0] = (ushort)MathF.Floor(bounds.X); + area.Value[1] = (ushort)MathF.Floor(bounds.Y); + area.Value[2] = (ushort)MathF.Ceiling(bounds.Width); + area.Value[3] = (ushort)MathF.Ceiling(bounds.Height); + this.SetValue(ExifTag.SubjectArea, area.Value); + } + else + { + this.RemoveValue(ExifTag.SubjectArea); + } + } + } + + /// + /// Synchronizes the profiles with the specified metadata. + /// + /// The metadata. +#pragma warning disable CA1822, RCS1163, IDE0060 + internal void Sync(ImageFrameMetadata metadata) +#pragma warning restore IDE0060, RCS1163, CA1822 + { + // Nothing to do ....YET. + } + + private void SyncResolution(ExifTag tag, double resolution) + { + if (!this.TryGetValue(tag, out IExifValue? value)) + { + return; + } + + if (value.IsArray || value.DataType != ExifDataType.Rational) + { + this.RemoveValue(value.Tag); + } + + Rational newResolution = new(resolution, false); + this.SetValue(tag, newResolution); + } + + [MemberNotNull(nameof(values))] + private void InitializeValues() + { + if (this.values != null) + { + return; + } + + if (this.data is null) + { + this.values = []; + return; + } + + ExifReader reader = new(this.data); + + this.values = reader.ReadValues(); + + this.InvalidTags = reader.InvalidTags.Count > 0 + ? new List(reader.InvalidTags) + : Array.Empty(); + + this.thumbnailOffset = (int)reader.ThumbnailOffset; + this.thumbnailLength = (int)reader.ThumbnailLength; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs b/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs new file mode 100644 index 0000000..673bf44 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs @@ -0,0 +1,716 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal class ExifReader : BaseExifReader + { + public ExifReader(byte[] exifData) + : this(exifData, null) + { + } + + public ExifReader(byte[] exifData, MemoryAllocator? allocator) + : base(new MemoryStream(exifData ?? throw new ArgumentNullException(nameof(exifData))), allocator) + { + // TODO: We never call this constructor passing a non-null allocator. + } + + /// + /// Reads and returns the collection of EXIF values. + /// + /// + /// The . + /// + public List ReadValues() + { + List values = []; + + // II == 0x4949 + this.IsBigEndian = this.ReadUInt16() != 0x4949; + + if (this.ReadUInt16() != 0x002A) + { + return values; + } + + uint ifdOffset = this.ReadUInt32(); + this.ReadValues(values, ifdOffset); + + uint thumbnailOffset = this.ReadUInt32(); + this.GetThumbnail(thumbnailOffset); + + this.ReadSubIfd(values); + + this.ReadBigValues(values); + + return values; + } + + private void GetThumbnail(uint offset) + { + if (offset == 0) + { + return; + } + + List values = []; + this.ReadValues(values, offset); + + for (int i = 0; i < values.Count; i++) + { + ExifValue value = (ExifValue)values[i]; + if (value == ExifTag.JPEGInterchangeFormat) + { + this.ThumbnailOffset = ((ExifLong)value).Value; + } + else if (value == ExifTag.JPEGInterchangeFormatLength) + { + this.ThumbnailLength = ((ExifLong)value).Value; + } + } + } + } + + /// + /// Reads and parses EXIF data from a stream. + /// + internal abstract class BaseExifReader + { + private readonly MemoryAllocator? allocator; + private readonly Stream data; + private List? invalidTags; + private List? subIfds; + private bool isBigEndian; + + protected BaseExifReader(Stream stream, MemoryAllocator? allocator) + { + this.data = stream ?? throw new ArgumentNullException(nameof(stream)); + this.allocator = allocator; + } + + private delegate TDataType ConverterMethod(ReadOnlySpan data); + + /// + /// Gets the invalid tags. + /// + public IReadOnlyList InvalidTags => this.invalidTags ?? (IReadOnlyList)[]; + + /// + /// Gets or sets the thumbnail length in the byte stream. + /// + public uint ThumbnailLength { get; protected set; } + + /// + /// Gets or sets the thumbnail offset position in the byte stream. + /// + public uint ThumbnailOffset { get; protected set; } + + public bool IsBigEndian + { + get => this.isBigEndian; + protected set + { + this.isBigEndian = value; + this.ByteOrder = value ? ByteOrder.BigEndian : ByteOrder.LittleEndian; + } + } + + protected ByteOrder ByteOrder { get; private set; } + + public List<(ulong Offset, ExifDataType DataType, ulong NumberOfComponents, ExifValue Exif)> BigValues { get; } = []; + + protected void ReadBigValues(List values) + { + if (this.BigValues.Count == 0) + { + return; + } + + int maxSize = 0; + foreach ((ulong offset, ExifDataType dataType, ulong numberOfComponents, ExifValue exif) in this.BigValues) + { + ulong size = numberOfComponents * ExifDataTypes.GetSize(dataType); + DebugGuard.MustBeLessThanOrEqualTo(size, int.MaxValue, nameof(size)); + + if ((int)size > maxSize) + { + maxSize = (int)size; + } + } + + if (this.allocator != null) + { + // tiff, bigTiff + using IMemoryOwner memory = this.allocator.Allocate(maxSize); + Span buf = memory.GetSpan(); + foreach ((ulong Offset, ExifDataType DataType, ulong NumberOfComponents, ExifValue Exif) tag in this.BigValues) + { + ulong size = tag.NumberOfComponents * ExifDataTypes.GetSize(tag.DataType); + this.ReadBigValue(values, tag, buf[..(int)size]); + } + } + else + { + // embedded exif + Span buf = maxSize <= 256 ? stackalloc byte[256] : new byte[maxSize]; + foreach ((ulong Offset, ExifDataType DataType, ulong NumberOfComponents, ExifValue Exif) tag in this.BigValues) + { + ulong size = tag.NumberOfComponents * ExifDataTypes.GetSize(tag.DataType); + this.ReadBigValue(values, tag, buf[..(int)size]); + } + } + + this.BigValues.Clear(); + } + + /// + /// Reads the values to the values collection. + /// + /// The values. + /// The IFD offset. + protected void ReadValues(List values, uint offset) + { + if (offset > this.data.Length) + { + return; + } + + this.Seek(offset); + int count = this.ReadUInt16(); + + Span offsetBuffer = stackalloc byte[4]; + for (int i = 0; i < count; i++) + { + this.ReadValue(values, offsetBuffer); + } + } + + protected void ReadSubIfd(List values) + { + if (this.subIfds != null) + { + const int maxSubIfds = 8; + const int maxNestingLevel = 8; + Span buf = stackalloc ulong[maxSubIfds]; + for (int i = 0; i < maxNestingLevel && this.subIfds.Count > 0; i++) + { + int sz = Math.Min(this.subIfds.Count, maxSubIfds); + CollectionsMarshal.AsSpan(this.subIfds)[..sz].CopyTo(buf); + + this.subIfds.Clear(); + foreach (ulong subIfdOffset in buf[..sz]) + { + this.ReadValues(values, (uint)subIfdOffset); + } + } + } + } + + protected void ReadValues64(List values, ulong offset) + { + DebugGuard.MustBeLessThanOrEqualTo(offset, (ulong)this.data.Length, "By spec UInt64.MaxValue is supported, but .NET Stream.Length can Int64.MaxValue."); + + this.Seek(offset); + ulong count = this.ReadUInt64(); + + Span offsetBuffer = stackalloc byte[8]; + for (ulong i = 0; i < count; i++) + { + this.ReadValue64(values, offsetBuffer); + } + } + + protected void ReadBigValue(IList values, (ulong Offset, ExifDataType DataType, ulong NumberOfComponents, ExifValue Exif) tag, Span buffer) + { + this.Seek(tag.Offset); + if (this.TryReadSpan(buffer)) + { + object? value = this.ConvertValue(tag.DataType, buffer, tag.NumberOfComponents > 1 || tag.Exif.IsArray); + this.Add(values, tag.Exif, value); + } + } + + private static TDataType[] ToArray(ExifDataType dataType, ReadOnlySpan data, ConverterMethod converter) + { + int dataTypeSize = (int)ExifDataTypes.GetSize(dataType); + int length = data.Length / dataTypeSize; + + TDataType[] result = new TDataType[length]; + + for (int i = 0; i < length; i++) + { + ReadOnlySpan buffer = data.Slice(i * dataTypeSize, dataTypeSize); + + result.SetValue(converter(buffer), i); + } + + return result; + } + + private static string ConvertToString(Encoding encoding, ReadOnlySpan buffer) + { + int nullCharIndex = buffer.IndexOf((byte)0); + + if (nullCharIndex > -1) + { + buffer = buffer[..nullCharIndex]; + } + + return encoding.GetString(buffer); + } + + private static byte ConvertToByte(ReadOnlySpan buffer) => buffer[0]; + + private object? ConvertValue(ExifDataType dataType, ReadOnlySpan buffer, bool isArray) + { + if (buffer.Length == 0) + { + return null; + } + + switch (dataType) + { + case ExifDataType.Unknown: + return null; + case ExifDataType.Ascii: + return ConvertToString(ExifConstants.DefaultEncoding, buffer); + case ExifDataType.Byte: + case ExifDataType.Undefined: + if (!isArray) + { + return ConvertToByte(buffer); + } + + return buffer.ToArray(); + case ExifDataType.DoubleFloat: + if (!isArray) + { + return this.ConvertToDouble(buffer); + } + + return ToArray(dataType, buffer, this.ConvertToDouble); + case ExifDataType.Long: + case ExifDataType.Ifd: + if (!isArray) + { + return this.ConvertToUInt32(buffer); + } + + return ToArray(dataType, buffer, this.ConvertToUInt32); + case ExifDataType.Rational: + if (!isArray) + { + return this.ToRational(buffer); + } + + return ToArray(dataType, buffer, this.ToRational); + case ExifDataType.Short: + if (!isArray) + { + return this.ConvertToShort(buffer); + } + + return ToArray(dataType, buffer, this.ConvertToShort); + case ExifDataType.SignedByte: + if (!isArray) + { + return this.ConvertToSignedByte(buffer); + } + + return ToArray(dataType, buffer, this.ConvertToSignedByte); + case ExifDataType.SignedLong: + if (!isArray) + { + return this.ConvertToInt32(buffer); + } + + return ToArray(dataType, buffer, this.ConvertToInt32); + case ExifDataType.SignedRational: + if (!isArray) + { + return this.ToSignedRational(buffer); + } + + return ToArray(dataType, buffer, this.ToSignedRational); + case ExifDataType.SignedShort: + if (!isArray) + { + return this.ConvertToSignedShort(buffer); + } + + return ToArray(dataType, buffer, this.ConvertToSignedShort); + case ExifDataType.SingleFloat: + if (!isArray) + { + return this.ConvertToSingle(buffer); + } + + return ToArray(dataType, buffer, this.ConvertToSingle); + case ExifDataType.Long8: + case ExifDataType.Ifd8: + if (!isArray) + { + return this.ConvertToUInt64(buffer); + } + + return ToArray(dataType, buffer, this.ConvertToUInt64); + case ExifDataType.SignedLong8: + if (!isArray) + { + return this.ConvertToInt64(buffer); + } + + return ToArray(dataType, buffer, this.ConvertToUInt64); + + default: + throw new NotSupportedException($"Data type {dataType} is not supported."); + } + } + + private void ReadValue(List values, Span offsetBuffer) + { + // 2 | 2 | 4 | 4 + // tag | type | count | value offset + if ((this.data.Length - this.data.Position) < 12) + { + return; + } + + ExifTagValue tag = (ExifTagValue)this.ReadUInt16(); + ExifDataType dataType = EnumUtils.Parse(this.ReadUInt16(), ExifDataType.Unknown); + + uint numberOfComponents = this.ReadUInt32(); + + this.TryReadSpan(offsetBuffer); + + // Ensure that the data type is valid + if (dataType == ExifDataType.Unknown) + { + return; + } + + // Issue #132: ExifDataType == Undefined is treated like a byte array. + // If numberOfComponents == 0 this value can only be handled as an inline value and must fallback to 4 (bytes) + if (numberOfComponents == 0) + { + numberOfComponents = 4 / ExifDataTypes.GetSize(dataType); + } + + ExifValue? exifValue = ExifValues.Create(tag) ?? ExifValues.Create(tag, dataType, numberOfComponents); + + if (exifValue is null) + { + this.AddInvalidTag(new UnkownExifTag(tag)); + return; + } + + uint size = numberOfComponents * ExifDataTypes.GetSize(dataType); + if (size > 4) + { + uint newOffset = this.ConvertToUInt32(offsetBuffer); + + // Ensure that the new index does not overrun the data. + if (newOffset > int.MaxValue || (newOffset + size) > this.data.Length) + { + this.AddInvalidTag(new UnkownExifTag(tag)); + return; + } + + this.BigValues.Add((newOffset, dataType, numberOfComponents, exifValue)); + } + else + { + object? value = this.ConvertValue(dataType, offsetBuffer[..(int)size], numberOfComponents > 1 || exifValue.IsArray); + this.Add(values, exifValue, value); + } + } + + private void ReadValue64(List values, Span offsetBuffer) + { + if ((this.data.Length - this.data.Position) < 20) + { + return; + } + + ExifTagValue tag = (ExifTagValue)this.ReadUInt16(); + ExifDataType dataType = EnumUtils.Parse(this.ReadUInt16(), ExifDataType.Unknown); + + ulong numberOfComponents = this.ReadUInt64(); + + this.TryReadSpan(offsetBuffer); + + if (dataType == ExifDataType.Unknown) + { + return; + } + + if (numberOfComponents == 0) + { + numberOfComponents = 8 / ExifDataTypes.GetSize(dataType); + } + + ExifValue? exifValue = tag switch + { + ExifTagValue.StripOffsets => new ExifLong8Array(ExifTagValue.StripOffsets), + ExifTagValue.StripByteCounts => new ExifLong8Array(ExifTagValue.StripByteCounts), + ExifTagValue.TileOffsets => new ExifLong8Array(ExifTagValue.TileOffsets), + ExifTagValue.TileByteCounts => new ExifLong8Array(ExifTagValue.TileByteCounts), + _ => ExifValues.Create(tag) ?? ExifValues.Create(tag, dataType, numberOfComponents), + }; + + if (exifValue is null) + { + this.AddInvalidTag(new UnkownExifTag(tag)); + return; + } + + ulong size = numberOfComponents * ExifDataTypes.GetSize(dataType); + if (size > 8) + { + ulong newOffset = this.ConvertToUInt64(offsetBuffer); + if (newOffset > ulong.MaxValue || newOffset > ((ulong)this.data.Length - size)) + { + this.AddInvalidTag(new UnkownExifTag(tag)); + return; + } + + this.BigValues.Add((newOffset, dataType, numberOfComponents, exifValue)); + } + else + { + object? value = this.ConvertValue(dataType, offsetBuffer[..(int)size], numberOfComponents > 1 || exifValue.IsArray); + this.Add(values, exifValue, value); + } + } + + private void Add(IList values, ExifValue exif, object? value) + { + if (exif is ExifEncodedString encodedString) + { + if (!encodedString.TrySetValue(value, this.ByteOrder)) + { + return; + } + } + else if (!exif.TrySetValue(value)) + { + return; + } + + foreach (IExifValue val in values) + { + // To skip duplicates must be used Equals method, + // == operator not defined for ExifValue and IExifValue + if (exif.Equals(val)) + { + Debug.WriteLine($"Duplicate Exif tag: tag={exif.Tag}, dataType={exif.DataType}"); + return; + } + } + + if (exif.Tag == ExifTag.SubIFDOffset) + { + this.AddSubIfd(value); + } + else if (exif.Tag == ExifTag.GPSIFDOffset) + { + this.AddSubIfd(value); + } + else + { + values.Add(exif); + } + } + + private void AddInvalidTag(ExifTag tag) + => (this.invalidTags ??= []).Add(tag); + + private void AddSubIfd(object? val) + => (this.subIfds ??= []).Add(Convert.ToUInt64(val, CultureInfo.InvariantCulture)); + + private void Seek(ulong pos) + => this.data.Seek((long)pos, SeekOrigin.Begin); + + private bool TryReadSpan(Span span) + { + int length = span.Length; + if ((this.data.Length - this.data.Position) < length) + { + return false; + } + + int read = this.data.Read(span); + return read == length; + } + + protected ulong ReadUInt64() + { + Span buffer = stackalloc byte[8]; + + return this.TryReadSpan(buffer) + ? this.ConvertToUInt64(buffer) + : default; + } + + // Known as Long in Exif Specification. + protected uint ReadUInt32() + { + Span buffer = stackalloc byte[4]; + + return this.TryReadSpan(buffer) + ? this.ConvertToUInt32(buffer) + : default; + } + + protected ushort ReadUInt16() + { + Span buffer = stackalloc byte[2]; + + return this.TryReadSpan(buffer) + ? this.ConvertToShort(buffer) + : default; + } + + private long ConvertToInt64(ReadOnlySpan buffer) + { + if (buffer.Length < 8) + { + return default; + } + + return this.IsBigEndian + ? BinaryPrimitives.ReadInt64BigEndian(buffer) + : BinaryPrimitives.ReadInt64LittleEndian(buffer); + } + + private ulong ConvertToUInt64(ReadOnlySpan buffer) + { + if (buffer.Length < 8) + { + return default; + } + + return this.IsBigEndian + ? BinaryPrimitives.ReadUInt64BigEndian(buffer) + : BinaryPrimitives.ReadUInt64LittleEndian(buffer); + } + + private double ConvertToDouble(ReadOnlySpan buffer) + { + if (buffer.Length < 8) + { + return default; + } + + long intValue = this.IsBigEndian + ? BinaryPrimitives.ReadInt64BigEndian(buffer) + : BinaryPrimitives.ReadInt64LittleEndian(buffer); + + return Unsafe.As(ref intValue); + } + + private uint ConvertToUInt32(ReadOnlySpan buffer) + { + // Known as Long in Exif Specification. + if (buffer.Length < 4) + { + return default; + } + + return this.IsBigEndian + ? BinaryPrimitives.ReadUInt32BigEndian(buffer) + : BinaryPrimitives.ReadUInt32LittleEndian(buffer); + } + + private ushort ConvertToShort(ReadOnlySpan buffer) + { + if (buffer.Length < 2) + { + return default; + } + + return this.IsBigEndian + ? BinaryPrimitives.ReadUInt16BigEndian(buffer) + : BinaryPrimitives.ReadUInt16LittleEndian(buffer); + } + + private float ConvertToSingle(ReadOnlySpan buffer) + { + if (buffer.Length < 4) + { + return default; + } + + int intValue = this.IsBigEndian + ? BinaryPrimitives.ReadInt32BigEndian(buffer) + : BinaryPrimitives.ReadInt32LittleEndian(buffer); + + return Unsafe.As(ref intValue); + } + + private Rational ToRational(ReadOnlySpan buffer) + { + if (buffer.Length < 8) + { + return default; + } + + uint numerator = this.ConvertToUInt32(buffer[..4]); + uint denominator = this.ConvertToUInt32(buffer.Slice(4, 4)); + + return new Rational(numerator, denominator, false); + } + + private sbyte ConvertToSignedByte(ReadOnlySpan buffer) => unchecked((sbyte)buffer[0]); + + private int ConvertToInt32(ReadOnlySpan buffer) // SignedLong in Exif Specification + { + if (buffer.Length < 4) + { + return default; + } + + return this.IsBigEndian + ? BinaryPrimitives.ReadInt32BigEndian(buffer) + : BinaryPrimitives.ReadInt32LittleEndian(buffer); + } + + private SignedRational ToSignedRational(ReadOnlySpan buffer) + { + if (buffer.Length < 8) + { + return default; + } + + int numerator = this.ConvertToInt32(buffer[..4]); + int denominator = this.ConvertToInt32(buffer.Slice(4, 4)); + + return new SignedRational(numerator, denominator, false); + } + + private short ConvertToSignedShort(ReadOnlySpan buffer) + { + if (buffer.Length < 2) + { + return default; + } + + return this.IsBigEndian + ? BinaryPrimitives.ReadInt16BigEndian(buffer) + : BinaryPrimitives.ReadInt16LittleEndian(buffer); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/ExifTagDescriptionAttribute.cs b/ImageSharp/Metadata/Profiles/Exif/ExifTagDescriptionAttribute.cs new file mode 100644 index 0000000..595ec34 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/ExifTagDescriptionAttribute.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + /// Class that provides a description for an ExifTag value. + /// + [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] + internal sealed class ExifTagDescriptionAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The value of the exif tag. + /// The description for the value of the exif tag. + public ExifTagDescriptionAttribute(object value, string description) + { + } + + /// + /// Gets the tag description from any custom attributes. + /// + /// The tag. + /// The value. + /// The description. + /// + /// True when description was found + /// + public static bool TryGetDescription(ExifTag tag, object? value, [NotNullWhen(true)] out string? description) + { + ExifTagValue tagValue = (ExifTagValue)(ushort)tag; + FieldInfo? field = typeof(ExifTagValue).GetField(tagValue.ToString(), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + + description = null; + + if (field is null) + { + return false; + } + + foreach (CustomAttributeData customAttribute in field.CustomAttributes) + { + object? attributeValue = customAttribute.ConstructorArguments[0].Value; + + if (Equals(attributeValue, value)) + { + description = (string?)customAttribute.ConstructorArguments[1].Value; + + return description is not null; + } + } + + return false; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/ExifTags.cs b/ImageSharp/Metadata/Profiles/Exif/ExifTags.cs new file mode 100644 index 0000000..ff99a30 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/ExifTags.cs @@ -0,0 +1,274 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal static class ExifTags + { + public static ExifParts GetPart(ExifTag tag) + { + switch ((ExifTagValue)(ushort)tag) + { + case ExifTagValue.SubfileType: + case ExifTagValue.OldSubfileType: + case ExifTagValue.ImageWidth: + case ExifTagValue.ImageLength: + case ExifTagValue.BitsPerSample: + case ExifTagValue.Compression: + case ExifTagValue.PhotometricInterpretation: + case ExifTagValue.Thresholding: + case ExifTagValue.CellWidth: + case ExifTagValue.CellLength: + case ExifTagValue.FillOrder: + case ExifTagValue.DocumentName: + case ExifTagValue.ImageDescription: + case ExifTagValue.Make: + case ExifTagValue.Model: + case ExifTagValue.StripOffsets: + case ExifTagValue.Orientation: + case ExifTagValue.SamplesPerPixel: + case ExifTagValue.RowsPerStrip: + case ExifTagValue.StripByteCounts: + case ExifTagValue.MinSampleValue: + case ExifTagValue.MaxSampleValue: + case ExifTagValue.XResolution: + case ExifTagValue.YResolution: + case ExifTagValue.PlanarConfiguration: + case ExifTagValue.PageName: + case ExifTagValue.XPosition: + case ExifTagValue.YPosition: + case ExifTagValue.FreeOffsets: + case ExifTagValue.FreeByteCounts: + case ExifTagValue.GrayResponseUnit: + case ExifTagValue.GrayResponseCurve: + case ExifTagValue.T4Options: + case ExifTagValue.T6Options: + case ExifTagValue.ResolutionUnit: + case ExifTagValue.PageNumber: + case ExifTagValue.ColorResponseUnit: + case ExifTagValue.TransferFunction: + case ExifTagValue.Software: + case ExifTagValue.DateTime: + case ExifTagValue.Artist: + case ExifTagValue.HostComputer: + case ExifTagValue.Predictor: + case ExifTagValue.WhitePoint: + case ExifTagValue.PrimaryChromaticities: + case ExifTagValue.ColorMap: + case ExifTagValue.HalftoneHints: + case ExifTagValue.TileWidth: + case ExifTagValue.TileLength: + case ExifTagValue.TileOffsets: + case ExifTagValue.TileByteCounts: + case ExifTagValue.BadFaxLines: + case ExifTagValue.CleanFaxData: + case ExifTagValue.ConsecutiveBadFaxLines: + case ExifTagValue.InkSet: + case ExifTagValue.InkNames: + case ExifTagValue.NumberOfInks: + case ExifTagValue.DotRange: + case ExifTagValue.TargetPrinter: + case ExifTagValue.ExtraSamples: + case ExifTagValue.SampleFormat: + case ExifTagValue.SMinSampleValue: + case ExifTagValue.SMaxSampleValue: + case ExifTagValue.TransferRange: + case ExifTagValue.ClipPath: + case ExifTagValue.XClipPathUnits: + case ExifTagValue.YClipPathUnits: + case ExifTagValue.Indexed: + case ExifTagValue.JPEGTables: + case ExifTagValue.OPIProxy: + case ExifTagValue.ProfileType: + case ExifTagValue.FaxProfile: + case ExifTagValue.CodingMethods: + case ExifTagValue.VersionYear: + case ExifTagValue.ModeNumber: + case ExifTagValue.Decode: + case ExifTagValue.DefaultImageColor: + case ExifTagValue.T82ptions: + case ExifTagValue.JPEGProc: + case ExifTagValue.JPEGInterchangeFormat: + case ExifTagValue.JPEGInterchangeFormatLength: + case ExifTagValue.JPEGRestartInterval: + case ExifTagValue.JPEGLosslessPredictors: + case ExifTagValue.JPEGPointTransforms: + case ExifTagValue.JPEGQTables: + case ExifTagValue.JPEGDCTables: + case ExifTagValue.JPEGACTables: + case ExifTagValue.YCbCrCoefficients: + case ExifTagValue.YCbCrPositioning: + case ExifTagValue.YCbCrSubsampling: + case ExifTagValue.ReferenceBlackWhite: + case ExifTagValue.StripRowCounts: + case ExifTagValue.XMP: + case ExifTagValue.Rating: + case ExifTagValue.RatingPercent: + case ExifTagValue.ImageID: + case ExifTagValue.CFARepeatPatternDim: + case ExifTagValue.CFAPattern2: + case ExifTagValue.BatteryLevel: + case ExifTagValue.Copyright: + case ExifTagValue.MDFileTag: + case ExifTagValue.MDScalePixel: + case ExifTagValue.MDLabName: + case ExifTagValue.MDSampleInfo: + case ExifTagValue.MDPrepDate: + case ExifTagValue.MDPrepTime: + case ExifTagValue.MDFileUnits: + case ExifTagValue.PixelScale: + case ExifTagValue.IntergraphPacketData: + case ExifTagValue.IntergraphRegisters: + case ExifTagValue.IntergraphMatrix: + case ExifTagValue.ModelTiePoint: + case ExifTagValue.SEMInfo: + case ExifTagValue.ModelTransform: + case ExifTagValue.ImageLayer: + case ExifTagValue.FaxRecvParams: + case ExifTagValue.FaxSubaddress: + case ExifTagValue.FaxRecvTime: + case ExifTagValue.ImageSourceData: + case ExifTagValue.XPTitle: + case ExifTagValue.XPComment: + case ExifTagValue.XPAuthor: + case ExifTagValue.XPKeywords: + case ExifTagValue.XPSubject: + case ExifTagValue.GDALMetadata: + case ExifTagValue.GDALNoData: + return ExifParts.IfdTags; + + case ExifTagValue.ExposureTime: + case ExifTagValue.FNumber: + case ExifTagValue.ExposureProgram: + case ExifTagValue.SpectralSensitivity: + case ExifTagValue.ISOSpeedRatings: + case ExifTagValue.OECF: + case ExifTagValue.Interlace: + case ExifTagValue.TimeZoneOffset: + case ExifTagValue.SelfTimerMode: + case ExifTagValue.SensitivityType: + case ExifTagValue.StandardOutputSensitivity: + case ExifTagValue.RecommendedExposureIndex: + case ExifTagValue.ISOSpeed: + case ExifTagValue.ISOSpeedLatitudeyyy: + case ExifTagValue.ISOSpeedLatitudezzz: + case ExifTagValue.ExifVersion: + case ExifTagValue.DateTimeOriginal: + case ExifTagValue.DateTimeDigitized: + case ExifTagValue.OffsetTime: + case ExifTagValue.OffsetTimeOriginal: + case ExifTagValue.OffsetTimeDigitized: + case ExifTagValue.ComponentsConfiguration: + case ExifTagValue.CompressedBitsPerPixel: + case ExifTagValue.ShutterSpeedValue: + case ExifTagValue.ApertureValue: + case ExifTagValue.BrightnessValue: + case ExifTagValue.ExposureBiasValue: + case ExifTagValue.MaxApertureValue: + case ExifTagValue.SubjectDistance: + case ExifTagValue.MeteringMode: + case ExifTagValue.LightSource: + case ExifTagValue.Flash: + case ExifTagValue.FocalLength: + case ExifTagValue.FlashEnergy2: + case ExifTagValue.SpatialFrequencyResponse2: + case ExifTagValue.Noise: + case ExifTagValue.FocalPlaneXResolution2: + case ExifTagValue.FocalPlaneYResolution2: + case ExifTagValue.FocalPlaneResolutionUnit2: + case ExifTagValue.ImageNumber: + case ExifTagValue.SecurityClassification: + case ExifTagValue.ImageHistory: + case ExifTagValue.SubjectArea: + case ExifTagValue.ExposureIndex2: + case ExifTagValue.TIFFEPStandardID: + case ExifTagValue.SensingMethod2: + case ExifTagValue.MakerNote: + case ExifTagValue.UserComment: + case ExifTagValue.SubsecTime: + case ExifTagValue.SubsecTimeOriginal: + case ExifTagValue.SubsecTimeDigitized: + case ExifTagValue.AmbientTemperature: + case ExifTagValue.Humidity: + case ExifTagValue.Pressure: + case ExifTagValue.WaterDepth: + case ExifTagValue.Acceleration: + case ExifTagValue.CameraElevationAngle: + case ExifTagValue.FlashpixVersion: + case ExifTagValue.ColorSpace: + case ExifTagValue.PixelXDimension: + case ExifTagValue.PixelYDimension: + case ExifTagValue.RelatedSoundFile: + case ExifTagValue.FlashEnergy: + case ExifTagValue.SpatialFrequencyResponse: + case ExifTagValue.FocalPlaneXResolution: + case ExifTagValue.FocalPlaneYResolution: + case ExifTagValue.FocalPlaneResolutionUnit: + case ExifTagValue.SubjectLocation: + case ExifTagValue.ExposureIndex: + case ExifTagValue.SensingMethod: + case ExifTagValue.FileSource: + case ExifTagValue.SceneType: + case ExifTagValue.CFAPattern: + case ExifTagValue.CustomRendered: + case ExifTagValue.ExposureMode: + case ExifTagValue.WhiteBalance: + case ExifTagValue.DigitalZoomRatio: + case ExifTagValue.FocalLengthIn35mmFilm: + case ExifTagValue.SceneCaptureType: + case ExifTagValue.GainControl: + case ExifTagValue.Contrast: + case ExifTagValue.Saturation: + case ExifTagValue.Sharpness: + case ExifTagValue.DeviceSettingDescription: + case ExifTagValue.SubjectDistanceRange: + case ExifTagValue.ImageUniqueID: + case ExifTagValue.OwnerName: + case ExifTagValue.SerialNumber: + case ExifTagValue.LensSpecification: + case ExifTagValue.LensMake: + case ExifTagValue.LensModel: + case ExifTagValue.LensSerialNumber: + return ExifParts.ExifTags; + + case ExifTagValue.GPSVersionID: + case ExifTagValue.GPSLatitudeRef: + case ExifTagValue.GPSLatitude: + case ExifTagValue.GPSLongitudeRef: + case ExifTagValue.GPSLongitude: + case ExifTagValue.GPSAltitudeRef: + case ExifTagValue.GPSAltitude: + case ExifTagValue.GPSTimestamp: + case ExifTagValue.GPSSatellites: + case ExifTagValue.GPSStatus: + case ExifTagValue.GPSMeasureMode: + case ExifTagValue.GPSDOP: + case ExifTagValue.GPSSpeedRef: + case ExifTagValue.GPSSpeed: + case ExifTagValue.GPSTrackRef: + case ExifTagValue.GPSTrack: + case ExifTagValue.GPSImgDirectionRef: + case ExifTagValue.GPSImgDirection: + case ExifTagValue.GPSMapDatum: + case ExifTagValue.GPSDestLatitudeRef: + case ExifTagValue.GPSDestLatitude: + case ExifTagValue.GPSDestLongitudeRef: + case ExifTagValue.GPSDestLongitude: + case ExifTagValue.GPSDestBearingRef: + case ExifTagValue.GPSDestBearing: + case ExifTagValue.GPSDestDistanceRef: + case ExifTagValue.GPSDestDistance: + case ExifTagValue.GPSProcessingMethod: + case ExifTagValue.GPSAreaInformation: + case ExifTagValue.GPSDateStamp: + case ExifTagValue.GPSDifferential: + return ExifParts.GpsTags; + + case ExifTagValue.Unknown: + case ExifTagValue.SubIFDOffset: + case ExifTagValue.GPSIFDOffset: + default: + return ExifParts.None; + } + } + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/ExifUcs2StringHelpers.cs b/ImageSharp/Metadata/Profiles/Exif/ExifUcs2StringHelpers.cs new file mode 100644 index 0000000..e9bf552 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/ExifUcs2StringHelpers.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Text; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal static class ExifUcs2StringHelpers + { + public static Encoding Ucs2Encoding => Encoding.GetEncoding("UCS-2"); + + public static bool IsUcs2Tag(ExifTagValue tag) => tag switch + { + ExifTagValue.XPAuthor or ExifTagValue.XPComment or ExifTagValue.XPKeywords or ExifTagValue.XPSubject or ExifTagValue.XPTitle => true, + _ => false, + }; + + public static int Write(string value, Span destination) => ExifEncodedStringHelpers.Write(Ucs2Encoding, value, destination); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs b/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs new file mode 100644 index 0000000..7b83d59 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs @@ -0,0 +1,454 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + /// Contains methods for writing EXIF metadata. + /// + internal sealed class ExifWriter + { + /// + /// Which parts will be written. + /// + private readonly ExifParts allowedParts; + private readonly IList values; + private List? dataOffsets; + private readonly List ifdValues; + private readonly List exifValues; + private readonly List gpsValues; + + /// + /// Initializes a new instance of the class. + /// + /// The values. + /// The allowed parts. + public ExifWriter(IList values, ExifParts allowedParts) + { + this.values = values; + this.allowedParts = allowedParts; + this.ifdValues = this.GetPartValues(ExifParts.IfdTags); + this.exifValues = this.GetPartValues(ExifParts.ExifTags); + this.gpsValues = this.GetPartValues(ExifParts.GpsTags); + } + + /// + /// Returns the EXIF data. + /// + /// + /// The . + /// + public byte[] GetData() + { + const uint startIndex = 0; + + IExifValue? exifOffset = GetOffsetValue(this.ifdValues, this.exifValues, ExifTag.SubIFDOffset); + IExifValue? gpsOffset = GetOffsetValue(this.ifdValues, this.gpsValues, ExifTag.GPSIFDOffset); + + uint ifdLength = GetLength(this.ifdValues); + uint exifLength = GetLength(this.exifValues); + uint gpsLength = GetLength(this.gpsValues); + + uint length = ifdLength + exifLength + gpsLength; + + if (length == 0) + { + return []; + } + + // 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; + + // first IFD offset + length += 4; + + byte[] result = new byte[length]; + + int i = 0; + + // The byte order marker for little-endian, followed by the number 42 and a 0 + ExifConstants.LittleEndianByteOrderMarker.CopyTo(result.AsSpan(start: i)); + i += ExifConstants.LittleEndianByteOrderMarker.Length; + + 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 = this.WriteData(startIndex, this.ifdValues, result, i); + + if (exifLength > 0) + { + i = this.WriteHeaders(this.exifValues, result, i); + i = this.WriteData(startIndex, this.exifValues, result, i); + } + + if (gpsLength > 0) + { + i = this.WriteHeaders(this.gpsValues, result, i); + this.WriteData(startIndex, this.gpsValues, result, i); + } + + return result; + } + + private static unsafe int WriteSingle(float value, Span destination, int offset) + { + BinaryPrimitives.WriteInt32LittleEndian(destination.Slice(offset, 4), *(int*)&value); + + return offset + 4; + } + + private static unsafe int WriteDouble(double value, Span destination, int offset) + { + BinaryPrimitives.WriteInt64LittleEndian(destination.Slice(offset, 8), *(long*)&value); + + return offset + 8; + } + + private static int Write(ReadOnlySpan source, Span destination, int offset) + { + source.CopyTo(destination.Slice(offset, source.Length)); + + return offset + source.Length; + } + + private static int WriteInt16(short value, Span destination, int offset) + { + BinaryPrimitives.WriteInt16LittleEndian(destination.Slice(offset, 2), value); + + return offset + 2; + } + + private static int WriteUInt16(ushort value, Span destination, int offset) + { + BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(offset, 2), value); + + return offset + 2; + } + + private static int WriteUInt32(uint value, Span destination, int offset) + { + BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(offset, 4), value); + + return offset + 4; + } + + private static int WriteInt64(long value, Span destination, int offset) + { + BinaryPrimitives.WriteInt64LittleEndian(destination.Slice(offset, 8), value); + + return offset + 8; + } + + private static int WriteUInt64(ulong value, Span destination, int offset) + { + BinaryPrimitives.WriteUInt64LittleEndian(destination.Slice(offset, 8), value); + + return offset + 8; + } + + private static int WriteInt32(int value, Span destination, int offset) + { + BinaryPrimitives.WriteInt32LittleEndian(destination.Slice(offset, 4), value); + + return offset + 4; + } + + private static IExifValue? GetOffsetValue(List ifdValues, List values, ExifTag offset) + { + int index = -1; + + for (int i = 0; i < ifdValues.Count; i++) + { + if (ifdValues[i].Tag == offset) + { + index = i; + } + } + + if (values.Count > 0) + { + if (index != -1) + { + return ifdValues[index]; + } + + ExifValue? result = ExifValues.Create(offset); + + if (result is not null) + { + ifdValues.Add(result); + } + + return result; + } + else if (index != -1) + { + ifdValues.RemoveAt(index); + } + + return null; + } + + private List GetPartValues(ExifParts part) + { + List result = []; + + if (!EnumUtils.HasFlag(this.allowedParts, part)) + { + return result; + } + + foreach (IExifValue value in this.values) + { + if (!HasValue(value)) + { + continue; + } + + if (ExifTags.GetPart(value.Tag) == part) + { + result.Add(value); + } + } + + return result; + } + + private static bool HasValue(IExifValue exifValue) + { + object? value = exifValue.GetValue(); + if (value is null) + { + return false; + } + + if (exifValue.DataType == ExifDataType.Ascii && value is string stringValue) + { + return stringValue.Length > 0; + } + + if (value is Array arrayValue) + { + return arrayValue.Length > 0; + } + + return true; + } + + private static uint GetLength(List values) + { + if (values.Count == 0) + { + return 0; + } + + uint length = 2; + + foreach (IExifValue value in values) + { + uint valueLength = GetLength(value); + + length += 12; + + if (valueLength > 4) + { + length += valueLength; + } + } + + // next IFD offset + length += 4; + + return length; + } + + internal static uint GetLength(IExifValue value) => GetNumberOfComponents(value) * ExifDataTypes.GetSize(value.DataType); + + internal static uint GetNumberOfComponents(IExifValue exifValue) + { + object? value = exifValue.GetValue(); + + if (ExifUcs2StringHelpers.IsUcs2Tag((ExifTagValue)(ushort)exifValue.Tag)) + { + return (uint)ExifUcs2StringHelpers.Ucs2Encoding.GetByteCount((string?)value!); + } + + if (value is EncodedString encodedString) + { + return ExifEncodedStringHelpers.GetDataLength(encodedString); + } + + if (exifValue.DataType == ExifDataType.Ascii) + { + return (uint)ExifConstants.DefaultEncoding.GetByteCount((string?)value!) + 1; + } + + if (value is Array arrayValue) + { + return (uint)arrayValue.Length; + } + + return 1; + } + + private static int WriteArray(IExifValue value, Span destination, int offset) + { + int newOffset = offset; + foreach (object obj in (Array)value.GetValue()!) + { + newOffset = WriteValue(value.DataType, obj, destination, newOffset); + } + + return newOffset; + } + + private int WriteData(uint startIndex, List values, Span destination, int offset) + { + if (this.dataOffsets is null || this.dataOffsets.Count == 0) + { + return offset; + } + + int newOffset = offset; + + int i = 0; + foreach (IExifValue value in values) + { + if (GetLength(value) > 4) + { + WriteUInt32((uint)(newOffset - startIndex), destination, this.dataOffsets[i++]); + newOffset = WriteValue(value, destination, newOffset); + } + } + + return newOffset; + } + + private int WriteHeaders(List values, Span destination, int offset) + { + this.dataOffsets = []; + + int newOffset = WriteUInt16((ushort)values.Count, destination, offset); + + if (values.Count == 0) + { + return newOffset; + } + + foreach (IExifValue value in values) + { + newOffset = WriteUInt16((ushort)value.Tag, destination, newOffset); + newOffset = WriteUInt16((ushort)value.DataType, destination, newOffset); + newOffset = WriteUInt32(GetNumberOfComponents(value), destination, newOffset); + + uint length = GetLength(value); + if (length > 4) + { + this.dataOffsets.Add(newOffset); + } + else + { + WriteValue(value, destination, newOffset); + } + + newOffset += 4; + } + + // next IFD offset + return WriteUInt32(0, destination, newOffset); + } + + private static void WriteRational(Span destination, in Rational value) + { + BinaryPrimitives.WriteUInt32LittleEndian(destination[..4], value.Numerator); + BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(4, 4), value.Denominator); + } + + private static void WriteSignedRational(Span destination, in SignedRational value) + { + BinaryPrimitives.WriteInt32LittleEndian(destination[..4], value.Numerator); + BinaryPrimitives.WriteInt32LittleEndian(destination.Slice(4, 4), value.Denominator); + } + + private static int WriteValue(ExifDataType dataType, object value, Span destination, int offset) + { + switch (dataType) + { + case ExifDataType.Ascii: + offset = Write(ExifConstants.DefaultEncoding.GetBytes((string)value), destination, offset); + destination[offset] = 0; + return offset + 1; + case ExifDataType.Byte: + case ExifDataType.Undefined: + destination[offset] = (byte)value; + return offset + 1; + case ExifDataType.DoubleFloat: + return WriteDouble((double)value, destination, offset); + case ExifDataType.Short: + if (value is Number shortNumber) + { + return WriteUInt16((ushort)shortNumber, destination, offset); + } + + return WriteUInt16((ushort)value, destination, offset); + case ExifDataType.Long: + if (value is Number longNumber) + { + return WriteUInt32((uint)longNumber, destination, offset); + } + + return WriteUInt32((uint)value, destination, offset); + case ExifDataType.Long8: + return WriteUInt64((ulong)value, destination, offset); + case ExifDataType.SignedLong8: + return WriteInt64((long)value, destination, offset); + case ExifDataType.Rational: + WriteRational(destination.Slice(offset, 8), (Rational)value); + return offset + 8; + case ExifDataType.SignedByte: + destination[offset] = unchecked((byte)(sbyte)value); + return offset + 1; + case ExifDataType.SignedLong: + return WriteInt32((int)value, destination, offset); + case ExifDataType.SignedShort: + return WriteInt16((short)value, destination, offset); + case ExifDataType.SignedRational: + WriteSignedRational(destination.Slice(offset, 8), (SignedRational)value); + return offset + 8; + case ExifDataType.SingleFloat: + return WriteSingle((float)value, destination, offset); + default: + throw new NotImplementedException(); + } + } + + internal static int WriteValue(IExifValue exifValue, Span destination, int offset) + { + object? value = exifValue.GetValue(); + Guard.NotNull(value); + + if (ExifUcs2StringHelpers.IsUcs2Tag((ExifTagValue)(ushort)exifValue.Tag)) + { + return offset + ExifUcs2StringHelpers.Write((string)value, destination[offset..]); + } + else if (value is EncodedString encodedString) + { + return offset + ExifEncodedStringHelpers.Write(encodedString, destination[offset..]); + } + + if (exifValue.IsArray) + { + return WriteArray(exifValue, destination, offset); + } + + return WriteValue(exifValue.DataType, value, destination, offset); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/README.md b/ImageSharp/Metadata/Profiles/Exif/README.md new file mode 100644 index 0000000..7901527 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/README.md @@ -0,0 +1,3 @@ +Adapted from Magick.NET: + +https://github.com/dlemstra/Magick.NET diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Byte.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Byte.cs new file mode 100644 index 0000000..c8d5558 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Byte.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the FaxProfile exif tag. + /// + public static ExifTag FaxProfile { get; } = new(ExifTagValue.FaxProfile); + + /// + /// Gets the ModeNumber exif tag. + /// + public static ExifTag ModeNumber { get; } = new(ExifTagValue.ModeNumber); + + /// + /// Gets the GPSAltitudeRef exif tag. + /// + public static ExifTag GPSAltitudeRef { get; } = new(ExifTagValue.GPSAltitudeRef); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs new file mode 100644 index 0000000..6f9c7cd --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the ClipPath exif tag. + /// + public static ExifTag ClipPath => new(ExifTagValue.ClipPath); + + /// + /// Gets the VersionYear exif tag. + /// + public static ExifTag VersionYear => new(ExifTagValue.VersionYear); + + /// + /// Gets the XMP exif tag. + /// + public static ExifTag XMP => new(ExifTagValue.XMP); + + /// + /// Gets the IPTC exif tag. + /// + public static ExifTag IPTC => new(ExifTagValue.IPTC); + + /// + /// Gets the IccProfile exif tag. + /// + public static ExifTag IccProfile => new(ExifTagValue.IccProfile); + + /// + /// Gets the CFAPattern2 exif tag. + /// + public static ExifTag CFAPattern2 => new(ExifTagValue.CFAPattern2); + + /// + /// Gets the TIFFEPStandardID exif tag. + /// + public static ExifTag TIFFEPStandardID => new(ExifTagValue.TIFFEPStandardID); + + /// + /// Gets the GPSVersionID exif tag. + /// + public static ExifTag GPSVersionID => new(ExifTagValue.GPSVersionID); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.DoubleArray.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.DoubleArray.cs new file mode 100644 index 0000000..e2f97d4 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.DoubleArray.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the PixelScale exif tag. + /// + public static ExifTag PixelScale { get; } = new(ExifTagValue.PixelScale); + + /// + /// Gets the IntergraphMatrix exif tag. + /// + public static ExifTag IntergraphMatrix { get; } = new(ExifTagValue.IntergraphMatrix); + + /// + /// Gets the ModelTiePoint exif tag. + /// + public static ExifTag ModelTiePoint { get; } = new(ExifTagValue.ModelTiePoint); + + /// + /// Gets the ModelTransform exif tag. + /// + public static ExifTag ModelTransform { get; } = new(ExifTagValue.ModelTransform); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.EncodedString.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.EncodedString.cs new file mode 100644 index 0000000..f85b3e2 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.EncodedString.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the UserComment exif tag. + /// + public static ExifTag UserComment { get; } = new(ExifTagValue.UserComment); + + /// + /// Gets the GPSProcessingMethod exif tag. + /// + public static ExifTag GPSProcessingMethod { get; } = new(ExifTagValue.GPSProcessingMethod); + + /// + /// Gets the GPSAreaInformation exif tag. + /// + public static ExifTag GPSAreaInformation { get; } = new(ExifTagValue.GPSAreaInformation); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Long.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Long.cs new file mode 100644 index 0000000..54a7a82 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Long.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the SubfileType exif tag. + /// + public static ExifTag SubfileType { get; } = new(ExifTagValue.SubfileType); + + /// + /// Gets the SubIFDOffset exif tag. + /// + public static ExifTag SubIFDOffset { get; } = new(ExifTagValue.SubIFDOffset); + + /// + /// Gets the GPSIFDOffset exif tag. + /// + public static ExifTag GPSIFDOffset { get; } = new(ExifTagValue.GPSIFDOffset); + + /// + /// Gets the T4Options exif tag. + /// + public static ExifTag T4Options { get; } = new(ExifTagValue.T4Options); + + /// + /// Gets the T6Options exif tag. + /// + public static ExifTag T6Options { get; } = new(ExifTagValue.T6Options); + + /// + /// Gets the XClipPathUnits exif tag. + /// + public static ExifTag XClipPathUnits { get; } = new(ExifTagValue.XClipPathUnits); + + /// + /// Gets the YClipPathUnits exif tag. + /// + public static ExifTag YClipPathUnits { get; } = new(ExifTagValue.YClipPathUnits); + + /// + /// Gets the ProfileType exif tag. + /// + public static ExifTag ProfileType { get; } = new(ExifTagValue.ProfileType); + + /// + /// Gets the CodingMethods exif tag. + /// + public static ExifTag CodingMethods { get; } = new(ExifTagValue.CodingMethods); + + /// + /// Gets the T82ptions exif tag. + /// + public static ExifTag T82ptions { get; } = new(ExifTagValue.T82ptions); + + /// + /// Gets the JPEGInterchangeFormat exif tag. + /// + public static ExifTag JPEGInterchangeFormat { get; } = new(ExifTagValue.JPEGInterchangeFormat); + + /// + /// Gets the JPEGInterchangeFormatLength exif tag. + /// + public static ExifTag JPEGInterchangeFormatLength { get; } = new(ExifTagValue.JPEGInterchangeFormatLength); + + /// + /// Gets the MDFileTag exif tag. + /// + public static ExifTag MDFileTag { get; } = new(ExifTagValue.MDFileTag); + + /// + /// Gets the StandardOutputSensitivity exif tag. + /// + public static ExifTag StandardOutputSensitivity { get; } = new(ExifTagValue.StandardOutputSensitivity); + + /// + /// Gets the RecommendedExposureIndex exif tag. + /// + public static ExifTag RecommendedExposureIndex { get; } = new(ExifTagValue.RecommendedExposureIndex); + + /// + /// Gets the ISOSpeed exif tag. + /// + public static ExifTag ISOSpeed { get; } = new(ExifTagValue.ISOSpeed); + + /// + /// Gets the ISOSpeedLatitudeyyy exif tag. + /// + public static ExifTag ISOSpeedLatitudeyyy { get; } = new(ExifTagValue.ISOSpeedLatitudeyyy); + + /// + /// Gets the ISOSpeedLatitudezzz exif tag. + /// + public static ExifTag ISOSpeedLatitudezzz { get; } = new(ExifTagValue.ISOSpeedLatitudezzz); + + /// + /// Gets the FaxRecvParams exif tag. + /// + public static ExifTag FaxRecvParams { get; } = new(ExifTagValue.FaxRecvParams); + + /// + /// Gets the FaxRecvTime exif tag. + /// + public static ExifTag FaxRecvTime { get; } = new(ExifTagValue.FaxRecvTime); + + /// + /// Gets the ImageNumber exif tag. + /// + public static ExifTag ImageNumber { get; } = new(ExifTagValue.ImageNumber); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs new file mode 100644 index 0000000..40ff295 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the FreeOffsets exif tag. + /// + public static ExifTag FreeOffsets { get; } = new(ExifTagValue.FreeOffsets); + + /// + /// Gets the FreeByteCounts exif tag. + /// + public static ExifTag FreeByteCounts { get; } = new(ExifTagValue.FreeByteCounts); + + /// + /// Gets the ColorResponseUnit exif tag. + /// + public static ExifTag ColorResponseUnit { get; } = new(ExifTagValue.ColorResponseUnit); + + /// + /// Gets the SMinSampleValue exif tag. + /// + public static ExifTag SMinSampleValue { get; } = new(ExifTagValue.SMinSampleValue); + + /// + /// Gets the SMaxSampleValue exif tag. + /// + public static ExifTag SMaxSampleValue { get; } = new(ExifTagValue.SMaxSampleValue); + + /// + /// Gets the JPEGQTables exif tag. + /// + public static ExifTag JPEGQTables { get; } = new(ExifTagValue.JPEGQTables); + + /// + /// Gets the JPEGDCTables exif tag. + /// + public static ExifTag JPEGDCTables { get; } = new(ExifTagValue.JPEGDCTables); + + /// + /// Gets the JPEGACTables exif tag. + /// + public static ExifTag JPEGACTables { get; } = new(ExifTagValue.JPEGACTables); + + /// + /// Gets the StripRowCounts exif tag. + /// + public static ExifTag StripRowCounts { get; } = new(ExifTagValue.StripRowCounts); + + /// + /// Gets the IntergraphRegisters exif tag. + /// + public static ExifTag IntergraphRegisters { get; } = new(ExifTagValue.IntergraphRegisters); + + /// + /// Gets the offset to child IFDs exif tag. + /// + public static ExifTag SubIFDs { get; } = new(ExifTagValue.SubIFDs); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Number.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Number.cs new file mode 100644 index 0000000..2b907ca --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Number.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the ImageWidth exif tag. + /// + public static ExifTag ImageWidth { get; } = new(ExifTagValue.ImageWidth); + + /// + /// Gets the ImageLength exif tag. + /// + public static ExifTag ImageLength { get; } = new(ExifTagValue.ImageLength); + + /// + /// Gets the RowsPerStrip exif tag. + /// + public static ExifTag RowsPerStrip { get; } = new(ExifTagValue.RowsPerStrip); + + /// + /// Gets the TileWidth exif tag. + /// + public static ExifTag TileWidth { get; } = new(ExifTagValue.TileWidth); + + /// + /// Gets the TileLength exif tag. + /// + public static ExifTag TileLength { get; } = new(ExifTagValue.TileLength); + + /// + /// Gets the BadFaxLines exif tag. + /// + public static ExifTag BadFaxLines { get; } = new(ExifTagValue.BadFaxLines); + + /// + /// Gets the ConsecutiveBadFaxLines exif tag. + /// + public static ExifTag ConsecutiveBadFaxLines { get; } = new(ExifTagValue.ConsecutiveBadFaxLines); + + /// + /// Gets the PixelXDimension exif tag. + /// + public static ExifTag PixelXDimension { get; } = new(ExifTagValue.PixelXDimension); + + /// + /// Gets the PixelYDimension exif tag. + /// + public static ExifTag PixelYDimension { get; } = new(ExifTagValue.PixelYDimension); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs new file mode 100644 index 0000000..d0bf4ce --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the StripOffsets exif tag. + /// + public static ExifTag StripOffsets { get; } = new(ExifTagValue.StripOffsets); + + /// + /// Gets the StripByteCounts exif tag. + /// + public static ExifTag StripByteCounts { get; } = new(ExifTagValue.StripByteCounts); + + /// + /// Gets the TileByteCounts exif tag. + /// + public static ExifTag TileByteCounts { get; } = new(ExifTagValue.TileByteCounts); + + /// + /// Gets the TileOffsets exif tag. + /// + public static ExifTag TileOffsets { get; } = new(ExifTagValue.TileOffsets); + + /// + /// Gets the ImageLayer exif tag. + /// + public static ExifTag ImageLayer { get; } = new(ExifTagValue.ImageLayer); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Rational.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Rational.cs new file mode 100644 index 0000000..8132d38 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Rational.cs @@ -0,0 +1,173 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the XPosition exif tag. + /// + public static ExifTag XPosition { get; } = new(ExifTagValue.XPosition); + + /// + /// Gets the YPosition exif tag. + /// + public static ExifTag YPosition { get; } = new(ExifTagValue.YPosition); + + /// + /// Gets the XResolution exif tag. + /// + public static ExifTag XResolution { get; } = new(ExifTagValue.XResolution); + + /// + /// Gets the YResolution exif tag. + /// + public static ExifTag YResolution { get; } = new(ExifTagValue.YResolution); + + /// + /// Gets the BatteryLevel exif tag. + /// + public static ExifTag BatteryLevel { get; } = new(ExifTagValue.BatteryLevel); + + /// + /// Gets the ExposureTime exif tag. + /// + public static ExifTag ExposureTime { get; } = new(ExifTagValue.ExposureTime); + + /// + /// Gets the FNumber exif tag. + /// + public static ExifTag FNumber { get; } = new(ExifTagValue.FNumber); + + /// + /// Gets the MDScalePixel exif tag. + /// + public static ExifTag MDScalePixel { get; } = new(ExifTagValue.MDScalePixel); + + /// + /// Gets the CompressedBitsPerPixel exif tag. + /// + public static ExifTag CompressedBitsPerPixel { get; } = new(ExifTagValue.CompressedBitsPerPixel); + + /// + /// Gets the ApertureValue exif tag. + /// + public static ExifTag ApertureValue { get; } = new(ExifTagValue.ApertureValue); + + /// + /// Gets the MaxApertureValue exif tag. + /// + public static ExifTag MaxApertureValue { get; } = new(ExifTagValue.MaxApertureValue); + + /// + /// Gets the SubjectDistance exif tag. + /// + public static ExifTag SubjectDistance { get; } = new(ExifTagValue.SubjectDistance); + + /// + /// Gets the FocalLength exif tag. + /// + public static ExifTag FocalLength { get; } = new(ExifTagValue.FocalLength); + + /// + /// Gets the FlashEnergy2 exif tag. + /// + public static ExifTag FlashEnergy2 { get; } = new(ExifTagValue.FlashEnergy2); + + /// + /// Gets the FocalPlaneXResolution2 exif tag. + /// + public static ExifTag FocalPlaneXResolution2 { get; } = new(ExifTagValue.FocalPlaneXResolution2); + + /// + /// Gets the FocalPlaneYResolution2 exif tag. + /// + public static ExifTag FocalPlaneYResolution2 { get; } = new(ExifTagValue.FocalPlaneYResolution2); + + /// + /// Gets the ExposureIndex2 exif tag. + /// + public static ExifTag ExposureIndex2 { get; } = new(ExifTagValue.ExposureIndex2); + + /// + /// Gets the Humidity exif tag. + /// + public static ExifTag Humidity { get; } = new(ExifTagValue.Humidity); + + /// + /// Gets the Pressure exif tag. + /// + public static ExifTag Pressure { get; } = new(ExifTagValue.Pressure); + + /// + /// Gets the Acceleration exif tag. + /// + public static ExifTag Acceleration { get; } = new(ExifTagValue.Acceleration); + + /// + /// Gets the FlashEnergy exif tag. + /// + public static ExifTag FlashEnergy { get; } = new(ExifTagValue.FlashEnergy); + + /// + /// Gets the FocalPlaneXResolution exif tag. + /// + public static ExifTag FocalPlaneXResolution { get; } = new(ExifTagValue.FocalPlaneXResolution); + + /// + /// Gets the FocalPlaneYResolution exif tag. + /// + public static ExifTag FocalPlaneYResolution { get; } = new(ExifTagValue.FocalPlaneYResolution); + + /// + /// Gets the ExposureIndex exif tag. + /// + public static ExifTag ExposureIndex { get; } = new(ExifTagValue.ExposureIndex); + + /// + /// Gets the DigitalZoomRatio exif tag. + /// + public static ExifTag DigitalZoomRatio { get; } = new(ExifTagValue.DigitalZoomRatio); + + /// + /// Gets the GPSAltitude exif tag. + /// + public static ExifTag GPSAltitude { get; } = new(ExifTagValue.GPSAltitude); + + /// + /// Gets the GPSDOP exif tag. + /// + public static ExifTag GPSDOP { get; } = new(ExifTagValue.GPSDOP); + + /// + /// Gets the GPSSpeed exif tag. + /// + public static ExifTag GPSSpeed { get; } = new(ExifTagValue.GPSSpeed); + + /// + /// Gets the GPSTrack exif tag. + /// + public static ExifTag GPSTrack { get; } = new(ExifTagValue.GPSTrack); + + /// + /// Gets the GPSImgDirection exif tag. + /// + public static ExifTag GPSImgDirection { get; } = new(ExifTagValue.GPSImgDirection); + + /// + /// Gets the GPSDestBearing exif tag. + /// + public static ExifTag GPSDestBearing { get; } = new(ExifTagValue.GPSDestBearing); + + /// + /// Gets the GPSDestDistance exif tag. + /// + public static ExifTag GPSDestDistance { get; } = new(ExifTagValue.GPSDestDistance); + + /// + /// Gets the GPSHPositioningError exif tag. + /// + public static ExifTag GPSHPositioningError { get; } = new(ExifTagValue.GPSHPositioningError); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.RationalArray.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.RationalArray.cs new file mode 100644 index 0000000..ea2a077 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.RationalArray.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the WhitePoint exif tag. + /// + public static ExifTag WhitePoint { get; } = new(ExifTagValue.WhitePoint); + + /// + /// Gets the PrimaryChromaticities exif tag. + /// + public static ExifTag PrimaryChromaticities { get; } = new(ExifTagValue.PrimaryChromaticities); + + /// + /// Gets the YCbCrCoefficients exif tag. + /// + public static ExifTag YCbCrCoefficients { get; } = new(ExifTagValue.YCbCrCoefficients); + + /// + /// Gets the ReferenceBlackWhite exif tag. + /// + public static ExifTag ReferenceBlackWhite { get; } = new(ExifTagValue.ReferenceBlackWhite); + + /// + /// Gets the GPSLatitude exif tag. + /// + public static ExifTag GPSLatitude { get; } = new(ExifTagValue.GPSLatitude); + + /// + /// Gets the GPSLongitude exif tag. + /// + public static ExifTag GPSLongitude { get; } = new(ExifTagValue.GPSLongitude); + + /// + /// Gets the GPSTimestamp exif tag. + /// + public static ExifTag GPSTimestamp { get; } = new(ExifTagValue.GPSTimestamp); + + /// + /// Gets the GPSDestLatitude exif tag. + /// + public static ExifTag GPSDestLatitude { get; } = new(ExifTagValue.GPSDestLatitude); + + /// + /// Gets the GPSDestLongitude exif tag. + /// + public static ExifTag GPSDestLongitude { get; } = new(ExifTagValue.GPSDestLongitude); + + /// + /// Gets the LensSpecification exif tag. + /// + public static ExifTag LensSpecification { get; } = new(ExifTagValue.LensSpecification); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Short.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Short.cs new file mode 100644 index 0000000..61c9e73 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Short.cs @@ -0,0 +1,243 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the OldSubfileType exif tag. + /// + public static ExifTag OldSubfileType { get; } = new(ExifTagValue.OldSubfileType); + + /// + /// Gets the Compression exif tag. + /// + public static ExifTag Compression { get; } = new(ExifTagValue.Compression); + + /// + /// Gets the PhotometricInterpretation exif tag. + /// + public static ExifTag PhotometricInterpretation { get; } = new(ExifTagValue.PhotometricInterpretation); + + /// + /// Gets the Thresholding exif tag. + /// + public static ExifTag Thresholding { get; } = new(ExifTagValue.Thresholding); + + /// + /// Gets the CellWidth exif tag. + /// + public static ExifTag CellWidth { get; } = new(ExifTagValue.CellWidth); + + /// + /// Gets the CellLength exif tag. + /// + public static ExifTag CellLength { get; } = new(ExifTagValue.CellLength); + + /// + /// Gets the FillOrder exif tag. + /// + public static ExifTag FillOrder { get; } = new(ExifTagValue.FillOrder); + + /// + /// Gets the Orientation exif tag. + /// + public static ExifTag Orientation { get; } = new(ExifTagValue.Orientation); + + /// + /// Gets the SamplesPerPixel exif tag. + /// + public static ExifTag SamplesPerPixel { get; } = new(ExifTagValue.SamplesPerPixel); + + /// + /// Gets the PlanarConfiguration exif tag. + /// + public static ExifTag PlanarConfiguration { get; } = new(ExifTagValue.PlanarConfiguration); + + /// + /// Gets the Predictor exif tag. + /// + public static ExifTag Predictor { get; } = new(ExifTagValue.Predictor); + + /// + /// Gets the GrayResponseUnit exif tag. + /// + public static ExifTag GrayResponseUnit { get; } = new(ExifTagValue.GrayResponseUnit); + + /// + /// Gets the ResolutionUnit exif tag. + /// + public static ExifTag ResolutionUnit { get; } = new(ExifTagValue.ResolutionUnit); + + /// + /// Gets the CleanFaxData exif tag. + /// + public static ExifTag CleanFaxData { get; } = new(ExifTagValue.CleanFaxData); + + /// + /// Gets the InkSet exif tag. + /// + public static ExifTag InkSet { get; } = new(ExifTagValue.InkSet); + + /// + /// Gets the NumberOfInks exif tag. + /// + public static ExifTag NumberOfInks { get; } = new(ExifTagValue.NumberOfInks); + + /// + /// Gets the DotRange exif tag. + /// + public static ExifTag DotRange { get; } = new(ExifTagValue.DotRange); + + /// + /// Gets the Indexed exif tag. + /// + public static ExifTag Indexed { get; } = new(ExifTagValue.Indexed); + + /// + /// Gets the OPIProxy exif tag. + /// + public static ExifTag OPIProxy { get; } = new(ExifTagValue.OPIProxy); + + /// + /// Gets the JPEGProc exif tag. + /// + public static ExifTag JPEGProc { get; } = new(ExifTagValue.JPEGProc); + + /// + /// Gets the JPEGRestartInterval exif tag. + /// + public static ExifTag JPEGRestartInterval { get; } = new(ExifTagValue.JPEGRestartInterval); + + /// + /// Gets the YCbCrPositioning exif tag. + /// + public static ExifTag YCbCrPositioning { get; } = new(ExifTagValue.YCbCrPositioning); + + /// + /// Gets the Rating exif tag. + /// + public static ExifTag Rating { get; } = new(ExifTagValue.Rating); + + /// + /// Gets the RatingPercent exif tag. + /// + public static ExifTag RatingPercent { get; } = new(ExifTagValue.RatingPercent); + + /// + /// Gets the ExposureProgram exif tag. + /// + public static ExifTag ExposureProgram { get; } = new(ExifTagValue.ExposureProgram); + + /// + /// Gets the Interlace exif tag. + /// + public static ExifTag Interlace { get; } = new(ExifTagValue.Interlace); + + /// + /// Gets the SelfTimerMode exif tag. + /// + public static ExifTag SelfTimerMode { get; } = new(ExifTagValue.SelfTimerMode); + + /// + /// Gets the SensitivityType exif tag. + /// + public static ExifTag SensitivityType { get; } = new(ExifTagValue.SensitivityType); + + /// + /// Gets the MeteringMode exif tag. + /// + public static ExifTag MeteringMode { get; } = new(ExifTagValue.MeteringMode); + + /// + /// Gets the LightSource exif tag. + /// + public static ExifTag LightSource { get; } = new(ExifTagValue.LightSource); + + /// + /// Gets the FocalPlaneResolutionUnit2 exif tag. + /// + public static ExifTag FocalPlaneResolutionUnit2 { get; } = new(ExifTagValue.FocalPlaneResolutionUnit2); + + /// + /// Gets the SensingMethod2 exif tag. + /// + public static ExifTag SensingMethod2 { get; } = new(ExifTagValue.SensingMethod2); + + /// + /// Gets the Flash exif tag. + /// + public static ExifTag Flash { get; } = new(ExifTagValue.Flash); + + /// + /// Gets the ColorSpace exif tag. + /// + public static ExifTag ColorSpace { get; } = new(ExifTagValue.ColorSpace); + + /// + /// Gets the FocalPlaneResolutionUnit exif tag. + /// + public static ExifTag FocalPlaneResolutionUnit { get; } = new(ExifTagValue.FocalPlaneResolutionUnit); + + /// + /// Gets the SensingMethod exif tag. + /// + public static ExifTag SensingMethod { get; } = new(ExifTagValue.SensingMethod); + + /// + /// Gets the CustomRendered exif tag. + /// + public static ExifTag CustomRendered { get; } = new(ExifTagValue.CustomRendered); + + /// + /// Gets the ExposureMode exif tag. + /// + public static ExifTag ExposureMode { get; } = new(ExifTagValue.ExposureMode); + + /// + /// Gets the WhiteBalance exif tag. + /// + public static ExifTag WhiteBalance { get; } = new(ExifTagValue.WhiteBalance); + + /// + /// Gets the FocalLengthIn35mmFilm exif tag. + /// + public static ExifTag FocalLengthIn35mmFilm { get; } = new(ExifTagValue.FocalLengthIn35mmFilm); + + /// + /// Gets the SceneCaptureType exif tag. + /// + public static ExifTag SceneCaptureType { get; } = new(ExifTagValue.SceneCaptureType); + + /// + /// Gets the GainControl exif tag. + /// + public static ExifTag GainControl { get; } = new(ExifTagValue.GainControl); + + /// + /// Gets the Contrast exif tag. + /// + public static ExifTag Contrast { get; } = new(ExifTagValue.Contrast); + + /// + /// Gets the Saturation exif tag. + /// + public static ExifTag Saturation { get; } = new(ExifTagValue.Saturation); + + /// + /// Gets the Sharpness exif tag. + /// + public static ExifTag Sharpness { get; } = new(ExifTagValue.Sharpness); + + /// + /// Gets the SubjectDistanceRange exif tag. + /// + public static ExifTag SubjectDistanceRange { get; } = new(ExifTagValue.SubjectDistanceRange); + + /// + /// Gets the GPSDifferential exif tag. + /// + public static ExifTag GPSDifferential { get; } = new(ExifTagValue.GPSDifferential); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ShortArray.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ShortArray.cs new file mode 100644 index 0000000..fa04939 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ShortArray.cs @@ -0,0 +1,108 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the BitsPerSample exif tag. + /// + public static ExifTag BitsPerSample { get; } = new(ExifTagValue.BitsPerSample); + + /// + /// Gets the MinSampleValue exif tag. + /// + public static ExifTag MinSampleValue { get; } = new(ExifTagValue.MinSampleValue); + + /// + /// Gets the MaxSampleValue exif tag. + /// + public static ExifTag MaxSampleValue { get; } = new(ExifTagValue.MaxSampleValue); + + /// + /// Gets the GrayResponseCurve exif tag. + /// + public static ExifTag GrayResponseCurve { get; } = new(ExifTagValue.GrayResponseCurve); + + /// + /// Gets the ColorMap exif tag. + /// + public static ExifTag ColorMap { get; } = new(ExifTagValue.ColorMap); + + /// + /// Gets the ExtraSamples exif tag. + /// + public static ExifTag ExtraSamples { get; } = new(ExifTagValue.ExtraSamples); + + /// + /// Gets the PageNumber exif tag. + /// + public static ExifTag PageNumber { get; } = new(ExifTagValue.PageNumber); + + /// + /// Gets the TransferFunction exif tag. + /// + public static ExifTag TransferFunction { get; } = new(ExifTagValue.TransferFunction); + + /// + /// Gets the HalftoneHints exif tag. + /// + public static ExifTag HalftoneHints { get; } = new(ExifTagValue.HalftoneHints); + + /// + /// Gets the SampleFormat exif tag. + /// + public static ExifTag SampleFormat { get; } = new(ExifTagValue.SampleFormat); + + /// + /// Gets the TransferRange exif tag. + /// + public static ExifTag TransferRange { get; } = new(ExifTagValue.TransferRange); + + /// + /// Gets the DefaultImageColor exif tag. + /// + public static ExifTag DefaultImageColor { get; } = new(ExifTagValue.DefaultImageColor); + + /// + /// Gets the JPEGLosslessPredictors exif tag. + /// + public static ExifTag JPEGLosslessPredictors { get; } = new(ExifTagValue.JPEGLosslessPredictors); + + /// + /// Gets the JPEGPointTransforms exif tag. + /// + public static ExifTag JPEGPointTransforms { get; } = new(ExifTagValue.JPEGPointTransforms); + + /// + /// Gets the YCbCrSubsampling exif tag. + /// + public static ExifTag YCbCrSubsampling { get; } = new(ExifTagValue.YCbCrSubsampling); + + /// + /// Gets the CFARepeatPatternDim exif tag. + /// + public static ExifTag CFARepeatPatternDim { get; } = new(ExifTagValue.CFARepeatPatternDim); + + /// + /// Gets the IntergraphPacketData exif tag. + /// + public static ExifTag IntergraphPacketData { get; } = new(ExifTagValue.IntergraphPacketData); + + /// + /// Gets the ISOSpeedRatings exif tag. + /// + public static ExifTag ISOSpeedRatings { get; } = new(ExifTagValue.ISOSpeedRatings); + + /// + /// Gets the SubjectArea exif tag. + /// + public static ExifTag SubjectArea { get; } = new(ExifTagValue.SubjectArea); + + /// + /// Gets the SubjectLocation exif tag. + /// + public static ExifTag SubjectLocation { get; } = new(ExifTagValue.SubjectLocation); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedRational.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedRational.cs new file mode 100644 index 0000000..76718d1 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedRational.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the ShutterSpeedValue exif tag. + /// + public static ExifTag ShutterSpeedValue { get; } = new(ExifTagValue.ShutterSpeedValue); + + /// + /// Gets the BrightnessValue exif tag. + /// + public static ExifTag BrightnessValue { get; } = new(ExifTagValue.BrightnessValue); + + /// + /// Gets the ExposureBiasValue exif tag. + /// + public static ExifTag ExposureBiasValue { get; } = new(ExifTagValue.ExposureBiasValue); + + /// + /// Gets the AmbientTemperature exif tag. + /// + public static ExifTag AmbientTemperature { get; } = new(ExifTagValue.AmbientTemperature); + + /// + /// Gets the WaterDepth exif tag. + /// + public static ExifTag WaterDepth { get; } = new(ExifTagValue.WaterDepth); + + /// + /// Gets the CameraElevationAngle exif tag. + /// + public static ExifTag CameraElevationAngle { get; } = new(ExifTagValue.CameraElevationAngle); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedRationalArray.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedRationalArray.cs new file mode 100644 index 0000000..76d0aa7 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedRationalArray.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the Decode exif tag. + /// + public static ExifTag Decode { get; } = new(ExifTagValue.Decode); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedShortArray.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedShortArray.cs new file mode 100644 index 0000000..edd7ad8 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedShortArray.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the TimeZoneOffset exif tag. + /// + public static ExifTag TimeZoneOffset { get; } = new(ExifTagValue.TimeZoneOffset); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.String.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.String.cs new file mode 100644 index 0000000..7acaf52 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.String.cs @@ -0,0 +1,278 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the ImageDescription exif tag. + /// + public static ExifTag ImageDescription { get; } = new(ExifTagValue.ImageDescription); + + /// + /// Gets the Make exif tag. + /// + public static ExifTag Make { get; } = new(ExifTagValue.Make); + + /// + /// Gets the Model exif tag. + /// + public static ExifTag Model { get; } = new(ExifTagValue.Model); + + /// + /// Gets the Software exif tag. + /// + public static ExifTag Software { get; } = new(ExifTagValue.Software); + + /// + /// Gets the DateTime exif tag. + /// + public static ExifTag DateTime { get; } = new(ExifTagValue.DateTime); + + /// + /// Gets the Artist exif tag. + /// + public static ExifTag Artist { get; } = new(ExifTagValue.Artist); + + /// + /// Gets the HostComputer exif tag. + /// + public static ExifTag HostComputer { get; } = new(ExifTagValue.HostComputer); + + /// + /// Gets the Copyright exif tag. + /// + public static ExifTag Copyright { get; } = new(ExifTagValue.Copyright); + + /// + /// Gets the DocumentName exif tag. + /// + public static ExifTag DocumentName { get; } = new(ExifTagValue.DocumentName); + + /// + /// Gets the PageName exif tag. + /// + public static ExifTag PageName { get; } = new(ExifTagValue.PageName); + + /// + /// Gets the InkNames exif tag. + /// + public static ExifTag InkNames { get; } = new(ExifTagValue.InkNames); + + /// + /// Gets the TargetPrinter exif tag. + /// + public static ExifTag TargetPrinter { get; } = new(ExifTagValue.TargetPrinter); + + /// + /// Gets the ImageID exif tag. + /// + public static ExifTag ImageID { get; } = new(ExifTagValue.ImageID); + + /// + /// Gets the MDLabName exif tag. + /// + public static ExifTag MDLabName { get; } = new(ExifTagValue.MDLabName); + + /// + /// Gets the MDSampleInfo exif tag. + /// + public static ExifTag MDSampleInfo { get; } = new(ExifTagValue.MDSampleInfo); + + /// + /// Gets the MDPrepDate exif tag. + /// + public static ExifTag MDPrepDate { get; } = new(ExifTagValue.MDPrepDate); + + /// + /// Gets the MDPrepTime exif tag. + /// + public static ExifTag MDPrepTime { get; } = new(ExifTagValue.MDPrepTime); + + /// + /// Gets the MDFileUnits exif tag. + /// + public static ExifTag MDFileUnits { get; } = new(ExifTagValue.MDFileUnits); + + /// + /// Gets the SEMInfo exif tag. + /// + public static ExifTag SEMInfo { get; } = new(ExifTagValue.SEMInfo); + + /// + /// Gets the SpectralSensitivity exif tag. + /// + public static ExifTag SpectralSensitivity { get; } = new(ExifTagValue.SpectralSensitivity); + + /// + /// Gets the DateTimeOriginal exif tag. + /// + public static ExifTag DateTimeOriginal { get; } = new(ExifTagValue.DateTimeOriginal); + + /// + /// Gets the DateTimeDigitized exif tag. + /// + public static ExifTag DateTimeDigitized { get; } = new(ExifTagValue.DateTimeDigitized); + + /// + /// Gets the SubsecTime exif tag. + /// + public static ExifTag SubsecTime { get; } = new(ExifTagValue.SubsecTime); + + /// + /// Gets the SubsecTimeOriginal exif tag. + /// + public static ExifTag SubsecTimeOriginal { get; } = new(ExifTagValue.SubsecTimeOriginal); + + /// + /// Gets the SubsecTimeDigitized exif tag. + /// + public static ExifTag SubsecTimeDigitized { get; } = new(ExifTagValue.SubsecTimeDigitized); + + /// + /// Gets the RelatedSoundFile exif tag. + /// + public static ExifTag RelatedSoundFile { get; } = new(ExifTagValue.RelatedSoundFile); + + /// + /// Gets the FaxSubaddress exif tag. + /// + public static ExifTag FaxSubaddress { get; } = new(ExifTagValue.FaxSubaddress); + + /// + /// Gets the OffsetTime exif tag. + /// + public static ExifTag OffsetTime { get; } = new(ExifTagValue.OffsetTime); + + /// + /// Gets the OffsetTimeOriginal exif tag. + /// + public static ExifTag OffsetTimeOriginal { get; } = new(ExifTagValue.OffsetTimeOriginal); + + /// + /// Gets the OffsetTimeDigitized exif tag. + /// + public static ExifTag OffsetTimeDigitized { get; } = new(ExifTagValue.OffsetTimeDigitized); + + /// + /// Gets the SecurityClassification exif tag. + /// + public static ExifTag SecurityClassification { get; } = new(ExifTagValue.SecurityClassification); + + /// + /// Gets the ImageHistory exif tag. + /// + public static ExifTag ImageHistory { get; } = new(ExifTagValue.ImageHistory); + + /// + /// Gets the ImageUniqueID exif tag. + /// + public static ExifTag ImageUniqueID { get; } = new(ExifTagValue.ImageUniqueID); + + /// + /// Gets the OwnerName exif tag. + /// + public static ExifTag OwnerName { get; } = new(ExifTagValue.OwnerName); + + /// + /// Gets the SerialNumber exif tag. + /// + public static ExifTag SerialNumber { get; } = new(ExifTagValue.SerialNumber); + + /// + /// Gets the LensMake exif tag. + /// + public static ExifTag LensMake { get; } = new(ExifTagValue.LensMake); + + /// + /// Gets the LensModel exif tag. + /// + public static ExifTag LensModel { get; } = new(ExifTagValue.LensModel); + + /// + /// Gets the LensSerialNumber exif tag. + /// + public static ExifTag LensSerialNumber { get; } = new(ExifTagValue.LensSerialNumber); + + /// + /// Gets the GDALMetadata exif tag. + /// + public static ExifTag GDALMetadata { get; } = new(ExifTagValue.GDALMetadata); + + /// + /// Gets the GDALNoData exif tag. + /// + public static ExifTag GDALNoData { get; } = new(ExifTagValue.GDALNoData); + + /// + /// Gets the GPSLatitudeRef exif tag. + /// + public static ExifTag GPSLatitudeRef { get; } = new(ExifTagValue.GPSLatitudeRef); + + /// + /// Gets the GPSLongitudeRef exif tag. + /// + public static ExifTag GPSLongitudeRef { get; } = new(ExifTagValue.GPSLongitudeRef); + + /// + /// Gets the GPSSatellites exif tag. + /// + public static ExifTag GPSSatellites { get; } = new(ExifTagValue.GPSSatellites); + + /// + /// Gets the GPSStatus exif tag. + /// + public static ExifTag GPSStatus { get; } = new(ExifTagValue.GPSStatus); + + /// + /// Gets the GPSMeasureMode exif tag. + /// + public static ExifTag GPSMeasureMode { get; } = new(ExifTagValue.GPSMeasureMode); + + /// + /// Gets the GPSSpeedRef exif tag. + /// + public static ExifTag GPSSpeedRef { get; } = new(ExifTagValue.GPSSpeedRef); + + /// + /// Gets the GPSTrackRef exif tag. + /// + public static ExifTag GPSTrackRef { get; } = new(ExifTagValue.GPSTrackRef); + + /// + /// Gets the GPSImgDirectionRef exif tag. + /// + public static ExifTag GPSImgDirectionRef { get; } = new(ExifTagValue.GPSImgDirectionRef); + + /// + /// Gets the GPSMapDatum exif tag. + /// + public static ExifTag GPSMapDatum { get; } = new(ExifTagValue.GPSMapDatum); + + /// + /// Gets the GPSDestLatitudeRef exif tag. + /// + public static ExifTag GPSDestLatitudeRef { get; } = new(ExifTagValue.GPSDestLatitudeRef); + + /// + /// Gets the GPSDestLongitudeRef exif tag. + /// + public static ExifTag GPSDestLongitudeRef { get; } = new(ExifTagValue.GPSDestLongitudeRef); + + /// + /// Gets the GPSDestBearingRef exif tag. + /// + public static ExifTag GPSDestBearingRef { get; } = new(ExifTagValue.GPSDestBearingRef); + + /// + /// Gets the GPSDestDistanceRef exif tag. + /// + public static ExifTag GPSDestDistanceRef { get; } = new(ExifTagValue.GPSDestDistanceRef); + + /// + /// Gets the GPSDateStamp exif tag. + /// + public static ExifTag GPSDateStamp { get; } = new(ExifTagValue.GPSDateStamp); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Ucs2String.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Ucs2String.cs new file mode 100644 index 0000000..e3b2a82 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Ucs2String.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the title tag used by Windows (encoded in UCS2). + /// + public static ExifTag XPTitle => new(ExifTagValue.XPTitle); + + /// + /// Gets the comment tag used by Windows (encoded in UCS2). + /// + public static ExifTag XPComment => new(ExifTagValue.XPComment); + + /// + /// Gets the author tag used by Windows (encoded in UCS2). + /// + public static ExifTag XPAuthor => new(ExifTagValue.XPAuthor); + + /// + /// Gets the keywords tag used by Windows (encoded in UCS2). + /// + public static ExifTag XPKeywords => new(ExifTagValue.XPKeywords); + + /// + /// Gets the subject tag used by Windows (encoded in UCS2). + /// + public static ExifTag XPSubject => new(ExifTagValue.XPSubject); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Undefined.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Undefined.cs new file mode 100644 index 0000000..9f458b7 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Undefined.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + public abstract partial class ExifTag + { + /// + /// Gets the JPEGTables exif tag. + /// + public static ExifTag JPEGTables { get; } = new(ExifTagValue.JPEGTables); + + /// + /// Gets the OECF exif tag. + /// + public static ExifTag OECF { get; } = new(ExifTagValue.OECF); + + /// + /// Gets the ExifVersion exif tag. + /// + public static ExifTag ExifVersion { get; } = new(ExifTagValue.ExifVersion); + + /// + /// Gets the ComponentsConfiguration exif tag. + /// + public static ExifTag ComponentsConfiguration { get; } = new(ExifTagValue.ComponentsConfiguration); + + /// + /// Gets the MakerNote exif tag. + /// + public static ExifTag MakerNote { get; } = new(ExifTagValue.MakerNote); + + /// + /// Gets the FlashpixVersion exif tag. + /// + public static ExifTag FlashpixVersion { get; } = new(ExifTagValue.FlashpixVersion); + + /// + /// Gets the SpatialFrequencyResponse exif tag. + /// + public static ExifTag SpatialFrequencyResponse { get; } = new(ExifTagValue.SpatialFrequencyResponse); + + /// + /// Gets the SpatialFrequencyResponse2 exif tag. + /// + public static ExifTag SpatialFrequencyResponse2 { get; } = new(ExifTagValue.SpatialFrequencyResponse2); + + /// + /// Gets the Noise exif tag. + /// + public static ExifTag Noise { get; } = new(ExifTagValue.Noise); + + /// + /// Gets the CFAPattern exif tag. + /// + public static ExifTag CFAPattern { get; } = new(ExifTagValue.CFAPattern); + + /// + /// Gets the DeviceSettingDescription exif tag. + /// + public static ExifTag DeviceSettingDescription { get; } = new(ExifTagValue.DeviceSettingDescription); + + /// + /// Gets the ImageSourceData exif tag. + /// + public static ExifTag ImageSourceData { get; } = new(ExifTagValue.ImageSourceData); + + /// + /// Gets the FileSource exif tag. + /// + public static ExifTag FileSource { get; } = new(ExifTagValue.FileSource); + + /// + /// Gets the ImageDescription exif tag. + /// + public static ExifTag SceneType { get; } = new(ExifTagValue.SceneType); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.cs new file mode 100644 index 0000000..1f544b1 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + /// Class that represents an Exif tag from the Exif standard 2.31. + /// + public abstract partial class ExifTag : IEquatable + { + private readonly ushort value; + + internal ExifTag(ushort value) => this.value = value; + + /// + /// Converts the specified to a . + /// + /// The to convert. + public static explicit operator ushort(ExifTag? tag) => tag?.value ?? (ushort)ExifTagValue.Unknown; + + /// + /// Determines whether the specified instances are considered equal. + /// + /// The first to compare. + /// The second to compare. + public static bool operator ==(ExifTag? left, ExifTag? right) => left?.Equals(right) == true; + + /// + /// Determines whether the specified instances are not considered equal. + /// + /// The first to compare. + /// The second to compare. + public static bool operator !=(ExifTag? left, ExifTag? right) => !(left == right); + + /// + public override bool Equals(object? obj) + { + if (obj is ExifTag value) + { + return this.Equals(value); + } + + return false; + } + + /// + public bool Equals(ExifTag? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return this.value == other.value; + } + + /// + public override int GetHashCode() => this.value.GetHashCode(); + + /// + public override string ToString() => ((ExifTagValue)this.value).ToString(); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs new file mode 100644 index 0000000..a7932a6 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs @@ -0,0 +1,1726 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + /// All exif tags from the Exif standard 2.31. + /// + internal enum ExifTagValue + { + /// + /// Unknown + /// + Unknown = 0xFFFF, + + /// + /// SubIFDOffset + /// + SubIFDOffset = 0x8769, + + /// + /// GPSIFDOffset + /// + GPSIFDOffset = 0x8825, + + /// + /// A general indication of the kind of data contained in this subfile. + /// See Section 8: Baseline Fields. + /// + [ExifTagDescription(0U, "Full-resolution Image")] + [ExifTagDescription(1U, "Reduced-resolution image")] + [ExifTagDescription(2U, "Single page of multi-page image")] + [ExifTagDescription(3U, "Single page of multi-page reduced-resolution image")] + [ExifTagDescription(4U, "Transparency mask")] + [ExifTagDescription(5U, "Transparency mask of reduced-resolution image")] + [ExifTagDescription(6U, "Transparency mask of multi-page image")] + [ExifTagDescription(7U, "Transparency mask of reduced-resolution multi-page image")] + [ExifTagDescription(0x10001U, "Alternate reduced-resolution image ")] + SubfileType = 0x00FE, + + /// + /// A general indication of the kind of data contained in this subfile. + /// See Section 8: Baseline Fields. + /// + [ExifTagDescription((ushort)1, "Full-resolution Image")] + [ExifTagDescription((ushort)2, "Reduced-resolution image")] + [ExifTagDescription((ushort)3, "Single page of multi-page image")] + OldSubfileType = 0x00FF, + + /// + /// The number of columns in the image, i.e., the number of pixels per row. + /// See Section 8: Baseline Fields. + /// + ImageWidth = 0x0100, + + /// + /// The number of rows of pixels in the image. + /// See Section 8: Baseline Fields. + /// + ImageLength = 0x0101, + + /// + /// Number of bits per component. + /// See Section 8: Baseline Fields. + /// + BitsPerSample = 0x0102, + + /// + /// Compression scheme used on the image data. + /// See Section 8: Baseline Fields. + /// + [ExifTagDescription((ushort)1, "Uncompressed")] + [ExifTagDescription((ushort)2, "CCITT 1D")] + [ExifTagDescription((ushort)3, "T4/Group 3 Fax")] + [ExifTagDescription((ushort)4, "T6/Group 4 Fax")] + [ExifTagDescription((ushort)5, "LZW")] + [ExifTagDescription((ushort)6, "JPEG (old-style)")] + [ExifTagDescription((ushort)7, "JPEG")] + [ExifTagDescription((ushort)8, "Adobe Deflate")] + [ExifTagDescription((ushort)9, "JBIG B&W")] + [ExifTagDescription((ushort)10, "JBIG Color")] + [ExifTagDescription((ushort)99, "JPEG")] + [ExifTagDescription((ushort)262, "Kodak 262")] + [ExifTagDescription((ushort)32766, "Next")] + [ExifTagDescription((ushort)32767, "Sony ARW Compressed")] + [ExifTagDescription((ushort)32769, "Packed RAW")] + [ExifTagDescription((ushort)32770, "Samsung SRW Compressed")] + [ExifTagDescription((ushort)32771, "CCIRLEW")] + [ExifTagDescription((ushort)32772, "Samsung SRW Compressed 2")] + [ExifTagDescription((ushort)32773, "PackBits")] + [ExifTagDescription((ushort)32809, "Thunderscan")] + [ExifTagDescription((ushort)32867, "Kodak KDC Compressed")] + [ExifTagDescription((ushort)32895, "IT8CTPAD")] + [ExifTagDescription((ushort)32896, "IT8LW")] + [ExifTagDescription((ushort)32897, "IT8MP")] + [ExifTagDescription((ushort)32898, "IT8BL")] + [ExifTagDescription((ushort)32908, "PixarFilm")] + [ExifTagDescription((ushort)32909, "PixarLog")] + [ExifTagDescription((ushort)32946, "Deflate")] + [ExifTagDescription((ushort)32947, "DCS")] + [ExifTagDescription((ushort)34661, "JBIG")] + [ExifTagDescription((ushort)34676, "SGILog")] + [ExifTagDescription((ushort)34677, "SGILog24")] + [ExifTagDescription((ushort)34712, "JPEG 2000")] + [ExifTagDescription((ushort)34713, "Nikon NEF Compressed")] + [ExifTagDescription((ushort)34715, "JBIG2 TIFF FX")] + [ExifTagDescription((ushort)34718, "Microsoft Document Imaging (MDI) Binary Level Codec")] + [ExifTagDescription((ushort)34719, "Microsoft Document Imaging (MDI) Progressive Transform Codec")] + [ExifTagDescription((ushort)34720, "Microsoft Document Imaging (MDI) Vector")] + [ExifTagDescription((ushort)34892, "Lossy JPEG")] + [ExifTagDescription((ushort)65000, "Kodak DCR Compressed")] + [ExifTagDescription((ushort)65535, "Pentax PEF Compressed")] + Compression = 0x0103, + + /// + /// The color space of the image data. + /// See Section 8: Baseline Fields. + /// + [ExifTagDescription((ushort)0, "WhiteIsZero")] + [ExifTagDescription((ushort)1, "BlackIsZero")] + [ExifTagDescription((ushort)2, "RGB")] + [ExifTagDescription((ushort)3, "RGB Palette")] + [ExifTagDescription((ushort)4, "Transparency Mask")] + [ExifTagDescription((ushort)5, "CMYK")] + [ExifTagDescription((ushort)6, "YCbCr")] + [ExifTagDescription((ushort)8, "CIELab")] + [ExifTagDescription((ushort)9, "ICCLab")] + [ExifTagDescription((ushort)10, "TULab")] + [ExifTagDescription((ushort)32803, "Color Filter Array")] + [ExifTagDescription((ushort)32844, "Pixar LogL")] + [ExifTagDescription((ushort)32845, "Pixar LogLuv")] + [ExifTagDescription((ushort)34892, "Linear Raw")] + PhotometricInterpretation = 0x0106, + + /// + /// For black and white TIFF files that represent shades of gray, the technique used to convert from gray to black and white pixels. + /// See Section 8: Baseline Fields. + /// + [ExifTagDescription((ushort)1, "No dithering or halftoning")] + [ExifTagDescription((ushort)2, "Ordered dither or halftone")] + [ExifTagDescription((ushort)3, "Randomized dither")] + Thresholding = 0x0107, + + /// + /// The width of the dithering or halftoning matrix used to create a dithered or halftoned bilevel file. + /// See Section 8: Baseline Fields. + /// + CellWidth = 0x0108, + + /// + /// The length of the dithering or halftoning matrix used to create a dithered or halftoned bilevel file. + /// See Section 8: Baseline Fields. + /// + CellLength = 0x0109, + + /// + /// The logical order of bits within a byte. + /// See Section 8: Baseline Fields. + /// + [ExifTagDescription((ushort)1, "Normal")] + [ExifTagDescription((ushort)2, "Reversed")] + FillOrder = 0x010A, + + /// + /// The name of the document from which this image was scanned. + /// See Section 12: Document Storage and Retrieval. + /// + DocumentName = 0x010D, + + /// + /// A string that describes the subject of the image. + /// See Section 8: Baseline Fields. + /// + ImageDescription = 0x010E, + + /// + /// The scanner manufacturer. + /// See Section 8: Baseline Fields. + /// + Make = 0x010F, + + /// + /// The scanner model name or number. + /// See Section 8: Baseline Fields. + /// + Model = 0x0110, + + /// + /// For each strip, the byte offset of that strip. + /// See Section 8: Baseline Fields. + /// + StripOffsets = 0x0111, + + /// + /// The orientation of the image with respect to the rows and columns. + /// See Section 8: Baseline Fields. + /// + [ExifTagDescription((ushort)1, "Horizontal (normal)")] + [ExifTagDescription((ushort)2, "Mirror horizontal")] + [ExifTagDescription((ushort)3, "Rotate 180")] + [ExifTagDescription((ushort)4, "Mirror vertical")] + [ExifTagDescription((ushort)5, "Mirror horizontal and rotate 270 CW")] + [ExifTagDescription((ushort)6, "Rotate 90 CW")] + [ExifTagDescription((ushort)7, "Mirror horizontal and rotate 90 CW")] + [ExifTagDescription((ushort)8, "Rotate 270 CW")] + Orientation = 0x0112, + + /// + /// The number of components per pixel. + /// See Section 8: Baseline Fields. + /// + SamplesPerPixel = 0x0115, + + /// + /// The number of rows per strip. + /// See Section 8: Baseline Fields. + /// + RowsPerStrip = 0x0116, + + /// + /// For each strip, the number of bytes in the strip after compression. + /// See Section 8: Baseline Fields. + /// + StripByteCounts = 0x0117, + + /// + /// The minimum component value used. + /// See Section 8: Baseline Fields. + /// + MinSampleValue = 0x0118, + + /// + /// The maximum component value used. + /// See Section 8: Baseline Fields. + /// + MaxSampleValue = 0x0119, + + /// + /// The number of pixels per ResolutionUnit in the ImageWidth direction. + /// See Section 8: Baseline Fields. + /// + XResolution = 0x011A, + + /// + /// The number of pixels per ResolutionUnit in the direction. + /// See Section 8: Baseline Fields. + /// + YResolution = 0x011B, + + /// + /// How the components of each pixel are stored. + /// See Section 8: Baseline Fields. + /// + [ExifTagDescription((ushort)1, "Chunky")] + [ExifTagDescription((ushort)2, "Planar")] + PlanarConfiguration = 0x011C, + + /// + /// The name of the page from which this image was scanned. + /// See Section 12: Document Storage and Retrieval. + /// + PageName = 0x011D, + + /// + /// X position of the image. + /// See Section 12: Document Storage and Retrieval. + /// + XPosition = 0x011E, + + /// + /// Y position of the image. + /// See Section 12: Document Storage and Retrieval. + /// + YPosition = 0x011F, + + /// + /// For each string of contiguous unused bytes in a TIFF file, the byte offset of the string. + /// See Section 8: Baseline Fields. + /// + FreeOffsets = 0x0120, + + /// + /// For each string of contiguous unused bytes in a TIFF file, the number of bytes in the string. + /// See Section 8: Baseline Fields. + /// + FreeByteCounts = 0x0121, + + /// + /// The precision of the information contained in the GrayResponseCurve. + /// See Section 8: Baseline Fields. + /// + [ExifTagDescription((ushort)1, "0.1")] + [ExifTagDescription((ushort)2, "0.001")] + [ExifTagDescription((ushort)3, "0.0001")] + [ExifTagDescription((ushort)4, "1e-05")] + [ExifTagDescription((ushort)5, "1e-06")] + GrayResponseUnit = 0x0122, + + /// + /// For grayscale data, the optical density of each possible pixel value. + /// See Section 8: Baseline Fields. + /// + GrayResponseCurve = 0x0123, + + /// + /// Options for Group 3 Fax compression. + /// + [ExifTagDescription(0U, "2-Dimensional encoding")] + [ExifTagDescription(1U, "Uncompressed")] + [ExifTagDescription(2U, "Fill bits added")] + T4Options = 0x0124, + + /// + /// Options for Group 4 Fax compression. + /// + [ExifTagDescription(1U, "Uncompressed")] + T6Options = 0x0125, + + /// + /// The unit of measurement for XResolution and YResolution. + /// See Section 8: Baseline Fields. + /// + [ExifTagDescription((ushort)1, "None")] + [ExifTagDescription((ushort)2, "Inches")] + [ExifTagDescription((ushort)3, "Centimeter")] + ResolutionUnit = 0x0128, + + /// + /// The page number of the page from which this image was scanned. + /// See Section 12: Document Storage and Retrieval. + /// + PageNumber = 0x0129, + + /// + /// ColorResponseUnit + /// + ColorResponseUnit = 0x012C, + + /// + /// TransferFunction + /// + TransferFunction = 0x012D, + + /// + /// Name and version number of the software package(s) used to create the image. + /// See Section 8: Baseline Fields. + /// + Software = 0x0131, + + /// + /// Date and time of image creation. + /// See Section 8: Baseline Fields. + /// + DateTime = 0x0132, + + /// + /// Person who created the image. + /// See Section 8: Baseline Fields. + /// + Artist = 0x013B, + + /// + /// The computer and/or operating system in use at the time of image creation. + /// See Section 8: Baseline Fields. + /// + HostComputer = 0x013C, + + /// + /// Predictor + /// + Predictor = 0x013D, + + /// + /// WhitePoint + /// + WhitePoint = 0x013E, + + /// + /// PrimaryChromaticities + /// + PrimaryChromaticities = 0x013F, + + /// + /// A color map for palette color images. + /// See Section 8: Baseline Fields. + /// + ColorMap = 0x0140, + + /// + /// HalftoneHints + /// + HalftoneHints = 0x0141, + + /// + /// TileWidth + /// + TileWidth = 0x0142, + + /// + /// TileLength + /// + TileLength = 0x0143, + + /// + /// TileOffsets + /// + TileOffsets = 0x0144, + + /// + /// TileByteCounts + /// + TileByteCounts = 0x0145, + + /// + /// BadFaxLines + /// + BadFaxLines = 0x0146, + + /// + /// CleanFaxData + /// + [ExifTagDescription(0U, "Clean")] + [ExifTagDescription(1U, "Regenerated")] + [ExifTagDescription(2U, "Unclean")] + CleanFaxData = 0x0147, + + /// + /// ConsecutiveBadFaxLines + /// + ConsecutiveBadFaxLines = 0x0148, + + /// + /// Offset to child IFDs. + /// See TIFF Supplement 1: Adobe Pagemaker 6.0. + /// Each value is an offset (from the beginning of the TIFF file, as always) to a child IFD. Child images provide extra information for the parent image - such as a subsampled version of the parent image. + /// TIFF data type is Long or 13, IFD. The IFD type is identical to LONG, except that it is only used to point to other valid IFDs. + /// + SubIFDs = 0x014A, + + /// + /// InkSet + /// + [ExifTagDescription((ushort)1, "CMYK")] + [ExifTagDescription((ushort)2, "Not CMYK")] + InkSet = 0x014C, + + /// + /// InkNames + /// + InkNames = 0x014D, + + /// + /// NumberOfInks + /// + NumberOfInks = 0x014E, + + /// + /// DotRange + /// + DotRange = 0x0150, + + /// + /// TargetPrinter + /// + TargetPrinter = 0x0151, + + /// + /// Description of extra components. + /// See Section 8: Baseline Fields. + /// + [ExifTagDescription((ushort)0, "Unspecified")] + [ExifTagDescription((ushort)1, "Associated Alpha")] + [ExifTagDescription((ushort)2, "Unassociated Alpha")] + ExtraSamples = 0x0152, + + /// + /// SampleFormat + /// + [ExifTagDescription((ushort)1, "Unsigned")] + [ExifTagDescription((ushort)2, "Signed")] + [ExifTagDescription((ushort)3, "Float")] + [ExifTagDescription((ushort)4, "Undefined")] + [ExifTagDescription((ushort)5, "Complex int")] + [ExifTagDescription((ushort)6, "Complex float")] + SampleFormat = 0x0153, + + /// + /// SMinSampleValue + /// + SMinSampleValue = 0x0154, + + /// + /// SMaxSampleValue + /// + SMaxSampleValue = 0x0155, + + /// + /// TransferRange + /// + TransferRange = 0x0156, + + /// + /// ClipPath + /// + ClipPath = 0x0157, + + /// + /// XClipPathUnits + /// + XClipPathUnits = 0x0158, + + /// + /// YClipPathUnits + /// + YClipPathUnits = 0x0159, + + /// + /// Indexed + /// + [ExifTagDescription((ushort)0, "Not indexed")] + [ExifTagDescription((ushort)1, "Indexed")] + Indexed = 0x015A, + + /// + /// JPEGTables + /// + JPEGTables = 0x015B, + + /// + /// OPIProxy + /// + [ExifTagDescription((ushort)0, "Higher resolution image does not exist")] + [ExifTagDescription((ushort)1, "Higher resolution image exists")] + OPIProxy = 0x015F, + + /// + /// Used in the TIFF-FX standard to point to an IFD containing tags that are globally applicable to the complete TIFF file. + /// See RFC2301: TIFF-F/FX Specification. + /// It is recommended that a TIFF writer place this field in the first IFD, where a TIFF reader would find it quickly. + /// Each field in the GlobalParametersIFD is a TIFF field that is legal in any IFD. Required baseline fields should not be located in the GlobalParametersIFD, but should be in each image IFD. If a conflict exists between fields in the GlobalParametersIFD and in the image IFDs, then the data in the image IFD shall prevail. + /// + GlobalParametersIFD = 0x0190, + + /// + /// ProfileType + /// + [ExifTagDescription(0U, "Unspecified")] + [ExifTagDescription(1U, "Group 3 FAX")] + ProfileType = 0x0191, + + /// + /// FaxProfile + /// + [ExifTagDescription((byte)0, "Unknown")] + [ExifTagDescription((byte)1, "Minimal B&W lossless, S")] + [ExifTagDescription((byte)2, "Extended B&W lossless, F")] + [ExifTagDescription((byte)3, "Lossless JBIG B&W, J")] + [ExifTagDescription((byte)4, "Lossy color and grayscale, C")] + [ExifTagDescription((byte)5, "Lossless color and grayscale, L")] + [ExifTagDescription((byte)6, "Mixed raster content, M")] + [ExifTagDescription((byte)7, "Profile T")] + [ExifTagDescription((byte)255, "Multi Profiles")] + FaxProfile = 0x0192, + + /// + /// CodingMethods + /// + [ExifTagDescription(0UL, "Unspecified compression")] + [ExifTagDescription(1UL, "Modified Huffman")] + [ExifTagDescription(2UL, "Modified Read")] + [ExifTagDescription(4UL, "Modified MR")] + [ExifTagDescription(8UL, "JBIG")] + [ExifTagDescription(16UL, "Baseline JPEG")] + [ExifTagDescription(32UL, "JBIG color")] + CodingMethods = 0x0193, + + /// + /// VersionYear + /// + VersionYear = 0x0194, + + /// + /// ModeNumber + /// + ModeNumber = 0x0195, + + /// + /// Decode + /// + Decode = 0x01B1, + + /// + /// DefaultImageColor + /// + DefaultImageColor = 0x01B2, + + /// + /// T82ptions + /// + T82ptions = 0x01B3, + + /// + /// JPEGProc + /// + [ExifTagDescription((ushort)1, "Baseline")] + [ExifTagDescription((ushort)14, "Lossless")] + JPEGProc = 0x0200, + + /// + /// JPEGInterchangeFormat + /// + JPEGInterchangeFormat = 0x0201, + + /// + /// JPEGInterchangeFormatLength + /// + JPEGInterchangeFormatLength = 0x0202, + + /// + /// JPEGRestartInterval + /// + JPEGRestartInterval = 0x0203, + + /// + /// JPEGLosslessPredictors + /// + JPEGLosslessPredictors = 0x0205, + + /// + /// JPEGPointTransforms + /// + JPEGPointTransforms = 0x0206, + + /// + /// JPEGQTables + /// + JPEGQTables = 0x0207, + + /// + /// JPEGDCTables + /// + JPEGDCTables = 0x0208, + + /// + /// JPEGACTables + /// + JPEGACTables = 0x0209, + + /// + /// YCbCrCoefficients + /// + YCbCrCoefficients = 0x0211, + + /// + /// YCbCrSubsampling + /// + YCbCrSubsampling = 0x0212, + + /// + /// YCbCrPositioning + /// + [ExifTagDescription((ushort)1, "Centered")] + [ExifTagDescription((ushort)2, "Co-sited")] + YCbCrPositioning = 0x0213, + + /// + /// ReferenceBlackWhite + /// + ReferenceBlackWhite = 0x0214, + + /// + /// StripRowCounts + /// + StripRowCounts = 0x022F, + + /// + /// XMP + /// + XMP = 0x02BC, + + /// + /// Rating + /// + Rating = 0x4746, + + /// + /// RatingPercent + /// + RatingPercent = 0x4749, + + /// + /// ImageID + /// + ImageID = 0x800D, + + /// + /// Annotation data, as used in 'Imaging for Windows'. + /// See Other Private TIFF tags: http://www.awaresystems.be/imaging/tiff/tifftags/private.html + /// + WangAnnotation = 0x80A4, + + /// + /// CFARepeatPatternDim + /// + CFARepeatPatternDim = 0x828D, + + /// + /// CFAPattern2 + /// + CFAPattern2 = 0x828E, + + /// + /// BatteryLevel + /// + BatteryLevel = 0x828F, + + /// + /// Copyright notice. + /// See Section 8: Baseline Fields. + /// + Copyright = 0x8298, + + /// + /// ExposureTime + /// + ExposureTime = 0x829A, + + /// + /// FNumber + /// + FNumber = 0x829D, + + /// + /// Specifies the pixel data format encoding in the Molecular Dynamics GEL file format. + /// See Molecular Dynamics GEL File Format and Private Tags: https://www.awaresystems.be/imaging/tiff/tifftags/docs/gel.html + /// + [ExifTagDescription((ushort)2, "Squary root data format")] + [ExifTagDescription((ushort)128, "Linear data format")] + MDFileTag = 0x82A5, + + /// + /// Specifies a scale factor in the Molecular Dynamics GEL file format. + /// See Molecular Dynamics GEL File Format and Private Tags: https://www.awaresystems.be/imaging/tiff/tifftags/docs/gel.html + /// The scale factor is to be applies to each pixel before presenting it to the user. + /// + MDScalePixel = 0x82A6, + + /// + /// Used to specify the conversion from 16bit to 8bit in the Molecular Dynamics GEL file format. + /// See Molecular Dynamics GEL File Format and Private Tags: https://www.awaresystems.be/imaging/tiff/tifftags/docs/gel.html + /// Since the display is only 9bit, the 16bit data must be converted before display. + /// 8bit value = (16bit value - low range ) * 255 / (high range - low range) + /// Count: n. + /// + [ExifTagDescription((ushort)0, "lowest possible")] + [ExifTagDescription((ushort)1, "low range")] + [ExifTagDescription("n-2", "high range")] + [ExifTagDescription("n-1", "highest possible")] + MDColorTable = 0x82A7, + + /// + /// Name of the lab that scanned this file, as used in the Molecular Dynamics GEL file format. + /// See Molecular Dynamics GEL File Format and Private Tags: https://www.awaresystems.be/imaging/tiff/tifftags/docs/gel.html + /// + MDLabName = 0x82A8, + + /// + /// Information about the sample, as used in the Molecular Dynamics GEL file format. + /// See Molecular Dynamics GEL File Format and Private Tags: https://www.awaresystems.be/imaging/tiff/tifftags/docs/gel.html + /// This information is entered by the person that scanned the file. + /// Note that the word 'sample' as used here, refers to the scanned sample, not an image channel. + /// + MDSampleInfo = 0x82A9, + + /// + /// Date the sample was prepared, as used in the Molecular Dynamics GEL file format. + /// See Molecular Dynamics GEL File Format and Private Tags: https://www.awaresystems.be/imaging/tiff/tifftags/docs/gel.html + /// The format of this data is YY/MM/DD. + /// Note that the word 'sample' as used here, refers to the scanned sample, not an image channel. + /// + MDPrepDate = 0x82AA, + + /// + /// Time the sample was prepared, as used in the Molecular Dynamics GEL file format. + /// See Molecular Dynamics GEL File Format and Private Tags: https://www.awaresystems.be/imaging/tiff/tifftags/docs/gel.html + /// Format of this data is HH:MM using the 24-hour clock. + /// Note that the word 'sample' as used here, refers to the scanned sample, not an image channel. + /// + MDPrepTime = 0x82AB, + + /// + /// Units for data in this file, as used in the Molecular Dynamics GEL file format. + /// See Molecular Dynamics GEL File Format and Private Tags: https://www.awaresystems.be/imaging/tiff/tifftags/docs/gel.html + /// + [ExifTagDescription("O.D.", "Densitometer")] + [ExifTagDescription("Counts", "PhosphorImager")] + [ExifTagDescription("RFU", "FluorImager")] + MDFileUnits = 0x82AC, + + /// + /// PixelScale + /// + PixelScale = 0x830E, + + /// + /// IPTC (International Press Telecommunications Council) metadata. + /// See IPTC 4.1 specification. + /// + IPTC = 0x83BB, + + /// + /// IntergraphPacketData + /// + IntergraphPacketData = 0x847E, + + /// + /// IntergraphRegisters + /// + IntergraphRegisters = 0x847F, + + /// + /// IntergraphMatrix + /// + IntergraphMatrix = 0x8480, + + /// + /// ModelTiePoint + /// + ModelTiePoint = 0x8482, + + /// + /// SEMInfo + /// + SEMInfo = 0x8546, + + /// + /// ModelTransform + /// + ModelTransform = 0x85D8, + + /// + /// Collection of Photoshop 'Image Resource Blocks' (Embedded Metadata). + /// See Extracting the Thumbnail from the PhotoShop private TIFF Tag: https://www.awaresystems.be/imaging/tiff/tifftags/docs/photoshopthumbnail.html + /// + Photoshop = 0x8649, + + /// + /// ICC profile data. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/iccprofile.html + /// + IccProfile = 0x8773, + + /// + /// Used in interchangeable GeoTIFF files. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/geokeydirectorytag.html + /// This tag is also know as 'ProjectionInfoTag' and 'CoordSystemInfoTag' + /// This tag may be used to store the GeoKey Directory, which defines and references the "GeoKeys". + /// + GeoKeyDirectoryTag = 0x87AF, + + /// + /// Used in interchangeable GeoTIFF files. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/geodoubleparamstag.html + /// This tag is used to store all of the DOUBLE valued GeoKeys, referenced by the GeoKeyDirectoryTag. The meaning of any value of this double array is determined from the GeoKeyDirectoryTag reference pointing to it. FLOAT values should first be converted to DOUBLE and stored here. + /// + GeoDoubleParamsTag = 0x87B0, + + /// + /// Used in interchangeable GeoTIFF files. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/geoasciiparamstag.html + /// This tag is used to store all of the ASCII valued GeoKeys, referenced by the GeoKeyDirectoryTag. Since keys use offsets into tags, any special comments may be placed at the beginning of this tag. For the most part, the only keys that are ASCII valued are "Citation" keys, giving documentation and references for obscure projections, datums, etc. + /// + GeoAsciiParamsTag = 0x87B1, + + /// + /// ImageLayer + /// + ImageLayer = 0x87AC, + + /// + /// ExposureProgram + /// + [ExifTagDescription((ushort)0, "Not Defined")] + [ExifTagDescription((ushort)1, "Manual")] + [ExifTagDescription((ushort)2, "Program AE")] + [ExifTagDescription((ushort)3, "Aperture-priority AE")] + [ExifTagDescription((ushort)4, "Shutter speed priority AE")] + [ExifTagDescription((ushort)5, "Creative (Slow speed)")] + [ExifTagDescription((ushort)6, "Action (High speed)")] + [ExifTagDescription((ushort)7, "Portrait")] + [ExifTagDescription((ushort)8, "Landscape")] + [ExifTagDescription((ushort)9, "Bulb")] + ExposureProgram = 0x8822, + + /// + /// SpectralSensitivity + /// + SpectralSensitivity = 0x8824, + + /// + /// ISOSpeedRatings + /// + ISOSpeedRatings = 0x8827, + + /// + /// OECF + /// + OECF = 0x8828, + + /// + /// Interlace + /// + Interlace = 0x8829, + + /// + /// TimeZoneOffset + /// + TimeZoneOffset = 0x882A, + + /// + /// SelfTimerMode + /// + SelfTimerMode = 0x882B, + + /// + /// SensitivityType + /// + [ExifTagDescription((ushort)0, "Unknown")] + [ExifTagDescription((ushort)1, "Standard Output Sensitivity")] + [ExifTagDescription((ushort)2, "Recommended Exposure Index")] + [ExifTagDescription((ushort)3, "ISO Speed")] + [ExifTagDescription((ushort)4, "Standard Output Sensitivity and Recommended Exposure Index")] + [ExifTagDescription((ushort)5, "Standard Output Sensitivity and ISO Speed")] + [ExifTagDescription((ushort)6, "Recommended Exposure Index and ISO Speed")] + [ExifTagDescription((ushort)7, "Standard Output Sensitivity, Recommended Exposure Index and ISO Speed")] + SensitivityType = 0x8830, + + /// + /// StandardOutputSensitivity + /// + StandardOutputSensitivity = 0x8831, + + /// + /// RecommendedExposureIndex + /// + RecommendedExposureIndex = 0x8832, + + /// + /// ISOSpeed + /// + ISOSpeed = 0x8833, + + /// + /// ISOSpeedLatitudeyyy + /// + ISOSpeedLatitudeyyy = 0x8834, + + /// + /// ISOSpeedLatitudezzz + /// + ISOSpeedLatitudezzz = 0x8835, + + /// + /// FaxRecvParams + /// + FaxRecvParams = 0x885C, + + /// + /// FaxSubaddress + /// + FaxSubaddress = 0x885D, + + /// + /// FaxRecvTime + /// + FaxRecvTime = 0x885E, + + /// + /// ExifVersion + /// + ExifVersion = 0x9000, + + /// + /// DateTimeOriginal + /// + DateTimeOriginal = 0x9003, + + /// + /// DateTimeDigitized + /// + DateTimeDigitized = 0x9004, + + /// + /// OffsetTime + /// + OffsetTime = 0x9010, + + /// + /// OffsetTimeOriginal + /// + OffsetTimeOriginal = 0x9011, + + /// + /// OffsetTimeDigitized + /// + OffsetTimeDigitized = 0x9012, + + /// + /// ComponentsConfiguration + /// + ComponentsConfiguration = 0x9101, + + /// + /// CompressedBitsPerPixel + /// + CompressedBitsPerPixel = 0x9102, + + /// + /// ShutterSpeedValue + /// + ShutterSpeedValue = 0x9201, + + /// + /// ApertureValue + /// + ApertureValue = 0x9202, + + /// + /// BrightnessValue + /// + BrightnessValue = 0x9203, + + /// + /// ExposureBiasValue + /// + ExposureBiasValue = 0x9204, + + /// + /// MaxApertureValue + /// + MaxApertureValue = 0x9205, + + /// + /// SubjectDistance + /// + SubjectDistance = 0x9206, + + /// + /// MeteringMode + /// + [ExifTagDescription((ushort)0, "Unknown")] + [ExifTagDescription((ushort)1, "Average")] + [ExifTagDescription((ushort)2, "Center-weighted average")] + [ExifTagDescription((ushort)3, "Spot")] + [ExifTagDescription((ushort)4, "Multi-spot")] + [ExifTagDescription((ushort)5, "Multi-segment")] + [ExifTagDescription((ushort)6, "Partial")] + [ExifTagDescription((ushort)255, "Other")] + MeteringMode = 0x9207, + + /// + /// LightSource + /// + [ExifTagDescription((ushort)0, "Unknown")] + [ExifTagDescription((ushort)1, "Daylight")] + [ExifTagDescription((ushort)2, "Fluorescent")] + [ExifTagDescription((ushort)3, "Tungsten (Incandescent)")] + [ExifTagDescription((ushort)4, "Flash")] + [ExifTagDescription((ushort)9, "Fine Weather")] + [ExifTagDescription((ushort)10, "Cloudy")] + [ExifTagDescription((ushort)11, "Shade")] + [ExifTagDescription((ushort)12, "Daylight Fluorescent")] + [ExifTagDescription((ushort)13, "Day White Fluorescent")] + [ExifTagDescription((ushort)14, "Cool White Fluorescent")] + [ExifTagDescription((ushort)15, "White Fluorescent")] + [ExifTagDescription((ushort)16, "Warm White Fluorescent")] + [ExifTagDescription((ushort)17, "Standard Light A")] + [ExifTagDescription((ushort)18, "Standard Light B")] + [ExifTagDescription((ushort)19, "Standard Light C")] + [ExifTagDescription((ushort)20, "D55")] + [ExifTagDescription((ushort)21, "D65")] + [ExifTagDescription((ushort)22, "D75")] + [ExifTagDescription((ushort)23, "D50")] + [ExifTagDescription((ushort)24, "ISO Studio Tungsten")] + [ExifTagDescription((ushort)255, "Other")] + LightSource = 0x9208, + + /// + /// Flash + /// + [ExifTagDescription((ushort)0, "No Flash")] + [ExifTagDescription((ushort)1, "Fired")] + [ExifTagDescription((ushort)5, "Fired, Return not detected")] + [ExifTagDescription((ushort)7, "Fired, Return detected")] + [ExifTagDescription((ushort)8, "On, Did not fire")] + [ExifTagDescription((ushort)9, "On, Fired")] + [ExifTagDescription((ushort)13, "On, Return not detected")] + [ExifTagDescription((ushort)15, "On, Return detected")] + [ExifTagDescription((ushort)16, "Off, Did not fire")] + [ExifTagDescription((ushort)20, "Off, Did not fire, Return not detected")] + [ExifTagDescription((ushort)24, "Auto, Did not fire")] + [ExifTagDescription((ushort)25, "Auto, Fired")] + [ExifTagDescription((ushort)29, "Auto, Fired, Return not detected")] + [ExifTagDescription((ushort)31, "Auto, Fired, Return detected")] + [ExifTagDescription((ushort)32, "No flash function")] + [ExifTagDescription((ushort)48, "Off, No flash function")] + [ExifTagDescription((ushort)65, "Fired, Red-eye reduction")] + [ExifTagDescription((ushort)69, "Fired, Red-eye reduction, Return not detected")] + [ExifTagDescription((ushort)71, "Fired, Red-eye reduction, Return detected")] + [ExifTagDescription((ushort)73, "On, Red-eye reduction")] + [ExifTagDescription((ushort)77, "On, Red-eye reduction, Return not detected")] + [ExifTagDescription((ushort)79, "On, Red-eye reduction, Return detected")] + [ExifTagDescription((ushort)80, "Off, Red-eye reduction")] + [ExifTagDescription((ushort)88, "Auto, Did not fire, Red-eye reduction")] + [ExifTagDescription((ushort)89, "Auto, Fired, Red-eye reduction")] + [ExifTagDescription((ushort)93, "Auto, Fired, Red-eye reduction, Return not detected")] + [ExifTagDescription((ushort)95, "Auto, Fired, Red-eye reduction, Return detected")] + Flash = 0x9209, + + /// + /// FocalLength + /// + FocalLength = 0x920A, + + /// + /// FlashEnergy2 + /// + FlashEnergy2 = 0x920B, + + /// + /// SpatialFrequencyResponse2 + /// + SpatialFrequencyResponse2 = 0x920C, + + /// + /// Noise + /// + Noise = 0x920D, + + /// + /// FocalPlaneXResolution2 + /// + FocalPlaneXResolution2 = 0x920E, + + /// + /// FocalPlaneYResolution2 + /// + FocalPlaneYResolution2 = 0x920F, + + /// + /// FocalPlaneResolutionUnit2 + /// + [ExifTagDescription((ushort)1, "None")] + [ExifTagDescription((ushort)2, "Inches")] + [ExifTagDescription((ushort)3, "Centimeter")] + [ExifTagDescription((ushort)4, "Millimeter")] + [ExifTagDescription((ushort)5, "Micrometer")] + FocalPlaneResolutionUnit2 = 0x9210, + + /// + /// ImageNumber + /// + ImageNumber = 0x9211, + + /// + /// SecurityClassification + /// + [ExifTagDescription("C", "Confidential")] + [ExifTagDescription("R", "Restricted")] + [ExifTagDescription("S", "Secret")] + [ExifTagDescription("T", "Top Secret")] + [ExifTagDescription("U", "Unclassified")] + SecurityClassification = 0x9212, + + /// + /// ImageHistory + /// + ImageHistory = 0x9213, + + /// + /// SubjectArea + /// + SubjectArea = 0x9214, + + /// + /// ExposureIndex2 + /// + ExposureIndex2 = 0x9215, + + /// + /// TIFFEPStandardID + /// + TIFFEPStandardID = 0x9216, + + /// + /// SensingMethod + /// + [ExifTagDescription((ushort)1, "Not defined")] + [ExifTagDescription((ushort)2, "One-chip color area")] + [ExifTagDescription((ushort)3, "Two-chip color area")] + [ExifTagDescription((ushort)4, "Three-chip color area")] + [ExifTagDescription((ushort)5, "Color sequential area")] + [ExifTagDescription((ushort)7, "Trilinear")] + [ExifTagDescription((ushort)8, "Color sequential linear")] + SensingMethod2 = 0x9217, + + /// + /// MakerNote + /// + MakerNote = 0x927C, + + /// + /// UserComment + /// + UserComment = 0x9286, + + /// + /// SubsecTime + /// + SubsecTime = 0x9290, + + /// + /// SubsecTimeOriginal + /// + SubsecTimeOriginal = 0x9291, + + /// + /// SubsecTimeDigitized + /// + SubsecTimeDigitized = 0x9292, + + /// + /// ImageSourceData + /// + ImageSourceData = 0x935C, + + /// + /// AmbientTemperature + /// + AmbientTemperature = 0x9400, + + /// + /// Humidity + /// + Humidity = 0x9401, + + /// + /// Pressure + /// + Pressure = 0x9402, + + /// + /// WaterDepth + /// + WaterDepth = 0x9403, + + /// + /// Acceleration + /// + Acceleration = 0x9404, + + /// + /// CameraElevationAngle + /// + CameraElevationAngle = 0x9405, + + /// + /// XPTitle + /// + XPTitle = 0x9C9B, + + /// + /// XPComment + /// + XPComment = 0x9C9C, + + /// + /// XPAuthor + /// + XPAuthor = 0x9C9D, + + /// + /// XPKeywords + /// + XPKeywords = 0x9C9E, + + /// + /// XPSubject + /// + XPSubject = 0x9C9F, + + /// + /// FlashpixVersion + /// + FlashpixVersion = 0xA000, + + /// + /// ColorSpace + /// + [ExifTagDescription((ushort)1, "sRGB")] + [ExifTagDescription((ushort)2, "Adobe RGB")] + [ExifTagDescription((ushort)4093, "Wide Gamut RGB")] + [ExifTagDescription((ushort)65534, "ICC Profile")] + [ExifTagDescription((ushort)65535, "Uncalibrated")] + ColorSpace = 0xA001, + + /// + /// PixelXDimension + /// + PixelXDimension = 0xA002, + + /// + /// PixelYDimension + /// + PixelYDimension = 0xA003, + + /// + /// RelatedSoundFile + /// + RelatedSoundFile = 0xA004, + + /// + /// A pointer to the Exif-related Interoperability IFD. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/interoperability.html + /// Interoperability IFD is composed of tags which stores the information to ensure the Interoperability. + /// The Interoperability structure of Interoperability IFD is same as TIFF defined IFD structure but does not contain the image data characteristically compared with normal TIFF IFD. + /// + InteroperabilityIFD = 0xA005, + + /// + /// FlashEnergy + /// + FlashEnergy = 0xA20B, + + /// + /// SpatialFrequencyResponse + /// + SpatialFrequencyResponse = 0xA20C, + + /// + /// FocalPlaneXResolution + /// + FocalPlaneXResolution = 0xA20E, + + /// + /// FocalPlaneYResolution + /// + FocalPlaneYResolution = 0xA20F, + + /// + /// FocalPlaneResolutionUnit + /// + [ExifTagDescription((ushort)1, "None")] + [ExifTagDescription((ushort)2, "Inches")] + [ExifTagDescription((ushort)3, "Centimeter")] + [ExifTagDescription((ushort)4, "Millimeter")] + [ExifTagDescription((ushort)5, "Micrometer")] + FocalPlaneResolutionUnit = 0xA210, + + /// + /// SubjectLocation + /// + SubjectLocation = 0xA214, + + /// + /// ExposureIndex + /// + ExposureIndex = 0xA215, + + /// + /// SensingMethod + /// + [ExifTagDescription((ushort)1, "Not defined")] + [ExifTagDescription((ushort)2, "One-chip color area")] + [ExifTagDescription((ushort)3, "Two-chip color area")] + [ExifTagDescription((ushort)4, "Three-chip color area")] + [ExifTagDescription((ushort)5, "Color sequential area")] + [ExifTagDescription((ushort)7, "Trilinear")] + [ExifTagDescription((ushort)8, "Color sequential linear")] + SensingMethod = 0xA217, + + /// + /// FileSource + /// + FileSource = 0xA300, + + /// + /// SceneType + /// + SceneType = 0xA301, + + /// + /// CFAPattern + /// + CFAPattern = 0xA302, + + /// + /// CustomRendered + /// + [ExifTagDescription((ushort)1, "Normal")] + [ExifTagDescription((ushort)2, "Custom")] + CustomRendered = 0xA401, + + /// + /// ExposureMode + /// + [ExifTagDescription((ushort)0, "Auto")] + [ExifTagDescription((ushort)1, "Manual")] + [ExifTagDescription((ushort)2, "Auto bracket")] + ExposureMode = 0xA402, + + /// + /// WhiteBalance + /// + [ExifTagDescription((ushort)0, "Auto")] + [ExifTagDescription((ushort)1, "Manual")] + WhiteBalance = 0xA403, + + /// + /// DigitalZoomRatio + /// + DigitalZoomRatio = 0xA404, + + /// + /// FocalLengthIn35mmFilm + /// + FocalLengthIn35mmFilm = 0xA405, + + /// + /// SceneCaptureType + /// + [ExifTagDescription((ushort)0, "Standard")] + [ExifTagDescription((ushort)1, "Landscape")] + [ExifTagDescription((ushort)2, "Portrait")] + [ExifTagDescription((ushort)3, "Night")] + SceneCaptureType = 0xA406, + + /// + /// GainControl + /// + [ExifTagDescription((ushort)0, "None")] + [ExifTagDescription((ushort)1, "Low gain up")] + [ExifTagDescription((ushort)2, "High gain up")] + [ExifTagDescription((ushort)3, "Low gain down")] + [ExifTagDescription((ushort)4, "High gain down")] + GainControl = 0xA407, + + /// + /// Contrast + /// + [ExifTagDescription((ushort)0, "Normal")] + [ExifTagDescription((ushort)1, "Low")] + [ExifTagDescription((ushort)2, "High")] + Contrast = 0xA408, + + /// + /// Saturation + /// + [ExifTagDescription((ushort)0, "Normal")] + [ExifTagDescription((ushort)1, "Low")] + [ExifTagDescription((ushort)2, "High")] + Saturation = 0xA409, + + /// + /// Sharpness + /// + [ExifTagDescription((ushort)0, "Normal")] + [ExifTagDescription((ushort)1, "Soft")] + [ExifTagDescription((ushort)2, "Hard")] + Sharpness = 0xA40A, + + /// + /// DeviceSettingDescription + /// + DeviceSettingDescription = 0xA40B, + + /// + /// SubjectDistanceRange + /// + [ExifTagDescription((ushort)0, "Unknown")] + [ExifTagDescription((ushort)1, "Macro")] + [ExifTagDescription((ushort)2, "Close")] + [ExifTagDescription((ushort)3, "Distant")] + SubjectDistanceRange = 0xA40C, + + /// + /// ImageUniqueID + /// + ImageUniqueID = 0xA420, + + /// + /// OwnerName + /// + OwnerName = 0xA430, + + /// + /// SerialNumber + /// + SerialNumber = 0xA431, + + /// + /// LensSpecification + /// + LensSpecification = 0xA432, + + /// + /// LensMake + /// + LensMake = 0xA433, + + /// + /// LensModel + /// + LensModel = 0xA434, + + /// + /// LensSerialNumber + /// + LensSerialNumber = 0xA435, + + /// + /// GDALMetadata + /// + GDALMetadata = 0xA480, + + /// + /// GDALNoData + /// + GDALNoData = 0xA481, + + /// + /// GPSVersionID + /// + GPSVersionID = 0x0000, + + /// + /// GPSLatitudeRef + /// + GPSLatitudeRef = 0x0001, + + /// + /// GPSLatitude + /// + GPSLatitude = 0x0002, + + /// + /// GPSLongitudeRef + /// + GPSLongitudeRef = 0x0003, + + /// + /// GPSLongitude + /// + GPSLongitude = 0x0004, + + /// + /// GPSAltitudeRef + /// + GPSAltitudeRef = 0x0005, + + /// + /// GPSAltitude + /// + GPSAltitude = 0x0006, + + /// + /// GPSTimestamp + /// + GPSTimestamp = 0x0007, + + /// + /// GPSSatellites + /// + GPSSatellites = 0x0008, + + /// + /// GPSStatus + /// + GPSStatus = 0x0009, + + /// + /// GPSMeasureMode + /// + GPSMeasureMode = 0x000A, + + /// + /// GPSDOP + /// + GPSDOP = 0x000B, + + /// + /// GPSSpeedRef + /// + GPSSpeedRef = 0x000C, + + /// + /// GPSSpeed + /// + GPSSpeed = 0x000D, + + /// + /// GPSTrackRef + /// + GPSTrackRef = 0x000E, + + /// + /// GPSTrack + /// + GPSTrack = 0x000F, + + /// + /// GPSImgDirectionRef + /// + GPSImgDirectionRef = 0x0010, + + /// + /// GPSImgDirection + /// + GPSImgDirection = 0x0011, + + /// + /// GPSMapDatum + /// + GPSMapDatum = 0x0012, + + /// + /// GPSDestLatitudeRef + /// + GPSDestLatitudeRef = 0x0013, + + /// + /// GPSDestLatitude + /// + GPSDestLatitude = 0x0014, + + /// + /// GPSDestLongitudeRef + /// + GPSDestLongitudeRef = 0x0015, + + /// + /// GPSDestLongitude + /// + GPSDestLongitude = 0x0016, + + /// + /// GPSDestBearingRef + /// + GPSDestBearingRef = 0x0017, + + /// + /// GPSDestBearing + /// + GPSDestBearing = 0x0018, + + /// + /// GPSDestDistanceRef + /// + GPSDestDistanceRef = 0x0019, + + /// + /// GPSDestDistance + /// + GPSDestDistance = 0x001A, + + /// + /// GPSProcessingMethod + /// + GPSProcessingMethod = 0x001B, + + /// + /// GPSAreaInformation + /// + GPSAreaInformation = 0x001C, + + /// + /// GPSDateStamp + /// + GPSDateStamp = 0x001D, + + /// + /// GPSDifferential + /// + GPSDifferential = 0x001E, + + /// + /// GPSHPositioningError + /// + GPSHPositioningError = 0x001F, + + /// + /// Used in the Oce scanning process. + /// Identifies the scanticket used in the scanning process. + /// Includes a trailing zero. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/docs/oce.html + /// + OceScanjobDescription = 0xC427, + + /// + /// Used in the Oce scanning process. + /// Identifies the application to process the TIFF file that results from scanning. + /// Includes a trailing zero. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/docs/oce.html + /// + OceApplicationSelector = 0xC428, + + /// + /// Used in the Oce scanning process. + /// This is the user's answer to an optional question embedded in the Oce scanticket, and presented to that user before scanning. It can serve in further determination of the workflow. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/docs/oce.html + /// + OceIdentificationNumber = 0xC429, + + /// + /// Used in the Oce scanning process. + /// This tag encodes the imageprocessing done by the Oce ImageLogic module in the scanner to ensure optimal quality for certain workflows. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/docs/oce.html + /// + OceImageLogicCharacteristics = 0xC42A, + + /// + /// Alias Sketchbook Pro layer usage description. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/docs/alias.html + /// + AliasLayerMetadata = 0xC660, + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag{TValueType}.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag{TValueType}.cs new file mode 100644 index 0000000..37dc2f6 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag{TValueType}.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + /// Class that represents an exif tag from the Exif standard 2.31 with as the data type of the tag. + /// + /// The data type of the tag. + public sealed class ExifTag : ExifTag + { + internal ExifTag(ExifTagValue value) + : base((ushort)value) + { + } + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Tags/UnkownExifTag.cs b/ImageSharp/Metadata/Profiles/Exif/Tags/UnkownExifTag.cs new file mode 100644 index 0000000..4ceb2c4 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Tags/UnkownExifTag.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class UnkownExifTag : ExifTag + { + internal UnkownExifTag(ExifTagValue value) + : base((ushort)value) + { + } + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/EncodedString.cs b/ImageSharp/Metadata/Profiles/Exif/Values/EncodedString.cs new file mode 100644 index 0000000..18ecb2c --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/EncodedString.cs @@ -0,0 +1,115 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + /// The EXIF encoded string structure. + /// + public readonly struct EncodedString : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// Default use Unicode character code. + /// + /// The text value. + public EncodedString(string text) + : this(CharacterCode.Unicode, text) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The character code. + /// The text value. + public EncodedString(CharacterCode code, string text) + { + this.Text = text; + this.Code = code; + } + + /// + /// The 8-byte character code enum. + /// + public enum CharacterCode + { + /// + /// The ASCII (ITU-T T.50 IA5) character code. + /// + ASCII, + + /// + /// The JIS (X208-1990) character code. + /// + JIS, + + /// + /// The Unicode character code. + /// + Unicode, + + /// + /// The undefined character code. + /// + Undefined + } + + /// + /// Gets the character ode. + /// + public CharacterCode Code { get; } + + /// + /// Gets the text. + /// + public string Text { get; } + + /// + /// Converts the specified to an instance of this type. + /// + /// The text value. + public static implicit operator EncodedString(string text) => new(text); + + /// + /// Converts the specified to a . + /// + /// The to convert. + public static explicit operator string(EncodedString encodedString) => encodedString.Text; + + /// + /// Checks whether two structures are equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is equal to the parameter; + /// otherwise, false. + /// + public static bool operator ==(EncodedString left, EncodedString right) => left.Equals(right); + + /// + /// Checks whether two structures are not equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is not equal to the parameter; + /// otherwise, false. + /// + public static bool operator !=(EncodedString left, EncodedString right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is EncodedString other && this.Equals(other); + + /// + public bool Equals(EncodedString other) => this.Text == other.Text && this.Code == other.Code; + + /// + public override int GetHashCode() => HashCode.Combine(this.Text, this.Code); + + /// + public override string ToString() => this.Text; + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifArrayValue{TValueType}.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifArrayValue{TValueType}.cs new file mode 100644 index 0000000..18c8340 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifArrayValue{TValueType}.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal abstract class ExifArrayValue : ExifValue, IExifValue + { + protected ExifArrayValue(ExifTag tag) + : base(tag) + { + } + + protected ExifArrayValue(ExifTagValue tag) + : base(tag) + { + } + + internal ExifArrayValue(ExifArrayValue value) + : base(value) + { + } + + public override bool IsArray => true; + + public TValueType[]? Value { get; set; } + + public override object? GetValue() => this.Value; + + public override bool TrySetValue(object? value) + { + if (value is null) + { + this.Value = null; + return true; + } + + Type type = value.GetType(); + if (value.GetType() == typeof(TValueType[])) + { + this.Value = (TValueType[])value; + return true; + } + + if (type == typeof(TValueType)) + { + this.Value = [(TValueType)value]; + return true; + } + + return false; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifByte.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifByte.cs new file mode 100644 index 0000000..5eccf90 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifByte.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifByte : ExifValue + { + public ExifByte(ExifTag tag, ExifDataType dataType) + : base(tag) => this.DataType = dataType; + + public ExifByte(ExifTagValue tag, ExifDataType dataType) + : base(tag) => this.DataType = dataType; + + private ExifByte(ExifByte value) + : base(value) => this.DataType = value.DataType; + + public override ExifDataType DataType { get; } + + protected override string StringValue => this.Value.ToString("X2", CultureInfo.InvariantCulture); + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int intValue: + if (intValue is >= byte.MinValue and <= byte.MaxValue) + { + this.Value = (byte)intValue; + return true; + } + + return false; + default: + return base.TrySetValue(value); + } + } + + public override IExifValue DeepClone() => new ExifByte(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifByteArray.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifByteArray.cs new file mode 100644 index 0000000..074472f --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifByteArray.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifByteArray : ExifArrayValue + { + public ExifByteArray(ExifTag tag, ExifDataType dataType) + : base(tag) => this.DataType = dataType; + + public ExifByteArray(ExifTagValue tag, ExifDataType dataType) + : base(tag) => this.DataType = dataType; + + private ExifByteArray(ExifByteArray value) + : base(value) => this.DataType = value.DataType; + + public override ExifDataType DataType { get; } + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + if (value is int[] intArrayValue) + { + return this.TrySetSignedIntArray(intArrayValue); + } + + if (value is int intValue) + { + if (intValue is >= byte.MinValue and <= byte.MaxValue) + { + this.Value = [(byte)intValue]; + } + + return true; + } + + return false; + } + + public override IExifValue DeepClone() => new ExifByteArray(this); + + private bool TrySetSignedIntArray(int[] intArrayValue) + { + if (Array.FindIndex(intArrayValue, x => (uint)x > byte.MaxValue) >= 0) + { + return false; + } + + byte[] value = new byte[intArrayValue.Length]; + for (int i = 0; i < intArrayValue.Length; i++) + { + int s = intArrayValue[i]; + value[i] = (byte)s; + } + + this.Value = value; + return true; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifDouble.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifDouble.cs new file mode 100644 index 0000000..0dfb82b --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifDouble.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifDouble : ExifValue + { + public ExifDouble(ExifTag tag) + : base(tag) + { + } + + public ExifDouble(ExifTagValue tag) + : base(tag) + { + } + + private ExifDouble(ExifDouble value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.DoubleFloat; + + protected override string StringValue => this.Value.ToString(CultureInfo.InvariantCulture); + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int intValue: + this.Value = intValue; + return true; + default: + return false; + } + } + + public override IExifValue DeepClone() => new ExifDouble(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifDoubleArray.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifDoubleArray.cs new file mode 100644 index 0000000..ee99a1e --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifDoubleArray.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifDoubleArray : ExifArrayValue + { + public ExifDoubleArray(ExifTag tag) + : base(tag) + { + } + + public ExifDoubleArray(ExifTagValue tag) + : base(tag) + { + } + + private ExifDoubleArray(ExifDoubleArray value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.DoubleFloat; + + public override IExifValue DeepClone() => new ExifDoubleArray(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifEncodedString.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifEncodedString.cs new file mode 100644 index 0000000..1dcf3fe --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifEncodedString.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifEncodedString : ExifValue + { + public ExifEncodedString(ExifTag tag) + : base(tag) + { + } + + public ExifEncodedString(ExifTagValue tag) + : base(tag) + { + } + + private ExifEncodedString(ExifEncodedString value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.Undefined; + + protected override string StringValue => this.Value.Text; + + public bool TrySetValue(object? value, ByteOrder order) + { + if (base.TrySetValue(value)) + { + return true; + } + + if (value is string stringValue) + { + this.Value = new EncodedString(stringValue); + return true; + } + else if (value is byte[] buffer) + { + if (ExifEncodedStringHelpers.TryParse(buffer, order, out EncodedString encodedString)) + { + this.Value = encodedString; + return true; + } + } + + return false; + } + + public override bool TrySetValue(object? value) + => this.TrySetValue(value, ByteOrder.LittleEndian); + + public override IExifValue DeepClone() => new ExifEncodedString(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifFloat.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifFloat.cs new file mode 100644 index 0000000..4e718ad --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifFloat.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifFloat : ExifValue + { + public ExifFloat(ExifTagValue tag) + : base(tag) + { + } + + private ExifFloat(ExifFloat value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SingleFloat; + + protected override string StringValue => this.Value.ToString(CultureInfo.InvariantCulture); + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int intValue: + this.Value = intValue; + return true; + default: + return false; + } + } + + public override IExifValue DeepClone() => new ExifFloat(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifFloatArray.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifFloatArray.cs new file mode 100644 index 0000000..dbebb28 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifFloatArray.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifFloatArray : ExifArrayValue + { + public ExifFloatArray(ExifTagValue tag) + : base(tag) + { + } + + private ExifFloatArray(ExifFloatArray value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SingleFloat; + + public override IExifValue DeepClone() => new ExifFloatArray(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong.cs new file mode 100644 index 0000000..c688de4 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifLong : ExifValue + { + public ExifLong(ExifTag tag) + : base(tag) + { + } + + public ExifLong(ExifTagValue tag) + : base(tag) + { + } + + private ExifLong(ExifLong value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.Long; + + protected override string StringValue => this.Value.ToString(CultureInfo.InvariantCulture); + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int intValue: + if (intValue >= uint.MinValue) + { + this.Value = (uint)intValue; + return true; + } + + return false; + default: + return false; + } + } + + public override IExifValue DeepClone() => new ExifLong(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8.cs new file mode 100644 index 0000000..d8f6a8b --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifLong8 : ExifValue + { + public ExifLong8(ExifTag tag) + : base(tag) + { + } + + public ExifLong8(ExifTagValue tag) + : base(tag) + { + } + + private ExifLong8(ExifLong8 value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.Long8; + + protected override string StringValue => this.Value.ToString(CultureInfo.InvariantCulture); + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int intValue: + if (intValue >= uint.MinValue) + { + this.Value = (uint)intValue; + return true; + } + + return false; + case uint uintValue: + this.Value = uintValue; + + return true; + case long intValue: + if (intValue >= 0) + { + this.Value = (ulong)intValue; + return true; + } + + return false; + default: + + return false; + } + } + + public override IExifValue DeepClone() => new ExifLong8(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8Array.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8Array.cs new file mode 100644 index 0000000..f2793a3 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8Array.cs @@ -0,0 +1,162 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifLong8Array : ExifArrayValue + { + public ExifLong8Array(ExifTagValue tag) + : base(tag) + { + } + + private ExifLong8Array(ExifLong8Array value) + : base(value) + { + } + + public override ExifDataType DataType + { + get + { + if (this.Value is not null) + { + foreach (ulong value in this.Value) + { + if (value > uint.MaxValue) + { + return ExifDataType.Long8; + } + } + } + + return ExifDataType.Long; + } + } + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int val: + return this.SetSingle((ulong)Numerics.Clamp(val, 0, int.MaxValue)); + + case uint val: + return this.SetSingle(val); + + case short val: + return this.SetSingle((ulong)Numerics.Clamp(val, 0, short.MaxValue)); + + case ushort val: + return this.SetSingle(val); + + case long val: + return this.SetSingle((ulong)Numerics.Clamp(val, 0, long.MaxValue)); + + case long[] array: + if (value.GetType() == typeof(ulong[])) + { + return this.SetArray((ulong[])value); + } + + return this.SetArray(array); + + case int[] array: + if (value.GetType() == typeof(uint[])) + { + return this.SetArray((uint[])value); + } + + return this.SetArray(array); + + case short[] array: + if (value.GetType() == typeof(ushort[])) + { + return this.SetArray((ushort[])value); + } + + return this.SetArray(array); + } + + return false; + } + + public override IExifValue DeepClone() => new ExifLong8Array(this); + + private bool SetSingle(ulong value) + { + this.Value = [value]; + return true; + } + + private bool SetArray(long[] values) + { + ulong[] numbers = new ulong[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = (ulong)(values[i] < 0 ? 0 : values[i]); + } + + this.Value = numbers; + return true; + } + + private bool SetArray(ulong[] values) + { + this.Value = values; + return true; + } + + private bool SetArray(int[] values) + { + ulong[] numbers = new ulong[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = (ulong)Numerics.Clamp(values[i], 0, int.MaxValue); + } + + this.Value = numbers; + return true; + } + + private bool SetArray(uint[] values) + { + ulong[] numbers = new ulong[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = values[i]; + } + + this.Value = numbers; + return true; + } + + private bool SetArray(short[] values) + { + ulong[] numbers = new ulong[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = (ulong)Numerics.Clamp(values[i], 0, short.MaxValue); + } + + this.Value = numbers; + return true; + } + + private bool SetArray(ushort[] values) + { + ulong[] numbers = new ulong[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = values[i]; + } + + this.Value = numbers; + return true; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifLongArray.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifLongArray.cs new file mode 100644 index 0000000..689eff7 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifLongArray.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifLongArray : ExifArrayValue + { + public ExifLongArray(ExifTag tag) + : base(tag) + { + } + + public ExifLongArray(ExifTagValue tag) + : base(tag) + { + } + + private ExifLongArray(ExifLongArray value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.Long; + + public override IExifValue DeepClone() => new ExifLongArray(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumber.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumber.cs new file mode 100644 index 0000000..08144a8 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumber.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifNumber : ExifValue + { + public ExifNumber(ExifTag tag) + : base(tag) + { + } + + private ExifNumber(ExifNumber value) + : base(value) + { + } + + public override ExifDataType DataType + { + get + { + if (this.Value > ushort.MaxValue) + { + return ExifDataType.Long; + } + + return ExifDataType.Short; + } + } + + protected override string StringValue => this.Value.ToString(CultureInfo.InvariantCulture); + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int intValue: + if (intValue >= uint.MinValue) + { + this.Value = (uint)intValue; + return true; + } + + return false; + case uint uintValue: + this.Value = uintValue; + return true; + case short shortValue: + if (shortValue >= uint.MinValue) + { + this.Value = (uint)shortValue; + return true; + } + + return false; + case ushort ushortValue: + this.Value = ushortValue; + return true; + default: + return false; + } + } + + public override IExifValue DeepClone() => new ExifNumber(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs new file mode 100644 index 0000000..de22492 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs @@ -0,0 +1,130 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifNumberArray : ExifArrayValue + { + public ExifNumberArray(ExifTag tag) + : base(tag) + { + } + + private ExifNumberArray(ExifNumberArray value) + : base(value) + { + } + + public override ExifDataType DataType + { + get + { + if (this.Value is not null) + { + foreach (Number value in this.Value) + { + if (value > ushort.MaxValue) + { + return ExifDataType.Long; + } + } + } + + return ExifDataType.Short; + } + } + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int val: + return this.SetSingle(val); + case uint val: + return this.SetSingle(val); + case short val: + return this.SetSingle(val); + case ushort val: + return this.SetSingle(val); + case int[] array: + // workaround for inconsistent covariance of value-typed arrays + if (value.GetType() == typeof(uint[])) + { + return this.SetArray((uint[])value); + } + + return this.SetArray(array); + + case short[] array: + if (value.GetType() == typeof(ushort[])) + { + return this.SetArray((ushort[])value); + } + + return this.SetArray(array); + } + + return false; + } + + public override IExifValue DeepClone() => new ExifNumberArray(this); + + private bool SetSingle(Number value) + { + this.Value = [value]; + return true; + } + + private bool SetArray(int[] values) + { + Number[] numbers = new Number[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = values[i]; + } + + this.Value = numbers; + return true; + } + + private bool SetArray(uint[] values) + { + Number[] numbers = new Number[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = values[i]; + } + + this.Value = numbers; + return true; + } + + private bool SetArray(short[] values) + { + Number[] numbers = new Number[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = values[i]; + } + + this.Value = numbers; + return true; + } + + private bool SetArray(ushort[] values) + { + Number[] numbers = new Number[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = values[i]; + } + + this.Value = numbers; + return true; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifOrientationMode.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifOrientationMode.cs new file mode 100644 index 0000000..40ab8d4 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifOrientationMode.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + /// Enumerates the available orientation values supplied by EXIF metadata. + /// + public static class ExifOrientationMode + { + /// + /// Unknown rotation. + /// + public const ushort Unknown = 0; + + /// + /// The 0th row at the top, the 0th column on the left. + /// + public const ushort TopLeft = 1; + + /// + /// The 0th row at the top, the 0th column on the right. + /// + public const ushort TopRight = 2; + + /// + /// The 0th row at the bottom, the 0th column on the right. + /// + public const ushort BottomRight = 3; + + /// + /// The 0th row at the bottom, the 0th column on the left. + /// + public const ushort BottomLeft = 4; + + /// + /// The 0th row on the left, the 0th column at the top. + /// + public const ushort LeftTop = 5; + + /// + /// The 0th row at the right, the 0th column at the top. + /// + public const ushort RightTop = 6; + + /// + /// The 0th row on the right, the 0th column at the bottom. + /// + public const ushort RightBottom = 7; + + /// + /// The 0th row on the left, the 0th column at the bottom. + /// + public const ushort LeftBottom = 8; + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifRational.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifRational.cs new file mode 100644 index 0000000..f703450 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifRational.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifRational : ExifValue + { + public ExifRational(ExifTag tag) + : base(tag) + { + } + + public ExifRational(ExifTagValue tag) + : base(tag) + { + } + + private ExifRational(ExifRational value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.Rational; + + protected override string StringValue => this.Value.ToString(CultureInfo.InvariantCulture); + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case SignedRational signed: + + if (signed.Numerator >= uint.MinValue && signed.Denominator >= uint.MinValue) + { + this.Value = new Rational((uint)signed.Numerator, (uint)signed.Denominator); + } + + return true; + default: + return false; + } + } + + public override IExifValue DeepClone() => new ExifRational(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifRationalArray.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifRationalArray.cs new file mode 100644 index 0000000..3e1baee --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifRationalArray.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifRationalArray : ExifArrayValue + { + public ExifRationalArray(ExifTag tag) + : base(tag) + { + } + + public ExifRationalArray(ExifTagValue tag) + : base(tag) + { + } + + private ExifRationalArray(ExifRationalArray value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.Rational; + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + if (value is SignedRational[] signedArray) + { + return this.TrySetSignedArray(signedArray); + } + + if (value is SignedRational signed) + { + if (signed.Numerator >= 0 && signed.Denominator >= 0) + { + this.Value = [new Rational((uint)signed.Numerator, (uint)signed.Denominator)]; + } + + return true; + } + + return false; + } + + public override IExifValue DeepClone() => new ExifRationalArray(this); + + private bool TrySetSignedArray(SignedRational[] signed) + { + if (Array.FindIndex(signed, x => x.Numerator < 0 || x.Denominator < 0) > -1) + { + return false; + } + + Rational[] unsigned = new Rational[signed.Length]; + for (int i = 0; i < signed.Length; i++) + { + SignedRational s = signed[i]; + unsigned[i] = new Rational((uint)s.Numerator, (uint)s.Denominator); + } + + this.Value = unsigned; + return true; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifShort.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifShort.cs new file mode 100644 index 0000000..92fdf99 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifShort.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifShort : ExifValue + { + public ExifShort(ExifTag tag) + : base(tag) + { + } + + public ExifShort(ExifTagValue tag) + : base(tag) + { + } + + private ExifShort(ExifShort value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.Short; + + protected override string StringValue => this.Value.ToString(CultureInfo.InvariantCulture); + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int intValue: + if (intValue is >= ushort.MinValue and <= ushort.MaxValue) + { + this.Value = (ushort)intValue; + return true; + } + + return false; + case short shortValue: + if (shortValue >= ushort.MinValue) + { + this.Value = (ushort)shortValue; + return true; + } + + return false; + default: + return false; + } + } + + public override IExifValue DeepClone() => new ExifShort(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifShortArray.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifShortArray.cs new file mode 100644 index 0000000..de082aa --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifShortArray.cs @@ -0,0 +1,104 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifShortArray : ExifArrayValue + { + public ExifShortArray(ExifTag tag) + : base(tag) + { + } + + public ExifShortArray(ExifTagValue tag) + : base(tag) + { + } + + private ExifShortArray(ExifShortArray value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.Short; + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + if (value is int[] signedIntArray) + { + return this.TrySetSignedIntArray(signedIntArray); + } + + if (value is short[] signedShortArray) + { + return this.TrySetSignedShortArray(signedShortArray); + } + + if (value is int signedInt) + { + if (signedInt is >= ushort.MinValue and <= ushort.MaxValue) + { + this.Value = [(ushort)signedInt]; + } + + return true; + } + + if (value is short signedShort) + { + if (signedShort >= ushort.MinValue) + { + this.Value = [(ushort)signedShort]; + } + + return true; + } + + return false; + } + + public override IExifValue DeepClone() => new ExifShortArray(this); + + private bool TrySetSignedIntArray(int[] signed) + { + if (Array.FindIndex(signed, x => x is < ushort.MinValue or > ushort.MaxValue) > -1) + { + return false; + } + + ushort[] unsigned = new ushort[signed.Length]; + for (int i = 0; i < signed.Length; i++) + { + int s = signed[i]; + unsigned[i] = (ushort)s; + } + + this.Value = unsigned; + return true; + } + + private bool TrySetSignedShortArray(short[] signed) + { + if (Array.FindIndex(signed, x => x < ushort.MinValue) > -1) + { + return false; + } + + ushort[] unsigned = new ushort[signed.Length]; + for (int i = 0; i < signed.Length; i++) + { + short s = signed[i]; + unsigned[i] = (ushort)s; + } + + this.Value = unsigned; + return true; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedByte.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedByte.cs new file mode 100644 index 0000000..eb15583 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedByte.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifSignedByte : ExifValue + { + public ExifSignedByte(ExifTagValue tag) + : base(tag) + { + } + + private ExifSignedByte(ExifSignedByte value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SignedByte; + + protected override string StringValue => this.Value.ToString("X2", CultureInfo.InvariantCulture); + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int intValue: + if (intValue is >= sbyte.MinValue and <= sbyte.MaxValue) + { + this.Value = (sbyte)intValue; + return true; + } + + return false; + default: + return false; + } + } + + public override IExifValue DeepClone() => new ExifSignedByte(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedByteArray.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedByteArray.cs new file mode 100644 index 0000000..88817d4 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedByteArray.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifSignedByteArray : ExifArrayValue + { + public ExifSignedByteArray(ExifTagValue tag) + : base(tag) + { + } + + private ExifSignedByteArray(ExifSignedByteArray value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SignedByte; + + public override IExifValue DeepClone() => new ExifSignedByteArray(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong.cs new file mode 100644 index 0000000..5c6f6ff --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifSignedLong : ExifValue + { + public ExifSignedLong(ExifTagValue tag) + : base(tag) + { + } + + private ExifSignedLong(ExifSignedLong value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SignedLong; + + protected override string StringValue => this.Value.ToString(CultureInfo.InvariantCulture); + + public override IExifValue DeepClone() => new ExifSignedLong(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8.cs new file mode 100644 index 0000000..02af1a0 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifSignedLong8 : ExifValue + { + public ExifSignedLong8(ExifTagValue tag) + : base(tag) + { + } + + private ExifSignedLong8(ExifSignedLong8 value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SignedLong8; + + protected override string StringValue => this.Value.ToString(CultureInfo.InvariantCulture); + + public override IExifValue DeepClone() => new ExifSignedLong8(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8Array.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8Array.cs new file mode 100644 index 0000000..3d6058d --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8Array.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifSignedLong8Array : ExifArrayValue + { + public ExifSignedLong8Array(ExifTagValue tag) + : base(tag) + { + } + + private ExifSignedLong8Array(ExifSignedLong8Array value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SignedLong8; + + public override IExifValue DeepClone() => new ExifSignedLong8Array(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLongArray.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLongArray.cs new file mode 100644 index 0000000..cb1a0bc --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLongArray.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifSignedLongArray : ExifArrayValue + { + public ExifSignedLongArray(ExifTagValue tag) + : base(tag) + { + } + + private ExifSignedLongArray(ExifSignedLongArray value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SignedLong; + + public override IExifValue DeepClone() => new ExifSignedLongArray(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedRational.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedRational.cs new file mode 100644 index 0000000..313ba98 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedRational.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifSignedRational : ExifValue + { + internal ExifSignedRational(ExifTag tag) + : base(tag) + { + } + + internal ExifSignedRational(ExifTagValue tag) + : base(tag) + { + } + + private ExifSignedRational(ExifSignedRational value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SignedRational; + + protected override string StringValue => this.Value.ToString(CultureInfo.InvariantCulture); + + public override IExifValue DeepClone() => new ExifSignedRational(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedRationalArray.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedRationalArray.cs new file mode 100644 index 0000000..4a9adb5 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedRationalArray.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifSignedRationalArray : ExifArrayValue + { + public ExifSignedRationalArray(ExifTag tag) + : base(tag) + { + } + + public ExifSignedRationalArray(ExifTagValue tag) + : base(tag) + { + } + + private ExifSignedRationalArray(ExifSignedRationalArray value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SignedRational; + + public override IExifValue DeepClone() => new ExifSignedRationalArray(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShort.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShort.cs new file mode 100644 index 0000000..4d07eb5 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShort.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifSignedShort : ExifValue + { + public ExifSignedShort(ExifTagValue tag) + : base(tag) + { + } + + private ExifSignedShort(ExifSignedShort value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SignedShort; + + protected override string StringValue => this.Value.ToString(CultureInfo.InvariantCulture); + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int intValue: + if (intValue is >= short.MinValue and <= short.MaxValue) + { + this.Value = (short)intValue; + return true; + } + + return false; + default: + return false; + } + } + + public override IExifValue DeepClone() => new ExifSignedShort(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShortArray.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShortArray.cs new file mode 100644 index 0000000..2aca8a3 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShortArray.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifSignedShortArray : ExifArrayValue + { + public ExifSignedShortArray(ExifTag tag) + : base(tag) + { + } + + public ExifSignedShortArray(ExifTagValue tag) + : base(tag) + { + } + + private ExifSignedShortArray(ExifSignedShortArray value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SignedShort; + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + if (value is int[] intArray) + { + return this.TrySetSignedArray(intArray); + } + + if (value is int intValue) + { + if (intValue is >= short.MinValue and <= short.MaxValue) + { + this.Value = [(short)intValue]; + } + + return true; + } + + return false; + } + + public override IExifValue DeepClone() => new ExifSignedShortArray(this); + + private bool TrySetSignedArray(int[] intArray) + { + if (Array.FindIndex(intArray, x => x is < short.MinValue or > short.MaxValue) > -1) + { + return false; + } + + short[] value = new short[intArray.Length]; + for (int i = 0; i < intArray.Length; i++) + { + int s = intArray[i]; + value[i] = (short)s; + } + + this.Value = value; + return true; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifString.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifString.cs new file mode 100644 index 0000000..063c236 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifString.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifString : ExifValue + { + public ExifString(ExifTag tag) + : base(tag) + { + } + + public ExifString(ExifTagValue tag) + : base(tag) + { + } + + private ExifString(ExifString value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.Ascii; + + protected override string? StringValue => this.Value; + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int intValue: + this.Value = intValue.ToString(CultureInfo.InvariantCulture); + return true; + default: + return false; + } + } + + public override IExifValue DeepClone() => new ExifString(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifUcs2String.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifUcs2String.cs new file mode 100644 index 0000000..6a5a812 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifUcs2String.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal sealed class ExifUcs2String : ExifValue + { + public ExifUcs2String(ExifTag tag) + : base(tag) + { + } + + public ExifUcs2String(ExifTagValue tag) + : base(tag) + { + } + + private ExifUcs2String(ExifUcs2String value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.Byte; + + protected override string? StringValue => this.Value; + + public override object? GetValue() => this.Value; + + public override bool TrySetValue(object? value) + { + if (base.TrySetValue(value)) + { + return true; + } + + if (value is byte[] buffer) + { + this.Value = ExifUcs2StringHelpers.Ucs2Encoding.GetString(buffer); + return true; + } + + return false; + } + + public override IExifValue DeepClone() => new ExifUcs2String(this); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifValue.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifValue.cs new file mode 100644 index 0000000..d585778 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifValue.cs @@ -0,0 +1,84 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + [DebuggerDisplay("{Tag} = {IsArray?\"[..]\":ToString(),nq} ({GetType().Name,nq})")] + internal abstract class ExifValue : IExifValue, IEquatable + { + protected ExifValue(ExifTag tag) => this.Tag = tag; + + protected ExifValue(ExifTagValue tag) => this.Tag = new UnkownExifTag(tag); + + internal ExifValue(ExifValue other) + { + Guard.NotNull(other, nameof(other)); + + this.DataType = other.DataType; + this.IsArray = other.IsArray; + this.Tag = other.Tag; + + if (!other.IsArray) + { + // All types are value types except for string which is immutable so safe to simply assign. + this.TrySetValue(other.GetValue()); + } + else + { + // All array types are value types so Clone() is sufficient here. + Array? array = (Array?)other.GetValue(); + this.TrySetValue(array?.Clone()); + } + } + + public virtual ExifDataType DataType { get; } + + public virtual bool IsArray { get; } + + public ExifTag Tag { get; } + + public static bool operator ==(ExifValue left, ExifTag right) => Equals(left, right); + + public static bool operator !=(ExifValue left, ExifTag right) => !Equals(left, right); + + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is ExifTag tag) + { + return this.Equals(tag); + } + + if (obj is ExifValue value) + { + return this.Tag.Equals(value.Tag) && Equals(this.GetValue(), value.GetValue()); + } + + return false; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public bool Equals(ExifTag? other) => this.Tag.Equals(other); + + [MethodImpl(InliningOptions.ShortMethod)] + public override int GetHashCode() => HashCode.Combine(this.Tag, this.GetValue()); + + public abstract object? GetValue(); + + public abstract bool TrySetValue(object? value); + + public abstract IExifValue DeepClone(); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs new file mode 100644 index 0000000..851b680 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs @@ -0,0 +1,292 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal static class ExifValues + { + public static ExifValue? Create(ExifTagValue tag) => (ExifValue?)CreateValue(tag); + + public static ExifValue? Create(ExifTag tag) => (ExifValue?)CreateValue((ExifTagValue)(ushort)tag); + + public static ExifValue? Create(ExifTagValue tag, ExifDataType dataType, ulong numberOfComponents) => Create(tag, dataType, numberOfComponents != 1); + + public static ExifValue? Create(ExifTagValue tag, ExifDataType dataType, bool isArray) + => dataType switch + { + ExifDataType.Byte => isArray ? new ExifByteArray(tag, dataType) : new ExifByte(tag, dataType), + ExifDataType.DoubleFloat => isArray ? new ExifDoubleArray(tag) : new ExifDouble(tag), + ExifDataType.SingleFloat => isArray ? new ExifFloatArray(tag) : new ExifFloat(tag), + ExifDataType.Long => isArray ? new ExifLongArray(tag) : new ExifLong(tag), + ExifDataType.Long8 => isArray ? new ExifLong8Array(tag) : new ExifLong8(tag), + ExifDataType.Rational => isArray ? new ExifRationalArray(tag) : new ExifRational(tag), + ExifDataType.Short => isArray ? new ExifShortArray(tag) : new ExifShort(tag), + ExifDataType.SignedByte => isArray ? new ExifSignedByteArray(tag) : new ExifSignedByte(tag), + ExifDataType.SignedLong => isArray ? new ExifSignedLongArray(tag) : new ExifSignedLong(tag), + ExifDataType.SignedLong8 => isArray ? new ExifSignedLong8Array(tag) : new ExifSignedLong8(tag), + ExifDataType.SignedRational => isArray ? new ExifSignedRationalArray(tag) : new ExifSignedRational(tag), + ExifDataType.SignedShort => isArray ? new ExifSignedShortArray(tag) : new ExifSignedShort(tag), + ExifDataType.Ascii => new ExifString(tag), + ExifDataType.Undefined => isArray ? new ExifByteArray(tag, dataType) : new ExifByte(tag, dataType), + _ => null, + }; + + private static object? CreateValue(ExifTagValue tag) + => tag switch + { + ExifTagValue.FaxProfile => new ExifByte(ExifTag.FaxProfile, ExifDataType.Byte), + ExifTagValue.ModeNumber => new ExifByte(ExifTag.ModeNumber, ExifDataType.Byte), + ExifTagValue.GPSAltitudeRef => new ExifByte(ExifTag.GPSAltitudeRef, ExifDataType.Byte), + ExifTagValue.ClipPath => new ExifByteArray(ExifTag.ClipPath, ExifDataType.Byte), + ExifTagValue.VersionYear => new ExifByteArray(ExifTag.VersionYear, ExifDataType.Byte), + ExifTagValue.XMP => new ExifByteArray(ExifTag.XMP, ExifDataType.Byte), + ExifTagValue.CFAPattern2 => new ExifByteArray(ExifTag.CFAPattern2, ExifDataType.Byte), + ExifTagValue.TIFFEPStandardID => new ExifByteArray(ExifTag.TIFFEPStandardID, ExifDataType.Byte), + ExifTagValue.GPSVersionID => new ExifByteArray(ExifTag.GPSVersionID, ExifDataType.Byte), + ExifTagValue.PixelScale => new ExifDoubleArray(ExifTag.PixelScale), + ExifTagValue.IntergraphMatrix => new ExifDoubleArray(ExifTag.IntergraphMatrix), + ExifTagValue.ModelTiePoint => new ExifDoubleArray(ExifTag.ModelTiePoint), + ExifTagValue.ModelTransform => new ExifDoubleArray(ExifTag.ModelTransform), + ExifTagValue.SubfileType => new ExifLong(ExifTag.SubfileType), + ExifTagValue.SubIFDOffset => new ExifLong(ExifTag.SubIFDOffset), + ExifTagValue.GPSIFDOffset => new ExifLong(ExifTag.GPSIFDOffset), + ExifTagValue.T4Options => new ExifLong(ExifTag.T4Options), + ExifTagValue.T6Options => new ExifLong(ExifTag.T6Options), + ExifTagValue.XClipPathUnits => new ExifLong(ExifTag.XClipPathUnits), + ExifTagValue.YClipPathUnits => new ExifLong(ExifTag.YClipPathUnits), + ExifTagValue.ProfileType => new ExifLong(ExifTag.ProfileType), + ExifTagValue.CodingMethods => new ExifLong(ExifTag.CodingMethods), + ExifTagValue.T82ptions => new ExifLong(ExifTag.T82ptions), + ExifTagValue.JPEGInterchangeFormat => new ExifLong(ExifTag.JPEGInterchangeFormat), + ExifTagValue.JPEGInterchangeFormatLength => new ExifLong(ExifTag.JPEGInterchangeFormatLength), + ExifTagValue.MDFileTag => new ExifLong(ExifTag.MDFileTag), + ExifTagValue.StandardOutputSensitivity => new ExifLong(ExifTag.StandardOutputSensitivity), + ExifTagValue.RecommendedExposureIndex => new ExifLong(ExifTag.RecommendedExposureIndex), + ExifTagValue.ISOSpeed => new ExifLong(ExifTag.ISOSpeed), + ExifTagValue.ISOSpeedLatitudeyyy => new ExifLong(ExifTag.ISOSpeedLatitudeyyy), + ExifTagValue.ISOSpeedLatitudezzz => new ExifLong(ExifTag.ISOSpeedLatitudezzz), + ExifTagValue.FaxRecvParams => new ExifLong(ExifTag.FaxRecvParams), + ExifTagValue.FaxRecvTime => new ExifLong(ExifTag.FaxRecvTime), + ExifTagValue.ImageNumber => new ExifLong(ExifTag.ImageNumber), + ExifTagValue.FreeOffsets => new ExifLongArray(ExifTag.FreeOffsets), + ExifTagValue.FreeByteCounts => new ExifLongArray(ExifTag.FreeByteCounts), + ExifTagValue.ColorResponseUnit => new ExifLongArray(ExifTag.ColorResponseUnit), + ExifTagValue.SMinSampleValue => new ExifLongArray(ExifTag.SMinSampleValue), + ExifTagValue.SMaxSampleValue => new ExifLongArray(ExifTag.SMaxSampleValue), + ExifTagValue.JPEGQTables => new ExifLongArray(ExifTag.JPEGQTables), + ExifTagValue.JPEGDCTables => new ExifLongArray(ExifTag.JPEGDCTables), + ExifTagValue.JPEGACTables => new ExifLongArray(ExifTag.JPEGACTables), + ExifTagValue.StripRowCounts => new ExifLongArray(ExifTag.StripRowCounts), + ExifTagValue.IntergraphRegisters => new ExifLongArray(ExifTag.IntergraphRegisters), + ExifTagValue.SubIFDs => new ExifLongArray(ExifTag.SubIFDs), + ExifTagValue.ImageWidth => new ExifNumber(ExifTag.ImageWidth), + ExifTagValue.ImageLength => new ExifNumber(ExifTag.ImageLength), + ExifTagValue.RowsPerStrip => new ExifNumber(ExifTag.RowsPerStrip), + ExifTagValue.TileWidth => new ExifNumber(ExifTag.TileWidth), + ExifTagValue.TileLength => new ExifNumber(ExifTag.TileLength), + ExifTagValue.BadFaxLines => new ExifNumber(ExifTag.BadFaxLines), + ExifTagValue.ConsecutiveBadFaxLines => new ExifNumber(ExifTag.ConsecutiveBadFaxLines), + ExifTagValue.PixelXDimension => new ExifNumber(ExifTag.PixelXDimension), + ExifTagValue.PixelYDimension => new ExifNumber(ExifTag.PixelYDimension), + ExifTagValue.StripByteCounts => new ExifNumberArray(ExifTag.StripByteCounts), + ExifTagValue.StripOffsets => new ExifNumberArray(ExifTag.StripOffsets), + ExifTagValue.TileByteCounts => new ExifNumberArray(ExifTag.TileByteCounts), + ExifTagValue.TileOffsets => new ExifNumberArray(ExifTag.TileOffsets), + ExifTagValue.ImageLayer => new ExifNumberArray(ExifTag.ImageLayer), + ExifTagValue.XPosition => new ExifRational(ExifTag.XPosition), + ExifTagValue.YPosition => new ExifRational(ExifTag.YPosition), + ExifTagValue.XResolution => new ExifRational(ExifTag.XResolution), + ExifTagValue.YResolution => new ExifRational(ExifTag.YResolution), + ExifTagValue.BatteryLevel => new ExifRational(ExifTag.BatteryLevel), + ExifTagValue.ExposureTime => new ExifRational(ExifTag.ExposureTime), + ExifTagValue.FNumber => new ExifRational(ExifTag.FNumber), + ExifTagValue.MDScalePixel => new ExifRational(ExifTag.MDScalePixel), + ExifTagValue.CompressedBitsPerPixel => new ExifRational(ExifTag.CompressedBitsPerPixel), + ExifTagValue.ApertureValue => new ExifRational(ExifTag.ApertureValue), + ExifTagValue.MaxApertureValue => new ExifRational(ExifTag.MaxApertureValue), + ExifTagValue.SubjectDistance => new ExifRational(ExifTag.SubjectDistance), + ExifTagValue.FocalLength => new ExifRational(ExifTag.FocalLength), + ExifTagValue.FlashEnergy2 => new ExifRational(ExifTag.FlashEnergy2), + ExifTagValue.FocalPlaneXResolution2 => new ExifRational(ExifTag.FocalPlaneXResolution2), + ExifTagValue.FocalPlaneYResolution2 => new ExifRational(ExifTag.FocalPlaneYResolution2), + ExifTagValue.ExposureIndex2 => new ExifRational(ExifTag.ExposureIndex2), + ExifTagValue.Humidity => new ExifRational(ExifTag.Humidity), + ExifTagValue.Pressure => new ExifRational(ExifTag.Pressure), + ExifTagValue.Acceleration => new ExifRational(ExifTag.Acceleration), + ExifTagValue.FlashEnergy => new ExifRational(ExifTag.FlashEnergy), + ExifTagValue.FocalPlaneXResolution => new ExifRational(ExifTag.FocalPlaneXResolution), + ExifTagValue.FocalPlaneYResolution => new ExifRational(ExifTag.FocalPlaneYResolution), + ExifTagValue.ExposureIndex => new ExifRational(ExifTag.ExposureIndex), + ExifTagValue.DigitalZoomRatio => new ExifRational(ExifTag.DigitalZoomRatio), + ExifTagValue.GPSAltitude => new ExifRational(ExifTag.GPSAltitude), + ExifTagValue.GPSDOP => new ExifRational(ExifTag.GPSDOP), + ExifTagValue.GPSSpeed => new ExifRational(ExifTag.GPSSpeed), + ExifTagValue.GPSTrack => new ExifRational(ExifTag.GPSTrack), + ExifTagValue.GPSImgDirection => new ExifRational(ExifTag.GPSImgDirection), + ExifTagValue.GPSDestBearing => new ExifRational(ExifTag.GPSDestBearing), + ExifTagValue.GPSDestDistance => new ExifRational(ExifTag.GPSDestDistance), + ExifTagValue.GPSHPositioningError => new ExifRational(ExifTag.GPSHPositioningError), + ExifTagValue.WhitePoint => new ExifRationalArray(ExifTag.WhitePoint), + ExifTagValue.PrimaryChromaticities => new ExifRationalArray(ExifTag.PrimaryChromaticities), + ExifTagValue.YCbCrCoefficients => new ExifRationalArray(ExifTag.YCbCrCoefficients), + ExifTagValue.ReferenceBlackWhite => new ExifRationalArray(ExifTag.ReferenceBlackWhite), + ExifTagValue.GPSLatitude => new ExifRationalArray(ExifTag.GPSLatitude), + ExifTagValue.GPSLongitude => new ExifRationalArray(ExifTag.GPSLongitude), + ExifTagValue.GPSTimestamp => new ExifRationalArray(ExifTag.GPSTimestamp), + ExifTagValue.GPSDestLatitude => new ExifRationalArray(ExifTag.GPSDestLatitude), + ExifTagValue.GPSDestLongitude => new ExifRationalArray(ExifTag.GPSDestLongitude), + ExifTagValue.LensSpecification => new ExifRationalArray(ExifTag.LensSpecification), + ExifTagValue.OldSubfileType => new ExifShort(ExifTag.OldSubfileType), + ExifTagValue.Compression => new ExifShort(ExifTag.Compression), + ExifTagValue.PhotometricInterpretation => new ExifShort(ExifTag.PhotometricInterpretation), + ExifTagValue.Thresholding => new ExifShort(ExifTag.Thresholding), + ExifTagValue.CellWidth => new ExifShort(ExifTag.CellWidth), + ExifTagValue.CellLength => new ExifShort(ExifTag.CellLength), + ExifTagValue.FillOrder => new ExifShort(ExifTag.FillOrder), + ExifTagValue.Orientation => new ExifShort(ExifTag.Orientation), + ExifTagValue.SamplesPerPixel => new ExifShort(ExifTag.SamplesPerPixel), + ExifTagValue.PlanarConfiguration => new ExifShort(ExifTag.PlanarConfiguration), + ExifTagValue.Predictor => new ExifShort(ExifTag.Predictor), + ExifTagValue.GrayResponseUnit => new ExifShort(ExifTag.GrayResponseUnit), + ExifTagValue.ResolutionUnit => new ExifShort(ExifTag.ResolutionUnit), + ExifTagValue.CleanFaxData => new ExifShort(ExifTag.CleanFaxData), + ExifTagValue.InkSet => new ExifShort(ExifTag.InkSet), + ExifTagValue.NumberOfInks => new ExifShort(ExifTag.NumberOfInks), + ExifTagValue.DotRange => new ExifShort(ExifTag.DotRange), + ExifTagValue.Indexed => new ExifShort(ExifTag.Indexed), + ExifTagValue.OPIProxy => new ExifShort(ExifTag.OPIProxy), + ExifTagValue.JPEGProc => new ExifShort(ExifTag.JPEGProc), + ExifTagValue.JPEGRestartInterval => new ExifShort(ExifTag.JPEGRestartInterval), + ExifTagValue.YCbCrPositioning => new ExifShort(ExifTag.YCbCrPositioning), + ExifTagValue.Rating => new ExifShort(ExifTag.Rating), + ExifTagValue.RatingPercent => new ExifShort(ExifTag.RatingPercent), + ExifTagValue.ExposureProgram => new ExifShort(ExifTag.ExposureProgram), + ExifTagValue.Interlace => new ExifShort(ExifTag.Interlace), + ExifTagValue.SelfTimerMode => new ExifShort(ExifTag.SelfTimerMode), + ExifTagValue.SensitivityType => new ExifShort(ExifTag.SensitivityType), + ExifTagValue.MeteringMode => new ExifShort(ExifTag.MeteringMode), + ExifTagValue.LightSource => new ExifShort(ExifTag.LightSource), + ExifTagValue.FocalPlaneResolutionUnit2 => new ExifShort(ExifTag.FocalPlaneResolutionUnit2), + ExifTagValue.SensingMethod2 => new ExifShort(ExifTag.SensingMethod2), + ExifTagValue.Flash => new ExifShort(ExifTag.Flash), + ExifTagValue.ColorSpace => new ExifShort(ExifTag.ColorSpace), + ExifTagValue.FocalPlaneResolutionUnit => new ExifShort(ExifTag.FocalPlaneResolutionUnit), + ExifTagValue.SensingMethod => new ExifShort(ExifTag.SensingMethod), + ExifTagValue.CustomRendered => new ExifShort(ExifTag.CustomRendered), + ExifTagValue.ExposureMode => new ExifShort(ExifTag.ExposureMode), + ExifTagValue.WhiteBalance => new ExifShort(ExifTag.WhiteBalance), + ExifTagValue.FocalLengthIn35mmFilm => new ExifShort(ExifTag.FocalLengthIn35mmFilm), + ExifTagValue.SceneCaptureType => new ExifShort(ExifTag.SceneCaptureType), + ExifTagValue.GainControl => new ExifShort(ExifTag.GainControl), + ExifTagValue.Contrast => new ExifShort(ExifTag.Contrast), + ExifTagValue.Saturation => new ExifShort(ExifTag.Saturation), + ExifTagValue.Sharpness => new ExifShort(ExifTag.Sharpness), + ExifTagValue.SubjectDistanceRange => new ExifShort(ExifTag.SubjectDistanceRange), + ExifTagValue.GPSDifferential => new ExifShort(ExifTag.GPSDifferential), + ExifTagValue.BitsPerSample => new ExifShortArray(ExifTag.BitsPerSample), + ExifTagValue.MinSampleValue => new ExifShortArray(ExifTag.MinSampleValue), + ExifTagValue.MaxSampleValue => new ExifShortArray(ExifTag.MaxSampleValue), + ExifTagValue.GrayResponseCurve => new ExifShortArray(ExifTag.GrayResponseCurve), + ExifTagValue.ColorMap => new ExifShortArray(ExifTag.ColorMap), + ExifTagValue.ExtraSamples => new ExifShortArray(ExifTag.ExtraSamples), + ExifTagValue.PageNumber => new ExifShortArray(ExifTag.PageNumber), + ExifTagValue.TransferFunction => new ExifShortArray(ExifTag.TransferFunction), + ExifTagValue.HalftoneHints => new ExifShortArray(ExifTag.HalftoneHints), + ExifTagValue.SampleFormat => new ExifShortArray(ExifTag.SampleFormat), + ExifTagValue.TransferRange => new ExifShortArray(ExifTag.TransferRange), + ExifTagValue.DefaultImageColor => new ExifShortArray(ExifTag.DefaultImageColor), + ExifTagValue.JPEGLosslessPredictors => new ExifShortArray(ExifTag.JPEGLosslessPredictors), + ExifTagValue.JPEGPointTransforms => new ExifShortArray(ExifTag.JPEGPointTransforms), + ExifTagValue.YCbCrSubsampling => new ExifShortArray(ExifTag.YCbCrSubsampling), + ExifTagValue.CFARepeatPatternDim => new ExifShortArray(ExifTag.CFARepeatPatternDim), + ExifTagValue.IntergraphPacketData => new ExifShortArray(ExifTag.IntergraphPacketData), + ExifTagValue.ISOSpeedRatings => new ExifShortArray(ExifTag.ISOSpeedRatings), + ExifTagValue.SubjectArea => new ExifShortArray(ExifTag.SubjectArea), + ExifTagValue.SubjectLocation => new ExifShortArray(ExifTag.SubjectLocation), + ExifTagValue.ShutterSpeedValue => new ExifSignedRational(ExifTag.ShutterSpeedValue), + ExifTagValue.BrightnessValue => new ExifSignedRational(ExifTag.BrightnessValue), + ExifTagValue.ExposureBiasValue => new ExifSignedRational(ExifTag.ExposureBiasValue), + ExifTagValue.AmbientTemperature => new ExifSignedRational(ExifTag.AmbientTemperature), + ExifTagValue.WaterDepth => new ExifSignedRational(ExifTag.WaterDepth), + ExifTagValue.CameraElevationAngle => new ExifSignedRational(ExifTag.CameraElevationAngle), + ExifTagValue.Decode => new ExifSignedRationalArray(ExifTag.Decode), + ExifTagValue.TimeZoneOffset => new ExifSignedShortArray(ExifTag.TimeZoneOffset), + ExifTagValue.ImageDescription => new ExifString(ExifTag.ImageDescription), + ExifTagValue.Make => new ExifString(ExifTag.Make), + ExifTagValue.Model => new ExifString(ExifTag.Model), + ExifTagValue.Software => new ExifString(ExifTag.Software), + ExifTagValue.DateTime => new ExifString(ExifTag.DateTime), + ExifTagValue.Artist => new ExifString(ExifTag.Artist), + ExifTagValue.HostComputer => new ExifString(ExifTag.HostComputer), + ExifTagValue.Copyright => new ExifString(ExifTag.Copyright), + ExifTagValue.DocumentName => new ExifString(ExifTag.DocumentName), + ExifTagValue.PageName => new ExifString(ExifTag.PageName), + ExifTagValue.InkNames => new ExifString(ExifTag.InkNames), + ExifTagValue.TargetPrinter => new ExifString(ExifTag.TargetPrinter), + ExifTagValue.ImageID => new ExifString(ExifTag.ImageID), + ExifTagValue.MDLabName => new ExifString(ExifTag.MDLabName), + ExifTagValue.MDSampleInfo => new ExifString(ExifTag.MDSampleInfo), + ExifTagValue.MDPrepDate => new ExifString(ExifTag.MDPrepDate), + ExifTagValue.MDPrepTime => new ExifString(ExifTag.MDPrepTime), + ExifTagValue.MDFileUnits => new ExifString(ExifTag.MDFileUnits), + ExifTagValue.SEMInfo => new ExifString(ExifTag.SEMInfo), + ExifTagValue.SpectralSensitivity => new ExifString(ExifTag.SpectralSensitivity), + ExifTagValue.DateTimeOriginal => new ExifString(ExifTag.DateTimeOriginal), + ExifTagValue.DateTimeDigitized => new ExifString(ExifTag.DateTimeDigitized), + ExifTagValue.SubsecTime => new ExifString(ExifTag.SubsecTime), + ExifTagValue.SubsecTimeOriginal => new ExifString(ExifTag.SubsecTimeOriginal), + ExifTagValue.SubsecTimeDigitized => new ExifString(ExifTag.SubsecTimeDigitized), + ExifTagValue.RelatedSoundFile => new ExifString(ExifTag.RelatedSoundFile), + ExifTagValue.FaxSubaddress => new ExifString(ExifTag.FaxSubaddress), + ExifTagValue.OffsetTime => new ExifString(ExifTag.OffsetTime), + ExifTagValue.OffsetTimeOriginal => new ExifString(ExifTag.OffsetTimeOriginal), + ExifTagValue.OffsetTimeDigitized => new ExifString(ExifTag.OffsetTimeDigitized), + ExifTagValue.SecurityClassification => new ExifString(ExifTag.SecurityClassification), + ExifTagValue.ImageHistory => new ExifString(ExifTag.ImageHistory), + ExifTagValue.ImageUniqueID => new ExifString(ExifTag.ImageUniqueID), + ExifTagValue.OwnerName => new ExifString(ExifTag.OwnerName), + ExifTagValue.SerialNumber => new ExifString(ExifTag.SerialNumber), + ExifTagValue.LensMake => new ExifString(ExifTag.LensMake), + ExifTagValue.LensModel => new ExifString(ExifTag.LensModel), + ExifTagValue.LensSerialNumber => new ExifString(ExifTag.LensSerialNumber), + ExifTagValue.GDALMetadata => new ExifString(ExifTag.GDALMetadata), + ExifTagValue.GDALNoData => new ExifString(ExifTag.GDALNoData), + ExifTagValue.GPSLatitudeRef => new ExifString(ExifTag.GPSLatitudeRef), + ExifTagValue.GPSLongitudeRef => new ExifString(ExifTag.GPSLongitudeRef), + ExifTagValue.GPSSatellites => new ExifString(ExifTag.GPSSatellites), + ExifTagValue.GPSStatus => new ExifString(ExifTag.GPSStatus), + ExifTagValue.GPSMeasureMode => new ExifString(ExifTag.GPSMeasureMode), + ExifTagValue.GPSSpeedRef => new ExifString(ExifTag.GPSSpeedRef), + ExifTagValue.GPSTrackRef => new ExifString(ExifTag.GPSTrackRef), + ExifTagValue.GPSImgDirectionRef => new ExifString(ExifTag.GPSImgDirectionRef), + ExifTagValue.GPSMapDatum => new ExifString(ExifTag.GPSMapDatum), + ExifTagValue.GPSDestLatitudeRef => new ExifString(ExifTag.GPSDestLatitudeRef), + ExifTagValue.GPSDestLongitudeRef => new ExifString(ExifTag.GPSDestLongitudeRef), + ExifTagValue.GPSDestBearingRef => new ExifString(ExifTag.GPSDestBearingRef), + ExifTagValue.GPSDestDistanceRef => new ExifString(ExifTag.GPSDestDistanceRef), + ExifTagValue.GPSDateStamp => new ExifString(ExifTag.GPSDateStamp), + ExifTagValue.FileSource => new ExifByte(ExifTag.FileSource, ExifDataType.Undefined), + ExifTagValue.SceneType => new ExifByte(ExifTag.SceneType, ExifDataType.Undefined), + ExifTagValue.JPEGTables => new ExifByteArray(ExifTag.JPEGTables, ExifDataType.Undefined), + ExifTagValue.OECF => new ExifByteArray(ExifTag.OECF, ExifDataType.Undefined), + ExifTagValue.ExifVersion => new ExifByteArray(ExifTag.ExifVersion, ExifDataType.Undefined), + ExifTagValue.ComponentsConfiguration => new ExifByteArray(ExifTag.ComponentsConfiguration, ExifDataType.Undefined), + ExifTagValue.MakerNote => new ExifByteArray(ExifTag.MakerNote, ExifDataType.Undefined), + ExifTagValue.FlashpixVersion => new ExifByteArray(ExifTag.FlashpixVersion, ExifDataType.Undefined), + ExifTagValue.SpatialFrequencyResponse => new ExifByteArray(ExifTag.SpatialFrequencyResponse, ExifDataType.Undefined), + ExifTagValue.SpatialFrequencyResponse2 => new ExifByteArray(ExifTag.SpatialFrequencyResponse2, ExifDataType.Undefined), + ExifTagValue.Noise => new ExifByteArray(ExifTag.Noise, ExifDataType.Undefined), + ExifTagValue.CFAPattern => new ExifByteArray(ExifTag.CFAPattern, ExifDataType.Undefined), + ExifTagValue.DeviceSettingDescription => new ExifByteArray(ExifTag.DeviceSettingDescription, ExifDataType.Undefined), + ExifTagValue.ImageSourceData => new ExifByteArray(ExifTag.ImageSourceData, ExifDataType.Undefined), + ExifTagValue.XPTitle => new ExifUcs2String(ExifTag.XPTitle), + ExifTagValue.XPComment => new ExifUcs2String(ExifTag.XPComment), + ExifTagValue.XPAuthor => new ExifUcs2String(ExifTag.XPAuthor), + ExifTagValue.XPKeywords => new ExifUcs2String(ExifTag.XPKeywords), + ExifTagValue.XPSubject => new ExifUcs2String(ExifTag.XPSubject), + ExifTagValue.UserComment => new ExifEncodedString(ExifTag.UserComment), + ExifTagValue.GPSProcessingMethod => new ExifEncodedString(ExifTag.GPSProcessingMethod), + ExifTagValue.GPSAreaInformation => new ExifEncodedString(ExifTag.GPSAreaInformation), + _ => null, + }; + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/ExifValue{TValueType}.cs b/ImageSharp/Metadata/Profiles/Exif/Values/ExifValue{TValueType}.cs new file mode 100644 index 0000000..f90ed23 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/ExifValue{TValueType}.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal abstract class ExifValue : ExifValue, IExifValue + { + protected ExifValue(ExifTag tag) + : base(tag) + { + } + + protected ExifValue(ExifTagValue tag) + : base(tag) + { + } + + internal ExifValue(ExifValue value) + : base(value) + { + } + + public TValueType? Value { get; set; } + + /// + /// Gets the value of the current instance as a string. + /// + protected abstract string? StringValue { get; } + + public override object? GetValue() => this.Value; + + public override bool TrySetValue(object? value) + { + if (value is null) + { + this.Value = default; + return true; + } + + // We use type comparison here over "is" to avoid compiler optimizations + // that equate short with ushort, and sbyte with byte. + if (value.GetType() == typeof(TValueType)) + { + this.Value = (TValueType)value; + return true; + } + + return false; + } + + public override string? ToString() => ExifTagDescriptionAttribute.TryGetDescription(this.Tag, this.Value, out string? description) ? description : this.StringValue; + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/IExifValue.cs b/ImageSharp/Metadata/Profiles/Exif/Values/IExifValue.cs new file mode 100644 index 0000000..297f707 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/IExifValue.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + /// A value of the Exif profile. + /// + public interface IExifValue : IDeepCloneable + { + /// + /// Gets the data type of the Exif value. + /// + public ExifDataType DataType { get; } + + /// + /// Gets a value indicating whether the value is an array. + /// + public bool IsArray { get; } + + /// + /// Gets the tag of the Exif value. + /// + public ExifTag Tag { get; } + + /// + /// Gets the value of this Exif value. + /// + /// The value of this Exif value. + public object? GetValue(); + + /// + /// Sets the value of this Exif value. + /// + /// The value of this Exif value. + /// A value indicating whether the value could be set. + public bool TrySetValue(object? value); + } +} diff --git a/ImageSharp/Metadata/Profiles/Exif/Values/IExifValue{TValueType}.cs b/ImageSharp/Metadata/Profiles/Exif/Values/IExifValue{TValueType}.cs new file mode 100644 index 0000000..ce87cb1 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/Exif/Values/IExifValue{TValueType}.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + /// + /// A value of the exif profile. + /// + /// The type of the value. + public interface IExifValue : IExifValue + { + /// + /// Gets or sets the value. + /// + TValueType? Value { get; set; } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Curves/IccCurveSegment.cs b/ImageSharp/Metadata/Profiles/ICC/Curves/IccCurveSegment.cs new file mode 100644 index 0000000..622a359 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Curves/IccCurveSegment.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// A segment of a curve + /// + internal abstract class IccCurveSegment : IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The signature of this segment + protected IccCurveSegment(IccCurveSegmentSignature signature) + => this.Signature = signature; + + /// + /// Gets the signature of this segment + /// + public IccCurveSegmentSignature Signature { get; } + + /// + public virtual bool Equals(IccCurveSegment? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return this.Signature == other.Signature; + } + + /// + public override bool Equals(object? obj) => this.Equals(obj as IccCurveSegment); + + /// + public override int GetHashCode() => this.Signature.GetHashCode(); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Curves/IccFormulaCurveElement.cs b/ImageSharp/Metadata/Profiles/ICC/Curves/IccFormulaCurveElement.cs new file mode 100644 index 0000000..515010d --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Curves/IccFormulaCurveElement.cs @@ -0,0 +1,103 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// A formula based curve segment + /// + internal sealed class IccFormulaCurveElement : IccCurveSegment, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The type of this segment + /// Gamma segment parameter + /// A segment parameter + /// B segment parameter + /// C segment parameter + /// D segment parameter + /// E segment parameter + public IccFormulaCurveElement(IccFormulaCurveType type, float gamma, float a, float b, float c, float d, float e) + : base(IccCurveSegmentSignature.FormulaCurve) + { + this.Type = type; + this.Gamma = gamma; + this.A = a; + this.B = b; + this.C = c; + this.D = d; + this.E = e; + } + + /// + /// Gets the type of this curve + /// + public IccFormulaCurveType Type { get; } + + /// + /// Gets the gamma curve parameter + /// + public float Gamma { get; } + + /// + /// Gets the A curve parameter + /// + public float A { get; } + + /// + /// Gets the B curve parameter + /// + public float B { get; } + + /// + /// Gets the C curve parameter + /// + public float C { get; } + + /// + /// Gets the D curve parameter + /// + public float D { get; } + + /// + /// Gets the E curve parameter + /// + public float E { get; } + + /// + public override bool Equals(IccCurveSegment? other) + { + if (base.Equals(other) && other is IccFormulaCurveElement segment) + { + return this.Type == segment.Type + && this.Gamma == segment.Gamma + && this.A == segment.A + && this.B == segment.B + && this.C == segment.C + && this.D == segment.D + && this.E == segment.E; + } + + return false; + } + + /// + public bool Equals(IccFormulaCurveElement? other) => this.Equals((IccCurveSegment?)other); + + /// + public override bool Equals(object? obj) => this.Equals(obj as IccFormulaCurveElement); + + /// + public override int GetHashCode() + => HashCode.Combine( + this.Type, + this.Gamma, + this.A, + this.B, + this.C, + this.D, + this.E); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Curves/IccOneDimensionalCurve.cs b/ImageSharp/Metadata/Profiles/ICC/Curves/IccOneDimensionalCurve.cs new file mode 100644 index 0000000..8ae897f --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Curves/IccOneDimensionalCurve.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// A one dimensional ICC curve. + /// + internal sealed class IccOneDimensionalCurve : IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The break points of this curve + /// The segments of this curve + public IccOneDimensionalCurve(float[] breakPoints, IccCurveSegment[] segments) + { + Guard.NotNull(breakPoints, nameof(breakPoints)); + Guard.NotNull(segments, nameof(segments)); + + bool isSizeCorrect = breakPoints.Length == segments.Length - 1; + Guard.IsTrue(isSizeCorrect, $"{nameof(breakPoints)},{nameof(segments)}", "Number of BreakPoints must be one less than number of Segments"); + + this.BreakPoints = breakPoints; + this.Segments = segments; + } + + /// + /// Gets the breakpoints that separate two curve segments + /// + public float[] BreakPoints { get; } + + /// + /// Gets an array of curve segments + /// + public IccCurveSegment[] Segments { get; } + + /// + public bool Equals(IccOneDimensionalCurve? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return this.BreakPoints.AsSpan().SequenceEqual(other.BreakPoints) + && this.Segments.AsSpan().SequenceEqual(other.Segments); + } + + /// + public override bool Equals(object? obj) + => this.Equals(obj as IccOneDimensionalCurve); + + /// + public override int GetHashCode() + => HashCode.Combine(this.BreakPoints, this.Segments); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Curves/IccParametricCurve.cs b/ImageSharp/Metadata/Profiles/ICC/Curves/IccParametricCurve.cs new file mode 100644 index 0000000..bfe3f2b --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Curves/IccParametricCurve.cs @@ -0,0 +1,167 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// A parametric curve + /// + internal sealed class IccParametricCurve : IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// G curve parameter + public IccParametricCurve(float g) + : this(IccParametricCurveType.Type1, g, 0, 0, 0, 0, 0, 0) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// G curve parameter + /// A curve parameter + /// B curve parameter + public IccParametricCurve(float g, float a, float b) + : this(IccParametricCurveType.Cie122_1996, g, a, b, 0, 0, 0, 0) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// G curve parameter + /// A curve parameter + /// B curve parameter + /// C curve parameter + public IccParametricCurve(float g, float a, float b, float c) + : this(IccParametricCurveType.Iec61966_3, g, a, b, c, 0, 0, 0) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// G curve parameter + /// A curve parameter + /// B curve parameter + /// C curve parameter + /// D curve parameter + public IccParametricCurve(float g, float a, float b, float c, float d) + : this(IccParametricCurveType.SRgb, g, a, b, c, d, 0, 0) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// G curve parameter + /// A curve parameter + /// B curve parameter + /// C curve parameter + /// D curve parameter + /// E curve parameter + /// F curve parameter + public IccParametricCurve(float g, float a, float b, float c, float d, float e, float f) + : this(IccParametricCurveType.Type5, g, a, b, c, d, e, f) + { + } + + private IccParametricCurve(IccParametricCurveType type, float g, float a, float b, float c, float d, float e, float f) + { + this.Type = type; + this.G = g; + this.A = a; + this.B = b; + this.C = c; + this.D = d; + this.E = e; + this.F = f; + } + + /// + /// Gets the type of this curve + /// + public IccParametricCurveType Type { get; } + + /// + /// Gets the G curve parameter + /// + public float G { get; } + + /// + /// Gets the A curve parameter + /// + public float A { get; } + + /// + /// Gets the B curve parameter + /// + public float B { get; } + + /// + /// Gets the C curve parameter + /// + public float C { get; } + + /// + /// Gets the D curve parameter + /// + public float D { get; } + + /// + /// Gets the E curve parameter + /// + public float E { get; } + + /// + /// Gets the F curve parameter + /// + public float F { get; } + + /// + public bool Equals(IccParametricCurve? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return this.Type == other.Type + && this.G.Equals(other.G) + && this.A.Equals(other.A) + && this.B.Equals(other.B) + && this.C.Equals(other.C) + && this.D.Equals(other.D) + && this.E.Equals(other.E) + && this.F.Equals(other.F); + } + + /// + public override bool Equals(object? obj) + { + return obj is IccParametricCurve other && this.Equals(other); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine( + this.Type, + this.G.GetHashCode(), + this.A.GetHashCode(), + this.B.GetHashCode(), + this.C.GetHashCode(), + this.D.GetHashCode(), + this.E.GetHashCode(), + this.F.GetHashCode()); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Curves/IccResponseCurve.cs b/ImageSharp/Metadata/Profiles/ICC/Curves/IccResponseCurve.cs new file mode 100644 index 0000000..c6b0163 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Curves/IccResponseCurve.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// A response curve + /// + internal sealed class IccResponseCurve : IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The type of this curve + /// The XYZ values + /// The response arrays + public IccResponseCurve(IccCurveMeasurementEncodings curveType, Vector3[] xyzValues, IccResponseNumber[][] responseArrays) + { + Guard.NotNull(xyzValues, nameof(xyzValues)); + Guard.NotNull(responseArrays, nameof(responseArrays)); + + Guard.IsTrue(xyzValues.Length == responseArrays.Length, $"{nameof(xyzValues)},{nameof(responseArrays)}", "Arrays must have same length"); + Guard.MustBeBetweenOrEqualTo(xyzValues.Length, 1, 15, nameof(xyzValues)); + + this.CurveType = curveType; + this.XyzValues = xyzValues; + this.ResponseArrays = responseArrays; + } + + /// + /// Gets the type of this curve + /// + public IccCurveMeasurementEncodings CurveType { get; } + + /// + /// Gets the XYZ values + /// + public Vector3[] XyzValues { get; } + + /// + /// Gets the response arrays + /// + public IccResponseNumber[][] ResponseArrays { get; } + + /// + public bool Equals(IccResponseCurve? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return this.CurveType == other.CurveType + && this.XyzValues.AsSpan().SequenceEqual(other.XyzValues) + && this.EqualsResponseArray(other); + } + + /// + public override bool Equals(object? obj) => obj is IccResponseCurve other && this.Equals(other); + + /// + public override int GetHashCode() + { + return HashCode.Combine( + this.CurveType, + this.XyzValues, + this.ResponseArrays); + } + + private bool EqualsResponseArray(IccResponseCurve other) + { + if (this.ResponseArrays.Length != other.ResponseArrays.Length) + { + return false; + } + + for (int i = 0; i < this.ResponseArrays.Length; i++) + { + if (!this.ResponseArrays[i].AsSpan().SequenceEqual(other.ResponseArrays[i])) + { + return false; + } + } + + return true; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Curves/IccSampledCurveElement.cs b/ImageSharp/Metadata/Profiles/ICC/Curves/IccSampledCurveElement.cs new file mode 100644 index 0000000..019f91d --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Curves/IccSampledCurveElement.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// A sampled curve segment + /// + internal sealed class IccSampledCurveElement : IccCurveSegment, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The curve values of this segment + public IccSampledCurveElement(float[] curveEntries) + : base(IccCurveSegmentSignature.SampledCurve) + { + Guard.NotNull(curveEntries, nameof(curveEntries)); + Guard.IsTrue(curveEntries.Length > 0, nameof(curveEntries), "There must be at least one value"); + + this.CurveEntries = curveEntries; + } + + /// + /// Gets the curve values of this segment + /// + public float[] CurveEntries { get; } + + /// + public override bool Equals(IccCurveSegment? other) + { + if (base.Equals(other) && other is IccSampledCurveElement segment) + { + return this.CurveEntries.AsSpan().SequenceEqual(segment.CurveEntries); + } + + return false; + } + + /// + public bool Equals(IccSampledCurveElement? other) + => this.Equals((IccCurveSegment?)other); + + /// + public override bool Equals(object? obj) + => this.Equals(obj as IccSampledCurveElement); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), this.CurveEntries); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Curves.cs b/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Curves.cs new file mode 100644 index 0000000..87438d7 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Curves.cs @@ -0,0 +1,218 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Provides methods to read ICC data types + /// + internal sealed partial class IccDataReader + { + /// + /// Reads a + /// + /// The read curve + public IccOneDimensionalCurve ReadOneDimensionalCurve() + { + ushort segmentCount = this.ReadUInt16(); + this.AddIndex(2); // 2 bytes reserved + float[] breakPoints = new float[segmentCount - 1]; + for (int i = 0; i < breakPoints.Length; i++) + { + breakPoints[i] = this.ReadSingle(); + } + + IccCurveSegment[] segments = new IccCurveSegment[segmentCount]; + for (int i = 0; i < segmentCount; i++) + { + segments[i] = this.ReadCurveSegment(); + } + + return new IccOneDimensionalCurve(breakPoints, segments); + } + + /// + /// Reads a + /// + /// The number of channels + /// The read curve + public IccResponseCurve ReadResponseCurve(int channelCount) + { + IccCurveMeasurementEncodings type = (IccCurveMeasurementEncodings)this.ReadUInt32(); + uint[] measurement = new uint[channelCount]; + for (int i = 0; i < channelCount; i++) + { + measurement[i] = this.ReadUInt32(); + } + + Vector3[] xyzValues = new Vector3[channelCount]; + for (int i = 0; i < channelCount; i++) + { + xyzValues[i] = this.ReadXyzNumber(); + } + + IccResponseNumber[][] response = new IccResponseNumber[channelCount][]; + for (int i = 0; i < channelCount; i++) + { + response[i] = new IccResponseNumber[measurement[i]]; + for (uint j = 0; j < measurement[i]; j++) + { + response[i][j] = this.ReadResponseNumber(); + } + } + + return new IccResponseCurve(type, xyzValues, response); + } + + /// + /// Reads a + /// + /// The read curve + public IccParametricCurve ReadParametricCurve() + { + ushort type = this.ReadUInt16(); + this.AddIndex(2); // 2 bytes reserved + float gamma, a, b, c, d, e, f; + gamma = a = b = c = d = e = f = 0; + + if (type <= 4) + { + gamma = this.ReadFix16(); + } + + if (type > 0 && type <= 4) + { + a = this.ReadFix16(); + b = this.ReadFix16(); + } + + if (type > 1 && type <= 4) + { + c = this.ReadFix16(); + } + + if (type > 2 && type <= 4) + { + d = this.ReadFix16(); + } + + if (type == 4) + { + e = this.ReadFix16(); + f = this.ReadFix16(); + } + + switch (type) + { + case 0: return new IccParametricCurve(gamma); + case 1: return new IccParametricCurve(gamma, a, b); + case 2: return new IccParametricCurve(gamma, a, b, c); + case 3: return new IccParametricCurve(gamma, a, b, c, d); + case 4: return new IccParametricCurve(gamma, a, b, c, d, e, f); + default: throw new InvalidIccProfileException($"Invalid parametric curve type of {type}"); + } + } + + /// + /// Reads a + /// + /// The read segment + public IccCurveSegment ReadCurveSegment() + { + IccCurveSegmentSignature signature = (IccCurveSegmentSignature)this.ReadUInt32(); + this.AddIndex(4); // 4 bytes reserved + + switch (signature) + { + case IccCurveSegmentSignature.FormulaCurve: + return this.ReadFormulaCurveElement(); + case IccCurveSegmentSignature.SampledCurve: + return this.ReadSampledCurveElement(); + default: + throw new InvalidIccProfileException($"Invalid curve segment type of {signature}"); + } + } + + /// + /// Reads a + /// + /// The read segment + public IccFormulaCurveElement ReadFormulaCurveElement() + { + IccFormulaCurveType type = (IccFormulaCurveType)this.ReadUInt16(); + this.AddIndex(2); // 2 bytes reserved + float gamma, a, b, c, d, e; + gamma = d = e = 0; + + if (type == IccFormulaCurveType.Type1 || type == IccFormulaCurveType.Type2) + { + gamma = this.ReadSingle(); + } + + a = this.ReadSingle(); + b = this.ReadSingle(); + c = this.ReadSingle(); + + if (type == IccFormulaCurveType.Type2 || type == IccFormulaCurveType.Type3) + { + d = this.ReadSingle(); + } + + if (type == IccFormulaCurveType.Type3) + { + e = this.ReadSingle(); + } + + return new IccFormulaCurveElement(type, gamma, a, b, c, d, e); + } + + /// + /// Reads a + /// + /// The read segment + public IccSampledCurveElement ReadSampledCurveElement() + { + uint count = this.ReadUInt32(); + float[] entries = new float[count]; + for (int i = 0; i < count; i++) + { + entries[i] = this.ReadSingle(); + } + + return new IccSampledCurveElement(entries); + } + + /// + /// Reads curve data + /// + /// Number of input channels + /// The curve data + private IccTagDataEntry[] ReadCurves(int count) + { + IccTagDataEntry[] tdata = new IccTagDataEntry[count]; + for (int i = 0; i < count; i++) + { + IccTypeSignature type = this.ReadTagDataEntryHeader(); + if (type != IccTypeSignature.Curve && type != IccTypeSignature.ParametricCurve) + { + throw new InvalidIccProfileException($"Curve has to be either \"{nameof(IccTypeSignature)}.{nameof(IccTypeSignature.Curve)}\" or" + + $" \"{nameof(IccTypeSignature)}.{nameof(IccTypeSignature.ParametricCurve)}\" for LutAToB- and LutBToA-TagDataEntries"); + } + + if (type == IccTypeSignature.Curve) + { + tdata[i] = this.ReadCurveTagDataEntry(); + } + else if (type == IccTypeSignature.ParametricCurve) + { + tdata[i] = this.ReadParametricCurveTagDataEntry(); + } + + this.AddPadding(); + } + + return tdata; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs b/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs new file mode 100644 index 0000000..bda9024 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs @@ -0,0 +1,165 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Provides methods to read ICC data types. + /// + internal sealed partial class IccDataReader + { + /// + /// Reads an 8bit lookup table. + /// + /// The read LUT. + public IccLut ReadLut8() => new(this.ReadBytes(256)); + + /// + /// Reads a 16bit lookup table. + /// + /// The number of entries. + /// The read LUT. + public IccLut ReadLut16(int count) + { + ushort[] values = new ushort[count]; + for (int i = 0; i < count; i++) + { + values[i] = this.ReadUInt16(); + } + + return new IccLut(values); + } + + /// + /// Reads a CLUT depending on type. + /// + /// Input channel count. + /// Output channel count. + /// If true, it's read as CLUTf32, + /// else read as either CLUT8 or CLUT16 depending on embedded information. + /// The read CLUT. + public IccClut ReadClut(int inChannelCount, int outChannelCount, bool isFloat) + { + // Grid-points are always 16 bytes long but only 0-inChCount are used. + byte[] gridPointCount = new byte[inChannelCount]; + Buffer.BlockCopy(this.data, this.AddIndex(16), gridPointCount, 0, inChannelCount); + + if (!isFloat) + { + byte size = this.data[this.AddIndex(4)]; // First byte is info, last 3 bytes are reserved + if (size == 1) + { + return this.ReadClut8(inChannelCount, outChannelCount, gridPointCount); + } + + if (size == 2) + { + return this.ReadClut16(inChannelCount, outChannelCount, gridPointCount); + } + + throw new InvalidIccProfileException($"Invalid CLUT size of {size}"); + } + + return this.ReadClutF32(inChannelCount, outChannelCount, gridPointCount); + } + + /// + /// Reads an 8 bit CLUT. + /// + /// Input channel count. + /// Output channel count. + /// Grid point count for each CLUT channel. + /// The read CLUT8. + public IccClut ReadClut8(int inChannelCount, int outChannelCount, byte[] gridPointCount) + { + int length = 0; + for (int i = 0; i < inChannelCount; i++) + { + length += (int)Math.Pow(gridPointCount[i], inChannelCount); + } + + length /= inChannelCount; + + const float Max = byte.MaxValue; + + float[] values = new float[length * outChannelCount]; + int offset = 0; + for (int i = 0; i < length; i++) + { + for (int j = 0; j < outChannelCount; j++) + { + values[offset++] = this.data[this.currentIndex++] / Max; + } + } + + return new IccClut(values, gridPointCount, IccClutDataType.UInt8, outChannelCount); + } + + /// + /// Reads a 16 bit CLUT. + /// + /// Input channel count. + /// Output channel count. + /// Grid point count for each CLUT channel. + /// The read CLUT16. + public IccClut ReadClut16(int inChannelCount, int outChannelCount, byte[] gridPointCount) + { + int start = this.currentIndex; + int length = 0; + for (int i = 0; i < inChannelCount; i++) + { + length += (int)Math.Pow(gridPointCount[i], inChannelCount); + } + + length /= inChannelCount; + + const float Max = ushort.MaxValue; + + float[] values = new float[length * outChannelCount]; + int offset = 0; + for (int i = 0; i < length; i++) + { + for (int j = 0; j < outChannelCount; j++) + { + values[offset++] = this.ReadUInt16() / Max; + } + } + + this.currentIndex = start + (length * outChannelCount * 2); + return new IccClut(values, gridPointCount, IccClutDataType.UInt16, outChannelCount); + } + + /// + /// Reads a 32bit floating point CLUT. + /// + /// Input channel count. + /// Output channel count. + /// Grid point count for each CLUT channel. + /// The read CLUTf32. + public IccClut ReadClutF32(int inChCount, int outChCount, byte[] gridPointCount) + { + int start = this.currentIndex; + int length = 0; + for (int i = 0; i < inChCount; i++) + { + length += (int)Math.Pow(gridPointCount[i], inChCount); + } + + length /= inChCount; + + float[] values = new float[length * outChCount]; + int offset = 0; + for (int i = 0; i < length; i++) + { + for (int j = 0; j < outChCount; j++) + { + values[offset++] = this.ReadSingle(); + } + } + + this.currentIndex = start + (length * outChCount * 4); + return new IccClut(values, gridPointCount, IccClutDataType.Float, outChCount); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Matrix.cs b/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Matrix.cs new file mode 100644 index 0000000..3cfd746 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Matrix.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Provides methods to read ICC data types + /// + internal sealed partial class IccDataReader + { + /// + /// Reads a two dimensional matrix + /// + /// Number of values in X + /// Number of values in Y + /// True if the values are encoded as Single; false if encoded as Fix16 + /// The read matrix + public float[,] ReadMatrix(int xCount, int yCount, bool isSingle) + { + float[,] matrix = new float[xCount, yCount]; + + if (isSingle) + { + for (int y = 0; y < yCount; y++) + { + for (int x = 0; x < xCount; x++) + { + matrix[x, y] = this.ReadSingle(); + } + } + } + else + { + for (int y = 0; y < yCount; y++) + { + for (int x = 0; x < xCount; x++) + { + matrix[x, y] = this.ReadFix16(); + } + } + } + + return matrix; + } + + /// + /// Reads a one dimensional matrix + /// + /// Number of values + /// True if the values are encoded as Single; false if encoded as Fix16 + /// The read matrix + public float[] ReadMatrix(int yCount, bool isSingle) + { + float[] matrix = new float[yCount]; + if (isSingle) + { + for (int i = 0; i < yCount; i++) + { + matrix[i] = this.ReadSingle(); + } + } + else + { + for (int i = 0; i < yCount; i++) + { + matrix[i] = this.ReadFix16(); + } + } + + return matrix; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.MultiProcessElement.cs b/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.MultiProcessElement.cs new file mode 100644 index 0000000..0c6158f --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.MultiProcessElement.cs @@ -0,0 +1,84 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Provides methods to read ICC data types + /// + internal sealed partial class IccDataReader + { + /// + /// Reads a + /// + /// The read + public IccMultiProcessElement ReadMultiProcessElement() + { + IccMultiProcessElementSignature signature = (IccMultiProcessElementSignature)this.ReadUInt32(); + ushort inChannelCount = this.ReadUInt16(); + ushort outChannelCount = this.ReadUInt16(); + + switch (signature) + { + case IccMultiProcessElementSignature.CurveSet: + return this.ReadCurveSetProcessElement(inChannelCount, outChannelCount); + case IccMultiProcessElementSignature.Matrix: + return this.ReadMatrixProcessElement(inChannelCount, outChannelCount); + case IccMultiProcessElementSignature.Clut: + return this.ReadClutProcessElement(inChannelCount, outChannelCount); + + // Currently just placeholders for future ICC expansion + case IccMultiProcessElementSignature.BAcs: + this.AddIndex(8); + return new IccBAcsProcessElement(inChannelCount, outChannelCount); + case IccMultiProcessElementSignature.EAcs: + this.AddIndex(8); + return new IccEAcsProcessElement(inChannelCount, outChannelCount); + + default: + throw new InvalidIccProfileException($"Invalid MultiProcessElement type of {signature}"); + } + } + + /// + /// Reads a CurveSet + /// + /// Number of input channels + /// Number of output channels + /// The read + public IccCurveSetProcessElement ReadCurveSetProcessElement(int inChannelCount, int outChannelCount) + { + IccOneDimensionalCurve[] curves = new IccOneDimensionalCurve[inChannelCount]; + for (int i = 0; i < inChannelCount; i++) + { + curves[i] = this.ReadOneDimensionalCurve(); + this.AddPadding(); + } + + return new IccCurveSetProcessElement(curves); + } + + /// + /// Reads a Matrix + /// + /// Number of input channels + /// Number of output channels + /// The read + public IccMatrixProcessElement ReadMatrixProcessElement(int inChannelCount, int outChannelCount) + { + return new IccMatrixProcessElement( + this.ReadMatrix(inChannelCount, outChannelCount, true), + this.ReadMatrix(outChannelCount, true)); + } + + /// + /// Reads a CLUT + /// + /// Number of input channels + /// Number of output channels + /// The read + public IccClutProcessElement ReadClutProcessElement(int inChannelCount, int outChannelCount) + { + return new IccClutProcessElement(this.ReadClut(inChannelCount, outChannelCount, true)); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.NonPrimitives.cs b/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.NonPrimitives.cs new file mode 100644 index 0000000..2c15d63 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.NonPrimitives.cs @@ -0,0 +1,180 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Provides methods to read ICC data types + /// + internal sealed partial class IccDataReader + { + /// + /// Reads a DateTime + /// + /// the value + public DateTime ReadDateTime() + { + try + { + return new DateTime( + year: this.ReadUInt16(), + month: this.ReadUInt16(), + day: this.ReadUInt16(), + hour: this.ReadUInt16(), + minute: this.ReadUInt16(), + second: this.ReadUInt16(), + kind: DateTimeKind.Utc); + } + catch (ArgumentOutOfRangeException) + { + return DateTime.MinValue; + } + } + + /// + /// Reads an ICC profile version number + /// + /// the version number + public IccVersion ReadVersionNumber() + { + int version = this.ReadInt32(); + + int major = (version >> 24) & 0xFF; + int minor = (version >> 20) & 0x0F; + int bugfix = (version >> 16) & 0x0F; + + return new IccVersion(major, minor, bugfix); + } + + /// + /// Reads an XYZ number + /// + /// the XYZ number + public Vector3 ReadXyzNumber() + { + return new Vector3( + x: this.ReadFix16(), + y: this.ReadFix16(), + z: this.ReadFix16()); + } + + /// + /// Reads a profile ID + /// + /// the profile ID + public IccProfileId ReadProfileId() + { + return new IccProfileId( + p1: this.ReadUInt32(), + p2: this.ReadUInt32(), + p3: this.ReadUInt32(), + p4: this.ReadUInt32()); + } + + /// + /// Reads a position number + /// + /// the position number + public IccPositionNumber ReadPositionNumber() + { + return new IccPositionNumber( + offset: this.ReadUInt32(), + size: this.ReadUInt32()); + } + + /// + /// Reads a response number + /// + /// the response number + public IccResponseNumber ReadResponseNumber() + { + return new IccResponseNumber( + deviceCode: this.ReadUInt16(), + measurementValue: this.ReadFix16()); + } + + /// + /// Reads a named color + /// + /// Number of device coordinates + /// the named color + public IccNamedColor ReadNamedColor(uint deviceCoordCount) + { + string name = this.ReadAsciiString(32); + ushort[] pcsCoord = [this.ReadUInt16(), this.ReadUInt16(), this.ReadUInt16()]; + ushort[] deviceCoord = new ushort[deviceCoordCount]; + + for (int i = 0; i < deviceCoordCount; i++) + { + deviceCoord[i] = this.ReadUInt16(); + } + + return new IccNamedColor(name, pcsCoord, deviceCoord); + } + + /// + /// Reads a profile description + /// + /// the profile description + public IccProfileDescription ReadProfileDescription() + { + uint manufacturer = this.ReadUInt32(); + uint model = this.ReadUInt32(); + IccDeviceAttribute attributes = (IccDeviceAttribute)this.ReadInt64(); + IccProfileTag technologyInfo = (IccProfileTag)this.ReadUInt32(); + + IccMultiLocalizedUnicodeTagDataEntry manufacturerInfo = ReadText(); + IccMultiLocalizedUnicodeTagDataEntry modelInfo = ReadText(); + + return new IccProfileDescription( + manufacturer, + model, + attributes, + technologyInfo, + manufacturerInfo.Texts, + modelInfo.Texts); + + IccMultiLocalizedUnicodeTagDataEntry ReadText() + { + IccTypeSignature type = this.ReadTagDataEntryHeader(); + switch (type) + { + case IccTypeSignature.MultiLocalizedUnicode: + return this.ReadMultiLocalizedUnicodeTagDataEntry(); + case IccTypeSignature.TextDescription: + return (IccMultiLocalizedUnicodeTagDataEntry)this.ReadTextDescriptionTagDataEntry(); + + default: + throw new InvalidIccProfileException("Profile description can only have multi-localized Unicode or text description entries"); + } + } + } + + /// + /// Reads a colorant table entry + /// + /// the profile description + public IccColorantTableEntry ReadColorantTableEntry() + { + return new IccColorantTableEntry( + name: this.ReadAsciiString(32), + pcs1: this.ReadUInt16(), + pcs2: this.ReadUInt16(), + pcs3: this.ReadUInt16()); + } + + /// + /// Reads a screening channel + /// + /// the screening channel + public IccScreeningChannel ReadScreeningChannel() + { + return new IccScreeningChannel( + frequency: this.ReadFix16(), + angle: this.ReadFix16(), + spotShape: (IccScreeningSpotType)this.ReadInt32()); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Primitives.cs b/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Primitives.cs new file mode 100644 index 0000000..71361dc --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Primitives.cs @@ -0,0 +1,151 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Text; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Provides methods to read ICC data types + /// + internal sealed partial class IccDataReader + { + /// + /// Reads an ushort + /// + /// the value + public ushort ReadUInt16() => BinaryPrimitives.ReadUInt16BigEndian(this.data.AsSpan(this.AddIndex(2), 2)); + + /// + /// Reads a short + /// + /// the value + public short ReadInt16() => BinaryPrimitives.ReadInt16BigEndian(this.data.AsSpan(this.AddIndex(2), 2)); + + /// + /// Reads an uint + /// + /// the value + public uint ReadUInt32() => BinaryPrimitives.ReadUInt32BigEndian(this.data.AsSpan(this.AddIndex(4), 4)); + + /// + /// Reads an int + /// + /// the value + public int ReadInt32() => BinaryPrimitives.ReadInt32BigEndian(this.data.AsSpan(this.AddIndex(4), 4)); + + /// + /// Reads an ulong + /// + /// the value + public ulong ReadUInt64() => BinaryPrimitives.ReadUInt64BigEndian(this.data.AsSpan(this.AddIndex(8), 8)); + + /// + /// Reads a long + /// + /// the value + public long ReadInt64() => BinaryPrimitives.ReadInt64BigEndian(this.data.AsSpan(this.AddIndex(8), 8)); + + /// + /// Reads a float. + /// + /// the value + public float ReadSingle() + { + int intValue = this.ReadInt32(); + + return Unsafe.As(ref intValue); + } + + /// + /// Reads a double + /// + /// the value + public double ReadDouble() + { + long intValue = this.ReadInt64(); + + return Unsafe.As(ref intValue); + } + + /// + /// Reads an ASCII encoded string. + /// + /// number of bytes to read + /// The value as a string + public string ReadAsciiString(int length) + { + if (length == 0) + { + return string.Empty; + } + + Guard.MustBeGreaterThan(length, 0, nameof(length)); + string value = Encoding.ASCII.GetString(this.data, this.AddIndex(length), length); + + // remove data after (potential) null terminator + int pos = value.IndexOf('\0'); + if (pos >= 0) + { + value = value[..pos]; + } + + return value; + } + + /// + /// Reads an UTF-16 big-endian encoded string. + /// + /// number of bytes to read + /// The value as a string + public string ReadUnicodeString(int length) + { + if (length == 0) + { + return string.Empty; + } + + Guard.MustBeGreaterThan(length, 0, nameof(length)); + + return Encoding.BigEndianUnicode.GetString(this.data, this.AddIndex(length), length); + } + + /// + /// Reads a signed 32bit number with 1 sign bit, 15 value bits and 16 fractional bits. + /// + /// The number as double + public float ReadFix16() => this.ReadInt32() / 65536f; + + /// + /// Reads an unsigned 32bit number with 16 value bits and 16 fractional bits. + /// + /// The number as double + public float ReadUFix16() => this.ReadUInt32() / 65536f; + + /// + /// Reads an unsigned 16bit number with 1 value bit and 15 fractional bits. + /// + /// The number as double + public float ReadU1Fix15() => this.ReadUInt16() / 32768f; + + /// + /// Reads an unsigned 16bit number with 8 value bits and 8 fractional bits. + /// + /// The number as double + public float ReadUFix8() => this.ReadUInt16() / 256f; + + /// + /// Reads a number of bytes and advances the index. + /// + /// The number of bytes to read + /// The read bytes + public byte[] ReadBytes(int count) + { + byte[] bytes = new byte[count]; + Buffer.BlockCopy(this.data, this.AddIndex(count), bytes, 0, count); + return bytes; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs new file mode 100644 index 0000000..a0ad827 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs @@ -0,0 +1,846 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Globalization; +using System.Numerics; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Provides methods to read ICC data types. + /// + internal sealed partial class IccDataReader + { + /// + /// Reads a tag data entry. + /// + /// The table entry with reading information. + /// The tag data entry. + public IccTagDataEntry ReadTagDataEntry(IccTagTableEntry info) + { + this.currentIndex = (int)info.Offset; + return this.ReadTagDataEntryHeader() switch + { + IccTypeSignature.Chromaticity => this.ReadChromaticityTagDataEntry(), + IccTypeSignature.ColorantOrder => this.ReadColorantOrderTagDataEntry(), + IccTypeSignature.ColorantTable => this.ReadColorantTableTagDataEntry(), + IccTypeSignature.Curve => this.ReadCurveTagDataEntry(), + IccTypeSignature.Data => this.ReadDataTagDataEntry(info.DataSize), + IccTypeSignature.DateTime => this.ReadDateTimeTagDataEntry(), + IccTypeSignature.Lut16 => this.ReadLut16TagDataEntry(), + IccTypeSignature.Lut8 => this.ReadLut8TagDataEntry(), + IccTypeSignature.LutAToB => this.ReadLutAtoBTagDataEntry(), + IccTypeSignature.LutBToA => this.ReadLutBtoATagDataEntry(), + IccTypeSignature.Measurement => this.ReadMeasurementTagDataEntry(), + IccTypeSignature.MultiLocalizedUnicode => this.ReadMultiLocalizedUnicodeTagDataEntry(), + IccTypeSignature.MultiProcessElements => this.ReadMultiProcessElementsTagDataEntry(), + IccTypeSignature.NamedColor2 => this.ReadNamedColor2TagDataEntry(), + IccTypeSignature.ParametricCurve => this.ReadParametricCurveTagDataEntry(), + IccTypeSignature.ProfileSequenceDesc => this.ReadProfileSequenceDescTagDataEntry(), + IccTypeSignature.ProfileSequenceIdentifier => this.ReadProfileSequenceIdentifierTagDataEntry(), + IccTypeSignature.ResponseCurveSet16 => this.ReadResponseCurveSet16TagDataEntry(), + IccTypeSignature.S15Fixed16Array => this.ReadFix16ArrayTagDataEntry(info.DataSize), + IccTypeSignature.Signature => this.ReadSignatureTagDataEntry(), + IccTypeSignature.Text => this.ReadTextTagDataEntry(info.DataSize), + IccTypeSignature.U16Fixed16Array => this.ReadUFix16ArrayTagDataEntry(info.DataSize), + IccTypeSignature.UInt16Array => this.ReadUInt16ArrayTagDataEntry(info.DataSize), + IccTypeSignature.UInt32Array => this.ReadUInt32ArrayTagDataEntry(info.DataSize), + IccTypeSignature.UInt64Array => this.ReadUInt64ArrayTagDataEntry(info.DataSize), + IccTypeSignature.UInt8Array => this.ReadUInt8ArrayTagDataEntry(info.DataSize), + IccTypeSignature.ViewingConditions => this.ReadViewingConditionsTagDataEntry(), + IccTypeSignature.Xyz => this.ReadXyzTagDataEntry(info.DataSize), + + // V2 Types: + IccTypeSignature.TextDescription => this.ReadTextDescriptionTagDataEntry(), + IccTypeSignature.CrdInfo => this.ReadCrdInfoTagDataEntry(), + IccTypeSignature.Screening => this.ReadScreeningTagDataEntry(), + IccTypeSignature.UcrBg => this.ReadUcrBgTagDataEntry(info.DataSize), + + // Unsupported or unknown + _ => this.ReadUnknownTagDataEntry(info.DataSize), + }; + } + + /// + /// Reads the header of a + /// + /// The read signature. + public IccTypeSignature ReadTagDataEntryHeader() + { + IccTypeSignature type = (IccTypeSignature)this.ReadUInt32(); + this.AddIndex(4); // 4 bytes are not used + return type; + } + + /// + /// Reads the header of a and checks if it's the expected value + /// + /// The expected value to check against. + public void ReadCheckTagDataEntryHeader(IccTypeSignature expected) + { + IccTypeSignature type = this.ReadTagDataEntryHeader(); + if (expected != (IccTypeSignature)uint.MaxValue && type != expected) + { + throw new InvalidIccProfileException($"Read signature {type} is not the expected {expected}"); + } + } + + /// + /// Reads a with an unknown + /// + /// The size of the entry in bytes. + /// The read entry. + public IccUnknownTagDataEntry ReadUnknownTagDataEntry(uint size) + { + int count = (int)size - 8; // 8 is the tag header size + return new IccUnknownTagDataEntry(this.ReadBytes(count)); + } + + /// + /// Reads a + /// + /// The read entry. + public IccChromaticityTagDataEntry ReadChromaticityTagDataEntry() + { + ushort channelCount = this.ReadUInt16(); + IccColorantEncoding colorant = (IccColorantEncoding)this.ReadUInt16(); + + if (Enum.IsDefined(colorant) && colorant != IccColorantEncoding.Unknown) + { + // The type is known and so are the values (they are constant) + // channelCount should always be 3 but it doesn't really matter if it's not + return new IccChromaticityTagDataEntry(colorant); + } + else + { + // The type is not know, so the values need be read. + double[][] values = new double[channelCount][]; + for (int i = 0; i < channelCount; i++) + { + values[i] = [this.ReadUFix16(), this.ReadUFix16()]; + } + + return new IccChromaticityTagDataEntry(values); + } + } + + /// + /// Reads a + /// + /// The read entry. + public IccColorantOrderTagDataEntry ReadColorantOrderTagDataEntry() + { + uint colorantCount = this.ReadUInt32(); + byte[] number = this.ReadBytes((int)colorantCount); + return new IccColorantOrderTagDataEntry(number); + } + + /// + /// Reads a + /// + /// The read entry. + public IccColorantTableTagDataEntry ReadColorantTableTagDataEntry() + { + uint colorantCount = this.ReadUInt32(); + IccColorantTableEntry[] cdata = new IccColorantTableEntry[colorantCount]; + for (int i = 0; i < colorantCount; i++) + { + cdata[i] = this.ReadColorantTableEntry(); + } + + return new IccColorantTableTagDataEntry(cdata); + } + + /// + /// Reads a + /// + /// The read entry. + public IccCurveTagDataEntry ReadCurveTagDataEntry() + { + uint pointCount = this.ReadUInt32(); + + if (pointCount == 0) + { + return new IccCurveTagDataEntry(); + } + + if (pointCount == 1) + { + return new IccCurveTagDataEntry(this.ReadUFix8()); + } + + float[] cdata = new float[pointCount]; + for (int i = 0; i < pointCount; i++) + { + cdata[i] = this.ReadUInt16() / 65535f; + } + + return new IccCurveTagDataEntry(cdata); + + // TODO: If the input is PCSXYZ, 1+(32 767/32 768) shall be mapped to the value 1,0. If the output is PCSXYZ, the value 1,0 shall be mapped to 1+(32 767/32 768). + } + + /// + /// Reads a + /// + /// The size of the entry in bytes. + /// The read entry + public IccDataTagDataEntry ReadDataTagDataEntry(uint size) + { + this.AddIndex(3); // first 3 bytes are zero + byte b = this.data[this.AddIndex(1)]; + + // last bit of 4th byte is either 0 = ASCII or 1 = binary + bool ascii = GetBit(b, 7); + int length = (int)size - 12; + byte[] cdata = this.ReadBytes(length); + + return new IccDataTagDataEntry(cdata, ascii); + } + + /// + /// Reads a + /// + /// The read entry. + public IccDateTimeTagDataEntry ReadDateTimeTagDataEntry() => new(this.ReadDateTime()); + + /// + /// Reads a + /// + /// The read entry. + public IccLut16TagDataEntry ReadLut16TagDataEntry() + { + byte inChCount = this.data[this.AddIndex(1)]; + byte outChCount = this.data[this.AddIndex(1)]; + byte clutPointCount = this.data[this.AddIndex(1)]; + this.AddIndex(1); // 1 byte reserved + + float[,] matrix = this.ReadMatrix(3, 3, false); + + ushort inTableCount = this.ReadUInt16(); + ushort outTableCount = this.ReadUInt16(); + + // Input LUT + IccLut[] inValues = new IccLut[inChCount]; + byte[] gridPointCount = new byte[inChCount]; + for (int i = 0; i < inChCount; i++) + { + inValues[i] = this.ReadLut16(inTableCount); + gridPointCount[i] = clutPointCount; + } + + // CLUT + IccClut clut = this.ReadClut16(inChCount, outChCount, gridPointCount); + + // Output LUT + IccLut[] outValues = new IccLut[outChCount]; + for (int i = 0; i < outChCount; i++) + { + outValues[i] = this.ReadLut16(outTableCount); + } + + return new IccLut16TagDataEntry(matrix, inValues, clut, outValues); + } + + /// + /// Reads a + /// + /// The read entry. + public IccLut8TagDataEntry ReadLut8TagDataEntry() + { + byte inChCount = this.data[this.AddIndex(1)]; + byte outChCount = this.data[this.AddIndex(1)]; + byte clutPointCount = this.data[this.AddIndex(1)]; + this.AddIndex(1); // 1 byte reserved + + float[,] matrix = this.ReadMatrix(3, 3, false); + + // Input LUT + IccLut[] inValues = new IccLut[inChCount]; + byte[] gridPointCount = new byte[inChCount]; + for (int i = 0; i < inChCount; i++) + { + inValues[i] = this.ReadLut8(); + gridPointCount[i] = clutPointCount; + } + + // CLUT + IccClut clut = this.ReadClut8(inChCount, outChCount, gridPointCount); + + // Output LUT + IccLut[] outValues = new IccLut[outChCount]; + for (int i = 0; i < outChCount; i++) + { + outValues[i] = this.ReadLut8(); + } + + return new IccLut8TagDataEntry(matrix, inValues, clut, outValues); + } + + /// + /// Reads a + /// + /// The read entry. + public IccLutAToBTagDataEntry ReadLutAtoBTagDataEntry() + { + int start = this.currentIndex - 8; // 8 is the tag header size + + byte inChCount = this.data[this.AddIndex(1)]; + byte outChCount = this.data[this.AddIndex(1)]; + this.AddIndex(2); // 2 bytes reserved + + uint bCurveOffset = this.ReadUInt32(); + uint matrixOffset = this.ReadUInt32(); + uint mCurveOffset = this.ReadUInt32(); + uint clutOffset = this.ReadUInt32(); + uint aCurveOffset = this.ReadUInt32(); + + IccTagDataEntry[] bCurve = null; + IccTagDataEntry[] mCurve = null; + IccTagDataEntry[] aCurve = null; + IccClut clut = null; + float[,] matrix3x3 = null; + float[] matrix3x1 = null; + + if (bCurveOffset != 0) + { + this.currentIndex = (int)bCurveOffset + start; + bCurve = this.ReadCurves(outChCount); + } + + if (mCurveOffset != 0) + { + this.currentIndex = (int)mCurveOffset + start; + mCurve = this.ReadCurves(outChCount); + } + + if (aCurveOffset != 0) + { + this.currentIndex = (int)aCurveOffset + start; + aCurve = this.ReadCurves(inChCount); + } + + if (clutOffset != 0) + { + this.currentIndex = (int)clutOffset + start; + clut = this.ReadClut(inChCount, outChCount, false); + } + + if (matrixOffset != 0) + { + this.currentIndex = (int)matrixOffset + start; + matrix3x3 = this.ReadMatrix(3, 3, false); + matrix3x1 = this.ReadMatrix(3, false); + } + + return new IccLutAToBTagDataEntry(bCurve, matrix3x3, matrix3x1, mCurve, clut, aCurve); + } + + /// + /// Reads a + /// + /// The read entry. + public IccLutBToATagDataEntry ReadLutBtoATagDataEntry() + { + int start = this.currentIndex - 8; // 8 is the tag header size + + byte inChCount = this.data[this.AddIndex(1)]; + byte outChCount = this.data[this.AddIndex(1)]; + this.AddIndex(2); // 2 bytes reserved + + uint bCurveOffset = this.ReadUInt32(); + uint matrixOffset = this.ReadUInt32(); + uint mCurveOffset = this.ReadUInt32(); + uint clutOffset = this.ReadUInt32(); + uint aCurveOffset = this.ReadUInt32(); + + IccTagDataEntry[] bCurve = null; + IccTagDataEntry[] mCurve = null; + IccTagDataEntry[] aCurve = null; + IccClut clut = null; + float[,] matrix3x3 = null; + float[] matrix3x1 = null; + + if (bCurveOffset != 0) + { + this.currentIndex = (int)bCurveOffset + start; + bCurve = this.ReadCurves(inChCount); + } + + if (mCurveOffset != 0) + { + this.currentIndex = (int)mCurveOffset + start; + mCurve = this.ReadCurves(inChCount); + } + + if (aCurveOffset != 0) + { + this.currentIndex = (int)aCurveOffset + start; + aCurve = this.ReadCurves(outChCount); + } + + if (clutOffset != 0) + { + this.currentIndex = (int)clutOffset + start; + clut = this.ReadClut(inChCount, outChCount, false); + } + + if (matrixOffset != 0) + { + this.currentIndex = (int)matrixOffset + start; + matrix3x3 = this.ReadMatrix(3, 3, false); + matrix3x1 = this.ReadMatrix(3, false); + } + + return new IccLutBToATagDataEntry(bCurve, matrix3x3, matrix3x1, mCurve, clut, aCurve); + } + + /// + /// Reads a + /// + /// The read entry. + public IccMeasurementTagDataEntry ReadMeasurementTagDataEntry() => new( + observer: (IccStandardObserver)this.ReadUInt32(), + xyzBacking: this.ReadXyzNumber(), + geometry: (IccMeasurementGeometry)this.ReadUInt32(), + flare: this.ReadUFix16(), + illuminant: (IccStandardIlluminant)this.ReadUInt32()); + + /// + /// Reads a + /// + /// The read entry. + public IccMultiLocalizedUnicodeTagDataEntry ReadMultiLocalizedUnicodeTagDataEntry() + { + int start = this.currentIndex - 8; // 8 is the tag header size + uint recordCount = this.ReadUInt32(); + + this.ReadUInt32(); // Record size (always 12) + IccLocalizedString[] text = new IccLocalizedString[recordCount]; + + CultureInfo[] culture = new CultureInfo[recordCount]; + uint[] length = new uint[recordCount]; + uint[] offset = new uint[recordCount]; + + for (int i = 0; i < recordCount; i++) + { + string languageCode = this.ReadAsciiString(2); + string countryCode = this.ReadAsciiString(2); + + culture[i] = ReadCulture(languageCode, countryCode); + length[i] = this.ReadUInt32(); + offset[i] = this.ReadUInt32(); + } + + for (int i = 0; i < recordCount; i++) + { + this.currentIndex = (int)(start + offset[i]); + text[i] = new IccLocalizedString(culture[i], this.ReadUnicodeString((int)length[i])); + } + + return new IccMultiLocalizedUnicodeTagDataEntry(text); + + static CultureInfo ReadCulture(string language, string country) + { + if (string.IsNullOrWhiteSpace(language)) + { + return CultureInfo.InvariantCulture; + } + else if (string.IsNullOrWhiteSpace(country)) + { + try + { + return new CultureInfo(language); + } + catch (CultureNotFoundException) + { + return CultureInfo.InvariantCulture; + } + } + else + { + try + { + return new CultureInfo($"{language}-{country}"); + } + catch (CultureNotFoundException) + { + return ReadCulture(language, null); + } + } + } + } + + /// + /// Reads a + /// + /// The read entry. + public IccMultiProcessElementsTagDataEntry ReadMultiProcessElementsTagDataEntry() + { + int start = this.currentIndex - 8; + + this.ReadUInt16(); + this.ReadUInt16(); + uint elementCount = this.ReadUInt32(); + + IccPositionNumber[] positionTable = new IccPositionNumber[elementCount]; + for (int i = 0; i < elementCount; i++) + { + positionTable[i] = this.ReadPositionNumber(); + } + + IccMultiProcessElement[] elements = new IccMultiProcessElement[elementCount]; + for (int i = 0; i < elementCount; i++) + { + this.currentIndex = (int)positionTable[i].Offset + start; + elements[i] = this.ReadMultiProcessElement(); + } + + return new IccMultiProcessElementsTagDataEntry(elements); + } + + /// + /// Reads a + /// + /// The read entry. + public IccNamedColor2TagDataEntry ReadNamedColor2TagDataEntry() + { + int vendorFlag = this.ReadInt32(); + uint colorCount = this.ReadUInt32(); + uint coordCount = this.ReadUInt32(); + string prefix = this.ReadAsciiString(32); + string suffix = this.ReadAsciiString(32); + + IccNamedColor[] colors = new IccNamedColor[colorCount]; + for (int i = 0; i < colorCount; i++) + { + colors[i] = this.ReadNamedColor(coordCount); + } + + return new IccNamedColor2TagDataEntry(vendorFlag, prefix, suffix, colors); + } + + /// + /// Reads a + /// + /// The read entry + public IccParametricCurveTagDataEntry ReadParametricCurveTagDataEntry() => new(this.ReadParametricCurve()); + + /// + /// Reads a + /// + /// The read entry. + public IccProfileSequenceDescTagDataEntry ReadProfileSequenceDescTagDataEntry() + { + uint count = this.ReadUInt32(); + IccProfileDescription[] description = new IccProfileDescription[count]; + for (int i = 0; i < count; i++) + { + description[i] = this.ReadProfileDescription(); + } + + return new IccProfileSequenceDescTagDataEntry(description); + } + + /// + /// Reads a + /// + /// The read entry. + public IccProfileSequenceIdentifierTagDataEntry ReadProfileSequenceIdentifierTagDataEntry() + { + int start = this.currentIndex - 8; // 8 is the tag header size + uint count = this.ReadUInt32(); + IccPositionNumber[] table = new IccPositionNumber[count]; + for (int i = 0; i < count; i++) + { + table[i] = this.ReadPositionNumber(); + } + + IccProfileSequenceIdentifier[] entries = new IccProfileSequenceIdentifier[count]; + for (int i = 0; i < count; i++) + { + this.currentIndex = (int)(start + table[i].Offset); + IccProfileId id = this.ReadProfileId(); + this.ReadCheckTagDataEntryHeader(IccTypeSignature.MultiLocalizedUnicode); + IccMultiLocalizedUnicodeTagDataEntry description = this.ReadMultiLocalizedUnicodeTagDataEntry(); + entries[i] = new IccProfileSequenceIdentifier(id, description.Texts); + } + + return new IccProfileSequenceIdentifierTagDataEntry(entries); + } + + /// + /// Reads a + /// + /// The read entry. + public IccResponseCurveSet16TagDataEntry ReadResponseCurveSet16TagDataEntry() + { + int start = this.currentIndex - 8; // 8 is the tag header size + ushort channelCount = this.ReadUInt16(); + ushort measurementCount = this.ReadUInt16(); + + uint[] offset = new uint[measurementCount]; + for (int i = 0; i < measurementCount; i++) + { + offset[i] = this.ReadUInt32(); + } + + IccResponseCurve[] curves = new IccResponseCurve[measurementCount]; + for (int i = 0; i < measurementCount; i++) + { + this.currentIndex = (int)(start + offset[i]); + curves[i] = this.ReadResponseCurve(channelCount); + } + + return new IccResponseCurveSet16TagDataEntry(curves); + } + + /// + /// Reads a + /// + /// The size of the entry in bytes. + /// The read entry. + public IccFix16ArrayTagDataEntry ReadFix16ArrayTagDataEntry(uint size) + { + uint count = (size - 8) / 4; + float[] arrayData = new float[count]; + for (int i = 0; i < count; i++) + { + arrayData[i] = this.ReadFix16() / 256f; + } + + return new IccFix16ArrayTagDataEntry(arrayData); + } + + /// + /// Reads a + /// + /// The read entry. + public IccSignatureTagDataEntry ReadSignatureTagDataEntry() => new(this.ReadAsciiString(4)); + + /// + /// Reads a + /// + /// The size of the entry in bytes. + /// The read entry. + public IccTextTagDataEntry ReadTextTagDataEntry(uint size) => new(this.ReadAsciiString((int)size - 8)); // 8 is the tag header size + + /// + /// Reads a + /// + /// The size of the entry in bytes. + /// The read entry. + public IccUFix16ArrayTagDataEntry ReadUFix16ArrayTagDataEntry(uint size) + { + uint count = (size - 8) / 4; + float[] arrayData = new float[count]; + for (int i = 0; i < count; i++) + { + arrayData[i] = this.ReadUFix16(); + } + + return new IccUFix16ArrayTagDataEntry(arrayData); + } + + /// + /// Reads a + /// + /// The size of the entry in bytes. + /// The read entry. + public IccUInt16ArrayTagDataEntry ReadUInt16ArrayTagDataEntry(uint size) + { + uint count = (size - 8) / 2; + ushort[] arrayData = new ushort[count]; + for (int i = 0; i < count; i++) + { + arrayData[i] = this.ReadUInt16(); + } + + return new IccUInt16ArrayTagDataEntry(arrayData); + } + + /// + /// Reads a + /// + /// The size of the entry in bytes. + /// The read entry. + public IccUInt32ArrayTagDataEntry ReadUInt32ArrayTagDataEntry(uint size) + { + uint count = (size - 8) / 4; + uint[] arrayData = new uint[count]; + for (int i = 0; i < count; i++) + { + arrayData[i] = this.ReadUInt32(); + } + + return new IccUInt32ArrayTagDataEntry(arrayData); + } + + /// + /// Reads a + /// + /// The size of the entry in bytes. + /// The read entry. + public IccUInt64ArrayTagDataEntry ReadUInt64ArrayTagDataEntry(uint size) + { + uint count = (size - 8) / 8; + ulong[] arrayData = new ulong[count]; + for (int i = 0; i < count; i++) + { + arrayData[i] = this.ReadUInt64(); + } + + return new IccUInt64ArrayTagDataEntry(arrayData); + } + + /// + /// Reads a + /// + /// The size of the entry in bytes. + /// The read entry. + public IccUInt8ArrayTagDataEntry ReadUInt8ArrayTagDataEntry(uint size) + { + int count = (int)size - 8; // 8 is the tag header size + byte[] adata = this.ReadBytes(count); + + return new IccUInt8ArrayTagDataEntry(adata); + } + + /// + /// Reads a + /// + /// The read entry. + public IccViewingConditionsTagDataEntry ReadViewingConditionsTagDataEntry() => new( + illuminantXyz: this.ReadXyzNumber(), + surroundXyz: this.ReadXyzNumber(), + illuminant: (IccStandardIlluminant)this.ReadUInt32()); + + /// + /// Reads a + /// + /// The size of the entry in bytes. + /// The read entry. + public IccXyzTagDataEntry ReadXyzTagDataEntry(uint size) + { + uint count = (size - 8) / 12; + Vector3[] arrayData = new Vector3[count]; + for (int i = 0; i < count; i++) + { + arrayData[i] = this.ReadXyzNumber(); + } + + return new IccXyzTagDataEntry(arrayData); + } + + /// + /// Reads a + /// + /// The read entry. + public IccTextDescriptionTagDataEntry ReadTextDescriptionTagDataEntry() + { + string unicodeValue, scriptcodeValue; + string asciiValue = unicodeValue = scriptcodeValue = null; + + int asciiCount = (int)this.ReadUInt32(); + if (asciiCount > 0) + { + asciiValue = this.ReadAsciiString(asciiCount - 1); + this.AddIndex(1); // Null terminator + } + + uint unicodeLangCode = this.ReadUInt32(); + int unicodeCount = (int)this.ReadUInt32(); + if (unicodeCount > 0) + { + unicodeValue = this.ReadUnicodeString((unicodeCount * 2) - 2); + this.AddIndex(2); // Null terminator + } + + ushort scriptcodeCode = this.ReadUInt16(); + int scriptcodeCount = Math.Min(this.data[this.AddIndex(1)], (byte)67); + if (scriptcodeCount > 0) + { + scriptcodeValue = this.ReadAsciiString(scriptcodeCount - 1); + this.AddIndex(1); // Null terminator + } + + return new IccTextDescriptionTagDataEntry( + asciiValue, + unicodeValue, + scriptcodeValue, + unicodeLangCode, + scriptcodeCode); + } + + /// + /// Reads a + /// + /// The read entry. + public IccCrdInfoTagDataEntry ReadCrdInfoTagDataEntry() + { + uint productNameCount = this.ReadUInt32(); + string productName = this.ReadAsciiString((int)productNameCount); + + uint crd0Count = this.ReadUInt32(); + string crd0Name = this.ReadAsciiString((int)crd0Count); + + uint crd1Count = this.ReadUInt32(); + string crd1Name = this.ReadAsciiString((int)crd1Count); + + uint crd2Count = this.ReadUInt32(); + string crd2Name = this.ReadAsciiString((int)crd2Count); + + uint crd3Count = this.ReadUInt32(); + string crd3Name = this.ReadAsciiString((int)crd3Count); + + return new IccCrdInfoTagDataEntry(productName, crd0Name, crd1Name, crd2Name, crd3Name); + } + + /// + /// Reads a + /// + /// The read entry. + public IccScreeningTagDataEntry ReadScreeningTagDataEntry() + { + IccScreeningFlag flags = (IccScreeningFlag)this.ReadInt32(); + uint channelCount = this.ReadUInt32(); + IccScreeningChannel[] channels = new IccScreeningChannel[channelCount]; + for (int i = 0; i < channels.Length; i++) + { + channels[i] = this.ReadScreeningChannel(); + } + + return new IccScreeningTagDataEntry(flags, channels); + } + + /// + /// Reads a + /// + /// The size of the entry in bytes. + /// The read entry + public IccUcrBgTagDataEntry ReadUcrBgTagDataEntry(uint size) + { + uint ucrCount = this.ReadUInt32(); + ushort[] ucrCurve = new ushort[ucrCount]; + for (int i = 0; i < ucrCurve.Length; i++) + { + ucrCurve[i] = this.ReadUInt16(); + } + + uint bgCount = this.ReadUInt32(); + ushort[] bgCurve = new ushort[bgCount]; + for (int i = 0; i < bgCurve.Length; i++) + { + bgCurve[i] = this.ReadUInt16(); + } + + // ((ucr length + bg length) * UInt16 size) + (ucrCount + bgCount) + uint dataSize = ((ucrCount + bgCount) * 2) + 8; + int descriptionLength = (int)(size - 8 - dataSize); // 8 is the tag header size + string description = this.ReadAsciiString(descriptionLength); + + return new IccUcrBgTagDataEntry(ucrCurve, bgCurve, description); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.cs b/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.cs new file mode 100644 index 0000000..90a5297 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Provides methods to read ICC data types + /// + internal sealed partial class IccDataReader + { + /// + /// The data that is read + /// + private readonly byte[] data; + + /// + /// The current reading position + /// + private int currentIndex; + + /// + /// Initializes a new instance of the class. + /// + /// The data to read + public IccDataReader(byte[] data) + => this.data = data ?? throw new ArgumentNullException(nameof(data)); + + /// + /// Gets the length in bytes of the raw data + /// + public int DataLength => this.data.Length; + + /// + /// Sets the reading position to the given value + /// + /// The new index position + public void SetIndex(int index) + => this.currentIndex = Numerics.Clamp(index, 0, this.data.Length); + + /// + /// Returns the current without increment and adds the given increment + /// + /// The value to increment + /// The current without the increment + private int AddIndex(int increment) + { + int tmp = this.currentIndex; + this.currentIndex += increment; + return tmp; + } + + /// + /// Calculates the 4 byte padding and adds it to the variable + /// + private void AddPadding() + => this.currentIndex += this.CalcPadding(); + + /// + /// Calculates the 4 byte padding + /// + /// the number of bytes to pad + private int CalcPadding() + { + int p = 4 - (this.currentIndex % 4); + return p >= 4 ? 0 : p; + } + + /// + /// Gets the bit value at a specified position + /// + /// The value from where the bit will be extracted + /// Position of the bit. Zero based index from left to right. + /// The bit value at specified position + private static bool GetBit(byte value, int position) + => ((value >> (7 - position)) & 1) == 1; + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.Curves.cs b/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.Curves.cs new file mode 100644 index 0000000..998c49c --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.Curves.cs @@ -0,0 +1,175 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Provides methods to write ICC data types + /// + internal sealed partial class IccDataWriter + { + /// + /// Writes a + /// + /// The curve to write + /// The number of bytes written + public int WriteOneDimensionalCurve(IccOneDimensionalCurve value) + { + int count = this.WriteUInt16((ushort)value.Segments.Length); + count += this.WriteEmpty(2); + + foreach (float point in value.BreakPoints) + { + count += this.WriteSingle(point); + } + + foreach (IccCurveSegment segment in value.Segments) + { + count += this.WriteCurveSegment(segment); + } + + return count; + } + + /// + /// Writes a + /// + /// The curve to write + /// The number of bytes written + public int WriteResponseCurve(IccResponseCurve value) + { + int count = this.WriteUInt32((uint)value.CurveType); + + foreach (IccResponseNumber[] responseArray in value.ResponseArrays) + { + count += this.WriteUInt32((uint)responseArray.Length); + } + + foreach (Vector3 xyz in value.XyzValues) + { + count += this.WriteXyzNumber(xyz); + } + + foreach (IccResponseNumber[] responseArray in value.ResponseArrays) + { + foreach (IccResponseNumber response in responseArray) + { + count += this.WriteResponseNumber(response); + } + } + + return count; + } + + /// + /// Writes a + /// + /// The curve to write + /// The number of bytes written + public int WriteParametricCurve(IccParametricCurve value) + { + ushort typeValue = (ushort)value.Type; + int count = this.WriteUInt16(typeValue); + count += this.WriteEmpty(2); + + if (typeValue <= 4) + { + count += this.WriteFix16(value.G); + } + + if (typeValue > 0 && typeValue <= 4) + { + count += this.WriteFix16(value.A); + count += this.WriteFix16(value.B); + } + + if (typeValue > 1 && typeValue <= 4) + { + count += this.WriteFix16(value.C); + } + + if (typeValue > 2 && typeValue <= 4) + { + count += this.WriteFix16(value.D); + } + + if (typeValue == 4) + { + count += this.WriteFix16(value.E); + count += this.WriteFix16(value.F); + } + + return count; + } + + /// + /// Writes a + /// + /// The curve to write + /// The number of bytes written + public int WriteCurveSegment(IccCurveSegment value) + { + int count = this.WriteUInt32((uint)value.Signature); + count += this.WriteEmpty(4); + + switch (value.Signature) + { + case IccCurveSegmentSignature.FormulaCurve: + return count + this.WriteFormulaCurveElement((IccFormulaCurveElement)value); + case IccCurveSegmentSignature.SampledCurve: + return count + this.WriteSampledCurveElement((IccSampledCurveElement)value); + default: + throw new InvalidIccProfileException($"Invalid CurveSegment type of {value.Signature}"); + } + } + + /// + /// Writes a + /// + /// The curve to write + /// The number of bytes written + public int WriteFormulaCurveElement(IccFormulaCurveElement value) + { + int count = this.WriteUInt16((ushort)value.Type); + count += this.WriteEmpty(2); + + if (value.Type == IccFormulaCurveType.Type1 || value.Type == IccFormulaCurveType.Type2) + { + count += this.WriteSingle(value.Gamma); + } + + count += this.WriteSingle(value.A); + count += this.WriteSingle(value.B); + count += this.WriteSingle(value.C); + + if (value.Type == IccFormulaCurveType.Type2 || value.Type == IccFormulaCurveType.Type3) + { + count += this.WriteSingle(value.D); + } + + if (value.Type == IccFormulaCurveType.Type3) + { + count += this.WriteSingle(value.E); + } + + return count; + } + + /// + /// Writes a + /// + /// The curve to write + /// The number of bytes written + public int WriteSampledCurveElement(IccSampledCurveElement value) + { + int count = this.WriteUInt32((uint)value.CurveEntries.Length); + foreach (float entry in value.CurveEntries) + { + count += this.WriteSingle(entry); + } + + return count; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.Lut.cs b/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.Lut.cs new file mode 100644 index 0000000..04a682f --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.Lut.cs @@ -0,0 +1,116 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Provides methods to write ICC data types. + /// + internal sealed partial class IccDataWriter + { + /// + /// Writes an 8bit lookup table. + /// + /// The LUT to write. + /// The number of bytes written. + public int WriteLut8(IccLut value) + { + foreach (float item in value.Values) + { + this.WriteByte((byte)Numerics.Clamp((item * byte.MaxValue) + 0.5F, 0, byte.MaxValue)); + } + + return value.Values.Length; + } + + /// + /// Writes an 16bit lookup table. + /// + /// The LUT to write. + /// The number of bytes written. + public int WriteLut16(IccLut value) + { + foreach (float item in value.Values) + { + this.WriteUInt16((ushort)Numerics.Clamp((item * ushort.MaxValue) + 0.5F, 0, ushort.MaxValue)); + } + + return value.Values.Length * 2; + } + + /// + /// Writes an color lookup table. + /// + /// The CLUT to write. + /// The number of bytes written. + public int WriteClut(IccClut value) + { + int count = this.WriteArray(value.GridPointCount); + count += this.WriteEmpty(16 - value.GridPointCount.Length); + + switch (value.DataType) + { + case IccClutDataType.Float: + return count + this.WriteClutF32(value); + case IccClutDataType.UInt8: + count += this.WriteByte(1); + count += this.WriteEmpty(3); + return count + this.WriteClut8(value); + case IccClutDataType.UInt16: + count += this.WriteByte(2); + count += this.WriteEmpty(3); + return count + this.WriteClut16(value); + + default: + throw new InvalidIccProfileException($"Invalid CLUT data type of {value.DataType}"); + } + } + + /// + /// Writes a 8bit color lookup table. + /// + /// The CLUT to write. + /// The number of bytes written. + public int WriteClut8(IccClut value) + { + int count = 0; + foreach (float item in value.Values) + { + count += this.WriteByte((byte)Numerics.Clamp((item * byte.MaxValue) + 0.5F, 0, byte.MaxValue)); + } + + return count; + } + + /// + /// Writes a 16bit color lookup table. + /// + /// The CLUT to write. + /// The number of bytes written. + public int WriteClut16(IccClut value) + { + int count = 0; + foreach (float item in value.Values) + { + count += this.WriteUInt16((ushort)Numerics.Clamp((item * ushort.MaxValue) + 0.5F, 0, ushort.MaxValue)); + } + + return count; + } + + /// + /// Writes a 32bit float color lookup table. + /// + /// The CLUT to write. + /// The number of bytes written. + public int WriteClutF32(IccClut value) + { + int count = 0; + foreach (float item in value.Values) + { + count += this.WriteSingle(item); + } + + return count; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.Matrix.cs b/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.Matrix.cs new file mode 100644 index 0000000..4e13f6e --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.Matrix.cs @@ -0,0 +1,170 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Provides methods to write ICC data types + /// + internal sealed partial class IccDataWriter + { + /// + /// Writes a two dimensional matrix + /// + /// The matrix to write + /// True if the values are encoded as Single; false if encoded as Fix16 + /// The number of bytes written + public int WriteMatrix(Matrix4x4 value, bool isSingle) + { + int count = 0; + + if (isSingle) + { + count += this.WriteSingle(value.M11); + count += this.WriteSingle(value.M21); + count += this.WriteSingle(value.M31); + + count += this.WriteSingle(value.M12); + count += this.WriteSingle(value.M22); + count += this.WriteSingle(value.M32); + + count += this.WriteSingle(value.M13); + count += this.WriteSingle(value.M23); + count += this.WriteSingle(value.M33); + } + else + { + count += this.WriteFix16(value.M11); + count += this.WriteFix16(value.M21); + count += this.WriteFix16(value.M31); + + count += this.WriteFix16(value.M12); + count += this.WriteFix16(value.M22); + count += this.WriteFix16(value.M32); + + count += this.WriteFix16(value.M13); + count += this.WriteFix16(value.M23); + count += this.WriteFix16(value.M33); + } + + return count; + } + + /// + /// Writes a two dimensional matrix + /// + /// The matrix to write + /// True if the values are encoded as Single; false if encoded as Fix16 + /// The number of bytes written + public int WriteMatrix(in DenseMatrix value, bool isSingle) + { + int count = 0; + if (isSingle) + { + for (int y = 0; y < value.Rows; y++) + { + for (int x = 0; x < value.Columns; x++) + { + count += this.WriteSingle(value[x, y]); + } + } + } + else + { + for (int y = 0; y < value.Rows; y++) + { + for (int x = 0; x < value.Columns; x++) + { + count += this.WriteFix16(value[x, y]); + } + } + } + + return count; + } + + /// + /// Writes a two dimensional matrix + /// + /// The matrix to write + /// True if the values are encoded as Single; false if encoded as Fix16 + /// The number of bytes written + public int WriteMatrix(float[,] value, bool isSingle) + { + int count = 0; + + if (isSingle) + { + for (int y = 0; y < value.GetLength(1); y++) + { + for (int x = 0; x < value.GetLength(0); x++) + { + count += this.WriteSingle(value[x, y]); + } + } + } + else + { + for (int y = 0; y < value.GetLength(1); y++) + { + for (int x = 0; x < value.GetLength(0); x++) + { + count += this.WriteFix16(value[x, y]); + } + } + } + + return count; + } + + /// + /// Writes a one dimensional matrix + /// + /// The matrix to write + /// True if the values are encoded as Single; false if encoded as Fix16 + /// The number of bytes written + public int WriteMatrix(Vector3 value, bool isSingle) + { + int count = 0; + if (isSingle) + { + count += this.WriteSingle(value.X); + count += this.WriteSingle(value.Y); + count += this.WriteSingle(value.Z); + } + else + { + count += this.WriteFix16(value.X); + count += this.WriteFix16(value.Y); + count += this.WriteFix16(value.Z); + } + + return count; + } + + /// + /// Writes a one dimensional matrix + /// + /// The matrix to write + /// True if the values are encoded as Single; false if encoded as Fix16 + /// The number of bytes written + public int WriteMatrix(float[] value, bool isSingle) + { + int count = 0; + for (int i = 0; i < value.Length; i++) + { + if (isSingle) + { + count += this.WriteSingle(value[i]); + } + else + { + count += this.WriteFix16(value[i]); + } + } + + return count; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.MultiProcessElement.cs b/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.MultiProcessElement.cs new file mode 100644 index 0000000..5125369 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.MultiProcessElement.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Provides methods to write ICC data types + /// + internal sealed partial class IccDataWriter + { + /// + /// Writes a + /// + /// The element to write + /// The number of bytes written + public int WriteMultiProcessElement(IccMultiProcessElement value) + { + int count = this.WriteUInt32((uint)value.Signature); + count += this.WriteUInt16((ushort)value.InputChannelCount); + count += this.WriteUInt16((ushort)value.OutputChannelCount); + + switch (value.Signature) + { + case IccMultiProcessElementSignature.CurveSet: + return count + this.WriteCurveSetProcessElement((IccCurveSetProcessElement)value); + case IccMultiProcessElementSignature.Matrix: + return count + this.WriteMatrixProcessElement((IccMatrixProcessElement)value); + case IccMultiProcessElementSignature.Clut: + return count + this.WriteClutProcessElement((IccClutProcessElement)value); + + case IccMultiProcessElementSignature.BAcs: + case IccMultiProcessElementSignature.EAcs: + return count + this.WriteEmpty(8); + + default: + throw new InvalidIccProfileException($"Invalid MultiProcessElement type of {value.Signature}"); + } + } + + /// + /// Writes a CurveSet + /// + /// The element to write + /// The number of bytes written + public int WriteCurveSetProcessElement(IccCurveSetProcessElement value) + { + int count = 0; + foreach (IccOneDimensionalCurve curve in value.Curves) + { + count += this.WriteOneDimensionalCurve(curve); + count += this.WritePadding(); + } + + return count; + } + + /// + /// Writes a Matrix + /// + /// The element to write + /// The number of bytes written + public int WriteMatrixProcessElement(IccMatrixProcessElement value) + { + return this.WriteMatrix(value.MatrixIxO, true) + + this.WriteMatrix(value.MatrixOx1, true); + } + + /// + /// Writes a CLUT + /// + /// The element to write + /// The number of bytes written + public int WriteClutProcessElement(IccClutProcessElement value) + { + return this.WriteClut(value.ClutValue); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.NonPrimitives.cs b/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.NonPrimitives.cs new file mode 100644 index 0000000..54c21d7 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.NonPrimitives.cs @@ -0,0 +1,131 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Provides methods to write ICC data types + /// + internal sealed partial class IccDataWriter + { + /// + /// Writes a DateTime + /// + /// The value to write + /// the number of bytes written + public int WriteDateTime(DateTime value) + { + return this.WriteUInt16((ushort)value.Year) + + this.WriteUInt16((ushort)value.Month) + + this.WriteUInt16((ushort)value.Day) + + this.WriteUInt16((ushort)value.Hour) + + this.WriteUInt16((ushort)value.Minute) + + this.WriteUInt16((ushort)value.Second); + } + + /// + /// Writes an ICC profile version number + /// + /// The value to write + /// the number of bytes written + public int WriteVersionNumber(in IccVersion value) + { + int major = Numerics.Clamp(value.Major, 0, byte.MaxValue); + int minor = Numerics.Clamp(value.Minor, 0, 15); + int bugfix = Numerics.Clamp(value.Patch, 0, 15); + + int version = (major << 24) | (minor << 20) | (bugfix << 16); + return this.WriteInt32(version); + } + + /// + /// Writes an XYZ number + /// + /// The value to write + /// the number of bytes written + public int WriteXyzNumber(Vector3 value) + { + return this.WriteFix16(value.X) + + this.WriteFix16(value.Y) + + this.WriteFix16(value.Z); + } + + /// + /// Writes a profile ID + /// + /// The value to write + /// the number of bytes written + public int WriteProfileId(in IccProfileId value) + { + return this.WriteUInt32(value.Part1) + + this.WriteUInt32(value.Part2) + + this.WriteUInt32(value.Part3) + + this.WriteUInt32(value.Part4); + } + + /// + /// Writes a position number + /// + /// The value to write + /// the number of bytes written + public int WritePositionNumber(in IccPositionNumber value) + { + return this.WriteUInt32(value.Offset) + + this.WriteUInt32(value.Size); + } + + /// + /// Writes a response number + /// + /// The value to write + /// the number of bytes written + public int WriteResponseNumber(in IccResponseNumber value) + { + return this.WriteUInt16(value.DeviceCode) + + this.WriteFix16(value.MeasurementValue); + } + + /// + /// Writes a named color + /// + /// The value to write + /// the number of bytes written + public int WriteNamedColor(in IccNamedColor value) + { + return this.WriteAsciiString(value.Name, 32, true) + + this.WriteArray(value.PcsCoordinates) + + this.WriteArray(value.DeviceCoordinates); + } + + /// + /// Writes a profile description + /// + /// The value to write + /// the number of bytes written + public int WriteProfileDescription(in IccProfileDescription value) + { + return this.WriteUInt32(value.DeviceManufacturer) + + this.WriteUInt32(value.DeviceModel) + + this.WriteInt64((long)value.DeviceAttributes) + + this.WriteUInt32((uint)value.TechnologyInformation) + + this.WriteTagDataEntryHeader(IccTypeSignature.MultiLocalizedUnicode) + + this.WriteMultiLocalizedUnicodeTagDataEntry(new IccMultiLocalizedUnicodeTagDataEntry(value.DeviceManufacturerInfo)) + + this.WriteTagDataEntryHeader(IccTypeSignature.MultiLocalizedUnicode) + + this.WriteMultiLocalizedUnicodeTagDataEntry(new IccMultiLocalizedUnicodeTagDataEntry(value.DeviceModelInfo)); + } + + /// + /// Writes a screening channel + /// + /// The value to write + /// the number of bytes written + public int WriteScreeningChannel(in IccScreeningChannel value) + { + return this.WriteFix16(value.Frequency) + + this.WriteFix16(value.Angle) + + this.WriteInt32((int)value.SpotShape); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.Primitives.cs b/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.Primitives.cs new file mode 100644 index 0000000..ce4e06e --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.Primitives.cs @@ -0,0 +1,245 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Text; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Provides methods to write ICC data types + /// + internal sealed partial class IccDataWriter + { + /// + /// Writes a byte + /// + /// The value to write + /// the number of bytes written + public int WriteByte(byte value) + { + this.dataStream.WriteByte(value); + return 1; + } + + /// + /// Writes an ushort + /// + /// The value to write + /// the number of bytes written + public unsafe int WriteUInt16(ushort value) + { + return this.WriteBytes((byte*)&value, 2); + } + + /// + /// Writes a short + /// + /// The value to write + /// the number of bytes written + public unsafe int WriteInt16(short value) + { + return this.WriteBytes((byte*)&value, 2); + } + + /// + /// Writes an uint + /// + /// The value to write + /// the number of bytes written + public unsafe int WriteUInt32(uint value) + { + return this.WriteBytes((byte*)&value, 4); + } + + /// + /// Writes an int + /// + /// The value to write + /// the number of bytes written + public unsafe int WriteInt32(int value) + { + return this.WriteBytes((byte*)&value, 4); + } + + /// + /// Writes an ulong + /// + /// The value to write + /// the number of bytes written + public unsafe int WriteUInt64(ulong value) + { + return this.WriteBytes((byte*)&value, 8); + } + + /// + /// Writes a long + /// + /// The value to write + /// the number of bytes written + public unsafe int WriteInt64(long value) + { + return this.WriteBytes((byte*)&value, 8); + } + + /// + /// Writes a float + /// + /// The value to write + /// the number of bytes written + public unsafe int WriteSingle(float value) + { + return this.WriteBytes((byte*)&value, 4); + } + + /// + /// Writes a double + /// + /// The value to write + /// the number of bytes written + public unsafe int WriteDouble(double value) + { + return this.WriteBytes((byte*)&value, 8); + } + + /// + /// Writes a signed 32bit number with 1 sign bit, 15 value bits and 16 fractional bits + /// + /// The value to write + /// the number of bytes written + public int WriteFix16(double value) + { + const double Max = short.MaxValue + (65535d / 65536d); + const double Min = short.MinValue; + + value = Numerics.Clamp(value, Min, Max); + value *= 65536d; + + return this.WriteInt32((int)Math.Round(value, MidpointRounding.AwayFromZero)); + } + + /// + /// Writes an unsigned 32bit number with 16 value bits and 16 fractional bits + /// + /// The value to write + /// the number of bytes written + public int WriteUFix16(double value) + { + const double Max = ushort.MaxValue + (65535d / 65536d); + const double Min = ushort.MinValue; + + value = Numerics.Clamp(value, Min, Max); + value *= 65536d; + + return this.WriteUInt32((uint)Math.Round(value, MidpointRounding.AwayFromZero)); + } + + /// + /// Writes an unsigned 16bit number with 1 value bit and 15 fractional bits + /// + /// The value to write + /// the number of bytes written + public int WriteU1Fix15(double value) + { + const double Max = 1 + (32767d / 32768d); + const double Min = 0; + + value = Numerics.Clamp(value, Min, Max); + value *= 32768d; + + return this.WriteUInt16((ushort)Math.Round(value, MidpointRounding.AwayFromZero)); + } + + /// + /// Writes an unsigned 16bit number with 8 value bits and 8 fractional bits + /// + /// The value to write + /// the number of bytes written + public int WriteUFix8(double value) + { + const double Max = byte.MaxValue + (255d / 256d); + const double Min = byte.MinValue; + + value = Numerics.Clamp(value, Min, Max); + value *= 256d; + + return this.WriteUInt16((ushort)Math.Round(value, MidpointRounding.AwayFromZero)); + } + + /// + /// Writes an ASCII encoded string + /// + /// the string to write + /// the number of bytes written + public int WriteAsciiString(string value) + { + if (string.IsNullOrEmpty(value)) + { + return 0; + } + + byte[] data = Encoding.ASCII.GetBytes(value); + this.dataStream.Write(data, 0, data.Length); + return data.Length; + } + + /// + /// Writes an ASCII encoded string resizes it to the given length + /// + /// The string to write + /// The desired length of the string (including potential null terminator) + /// If True, there will be a \0 added at the end + /// the number of bytes written + public int WriteAsciiString(string value, int length, bool ensureNullTerminator) + { + if (length == 0) + { + return 0; + } + + Guard.MustBeGreaterThan(length, 0, nameof(length)); + + if (value is null) + { + value = string.Empty; + } + + byte paddingChar = (byte)' '; + int lengthAdjust = 0; + + if (ensureNullTerminator) + { + paddingChar = 0; + lengthAdjust = 1; + } + + value = value[..Math.Min(length - lengthAdjust, value.Length)]; + + byte[] textData = Encoding.ASCII.GetBytes(value); + int actualLength = Math.Min(length - lengthAdjust, textData.Length); + this.dataStream.Write(textData, 0, actualLength); + for (int i = 0; i < length - actualLength; i++) + { + this.dataStream.WriteByte(paddingChar); + } + + return length; + } + + /// + /// Writes an UTF-16 big-endian encoded string + /// + /// the string to write + /// the number of bytes written + public int WriteUnicodeString(string value) + { + if (string.IsNullOrEmpty(value)) + { + return 0; + } + + byte[] data = Encoding.BigEndianUnicode.GetBytes(value); + this.dataStream.Write(data, 0, data.Length); + return data.Length; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.TagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.TagDataEntry.cs new file mode 100644 index 0000000..d1f3ee7 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.TagDataEntry.cs @@ -0,0 +1,922 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System.Linq; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Provides methods to write ICC data types + /// + internal sealed partial class IccDataWriter + { + /// + /// Writes a tag data entry + /// + /// The entry to write + /// The table entry for the written data entry + /// The number of bytes written (excluding padding) + public int WriteTagDataEntry(IccTagDataEntry data, out IccTagTableEntry table) + { + uint offset = (uint)this.dataStream.Position; + int count = this.WriteTagDataEntry(data); + this.WritePadding(); + table = new IccTagTableEntry(data.TagSignature, offset, (uint)count); + return count; + } + + /// + /// Writes a tag data entry (without padding) + /// + /// The entry to write + /// The number of bytes written + public int WriteTagDataEntry(IccTagDataEntry entry) + { + int count = this.WriteTagDataEntryHeader(entry.Signature); + + count += entry.Signature switch + { + IccTypeSignature.Chromaticity => this.WriteChromaticityTagDataEntry((IccChromaticityTagDataEntry)entry), + IccTypeSignature.ColorantOrder => this.WriteColorantOrderTagDataEntry((IccColorantOrderTagDataEntry)entry), + IccTypeSignature.ColorantTable => this.WriteColorantTableTagDataEntry((IccColorantTableTagDataEntry)entry), + IccTypeSignature.Curve => this.WriteCurveTagDataEntry((IccCurveTagDataEntry)entry), + IccTypeSignature.Data => this.WriteDataTagDataEntry((IccDataTagDataEntry)entry), + IccTypeSignature.DateTime => this.WriteDateTimeTagDataEntry((IccDateTimeTagDataEntry)entry), + IccTypeSignature.Lut16 => this.WriteLut16TagDataEntry((IccLut16TagDataEntry)entry), + IccTypeSignature.Lut8 => this.WriteLut8TagDataEntry((IccLut8TagDataEntry)entry), + IccTypeSignature.LutAToB => this.WriteLutAtoBTagDataEntry((IccLutAToBTagDataEntry)entry), + IccTypeSignature.LutBToA => this.WriteLutBtoATagDataEntry((IccLutBToATagDataEntry)entry), + IccTypeSignature.Measurement => this.WriteMeasurementTagDataEntry((IccMeasurementTagDataEntry)entry), + IccTypeSignature.MultiLocalizedUnicode => this.WriteMultiLocalizedUnicodeTagDataEntry((IccMultiLocalizedUnicodeTagDataEntry)entry), + IccTypeSignature.MultiProcessElements => this.WriteMultiProcessElementsTagDataEntry((IccMultiProcessElementsTagDataEntry)entry), + IccTypeSignature.NamedColor2 => this.WriteNamedColor2TagDataEntry((IccNamedColor2TagDataEntry)entry), + IccTypeSignature.ParametricCurve => this.WriteParametricCurveTagDataEntry((IccParametricCurveTagDataEntry)entry), + IccTypeSignature.ProfileSequenceDesc => this.WriteProfileSequenceDescTagDataEntry((IccProfileSequenceDescTagDataEntry)entry), + IccTypeSignature.ProfileSequenceIdentifier => this.WriteProfileSequenceIdentifierTagDataEntry((IccProfileSequenceIdentifierTagDataEntry)entry), + IccTypeSignature.ResponseCurveSet16 => this.WriteResponseCurveSet16TagDataEntry((IccResponseCurveSet16TagDataEntry)entry), + IccTypeSignature.S15Fixed16Array => this.WriteFix16ArrayTagDataEntry((IccFix16ArrayTagDataEntry)entry), + IccTypeSignature.Signature => this.WriteSignatureTagDataEntry((IccSignatureTagDataEntry)entry), + IccTypeSignature.Text => this.WriteTextTagDataEntry((IccTextTagDataEntry)entry), + IccTypeSignature.U16Fixed16Array => this.WriteUFix16ArrayTagDataEntry((IccUFix16ArrayTagDataEntry)entry), + IccTypeSignature.UInt16Array => this.WriteUInt16ArrayTagDataEntry((IccUInt16ArrayTagDataEntry)entry), + IccTypeSignature.UInt32Array => this.WriteUInt32ArrayTagDataEntry((IccUInt32ArrayTagDataEntry)entry), + IccTypeSignature.UInt64Array => this.WriteUInt64ArrayTagDataEntry((IccUInt64ArrayTagDataEntry)entry), + IccTypeSignature.UInt8Array => this.WriteUInt8ArrayTagDataEntry((IccUInt8ArrayTagDataEntry)entry), + IccTypeSignature.ViewingConditions => this.WriteViewingConditionsTagDataEntry((IccViewingConditionsTagDataEntry)entry), + IccTypeSignature.Xyz => this.WriteXyzTagDataEntry((IccXyzTagDataEntry)entry), + + // V2 Types: + IccTypeSignature.TextDescription => this.WriteTextDescriptionTagDataEntry((IccTextDescriptionTagDataEntry)entry), + IccTypeSignature.CrdInfo => this.WriteCrdInfoTagDataEntry((IccCrdInfoTagDataEntry)entry), + IccTypeSignature.Screening => this.WriteScreeningTagDataEntry((IccScreeningTagDataEntry)entry), + IccTypeSignature.UcrBg => this.WriteUcrBgTagDataEntry((IccUcrBgTagDataEntry)entry), + + // Unsupported or unknown + _ => this.WriteUnknownTagDataEntry(entry as IccUnknownTagDataEntry), + }; + return count; + } + + /// + /// Writes the header of a + /// + /// The signature of the entry + /// The number of bytes written + public int WriteTagDataEntryHeader(IccTypeSignature signature) + => this.WriteUInt32((uint)signature) + this.WriteEmpty(4); + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteUnknownTagDataEntry(IccUnknownTagDataEntry value) => this.WriteArray(value.Data); + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteChromaticityTagDataEntry(IccChromaticityTagDataEntry value) + { + int count = this.WriteUInt16((ushort)value.ChannelCount); + count += this.WriteUInt16((ushort)value.ColorantType); + + for (int i = 0; i < value.ChannelCount; i++) + { + count += this.WriteUFix16(value.ChannelValues[i][0]); + count += this.WriteUFix16(value.ChannelValues[i][1]); + } + + return count; + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteColorantOrderTagDataEntry(IccColorantOrderTagDataEntry value) + => this.WriteUInt32((uint)value.ColorantNumber.Length) + + this.WriteArray(value.ColorantNumber); + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteColorantTableTagDataEntry(IccColorantTableTagDataEntry value) + { + int count = this.WriteUInt32((uint)value.ColorantData.Length); + + for (int i = 0; i < value.ColorantData.Length; i++) + { + ref IccColorantTableEntry colorant = ref value.ColorantData[i]; + + count += this.WriteAsciiString(colorant.Name, 32, true); + count += this.WriteUInt16(colorant.Pcs1); + count += this.WriteUInt16(colorant.Pcs2); + count += this.WriteUInt16(colorant.Pcs3); + } + + return count; + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteCurveTagDataEntry(IccCurveTagDataEntry value) + { + int count = 0; + + if (value.IsIdentityResponse) + { + count += this.WriteUInt32(0); + } + else if (value.IsGamma) + { + count += this.WriteUInt32(1); + count += this.WriteUFix8(value.Gamma); + } + else + { + count += this.WriteUInt32((uint)value.CurveData.Length); + for (int i = 0; i < value.CurveData.Length; i++) + { + count += this.WriteUInt16((ushort)Numerics.Clamp((value.CurveData[i] * ushort.MaxValue) + 0.5F, 0, ushort.MaxValue)); + } + } + + return count; + + // TODO: Page 48: If the input is PCSXYZ, 1+(32 767/32 768) shall be mapped to the value 1,0. If the output is PCSXYZ, the value 1,0 shall be mapped to 1+(32 767/32 768). + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteDataTagDataEntry(IccDataTagDataEntry value) + => this.WriteEmpty(3) + + this.WriteByte((byte)(value.IsAscii ? 0x01 : 0x00)) + + this.WriteArray(value.Data); + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteDateTimeTagDataEntry(IccDateTimeTagDataEntry value) => this.WriteDateTime(value.Value); + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteLut16TagDataEntry(IccLut16TagDataEntry value) + { + int count = this.WriteByte((byte)value.InputValues.Length); + count += this.WriteByte((byte)value.OutputValues.Length); + count += this.WriteByte(value.ClutValues.GridPointCount[0]); + count += this.WriteEmpty(1); + + count += this.WriteMatrix(value.Matrix, false); + + count += this.WriteUInt16((ushort)value.InputValues[0].Values.Length); + count += this.WriteUInt16((ushort)value.OutputValues[0].Values.Length); + + foreach (IccLut lut in value.InputValues) + { + count += this.WriteLut16(lut); + } + + count += this.WriteClut16(value.ClutValues); + + foreach (IccLut lut in value.OutputValues) + { + count += this.WriteLut16(lut); + } + + return count; + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteLut8TagDataEntry(IccLut8TagDataEntry value) + { + int count = this.WriteByte((byte)value.InputChannelCount); + count += this.WriteByte((byte)value.OutputChannelCount); + count += this.WriteByte((byte)value.ClutValues.OutputChannelCount); + count += this.WriteEmpty(1); + + count += this.WriteMatrix(value.Matrix, false); + + foreach (IccLut lut in value.InputValues) + { + count += this.WriteLut8(lut); + } + + count += this.WriteClut8(value.ClutValues); + + foreach (IccLut lut in value.OutputValues) + { + count += this.WriteLut8(lut); + } + + return count; + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteLutAtoBTagDataEntry(IccLutAToBTagDataEntry value) + { + long start = this.dataStream.Position - 8; // 8 is the tag header size + + int count = this.WriteByte((byte)value.InputChannelCount); + count += this.WriteByte((byte)value.OutputChannelCount); + count += this.WriteEmpty(2); + + long bCurveOffset = 0; + long matrixOffset = 0; + long mCurveOffset = 0; + long clutOffset = 0; + long aCurveOffset = 0; + + // Jump over offset values + long offsetpos = this.dataStream.Position; + this.dataStream.Position += 5 * 4; + + if (value.CurveB != null) + { + bCurveOffset = this.dataStream.Position; + count += this.WriteCurves(value.CurveB); + count += this.WritePadding(); + } + + if (value.Matrix3x1 != null && value.Matrix3x3 != null) + { + matrixOffset = this.dataStream.Position; + count += this.WriteMatrix(value.Matrix3x3.Value, false); + count += this.WriteMatrix(value.Matrix3x1.Value, false); + count += this.WritePadding(); + } + + if (value.CurveM != null) + { + mCurveOffset = this.dataStream.Position; + count += this.WriteCurves(value.CurveM); + count += this.WritePadding(); + } + + if (value.ClutValues != null) + { + clutOffset = this.dataStream.Position; + count += this.WriteClut(value.ClutValues); + count += this.WritePadding(); + } + + if (value.CurveA != null) + { + aCurveOffset = this.dataStream.Position; + count += this.WriteCurves(value.CurveA); + count += this.WritePadding(); + } + + // Set offset values + long lpos = this.dataStream.Position; + this.dataStream.Position = offsetpos; + + if (bCurveOffset != 0) + { + bCurveOffset -= start; + } + + if (matrixOffset != 0) + { + matrixOffset -= start; + } + + if (mCurveOffset != 0) + { + mCurveOffset -= start; + } + + if (clutOffset != 0) + { + clutOffset -= start; + } + + if (aCurveOffset != 0) + { + aCurveOffset -= start; + } + + count += this.WriteUInt32((uint)bCurveOffset); + count += this.WriteUInt32((uint)matrixOffset); + count += this.WriteUInt32((uint)mCurveOffset); + count += this.WriteUInt32((uint)clutOffset); + count += this.WriteUInt32((uint)aCurveOffset); + + this.dataStream.Position = lpos; + return count; + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteLutBtoATagDataEntry(IccLutBToATagDataEntry value) + { + long start = this.dataStream.Position - 8; // 8 is the tag header size + + int count = this.WriteByte((byte)value.InputChannelCount); + count += this.WriteByte((byte)value.OutputChannelCount); + count += this.WriteEmpty(2); + + long bCurveOffset = 0; + long matrixOffset = 0; + long mCurveOffset = 0; + long clutOffset = 0; + long aCurveOffset = 0; + + // Jump over offset values + long offsetpos = this.dataStream.Position; + this.dataStream.Position += 5 * 4; + + if (value.CurveB != null) + { + bCurveOffset = this.dataStream.Position; + count += this.WriteCurves(value.CurveB); + count += this.WritePadding(); + } + + if (value.Matrix3x1 != null && value.Matrix3x3 != null) + { + matrixOffset = this.dataStream.Position; + count += this.WriteMatrix(value.Matrix3x3.Value, false); + count += this.WriteMatrix(value.Matrix3x1.Value, false); + count += this.WritePadding(); + } + + if (value.CurveM != null) + { + mCurveOffset = this.dataStream.Position; + count += this.WriteCurves(value.CurveM); + count += this.WritePadding(); + } + + if (value.ClutValues != null) + { + clutOffset = this.dataStream.Position; + count += this.WriteClut(value.ClutValues); + count += this.WritePadding(); + } + + if (value.CurveA != null) + { + aCurveOffset = this.dataStream.Position; + count += this.WriteCurves(value.CurveA); + count += this.WritePadding(); + } + + // Set offset values + long lpos = this.dataStream.Position; + this.dataStream.Position = offsetpos; + + if (bCurveOffset != 0) + { + bCurveOffset -= start; + } + + if (matrixOffset != 0) + { + matrixOffset -= start; + } + + if (mCurveOffset != 0) + { + mCurveOffset -= start; + } + + if (clutOffset != 0) + { + clutOffset -= start; + } + + if (aCurveOffset != 0) + { + aCurveOffset -= start; + } + + count += this.WriteUInt32((uint)bCurveOffset); + count += this.WriteUInt32((uint)matrixOffset); + count += this.WriteUInt32((uint)mCurveOffset); + count += this.WriteUInt32((uint)clutOffset); + count += this.WriteUInt32((uint)aCurveOffset); + + this.dataStream.Position = lpos; + return count; + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteMeasurementTagDataEntry(IccMeasurementTagDataEntry value) + => this.WriteUInt32((uint)value.Observer) + + this.WriteXyzNumber(value.XyzBacking) + + this.WriteUInt32((uint)value.Geometry) + + this.WriteUFix16(value.Flare) + + this.WriteUInt32((uint)value.Illuminant); + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteMultiLocalizedUnicodeTagDataEntry(IccMultiLocalizedUnicodeTagDataEntry value) + { + long start = this.dataStream.Position - 8; // 8 is the tag header size + + int cultureCount = value.Texts.Length; + + int count = this.WriteUInt32((uint)cultureCount); + count += this.WriteUInt32(12); // One record has always 12 bytes size + + // Jump over position table + long tpos = this.dataStream.Position; + this.dataStream.Position += cultureCount * 12; + + // TODO: Investigate cost of Linq GroupBy + IGrouping[] texts = value.Texts.GroupBy(t => t.Text).ToArray(); + + uint[] offset = new uint[texts.Length]; + int[] lengths = new int[texts.Length]; + + for (int i = 0; i < texts.Length; i++) + { + offset[i] = (uint)(this.dataStream.Position - start); + count += lengths[i] = this.WriteUnicodeString(texts[i].Key); + } + + // Write position table + long lpos = this.dataStream.Position; + this.dataStream.Position = tpos; + for (int i = 0; i < texts.Length; i++) + { + foreach (IccLocalizedString localizedString in texts[i]) + { + string cultureName = localizedString.Culture.Name; + if (string.IsNullOrEmpty(cultureName)) + { + count += this.WriteAsciiString("xx", 2, false); + count += this.WriteAsciiString("\0\0", 2, false); + } + else if (cultureName.Contains('-')) + { + string[] code = cultureName.Split('-'); + count += this.WriteAsciiString(code[0].ToLower(localizedString.Culture), 2, false); + count += this.WriteAsciiString(code[1].ToUpper(localizedString.Culture), 2, false); + } + else + { + count += this.WriteAsciiString(cultureName, 2, false); + count += this.WriteAsciiString("\0\0", 2, false); + } + + count += this.WriteUInt32((uint)lengths[i]); + count += this.WriteUInt32(offset[i]); + } + } + + this.dataStream.Position = lpos; + return count; + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteMultiProcessElementsTagDataEntry(IccMultiProcessElementsTagDataEntry value) + { + long start = this.dataStream.Position - 8; // 8 is the tag header size + + int count = this.WriteUInt16((ushort)value.InputChannelCount); + count += this.WriteUInt16((ushort)value.OutputChannelCount); + count += this.WriteUInt32((uint)value.Data.Length); + + // Jump over position table + long tpos = this.dataStream.Position; + this.dataStream.Position += value.Data.Length * 8; + + IccPositionNumber[] posTable = new IccPositionNumber[value.Data.Length]; + for (int i = 0; i < value.Data.Length; i++) + { + uint offset = (uint)(this.dataStream.Position - start); + int size = this.WriteMultiProcessElement(value.Data[i]); + count += this.WritePadding(); + posTable[i] = new IccPositionNumber(offset, (uint)size); + count += size; + } + + // Write position table + long lpos = this.dataStream.Position; + this.dataStream.Position = tpos; + foreach (IccPositionNumber pos in posTable) + { + count += this.WritePositionNumber(pos); + } + + this.dataStream.Position = lpos; + return count; + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteNamedColor2TagDataEntry(IccNamedColor2TagDataEntry value) + { + int count = this.WriteInt32(value.VendorFlags) + + this.WriteUInt32((uint)value.Colors.Length) + + this.WriteUInt32((uint)value.CoordinateCount) + + this.WriteAsciiString(value.Prefix, 32, true) + + this.WriteAsciiString(value.Suffix, 32, true); + + foreach (IccNamedColor color in value.Colors) + { + count += this.WriteNamedColor(color); + } + + return count; + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteParametricCurveTagDataEntry(IccParametricCurveTagDataEntry value) => this.WriteParametricCurve(value.Curve); + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteProfileSequenceDescTagDataEntry(IccProfileSequenceDescTagDataEntry value) + { + int count = this.WriteUInt32((uint)value.Descriptions.Length); + + for (int i = 0; i < value.Descriptions.Length; i++) + { + ref IccProfileDescription desc = ref value.Descriptions[i]; + + count += this.WriteProfileDescription(desc); + } + + return count; + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteProfileSequenceIdentifierTagDataEntry(IccProfileSequenceIdentifierTagDataEntry value) + { + long start = this.dataStream.Position - 8; // 8 is the tag header size + int length = value.Data.Length; + + int count = this.WriteUInt32((uint)length); + + // Jump over position table + long tablePosition = this.dataStream.Position; + this.dataStream.Position += length * 8; + IccPositionNumber[] table = new IccPositionNumber[length]; + + for (int i = 0; i < length; i++) + { + ref IccProfileSequenceIdentifier sequenceIdentifier = ref value.Data[i]; + + uint offset = (uint)(this.dataStream.Position - start); + int size = this.WriteProfileId(sequenceIdentifier.Id); + size += this.WriteTagDataEntry(new IccMultiLocalizedUnicodeTagDataEntry(sequenceIdentifier.Description)); + size += this.WritePadding(); + table[i] = new IccPositionNumber(offset, (uint)size); + count += size; + } + + // Write position table + long lpos = this.dataStream.Position; + this.dataStream.Position = tablePosition; + foreach (IccPositionNumber pos in table) + { + count += this.WritePositionNumber(pos); + } + + this.dataStream.Position = lpos; + return count; + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteResponseCurveSet16TagDataEntry(IccResponseCurveSet16TagDataEntry value) + { + long start = this.dataStream.Position - 8; + + int count = this.WriteUInt16(value.ChannelCount); + count += this.WriteUInt16((ushort)value.Curves.Length); + + // Jump over position table + long tablePosition = this.dataStream.Position; + this.dataStream.Position += value.Curves.Length * 4; + + uint[] offset = new uint[value.Curves.Length]; + + for (int i = 0; i < value.Curves.Length; i++) + { + offset[i] = (uint)(this.dataStream.Position - start); + count += this.WriteResponseCurve(value.Curves[i]); + count += this.WritePadding(); + } + + // Write position table + long lpos = this.dataStream.Position; + this.dataStream.Position = tablePosition; + count += this.WriteArray(offset); + + this.dataStream.Position = lpos; + return count; + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteFix16ArrayTagDataEntry(IccFix16ArrayTagDataEntry value) + { + int count = 0; + for (int i = 0; i < value.Data.Length; i++) + { + count += this.WriteFix16(value.Data[i] * 256d); + } + + return count; + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteSignatureTagDataEntry(IccSignatureTagDataEntry value) => this.WriteAsciiString(value.SignatureData, 4, false); + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteTextTagDataEntry(IccTextTagDataEntry value) => this.WriteAsciiString(value.Text); + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteUFix16ArrayTagDataEntry(IccUFix16ArrayTagDataEntry value) + { + int count = 0; + for (int i = 0; i < value.Data.Length; i++) + { + count += this.WriteUFix16(value.Data[i]); + } + + return count; + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteUInt16ArrayTagDataEntry(IccUInt16ArrayTagDataEntry value) => this.WriteArray(value.Data); + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteUInt32ArrayTagDataEntry(IccUInt32ArrayTagDataEntry value) => this.WriteArray(value.Data); + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteUInt64ArrayTagDataEntry(IccUInt64ArrayTagDataEntry value) => this.WriteArray(value.Data); + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteUInt8ArrayTagDataEntry(IccUInt8ArrayTagDataEntry value) => this.WriteArray(value.Data); + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteViewingConditionsTagDataEntry(IccViewingConditionsTagDataEntry value) + => this.WriteXyzNumber(value.IlluminantXyz) + + this.WriteXyzNumber(value.SurroundXyz) + + this.WriteUInt32((uint)value.Illuminant); + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteXyzTagDataEntry(IccXyzTagDataEntry value) + { + int count = 0; + for (int i = 0; i < value.Data.Length; i++) + { + count += this.WriteXyzNumber(value.Data[i]); + } + + return count; + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteTextDescriptionTagDataEntry(IccTextDescriptionTagDataEntry value) + { + int size, count = 0; + + if (value.Ascii is null) + { + count += this.WriteUInt32(0); + } + else + { + this.dataStream.Position += 4; + count += size = this.WriteAsciiString(value.Ascii + '\0'); + this.dataStream.Position -= size + 4; + count += this.WriteUInt32((uint)size); + this.dataStream.Position += size; + } + + if (value.Unicode is null) + { + count += this.WriteUInt32(0); + count += this.WriteUInt32(0); + } + else + { + this.dataStream.Position += 8; + count += size = this.WriteUnicodeString(value.Unicode + '\0'); + this.dataStream.Position -= size + 8; + count += this.WriteUInt32(value.UnicodeLanguageCode); + count += this.WriteUInt32((uint)value.Unicode.Length + 1); + this.dataStream.Position += size; + } + + if (value.ScriptCode is null) + { + count += this.WriteUInt16(0); + count += this.WriteByte(0); + count += this.WriteEmpty(67); + } + else + { + this.dataStream.Position += 3; + count += size = this.WriteAsciiString(value.ScriptCode, 67, true); + this.dataStream.Position -= size + 3; + count += this.WriteUInt16(value.ScriptCodeCode); + count += this.WriteByte((byte)(value.ScriptCode.Length > 66 ? 67 : value.ScriptCode.Length + 1)); + this.dataStream.Position += size; + } + + return count; + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteCrdInfoTagDataEntry(IccCrdInfoTagDataEntry value) + { + int count = 0; + WriteString(value.PostScriptProductName); + WriteString(value.RenderingIntent0Crd); + WriteString(value.RenderingIntent1Crd); + WriteString(value.RenderingIntent2Crd); + WriteString(value.RenderingIntent3Crd); + + return count; + + void WriteString(string text) + { + int textLength; + if (string.IsNullOrEmpty(text)) + { + textLength = 0; + } + else + { + textLength = text.Length + 1; // + 1 for null terminator + } + + count += this.WriteUInt32((uint)textLength); + count += this.WriteAsciiString(text, textLength, true); + } + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteScreeningTagDataEntry(IccScreeningTagDataEntry value) + { + int count = 0; + + count += this.WriteInt32((int)value.Flags); + count += this.WriteUInt32((uint)value.Channels.Length); + for (int i = 0; i < value.Channels.Length; i++) + { + count += this.WriteScreeningChannel(value.Channels[i]); + } + + return count; + } + + /// + /// Writes a + /// + /// The entry to write + /// The number of bytes written + public int WriteUcrBgTagDataEntry(IccUcrBgTagDataEntry value) + { + int count = 0; + + count += this.WriteUInt32((uint)value.UcrCurve.Length); + for (int i = 0; i < value.UcrCurve.Length; i++) + { + count += this.WriteUInt16(value.UcrCurve[i]); + } + + count += this.WriteUInt32((uint)value.BgCurve.Length); + for (int i = 0; i < value.BgCurve.Length; i++) + { + count += this.WriteUInt16(value.BgCurve[i]); + } + + count += this.WriteAsciiString(value.Description + '\0'); + + return count; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.cs b/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.cs new file mode 100644 index 0000000..e65d731 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.cs @@ -0,0 +1,246 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Provides methods to write ICC data types + /// + internal sealed partial class IccDataWriter : IDisposable + { + /// + /// The underlying stream where the data is written to + /// + private readonly MemoryStream dataStream; + + /// + /// To detect redundant calls + /// + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + public IccDataWriter() + { + this.dataStream = new MemoryStream(); + } + + /// + /// Gets the currently written length in bytes + /// + public uint Length => (uint)this.dataStream.Length; + + /// + /// Gets the written data bytes + /// + /// The written data + public byte[] GetData() + { + return this.dataStream.ToArray(); + } + + /// + /// Sets the writing position to the given value + /// + /// The new index position + public void SetIndex(int index) + { + this.dataStream.Position = index; + } + + /// + /// Writes a byte array + /// + /// The array to write + /// The number of bytes written + public int WriteArray(byte[] data) + { + this.dataStream.Write(data, 0, data.Length); + return data.Length; + } + + /// + /// Writes a ushort array + /// + /// The array to write + /// The number of bytes written + public int WriteArray(ushort[] data) + { + for (int i = 0; i < data.Length; i++) + { + this.WriteUInt16(data[i]); + } + + return data.Length * 2; + } + + /// + /// Writes a short array + /// + /// The array to write + /// The number of bytes written + public int WriteArray(short[] data) + { + for (int i = 0; i < data.Length; i++) + { + this.WriteInt16(data[i]); + } + + return data.Length * 2; + } + + /// + /// Writes a uint array + /// + /// The array to write + /// The number of bytes written + public int WriteArray(uint[] data) + { + for (int i = 0; i < data.Length; i++) + { + this.WriteUInt32(data[i]); + } + + return data.Length * 4; + } + + /// + /// Writes an int array + /// + /// The array to write + /// The number of bytes written + public int WriteArray(int[] data) + { + for (int i = 0; i < data.Length; i++) + { + this.WriteInt32(data[i]); + } + + return data.Length * 4; + } + + /// + /// Writes a ulong array + /// + /// The array to write + /// The number of bytes written + public int WriteArray(ulong[] data) + { + for (int i = 0; i < data.Length; i++) + { + this.WriteUInt64(data[i]); + } + + return data.Length * 8; + } + + /// + /// Write a number of empty bytes + /// + /// The number of bytes to write + /// The number of bytes written + public int WriteEmpty(int length) + { + for (int i = 0; i < length; i++) + { + this.dataStream.WriteByte(0); + } + + return length; + } + + /// + /// Writes empty bytes to a 4-byte margin + /// + /// The number of bytes written + public int WritePadding() + { + int p = 4 - ((int)this.dataStream.Position % 4); + return this.WriteEmpty(p >= 4 ? 0 : p); + } + + /// + public void Dispose() + { + this.Dispose(true); + } + + /// + /// Writes given bytes from pointer + /// + /// Pointer to the bytes to write + /// The number of bytes to write + /// The number of bytes written + private unsafe int WriteBytes(byte* data, int length) + { + if (BitConverter.IsLittleEndian) + { + for (int i = length - 1; i >= 0; i--) + { + this.dataStream.WriteByte(data[i]); + } + } + else + { + this.WriteBytesDirect(data, length); + } + + return length; + } + + /// + /// Writes given bytes from pointer ignoring endianness + /// + /// Pointer to the bytes to write + /// The number of bytes to write + /// The number of bytes written + private unsafe int WriteBytesDirect(byte* data, int length) + { + for (int i = 0; i < length; i++) + { + this.dataStream.WriteByte(data[i]); + } + + return length; + } + + /// + /// Writes curve data + /// + /// The curves to write + /// The number of bytes written + private int WriteCurves(IccTagDataEntry[] curves) + { + int count = 0; + foreach (IccTagDataEntry curve in curves) + { + if (curve.Signature != IccTypeSignature.Curve && curve.Signature != IccTypeSignature.ParametricCurve) + { + throw new InvalidIccProfileException($"Curve has to be either \"{nameof(IccTypeSignature)}.{nameof(IccTypeSignature.Curve)}\" or" + + $" \"{nameof(IccTypeSignature)}.{nameof(IccTypeSignature.ParametricCurve)}\" for LutAToB- and LutBToA-TagDataEntries"); + } + + count += this.WriteTagDataEntry(curve); + count += this.WritePadding(); + } + + return count; + } + + private void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + this.dataStream?.Dispose(); + } + + this.isDisposed = true; + } + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccClutDataType.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccClutDataType.cs new file mode 100644 index 0000000..1279294 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccClutDataType.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Color lookup table data type + /// + internal enum IccClutDataType + { + /// + /// 32bit floating point + /// + Float, + + /// + /// 8bit unsigned integer (byte) + /// + UInt8, + + /// + /// 16bit unsigned integer (ushort) + /// + UInt16, + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccColorSpaceType.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccColorSpaceType.cs new file mode 100644 index 0000000..e4265a2 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccColorSpaceType.cs @@ -0,0 +1,135 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Color Space Type + /// + public enum IccColorSpaceType : uint + { + /// + /// CIE XYZ + /// + CieXyz = 0x58595A20, // XYZ + + /// + /// CIE Lab + /// + CieLab = 0x4C616220, // Lab + + /// + /// CIE Luv + /// + CieLuv = 0x4C757620, // Luv + + /// + /// YCbCr + /// + YCbCr = 0x59436272, // YCbr + + /// + /// CIE Yxy + /// + CieYxy = 0x59787920, // Yxy + + /// + /// RGB + /// + Rgb = 0x52474220, // RGB + + /// + /// Gray + /// + Gray = 0x47524159, // GRAY + + /// + /// HSV + /// + Hsv = 0x48535620, // HSV + + /// + /// HLS + /// + Hls = 0x484C5320, // HLS + + /// + /// CMYK + /// + Cmyk = 0x434D594B, // CMYK + + /// + /// CMY + /// + Cmy = 0x434D5920, // CMY + + /// + /// Generic 2 channel color + /// + Color2 = 0x32434C52, // 2CLR + + /// + /// Generic 3 channel color + /// + Color3 = 0x33434C52, // 3CLR + + /// + /// Generic 4 channel color + /// + Color4 = 0x34434C52, // 4CLR + + /// + /// Generic 5 channel color + /// + Color5 = 0x35434C52, // 5CLR + + /// + /// Generic 6 channel color + /// + Color6 = 0x36434C52, // 6CLR + + /// + /// Generic 7 channel color + /// + Color7 = 0x37434C52, // 7CLR + + /// + /// Generic 8 channel color + /// + Color8 = 0x38434C52, // 8CLR + + /// + /// Generic 9 channel color + /// + Color9 = 0x39434C52, // 9CLR + + /// + /// Generic 10 channel color + /// + Color10 = 0x41434C52, // ACLR + + /// + /// Generic 11 channel color + /// + Color11 = 0x42434C52, // BCLR + + /// + /// Generic 12 channel color + /// + Color12 = 0x43434C52, // CCLR + + /// + /// Generic 13 channel color + /// + Color13 = 0x44434C52, // DCLR + + /// + /// Generic 14 channel color + /// + Color14 = 0x45434C52, // ECLR + + /// + /// Generic 15 channel color + /// + Color15 = 0x46434C52, // FCLR + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccColorantEncoding.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccColorantEncoding.cs new file mode 100644 index 0000000..80cb13d --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccColorantEncoding.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Colorant Encoding + /// + internal enum IccColorantEncoding : ushort + { + /// + /// Unknown colorant encoding + /// + Unknown = 0x0000, + + /// + /// ITU-R BT.709-2 colorant encoding + /// + ItuRBt709_2 = 0x0001, + + /// + /// SMPTE RP145 colorant encoding + /// + SmpteRp145 = 0x0002, + + /// + /// EBU Tech.3213-E colorant encoding + /// + EbuTech3213E = 0x0003, + + /// + /// P22 colorant encoding + /// + P22 = 0x0004, + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccCurveMeasurementEncodings.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccCurveMeasurementEncodings.cs new file mode 100644 index 0000000..2e48353 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccCurveMeasurementEncodings.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Curve Measurement Encodings + /// + internal enum IccCurveMeasurementEncodings : uint + { + /// + /// ISO 5-3 densitometer response. This is the accepted standard for + /// reflection densitometers for measuring photographic color prints + /// + StatusA = 0x53746141, // StaA + + /// + /// ISO 5-3 densitometer response which is the accepted standard in + /// Europe for color reflection densitometers + /// + StatusE = 0x53746145, // StaE + + /// + /// ISO 5-3 densitometer response commonly referred to as narrow band + /// or interference-type response. + /// + StatusI = 0x53746149, // StaI + + /// + /// ISO 5-3 wide band color reflection densitometer response which is + /// the accepted standard in the United States for color reflection densitometers + /// + StatusT = 0x53746154, // StaT + + /// + /// ISO 5-3 densitometer response for measuring color negatives + /// + StatusM = 0x5374614D, // StaM + + /// + /// DIN 16536-2 densitometer response, with no polarizing filter + /// + DinE = 0x434E2020, // DN + + /// + /// DIN 16536-2 densitometer response, with polarizing filter + /// + DinEPol = 0x434E2050, // DNP + + /// + /// DIN 16536-2 narrow band densitometer response, with no polarizing filter + /// + DinI = 0x434E4E20, // DNN + + /// + /// DIN 16536-2 narrow band densitometer response, with polarizing filter + /// + DinIPol = 0x434E4E50, // DNNP + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccCurveSegmentSignature.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccCurveSegmentSignature.cs new file mode 100644 index 0000000..3d192c6 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccCurveSegmentSignature.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Curve Segment Signature + /// + internal enum IccCurveSegmentSignature : uint + { + /// + /// Curve defined by a formula + /// + FormulaCurve = 0x70617266, // parf + + /// + /// Curve defined by multiple segments + /// + SampledCurve = 0x73616D66, // samf + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccDataType.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccDataType.cs new file mode 100644 index 0000000..c8ede59 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccDataType.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Enumerates the basic data types as defined in ICC.1:2010 version 4.3.0.0 + /// Section 4.2 to 4.15 + /// + internal enum IccDataType + { + /// + /// A 12-byte value representation of the time and date + /// + DateTime, + + /// + /// A single-precision 32-bit floating-point as specified in IEEE 754, + /// excluding un-normalized s, infinities, and not a "" (NaN) values + /// + Float32, + + /// + /// Positions of some data elements are indicated using a position offset with the data element's size. + /// + Position, + + /// + /// An 8-byte value, used to associate a normalized device code with a measurement value + /// + Response16, + + /// + /// A fixed signed 4-byte (32-bit) quantity which has 16 fractional bits + /// + S15Fixed16, + + /// + /// A fixed unsigned 4-byte (32-bit) quantity having 16 fractional bits + /// + U16Fixed16, + + /// + /// A fixed unsigned 2-byte (16-bit) quantity having15 fractional bits + /// + U1Fixed15, + + /// + /// A fixed unsigned 2-byte (16-bit) quantity having 8 fractional bits + /// + U8Fixed8, + + /// + /// An unsigned 2-byte (16-bit) integer + /// + UInt16, + + /// + /// An unsigned 4-byte (32-bit) integer + /// + UInt32, + + /// + /// An unsigned 8-byte (64-bit) integer + /// + UInt64, + + /// + /// An unsigned 1-byte (8-bit) integer + /// + UInt8, + + /// + /// A set of three fixed signed 4-byte (32-bit) quantities used to encode CIEXYZ, nCIEXYZ, and PCSXYZ tristimulus values + /// + Xyz, + + /// + /// Alpha-numeric values, and other input and output codes, shall conform to the American Standard Code for + /// Information Interchange (ASCII) specified in ISO/IEC 646. + /// + Ascii + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccDeviceAttribute.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccDeviceAttribute.cs new file mode 100644 index 0000000..295b758 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccDeviceAttribute.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Device attributes. Can be combined with a logical OR + /// The least-significant 32 bits are defined by the ICC, + /// the rest can be used for vendor specific values + /// + [Flags] + public enum IccDeviceAttribute : long + { + /// + /// Opacity transparent + /// + OpacityTransparent = 1 << 0, + + /// + /// Opacity reflective + /// + OpacityReflective = 0, + + /// + /// Reflectivity matte + /// + ReflectivityMatte = 1 << 1, + + /// + /// Reflectivity glossy + /// + ReflectivityGlossy = 0, + + /// + /// Polarity negative + /// + PolarityNegative = 1 << 2, + + /// + /// Polarity positive + /// + PolarityPositive = 0, + + /// + /// Chroma black and white + /// + ChromaBlackWhite = 1 << 3, + + /// + /// Chroma color + /// + ChromaColor = 0, + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccFormulaCurveType.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccFormulaCurveType.cs new file mode 100644 index 0000000..1f70f60 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccFormulaCurveType.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Formula curve segment type + /// + internal enum IccFormulaCurveType : ushort + { + /// + /// Type 1: Y = (a * X + b)^γ + c + /// + Type1 = 0, + + /// + /// Type 2: Y = a * log10 (b * X^γ + c) + d + /// + Type2 = 1, + + /// + /// Type 3: Y = a * b^(c * X + d) + e + /// + Type3 = 2 + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccMeasurementGeometry.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccMeasurementGeometry.cs new file mode 100644 index 0000000..20d47e1 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccMeasurementGeometry.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Measurement Geometry + /// + internal enum IccMeasurementGeometry : uint + { + /// + /// Unknown geometry + /// + Unknown = 0, + + /// + /// Geometry of 0°:45° or 45°:0° + /// + Degree0To45Or45To0 = 1, + + /// + /// Geometry of 0°:d or d:0° + /// + Degree0ToDOrDTo0 = 2, + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccMultiProcessElementSignature.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccMultiProcessElementSignature.cs new file mode 100644 index 0000000..40ecb23 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccMultiProcessElementSignature.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Multi process element signature + /// + internal enum IccMultiProcessElementSignature : uint + { + /// + /// Set of curves + /// + CurveSet = 0x6D666C74, // cvst + + /// + /// Matrix transformation + /// + Matrix = 0x6D617466, // matf + + /// + /// Color lookup table + /// + Clut = 0x636C7574, // clut + + /// + /// Reserved for future expansion. Do not use! + /// + BAcs = 0x62414353, // bACS + + /// + /// Reserved for future expansion. Do not use! + /// + EAcs = 0x65414353, // eACS + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccParametricCurveType.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccParametricCurveType.cs new file mode 100644 index 0000000..9ee37e6 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccParametricCurveType.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Formula curve segment type + /// + internal enum IccParametricCurveType : ushort + { + /// + /// Type 1: Y = X^g + /// + Type1 = 0, + + /// + /// CIE 122-1996: + /// For X >= -b/a: Y =(a * X + b)^g + /// For X $lt; -b/a: Y = 0 + /// + Cie122_1996 = 1, + + /// + /// IEC 61966-3: + /// For X >= -b/a: Y =(a * X + b)^g + c + /// For X $lt; -b/a: Y = c + /// + Iec61966_3 = 2, + + /// + /// IEC 61966-2-1 (sRGB): + /// For X >= d: Y =(a * X + b)^g + /// For X $lt; d: Y = c * X + /// + SRgb = 3, + + /// + /// Type 5: + /// For X >= d: Y =(a * X + b)^g + c + /// For X $lt; d: Y = c * X + f + /// + Type5 = 4, + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccPrimaryPlatformType.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccPrimaryPlatformType.cs new file mode 100644 index 0000000..a98d251 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccPrimaryPlatformType.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Enumerates the primary platform/operating system framework for which the profile was created + /// + public enum IccPrimaryPlatformType : uint + { + /// + /// No platform identified + /// + NotIdentified = 0x00000000, + + /// + /// Apple Computer, Inc. + /// + AppleComputerInc = 0x4150504C, // APPL + + /// + /// Microsoft Corporation + /// + MicrosoftCorporation = 0x4D534654, // MSFT + + /// + /// Silicon Graphics, Inc. + /// + SiliconGraphicsInc = 0x53474920, // SGI + + /// + /// Sun Microsystems, Inc. + /// + SunMicrosystemsInc = 0x53554E57, // SUNW + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccProfileClass.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccProfileClass.cs new file mode 100644 index 0000000..ff08d57 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccProfileClass.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Profile Class Name + /// + public enum IccProfileClass : uint + { + /// + /// Input profiles are generally used with devices such as scanners and + /// digital cameras. The types of profiles available for use as Input + /// profiles are N-component LUT-based, Three-component matrix-based, + /// and monochrome. + /// + InputDevice = 0x73636E72, // scnr + + /// + /// This class of profiles represents display devices such as monitors. + /// The types of profiles available for use as Display profiles are + /// N-component LUT-based, Three-component matrix-based, and monochrome. + /// + DisplayDevice = 0x6D6E7472, // mntr + + /// + /// Output profiles are used to support devices such as printers and + /// film recorders. The types of profiles available for use as Output + /// profiles are N-component LUT-based and Monochrome. + /// + OutputDevice = 0x70727472, // prtr + + /// + /// This profile contains a pre-evaluated transform that cannot be undone, + /// which represents a one-way link or connection between devices. It does + /// not represent any device model nor can it be embedded into images. + /// + DeviceLink = 0x6C696E6B, // link + + /// + /// This profile provides the relevant information to perform a transformation + /// between color encodings and the PCS. This type of profile is based on + /// modeling rather than device measurement or characterization data. + /// ColorSpace profiles may be embedded in images. + /// + ColorSpace = 0x73706163, // spac + + /// + /// This profile represents abstract transforms and does not represent any + /// device model. Color transformations using Abstract profiles are performed + /// from PCS to PCS. Abstract profiles cannot be embedded in images. + /// + Abstract = 0x61627374, // abst + + /// + /// NamedColor profiles can be thought of as sibling profiles to device profiles. + /// For a given device there would be one or more device profiles to handle + /// process color conversions and one or more named color profiles to handle + /// named colors. + /// + NamedColor = 0x6E6D636C, // nmcl + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccProfileFlag.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccProfileFlag.cs new file mode 100644 index 0000000..ab2a376 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccProfileFlag.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Profile flags. Can be combined with a logical OR. + /// The least-significant 16 bits are reserved for the ICC, + /// the rest can be used for vendor specific values + /// + [Flags] + public enum IccProfileFlag + { + /// + /// No flags (equivalent to NotEmbedded and Independent) + /// + None = 0, + + /// + /// Profile is embedded within another file + /// + Embedded = 1 << 0, + + /// + /// Profile is not embedded within another file + /// + NotEmbedded = 0, + + /// + /// Profile cannot be used independently of the embedded color data + /// + NotIndependent = 1 << 1, + + /// + /// Profile can be used independently of the embedded color data + /// + Independent = 0, + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccProfileTag.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccProfileTag.cs new file mode 100644 index 0000000..3ad22dd --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccProfileTag.cs @@ -0,0 +1,361 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Enumerates the ICC Profile Tags as defined in ICC.1:2010 version 4.3.0.0 + /// Section 9 + /// + /// Each tag value represent the size of the tag in the profile. + /// + /// + public enum IccProfileTag : uint + { + /// + /// Unknown tag + /// + Unknown, + + /// + /// A2B0 - This tag defines a color transform from Device, Color Encoding or PCS, to PCS, or a color transform + /// from Device 1 to Device 2, using lookup table tag element structures + /// + AToB0 = 0x41324230, + + /// + /// A2B2 - This tag describes the color transform from Device or Color Encoding to PCS using lookup table tag element structures + /// + AToB1 = 0x41324231, + + /// + /// A2B2 - This tag describes the color transform from Device or Color Encoding to PCS using lookup table tag element structures + /// + AToB2 = 0x41324232, + + /// + /// bXYZ - This tag contains the third column in the matrix used in matrix/TRC transforms. + /// + BlueMatrixColumn = 0x6258595A, + + /// + /// bTRC - This tag contains the blue channel tone reproduction curve. The first element represents no colorant (white) or + /// phosphor (black) and the last element represents 100 % colorant (blue) or 100 % phosphor (blue). + /// + BlueTrc = 0x62545243, + + /// + /// B2A0 - This tag defines a color transform from PCS to Device or Color Encoding using the lookup table tag element structures + /// + BToA0 = 0x42324130, + + /// + /// B2A1 - This tag defines a color transform from PCS to Device or Color Encoding using the lookup table tag element structures. + /// + BToA1 = 0x42324131, + + /// + /// B2A2 - This tag defines a color transform from PCS to Device or Color Encoding using the lookup table tag element structures. + /// + BToA2 = 0x42324132, + + /// + /// B2D0 - This tag defines a color transform from PCS to Device. It supports float32Number-encoded input range, output range and transform, and + /// provides a means to override the BToA0 tag. + /// + BToD0 = 0x42324430, + + /// + /// B2D1 - This tag defines a color transform from PCS to Device. It supports float32Number-encoded input range, output range and transform, and + /// provides a means to override the BToA1 tag. + /// + BToD1 = 0x42324431, + + /// + /// B2D2 - This tag defines a color transform from PCS to Device. It supports float32Number-encoded input range, output range and transform, and + /// provides a means to override the BToA2 tag. + /// + BToD2 = 0x42324432, + + /// + /// B2D3 - This tag defines a color transform from PCS to Device. It supports float32Number-encoded input range, output range and transform, and + /// provides a means to override the BToA1 tag. + /// + BToD3 = 0x42324433, + + /// + /// calt - This tag contains the profile calibration date and time. This allows applications and utilities to verify if this profile matches a + /// vendor's profile and how recently calibration has been performed. + /// + CalibrationDateTime = 0x63616C74, + + /// + /// targ - This tag contains the name of the registered characterization data set, or it contains the measurement + /// data for a characterization target. + /// + CharTarget = 0x74617267, + + /// + /// chad - This tag contains a matrix, which shall be invertible, and which converts an nCIEXYZ color, measured using the actual illumination + /// conditions and relative to the actual adopted white, to an nCIEXYZ color relative to the PCS adopted white + /// + ChromaticAdaptation = 0x63686164, + + /// + /// chrm - This tag contains the type and the data of the phosphor/colorant chromaticity set used. + /// + Chromaticity = 0x6368726D, + + /// + /// clro - This tag specifies the laydown order of colorants. + /// + ColorantOrder = 0x636C726F, + + /// + /// clrt + /// + ColorantTable = 0x636C7274, + + /// + /// clot - This tag identifies the colorants used in the profile by a unique name and set of PCSXYZ or PCSLAB values. + /// When used in DeviceLink profiles only the PCSLAB values shall be permitted. + /// + ColorantTableOut = 0x636C6F74, + + /// + /// ciis - This tag indicates the image state of PCS colorimetry produced using the colorimetric intent transforms. + /// + ColorimetricIntentImageStat = 0x63696973, + + /// + /// cprt - This tag contains the text copyright information for the profile. + /// + Copyright = 0x63707274, + + /// + /// crdi - Removed in V4 + /// + CrdInfo = 0x63726469, + + /// + /// data - Removed in V4 + /// + Data = 0x64617461, + + /// + /// dtim - Removed in V4 + /// + DateTime = 0x6474696D, + + /// + /// dmnd - This tag describes the structure containing invariant and localizable + /// versions of the device manufacturer for display + /// + DeviceManufacturerDescription = 0x646D6E64, + + /// + /// dmdd - This tag describes the structure containing invariant and localizable + /// versions of the device model for display. + /// + DeviceModelDescription = 0x646D6464, + + /// + /// devs - Removed in V4 + /// + DeviceSettings = 0x64657673, + + /// + /// D2B0 - This tag defines a color transform from Device to PCS. It supports float32Number-encoded + /// input range, output range and transform, and provides a means to override the AToB0 tag + /// + DToB0 = 0x44324230, + + /// + /// D2B1 - This tag defines a color transform from Device to PCS. It supports float32Number-encoded + /// input range, output range and transform, and provides a means to override the AToB1 tag + /// + DToB1 = 0x44324230, + + /// + /// D2B2 - This tag defines a color transform from Device to PCS. It supports float32Number-encoded + /// input range, output range and transform, and provides a means to override the AToB1 tag + /// + DToB2 = 0x44324230, + + /// + /// D2B3 - This tag defines a color transform from Device to PCS. It supports float32Number-encoded + /// input range, output range and transform, and provides a means to override the AToB1 tag + /// + DToB3 = 0x44324230, + + /// + /// gamt - This tag provides a table in which PCS values are the input and a single + /// output value for each input value is the output. If the output value is 0, the PCS color is in-gamut. + /// If the output is non-zero, the PCS color is out-of-gamut + /// + Gamut = 0x67616D74, + + /// + /// kTRC - This tag contains the grey tone reproduction curve. The tone reproduction curve provides the necessary + /// information to convert between a single device channel and the PCSXYZ or PCSLAB encoding. + /// + GrayTrc = 0x6b545243, + + /// + /// gXYZ - This tag contains the second column in the matrix, which is used in matrix/TRC transforms. + /// + GreenMatrixColumn = 0x6758595A, + + /// + /// gTRC - This tag contains the green channel tone reproduction curve. The first element represents no + /// colorant (white) or phosphor (black) and the last element represents 100 % colorant (green) or 100 % phosphor (green). + /// + GreenTrc = 0x67545243, + + /// + /// lumi - This tag contains the absolute luminance of emissive devices in candelas per square meter as described by the Y channel. + /// + Luminance = 0x6C756d69, + + /// + /// meas - This tag describes the alternative measurement specification, such as a D65 illuminant instead of the default D50. + /// + Measurement = 0x6D656173, + + /// + /// bkpt - Removed in V4 + /// + MediaBlackPoint = 0x626B7074, + + /// + /// wtpt - This tag, which is used for generating the ICC-absolute colorimetric intent, specifies the chromatically + /// adapted nCIEXYZ tristimulus values of the media white point. + /// + MediaWhitePoint = 0x77747074, + + /// + /// ncol - OBSOLETE, use + /// + NamedColor = 0x6E636f6C, + + /// + /// ncl2 - This tag contains the named color information providing a PCS and optional device representation + /// for a list of named colors. + /// + NamedColor2 = 0x6E636C32, + + /// + /// resp - This tag describes the structure containing a description of the device response for which the profile is intended. + /// + OutputResponse = 0x72657370, + + /// + /// rig0 - There is only one standard reference medium gamut, as defined in ISO 12640-3 + /// + PerceptualRenderingIntentGamut = 0x72696730, + + /// + /// pre0 - This tag contains the preview transformation from PCS to device space and back to the PCS. + /// + Preview0 = 0x70726530, + + /// + /// pre1 - This tag defines the preview transformation from PCS to device space and back to the PCS. + /// + Preview1 = 0x70726531, + + /// + /// pre2 - This tag contains the preview transformation from PCS to device space and back to the PCS. + /// + Preview2 = 0x70726532, + + /// + /// desc - This tag describes the structure containing invariant and localizable versions of the profile + /// description for display. + /// + ProfileDescription = 0x64657363, + + /// + /// pseq - This tag describes the structure containing a description of the profile sequence from source to + /// destination, typically used with the DeviceLink profile. + /// + ProfileSequenceDescription = 0x70736571, + + /// + /// psd0 - Removed in V4 + /// + PostScript2Crd0 = 0x70736430, + + /// + /// psd1 - Removed in V4 + /// + PostScript2Crd1 = 0x70736431, + + /// + /// psd2 - Removed in V4 + /// + PostScript2Crd2 = 0x70736432, + + /// + /// psd3 - Removed in V4 + /// + PostScript2Crd3 = 0x70736433, + + /// + /// ps2s - Removed in V4 + /// + PostScript2Csa = 0x70733273, + + /// + /// psd2i- Removed in V4 + /// + PostScript2RenderingIntent = 0x70733269, + + /// + /// rXYZ - This tag contains the first column in the matrix, which is used in matrix/TRC transforms. + /// + RedMatrixColumn = 0x7258595A, + + /// + /// This tag contains the red channel tone reproduction curve. The first element represents no colorant + /// (white) or phosphor (black) and the last element represents 100 % colorant (red) or 100 % phosphor (red). + /// + RedTrc = 0x72545243, + + /// + /// rig2 - There is only one standard reference medium gamut, as defined in ISO 12640-3. + /// + SaturationRenderingIntentGamut = 0x72696732, + + /// + /// scrd - Removed in V4 + /// + ScreeningDescription = 0x73637264, + + /// + /// scrn - Removed in V4 + /// + Screening = 0x7363726E, + + /// + /// tech - The device technology signature + /// + Technology = 0x74656368, + + /// + /// bfd - Removed in V4 + /// + UcrBgSpecification = 0x62666420, + + /// + /// vued - This tag describes the structure containing invariant and localizable + /// versions of the viewing conditions. + /// + ViewingCondDescription = 0x76756564, + + /// + /// view - This tag defines the viewing conditions parameters + /// + ViewingConditions = 0x76696577, + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccRenderingIntent.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccRenderingIntent.cs new file mode 100644 index 0000000..d48bb01 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccRenderingIntent.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Rendering intent + /// + public enum IccRenderingIntent : uint + { + /// + /// In perceptual transforms the PCS values represent hypothetical + /// measurements of a color reproduction on the reference reflective + /// medium. By extension, for the perceptual intent, the PCS represents + /// the appearance of that reproduction as viewed in the reference viewing + /// environment by a human observer adapted to that environment. The exact + /// color rendering of the perceptual intent is vendor specific. + /// + Perceptual = 0, + + /// + /// Transformations for this intent shall re-scale the in-gamut, + /// chromatically adapted tristimulus values such that the white + /// point of the actual medium is mapped to the PCS white point + /// (for either input or output) + /// + MediaRelativeColorimetric = 1, + + /// + /// The exact color rendering of the saturation intent is vendor + /// specific and involves compromises such as trading off + /// preservation of hue in order to preserve the vividness of pure colors. + /// + Saturation = 2, + + /// + /// Transformations for this intent shall leave the chromatically + /// adapted nCIEXYZ tristimulus values of the in-gamut colors unchanged. + /// + AbsoluteColorimetric = 3, + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccScreeningFlag.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccScreeningFlag.cs new file mode 100644 index 0000000..9bc88d7 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccScreeningFlag.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Screening flags. Can be combined with a logical OR. + /// + [Flags] + internal enum IccScreeningFlag + { + /// + /// No flags (equivalent to NotDefaultScreens and UnitLinesPerCm) + /// + None = 0, + + /// + /// Use printer default screens + /// + DefaultScreens = 1 << 0, + + /// + /// Don't use printer default screens + /// + NotDefaultScreens = 0, + + /// + /// Frequency units in Lines/Inch + /// + UnitLinesPerInch = 1 << 1, + + /// + /// Frequency units in Lines/cm + /// + UnitLinesPerCm = 0, + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccScreeningSpotType.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccScreeningSpotType.cs new file mode 100644 index 0000000..37ad24f --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccScreeningSpotType.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Enumerates the screening spot types + /// + internal enum IccScreeningSpotType : int + { + /// + /// Unknown spot type + /// + Unknown = 0, + + /// + /// Default printer spot type + /// + PrinterDefault = 1, + + /// + /// Round stop type + /// + Round = 2, + + /// + /// Diamond spot type + /// + Diamond = 3, + + /// + /// Ellipse spot type + /// + Ellipse = 4, + + /// + /// Line spot type + /// + Line = 5, + + /// + /// Square spot type + /// + Square = 6, + + /// + /// Cross spot type + /// + Cross = 7, + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccSignatureName.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccSignatureName.cs new file mode 100644 index 0000000..e6945f1 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccSignatureName.cs @@ -0,0 +1,175 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Signature Name + /// + internal enum IccSignatureName : uint + { + /// + /// Unknown signature + /// + Unknown = 0, + + /// + /// Scene Colorimetry Estimates + /// + SceneColorimetryEstimates = 0x73636F65, // scoe + + /// + /// Scene Appearance Estimates + /// + SceneAppearanceEstimates = 0x73617065, // sape + + /// + /// Focal Plane Colorimetry Estimates + /// + FocalPlaneColorimetryEstimates = 0x66706365, // fpce + + /// + /// Reflection Hardcopy Original Colorimetry + /// + ReflectionHardcopyOriginalColorimetry = 0x72686F63, // rhoc + + /// + /// Reflection Print Output Colorimetry + /// + ReflectionPrintOutputColorimetry = 0x72706F63, // rpoc + + /// + /// Perceptual Reference Medium Gamut + /// + PerceptualReferenceMediumGamut = 0x70726D67, // prmg + + /// + /// Film Scanner + /// + FilmScanner = 0x6673636E, // fscn + + /// + /// Digital Camera + /// + DigitalCamera = 0x6463616D, // dcam + + /// + /// Reflective Scanner + /// + ReflectiveScanner = 0x7273636E, // rscn + + /// + /// InkJet Printer + /// + InkJetPrinter = 0x696A6574, // ijet + + /// + /// Thermal Wax Printer + /// + ThermalWaxPrinter = 0x74776178, // twax + + /// + /// Electrophotographic Printer + /// + ElectrophotographicPrinter = 0x6570686F, // epho + + /// + /// Electrostatic Printer + /// + ElectrostaticPrinter = 0x65737461, // esta + + /// + /// Dye Sublimation Printer + /// + DyeSublimationPrinter = 0x64737562, // dsub + + /// + /// Photographic Paper Printer + /// + PhotographicPaperPrinter = 0x7270686F, // rpho + + /// + /// Film Writer + /// + FilmWriter = 0x6670726E, // fprn + + /// + /// Video Monitor + /// + VideoMonitor = 0x7669646D, // vidm + + /// + /// Video Camera + /// + VideoCamera = 0x76696463, // vidc + + /// + /// Projection Television + /// + ProjectionTelevision = 0x706A7476, // pjtv + + /// + /// Cathode Ray Tube Display + /// + CathodeRayTubeDisplay = 0x43525420, // CRT + + /// + /// Passive Matrix Display + /// + PassiveMatrixDisplay = 0x504D4420, // PMD + + /// + /// Active Matrix Display + /// + ActiveMatrixDisplay = 0x414D4420, // AMD + + /// + /// Photo CD + /// + PhotoCD = 0x4B504344, // KPCD + + /// + /// Photographic Image Setter + /// + PhotographicImageSetter = 0x696D6773, // imgs + + /// + /// Gravure + /// + Gravure = 0x67726176, // grav + + /// + /// Offset Lithography + /// + OffsetLithography = 0x6F666673, // offs + + /// + /// Silkscreen + /// + Silkscreen = 0x73696C6B, // silk + + /// + /// Flexography + /// + Flexography = 0x666C6578, // flex + + /// + /// Motion Picture Film Scanner + /// + MotionPictureFilmScanner = 0x6D706673, // mpfs + + /// + /// Motion Picture Film Recorder + /// + MotionPictureFilmRecorder = 0x6D706672, // mpfr + + /// + /// Digital Motion Picture Camera + /// + DigitalMotionPictureCamera = 0x646D7063, // dmpc + + /// + /// Digital Cinema Projector + /// + DigitalCinemaProjector = 0x64636A70, // dcpj + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccStandardIlluminant.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccStandardIlluminant.cs new file mode 100644 index 0000000..0dcedc4 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccStandardIlluminant.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Standard Illuminant + /// + internal enum IccStandardIlluminant : uint + { + /// + /// Unknown illuminant + /// + Unknown = 0, + + /// + /// D50 illuminant + /// + D50 = 1, + + /// + /// D65 illuminant + /// + D65 = 2, + + /// + /// D93 illuminant + /// + D93 = 3, + + /// + /// F2 illuminant + /// + F2 = 4, + + /// + /// D55 illuminant + /// + D55 = 5, + + /// + /// A illuminant + /// + A = 6, + + /// + /// D50 illuminant + /// + EquiPowerE = 7, + + /// + /// F8 illuminant + /// + F8 = 8, + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccStandardObserver.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccStandardObserver.cs new file mode 100644 index 0000000..97e9278 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccStandardObserver.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Standard Observer + /// + internal enum IccStandardObserver : uint + { + /// + /// Unknown observer + /// + Unknown = 0, + + /// + /// CIE 1931 observer + /// + Cie1931Observer = 1, + + /// + /// CIE 1964 observer + /// + Cie1964Observer = 2, + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Enums/IccTypeSignature.cs b/ImageSharp/Metadata/Profiles/ICC/Enums/IccTypeSignature.cs new file mode 100644 index 0000000..47b83d3 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Enums/IccTypeSignature.cs @@ -0,0 +1,270 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Type Signature + /// + public enum IccTypeSignature : uint + { + /// + /// Unknown type signature + /// + Unknown, + + /// + /// The chromaticity tag type provides basic chromaticity data and type of + /// phosphors or colorants of a monitor to applications and utilities + /// + Chromaticity = 0x6368726D, + + /// + /// This is an optional tag which specifies the laydown order in which colorants + /// will be printed on an n-colorant device. The laydown order may be the same + /// as the channel generation order listed in the colorantTableTag or the channel + /// order of a color encoding type such as CMYK, in which case this tag is not + /// needed. When this is not the case (for example, ink-towers sometimes use + /// the order KCMY), this tag may be used to specify the laydown order of the + /// colorants + /// + ColorantOrder = 0x636c726f, + + /// + /// The purpose of this tag is to identify the colorants used in the profile + /// by a unique name and set of PCSXYZ or PCSLAB values to give the colorant + /// an unambiguous value. The first colorant listed is the colorant of the + /// first device channel of a LUT tag. The second colorant listed is the + /// colorant of the second device channel of a LUT tag, and so on + /// + ColorantTable = 0x636c7274, + + /// + /// The curveType embodies a one-dimensional function which maps an input + /// value in the domain of the function to an output value in the range + /// of the function + /// + Curve = 0x63757276, + + /// + /// The dataType is a simple data structure that contains either 7-bit ASCII + /// or binary data + /// + Data = 0x64617461, + + /// + /// Date and time defined by 6 unsigned 16bit integers + /// (year, month, day, hour, minute, second) + /// + DateTime = 0x6474696D, + + /// + /// This structure represents a color transform using tables with 16-bit + /// precision. This type contains four processing elements: a 3 × 3 matrix + /// (which shall be the identity matrix unless the input color space is + /// PCSXYZ), a set of one-dimensional input tables, a multi-dimensional + /// lookup table, and a set of one-dimensional output tables + /// + Lut16 = 0x6D667432, + + /// + /// This structure represents a color transform using tables of 8-bit + /// precision. This type contains four processing elements: a 3 × 3 matrix + /// (which shall be the identity matrix unless the input color space is + /// PCSXYZ), a set of one-dimensional input tables, a multi-dimensional + /// lookup table, and a set of one-dimensional output tables. + /// + Lut8 = 0x6D667431, + + /// + /// This structure represents a color transform. The type contains up + /// to five processing elements which are stored in the AToBTag tag + /// in the following order: a set of one-dimensional curves, a 3 × 3 + /// matrix with offset terms, a set of one-dimensional curves, a + /// multi-dimensional lookup table, and a set of one-dimensional + /// output curves + /// + LutAToB = 0x6D414220, + + /// + /// This structure represents a color transform. The type contains + /// up to five processing elements which are stored in the BToATag + /// in the following order: a set of one-dimensional curves, a 3 × 3 + /// matrix with offset terms, a set of one-dimensional curves, a + /// multi-dimensional lookup table, and a set of one-dimensional curves. + /// + LutBToA = 0x6D424120, + + /// + /// This information refers only to the internal + /// profile data and is meant to provide profile makers an alternative + /// to the default measurement specifications + /// + Measurement = 0x6D656173, + + /// + /// This tag structure contains a set of records each referencing a + /// multilingual Unicode string associated with a profile. Each string + /// is referenced in a separate record with the information about what + /// language and region the string is for. + /// + MultiLocalizedUnicode = 0x6D6C7563, + + /// + /// This structure represents a color transform, containing a sequence + /// of processing elements. The processing elements contained in the + /// structure are defined in the structure itself, allowing for a flexible + /// structure. Currently supported processing elements are: a set of one + /// dimensional curves, a matrix with offset terms, and a multidimensional + /// lookup table (CLUT). Other processing element types may be added in + /// the future. Each type of processing element may be contained any + /// number of times in the structure. + /// + MultiProcessElements = 0x6D706574, + + /// + /// This type is a count value and array of structures that provide color + /// coordinates for color names. For each named color, a PCS and optional + /// device representation of the color are given. Both representations are + /// 16-bit values and PCS values shall be relative colorimetric. The device + /// representation corresponds to the header’s "data color space" field. + /// This representation should be consistent with the "number of device + /// coordinates" field in the namedColor2Type. If this field is 0, device + /// coordinates are not provided. The PCS representation corresponds to the + /// header's PCS field. The PCS representation is always provided. Color + /// names are fixed-length, 32-byte fields including null termination. In + /// order to maintain maximum portability, it is strongly recommended that + /// special characters of the 7-bit ASCII set not be used. + /// + NamedColor2 = 0x6E636C32, + + /// + /// This type describes a one-dimensional curve by specifying one of a + /// predefined set of functions using the parameters. + /// + ParametricCurve = 0x70617261, + + /// + /// This type is an array of structures, each of which contains information + /// from the header fields and tags from the original profiles which were + /// combined to create the final profile. The order of the structures is + /// the order in which the profiles were combined and includes a structure + /// for the final profile. This provides a description of the profile + /// sequence from source to destination, typically used with the DeviceLink + /// profile. + /// + ProfileSequenceDesc = 0x70736571, + + /// + /// This type is an array of structures, each of which contains information + /// for identification of a profile used in a sequence. + /// + ProfileSequenceIdentifier = 0x70736964, + + /// + /// The purpose of this tag type is to provide a mechanism to relate physical + /// colorant amounts with the normalized device codes produced by lut8Type, + /// lut16Type, lutAToBType, lutBToAType or multiProcessElementsType tags + /// so that corrections can be made for variation in the device without + /// having to produce a new profile. The mechanism can be used by applications + /// to allow users with relatively inexpensive and readily available + /// instrumentation to apply corrections to individual output color + /// channels in order to achieve consistent results. + /// + ResponseCurveSet16 = 0x72637332, + + /// + /// Array of signed floating point numbers with 1 sign bit, 15 value bits and 16 fractional bits + /// + S15Fixed16Array = 0x73663332, + + /// + /// The signatureType contains a 4-byte sequence. Sequences of less than four + /// characters are padded at the end with spaces. Typically this type is used + /// for registered tags that can be displayed on many development systems as + /// a sequence of four characters. + /// + Signature = 0x73696720, + + /// + /// Simple ASCII text + /// + Text = 0x74657874, + + /// + /// Array of unsigned floating point numbers with 16 value bits and 16 fractional bits + /// + U16Fixed16Array = 0x75663332, + + /// + /// Array of unsigned 16bit integers (ushort) + /// + UInt16Array = 0x75693136, + + /// + /// Array of unsigned 32bit integers (uint) + /// + UInt32Array = 0x75693332, + + /// + /// Array of unsigned 64bit integers (ulong) + /// + UInt64Array = 0x75693634, + + /// + /// Array of unsigned 8bit integers (byte) + /// + UInt8Array = 0x75693038, + + /// + /// This type represents a set of viewing condition parameters. + /// + ViewingConditions = 0x76696577, + + /// + /// 3 floating point values describing a XYZ color value + /// + Xyz = 0x58595A20, + + /// + /// REMOVED IN V4 - The textDescriptionType is a complex structure that contains three + /// types of text description structures: 7-bit ASCII, Unicode and ScriptCode. Since no + /// single standard method for specifying localizable character sets exists across + /// the major platform vendors, including all three provides access for the major + /// operating systems. The 7-bit ASCII description is to be an invariant, + /// nonlocalizable name for consistent reference. It is preferred that both the + /// Unicode and ScriptCode structures be properly localized. + /// + TextDescription = 0x64657363, + + /// + /// REMOVED IN V4 - This type contains the PostScript product name to which this + /// profile corresponds and the names of the companion CRDs + /// + CrdInfo = 0x63726469, + + /// + /// REMOVED IN V4 - The screeningType describes various screening parameters including + /// screen frequency, screening angle, and spot shape + /// + Screening = 0x7363726E, + + /// + /// REMOVED IN V4 - This type contains curves representing the under color removal and + /// black generation and a text string which is a general description of the method + /// used for the UCR and BG + /// + UcrBg = 0x62666420, + + /// + /// REMOVED IN V4 - This type is an array of structures each of which contains + /// platform-specific information about the settings of the device for which + /// this profile is valid. This type is not supported. + /// + DeviceSettings = 0x64657673, // not supported + + /// + /// REMOVED IN V2 - use instead. This type is not supported. + /// + NamedColor = 0x6E636F6C, // not supported + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Exceptions/InvalidIccProfileException.cs b/ImageSharp/Metadata/Profiles/ICC/Exceptions/InvalidIccProfileException.cs new file mode 100644 index 0000000..6d90658 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Exceptions/InvalidIccProfileException.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Represents an error that happened while reading or writing a corrupt/invalid ICC profile + /// + public class InvalidIccProfileException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error + public InvalidIccProfileException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error + /// The exception that is the cause of the current exception, or a null reference + /// (Nothing in Visual Basic) if no inner exception is specified + public InvalidIccProfileException(string message, Exception inner) + : base(message, inner) + { + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/ICC.1-2022-05.pdf b/ImageSharp/Metadata/Profiles/ICC/ICC.1-2022-05.pdf new file mode 100644 index 0000000..6c488c8 Binary files /dev/null and b/ImageSharp/Metadata/Profiles/ICC/ICC.1-2022-05.pdf differ diff --git a/ImageSharp/Metadata/Profiles/ICC/IccProfile.SRGB.cs b/ImageSharp/Metadata/Profiles/ICC/IccProfile.SRGB.cs new file mode 100644 index 0000000..3909b68 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/IccProfile.SRGB.cs @@ -0,0 +1,347 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Provides logic for identifying canonical IEC 61966-2-1 (sRGB) matrix-TRC ICC profiles, + /// distinguishing them from appearance or device-specific variants. + /// + public sealed partial class IccProfile + { + // sRGB v2 Preference + private static readonly IccProfileId StandardRgbV2 = new(0x3D0EB2DE, 0xAE9397BE, 0x9B6726CE, 0x8C0A43CE); + + // sRGB v4 Preference + private static readonly IccProfileId StandardRgbV4 = new(0x34562ABF, 0x994CCD06, 0x6D2C5721, 0xD0D68C5D); + + /// + /// Detects canonical sRGB matrix+TRC profiles quickly and safely. + /// Rules: + /// 1) Accept known IEC sRGB v2 and v4 by profile ID. + /// 2) Require RGB, PCS=XYZ, ICC v2 or v4, and no A2B*/B2A* LUTs. + /// 3) Require rTRC, gTRC, bTRC to exist and be identical by parameters or sampled shape. + /// 4) Accept if rXYZ/gXYZ/bXYZ already match the D50-adapted sRGB colorants within tolerance. + /// 5) If white point ≈ D65, adapt only the colorant columns to D50 using Bradford + /// via and then compare. + /// This rejects channel-swapped and appearance profiles while allowing real sRGB. + /// + /// + /// Reference D50-adapted sRGB colorants from Bruce Lindbloom: + /// + /// R=(0.4360747, 0.2225045, 0.0139322) + /// G=(0.3850649, 0.7168786, 0.0971045) + /// B=(0.1430804, 0.0606169, 0.7141733) + /// + internal bool IsCanonicalSrgbMatrixTrc() + { + IccProfileHeader h = this.Header; + + // Fast path for known IEC sRGB profile IDs + if (h.Id == StandardRgbV2 || h.Id == StandardRgbV4) + { + return true; + } + + // Header gating to avoid parsing work for obvious non-matches + if (h.FileSignature != "acsp") + { + return false; + } + + if (h.DataColorSpace != IccColorSpaceType.Rgb) + { + return false; + } + + if (h.ProfileConnectionSpace != IccColorSpaceType.CieXyz) + { + return false; + } + + if (h.Version.Major is not 2 and not 4) + { + return false; + } + + this.InitializeEntries(); + IccTagDataEntry[] entries = this.entries; + + // Reject device/display LUT profiles. We only accept matrix+TRC encodings. + if (Has(entries, IccProfileTag.AToB0) || Has(entries, IccProfileTag.AToB1) || Has(entries, IccProfileTag.AToB2) || + Has(entries, IccProfileTag.BToA0) || Has(entries, IccProfileTag.BToA1) || Has(entries, IccProfileTag.BToA2)) + { + return false; + } + + // Required matrix+TRC tags + if (!TryGetXyz(entries, IccProfileTag.MediaWhitePoint, out Vector3 wtpt)) + { + return false; + } + + if (!TryGetXyz(entries, IccProfileTag.RedMatrixColumn, out Vector3 rXYZ)) + { + return false; + } + + if (!TryGetXyz(entries, IccProfileTag.GreenMatrixColumn, out Vector3 gXYZ)) + { + return false; + } + + if (!TryGetXyz(entries, IccProfileTag.BlueMatrixColumn, out Vector3 bXYZ)) + { + return false; + } + + // TRCs must exist and be identical across channels. This filters many trick profiles. + if (!TryGetTrc(entries, IccProfileTag.RedTrc, out Trc tR)) + { + return false; + } + + if (!TryGetTrc(entries, IccProfileTag.GreenTrc, out Trc tG)) + { + return false; + } + + if (!TryGetTrc(entries, IccProfileTag.BlueTrc, out Trc tB)) + { + return false; + } + + if (!tR.Equals(tG) || !tR.Equals(tB)) + { + return false; + } + + // D50-adapted sRGB colorants (compare as columns: r,g,b), tight epsilon + const float eps = 2e-3F; + Vector3 rRef = new(0.4360747F, 0.2225045F, 0.0139322F); + Vector3 gRef = new(0.3850649F, 0.7168786F, 0.0971045F); + Vector3 bRef = new(0.1430804F, 0.0606169F, 0.7141733F); + + // First, accept if the stored colorants are already the D50 sRGB primaries. + // Many v2 sRGB profiles store D50-adapted colorants while declaring wtpt≈D65. + if (Near(rXYZ, rRef, eps) && Near(gXYZ, gRef, eps) && Near(bXYZ, bRef, eps)) + { + return true; + } + + // If the profile declares a D65 white, adapt the colorant columns to D50 and compare again. + // We never adapt when they already match, to avoid compounding rounding. + if (Near(wtpt, KnownIlluminants.D65.AsVector3Unsafe(), 2e-3F)) + { + CieXyz fromWp = new(wtpt); // Declared white + CieXyz toWp = KnownIlluminants.D50; // PCS white + Matrix4x4 matrix = KnownChromaticAdaptationMatrices.Bradford; + + rXYZ = VonKriesChromaticAdaptation.Transform(new CieXyz(rXYZ), (fromWp, toWp), matrix).AsVector3Unsafe(); + gXYZ = VonKriesChromaticAdaptation.Transform(new CieXyz(gXYZ), (fromWp, toWp), matrix).AsVector3Unsafe(); + bXYZ = VonKriesChromaticAdaptation.Transform(new CieXyz(bXYZ), (fromWp, toWp), matrix).AsVector3Unsafe(); + } + + // Require identity mapping of primaries, no permutation + if (!Near(rXYZ, rRef, eps) || !Near(gXYZ, gRef, eps) || !Near(bXYZ, bRef, eps)) + { + return false; + } + + return true; + + static bool Has(ReadOnlySpan span, IccProfileTag tag) + { + for (int i = 0; i < span.Length; i++) + { + if (span[i].TagSignature == tag) + { + return true; + } + } + + return false; + } + + static bool TryGetXyz(ReadOnlySpan span, IccProfileTag tag, out Vector3 xyz) + { + for (int i = 0; i < span.Length; i++) + { + IccTagDataEntry e = span[i]; + if (e.TagSignature != tag) + { + continue; + } + + if (e is IccXyzTagDataEntry x && x.Data is { Length: >= 1 }) + { + xyz = x.Data[0]; + return true; + } + + break; + } + + xyz = default; + return false; + } + + static bool TryGetTrc(ReadOnlySpan span, IccProfileTag tag, out Trc trc) + { + for (int i = 0; i < span.Length; i++) + { + IccTagDataEntry e = span[i]; + if (e.TagSignature != tag) + { + continue; + } + + if (e is IccParametricCurveTagDataEntry p) + { + trc = Trc.FromParametric(p.Curve); + return true; + } + + if (e is IccCurveTagDataEntry c) + { + trc = Trc.FromCurveLut(c.CurveData); + return true; + } + + break; + } + + trc = default; + return false; + } + + static bool Near(in Vector3 a, in Vector3 b, float tol) + => MathF.Abs(a.X - b.X) <= tol && + MathF.Abs(a.Y - b.Y) <= tol && + MathF.Abs(a.Z - b.Z) <= tol; + } + + /// + /// Compact, allocation-free descriptor of a TRC for equality and optional sRGB check. + /// + private readonly struct Trc : IEquatable + { + private readonly byte kind; // 0 = none, 1 = parametric, 2 = sampled + private readonly float g; // parametric payload or downsampled hash + private readonly float a; + private readonly float b; + private readonly float c; + private readonly float d; + private readonly float e; + private readonly float f; + private readonly int n; // for sampled, length or a small signature + + private Trc(byte kind, float g, float a, float b, float c, float d, float e, float f, int n) + { + this.kind = kind; + this.g = g; + this.a = a; + this.b = b; + this.c = c; + this.d = d; + this.e = e; + this.f = f; + this.n = n; + } + + public static Trc FromParametric(IccParametricCurve c) + + // Normalize by curve type to a stable tuple + // The types map to piecewise forms, but equality across channels is the key requirement here + => new(1, c.G, c.A, c.B, c.C, c.D, c.E, c.F, (int)c.Type); + + public static Trc FromCurveLut(float[] data) + { + // Exact sequence equality is enforced by the calling code using the same Trc construction + // Record a short signature to compare cheaply, avoid copying + if (data == null) + { + return default; + } + + int n = data.Length; + if (n == 0) + { + return default; + } + + // Downsample a few points to a robust fingerprint + // Use fixed indices to avoid allocations + float s0 = data[0]; + float s1 = data[n >> 2]; + float s2 = data[n >> 1]; + float s3 = data[(n * 3) >> 2]; + float s4 = data[n - 1]; + + return new Trc( + 2, + s0, + s1, + s2, + s3, + s4, + 0F, + 0F, + n); + } + + public override bool Equals(object? obj) => obj is Trc trc && this.Equals(trc); + + public bool Equals(Trc other) + { + if (this.kind != other.kind) + { + return false; + } + + if (this.kind == 0) + { + return false; + } + + if (this.kind == 1) + { + // parametric: exact parameter match and type match + return this.n == other.n && + this.g == other.g && this.a == other.a && + this.b == other.b && this.c == other.c && + this.d == other.d && this.e == other.e && this.f == other.f; + } + + // sampled: same length and same 5-point fingerprint + return this.n == other.n && + this.g == other.g && this.a == other.a && + this.b == other.b && this.c == other.c && this.d == other.d; + } + + // Optional stricter sRGB check if you need it later + public bool IsSrgbLike() + { + if (this.kind == 1) + { + // Accept common sRGB parametric encodings where type and parameters match + // IEC 61966-2-1 maps to Type4 or Type5 forms in practice + // Tighten only if you must exclude gamma~2.2 profiles that share primaries + return true; + } + + return true; + } + + public override int GetHashCode() + { + int a = HashCode.Combine(this.kind, this.g, this.a, this.b, this.c, this.d, this.e); + int b = HashCode.Combine(this.f, this.n); + return HashCode.Combine(a, b); + } + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs b/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs new file mode 100644 index 0000000..6ad978d --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs @@ -0,0 +1,212 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Security.Cryptography; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Represents an ICC profile + /// + public sealed partial class IccProfile : IDeepCloneable + { + /// + /// The byte array to read the ICC profile from + /// + private readonly byte[] data; + + /// + /// The backing file for the property + /// + private IccTagDataEntry[] entries; + + /// + /// ICC profile header + /// + private IccProfileHeader header; + + /// + /// Initializes a new instance of the class. + /// + public IccProfile() + : this((byte[])null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The raw ICC profile data + public IccProfile(byte[] data) => this.data = data; + + /// + /// Initializes a new instance of the class. + /// + /// The profile header + /// The actual profile data + internal IccProfile(IccProfileHeader header, IccTagDataEntry[] entries) + { + this.header = header ?? throw new ArgumentNullException(nameof(header)); + this.entries = entries ?? throw new ArgumentNullException(nameof(entries)); + } + + /// + /// Initializes a new instance of the class + /// by making a copy from another ICC profile. + /// + /// The other ICC profile, where the clone should be made from. + /// is null.> + private IccProfile(IccProfile other) + { + Guard.NotNull(other, nameof(other)); + + this.data = other.ToByteArray(); + } + + /// + /// Gets or sets the profile header + /// + public IccProfileHeader Header + { + get + { + this.InitializeHeader(); + return this.header; + } + + set => this.header = value; + } + + /// + /// Gets the actual profile data + /// + public IccTagDataEntry[] Entries + { + get + { + this.InitializeEntries(); + return this.entries; + } + } + + /// + public IccProfile DeepClone() => new(this); + + /// + /// Calculates the MD5 hash value of an ICC profile + /// + /// The data of which to calculate the hash value + /// The calculated hash + public static IccProfileId CalculateHash(byte[] data) + { + Guard.NotNull(data, nameof(data)); + Guard.IsTrue(data.Length >= 128, nameof(data), "Data length must be at least 128 to be a valid profile header"); + + const int profileFlagPos = 44; + const int renderingIntentPos = 64; + const int profileIdPos = 84; + + // need to copy some values because they need to be zero for the hashing + Span temp = stackalloc byte[24]; + data.AsSpan(profileFlagPos, 4).CopyTo(temp); + data.AsSpan(renderingIntentPos, 4).CopyTo(temp[4..]); + data.AsSpan(profileIdPos, 16).CopyTo(temp[8..]); + + try + { + // Zero out some values + Array.Clear(data, profileFlagPos, 4); + Array.Clear(data, renderingIntentPos, 4); + Array.Clear(data, profileIdPos, 16); + + // Calculate hash +#pragma warning disable CA5351 // Do Not Use Broken Cryptographic Algorithms + byte[] hash = MD5.HashData(data); +#pragma warning restore CA5351 // Do Not Use Broken Cryptographic Algorithms + + // Read values from hash + IccDataReader reader = new(hash); + return reader.ReadProfileId(); + } + finally + { + temp[..4].CopyTo(data.AsSpan(profileFlagPos)); + temp.Slice(4, 4).CopyTo(data.AsSpan(renderingIntentPos)); + temp.Slice(8, 16).CopyTo(data.AsSpan(profileIdPos)); + } + } + + /// + /// Checks for signs of a corrupt profile. + /// + /// This is not an absolute proof of validity but should weed out most corrupt data. + /// True if the profile is valid; False otherwise + public bool CheckIsValid() + { + const int minSize = 128; + const int maxSize = 50_000_000; // it's unlikely there is a profile bigger than 50MB + + bool arrayValid = true; + if (this.data != null) + { + arrayValid = this.data.Length >= minSize && + this.data.Length >= this.Header.Size; + } + + return arrayValid && + Enum.IsDefined(this.Header.DataColorSpace) && + Enum.IsDefined(this.Header.ProfileConnectionSpace) && + Enum.IsDefined(this.Header.RenderingIntent) && + this.Header.Size is >= minSize and < maxSize; + } + + /// + /// Converts this instance to a byte array. + /// + /// The + public byte[] ToByteArray() + { + if (this.data != null) + { + byte[] copy = new byte[this.data.Length]; + Buffer.BlockCopy(this.data, 0, copy, 0, copy.Length); + return copy; + } + + return IccWriter.Write(this); + } + + private void InitializeHeader() + { + if (this.header != null) + { + return; + } + + if (this.data is null) + { + this.header = new IccProfileHeader(); + return; + } + + this.header = IccReader.ReadHeader(this.data); + } + + private void InitializeEntries() + { + if (this.entries != null) + { + return; + } + + if (this.data is null) + { + this.entries = []; + return; + } + + this.entries = IccReader.ReadTagData(this.data); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs b/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs new file mode 100644 index 0000000..0eb67d1 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Contains all values of an ICC profile header. + /// + public sealed class IccProfileHeader + { + /// + /// Gets or sets the profile size in bytes (will be ignored when writing a profile). + /// + public uint Size { get; set; } + + /// + /// Gets or sets the preferred CMM (Color Management Module) type. + /// + public string CmmType { get; set; } + + /// + /// Gets or sets the profiles version number. + /// + public IccVersion Version { get; set; } + + /// + /// Gets or sets the type of the profile. + /// + public IccProfileClass Class { get; set; } + + /// + /// Gets or sets the data colorspace. + /// + public IccColorSpaceType DataColorSpace { get; set; } + + /// + /// Gets or sets the profile connection space. + /// + public IccColorSpaceType ProfileConnectionSpace { get; set; } + + /// + /// Gets or sets the date and time this profile was created. + /// + public DateTime CreationDate { get; set; } + + /// + /// Gets or sets the file signature. Should always be "acsp". + /// Value will be ignored when writing a profile. + /// + public string FileSignature { get; set; } + + /// + /// Gets or sets the primary platform this profile as created for + /// + public IccPrimaryPlatformType PrimaryPlatformSignature { get; set; } + + /// + /// Gets or sets the profile flags to indicate various options for the CMM + /// such as distributed processing and caching options. + /// + public IccProfileFlag Flags { get; set; } + + /// + /// Gets or sets the device manufacturer of the device for which this profile is created. + /// + public uint DeviceManufacturer { get; set; } + + /// + /// Gets or sets the model of the device for which this profile is created. + /// + public uint DeviceModel { get; set; } + + /// + /// Gets or sets the device attributes unique to the particular device setup such as media type. + /// + public IccDeviceAttribute DeviceAttributes { get; set; } + + /// + /// Gets or sets the rendering Intent. + /// + public IccRenderingIntent RenderingIntent { get; set; } + + /// + /// Gets or sets The normalized XYZ values of the illuminant of the PCS. + /// + public Vector3 PcsIlluminant { get; set; } + + /// + /// Gets or sets profile creator signature. + /// + public string CreatorSignature { get; set; } + + /// + /// Gets or sets the profile ID (hash). + /// + public IccProfileId Id { get; set; } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/IccReader.cs b/ImageSharp/Metadata/Profiles/ICC/IccReader.cs new file mode 100644 index 0000000..f921b0d --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/IccReader.cs @@ -0,0 +1,139 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Reads and parses ICC data from a byte array + /// + internal sealed class IccReader + { + /// + /// Reads an ICC profile + /// + /// The raw ICC data + /// The read ICC profile + public static IccProfile Read(byte[] data) + { + Guard.NotNull(data, nameof(data)); + Guard.IsTrue(data.Length >= 128, nameof(data), "Data length must be at least 128 to be a valid ICC profile"); + + IccDataReader reader = new(data); + IccProfileHeader header = ReadHeader(reader); + IccTagDataEntry[] tagData = ReadTagData(reader); + + return new IccProfile(header, tagData); + } + + /// + /// Reads an ICC profile header + /// + /// The raw ICC data + /// The read ICC profile header + public static IccProfileHeader ReadHeader(byte[] data) + { + Guard.NotNull(data, nameof(data)); + Guard.IsTrue(data.Length >= 128, nameof(data), "Data length must be at least 128 to be a valid profile header"); + + IccDataReader reader = new(data); + return ReadHeader(reader); + } + + /// + /// Reads the ICC profile tag data + /// + /// The raw ICC data + /// The read ICC profile tag data + public static IccTagDataEntry[] ReadTagData(byte[] data) + { + Guard.NotNull(data, nameof(data)); + Guard.IsTrue(data.Length >= 128, nameof(data), "Data length must be at least 128 to be a valid ICC profile"); + + IccDataReader reader = new(data); + return ReadTagData(reader); + } + + private static IccProfileHeader ReadHeader(IccDataReader reader) + { + reader.SetIndex(0); + + return new IccProfileHeader + { + Size = reader.ReadUInt32(), + CmmType = reader.ReadAsciiString(4), + Version = reader.ReadVersionNumber(), + Class = (IccProfileClass)reader.ReadUInt32(), + DataColorSpace = (IccColorSpaceType)reader.ReadUInt32(), + ProfileConnectionSpace = (IccColorSpaceType)reader.ReadUInt32(), + CreationDate = reader.ReadDateTime(), + FileSignature = reader.ReadAsciiString(4), + PrimaryPlatformSignature = (IccPrimaryPlatformType)reader.ReadUInt32(), + Flags = (IccProfileFlag)reader.ReadInt32(), + DeviceManufacturer = reader.ReadUInt32(), + DeviceModel = reader.ReadUInt32(), + DeviceAttributes = (IccDeviceAttribute)reader.ReadInt64(), + RenderingIntent = (IccRenderingIntent)reader.ReadUInt32(), + PcsIlluminant = reader.ReadXyzNumber(), + CreatorSignature = reader.ReadAsciiString(4), + Id = reader.ReadProfileId(), + }; + } + + private static IccTagDataEntry[] ReadTagData(IccDataReader reader) + { + IccTagTableEntry[] tagTable = ReadTagTable(reader); + List entries = new(tagTable.Length); + + foreach (IccTagTableEntry tag in tagTable) + { + IccTagDataEntry entry; + + try + { + entry = reader.ReadTagDataEntry(tag); + } + catch + { + // Ignore tags that could not be read + continue; + } + + entry.TagSignature = tag.Signature; + entries.Add(entry); + } + + return entries.ToArray(); + } + + private static IccTagTableEntry[] ReadTagTable(IccDataReader reader) + { + reader.SetIndex(128); // An ICC header is 128 bytes long + + uint tagCount = reader.ReadUInt32(); + + // Prevent creating huge arrays because of corrupt profiles. + // A normal profile usually has 5-15 entries + if (tagCount > 100) + { + return []; + } + + List table = new((int)tagCount); + for (int i = 0; i < tagCount; i++) + { + uint tagSignature = reader.ReadUInt32(); + uint tagOffset = reader.ReadUInt32(); + uint tagSize = reader.ReadUInt32(); + + // Exclude entries that have nonsense values and could cause exceptions further on + if (tagOffset < reader.DataLength && tagSize < reader.DataLength - 128) + { + table.Add(new IccTagTableEntry((IccProfileTag)tagSignature, tagOffset, tagSize)); + } + } + + return table.ToArray(); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/IccTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/IccTagDataEntry.cs new file mode 100644 index 0000000..494004c --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/IccTagDataEntry.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// The data of an ICC tag entry + /// + public abstract class IccTagDataEntry : IEquatable + { + /// + /// Initializes a new instance of the class. + /// TagSignature will be + /// + /// Type Signature + protected IccTagDataEntry(IccTypeSignature signature) + : this(signature, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Type Signature + /// Tag Signature + protected IccTagDataEntry(IccTypeSignature signature, IccProfileTag tagSignature) + { + this.Signature = signature; + this.TagSignature = tagSignature; + } + + /// + /// Gets the type Signature + /// + public IccTypeSignature Signature { get; } + + /// + /// Gets or sets the tag Signature + /// + public IccProfileTag TagSignature { get; set; } + + /// + public override bool Equals(object? obj) + => obj is IccTagDataEntry entry && this.Equals(entry); + + /// + public virtual bool Equals(IccTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return this.Signature == other.Signature; + } + + /// + public override int GetHashCode() => this.Signature.GetHashCode(); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/IccWriter.cs b/ImageSharp/Metadata/Profiles/ICC/IccWriter.cs new file mode 100644 index 0000000..dddd9fe --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/IccWriter.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Linq; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Contains methods for writing ICC profiles. + /// + internal sealed class IccWriter + { + /// + /// Writes the ICC profile into a byte array + /// + /// The ICC profile to write + /// The ICC profile as a byte array + public static byte[] Write(IccProfile profile) + { + Guard.NotNull(profile, nameof(profile)); + + using IccDataWriter writer = new(); + IccTagTableEntry[] tagTable = WriteTagData(writer, profile.Entries); + WriteTagTable(writer, tagTable); + WriteHeader(writer, profile.Header); + return writer.GetData(); + } + + private static void WriteHeader(IccDataWriter writer, IccProfileHeader header) + { + writer.SetIndex(0); + + writer.WriteUInt32(writer.Length); + writer.WriteAsciiString(header.CmmType, 4, false); + writer.WriteVersionNumber(header.Version); + writer.WriteUInt32((uint)header.Class); + writer.WriteUInt32((uint)header.DataColorSpace); + writer.WriteUInt32((uint)header.ProfileConnectionSpace); + writer.WriteDateTime(header.CreationDate); + writer.WriteAsciiString("acsp"); + writer.WriteUInt32((uint)header.PrimaryPlatformSignature); + writer.WriteInt32((int)header.Flags); + writer.WriteUInt32(header.DeviceManufacturer); + writer.WriteUInt32(header.DeviceModel); + writer.WriteInt64((long)header.DeviceAttributes); + writer.WriteUInt32((uint)header.RenderingIntent); + writer.WriteXyzNumber(header.PcsIlluminant); + writer.WriteAsciiString(header.CreatorSignature, 4, false); + + IccProfileId id = IccProfile.CalculateHash(writer.GetData()); + writer.WriteProfileId(id); + } + + private static void WriteTagTable(IccDataWriter writer, IccTagTableEntry[] table) + { + // 128 = size of ICC header + writer.SetIndex(128); + + writer.WriteUInt32((uint)table.Length); + foreach (IccTagTableEntry entry in table) + { + writer.WriteUInt32((uint)entry.Signature); + writer.WriteUInt32(entry.Offset); + writer.WriteUInt32(entry.DataSize); + } + } + + private static IccTagTableEntry[] WriteTagData(IccDataWriter writer, IccTagDataEntry[] entries) + { + // TODO: Investigate cost of Linq GroupBy + IEnumerable> grouped = entries.GroupBy(t => t); + + // (Header size) + (entry count) + (nr of entries) * (size of table entry) + writer.SetIndex(128 + 4 + (entries.Length * 12)); + + List table = []; + foreach (IGrouping group in grouped) + { + writer.WriteTagDataEntry(group.Key, out IccTagTableEntry tableEntry); + foreach (IccTagDataEntry item in group) + { + table.Add(new IccTagTableEntry(item.TagSignature, tableEntry.Offset, tableEntry.DataSize)); + } + } + + return table.ToArray(); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccBAcsProcessElement.cs b/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccBAcsProcessElement.cs new file mode 100644 index 0000000..ee9a1c3 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccBAcsProcessElement.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// A placeholder (might be used for future ICC versions) + /// + internal sealed class IccBAcsProcessElement : IccMultiProcessElement, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// Number of input channels + /// Number of output channels + public IccBAcsProcessElement(int inChannelCount, int outChannelCount) + : base(IccMultiProcessElementSignature.BAcs, inChannelCount, outChannelCount) + { + } + + /// + public bool Equals(IccBAcsProcessElement? other) => base.Equals(other); + + /// + public override bool Equals(object? obj) => this.Equals(obj as IccBAcsProcessElement); + + /// + public override int GetHashCode() => base.GetHashCode(); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccClutProcessElement.cs b/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccClutProcessElement.cs new file mode 100644 index 0000000..9f79673 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccClutProcessElement.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// A CLUT (color lookup table) element to process data + /// + internal sealed class IccClutProcessElement : IccMultiProcessElement, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The color lookup table of this element + public IccClutProcessElement(IccClut clutValue) + : base(IccMultiProcessElementSignature.Clut, clutValue?.InputChannelCount ?? 1, clutValue?.OutputChannelCount ?? 1) + => this.ClutValue = clutValue ?? throw new ArgumentNullException(nameof(clutValue)); + + /// + /// Gets the color lookup table of this element + /// + public IccClut ClutValue { get; } + + /// + public override bool Equals(IccMultiProcessElement? other) + { + if (base.Equals(other) && other is IccClutProcessElement element) + { + return this.ClutValue.Equals(element.ClutValue); + } + + return false; + } + + /// + public bool Equals(IccClutProcessElement? other) => this.Equals((IccMultiProcessElement?)other); + + /// + public override bool Equals(object? obj) => this.Equals(obj as IccClutProcessElement); + + /// + public override int GetHashCode() => base.GetHashCode(); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccCurveSetProcessElement.cs b/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccCurveSetProcessElement.cs new file mode 100644 index 0000000..3f8c795 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccCurveSetProcessElement.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// A set of curves to process data + /// + internal sealed class IccCurveSetProcessElement : IccMultiProcessElement, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// An array with one dimensional curves + public IccCurveSetProcessElement(IccOneDimensionalCurve[] curves) + : base(IccMultiProcessElementSignature.CurveSet, curves?.Length ?? 1, curves?.Length ?? 1) + => this.Curves = curves ?? throw new ArgumentNullException(nameof(curves)); + + /// + /// Gets an array of one dimensional curves + /// + public IccOneDimensionalCurve[] Curves { get; } + + /// + public override bool Equals(IccMultiProcessElement? other) + { + if (base.Equals(other) && other is IccCurveSetProcessElement element) + { + return this.Curves.AsSpan().SequenceEqual(element.Curves); + } + + return false; + } + + /// + public bool Equals(IccCurveSetProcessElement? other) => this.Equals((IccMultiProcessElement?)other); + + /// + public override bool Equals(object? obj) => this.Equals(obj as IccCurveSetProcessElement); + + /// + public override int GetHashCode() => base.GetHashCode(); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccEAcsProcessElement.cs b/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccEAcsProcessElement.cs new file mode 100644 index 0000000..456ba65 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccEAcsProcessElement.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// A placeholder (might be used for future ICC versions) + /// + internal sealed class IccEAcsProcessElement : IccMultiProcessElement, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// Number of input channels + /// Number of output channels + public IccEAcsProcessElement(int inChannelCount, int outChannelCount) + : base(IccMultiProcessElementSignature.EAcs, inChannelCount, outChannelCount) + { + } + + /// + public bool Equals(IccEAcsProcessElement? other) => base.Equals(other); + + public override bool Equals(object? obj) => this.Equals(obj as IccEAcsProcessElement); + + /// + public override int GetHashCode() => base.GetHashCode(); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccMatrixProcessElement.cs b/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccMatrixProcessElement.cs new file mode 100644 index 0000000..30b2564 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccMatrixProcessElement.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// A matrix element to process data + /// + internal sealed class IccMatrixProcessElement : IccMultiProcessElement, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// Two dimensional matrix with size of Input-Channels x Output-Channels + /// One dimensional matrix with size of Output-Channels x 1 + public IccMatrixProcessElement(float[,] matrixIxO, float[] matrixOx1) + : base(IccMultiProcessElementSignature.Matrix, matrixIxO?.GetLength(0) ?? 1, matrixIxO?.GetLength(1) ?? 1) + { + Guard.NotNull(matrixIxO, nameof(matrixIxO)); + Guard.NotNull(matrixOx1, nameof(matrixOx1)); + + bool matrixSizeCorrect = matrixIxO.GetLength(1) == matrixOx1.Length; + Guard.IsTrue(matrixSizeCorrect, $"{nameof(matrixIxO)},{nameof(matrixIxO)}", "Output channel length must match"); + + this.MatrixIxO = matrixIxO; + this.MatrixOx1 = matrixOx1; + } + + /// + /// Gets the two dimensional matrix with size of Input-Channels x Output-Channels + /// + public DenseMatrix MatrixIxO { get; } + + /// + /// Gets the one dimensional matrix with size of Output-Channels x 1 + /// + public float[] MatrixOx1 { get; } + + /// + public override bool Equals(IccMultiProcessElement? other) + { + if (base.Equals(other) && other is IccMatrixProcessElement element) + { + return this.EqualsMatrix(element) + && this.MatrixOx1.AsSpan().SequenceEqual(element.MatrixOx1); + } + + return false; + } + + /// + public bool Equals(IccMatrixProcessElement? other) + => this.Equals((IccMultiProcessElement?)other); + + /// + public override bool Equals(object? obj) + => this.Equals(obj as IccMatrixProcessElement); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), this.MatrixIxO, this.MatrixOx1); + + private bool EqualsMatrix(IccMatrixProcessElement element) + => this.MatrixIxO.Equals(element.MatrixIxO); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccMultiProcessElement.cs b/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccMultiProcessElement.cs new file mode 100644 index 0000000..6b91d76 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccMultiProcessElement.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// An element to process data + /// + internal abstract class IccMultiProcessElement : IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The signature of this element + /// Number of input channels + /// Number of output channels + protected IccMultiProcessElement(IccMultiProcessElementSignature signature, int inChannelCount, int outChannelCount) + { + Guard.MustBeBetweenOrEqualTo(inChannelCount, 1, 15, nameof(inChannelCount)); + Guard.MustBeBetweenOrEqualTo(outChannelCount, 1, 15, nameof(outChannelCount)); + + this.Signature = signature; + this.InputChannelCount = inChannelCount; + this.OutputChannelCount = outChannelCount; + } + + /// + /// Gets the signature of this element, + /// + public IccMultiProcessElementSignature Signature { get; } + + /// + /// Gets the number of input channels + /// + public int InputChannelCount { get; } + + /// + /// Gets the number of output channels. + /// + public int OutputChannelCount { get; } + + /// + public virtual bool Equals(IccMultiProcessElement? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return this.Signature == other.Signature + && this.InputChannelCount == other.InputChannelCount + && this.OutputChannelCount == other.OutputChannelCount; + } + + public override bool Equals(object? obj) => this.Equals(obj as IccMultiProcessElement); + + /// + public override int GetHashCode() + => HashCode.Combine(this.Signature, this.InputChannelCount, this.OutputChannelCount); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccChromaticityTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccChromaticityTagDataEntry.cs new file mode 100644 index 0000000..1da5ba3 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccChromaticityTagDataEntry.cs @@ -0,0 +1,167 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Linq; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// The chromaticity tag type provides basic chromaticity data + /// and type of phosphors or colorants of a monitor to applications and utilities. + /// + internal sealed class IccChromaticityTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// Colorant Type + public IccChromaticityTagDataEntry(IccColorantEncoding colorantType) + : this(colorantType, GetColorantArray(colorantType), IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Values per channel + public IccChromaticityTagDataEntry(double[][] channelValues) + : this(IccColorantEncoding.Unknown, channelValues, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Colorant Type + /// Tag Signature + public IccChromaticityTagDataEntry(IccColorantEncoding colorantType, IccProfileTag tagSignature) + : this(colorantType, GetColorantArray(colorantType), tagSignature) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Values per channel + /// Tag Signature + public IccChromaticityTagDataEntry(double[][] channelValues, IccProfileTag tagSignature) + : this(IccColorantEncoding.Unknown, channelValues, tagSignature) + { + } + + private IccChromaticityTagDataEntry(IccColorantEncoding colorantType, double[][] channelValues, IccProfileTag tagSignature) + : base(IccTypeSignature.Chromaticity, tagSignature) + { + Guard.NotNull(channelValues, nameof(channelValues)); + Guard.MustBeBetweenOrEqualTo(channelValues.Length, 1, 15, nameof(channelValues)); + + this.ColorantType = colorantType; + this.ChannelValues = channelValues; + + int channelLength = channelValues[0].Length; + bool channelsNotSame = channelValues.Any(t => t is null || t.Length != channelLength); + Guard.IsFalse(channelsNotSame, nameof(channelValues), "The number of values per channel is not the same for all channels"); + } + + /// + /// Gets the number of channels + /// + public int ChannelCount => this.ChannelValues.Length; + + /// + /// Gets the colorant type + /// + public IccColorantEncoding ColorantType { get; } + + /// + /// Gets the values per channel + /// + public double[][] ChannelValues { get; } + + /// + public override bool Equals(IccTagDataEntry? other) => other is IccChromaticityTagDataEntry entry && this.Equals(entry); + + /// + public bool Equals(IccChromaticityTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && this.ColorantType == other.ColorantType && this.EqualsChannelValues(other); + } + + /// + public override bool Equals(object? obj) => obj is IccChromaticityTagDataEntry other && this.Equals(other); + + /// + public override int GetHashCode() + { + return HashCode.Combine( + this.Signature, + this.ColorantType, + this.ChannelValues); + } + + private static double[][] GetColorantArray(IccColorantEncoding colorantType) + { + switch (colorantType) + { + case IccColorantEncoding.EbuTech3213E: + return + [ + [0.640, 0.330], + [0.290, 0.600], + [0.150, 0.060] + ]; + case IccColorantEncoding.ItuRBt709_2: + return + [ + [0.640, 0.330], + [0.300, 0.600], + [0.150, 0.060] + ]; + case IccColorantEncoding.P22: + return + [ + [0.625, 0.340], + [0.280, 0.605], + [0.155, 0.070] + ]; + case IccColorantEncoding.SmpteRp145: + return + [ + [0.630, 0.340], + [0.310, 0.595], + [0.155, 0.070] + ]; + default: + throw new InvalidIccProfileException("Unrecognized colorant encoding"); + } + } + + private bool EqualsChannelValues(IccChromaticityTagDataEntry entry) + { + if (this.ChannelValues.Length != entry.ChannelValues.Length) + { + return false; + } + + for (int i = 0; i < this.ChannelValues.Length; i++) + { + if (!this.ChannelValues[i].AsSpan().SequenceEqual(entry.ChannelValues[i])) + { + return false; + } + } + + return true; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccColorantOrderTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccColorantOrderTagDataEntry.cs new file mode 100644 index 0000000..12e9c93 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccColorantOrderTagDataEntry.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This tag specifies the laydown order in which colorants + /// will be printed on an n-colorant device. + /// + internal sealed class IccColorantOrderTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// Colorant order numbers + public IccColorantOrderTagDataEntry(byte[] colorantNumber) + : this(colorantNumber, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Colorant order numbers + /// Tag Signature + public IccColorantOrderTagDataEntry(byte[] colorantNumber, IccProfileTag tagSignature) + : base(IccTypeSignature.ColorantOrder, tagSignature) + { + Guard.NotNull(colorantNumber, nameof(colorantNumber)); + Guard.MustBeBetweenOrEqualTo(colorantNumber.Length, 1, 15, nameof(colorantNumber)); + + this.ColorantNumber = colorantNumber; + } + + /// + /// Gets the colorant order numbers + /// + public byte[] ColorantNumber { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + { + return other is IccColorantOrderTagDataEntry entry && this.Equals(entry); + } + + /// + public bool Equals(IccColorantOrderTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && this.ColorantNumber.AsSpan().SequenceEqual(other.ColorantNumber); + } + + /// + public override bool Equals(object? obj) + { + return obj is IccColorantOrderTagDataEntry other && this.Equals(other); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(this.Signature, this.ColorantNumber); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccColorantTableTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccColorantTableTagDataEntry.cs new file mode 100644 index 0000000..3c48929 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccColorantTableTagDataEntry.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// The purpose of this tag is to identify the colorants used in + /// the profile by a unique name and set of PCSXYZ or PCSLAB values + /// to give the colorant an unambiguous value. + /// + internal sealed class IccColorantTableTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// Colorant Data + public IccColorantTableTagDataEntry(IccColorantTableEntry[] colorantData) + : this(colorantData, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Colorant Data + /// Tag Signature + public IccColorantTableTagDataEntry(IccColorantTableEntry[] colorantData, IccProfileTag tagSignature) + : base(IccTypeSignature.ColorantTable, tagSignature) + { + Guard.NotNull(colorantData, nameof(colorantData)); + Guard.MustBeBetweenOrEqualTo(colorantData.Length, 1, 15, nameof(colorantData)); + + this.ColorantData = colorantData; + } + + /// + /// Gets the colorant data + /// + public IccColorantTableEntry[] ColorantData { get; } + + /// + public override bool Equals(IccTagDataEntry? other) => other is IccColorantTableTagDataEntry entry && this.Equals(entry); + + /// + public bool Equals(IccColorantTableTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && this.ColorantData.AsSpan().SequenceEqual(other.ColorantData); + } + + /// + public override bool Equals(object? obj) => obj is IccColorantTableTagDataEntry other && this.Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(this.Signature, this.ColorantData); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccCrdInfoTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccCrdInfoTagDataEntry.cs new file mode 100644 index 0000000..f0bf2a2 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccCrdInfoTagDataEntry.cs @@ -0,0 +1,126 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This type contains the PostScript product name to which this profile + /// corresponds and the names of the companion CRDs + /// + internal sealed class IccCrdInfoTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// the PostScript product name + /// the rendering intent 0 CRD name + /// the rendering intent 1 CRD name + /// the rendering intent 2 CRD name + /// the rendering intent 3 CRD name + public IccCrdInfoTagDataEntry( + string postScriptProductName, + string renderingIntent0Crd, + string renderingIntent1Crd, + string renderingIntent2Crd, + string renderingIntent3Crd) + : this( + postScriptProductName, + renderingIntent0Crd, + renderingIntent1Crd, + renderingIntent2Crd, + renderingIntent3Crd, + IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// the PostScript product name + /// the rendering intent 0 CRD name + /// the rendering intent 1 CRD name + /// the rendering intent 2 CRD name + /// the rendering intent 3 CRD name + /// Tag Signature + public IccCrdInfoTagDataEntry( + string postScriptProductName, + string renderingIntent0Crd, + string renderingIntent1Crd, + string renderingIntent2Crd, + string renderingIntent3Crd, + IccProfileTag tagSignature) + : base(IccTypeSignature.CrdInfo, tagSignature) + { + this.PostScriptProductName = postScriptProductName; + this.RenderingIntent0Crd = renderingIntent0Crd; + this.RenderingIntent1Crd = renderingIntent1Crd; + this.RenderingIntent2Crd = renderingIntent2Crd; + this.RenderingIntent3Crd = renderingIntent3Crd; + } + + /// + /// Gets the PostScript product name + /// + public string PostScriptProductName { get; } + + /// + /// Gets the rendering intent 0 CRD name + /// + public string RenderingIntent0Crd { get; } + + /// + /// Gets the rendering intent 1 CRD name + /// + public string RenderingIntent1Crd { get; } + + /// + /// Gets the rendering intent 2 CRD name + /// + public string RenderingIntent2Crd { get; } + + /// + /// Gets the rendering intent 3 CRD name + /// + public string RenderingIntent3Crd { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + => other is IccCrdInfoTagDataEntry entry && this.Equals(entry); + + /// + public bool Equals(IccCrdInfoTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) + && string.Equals(this.PostScriptProductName, other.PostScriptProductName, StringComparison.OrdinalIgnoreCase) + && string.Equals(this.RenderingIntent0Crd, other.RenderingIntent0Crd, StringComparison.OrdinalIgnoreCase) + && string.Equals(this.RenderingIntent1Crd, other.RenderingIntent1Crd, StringComparison.OrdinalIgnoreCase) + && string.Equals(this.RenderingIntent2Crd, other.RenderingIntent2Crd, StringComparison.OrdinalIgnoreCase) + && string.Equals(this.RenderingIntent3Crd, other.RenderingIntent3Crd, StringComparison.OrdinalIgnoreCase); + } + + /// + public override bool Equals(object? obj) + => obj is IccCrdInfoTagDataEntry other && this.Equals(other); + + /// + public override int GetHashCode() + => HashCode.Combine( + this.Signature, + this.PostScriptProductName, + this.RenderingIntent0Crd, + this.RenderingIntent1Crd, + this.RenderingIntent2Crd, + this.RenderingIntent3Crd); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccCurveTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccCurveTagDataEntry.cs new file mode 100644 index 0000000..839c637 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccCurveTagDataEntry.cs @@ -0,0 +1,120 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// The type contains a one-dimensional table of double values. + /// + internal sealed class IccCurveTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + public IccCurveTagDataEntry() + : this([], IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Gamma value + public IccCurveTagDataEntry(float gamma) + : this([gamma], IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Curve Data + public IccCurveTagDataEntry(float[] curveData) + : this(curveData, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Tag Signature + public IccCurveTagDataEntry(IccProfileTag tagSignature) + : this([], tagSignature) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Gamma value + /// Tag Signature + public IccCurveTagDataEntry(float gamma, IccProfileTag tagSignature) + : this([gamma], tagSignature) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Curve Data + /// Tag Signature + public IccCurveTagDataEntry(float[] curveData, IccProfileTag tagSignature) + : base(IccTypeSignature.Curve, tagSignature) + { + this.CurveData = curveData ?? []; + } + + /// + /// Gets the curve data + /// + public float[] CurveData { get; } + + /// + /// Gets the gamma value. + /// Only valid if is true + /// + public float Gamma => this.IsGamma ? this.CurveData[0] : 0; + + /// + /// Gets a value indicating whether the curve maps input directly to output. + /// + public bool IsIdentityResponse => this.CurveData.Length == 0; + + /// + /// Gets a value indicating whether the curve is a gamma curve. + /// + public bool IsGamma => this.CurveData.Length == 1; + + /// + public override bool Equals(IccTagDataEntry? other) + { + return other is IccCurveTagDataEntry entry && this.Equals(entry); + } + + /// + public bool Equals(IccCurveTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && this.CurveData.AsSpan().SequenceEqual(other.CurveData); + } + + /// + public override bool Equals(object? obj) + { + return obj is IccCurveTagDataEntry other && this.Equals(other); + } + + /// + public override int GetHashCode() => HashCode.Combine(this.Signature, this.CurveData); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccDataTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccDataTagDataEntry.cs new file mode 100644 index 0000000..cbb9b6a --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccDataTagDataEntry.cs @@ -0,0 +1,93 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Text; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// The dataType is a simple data structure that contains + /// either 7-bit ASCII or binary data, i.e. textType data or transparent bytes. + /// + internal sealed class IccDataTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The raw data + public IccDataTagDataEntry(byte[] data) + : this(data, false, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The raw data + /// True if the given data is 7bit ASCII encoded text + public IccDataTagDataEntry(byte[] data, bool isAscii) + : this(data, isAscii, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The raw data + /// True if the given data is 7bit ASCII encoded text + /// Tag Signature + public IccDataTagDataEntry(byte[] data, bool isAscii, IccProfileTag tagSignature) + : base(IccTypeSignature.Data, tagSignature) + { + this.Data = data ?? throw new ArgumentNullException(nameof(data)); + this.IsAscii = isAscii; + } + + /// + /// Gets the raw Data + /// + public byte[] Data { get; } + + /// + /// Gets a value indicating whether the represents 7bit ASCII encoded text + /// + public bool IsAscii { get; } + + /// + /// Gets the decoded as 7bit ASCII. + /// If is false, returns null + /// + public string? AsciiString => this.IsAscii ? Encoding.ASCII.GetString(this.Data, 0, this.Data.Length) : null; + + /// + public override bool Equals(IccTagDataEntry? other) + => other is IccDataTagDataEntry entry && this.Equals(entry); + + /// + public bool Equals(IccDataTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && this.Data.AsSpan().SequenceEqual(other.Data) && this.IsAscii == other.IsAscii; + } + + /// + public override bool Equals(object? obj) + => obj is IccDataTagDataEntry other && this.Equals(other); + + /// + public override int GetHashCode() + => HashCode.Combine( + this.Signature, + this.Data, + this.IsAscii); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccDateTimeTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccDateTimeTagDataEntry.cs new file mode 100644 index 0000000..93b0f1d --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccDateTimeTagDataEntry.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This type is a representation of the time and date. + /// + internal sealed class IccDateTimeTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The DateTime value + public IccDateTimeTagDataEntry(DateTime value) + : this(value, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The DateTime value + /// Tag Signature + public IccDateTimeTagDataEntry(DateTime value, IccProfileTag tagSignature) + : base(IccTypeSignature.DateTime, tagSignature) + { + this.Value = value; + } + + /// + /// Gets the date and time value + /// + public DateTime Value { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + { + return other is IccDateTimeTagDataEntry entry && this.Equals(entry); + } + + /// + public bool Equals(IccDateTimeTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && this.Value.Equals(other.Value); + } + + /// + public override bool Equals(object? obj) + { + return obj is IccDateTimeTagDataEntry other && this.Equals(other); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(this.Signature, this.Value); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccFix16ArrayTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccFix16ArrayTagDataEntry.cs new file mode 100644 index 0000000..3ba262d --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccFix16ArrayTagDataEntry.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This type represents an array of doubles (from 32bit fixed point values). + /// + internal sealed class IccFix16ArrayTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The array data + public IccFix16ArrayTagDataEntry(float[] data) + : this(data, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The array data + /// Tag Signature + public IccFix16ArrayTagDataEntry(float[] data, IccProfileTag tagSignature) + : base(IccTypeSignature.S15Fixed16Array, tagSignature) + { + this.Data = data ?? throw new ArgumentNullException(nameof(data)); + } + + /// + /// Gets the array data + /// + public float[] Data { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + { + return other is IccFix16ArrayTagDataEntry entry && this.Equals(entry); + } + + /// + public bool Equals(IccFix16ArrayTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && this.Data.AsSpan().SequenceEqual(other.Data); + } + + /// + public override bool Equals(object? obj) + { + return obj is IccFix16ArrayTagDataEntry other && this.Equals(other); + } + + /// + public override int GetHashCode() => HashCode.Combine(this.Signature, this.Data); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLut16TagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLut16TagDataEntry.cs new file mode 100644 index 0000000..470e485 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLut16TagDataEntry.cs @@ -0,0 +1,169 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This structure represents a color transform using tables + /// with 16-bit precision. + /// + internal sealed class IccLut16TagDataEntry : IccTagDataEntry, IEquatable + { + private static readonly float[,] IdentityMatrix = + { + { 1, 0, 0 }, + { 0, 1, 0 }, + { 0, 0, 1 } + }; + + /// + /// Initializes a new instance of the class. + /// + /// Input LUT + /// CLUT + /// Output LUT + public IccLut16TagDataEntry(IccLut[] inputValues, IccClut clutValues, IccLut[] outputValues) + : this(IdentityMatrix, inputValues, clutValues, outputValues, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Input LUT + /// CLUT + /// Output LUT + /// Tag Signature + public IccLut16TagDataEntry(IccLut[] inputValues, IccClut clutValues, IccLut[] outputValues, IccProfileTag tagSignature) + : this(IdentityMatrix, inputValues, clutValues, outputValues, tagSignature) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Conversion matrix (must be 3x3) + /// Input LUT + /// CLUT + /// Output LUT + public IccLut16TagDataEntry(float[,] matrix, IccLut[] inputValues, IccClut clutValues, IccLut[] outputValues) + : this(matrix, inputValues, clutValues, outputValues, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Conversion matrix (must be 3x3) + /// Input LUT + /// CLUT + /// Output LUT + /// Tag Signature + public IccLut16TagDataEntry(float[,] matrix, IccLut[] inputValues, IccClut clutValues, IccLut[] outputValues, IccProfileTag tagSignature) + : base(IccTypeSignature.Lut16, tagSignature) + { + Guard.NotNull(matrix, nameof(matrix)); + + bool is3By3 = matrix.GetLength(0) == 3 && matrix.GetLength(1) == 3; + Guard.IsTrue(is3By3, nameof(matrix), "Matrix must have a size of three by three"); + + this.Matrix = CreateMatrix(matrix); + this.InputValues = inputValues ?? throw new ArgumentNullException(nameof(inputValues)); + this.ClutValues = clutValues ?? throw new ArgumentNullException(nameof(clutValues)); + this.OutputValues = outputValues ?? throw new ArgumentNullException(nameof(outputValues)); + + Guard.IsTrue(this.InputChannelCount == clutValues.InputChannelCount, nameof(clutValues), "Input channel count does not match the CLUT size"); + Guard.IsTrue(this.OutputChannelCount == clutValues.OutputChannelCount, nameof(clutValues), "Output channel count does not match the CLUT size"); + } + + /// + /// Gets the number of input channels + /// + public int InputChannelCount => this.InputValues.Length; + + /// + /// Gets the number of output channels + /// + public int OutputChannelCount => this.OutputValues.Length; + + /// + /// Gets the conversion matrix + /// + public Matrix4x4 Matrix { get; } + + /// + /// Gets the input lookup table + /// + public IccLut[] InputValues { get; } + + /// + /// Gets the color lookup table + /// + public IccClut ClutValues { get; } + + /// + /// Gets the output lookup table + /// + public IccLut[] OutputValues { get; } + + /// + public override bool Equals(IccTagDataEntry? other) => other is IccLut16TagDataEntry entry && this.Equals(entry); + + /// + public bool Equals(IccLut16TagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) + && this.Matrix.Equals(other.Matrix) + && this.InputValues.AsSpan().SequenceEqual(other.InputValues) + && this.ClutValues.Equals(other.ClutValues) + && this.OutputValues.AsSpan().SequenceEqual(other.OutputValues); + } + + /// + public override bool Equals(object? obj) => obj is IccLut16TagDataEntry other && this.Equals(other); + + /// + public override int GetHashCode() + { + return HashCode.Combine( + this.Signature, + this.Matrix, + this.InputValues, + this.ClutValues, + this.OutputValues); + } + + private static Matrix4x4 CreateMatrix(float[,] matrix) + { + return new Matrix4x4( + matrix[0, 0], + matrix[0, 1], + matrix[0, 2], + 0, + matrix[1, 0], + matrix[1, 1], + matrix[1, 2], + 0, + matrix[2, 0], + matrix[2, 1], + matrix[2, 2], + 0, + 0, + 0, + 0, + 1); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLut8TagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLut8TagDataEntry.cs new file mode 100644 index 0000000..aaade54 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLut8TagDataEntry.cs @@ -0,0 +1,169 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Linq; +using System.Numerics; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This structure represents a color transform using tables + /// with 8-bit precision. + /// + internal sealed class IccLut8TagDataEntry : IccTagDataEntry, IEquatable + { + private static readonly float[,] IdentityMatrix = + { + { 1, 0, 0 }, + { 0, 1, 0 }, + { 0, 0, 1 } + }; + + /// + /// Initializes a new instance of the class. + /// + /// Input LUT + /// CLUT + /// Output LUT + public IccLut8TagDataEntry(IccLut[] inputValues, IccClut clutValues, IccLut[] outputValues) + : this(IdentityMatrix, inputValues, clutValues, outputValues, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Input LUT + /// CLUT + /// Output LUT + /// Tag Signature + public IccLut8TagDataEntry(IccLut[] inputValues, IccClut clutValues, IccLut[] outputValues, IccProfileTag tagSignature) + : this(IdentityMatrix, inputValues, clutValues, outputValues, tagSignature) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Conversion matrix (must be 3x3) + /// Input LUT + /// CLUT + /// Output LUT + public IccLut8TagDataEntry(float[,] matrix, IccLut[] inputValues, IccClut clutValues, IccLut[] outputValues) + : this(matrix, inputValues, clutValues, outputValues, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Conversion matrix (must be 3x3) + /// Input LUT + /// CLUT + /// Output LUT + /// Tag Signature + public IccLut8TagDataEntry(float[,] matrix, IccLut[] inputValues, IccClut clutValues, IccLut[] outputValues, IccProfileTag tagSignature) + : base(IccTypeSignature.Lut8, tagSignature) + { + Guard.NotNull(matrix, nameof(matrix)); + + bool is3By3 = matrix.GetLength(0) == 3 && matrix.GetLength(1) == 3; + Guard.IsTrue(is3By3, nameof(matrix), "Matrix must have a size of three by three"); + + this.Matrix = CreateMatrix(matrix); + this.InputValues = inputValues ?? throw new ArgumentNullException(nameof(inputValues)); + this.ClutValues = clutValues ?? throw new ArgumentNullException(nameof(clutValues)); + this.OutputValues = outputValues ?? throw new ArgumentNullException(nameof(outputValues)); + + Guard.IsTrue(this.InputChannelCount == clutValues.InputChannelCount, nameof(clutValues), "Input channel count does not match the CLUT size"); + Guard.IsTrue(this.OutputChannelCount == clutValues.OutputChannelCount, nameof(clutValues), "Output channel count does not match the CLUT size"); + + Guard.IsFalse(inputValues.Any(t => t.Values.Length != 256), nameof(inputValues), "Input lookup table has to have a length of 256"); + Guard.IsFalse(outputValues.Any(t => t.Values.Length != 256), nameof(outputValues), "Output lookup table has to have a length of 256"); + } + + /// + /// Gets the number of input channels + /// + public int InputChannelCount => this.InputValues.Length; + + /// + /// Gets the number of output channels + /// + public int OutputChannelCount => this.OutputValues.Length; + + /// + /// Gets the conversion matrix + /// + public Matrix4x4 Matrix { get; } + + /// + /// Gets the input lookup table + /// + public IccLut[] InputValues { get; } + + /// + /// Gets the color lookup table + /// + public IccClut ClutValues { get; } + + /// + /// Gets the output lookup table + /// + public IccLut[] OutputValues { get; } + + /// + public override bool Equals(IccTagDataEntry? other) => other is IccLut8TagDataEntry entry && this.Equals(entry); + + /// + public bool Equals(IccLut8TagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) + && this.Matrix.Equals(other.Matrix) + && this.InputValues.AsSpan().SequenceEqual(other.InputValues) + && this.ClutValues.Equals(other.ClutValues) + && this.OutputValues.AsSpan().SequenceEqual(other.OutputValues); + } + + /// + public override bool Equals(object? obj) => obj is IccLut8TagDataEntry other && this.Equals(other); + + /// + public override int GetHashCode() + => HashCode.Combine( + this.Signature, + this.Matrix, + this.InputValues, + this.ClutValues, + this.OutputValues); + + private static Matrix4x4 CreateMatrix(float[,] matrix) + => new( + matrix[0, 0], + matrix[0, 1], + matrix[0, 2], + 0, + matrix[1, 0], + matrix[1, 1], + matrix[1, 2], + 0, + matrix[2, 0], + matrix[2, 1], + matrix[2, 2], + 0, + 0, + 0, + 0, + 1); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs new file mode 100644 index 0000000..23a1ec4 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs @@ -0,0 +1,306 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +// TODO: Review the use of base IccTagDataEntry comparison. +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This structure represents a color transform. + /// + internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// B Curve + /// Two dimensional conversion matrix (3x3) + /// One dimensional conversion matrix (3x1) + /// M Curve + /// CLUT + /// A Curve + public IccLutAToBTagDataEntry( + IccTagDataEntry[] curveB, + float[,] matrix3x3, + float[] matrix3x1, + IccTagDataEntry[] curveM, + IccClut clutValues, + IccTagDataEntry[] curveA) + : this(curveB, matrix3x3, matrix3x1, curveM, clutValues, curveA, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// B Curve + /// Two dimensional conversion matrix (3x3) + /// One dimensional conversion matrix (3x1) + /// M Curve + /// CLUT + /// A Curve + /// Tag Signature + public IccLutAToBTagDataEntry( + IccTagDataEntry[] curveB, + float[,] matrix3x3, + float[] matrix3x1, + IccTagDataEntry[] curveM, + IccClut clutValues, + IccTagDataEntry[] curveA, + IccProfileTag tagSignature) + : base(IccTypeSignature.LutAToB, tagSignature) + { + VerifyMatrix(matrix3x3, matrix3x1); + this.VerifyCurve(curveA, nameof(curveA)); + this.VerifyCurve(curveB, nameof(curveB)); + this.VerifyCurve(curveM, nameof(curveM)); + + this.Matrix3x3 = CreateMatrix3x3(matrix3x3); + this.Matrix3x1 = CreateMatrix3x1(matrix3x1); + this.CurveA = curveA; + this.CurveB = curveB; + this.CurveM = curveM; + this.ClutValues = clutValues; + + (this.InputChannelCount, this.OutputChannelCount) = this.GetChannelCounts(); + } + + /// + /// Gets the number of input channels + /// + public int InputChannelCount { get; } + + /// + /// Gets the number of output channels + /// + public int OutputChannelCount { get; } + + /// + /// Gets the two dimensional conversion matrix (3x3) + /// + public Matrix4x4? Matrix3x3 { get; } + + /// + /// Gets the one dimensional conversion matrix (3x1) + /// + public Vector3? Matrix3x1 { get; } + + /// + /// Gets the color lookup table + /// + public IccClut ClutValues { get; } + + /// + /// Gets the B Curve + /// + public IccTagDataEntry[] CurveB { get; } + + /// + /// Gets the M Curve + /// + public IccTagDataEntry[] CurveM { get; } + + /// + /// Gets the A Curve + /// + public IccTagDataEntry[] CurveA { get; } + + /// + public override bool Equals(IccTagDataEntry other) => other is IccLutAToBTagDataEntry entry && this.Equals(entry); + + /// + public bool Equals(IccLutAToBTagDataEntry other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) + && this.InputChannelCount == other.InputChannelCount + && this.OutputChannelCount == other.OutputChannelCount + && this.Matrix3x3.Equals(other.Matrix3x3) + && this.Matrix3x1.Equals(other.Matrix3x1) + && Equals(this.ClutValues, other.ClutValues) + && EqualsCurve(this.CurveB, other.CurveB) + && EqualsCurve(this.CurveM, other.CurveM) + && EqualsCurve(this.CurveA, other.CurveA); + } + + /// + public override bool Equals(object obj) => obj is IccLutAToBTagDataEntry other && this.Equals(other); + + /// + public override int GetHashCode() + { + HashCode hashCode = default; + + hashCode.Add(this.Signature); + hashCode.Add(this.InputChannelCount); + hashCode.Add(this.OutputChannelCount); + hashCode.Add(this.Matrix3x3); + hashCode.Add(this.Matrix3x1); + hashCode.Add(this.ClutValues); + hashCode.Add(this.CurveB); + hashCode.Add(this.CurveM); + hashCode.Add(this.CurveA); + + return hashCode.ToHashCode(); + } + + /// + /// Compares two curve arrays, treating consistently. + /// + private static bool EqualsCurve(IccTagDataEntry[] thisCurves, IccTagDataEntry[] entryCurves) + { + bool thisNull = thisCurves is null; + bool entryNull = entryCurves is null; + + if (thisNull && entryNull) + { + return true; + } + + if (thisNull || entryNull) + { + return false; + } + + return thisCurves.SequenceEqual(entryCurves); + } + + /// + /// Validates the configured processing stages and derives the external channel counts. + /// + /// + /// Stages are evaluated in ICC mAB order: A, CLUT, M, Matrix, B. + /// Sparse pipelines are valid as long as adjacent stages agree on channel counts. + /// + private (int InputChannelCount, int OutputChannelCount) GetChannelCounts() + { + // There are at most five possible mAB stages: A, CLUT, M, Matrix, and B. + List<(int Input, int Output, string Name)> stages = new(5); + + if (this.CurveA != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveA.Length, 1, 15, nameof(this.CurveA)); + stages.Add((this.CurveA.Length, this.CurveA.Length, nameof(this.CurveA))); + } + + if (this.ClutValues != null) + { + stages.Add((this.ClutValues.InputChannelCount, this.ClutValues.OutputChannelCount, nameof(this.ClutValues))); + } + + if (this.CurveM != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveM.Length, 1, 15, nameof(this.CurveM)); + stages.Add((this.CurveM.Length, this.CurveM.Length, nameof(this.CurveM))); + } + + if (this.Matrix3x3 != null || this.Matrix3x1 != null) + { + Guard.IsTrue(this.Matrix3x3 != null && this.Matrix3x1 != null, nameof(this.Matrix3x3), "Matrix must include both the 3x3 and 3x1 components"); + stages.Add((3, 3, nameof(this.Matrix3x3))); + } + + if (this.CurveB != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveB.Length, 1, 15, nameof(this.CurveB)); + stages.Add((this.CurveB.Length, this.CurveB.Length, nameof(this.CurveB))); + } + + Guard.IsTrue(stages.Count > 0, nameof(this.CurveB), "AToB tag must contain at least one processing element"); + + for (int i = 1; i < stages.Count; i++) + { + Guard.IsTrue( + stages[i - 1].Output == stages[i].Input, + stages[i].Name, + $"Output channel count of {stages[i - 1].Name} does not match input channel count of {stages[i].Name}"); + } + + return (stages[0].Input, stages[^1].Output); + } + + /// + /// Verifies that every supplied curve entry is a supported one-dimensional curve type. + /// + private void VerifyCurve(IccTagDataEntry[] curves, string name) + { + if (curves != null) + { + bool isNotCurve = curves.Any(t => t is not IccParametricCurveTagDataEntry and not IccCurveTagDataEntry); + Guard.IsFalse(isNotCurve, nameof(name), $"{nameof(name)} must be of type {nameof(IccParametricCurveTagDataEntry)} or {nameof(IccCurveTagDataEntry)}"); + } + } + + /// + /// Verifies the dimensions of the optional matrix components. + /// + private static void VerifyMatrix(float[,] matrix3x3, float[] matrix3x1) + { + if (matrix3x1 != null) + { + Guard.IsTrue(matrix3x1.Length == 3, nameof(matrix3x1), "Matrix must have a size of three"); + } + + if (matrix3x3 != null) + { + bool is3By3 = matrix3x3.GetLength(0) == 3 && matrix3x3.GetLength(1) == 3; + Guard.IsTrue(is3By3, nameof(matrix3x3), "Matrix must have a size of three by three"); + } + } + + /// + /// Creates the one-dimensional matrix vector when present. + /// + private static Vector3? CreateMatrix3x1(float[] matrix) + { + if (matrix is null) + { + return null; + } + + return new Vector3(matrix[0], matrix[1], matrix[2]); + } + + /// + /// Creates the three-by-three matrix when present. + /// + private static Matrix4x4? CreateMatrix3x3(float[,] matrix) + { + if (matrix is null) + { + return null; + } + + return new Matrix4x4( + matrix[0, 0], + matrix[0, 1], + matrix[0, 2], + 0, + matrix[1, 0], + matrix[1, 1], + matrix[1, 2], + 0, + matrix[2, 0], + matrix[2, 1], + matrix[2, 2], + 0, + 0, + 0, + 0, + 1); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs new file mode 100644 index 0000000..6104ab7 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs @@ -0,0 +1,305 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +// TODO: Review the use of base IccTagDataEntry comparison. +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This structure represents a color transform. + /// + internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// B Curve + /// Two dimensional conversion matrix (3x3) + /// One dimensional conversion matrix (3x1) + /// M Curve + /// CLUT + /// A Curve + public IccLutBToATagDataEntry( + IccTagDataEntry[] curveB, + float[,] matrix3x3, + float[] matrix3x1, + IccTagDataEntry[] curveM, + IccClut clutValues, + IccTagDataEntry[] curveA) + : this(curveB, matrix3x3, matrix3x1, curveM, clutValues, curveA, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// B Curve + /// Two dimensional conversion matrix (3x3) + /// One dimensional conversion matrix (3x1) + /// M Curve + /// CLUT + /// A Curve + /// Tag Signature + public IccLutBToATagDataEntry( + IccTagDataEntry[] curveB, + float[,] matrix3x3, + float[] matrix3x1, + IccTagDataEntry[] curveM, + IccClut clutValues, + IccTagDataEntry[] curveA, + IccProfileTag tagSignature) + : base(IccTypeSignature.LutBToA, tagSignature) + { + VerifyMatrix(matrix3x3, matrix3x1); + this.VerifyCurve(curveA, nameof(curveA)); + this.VerifyCurve(curveB, nameof(curveB)); + this.VerifyCurve(curveM, nameof(curveM)); + + this.Matrix3x3 = CreateMatrix3x3(matrix3x3); + this.Matrix3x1 = CreateMatrix3x1(matrix3x1); + this.CurveA = curveA; + this.CurveB = curveB; + this.CurveM = curveM; + this.ClutValues = clutValues; + + (this.InputChannelCount, this.OutputChannelCount) = this.GetChannelCounts(); + } + + /// + /// Gets the number of input channels + /// + public int InputChannelCount { get; } + + /// + /// Gets the number of output channels + /// + public int OutputChannelCount { get; } + + /// + /// Gets the two dimensional conversion matrix (3x3) + /// + public Matrix4x4? Matrix3x3 { get; } + + /// + /// Gets the one dimensional conversion matrix (3x1) + /// + public Vector3? Matrix3x1 { get; } + + /// + /// Gets the color lookup table + /// + public IccClut ClutValues { get; } + + /// + /// Gets the B Curve + /// + public IccTagDataEntry[] CurveB { get; } + + /// + /// Gets the M Curve + /// + public IccTagDataEntry[] CurveM { get; } + + /// + /// Gets the A Curve + /// + public IccTagDataEntry[] CurveA { get; } + + /// + public override bool Equals(IccTagDataEntry other) => other is IccLutBToATagDataEntry entry && this.Equals(entry); + + /// + public bool Equals(IccLutBToATagDataEntry other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) + && this.InputChannelCount == other.InputChannelCount + && this.OutputChannelCount == other.OutputChannelCount + && this.Matrix3x3.Equals(other.Matrix3x3) + && this.Matrix3x1.Equals(other.Matrix3x1) + && Equals(this.ClutValues, other.ClutValues) + && EqualsCurve(this.CurveB, other.CurveB) + && EqualsCurve(this.CurveM, other.CurveM) + && EqualsCurve(this.CurveA, other.CurveA); + } + + /// + public override bool Equals(object obj) => obj is IccLutBToATagDataEntry other && this.Equals(other); + + /// + public override int GetHashCode() + { + HashCode hashCode = default; + hashCode.Add(this.Signature); + hashCode.Add(this.InputChannelCount); + hashCode.Add(this.OutputChannelCount); + hashCode.Add(this.Matrix3x3); + hashCode.Add(this.Matrix3x1); + hashCode.Add(this.ClutValues); + hashCode.Add(this.CurveB); + hashCode.Add(this.CurveM); + hashCode.Add(this.CurveA); + + return hashCode.ToHashCode(); + } + + /// + /// Compares two curve arrays, treating consistently. + /// + private static bool EqualsCurve(IccTagDataEntry[] thisCurves, IccTagDataEntry[] entryCurves) + { + bool thisNull = thisCurves is null; + bool entryNull = entryCurves is null; + + if (thisNull && entryNull) + { + return true; + } + + if (thisNull || entryNull) + { + return false; + } + + return thisCurves.SequenceEqual(entryCurves); + } + + /// + /// Validates the configured processing stages and derives the external channel counts. + /// + /// + /// Stages are evaluated in ICC mBA order: B, Matrix, M, CLUT, A. + /// Sparse pipelines are valid as long as adjacent stages agree on channel counts. + /// + private (int InputChannelCount, int OutputChannelCount) GetChannelCounts() + { + // There are at most five possible mBA stages: B, Matrix, M, CLUT, and A. + List<(int Input, int Output, string Name)> stages = new(5); + + if (this.CurveB != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveB.Length, 1, 15, nameof(this.CurveB)); + stages.Add((this.CurveB.Length, this.CurveB.Length, nameof(this.CurveB))); + } + + if (this.Matrix3x3 != null || this.Matrix3x1 != null) + { + Guard.IsTrue(this.Matrix3x3 != null && this.Matrix3x1 != null, nameof(this.Matrix3x3), "Matrix must include both the 3x3 and 3x1 components"); + stages.Add((3, 3, nameof(this.Matrix3x3))); + } + + if (this.CurveM != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveM.Length, 1, 15, nameof(this.CurveM)); + stages.Add((this.CurveM.Length, this.CurveM.Length, nameof(this.CurveM))); + } + + if (this.ClutValues != null) + { + stages.Add((this.ClutValues.InputChannelCount, this.ClutValues.OutputChannelCount, nameof(this.ClutValues))); + } + + if (this.CurveA != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveA.Length, 1, 15, nameof(this.CurveA)); + stages.Add((this.CurveA.Length, this.CurveA.Length, nameof(this.CurveA))); + } + + Guard.IsTrue(stages.Count > 0, nameof(this.CurveB), "BToA tag must contain at least one processing element"); + + for (int i = 1; i < stages.Count; i++) + { + Guard.IsTrue( + stages[i - 1].Output == stages[i].Input, + stages[i].Name, + $"Output channel count of {stages[i - 1].Name} does not match input channel count of {stages[i].Name}"); + } + + return (stages[0].Input, stages[^1].Output); + } + + /// + /// Verifies that every supplied curve entry is a supported one-dimensional curve type. + /// + private void VerifyCurve(IccTagDataEntry[] curves, string name) + { + if (curves != null) + { + bool isNotCurve = curves.Any(t => t is not IccParametricCurveTagDataEntry and not IccCurveTagDataEntry); + Guard.IsFalse(isNotCurve, nameof(name), $"{nameof(name)} must be of type {nameof(IccParametricCurveTagDataEntry)} or {nameof(IccCurveTagDataEntry)}"); + } + } + + /// + /// Verifies the dimensions of the optional matrix components. + /// + private static void VerifyMatrix(float[,] matrix3x3, float[] matrix3x1) + { + if (matrix3x1 != null) + { + Guard.IsTrue(matrix3x1.Length == 3, nameof(matrix3x1), "Matrix must have a size of three"); + } + + if (matrix3x3 != null) + { + bool is3By3 = matrix3x3.GetLength(0) == 3 && matrix3x3.GetLength(1) == 3; + Guard.IsTrue(is3By3, nameof(matrix3x3), "Matrix must have a size of three by three"); + } + } + + /// + /// Creates the one-dimensional matrix vector when present. + /// + private static Vector3? CreateMatrix3x1(float[] matrix) + { + if (matrix is null) + { + return null; + } + + return new Vector3(matrix[0], matrix[1], matrix[2]); + } + + /// + /// Creates the three-by-three matrix when present. + /// + private static Matrix4x4? CreateMatrix3x3(float[,] matrix) + { + if (matrix is null) + { + return null; + } + + return new Matrix4x4( + matrix[0, 0], + matrix[0, 1], + matrix[0, 2], + 0, + matrix[1, 0], + matrix[1, 1], + matrix[1, 2], + 0, + matrix[2, 0], + matrix[2, 1], + matrix[2, 2], + 0, + 0, + 0, + 0, + 1); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccMeasurementTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccMeasurementTagDataEntry.cs new file mode 100644 index 0000000..7d64ba3 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccMeasurementTagDataEntry.cs @@ -0,0 +1,117 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// The measurementType information refers only to the internal + /// profile data and is meant to provide profile makers an alternative + /// to the default measurement specifications. + /// + internal sealed class IccMeasurementTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// Observer + /// XYZ Backing values + /// Geometry + /// Flare + /// Illuminant + public IccMeasurementTagDataEntry(IccStandardObserver observer, Vector3 xyzBacking, IccMeasurementGeometry geometry, float flare, IccStandardIlluminant illuminant) + : this(observer, xyzBacking, geometry, flare, illuminant, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Observer + /// XYZ Backing values + /// Geometry + /// Flare + /// Illuminant + /// Tag Signature + public IccMeasurementTagDataEntry(IccStandardObserver observer, Vector3 xyzBacking, IccMeasurementGeometry geometry, float flare, IccStandardIlluminant illuminant, IccProfileTag tagSignature) + : base(IccTypeSignature.Measurement, tagSignature) + { + this.Observer = observer; + this.XyzBacking = xyzBacking; + this.Geometry = geometry; + this.Flare = flare; + this.Illuminant = illuminant; + } + + /// + /// Gets the observer + /// + public IccStandardObserver Observer { get; } + + /// + /// Gets the XYZ Backing values + /// + public Vector3 XyzBacking { get; } + + /// + /// Gets the geometry + /// + public IccMeasurementGeometry Geometry { get; } + + /// + /// Gets the flare + /// + public float Flare { get; } + + /// + /// Gets the illuminant + /// + public IccStandardIlluminant Illuminant { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + { + return other is IccMeasurementTagDataEntry entry && this.Equals(entry); + } + + /// + public bool Equals(IccMeasurementTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) + && this.Observer == other.Observer + && this.XyzBacking.Equals(other.XyzBacking) + && this.Geometry == other.Geometry + && this.Flare.Equals(other.Flare) + && this.Illuminant == other.Illuminant; + } + + /// + public override bool Equals(object? obj) + { + return obj is IccMeasurementTagDataEntry other && this.Equals(other); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine( + this.Signature, + this.Observer, + this.XyzBacking, + this.Geometry, + this.Flare, + this.Illuminant); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccMultiLocalizedUnicodeTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccMultiLocalizedUnicodeTagDataEntry.cs new file mode 100644 index 0000000..6b5e6de --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccMultiLocalizedUnicodeTagDataEntry.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This tag structure contains a set of records each referencing + /// a multilingual string associated with a profile. + /// + internal sealed class IccMultiLocalizedUnicodeTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// Localized Text + public IccMultiLocalizedUnicodeTagDataEntry(IccLocalizedString[] texts) + : this(texts, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Localized Text + /// Tag Signature + public IccMultiLocalizedUnicodeTagDataEntry(IccLocalizedString[] texts, IccProfileTag tagSignature) + : base(IccTypeSignature.MultiLocalizedUnicode, tagSignature) + { + this.Texts = texts ?? throw new ArgumentNullException(nameof(texts)); + } + + /// + /// Gets the localized texts + /// + public IccLocalizedString[] Texts { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + { + return other is IccMultiLocalizedUnicodeTagDataEntry entry && this.Equals(entry); + } + + /// + public bool Equals(IccMultiLocalizedUnicodeTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && this.Texts.AsSpan().SequenceEqual(other.Texts); + } + + /// + public override bool Equals(object? obj) + { + return obj is IccMultiLocalizedUnicodeTagDataEntry other && this.Equals(other); + } + + /// + public override int GetHashCode() => HashCode.Combine(this.Signature, this.Texts); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccMultiProcessElementsTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccMultiProcessElementsTagDataEntry.cs new file mode 100644 index 0000000..6cc0435 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccMultiProcessElementsTagDataEntry.cs @@ -0,0 +1,93 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Linq; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This structure represents a color transform, containing + /// a sequence of processing elements. + /// + internal sealed class IccMultiProcessElementsTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// Processing elements + public IccMultiProcessElementsTagDataEntry(IccMultiProcessElement[] data) + : this(data, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Processing elements + /// Tag Signature + public IccMultiProcessElementsTagDataEntry(IccMultiProcessElement[] data, IccProfileTag tagSignature) + : base(IccTypeSignature.MultiProcessElements, tagSignature) + { + Guard.NotNull(data, nameof(data)); + Guard.IsTrue(data.Length > 0, nameof(data), $"{nameof(data)} must have at least one element"); + + this.InputChannelCount = data[0].InputChannelCount; + this.OutputChannelCount = data[0].OutputChannelCount; + this.Data = data; + + bool channelsNotSame = data.Any(t => t.InputChannelCount != this.InputChannelCount || t.OutputChannelCount != this.OutputChannelCount); + Guard.IsFalse(channelsNotSame, nameof(data), "The number of input and output channels are not the same for all elements"); + } + + /// + /// Gets the number of input channels + /// + public int InputChannelCount { get; } + + /// + /// Gets the number of output channels + /// + public int OutputChannelCount { get; } + + /// + /// Gets the processing elements + /// + public IccMultiProcessElement[] Data { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + => other is IccMultiProcessElementsTagDataEntry entry && this.Equals(entry); + + /// + public bool Equals(IccMultiProcessElementsTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) + && this.InputChannelCount == other.InputChannelCount + && this.OutputChannelCount == other.OutputChannelCount + && this.Data.AsSpan().SequenceEqual(other.Data); + } + + /// + public override bool Equals(object? obj) => obj is IccMultiProcessElementsTagDataEntry other && this.Equals(other); + + /// + public override int GetHashCode() + { + return HashCode.Combine( + this.Signature, + this.InputChannelCount, + this.OutputChannelCount, + this.Data); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccNamedColor2TagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccNamedColor2TagDataEntry.cs new file mode 100644 index 0000000..91b2279 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccNamedColor2TagDataEntry.cs @@ -0,0 +1,160 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Linq; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// The namedColor2Type is a count value and array of structures + /// that provide color coordinates for color names. + /// + internal sealed class IccNamedColor2TagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The named colors + public IccNamedColor2TagDataEntry(IccNamedColor[] colors) + : this(0, null, null, colors, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Prefix + /// Suffix + /// /// The named colors + public IccNamedColor2TagDataEntry(string prefix, string suffix, IccNamedColor[] colors) + : this(0, prefix, suffix, colors, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Vendor specific flags + /// Prefix + /// Suffix + /// The named colors + public IccNamedColor2TagDataEntry(int vendorFlags, string prefix, string suffix, IccNamedColor[] colors) + : this(vendorFlags, prefix, suffix, colors, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The named colors + /// Tag Signature + public IccNamedColor2TagDataEntry(IccNamedColor[] colors, IccProfileTag tagSignature) + : this(0, null, null, colors, tagSignature) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Prefix + /// Suffix + /// The named colors + /// Tag Signature + public IccNamedColor2TagDataEntry(string prefix, string suffix, IccNamedColor[] colors, IccProfileTag tagSignature) + : this(0, prefix, suffix, colors, tagSignature) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Vendor specific flags + /// Prefix + /// Suffix + /// The named colors + /// Tag Signature + public IccNamedColor2TagDataEntry(int vendorFlags, string? prefix, string? suffix, IccNamedColor[] colors, IccProfileTag tagSignature) + : base(IccTypeSignature.NamedColor2, tagSignature) + { + Guard.NotNull(colors, nameof(colors)); + + int coordinateCount = 0; + if (colors.Length > 0) + { + coordinateCount = colors[0].DeviceCoordinates?.Length ?? 0; + + Guard.IsFalse(colors.Any(t => (t.DeviceCoordinates?.Length ?? 0) != coordinateCount), nameof(colors), "Device coordinate count must be the same for all colors"); + } + + this.VendorFlags = vendorFlags; + this.CoordinateCount = coordinateCount; + this.Prefix = prefix; + this.Suffix = suffix; + this.Colors = colors; + } + + /// + /// Gets the number of coordinates + /// + public int CoordinateCount { get; } + + /// + /// Gets the prefix + /// + public string? Prefix { get; } + + /// + /// Gets the suffix + /// + public string? Suffix { get; } + + /// + /// Gets the vendor specific flags + /// + public int VendorFlags { get; } + + /// + /// Gets the named colors + /// + public IccNamedColor[] Colors { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + => other is IccNamedColor2TagDataEntry entry && this.Equals(entry); + + /// + public bool Equals(IccNamedColor2TagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) + && this.CoordinateCount == other.CoordinateCount + && string.Equals(this.Prefix, other.Prefix, StringComparison.OrdinalIgnoreCase) + && string.Equals(this.Suffix, other.Suffix, StringComparison.OrdinalIgnoreCase) + && this.VendorFlags == other.VendorFlags + && this.Colors.AsSpan().SequenceEqual(other.Colors); + } + + /// + public override bool Equals(object? obj) + => obj is IccNamedColor2TagDataEntry other && this.Equals(other); + + /// + public override int GetHashCode() + => HashCode.Combine( + this.Signature, + this.CoordinateCount, + this.Prefix, + this.Suffix, + this.VendorFlags, + this.Colors); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccParametricCurveTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccParametricCurveTagDataEntry.cs new file mode 100644 index 0000000..db93d53 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccParametricCurveTagDataEntry.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// The parametricCurveType describes a one-dimensional curve by + /// specifying one of a predefined set of functions using the parameters. + /// + internal sealed class IccParametricCurveTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The Curve + public IccParametricCurveTagDataEntry(IccParametricCurve curve) + : this(curve, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Curve + /// Tag Signature + public IccParametricCurveTagDataEntry(IccParametricCurve curve, IccProfileTag tagSignature) + : base(IccTypeSignature.ParametricCurve, tagSignature) + { + this.Curve = curve ?? throw new ArgumentNullException(nameof(curve)); + } + + /// + /// Gets the Curve + /// + public IccParametricCurve Curve { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + { + return other is IccParametricCurveTagDataEntry entry && this.Equals(entry); + } + + /// + public bool Equals(IccParametricCurveTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && this.Curve.Equals(other.Curve); + } + + /// + public override bool Equals(object? obj) + { + return obj is IccParametricCurveTagDataEntry other && this.Equals(other); + } + + /// + public override int GetHashCode() => HashCode.Combine(this.Signature, this.Curve); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccProfileSequenceDescTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccProfileSequenceDescTagDataEntry.cs new file mode 100644 index 0000000..39fd96a --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccProfileSequenceDescTagDataEntry.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This type is an array of structures, each of which contains information + /// from the header fields and tags from the original profiles which were + /// combined to create the final profile. + /// + internal sealed class IccProfileSequenceDescTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// Profile Descriptions + public IccProfileSequenceDescTagDataEntry(IccProfileDescription[] descriptions) + : this(descriptions, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Profile Descriptions + /// Tag Signature + public IccProfileSequenceDescTagDataEntry(IccProfileDescription[] descriptions, IccProfileTag tagSignature) + : base(IccTypeSignature.ProfileSequenceDesc, tagSignature) + => this.Descriptions = descriptions ?? throw new ArgumentNullException(nameof(descriptions)); + + /// + /// Gets the profile descriptions + /// + public IccProfileDescription[] Descriptions { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + => other is IccProfileSequenceDescTagDataEntry entry && this.Equals(entry); + + /// + public bool Equals(IccProfileSequenceDescTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && this.Descriptions.AsSpan().SequenceEqual(other.Descriptions); + } + + /// + public override bool Equals(object? obj) + => obj is IccProfileSequenceDescTagDataEntry other && this.Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(this.Signature, this.Descriptions); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccProfileSequenceIdentifierTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccProfileSequenceIdentifierTagDataEntry.cs new file mode 100644 index 0000000..4c38c4e --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccProfileSequenceIdentifierTagDataEntry.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This type is an array of structures, each of which contains information + /// for identification of a profile used in a sequence. + /// + internal sealed class IccProfileSequenceIdentifierTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// Profile Identifiers + public IccProfileSequenceIdentifierTagDataEntry(IccProfileSequenceIdentifier[] data) + : this(data, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Profile Identifiers + /// Tag Signature + public IccProfileSequenceIdentifierTagDataEntry(IccProfileSequenceIdentifier[] data, IccProfileTag tagSignature) + : base(IccTypeSignature.ProfileSequenceIdentifier, tagSignature) + { + this.Data = data ?? throw new ArgumentNullException(nameof(data)); + } + + /// + /// Gets the profile identifiers + /// + public IccProfileSequenceIdentifier[] Data { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + { + return other is IccProfileSequenceIdentifierTagDataEntry entry && this.Equals(entry); + } + + /// + public bool Equals(IccProfileSequenceIdentifierTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && this.Data.AsSpan().SequenceEqual(other.Data); + } + + /// + public override bool Equals(object? obj) + { + return obj is IccProfileSequenceIdentifierTagDataEntry other && this.Equals(other); + } + + /// + public override int GetHashCode() => HashCode.Combine(this.Signature, this.Data); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccResponseCurveSet16TagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccResponseCurveSet16TagDataEntry.cs new file mode 100644 index 0000000..952a752 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccResponseCurveSet16TagDataEntry.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Linq; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// The purpose of this tag type is to provide a mechanism to relate physical + /// colorant amounts with the normalized device codes produced by lut8Type, lut16Type, + /// lutAToBType, lutBToAType or multiProcessElementsType tags so that corrections can + /// be made for variation in the device without having to produce a new profile. + /// + internal sealed class IccResponseCurveSet16TagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The Curves + public IccResponseCurveSet16TagDataEntry(IccResponseCurve[] curves) + : this(curves, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Curves + /// Tag Signature + public IccResponseCurveSet16TagDataEntry(IccResponseCurve[] curves, IccProfileTag tagSignature) + : base(IccTypeSignature.ResponseCurveSet16, tagSignature) + { + Guard.NotNull(curves, nameof(curves)); + Guard.IsTrue(curves.Length > 0, nameof(curves), $"{nameof(curves)} needs at least one element"); + + this.Curves = curves; + this.ChannelCount = (ushort)curves[0].ResponseArrays.Length; + + Guard.IsFalse(curves.Any(t => t.ResponseArrays.Length != this.ChannelCount), nameof(curves), "All curves need to have the same number of channels"); + } + + /// + /// Gets the number of channels + /// + public ushort ChannelCount { get; } + + /// + /// Gets the curves + /// + public IccResponseCurve[] Curves { get; } + + /// + public override bool Equals(IccTagDataEntry? other) => other is IccResponseCurveSet16TagDataEntry entry && this.Equals(entry); + + /// + public bool Equals(IccResponseCurveSet16TagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) + && this.ChannelCount == other.ChannelCount + && this.Curves.AsSpan().SequenceEqual(other.Curves); + } + + /// + public override bool Equals(object? obj) => obj is IccResponseCurveSet16TagDataEntry other && this.Equals(other); + + /// + public override int GetHashCode() + { + return HashCode.Combine( + this.Signature, + this.ChannelCount, + this.Curves); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccScreeningTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccScreeningTagDataEntry.cs new file mode 100644 index 0000000..d1bcf01 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccScreeningTagDataEntry.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This type describes various screening parameters including + /// screen frequency, screening angle, and spot shape. + /// + internal sealed class IccScreeningTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// Screening flags + /// Channel information + public IccScreeningTagDataEntry(IccScreeningFlag flags, IccScreeningChannel[] channels) + : this(flags, channels, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Screening flags + /// Channel information + /// Tag Signature + public IccScreeningTagDataEntry(IccScreeningFlag flags, IccScreeningChannel[] channels, IccProfileTag tagSignature) + : base(IccTypeSignature.Screening, tagSignature) + { + this.Flags = flags; + this.Channels = channels ?? throw new ArgumentNullException(nameof(channels)); + } + + /// + /// Gets the screening flags + /// + public IccScreeningFlag Flags { get; } + + /// + /// Gets the channel information + /// + public IccScreeningChannel[] Channels { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + { + return other is IccScreeningTagDataEntry entry && this.Equals(entry); + } + + /// + public bool Equals(IccScreeningTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) + && this.Flags == other.Flags + && this.Channels.AsSpan().SequenceEqual(other.Channels); + } + + /// + public override bool Equals(object? obj) + { + return obj is IccScreeningTagDataEntry other && this.Equals(other); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(this.Signature, this.Flags, this.Channels); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccSignatureTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccSignatureTagDataEntry.cs new file mode 100644 index 0000000..480a44e --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccSignatureTagDataEntry.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Typically this type is used for registered tags that can + /// be displayed on many development systems as a sequence of four characters. + /// + internal sealed class IccSignatureTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The Signature + public IccSignatureTagDataEntry(string signatureData) + : this(signatureData, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Signature + /// Tag Signature + public IccSignatureTagDataEntry(string signatureData, IccProfileTag tagSignature) + : base(IccTypeSignature.Signature, tagSignature) + => this.SignatureData = signatureData ?? throw new ArgumentNullException(nameof(signatureData)); + + /// + /// Gets the signature data + /// + public string SignatureData { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + => other is IccSignatureTagDataEntry entry && this.Equals(entry); + + /// + public bool Equals(IccSignatureTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) + && string.Equals(this.SignatureData, other.SignatureData, StringComparison.OrdinalIgnoreCase); + } + + /// + public override bool Equals(object? obj) + => obj is IccSignatureTagDataEntry other && this.Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(this.Signature, this.SignatureData); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccTextDescriptionTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccTextDescriptionTagDataEntry.cs new file mode 100644 index 0000000..e5592c7 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccTextDescriptionTagDataEntry.cs @@ -0,0 +1,172 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System; +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// The TextDescriptionType contains three types of text description. + /// + internal sealed class IccTextDescriptionTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// ASCII text + /// Unicode text + /// ScriptCode text + /// Unicode Language-Code + /// ScriptCode Code + public IccTextDescriptionTagDataEntry(string ascii, string unicode, string scriptCode, uint unicodeLanguageCode, ushort scriptCodeCode) + : this(ascii, unicode, scriptCode, unicodeLanguageCode, scriptCodeCode, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// ASCII text + /// Unicode text + /// ScriptCode text + /// Unicode Language-Code + /// ScriptCode Code + /// Tag Signature + public IccTextDescriptionTagDataEntry(string ascii, string unicode, string scriptCode, uint unicodeLanguageCode, ushort scriptCodeCode, IccProfileTag tagSignature) + : base(IccTypeSignature.TextDescription, tagSignature) + { + this.Ascii = ascii; + this.Unicode = unicode; + this.ScriptCode = scriptCode; + this.UnicodeLanguageCode = unicodeLanguageCode; + this.ScriptCodeCode = scriptCodeCode; + } + + /// + /// Gets the ASCII text + /// + public string Ascii { get; } + + /// + /// Gets the Unicode text + /// + public string Unicode { get; } + + /// + /// Gets the ScriptCode text + /// + public string ScriptCode { get; } + + /// + /// Gets the Unicode Language-Code + /// + public uint UnicodeLanguageCode { get; } + + /// + /// Gets the ScriptCode Code + /// + public ushort ScriptCodeCode { get; } + + /// + /// Performs an explicit conversion from + /// to . + /// + /// The entry to convert + /// The converted entry + public static explicit operator IccMultiLocalizedUnicodeTagDataEntry(IccTextDescriptionTagDataEntry textEntry) + { + if (textEntry is null) + { + return null; + } + + IccLocalizedString localString; + if (!string.IsNullOrEmpty(textEntry.Unicode)) + { + CultureInfo culture = GetCulture(textEntry.UnicodeLanguageCode); + localString = culture != null + ? new IccLocalizedString(culture, textEntry.Unicode) + : new IccLocalizedString(textEntry.Unicode); + } + else if (!string.IsNullOrEmpty(textEntry.Ascii)) + { + localString = new IccLocalizedString(textEntry.Ascii); + } + else if (!string.IsNullOrEmpty(textEntry.ScriptCode)) + { + localString = new IccLocalizedString(textEntry.ScriptCode); + } + else + { + localString = new IccLocalizedString(string.Empty); + } + + return new IccMultiLocalizedUnicodeTagDataEntry(new[] { localString }, textEntry.TagSignature); + + static CultureInfo GetCulture(uint value) + { + if (value == 0) + { + return null; + } + + byte p1 = (byte)(value >> 24); + byte p2 = (byte)(value >> 16); + byte p3 = (byte)(value >> 8); + byte p4 = (byte)value; + + // Check if the values are [a-z]{2}[A-Z]{2} + if (p1 >= 0x61 && p1 <= 0x7A + && p2 >= 0x61 && p2 <= 0x7A + && p3 >= 0x41 && p3 <= 0x5A + && p4 >= 0x41 && p4 <= 0x5A) + { + string culture = new(new[] { (char)p1, (char)p2, '-', (char)p3, (char)p4 }); + return new CultureInfo(culture); + } + + return null; + } + } + + /// + public override bool Equals(IccTagDataEntry other) + => other is IccTextDescriptionTagDataEntry entry && this.Equals(entry); + + /// + public bool Equals(IccTextDescriptionTagDataEntry other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) + && string.Equals(this.Ascii, other.Ascii, StringComparison.OrdinalIgnoreCase) + && string.Equals(this.Unicode, other.Unicode, StringComparison.OrdinalIgnoreCase) + && string.Equals(this.ScriptCode, other.ScriptCode, StringComparison.OrdinalIgnoreCase) + && this.UnicodeLanguageCode == other.UnicodeLanguageCode + && this.ScriptCodeCode == other.ScriptCodeCode; + } + + /// + public override bool Equals(object obj) + => obj is IccTextDescriptionTagDataEntry other && this.Equals(other); + + /// + public override int GetHashCode() + => HashCode.Combine( + this.Signature, + this.Ascii, + this.Unicode, + this.ScriptCode, + this.UnicodeLanguageCode, + this.ScriptCodeCode); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccTextTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccTextTagDataEntry.cs new file mode 100644 index 0000000..7841032 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccTextTagDataEntry.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This is a simple text structure that contains a text string. + /// + internal sealed class IccTextTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The Text + public IccTextTagDataEntry(string text) + : this(text, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Text + /// Tag Signature + public IccTextTagDataEntry(string text, IccProfileTag tagSignature) + : base(IccTypeSignature.Text, tagSignature) + => this.Text = text ?? throw new ArgumentNullException(nameof(text)); + + /// + /// Gets the Text + /// + public string Text { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + => other is IccTextTagDataEntry entry && this.Equals(entry); + + /// + public bool Equals(IccTextTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && string.Equals(this.Text, other.Text, StringComparison.OrdinalIgnoreCase); + } + + /// + public override bool Equals(object? obj) + => obj is IccTextTagDataEntry other && this.Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(this.Signature, this.Text); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUFix16ArrayTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUFix16ArrayTagDataEntry.cs new file mode 100644 index 0000000..0c73c9e --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUFix16ArrayTagDataEntry.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This type represents an array of doubles (from 32bit values). + /// + internal sealed class IccUFix16ArrayTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The array data + public IccUFix16ArrayTagDataEntry(float[] data) + : this(data, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The array data + /// Tag Signature + public IccUFix16ArrayTagDataEntry(float[] data, IccProfileTag tagSignature) + : base(IccTypeSignature.U16Fixed16Array, tagSignature) + { + this.Data = data ?? throw new ArgumentNullException(nameof(data)); + } + + /// + /// Gets the array data. + /// + public float[] Data { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + { + return other is IccUFix16ArrayTagDataEntry entry && this.Equals(entry); + } + + /// + public bool Equals(IccUFix16ArrayTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && this.Data.AsSpan().SequenceEqual(other.Data); + } + + /// + public override bool Equals(object? obj) + { + return obj is IccUFix16ArrayTagDataEntry other && this.Equals(other); + } + + /// + public override int GetHashCode() => HashCode.Combine(this.Signature, this.Data); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUInt16ArrayTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUInt16ArrayTagDataEntry.cs new file mode 100644 index 0000000..3c245a1 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUInt16ArrayTagDataEntry.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This type represents an array of unsigned shorts. + /// + internal sealed class IccUInt16ArrayTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The array data + public IccUInt16ArrayTagDataEntry(ushort[] data) + : this(data, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The array data + /// Tag Signature + public IccUInt16ArrayTagDataEntry(ushort[] data, IccProfileTag tagSignature) + : base(IccTypeSignature.UInt16Array, tagSignature) + { + this.Data = data ?? throw new ArgumentNullException(nameof(data)); + } + + /// + /// Gets the array data + /// + public ushort[] Data { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + { + return other is IccUInt16ArrayTagDataEntry entry && this.Equals(entry); + } + + /// + public bool Equals(IccUInt16ArrayTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && this.Data.AsSpan().SequenceEqual(other.Data); + } + + /// + public override bool Equals(object? obj) + { + return obj is IccUInt16ArrayTagDataEntry other && this.Equals(other); + } + + /// + public override int GetHashCode() => HashCode.Combine(this.Signature, this.Data); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUInt32ArrayTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUInt32ArrayTagDataEntry.cs new file mode 100644 index 0000000..8268e36 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUInt32ArrayTagDataEntry.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This type represents an array of unsigned 32bit integers. + /// + internal sealed class IccUInt32ArrayTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The array data + public IccUInt32ArrayTagDataEntry(uint[] data) + : this(data, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The array data + /// Tag Signature + public IccUInt32ArrayTagDataEntry(uint[] data, IccProfileTag tagSignature) + : base(IccTypeSignature.UInt32Array, tagSignature) + { + this.Data = data ?? throw new ArgumentNullException(nameof(data)); + } + + /// + /// Gets the array data + /// + public uint[] Data { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + { + return other is IccUInt32ArrayTagDataEntry entry && this.Equals(entry); + } + + /// + public bool Equals(IccUInt32ArrayTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && this.Data.AsSpan().SequenceEqual(other.Data); + } + + /// + public override bool Equals(object? obj) + { + return obj is IccUInt32ArrayTagDataEntry other && this.Equals(other); + } + + /// + public override int GetHashCode() => HashCode.Combine(this.Signature, this.Data); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUInt64ArrayTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUInt64ArrayTagDataEntry.cs new file mode 100644 index 0000000..190f86c --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUInt64ArrayTagDataEntry.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This type represents an array of unsigned 64bit integers. + /// + internal sealed class IccUInt64ArrayTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The array data + public IccUInt64ArrayTagDataEntry(ulong[] data) + : this(data, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The array data + /// Tag Signature + public IccUInt64ArrayTagDataEntry(ulong[] data, IccProfileTag tagSignature) + : base(IccTypeSignature.UInt64Array, tagSignature) => this.Data = data ?? throw new ArgumentNullException(nameof(data)); + + /// + /// Gets the array data + /// + public ulong[] Data { get; } + + /// + public override bool Equals(IccTagDataEntry? other) => other is IccUInt64ArrayTagDataEntry entry && this.Equals(entry); + + /// + public bool Equals(IccUInt64ArrayTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && this.Data.AsSpan().SequenceEqual(other.Data); + } + + /// + public override bool Equals(object? obj) => obj is IccUInt64ArrayTagDataEntry other && this.Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(this.Signature, this.Data); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUInt8ArrayTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUInt8ArrayTagDataEntry.cs new file mode 100644 index 0000000..f4e061b --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUInt8ArrayTagDataEntry.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This type represents an array of bytes. + /// + internal sealed class IccUInt8ArrayTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The array data + public IccUInt8ArrayTagDataEntry(byte[] data) + : this(data, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The array data + /// Tag Signature + public IccUInt8ArrayTagDataEntry(byte[] data, IccProfileTag tagSignature) + : base(IccTypeSignature.UInt8Array, tagSignature) + { + this.Data = data ?? throw new ArgumentNullException(nameof(data)); + } + + /// + /// Gets the array data. + /// + public byte[] Data { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + { + return other is IccUInt8ArrayTagDataEntry entry && this.Equals(entry); + } + + /// + public bool Equals(IccUInt8ArrayTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && this.Data.AsSpan().SequenceEqual(other.Data); + } + + /// + public override bool Equals(object? obj) + { + return obj is IccUInt8ArrayTagDataEntry other && this.Equals(other); + } + + /// + public override int GetHashCode() => HashCode.Combine(this.Signature, this.Data); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUcrBgTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUcrBgTagDataEntry.cs new file mode 100644 index 0000000..25d6bf2 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUcrBgTagDataEntry.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This type contains curves representing the under color removal and black generation + /// and a text string which is a general description of the method used for the UCR and BG. + /// + internal sealed class IccUcrBgTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// UCR (under color removal) curve values + /// BG (black generation) curve values + /// Description of the used UCR and BG method + public IccUcrBgTagDataEntry(ushort[] ucrCurve, ushort[] bgCurve, string description) + : this(ucrCurve, bgCurve, description, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// UCR (under color removal) curve values + /// BG (black generation) curve values + /// Description of the used UCR and BG method + /// Tag Signature + public IccUcrBgTagDataEntry(ushort[] ucrCurve, ushort[] bgCurve, string description, IccProfileTag tagSignature) + : base(IccTypeSignature.UcrBg, tagSignature) + { + this.UcrCurve = ucrCurve ?? throw new ArgumentNullException(nameof(ucrCurve)); + this.BgCurve = bgCurve ?? throw new ArgumentNullException(nameof(bgCurve)); + this.Description = description ?? throw new ArgumentNullException(nameof(description)); + } + + /// + /// Gets the UCR (under color removal) curve values + /// + public ushort[] UcrCurve { get; } + + /// + /// Gets the BG (black generation) curve values + /// + public ushort[] BgCurve { get; } + + /// + /// Gets a description of the used UCR and BG method + /// + public string Description { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + => other is IccUcrBgTagDataEntry entry && this.Equals(entry); + + /// + public bool Equals(IccUcrBgTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) + && this.UcrCurve.AsSpan().SequenceEqual(other.UcrCurve) + && this.BgCurve.AsSpan().SequenceEqual(other.BgCurve) + && string.Equals(this.Description, other.Description, StringComparison.OrdinalIgnoreCase); + } + + /// + public override bool Equals(object? obj) + => obj is IccUcrBgTagDataEntry other && this.Equals(other); + + /// + public override int GetHashCode() + => HashCode.Combine( + this.Signature, + this.UcrCurve, + this.BgCurve, + this.Description); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUnknownTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUnknownTagDataEntry.cs new file mode 100644 index 0000000..38c83a7 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUnknownTagDataEntry.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This tag stores data of an unknown tag data entry + /// + internal sealed class IccUnknownTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The raw data of the entry + public IccUnknownTagDataEntry(byte[] data) + : this(data, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The raw data of the entry + /// Tag Signature + public IccUnknownTagDataEntry(byte[] data, IccProfileTag tagSignature) + : base(IccTypeSignature.Unknown, tagSignature) + { + this.Data = data ?? throw new ArgumentNullException(nameof(data)); + } + + /// + /// Gets the raw data of the entry. + /// + public byte[] Data { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + { + return other is IccUnknownTagDataEntry entry && this.Equals(entry); + } + + /// + public bool Equals(IccUnknownTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && this.Data.AsSpan().SequenceEqual(other.Data); + } + + /// + public override bool Equals(object? obj) + { + return obj is IccUnknownTagDataEntry other && this.Equals(other); + } + + /// + public override int GetHashCode() => HashCode.Combine(this.Signature, this.Data); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccViewingConditionsTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccViewingConditionsTagDataEntry.cs new file mode 100644 index 0000000..ea07f12 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccViewingConditionsTagDataEntry.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// This type represents a set of viewing condition parameters. + /// + internal sealed class IccViewingConditionsTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// XYZ values of Illuminant + /// XYZ values of Surrounding + /// Illuminant + public IccViewingConditionsTagDataEntry(Vector3 illuminantXyz, Vector3 surroundXyz, IccStandardIlluminant illuminant) + : this(illuminantXyz, surroundXyz, illuminant, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// XYZ values of Illuminant + /// XYZ values of Surrounding + /// Illuminant + /// Tag Signature + public IccViewingConditionsTagDataEntry(Vector3 illuminantXyz, Vector3 surroundXyz, IccStandardIlluminant illuminant, IccProfileTag tagSignature) + : base(IccTypeSignature.ViewingConditions, tagSignature) + { + this.IlluminantXyz = illuminantXyz; + this.SurroundXyz = surroundXyz; + this.Illuminant = illuminant; + } + + /// + /// Gets the XYZ values of illuminant. + /// + public Vector3 IlluminantXyz { get; } + + /// + /// Gets the XYZ values of Surrounding + /// + public Vector3 SurroundXyz { get; } + + /// + /// Gets the illuminant. + /// + public IccStandardIlluminant Illuminant { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + { + return other is IccViewingConditionsTagDataEntry entry && this.Equals(entry); + } + + /// + public bool Equals(IccViewingConditionsTagDataEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) + && this.IlluminantXyz.Equals(other.IlluminantXyz) + && this.SurroundXyz.Equals(other.SurroundXyz) + && this.Illuminant == other.Illuminant; + } + + /// + public override bool Equals(object? obj) + { + return obj is IccViewingConditionsTagDataEntry other && this.Equals(other); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine( + this.Signature, + this.IlluminantXyz, + this.SurroundXyz, + this.Illuminant); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccXyzTagDataEntry.cs b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccXyzTagDataEntry.cs new file mode 100644 index 0000000..78bca3a --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccXyzTagDataEntry.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// The XYZType contains an array of XYZ values. + /// + internal sealed class IccXyzTagDataEntry : IccTagDataEntry, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The XYZ numbers. + public IccXyzTagDataEntry(Vector3[] data) + : this(data, IccProfileTag.Unknown) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The XYZ numbers + /// Tag Signature + public IccXyzTagDataEntry(Vector3[] data, IccProfileTag tagSignature) + : base(IccTypeSignature.Xyz, tagSignature) + => this.Data = data ?? throw new ArgumentNullException(nameof(data)); + + /// + /// Gets the XYZ numbers. + /// + public Vector3[] Data { get; } + + /// + public override bool Equals(IccTagDataEntry? other) + { + if (base.Equals(other) && other is IccXyzTagDataEntry entry) + { + return this.Data.AsSpan().SequenceEqual(entry.Data); + } + + return false; + } + + /// + public bool Equals(IccXyzTagDataEntry? other) + => this.Equals((IccTagDataEntry?)other); + + /// + public override bool Equals(object? obj) + => this.Equals(obj as IccXyzTagDataEntry); + + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), this.Data); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Various/IccClut.cs b/ImageSharp/Metadata/Profiles/ICC/Various/IccClut.cs new file mode 100644 index 0000000..14dbd24 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Various/IccClut.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Color Lookup Table. + /// + internal sealed class IccClut : IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The CLUT values. + /// The gridpoint count. + /// The data type of this CLUT. + /// The output channels count. + public IccClut(float[] values, byte[] gridPointCount, IccClutDataType type, int outputChannelCount) + { + Guard.NotNull(values, nameof(values)); + Guard.NotNull(gridPointCount, nameof(gridPointCount)); + + this.Values = values; + this.DataType = type; + this.InputChannelCount = gridPointCount.Length; + this.OutputChannelCount = outputChannelCount; + this.GridPointCount = gridPointCount; + this.CheckValues(); + } + + /// + /// Gets the values that make up this table. + /// + public float[] Values { get; } + + /// + /// Gets the CLUT data type (important when writing a profile). + /// + public IccClutDataType DataType { get; } + + /// + /// Gets the number of input channels. + /// + public int InputChannelCount { get; } + + /// + /// Gets the number of output channels. + /// + public int OutputChannelCount { get; } + + /// + /// Gets the number of grid points per input channel. + /// + public byte[] GridPointCount { get; } + + /// + public bool Equals(IccClut? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return this.EqualsValuesArray(other) + && this.DataType == other.DataType + && this.InputChannelCount == other.InputChannelCount + && this.OutputChannelCount == other.OutputChannelCount + && this.GridPointCount.AsSpan().SequenceEqual(other.GridPointCount); + } + + /// + public override bool Equals(object? obj) => obj is IccClut other && this.Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine( + this.Values, + this.DataType, + this.InputChannelCount, + this.OutputChannelCount, + this.GridPointCount); + + private bool EqualsValuesArray(IccClut other) + { + if (this.Values.Length != other.Values.Length) + { + return false; + } + + return this.Values.SequenceEqual(other.Values); + } + + private void CheckValues() + { + Guard.MustBeBetweenOrEqualTo(this.InputChannelCount, 1, 15, nameof(this.InputChannelCount)); + Guard.MustBeBetweenOrEqualTo(this.OutputChannelCount, 1, 15, nameof(this.OutputChannelCount)); + + int length = 0; + for (int i = 0; i < this.InputChannelCount; i++) + { + length += (int)Math.Pow(this.GridPointCount[i], this.InputChannelCount); + } + + // TODO: Disabled this check, not sure if this check is correct. + // Guard.IsTrue(this.Values.Length == length, nameof(this.Values), "Length of values array does not match the grid points"); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Various/IccColorantTableEntry.cs b/ImageSharp/Metadata/Profiles/ICC/Various/IccColorantTableEntry.cs new file mode 100644 index 0000000..425f84f --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Various/IccColorantTableEntry.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Entry of ICC colorant table + /// + internal readonly struct IccColorantTableEntry : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// Name of the colorant + public IccColorantTableEntry(string name) + : this(name, 0, 0, 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// Name of the colorant + /// First PCS value + /// Second PCS value + /// Third PCS value + public IccColorantTableEntry(string name, ushort pcs1, ushort pcs2, ushort pcs3) + { + this.Name = name ?? throw new ArgumentNullException(nameof(name)); + this.Pcs1 = pcs1; + this.Pcs2 = pcs2; + this.Pcs3 = pcs3; + } + + /// + /// Gets the colorant name. + /// + public string Name { get; } + + /// + /// Gets the first PCS value. + /// + public ushort Pcs1 { get; } + + /// + /// Gets the second PCS value. + /// + public ushort Pcs2 { get; } + + /// + /// Gets the third PCS value. + /// + public ushort Pcs3 { get; } + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + public static bool operator ==(IccColorantTableEntry left, IccColorantTableEntry right) + { + return left.Equals(right); + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + public static bool operator !=(IccColorantTableEntry left, IccColorantTableEntry right) + { + return !left.Equals(right); + } + + /// + public override bool Equals(object? obj) + { + return obj is IccColorantTableEntry other && this.Equals(other); + } + + /// + public bool Equals(IccColorantTableEntry other) + { + return this.Name == other.Name + && this.Pcs1 == other.Pcs1 + && this.Pcs2 == other.Pcs2 + && this.Pcs3 == other.Pcs3; + } + + /// + public override int GetHashCode() + { + return HashCode.Combine( + this.Name, + this.Pcs1, + this.Pcs2, + this.Pcs3); + } + + /// + public override string ToString() => $"{this.Name}: {this.Pcs1}; {this.Pcs2}; {this.Pcs3}"; + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Various/IccLocalizedString.cs b/ImageSharp/Metadata/Profiles/ICC/Various/IccLocalizedString.cs new file mode 100644 index 0000000..61a0812 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Various/IccLocalizedString.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// A string with a specific locale. + /// + internal readonly struct IccLocalizedString : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// The culture will be + /// + /// The text value of this string + public IccLocalizedString(string text) + : this(CultureInfo.CurrentCulture, text) + { + } + + /// + /// Initializes a new instance of the struct. + /// The culture will be + /// + /// The culture of this string + /// The text value of this string + public IccLocalizedString(CultureInfo culture, string text) + { + this.Culture = culture ?? throw new ArgumentNullException(nameof(culture)); + this.Text = text ?? throw new ArgumentNullException(nameof(text)); + } + + /// + /// Gets the text value. + /// + public string Text { get; } + + /// + /// Gets the culture of text. + /// + public CultureInfo Culture { get; } + + /// + public bool Equals(IccLocalizedString other) => + this.Culture.Equals(other.Culture) && + this.Text == other.Text; + + /// + public override string ToString() => $"{this.Culture.Name}: {this.Text}"; + + public override bool Equals(object? obj) + => obj is IccLocalizedString iccLocalizedString && this.Equals(iccLocalizedString); + + public override int GetHashCode() + => HashCode.Combine(this.Culture, this.Text); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Various/IccLut.cs b/ImageSharp/Metadata/Profiles/ICC/Various/IccLut.cs new file mode 100644 index 0000000..9755ef8 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Various/IccLut.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Lookup Table + /// + internal readonly struct IccLut : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The LUT values + public IccLut(float[] values) + => this.Values = values ?? throw new ArgumentNullException(nameof(values)); + + /// + /// Initializes a new instance of the struct. + /// + /// The LUT values + public IccLut(ushort[] values) + { + Guard.NotNull(values, nameof(values)); + + const float max = ushort.MaxValue; + + this.Values = new float[values.Length]; + for (int i = 0; i < values.Length; i++) + { + this.Values[i] = values[i] / max; + } + } + + /// + /// Initializes a new instance of the struct. + /// + /// The LUT values + public IccLut(byte[] values) + { + Guard.NotNull(values, nameof(values)); + + const float max = byte.MaxValue; + + this.Values = new float[values.Length]; + for (int i = 0; i < values.Length; i++) + { + this.Values[i] = values[i] / max; + } + } + + /// + /// Gets the values that make up this table + /// + public float[] Values { get; } + + /// + public bool Equals(IccLut other) + { + if (ReferenceEquals(this.Values, other.Values)) + { + return true; + } + + return this.Values.AsSpan().SequenceEqual(other.Values); + } + + /// + public override bool Equals(object? obj) + => obj is IccLut iccLut && this.Equals(iccLut); + + /// + public override int GetHashCode() + => this.Values.GetHashCode(); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Various/IccNamedColor.cs b/ImageSharp/Metadata/Profiles/ICC/Various/IccNamedColor.cs new file mode 100644 index 0000000..06bda07 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Various/IccNamedColor.cs @@ -0,0 +1,87 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// A specific color with a name + /// + internal readonly struct IccNamedColor : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// Name of the color + /// Coordinates of the color in the profiles PCS + /// Coordinates of the color in the profiles Device-Space + public IccNamedColor(string name, ushort[] pcsCoordinates, ushort[] deviceCoordinates) + { + Guard.NotNull(name, nameof(name)); + Guard.NotNull(pcsCoordinates, nameof(pcsCoordinates)); + Guard.IsTrue(pcsCoordinates.Length == 3, nameof(pcsCoordinates), "Must have a length of 3"); + + this.Name = name; + this.PcsCoordinates = pcsCoordinates; + this.DeviceCoordinates = deviceCoordinates; + } + + /// + /// Gets the name of the color + /// + public string Name { get; } + + /// + /// Gets the coordinates of the color in the profiles PCS + /// + public ushort[] PcsCoordinates { get; } + + /// + /// Gets the coordinates of the color in the profiles Device-Space + /// + public ushort[] DeviceCoordinates { get; } + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + public static bool operator ==(IccNamedColor left, IccNamedColor right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + public static bool operator !=(IccNamedColor left, IccNamedColor right) => !left.Equals(right); + + /// + public override bool Equals(object? obj) => obj is IccNamedColor other && this.Equals(other); + + /// + public bool Equals(IccNamedColor other) + => this.Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) + && this.PcsCoordinates.AsSpan().SequenceEqual(other.PcsCoordinates) + && this.DeviceCoordinates.AsSpan().SequenceEqual(other.DeviceCoordinates); + + /// + public override int GetHashCode() + => HashCode.Combine( + this.Name, + this.PcsCoordinates, + this.DeviceCoordinates); + + /// + public override string ToString() => this.Name; + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Various/IccPositionNumber.cs b/ImageSharp/Metadata/Profiles/ICC/Various/IccPositionNumber.cs new file mode 100644 index 0000000..58e422a --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Various/IccPositionNumber.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Position of an object within an ICC profile + /// + internal readonly struct IccPositionNumber : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// Offset in bytes + /// Size in bytes + public IccPositionNumber(uint offset, uint size) + { + this.Offset = offset; + this.Size = size; + } + + /// + /// Gets the offset in bytes + /// + public uint Offset { get; } + + /// + /// Gets the size in bytes + /// + public uint Size { get; } + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + public static bool operator ==(IccPositionNumber left, IccPositionNumber right) + { + return left.Equals(right); + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + public static bool operator !=(IccPositionNumber left, IccPositionNumber right) + { + return !left.Equals(right); + } + + /// + public override bool Equals(object? obj) + { + return obj is IccPositionNumber other && this.Equals(other); + } + + /// + public bool Equals(IccPositionNumber other) => + this.Offset == other.Offset && + this.Size == other.Size; + + /// + public override int GetHashCode() => unchecked((int)(this.Offset ^ this.Size)); + + /// + public override string ToString() => $"{this.Offset}; {this.Size}"; + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Various/IccProfileDescription.cs b/ImageSharp/Metadata/Profiles/ICC/Various/IccProfileDescription.cs new file mode 100644 index 0000000..273e35a --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Various/IccProfileDescription.cs @@ -0,0 +1,91 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// ICC Profile description + /// + internal readonly struct IccProfileDescription : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// Device Manufacturer + /// Device Model + /// Device Attributes + /// Technology Information + /// Device Manufacturer Info + /// Device Model Info + public IccProfileDescription( + uint deviceManufacturer, + uint deviceModel, + IccDeviceAttribute deviceAttributes, + IccProfileTag technologyInformation, + IccLocalizedString[] deviceManufacturerInfo, + IccLocalizedString[] deviceModelInfo) + { + this.DeviceManufacturer = deviceManufacturer; + this.DeviceModel = deviceModel; + this.DeviceAttributes = deviceAttributes; + this.TechnologyInformation = technologyInformation; + this.DeviceManufacturerInfo = deviceManufacturerInfo ?? throw new ArgumentNullException(nameof(deviceManufacturerInfo)); + this.DeviceModelInfo = deviceModelInfo ?? throw new ArgumentNullException(nameof(deviceModelInfo)); + } + + /// + /// Gets the device manufacturer. + /// + public uint DeviceManufacturer { get; } + + /// + /// Gets the device model. + /// + public uint DeviceModel { get; } + + /// + /// Gets the device attributes. + /// + public IccDeviceAttribute DeviceAttributes { get; } + + /// + /// Gets the technology information. + /// + public IccProfileTag TechnologyInformation { get; } + + /// + /// Gets the device manufacturer info. + /// + public IccLocalizedString[] DeviceManufacturerInfo { get; } + + /// + /// Gets the device model info. + /// + public IccLocalizedString[] DeviceModelInfo { get; } + + /// + public bool Equals(IccProfileDescription other) => + this.DeviceManufacturer == other.DeviceManufacturer + && this.DeviceModel == other.DeviceModel + && this.DeviceAttributes == other.DeviceAttributes + && this.TechnologyInformation == other.TechnologyInformation + && this.DeviceManufacturerInfo.AsSpan().SequenceEqual(other.DeviceManufacturerInfo) + && this.DeviceModelInfo.AsSpan().SequenceEqual(other.DeviceModelInfo); + + /// + public override bool Equals(object? obj) => obj is IccProfileDescription other && this.Equals(other); + + /// + public override int GetHashCode() + { + return HashCode.Combine( + this.DeviceManufacturer, + this.DeviceModel, + this.DeviceAttributes, + this.TechnologyInformation, + this.DeviceManufacturerInfo, + this.DeviceModelInfo); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Various/IccProfileId.cs b/ImageSharp/Metadata/Profiles/ICC/Various/IccProfileId.cs new file mode 100644 index 0000000..cd34d51 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Various/IccProfileId.cs @@ -0,0 +1,105 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// ICC Profile ID + /// + public readonly struct IccProfileId : IEquatable + { + /// + /// A profile ID with all values set to zero + /// + public static readonly IccProfileId Zero; + + /// + /// Initializes a new instance of the struct. + /// + /// Part 1 of the ID + /// Part 2 of the ID + /// Part 3 of the ID + /// Part 4 of the ID + public IccProfileId(uint p1, uint p2, uint p3, uint p4) + { + this.Part1 = p1; + this.Part2 = p2; + this.Part3 = p3; + this.Part4 = p4; + } + + /// + /// Gets the first part of the ID. + /// + public uint Part1 { get; } + + /// + /// Gets the second part of the ID. + /// + public uint Part2 { get; } + + /// + /// Gets the third part of the ID. + /// + public uint Part3 { get; } + + /// + /// Gets the fourth part of the ID. + /// + public uint Part4 { get; } + + /// + /// Gets a value indicating whether the ID is set or just consists of zeros. + /// + public bool IsSet => !this.Equals(Zero); + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + public static bool operator ==(IccProfileId left, IccProfileId right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + public static bool operator !=(IccProfileId left, IccProfileId right) => !left.Equals(right); + + /// + public override bool Equals(object? obj) => obj is IccProfileId other && this.Equals(other); + + /// + public bool Equals(IccProfileId other) => + this.Part1 == other.Part1 && + this.Part2 == other.Part2 && + this.Part3 == other.Part3 && + this.Part4 == other.Part4; + + /// + public override int GetHashCode() + => HashCode.Combine( + this.Part1, + this.Part2, + this.Part3, + this.Part4); + + /// + public override string ToString() => $"{ToHex(this.Part1)}-{ToHex(this.Part2)}-{ToHex(this.Part3)}-{ToHex(this.Part4)}"; + + private static string ToHex(uint value) => value.ToString("X", CultureInfo.InvariantCulture).PadLeft(8, '0'); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Various/IccProfileSequenceIdentifier.cs b/ImageSharp/Metadata/Profiles/ICC/Various/IccProfileSequenceIdentifier.cs new file mode 100644 index 0000000..dfef514 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Various/IccProfileSequenceIdentifier.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Description of a profile within a sequence. + /// + internal readonly struct IccProfileSequenceIdentifier : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// ID of the profile + /// Description of the profile + public IccProfileSequenceIdentifier(IccProfileId id, IccLocalizedString[] description) + { + this.Id = id; + this.Description = description ?? throw new ArgumentNullException(nameof(description)); + } + + /// + /// Gets the ID of the profile. + /// + public IccProfileId Id { get; } + + /// + /// Gets the description of the profile. + /// + public IccLocalizedString[] Description { get; } + + /// + public bool Equals(IccProfileSequenceIdentifier other) => + this.Id.Equals(other.Id) + && this.Description.AsSpan().SequenceEqual(other.Description); + + /// + public override bool Equals(object? obj) => obj is IccProfileSequenceIdentifier other && this.Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(this.Id, this.Description); + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Various/IccResponseNumber.cs b/ImageSharp/Metadata/Profiles/ICC/Various/IccResponseNumber.cs new file mode 100644 index 0000000..06c8f38 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Various/IccResponseNumber.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Associates a normalized device code with a measurement value + /// + internal readonly struct IccResponseNumber : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// Device Code + /// Measurement Value + public IccResponseNumber(ushort deviceCode, float measurementValue) + { + this.DeviceCode = deviceCode; + this.MeasurementValue = measurementValue; + } + + /// + /// Gets the device code + /// + public ushort DeviceCode { get; } + + /// + /// Gets the measurement value + /// + public float MeasurementValue { get; } + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + public static bool operator ==(IccResponseNumber left, IccResponseNumber right) + { + return left.Equals(right); + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + public static bool operator !=(IccResponseNumber left, IccResponseNumber right) + { + return !left.Equals(right); + } + + /// + public override bool Equals(object? obj) + { + return obj is IccResponseNumber other && this.Equals(other); + } + + /// + public bool Equals(IccResponseNumber other) => + this.DeviceCode == other.DeviceCode && + this.MeasurementValue == other.MeasurementValue; + + /// + public override int GetHashCode() => HashCode.Combine(this.DeviceCode, this.MeasurementValue); + + /// + public override string ToString() => $"Code: {this.DeviceCode}; Value: {this.MeasurementValue}"; + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Various/IccScreeningChannel.cs b/ImageSharp/Metadata/Profiles/ICC/Various/IccScreeningChannel.cs new file mode 100644 index 0000000..28bd6e5 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Various/IccScreeningChannel.cs @@ -0,0 +1,93 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// A single channel of a + /// + [StructLayout(LayoutKind.Sequential)] + internal readonly struct IccScreeningChannel : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// Screen frequency + /// Angle in degrees + /// Spot shape + public IccScreeningChannel(float frequency, float angle, IccScreeningSpotType spotShape) + { + this.Frequency = frequency; + this.Angle = angle; + this.SpotShape = spotShape; + } + + /// + /// Gets the screen frequency. + /// + public float Frequency { get; } + + /// + /// Gets the angle in degrees. + /// + public float Angle { get; } + + /// + /// Gets the spot shape + /// + public IccScreeningSpotType SpotShape { get; } + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + public static bool operator ==(IccScreeningChannel left, IccScreeningChannel right) + { + return left.Equals(right); + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + public static bool operator !=(IccScreeningChannel left, IccScreeningChannel right) + { + return !left.Equals(right); + } + + /// + public bool Equals(IccScreeningChannel other) => + this.Frequency == other.Frequency && + this.Angle == other.Angle && + this.SpotShape == other.SpotShape; + + /// + public override bool Equals(object? obj) + { + return obj is IccScreeningChannel other && this.Equals(other); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(this.Frequency, this.Angle, this.SpotShape); + } + + /// + public override string ToString() => $"{this.Frequency}Hz; {this.Angle}°; {this.SpotShape}"; + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Various/IccTagTableEntry.cs b/ImageSharp/Metadata/Profiles/ICC/Various/IccTagTableEntry.cs new file mode 100644 index 0000000..eb9e4ed --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Various/IccTagTableEntry.cs @@ -0,0 +1,81 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Entry of ICC tag table + /// + internal readonly struct IccTagTableEntry : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// Signature of the tag + /// Offset of entry in bytes + /// Size of entry in bytes + public IccTagTableEntry(IccProfileTag signature, uint offset, uint dataSize) + { + this.Signature = signature; + this.Offset = offset; + this.DataSize = dataSize; + } + + /// + /// Gets the signature of the tag. + /// + public IccProfileTag Signature { get; } + + /// + /// Gets the offset of entry in bytes. + /// + public uint Offset { get; } + + /// + /// Gets the size of entry in bytes. + /// + public uint DataSize { get; } + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + public static bool operator ==(IccTagTableEntry left, IccTagTableEntry right) + => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + public static bool operator !=(IccTagTableEntry left, IccTagTableEntry right) + => !left.Equals(right); + + /// + public override bool Equals(object? obj) => obj is IccTagTableEntry other && this.Equals(other); + + /// + public bool Equals(IccTagTableEntry other) => + this.Signature.Equals(other.Signature) && + this.Offset.Equals(other.Offset) && + this.DataSize.Equals(other.DataSize); + + /// + public override int GetHashCode() => HashCode.Combine(this.Signature, this.Offset, this.DataSize); + + /// + public override string ToString() => $"{this.Signature} (Offset: {this.Offset}; Size: {this.DataSize})"; + } +} diff --git a/ImageSharp/Metadata/Profiles/ICC/Various/IccVersion.cs b/ImageSharp/Metadata/Profiles/ICC/Various/IccVersion.cs new file mode 100644 index 0000000..a07635b --- /dev/null +++ b/ImageSharp/Metadata/Profiles/ICC/Various/IccVersion.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc { + /// + /// Represents the ICC profile version number. + /// + public readonly struct IccVersion : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The major version number. + /// The minor version number. + /// The patch version number. + public IccVersion(int major, int minor, int patch) + { + this.Major = major; + this.Minor = minor; + this.Patch = patch; + } + + /// + /// Gets the major version number. + /// + public int Major { get; } + + /// + /// Gets the minor version number. + /// + public int Minor { get; } + + /// + /// Gets the patch number. + /// + public int Patch { get; } + + /// + /// Returns a value indicating whether the two values are equal. + /// + /// The first value. + /// The second value. + /// if the two value are equal; otherwise, . + public static bool operator ==(IccVersion left, IccVersion right) + => left.Equals(right); + + /// + /// Returns a value indicating whether the two values are not equal. + /// + /// The first value. + /// The second value. + /// if the two value are not equal; otherwise, . + public static bool operator !=(IccVersion left, IccVersion right) + => !(left == right); + + /// + public override bool Equals(object? obj) + => obj is IccVersion iccVersion && this.Equals(iccVersion); + + /// + public bool Equals(IccVersion other) => + this.Major == other.Major && + this.Minor == other.Minor && + this.Patch == other.Patch; + + /// + public override string ToString() + => string.Join(".", this.Major, this.Minor, this.Patch); + + /// + public override int GetHashCode() + => HashCode.Combine(this.Major, this.Minor, this.Patch); + } +} diff --git a/ImageSharp/Metadata/Profiles/IPTC/IIMV4.2_IPTC.pdf b/ImageSharp/Metadata/Profiles/IPTC/IIMV4.2_IPTC.pdf new file mode 100644 index 0000000..b003551 Binary files /dev/null and b/ImageSharp/Metadata/Profiles/IPTC/IIMV4.2_IPTC.pdf differ diff --git a/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs b/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs new file mode 100644 index 0000000..9e3c8cd --- /dev/null +++ b/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs @@ -0,0 +1,350 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Text; +using SixLabors.ImageSharp.Metadata.Profiles.IPTC; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc { + /// + /// Represents an IPTC profile providing access to the collection of values. + /// + public sealed class IptcProfile : IDeepCloneable + { + private readonly Collection values = []; + + private const byte IptcTagMarkerByte = 0x1c; + + private const uint MaxStandardDataTagSize = 0x7FFF; + + /// + /// 1:90 Coded Character Set. + /// + private const byte IptcEnvelopeCodedCharacterSet = 0x5A; + + /// + /// Initializes a new instance of the class. + /// + public IptcProfile() + : this((byte[]?)null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The byte array to read the iptc profile from. + public IptcProfile(byte[]? data) + { + this.Data = data; + this.Initialize(); + } + + /// + /// Initializes a new instance of the class + /// by making a copy from another IPTC profile. + /// + /// The other IPTC profile, from which the clone should be made from. + private IptcProfile(IptcProfile other) + { + Guard.NotNull(other, nameof(other)); + + foreach (IptcValue value in other.Values) + { + this.values.Add(value.DeepClone()); + } + + if (other.Data != null) + { + this.Data = new byte[other.Data.Length]; + other.Data.AsSpan().CopyTo(this.Data); + } + } + + /// + /// Gets a byte array marking that UTF-8 encoding is used in application records. + /// + private static ReadOnlySpan CodedCharacterSetUtf8Value => [0x1B, 0x25, 0x47]; // Uses C#'s optimization to refer to the data segment in the assembly directly, no allocation occurs. + + /// + /// Gets the byte data of the IPTC profile. + /// + public byte[]? Data { get; private set; } + + /// + /// Gets the values of this iptc profile. + /// + public IEnumerable Values => this.values; + + /// + public IptcProfile DeepClone() => new(this); + + /// + /// Returns all values with the specified tag. + /// + /// The tag of the iptc value. + /// The values found with the specified tag. + public List GetValues(IptcTag tag) + { + List iptcValues = []; + foreach (IptcValue iptcValue in this.Values) + { + if (iptcValue.Tag == tag) + { + iptcValues.Add(iptcValue); + } + } + + return iptcValues; + } + + /// + /// Removes all values with the specified tag. + /// + /// The tag of the iptc value to remove. + /// True when the value was found and removed. + public bool RemoveValue(IptcTag tag) + { + bool removed = false; + for (int i = this.values.Count - 1; i >= 0; i--) + { + if (this.values[i].Tag == tag) + { + this.values.RemoveAt(i); + removed = true; + } + } + + return removed; + } + + /// + /// Removes values with the specified tag and value. + /// + /// The tag of the iptc value to remove. + /// The value of the iptc item to remove. + /// True when the value was found and removed. + public bool RemoveValue(IptcTag tag, string value) + { + bool removed = false; + for (int i = this.values.Count - 1; i >= 0; i--) + { + if (this.values[i].Tag == tag && this.values[i].Value.Equals(value, StringComparison.OrdinalIgnoreCase)) + { + this.values.RemoveAt(i); + removed = true; + } + } + + return removed; + } + + /// + /// Changes the encoding for all the values. + /// + /// The encoding to use when storing the bytes. + public void SetEncoding(Encoding encoding) + { + Guard.NotNull(encoding, nameof(encoding)); + + foreach (IptcValue value in this.Values) + { + value.Encoding = encoding; + } + } + + /// + /// Sets the value for the specified tag. + /// + /// The tag of the iptc value. + /// The encoding to use when storing the bytes. + /// The value. + /// + /// Indicates if length restrictions from the specification should be followed strictly. + /// Defaults to true. + /// + public void SetValue(IptcTag tag, Encoding encoding, string value, bool strict = true) + { + Guard.NotNull(encoding, nameof(encoding)); + Guard.NotNull(value, nameof(value)); + + if (!tag.IsRepeatable()) + { + foreach (IptcValue iptcValue in this.Values) + { + if (iptcValue.Tag == tag) + { + iptcValue.Strict = strict; + iptcValue.Encoding = encoding; + iptcValue.Value = value; + return; + } + } + } + + this.values.Add(new IptcValue(tag, encoding, value, strict)); + } + + /// + /// Sets the value of the specified tag. + /// + /// The tag of the iptc value. + /// The value. + /// + /// Indicates if length restrictions from the specification should be followed strictly. + /// Defaults to true. + /// + public void SetValue(IptcTag tag, string value, bool strict = true) => this.SetValue(tag, Encoding.UTF8, value, strict); + + /// + /// Makes sure the datetime is formatted according to the iptc specification. + /// + /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989. + /// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time, + /// two hours ahead of UTC. + /// + /// + /// The tag of the iptc value. + /// The datetime. + /// Iptc tag is not a time or date type. + public void SetDateTimeValue(IptcTag tag, DateTimeOffset dateTimeOffset) + { + if (!tag.IsDate() && !tag.IsTime()) + { + throw new ArgumentException("Iptc tag is not a time or date type."); + } + + string formattedDate = tag.IsDate() + ? dateTimeOffset.ToString("yyyyMMdd", CultureInfo.InvariantCulture) + : dateTimeOffset.ToString("HHmmsszzzz", CultureInfo.InvariantCulture) + .Replace(":", string.Empty); + + this.SetValue(tag, Encoding.UTF8, formattedDate); + } + + /// + /// Updates the data of the profile. + /// + public void UpdateData() + { + int length = 0; + foreach (IptcValue value in this.Values) + { + length += value.Length + 5; + } + + bool hasValuesInUtf8 = this.HasValuesInUtf8(); + + if (hasValuesInUtf8) + { + // Additional length for UTF-8 Tag. + length += 5 + CodedCharacterSetUtf8Value.Length; + } + + this.Data = new byte[length]; + int offset = 0; + if (hasValuesInUtf8) + { + // Write Envelope Record. + offset = this.WriteRecord(offset, CodedCharacterSetUtf8Value, IptcRecordNumber.Envelope, IptcEnvelopeCodedCharacterSet); + } + + foreach (IptcValue value in this.Values) + { + // Write Application Record. + // +-----------+----------------+---------------------------------------------------------------------------------+ + // | Octet Pos | Name | Description | + // +==========-+================+=================================================================================+ + // | 1 | Tag Marker | Is the tag marker that initiates the start of a DataSet 0x1c. | + // +-----------+----------------+---------------------------------------------------------------------------------+ + // | 2 | Record Number | Octet 2 is the binary representation of the record number. Note that the | + // | | | envelope record number is always 1, and that the application records are | + // | | | numbered 2 through 6, the pre-object descriptor record is 7, the object record | + // | | | is 8, and the post - object descriptor record is 9. | + // +-----------+----------------+---------------------------------------------------------------------------------+ + // | 3 | DataSet Number | Octet 3 is the binary representation of the DataSet number. | + // +-----------+----------------+---------------------------------------------------------------------------------+ + // | 4 and 5 | Data Field | Octets 4 and 5, taken together, are the binary count of the number of octets in | + // | | Octet Count | the following data field(32767 or fewer octets). Note that the value of bit 7 of| + // | | | octet 4(most significant bit) always will be 0. | + // +-----------+----------------+---------------------------------------------------------------------------------+ + offset = this.WriteRecord(offset, value.ToByteArray(), IptcRecordNumber.Application, (byte)value.Tag); + } + } + + private int WriteRecord(int offset, ReadOnlySpan recordData, IptcRecordNumber recordNumber, byte recordBinaryRepresentation) + { + Span data = this.Data.AsSpan(offset, 5); + data[0] = IptcTagMarkerByte; + data[1] = (byte)recordNumber; + data[2] = recordBinaryRepresentation; + data[3] = (byte)(recordData.Length >> 8); + data[4] = (byte)recordData.Length; + offset += 5; + if (recordData.Length > 0) + { + recordData.CopyTo(this.Data.AsSpan(offset)); + offset += recordData.Length; + } + + return offset; + } + + private void Initialize() + { + if (this.Data == null || this.Data[0] != IptcTagMarkerByte) + { + return; + } + + int offset = 0; + while (offset < this.Data.Length - 4) + { + bool isValidTagMarker = this.Data[offset++] == IptcTagMarkerByte; + byte recordNumber = this.Data[offset++]; + bool isValidRecordNumber = recordNumber is >= 1 and <= 9; + IptcTag tag = (IptcTag)this.Data[offset++]; + bool isValidEntry = isValidTagMarker && isValidRecordNumber; + bool isApplicationRecord = recordNumber == (byte)IptcRecordNumber.Application; + + uint byteCount = BinaryPrimitives.ReadUInt16BigEndian(this.Data.AsSpan(offset, 2)); + offset += 2; + if (byteCount > MaxStandardDataTagSize) + { + // Extended data set tag's are not supported. + break; + } + + if (isValidEntry && isApplicationRecord && byteCount > 0 && (offset <= this.Data.Length - byteCount)) + { + byte[] iptcData = new byte[byteCount]; + Buffer.BlockCopy(this.Data, offset, iptcData, 0, (int)byteCount); + this.values.Add(new IptcValue(tag, iptcData, false)); + } + + offset += (int)byteCount; + } + } + + /// + /// Gets if any value has UTF-8 encoding. + /// + /// true if any value has UTF-8 encoding. + private bool HasValuesInUtf8() + { + foreach (IptcValue value in this.values) + { + if (value.Encoding == Encoding.UTF8) + { + return true; + } + } + + return false; + } + } +} diff --git a/ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs b/ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs new file mode 100644 index 0000000..3e2c8c7 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.IPTC { + /// + /// Enum for the different record types of a IPTC value. + /// + internal enum IptcRecordNumber : byte + { + /// + /// An Envelope Record. + /// + Envelope = 0x01, + + /// + /// An Application Record. + /// + Application = 0x02 + } +} diff --git a/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs b/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs new file mode 100644 index 0000000..dcfd73a --- /dev/null +++ b/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs @@ -0,0 +1,396 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc { + /// + /// Provides enumeration of all IPTC tags relevant for images. + /// + public enum IptcTag + { + /// + /// Unknown. + /// + Unknown = -1, + + /// + /// Record version identifying the version of the Information Interchange Model. + /// Not repeatable. Max length is 2. + /// + RecordVersion = 0, + + /// + /// Object type, not repeatable. Max Length is 67. + /// + ObjectType = 3, + + /// + /// Object attribute. Max length is 68. + /// + ObjectAttribute = 4, + + /// + /// Object Name, not repeatable. Max length is 64. + /// + Name = 5, + + /// + /// Edit status, not repeatable. Max length is 64. + /// + EditStatus = 7, + + /// + /// Editorial update, not repeatable. Max length is 2. + /// + EditorialUpdate = 8, + + /// + /// Urgency, not repeatable. Max length is 2. + /// + Urgency = 10, + + /// + /// Subject Reference. Max length is 236. + /// + SubjectReference = 12, + + /// + /// Category, not repeatable. Max length is 3. + /// + Category = 15, + + /// + /// Supplemental categories. Max length is 32. + /// + SupplementalCategories = 20, + + /// + /// Fixture identifier, not repeatable. Max length is 32. + /// + FixtureIdentifier = 22, + + /// + /// Keywords. Max length is 64. + /// + Keywords = 25, + + /// + /// Location code. Max length is 3. + /// + LocationCode = 26, + + /// + /// Location name. Max length is 64. + /// + LocationName = 27, + + /// + /// Release date. Format should be CCYYMMDD. + /// Not repeatable, max length is 8. + /// + /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989. + /// + /// + ReleaseDate = 30, + + /// + /// Release time. Format should be HHMMSS±HHMM. + /// Not repeatable, max length is 11. + /// + /// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time, + /// two hours ahead of UTC. + /// + /// + ReleaseTime = 35, + + /// + /// Expiration date. Format should be CCYYMMDD. + /// Not repeatable, max length is 8. + /// + /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989. + /// + /// + ExpirationDate = 37, + + /// + /// Expiration time. Format should be HHMMSS±HHMM. + /// Not repeatable, max length is 11. + /// + /// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time, + /// two hours ahead of UTC. + /// + /// + ExpirationTime = 38, + + /// + /// Special instructions, not repeatable. Max length is 256. + /// + SpecialInstructions = 40, + + /// + /// Action advised, not repeatable. Max length is 2. + /// + ActionAdvised = 42, + + /// + /// Reference service. Max length is 10. + /// + ReferenceService = 45, + + /// + /// Reference date. Format should be CCYYMMDD. + /// Not repeatable, max length is 8. + /// + /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989. + /// + /// + ReferenceDate = 47, + + /// + /// ReferenceNumber. Max length is 8. + /// + ReferenceNumber = 50, + + /// + /// Created date. Format should be CCYYMMDD. + /// Not repeatable, max length is 8. + /// + /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989. + /// + /// + CreatedDate = 55, + + /// + /// Created time. Format should be HHMMSS±HHMM. + /// Not repeatable, max length is 11. + /// + /// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time, + /// two hours ahead of UTC. + /// + /// + CreatedTime = 60, + + /// + /// Digital creation date. Format should be CCYYMMDD. + /// Not repeatable, max length is 8. + /// + /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989. + /// + /// + DigitalCreationDate = 62, + + /// + /// Digital creation time. Format should be HHMMSS±HHMM. + /// Not repeatable, max length is 11. + /// + /// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time, + /// two hours ahead of UTC. + /// + /// + DigitalCreationTime = 63, + + /// + /// Originating program, not repeatable. Max length is 32. + /// + OriginatingProgram = 65, + + /// + /// Program version, not repeatable. Max length is 10. + /// + ProgramVersion = 70, + + /// + /// Object cycle, not repeatable. Max length is 1. + /// + ObjectCycle = 75, + + /// + /// Byline. Max length is 32. + /// + Byline = 80, + + /// + /// Byline title. Max length is 32. + /// + BylineTitle = 85, + + /// + /// City, not repeatable. Max length is 32. + /// + City = 90, + + /// + /// Sub location, not repeatable. Max length is 32. + /// + SubLocation = 92, + + /// + /// Province/State, not repeatable. Max length is 32. + /// + ProvinceState = 95, + + /// + /// Country code, not repeatable. Max length is 3. + /// + CountryCode = 100, + + /// + /// Country, not repeatable. Max length is 64. + /// + Country = 101, + + /// + /// Original transmission reference, not repeatable. Max length is 32. + /// + OriginalTransmissionReference = 103, + + /// + /// Headline, not repeatable. Max length is 256. + /// + Headline = 105, + + /// + /// Credit, not repeatable. Max length is 32. + /// + Credit = 110, + + /// + /// Source, not repeatable. Max length is 32. + /// + Source = 115, + + /// + /// Copyright notice, not repeatable. Max length is 128. + /// + CopyrightNotice = 116, + + /// + /// Contact. Max length 128. + /// + Contact = 118, + + /// + /// Caption, not repeatable. Max length is 2000. + /// + Caption = 120, + + /// + /// Local caption. + /// + LocalCaption = 121, + + /// + /// Caption writer. Max length is 32. + /// + CaptionWriter = 122, + + /// + /// Image type, not repeatable. Max length is 2. + /// + ImageType = 130, + + /// + /// Image orientation, not repeatable. Max length is 1. + /// + ImageOrientation = 131, + + /// + /// Custom field 1 + /// + CustomField1 = 200, + + /// + /// Custom field 2 + /// + CustomField2 = 201, + + /// + /// Custom field 3 + /// + CustomField3 = 202, + + /// + /// Custom field 4 + /// + CustomField4 = 203, + + /// + /// Custom field 5 + /// + CustomField5 = 204, + + /// + /// Custom field 6 + /// + CustomField6 = 205, + + /// + /// Custom field 7 + /// + CustomField7 = 206, + + /// + /// Custom field 8 + /// + CustomField8 = 207, + + /// + /// Custom field 9 + /// + CustomField9 = 208, + + /// + /// Custom field 10 + /// + CustomField10 = 209, + + /// + /// Custom field 11 + /// + CustomField11 = 210, + + /// + /// Custom field 12 + /// + CustomField12 = 211, + + /// + /// Custom field 13 + /// + CustomField13 = 212, + + /// + /// Custom field 14 + /// + CustomField14 = 213, + + /// + /// Custom field 15 + /// + CustomField15 = 214, + + /// + /// Custom field 16 + /// + CustomField16 = 215, + + /// + /// Custom field 17 + /// + CustomField17 = 216, + + /// + /// Custom field 18 + /// + CustomField18 = 217, + + /// + /// Custom field 19 + /// + CustomField19 = 218, + + /// + /// Custom field 20 + /// + CustomField20 = 219, + } +} diff --git a/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs b/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs new file mode 100644 index 0000000..cc57168 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs @@ -0,0 +1,158 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc { + /// + /// Extension methods for IPTC tags. + /// + public static class IptcTagExtensions + { + /// + /// Maximum length of the IPTC value with the given tag according to the specification. + /// + /// The tag to check the max length for. + /// The maximum length. + public static int MaxLength(this IptcTag tag) => tag switch + { + IptcTag.RecordVersion => 2, + IptcTag.ObjectType => 67, + IptcTag.ObjectAttribute => 68, + IptcTag.Name => 64, + IptcTag.EditStatus => 64, + IptcTag.EditorialUpdate => 2, + IptcTag.Urgency => 1, + IptcTag.SubjectReference => 236, + IptcTag.Category => 3, + IptcTag.SupplementalCategories => 32, + IptcTag.FixtureIdentifier => 32, + IptcTag.Keywords => 64, + IptcTag.LocationCode => 3, + IptcTag.LocationName => 64, + IptcTag.ReleaseDate => 8, + IptcTag.ReleaseTime => 11, + IptcTag.ExpirationDate => 8, + IptcTag.ExpirationTime => 11, + IptcTag.SpecialInstructions => 256, + IptcTag.ActionAdvised => 2, + IptcTag.ReferenceService => 10, + IptcTag.ReferenceDate => 8, + IptcTag.ReferenceNumber => 8, + IptcTag.CreatedDate => 8, + IptcTag.CreatedTime => 11, + IptcTag.DigitalCreationDate => 8, + IptcTag.DigitalCreationTime => 11, + IptcTag.OriginatingProgram => 32, + IptcTag.ProgramVersion => 10, + IptcTag.ObjectCycle => 1, + IptcTag.Byline => 32, + IptcTag.BylineTitle => 32, + IptcTag.City => 32, + IptcTag.SubLocation => 32, + IptcTag.ProvinceState => 32, + IptcTag.CountryCode => 3, + IptcTag.Country => 64, + IptcTag.OriginalTransmissionReference => 32, + IptcTag.Headline => 256, + IptcTag.Credit => 32, + IptcTag.Source => 32, + IptcTag.CopyrightNotice => 128, + IptcTag.Contact => 128, + IptcTag.Caption => 2000, + IptcTag.CaptionWriter => 32, + IptcTag.ImageType => 2, + IptcTag.ImageOrientation => 1, + _ => 256 + }; + + /// + /// Determines if the given tag can be repeated according to the specification. + /// + /// The tag to check. + /// True, if the tag can occur multiple times. + public static bool IsRepeatable(this IptcTag tag) + { + switch (tag) + { + case IptcTag.RecordVersion: + case IptcTag.ObjectType: + case IptcTag.Name: + case IptcTag.EditStatus: + case IptcTag.EditorialUpdate: + case IptcTag.Urgency: + case IptcTag.Category: + case IptcTag.FixtureIdentifier: + case IptcTag.ReleaseDate: + case IptcTag.ReleaseTime: + case IptcTag.ExpirationDate: + case IptcTag.ExpirationTime: + case IptcTag.SpecialInstructions: + case IptcTag.ActionAdvised: + case IptcTag.CreatedDate: + case IptcTag.CreatedTime: + case IptcTag.DigitalCreationDate: + case IptcTag.DigitalCreationTime: + case IptcTag.OriginatingProgram: + case IptcTag.ProgramVersion: + case IptcTag.ObjectCycle: + case IptcTag.City: + case IptcTag.SubLocation: + case IptcTag.ProvinceState: + case IptcTag.CountryCode: + case IptcTag.Country: + case IptcTag.OriginalTransmissionReference: + case IptcTag.Headline: + case IptcTag.Credit: + case IptcTag.Source: + case IptcTag.CopyrightNotice: + case IptcTag.Caption: + case IptcTag.ImageType: + case IptcTag.ImageOrientation: + return false; + + default: + return true; + } + } + + /// + /// Determines if the tag is a datetime tag which needs to be formatted as CCYYMMDD. + /// + /// The tag to check. + /// True, if its a datetime tag. + public static bool IsDate(this IptcTag tag) + { + switch (tag) + { + case IptcTag.CreatedDate: + case IptcTag.DigitalCreationDate: + case IptcTag.ExpirationDate: + case IptcTag.ReferenceDate: + case IptcTag.ReleaseDate: + return true; + + default: + return false; + } + } + + /// + /// Determines if the tag is a time tag which need to be formatted as HHMMSS±HHMM. + /// + /// The tag to check. + /// True, if its a time tag. + public static bool IsTime(this IptcTag tag) + { + switch (tag) + { + case IptcTag.CreatedTime: + case IptcTag.DigitalCreationTime: + case IptcTag.ExpirationTime: + case IptcTag.ReleaseTime: + return true; + + default: + return false; + } + } + } +} diff --git a/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs b/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs new file mode 100644 index 0000000..b049092 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs @@ -0,0 +1,251 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.Text; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc { + /// + /// Represents a single value of the IPTC profile. + /// + [DebuggerDisplay("{Tag} = {DebuggerDisplayValue(),nq} ({GetType().Name,nq})")] + public sealed class IptcValue : IDeepCloneable + { + private byte[] data = []; + private Encoding encoding; + + internal IptcValue(IptcValue other) + { + if (other.data != null) + { + this.data = new byte[other.data.Length]; + other.data.AsSpan().CopyTo(this.data); + } + + this.encoding = (Encoding)other.Encoding.Clone(); + + this.Tag = other.Tag; + this.Strict = other.Strict; + } + + internal IptcValue(IptcTag tag, byte[] value, bool strict) + { + Guard.NotNull(value, nameof(value)); + + this.Strict = strict; + this.Tag = tag; + this.data = value; + this.encoding = Encoding.UTF8; + } + + internal IptcValue(IptcTag tag, Encoding encoding, string value, bool strict) + { + this.Strict = strict; + this.Tag = tag; + this.encoding = encoding; + this.Value = value; + } + + internal IptcValue(IptcTag tag, string value, bool strict) + { + this.Strict = strict; + this.Tag = tag; + this.encoding = Encoding.UTF8; + this.Value = value; + } + + /// + /// Gets or sets the encoding to use for the Value. + /// + public Encoding Encoding + { + get => this.encoding; + set + { + if (value != null) + { + this.encoding = value; + } + } + } + + /// + /// Gets the tag of the iptc value. + /// + public IptcTag Tag { get; } + + /// + /// Gets or sets a value indicating whether to be enforce value length restrictions according + /// to the specification. + /// + public bool Strict { get; set; } + + /// + /// Gets or sets the value. + /// + public string Value + { + get => this.encoding.GetString(this.data); + set + { + if (string.IsNullOrEmpty(value)) + { + this.data = []; + } + else + { + int maxLength = this.Tag.MaxLength(); + byte[] valueBytes; + if (this.Strict && value.Length > maxLength) + { + string cappedValue = value[..maxLength]; + valueBytes = this.encoding.GetBytes(cappedValue); + + // It is still possible that the bytes of the string exceed the limit. + if (valueBytes.Length > maxLength) + { + throw new ArgumentException($"The iptc value exceeds the limit of {maxLength} bytes for the tag {this.Tag}"); + } + } + else + { + valueBytes = this.encoding.GetBytes(value); + } + + this.data = valueBytes; + } + } + } + + /// + /// Gets the length of the value. + /// + public int Length => this.data.Length; + + /// + public IptcValue DeepClone() => new(this); + + /// + /// Determines whether the specified object is equal to the current . + /// + /// The object to compare this with. + /// True when the specified object is equal to the current . + public override bool Equals(object? obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + return this.Equals(obj as IptcValue); + } + + /// + /// Determines whether the specified iptc value is equal to the current . + /// + /// The iptc value to compare this with. + /// True when the specified iptc value is equal to the current . + public bool Equals(IptcValue? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (this.Tag != other.Tag) + { + return false; + } + + if (this.data.Length != other.data.Length) + { + return false; + } + + for (int i = 0; i < this.data.Length; i++) + { + if (this.data[i] != other.data[i]) + { + return false; + } + } + + return true; + } + + /// + /// Serves as a hash of this type. + /// + /// A hash code for the current instance. + public override int GetHashCode() => HashCode.Combine(this.data, this.Tag); + + /// + /// Converts this instance to a byte array. + /// + /// A array. + public byte[] ToByteArray() + { + byte[] result = new byte[this.data.Length]; + this.data.CopyTo(result, 0); + return result; + } + + /// + /// Returns a string that represents the current value. + /// + /// A string that represents the current value. + public override string ToString() => this.Value; + + /// + /// Returns a string that represents the current value with the specified encoding. + /// + /// The encoding to use. + /// A string that represents the current value with the specified encoding. + public string ToString(Encoding encoding) + { + Guard.NotNull(encoding, nameof(encoding)); + + return encoding.GetString(this.data); + } + + private string DebuggerDisplayValue() + { + // IPTC RecordVersion (2:00) is a 2-byte binary value, commonly 0x0004. + // Showing it as UTF-8 produces control characters like "\0\u0004". + if (this.Tag == IptcTag.RecordVersion && this.data.Length == 2) + { + int version = (this.data[0] << 8) | this.data[1]; + return version.ToString(CultureInfo.InvariantCulture); + } + + // Prefer readable text if it looks like it, otherwise show hex. + // (Avoid surprising debugger output for binary payloads.) + bool printable = true; + for (int i = 0; i < this.data.Length; i++) + { + byte b = this.data[i]; + + // If any byte is an ASCII control character, treat this value as binary. + if (b is < 0x20 or 0x7F) + { + printable = false; + break; + } + } + + if (printable) + { + return this.Value; + } + + return Convert.ToHexString(this.data); + } + } +} diff --git a/ImageSharp/Metadata/Profiles/IPTC/README.md b/ImageSharp/Metadata/Profiles/IPTC/README.md new file mode 100644 index 0000000..1217ca0 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/IPTC/README.md @@ -0,0 +1,11 @@ +IPTC source code is from [Magick.NET](https://github.com/dlemstra/Magick.NET) + +Information about IPTC can be found here in the following sources: + +- [metacpan.org, APP13-segment](https://metacpan.org/pod/Image::MetaData::JPEG::Structures#Structure-of-a-Photoshop-style-APP13-segment) + +- [iptc.org](https://www.iptc.org/std/photometadata/documentation/userguide/) + +- [Adobe File Formats Specification](http://oldschoolprg.x10.mx/downloads/ps6ffspecsv2.pdf) + +- [Tag Overview](https://exiftool.org/TagNames/IPTC.html) \ No newline at end of file diff --git a/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs b/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs new file mode 100644 index 0000000..e2822d3 --- /dev/null +++ b/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs @@ -0,0 +1,176 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; +using System.Text; +using System.Xml; +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 = NormalizeDataIfNeeded(data); + + /// + /// Initializes a new instance of the class from an XML document. + /// The document is serialized as UTF-8 without BOM. + /// + /// The XMP XML document. + public XmpProfile(XDocument document) + { + Guard.NotNull(document, nameof(document)); + this.Data = SerializeDocument(document); + } + + /// + /// Gets the XMP raw data byte array. + /// + internal byte[]? Data { get; private set; } + + /// + /// Convert the content of this into an . + /// + /// The instance, or if no XMP data is present. + public XDocument? ToXDocument() + { + byte[]? data = this.Data; + if (data is null || data.Length == 0) + { + return null; + } + + using MemoryStream stream = new(data, writable: false); + + XmlReaderSettings settings = new() + { + DtdProcessing = DtdProcessing.Ignore, + XmlResolver = null, + CloseInput = false + }; + + using XmlReader reader = XmlReader.Create(stream, settings); + return XDocument.Load(reader, LoadOptions.PreserveWhitespace); + } + + /// + /// Convert the content of this into a byte array. + /// + /// The + public byte[] ToByteArray() + { + byte[]? data = this.Data; + + if (data is null) + { + return []; + } + + byte[] result = new byte[data.Length]; + this.Data.AsSpan().CopyTo(result); + return result; + } + + /// + public XmpProfile DeepClone() + { + byte[]? data = this.Data; + if (data is null) + { + // Preserve the semantics of an "empty" profile when cloning. + return new XmpProfile(); + } + + byte[] clone = new byte[data.Length]; + data.AsSpan().CopyTo(clone); + return new XmpProfile(clone); + } + + private static byte[] SerializeDocument(XDocument document) + { + using MemoryStream ms = new(); + + XmlWriterSettings writerSettings = new() + { + Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), // no BOM + OmitXmlDeclaration = true, // generally safer for XMP consumers + Indent = false, + NewLineHandling = NewLineHandling.None + }; + + using (XmlWriter xw = XmlWriter.Create(ms, writerSettings)) + { + document.Save(xw); + } + + return ms.ToArray(); + } + + private static byte[]? NormalizeDataIfNeeded(byte[]? data) + { + if (data is null || data.Length == 0) + { + return data; + } + + // Allocation-free fast path for the normal case. + + // Check for UTF-8 BOM (0xEF,0xBB,0xBF) + bool hasBom = data.Length >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF; + + // XMP metadata is commonly stored in fixed-size container blocks (e.g. TIFF tag 700). + // Producers often pad unused space so the packet can be updated in-place without + // rewriting the file. In practice this padding is either NUL (0x00) from the container + // or 0x0F used by Adobe XMP writers. Both are invalid XML and must be trimmed. + bool hasTrailingPad = data[^1] is 0 or 0x0F; + + if (!hasBom && !hasTrailingPad) + { + return data; + } + + int start = hasBom ? 3 : 0; + int end = data.Length; + + if (hasTrailingPad) + { + while (end > start) + { + byte b = data[end - 1]; + if (b is not 0 and not 0x0F) + { + break; + } + + end--; + } + } + + int length = end - start; + if (length <= 0) + { + return null; + } + + byte[] normalized = new byte[length]; + Buffer.BlockCopy(data, start, normalized, 0, length); + return normalized; + } + } +} diff --git a/ImageSharp/PixelAccessor{TPixel}.cs b/ImageSharp/PixelAccessor{TPixel}.cs new file mode 100644 index 0000000..fb9ccf6 --- /dev/null +++ b/ImageSharp/PixelAccessor{TPixel}.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp { + /// + /// A delegate to be executed on a . + /// + /// The pixel type. + public delegate void PixelAccessorAction(PixelAccessor pixelAccessor) + where TPixel : unmanaged, IPixel; + + /// + /// A delegate to be executed on two instances of . + /// + /// The first pixel type. + /// The second pixel type. + public delegate void PixelAccessorAction( + PixelAccessor pixelAccessor1, + PixelAccessor pixelAccessor2) + where TPixel1 : unmanaged, IPixel + where TPixel2 : unmanaged, IPixel; + + /// + /// A delegate to be executed on three instances of . + /// + /// The first pixel type. + /// The second pixel type. + /// The third pixel type. + public delegate void PixelAccessorAction( + PixelAccessor pixelAccessor1, + PixelAccessor pixelAccessor2, + PixelAccessor pixelAccessor3) + where TPixel1 : unmanaged, IPixel + where TPixel2 : unmanaged, IPixel + where TPixel3 : unmanaged, IPixel; + + /// + /// Provides efficient access the pixel buffers of an . + /// + /// The pixel type. + public ref struct PixelAccessor + where TPixel : unmanaged, IPixel + { + private Buffer2D buffer; + + internal PixelAccessor(Buffer2D buffer) => this.buffer = buffer; + + /// + /// Gets the width of the backing . + /// + public int Width => this.buffer.Width; + + /// + /// Gets the height of the backing . + /// + public int Height => this.buffer.Height; + + /// + /// Gets the representation of the pixels as a of contiguous memory + /// at row beginning from the first pixel on that row. + /// + /// The row index. + /// The . + /// Thrown when row index is out of range. + public Span GetRowSpan(int rowIndex) => this.buffer.DangerousGetRowSpan(rowIndex); + } +} diff --git a/ImageSharp/PixelFormats/HalfTypeHelper.cs b/ImageSharp/PixelFormats/HalfTypeHelper.cs new file mode 100644 index 0000000..6ff3fce --- /dev/null +++ b/ImageSharp/PixelFormats/HalfTypeHelper.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Helper methods for packing and unpacking floating point values + /// + internal static class HalfTypeHelper + { + /// + /// Packs a into an + /// + /// The float to pack + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ushort Pack(float value) => BitConverter.HalfToUInt16Bits((Half)value); + + /// + /// Unpacks a into a . + /// + /// The value. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static float Unpack(ushort value) => (float)BitConverter.UInt16BitsToHalf(value); + } +} diff --git a/ImageSharp/PixelFormats/IPackedVector{TPacked}.cs b/ImageSharp/PixelFormats/IPackedVector{TPacked}.cs new file mode 100644 index 0000000..ce98a9b --- /dev/null +++ b/ImageSharp/PixelFormats/IPackedVector{TPacked}.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// This interface exists for ensuring signature compatibility to MonoGame and XNA packed color types. + /// + /// + /// The packed format. uint, long, float. + public interface IPackedVector : IPixel + where TPacked : struct, IEquatable + { + /// + /// Gets or sets the packed representation of the value. + /// + TPacked PackedValue { get; set; } + } +} diff --git a/ImageSharp/PixelFormats/IPixel.cs b/ImageSharp/PixelFormats/IPixel.cs new file mode 100644 index 0000000..c3e555b --- /dev/null +++ b/ImageSharp/PixelFormats/IPixel.cs @@ -0,0 +1,165 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// An interface that represents a generic pixel type. + /// The naming convention of each pixel format is to order the color components from least significant to most significant, reading from left to right. + /// For example in the pixel format the R component is the least significant byte, and the A component is the most significant. + /// + /// The type implementing this interface + public interface IPixel : IPixel, IEquatable + where TSelf : unmanaged, IPixel + { +#pragma warning disable CA1000 // Do not declare static members on generic types + /// + /// Creates a instance for this pixel type. + /// This method is not intended to be consumed directly. Use instead. + /// + /// The instance. + static abstract PixelOperations CreatePixelOperations(); + + /// + /// Initializes the pixel instance from a generic a generic ("scaled") representation + /// with values scaled and clamped between 0 and 1 + /// + /// The vector to load the pixel from. + /// The . + static abstract TSelf FromScaledVector4(Vector4 source); + + /// + /// Initializes the pixel instance from a which is specific to the current pixel type. + /// + /// The vector to load the pixel from. + /// The . + static abstract TSelf FromVector4(Vector4 source); + + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + /// The . + static abstract TSelf FromAbgr32(Abgr32 source); + + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + /// The . + static abstract TSelf FromArgb32(Argb32 source); + + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + /// The . + static abstract TSelf FromBgra5551(Bgra5551 source); + + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + /// The . + static abstract TSelf FromBgr24(Bgr24 source); + + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + /// The . + static abstract TSelf FromBgra32(Bgra32 source); + + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + /// The . + static abstract TSelf FromL8(L8 source); + + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + /// The . + static abstract TSelf FromL16(L16 source); + + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + /// The . + static abstract TSelf FromLa16(La16 source); + + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + /// The . + static abstract TSelf FromLa32(La32 source); + + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + /// The . + static abstract TSelf FromRgb24(Rgb24 source); + + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + /// The . + static abstract TSelf FromRgba32(Rgba32 source); + + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + /// The . + static abstract TSelf FromRgb48(Rgb48 source); + + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + /// The . + static abstract TSelf FromRgba64(Rgba64 source); +#pragma warning restore CA1000 // Do not declare static members on generic types + } + + /// + /// A base interface for all pixels, defining the mandatory operations to be implemented by a pixel type. + /// + public interface IPixel + { + /// + /// Gets the pixel type information. + /// + /// The . + static abstract PixelTypeInfo GetPixelTypeInfo(); + + /// + /// Convert the pixel instance into representation. + /// + /// The + Rgba32 ToRgba32(); + + /// + /// Expands the pixel into a generic ("scaled") representation + /// with values scaled and clamped between 0 and 1. + /// The vector components are typically expanded in least to greatest significance order. + /// + /// The . + Vector4 ToScaledVector4(); + + /// + /// Expands the pixel into a which is specific to the current pixel type. + /// The vector components are typically expanded in least to greatest significance order. + /// + /// The . + Vector4 ToVector4(); + } +} diff --git a/ImageSharp/PixelFormats/PixelAlphaCompositionMode.cs b/ImageSharp/PixelFormats/PixelAlphaCompositionMode.cs new file mode 100644 index 0000000..d010f90 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelAlphaCompositionMode.cs @@ -0,0 +1,70 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Enumerates the various alpha composition modes. + /// + public enum PixelAlphaCompositionMode + { + /// + /// Returns the destination over the source. + /// + SrcOver = 0, + + /// + /// Returns the source colors. + /// + Src, + + /// + /// Returns the source over the destination. + /// + SrcAtop, + + /// + /// The source where the destination and source overlap. + /// + SrcIn, + + /// + /// The destination where the destination and source overlap. + /// + SrcOut, + + /// + /// The destination where the source does not overlap it. + /// + Dest, + + /// + /// The source where they don't overlap otherwise dest in overlapping parts. + /// + DestAtop, + + /// + /// The destination over the source. + /// + DestOver, + + /// + /// The destination where the destination and source overlap. + /// + DestIn, + + /// + /// The source where the destination and source overlap. + /// + DestOut, + + /// + /// The clear. + /// + Clear, + + /// + /// Clear where they overlap. + /// + Xor + } +} diff --git a/ImageSharp/PixelFormats/PixelAlphaRepresentation.cs b/ImageSharp/PixelFormats/PixelAlphaRepresentation.cs new file mode 100644 index 0000000..075cee2 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelAlphaRepresentation.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides enumeration of the alpha value transparency behavior of a pixel format. + /// + public enum PixelAlphaRepresentation + { + /// + /// Indicates that the pixel format does not contain an alpha channel. + /// + None, + + /// + /// Indicates that the transparency behavior is premultiplied. + /// Each color is first scaled by the alpha value. The alpha value itself is the same + /// in both straight and premultiplied alpha. Typically, no color channel value is + /// greater than the alpha channel value. + /// If a color channel value in a premultiplied format is greater than the alpha + /// channel, the standard source-over blending math results in an additive blend. + /// + Associated, + + /// + /// Indicates that the transparency behavior is not premultiplied. + /// The alpha channel indicates the transparency of the color. + /// + Unassociated + } +} diff --git a/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.cs b/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.cs new file mode 100644 index 0000000..6d7ee28 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.cs @@ -0,0 +1,36209 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders; + +/// +/// Collection of Porter Duff alpha blending functions applying different composition models. +/// +/// +/// These functions are designed to be a general solution for all color cases, +/// that is, they take in account the alpha value of both the backdrop +/// and source, and there's no need to alpha-premultiply neither the backdrop +/// nor the source. +/// Note there are faster functions for when the backdrop color is known +/// to be opaque +/// +internal static class DefaultPixelBlenders + where TPixel : unmanaged, IPixel +{ + + /// + /// A pixel blender that implements the "NormalSrc" composition equation. + /// + public class NormalSrc : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static NormalSrc Instance { get; } = new NormalSrc(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.NormalSrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrc(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrc(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrc(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrc(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrc(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrc(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "MultiplySrc" composition equation. + /// + public class MultiplySrc : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static MultiplySrc Instance { get; } = new MultiplySrc(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplySrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrc(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrc(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrc(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrc(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrc(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrc(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "AddSrc" composition equation. + /// + public class AddSrc : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static AddSrc Instance { get; } = new AddSrc(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.AddSrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrc(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrc(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrc(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrc(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrc(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrc(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "SubtractSrc" composition equation. + /// + public class SubtractSrc : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static SubtractSrc Instance { get; } = new SubtractSrc(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractSrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrc(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrc(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrc(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrc(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrc(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrc(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "ScreenSrc" composition equation. + /// + public class ScreenSrc : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static ScreenSrc Instance { get; } = new ScreenSrc(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenSrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrc(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrc(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrc(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrc(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrc(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrc(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "DarkenSrc" composition equation. + /// + public class DarkenSrc : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static DarkenSrc Instance { get; } = new DarkenSrc(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenSrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrc(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrc(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrc(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrc(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrc(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrc(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "LightenSrc" composition equation. + /// + public class LightenSrc : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static LightenSrc Instance { get; } = new LightenSrc(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.LightenSrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrc(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrc(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrc(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrc(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrc(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrc(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "OverlaySrc" composition equation. + /// + public class OverlaySrc : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static OverlaySrc Instance { get; } = new OverlaySrc(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.OverlaySrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrc(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrc(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrc(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrc(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrc(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrc(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "HardLightSrc" composition equation. + /// + public class HardLightSrc : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static HardLightSrc Instance { get; } = new HardLightSrc(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightSrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrc(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrc(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrc(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrc(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrc(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrc(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrc(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrc(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "NormalSrcAtop" composition equation. + /// + public class NormalSrcAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static NormalSrcAtop Instance { get; } = new NormalSrcAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.NormalSrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrcAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrcAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "MultiplySrcAtop" composition equation. + /// + public class MultiplySrcAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static MultiplySrcAtop Instance { get; } = new MultiplySrcAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplySrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrcAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrcAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "AddSrcAtop" composition equation. + /// + public class AddSrcAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static AddSrcAtop Instance { get; } = new AddSrcAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.AddSrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrcAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrcAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "SubtractSrcAtop" composition equation. + /// + public class SubtractSrcAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static SubtractSrcAtop Instance { get; } = new SubtractSrcAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractSrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrcAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrcAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "ScreenSrcAtop" composition equation. + /// + public class ScreenSrcAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static ScreenSrcAtop Instance { get; } = new ScreenSrcAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenSrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrcAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrcAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "DarkenSrcAtop" composition equation. + /// + public class DarkenSrcAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static DarkenSrcAtop Instance { get; } = new DarkenSrcAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenSrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrcAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrcAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "LightenSrcAtop" composition equation. + /// + public class LightenSrcAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static LightenSrcAtop Instance { get; } = new LightenSrcAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.LightenSrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrcAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrcAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "OverlaySrcAtop" composition equation. + /// + public class OverlaySrcAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static OverlaySrcAtop Instance { get; } = new OverlaySrcAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.OverlaySrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrcAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrcAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "HardLightSrcAtop" composition equation. + /// + public class HardLightSrcAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static HardLightSrcAtop Instance { get; } = new HardLightSrcAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightSrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrcAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrcAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrcAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "NormalSrcOver" composition equation. + /// + public class NormalSrcOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static NormalSrcOver Instance { get; } = new NormalSrcOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.NormalSrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrcOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrcOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "MultiplySrcOver" composition equation. + /// + public class MultiplySrcOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static MultiplySrcOver Instance { get; } = new MultiplySrcOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplySrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrcOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrcOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "AddSrcOver" composition equation. + /// + public class AddSrcOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static AddSrcOver Instance { get; } = new AddSrcOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.AddSrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrcOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrcOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "SubtractSrcOver" composition equation. + /// + public class SubtractSrcOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static SubtractSrcOver Instance { get; } = new SubtractSrcOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractSrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrcOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrcOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "ScreenSrcOver" composition equation. + /// + public class ScreenSrcOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static ScreenSrcOver Instance { get; } = new ScreenSrcOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenSrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrcOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrcOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "DarkenSrcOver" composition equation. + /// + public class DarkenSrcOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static DarkenSrcOver Instance { get; } = new DarkenSrcOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenSrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrcOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrcOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "LightenSrcOver" composition equation. + /// + public class LightenSrcOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static LightenSrcOver Instance { get; } = new LightenSrcOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.LightenSrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrcOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrcOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "OverlaySrcOver" composition equation. + /// + public class OverlaySrcOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static OverlaySrcOver Instance { get; } = new OverlaySrcOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.OverlaySrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrcOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrcOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "HardLightSrcOver" composition equation. + /// + public class HardLightSrcOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static HardLightSrcOver Instance { get; } = new HardLightSrcOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightSrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrcOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrcOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrcOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "NormalSrcIn" composition equation. + /// + public class NormalSrcIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static NormalSrcIn Instance { get; } = new NormalSrcIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.NormalSrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrcIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrcIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "MultiplySrcIn" composition equation. + /// + public class MultiplySrcIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static MultiplySrcIn Instance { get; } = new MultiplySrcIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplySrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrcIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrcIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "AddSrcIn" composition equation. + /// + public class AddSrcIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static AddSrcIn Instance { get; } = new AddSrcIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.AddSrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrcIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrcIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "SubtractSrcIn" composition equation. + /// + public class SubtractSrcIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static SubtractSrcIn Instance { get; } = new SubtractSrcIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractSrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrcIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrcIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "ScreenSrcIn" composition equation. + /// + public class ScreenSrcIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static ScreenSrcIn Instance { get; } = new ScreenSrcIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenSrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrcIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrcIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "DarkenSrcIn" composition equation. + /// + public class DarkenSrcIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static DarkenSrcIn Instance { get; } = new DarkenSrcIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenSrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrcIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrcIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "LightenSrcIn" composition equation. + /// + public class LightenSrcIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static LightenSrcIn Instance { get; } = new LightenSrcIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.LightenSrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrcIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrcIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "OverlaySrcIn" composition equation. + /// + public class OverlaySrcIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static OverlaySrcIn Instance { get; } = new OverlaySrcIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.OverlaySrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrcIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrcIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "HardLightSrcIn" composition equation. + /// + public class HardLightSrcIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static HardLightSrcIn Instance { get; } = new HardLightSrcIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightSrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrcIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrcIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrcIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "NormalSrcOut" composition equation. + /// + public class NormalSrcOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static NormalSrcOut Instance { get; } = new NormalSrcOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.NormalSrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrcOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrcOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "MultiplySrcOut" composition equation. + /// + public class MultiplySrcOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static MultiplySrcOut Instance { get; } = new MultiplySrcOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplySrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrcOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplySrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrcOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplySrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplySrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplySrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "AddSrcOut" composition equation. + /// + public class AddSrcOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static AddSrcOut Instance { get; } = new AddSrcOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.AddSrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrcOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrcOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "SubtractSrcOut" composition equation. + /// + public class SubtractSrcOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static SubtractSrcOut Instance { get; } = new SubtractSrcOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractSrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrcOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrcOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "ScreenSrcOut" composition equation. + /// + public class ScreenSrcOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static ScreenSrcOut Instance { get; } = new ScreenSrcOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenSrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrcOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrcOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "DarkenSrcOut" composition equation. + /// + public class DarkenSrcOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static DarkenSrcOut Instance { get; } = new DarkenSrcOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenSrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrcOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrcOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "LightenSrcOut" composition equation. + /// + public class LightenSrcOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static LightenSrcOut Instance { get; } = new LightenSrcOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.LightenSrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrcOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrcOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "OverlaySrcOut" composition equation. + /// + public class OverlaySrcOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static OverlaySrcOut Instance { get; } = new OverlaySrcOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.OverlaySrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrcOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlaySrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrcOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlaySrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlaySrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlaySrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "HardLightSrcOut" composition equation. + /// + public class HardLightSrcOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static HardLightSrcOut Instance { get; } = new HardLightSrcOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightSrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrcOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrcOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightSrcOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightSrcOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "NormalDest" composition equation. + /// + public class NormalDest : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static NormalDest Instance { get; } = new NormalDest(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.NormalDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDest(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDest(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDest(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDest(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDest(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDest(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "MultiplyDest" composition equation. + /// + public class MultiplyDest : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static MultiplyDest Instance { get; } = new MultiplyDest(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplyDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDest(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDest(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDest(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDest(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDest(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDest(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "AddDest" composition equation. + /// + public class AddDest : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static AddDest Instance { get; } = new AddDest(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.AddDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDest(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDest(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDest(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDest(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDest(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDest(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "SubtractDest" composition equation. + /// + public class SubtractDest : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static SubtractDest Instance { get; } = new SubtractDest(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDest(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDest(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDest(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDest(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDest(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDest(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "ScreenDest" composition equation. + /// + public class ScreenDest : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static ScreenDest Instance { get; } = new ScreenDest(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDest(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDest(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDest(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDest(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDest(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDest(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "DarkenDest" composition equation. + /// + public class DarkenDest : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static DarkenDest Instance { get; } = new DarkenDest(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDest(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDest(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDest(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDest(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDest(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDest(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "LightenDest" composition equation. + /// + public class LightenDest : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static LightenDest Instance { get; } = new LightenDest(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.LightenDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDest(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDest(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDest(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDest(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDest(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDest(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "OverlayDest" composition equation. + /// + public class OverlayDest : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static OverlayDest Instance { get; } = new OverlayDest(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.OverlayDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDest(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDest(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDest(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDest(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDest(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDest(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "HardLightDest" composition equation. + /// + public class HardLightDest : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static HardLightDest Instance { get; } = new HardLightDest(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDest(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDest(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDest(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDest(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDest(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDest(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDest(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDest(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "NormalDestAtop" composition equation. + /// + public class NormalDestAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static NormalDestAtop Instance { get; } = new NormalDestAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.NormalDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDestAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDestAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "MultiplyDestAtop" composition equation. + /// + public class MultiplyDestAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static MultiplyDestAtop Instance { get; } = new MultiplyDestAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplyDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDestAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDestAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "AddDestAtop" composition equation. + /// + public class AddDestAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static AddDestAtop Instance { get; } = new AddDestAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.AddDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDestAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDestAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "SubtractDestAtop" composition equation. + /// + public class SubtractDestAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static SubtractDestAtop Instance { get; } = new SubtractDestAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDestAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDestAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "ScreenDestAtop" composition equation. + /// + public class ScreenDestAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static ScreenDestAtop Instance { get; } = new ScreenDestAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDestAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDestAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "DarkenDestAtop" composition equation. + /// + public class DarkenDestAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static DarkenDestAtop Instance { get; } = new DarkenDestAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDestAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDestAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "LightenDestAtop" composition equation. + /// + public class LightenDestAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static LightenDestAtop Instance { get; } = new LightenDestAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.LightenDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDestAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDestAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "OverlayDestAtop" composition equation. + /// + public class OverlayDestAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static OverlayDestAtop Instance { get; } = new OverlayDestAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.OverlayDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDestAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDestAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "HardLightDestAtop" composition equation. + /// + public class HardLightDestAtop : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static HardLightDestAtop Instance { get; } = new HardLightDestAtop(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestAtop(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDestAtop(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestAtop(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestAtop(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDestAtop(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestAtop(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDestAtop(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestAtop(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "NormalDestOver" composition equation. + /// + public class NormalDestOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static NormalDestOver Instance { get; } = new NormalDestOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.NormalDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDestOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDestOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "MultiplyDestOver" composition equation. + /// + public class MultiplyDestOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static MultiplyDestOver Instance { get; } = new MultiplyDestOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplyDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDestOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDestOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "AddDestOver" composition equation. + /// + public class AddDestOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static AddDestOver Instance { get; } = new AddDestOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.AddDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDestOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDestOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "SubtractDestOver" composition equation. + /// + public class SubtractDestOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static SubtractDestOver Instance { get; } = new SubtractDestOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDestOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDestOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "ScreenDestOver" composition equation. + /// + public class ScreenDestOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static ScreenDestOver Instance { get; } = new ScreenDestOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDestOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDestOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "DarkenDestOver" composition equation. + /// + public class DarkenDestOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static DarkenDestOver Instance { get; } = new DarkenDestOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDestOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDestOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "LightenDestOver" composition equation. + /// + public class LightenDestOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static LightenDestOver Instance { get; } = new LightenDestOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.LightenDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDestOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDestOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "OverlayDestOver" composition equation. + /// + public class OverlayDestOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static OverlayDestOver Instance { get; } = new OverlayDestOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.OverlayDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDestOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDestOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "HardLightDestOver" composition equation. + /// + public class HardLightDestOver : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static HardLightDestOver Instance { get; } = new HardLightDestOver(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestOver(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDestOver(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestOver(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestOver(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDestOver(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestOver(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDestOver(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestOver(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "NormalDestIn" composition equation. + /// + public class NormalDestIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static NormalDestIn Instance { get; } = new NormalDestIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.NormalDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDestIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDestIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "MultiplyDestIn" composition equation. + /// + public class MultiplyDestIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static MultiplyDestIn Instance { get; } = new MultiplyDestIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplyDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDestIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDestIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "AddDestIn" composition equation. + /// + public class AddDestIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static AddDestIn Instance { get; } = new AddDestIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.AddDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDestIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDestIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "SubtractDestIn" composition equation. + /// + public class SubtractDestIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static SubtractDestIn Instance { get; } = new SubtractDestIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDestIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDestIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "ScreenDestIn" composition equation. + /// + public class ScreenDestIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static ScreenDestIn Instance { get; } = new ScreenDestIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDestIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDestIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "DarkenDestIn" composition equation. + /// + public class DarkenDestIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static DarkenDestIn Instance { get; } = new DarkenDestIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDestIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDestIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "LightenDestIn" composition equation. + /// + public class LightenDestIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static LightenDestIn Instance { get; } = new LightenDestIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.LightenDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDestIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDestIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "OverlayDestIn" composition equation. + /// + public class OverlayDestIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static OverlayDestIn Instance { get; } = new OverlayDestIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.OverlayDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDestIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDestIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "HardLightDestIn" composition equation. + /// + public class HardLightDestIn : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static HardLightDestIn Instance { get; } = new HardLightDestIn(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestIn(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDestIn(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestIn(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestIn(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDestIn(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestIn(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDestIn(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestIn(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "NormalDestOut" composition equation. + /// + public class NormalDestOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static NormalDestOut Instance { get; } = new NormalDestOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.NormalDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDestOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDestOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "MultiplyDestOut" composition equation. + /// + public class MultiplyDestOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static MultiplyDestOut Instance { get; } = new MultiplyDestOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplyDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDestOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDestOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "AddDestOut" composition equation. + /// + public class AddDestOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static AddDestOut Instance { get; } = new AddDestOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.AddDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDestOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDestOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "SubtractDestOut" composition equation. + /// + public class SubtractDestOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static SubtractDestOut Instance { get; } = new SubtractDestOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDestOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDestOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "ScreenDestOut" composition equation. + /// + public class ScreenDestOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static ScreenDestOut Instance { get; } = new ScreenDestOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDestOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDestOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "DarkenDestOut" composition equation. + /// + public class DarkenDestOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static DarkenDestOut Instance { get; } = new DarkenDestOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDestOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDestOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "LightenDestOut" composition equation. + /// + public class LightenDestOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static LightenDestOut Instance { get; } = new LightenDestOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.LightenDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDestOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDestOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "OverlayDestOut" composition equation. + /// + public class OverlayDestOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static OverlayDestOut Instance { get; } = new OverlayDestOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.OverlayDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDestOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDestOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "HardLightDestOut" composition equation. + /// + public class HardLightDestOut : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static HardLightDestOut Instance { get; } = new HardLightDestOut(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestOut(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDestOut(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestOut(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestOut(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDestOut(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestOut(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightDestOut(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightDestOut(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "NormalClear" composition equation. + /// + public class NormalClear : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static NormalClear Instance { get; } = new NormalClear(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.NormalClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalClear(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalClear(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalClear(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalClear(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalClear(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalClear(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "MultiplyClear" composition equation. + /// + public class MultiplyClear : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static MultiplyClear Instance { get; } = new MultiplyClear(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplyClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyClear(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyClear(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyClear(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyClear(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyClear(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyClear(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "AddClear" composition equation. + /// + public class AddClear : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static AddClear Instance { get; } = new AddClear(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.AddClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddClear(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddClear(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddClear(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddClear(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddClear(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddClear(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "SubtractClear" composition equation. + /// + public class SubtractClear : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static SubtractClear Instance { get; } = new SubtractClear(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractClear(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractClear(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractClear(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractClear(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractClear(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractClear(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "ScreenClear" composition equation. + /// + public class ScreenClear : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static ScreenClear Instance { get; } = new ScreenClear(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenClear(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenClear(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenClear(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenClear(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenClear(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenClear(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "DarkenClear" composition equation. + /// + public class DarkenClear : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static DarkenClear Instance { get; } = new DarkenClear(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenClear(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenClear(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenClear(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenClear(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenClear(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenClear(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "LightenClear" composition equation. + /// + public class LightenClear : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static LightenClear Instance { get; } = new LightenClear(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.LightenClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenClear(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenClear(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenClear(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenClear(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenClear(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenClear(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "OverlayClear" composition equation. + /// + public class OverlayClear : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static OverlayClear Instance { get; } = new OverlayClear(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.OverlayClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayClear(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayClear(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayClear(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayClear(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayClear(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayClear(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "HardLightClear" composition equation. + /// + public class HardLightClear : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static HardLightClear Instance { get; } = new HardLightClear(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightClear(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightClear(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightClear(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightClear(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightClear(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightClear(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightClear(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightClear(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "NormalXor" composition equation. + /// + public class NormalXor : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static NormalXor Instance { get; } = new NormalXor(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.NormalXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalXor(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalXor(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalXor(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalXor(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.NormalXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalXor(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalXor(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.NormalXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.NormalXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.NormalXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "MultiplyXor" composition equation. + /// + public class MultiplyXor : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static MultiplyXor Instance { get; } = new MultiplyXor(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplyXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyXor(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyXor(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyXor(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyXor(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.MultiplyXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyXor(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyXor(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.MultiplyXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.MultiplyXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.MultiplyXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "AddXor" composition equation. + /// + public class AddXor : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static AddXor Instance { get; } = new AddXor(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.AddXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddXor(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddXor(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddXor(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddXor(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.AddXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddXor(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddXor(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.AddXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.AddXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.AddXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "SubtractXor" composition equation. + /// + public class SubtractXor : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static SubtractXor Instance { get; } = new SubtractXor(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractXor(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractXor(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractXor(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractXor(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.SubtractXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractXor(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractXor(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.SubtractXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.SubtractXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.SubtractXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "ScreenXor" composition equation. + /// + public class ScreenXor : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static ScreenXor Instance { get; } = new ScreenXor(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenXor(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenXor(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenXor(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenXor(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.ScreenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenXor(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenXor(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.ScreenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.ScreenXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.ScreenXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "DarkenXor" composition equation. + /// + public class DarkenXor : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static DarkenXor Instance { get; } = new DarkenXor(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenXor(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenXor(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenXor(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenXor(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.DarkenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenXor(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenXor(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.DarkenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.DarkenXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.DarkenXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "LightenXor" composition equation. + /// + public class LightenXor : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static LightenXor Instance { get; } = new LightenXor(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.LightenXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenXor(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenXor(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenXor(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenXor(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.LightenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenXor(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenXor(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.LightenXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.LightenXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.LightenXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "OverlayXor" composition equation. + /// + public class OverlayXor : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static OverlayXor Instance { get; } = new OverlayXor(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.OverlayXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayXor(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayXor(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayXor(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayXor(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.OverlayXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayXor(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayXor(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.OverlayXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.OverlayXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.OverlayXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + + /// + /// A pixel blender that implements the "HardLightXor" composition equation. + /// + public class HardLightXor : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static HardLightXor Instance { get; } = new HardLightXor(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightXor(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightXor(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightXor(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightXor(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.HardLightXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightXor(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightXor(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.HardLightXor(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.HardLightXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.HardLightXor(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + +} diff --git a/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.tt b/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.tt new file mode 100644 index 0000000..abd93b6 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.tt @@ -0,0 +1,411 @@ +<# +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#> +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ output extension=".cs" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders; + +/// +/// Collection of Porter Duff alpha blending functions applying different composition models. +/// +/// +/// These functions are designed to be a general solution for all color cases, +/// that is, they take in account the alpha value of both the backdrop +/// and source, and there's no need to alpha-premultiply neither the backdrop +/// nor the source. +/// Note there are faster functions for when the backdrop color is known +/// to be opaque +/// +internal static class DefaultPixelBlenders + where TPixel : unmanaged, IPixel +{ + +<# +var composers = new []{ + "Src", + "SrcAtop", + "SrcOver", + "SrcIn", + "SrcOut", + "Dest", + "DestAtop", + "DestOver", + "DestIn", + "DestOut", + "Clear", + "Xor", +}; + +var blenders = new []{ + "Normal", + "Multiply", + "Add", + "Subtract", + "Screen", + "Darken", + "Lighten", + "Overlay", + "HardLight" +}; + + foreach(var composer in composers) { + foreach(var blender in blenders) { + + var blender_composer= $"{blender}{composer}"; +#> + /// + /// A pixel blender that implements the "<#= blender_composer#>" composition equation. + /// + public class <#= blender_composer#> : PixelBlender + { + /// + /// Gets the static instance of this blender. + /// + public static <#=blender_composer#> Instance { get; } = new <#=blender_composer#>(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + return TPixel.FromScaledVector4(PorterDuffFunctions.<#=blender_composer#>(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.<#=blender_composer#>(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source[i], amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.<#=blender_composer#>(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source[i], amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source[i], amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + amount = Numerics.Clamp(amount, 0, 1); + + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 opacity = Vector512.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.<#=blender_composer#>(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source, amount); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 opacity = Vector256.Create(amount); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + destinationBase = PorterDuffFunctions.<#=blender_composer#>(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source, amount); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source, amount); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.<#=blender_composer#>(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.<#=blender_composer#>(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + + /// + protected override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + if (Avx512F.IsSupported && destination.Length >= 4) + { + // Divide by 4 as 4 elements per Vector4 and 16 per Vector512 + ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector512 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 4u); + + ref Vector512 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector512 sourceBase = Vector512.Create( + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W, + source.X, source.Y, source.Z, source.W); + Vector512 vOne = Vector512.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + float amount0 = amountBase; + float amount1 = Unsafe.Add(ref amountBase, 1); + float amount2 = Unsafe.Add(ref amountBase, 2); + float amount3 = Unsafe.Add(ref amountBase, 3); + + // We need to create a Vector512 containing the current four amount values + // taking up each quarter of the Vector512 and then clamp them. + Vector512 opacity = Vector512.Create( + amount0, amount0, amount0, amount0, + amount1, amount1, amount1, amount1, + amount2, amount2, amount2, amount2, + amount3, amount3, amount3, amount3); + opacity = Vector512.Min(Vector512.Max(Vector512.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.<#=blender_composer#>(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 4); + } + + int remainder = Numerics.Modulo4(destination.Length); + if (remainder != 0) + { + for (int i = destination.Length - remainder; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + else if (Avx2.IsSupported && destination.Length >= 2) + { + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref float amountBase = ref MemoryMarshal.GetReference(amount); + + Vector256 sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W); + Vector256 vOne = Vector256.Create(1F); + + while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) + { + // We need to create a Vector256 containing the current and next amount values + // taking up each half of the Vector256 and then clamp them. + Vector256 opacity = Vector256.Create( + Vector128.Create(amountBase), + Vector128.Create(Unsafe.Add(ref amountBase, 1))); + opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); + + destinationBase = PorterDuffFunctions.<#=blender_composer#>(backgroundBase, sourceBase, opacity); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); + amountBase = ref Unsafe.Add(ref amountBase, 2); + } + + if (Numerics.Modulo2(destination.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + int i = destination.Length - 1; + destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + else + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source, Numerics.Clamp(amount[i], 0, 1F)); + } + } + } + } + +<# + } +} + +#> +} diff --git a/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.cs b/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.cs new file mode 100644 index 0000000..d32966c --- /dev/null +++ b/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.cs @@ -0,0 +1,6054 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders; + +internal static partial class PorterDuffFunctions +{ + + /// + /// Returns the result of the "NormalSrc" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalSrc(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return source; + } + + /// + /// Returns the result of the "NormalSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "NormalSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + /// + /// Returns the result of the "NormalSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalSrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(backdrop, source, Normal(backdrop, source)); + } + + /// + /// Returns the result of the "NormalSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, Normal(backdrop, source)); + } + + /// + /// Returns the result of the "NormalSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(backdrop, source, Normal(backdrop, source)); + } + + /// + /// Returns the result of the "NormalSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalSrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(backdrop, source, Normal(backdrop, source)); + } + + /// + /// Returns the result of the "NormalSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, Normal(backdrop, source)); + } + + /// + /// Returns the result of the "NormalSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(backdrop, source, Normal(backdrop, source)); + } + + /// + /// Returns the result of the "NormalSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalSrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(backdrop, source); + } + + /// + /// Returns the result of the "NormalSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "NormalSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "NormalSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalSrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(backdrop, source); + } + + /// + /// Returns the result of the "NormalSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "NormalSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "NormalDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "NormalDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "NormalDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "NormalDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(source, backdrop, Normal(source, backdrop)); + } + + /// + /// Returns the result of the "NormalDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, Normal(source, backdrop)); + } + + /// + /// Returns the result of the "NormalDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(source, backdrop, Normal(source, backdrop)); + } + + /// + /// Returns the result of the "NormalDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(source, backdrop, Normal(source, backdrop)); + } + + /// + /// Returns the result of the "NormalDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, Normal(source, backdrop)); + } + + /// + /// Returns the result of the "NormalDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(source, backdrop, Normal(source, backdrop)); + } + + /// + /// Returns the result of the "NormalDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(source, backdrop); + } + + /// + /// Returns the result of the "NormalDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "NormalDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "NormalDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(source, backdrop); + } + + /// + /// Returns the result of the "NormalDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "NormalDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "NormalXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalXor(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Xor(backdrop, source); + } + + /// + /// Returns the result of the "NormalXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "NormalXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "NormalClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalClear(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Clear(backdrop, source); + } + + /// + /// Returns the result of the "NormalClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "NormalClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + + /// + /// Returns the result of the "NormalSrc" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel NormalSrc(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(NormalSrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "NormalSrcAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel NormalSrcAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(NormalSrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "NormalSrcOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel NormalSrcOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(NormalSrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "NormalSrcIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel NormalSrcIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(NormalSrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "NormalSrcOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel NormalSrcOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(NormalSrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "NormalDest" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel NormalDest(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(NormalDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "NormalDestAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel NormalDestAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(NormalDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "NormalDestOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel NormalDestOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(NormalDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "NormalDestIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel NormalDestIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(NormalDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "NormalDestOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel NormalDestOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(NormalDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "NormalClear" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel NormalClear(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(NormalClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "NormalXor" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel NormalXor(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(NormalXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "MultiplySrc" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplySrc(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return source; + } + + /// + /// Returns the result of the "MultiplySrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplySrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "MultiplySrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplySrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + /// + /// Returns the result of the "MultiplySrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplySrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(backdrop, source, Multiply(backdrop, source)); + } + + /// + /// Returns the result of the "MultiplySrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplySrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, Multiply(backdrop, source)); + } + + /// + /// Returns the result of the "MultiplySrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplySrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(backdrop, source, Multiply(backdrop, source)); + } + + /// + /// Returns the result of the "MultiplySrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplySrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(backdrop, source, Multiply(backdrop, source)); + } + + /// + /// Returns the result of the "MultiplySrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplySrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, Multiply(backdrop, source)); + } + + /// + /// Returns the result of the "MultiplySrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplySrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(backdrop, source, Multiply(backdrop, source)); + } + + /// + /// Returns the result of the "MultiplySrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplySrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(backdrop, source); + } + + /// + /// Returns the result of the "MultiplySrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplySrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "MultiplySrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplySrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "MultiplySrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplySrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(backdrop, source); + } + + /// + /// Returns the result of the "MultiplySrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplySrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "MultiplySrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplySrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "MultiplyDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplyDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "MultiplyDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "MultiplyDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "MultiplyDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplyDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(source, backdrop, Multiply(source, backdrop)); + } + + /// + /// Returns the result of the "MultiplyDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, Multiply(source, backdrop)); + } + + /// + /// Returns the result of the "MultiplyDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(source, backdrop, Multiply(source, backdrop)); + } + + /// + /// Returns the result of the "MultiplyDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplyDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(source, backdrop, Multiply(source, backdrop)); + } + + /// + /// Returns the result of the "MultiplyDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, Multiply(source, backdrop)); + } + + /// + /// Returns the result of the "MultiplyDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(source, backdrop, Multiply(source, backdrop)); + } + + /// + /// Returns the result of the "MultiplyDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplyDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(source, backdrop); + } + + /// + /// Returns the result of the "MultiplyDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "MultiplyDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "MultiplyDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplyDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(source, backdrop); + } + + /// + /// Returns the result of the "MultiplyDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "MultiplyDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "MultiplyXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplyXor(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Xor(backdrop, source); + } + + /// + /// Returns the result of the "MultiplyXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "MultiplyXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "MultiplyClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplyClear(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Clear(backdrop, source); + } + + /// + /// Returns the result of the "MultiplyClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "MultiplyClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + + /// + /// Returns the result of the "MultiplySrc" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel MultiplySrc(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(MultiplySrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "MultiplySrcAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel MultiplySrcAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(MultiplySrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "MultiplySrcOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel MultiplySrcOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(MultiplySrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "MultiplySrcIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel MultiplySrcIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(MultiplySrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "MultiplySrcOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel MultiplySrcOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(MultiplySrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "MultiplyDest" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel MultiplyDest(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(MultiplyDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "MultiplyDestAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel MultiplyDestAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(MultiplyDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "MultiplyDestOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel MultiplyDestOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(MultiplyDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "MultiplyDestIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel MultiplyDestIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(MultiplyDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "MultiplyDestOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel MultiplyDestOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(MultiplyDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "MultiplyClear" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel MultiplyClear(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(MultiplyClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "MultiplyXor" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel MultiplyXor(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(MultiplyXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "AddSrc" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddSrc(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return source; + } + + /// + /// Returns the result of the "AddSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "AddSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + /// + /// Returns the result of the "AddSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddSrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(backdrop, source, Add(backdrop, source)); + } + + /// + /// Returns the result of the "AddSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, Add(backdrop, source)); + } + + /// + /// Returns the result of the "AddSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(backdrop, source, Add(backdrop, source)); + } + + /// + /// Returns the result of the "AddSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddSrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(backdrop, source, Add(backdrop, source)); + } + + /// + /// Returns the result of the "AddSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, Add(backdrop, source)); + } + + /// + /// Returns the result of the "AddSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(backdrop, source, Add(backdrop, source)); + } + + /// + /// Returns the result of the "AddSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddSrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(backdrop, source); + } + + /// + /// Returns the result of the "AddSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "AddSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "AddSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddSrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(backdrop, source); + } + + /// + /// Returns the result of the "AddSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "AddSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "AddDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "AddDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "AddDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "AddDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(source, backdrop, Add(source, backdrop)); + } + + /// + /// Returns the result of the "AddDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, Add(source, backdrop)); + } + + /// + /// Returns the result of the "AddDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(source, backdrop, Add(source, backdrop)); + } + + /// + /// Returns the result of the "AddDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(source, backdrop, Add(source, backdrop)); + } + + /// + /// Returns the result of the "AddDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, Add(source, backdrop)); + } + + /// + /// Returns the result of the "AddDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(source, backdrop, Add(source, backdrop)); + } + + /// + /// Returns the result of the "AddDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(source, backdrop); + } + + /// + /// Returns the result of the "AddDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "AddDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "AddDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(source, backdrop); + } + + /// + /// Returns the result of the "AddDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "AddDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "AddXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddXor(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Xor(backdrop, source); + } + + /// + /// Returns the result of the "AddXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "AddXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "AddClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddClear(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Clear(backdrop, source); + } + + /// + /// Returns the result of the "AddClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "AddClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + + /// + /// Returns the result of the "AddSrc" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel AddSrc(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(AddSrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "AddSrcAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel AddSrcAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(AddSrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "AddSrcOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel AddSrcOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(AddSrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "AddSrcIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel AddSrcIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(AddSrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "AddSrcOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel AddSrcOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(AddSrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "AddDest" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel AddDest(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(AddDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "AddDestAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel AddDestAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(AddDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "AddDestOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel AddDestOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(AddDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "AddDestIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel AddDestIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(AddDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "AddDestOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel AddDestOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(AddDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "AddClear" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel AddClear(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(AddClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "AddXor" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel AddXor(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(AddXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "SubtractSrc" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractSrc(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return source; + } + + /// + /// Returns the result of the "SubtractSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "SubtractSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + /// + /// Returns the result of the "SubtractSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractSrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(backdrop, source, Subtract(backdrop, source)); + } + + /// + /// Returns the result of the "SubtractSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, Subtract(backdrop, source)); + } + + /// + /// Returns the result of the "SubtractSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(backdrop, source, Subtract(backdrop, source)); + } + + /// + /// Returns the result of the "SubtractSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractSrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(backdrop, source, Subtract(backdrop, source)); + } + + /// + /// Returns the result of the "SubtractSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, Subtract(backdrop, source)); + } + + /// + /// Returns the result of the "SubtractSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(backdrop, source, Subtract(backdrop, source)); + } + + /// + /// Returns the result of the "SubtractSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractSrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(backdrop, source); + } + + /// + /// Returns the result of the "SubtractSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "SubtractSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "SubtractSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractSrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(backdrop, source); + } + + /// + /// Returns the result of the "SubtractSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "SubtractSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "SubtractDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "SubtractDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "SubtractDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "SubtractDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(source, backdrop, Subtract(source, backdrop)); + } + + /// + /// Returns the result of the "SubtractDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, Subtract(source, backdrop)); + } + + /// + /// Returns the result of the "SubtractDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(source, backdrop, Subtract(source, backdrop)); + } + + /// + /// Returns the result of the "SubtractDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(source, backdrop, Subtract(source, backdrop)); + } + + /// + /// Returns the result of the "SubtractDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, Subtract(source, backdrop)); + } + + /// + /// Returns the result of the "SubtractDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(source, backdrop, Subtract(source, backdrop)); + } + + /// + /// Returns the result of the "SubtractDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(source, backdrop); + } + + /// + /// Returns the result of the "SubtractDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "SubtractDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "SubtractDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(source, backdrop); + } + + /// + /// Returns the result of the "SubtractDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "SubtractDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "SubtractXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractXor(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Xor(backdrop, source); + } + + /// + /// Returns the result of the "SubtractXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "SubtractXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "SubtractClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractClear(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Clear(backdrop, source); + } + + /// + /// Returns the result of the "SubtractClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "SubtractClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + + /// + /// Returns the result of the "SubtractSrc" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel SubtractSrc(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(SubtractSrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "SubtractSrcAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel SubtractSrcAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(SubtractSrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "SubtractSrcOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel SubtractSrcOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(SubtractSrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "SubtractSrcIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel SubtractSrcIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(SubtractSrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "SubtractSrcOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel SubtractSrcOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(SubtractSrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "SubtractDest" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel SubtractDest(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(SubtractDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "SubtractDestAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel SubtractDestAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(SubtractDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "SubtractDestOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel SubtractDestOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(SubtractDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "SubtractDestIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel SubtractDestIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(SubtractDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "SubtractDestOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel SubtractDestOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(SubtractDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "SubtractClear" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel SubtractClear(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(SubtractClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "SubtractXor" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel SubtractXor(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(SubtractXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "ScreenSrc" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenSrc(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return source; + } + + /// + /// Returns the result of the "ScreenSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "ScreenSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + /// + /// Returns the result of the "ScreenSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenSrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(backdrop, source, Screen(backdrop, source)); + } + + /// + /// Returns the result of the "ScreenSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, Screen(backdrop, source)); + } + + /// + /// Returns the result of the "ScreenSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(backdrop, source, Screen(backdrop, source)); + } + + /// + /// Returns the result of the "ScreenSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenSrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(backdrop, source, Screen(backdrop, source)); + } + + /// + /// Returns the result of the "ScreenSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, Screen(backdrop, source)); + } + + /// + /// Returns the result of the "ScreenSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(backdrop, source, Screen(backdrop, source)); + } + + /// + /// Returns the result of the "ScreenSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenSrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(backdrop, source); + } + + /// + /// Returns the result of the "ScreenSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "ScreenSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "ScreenSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenSrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(backdrop, source); + } + + /// + /// Returns the result of the "ScreenSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "ScreenSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "ScreenDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "ScreenDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "ScreenDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "ScreenDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(source, backdrop, Screen(source, backdrop)); + } + + /// + /// Returns the result of the "ScreenDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, Screen(source, backdrop)); + } + + /// + /// Returns the result of the "ScreenDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(source, backdrop, Screen(source, backdrop)); + } + + /// + /// Returns the result of the "ScreenDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(source, backdrop, Screen(source, backdrop)); + } + + /// + /// Returns the result of the "ScreenDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, Screen(source, backdrop)); + } + + /// + /// Returns the result of the "ScreenDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(source, backdrop, Screen(source, backdrop)); + } + + /// + /// Returns the result of the "ScreenDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(source, backdrop); + } + + /// + /// Returns the result of the "ScreenDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "ScreenDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "ScreenDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(source, backdrop); + } + + /// + /// Returns the result of the "ScreenDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "ScreenDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "ScreenXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenXor(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Xor(backdrop, source); + } + + /// + /// Returns the result of the "ScreenXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "ScreenXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "ScreenClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenClear(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Clear(backdrop, source); + } + + /// + /// Returns the result of the "ScreenClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "ScreenClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + + /// + /// Returns the result of the "ScreenSrc" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ScreenSrc(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(ScreenSrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "ScreenSrcAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ScreenSrcAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(ScreenSrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "ScreenSrcOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ScreenSrcOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(ScreenSrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "ScreenSrcIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ScreenSrcIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(ScreenSrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "ScreenSrcOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ScreenSrcOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(ScreenSrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "ScreenDest" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ScreenDest(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(ScreenDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "ScreenDestAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ScreenDestAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(ScreenDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "ScreenDestOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ScreenDestOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(ScreenDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "ScreenDestIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ScreenDestIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(ScreenDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "ScreenDestOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ScreenDestOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(ScreenDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "ScreenClear" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ScreenClear(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(ScreenClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "ScreenXor" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ScreenXor(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(ScreenXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "DarkenSrc" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenSrc(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return source; + } + + /// + /// Returns the result of the "DarkenSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "DarkenSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + /// + /// Returns the result of the "DarkenSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenSrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(backdrop, source, Darken(backdrop, source)); + } + + /// + /// Returns the result of the "DarkenSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, Darken(backdrop, source)); + } + + /// + /// Returns the result of the "DarkenSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(backdrop, source, Darken(backdrop, source)); + } + + /// + /// Returns the result of the "DarkenSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenSrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(backdrop, source, Darken(backdrop, source)); + } + + /// + /// Returns the result of the "DarkenSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, Darken(backdrop, source)); + } + + /// + /// Returns the result of the "DarkenSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(backdrop, source, Darken(backdrop, source)); + } + + /// + /// Returns the result of the "DarkenSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenSrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(backdrop, source); + } + + /// + /// Returns the result of the "DarkenSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "DarkenSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "DarkenSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenSrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(backdrop, source); + } + + /// + /// Returns the result of the "DarkenSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "DarkenSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "DarkenDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "DarkenDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "DarkenDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "DarkenDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(source, backdrop, Darken(source, backdrop)); + } + + /// + /// Returns the result of the "DarkenDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, Darken(source, backdrop)); + } + + /// + /// Returns the result of the "DarkenDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(source, backdrop, Darken(source, backdrop)); + } + + /// + /// Returns the result of the "DarkenDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(source, backdrop, Darken(source, backdrop)); + } + + /// + /// Returns the result of the "DarkenDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, Darken(source, backdrop)); + } + + /// + /// Returns the result of the "DarkenDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(source, backdrop, Darken(source, backdrop)); + } + + /// + /// Returns the result of the "DarkenDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(source, backdrop); + } + + /// + /// Returns the result of the "DarkenDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "DarkenDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "DarkenDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(source, backdrop); + } + + /// + /// Returns the result of the "DarkenDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "DarkenDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "DarkenXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenXor(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Xor(backdrop, source); + } + + /// + /// Returns the result of the "DarkenXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "DarkenXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "DarkenClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenClear(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Clear(backdrop, source); + } + + /// + /// Returns the result of the "DarkenClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "DarkenClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + + /// + /// Returns the result of the "DarkenSrc" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel DarkenSrc(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(DarkenSrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "DarkenSrcAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel DarkenSrcAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(DarkenSrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "DarkenSrcOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel DarkenSrcOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(DarkenSrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "DarkenSrcIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel DarkenSrcIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(DarkenSrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "DarkenSrcOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel DarkenSrcOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(DarkenSrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "DarkenDest" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel DarkenDest(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(DarkenDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "DarkenDestAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel DarkenDestAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(DarkenDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "DarkenDestOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel DarkenDestOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(DarkenDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "DarkenDestIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel DarkenDestIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(DarkenDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "DarkenDestOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel DarkenDestOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(DarkenDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "DarkenClear" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel DarkenClear(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(DarkenClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "DarkenXor" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel DarkenXor(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(DarkenXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "LightenSrc" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenSrc(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return source; + } + + /// + /// Returns the result of the "LightenSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "LightenSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + /// + /// Returns the result of the "LightenSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenSrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(backdrop, source, Lighten(backdrop, source)); + } + + /// + /// Returns the result of the "LightenSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, Lighten(backdrop, source)); + } + + /// + /// Returns the result of the "LightenSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(backdrop, source, Lighten(backdrop, source)); + } + + /// + /// Returns the result of the "LightenSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenSrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(backdrop, source, Lighten(backdrop, source)); + } + + /// + /// Returns the result of the "LightenSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, Lighten(backdrop, source)); + } + + /// + /// Returns the result of the "LightenSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(backdrop, source, Lighten(backdrop, source)); + } + + /// + /// Returns the result of the "LightenSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenSrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(backdrop, source); + } + + /// + /// Returns the result of the "LightenSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "LightenSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "LightenSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenSrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(backdrop, source); + } + + /// + /// Returns the result of the "LightenSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "LightenSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "LightenDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "LightenDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "LightenDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "LightenDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(source, backdrop, Lighten(source, backdrop)); + } + + /// + /// Returns the result of the "LightenDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, Lighten(source, backdrop)); + } + + /// + /// Returns the result of the "LightenDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(source, backdrop, Lighten(source, backdrop)); + } + + /// + /// Returns the result of the "LightenDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(source, backdrop, Lighten(source, backdrop)); + } + + /// + /// Returns the result of the "LightenDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, Lighten(source, backdrop)); + } + + /// + /// Returns the result of the "LightenDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(source, backdrop, Lighten(source, backdrop)); + } + + /// + /// Returns the result of the "LightenDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(source, backdrop); + } + + /// + /// Returns the result of the "LightenDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "LightenDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "LightenDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(source, backdrop); + } + + /// + /// Returns the result of the "LightenDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "LightenDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "LightenXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenXor(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Xor(backdrop, source); + } + + /// + /// Returns the result of the "LightenXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "LightenXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "LightenClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenClear(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Clear(backdrop, source); + } + + /// + /// Returns the result of the "LightenClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "LightenClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + + /// + /// Returns the result of the "LightenSrc" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel LightenSrc(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(LightenSrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "LightenSrcAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel LightenSrcAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(LightenSrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "LightenSrcOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel LightenSrcOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(LightenSrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "LightenSrcIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel LightenSrcIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(LightenSrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "LightenSrcOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel LightenSrcOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(LightenSrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "LightenDest" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel LightenDest(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(LightenDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "LightenDestAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel LightenDestAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(LightenDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "LightenDestOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel LightenDestOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(LightenDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "LightenDestIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel LightenDestIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(LightenDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "LightenDestOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel LightenDestOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(LightenDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "LightenClear" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel LightenClear(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(LightenClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "LightenXor" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel LightenXor(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(LightenXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "OverlaySrc" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlaySrc(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return source; + } + + /// + /// Returns the result of the "OverlaySrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlaySrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "OverlaySrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlaySrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + /// + /// Returns the result of the "OverlaySrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlaySrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(backdrop, source, Overlay(backdrop, source)); + } + + /// + /// Returns the result of the "OverlaySrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlaySrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, Overlay(backdrop, source)); + } + + /// + /// Returns the result of the "OverlaySrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlaySrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(backdrop, source, Overlay(backdrop, source)); + } + + /// + /// Returns the result of the "OverlaySrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlaySrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(backdrop, source, Overlay(backdrop, source)); + } + + /// + /// Returns the result of the "OverlaySrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlaySrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, Overlay(backdrop, source)); + } + + /// + /// Returns the result of the "OverlaySrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlaySrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(backdrop, source, Overlay(backdrop, source)); + } + + /// + /// Returns the result of the "OverlaySrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlaySrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(backdrop, source); + } + + /// + /// Returns the result of the "OverlaySrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlaySrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "OverlaySrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlaySrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "OverlaySrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlaySrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(backdrop, source); + } + + /// + /// Returns the result of the "OverlaySrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlaySrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "OverlaySrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlaySrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "OverlayDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlayDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "OverlayDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlayDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "OverlayDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "OverlayDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlayDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(source, backdrop, Overlay(source, backdrop)); + } + + /// + /// Returns the result of the "OverlayDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlayDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, Overlay(source, backdrop)); + } + + /// + /// Returns the result of the "OverlayDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(source, backdrop, Overlay(source, backdrop)); + } + + /// + /// Returns the result of the "OverlayDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlayDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(source, backdrop, Overlay(source, backdrop)); + } + + /// + /// Returns the result of the "OverlayDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlayDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, Overlay(source, backdrop)); + } + + /// + /// Returns the result of the "OverlayDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(source, backdrop, Overlay(source, backdrop)); + } + + /// + /// Returns the result of the "OverlayDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlayDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(source, backdrop); + } + + /// + /// Returns the result of the "OverlayDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlayDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "OverlayDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "OverlayDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlayDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(source, backdrop); + } + + /// + /// Returns the result of the "OverlayDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlayDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "OverlayDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "OverlayXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlayXor(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Xor(backdrop, source); + } + + /// + /// Returns the result of the "OverlayXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlayXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "OverlayXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "OverlayClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlayClear(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Clear(backdrop, source); + } + + /// + /// Returns the result of the "OverlayClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlayClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "OverlayClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + + /// + /// Returns the result of the "OverlaySrc" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel OverlaySrc(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(OverlaySrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "OverlaySrcAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel OverlaySrcAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(OverlaySrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "OverlaySrcOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel OverlaySrcOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(OverlaySrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "OverlaySrcIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel OverlaySrcIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(OverlaySrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "OverlaySrcOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel OverlaySrcOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(OverlaySrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "OverlayDest" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel OverlayDest(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(OverlayDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "OverlayDestAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel OverlayDestAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(OverlayDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "OverlayDestOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel OverlayDestOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(OverlayDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "OverlayDestIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel OverlayDestIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(OverlayDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "OverlayDestOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel OverlayDestOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(OverlayDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "OverlayClear" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel OverlayClear(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(OverlayClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "OverlayXor" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel OverlayXor(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(OverlayXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "HardLightSrc" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightSrc(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return source; + } + + /// + /// Returns the result of the "HardLightSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "HardLightSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + /// + /// Returns the result of the "HardLightSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightSrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(backdrop, source, HardLight(backdrop, source)); + } + + /// + /// Returns the result of the "HardLightSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, HardLight(backdrop, source)); + } + + /// + /// Returns the result of the "HardLightSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(backdrop, source, HardLight(backdrop, source)); + } + + /// + /// Returns the result of the "HardLightSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightSrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(backdrop, source, HardLight(backdrop, source)); + } + + /// + /// Returns the result of the "HardLightSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, HardLight(backdrop, source)); + } + + /// + /// Returns the result of the "HardLightSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(backdrop, source, HardLight(backdrop, source)); + } + + /// + /// Returns the result of the "HardLightSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightSrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(backdrop, source); + } + + /// + /// Returns the result of the "HardLightSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "HardLightSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "HardLightSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightSrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(backdrop, source); + } + + /// + /// Returns the result of the "HardLightSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "HardLightSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "HardLightDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "HardLightDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "HardLightDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "HardLightDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(source, backdrop, HardLight(source, backdrop)); + } + + /// + /// Returns the result of the "HardLightDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, HardLight(source, backdrop)); + } + + /// + /// Returns the result of the "HardLightDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(source, backdrop, HardLight(source, backdrop)); + } + + /// + /// Returns the result of the "HardLightDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(source, backdrop, HardLight(source, backdrop)); + } + + /// + /// Returns the result of the "HardLightDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, HardLight(source, backdrop)); + } + + /// + /// Returns the result of the "HardLightDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(source, backdrop, HardLight(source, backdrop)); + } + + /// + /// Returns the result of the "HardLightDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(source, backdrop); + } + + /// + /// Returns the result of the "HardLightDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "HardLightDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "HardLightDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(source, backdrop); + } + + /// + /// Returns the result of the "HardLightDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "HardLightDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "HardLightXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightXor(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Xor(backdrop, source); + } + + /// + /// Returns the result of the "HardLightXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "HardLightXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "HardLightClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightClear(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Clear(backdrop, source); + } + + /// + /// Returns the result of the "HardLightClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "HardLightClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + + /// + /// Returns the result of the "HardLightSrc" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel HardLightSrc(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(HardLightSrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "HardLightSrcAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel HardLightSrcAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(HardLightSrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "HardLightSrcOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel HardLightSrcOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(HardLightSrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "HardLightSrcIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel HardLightSrcIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(HardLightSrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "HardLightSrcOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel HardLightSrcOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(HardLightSrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "HardLightDest" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel HardLightDest(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(HardLightDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "HardLightDestAtop" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel HardLightDestAtop(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(HardLightDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "HardLightDestOver" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel HardLightDestOver(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(HardLightDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "HardLightDestIn" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel HardLightDestIn(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(HardLightDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "HardLightDestOut" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel HardLightDestOut(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(HardLightDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "HardLightClear" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel HardLightClear(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(HardLightClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } + + /// + /// Returns the result of the "HardLightXor" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel HardLightXor(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(HardLightXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } +} diff --git a/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.tt b/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.tt new file mode 100644 index 0000000..6ca1d6a --- /dev/null +++ b/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.tt @@ -0,0 +1,561 @@ +<# +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#> +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ output extension=".cs" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders; + +internal static partial class PorterDuffFunctions +{<# void GeneratePixelBlenders(string blender) { #> + + /// + /// Returns the result of the "<#=blender#>Src" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 <#=blender#>Src(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return source; + } + + /// + /// Returns the result of the "<#=blender#>Src compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 <#=blender#>Src(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "<#=blender#>Src compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>Src(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + /// + /// Returns the result of the "<#=blender#>SrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 <#=blender#>SrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(backdrop, source, <#=blender#>(backdrop, source)); + } + + /// + /// Returns the result of the "<#=blender#>SrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 <#=blender#>SrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, <#=blender#>(backdrop, source)); + } + + /// + /// Returns the result of the "<#=blender#>SrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>SrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(backdrop, source, <#=blender#>(backdrop, source)); + } + + /// + /// Returns the result of the "<#=blender#>SrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 <#=blender#>SrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(backdrop, source, <#=blender#>(backdrop, source)); + } + + /// + /// Returns the result of the "<#=blender#>SrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 <#=blender#>SrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, <#=blender#>(backdrop, source)); + } + + /// + /// Returns the result of the "<#=blender#>SrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>SrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(backdrop, source, <#=blender#>(backdrop, source)); + } + + /// + /// Returns the result of the "<#=blender#>SrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 <#=blender#>SrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(backdrop, source); + } + + /// + /// Returns the result of the "<#=blender#>SrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 <#=blender#>SrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "<#=blender#>SrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>SrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "<#=blender#>SrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 <#=blender#>SrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(backdrop, source); + } + + /// + /// Returns the result of the "<#=blender#>SrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 <#=blender#>SrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "<#=blender#>SrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>SrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "<#=blender#>Dest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 <#=blender#>Dest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "<#=blender#>Dest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 <#=blender#>Dest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "<#=blender#>Dest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>Dest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the result of the "<#=blender#>DestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 <#=blender#>DestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Atop(source, backdrop, <#=blender#>(source, backdrop)); + } + + /// + /// Returns the result of the "<#=blender#>DestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 <#=blender#>DestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, <#=blender#>(source, backdrop)); + } + + /// + /// Returns the result of the "<#=blender#>DestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>DestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Atop(source, backdrop, <#=blender#>(source, backdrop)); + } + + /// + /// Returns the result of the "<#=blender#>DestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 <#=blender#>DestOver(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Over(source, backdrop, <#=blender#>(source, backdrop)); + } + + /// + /// Returns the result of the "<#=blender#>DestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 <#=blender#>DestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, <#=blender#>(source, backdrop)); + } + + /// + /// Returns the result of the "<#=blender#>DestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>DestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); + + return Over(source, backdrop, <#=blender#>(source, backdrop)); + } + + /// + /// Returns the result of the "<#=blender#>DestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 <#=blender#>DestIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(source, backdrop); + } + + /// + /// Returns the result of the "<#=blender#>DestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 <#=blender#>DestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "<#=blender#>DestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>DestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "<#=blender#>DestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 <#=blender#>DestOut(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Out(source, backdrop); + } + + /// + /// Returns the result of the "<#=blender#>DestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 <#=blender#>DestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "<#=blender#>DestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>DestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); + + /// + /// Returns the result of the "<#=blender#>Xor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 <#=blender#>Xor(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Xor(backdrop, source); + } + + /// + /// Returns the result of the "<#=blender#>Xor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 <#=blender#>Xor(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "<#=blender#>Xor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>Xor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + + /// + /// Returns the result of the "<#=blender#>Clear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 <#=blender#>Clear(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return Clear(backdrop, source); + } + + /// + /// Returns the result of the "<#=blender#>Clear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 <#=blender#>Clear(Vector256 backdrop, Vector256 source, Vector256 opacity) + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "<#=blender#>Clear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>Clear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); + +<#} #> + +<# void GenerateGenericPixelBlender(string blender, string composer) { #> + + /// + /// Returns the result of the "<#=blender#><#=composer#>" compositing equation. + /// + /// The pixel format. + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel <#=blender#><#=composer#>(TPixel backdrop, TPixel source, float opacity) + where TPixel : unmanaged, IPixel + { + opacity = Numerics.Clamp(opacity, 0, 1); + return TPixel.FromScaledVector4(<#=blender#><#=composer#>(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + } +<# } #> +<# +var composers = new []{ + "Src", + "SrcAtop", + "SrcOver", + "SrcIn", + "SrcOut", + "Dest", + "DestAtop", + "DestOver", + "DestIn", + "DestOut", + "Clear", + "Xor", +}; + +var blenders = new []{ + "Normal", + "Multiply", + "Add", + "Subtract", + "Screen", + "Darken", + "Lighten", + "Overlay", + "HardLight" +}; + +foreach(var blender in blenders) +{ + GeneratePixelBlenders(blender); + foreach(var composer in composers) + { + GenerateGenericPixelBlender(blender,composer); + } +} +#> +} diff --git a/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.cs b/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.cs new file mode 100644 index 0000000..50a525b --- /dev/null +++ b/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.cs @@ -0,0 +1,737 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders { + /// + /// Collection of Porter Duff Color Blending and Alpha Composition Functions. + /// + /// + /// These functions are designed to be a general solution for all color cases, + /// that is, they take in account the alpha value of both the backdrop + /// and source, and there's no need to alpha-premultiply neither the backdrop + /// nor the source. + /// Note there are faster functions for when the backdrop color is known + /// to be opaque + /// + internal static partial class PorterDuffFunctions + { + private const int BlendAlphaControl = 0b_10_00_10_00; + private const int ShuffleAlphaControl = 0b_11_11_11_11; + + /// + /// Returns the result of the "Normal" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Normal(Vector4 backdrop, Vector4 source) + => source; + + /// + /// Returns the result of the "Normal" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Normal(Vector256 backdrop, Vector256 source) + => source; + + /// + /// Returns the result of the "Normal" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Normal(Vector512 backdrop, Vector512 source) + => source; + + /// + /// Returns the result of the "Multiply" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Multiply(Vector4 backdrop, Vector4 source) + => backdrop * source; + + /// + /// Returns the result of the "Multiply" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Multiply(Vector256 backdrop, Vector256 source) + => backdrop * source; + + /// + /// Returns the result of the "Multiply" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Multiply(Vector512 backdrop, Vector512 source) + => backdrop * source; + + /// + /// Returns the result of the "Add" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Add(Vector4 backdrop, Vector4 source) + => Vector4.Min(Vector4.One, backdrop + source); + + /// + /// Returns the result of the "Add" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Add(Vector256 backdrop, Vector256 source) + => Vector256.Min(Vector256.Create(1F), backdrop + source); + + /// + /// Returns the result of the "Add" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Add(Vector512 backdrop, Vector512 source) + => Vector512.Min(Vector512.Create(1F), backdrop + source); + + /// + /// Returns the result of the "Subtract" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Subtract(Vector4 backdrop, Vector4 source) + => Vector4.Max(Vector4.Zero, backdrop - source); + + /// + /// Returns the result of the "Subtract" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Subtract(Vector256 backdrop, Vector256 source) + => Vector256.Max(Vector256.Zero, backdrop - source); + + /// + /// Returns the result of the "Subtract" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Subtract(Vector512 backdrop, Vector512 source) + => Vector512.Max(Vector512.Zero, backdrop - source); + + /// + /// Returns the result of the "Screen" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Screen(Vector4 backdrop, Vector4 source) + => Vector4.One - ((Vector4.One - backdrop) * (Vector4.One - source)); + + /// + /// Returns the result of the "Screen" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Screen(Vector256 backdrop, Vector256 source) + { + Vector256 vOne = Vector256.Create(1F); + return Vector256_.MultiplyAddNegated(vOne, vOne - backdrop, vOne - source); + } + + /// + /// Returns the result of the "Screen" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Screen(Vector512 backdrop, Vector512 source) + { + Vector512 vOne = Vector512.Create(1F); + return Vector512_.MultiplyAddNegated(vOne, vOne - backdrop, vOne - source); + } + + /// + /// Returns the result of the "Darken" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Darken(Vector4 backdrop, Vector4 source) + => Vector4.Min(backdrop, source); + + /// + /// Returns the result of the "Darken" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Darken(Vector256 backdrop, Vector256 source) + => Vector256.Min(backdrop, source); + + /// + /// Returns the result of the "Darken" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Darken(Vector512 backdrop, Vector512 source) + => Vector512.Min(backdrop, source); + + /// + /// Returns the result of the "Lighten" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Lighten(Vector4 backdrop, Vector4 source) => Vector4.Max(backdrop, source); + + /// + /// Returns the result of the "Lighten" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Lighten(Vector256 backdrop, Vector256 source) + => Vector256.Max(backdrop, source); + + /// + /// Returns the result of the "Lighten" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Lighten(Vector512 backdrop, Vector512 source) + => Vector512.Max(backdrop, source); + + /// + /// Returns the result of the "Overlay" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Overlay(Vector4 backdrop, Vector4 source) + { + float cr = OverlayValueFunction(backdrop.X, source.X); + float cg = OverlayValueFunction(backdrop.Y, source.Y); + float cb = OverlayValueFunction(backdrop.Z, source.Z); + + return Vector4.Min(Vector4.One, new Vector4(cr, cg, cb, 0)); + } + + /// + /// Returns the result of the "Overlay" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Overlay(Vector256 backdrop, Vector256 source) + { + Vector256 color = OverlayValueFunction(backdrop, source); + return Vector256.Min(Vector256.Create(1F), Avx.Blend(color, Vector256.Zero, BlendAlphaControl)); + } + + /// + /// Returns the result of the "Overlay" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Overlay(Vector512 backdrop, Vector512 source) + { + Vector512 color = OverlayValueFunction(backdrop, source); + return Vector512.Min(Vector512.Create(1F), Vector512.ConditionalSelect(AlphaMask512(), Vector512.Zero, color)); + } + + /// + /// Returns the result of the "HardLight" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLight(Vector4 backdrop, Vector4 source) + { + float cr = OverlayValueFunction(source.X, backdrop.X); + float cg = OverlayValueFunction(source.Y, backdrop.Y); + float cb = OverlayValueFunction(source.Z, backdrop.Z); + + return Vector4.Min(Vector4.One, new Vector4(cr, cg, cb, 0)); + } + + /// + /// Returns the result of the "HardLight" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLight(Vector256 backdrop, Vector256 source) + { + Vector256 color = OverlayValueFunction(source, backdrop); + return Vector256.Min(Vector256.Create(1F), Avx.Blend(color, Vector256.Zero, BlendAlphaControl)); + } + + /// + /// Returns the result of the "HardLight" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLight(Vector512 backdrop, Vector512 source) + { + Vector512 color = OverlayValueFunction(source, backdrop); + return Vector512.Min(Vector512.Create(1F), Vector512.ConditionalSelect(AlphaMask512(), Vector512.Zero, color)); + } + + /// + /// Helper function for Overlay and HardLight modes + /// + /// Backdrop color element + /// Source color element + /// Overlay value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float OverlayValueFunction(float backdrop, float source) + => backdrop <= 0.5f ? (2 * backdrop * source) : 1 - (2 * (1 - source) * (1 - backdrop)); + + /// + /// Helper function for Overlay and HardLight modes + /// + /// Backdrop color element + /// Source color element + /// Overlay value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlayValueFunction(Vector256 backdrop, Vector256 source) + { + Vector256 vOne = Vector256.Create(1F); + Vector256 left = (backdrop + backdrop) * source; + + Vector256 vOneMinusSource = Avx.Subtract(vOne, source); + Vector256 right = Vector256_.MultiplyAddNegated(vOne, vOneMinusSource + vOneMinusSource, vOne - backdrop); + Vector256 cmp = Avx.CompareGreaterThan(backdrop, Vector256.Create(.5F)); + return Avx.BlendVariable(left, right, cmp); + } + + /// + /// Helper function for Overlay and HardLight modes + /// + /// Backdrop color element + /// Source color element + /// Overlay value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayValueFunction(Vector512 backdrop, Vector512 source) + { + Vector512 vOne = Vector512.Create(1F); + Vector512 left = (backdrop + backdrop) * source; + + Vector512 vOneMinusSource = vOne - source; + Vector512 right = Vector512_.MultiplyAddNegated(vOne, vOneMinusSource + vOneMinusSource, vOne - backdrop); + Vector512 cmp = Avx512F.CompareGreaterThan(backdrop, Vector512.Create(.5F)); + return Vector512.ConditionalSelect(cmp, right, left); + } + + /// + /// Returns the result of the "Over" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The amount to blend. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Over(Vector4 destination, Vector4 source, Vector4 blend) + { + // calculate weights + Vector4 sW = Numerics.PermuteW(source); + Vector4 dW = Numerics.PermuteW(destination); + + Vector4 blendW = sW * dW; + Vector4 dstW = dW - blendW; + Vector4 srcW = sW - blendW; + + // calculate final alpha + Vector4 alpha = dstW + sW; + + // calculate final color + Vector4 color = (destination * dstW) + (source * srcW) + (blend * blendW); + + // unpremultiply + Numerics.UnPremultiply(ref color, alpha); + return color; + } + + /// + /// Returns the result of the "Over" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The amount to blend. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Over(Vector256 destination, Vector256 source, Vector256 blend) + { + // calculate weights + Vector256 sW = Avx.Permute(source, ShuffleAlphaControl); + Vector256 dW = Avx.Permute(destination, ShuffleAlphaControl); + + Vector256 blendW = sW * dW; + Vector256 dstW = dW - blendW; + Vector256 srcW = sW - blendW; + + // calculate final alpha + Vector256 alpha = dstW + sW; + + // calculate final color + Vector256 color = destination * dstW; + color = Vector256_.MultiplyAdd(color, source, srcW); + color = Vector256_.MultiplyAdd(color, blend, blendW); + + // unpremultiply + return Numerics.UnPremultiply(color, alpha); + } + + /// + /// Returns the result of the "Over" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The amount to blend. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Over(Vector512 destination, Vector512 source, Vector512 blend) + { + // calculate weights + Vector512 sW = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + Vector512 dW = Vector512_.ShuffleNative(destination, ShuffleAlphaControl); + + Vector512 blendW = sW * dW; + Vector512 dstW = dW - blendW; + Vector512 srcW = sW - blendW; + + // calculate final alpha + Vector512 alpha = dstW + sW; + + // calculate final color + Vector512 color = destination * dstW; + color = Vector512_.MultiplyAdd(color, source, srcW); + color = Vector512_.MultiplyAdd(color, blend, blendW); + + // unpremultiply + return Numerics.UnPremultiply(color, alpha); + } + + /// + /// Returns the result of the "Atop" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The amount to blend. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Atop(Vector4 destination, Vector4 source, Vector4 blend) + { + // calculate weights + Vector4 sW = Numerics.PermuteW(source); + Vector4 dW = Numerics.PermuteW(destination); + + Vector4 blendW = sW * dW; + Vector4 dstW = dW - blendW; + + // calculate final alpha + Vector4 alpha = dW; + + // calculate final color + Vector4 color = (destination * dstW) + (blend * blendW); + + // unpremultiply + Numerics.UnPremultiply(ref color, alpha); + return color; + } + + /// + /// Returns the result of the "Atop" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The amount to blend. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Atop(Vector256 destination, Vector256 source, Vector256 blend) + { + // calculate final alpha + Vector256 alpha = Avx.Permute(destination, ShuffleAlphaControl); + + // calculate weights + Vector256 sW = Avx.Permute(source, ShuffleAlphaControl); + Vector256 blendW = sW * alpha; + Vector256 dstW = alpha - blendW; + + // calculate final color + Vector256 color = Vector256_.MultiplyAdd(Avx.Multiply(blend, blendW), destination, dstW); + + // unpremultiply + return Numerics.UnPremultiply(color, alpha); + } + + /// + /// Returns the result of the "Atop" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The amount to blend. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Atop(Vector512 destination, Vector512 source, Vector512 blend) + { + // calculate final alpha + Vector512 alpha = Vector512_.ShuffleNative(destination, ShuffleAlphaControl); + + // calculate weights + Vector512 sW = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + Vector512 blendW = sW * alpha; + Vector512 dstW = alpha - blendW; + + // calculate final color + Vector512 color = Vector512_.MultiplyAdd(blend * blendW, destination, dstW); + + // unpremultiply + return Numerics.UnPremultiply(color, alpha); + } + + /// + /// Returns the result of the "In" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 In(Vector4 destination, Vector4 source) + { + Vector4 sW = Numerics.PermuteW(source); + Vector4 dW = Numerics.PermuteW(destination); + Vector4 alpha = dW * sW; + + Vector4 color = source * alpha; // premultiply + Numerics.UnPremultiply(ref color, alpha); // unpremultiply + return color; + } + + /// + /// Returns the result of the "In" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 In(Vector256 destination, Vector256 source) + { + // calculate alpha + Vector256 alpha = Avx.Permute(source * destination, ShuffleAlphaControl); + + // premultiply + Vector256 color = source * alpha; + + // unpremultiply + return Numerics.UnPremultiply(color, alpha); + } + + /// + /// Returns the result of the "In" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 In(Vector512 destination, Vector512 source) + { + // calculate alpha + Vector512 alpha = Vector512_.ShuffleNative(source * destination, ShuffleAlphaControl); + + // premultiply + Vector512 color = source * alpha; + + // unpremultiply + return Numerics.UnPremultiply(color, alpha); + } + + /// + /// Returns the result of the "Out" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Out(Vector4 destination, Vector4 source) + { + Vector4 sW = Numerics.PermuteW(source); + Vector4 dW = Numerics.PermuteW(destination); + Vector4 alpha = (Vector4.One - dW) * sW; + + Vector4 color = source * alpha; // premultiply + Numerics.UnPremultiply(ref color, alpha); // unpremultiply + return color; + } + + /// + /// Returns the result of the "Out" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Out(Vector256 destination, Vector256 source) + { + // calculate alpha + Vector256 alpha = Avx.Permute(source * (Vector256.Create(1F) - destination), ShuffleAlphaControl); + + // premultiply + Vector256 color = source * alpha; + + // unpremultiply + return Numerics.UnPremultiply(color, alpha); + } + + /// + /// Returns the result of the "Out" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Out(Vector512 destination, Vector512 source) + { + // calculate alpha + Vector512 alpha = Vector512_.ShuffleNative(source * (Vector512.Create(1F) - destination), ShuffleAlphaControl); + + // premultiply + Vector512 color = source * alpha; + + // unpremultiply + return Numerics.UnPremultiply(color, alpha); + } + + /// + /// Returns the result of the "XOr" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Xor(Vector4 destination, Vector4 source) + { + Vector4 sW = Numerics.PermuteW(source); + Vector4 dW = Numerics.PermuteW(destination); + + Vector4 srcW = Vector4.One - dW; + Vector4 dstW = Vector4.One - sW; + + Vector4 alpha = (sW * srcW) + (dW * dstW); + Vector4 color = (sW * source * srcW) + (dW * destination * dstW); + + // unpremultiply + Numerics.UnPremultiply(ref color, alpha); + return color; + } + + /// + /// Returns the result of the "XOr" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Xor(Vector256 destination, Vector256 source) + { + // calculate weights + Vector256 sW = Avx.Shuffle(source, source, ShuffleAlphaControl); + Vector256 dW = Avx.Shuffle(destination, destination, ShuffleAlphaControl); + + Vector256 vOne = Vector256.Create(1F); + Vector256 srcW = vOne - dW; + Vector256 dstW = vOne - sW; + + // calculate alpha + Vector256 alpha = Vector256_.MultiplyAdd(Avx.Multiply(dW, dstW), sW, srcW); + Vector256 color = Vector256_.MultiplyAdd(Avx.Multiply(Avx.Multiply(dW, destination), dstW), Avx.Multiply(sW, source), srcW); + + // unpremultiply + return Numerics.UnPremultiply(color, alpha); + } + + /// + /// Returns the result of the "XOr" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Xor(Vector512 destination, Vector512 source) + { + // calculate weights + Vector512 sW = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + Vector512 dW = Vector512_.ShuffleNative(destination, ShuffleAlphaControl); + + Vector512 vOne = Vector512.Create(1F); + Vector512 srcW = vOne - dW; + Vector512 dstW = vOne - sW; + + // calculate alpha + Vector512 alpha = Vector512_.MultiplyAdd(dW * dstW, sW, srcW); + Vector512 color = Vector512_.MultiplyAdd((dW * destination) * dstW, sW * source, srcW); + + // unpremultiply + return Numerics.UnPremultiply(color, alpha); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector4 Clear(Vector4 backdrop, Vector4 source) => Vector4.Zero; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Clear(Vector256 backdrop, Vector256 source) => Vector256.Zero; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Clear(Vector512 backdrop, Vector512 source) => Vector512.Zero; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 AlphaMask512() + => Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + } +} diff --git a/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs b/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs new file mode 100644 index 0000000..f422be7 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs @@ -0,0 +1,416 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Abstract base class for calling pixel composition functions + /// + /// The type of the pixel + public abstract class PixelBlender + where TPixel : unmanaged, IPixel + { + /// + /// Blend 2 pixels together. + /// + /// The background color. + /// The source color. + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// The final pixel value after composition. + public abstract TPixel Blend(TPixel background, TPixel source, float amount); + + /// + /// Blends 2 rows together + /// + /// the pixel format of the source span + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + float amount) + where TPixelSrc : unmanaged, IPixel + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length)); + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + + using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 3); + this.Blend( + configuration, + destination, + background, + source, + amount, + buffer.Memory.Span[..(maxLength * 3)]); + } + + /// + /// Blends 2 rows together using caller-provided temporary vector scratch. + /// + /// the pixel format of the source span + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 3 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + float amount, + Span workingBuffer) + where TPixelSrc : unmanaged, IPixel + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length)); + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 3, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); + Span sourceVectors = workingBuffer.Slice(maxLength * 2, maxLength); + + PixelOperations.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale); + PixelOperations.Instance.ToVector4(configuration, source[..maxLength], sourceVectors, PixelConversionModifiers.Scale); + + this.BlendFunction(destinationVectors, backgroundVectors, sourceVectors, amount); + + PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale); + } + + /// + /// Blends a row against a constant source color. + /// + /// to use internally + /// the destination span + /// the background span + /// the source color + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + TPixel source, + float amount) + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + + using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 2); + this.Blend( + configuration, + destination, + background, + source, + amount, + buffer.Memory.Span[..(maxLength * 2)]); + } + + /// + /// Blends a row against a constant source color using caller-provided temporary vector scratch. + /// + /// to use internally + /// the destination span + /// the background span + /// the source color + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 2 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + TPixel source, + float amount, + Span workingBuffer) + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 2, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); + + PixelOperations.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale); + + this.BlendFunction(destinationVectors, backgroundVectors, source.ToScaledVector4(), amount); + + PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale); + } + + /// + /// Blends 2 rows together + /// + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount) + => this.Blend(configuration, destination, background, source, amount); + + /// + /// Blends 2 rows together using caller-provided temporary vector scratch. + /// + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 3 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount, + Span workingBuffer) + => this.Blend(configuration, destination, background, source, amount, workingBuffer); + + /// + /// Blends 2 rows together + /// + /// the pixel format of the source span + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount) + where TPixelSrc : unmanaged, IPixel + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length)); + Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); + + using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 3); + this.Blend( + configuration, + destination, + background, + source, + amount, + buffer.Memory.Span[..(maxLength * 3)]); + } + + /// + /// Blends a row against a constant source color. + /// + /// to use internally + /// the destination span + /// the background span + /// the source color + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + TPixel source, + ReadOnlySpan amount) + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); + + using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 2); + this.Blend( + configuration, + destination, + background, + source, + amount, + buffer.Memory.Span[..(maxLength * 2)]); + } + + /// + /// Blends 2 rows together using caller-provided temporary vector scratch. + /// + /// the pixel format of the source span + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 3 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount, + Span workingBuffer) + where TPixelSrc : unmanaged, IPixel + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length)); + Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 3, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); + Span sourceVectors = workingBuffer.Slice(maxLength * 2, maxLength); + + PixelOperations.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale); + PixelOperations.Instance.ToVector4(configuration, source[..maxLength], sourceVectors, PixelConversionModifiers.Scale); + + this.BlendFunction(destinationVectors, backgroundVectors, sourceVectors, amount); + + PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale); + } + + /// + /// Blends a row against a constant source color using caller-provided temporary vector scratch. + /// + /// to use internally + /// the destination span + /// the background span + /// the source color + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 2 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + TPixel source, + ReadOnlySpan amount, + Span workingBuffer) + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 2, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); + + PixelOperations.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale); + + this.BlendFunction(destinationVectors, backgroundVectors, source.ToScaledVector4(), amount); + + PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale); + } + + /// + /// Blend 2 rows together. + /// + /// destination span + /// the background span + /// the source span + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + protected abstract void BlendFunction( + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + float amount); + + /// + /// Blend a row against a constant source color. + /// + /// destination span + /// the background span + /// the source color vector + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + protected abstract void BlendFunction( + Span destination, + ReadOnlySpan background, + Vector4 source, + float amount); + + /// + /// Blend 2 rows together. + /// + /// destination span + /// the background span + /// the source span + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + protected abstract void BlendFunction( + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount); + + /// + /// Blend a row against a constant source color. + /// + /// destination span + /// the background span + /// the source color vector + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + protected abstract void BlendFunction( + Span destination, + ReadOnlySpan background, + Vector4 source, + ReadOnlySpan amount); + } +} diff --git a/ImageSharp/PixelFormats/PixelColorBlendingMode.cs b/ImageSharp/PixelFormats/PixelColorBlendingMode.cs new file mode 100644 index 0000000..1897736 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelColorBlendingMode.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Enumerates the various color blending modes. + /// + public enum PixelColorBlendingMode + { + /// + /// Default blending mode, also known as "Normal" or "Alpha Blending" + /// + Normal = 0, + + /// + /// Blends the 2 values by multiplication. + /// + Multiply, + + /// + /// Blends the 2 values by addition. + /// + Add, + + /// + /// Blends the 2 values by subtraction. + /// + Subtract, + + /// + /// Multiplies the complements of the backdrop and source values, then complements the result. + /// + Screen, + + /// + /// Selects the minimum of the backdrop and source values. + /// + Darken, + + /// + /// Selects the max of the backdrop and source values. + /// + Lighten, + + /// + /// Multiplies or screens the values, depending on the backdrop vector values. + /// + Overlay, + + /// + /// Multiplies or screens the colors, depending on the source value. + /// + HardLight, + } +} diff --git a/ImageSharp/PixelFormats/PixelColorType.cs b/ImageSharp/PixelFormats/PixelColorType.cs new file mode 100644 index 0000000..87b5c35 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelColorType.cs @@ -0,0 +1,118 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Represents the color type and format of a pixel. + /// + [Flags] + public enum PixelColorType + { + /// + /// No color type. + /// + None = 0, + + /// + /// Represents the Red component of the color. + /// + Red = 1 << 0, + + /// + /// Represents the Green component of the color. + /// + Green = 1 << 1, + + /// + /// Represents the Blue component of the color. + /// + Blue = 1 << 2, + + /// + /// Represents the Alpha component of the color for transparency. + /// + Alpha = 1 << 3, + + /// + /// Represents the Exponent component used in formats like R9G9B9E5. + /// + Exponent = 1 << 4, + + /// + /// Indicates that the color is in luminance (grayscale) format. + /// + Luminance = 1 << 5, + + /// + /// Indicates that the color is in binary (black and white) format. + /// + Binary = 1 << 6, + + /// + /// Indicates that the color is indexed using a palette. + /// + Indexed = 1 << 7, + + /// + /// Indicates that the color is in RGB (Red, Green, Blue) format. + /// + RGB = Red | Green | Blue | (1 << 8), + + /// + /// Indicates that the color is in BGR (Blue, Green, Red) format. + /// + BGR = Blue | Green | Red | (1 << 9), + + /// + /// Represents the Chrominance Blue component. + /// + ChrominanceBlue = 1 << 10, + + /// + /// Represents the Chrominance Red component. + /// + ChrominanceRed = 1 << 11, + + /// + /// Indicates that the color is in YCbCr (Luminance, Chrominance Blue, Chrominance Red) format. + /// + YCbCr = Luminance | ChrominanceBlue | ChrominanceRed | (1 << 12), + + /// + /// Represents the Cyan component in CMYK. + /// + Cyan = 1 << 13, + + /// + /// Represents the Magenta component in CMYK. + /// + Magenta = 1 << 14, + + /// + /// Represents the Yellow component in CMYK. + /// + Yellow = 1 << 15, + + /// + /// Represents the Key (black) component in CMYK and YCCK. + /// + Key = 1 << 16, + + /// + /// Indicates that the color is in CMYK (Cyan, Magenta, Yellow, Key) format. + /// + CMYK = Cyan | Magenta | Yellow | Key, + + /// + /// Indicates that the color is in YCCK (Luminance, Chrominance Blue, Chrominance Red, Key) format. + /// + YCCK = Luminance | ChrominanceBlue | ChrominanceRed | Key, + + /// + /// Indicates that the color is of a type not specified in this enum. + /// + Other = 1 << 17 + } +} diff --git a/ImageSharp/PixelFormats/PixelComponentBitDepth.cs b/ImageSharp/PixelFormats/PixelComponentBitDepth.cs new file mode 100644 index 0000000..0d6c77d --- /dev/null +++ b/ImageSharp/PixelFormats/PixelComponentBitDepth.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides enumeration of the precision in bits of individual components within a pixel format. + /// + public enum PixelComponentBitDepth + { + /// + /// 1 bit per component. + /// + Bit1 = 1, + + /// + /// 2 bits per component. + /// + Bit2 = 2, + + /// + /// 4 bits per component. + /// + Bit4 = 4, + + /// + /// 8 bits per component. + /// + Bit8 = 8, + + /// + /// 16 bits per component. + /// + Bit16 = 16, + + /// + /// 32 bits per component. + /// + Bit32 = 32, + + /// + /// 64 bits per component. + /// + Bit64 = 64, + + /// + /// 128 bits per component. + /// + Bit128 = 128 + } +} diff --git a/ImageSharp/PixelFormats/PixelComponentInfo.cs b/ImageSharp/PixelFormats/PixelComponentInfo.cs new file mode 100644 index 0000000..3bbacfe --- /dev/null +++ b/ImageSharp/PixelFormats/PixelComponentInfo.cs @@ -0,0 +1,123 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Represents pixel component information within a pixel format. + /// + public readonly struct PixelComponentInfo + { + private readonly long precisionData1; + private readonly long precisionData2; + + private PixelComponentInfo(int count, int padding, long precisionData1, long precisionData2) + { + this.ComponentCount = count; + this.Padding = padding; + this.precisionData1 = precisionData1; + this.precisionData2 = precisionData2; + } + + /// + /// Gets the number of components within the pixel. + /// + public int ComponentCount { get; } + + /// + /// Gets the number of bytes of padding within the pixel. + /// + public int Padding { get; } + + /// + /// Creates a new instance. + /// + /// The type of pixel format. + /// The number of components within the pixel format. + /// The precision in bits of each component. + /// The . + /// The component precision and index cannot exceed the component range. + public static PixelComponentInfo Create(int count, params int[] precision) + where TPixel : unmanaged, IPixel + => Create(count, Unsafe.SizeOf() * 8, precision); + + /// + /// Creates a new instance. + /// + /// The number of components within the pixel format. + /// The number of bits per pixel. + /// The precision in bits of each component. + /// The . + /// The component precision and index cannot exceed the component range. + public static PixelComponentInfo Create(int count, int bitsPerPixel, params int[] precision) + { + if (precision.Length < count || precision.Length > 16) + { + throw new ArgumentOutOfRangeException(nameof(count), $"Count {count} must match the length of precision array and cannot exceed 16."); + } + + long precisionData1 = 0; + long precisionData2 = 0; + int sum = 0; + for (int i = 0; i < precision.Length; i++) + { + int p = precision[i]; + if (p is < 0 or > 255) + { + throw new ArgumentOutOfRangeException(nameof(precision), $"Precision {precision.Length} must be between 0 and 255."); + } + + if (i < 8) + { + precisionData1 |= ((long)p) << (8 * i); + } + else + { + precisionData2 |= ((long)p) << (8 * (i - 8)); + } + + sum += p; + } + + return new PixelComponentInfo(count, bitsPerPixel - sum, precisionData1, precisionData2); + } + + /// + /// Returns the precision of the component in bits at the given index. + /// + /// The component index. + /// The . + /// The component index cannot exceed the component range. + public int GetComponentPrecision(int componentIndex) + { + if (componentIndex < 0 || componentIndex >= this.ComponentCount) + { + throw new ArgumentOutOfRangeException($"Component index must be between 0 and {this.ComponentCount - 1} inclusive."); + } + + long selectedPrecisionData = componentIndex < 8 ? this.precisionData1 : this.precisionData2; + return (int)((selectedPrecisionData >> (8 * (componentIndex & 7))) & 0xFF); + } + + /// + /// Returns the maximum precision in bits of all components. + /// + /// The . + public int GetMaximumComponentPrecision() + { + int maxPrecision = 0; + for (int i = 0; i < this.ComponentCount; i++) + { + int componentPrecision = this.GetComponentPrecision(i); + if (componentPrecision > maxPrecision) + { + maxPrecision = componentPrecision; + } + } + + return maxPrecision; + } + } +} diff --git a/ImageSharp/PixelFormats/PixelConversionModifiers.cs b/ImageSharp/PixelFormats/PixelConversionModifiers.cs new file mode 100644 index 0000000..bc89517 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelConversionModifiers.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles.Companding; +using System; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Flags responsible to select additional operations which could be efficiently applied in + /// + /// or + /// + /// knowing the pixel type. + /// + [Flags] + public enum PixelConversionModifiers + { + /// + /// No special operation is selected + /// + None = 0, + + /// + /// Select and instead the standard (non scaled) variants. + /// + Scale = 1 << 0, + + /// + /// Enable alpha premultiplication / unpremultiplication + /// + Premultiply = 1 << 1, + + /// + /// Enable SRGB companding (defined in ). + /// + SRgbCompand = 1 << 2, + } +} diff --git a/ImageSharp/PixelFormats/PixelConversionModifiersExtensions.cs b/ImageSharp/PixelFormats/PixelConversionModifiersExtensions.cs new file mode 100644 index 0000000..da873ce --- /dev/null +++ b/ImageSharp/PixelFormats/PixelConversionModifiersExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Extension and utility methods for . + /// + internal static class PixelConversionModifiersExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsDefined(this PixelConversionModifiers modifiers, PixelConversionModifiers expected) => + (modifiers & expected) == expected; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PixelConversionModifiers Remove( + this PixelConversionModifiers modifiers, + PixelConversionModifiers removeThis) => + modifiers & ~removeThis; + + /// + /// Applies the union of and , + /// if is true, returns unmodified otherwise. + /// + /// + /// and + /// should be always used together! + /// + public static PixelConversionModifiers ApplyCompanding( + this PixelConversionModifiers originalModifiers, + bool compand) => + compand + ? originalModifiers | PixelConversionModifiers.Scale | PixelConversionModifiers.SRgbCompand + : originalModifiers; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/A8.cs b/ImageSharp/PixelFormats/PixelImplementations/A8.cs new file mode 100644 index 0000000..aa30ed3 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/A8.cs @@ -0,0 +1,171 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing a single 8-bit normalized alpha value. + /// + /// Ranges from [0, 0, 0, 0] to [0, 0, 0, 1] in vector form. + /// + /// + public partial struct A8 : IPixel, IPackedVector + { + /// + /// Initializes a new instance of the struct. + /// + /// The alpha component. + public A8(byte alpha) => this.PackedValue = alpha; + + /// + /// Initializes a new instance of the struct. + /// + /// The alpha component. + public A8(float alpha) => this.PackedValue = Pack(alpha); + + /// + public byte PackedValue { get; set; } + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(A8 left, A8 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(A8 left, A8 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => new() { A = this.PackedValue }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new(0, 0, 0, this.PackedValue / 255f); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(1, 8), + PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromVector4(Vector4 source) => new(Pack(source.W)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromAbgr32(Abgr32 source) => new(source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromArgb32(Argb32 source) => new(source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromBgr24(Bgr24 source) => new(byte.MaxValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromBgra32(Bgra32 source) => new(source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromL8(L8 source) => new(byte.MaxValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromL16(L16 source) => new(byte.MaxValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromLa16(La16 source) => new(source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromLa32(La32 source) => new(ColorNumerics.From16BitTo8Bit(source.A)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromRgb24(Rgb24 source) => new(byte.MaxValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromRgba32(Rgba32 source) => new(source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromRgb48(Rgb48 source) => new(byte.MaxValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromRgba64(Rgba64 source) => new(ColorNumerics.From16BitTo8Bit(source.A)); + + /// + /// Compares an object with the packed vector. + /// + /// The object to compare. + /// True if the object is equal to the packed vector. + public override readonly bool Equals(object? obj) => obj is A8 other && this.Equals(other); + + /// + /// Compares another A8 packed vector with the packed vector. + /// + /// The A8 packed vector to compare. + /// True if the packed vectors are equal. + public readonly bool Equals(A8 other) => this.PackedValue.Equals(other.PackedValue); + + /// + /// Gets a string representation of the packed vector. + /// + /// A string representation of the packed vector. + public override readonly string ToString() => $"A8({this.PackedValue})"; + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + /// Packs a into a byte. + /// + /// The float containing the value to pack. + /// The containing the packed values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte Pack(float alpha) => (byte)Math.Round(Numerics.Clamp(alpha, 0, 1f) * 255f); + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Abgr32.cs b/ImageSharp/PixelFormats/PixelImplementations/Abgr32.cs new file mode 100644 index 0000000..7904f9e --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Abgr32.cs @@ -0,0 +1,325 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing four 8-bit unsigned normalized values ranging from 0 to 255. + /// The color components are stored in alpha, red, green, and blue order (least significant to most significant byte). + /// + /// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. + /// + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + [StructLayout(LayoutKind.Sequential)] + public partial struct Abgr32 : IPixel, IPackedVector + { + /// + /// Gets or sets the alpha component. + /// + public byte A; + + /// + /// Gets or sets the blue component. + /// + public byte B; + + /// + /// Gets or sets the green component. + /// + public byte G; + + /// + /// Gets or sets the red component. + /// + public byte R; + + /// + /// The maximum byte value. + /// + private static readonly Vector4 MaxBytes = Vector128.Create(255f).AsVector4(); + + /// + /// The half vector value. + /// + private static readonly Vector4 Half = Vector128.Create(.5f).AsVector4(); + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Abgr32(byte r, byte g, byte b) + { + this.R = r; + this.G = g; + this.B = b; + this.A = byte.MaxValue; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The alpha component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Abgr32(byte r, byte g, byte b, byte a) + { + this.R = r; + this.G = g; + this.B = b; + this.A = a; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The alpha component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Abgr32(float r, float g, float b, float a = 1) + : this(new Vector4(r, g, b, a)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The vector containing the components for the packed vector. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Abgr32(Vector3 vector) + : this(new Vector4(vector, 1f)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The vector containing the components for the packed vector. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Abgr32(Vector4 vector) + : this() => this = Pack(vector); + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The packed value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Abgr32(uint packed) + : this() => this.Abgr = packed; + + /// + /// Gets or sets the packed representation of the Abgr struct. + /// + public uint Abgr + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => Unsafe.As(ref this) = value; + } + + /// + public uint PackedValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => this.Abgr; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => this.Abgr = value; + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Abgr32 left, Abgr32 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Abgr32 left, Abgr32 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromAbgr32(this); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new Vector4(this.R, this.G, this.B, this.A) / MaxBytes; + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 8, 8, 8, 8), + PixelColorType.Alpha | PixelColorType.BGR, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromVector4(Vector4 source) => Pack(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromAbgr32(Abgr32 source) => new() { PackedValue = source.PackedValue }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromArgb32(Argb32 source) => new(source.R, source.G, source.B, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromBgr24(Bgr24 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromBgra32(Bgra32 source) => new(source.R, source.G, source.B, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromL8(L8 source) => new(source.PackedValue, source.PackedValue, source.PackedValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromL16(L16 source) + { + byte rgb = ColorNumerics.From16BitTo8Bit(source.PackedValue); + return new Abgr32(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromLa16(La16 source) => new(source.L, source.L, source.L, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromLa32(La32 source) + { + byte rgb = ColorNumerics.From16BitTo8Bit(source.L); + return new Abgr32(rgb, rgb, rgb, ColorNumerics.From16BitTo8Bit(source.A)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromRgb24(Rgb24 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromRgba32(Rgba32 source) => new(source.R, source.G, source.B, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromRgb48(Rgb48 source) + => new() + { + R = ColorNumerics.From16BitTo8Bit(source.R), + G = ColorNumerics.From16BitTo8Bit(source.G), + B = ColorNumerics.From16BitTo8Bit(source.B), + A = byte.MaxValue + }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromRgba64(Rgba64 source) + => new() + { + R = ColorNumerics.From16BitTo8Bit(source.R), + G = ColorNumerics.From16BitTo8Bit(source.G), + B = ColorNumerics.From16BitTo8Bit(source.B), + A = ColorNumerics.From16BitTo8Bit(source.A) + }; + + /// + public override readonly bool Equals(object? obj) => obj is Abgr32 abgr32 && this.Equals(abgr32); + + /// + public readonly bool Equals(Abgr32 other) => this.Abgr == other.Abgr; + + /// + /// Gets a string representation of the packed vector. + /// + /// A string representation of the packed vector. + public override readonly string ToString() => $"Abgr({this.A}, {this.B}, {this.G}, {this.R})"; + + /// + public override readonly int GetHashCode() => this.Abgr.GetHashCode(); + + /// + /// Packs the four floats into a color. + /// + /// The x-component + /// The y-component + /// The z-component + /// The w-component + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Abgr32 Pack(float x, float y, float z, float w) => Pack(new Vector4(x, y, z, w)); + + /// + /// Packs a into a uint. + /// + /// The vector containing the values to pack. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Abgr32 Pack(Vector3 vector) => Pack(new Vector4(vector, 1)); + + /// + /// Packs a into a color. + /// + /// The vector containing the values to pack. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Abgr32 Pack(Vector4 vector) + { + vector *= MaxBytes; + vector += Half; + vector = Numerics.Clamp(vector, Vector4.Zero, MaxBytes); + + Vector128 result = Vector128.ConvertToInt32(vector.AsVector128()).AsByte(); + return new Abgr32(result.GetElement(0), result.GetElement(4), result.GetElement(8), result.GetElement(12)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Argb32.cs b/ImageSharp/PixelFormats/PixelImplementations/Argb32.cs new file mode 100644 index 0000000..e505884 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Argb32.cs @@ -0,0 +1,301 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing four 8-bit unsigned normalized values ranging from 0 to 255. + /// The color components are stored in alpha, red, green, and blue order (least significant to most significant byte). + /// + /// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. + /// + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + [StructLayout(LayoutKind.Sequential)] + public partial struct Argb32 : IPixel, IPackedVector + { + /// + /// Gets or sets the alpha component. + /// + public byte A; + + /// + /// Gets or sets the red component. + /// + public byte R; + + /// + /// Gets or sets the green component. + /// + public byte G; + + /// + /// Gets or sets the blue component. + /// + public byte B; + + private static readonly Vector4 MaxBytes = Vector128.Create(255f).AsVector4(); + private static readonly Vector4 Half = Vector128.Create(.5f).AsVector4(); + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Argb32(byte r, byte g, byte b) + { + this.R = r; + this.G = g; + this.B = b; + this.A = byte.MaxValue; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The alpha component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Argb32(byte r, byte g, byte b, byte a) + { + this.R = r; + this.G = g; + this.B = b; + this.A = a; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The alpha component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Argb32(float r, float g, float b, float a = 1) + : this(new Vector4(r, g, b, a)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The vector containing the components for the packed vector. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Argb32(Vector3 vector) + : this(new Vector4(vector, 1f)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The vector containing the components for the packed vector. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Argb32(Vector4 vector) + : this() => this = Pack(vector); + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The packed value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Argb32(uint packed) + : this() => this.Argb = packed; + + /// + /// Gets or sets the packed representation of the Argb32 struct. + /// + public uint Argb + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => Unsafe.As(ref this) = value; + } + + /// + public uint PackedValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => this.Argb; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => this.Argb = value; + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Argb32 left, Argb32 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Argb32 left, Argb32 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromArgb32(this); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new Vector4(this.R, this.G, this.B, this.A) / MaxBytes; + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 8, 8, 8, 8), + PixelColorType.Alpha | PixelColorType.RGB, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromVector4(Vector4 source) => Pack(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromAbgr32(Abgr32 source) => new(source.R, source.G, source.B, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromArgb32(Argb32 source) => new() { PackedValue = source.PackedValue }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromBgr24(Bgr24 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromBgra32(Bgra32 source) => new(source.R, source.G, source.B, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromL8(L8 source) => new(source.PackedValue, source.PackedValue, source.PackedValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromL16(L16 source) + { + byte rgb = ColorNumerics.From16BitTo8Bit(source.PackedValue); + return new Argb32(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromLa16(La16 source) => new(source.L, source.L, source.L, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromLa32(La32 source) + { + byte rgb = ColorNumerics.From16BitTo8Bit(source.L); + return new Argb32(rgb, rgb, rgb, ColorNumerics.From16BitTo8Bit(source.A)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromRgb24(Rgb24 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromRgba32(Rgba32 source) => new(source.R, source.G, source.B, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromRgb48(Rgb48 source) + => new() + { + R = ColorNumerics.From16BitTo8Bit(source.R), + G = ColorNumerics.From16BitTo8Bit(source.G), + B = ColorNumerics.From16BitTo8Bit(source.B), + A = byte.MaxValue + }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromRgba64(Rgba64 source) + => new() + { + R = ColorNumerics.From16BitTo8Bit(source.R), + G = ColorNumerics.From16BitTo8Bit(source.G), + B = ColorNumerics.From16BitTo8Bit(source.B), + A = ColorNumerics.From16BitTo8Bit(source.A) + }; + + /// + public override readonly bool Equals(object? obj) => obj is Argb32 argb32 && this.Equals(argb32); + + /// + public readonly bool Equals(Argb32 other) => this.Argb == other.Argb; + + /// + /// Gets a string representation of the packed vector. + /// + /// A string representation of the packed vector. + public override readonly string ToString() => $"Argb({this.A}, {this.R}, {this.G}, {this.B})"; + + /// + public override readonly int GetHashCode() => this.Argb.GetHashCode(); + + /// + /// Packs a into a color. + /// + /// The vector containing the values to pack. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Argb32 Pack(Vector4 vector) + { + vector *= MaxBytes; + vector += Half; + vector = Numerics.Clamp(vector, Vector4.Zero, MaxBytes); + + Vector128 result = Vector128.ConvertToInt32(vector.AsVector128()).AsByte(); + return new Argb32(result.GetElement(0), result.GetElement(4), result.GetElement(8), result.GetElement(12)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Bgr24.cs b/ImageSharp/PixelFormats/PixelImplementations/Bgr24.cs new file mode 100644 index 0000000..ed8cda8 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Bgr24.cs @@ -0,0 +1,200 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Pixel type containing three 8-bit unsigned normalized values ranging from 0 to 255. + /// The color components are stored in blue, green, red order (least significant to most significant byte). + /// + /// Ranges from [0, 0, 0, 1] to [1, 1, 1, 1] in vector form. + /// + /// + [StructLayout(LayoutKind.Explicit)] + public partial struct Bgr24 : IPixel + { + /// + /// The blue component. + /// + [FieldOffset(0)] + public byte B; + + /// + /// The green component. + /// + [FieldOffset(1)] + public byte G; + + /// + /// The red component. + /// + [FieldOffset(2)] + public byte R; + + private static readonly Vector4 MaxBytes = Vector128.Create(255f).AsVector4(); + private static readonly Vector4 Half = Vector128.Create(.5f).AsVector4(); + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Bgr24(byte r, byte g, byte b) + { + this.R = r; + this.G = g; + this.B = b; + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Bgr24 left, Bgr24 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Bgr24 left, Bgr24 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromBgr24(this); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new Vector4(this.R, this.G, this.B, byte.MaxValue) / MaxBytes; + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(3, 8, 8, 8), + PixelColorType.BGR, + PixelAlphaRepresentation.None); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromVector4(Vector4 source) + { + source *= MaxBytes; + source += Half; + source = Numerics.Clamp(source, Vector4.Zero, MaxBytes); + + Vector128 result = Vector128.ConvertToInt32(source.AsVector128()).AsByte(); + return new Bgr24(result.GetElement(0), result.GetElement(4), result.GetElement(8)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromAbgr32(Abgr32 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromArgb32(Argb32 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromBgr24(Bgr24 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromBgra32(Bgra32 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromL8(L8 source) => new(source.PackedValue, source.PackedValue, source.PackedValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromL16(L16 source) + { + byte rgb = ColorNumerics.From16BitTo8Bit(source.PackedValue); + return new Bgr24(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromLa16(La16 source) => new(source.L, source.L, source.L); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromLa32(La32 source) + { + byte rgb = ColorNumerics.From16BitTo8Bit(source.L); + return new Bgr24(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromRgb24(Rgb24 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromRgba32(Rgba32 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromRgb48(Rgb48 source) + => new() + { + R = ColorNumerics.From16BitTo8Bit(source.R), + G = ColorNumerics.From16BitTo8Bit(source.G), + B = ColorNumerics.From16BitTo8Bit(source.B) + }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromRgba64(Rgba64 source) + => new() + { + R = ColorNumerics.From16BitTo8Bit(source.R), + G = ColorNumerics.From16BitTo8Bit(source.G), + B = ColorNumerics.From16BitTo8Bit(source.B) + }; + + /// + public readonly bool Equals(Bgr24 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B); + + /// + public override readonly bool Equals(object? obj) => obj is Bgr24 other && this.Equals(other); + + /// + public override readonly string ToString() => $"Bgr24({this.B}, {this.G}, {this.R})"; + + /// + public override readonly int GetHashCode() => HashCode.Combine(this.R, this.B, this.G); + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs b/ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs new file mode 100644 index 0000000..65aed9b --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs @@ -0,0 +1,178 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing unsigned normalized values ranging from 0 to 1. + /// The x and z components use 5 bits, and the y component uses 6 bits. + /// + /// Ranges from [0, 0, 0, 1] to [1, 1, 1, 1] in vector form. + /// + /// + /// + /// Initializes a new instance of the struct. + /// + /// + /// The vector containing the components for the packed value. + /// + public partial struct Bgr565(Vector3 vector) : IPixel, IPackedVector + { + /// + /// Initializes a new instance of the struct. + /// + /// The x-component + /// The y-component + /// The z-component + public Bgr565(float x, float y, float z) + : this(new Vector3(x, y, z)) + { + } + + /// + public ushort PackedValue { get; set; } = Pack(vector); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Bgr565 left, Bgr565 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Bgr565 left, Bgr565 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new(this.ToVector3(), 1F); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(3, 5, 6, 5), + PixelColorType.BGR, + PixelAlphaRepresentation.None); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromVector4(Vector4 source) => new() { PackedValue = Pack(new Vector3(source.X, source.Y, source.Z)) }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromRgba32(Rgba32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + /// Expands the packed representation into a . + /// The vector components are typically expanded in least to greatest significance order. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector3 ToVector3() => new( + ((this.PackedValue >> 11) & 0x1F) * (1F / 31F), + ((this.PackedValue >> 5) & 0x3F) * (1F / 63F), + (this.PackedValue & 0x1F) * (1F / 31F)); + + /// + public override readonly bool Equals(object? obj) => obj is Bgr565 other && this.Equals(other); + + /// + public readonly bool Equals(Bgr565 other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly string ToString() + { + Vector3 vector = this.ToVector3(); + return FormattableString.Invariant($"Bgr565({vector.Z:#0.##}, {vector.Y:#0.##}, {vector.X:#0.##})"); + } + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ushort Pack(Vector3 vector) + { + vector = Vector3.Clamp(vector, Vector3.Zero, Vector3.One); + + return (ushort)((((int)Math.Round(vector.X * 31F) & 0x1F) << 11) + | (((int)Math.Round(vector.Y * 63F) & 0x3F) << 5) + | ((int)Math.Round(vector.Z * 31F) & 0x1F)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Bgra32.cs b/ImageSharp/PixelFormats/PixelImplementations/Bgra32.cs new file mode 100644 index 0000000..4215d02 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Bgra32.cs @@ -0,0 +1,247 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing four 8-bit unsigned normalized values ranging from 0 to 255. + /// The color components are stored in blue, green, red, and alpha order (least significant to most significant byte). + /// The format is binary compatible with System.Drawing.Imaging.PixelFormat.Format32bppArgb + /// + /// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. + /// + /// + [StructLayout(LayoutKind.Sequential)] + public partial struct Bgra32 : IPixel, IPackedVector + { + /// + /// Gets or sets the blue component. + /// + public byte B; + + /// + /// Gets or sets the green component. + /// + public byte G; + + /// + /// Gets or sets the red component. + /// + public byte R; + + /// + /// Gets or sets the alpha component. + /// + public byte A; + + private static readonly Vector4 MaxBytes = Vector128.Create(255f).AsVector4(); + private static readonly Vector4 Half = Vector128.Create(.5f).AsVector4(); + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Bgra32(byte r, byte g, byte b) + { + this.R = r; + this.G = g; + this.B = b; + this.A = byte.MaxValue; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The alpha component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Bgra32(byte r, byte g, byte b, byte a) + { + this.R = r; + this.G = g; + this.B = b; + this.A = a; + } + + /// + /// Gets or sets the packed representation of the Bgra32 struct. + /// + public uint Bgra + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => Unsafe.As(ref this) = value; + } + + /// + public uint PackedValue + { + readonly get => this.Bgra; + set => this.Bgra = value; + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Bgra32 left, Bgra32 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Bgra32 left, Bgra32 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromBgra32(this); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new Vector4(this.R, this.G, this.B, this.A) / MaxBytes; + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 8, 8, 8, 8), + PixelColorType.BGR | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromVector4(Vector4 source) => Pack(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromAbgr32(Abgr32 source) => new(source.R, source.G, source.B, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromArgb32(Argb32 source) => new(source.R, source.G, source.B, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromBgr24(Bgr24 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromBgra32(Bgra32 source) => new() { PackedValue = source.PackedValue }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromL8(L8 source) => new(source.PackedValue, source.PackedValue, source.PackedValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromL16(L16 source) + { + byte rgb = ColorNumerics.From16BitTo8Bit(source.PackedValue); + return new Bgra32(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromLa16(La16 source) => new(source.L, source.L, source.L, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromLa32(La32 source) + { + byte rgb = ColorNumerics.From16BitTo8Bit(source.L); + return new Bgra32(rgb, rgb, rgb, ColorNumerics.From16BitTo8Bit(source.A)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromRgb24(Rgb24 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromRgba32(Rgba32 source) => new(source.R, source.G, source.B, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromRgb48(Rgb48 source) + => new() + { + R = ColorNumerics.From16BitTo8Bit(source.R), + G = ColorNumerics.From16BitTo8Bit(source.G), + B = ColorNumerics.From16BitTo8Bit(source.B), + A = byte.MaxValue + }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromRgba64(Rgba64 source) + => new() + { + R = ColorNumerics.From16BitTo8Bit(source.R), + G = ColorNumerics.From16BitTo8Bit(source.G), + B = ColorNumerics.From16BitTo8Bit(source.B), + A = ColorNumerics.From16BitTo8Bit(source.A) + }; + + /// + public override readonly bool Equals(object? obj) => obj is Bgra32 other && this.Equals(other); + + /// + public readonly bool Equals(Bgra32 other) => this.Bgra.Equals(other.Bgra); + + /// + public override readonly int GetHashCode() => this.Bgra.GetHashCode(); + + /// + public override readonly string ToString() => $"Bgra32({this.B}, {this.G}, {this.R}, {this.A})"; + + /// + /// Packs a into a color. + /// + /// The vector containing the values to pack. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Bgra32 Pack(Vector4 vector) + { + vector *= MaxBytes; + vector += Half; + vector = Numerics.Clamp(vector, Vector4.Zero, MaxBytes); + + Vector128 result = Vector128.ConvertToInt32(vector.AsVector128()).AsByte(); + return new Bgra32(result.GetElement(0), result.GetElement(4), result.GetElement(8), result.GetElement(12)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Bgra4444.cs b/ImageSharp/PixelFormats/PixelImplementations/Bgra4444.cs new file mode 100644 index 0000000..0f621e5 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Bgra4444.cs @@ -0,0 +1,177 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing unsigned normalized values, ranging from 0 to 1, using 4 bits each for x, y, z, and w. + /// + /// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. + /// + /// + public partial struct Bgra4444 : IPixel, IPackedVector + { + /// + /// Initializes a new instance of the struct. + /// + /// The x-component + /// The y-component + /// The z-component + /// The w-component + public Bgra4444(float x, float y, float z, float w) + : this(new Vector4(x, y, z, w)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector containing the components for the packed vector. + public Bgra4444(Vector4 vector) => this.PackedValue = Pack(vector); + + /// + public ushort PackedValue { get; set; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Bgra4444 left, Bgra4444 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Bgra4444 left, Bgra4444 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() + { + const float max = 1 / 15f; + + return new Vector4( + (this.PackedValue >> 8) & 0x0F, + (this.PackedValue >> 4) & 0x0F, + this.PackedValue & 0x0F, + (this.PackedValue >> 12) & 0x0F) * max; + } + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 4, 4, 4, 4), + PixelColorType.BGR | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromVector4(Vector4 source) => new() { PackedValue = Pack(source) }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromRgba32(Rgba32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is Bgra4444 other && this.Equals(other); + + /// + public readonly bool Equals(Bgra4444 other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly string ToString() + { + Vector4 vector = this.ToVector4(); + return FormattableString.Invariant($"Bgra4444({vector.Z:#0.##}, {vector.Y:#0.##}, {vector.X:#0.##}, {vector.W:#0.##})"); + } + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ushort Pack(Vector4 vector) + { + vector = Numerics.Clamp(vector, Vector4.Zero, Vector4.One); + return (ushort)((((int)Math.Round(vector.W * 15F) & 0x0F) << 12) + | (((int)Math.Round(vector.X * 15F) & 0x0F) << 8) + | (((int)Math.Round(vector.Y * 15F) & 0x0F) << 4) + | ((int)Math.Round(vector.Z * 15F) & 0x0F)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Bgra5551.cs b/ImageSharp/PixelFormats/PixelImplementations/Bgra5551.cs new file mode 100644 index 0000000..2b6302a --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Bgra5551.cs @@ -0,0 +1,176 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing unsigned normalized values ranging from 0 to 1. + /// The x , y and z components use 5 bits, and the w component uses 1 bit. + /// + /// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. + /// + /// + public partial struct Bgra5551 : IPixel, IPackedVector + { + /// + /// Initializes a new instance of the struct. + /// + /// The x-component + /// The y-component + /// The z-component + /// The w-component + public Bgra5551(float x, float y, float z, float w) + : this(new Vector4(x, y, z, w)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The vector containing the components for the packed vector. + /// + public Bgra5551(Vector4 vector) => this.PackedValue = Pack(vector); + + /// + public ushort PackedValue { get; set; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Bgra5551 left, Bgra5551 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Bgra5551 left, Bgra5551 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new( + ((this.PackedValue >> 10) & 0x1F) / 31F, + ((this.PackedValue >> 5) & 0x1F) / 31F, + ((this.PackedValue >> 0) & 0x1F) / 31F, + (this.PackedValue >> 15) & 0x01); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 5, 5, 5, 1), + PixelColorType.BGR | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromVector4(Vector4 source) => new() { PackedValue = Pack(source) }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromRgba32(Rgba32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is Bgra5551 other && this.Equals(other); + + /// + public readonly bool Equals(Bgra5551 other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly string ToString() + { + Vector4 vector = this.ToVector4(); + return FormattableString.Invariant($"Bgra5551({vector.Z:#0.##}, {vector.Y:#0.##}, {vector.X:#0.##}, {vector.W:#0.##})"); + } + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ushort Pack(Vector4 vector) + { + vector = Numerics.Clamp(vector, Vector4.Zero, Vector4.One); + return (ushort)( + (((int)Math.Round(vector.X * 31F) & 0x1F) << 10) + | (((int)Math.Round(vector.Y * 31F) & 0x1F) << 5) + | (((int)Math.Round(vector.Z * 31F) & 0x1F) << 0) + | (((int)Math.Round(vector.W) & 0x1) << 15)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Byte4.cs b/ImageSharp/PixelFormats/PixelImplementations/Byte4.cs new file mode 100644 index 0000000..4be0597 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Byte4.cs @@ -0,0 +1,185 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing four 8-bit unsigned integer values, ranging from 0 to 255. + /// + /// Ranges from [0, 0, 0, 0] to [255, 255, 255, 255] in vector form. + /// + /// + public partial struct Byte4 : IPixel, IPackedVector + { + private static readonly Vector4 MaxBytes = Vector128.Create(255f).AsVector4(); + + /// + /// Initializes a new instance of the struct. + /// + /// The x-component + /// The y-component + /// The z-component + /// The w-component + public Byte4(float x, float y, float z, float w) + : this(new Vector4(x, y, z, w)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// A vector containing the initial values for the components of the Byte4 structure. + /// + public Byte4(Vector4 vector) => this.PackedValue = Pack(vector); + + /// + public uint PackedValue { get; set; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Byte4 left, Byte4 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Byte4 left, Byte4 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba32 ToRgba32() => new() { PackedValue = this.PackedValue }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4() / 255f; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new( + this.PackedValue & 0xFF, + (this.PackedValue >> 8) & 0xFF, + (this.PackedValue >> 16) & 0xFF, + (this.PackedValue >> 24) & 0xFF); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 8, 8, 8, 8), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromScaledVector4(Vector4 source) => FromVector4(source * 255f); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromVector4(Vector4 source) => new() { PackedValue = Pack(source) }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromRgba32(Rgba32 source) => new() { PackedValue = source.PackedValue }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is Byte4 byte4 && this.Equals(byte4); + + /// + public readonly bool Equals(Byte4 other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + public override readonly string ToString() + { + Vector4 vector = this.ToVector4(); + return FormattableString.Invariant($"Byte4({vector.X:#0.##}, {vector.Y:#0.##}, {vector.Z:#0.##}, {vector.W:#0.##})"); + } + + /// + /// Packs a vector into a uint. + /// + /// The vector containing the values to pack. + /// The containing the packed values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Pack(Vector4 vector) + { + vector = Numerics.Clamp(vector, Vector4.Zero, MaxBytes); + + uint byte4 = (uint)Math.Round(vector.X) & 0xFF; + uint byte3 = ((uint)Math.Round(vector.Y) & 0xFF) << 0x8; + uint byte2 = ((uint)Math.Round(vector.Z) & 0xFF) << 0x10; + uint byte1 = ((uint)Math.Round(vector.W) & 0xFF) << 0x18; + + return byte4 | byte3 | byte2 | byte1; + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs b/ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs new file mode 100644 index 0000000..94e4bd7 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs @@ -0,0 +1,160 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing a single 16 bit floating point value. + /// + /// Ranges from [-1, 0, 0, 1] to [1, 0, 0, 1] in vector form. + /// + /// + public partial struct HalfSingle : IPixel, IPackedVector + { + /// + /// Initializes a new instance of the struct. + /// + /// The single component value. + public HalfSingle(float value) => this.PackedValue = HalfTypeHelper.Pack(value); + + /// + public ushort PackedValue { get; set; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(HalfSingle left, HalfSingle right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(HalfSingle left, HalfSingle right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() + { + float single = this.ToSingle() + 1F; + single /= 2F; + return new Vector4(single, 0, 0, 1F); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new(this.ToSingle(), 0, 0, 1F); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(1, 16), + PixelColorType.Red, + PixelAlphaRepresentation.None); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromScaledVector4(Vector4 source) + { + float scaled = source.X; + scaled *= 2F; + scaled--; + return new HalfSingle { PackedValue = HalfTypeHelper.Pack(scaled) }; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromVector4(Vector4 source) => new() { PackedValue = HalfTypeHelper.Pack(source.X) }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromRgba32(Rgba32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + /// Expands the packed representation into a . + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly float ToSingle() => HalfTypeHelper.Unpack(this.PackedValue); + + /// + public override readonly bool Equals(object? obj) => obj is HalfSingle other && this.Equals(other); + + /// + public readonly bool Equals(HalfSingle other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly string ToString() => FormattableString.Invariant($"HalfSingle({this.ToSingle():#0.##})"); + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs b/ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs new file mode 100644 index 0000000..2eac05f --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs @@ -0,0 +1,189 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing two 16-bit floating-point values. + /// + /// Ranges from [-1, -1, 0, 1] to [1, 1, 0, 1] in vector form. + /// + /// + public partial struct HalfVector2 : IPixel, IPackedVector + { + /// + /// Initializes a new instance of the struct. + /// + /// The x-component. + /// The y-component. + public HalfVector2(float x, float y) => this.PackedValue = Pack(x, y); + + /// + /// Initializes a new instance of the struct. + /// + /// A vector containing the initial values for the components. + public HalfVector2(Vector2 vector) => this.PackedValue = Pack(vector.X, vector.Y); + + /// + public uint PackedValue { get; set; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(HalfVector2 left, HalfVector2 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(HalfVector2 left, HalfVector2 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() + { + Vector2 scaled = this.ToVector2(); + scaled += Vector2.One; + scaled /= 2F; + return new Vector4(scaled, 0F, 1F); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() + { + Vector2 vector = this.ToVector2(); + return new Vector4(vector.X, vector.Y, 0F, 1F); + } + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(2, 16, 16), + PixelColorType.Red | PixelColorType.Green, + PixelAlphaRepresentation.None); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromScaledVector4(Vector4 source) + { + Vector2 scaled = new Vector2(source.X, source.Y) * 2F; + scaled -= Vector2.One; + return new HalfVector2 { PackedValue = Pack(scaled.X, scaled.Y) }; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromVector4(Vector4 source) => new() { PackedValue = Pack(source.X, source.Y) }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromRgba32(Rgba32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + /// Expands the packed representation into a . + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector2 ToVector2() + { + Vector2 vector; + vector.X = HalfTypeHelper.Unpack((ushort)this.PackedValue); + vector.Y = HalfTypeHelper.Unpack((ushort)(this.PackedValue >> 0x10)); + return vector; + } + + /// + public override readonly bool Equals(object? obj) => obj is HalfVector2 other && this.Equals(other); + + /// + public readonly bool Equals(HalfVector2 other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly string ToString() + { + Vector2 vector = this.ToVector2(); + return FormattableString.Invariant($"HalfVector2({vector.X:#0.##}, {vector.Y:#0.##})"); + } + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Pack(float x, float y) + { + uint num2 = HalfTypeHelper.Pack(x); + uint num = (uint)(HalfTypeHelper.Pack(y) << 0x10); + return num2 | num; + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs b/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs new file mode 100644 index 0000000..0f3d419 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs @@ -0,0 +1,188 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing four 16-bit floating-point values. + /// + /// Ranges from [-1, -1, -1, -1] to [1, 1, 1, 1] in vector form. + /// + /// + public partial struct HalfVector4 : IPixel, IPackedVector + { + /// + /// Initializes a new instance of the struct. + /// + /// The x-component. + /// The y-component. + /// The z-component. + /// The w-component. + public HalfVector4(float x, float y, float z, float w) + : this(new Vector4(x, y, z, w)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// A vector containing the initial values for the components + public HalfVector4(Vector4 vector) => this.PackedValue = Pack(vector); + + /// + public ulong PackedValue { get; set; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(HalfVector4 left, HalfVector4 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(HalfVector4 left, HalfVector4 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() + { + Vector4 scaled = this.ToVector4(); + scaled += Vector4.One; + scaled /= 2f; + return scaled; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new( + HalfTypeHelper.Unpack((ushort)this.PackedValue), + HalfTypeHelper.Unpack((ushort)(this.PackedValue >> 0x10)), + HalfTypeHelper.Unpack((ushort)(this.PackedValue >> 0x20)), + HalfTypeHelper.Unpack((ushort)(this.PackedValue >> 0x30))); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 16, 16, 16, 16), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromScaledVector4(Vector4 source) + { + source *= 2f; + source -= Vector4.One; + return FromVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromVector4(Vector4 source) => new() { PackedValue = Pack(source) }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromRgba32(Rgba32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is HalfVector4 other && this.Equals(other); + + /// + public readonly bool Equals(HalfVector4 other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly string ToString() + { + Vector4 vector = this.ToVector4(); + return FormattableString.Invariant($"HalfVector4({vector.X:#0.##}, {vector.Y:#0.##}, {vector.Z:#0.##}, {vector.W:#0.##})"); + } + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + /// Packs a into a . + /// + /// The vector containing the values to pack. + /// The containing the packed values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Pack(Vector4 vector) + { + ulong num4 = HalfTypeHelper.Pack(vector.X); + ulong num3 = (ulong)HalfTypeHelper.Pack(vector.Y) << 0x10; + ulong num2 = (ulong)HalfTypeHelper.Pack(vector.Z) << 0x20; + ulong num1 = (ulong)HalfTypeHelper.Pack(vector.W) << 0x30; + return num4 | num3 | num2 | num1; + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/L16.cs b/ImageSharp/PixelFormats/PixelImplementations/L16.cs new file mode 100644 index 0000000..ed9679e --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/L16.cs @@ -0,0 +1,158 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing a single 16-bit normalized luminance value. + /// + /// Ranges from [0, 0, 0, 1] to [1, 1, 1, 1] in vector form. + /// + /// + public partial struct L16 : IPixel, IPackedVector + { + private const float Max = ushort.MaxValue; + + /// + /// Initializes a new instance of the struct. + /// + /// The luminance component + public L16(ushort luminance) => this.PackedValue = luminance; + + /// + public ushort PackedValue { get; set; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(L16 left, L16 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(L16 left, L16 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() + { + byte rgb = ColorNumerics.From16BitTo8Bit(this.PackedValue); + return new Rgba32(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() + { + float scaled = this.PackedValue / Max; + return new Vector4(scaled, scaled, scaled, 1f); + } + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(1, 16), + PixelColorType.Luminance, + PixelAlphaRepresentation.None); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromVector4(Vector4 source) => new() { PackedValue = Pack(source) }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromAbgr32(Abgr32 source) => new(ColorNumerics.Get16BitBT709Luminance(source.R, source.G, source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromArgb32(Argb32 source) => new(ColorNumerics.Get16BitBT709Luminance(source.R, source.G, source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromBgr24(Bgr24 source) => new(ColorNumerics.Get16BitBT709Luminance(source.R, source.G, source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromBgra32(Bgra32 source) => new(ColorNumerics.Get16BitBT709Luminance(source.R, source.G, source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromL8(L8 source) => new(ColorNumerics.From8BitTo16Bit(source.PackedValue)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromL16(L16 source) => new(source.PackedValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromLa16(La16 source) => new(ColorNumerics.From8BitTo16Bit(source.L)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromLa32(La32 source) => new(source.L); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromRgb24(Rgb24 source) => new(ColorNumerics.Get16BitBT709Luminance(source.R, source.G, source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromRgba32(Rgba32 source) => new(ColorNumerics.Get16BitBT709Luminance(source.R, source.G, source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromRgb48(Rgb48 source) => new(ColorNumerics.Get16BitBT709Luminance(source.R, source.G, source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromRgba64(Rgba64 source) => new(ColorNumerics.Get16BitBT709Luminance(source.R, source.G, source.B)); + + /// + public override readonly bool Equals(object? obj) => obj is L16 other && this.Equals(other); + + /// + public readonly bool Equals(L16 other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly string ToString() => $"L16({this.PackedValue})"; + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ushort Pack(Vector4 vector) + { + vector = Numerics.Clamp(vector, Vector4.Zero, Vector4.One) * Max; + return ColorNumerics.Get16BitBT709Luminance(vector.X, vector.Y, vector.Z); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/L8.cs b/ImageSharp/PixelFormats/PixelImplementations/L8.cs new file mode 100644 index 0000000..c8a75ce --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/L8.cs @@ -0,0 +1,164 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing a single 8-bit normalized luminance value. + /// + /// Ranges from [0, 0, 0, 1] to [1, 1, 1, 1] in vector form. + /// + /// + public partial struct L8 : IPixel, IPackedVector + { + private static readonly Vector4 MaxBytes = Vector128.Create(255f).AsVector4(); + private static readonly Vector4 Half = Vector128.Create(.5f).AsVector4(); + + /// + /// Initializes a new instance of the struct. + /// + /// The luminance component. + public L8(byte luminance) => this.PackedValue = luminance; + + /// + public byte PackedValue { get; set; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(L8 left, L8 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(L8 left, L8 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() + { + byte rgb = this.PackedValue; + return new Rgba32(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() + { + float rgb = this.PackedValue / 255f; + return new Vector4(rgb, rgb, rgb, 1f); + } + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(1, 8), + PixelColorType.Luminance, + PixelAlphaRepresentation.None); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromVector4(Vector4 source) => new() { PackedValue = Pack(source) }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromAbgr32(Abgr32 source) => new(ColorNumerics.Get8BitBT709Luminance(source.R, source.G, source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromArgb32(Argb32 source) => new(ColorNumerics.Get8BitBT709Luminance(source.R, source.G, source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromBgr24(Bgr24 source) => new(ColorNumerics.Get8BitBT709Luminance(source.R, source.G, source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromBgra32(Bgra32 source) => new(ColorNumerics.Get8BitBT709Luminance(source.R, source.G, source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromL8(L8 source) => new(source.PackedValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromL16(L16 source) => new(ColorNumerics.From16BitTo8Bit(source.PackedValue)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromLa16(La16 source) => new(source.L); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromLa32(La32 source) => new(ColorNumerics.From16BitTo8Bit(source.L)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromRgb24(Rgb24 source) => new(ColorNumerics.Get8BitBT709Luminance(source.R, source.G, source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromRgba32(Rgba32 source) => new(ColorNumerics.Get8BitBT709Luminance(source.R, source.G, source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromRgb48(Rgb48 source) => new(ColorNumerics.Get8BitBT709Luminance(source.R, source.G, source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromRgba64(Rgba64 source) => new(ColorNumerics.Get8BitBT709Luminance(source.R, source.G, source.B)); + + /// + public override readonly bool Equals(object? obj) => obj is L8 other && this.Equals(other); + + /// + public readonly bool Equals(L8 other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly string ToString() => $"L8({this.PackedValue})"; + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte Pack(Vector4 vector) + { + vector *= MaxBytes; + vector += Half; + vector = Numerics.Clamp(vector, Vector4.Zero, MaxBytes); + + Vector128 result = Vector128.ConvertToInt32(vector.AsVector128()).AsByte(); + return ColorNumerics.Get8BitBT709Luminance(result.GetElement(0), result.GetElement(4), result.GetElement(8)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/La16.cs b/ImageSharp/PixelFormats/PixelImplementations/La16.cs new file mode 100644 index 0000000..f1124b2 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/La16.cs @@ -0,0 +1,193 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing two 8-bit normalized values representing luminance and alpha. + /// + /// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. + /// + /// + [StructLayout(LayoutKind.Explicit)] + public partial struct La16 : IPixel, IPackedVector + { + /// + /// The maximum byte value. + /// + private static readonly Vector4 MaxBytes = Vector128.Create(255f).AsVector4(); + + /// + /// The half vector value. + /// + private static readonly Vector4 Half = Vector128.Create(.5f).AsVector4(); + + /// + /// Gets or sets the luminance component. + /// + [FieldOffset(0)] + public byte L; + + /// + /// Gets or sets the alpha component. + /// + [FieldOffset(1)] + public byte A; + + /// + /// Initializes a new instance of the struct. + /// + /// The luminance component. + /// The alpha component. + public La16(byte l, byte a) + { + this.L = l; + this.A = a; + } + + /// + public ushort PackedValue + { + readonly get => Unsafe.As(ref Unsafe.AsRef(in this)); + set => Unsafe.As(ref this) = value; + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(La16 left, La16 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(La16 left, La16 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => new(this.L, this.L, this.L, this.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() + { + const float max = 255f; + float rgb = this.L / max; + return new Vector4(rgb, rgb, rgb, this.A / max); + } + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(2, 8, 8), + PixelColorType.Luminance | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromScaledVector4(Vector4 source) => Pack(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromVector4(Vector4 source) => Pack(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromAbgr32(Abgr32 source) => new(ColorNumerics.Get8BitBT709Luminance(source.R, source.G, source.B), source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromArgb32(Argb32 source) => new(ColorNumerics.Get8BitBT709Luminance(source.R, source.G, source.B), source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromBgr24(Bgr24 source) => new(ColorNumerics.Get8BitBT709Luminance(source.R, source.G, source.B), byte.MaxValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromBgra32(Bgra32 source) => new(ColorNumerics.Get8BitBT709Luminance(source.R, source.G, source.B), source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromL16(L16 source) => new(ColorNumerics.From16BitTo8Bit(source.PackedValue), byte.MaxValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromL8(L8 source) => new(source.PackedValue, byte.MaxValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromLa16(La16 source) => new(source.L, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromLa32(La32 source) => new(ColorNumerics.From16BitTo8Bit(source.L), ColorNumerics.From16BitTo8Bit(source.A)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromRgb24(Rgb24 source) => new(ColorNumerics.Get8BitBT709Luminance(source.R, source.G, source.B), byte.MaxValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromRgba32(Rgba32 source) => new(ColorNumerics.Get8BitBT709Luminance(source.R, source.G, source.B), source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromRgb48(Rgb48 source) => new(ColorNumerics.Get8BitBT709Luminance(source.R, source.G, source.B), byte.MaxValue); + + /// + public static La16 FromRgba64(Rgba64 source) => new(ColorNumerics.Get8BitBT709Luminance(source.R, source.G, source.B), ColorNumerics.From16BitTo8Bit(source.A)); + + /// + public override readonly bool Equals(object? obj) => obj is La16 other && this.Equals(other); + + /// + public readonly bool Equals(La16 other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly string ToString() => $"La16({this.L}, {this.A})"; + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static La16 Pack(Vector4 vector) + { + vector *= MaxBytes; + vector += Half; + vector = Numerics.Clamp(vector, Vector4.Zero, MaxBytes); + + Vector128 result = Vector128.ConvertToInt32(vector.AsVector128()).AsByte(); + byte l = ColorNumerics.Get8BitBT709Luminance(result.GetElement(0), result.GetElement(4), result.GetElement(8)); + byte a = result.GetElement(12); + + return new La16(l, a); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/La32.cs b/ImageSharp/PixelFormats/PixelImplementations/La32.cs new file mode 100644 index 0000000..3a055f7 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/La32.cs @@ -0,0 +1,216 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing two 16-bit normalized values representing luminance and alpha. + /// + /// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. + /// + /// + [StructLayout(LayoutKind.Explicit)] + public partial struct La32 : IPixel, IPackedVector + { + private const float Max = ushort.MaxValue; + + /// + /// Gets or sets the luminance component. + /// + [FieldOffset(0)] + public ushort L; + + /// + /// Gets or sets the alpha component. + /// + [FieldOffset(2)] + public ushort A; + + /// + /// Initializes a new instance of the struct. + /// + /// The luminance component. + /// The alpha component. + public La32(ushort l, ushort a) + { + this.L = l; + this.A = a; + } + + /// + public uint PackedValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => Unsafe.As(ref this) = value; + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(La32 left, La32 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(La32 left, La32 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() + { + byte rgb = ColorNumerics.From16BitTo8Bit(this.L); + return new Rgba32(rgb, rgb, rgb, ColorNumerics.From16BitTo8Bit(this.A)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() + { + float rgb = this.L / Max; + return new Vector4(rgb, rgb, rgb, this.A / Max); + } + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(2, 16, 16), + PixelColorType.Luminance | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromScaledVector4(Vector4 source) => Pack(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromVector4(Vector4 source) => Pack(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromAbgr32(Abgr32 source) + { + ushort l = ColorNumerics.Get16BitBT709Luminance(source.R, source.G, source.B); + ushort a = ColorNumerics.From8BitTo16Bit(source.A); + return new La32(l, a); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromArgb32(Argb32 source) + { + ushort l = ColorNumerics.Get16BitBT709Luminance(source.R, source.G, source.B); + ushort a = ColorNumerics.From8BitTo16Bit(source.A); + return new La32(l, a); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromBgr24(Bgr24 source) => new(ColorNumerics.Get16BitBT709Luminance(source.R, source.G, source.B), ushort.MaxValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromBgra32(Bgra32 source) + { + ushort l = ColorNumerics.Get16BitBT709Luminance(source.R, source.G, source.B); + ushort a = ColorNumerics.From8BitTo16Bit(source.A); + return new La32(l, a); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromL16(L16 source) => new(source.PackedValue, ushort.MaxValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromL8(L8 source) => new(ColorNumerics.From8BitTo16Bit(source.PackedValue), ushort.MaxValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromLa16(La16 source) + { + ushort l = ColorNumerics.From8BitTo16Bit(source.L); + ushort a = ColorNumerics.From8BitTo16Bit(source.A); + return new La32(l, a); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromLa32(La32 source) => new(source.L, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromRgb24(Rgb24 source) => new(ColorNumerics.Get16BitBT709Luminance(source.R, source.G, source.B), ushort.MaxValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromRgb48(Rgb48 source) + { + ushort l = ColorNumerics.Get16BitBT709Luminance(source.R, source.G, source.B); + return new La32(l, ushort.MaxValue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromRgba32(Rgba32 source) + { + ushort l = ColorNumerics.Get16BitBT709Luminance(source.R, source.G, source.B); + ushort a = ColorNumerics.From8BitTo16Bit(source.A); + return new La32(l, a); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromRgba64(Rgba64 source) => new(ColorNumerics.Get16BitBT709Luminance(source.R, source.G, source.B), source.A); + + /// + public override readonly bool Equals(object? obj) => obj is La32 other && this.Equals(other); + + /// + public readonly bool Equals(La32 other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly string ToString() => $"La32({this.L}, {this.A})"; + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static La32 Pack(Vector4 vector) + { + vector = Numerics.Clamp(vector, Vector4.Zero, Vector4.One) * Max; + ushort l = ColorNumerics.Get16BitBT709Luminance(vector.X, vector.Y, vector.Z); + ushort a = (ushort)MathF.Round(vector.W); + return new La32(l, a); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs b/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs new file mode 100644 index 0000000..ee56adf --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs @@ -0,0 +1,192 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing two 8-bit signed normalized values, ranging from −1 to 1. + /// + /// Ranges from [-1, -1, 0, 1] to [1, 1, 0, 1] in vector form. + /// + /// + public partial struct NormalizedByte2 : IPixel, IPackedVector + { + private const float MaxPos = 127f; + private static readonly Vector2 Half = new(MaxPos); + private static readonly Vector2 MinusOne = new(-1f); + + /// + /// Initializes a new instance of the struct. + /// + /// The x-component. + /// The y-component. + public NormalizedByte2(float x, float y) + : this(new Vector2(x, y)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector containing the component values. + public NormalizedByte2(Vector2 vector) => this.PackedValue = Pack(vector); + + /// + public ushort PackedValue { get; set; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(NormalizedByte2 left, NormalizedByte2 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(NormalizedByte2 left, NormalizedByte2 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() + { + Vector2 scaled = this.ToVector2(); + scaled += Vector2.One; + scaled /= 2f; + return new Vector4(scaled, 0f, 1f); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new(this.ToVector2(), 0f, 1f); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(2, 8, 8), + PixelColorType.Red | PixelColorType.Green, + PixelAlphaRepresentation.None); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromScaledVector4(Vector4 source) + { + Vector2 scaled = new Vector2(source.X, source.Y) * 2f; + scaled -= Vector2.One; + return new NormalizedByte2 { PackedValue = Pack(scaled) }; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromVector4(Vector4 source) => new() { PackedValue = Pack(new Vector2(source.X, source.Y)) }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromRgba32(Rgba32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + /// Expands the packed representation into a . + /// The vector components are typically expanded in least to greatest significance order. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector2 ToVector2() => new( + (sbyte)((this.PackedValue >> 0) & 0xFF) / MaxPos, + (sbyte)((this.PackedValue >> 8) & 0xFF) / MaxPos); + + /// + public override readonly bool Equals(object? obj) => obj is NormalizedByte2 other && this.Equals(other); + + /// + public readonly bool Equals(NormalizedByte2 other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + public override readonly string ToString() + { + Vector2 vector = this.ToVector2(); + return FormattableString.Invariant($"NormalizedByte2({vector.X:#0.##}, {vector.Y:#0.##})"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ushort Pack(Vector2 vector) + { + vector = Vector2.Clamp(vector, MinusOne, Vector2.One) * Half; + + int byte2 = ((ushort)Convert.ToInt16(Math.Round(vector.X)) & 0xFF) << 0; + int byte1 = ((ushort)Convert.ToInt16(Math.Round(vector.Y)) & 0xFF) << 8; + + return (ushort)(byte2 | byte1); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4.cs b/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4.cs new file mode 100644 index 0000000..aae8dec --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4.cs @@ -0,0 +1,191 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing four 8-bit signed normalized values, ranging from −1 to 1. + /// + /// Ranges from [-1, -1, -1, -1] to [1, 1, 1, 1] in vector form. + /// + /// + public partial struct NormalizedByte4 : IPixel, IPackedVector + { + private const float MaxPos = 127f; + private static readonly Vector4 Half = Vector128.Create(MaxPos).AsVector4(); + private static readonly Vector4 MinusOne = Vector128.Create(-1f).AsVector4(); + + /// + /// Initializes a new instance of the struct. + /// + /// The x-component. + /// The y-component. + /// The z-component. + /// The w-component. + public NormalizedByte4(float x, float y, float z, float w) + : this(new Vector4(x, y, z, w)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector containing the component values. + public NormalizedByte4(Vector4 vector) => this.PackedValue = Pack(vector); + + /// + public uint PackedValue { get; set; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(NormalizedByte4 left, NormalizedByte4 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(NormalizedByte4 left, NormalizedByte4 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() + { + Vector4 scaled = this.ToVector4(); + scaled += Vector4.One; + scaled /= 2f; + return scaled; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new( + (sbyte)((this.PackedValue >> 0) & 0xFF) / MaxPos, + (sbyte)((this.PackedValue >> 8) & 0xFF) / MaxPos, + (sbyte)((this.PackedValue >> 16) & 0xFF) / MaxPos, + (sbyte)((this.PackedValue >> 24) & 0xFF) / MaxPos); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 8, 8, 8, 8), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromScaledVector4(Vector4 source) + { + source *= 2f; + source -= Vector4.One; + return FromVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromRgba32(Rgba32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromVector4(Vector4 source) => new() { PackedValue = Pack(source) }; + + /// + public override readonly bool Equals(object? obj) => obj is NormalizedByte4 other && this.Equals(other); + + /// + public readonly bool Equals(NormalizedByte4 other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + public override readonly string ToString() + { + Vector4 vector = this.ToVector4(); + return FormattableString.Invariant($"NormalizedByte4({vector.X:#0.##}, {vector.Y:#0.##}, {vector.Z:#0.##}, {vector.W:#0.##})"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Pack(Vector4 vector) + { + vector = Numerics.Clamp(vector, MinusOne, Vector4.One) * Half; + + uint byte4 = ((uint)Convert.ToInt16(MathF.Round(vector.X)) & 0xFF) << 0; + uint byte3 = ((uint)Convert.ToInt16(MathF.Round(vector.Y)) & 0xFF) << 8; + uint byte2 = ((uint)Convert.ToInt16(MathF.Round(vector.Z)) & 0xFF) << 16; + uint byte1 = ((uint)Convert.ToInt16(MathF.Round(vector.W)) & 0xFF) << 24; + + return byte4 | byte3 | byte2 | byte1; + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs b/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs new file mode 100644 index 0000000..5d303d3 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs @@ -0,0 +1,196 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing two 16-bit signed normalized values, ranging from −1 to 1. + /// + /// Ranges from [-1, -1, 0, 1] to [1, 1, 0, 1] in vector form. + /// + /// + public partial struct NormalizedShort2 : IPixel, IPackedVector + { + // Largest two byte positive number 0xFFFF >> 1; + private const float MaxPos = 0x7FFF; + + private static readonly Vector2 Max = new(MaxPos); + private static readonly Vector2 Min = Vector2.Negate(Max); + + /// + /// Initializes a new instance of the struct. + /// + /// The x-component. + /// The y-component. + public NormalizedShort2(float x, float y) + : this(new Vector2(x, y)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector containing the component values. + public NormalizedShort2(Vector2 vector) => this.PackedValue = Pack(vector); + + /// + public uint PackedValue { get; set; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(NormalizedShort2 left, NormalizedShort2 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(NormalizedShort2 left, NormalizedShort2 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() + { + Vector2 scaled = this.ToVector2(); + scaled += Vector2.One; + scaled /= 2f; + return new Vector4(scaled, 0f, 1f); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new(this.ToVector2(), 0f, 1f); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(2, 16, 16), + PixelColorType.Red | PixelColorType.Green, + PixelAlphaRepresentation.None); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromScaledVector4(Vector4 source) + { + Vector2 scaled = new Vector2(source.X, source.Y) * 2f; + scaled -= Vector2.One; + return new NormalizedShort2 { PackedValue = Pack(scaled) }; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromVector4(Vector4 source) => new() { PackedValue = Pack(new Vector2(source.X, source.Y)) }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromRgba32(Rgba32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + /// Expands the packed representation into a . + /// The vector components are typically expanded in least to greatest significance order. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector2 ToVector2() => new( + (short)(this.PackedValue & 0xFFFF) / MaxPos, + (short)(this.PackedValue >> 0x10) / MaxPos); + + /// + public override readonly bool Equals(object? obj) => obj is NormalizedShort2 other && this.Equals(other); + + /// + public readonly bool Equals(NormalizedShort2 other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + public override readonly string ToString() + { + Vector2 vector = this.ToVector2(); + return FormattableString.Invariant($"NormalizedShort2({vector.X:#0.##}, {vector.Y:#0.##})"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Pack(Vector2 vector) + { + vector *= Max; + vector = Vector2.Clamp(vector, Min, Max); + + // Round rather than truncate. + uint word2 = (uint)((int)MathF.Round(vector.X) & 0xFFFF); + uint word1 = (uint)(((int)MathF.Round(vector.Y) & 0xFFFF) << 0x10); + + return word2 | word1; + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort4.cs b/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort4.cs new file mode 100644 index 0000000..67d2c1f --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort4.cs @@ -0,0 +1,194 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing four 16-bit signed normalized values, ranging from −1 to 1. + /// + /// Ranges from [-1, -1, -1, -1] to [1, 1, 1, 1] in vector form. + /// + /// + public partial struct NormalizedShort4 : IPixel, IPackedVector + { + // Largest two byte positive number 0xFFFF >> 1; + private const float MaxPos = 0x7FFF; + private static readonly Vector4 Max = Vector128.Create(MaxPos).AsVector4(); + private static readonly Vector4 Min = Vector4.Negate(Max); + + /// + /// Initializes a new instance of the struct. + /// + /// The x-component. + /// The y-component. + /// The z-component. + /// The w-component. + public NormalizedShort4(float x, float y, float z, float w) + : this(new Vector4(x, y, z, w)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector containing the component values. + public NormalizedShort4(Vector4 vector) => this.PackedValue = Pack(vector); + + /// + public ulong PackedValue { get; set; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(NormalizedShort4 left, NormalizedShort4 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(NormalizedShort4 left, NormalizedShort4 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() + { + Vector4 scaled = this.ToVector4(); + scaled += Vector4.One; + scaled /= 2f; + return scaled; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new( + (short)((this.PackedValue >> 0x00) & 0xFFFF) / MaxPos, + (short)((this.PackedValue >> 0x10) & 0xFFFF) / MaxPos, + (short)((this.PackedValue >> 0x20) & 0xFFFF) / MaxPos, + (short)((this.PackedValue >> 0x30) & 0xFFFF) / MaxPos); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 16, 16, 16, 16), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromScaledVector4(Vector4 source) + { + source *= 2f; + source -= Vector4.One; + return FromVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromVector4(Vector4 source) => new() { PackedValue = Pack(source) }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromRgba32(Rgba32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is NormalizedShort4 other && this.Equals(other); + + /// + public readonly bool Equals(NormalizedShort4 other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + public override readonly string ToString() + { + Vector4 vector = this.ToVector4(); + return FormattableString.Invariant($"NormalizedShort4({vector.X:#0.##}, {vector.Y:#0.##}, {vector.Z:#0.##}, {vector.W:#0.##})"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Pack(Vector4 vector) + { + vector *= Max; + vector = Numerics.Clamp(vector, Min, Max); + + // Round rather than truncate. + ulong word4 = ((ulong)Convert.ToInt32(MathF.Round(vector.X)) & 0xFFFF) << 0x00; + ulong word3 = ((ulong)Convert.ToInt32(MathF.Round(vector.Y)) & 0xFFFF) << 0x10; + ulong word2 = ((ulong)Convert.ToInt32(MathF.Round(vector.Z)) & 0xFFFF) << 0x20; + ulong word1 = ((ulong)Convert.ToInt32(MathF.Round(vector.W)) & 0xFFFF) << 0x30; + + return word4 | word3 | word2 | word1; + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/A8.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/A8.PixelOperations.cs new file mode 100644 index 0000000..9745550 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/A8.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct A8 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Abgr32.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Abgr32.PixelOperations.cs new file mode 100644 index 0000000..ce08466 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Abgr32.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct Abgr32 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Argb32.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Argb32.PixelOperations.cs new file mode 100644 index 0000000..803d7ee --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Argb32.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct Argb32 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgr24.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgr24.PixelOperations.cs new file mode 100644 index 0000000..1f7a5ae --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgr24.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct Bgr24 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgr565.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgr565.PixelOperations.cs new file mode 100644 index 0000000..7b01579 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgr565.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct Bgr565 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgra32.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgra32.PixelOperations.cs new file mode 100644 index 0000000..81e3191 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgra32.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct Bgra32 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgra4444.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgra4444.PixelOperations.cs new file mode 100644 index 0000000..7f7b878 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgra4444.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct Bgra4444 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgra5551.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgra5551.PixelOperations.cs new file mode 100644 index 0000000..86edfbb --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgra5551.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct Bgra5551 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Byte4.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Byte4.PixelOperations.cs new file mode 100644 index 0000000..70cfdfc --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Byte4.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct Byte4 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Abgr32.PixelOperations.Generated.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Abgr32.PixelOperations.Generated.cs new file mode 100644 index 0000000..c75e11f --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Abgr32.PixelOperations.Generated.cs @@ -0,0 +1,337 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Abgr32 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + /// + public override void FromAbgr32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToAbgr32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void FromVector4Destructive( + Configuration configuration, + Span sourceVectors, + Span destination, + PixelConversionModifiers modifiers) + { + Vector4Converters.RgbaCompatible.FromVector4(configuration, this, sourceVectors, destination, modifiers.Remove(PixelConversionModifiers.Scale)); + } + + /// + public override void ToVector4( + Configuration configuration, + ReadOnlySpan source, + Span destination, + PixelConversionModifiers modifiers) + { + Vector4Converters.RgbaCompatible.ToVector4(configuration, this, source, destination, modifiers.Remove(PixelConversionModifiers.Scale)); + } + + /// + public override void ToRgba32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromAbgr32.ToRgba32(sourceBytes, destinationBytes); + } + + /// + public override void FromRgba32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgba32.ToAbgr32(sourceBytes, destinationBytes); + } + + /// + public override void ToArgb32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromAbgr32.ToArgb32(sourceBytes, destinationBytes); + } + + /// + public override void FromArgb32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromArgb32.ToAbgr32(sourceBytes, destinationBytes); + } + + /// + public override void ToBgra32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromAbgr32.ToBgra32(sourceBytes, destinationBytes); + } + + /// + public override void FromBgra32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgra32.ToAbgr32(sourceBytes, destinationBytes); + } + + /// + public override void ToRgb24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromAbgr32.ToRgb24(sourceBytes, destinationBytes); + } + + /// + public override void FromRgb24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgb24.ToAbgr32(sourceBytes, destinationBytes); + } + + /// + public override void ToBgr24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromAbgr32.ToBgr24(sourceBytes, destinationBytes); + } + + /// + public override void FromBgr24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgr24.ToAbgr32(sourceBytes, destinationBytes); + } + + /// + public override void ToL8( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Abgr32 sourceBase = ref MemoryMarshal.GetReference(source); + ref L8 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L8.FromAbgr32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Abgr32 sourceBase = ref MemoryMarshal.GetReference(source); + ref L16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L16.FromAbgr32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Abgr32 sourceBase = ref MemoryMarshal.GetReference(source); + ref La16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La16.FromAbgr32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Abgr32 sourceBase = ref MemoryMarshal.GetReference(source); + ref La32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La32.FromAbgr32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb48( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Abgr32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb48 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb48.FromAbgr32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba64( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Abgr32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba64 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba64.FromAbgr32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra5551( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Abgr32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra5551 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra5551.FromAbgr32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void From( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + PixelOperations.Instance.ToAbgr32(configuration, source, destination.Slice(0, source.Length)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Abgr32.PixelOperations.Generated.tt b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Abgr32.PixelOperations.Generated.tt new file mode 100644 index 0000000..466c87a --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Abgr32.PixelOperations.Generated.tt @@ -0,0 +1,17 @@ +<#@include file="_Common.ttinclude" #> +<#@ output extension=".cs" #> +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Abgr32 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + <# GenerateAllDefaultConversionMethods("Abgr32"); #> + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Argb32.PixelOperations.Generated.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Argb32.PixelOperations.Generated.cs new file mode 100644 index 0000000..f860169 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Argb32.PixelOperations.Generated.cs @@ -0,0 +1,337 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Argb32 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + /// + public override void FromArgb32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToArgb32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void FromVector4Destructive( + Configuration configuration, + Span sourceVectors, + Span destination, + PixelConversionModifiers modifiers) + { + Vector4Converters.RgbaCompatible.FromVector4(configuration, this, sourceVectors, destination, modifiers.Remove(PixelConversionModifiers.Scale)); + } + + /// + public override void ToVector4( + Configuration configuration, + ReadOnlySpan source, + Span destination, + PixelConversionModifiers modifiers) + { + Vector4Converters.RgbaCompatible.ToVector4(configuration, this, source, destination, modifiers.Remove(PixelConversionModifiers.Scale)); + } + + /// + public override void ToRgba32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromArgb32.ToRgba32(sourceBytes, destinationBytes); + } + + /// + public override void FromRgba32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgba32.ToArgb32(sourceBytes, destinationBytes); + } + + /// + public override void ToAbgr32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromArgb32.ToAbgr32(sourceBytes, destinationBytes); + } + + /// + public override void FromAbgr32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromAbgr32.ToArgb32(sourceBytes, destinationBytes); + } + + /// + public override void ToBgra32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromArgb32.ToBgra32(sourceBytes, destinationBytes); + } + + /// + public override void FromBgra32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgra32.ToArgb32(sourceBytes, destinationBytes); + } + + /// + public override void ToRgb24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromArgb32.ToRgb24(sourceBytes, destinationBytes); + } + + /// + public override void FromRgb24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgb24.ToArgb32(sourceBytes, destinationBytes); + } + + /// + public override void ToBgr24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromArgb32.ToBgr24(sourceBytes, destinationBytes); + } + + /// + public override void FromBgr24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgr24.ToArgb32(sourceBytes, destinationBytes); + } + + /// + public override void ToL8( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Argb32 sourceBase = ref MemoryMarshal.GetReference(source); + ref L8 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L8.FromArgb32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Argb32 sourceBase = ref MemoryMarshal.GetReference(source); + ref L16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L16.FromArgb32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Argb32 sourceBase = ref MemoryMarshal.GetReference(source); + ref La16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La16.FromArgb32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Argb32 sourceBase = ref MemoryMarshal.GetReference(source); + ref La32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La32.FromArgb32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb48( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Argb32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb48 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb48.FromArgb32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba64( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Argb32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba64 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba64.FromArgb32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra5551( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Argb32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra5551 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra5551.FromArgb32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void From( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + PixelOperations.Instance.ToArgb32(configuration, source, destination.Slice(0, source.Length)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Argb32.PixelOperations.Generated.tt b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Argb32.PixelOperations.Generated.tt new file mode 100644 index 0000000..c157e59 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Argb32.PixelOperations.Generated.tt @@ -0,0 +1,17 @@ +<#@include file="_Common.ttinclude" #> +<#@ output extension=".cs" #> +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Argb32 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + <# GenerateAllDefaultConversionMethods("Argb32"); #> + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgr24.PixelOperations.Generated.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgr24.PixelOperations.Generated.cs new file mode 100644 index 0000000..5aa5a07 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgr24.PixelOperations.Generated.cs @@ -0,0 +1,337 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Bgr24 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + /// + public override void FromBgr24(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToBgr24(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void FromVector4Destructive( + Configuration configuration, + Span sourceVectors, + Span destination, + PixelConversionModifiers modifiers) + { + Vector4Converters.RgbaCompatible.FromVector4(configuration, this, sourceVectors, destination, modifiers.Remove(PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply)); + } + + /// + public override void ToVector4( + Configuration configuration, + ReadOnlySpan source, + Span destination, + PixelConversionModifiers modifiers) + { + Vector4Converters.RgbaCompatible.ToVector4(configuration, this, source, destination, modifiers.Remove(PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply)); + } + + /// + public override void ToRgba32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgr24.ToRgba32(sourceBytes, destinationBytes); + } + + /// + public override void FromRgba32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgba32.ToBgr24(sourceBytes, destinationBytes); + } + + /// + public override void ToArgb32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgr24.ToArgb32(sourceBytes, destinationBytes); + } + + /// + public override void FromArgb32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromArgb32.ToBgr24(sourceBytes, destinationBytes); + } + + /// + public override void ToAbgr32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgr24.ToAbgr32(sourceBytes, destinationBytes); + } + + /// + public override void FromAbgr32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromAbgr32.ToBgr24(sourceBytes, destinationBytes); + } + + /// + public override void ToBgra32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgr24.ToBgra32(sourceBytes, destinationBytes); + } + + /// + public override void FromBgra32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgra32.ToBgr24(sourceBytes, destinationBytes); + } + + /// + public override void ToRgb24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgr24.ToRgb24(sourceBytes, destinationBytes); + } + + /// + public override void FromRgb24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgb24.ToBgr24(sourceBytes, destinationBytes); + } + + /// + public override void ToL8( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgr24 sourceBase = ref MemoryMarshal.GetReference(source); + ref L8 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L8.FromBgr24(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgr24 sourceBase = ref MemoryMarshal.GetReference(source); + ref L16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L16.FromBgr24(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgr24 sourceBase = ref MemoryMarshal.GetReference(source); + ref La16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La16.FromBgr24(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgr24 sourceBase = ref MemoryMarshal.GetReference(source); + ref La32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La32.FromBgr24(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb48( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgr24 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb48 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb48.FromBgr24(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba64( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgr24 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba64 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba64.FromBgr24(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra5551( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgr24 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra5551 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra5551.FromBgr24(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void From( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + PixelOperations.Instance.ToBgr24(configuration, source, destination.Slice(0, source.Length)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgr24.PixelOperations.Generated.tt b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgr24.PixelOperations.Generated.tt new file mode 100644 index 0000000..863120e --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgr24.PixelOperations.Generated.tt @@ -0,0 +1,17 @@ +<#@include file="_Common.ttinclude" #> +<#@ output extension=".cs" #> +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Bgr24 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + <# GenerateAllDefaultConversionMethods("Bgr24"); #> + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra32.PixelOperations.Generated.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra32.PixelOperations.Generated.cs new file mode 100644 index 0000000..0083b9e --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra32.PixelOperations.Generated.cs @@ -0,0 +1,337 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Bgra32 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + /// + public override void FromBgra32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToBgra32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void FromVector4Destructive( + Configuration configuration, + Span sourceVectors, + Span destination, + PixelConversionModifiers modifiers) + { + Vector4Converters.RgbaCompatible.FromVector4(configuration, this, sourceVectors, destination, modifiers.Remove(PixelConversionModifiers.Scale)); + } + + /// + public override void ToVector4( + Configuration configuration, + ReadOnlySpan source, + Span destination, + PixelConversionModifiers modifiers) + { + Vector4Converters.RgbaCompatible.ToVector4(configuration, this, source, destination, modifiers.Remove(PixelConversionModifiers.Scale)); + } + + /// + public override void ToRgba32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgra32.ToRgba32(sourceBytes, destinationBytes); + } + + /// + public override void FromRgba32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgba32.ToBgra32(sourceBytes, destinationBytes); + } + + /// + public override void ToArgb32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgra32.ToArgb32(sourceBytes, destinationBytes); + } + + /// + public override void FromArgb32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromArgb32.ToBgra32(sourceBytes, destinationBytes); + } + + /// + public override void ToAbgr32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgra32.ToAbgr32(sourceBytes, destinationBytes); + } + + /// + public override void FromAbgr32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromAbgr32.ToBgra32(sourceBytes, destinationBytes); + } + + /// + public override void ToRgb24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgra32.ToRgb24(sourceBytes, destinationBytes); + } + + /// + public override void FromRgb24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgb24.ToBgra32(sourceBytes, destinationBytes); + } + + /// + public override void ToBgr24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgra32.ToBgr24(sourceBytes, destinationBytes); + } + + /// + public override void FromBgr24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgr24.ToBgra32(sourceBytes, destinationBytes); + } + + /// + public override void ToL8( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra32 sourceBase = ref MemoryMarshal.GetReference(source); + ref L8 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L8.FromBgra32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra32 sourceBase = ref MemoryMarshal.GetReference(source); + ref L16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L16.FromBgra32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra32 sourceBase = ref MemoryMarshal.GetReference(source); + ref La16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La16.FromBgra32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra32 sourceBase = ref MemoryMarshal.GetReference(source); + ref La32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La32.FromBgra32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb48( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb48 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb48.FromBgra32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba64( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba64 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba64.FromBgra32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra5551( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra5551 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra5551.FromBgra32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void From( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + PixelOperations.Instance.ToBgra32(configuration, source, destination.Slice(0, source.Length)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra32.PixelOperations.Generated.tt b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra32.PixelOperations.Generated.tt new file mode 100644 index 0000000..9970544 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra32.PixelOperations.Generated.tt @@ -0,0 +1,17 @@ +<#@include file="_Common.ttinclude" #> +<#@ output extension=".cs" #> +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Bgra32 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + <# GenerateAllDefaultConversionMethods("Bgra32"); #> + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra5551.PixelOperations.Generated.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra5551.PixelOperations.Generated.cs new file mode 100644 index 0000000..0254027 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra5551.PixelOperations.Generated.cs @@ -0,0 +1,267 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Bgra5551 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + /// + public override void FromBgra5551(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToBgra5551(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToArgb32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra5551 sourceBase = ref MemoryMarshal.GetReference(source); + ref Argb32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Argb32.FromBgra5551(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToAbgr32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra5551 sourceBase = ref MemoryMarshal.GetReference(source); + ref Abgr32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Abgr32.FromBgra5551(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgr24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra5551 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgr24 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgr24.FromBgra5551(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra5551 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra32.FromBgra5551(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL8( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra5551 sourceBase = ref MemoryMarshal.GetReference(source); + ref L8 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L8.FromBgra5551(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra5551 sourceBase = ref MemoryMarshal.GetReference(source); + ref L16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L16.FromBgra5551(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra5551 sourceBase = ref MemoryMarshal.GetReference(source); + ref La16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La16.FromBgra5551(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra5551 sourceBase = ref MemoryMarshal.GetReference(source); + ref La32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La32.FromBgra5551(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra5551 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb24 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb24.FromBgra5551(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra5551 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba32.FromBgra5551(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb48( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra5551 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb48 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb48.FromBgra5551(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba64( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra5551 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba64 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba64.FromBgra5551(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void From( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + PixelOperations.Instance.ToBgra5551(configuration, source, destination.Slice(0, source.Length)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra5551.PixelOperations.Generated.tt b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra5551.PixelOperations.Generated.tt new file mode 100644 index 0000000..56bf546 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra5551.PixelOperations.Generated.tt @@ -0,0 +1,17 @@ +<#@include file="_Common.ttinclude" #> +<#@ output extension=".cs" #> +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Bgra5551 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + <# GenerateAllDefaultConversionMethods("Bgra5551"); #> + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/L16.PixelOperations.Generated.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/L16.PixelOperations.Generated.cs new file mode 100644 index 0000000..8c9c42c --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/L16.PixelOperations.Generated.cs @@ -0,0 +1,267 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct L16 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + /// + public override void FromL16(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToL16(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToArgb32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Argb32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Argb32.FromL16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToAbgr32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Abgr32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Abgr32.FromL16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgr24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgr24 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgr24.FromL16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra32.FromL16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL8( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L16 sourceBase = ref MemoryMarshal.GetReference(source); + ref L8 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L8.FromL16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L16 sourceBase = ref MemoryMarshal.GetReference(source); + ref La16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La16.FromL16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L16 sourceBase = ref MemoryMarshal.GetReference(source); + ref La32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La32.FromL16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb24 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb24.FromL16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba32.FromL16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb48( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb48 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb48.FromL16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba64( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba64 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba64.FromL16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra5551( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra5551 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra5551.FromL16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void From( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + PixelOperations.Instance.ToL16(configuration, source, destination.Slice(0, source.Length)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/L16.PixelOperations.Generated.tt b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/L16.PixelOperations.Generated.tt new file mode 100644 index 0000000..09c44fc --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/L16.PixelOperations.Generated.tt @@ -0,0 +1,17 @@ +<#@include file="_Common.ttinclude" #> +<#@ output extension=".cs" #> +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct L16 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + <# GenerateAllDefaultConversionMethods("L16"); #> + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/L8.PixelOperations.Generated.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/L8.PixelOperations.Generated.cs new file mode 100644 index 0000000..6eb5e78 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/L8.PixelOperations.Generated.cs @@ -0,0 +1,267 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct L8 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + /// + public override void FromL8(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToL8(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToArgb32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L8 sourceBase = ref MemoryMarshal.GetReference(source); + ref Argb32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Argb32.FromL8(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToAbgr32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L8 sourceBase = ref MemoryMarshal.GetReference(source); + ref Abgr32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Abgr32.FromL8(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgr24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L8 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgr24 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgr24.FromL8(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L8 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra32.FromL8(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L8 sourceBase = ref MemoryMarshal.GetReference(source); + ref L16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L16.FromL8(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L8 sourceBase = ref MemoryMarshal.GetReference(source); + ref La16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La16.FromL8(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L8 sourceBase = ref MemoryMarshal.GetReference(source); + ref La32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La32.FromL8(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L8 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb24 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb24.FromL8(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L8 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba32.FromL8(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb48( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L8 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb48 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb48.FromL8(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba64( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L8 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba64 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba64.FromL8(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra5551( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L8 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra5551 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra5551.FromL8(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void From( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + PixelOperations.Instance.ToL8(configuration, source, destination.Slice(0, source.Length)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/L8.PixelOperations.Generated.tt b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/L8.PixelOperations.Generated.tt new file mode 100644 index 0000000..f007f76 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/L8.PixelOperations.Generated.tt @@ -0,0 +1,17 @@ +<#@include file="_Common.ttinclude" #> +<#@ output extension=".cs" #> +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct L8 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + <# GenerateAllDefaultConversionMethods("L8"); #> + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/La16.PixelOperations.Generated.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/La16.PixelOperations.Generated.cs new file mode 100644 index 0000000..8ede52f --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/La16.PixelOperations.Generated.cs @@ -0,0 +1,267 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct La16 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + /// + public override void FromLa16(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToLa16(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToArgb32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Argb32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Argb32.FromLa16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToAbgr32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Abgr32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Abgr32.FromLa16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgr24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgr24 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgr24.FromLa16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra32.FromLa16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL8( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La16 sourceBase = ref MemoryMarshal.GetReference(source); + ref L8 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L8.FromLa16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La16 sourceBase = ref MemoryMarshal.GetReference(source); + ref L16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L16.FromLa16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La16 sourceBase = ref MemoryMarshal.GetReference(source); + ref La32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La32.FromLa16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb24 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb24.FromLa16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba32.FromLa16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb48( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb48 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb48.FromLa16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba64( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba64 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba64.FromLa16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra5551( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La16 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra5551 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra5551.FromLa16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void From( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + PixelOperations.Instance.ToLa16(configuration, source, destination.Slice(0, source.Length)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/La16.PixelOperations.Generated.tt b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/La16.PixelOperations.Generated.tt new file mode 100644 index 0000000..ff5b96a --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/La16.PixelOperations.Generated.tt @@ -0,0 +1,17 @@ +<#@include file="_Common.ttinclude" #> +<#@ output extension=".cs" #> +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct La16 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + <# GenerateAllDefaultConversionMethods("La16"); #> + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/La32.PixelOperations.Generated.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/La32.PixelOperations.Generated.cs new file mode 100644 index 0000000..43f18ad --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/La32.PixelOperations.Generated.cs @@ -0,0 +1,267 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct La32 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + /// + public override void FromLa32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToLa32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToArgb32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Argb32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Argb32.FromLa32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToAbgr32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Abgr32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Abgr32.FromLa32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgr24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgr24 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgr24.FromLa32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra32.FromLa32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL8( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La32 sourceBase = ref MemoryMarshal.GetReference(source); + ref L8 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L8.FromLa32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La32 sourceBase = ref MemoryMarshal.GetReference(source); + ref L16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L16.FromLa32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La32 sourceBase = ref MemoryMarshal.GetReference(source); + ref La16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La16.FromLa32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb24 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb24.FromLa32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba32.FromLa32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb48( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb48 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb48.FromLa32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba64( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba64 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba64.FromLa32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra5551( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra5551 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra5551.FromLa32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void From( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + PixelOperations.Instance.ToLa32(configuration, source, destination.Slice(0, source.Length)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/La32.PixelOperations.Generated.tt b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/La32.PixelOperations.Generated.tt new file mode 100644 index 0000000..5af0015 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/La32.PixelOperations.Generated.tt @@ -0,0 +1,17 @@ +<#@include file="_Common.ttinclude" #> +<#@ output extension=".cs" #> +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct La32 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + <# GenerateAllDefaultConversionMethods("La32"); #> + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb24.PixelOperations.Generated.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb24.PixelOperations.Generated.cs new file mode 100644 index 0000000..490930f --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb24.PixelOperations.Generated.cs @@ -0,0 +1,337 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Rgb24 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + /// + public override void FromRgb24(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToRgb24(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void FromVector4Destructive( + Configuration configuration, + Span sourceVectors, + Span destination, + PixelConversionModifiers modifiers) + { + Vector4Converters.RgbaCompatible.FromVector4(configuration, this, sourceVectors, destination, modifiers.Remove(PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply)); + } + + /// + public override void ToVector4( + Configuration configuration, + ReadOnlySpan source, + Span destination, + PixelConversionModifiers modifiers) + { + Vector4Converters.RgbaCompatible.ToVector4(configuration, this, source, destination, modifiers.Remove(PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply)); + } + + /// + public override void ToRgba32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgb24.ToRgba32(sourceBytes, destinationBytes); + } + + /// + public override void FromRgba32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgba32.ToRgb24(sourceBytes, destinationBytes); + } + + /// + public override void ToArgb32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgb24.ToArgb32(sourceBytes, destinationBytes); + } + + /// + public override void FromArgb32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromArgb32.ToRgb24(sourceBytes, destinationBytes); + } + + /// + public override void ToAbgr32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgb24.ToAbgr32(sourceBytes, destinationBytes); + } + + /// + public override void FromAbgr32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromAbgr32.ToRgb24(sourceBytes, destinationBytes); + } + + /// + public override void ToBgra32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgb24.ToBgra32(sourceBytes, destinationBytes); + } + + /// + public override void FromBgra32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgra32.ToRgb24(sourceBytes, destinationBytes); + } + + /// + public override void ToBgr24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgb24.ToBgr24(sourceBytes, destinationBytes); + } + + /// + public override void FromBgr24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgr24.ToRgb24(sourceBytes, destinationBytes); + } + + /// + public override void ToL8( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb24 sourceBase = ref MemoryMarshal.GetReference(source); + ref L8 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L8.FromRgb24(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb24 sourceBase = ref MemoryMarshal.GetReference(source); + ref L16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L16.FromRgb24(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb24 sourceBase = ref MemoryMarshal.GetReference(source); + ref La16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La16.FromRgb24(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb24 sourceBase = ref MemoryMarshal.GetReference(source); + ref La32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La32.FromRgb24(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb48( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb24 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb48 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb48.FromRgb24(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba64( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb24 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba64 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba64.FromRgb24(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra5551( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb24 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra5551 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra5551.FromRgb24(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void From( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + PixelOperations.Instance.ToRgb24(configuration, source, destination.Slice(0, source.Length)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb24.PixelOperations.Generated.tt b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb24.PixelOperations.Generated.tt new file mode 100644 index 0000000..b38d781 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb24.PixelOperations.Generated.tt @@ -0,0 +1,17 @@ +<#@include file="_Common.ttinclude" #> +<#@ output extension=".cs" #> +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Rgb24 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + <# GenerateAllDefaultConversionMethods("Rgb24"); #> + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb48.PixelOperations.Generated.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb48.PixelOperations.Generated.cs new file mode 100644 index 0000000..f142c5a --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb48.PixelOperations.Generated.cs @@ -0,0 +1,267 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Rgb48 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + /// + public override void FromRgb48(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToRgb48(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToArgb32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb48 sourceBase = ref MemoryMarshal.GetReference(source); + ref Argb32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Argb32.FromRgb48(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToAbgr32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb48 sourceBase = ref MemoryMarshal.GetReference(source); + ref Abgr32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Abgr32.FromRgb48(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgr24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb48 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgr24 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgr24.FromRgb48(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb48 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra32.FromRgb48(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL8( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb48 sourceBase = ref MemoryMarshal.GetReference(source); + ref L8 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L8.FromRgb48(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb48 sourceBase = ref MemoryMarshal.GetReference(source); + ref L16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L16.FromRgb48(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb48 sourceBase = ref MemoryMarshal.GetReference(source); + ref La16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La16.FromRgb48(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb48 sourceBase = ref MemoryMarshal.GetReference(source); + ref La32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La32.FromRgb48(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb48 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb24 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb24.FromRgb48(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb48 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba32.FromRgb48(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba64( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb48 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba64 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba64.FromRgb48(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra5551( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb48 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra5551 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra5551.FromRgb48(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void From( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + PixelOperations.Instance.ToRgb48(configuration, source, destination.Slice(0, source.Length)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb48.PixelOperations.Generated.tt b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb48.PixelOperations.Generated.tt new file mode 100644 index 0000000..18b70d9 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb48.PixelOperations.Generated.tt @@ -0,0 +1,17 @@ +<#@include file="_Common.ttinclude" #> +<#@ output extension=".cs" #> +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Rgb48 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + <# GenerateAllDefaultConversionMethods("Rgb48"); #> + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgba32.PixelOperations.Generated.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgba32.PixelOperations.Generated.cs new file mode 100644 index 0000000..9bb5281 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgba32.PixelOperations.Generated.cs @@ -0,0 +1,317 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Rgba32 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + /// + public override void FromRgba32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToRgba32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToArgb32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgba32.ToArgb32(sourceBytes, destinationBytes); + } + + /// + public override void FromArgb32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromArgb32.ToRgba32(sourceBytes, destinationBytes); + } + + /// + public override void ToAbgr32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgba32.ToAbgr32(sourceBytes, destinationBytes); + } + + /// + public override void FromAbgr32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromAbgr32.ToRgba32(sourceBytes, destinationBytes); + } + + /// + public override void ToBgra32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgba32.ToBgra32(sourceBytes, destinationBytes); + } + + /// + public override void FromBgra32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgra32.ToRgba32(sourceBytes, destinationBytes); + } + + /// + public override void ToRgb24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgba32.ToRgb24(sourceBytes, destinationBytes); + } + + /// + public override void FromRgb24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgb24.ToRgba32(sourceBytes, destinationBytes); + } + + /// + public override void ToBgr24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromRgba32.ToBgr24(sourceBytes, destinationBytes); + } + + /// + public override void FromBgr24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + PixelConverter.FromBgr24.ToRgba32(sourceBytes, destinationBytes); + } + + /// + public override void ToL8( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba32 sourceBase = ref MemoryMarshal.GetReference(source); + ref L8 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L8.FromRgba32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba32 sourceBase = ref MemoryMarshal.GetReference(source); + ref L16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L16.FromRgba32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba32 sourceBase = ref MemoryMarshal.GetReference(source); + ref La16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La16.FromRgba32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba32 sourceBase = ref MemoryMarshal.GetReference(source); + ref La32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La32.FromRgba32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb48( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb48 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb48.FromRgba32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba64( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba64 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba64.FromRgba32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra5551( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba32 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra5551 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra5551.FromRgba32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void From( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + PixelOperations.Instance.ToRgba32(configuration, source, destination.Slice(0, source.Length)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgba32.PixelOperations.Generated.tt b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgba32.PixelOperations.Generated.tt new file mode 100644 index 0000000..ec0509a --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgba32.PixelOperations.Generated.tt @@ -0,0 +1,17 @@ +<#@include file="_Common.ttinclude" #> +<#@ output extension=".cs" #> +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Rgba32 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + <# GenerateAllDefaultConversionMethods("Rgba32"); #> + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgba64.PixelOperations.Generated.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgba64.PixelOperations.Generated.cs new file mode 100644 index 0000000..cef3bba --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgba64.PixelOperations.Generated.cs @@ -0,0 +1,267 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Rgba64 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + /// + public override void FromRgba64(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToRgba64(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void ToArgb32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba64 sourceBase = ref MemoryMarshal.GetReference(source); + ref Argb32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Argb32.FromRgba64(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToAbgr32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba64 sourceBase = ref MemoryMarshal.GetReference(source); + ref Abgr32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Abgr32.FromRgba64(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgr24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba64 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgr24 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgr24.FromRgba64(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba64 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra32.FromRgba64(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL8( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba64 sourceBase = ref MemoryMarshal.GetReference(source); + ref L8 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L8.FromRgba64(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToL16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba64 sourceBase = ref MemoryMarshal.GetReference(source); + ref L16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L16.FromRgba64(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa16( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba64 sourceBase = ref MemoryMarshal.GetReference(source); + ref La16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La16.FromRgba64(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToLa32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba64 sourceBase = ref MemoryMarshal.GetReference(source); + ref La32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La32.FromRgba64(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb24( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba64 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb24 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb24.FromRgba64(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgba32( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba64 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba32.FromRgba64(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToRgb48( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba64 sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb48 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb48.FromRgba64(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void ToBgra5551( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba64 sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra5551 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra5551.FromRgba64(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + public override void From( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + PixelOperations.Instance.ToRgba64(configuration, source, destination.Slice(0, source.Length)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgba64.PixelOperations.Generated.tt b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgba64.PixelOperations.Generated.tt new file mode 100644 index 0000000..540eb6a --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgba64.PixelOperations.Generated.tt @@ -0,0 +1,17 @@ +<#@include file="_Common.ttinclude" #> +<#@ output extension=".cs" #> +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Rgba64 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + <# GenerateAllDefaultConversionMethods("Rgba64"); #> + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/_Common.ttinclude b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/_Common.ttinclude new file mode 100644 index 0000000..bf0bc58 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/_Common.ttinclude @@ -0,0 +1,208 @@ +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; +<#+ + private static readonly string[] CommonPixelTypes = + [ + "Argb32", + "Abgr32", + "Bgr24", + "Bgra32", + "L8", + "L16", + "La16", + "La32", + "Rgb24", + "Rgba32", + "Rgb48", + "Rgba64", + "Bgra5551" + ]; + + private static readonly string[] OptimizedPixelTypes = + [ + "Rgba32", + "Argb32", + "Abgr32", + "Bgra32", + "Rgb24", + "Bgr24" + ]; + + // Types with Rgba32-combatable to/from Vector4 conversion + private static readonly string[] Rgba32CompatibleTypes = + [ + "Argb32", + "Abgr32", + "Bgra32", + "Rgb24", + "Bgr24" + ]; + + void GenerateGenericConverterMethods(string pixelType) + { +#> + + /// + public override void From( + Configuration configuration, + ReadOnlySpan source, + Span<<#=pixelType#>> destination) + { + PixelOperations.Instance.To<#=pixelType#>(configuration, source, destination.Slice(0, source.Length)); + } +<#+ + } + + void GenerateDefaultSelfConversionMethods(string pixelType) + { +#>/// + public override void From<#=pixelType#>(Configuration configuration, ReadOnlySpan<<#=pixelType#>> source, Span<<#=pixelType#>> destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } + + /// + public override void To<#=pixelType#>(Configuration configuration, ReadOnlySpan<<#=pixelType#>> source, Span<<#=pixelType#>> destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + source.CopyTo(destination.Slice(0, source.Length)); + } +<#+ + } + + void GenerateDefaultConvertToMethod(string fromPixelType, string toPixelType) + { +#> + + /// + public override void To<#=toPixelType#>( + Configuration configuration, + ReadOnlySpan<<#=fromPixelType#>> source, + Span<<#=toPixelType#>> destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref <#=fromPixelType#> sourceBase = ref MemoryMarshal.GetReference(source); + ref <#=toPixelType#> destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = <#=toPixelType#>.From<#=fromPixelType#>(Unsafe.Add(ref sourceBase, i)); + } + } +<#+ + } + + void GenerateOptimized32BitConversionMethods(string thisPixelType, string otherPixelType) + { + #> + + /// + public override void To<#=otherPixelType#>( + Configuration configuration, + ReadOnlySpan<<#=thisPixelType#>> source, + Span<<#=otherPixelType#>> destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast<<#=thisPixelType#>, byte>(source); + Span destinationBytes = MemoryMarshal.Cast<<#=otherPixelType#>, byte>(destination); + PixelConverter.From<#=thisPixelType#>.To<#=otherPixelType#>(sourceBytes, destinationBytes); + } + + /// + public override void From<#=otherPixelType#>( + Configuration configuration, + ReadOnlySpan<<#=otherPixelType#>> source, + Span<<#=thisPixelType#>> destination) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ReadOnlySpan sourceBytes = MemoryMarshal.Cast<<#=otherPixelType#>, byte>(source); + Span destinationBytes = MemoryMarshal.Cast<<#=thisPixelType#>, byte>(destination); + PixelConverter.From<#=otherPixelType#>.To<#=thisPixelType#>(sourceBytes, destinationBytes); + } +<#+ + } + + void GenerateRgba32CompatibleVector4ConversionMethods(string pixelType, bool hasAlpha) + { + string removeTheseModifiers = "PixelConversionModifiers.Scale"; + if (!hasAlpha) + { + removeTheseModifiers += " | PixelConversionModifiers.Premultiply"; + } +#> + + /// + public override void FromVector4Destructive( + Configuration configuration, + Span sourceVectors, + Span<<#=pixelType#>> destination, + PixelConversionModifiers modifiers) + { + Vector4Converters.RgbaCompatible.FromVector4(configuration, this, sourceVectors, destination, modifiers.Remove(<#=removeTheseModifiers#>)); + } + + /// + public override void ToVector4( + Configuration configuration, + ReadOnlySpan<<#=pixelType#>> source, + Span destination, + PixelConversionModifiers modifiers) + { + Vector4Converters.RgbaCompatible.ToVector4(configuration, this, source, destination, modifiers.Remove(<#=removeTheseModifiers#>)); + } +<#+ + } + + void GenerateAllDefaultConversionMethods(string pixelType) + { + GenerateDefaultSelfConversionMethods(pixelType); + + if (Rgba32CompatibleTypes.Contains(pixelType)) + { + GenerateRgba32CompatibleVector4ConversionMethods(pixelType, pixelType.EndsWith("32")); + } + + var matching32BitTypes = OptimizedPixelTypes.Contains(pixelType) ? + OptimizedPixelTypes.Where(p => p != pixelType) : + []; + + foreach (string destPixelType in matching32BitTypes) + { + GenerateOptimized32BitConversionMethods(pixelType, destPixelType); + } + + var otherCommonNon32Types = CommonPixelTypes + .Where(p => p != pixelType) + .Except(matching32BitTypes); + + foreach (string destPixelType in otherCommonNon32Types) + { + GenerateDefaultConvertToMethod(pixelType, destPixelType); + } + + GenerateGenericConverterMethods(pixelType); + } +#> diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfSingle.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfSingle.PixelOperations.cs new file mode 100644 index 0000000..a84a5dd --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfSingle.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct HalfSingle + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector2.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector2.PixelOperations.cs new file mode 100644 index 0000000..fd88263 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector2.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct HalfVector2 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs new file mode 100644 index 0000000..6daf5e3 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct HalfVector4 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L16.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L16.PixelOperations.cs new file mode 100644 index 0000000..e4092e3 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L16.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct L16 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L8.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L8.PixelOperations.cs new file mode 100644 index 0000000..9ee6f8d --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L8.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct L8 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/La16.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/La16.PixelOperations.cs new file mode 100644 index 0000000..dce97ab --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/La16.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct La16 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/La32.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/La32.PixelOperations.cs new file mode 100644 index 0000000..2f12b42 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/La32.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct La32 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte2.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte2.PixelOperations.cs new file mode 100644 index 0000000..165ae51 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte2.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct NormalizedByte2 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4.PixelOperations.cs new file mode 100644 index 0000000..187e755 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct NormalizedByte4 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedShort2.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedShort2.PixelOperations.cs new file mode 100644 index 0000000..74f2178 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedShort2.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct NormalizedShort2 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedShort4.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedShort4.PixelOperations.cs new file mode 100644 index 0000000..209404f --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedShort4.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct NormalizedShort4 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rg32.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rg32.PixelOperations.cs new file mode 100644 index 0000000..0ccf75f --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rg32.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct Rg32 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgb24.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgb24.PixelOperations.cs new file mode 100644 index 0000000..3e8514c --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgb24.PixelOperations.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct Rgb24 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations + { + /// + internal override void PackFromRgbPlanes( + ReadOnlySpan redChannel, + ReadOnlySpan greenChannel, + ReadOnlySpan blueChannel, + Span destination) + { + int count = redChannel.Length; + GuardPackFromRgbPlanes(greenChannel, blueChannel, destination, count); + + SimdUtils.PackFromRgbPlanes(redChannel, greenChannel, blueChannel, destination); + } + + /// + internal override void UnpackIntoRgbPlanes( + Span redChannel, + Span greenChannel, + Span blueChannel, + ReadOnlySpan source) + { + GuardUnpackIntoRgbPlanes(redChannel, greenChannel, blueChannel, source); + SimdUtils.UnpackToRgbPlanes(redChannel, greenChannel, blueChannel, source); + } + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgb48.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgb48.PixelOperations.cs new file mode 100644 index 0000000..f91479a --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgb48.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct Rgb48 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgba1010102.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgba1010102.PixelOperations.cs new file mode 100644 index 0000000..82f0338 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgba1010102.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct Rgba1010102 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgba32.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgba32.PixelOperations.cs new file mode 100644 index 0000000..97dd542 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgba32.PixelOperations.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct Rgba32 + { + /// + /// implementation optimized for . + /// + internal partial class PixelOperations : PixelOperations + { + /// + public override void ToVector4( + Configuration configuration, + ReadOnlySpan source, + Span destinationVectors, + PixelConversionModifiers modifiers) + { + Guard.DestinationShouldNotBeTooShort(source, destinationVectors, nameof(destinationVectors)); + + destinationVectors = destinationVectors[..source.Length]; + SimdUtils.ByteToNormalizedFloat( + MemoryMarshal.Cast(source), + MemoryMarshal.Cast(destinationVectors)); + Vector4Converters.ApplyForwardConversionModifiers(destinationVectors, modifiers); + } + + /// + public override void FromVector4Destructive( + Configuration configuration, + Span sourceVectors, + Span destination, + PixelConversionModifiers modifiers) + { + Guard.DestinationShouldNotBeTooShort(sourceVectors, destination, nameof(destination)); + + destination = destination[..sourceVectors.Length]; + Vector4Converters.ApplyBackwardConversionModifiers(sourceVectors, modifiers); + SimdUtils.NormalizedFloatToByteSaturate( + MemoryMarshal.Cast(sourceVectors), + MemoryMarshal.Cast(destination)); + } + + /// + internal override void PackFromRgbPlanes( + ReadOnlySpan redChannel, + ReadOnlySpan greenChannel, + ReadOnlySpan blueChannel, + Span destination) + { + int count = redChannel.Length; + GuardPackFromRgbPlanes(greenChannel, blueChannel, destination, count); + + SimdUtils.PackFromRgbPlanes(redChannel, greenChannel, blueChannel, destination); + } + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgba64.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgba64.PixelOperations.cs new file mode 100644 index 0000000..3bcefaf --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgba64.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct Rgba64 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal partial class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVector.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVector.PixelOperations.cs new file mode 100644 index 0000000..be4ca04 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVector.PixelOperations.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct RgbaVector + { + /// + /// implementation optimized for . + /// + internal class PixelOperations : PixelOperations + { + /// + public override void From( + Configuration configuration, + ReadOnlySpan sourcePixels, + Span destinationPixels) + { + Span destinationVectors = MemoryMarshal.Cast(destinationPixels); + + PixelOperations.Instance.ToVector4(configuration, sourcePixels, destinationVectors, PixelConversionModifiers.Scale); + } + + /// + public override void FromVector4Destructive( + Configuration configuration, + Span sourceVectors, + Span destinationPixels, + PixelConversionModifiers modifiers) + { + Guard.DestinationShouldNotBeTooShort(sourceVectors, destinationPixels, nameof(destinationPixels)); + + Vector4Converters.ApplyBackwardConversionModifiers(sourceVectors, modifiers); + MemoryMarshal.Cast(sourceVectors).CopyTo(destinationPixels); + } + + /// + public override void ToVector4( + Configuration configuration, + ReadOnlySpan sourcePixels, + Span destinationVectors, + PixelConversionModifiers modifiers) + { + Guard.DestinationShouldNotBeTooShort(sourcePixels, destinationVectors, nameof(destinationVectors)); + + MemoryMarshal.Cast(sourcePixels).CopyTo(destinationVectors); + Vector4Converters.ApplyForwardConversionModifiers(destinationVectors, modifiers); + } + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short2.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short2.PixelOperations.cs new file mode 100644 index 0000000..5bd916f --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short2.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct Short2 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short4.PixelOperations.cs b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short4.PixelOperations.cs new file mode 100644 index 0000000..dbc4709 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short4.PixelOperations.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides optimized overrides for bulk operations. + /// + public partial struct Short4 + { + /// + /// Provides optimized overrides for bulk operations. + /// + internal class PixelOperations : PixelOperations; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Rg32.cs b/ImageSharp/PixelFormats/PixelImplementations/Rg32.cs new file mode 100644 index 0000000..de31f83 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Rg32.cs @@ -0,0 +1,178 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing two 16-bit unsigned normalized values ranging from 0 to 1. + /// + /// Ranges from [0, 0, 0, 1] to [1, 1, 0, 1] in vector form. + /// + /// + public partial struct Rg32 : IPixel, IPackedVector + { + private static readonly Vector2 Max = new(ushort.MaxValue); + + /// + /// Initializes a new instance of the struct. + /// + /// The x-component + /// The y-component + public Rg32(float x, float y) + : this(new Vector2(x, y)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector containing the component values. + public Rg32(Vector2 vector) => this.PackedValue = Pack(vector); + + /// + public uint PackedValue { get; set; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Rg32 left, Rg32 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Rg32 left, Rg32 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() + => new( + ColorNumerics.From16BitTo8Bit((ushort)(this.PackedValue & 0xFFFF)), + ColorNumerics.From16BitTo8Bit((ushort)(this.PackedValue >> 16)), + byte.MinValue, + byte.MaxValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new(this.ToVector2(), 0f, 1f); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(2, 16, 16), + PixelColorType.Red | PixelColorType.Green, + PixelAlphaRepresentation.None); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromVector4(Vector4 source) => new() { PackedValue = Pack(new Vector2(source.X, source.Y)) }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromAbgr32(Abgr32 source) => new(ColorNumerics.From8BitTo16Bit(source.R), ColorNumerics.From8BitTo16Bit(source.G)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromArgb32(Argb32 source) => new(ColorNumerics.From8BitTo16Bit(source.R), ColorNumerics.From8BitTo16Bit(source.G)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromBgr24(Bgr24 source) => new(ColorNumerics.From8BitTo16Bit(source.R), ColorNumerics.From8BitTo16Bit(source.G)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromBgra32(Bgra32 source) => new(ColorNumerics.From8BitTo16Bit(source.R), ColorNumerics.From8BitTo16Bit(source.G)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromL8(L8 source) => new(ColorNumerics.From8BitTo16Bit(source.PackedValue), ColorNumerics.From8BitTo16Bit(source.PackedValue)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromL16(L16 source) => new(source.PackedValue, source.PackedValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromLa16(La16 source) => new(ColorNumerics.From8BitTo16Bit(source.L), ColorNumerics.From8BitTo16Bit(source.L)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromLa32(La32 source) => new(source.L, source.L); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromRgb24(Rgb24 source) => new(ColorNumerics.From8BitTo16Bit(source.R), ColorNumerics.From8BitTo16Bit(source.G)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromRgba32(Rgba32 source) => new(ColorNumerics.From8BitTo16Bit(source.R), ColorNumerics.From8BitTo16Bit(source.G)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromRgb48(Rgb48 source) => new(source.R, source.G); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromRgba64(Rgba64 source) => new(source.R, source.G); + + /// + /// Expands the packed representation into a . + /// The vector components are typically expanded in least to greatest significance order. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector2 ToVector2() => new Vector2(this.PackedValue & 0xFFFF, (this.PackedValue >> 16) & 0xFFFF) / Max; + + /// + public override readonly bool Equals(object? obj) => obj is Rg32 other && this.Equals(other); + + /// + public readonly bool Equals(Rg32 other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly string ToString() + { + Vector2 vector = this.ToVector2(); + return FormattableString.Invariant($"Rg32({vector.X:#0.##}, {vector.Y:#0.##})"); + } + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Pack(Vector2 vector) + { + vector = Vector2.Clamp(vector, Vector2.Zero, Vector2.One) * Max; + return (uint)(((int)Math.Round(vector.X) & 0xFFFF) | (((int)Math.Round(vector.Y) & 0xFFFF) << 16)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Rgb24.cs b/ImageSharp/PixelFormats/PixelImplementations/Rgb24.cs new file mode 100644 index 0000000..0965dcb --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Rgb24.cs @@ -0,0 +1,211 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Pixel type containing three 8-bit unsigned normalized values ranging from 0 to 255. + /// The color components are stored in red, green, blue order (least significant to most significant byte). + /// + /// Ranges from [0, 0, 0, 1] to [1, 1, 1, 1] in vector form. + /// + /// + [StructLayout(LayoutKind.Explicit)] + public partial struct Rgb24 : IPixel + { + /// + /// The red component. + /// + [FieldOffset(0)] + public byte R; + + /// + /// The green component. + /// + [FieldOffset(1)] + public byte G; + + /// + /// The blue component. + /// + [FieldOffset(2)] + public byte B; + + private static readonly Vector4 MaxBytes = Vector128.Create(255f).AsVector4(); + private static readonly Vector4 Half = Vector128.Create(.5f).AsVector4(); + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgb24(byte r, byte g, byte b) + { + this.R = r; + this.G = g; + this.B = b; + } + + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// The instance of to convert. + /// An instance of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Rgb24(Rgb color) + => FromScaledVector4(new Vector4(color.ToScaledVector3(), 1F)); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Rgb24 left, Rgb24 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Rgb24 left, Rgb24 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromRgb24(this); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new Rgba32(this.R, this.G, this.B, byte.MaxValue).ToVector4(); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(3, 8, 8, 8), + PixelColorType.RGB, + PixelAlphaRepresentation.None); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromVector4(Vector4 source) + { + source *= MaxBytes; + source += Half; + source = Numerics.Clamp(source, Vector4.Zero, MaxBytes); + + Vector128 result = Vector128.ConvertToInt32(source.AsVector128()).AsByte(); + return new Rgb24(result.GetElement(0), result.GetElement(4), result.GetElement(8)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromAbgr32(Abgr32 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromArgb32(Argb32 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromBgr24(Bgr24 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromBgra32(Bgra32 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromL8(L8 source) => new(source.PackedValue, source.PackedValue, source.PackedValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromL16(L16 source) + { + byte rgb = ColorNumerics.From16BitTo8Bit(source.PackedValue); + return new Rgb24(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromLa16(La16 source) => new(source.L, source.L, source.L); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromLa32(La32 source) + { + byte rgb = ColorNumerics.From16BitTo8Bit(source.L); + return new Rgb24(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromRgb24(Rgb24 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromRgba32(Rgba32 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromRgb48(Rgb48 source) + => new() + { + R = ColorNumerics.From16BitTo8Bit(source.R), + G = ColorNumerics.From16BitTo8Bit(source.G), + B = ColorNumerics.From16BitTo8Bit(source.B) + }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromRgba64(Rgba64 source) + => new() + { + R = ColorNumerics.From16BitTo8Bit(source.R), + G = ColorNumerics.From16BitTo8Bit(source.G), + B = ColorNumerics.From16BitTo8Bit(source.B) + }; + + /// + public override readonly bool Equals(object? obj) => obj is Rgb24 other && this.Equals(other); + + /// + public readonly bool Equals(Rgb24 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B); + + /// + public override readonly int GetHashCode() => HashCode.Combine(this.R, this.B, this.G); + + /// + public override readonly string ToString() => $"Rgb24({this.R}, {this.G}, {this.B})"; + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Rgb48.cs b/ImageSharp/PixelFormats/PixelImplementations/Rgb48.cs new file mode 100644 index 0000000..4eaab07 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Rgb48.cs @@ -0,0 +1,184 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing three 16-bit unsigned normalized values ranging from 0 to 65535. + /// + /// Ranges from [0, 0, 0, 1] to [1, 1, 1, 1] in vector form. + /// + /// + [StructLayout(LayoutKind.Sequential)] + public partial struct Rgb48 : IPixel + { + private const float Max = ushort.MaxValue; + + /// + /// Gets or sets the red component. + /// + public ushort R; + + /// + /// Gets or sets the green component. + /// + public ushort G; + + /// + /// Gets or sets the blue component. + /// + public ushort B; + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + public Rgb48(ushort r, ushort g, ushort b) + : this() + { + this.R = r; + this.G = g; + this.B = b; + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Rgb48 left, Rgb48 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Rgb48 left, Rgb48 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromRgb48(this); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new(this.R / Max, this.G / Max, this.B / Max, 1f); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(3, 16, 16, 16), + PixelColorType.RGB, + PixelAlphaRepresentation.None); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromVector4(Vector4 source) + { + source = Numerics.Clamp(source, Vector4.Zero, Vector4.One) * Max; + return new Rgb48((ushort)MathF.Round(source.X), (ushort)MathF.Round(source.Y), (ushort)MathF.Round(source.Z)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromAbgr32(Abgr32 source) + => new(ColorNumerics.From8BitTo16Bit(source.R), ColorNumerics.From8BitTo16Bit(source.G), ColorNumerics.From8BitTo16Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromArgb32(Argb32 source) + => new(ColorNumerics.From8BitTo16Bit(source.R), ColorNumerics.From8BitTo16Bit(source.G), ColorNumerics.From8BitTo16Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromBgr24(Bgr24 source) + => new(ColorNumerics.From8BitTo16Bit(source.R), ColorNumerics.From8BitTo16Bit(source.G), ColorNumerics.From8BitTo16Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromBgra32(Bgra32 source) + => new(ColorNumerics.From8BitTo16Bit(source.R), ColorNumerics.From8BitTo16Bit(source.G), ColorNumerics.From8BitTo16Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromL8(L8 source) + { + ushort rgb = ColorNumerics.From8BitTo16Bit(source.PackedValue); + return new Rgb48(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromL16(L16 source) => new(source.PackedValue, source.PackedValue, source.PackedValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromLa16(La16 source) + { + ushort rgb = ColorNumerics.From8BitTo16Bit(source.L); + return new Rgb48(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromLa32(La32 source) => new(source.L, source.L, source.L); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromRgb24(Rgb24 source) + => new(ColorNumerics.From8BitTo16Bit(source.R), ColorNumerics.From8BitTo16Bit(source.G), ColorNumerics.From8BitTo16Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromRgba32(Rgba32 source) + => new(ColorNumerics.From8BitTo16Bit(source.R), ColorNumerics.From8BitTo16Bit(source.G), ColorNumerics.From8BitTo16Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromRgb48(Rgb48 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromRgba64(Rgba64 source) => new(source.R, source.G, source.B); + + /// + public override readonly bool Equals(object? obj) => obj is Rgb48 rgb48 && this.Equals(rgb48); + + /// + public readonly bool Equals(Rgb48 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B); + + /// + public override readonly string ToString() => $"Rgb48({this.R}, {this.G}, {this.B})"; + + /// + public override readonly int GetHashCode() => HashCode.Combine(this.R, this.G, this.B); + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs b/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs new file mode 100644 index 0000000..13a2302 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs @@ -0,0 +1,204 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Pixel type containing three 32-bit unsigned normalized values ranging from 0 to 4294967295. + /// The color components are stored in red, green, blue. + /// + /// Ranges from [0, 0, 0] to [1, 1, 1] in vector form. + /// + /// + [StructLayout(LayoutKind.Sequential)] + public partial struct Rgb96 : IPixel, IEquatable + { + private const float InvMax = 1.0f / uint.MaxValue; + + // Use double here because at this magnitude a float cannot represent all 32-bit + // integer values exactly. A float only has 24 bits of precision, so around + // uint.MaxValue it can only represent multiples of 256 and will round + // 4294967295 up to 4294967296. Double has 53 bits of precision and can + // represent all uint values exactly, avoiding precision loss before scaling. + private const double Max = uint.MaxValue; + + /// + /// Gets the red component. + /// + public uint R; + + /// + /// Gets the green component. + /// + public uint G; + + /// + /// Gets the blue component. + /// + public uint B; + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgb96(uint r, uint g, uint b) + { + this.R = r; + this.G = g; + this.B = b; + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + /// The on the right side of the operand. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Rgb96 left, Rgb96 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Rgb96 left, Rgb96 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new( + this.R * InvMax, + this.G * InvMax, + this.B * InvMax, + 1.0f); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PixelOperations CreatePixelOperations() => new(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromVector4(Vector4 source) + { + source = Numerics.Clamp(source, Vector4.Zero, Vector4.One); + return new Rgb96( + (uint)Math.Round(source.X * Max), + (uint)Math.Round(source.Y * Max), + (uint)Math.Round(source.Z * Max)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromAbgr32(Abgr32 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromArgb32(Argb32 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromBgr24(Bgr24 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromBgra32(Bgra32 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromL8(L8 source) + { + uint rgb = ColorNumerics.From8BitTo32Bit(source.PackedValue); + return new Rgb96(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromL16(L16 source) + { + uint rgb = ColorNumerics.From16BitTo32Bit(source.PackedValue); + return new(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromLa16(La16 source) + { + uint rgb = ColorNumerics.From8BitTo32Bit((byte)source.PackedValue); + return new(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromLa32(La32 source) + { + uint rgb = ColorNumerics.From16BitTo32Bit(source.L); + return new(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromRgb24(Rgb24 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromRgba32(Rgba32 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromRgb48(Rgb48 source) => new(ColorNumerics.From16BitTo32Bit(source.R), ColorNumerics.From16BitTo32Bit(source.G), ColorNumerics.From16BitTo32Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromRgba64(Rgba64 source) => new(ColorNumerics.From16BitTo32Bit(source.R), ColorNumerics.From16BitTo32Bit(source.G), ColorNumerics.From16BitTo32Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PixelTypeInfo GetPixelTypeInfo() => PixelTypeInfo.Create( + PixelComponentInfo.Create(3, 32, 32, 32), + PixelColorType.RGB, + PixelAlphaRepresentation.None); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromRgb96(this); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override readonly int GetHashCode() => HashCode.Combine(this.R, this.G, this.B); + + /// + public override readonly string ToString() => FormattableString.Invariant($"Rgb96({this.R}, {this.G}, {this.B})"); + + /// + public override readonly bool Equals(object? obj) => obj is Rgb96 rgb && rgb.R == this.R && rgb.G == this.G && rgb.B == this.B; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Equals(Rgb96 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B); + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Rgba1010102.cs b/ImageSharp/PixelFormats/PixelImplementations/Rgba1010102.cs new file mode 100644 index 0000000..dad1dd6 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Rgba1010102.cs @@ -0,0 +1,177 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed vector type containing 4 unsigned normalized values ranging from 0 to 1. + /// The x, y and z components use 10 bits, and the w component uses 2 bits. + /// + /// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. + /// + /// + public partial struct Rgba1010102 : IPixel, IPackedVector + { + private static readonly Vector4 Multiplier = new(1023F, 1023F, 1023F, 3F); + + /// + /// Initializes a new instance of the struct. + /// + /// The x-component + /// The y-component + /// The z-component + /// The w-component + public Rgba1010102(float x, float y, float z, float w) + : this(new Vector4(x, y, z, w)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector containing the component values. + public Rgba1010102(Vector4 vector) => this.PackedValue = Pack(vector); + + /// + public uint PackedValue { get; set; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Rgba1010102 left, Rgba1010102 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Rgba1010102 left, Rgba1010102 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new Vector4( + (this.PackedValue >> 0) & 0x03FF, + (this.PackedValue >> 10) & 0x03FF, + (this.PackedValue >> 20) & 0x03FF, + (this.PackedValue >> 30) & 0x03) / Multiplier; + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 10, 10, 10, 2), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromVector4(Vector4 source) => new() { PackedValue = Pack(source) }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromRgba32(Rgba32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is Rgba1010102 other && this.Equals(other); + + /// + public readonly bool Equals(Rgba1010102 other) => this.PackedValue == other.PackedValue; + + /// + public override readonly string ToString() + { + Vector4 vector = this.ToVector4(); + return FormattableString.Invariant($"Rgba1010102({vector.X:#0.##}, {vector.Y:#0.##}, {vector.Z:#0.##}, {vector.W:#0.##})"); + } + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Pack(Vector4 vector) + { + vector = Numerics.Clamp(vector, Vector4.Zero, Vector4.One) * Multiplier; + + return (uint)( + (((int)Math.Round(vector.X) & 0x03FF) << 0) + | (((int)Math.Round(vector.Y) & 0x03FF) << 10) + | (((int)Math.Round(vector.Z) & 0x03FF) << 20) + | (((int)Math.Round(vector.W) & 0x03) << 30)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs b/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs new file mode 100644 index 0000000..ce09c69 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs @@ -0,0 +1,201 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Pixel type containing four 32-bit unsigned normalized values ranging from 0 to 4294967295. + /// The color components are stored in red, green, blue and alpha. + /// + /// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. + /// + /// + [StructLayout(LayoutKind.Sequential)] + public partial struct Rgba128 : IPixel, IEquatable + { + private const float InvMax = 1.0f / uint.MaxValue; + + // Use double here because at this magnitude a float cannot represent all 32-bit + // integer values exactly. A float only has 24 bits of precision, so around + // uint.MaxValue it can only represent multiples of 256 and will round + // 4294967295 up to 4294967296. Double has 53 bits of precision and can + // represent all uint values exactly, avoiding precision loss before scaling. + private const double Max = uint.MaxValue; + + /// + /// Gets the red component. + /// + public uint R; + + /// + /// Gets the green component. + /// + public uint G; + + /// + /// Gets the blue component. + /// + public uint B; + + /// + /// Gets the alpha channel. + /// + public uint A; + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The alpha component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba128(uint r, uint g, uint b, uint a) + { + this.R = r; + this.G = g; + this.B = b; + this.A = a; + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + /// The on the right side of the operand. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Rgba128 left, Rgba128 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Rgba128 left, Rgba128 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new( + this.R * InvMax, + this.G * InvMax, + this.B * InvMax, + this.A * InvMax); + + /// + public static PixelOperations CreatePixelOperations() => new(); + + /// + public static Rgba128 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + public static Rgba128 FromVector4(Vector4 source) + { + source = Numerics.Clamp(source, Vector4.Zero, Vector4.One); + return new Rgba128( + (uint)Math.Round(source.X * Max), + (uint)Math.Round(source.Y * Max), + (uint)Math.Round(source.Z * Max), + (uint)Math.Round(source.W * Max)); + } + + /// + public static Rgba128 FromAbgr32(Abgr32 source) + => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), ColorNumerics.From8BitTo32Bit(source.A)); + + /// + public static Rgba128 FromArgb32(Argb32 source) + => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), ColorNumerics.From8BitTo32Bit(source.A)); + + /// + public static Rgba128 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static Rgba128 FromBgr24(Bgr24 source) + => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), uint.MaxValue); + + /// + public static Rgba128 FromBgra32(Bgra32 source) + => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), ColorNumerics.From8BitTo32Bit(source.A)); + + /// + public static Rgba128 FromL8(L8 source) + { + uint rgb = ColorNumerics.From8BitTo32Bit(source.PackedValue); + return new Rgba128(rgb, rgb, rgb, rgb); + } + + /// + public static Rgba128 FromL16(L16 source) + { + uint rgb = ColorNumerics.From16BitTo32Bit(source.PackedValue); + return new(rgb, rgb, rgb, rgb); + } + + /// + public static Rgba128 FromLa16(La16 source) + { + uint rgb = ColorNumerics.From8BitTo32Bit((byte)source.PackedValue); + return new(rgb, rgb, rgb, rgb); + } + + /// + public static Rgba128 FromLa32(La32 source) + { + uint rgb = ColorNumerics.From16BitTo32Bit(source.L); + return new(rgb, rgb, rgb, rgb); + } + + /// + public static Rgba128 FromRgb24(Rgb24 source) + => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), uint.MaxValue); + + /// + public static Rgba128 FromRgba32(Rgba32 source) + => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), ColorNumerics.From8BitTo32Bit(source.A)); + + /// + public static Rgba128 FromRgb48(Rgb48 source) + => new(ColorNumerics.From16BitTo32Bit(source.R), ColorNumerics.From16BitTo32Bit(source.G), ColorNumerics.From16BitTo32Bit(source.B), uint.MaxValue); + + /// + public static Rgba128 FromRgba64(Rgba64 source) + => new(ColorNumerics.From16BitTo32Bit(source.R), ColorNumerics.From16BitTo32Bit(source.G), ColorNumerics.From16BitTo32Bit(source.B), ColorNumerics.From16BitTo32Bit(source.A)); + + /// + public static PixelTypeInfo GetPixelTypeInfo() => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 32, 32, 32, 32), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public readonly Rgba32 ToRgba32() => Rgba32.FromRgba128(this); + + /// + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override readonly int GetHashCode() => HashCode.Combine(this.R, this.G, this.B, this.A); + + /// + public override readonly string ToString() => FormattableString.Invariant($"Rgba128({this.R}, {this.G}, {this.B}, {this.A})"); + + /// + public override readonly bool Equals(object? obj) => obj is Rgba128 rgb && rgb.R == this.R && rgb.G == this.G && rgb.B == this.B && rgb.A == this.A; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Equals(Rgba128 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B) && this.A.Equals(other.A); + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs b/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs new file mode 100644 index 0000000..980f401 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs @@ -0,0 +1,383 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing four 8-bit unsigned normalized values ranging from 0 to 255. + /// The color components are stored in red, green, blue, and alpha order (least significant to most significant byte). + /// + /// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. + /// + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + [StructLayout(LayoutKind.Sequential)] + public partial struct Rgba32 : IPixel, IPackedVector + { + /// + /// Gets or sets the red component. + /// + public byte R; + + /// + /// Gets or sets the green component. + /// + public byte G; + + /// + /// Gets or sets the blue component. + /// + public byte B; + + /// + /// Gets or sets the alpha component. + /// + public byte A; + + private static readonly Vector4 MaxBytes = Vector128.Create(255f).AsVector4(); + private static readonly Vector4 Half = Vector128.Create(.5f).AsVector4(); + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba32(byte r, byte g, byte b) + { + this.R = r; + this.G = g; + this.B = b; + this.A = byte.MaxValue; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The alpha component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba32(byte r, byte g, byte b, byte a) + { + this.R = r; + this.G = g; + this.B = b; + this.A = a; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The alpha component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba32(float r, float g, float b, float a = 1) + : this(new Vector4(r, g, b, a)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The vector containing the components for the packed vector. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba32(Vector3 vector) + : this(new Vector4(vector, 1f)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The vector containing the components for the packed vector. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba32(Vector4 vector) + : this() => this = Pack(vector); + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The packed value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba32(uint packed) + : this() => this.Rgba = packed; + + /// + /// Gets or sets the packed representation of the Rgba32 struct. + /// + public uint Rgba + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => Unsafe.As(ref this) = value; + } + + /// + /// Gets or sets the RGB components of this struct as + /// + public Rgb24 Rgb + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => new(this.R, this.G, this.B); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + this.R = value.R; + this.G = value.G; + this.B = value.B; + } + } + + /// + /// Gets or sets the RGB components of this struct as reverting the component order. + /// + public Bgr24 Bgr + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => new(this.R, this.G, this.B); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + this.R = value.R; + this.G = value.G; + this.B = value.B; + } + } + + /// + public uint PackedValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => this.Rgba; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => this.Rgba = value; + } + + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// The instance of to convert. + /// An instance of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Rgba32(Rgb color) => FromScaledVector4(new Vector4(color.ToScaledVector3(), 1F)); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Rgba32 left, Rgba32 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Rgba32 left, Rgba32 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => this; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new Vector4(this.R, this.G, this.B, this.A) / MaxBytes; + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 8, 8, 8, 8), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromVector4(Vector4 source) => Pack(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromAbgr32(Abgr32 source) => new(source.R, source.G, source.B, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromArgb32(Argb32 source) => new(source.R, source.G, source.B, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromBgr24(Bgr24 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromBgra32(Bgra32 source) => new(source.R, source.G, source.B, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromL8(L8 source) => new(source.PackedValue, source.PackedValue, source.PackedValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromL16(L16 source) + { + byte rgb = ColorNumerics.From16BitTo8Bit(source.PackedValue); + return new Rgba32(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromLa16(La16 source) => new(source.L, source.L, source.L, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromLa32(La32 source) + { + byte rgb = ColorNumerics.From16BitTo8Bit(source.L); + return new Rgba32(rgb, rgb, rgb, ColorNumerics.From16BitTo8Bit(source.A)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromRgb24(Rgb24 source) => new(source.R, source.G, source.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromRgba32(Rgba32 source) => new() { PackedValue = source.PackedValue }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromRgb48(Rgb48 source) + => new() + { + R = ColorNumerics.From16BitTo8Bit(source.R), + G = ColorNumerics.From16BitTo8Bit(source.G), + B = ColorNumerics.From16BitTo8Bit(source.B), + A = byte.MaxValue + }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromRgba64(Rgba64 source) + => new() + { + R = ColorNumerics.From16BitTo8Bit(source.R), + G = ColorNumerics.From16BitTo8Bit(source.G), + B = ColorNumerics.From16BitTo8Bit(source.B), + A = ColorNumerics.From16BitTo8Bit(source.A) + }; + + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + /// The pixel value as Rgba32. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromRgb96(Rgb96 source) + => new() + { + R = ColorNumerics.From32BitTo8Bit(source.R), + G = ColorNumerics.From32BitTo8Bit(source.G), + B = ColorNumerics.From32BitTo8Bit(source.B), + A = byte.MaxValue + }; + + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + /// The pixel value as Rgba32. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromRgba128(Rgba128 source) + => new() + { + R = ColorNumerics.From32BitTo8Bit(source.R), + G = ColorNumerics.From32BitTo8Bit(source.G), + B = ColorNumerics.From32BitTo8Bit(source.B), + A = ColorNumerics.From32BitTo8Bit(source.A), + }; + + /// + /// Converts the value of this instance to a hexadecimal string. + /// + /// A hexadecimal string representation of the value. + public readonly string ToHex() + { + uint hexOrder = (uint)((this.A << 0) | (this.B << 8) | (this.G << 16) | (this.R << 24)); + return hexOrder.ToString("X8", CultureInfo.InvariantCulture); + } + + /// + public override readonly bool Equals(object? obj) => obj is Rgba32 rgba32 && this.Equals(rgba32); + + /// + public readonly bool Equals(Rgba32 other) => this.Rgba.Equals(other.Rgba); + + /// + public override readonly string ToString() => $"Rgba32({this.R}, {this.G}, {this.B}, {this.A})"; + + /// + public override readonly int GetHashCode() => this.Rgba.GetHashCode(); + + /// + /// Packs a into a color. + /// + /// The vector containing the values to pack. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Rgba32 Pack(Vector4 vector) + { + vector *= MaxBytes; + vector += Half; + vector = Numerics.Clamp(vector, Vector4.Zero, MaxBytes); + + Vector128 result = Vector128.ConvertToInt32(vector.AsVector128()).AsByte(); + return new Rgba32(result.GetElement(0), result.GetElement(4), result.GetElement(8), result.GetElement(12)); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Rgba64.cs b/ImageSharp/PixelFormats/PixelImplementations/Rgba64.cs new file mode 100644 index 0000000..51e5d7a --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Rgba64.cs @@ -0,0 +1,330 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing four 16-bit unsigned normalized values ranging from 0 to 65535. + /// + /// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. + /// + /// + [StructLayout(LayoutKind.Sequential)] + public partial struct Rgba64 : IPixel, IPackedVector + { + private const float Max = ushort.MaxValue; + + /// + /// Gets or sets the red component. + /// + public ushort R; + + /// + /// Gets or sets the green component. + /// + public ushort G; + + /// + /// Gets or sets the blue component. + /// + public ushort B; + + /// + /// Gets or sets the alpha component. + /// + public ushort A; + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The alpha component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba64(ushort r, ushort g, ushort b, ushort a) + { + this.R = r; + this.G = g; + this.B = b; + this.A = a; + } + + /// + /// Initializes a new instance of the struct. + /// + /// A structure of 4 bytes in RGBA byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba64(Rgba32 source) + { + this.R = ColorNumerics.From8BitTo16Bit(source.R); + this.G = ColorNumerics.From8BitTo16Bit(source.G); + this.B = ColorNumerics.From8BitTo16Bit(source.B); + this.A = ColorNumerics.From8BitTo16Bit(source.A); + } + + /// + /// Initializes a new instance of the struct. + /// + /// A structure of 4 bytes in BGRA byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba64(Bgra32 source) + { + this.R = ColorNumerics.From8BitTo16Bit(source.R); + this.G = ColorNumerics.From8BitTo16Bit(source.G); + this.B = ColorNumerics.From8BitTo16Bit(source.B); + this.A = ColorNumerics.From8BitTo16Bit(source.A); + } + + /// + /// Initializes a new instance of the struct. + /// + /// A structure of 4 bytes in ARGB byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba64(Argb32 source) + { + this.R = ColorNumerics.From8BitTo16Bit(source.R); + this.G = ColorNumerics.From8BitTo16Bit(source.G); + this.B = ColorNumerics.From8BitTo16Bit(source.B); + this.A = ColorNumerics.From8BitTo16Bit(source.A); + } + + /// + /// Initializes a new instance of the struct. + /// + /// A structure of 4 bytes in ABGR byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba64(Abgr32 source) + { + this.R = ColorNumerics.From8BitTo16Bit(source.R); + this.G = ColorNumerics.From8BitTo16Bit(source.G); + this.B = ColorNumerics.From8BitTo16Bit(source.B); + this.A = ColorNumerics.From8BitTo16Bit(source.A); + } + + /// + /// Initializes a new instance of the struct. + /// + /// A structure of 3 bytes in RGB byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba64(Rgb24 source) + { + this.R = ColorNumerics.From8BitTo16Bit(source.R); + this.G = ColorNumerics.From8BitTo16Bit(source.G); + this.B = ColorNumerics.From8BitTo16Bit(source.B); + this.A = ushort.MaxValue; + } + + /// + /// Initializes a new instance of the struct. + /// + /// A structure of 3 bytes in BGR byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba64(Bgr24 source) + { + this.R = ColorNumerics.From8BitTo16Bit(source.R); + this.G = ColorNumerics.From8BitTo16Bit(source.G); + this.B = ColorNumerics.From8BitTo16Bit(source.B); + this.A = ushort.MaxValue; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba64(Vector4 vector) + { + vector = Numerics.Clamp(vector, Vector4.Zero, Vector4.One) * Max; + this.R = (ushort)MathF.Round(vector.X); + this.G = (ushort)MathF.Round(vector.Y); + this.B = (ushort)MathF.Round(vector.Z); + this.A = (ushort)MathF.Round(vector.W); + } + + /// + /// Gets or sets the RGB components of this struct as . + /// + public Rgb48 Rgb + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => Unsafe.As(ref this) = value; + } + + /// + public ulong PackedValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => Unsafe.As(ref this) = value; + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Rgba64 left, Rgba64 right) => left.PackedValue == right.PackedValue; + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Rgba64 left, Rgba64 right) => left.PackedValue != right.PackedValue; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromRgba64(this); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new Vector4(this.R, this.G, this.B, this.A) / Max; + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 16, 16, 16, 16), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromVector4(Vector4 source) => new(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromAbgr32(Abgr32 source) => new(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromArgb32(Argb32 source) => new(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromBgr24(Bgr24 source) => new(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromBgra32(Bgra32 source) => new(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromL8(L8 source) + { + ushort rgb = ColorNumerics.From8BitTo16Bit(source.PackedValue); + return new Rgba64(rgb, rgb, rgb, ushort.MaxValue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromL16(L16 source) => new(source.PackedValue, source.PackedValue, source.PackedValue, ushort.MaxValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromLa16(La16 source) + { + ushort rgb = ColorNumerics.From8BitTo16Bit(source.L); + return new Rgba64(rgb, rgb, rgb, ColorNumerics.From8BitTo16Bit(source.A)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromLa32(La32 source) => new(source.L, source.L, source.L, source.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromRgb24(Rgb24 source) => new(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromRgba32(Rgba32 source) => new(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromRgb48(Rgb48 source) => new(source.R, source.G, source.B, ushort.MaxValue); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromRgba64(Rgba64 source) => new(source.R, source.G, source.B, source.A); + + /// + /// Convert to . + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Bgra32 ToBgra32() => Bgra32.FromRgba64(this); + + /// + /// Convert to . + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Argb32 ToArgb32() => Argb32.FromRgba64(this); + + /// + /// Convert to . + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Abgr32 ToAbgr32() => Abgr32.FromRgba64(this); + + /// + /// Convert to . + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgb24 ToRgb24() => Rgb24.FromRgba64(this); + + /// + /// Convert to . + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Bgr24 ToBgr24() => Bgr24.FromRgba64(this); + + /// + public override readonly bool Equals(object? obj) => obj is Rgba64 rgba64 && this.Equals(rgba64); + + /// + public readonly bool Equals(Rgba64 other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly string ToString() => $"Rgba64({this.R}, {this.G}, {this.B}, {this.A})"; + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/RgbaVector.cs b/ImageSharp/PixelFormats/PixelImplementations/RgbaVector.cs new file mode 100644 index 0000000..4de50cf --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/RgbaVector.cs @@ -0,0 +1,214 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Globalization; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Unpacked pixel type containing four 32-bit floating-point values typically ranging from 0 to 1. + /// The color components are stored in red, green, blue, and alpha order. + /// + /// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. + /// + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + [StructLayout(LayoutKind.Sequential)] + public partial struct RgbaVector : IPixel + { + /// + /// Gets or sets the red component. + /// + public float R; + + /// + /// Gets or sets the green component. + /// + public float G; + + /// + /// Gets or sets the blue component. + /// + public float B; + + /// + /// Gets or sets the alpha component. + /// + public float A; + + private const float MaxBytes = byte.MaxValue; + private static readonly Vector4 Max = new(MaxBytes); + private static readonly Vector4 Half = new(0.5F); + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The alpha component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public RgbaVector(float r, float g, float b, float a = 1) + { + this.R = r; + this.G = g; + this.B = b; + this.A = a; + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(RgbaVector left, RgbaVector right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(RgbaVector left, RgbaVector right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new(this.R, this.G, this.B, this.A); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 32, 32, 32, 32), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromVector4(Vector4 source) + { + source = Numerics.Clamp(source, Vector4.Zero, Vector4.One); + return new RgbaVector(source.X, source.Y, source.Z, source.W); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromRgba32(Rgba32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + /// Creates a new instance of the struct. + /// + /// + /// The hexadecimal representation of the combined color components arranged + /// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax. + /// + /// + /// The . + /// + public static RgbaVector FromHex(string hex) => Color.ParseHex(hex).ToPixel(); + + /// + /// Converts the value of this instance to a hexadecimal string. + /// + /// A hexadecimal string representation of the value. + public readonly string ToHex() + { + // Hex is RRGGBBAA + Vector4 vector = this.ToVector4() * Max; + vector += Half; + uint hexOrder = (uint)((byte)vector.W | ((byte)vector.Z << 8) | ((byte)vector.Y << 16) | ((byte)vector.X << 24)); + return hexOrder.ToString("X8", CultureInfo.InvariantCulture); + } + + /// + public override readonly bool Equals(object? obj) => obj is RgbaVector other && this.Equals(other); + + /// + public readonly bool Equals(RgbaVector other) => + this.R.Equals(other.R) + && this.G.Equals(other.G) + && this.B.Equals(other.B) + && this.A.Equals(other.A); + + /// + public override readonly string ToString() => FormattableString.Invariant($"RgbaVector({this.R:#0.##}, {this.G:#0.##}, {this.B:#0.##}, {this.A:#0.##})"); + + /// + public override readonly int GetHashCode() => HashCode.Combine(this.R, this.G, this.B, this.A); + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Short2.cs b/ImageSharp/PixelFormats/PixelImplementations/Short2.cs new file mode 100644 index 0000000..11547fe --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Short2.cs @@ -0,0 +1,194 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing two 16-bit signed integer values. + /// + /// Ranges from [-32767, -32767, 0, 1] to [32767, 32767, 0, 1] in vector form. + /// + /// + public partial struct Short2 : IPixel, IPackedVector + { + // Largest two byte positive number 0xFFFF >> 1; + private const float MaxPos = 0x7FFF; + + // Two's complement + private const float MinNeg = ~(int)MaxPos; + + private static readonly Vector2 Max = new(MaxPos); + private static readonly Vector2 Min = new(MinNeg); + + /// + /// Initializes a new instance of the struct. + /// + /// The x-component. + /// The y-component. + public Short2(float x, float y) + : this(new Vector2(x, y)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector containing the component values. + public Short2(Vector2 vector) => this.PackedValue = Pack(vector); + + /// + public uint PackedValue { get; set; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Short2 left, Short2 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Short2 left, Short2 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() + { + Vector2 scaled = this.ToVector2(); + scaled += new Vector2(32767f); + scaled /= 65534F; + return new Vector4(scaled, 0f, 1f); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new((short)(this.PackedValue & 0xFFFF), (short)(this.PackedValue >> 0x10), 0f, 1f); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(2, 16, 16), + PixelColorType.Red | PixelColorType.Green, + PixelAlphaRepresentation.None); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromScaledVector4(Vector4 source) + { + Vector2 scaled = new Vector2(source.X, source.Y) * 65534F; + scaled -= new Vector2(32767F); + return new Short2 { PackedValue = Pack(scaled) }; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromVector4(Vector4 source) => new() { PackedValue = Pack(new Vector2(source.X, source.Y)) }; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromRgba32(Rgba32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + /// Expands the packed representation into a . + /// The vector components are typically expanded in least to greatest significance order. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector2 ToVector2() => new((short)(this.PackedValue & 0xFFFF), (short)(this.PackedValue >> 0x10)); + + /// + public override readonly bool Equals(object? obj) => obj is Short2 other && this.Equals(other); + + /// + public readonly bool Equals(Short2 other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + public override readonly string ToString() + { + Vector2 vector = this.ToVector2(); + return FormattableString.Invariant($"Short2({vector.X:#0.##}, {vector.Y:#0.##})"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Pack(Vector2 vector) + { + vector = Vector2.Clamp(vector, Min, Max); + uint word2 = (uint)Convert.ToInt32(Math.Round(vector.X)) & 0xFFFF; + uint word1 = ((uint)Convert.ToInt32(Math.Round(vector.Y)) & 0xFFFF) << 0x10; + + return word2 | word1; + } + } +} diff --git a/ImageSharp/PixelFormats/PixelImplementations/Short4.cs b/ImageSharp/PixelFormats/PixelImplementations/Short4.cs new file mode 100644 index 0000000..25d0c9b --- /dev/null +++ b/ImageSharp/PixelFormats/PixelImplementations/Short4.cs @@ -0,0 +1,199 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Packed pixel type containing four 16-bit signed integer values. + /// + /// Ranges from [-37267, -37267, -37267, -37267] to [37267, 37267, 37267, 37267] in vector form. + /// + /// + public partial struct Short4 : IPixel, IPackedVector + { + // Largest two byte positive number 0xFFFF >> 1; + private const float MaxPos = 0x7FFF; + + // Two's complement + private const float MinNeg = ~(int)MaxPos; + + private static readonly Vector4 Max = new(MaxPos); + private static readonly Vector4 Min = new(MinNeg); + + /// + /// Initializes a new instance of the struct. + /// + /// The x-component. + /// The y-component. + /// The z-component. + /// The w-component. + public Short4(float x, float y, float z, float w) + : this(new Vector4(x, y, z, w)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// A vector containing the initial values for the components. + public Short4(Vector4 vector) => this.PackedValue = Pack(vector); + + /// + public ulong PackedValue { get; set; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Short4 left, Short4 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Short4 left, Short4 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() + { + Vector4 scaled = this.ToVector4(); + scaled += new Vector4(32767f); + scaled /= 65534f; + return scaled; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() + => new( + (short)(this.PackedValue & 0xFFFF), + (short)((this.PackedValue >> 0x10) & 0xFFFF), + (short)((this.PackedValue >> 0x20) & 0xFFFF), + (short)((this.PackedValue >> 0x30) & 0xFFFF)); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 16, 16, 16, 16), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromScaledVector4(Vector4 source) + { + source *= 65534F; + source -= new Vector4(32767F); + return FromVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromVector4(Vector4 source) => new(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromRgba32(Rgba32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is Short4 other && this.Equals(other); + + /// + public readonly bool Equals(Short4 other) => this.PackedValue.Equals(other.PackedValue); + + /// + /// Gets the hash code for the current instance. + /// + /// Hash code for the instance. + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + public override readonly string ToString() + { + Vector4 vector = this.ToVector4(); + return FormattableString.Invariant($"Short4({vector.X:#0.##}, {vector.Y:#0.##}, {vector.Z:#0.##}, {vector.W:#0.##})"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Pack(Vector4 vector) + { + // Clamp the value between min and max values + vector = Numerics.Clamp(vector, Min, Max); + ulong word4 = ((ulong)Convert.ToInt32(Math.Round(vector.X)) & 0xFFFF) << 0x00; + ulong word3 = ((ulong)Convert.ToInt32(Math.Round(vector.Y)) & 0xFFFF) << 0x10; + ulong word2 = ((ulong)Convert.ToInt32(Math.Round(vector.Z)) & 0xFFFF) << 0x20; + ulong word1 = ((ulong)Convert.ToInt32(Math.Round(vector.W)) & 0xFFFF) << 0x30; + + return word4 | word3 | word2 | word1; + } + } +} diff --git a/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.cs b/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.cs new file mode 100644 index 0000000..4efaa47 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.cs @@ -0,0 +1,870 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +// + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.PixelFormats; + +public partial class PixelOperations +{ + /// + /// Converts all pixels in 'source` span of into a span of -s. + /// + /// A to configure internal operations. + /// The source of data. + /// The to the destination pixels. + public virtual void FromArgb32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Argb32 sourceBase = ref MemoryMarshal.GetReference(source); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = TPixel.FromArgb32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + /// A helper for that expects a byte span. + /// The layout of the data in 'sourceBytes' must be compatible with layout. + /// + /// A to configure internal operations. + /// The to the source bytes. + /// The to the destination pixels. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromArgb32Bytes(Configuration configuration, ReadOnlySpan sourceBytes, Span destination, int count) + { + this.FromArgb32(configuration, MemoryMarshal.Cast(sourceBytes).Slice(0, count), destination); + } + + /// + /// Converts all pixels of the 'source` span to a span of -s. + /// + /// A to configure internal operations + /// The span of source pixels + /// The destination span of data. + public virtual void ToArgb32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref TPixel sourceBase = ref MemoryMarshal.GetReference(source); + ref Argb32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Argb32.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + } + } + + /// + /// A helper for that expects a byte span as destination. + /// The layout of the data in 'destination' must be compatible with layout. + /// + /// A to configure internal operations + /// The to the source pixels. + /// The to the destination bytes. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ToArgb32Bytes(Configuration configuration, ReadOnlySpan source, Span destination, int count) + { + this.ToArgb32(configuration, source.Slice(0, count), MemoryMarshal.Cast(destination)); + } + + /// + /// Converts all pixels in 'source` span of into a span of -s. + /// + /// A to configure internal operations. + /// The source of data. + /// The to the destination pixels. + public virtual void FromAbgr32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Abgr32 sourceBase = ref MemoryMarshal.GetReference(source); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = TPixel.FromAbgr32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + /// A helper for that expects a byte span. + /// The layout of the data in 'sourceBytes' must be compatible with layout. + /// + /// A to configure internal operations. + /// The to the source bytes. + /// The to the destination pixels. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromAbgr32Bytes(Configuration configuration, ReadOnlySpan sourceBytes, Span destination, int count) + { + this.FromAbgr32(configuration, MemoryMarshal.Cast(sourceBytes).Slice(0, count), destination); + } + + /// + /// Converts all pixels of the 'source` span to a span of -s. + /// + /// A to configure internal operations + /// The span of source pixels + /// The destination span of data. + public virtual void ToAbgr32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref TPixel sourceBase = ref MemoryMarshal.GetReference(source); + ref Abgr32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Abgr32.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + } + } + + /// + /// A helper for that expects a byte span as destination. + /// The layout of the data in 'destination' must be compatible with layout. + /// + /// A to configure internal operations + /// The to the source pixels. + /// The to the destination bytes. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ToAbgr32Bytes(Configuration configuration, ReadOnlySpan source, Span destination, int count) + { + this.ToAbgr32(configuration, source.Slice(0, count), MemoryMarshal.Cast(destination)); + } + + /// + /// Converts all pixels in 'source` span of into a span of -s. + /// + /// A to configure internal operations. + /// The source of data. + /// The to the destination pixels. + public virtual void FromBgr24(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgr24 sourceBase = ref MemoryMarshal.GetReference(source); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = TPixel.FromBgr24(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + /// A helper for that expects a byte span. + /// The layout of the data in 'sourceBytes' must be compatible with layout. + /// + /// A to configure internal operations. + /// The to the source bytes. + /// The to the destination pixels. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromBgr24Bytes(Configuration configuration, ReadOnlySpan sourceBytes, Span destination, int count) + { + this.FromBgr24(configuration, MemoryMarshal.Cast(sourceBytes).Slice(0, count), destination); + } + + /// + /// Converts all pixels of the 'source` span to a span of -s. + /// + /// A to configure internal operations + /// The span of source pixels + /// The destination span of data. + public virtual void ToBgr24(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref TPixel sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgr24 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgr24.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + } + } + + /// + /// A helper for that expects a byte span as destination. + /// The layout of the data in 'destination' must be compatible with layout. + /// + /// A to configure internal operations + /// The to the source pixels. + /// The to the destination bytes. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ToBgr24Bytes(Configuration configuration, ReadOnlySpan source, Span destination, int count) + { + this.ToBgr24(configuration, source.Slice(0, count), MemoryMarshal.Cast(destination)); + } + + /// + /// Converts all pixels in 'source` span of into a span of -s. + /// + /// A to configure internal operations. + /// The source of data. + /// The to the destination pixels. + public virtual void FromBgra32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra32 sourceBase = ref MemoryMarshal.GetReference(source); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = TPixel.FromBgra32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + /// A helper for that expects a byte span. + /// The layout of the data in 'sourceBytes' must be compatible with layout. + /// + /// A to configure internal operations. + /// The to the source bytes. + /// The to the destination pixels. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromBgra32Bytes(Configuration configuration, ReadOnlySpan sourceBytes, Span destination, int count) + { + this.FromBgra32(configuration, MemoryMarshal.Cast(sourceBytes).Slice(0, count), destination); + } + + /// + /// Converts all pixels of the 'source` span to a span of -s. + /// + /// A to configure internal operations + /// The span of source pixels + /// The destination span of data. + public virtual void ToBgra32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref TPixel sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra32.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + } + } + + /// + /// A helper for that expects a byte span as destination. + /// The layout of the data in 'destination' must be compatible with layout. + /// + /// A to configure internal operations + /// The to the source pixels. + /// The to the destination bytes. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ToBgra32Bytes(Configuration configuration, ReadOnlySpan source, Span destination, int count) + { + this.ToBgra32(configuration, source.Slice(0, count), MemoryMarshal.Cast(destination)); + } + + /// + /// Converts all pixels in 'source` span of into a span of -s. + /// + /// A to configure internal operations. + /// The source of data. + /// The to the destination pixels. + public virtual void FromL8(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L8 sourceBase = ref MemoryMarshal.GetReference(source); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = TPixel.FromL8(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + /// A helper for that expects a byte span. + /// The layout of the data in 'sourceBytes' must be compatible with layout. + /// + /// A to configure internal operations. + /// The to the source bytes. + /// The to the destination pixels. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromL8Bytes(Configuration configuration, ReadOnlySpan sourceBytes, Span destination, int count) + { + this.FromL8(configuration, MemoryMarshal.Cast(sourceBytes).Slice(0, count), destination); + } + + /// + /// Converts all pixels of the 'source` span to a span of -s. + /// + /// A to configure internal operations + /// The span of source pixels + /// The destination span of data. + public virtual void ToL8(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref TPixel sourceBase = ref MemoryMarshal.GetReference(source); + ref L8 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L8.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + } + } + + /// + /// A helper for that expects a byte span as destination. + /// The layout of the data in 'destination' must be compatible with layout. + /// + /// A to configure internal operations + /// The to the source pixels. + /// The to the destination bytes. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ToL8Bytes(Configuration configuration, ReadOnlySpan source, Span destination, int count) + { + this.ToL8(configuration, source.Slice(0, count), MemoryMarshal.Cast(destination)); + } + + /// + /// Converts all pixels in 'source` span of into a span of -s. + /// + /// A to configure internal operations. + /// The source of data. + /// The to the destination pixels. + public virtual void FromL16(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref L16 sourceBase = ref MemoryMarshal.GetReference(source); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = TPixel.FromL16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + /// A helper for that expects a byte span. + /// The layout of the data in 'sourceBytes' must be compatible with layout. + /// + /// A to configure internal operations. + /// The to the source bytes. + /// The to the destination pixels. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromL16Bytes(Configuration configuration, ReadOnlySpan sourceBytes, Span destination, int count) + { + this.FromL16(configuration, MemoryMarshal.Cast(sourceBytes).Slice(0, count), destination); + } + + /// + /// Converts all pixels of the 'source` span to a span of -s. + /// + /// A to configure internal operations + /// The span of source pixels + /// The destination span of data. + public virtual void ToL16(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref TPixel sourceBase = ref MemoryMarshal.GetReference(source); + ref L16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = L16.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + } + } + + /// + /// A helper for that expects a byte span as destination. + /// The layout of the data in 'destination' must be compatible with layout. + /// + /// A to configure internal operations + /// The to the source pixels. + /// The to the destination bytes. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ToL16Bytes(Configuration configuration, ReadOnlySpan source, Span destination, int count) + { + this.ToL16(configuration, source.Slice(0, count), MemoryMarshal.Cast(destination)); + } + + /// + /// Converts all pixels in 'source` span of into a span of -s. + /// + /// A to configure internal operations. + /// The source of data. + /// The to the destination pixels. + public virtual void FromLa16(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La16 sourceBase = ref MemoryMarshal.GetReference(source); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = TPixel.FromLa16(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + /// A helper for that expects a byte span. + /// The layout of the data in 'sourceBytes' must be compatible with layout. + /// + /// A to configure internal operations. + /// The to the source bytes. + /// The to the destination pixels. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromLa16Bytes(Configuration configuration, ReadOnlySpan sourceBytes, Span destination, int count) + { + this.FromLa16(configuration, MemoryMarshal.Cast(sourceBytes).Slice(0, count), destination); + } + + /// + /// Converts all pixels of the 'source` span to a span of -s. + /// + /// A to configure internal operations + /// The span of source pixels + /// The destination span of data. + public virtual void ToLa16(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref TPixel sourceBase = ref MemoryMarshal.GetReference(source); + ref La16 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La16.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + } + } + + /// + /// A helper for that expects a byte span as destination. + /// The layout of the data in 'destination' must be compatible with layout. + /// + /// A to configure internal operations + /// The to the source pixels. + /// The to the destination bytes. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ToLa16Bytes(Configuration configuration, ReadOnlySpan source, Span destination, int count) + { + this.ToLa16(configuration, source.Slice(0, count), MemoryMarshal.Cast(destination)); + } + + /// + /// Converts all pixels in 'source` span of into a span of -s. + /// + /// A to configure internal operations. + /// The source of data. + /// The to the destination pixels. + public virtual void FromLa32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref La32 sourceBase = ref MemoryMarshal.GetReference(source); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = TPixel.FromLa32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + /// A helper for that expects a byte span. + /// The layout of the data in 'sourceBytes' must be compatible with layout. + /// + /// A to configure internal operations. + /// The to the source bytes. + /// The to the destination pixels. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromLa32Bytes(Configuration configuration, ReadOnlySpan sourceBytes, Span destination, int count) + { + this.FromLa32(configuration, MemoryMarshal.Cast(sourceBytes).Slice(0, count), destination); + } + + /// + /// Converts all pixels of the 'source` span to a span of -s. + /// + /// A to configure internal operations + /// The span of source pixels + /// The destination span of data. + public virtual void ToLa32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref TPixel sourceBase = ref MemoryMarshal.GetReference(source); + ref La32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = La32.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + } + } + + /// + /// A helper for that expects a byte span as destination. + /// The layout of the data in 'destination' must be compatible with layout. + /// + /// A to configure internal operations + /// The to the source pixels. + /// The to the destination bytes. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ToLa32Bytes(Configuration configuration, ReadOnlySpan source, Span destination, int count) + { + this.ToLa32(configuration, source.Slice(0, count), MemoryMarshal.Cast(destination)); + } + + /// + /// Converts all pixels in 'source` span of into a span of -s. + /// + /// A to configure internal operations. + /// The source of data. + /// The to the destination pixels. + public virtual void FromRgb24(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb24 sourceBase = ref MemoryMarshal.GetReference(source); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = TPixel.FromRgb24(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + /// A helper for that expects a byte span. + /// The layout of the data in 'sourceBytes' must be compatible with layout. + /// + /// A to configure internal operations. + /// The to the source bytes. + /// The to the destination pixels. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromRgb24Bytes(Configuration configuration, ReadOnlySpan sourceBytes, Span destination, int count) + { + this.FromRgb24(configuration, MemoryMarshal.Cast(sourceBytes).Slice(0, count), destination); + } + + /// + /// Converts all pixels of the 'source` span to a span of -s. + /// + /// A to configure internal operations + /// The span of source pixels + /// The destination span of data. + public virtual void ToRgb24(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref TPixel sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb24 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb24.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + } + } + + /// + /// A helper for that expects a byte span as destination. + /// The layout of the data in 'destination' must be compatible with layout. + /// + /// A to configure internal operations + /// The to the source pixels. + /// The to the destination bytes. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ToRgb24Bytes(Configuration configuration, ReadOnlySpan source, Span destination, int count) + { + this.ToRgb24(configuration, source.Slice(0, count), MemoryMarshal.Cast(destination)); + } + + /// + /// Converts all pixels in 'source` span of into a span of -s. + /// + /// A to configure internal operations. + /// The source of data. + /// The to the destination pixels. + public virtual void FromRgba32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba32 sourceBase = ref MemoryMarshal.GetReference(source); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = TPixel.FromRgba32(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + /// A helper for that expects a byte span. + /// The layout of the data in 'sourceBytes' must be compatible with layout. + /// + /// A to configure internal operations. + /// The to the source bytes. + /// The to the destination pixels. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromRgba32Bytes(Configuration configuration, ReadOnlySpan sourceBytes, Span destination, int count) + { + this.FromRgba32(configuration, MemoryMarshal.Cast(sourceBytes).Slice(0, count), destination); + } + + /// + /// Converts all pixels of the 'source` span to a span of -s. + /// + /// A to configure internal operations + /// The span of source pixels + /// The destination span of data. + public virtual void ToRgba32(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref TPixel sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba32 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba32.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + } + } + + /// + /// A helper for that expects a byte span as destination. + /// The layout of the data in 'destination' must be compatible with layout. + /// + /// A to configure internal operations + /// The to the source pixels. + /// The to the destination bytes. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ToRgba32Bytes(Configuration configuration, ReadOnlySpan source, Span destination, int count) + { + this.ToRgba32(configuration, source.Slice(0, count), MemoryMarshal.Cast(destination)); + } + + /// + /// Converts all pixels in 'source` span of into a span of -s. + /// + /// A to configure internal operations. + /// The source of data. + /// The to the destination pixels. + public virtual void FromRgb48(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgb48 sourceBase = ref MemoryMarshal.GetReference(source); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = TPixel.FromRgb48(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + /// A helper for that expects a byte span. + /// The layout of the data in 'sourceBytes' must be compatible with layout. + /// + /// A to configure internal operations. + /// The to the source bytes. + /// The to the destination pixels. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromRgb48Bytes(Configuration configuration, ReadOnlySpan sourceBytes, Span destination, int count) + { + this.FromRgb48(configuration, MemoryMarshal.Cast(sourceBytes).Slice(0, count), destination); + } + + /// + /// Converts all pixels of the 'source` span to a span of -s. + /// + /// A to configure internal operations + /// The span of source pixels + /// The destination span of data. + public virtual void ToRgb48(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref TPixel sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgb48 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgb48.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + } + } + + /// + /// A helper for that expects a byte span as destination. + /// The layout of the data in 'destination' must be compatible with layout. + /// + /// A to configure internal operations + /// The to the source pixels. + /// The to the destination bytes. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ToRgb48Bytes(Configuration configuration, ReadOnlySpan source, Span destination, int count) + { + this.ToRgb48(configuration, source.Slice(0, count), MemoryMarshal.Cast(destination)); + } + + /// + /// Converts all pixels in 'source` span of into a span of -s. + /// + /// A to configure internal operations. + /// The source of data. + /// The to the destination pixels. + public virtual void FromRgba64(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Rgba64 sourceBase = ref MemoryMarshal.GetReference(source); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = TPixel.FromRgba64(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + /// A helper for that expects a byte span. + /// The layout of the data in 'sourceBytes' must be compatible with layout. + /// + /// A to configure internal operations. + /// The to the source bytes. + /// The to the destination pixels. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromRgba64Bytes(Configuration configuration, ReadOnlySpan sourceBytes, Span destination, int count) + { + this.FromRgba64(configuration, MemoryMarshal.Cast(sourceBytes).Slice(0, count), destination); + } + + /// + /// Converts all pixels of the 'source` span to a span of -s. + /// + /// A to configure internal operations + /// The span of source pixels + /// The destination span of data. + public virtual void ToRgba64(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref TPixel sourceBase = ref MemoryMarshal.GetReference(source); + ref Rgba64 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Rgba64.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + } + } + + /// + /// A helper for that expects a byte span as destination. + /// The layout of the data in 'destination' must be compatible with layout. + /// + /// A to configure internal operations + /// The to the source pixels. + /// The to the destination bytes. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ToRgba64Bytes(Configuration configuration, ReadOnlySpan source, Span destination, int count) + { + this.ToRgba64(configuration, source.Slice(0, count), MemoryMarshal.Cast(destination)); + } + + /// + /// Converts all pixels in 'source` span of into a span of -s. + /// + /// A to configure internal operations. + /// The source of data. + /// The to the destination pixels. + public virtual void FromBgra5551(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref Bgra5551 sourceBase = ref MemoryMarshal.GetReference(source); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = TPixel.FromBgra5551(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + /// A helper for that expects a byte span. + /// The layout of the data in 'sourceBytes' must be compatible with layout. + /// + /// A to configure internal operations. + /// The to the source bytes. + /// The to the destination pixels. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromBgra5551Bytes(Configuration configuration, ReadOnlySpan sourceBytes, Span destination, int count) + { + this.FromBgra5551(configuration, MemoryMarshal.Cast(sourceBytes).Slice(0, count), destination); + } + + /// + /// Converts all pixels of the 'source` span to a span of -s. + /// + /// A to configure internal operations + /// The span of source pixels + /// The destination span of data. + public virtual void ToBgra5551(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref TPixel sourceBase = ref MemoryMarshal.GetReference(source); + ref Bgra5551 destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = Bgra5551.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + } + } + + /// + /// A helper for that expects a byte span as destination. + /// The layout of the data in 'destination' must be compatible with layout. + /// + /// A to configure internal operations + /// The to the source pixels. + /// The to the destination bytes. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ToBgra5551Bytes(Configuration configuration, ReadOnlySpan source, Span destination, int count) + { + this.ToBgra5551(configuration, source.Slice(0, count), MemoryMarshal.Cast(destination)); + } +} diff --git a/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.tt b/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.tt new file mode 100644 index 0000000..5ede170 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.tt @@ -0,0 +1,144 @@ +<# +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#> +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ import namespace="System.Runtime.InteropServices" #> +<#@ output extension=".cs" #> +<# + + void GenerateFromMethods(string pixelType) + { + #> + + /// + /// Converts all pixels in 'source` span of into a span of -s. + /// + /// A to configure internal operations. + /// The source of data. + /// The to the destination pixels. + public virtual void From<#=pixelType#>(Configuration configuration, ReadOnlySpan<<#=pixelType#>> source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref <#=pixelType#> sourceBase = ref MemoryMarshal.GetReference(source); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = TPixel.From<#=pixelType#>(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + /// A helper for that expects a byte span. + /// The layout of the data in 'sourceBytes' must be compatible with layout. + /// + /// A to configure internal operations. + /// The to the source bytes. + /// The to the destination pixels. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void From<#=pixelType#>Bytes(Configuration configuration, ReadOnlySpan sourceBytes, Span destination, int count) + { + this.From<#=pixelType#>(configuration, MemoryMarshal.Cast>(sourceBytes).Slice(0, count), destination); + } + +<# + } + + void GenerateToDestFormatMethods(string pixelType) + { + #> + /// + /// Converts all pixels of the 'source` span to a span of -s. + /// + /// A to configure internal operations + /// The span of source pixels + /// The destination span of data. + public virtual void To<#=pixelType#>(Configuration configuration, ReadOnlySpan source, Span<<#=pixelType#>> destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ref TPixel sourceBase = ref MemoryMarshal.GetReference(source); + ref <#=pixelType#> destinationBase = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref destinationBase, i) = <#=pixelType#>.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + } + } + + /// + /// A helper for that expects a byte span as destination. + /// The layout of the data in 'destination' must be compatible with layout. + /// + /// A to configure internal operations + /// The to the source pixels. + /// The to the destination bytes. + /// The number of pixels to convert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void To<#=pixelType#>Bytes(Configuration configuration, ReadOnlySpan source, Span destination, int count) + { + this.To<#=pixelType#>(configuration, source.Slice(0, count), MemoryMarshal.Cast>(destination)); + } +<# + } + +#> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +// + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.PixelFormats; + +public partial class PixelOperations +{<# + +GenerateFromMethods("Argb32"); +GenerateToDestFormatMethods("Argb32"); + +GenerateFromMethods("Abgr32"); +GenerateToDestFormatMethods("Abgr32"); + +GenerateFromMethods("Bgr24"); +GenerateToDestFormatMethods("Bgr24"); + +GenerateFromMethods("Bgra32"); +GenerateToDestFormatMethods("Bgra32"); + +GenerateFromMethods("L8"); +GenerateToDestFormatMethods("L8"); + +GenerateFromMethods("L16"); +GenerateToDestFormatMethods("L16"); + +GenerateFromMethods("La16"); +GenerateToDestFormatMethods("La16"); + +GenerateFromMethods("La32"); +GenerateToDestFormatMethods("La32"); + +GenerateFromMethods("Rgb24"); +GenerateToDestFormatMethods("Rgb24"); + +GenerateFromMethods("Rgba32"); +GenerateToDestFormatMethods("Rgba32"); + +GenerateFromMethods("Rgb48"); +GenerateToDestFormatMethods("Rgb48"); + +GenerateFromMethods("Rgba64"); +GenerateToDestFormatMethods("Rgba64"); + +GenerateFromMethods("Bgra5551"); +GenerateToDestFormatMethods("Bgra5551"); + +#>} diff --git a/ImageSharp/PixelFormats/PixelOperations{TPixel}.PixelBenders.cs b/ImageSharp/PixelFormats/PixelOperations{TPixel}.PixelBenders.cs new file mode 100644 index 0000000..be81e21 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelOperations{TPixel}.PixelBenders.cs @@ -0,0 +1,216 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats.PixelBlenders; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Provides access to pixel blenders + /// + public partial class PixelOperations + where TPixel : unmanaged, IPixel + { + /// + /// Find an instance of the pixel blender. + /// + /// the blending and composition to apply + /// A . + public PixelBlender GetPixelBlender(GraphicsOptions options) + { + return this.GetPixelBlender(options.ColorBlendingMode, options.AlphaCompositionMode); + } + + /// + /// Find an instance of the pixel blender. + /// + /// The color blending mode to apply + /// The alpha composition mode to apply + /// A . + public virtual PixelBlender GetPixelBlender(PixelColorBlendingMode colorMode, PixelAlphaCompositionMode alphaMode) + { + switch (alphaMode) + { + case PixelAlphaCompositionMode.Clear: + switch (colorMode) + { + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyClear.Instance; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddClear.Instance; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractClear.Instance; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenClear.Instance; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenClear.Instance; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenClear.Instance; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayClear.Instance; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightClear.Instance; + case PixelColorBlendingMode.Normal: + default: return DefaultPixelBlenders.NormalClear.Instance; + } + + case PixelAlphaCompositionMode.Xor: + switch (colorMode) + { + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyXor.Instance; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddXor.Instance; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractXor.Instance; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenXor.Instance; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenXor.Instance; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenXor.Instance; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayXor.Instance; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightXor.Instance; + case PixelColorBlendingMode.Normal: + default: return DefaultPixelBlenders.NormalXor.Instance; + } + + case PixelAlphaCompositionMode.Src: + switch (colorMode) + { + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplySrc.Instance; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddSrc.Instance; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractSrc.Instance; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenSrc.Instance; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenSrc.Instance; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenSrc.Instance; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlaySrc.Instance; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightSrc.Instance; + case PixelColorBlendingMode.Normal: + default: return DefaultPixelBlenders.NormalSrc.Instance; + } + + case PixelAlphaCompositionMode.SrcAtop: + switch (colorMode) + { + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplySrcAtop.Instance; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddSrcAtop.Instance; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractSrcAtop.Instance; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenSrcAtop.Instance; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenSrcAtop.Instance; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenSrcAtop.Instance; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlaySrcAtop.Instance; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightSrcAtop.Instance; + case PixelColorBlendingMode.Normal: + default: return DefaultPixelBlenders.NormalSrcAtop.Instance; + } + + case PixelAlphaCompositionMode.SrcIn: + switch (colorMode) + { + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplySrcIn.Instance; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddSrcIn.Instance; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractSrcIn.Instance; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenSrcIn.Instance; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenSrcIn.Instance; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenSrcIn.Instance; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlaySrcIn.Instance; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightSrcIn.Instance; + case PixelColorBlendingMode.Normal: + default: return DefaultPixelBlenders.NormalSrcIn.Instance; + } + + case PixelAlphaCompositionMode.SrcOut: + switch (colorMode) + { + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplySrcOut.Instance; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddSrcOut.Instance; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractSrcOut.Instance; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenSrcOut.Instance; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenSrcOut.Instance; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenSrcOut.Instance; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlaySrcOut.Instance; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightSrcOut.Instance; + case PixelColorBlendingMode.Normal: + default: return DefaultPixelBlenders.NormalSrcOut.Instance; + } + + case PixelAlphaCompositionMode.Dest: + switch (colorMode) + { + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyDest.Instance; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddDest.Instance; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractDest.Instance; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenDest.Instance; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenDest.Instance; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenDest.Instance; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayDest.Instance; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightDest.Instance; + case PixelColorBlendingMode.Normal: + default: return DefaultPixelBlenders.NormalDest.Instance; + } + + case PixelAlphaCompositionMode.DestAtop: + switch (colorMode) + { + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyDestAtop.Instance; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddDestAtop.Instance; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractDestAtop.Instance; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenDestAtop.Instance; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenDestAtop.Instance; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenDestAtop.Instance; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayDestAtop.Instance; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightDestAtop.Instance; + case PixelColorBlendingMode.Normal: + default: return DefaultPixelBlenders.NormalDestAtop.Instance; + } + + case PixelAlphaCompositionMode.DestIn: + switch (colorMode) + { + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyDestIn.Instance; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddDestIn.Instance; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractDestIn.Instance; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenDestIn.Instance; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenDestIn.Instance; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenDestIn.Instance; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayDestIn.Instance; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightDestIn.Instance; + case PixelColorBlendingMode.Normal: + default: return DefaultPixelBlenders.NormalDestIn.Instance; + } + + case PixelAlphaCompositionMode.DestOut: + switch (colorMode) + { + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyDestOut.Instance; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddDestOut.Instance; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractDestOut.Instance; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenDestOut.Instance; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenDestOut.Instance; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenDestOut.Instance; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayDestOut.Instance; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightDestOut.Instance; + case PixelColorBlendingMode.Normal: + default: return DefaultPixelBlenders.NormalDestOut.Instance; + } + + case PixelAlphaCompositionMode.DestOver: + switch (colorMode) + { + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyDestOver.Instance; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddDestOver.Instance; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractDestOver.Instance; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenDestOver.Instance; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenDestOver.Instance; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenDestOver.Instance; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayDestOver.Instance; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightDestOver.Instance; + case PixelColorBlendingMode.Normal: + default: return DefaultPixelBlenders.NormalDestOver.Instance; + } + + case PixelAlphaCompositionMode.SrcOver: + default: + switch (colorMode) + { + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplySrcOver.Instance; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddSrcOver.Instance; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractSrcOver.Instance; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenSrcOver.Instance; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenSrcOver.Instance; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenSrcOver.Instance; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlaySrcOver.Instance; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightSrcOver.Instance; + case PixelColorBlendingMode.Normal: + default: return DefaultPixelBlenders.NormalSrcOver.Instance; + } + } + } + } +} diff --git a/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs b/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs new file mode 100644 index 0000000..c031d2f --- /dev/null +++ b/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs @@ -0,0 +1,246 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// A stateless class implementing Strategy Pattern for batched pixel-data conversion operations + /// for pixel buffers of type . + /// + /// The pixel format. + public partial class PixelOperations + where TPixel : unmanaged, IPixel + { + private static readonly Lazy> LazyInstance = new(TPixel.CreatePixelOperations, true); + + /// + /// Gets the global instance for the pixel type + /// +#pragma warning disable CA1000 // Do not declare static members on generic types + public static PixelOperations Instance => LazyInstance.Value; +#pragma warning restore CA1000 // Do not declare static members on generic types + + /// + /// Gets the pixel type info for the given . + /// + /// The . + public PixelTypeInfo GetPixelTypeInfo() => TPixel.GetPixelTypeInfo(); + + /// + /// Bulk version of converting 'sourceVectors.Length' pixels into 'destinationColors'. + /// The method is DESTRUCTIVE altering the contents of . + /// + /// + /// The destructive behavior is a design choice for performance reasons. + /// In a typical use case the contents of are abandoned after the conversion. + /// + /// A to configure internal operations + /// The to the source vectors. + /// The to the destination colors. + /// The to apply during the conversion + public virtual void FromVector4Destructive( + Configuration configuration, + Span sourceVectors, + Span destination, + PixelConversionModifiers modifiers) + { + Guard.NotNull(configuration, nameof(configuration)); + + Utils.Vector4Converters.Default.FromVector4(sourceVectors, destination, modifiers); + } + + /// + /// Bulk version of converting 'sourceVectors.Length' pixels into 'destinationColors'. + /// The method is DESTRUCTIVE altering the contents of . + /// + /// + /// The destructive behavior is a design choice for performance reasons. + /// In a typical use case the contents of are abandoned after the conversion. + /// + /// A to configure internal operations + /// The to the source vectors. + /// The to the destination colors. + public void FromVector4Destructive( + Configuration configuration, + Span sourceVectors, + Span destination) + => this.FromVector4Destructive(configuration, sourceVectors, destination, PixelConversionModifiers.None); + + /// + /// Bulk version of converting 'sourceColors.Length' pixels into 'destinationVectors'. + /// + /// A to configure internal operations + /// The to the source colors. + /// The to the destination vectors. + /// The to apply during the conversion + public virtual void ToVector4( + Configuration configuration, + ReadOnlySpan source, + Span destinationVectors, + PixelConversionModifiers modifiers) + { + Guard.NotNull(configuration, nameof(configuration)); + + Utils.Vector4Converters.Default.ToVector4(source, destinationVectors, modifiers); + } + + /// + /// Bulk version of converting 'sourceColors.Length' pixels into 'destinationVectors'. + /// + /// A to configure internal operations + /// The to the source colors. + /// The to the destination vectors. + public void ToVector4( + Configuration configuration, + ReadOnlySpan source, + Span destinationVectors) + => this.ToVector4(configuration, source, destinationVectors, PixelConversionModifiers.None); + + /// + /// Bulk operation that copies the to in + /// format. + /// + /// The destination pixel type. + /// A to configure internal operations. + /// The to the source pixels. + /// The to the destination pixels. + public virtual void From( + Configuration configuration, + ReadOnlySpan source, + Span destination) + where TSourcePixel : unmanaged, IPixel + { + const int sliceLength = 1024; + int numberOfSlices = source.Length / sliceLength; + + using IMemoryOwner tempVectors = configuration.MemoryAllocator.Allocate(sliceLength); + Span vectorSpan = tempVectors.GetSpan(); + for (int i = 0; i < numberOfSlices; i++) + { + int start = i * sliceLength; + ReadOnlySpan s = source.Slice(start, sliceLength); + Span d = destination.Slice(start, sliceLength); + PixelOperations.Instance.ToVector4(configuration, s, vectorSpan, PixelConversionModifiers.Scale); + this.FromVector4Destructive(configuration, vectorSpan, d, PixelConversionModifiers.Scale); + } + + int endOfCompleteSlices = numberOfSlices * sliceLength; + int remainder = source.Length - endOfCompleteSlices; + if (remainder > 0) + { + ReadOnlySpan s = source[endOfCompleteSlices..]; + Span d = destination[endOfCompleteSlices..]; + vectorSpan = vectorSpan[..remainder]; + PixelOperations.Instance.ToVector4(configuration, s, vectorSpan, PixelConversionModifiers.Scale); + this.FromVector4Destructive(configuration, vectorSpan, d, PixelConversionModifiers.Scale); + } + } + + /// + /// Bulk operation that copies the to in + /// format. + /// + /// The destination pixel type. + /// A to configure internal operations. + /// The to the source pixels. + /// The to the destination pixels. + public virtual void To( + Configuration configuration, + ReadOnlySpan source, + Span destination) + where TDestinationPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + PixelOperations.Instance.From(configuration, source, destination); + } + + /// + /// Bulk operation that packs 3 separate RGB channels to . + /// The destination must have a padding of 3. + /// + /// A to the red values. + /// A to the green values. + /// A to the blue values. + /// A to the destination pixels. + internal virtual void PackFromRgbPlanes( + ReadOnlySpan redChannel, + ReadOnlySpan greenChannel, + ReadOnlySpan blueChannel, + Span destination) + { + int count = redChannel.Length; + GuardPackFromRgbPlanes(greenChannel, blueChannel, destination, count); + + Rgb24 rgb24 = default; + ref byte r = ref MemoryMarshal.GetReference(redChannel); + ref byte g = ref MemoryMarshal.GetReference(greenChannel); + ref byte b = ref MemoryMarshal.GetReference(blueChannel); + ref TPixel d = ref MemoryMarshal.GetReference(destination); + + for (nuint i = 0; i < (uint)count; i++) + { + rgb24.R = Unsafe.Add(ref r, i); + rgb24.G = Unsafe.Add(ref g, i); + rgb24.B = Unsafe.Add(ref b, i); + Unsafe.Add(ref d, i) = TPixel.FromRgb24(rgb24); + } + } + + /// + /// Bulk operation that unpacks pixels from + /// into 3 separate RGB channels. + /// + /// A to the red values. + /// A to the green values. + /// A to the blue values. + /// A to the destination pixels. + internal virtual void UnpackIntoRgbPlanes( + Span redChannel, + Span greenChannel, + Span blueChannel, + ReadOnlySpan source) + { + GuardUnpackIntoRgbPlanes(redChannel, greenChannel, blueChannel, source); + + // TODO: This can be much faster. + // Convert to Rgba32 first using pixel operations then use the R, G, B properties. + int count = source.Length; + + ref float r = ref MemoryMarshal.GetReference(redChannel); + ref float g = ref MemoryMarshal.GetReference(greenChannel); + ref float b = ref MemoryMarshal.GetReference(blueChannel); + ref TPixel src = ref MemoryMarshal.GetReference(source); + for (nuint i = 0; i < (uint)count; i++) + { + Rgba32 rgba32 = Unsafe.Add(ref src, i).ToRgba32(); + Unsafe.Add(ref r, i) = rgba32.R; + Unsafe.Add(ref g, i) = rgba32.G; + Unsafe.Add(ref b, i) = rgba32.B; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + internal static void GuardUnpackIntoRgbPlanes(Span redChannel, Span greenChannel, Span blueChannel, ReadOnlySpan source) + { + Guard.IsTrue(greenChannel.Length == redChannel.Length, nameof(greenChannel), "Channels must be of same size!"); + Guard.IsTrue(blueChannel.Length == redChannel.Length, nameof(blueChannel), "Channels must be of same size!"); + Guard.IsTrue(source.Length <= redChannel.Length, nameof(source), "'source' span should not be bigger than the destination channels!"); + } + + [MethodImpl(InliningOptions.ShortMethod)] + internal static void GuardPackFromRgbPlanes(ReadOnlySpan greenChannel, ReadOnlySpan blueChannel, Span destination, int count) + { + Guard.IsTrue(greenChannel.Length == count, nameof(greenChannel), "Channels must be of same size!"); + Guard.IsTrue(blueChannel.Length == count, nameof(blueChannel), "Channels must be of same size!"); + Guard.IsTrue(destination.Length > count + 2, nameof(destination), "'destination' must contain a padding of 3 elements!"); + } + } +} diff --git a/ImageSharp/PixelFormats/PixelTypeInfo.cs b/ImageSharp/PixelFormats/PixelTypeInfo.cs new file mode 100644 index 0000000..0c32a75 --- /dev/null +++ b/ImageSharp/PixelFormats/PixelTypeInfo.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +// TODO: Review this type as it's used to represent 2 different things. +// 1. The encoded image pixel format. +// 2. The pixel format of the decoded image. +// Only the bits per pixel is used by the decoder, we should make it a property of the image metadata. +namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Contains information about the pixels that make up an images visual data. + /// + /// + /// Initializes a new instance of the struct. + /// + /// Color depth, in number of bits per pixel. + public readonly struct PixelTypeInfo(int bitsPerPixel) + { + /// + /// Gets color depth, in number of bits per pixel. + /// + public int BitsPerPixel { get; init; } = bitsPerPixel; + + /// + /// Gets the component bit depth and padding within the pixel. + /// + public PixelComponentInfo? ComponentInfo { get; init; } + + /// + /// Gets the pixel color type. + /// + public PixelColorType ColorType { get; init; } + + /// + /// Gets the pixel alpha transparency behavior. + /// means unknown, unspecified. + /// + public PixelAlphaRepresentation AlphaRepresentation { get; init; } + + /// + /// Creates a new instance. + /// + /// The type of pixel format. + /// The pixel component info. + /// The pixel color type. + /// The pixel alpha representation. + /// The . + public static PixelTypeInfo Create( + PixelComponentInfo info, + PixelColorType colorType, + PixelAlphaRepresentation alphaRepresentation) + where TPixel : unmanaged, IPixel + => new() + { + BitsPerPixel = Unsafe.SizeOf() * 8, + ComponentInfo = info, + ColorType = colorType, + AlphaRepresentation = alphaRepresentation + }; + } +} diff --git a/ImageSharp/PixelFormats/README.md b/ImageSharp/PixelFormats/README.md new file mode 100644 index 0000000..4c7ee54 --- /dev/null +++ b/ImageSharp/PixelFormats/README.md @@ -0,0 +1,6 @@ +Pixel formats adapted and extended from: + +https://github.com/MonoGame/MonoGame + +The naming convention of each pixel format is to order the color components from least significant to most significant, reading from left to right. +For example in the Rgba32 pixel format the R component is the least significant byte, and the A component is the most significant. diff --git a/ImageSharp/PixelFormats/RgbaComponent.cs b/ImageSharp/PixelFormats/RgbaComponent.cs new file mode 100644 index 0000000..42861cf --- /dev/null +++ b/ImageSharp/PixelFormats/RgbaComponent.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp { + /// + /// Enumerates the RGBA (red, green, blue, alpha) color components. + /// + internal enum RgbaComponent + { + /// + /// The red component. + /// + R = 0, + + /// + /// The green component. + /// + G = 1, + + /// + /// The blue component. + /// + B = 2, + + /// + /// The alpha component. + /// + A = 3 + } +} diff --git a/ImageSharp/PixelFormats/Utils/PixelConverter.cs b/ImageSharp/PixelFormats/Utils/PixelConverter.cs new file mode 100644 index 0000000..2d580bb --- /dev/null +++ b/ImageSharp/PixelFormats/Utils/PixelConverter.cs @@ -0,0 +1,398 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats.Utils { + /// + /// Contains optimized implementations for conversion between pixel formats. + /// + /// + /// Implementations are based on ideas in: + /// https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Buffers/Binary/Reader.cs#L84 + /// The JIT can detect and optimize rotation idioms ROTL (Rotate Left) + /// and ROTR (Rotate Right) emitting efficient CPU instructions: + /// https://github.com/dotnet/coreclr/pull/1830 + /// + internal static class PixelConverter + { + /// + /// Optimized converters from . + /// + public static class FromRgba32 + { + // Input pixels have: X = R, Y = G, Z = B and W = A. + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToArgb32(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4(source, dest, default); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToBgra32(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4(source, dest, default); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToAbgr32(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4(source, dest, default); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToRgb24(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4Slice3(source, dest, default); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToBgr24(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4Slice3(source, dest, new DefaultShuffle4Slice3(SimdUtils.Shuffle.MMShuffle3012)); + } + + /// + /// Optimized converters from . + /// + public static class FromArgb32 + { + // Input pixels have: X = A, Y = R, Z = G and W = B. + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToRgba32(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4(source, dest, default); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToBgra32(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4(source, dest, default); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToAbgr32(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4(source, dest, default); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToRgb24(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4Slice3(source, dest, new DefaultShuffle4Slice3(SimdUtils.Shuffle.MMShuffle0321)); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToBgr24(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4Slice3(source, dest, new DefaultShuffle4Slice3(SimdUtils.Shuffle.MMShuffle0123)); + } + + /// + /// Optimized converters from . + /// + public static class FromBgra32 + { + // Input pixels have: X = B, Y = G, Z = R and W = A. + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToArgb32(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4(source, dest, default); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToRgba32(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4(source, dest, default); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToAbgr32(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4(source, dest, default); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToRgb24(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4Slice3(source, dest, new DefaultShuffle4Slice3(SimdUtils.Shuffle.MMShuffle3012)); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToBgr24(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4Slice3(source, dest, default); + } + + /// + /// Optimized converters from . + /// + public static class FromAbgr32 + { + // Input pixels have: X = A, Y = B, Z = G and W = R. + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToArgb32(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4(source, dest, default); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToRgba32(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4(source, dest, default); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToBgra32(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4(source, dest, default); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToRgb24(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4Slice3(source, dest, new DefaultShuffle4Slice3(SimdUtils.Shuffle.MMShuffle0123)); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToBgr24(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle4Slice3(source, dest, new DefaultShuffle4Slice3(SimdUtils.Shuffle.MMShuffle0321)); + } + + /// + /// Optimized converters from . + /// + public static class FromRgb24 + { + // Input pixels have: X = R, Y = G and Z = B. + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToRgba32(ReadOnlySpan source, Span dest) + => SimdUtils.Pad3Shuffle4(source, dest, default); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToArgb32(ReadOnlySpan source, Span dest) + => SimdUtils.Pad3Shuffle4(source, dest, new DefaultPad3Shuffle4(SimdUtils.Shuffle.MMShuffle2103)); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToBgra32(ReadOnlySpan source, Span dest) + => SimdUtils.Pad3Shuffle4(source, dest, new DefaultPad3Shuffle4(SimdUtils.Shuffle.MMShuffle3012)); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToAbgr32(ReadOnlySpan source, Span dest) + => SimdUtils.Pad3Shuffle4(source, dest, new DefaultPad3Shuffle4(SimdUtils.Shuffle.MMShuffle0123)); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToBgr24(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle3(source, dest, new DefaultShuffle3(SimdUtils.Shuffle.MMShuffle3012)); + } + + /// + /// Optimized converters from . + /// + public static class FromBgr24 + { + // Input pixels have: X = B, Y = G and Z = R. + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToArgb32(ReadOnlySpan source, Span dest) + => SimdUtils.Pad3Shuffle4(source, dest, new DefaultPad3Shuffle4(SimdUtils.Shuffle.MMShuffle0123)); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToRgba32(ReadOnlySpan source, Span dest) + => SimdUtils.Pad3Shuffle4(source, dest, new DefaultPad3Shuffle4(SimdUtils.Shuffle.MMShuffle3012)); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToBgra32(ReadOnlySpan source, Span dest) + => SimdUtils.Pad3Shuffle4(source, dest, default); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToAbgr32(ReadOnlySpan source, Span dest) + => SimdUtils.Pad3Shuffle4(source, dest, new DefaultPad3Shuffle4(SimdUtils.Shuffle.MMShuffle2103)); + + /// + /// Converts a representing a collection of + /// pixels to a representing + /// a collection of pixels. + /// + /// The source span of bytes. + /// The destination span of bytes. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToRgb24(ReadOnlySpan source, Span dest) + => SimdUtils.Shuffle3(source, dest, new DefaultShuffle3(SimdUtils.Shuffle.MMShuffle3012)); + } + } +} diff --git a/ImageSharp/PixelFormats/Utils/Vector4Converters.Default.cs b/ImageSharp/PixelFormats/Utils/Vector4Converters.Default.cs new file mode 100644 index 0000000..76b4a3b --- /dev/null +++ b/ImageSharp/PixelFormats/Utils/Vector4Converters.Default.cs @@ -0,0 +1,161 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.PixelFormats.Utils { + /// + /// Helper class for (bulk) conversion of buffers to/from other buffer types. + /// + internal static partial class Vector4Converters + { + /// + /// Provides default implementations for batched to/from conversion. + /// WARNING: The methods prefixed with "Unsafe" are operating without bounds checking and input validation! + /// Input validation is the responsibility of the caller! + /// + public static class Default + { + [MethodImpl(InliningOptions.ShortMethod)] + public static void FromVector4( + Span source, + Span destination, + PixelConversionModifiers modifiers) + where TPixel : unmanaged, IPixel + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + UnsafeFromVector4(source, destination, modifiers); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void ToVector4( + ReadOnlySpan source, + Span destination, + PixelConversionModifiers modifiers) + where TPixel : unmanaged, IPixel + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + UnsafeToVector4(source, destination, modifiers); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void UnsafeFromVector4( + Span source, + Span destination, + PixelConversionModifiers modifiers) + where TPixel : unmanaged, IPixel + { + ApplyBackwardConversionModifiers(source, modifiers); + + if (modifiers.IsDefined(PixelConversionModifiers.Scale)) + { + UnsafeFromScaledVector4Core(source, destination); + } + else + { + UnsafeFromVector4Core(source, destination); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void UnsafeToVector4( + ReadOnlySpan source, + Span destination, + PixelConversionModifiers modifiers) + where TPixel : unmanaged, IPixel + { + if (modifiers.IsDefined(PixelConversionModifiers.Scale)) + { + UnsafeToScaledVector4Core(source, destination); + } + else + { + UnsafeToVector4Core(source, destination); + } + + ApplyForwardConversionModifiers(destination, modifiers); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void UnsafeFromVector4Core( + ReadOnlySpan source, + Span destination) + where TPixel : unmanaged, IPixel + { + ref Vector4 sourceStart = ref MemoryMarshal.GetReference(source); + ref Vector4 sourceEnd = ref Unsafe.Add(ref sourceStart, (uint)source.Length); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) + { + destinationBase = TPixel.FromVector4(sourceStart); + + sourceStart = ref Unsafe.Add(ref sourceStart, 1); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void UnsafeToVector4Core( + ReadOnlySpan source, + Span destination) + where TPixel : unmanaged, IPixel + { + ref TPixel sourceStart = ref MemoryMarshal.GetReference(source); + ref TPixel sourceEnd = ref Unsafe.Add(ref sourceStart, (uint)source.Length); + ref Vector4 destinationBase = ref MemoryMarshal.GetReference(destination); + + while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) + { + destinationBase = sourceStart.ToVector4(); + + sourceStart = ref Unsafe.Add(ref sourceStart, 1); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void UnsafeFromScaledVector4Core( + ReadOnlySpan source, + Span destination) + where TPixel : unmanaged, IPixel + { + ref Vector4 sourceStart = ref MemoryMarshal.GetReference(source); + ref Vector4 sourceEnd = ref Unsafe.Add(ref sourceStart, (uint)source.Length); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) + { + destinationBase = TPixel.FromScaledVector4(sourceStart); + + sourceStart = ref Unsafe.Add(ref sourceStart, 1); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void UnsafeToScaledVector4Core( + ReadOnlySpan source, + Span destination) + where TPixel : unmanaged, IPixel + { + ref TPixel sourceStart = ref MemoryMarshal.GetReference(source); + ref TPixel sourceEnd = ref Unsafe.Add(ref sourceStart, (uint)source.Length); + ref Vector4 destinationBase = ref MemoryMarshal.GetReference(destination); + + while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) + { + destinationBase = sourceStart.ToScaledVector4(); + + sourceStart = ref Unsafe.Add(ref sourceStart, 1); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + } + } + } + } +} diff --git a/ImageSharp/PixelFormats/Utils/Vector4Converters.RgbaCompatible.cs b/ImageSharp/PixelFormats/Utils/Vector4Converters.RgbaCompatible.cs new file mode 100644 index 0000000..e6187e0 --- /dev/null +++ b/ImageSharp/PixelFormats/Utils/Vector4Converters.RgbaCompatible.cs @@ -0,0 +1,149 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats.Utils { + /// + /// Contains + /// + internal static partial class Vector4Converters + { + /// + /// Provides efficient implementations for batched to/from conversion. + /// which is applicable for -compatible pixel types where + /// returns the same scaled result as . + /// The method is works by internally converting to a therefore it's not applicable for that type! + /// + public static class RgbaCompatible + { + /// + /// It's not worth to bother the transitive pixel conversion method below this limit. + /// The value depends on the actual gain brought by the SIMD characteristics of the executing CPU and JIT. + /// + private static readonly int Vector4ConversionThreshold = CalculateVector4ConversionThreshold(); + + /// + /// Provides an efficient default implementation for + /// The method works by internally converting to a therefore it's not applicable for that type! + /// + /// The type of pixel format. + /// The configuration. + /// The pixel operations instance. + /// The source buffer. + /// The destination buffer. + /// The conversion modifier flags. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ToVector4( + Configuration configuration, + PixelOperations pixelOperations, + ReadOnlySpan source, + Span destination, + PixelConversionModifiers modifiers) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + int count = source.Length; + + // Not worth for small buffers: + if (count < Vector4ConversionThreshold) + { + Default.UnsafeToVector4(source, destination, modifiers); + + return; + } + + // Using the last quarter of 'destination' as a temporary buffer to avoid allocation: + int countWithoutLastItem = count - 1; + ReadOnlySpan reducedSource = source[..countWithoutLastItem]; + Span lastQuarterOfDestination = MemoryMarshal.Cast(destination).Slice((3 * count) + 1, countWithoutLastItem); + pixelOperations.ToRgba32(configuration, reducedSource, lastQuarterOfDestination); + + // 'destination' and 'lastQuarterOfDestination' are overlapping buffers, + // but we are always reading/writing at different positions: + SimdUtils.ByteToNormalizedFloat( + MemoryMarshal.Cast(lastQuarterOfDestination), + MemoryMarshal.Cast(destination[..countWithoutLastItem])); + + destination[countWithoutLastItem] = source[countWithoutLastItem].ToVector4(); + + // TODO: Investigate optimized 1-pass approach! + ApplyForwardConversionModifiers(destination, modifiers); + } + + /// + /// Provides an efficient default implementation for + /// The method is works by internally converting to a therefore it's not applicable for that type! + /// + /// The type of pixel format. + /// The configuration. + /// The pixel operations instance. + /// The source buffer. + /// The destination buffer. + /// The conversion modifier flags. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void FromVector4( + Configuration configuration, + PixelOperations pixelOperations, + Span source, + Span destination, + PixelConversionModifiers modifiers) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + int count = source.Length; + + // Not worth for small buffers: + if (count < Vector4ConversionThreshold) + { + Default.UnsafeFromVector4(source, destination, modifiers); + + return; + } + + // TODO: Investigate optimized 1-pass approach! + ApplyBackwardConversionModifiers(source, modifiers); + + // For the opposite direction it's not easy to implement the trick used in RunRgba32CompatibleToVector4Conversion, + // so let's allocate a temporary buffer as usually: + using IMemoryOwner tempBuffer = configuration.MemoryAllocator.Allocate(count); + Span tempSpan = tempBuffer.Memory.Span; + + SimdUtils.NormalizedFloatToByteSaturate( + MemoryMarshal.Cast(source), + MemoryMarshal.Cast(tempSpan)); + + pixelOperations.FromRgba32(configuration, tempSpan, destination); + } + + private static int CalculateVector4ConversionThreshold() + { + if (!Vector128.IsHardwareAccelerated) + { + return int.MaxValue; + } + + if (Vector512.IsHardwareAccelerated) + { + return 512; + } + + if (Vector256.IsHardwareAccelerated) + { + return 256; + } + + return 128; + } + } + } +} diff --git a/ImageSharp/PixelFormats/Utils/Vector4Converters.cs b/ImageSharp/PixelFormats/Utils/Vector4Converters.cs new file mode 100644 index 0000000..ac13689 --- /dev/null +++ b/ImageSharp/PixelFormats/Utils/Vector4Converters.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.ColorProfiles.Companding; + +namespace SixLabors.ImageSharp.PixelFormats.Utils { + internal static partial class Vector4Converters + { + /// + /// Apply modifiers used requested by ToVector4() conversion. + /// + /// The span of vectors. + /// The modifier rule. + [MethodImpl(InliningOptions.ShortMethod)] + internal static void ApplyForwardConversionModifiers(Span vectors, PixelConversionModifiers modifiers) + { + if (modifiers.IsDefined(PixelConversionModifiers.SRgbCompand)) + { + SRgbCompanding.Expand(vectors); + } + + if (modifiers.IsDefined(PixelConversionModifiers.Premultiply)) + { + Numerics.Premultiply(vectors); + } + } + + /// + /// Apply modifiers used requested by FromVector4() conversion. + /// + /// The span of vectors. + /// The modifier rule. + [MethodImpl(InliningOptions.ShortMethod)] + internal static void ApplyBackwardConversionModifiers(Span vectors, PixelConversionModifiers modifiers) + { + if (modifiers.IsDefined(PixelConversionModifiers.Premultiply)) + { + Numerics.UnPremultiply(vectors); + } + + if (modifiers.IsDefined(PixelConversionModifiers.SRgbCompand)) + { + SRgbCompanding.Compress(vectors); + } + } + } +} diff --git a/ImageSharp/Primitives/ColorMatrix.Impl.cs b/ImageSharp/Primitives/ColorMatrix.Impl.cs new file mode 100644 index 0000000..417fe98 --- /dev/null +++ b/ImageSharp/Primitives/ColorMatrix.Impl.cs @@ -0,0 +1,209 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#pragma warning disable SA1117 // Parameters should be on same line or separate lines +using System; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp { + /// + /// A structure encapsulating a 5x4 matrix used for transforming the color and alpha components of an image. + /// + public partial struct ColorMatrix + { + [UnscopedRef] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ref Impl AsImpl() => ref Unsafe.As(ref this); + + [UnscopedRef] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly ref readonly Impl AsROImpl() => ref Unsafe.As(ref Unsafe.AsRef(in this)); + + internal struct Impl : IEquatable + { + public Vector4 X; + public Vector4 Y; + public Vector4 Z; + public Vector4 W; + public Vector4 V; + + public static Impl Identity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + Impl result; + + result.X = Vector4.UnitX; + result.Y = Vector4.UnitY; + result.Z = Vector4.UnitZ; + result.W = Vector4.UnitW; + result.V = Vector4.Zero; + + return result; + } + } + + public readonly bool IsIdentity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => + (this.X == Vector4.UnitX) + && (this.Y == Vector4.UnitY) + && (this.Z == Vector4.UnitZ) + && (this.W == Vector4.UnitW) + && (this.V == Vector4.Zero); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Impl operator +(in Impl left, in Impl right) + { + Impl result; + + result.X = left.X + right.X; + result.Y = left.Y + right.Y; + result.Z = left.Z + right.Z; + result.W = left.W + right.W; + result.V = left.V + right.V; + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Impl operator -(in Impl left, in Impl right) + { + Impl result; + + result.X = left.X - right.X; + result.Y = left.Y - right.Y; + result.Z = left.Z - right.Z; + result.W = left.W - right.W; + result.V = left.V - right.V; + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Impl operator -(in Impl value) + { + Impl result; + + result.X = -value.X; + result.Y = -value.Y; + result.Z = -value.Z; + result.W = -value.W; + result.V = -value.V; + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Impl operator *(in Impl left, in Impl right) + { + Impl result; + + // result.X = Transform(left.X, in right); + result.X = right.X * left.X.X; + result.X += right.Y * left.X.Y; + result.X += right.Z * left.X.Z; + result.X += right.W * left.X.W; + + // result.Y = Transform(left.Y, in right); + result.Y = right.X * left.Y.X; + result.Y += right.Y * left.Y.Y; + result.Y += right.Z * left.Y.Z; + result.Y += right.W * left.Y.W; + + // result.Z = Transform(left.Z, in right); + result.Z = right.X * left.Z.X; + result.Z += right.Y * left.Z.Y; + result.Z += right.Z * left.Z.Z; + result.Z += right.W * left.Z.W; + + // result.W = Transform(left.W, in right); + result.W = right.X * left.W.X; + result.W += right.Y * left.W.Y; + result.W += right.Z * left.W.Z; + result.W += right.W * left.W.W; + + // result.V = Transform(left.V, in right); + result.V = right.X * left.V.X; + result.V += right.Y * left.V.Y; + result.V += right.Z * left.V.Z; + result.V += right.W * left.V.W; + + result.V += right.V; + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Impl operator *(in Impl left, float right) + { + Impl result; + + result.X = left.X * right; + result.Y = left.Y * right; + result.Z = left.Z * right; + result.W = left.W * right; + result.V = left.V * right; + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(in Impl left, in Impl right) => + (left.X == right.X) + && (left.Y == right.Y) + && (left.Z == right.Z) + && (left.W == right.W) + && (left.V == right.V); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(in Impl left, in Impl right) => + (left.X != right.X) + && (left.Y != right.Y) + && (left.Z != right.Z) + && (left.W != right.W) + && (left.V != right.V); + + [UnscopedRef] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref ColorMatrix AsColorMatrix() => ref Unsafe.As(ref this); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Init( + float m11, float m12, float m13, float m14, + float m21, float m22, float m23, float m24, + float m31, float m32, float m33, float m34, + float m41, float m42, float m43, float m44, + float m51, float m52, float m53, float m54) + { + this.X = new Vector4(m11, m12, m13, m14); + this.Y = new Vector4(m21, m22, m23, m24); + this.Z = new Vector4(m31, m32, m33, m34); + this.W = new Vector4(m41, m42, m43, m44); + this.V = new Vector4(m51, m52, m53, m54); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override readonly bool Equals([NotNullWhen(true)] object? obj) + => (obj is ColorMatrix other) && this.Equals(in other.AsImpl()); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Equals(in Impl other) => + this.X.Equals(other.X) + && this.Y.Equals(other.Y) + && this.Z.Equals(other.Z) + && this.W.Equals(other.W) + && this.V.Equals(other.V); + + bool IEquatable.Equals(Impl other) => this.Equals(in other); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override readonly int GetHashCode() => HashCode.Combine(this.X, this.Y, this.Z, this.W, this.V); + } + } +} diff --git a/ImageSharp/Primitives/ColorMatrix.cs b/ImageSharp/Primitives/ColorMatrix.cs new file mode 100644 index 0000000..3c57d6a --- /dev/null +++ b/ImageSharp/Primitives/ColorMatrix.cs @@ -0,0 +1,263 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#pragma warning disable SA1117 // Parameters should be on same line or separate lines + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp { + /// + /// A structure encapsulating a 5x4 matrix used for transforming the color and alpha components of an image. + /// + [StructLayout(LayoutKind.Sequential)] + public partial struct ColorMatrix : IEquatable + { + /// + /// Value at row 1, column 1 of the matrix. + /// + public float M11; + + /// + /// Value at row 1, column 2 of the matrix. + /// + public float M12; + + /// + /// Value at row 1, column 3 of the matrix. + /// + public float M13; + + /// + /// Value at row 1, column 4 of the matrix. + /// + public float M14; + + /// + /// Value at row 2, column 1 of the matrix. + /// + public float M21; + + /// + /// Value at row 2, column 2 of the matrix. + /// + public float M22; + + /// + /// Value at row 2, column 3 of the matrix. + /// + public float M23; + + /// + /// Value at row 2, column 4 of the matrix. + /// + public float M24; + + /// + /// Value at row 3, column 1 of the matrix. + /// + public float M31; + + /// + /// Value at row 3, column 2 of the matrix. + /// + public float M32; + + /// + /// Value at row 3, column 3 of the matrix. + /// + public float M33; + + /// + /// Value at row 3, column 4 of the matrix. + /// + public float M34; + + /// + /// Value at row 4, column 1 of the matrix. + /// + public float M41; + + /// + /// Value at row 4, column 2 of the matrix. + /// + public float M42; + + /// + /// Value at row 4, column 3 of the matrix. + /// + public float M43; + + /// + /// Value at row 4, column 4 of the matrix. + /// + public float M44; + + /// + /// Value at row 5, column 1 of the matrix. + /// + public float M51; + + /// + /// Value at row 5, column 2 of the matrix. + /// + public float M52; + + /// + /// Value at row 5, column 3 of the matrix. + /// + public float M53; + + /// + /// Value at row 5, column 4 of the matrix. + /// + public float M54; + + /// + /// Initializes a new instance of the struct. + /// + /// The value at row 1, column 1 of the matrix. + /// The value at row 1, column 2 of the matrix. + /// The value at row 1, column 3 of the matrix. + /// The value at row 1, column 4 of the matrix. + /// The value at row 2, column 1 of the matrix. + /// The value at row 2, column 2 of the matrix. + /// The value at row 2, column 3 of the matrix. + /// The value at row 2, column 4 of the matrix. + /// The value at row 3, column 1 of the matrix. + /// The value at row 3, column 2 of the matrix. + /// The value at row 3, column 3 of the matrix. + /// The value at row 3, column 4 of the matrix. + /// The value at row 4, column 1 of the matrix. + /// The value at row 4, column 2 of the matrix. + /// The value at row 4, column 3 of the matrix. + /// The value at row 4, column 4 of the matrix. + /// The value at row 5, column 1 of the matrix. + /// The value at row 5, column 2 of the matrix. + /// The value at row 5, column 3 of the matrix. + /// The value at row 5, column 4 of the matrix. + public ColorMatrix(float m11, float m12, float m13, float m14, + float m21, float m22, float m23, float m24, + float m31, float m32, float m33, float m34, + float m41, float m42, float m43, float m44, + float m51, float m52, float m53, float m54) + { + Unsafe.SkipInit(out this); + + this.AsImpl().Init(m11, m12, m13, m14, m21, m22, m23, m24, m31, m32, m33, m34, m41, m42, m43, m44, m51, m52, m53, + m54); + } + + /// + /// Gets the multiplicative identity matrix. + /// + public static ColorMatrix Identity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Impl.Identity.AsColorMatrix(); + } + + /// + /// Gets a value indicating whether the matrix is the identity matrix. + /// + public bool IsIdentity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.AsROImpl().IsIdentity; + } + + /// + /// Adds two matrices together. + /// + /// The first source matrix. + /// The second source matrix. + /// The resulting matrix. + public static ColorMatrix operator +(ColorMatrix value1, ColorMatrix value2) + => (value1.AsImpl() + value2.AsImpl()).AsColorMatrix(); + + /// + /// Subtracts the second matrix from the first. + /// + /// The first source matrix. + /// The second source matrix. + /// The result of the subtraction. + public static ColorMatrix operator -(ColorMatrix value1, ColorMatrix value2) + => (value1.AsImpl() - value2.AsImpl()).AsColorMatrix(); + + /// + /// Returns a new matrix with the negated elements of the given matrix. + /// + /// The source matrix. + /// The negated matrix. + public static ColorMatrix operator -(ColorMatrix value) + => (-value.AsImpl()).AsColorMatrix(); + + /// + /// Multiplies a matrix by another matrix. + /// + /// The first source matrix. + /// The second source matrix. + /// The result of the multiplication. + public static ColorMatrix operator *(ColorMatrix value1, ColorMatrix value2) + => (value1.AsImpl() * value2.AsImpl()).AsColorMatrix(); + + /// + /// Multiplies a matrix by a scalar value. + /// + /// The source matrix. + /// The scaling factor. + /// The scaled matrix. + public static ColorMatrix operator *(ColorMatrix value1, float value2) + => (value1.AsImpl() * value2).AsColorMatrix(); + + /// + /// Returns a boolean indicating whether the given two matrices are equal. + /// + /// The first matrix to compare. + /// The second matrix to compare. + /// True if the given matrices are equal; False otherwise. + public static bool operator ==(ColorMatrix value1, ColorMatrix value2) + => value1.AsImpl() == value2.AsImpl(); + + /// + /// Returns a boolean indicating whether the given two matrices are not equal. + /// + /// The first matrix to compare. + /// The second matrix to compare. + /// True if the given matrices are equal; False otherwise. + public static bool operator !=(ColorMatrix value1, ColorMatrix value2) + => value1.AsImpl() != value2.AsImpl(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override readonly bool Equals([NotNullWhen(true)] object? obj) + => this.AsROImpl().Equals(obj); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Equals(ColorMatrix other) + => this.AsROImpl().Equals(in other.AsImpl()); + + /// + public override int GetHashCode() + => this.AsROImpl().GetHashCode(); + + /// + public override string ToString() + { + CultureInfo ci = CultureInfo.CurrentCulture; + + return string.Format( + ci, + "{{ {{M11:{0} M12:{1} M13:{2} M14:{3}}} {{M21:{4} M22:{5} M23:{6} M24:{7}}} {{M31:{8} M32:{9} M33:{10} M34:{11}}} {{M41:{12} M42:{13} M43:{14} M44:{15}}} {{M51:{16} M52:{17} M53:{18} M54:{19}}} }}", + this.M11.ToString(ci), this.M12.ToString(ci), this.M13.ToString(ci), this.M14.ToString(ci), + this.M21.ToString(ci), this.M22.ToString(ci), this.M23.ToString(ci), this.M24.ToString(ci), + this.M31.ToString(ci), this.M32.ToString(ci), this.M33.ToString(ci), this.M34.ToString(ci), + this.M41.ToString(ci), this.M42.ToString(ci), this.M43.ToString(ci), this.M44.ToString(ci), + this.M51.ToString(ci), this.M52.ToString(ci), this.M53.ToString(ci), this.M54.ToString(ci)); + } + } +} diff --git a/ImageSharp/Primitives/Complex64.cs b/ImageSharp/Primitives/Complex64.cs new file mode 100644 index 0000000..c593741 --- /dev/null +++ b/ImageSharp/Primitives/Complex64.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp { + /// + /// Represents a complex number, where the real and imaginary parts are stored as values. + /// + /// + /// This is a more efficient version of the type. + /// + internal readonly struct Complex64 : IEquatable + { + /// + /// The real part of the complex number + /// + public readonly float Real; + + /// + /// The imaginary part of the complex number + /// + public readonly float Imaginary; + + /// + /// Initializes a new instance of the struct. + /// + /// The real part in the complex number. + /// The imaginary part in the complex number. + public Complex64(float real, float imaginary) + { + this.Real = real; + this.Imaginary = imaginary; + } + + /// + /// Performs the multiplication operation between a instance and a scalar. + /// + /// The value to multiply. + /// The scalar to use to multiply the value. + /// The result + [MethodImpl(InliningOptions.ShortMethod)] + public static Complex64 operator *(Complex64 value, float scalar) => new(value.Real * scalar, value.Imaginary * scalar); + + /// + /// Performs the multiplication operation between a instance and a . + /// + /// The value to multiply. + /// The instance to use to multiply the value. + /// The result + [MethodImpl(InliningOptions.ShortMethod)] + public static ComplexVector4 operator *(Complex64 value, Vector4 vector) + { + return new ComplexVector4 { Real = vector * value.Real, Imaginary = vector * value.Imaginary }; + } + + /// + /// Performs the multiplication operation between a instance and a . + /// + /// The value to multiply. + /// The instance to use to multiply the value. + /// The result + [MethodImpl(InliningOptions.ShortMethod)] + public static ComplexVector4 operator *(Complex64 value, ComplexVector4 vector) + { + Vector4 real = (value.Real * vector.Real) - (value.Imaginary * vector.Imaginary); + Vector4 imaginary = (value.Real * vector.Imaginary) + (value.Imaginary * vector.Real); + return new ComplexVector4 { Real = real, Imaginary = imaginary }; + } + + /// + public bool Equals(Complex64 other) + { + return this.Real.Equals(other.Real) && this.Imaginary.Equals(other.Imaginary); + } + + /// + public override bool Equals(object? obj) => obj is Complex64 other && this.Equals(other); + + /// + public override int GetHashCode() + { + unchecked + { + return (this.Real.GetHashCode() * 397) ^ this.Imaginary.GetHashCode(); + } + } + + /// + public override string ToString() => $"{this.Real}{(this.Imaginary >= 0 ? "+" : string.Empty)}{this.Imaginary}j"; + } +} diff --git a/ImageSharp/Primitives/ComplexVector4.cs b/ImageSharp/Primitives/ComplexVector4.cs new file mode 100644 index 0000000..9581cd2 --- /dev/null +++ b/ImageSharp/Primitives/ComplexVector4.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp { + /// + /// A vector with 4 values of type . + /// + internal struct ComplexVector4 : IEquatable + { + /// + /// The real part of the complex vector + /// + public Vector4 Real; + + /// + /// The imaginary part of the complex number + /// + public Vector4 Imaginary; + + /// + /// Sums the values in the input to the current instance + /// + /// The input to sum + [MethodImpl(InliningOptions.ShortMethod)] + public void Sum(ComplexVector4 value) + { + this.Real += value.Real; + this.Imaginary += value.Imaginary; + } + + /// + /// Performs a weighted sum on the current instance according to the given parameters + /// + /// The 'a' parameter, for the real component + /// The 'b' parameter, for the imaginary component + /// The resulting value + [MethodImpl(InliningOptions.ShortMethod)] + public Vector4 WeightedSum(float a, float b) => (this.Real * a) + (this.Imaginary * b); + + /// + public bool Equals(ComplexVector4 other) + { + return this.Real.Equals(other.Real) && this.Imaginary.Equals(other.Imaginary); + } + + /// + public override bool Equals(object? obj) => obj is ComplexVector4 other && this.Equals(other); + + /// + public override int GetHashCode() + { + unchecked + { + return (this.Real.GetHashCode() * 397) ^ this.Imaginary.GetHashCode(); + } + } + } +} diff --git a/ImageSharp/Primitives/DenseMatrix{T}.cs b/ImageSharp/Primitives/DenseMatrix{T}.cs new file mode 100644 index 0000000..8718fdb --- /dev/null +++ b/ImageSharp/Primitives/DenseMatrix{T}.cs @@ -0,0 +1,279 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp { + /// + /// Represents a dense matrix with arbitrary elements. + /// Components that are adjacent in a column of the matrix are adjacent in the storage array. + /// The components are said to be stored in column major order. + /// + /// The type of elements in the matrix. + public readonly struct DenseMatrix : IEquatable> + where T : struct, IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The length of each side in the matrix. + public DenseMatrix(int length) + : this(length, length) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The number of columns. + /// The number of rows. + public DenseMatrix(int columns, int rows) + { + Guard.MustBeGreaterThan(columns, 0, nameof(columns)); + Guard.MustBeGreaterThan(rows, 0, nameof(rows)); + + this.Rows = rows; + this.Columns = columns; + this.Size = new Size(columns, rows); + this.Count = columns * rows; + this.Data = new T[this.Columns * this.Rows]; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The 2D array to provide access to. + public DenseMatrix(T[,] data) + { + Guard.NotNull(data, nameof(data)); + int rows = data.GetLength(0); + int columns = data.GetLength(1); + + Guard.MustBeGreaterThan(rows, 0, nameof(this.Rows)); + Guard.MustBeGreaterThan(columns, 0, nameof(this.Columns)); + + this.Rows = rows; + this.Columns = columns; + this.Size = new Size(columns, rows); + this.Count = this.Columns * this.Rows; + this.Data = new T[this.Columns * this.Rows]; + + for (int y = 0; y < this.Rows; y++) + { + for (int x = 0; x < this.Columns; x++) + { + ref T value = ref this[y, x]; + value = data[y, x]; + } + } + } + + /// + /// Initializes a new instance of the struct. + /// + /// The number of columns. + /// The number of rows. + /// The array to provide access to. + public DenseMatrix(int columns, int rows, Span data) + { + Guard.MustBeGreaterThan(rows, 0, nameof(this.Rows)); + Guard.MustBeGreaterThan(columns, 0, nameof(this.Columns)); + Guard.IsTrue(rows * columns == data.Length, nameof(data), "Length should be equal to ros * columns"); + + this.Rows = rows; + this.Columns = columns; + this.Size = new Size(columns, rows); + this.Count = this.Columns * this.Rows; + this.Data = new T[this.Columns * this.Rows]; + + data.CopyTo(this.Data); + } + + /// + /// Gets the 1D representation of the dense matrix. + /// + public readonly T[] Data { get; } + + /// + /// Gets the number of columns in the dense matrix. + /// + public readonly int Columns { get; } + + /// + /// Gets the number of rows in the dense matrix. + /// + public readonly int Rows { get; } + + /// + /// Gets the size of the dense matrix. + /// + public readonly Size Size { get; } + + /// + /// Gets the number of items in the array. + /// + public readonly int Count { get; } + + /// + /// Gets a span wrapping the . + /// + public Span Span => new(this.Data); + + /// + /// Gets or sets the item at the specified position. + /// + /// The row-coordinate of the item. Must be greater than or equal to zero and less than the height of the array. + /// The column-coordinate of the item. Must be greater than or equal to zero and less than the width of the array. + /// The at the specified position. + public ref T this[int row, int column] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + this.CheckCoordinates(row, column); + return ref this.Data[(row * this.Columns) + column]; + } + } + + /// + /// Performs an implicit conversion from a to a . + /// + /// The source array. + /// + /// The representation on the source data. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator DenseMatrix(T[,] data) => new(data); + + /// + /// Performs an implicit conversion from a to a . + /// + /// The source array. + /// + /// The representation on the source data. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#pragma warning disable SA1008 // Opening parenthesis should be spaced correctly + public static implicit operator T[,](in DenseMatrix data) +#pragma warning restore SA1008 // Opening parenthesis should be spaced correctly + { + T[,] result = new T[data.Rows, data.Columns]; + + for (int y = 0; y < data.Rows; y++) + { + for (int x = 0; x < data.Columns; x++) + { + ref T value = ref result[y, x]; + value = data[y, x]; + } + } + + return result; + } + + /// + /// Compares the two instances to determine whether they are unequal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator ==(DenseMatrix left, DenseMatrix right) + => left.Equals(right); + + /// + /// Compares the two instances to determine whether they are equal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator !=(DenseMatrix left, DenseMatrix right) + => !(left == right); + + /// + /// Transposes the rows and columns of the dense matrix. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public DenseMatrix Transpose() + { + DenseMatrix result = new(this.Rows, this.Columns); + + for (int y = 0; y < this.Rows; y++) + { + for (int x = 0; x < this.Columns; x++) + { + ref T value = ref result[x, y]; + value = this[y, x]; + } + } + + return result; + } + + /// + /// Fills the matrix with the given value + /// + /// The value to fill each item with + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Fill(T value) => this.Span.Fill(value); + + /// + /// Clears the matrix setting each value to the default value for the element type + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() => this.Span.Clear(); + + /// + /// Checks the coordinates to ensure they are within bounds. + /// + /// The y-coordinate of the item. Must be greater than zero and smaller than the height of the matrix. + /// The x-coordinate of the item. Must be greater than zero and smaller than the width of the matrix. + /// + /// Thrown if the coordinates are not within the bounds of the array. + /// + [Conditional("DEBUG")] + private void CheckCoordinates(int row, int column) + { + if (row < 0 || row >= this.Rows) + { + throw new ArgumentOutOfRangeException(nameof(row), row, $"{row} is outwith the matrix bounds."); + } + + if (column < 0 || column >= this.Columns) + { + throw new ArgumentOutOfRangeException(nameof(column), column, $"{column} is outwith the matrix bounds."); + } + } + + /// + public override bool Equals(object? obj) + => obj is DenseMatrix other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(DenseMatrix other) => + this.Columns == other.Columns + && this.Rows == other.Rows + && this.Span.SequenceEqual(other.Span); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() + { + HashCode code = default; + + code.Add(this.Columns); + code.Add(this.Rows); + + Span span = this.Span; + for (int i = 0; i < span.Length; i++) + { + code.Add(span[i]); + } + + return code.ToHashCode(); + } + } +} diff --git a/ImageSharp/Primitives/LongRational.cs b/ImageSharp/Primitives/LongRational.cs new file mode 100644 index 0000000..286a9ca --- /dev/null +++ b/ImageSharp/Primitives/LongRational.cs @@ -0,0 +1,224 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Globalization; +using System.Text; + +namespace SixLabors.ImageSharp { + /// + /// Represents a number that can be expressed as a fraction. + /// + /// + /// This is a very simplified implementation of a rational number designed for use with metadata only. + /// + internal readonly struct LongRational : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// + /// The number above the line in a vulgar fraction showing how many of the parts + /// indicated by the denominator are taken. + /// + /// + /// The number below the line in a vulgar fraction; a divisor. + /// + public LongRational(long numerator, long denominator) + { + this.Numerator = numerator; + this.Denominator = denominator; + } + + /// + /// Gets the numerator of a number. + /// + public long Numerator { get; } + + /// + /// Gets the denominator of a number. + /// + public long Denominator { get; } + + /// + /// Gets a value indicating whether this instance is indeterminate. + /// + public bool IsIndeterminate => this.Denominator == 0 && this.Numerator == 0; + + /// + /// Gets a value indicating whether this instance is an integer (n, 1) + /// + public bool IsInteger => this.Denominator == 1; + + /// + /// Gets a value indicating whether this instance is equal to negative infinity (-1, 0) + /// + public bool IsNegativeInfinity => this.Denominator == 0 && this.Numerator == -1; + + /// + /// Gets a value indicating whether this instance is equal to positive infinity (1, 0) + /// + public bool IsPositiveInfinity => this.Denominator == 0 && this.Numerator == 1; + + /// + /// Gets a value indicating whether this instance is equal to 0 (0, 1) + /// + public bool IsZero => this.Denominator == 1 && this.Numerator == 0; + + /// + public override bool Equals(object? obj) + => obj is LongRational longRational && this.Equals(longRational); + + /// + public bool Equals(LongRational other) + => this.Numerator == other.Numerator && this.Denominator == other.Denominator; + + /// + public override int GetHashCode() + => HashCode.Combine(this.Numerator, this.Denominator); + + /// + public override string ToString() + => this.ToString(CultureInfo.InvariantCulture); + + /// + /// Converts the numeric value of this instance to its equivalent string representation using + /// the specified culture-specific format information. + /// + /// + /// An object that supplies culture-specific formatting information. + /// + /// The + public string ToString(IFormatProvider provider) + { + if (this.IsIndeterminate) + { + return "[ Indeterminate ]"; + } + + if (this.IsPositiveInfinity) + { + return "[ PositiveInfinity ]"; + } + + if (this.IsNegativeInfinity) + { + return "[ NegativeInfinity ]"; + } + + if (this.IsZero) + { + return "0"; + } + + if (this.IsInteger) + { + return this.Numerator.ToString(provider); + } + + StringBuilder sb = new(); + sb.Append(this.Numerator.ToString(provider)) + .Append('/') + .Append(this.Denominator.ToString(provider)); + + return sb.ToString(); + } + + /// + /// Create a new instance of the struct from a double value. + /// + /// The to create the instance from. + /// Whether to use the best possible precision when parsing the value. + public static LongRational FromDouble(double value, bool bestPrecision) + { + if (value == 0.0) + { + return new LongRational(0, 1); + } + + if (double.IsNaN(value)) + { + return new LongRational(0, 0); + } + + if (double.IsPositiveInfinity(value)) + { + return new LongRational(1, 0); + } + + if (double.IsNegativeInfinity(value)) + { + return new LongRational(-1, 0); + } + + long numerator = 1; + long denominator = 1; + + double val = Math.Abs(value); + double df = numerator / (double)denominator; + double epsilon = bestPrecision ? double.Epsilon : .000001; + + while (Math.Abs(df - val) > epsilon) + { + if (df < val) + { + numerator++; + } + else + { + denominator++; + numerator = (int)(val * denominator); + } + + df = numerator / (double)denominator; + } + + if (value < 0.0) + { + numerator *= -1; + } + + return new LongRational(numerator, denominator).Simplify(); + } + + /// + /// Finds the greatest common divisor of two values. + /// + /// The first value + /// The second value + /// The + private static long GreatestCommonDivisor(long left, long right) + { + return right == 0 ? left : GreatestCommonDivisor(right, left % right); + } + + /// + /// Simplifies the + /// + public LongRational Simplify() + { + if (this.IsIndeterminate || + this.IsNegativeInfinity || + this.IsPositiveInfinity || + this.IsInteger || + this.IsZero) + { + return this; + } + + if (this.Numerator == this.Denominator) + { + return new LongRational(1, 1); + } + + long gcd = GreatestCommonDivisor(Math.Abs(this.Numerator), Math.Abs(this.Denominator)); + + if (gcd > 1) + { + return new LongRational(this.Numerator / gcd, this.Denominator / gcd); + } + + return this; + } + } +} diff --git a/ImageSharp/Primitives/Matrix3x2Extensions.cs b/ImageSharp/Primitives/Matrix3x2Extensions.cs new file mode 100644 index 0000000..3637fea --- /dev/null +++ b/ImageSharp/Primitives/Matrix3x2Extensions.cs @@ -0,0 +1,100 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp { + /// + /// Extension methods for the struct. + /// + public static class Matrix3x2Extensions + { + /// + /// Creates a translation matrix from the given vector. + /// + /// The translation position. + /// A translation matrix. + public static Matrix3x2 CreateTranslation(PointF position) => Matrix3x2.CreateTranslation(position); + + /// + /// Creates a scale matrix that is offset by a given center point. + /// + /// Value to scale by on the X-axis. + /// Value to scale by on the Y-axis. + /// The center point. + /// A scaling matrix. + public static Matrix3x2 CreateScale(float xScale, float yScale, PointF centerPoint) => Matrix3x2.CreateScale(xScale, yScale, centerPoint); + + /// + /// Creates a scale matrix from the given vector scale. + /// + /// The scale to use. + /// A scaling matrix. + public static Matrix3x2 CreateScale(SizeF scales) => Matrix3x2.CreateScale(scales); + + /// + /// Creates a scale matrix from the given vector scale with an offset from the given center point. + /// + /// The scale to use. + /// The center offset. + /// A scaling matrix. + public static Matrix3x2 CreateScale(SizeF scales, PointF centerPoint) => Matrix3x2.CreateScale(scales, centerPoint); + + /// + /// Creates a scale matrix that scales uniformly with the given scale with an offset from the given center. + /// + /// The uniform scale to use. + /// The center offset. + /// A scaling matrix. + public static Matrix3x2 CreateScale(float scale, PointF centerPoint) => Matrix3x2.CreateScale(scale, centerPoint); + + /// + /// Creates a skew matrix from the given angles in degrees. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// A skew matrix. + public static Matrix3x2 CreateSkewDegrees(float degreesX, float degreesY) => Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); + + /// + /// Creates a skew matrix from the given angles in radians and a center point. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The center point. + /// A skew matrix. + public static Matrix3x2 CreateSkew(float radiansX, float radiansY, PointF centerPoint) => Matrix3x2.CreateSkew(radiansX, radiansY, centerPoint); + + /// + /// Creates a skew matrix from the given angles in degrees and a center point. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The center point. + /// A skew matrix. + public static Matrix3x2 CreateSkewDegrees(float degreesX, float degreesY, PointF centerPoint) => Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), centerPoint); + + /// + /// Creates a rotation matrix using the given rotation in degrees. + /// + /// The amount of rotation, in degrees. + /// A rotation matrix. + public static Matrix3x2 CreateRotationDegrees(float degrees) => Matrix3x2.CreateRotation(GeometryUtilities.DegreeToRadian(degrees)); + + /// + /// Creates a rotation matrix using the given rotation in radians and a center point. + /// + /// The amount of rotation, in radians. + /// The center point. + /// A rotation matrix. + public static Matrix3x2 CreateRotation(float radians, PointF centerPoint) => Matrix3x2.CreateRotation(radians, centerPoint); + + /// + /// Creates a rotation matrix using the given rotation in degrees and a center point. + /// + /// The amount of rotation, in degrees. + /// The center point. + /// A rotation matrix. + public static Matrix3x2 CreateRotationDegrees(float degrees, PointF centerPoint) => Matrix3x2.CreateRotation(GeometryUtilities.DegreeToRadian(degrees), centerPoint); + } +} diff --git a/ImageSharp/Primitives/Number.cs b/ImageSharp/Primitives/Number.cs new file mode 100644 index 0000000..137b987 --- /dev/null +++ b/ImageSharp/Primitives/Number.cs @@ -0,0 +1,186 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Globalization; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp { + /// + /// Represents an integral number. + /// + [StructLayout(LayoutKind.Explicit)] + public struct Number : IEquatable, IComparable + { + [FieldOffset(0)] + private readonly int signedValue; + + [FieldOffset(0)] + private readonly uint unsignedValue; + + [FieldOffset(4)] + private readonly bool isSigned; + + /// + /// Initializes a new instance of the struct. + /// + /// The value of the number. + public Number(int value) + : this() + { + this.signedValue = value; + this.isSigned = true; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The value of the number. + public Number(uint value) + : this() + { + this.unsignedValue = value; + this.isSigned = false; + } + + /// + /// Converts the specified to an instance of this type. + /// + /// The value. + public static implicit operator Number(int value) => new(value); + + /// + /// Converts the specified to an instance of this type. + /// + /// The value. + public static implicit operator Number(uint value) => new(value); + + /// + /// Converts the specified to an instance of this type. + /// + /// The value. + public static implicit operator Number(ushort value) => new((uint)value); + + /// + /// Converts the specified to a . + /// + /// The to convert. + public static explicit operator int(Number number) + { + return number.isSigned + ? number.signedValue + : (int)Numerics.Clamp(number.unsignedValue, 0, int.MaxValue); + } + + /// + /// Converts the specified to a . + /// + /// The to convert. + public static explicit operator uint(Number number) + { + return number.isSigned + ? (uint)Numerics.Clamp(number.signedValue, 0, int.MaxValue) + : number.unsignedValue; + } + + /// + /// Converts the specified to a . + /// + /// The to convert. + public static explicit operator ushort(Number number) + { + return number.isSigned + ? (ushort)Numerics.Clamp(number.signedValue, ushort.MinValue, ushort.MaxValue) + : (ushort)Numerics.Clamp(number.unsignedValue, ushort.MinValue, ushort.MaxValue); + } + + /// + /// Determines whether the specified instances are considered equal. + /// + /// The first to compare. + /// The second to compare. + public static bool operator ==(Number left, Number right) => Equals(left, right); + + /// + /// Determines whether the specified instances are not considered equal. + /// + /// The first to compare. + /// The second to compare. + public static bool operator !=(Number left, Number right) => !Equals(left, right); + + /// + /// Determines whether the first is more than the second . + /// + /// The first to compare. + /// The second to compare. + public static bool operator >(Number left, Number right) => left.CompareTo(right) == 1; + + /// + /// Determines whether the first is less than the second . + /// + /// The first to compare. + /// The second to compare. + public static bool operator <(Number left, Number right) => left.CompareTo(right) == -1; + + /// + /// Determines whether the first is more than or equal to the second . + /// + /// The first to compare. + /// The second to compare. + public static bool operator >=(Number left, Number right) => left.CompareTo(right) >= 0; + + /// + /// Determines whether the first is less than or equal to the second . + /// + /// The first to compare. + /// The second to compare. + public static bool operator <=(Number left, Number right) => left.CompareTo(right) <= 0; + + /// + public int CompareTo(Number other) + { + return this.isSigned + ? this.signedValue.CompareTo(other.signedValue) + : this.unsignedValue.CompareTo(other.unsignedValue); + } + + /// + public override bool Equals(object? obj) => obj is Number other && this.Equals(other); + + /// + public bool Equals(Number other) + { + if (this.isSigned != other.isSigned) + { + return false; + } + + return this.isSigned + ? this.signedValue.Equals(other.signedValue) + : this.unsignedValue.Equals(other.unsignedValue); + } + + /// + public override int GetHashCode() + { + return this.isSigned + ? this.signedValue.GetHashCode() + : this.unsignedValue.GetHashCode(); + } + + /// + public override string ToString() => this.ToString(CultureInfo.InvariantCulture); + + /// + /// Converts the numeric value of this instance to its equivalent string representation using the specified culture-specific format information. + /// + /// An object that supplies culture-specific formatting information. + /// The string representation of the value of this instance, which consists of a sequence of digits ranging from 0 to 9, without a sign or leading zeros. + public string ToString(IFormatProvider provider) + { + return this.isSigned + ? this.signedValue.ToString(provider) + : this.unsignedValue.ToString(provider); + } + } +} diff --git a/ImageSharp/Primitives/Point.cs b/ImageSharp/Primitives/Point.cs new file mode 100644 index 0000000..8b32ddf --- /dev/null +++ b/ImageSharp/Primitives/Point.cs @@ -0,0 +1,300 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.ComponentModel; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp { + /// + /// Represents an ordered pair of integer x- and y-coordinates that defines a point in + /// a two-dimensional plane. + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + public struct Point : IEquatable + { + /// + /// Represents a that has X and Y values set to zero. + /// + public static readonly Point Empty; + + /// + /// Initializes a new instance of the struct. + /// + /// The horizontal and vertical position of the point. + public Point(int value) + : this() + { + this.X = LowInt16(value); + this.Y = HighInt16(value); + } + + /// + /// Initializes a new instance of the struct. + /// + /// The horizontal position of the point. + /// The vertical position of the point. + public Point(int x, int y) + : this() + { + this.X = x; + this.Y = y; + } + + /// + /// Initializes a new instance of the struct from the given . + /// + /// The size. + public Point(Size size) + { + this.X = size.Width; + this.Y = size.Height; + } + + /// + /// Gets or sets the x-coordinate of this . + /// + public int X { get; set; } + + /// + /// Gets or sets the y-coordinate of this . + /// + public int Y { get; set; } + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly bool IsEmpty => this.Equals(Empty); + + /// + /// Creates a with the coordinates of the specified . + /// + /// The point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator PointF(Point point) => new(point.X, point.Y); + + /// + /// Creates a with the coordinates of the specified . + /// + /// The point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Vector2(Point point) => new(point.X, point.Y); + + /// + /// Creates a with the coordinates of the specified . + /// + /// The point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator Size(Point point) => new(point.X, point.Y); + + /// + /// Negates the given point by multiplying all values by -1. + /// + /// The source point. + /// The negated point. + public static Point operator -(Point value) => new(-value.X, -value.Y); + + /// + /// Translates a by a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point operator +(Point point, Size size) => Add(point, size); + + /// + /// Translates a by the negative of a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point operator -(Point point, Size size) => Subtract(point, size); + + /// + /// Multiplies by a producing . + /// + /// Multiplier of type . + /// Multiplicand of type . + /// Product of type . + public static Point operator *(int left, Point right) => Multiply(right, left); + + /// + /// Multiplies by a producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type . + public static Point operator *(Point left, int right) => Multiply(left, right); + + /// + /// Divides by a producing . + /// + /// Dividend of type . + /// Divisor of type . + /// Result of type . + public static Point operator /(Point left, int right) + => new(left.X / right, left.Y / right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Point left, Point right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Point left, Point right) => !left.Equals(right); + + /// + /// Translates a by the negative of a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point Add(Point point, Size size) => new(unchecked(point.X + size.Width), unchecked(point.Y + size.Height)); + + /// + /// Translates a by the negative of a given value. + /// + /// The point on the left hand of the operand. + /// The value on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point Multiply(Point point, int value) => new(unchecked(point.X * value), unchecked(point.Y * value)); + + /// + /// Translates a by the negative of a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point Subtract(Point point, Size size) => new(unchecked(point.X - size.Width), unchecked(point.Y - size.Height)); + + /// + /// Converts a to a by performing a ceiling operation on all the coordinates. + /// + /// The point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point Ceiling(PointF point) => new(unchecked((int)MathF.Ceiling(point.X)), unchecked((int)MathF.Ceiling(point.Y))); + + /// + /// Converts a to a by performing a round operation on all the coordinates. + /// + /// The point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point Round(PointF point) => new(unchecked((int)MathF.Round(point.X)), unchecked((int)MathF.Round(point.Y))); + + /// + /// Converts a to a by performing a round operation on all the coordinates. + /// + /// The vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point Round(Vector2 vector) => new(unchecked((int)MathF.Round(vector.X)), unchecked((int)MathF.Round(vector.Y))); + + /// + /// Converts a to a by performing a truncate operation on all the coordinates. + /// + /// The point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point Truncate(PointF point) => new(unchecked((int)point.X), unchecked((int)point.Y)); + + /// + /// Transforms a point by a specified 3x2 matrix. + /// + /// The point to transform. + /// The transformation matrix used. + /// The transformed . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Transform(Point point, Matrix3x2 matrix) + => Vector2.Transform(new Vector2(point.X, point.Y), matrix); + + /// + /// Transforms a point by a specified 4x4 matrix, applying a projective transform + /// flattened into 2D space. + /// + /// The point to transform. + /// The transformation matrix used. + /// The transformed . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Transform(Point point, Matrix4x4 matrix) + => TransformUtilities.ProjectiveTransform2D(point.X, point.Y, matrix); + + /// + /// Deconstructs this point into two integers. + /// + /// The out value for X. + /// The out value for Y. + public readonly void Deconstruct(out int x, out int y) + { + x = this.X; + y = this.Y; + } + + /// + /// Translates this by the specified amount. + /// + /// The amount to offset the x-coordinate. + /// The amount to offset the y-coordinate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Offset(int dx, int dy) + { + unchecked + { + this.X += dx; + this.Y += dy; + } + } + + /// + /// Translates this by the specified amount. + /// + /// The used offset this . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Offset(Point point) => this.Offset(point.X, point.Y); + + /// + public override readonly int GetHashCode() => HashCode.Combine(this.X, this.Y); + + /// + public override readonly string ToString() => $"Point [ X={this.X}, Y={this.Y} ]"; + + /// + public override readonly bool Equals(object? obj) => obj is Point other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Equals(Point other) => this.X.Equals(other.X) && this.Y.Equals(other.Y); + + private static short HighInt16(int n) => unchecked((short)((n >> 16) & 0xffff)); + + private static short LowInt16(int n) => unchecked((short)(n & 0xffff)); + } +} diff --git a/ImageSharp/Primitives/PointF.cs b/ImageSharp/Primitives/PointF.cs new file mode 100644 index 0000000..9526917 --- /dev/null +++ b/ImageSharp/Primitives/PointF.cs @@ -0,0 +1,304 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.ComponentModel; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp { + /// + /// Represents an ordered pair of single precision floating point x- and y-coordinates that defines a point in + /// a two-dimensional plane. + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + public struct PointF : IEquatable + { + /// + /// Represents a that has X and Y values set to zero. + /// + public static readonly PointF Empty; + + /// + /// Initializes a new instance of the struct. + /// + /// The horizontal position of the point. + /// The vertical position of the point. + public PointF(float x, float y) + : this() + { + this.X = x; + this.Y = y; + } + + /// + /// Initializes a new instance of the struct from the given . + /// + /// The size. + public PointF(SizeF size) + { + this.X = size.Width; + this.Y = size.Height; + } + + /// + /// Gets or sets the x-coordinate of this . + /// + public float X { get; set; } + + /// + /// Gets or sets the y-coordinate of this . + /// + public float Y { get; set; } + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly bool IsEmpty => this.Equals(Empty); + + /// + /// Creates a with the coordinates of the specified . + /// + /// The vector. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator PointF(Vector2 vector) => new(vector.X, vector.Y); + + /// + /// Creates a with the coordinates of the specified . + /// + /// The point. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Vector2(PointF point) => new(point.X, point.Y); + + /// + /// Creates a with the coordinates of the specified by truncating each of the coordinates. + /// + /// The point. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator Point(PointF point) => Point.Truncate(point); + + /// + /// Negates the given point by multiplying all values by -1. + /// + /// The source point. + /// The negated point. + public static PointF operator -(PointF value) => new(-value.X, -value.Y); + + /// + /// Translates a by a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF operator +(PointF point, SizeF size) => Add(point, size); + + /// + /// Translates a by the negative of a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF operator -(PointF point, PointF size) => Subtract(point, size); + + /// + /// Translates a by a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF operator +(PointF point, PointF size) => Add(point, size); + + /// + /// Translates a by the negative of a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF operator -(PointF point, SizeF size) => Subtract(point, size); + + /// + /// Multiplies by a producing . + /// + /// Multiplier of type . + /// Multiplicand of type . + /// Product of type . + public static PointF operator *(float left, PointF right) => Multiply(right, left); + + /// + /// Multiplies by a producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type . + public static PointF operator *(PointF left, float right) => Multiply(left, right); + + /// + /// Divides by a producing . + /// + /// Dividend of type . + /// Divisor of type . + /// Result of type . + public static PointF operator /(PointF left, float right) + => new(left.X / right, left.Y / right); + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(PointF left, PointF right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(PointF left, PointF right) => !left.Equals(right); + + /// + /// Translates a by the given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Add(PointF point, SizeF size) => new(point.X + size.Width, point.Y + size.Height); + + /// + /// Translates a by the given . + /// + /// The point on the left hand of the operand. + /// The point on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Add(PointF point, PointF pointb) => new(point.X + pointb.X, point.Y + pointb.Y); + + /// + /// Translates a by the negative of a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Subtract(PointF point, SizeF size) => new(point.X - size.Width, point.Y - size.Height); + + /// + /// Translates a by the negative of a given . + /// + /// The point on the left hand of the operand. + /// The point on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Subtract(PointF point, PointF pointb) => new(point.X - pointb.X, point.Y - pointb.Y); + + /// + /// Translates a by the multiplying the X and Y by the given value. + /// + /// The point on the left hand of the operand. + /// The value on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Multiply(PointF point, float right) => new(point.X * right, point.Y * right); + + /// + /// Transforms a point by a specified 3x2 matrix. + /// + /// The point to transform. + /// The transformation matrix used. + /// The transformed . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Transform(PointF point, Matrix3x2 matrix) => Vector2.Transform(point, matrix); + + /// + /// Transforms a point by a specified 4x4 matrix, applying a projective transform + /// flattened into 2D space. + /// + /// The point to transform. + /// The transformation matrix used. + /// The transformed . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Transform(PointF point, Matrix4x4 matrix) + => TransformUtilities.ProjectiveTransform2D(point.X, point.Y, matrix); + + /// + /// Deconstructs this point into two floats. + /// + /// The out value for X. + /// The out value for Y. + public readonly void Deconstruct(out float x, out float y) + { + x = this.X; + y = this.Y; + } + + /// + /// Translates this by the specified amount. + /// + /// The amount to offset the x-coordinate. + /// The amount to offset the y-coordinate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Offset(float dx, float dy) + { + this.X += dx; + this.Y += dy; + } + + /// + /// Translates this by the specified amount. + /// + /// The used offset this . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Offset(PointF point) => this.Offset(point.X, point.Y); + + /// + public override readonly int GetHashCode() => HashCode.Combine(this.X, this.Y); + + /// + public override readonly string ToString() => $"PointF [ X={this.X}, Y={this.Y} ]"; + + /// + public override readonly bool Equals(object? obj) => obj is PointF pointF && this.Equals(pointF); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Equals(PointF other) => this.X.Equals(other.X) && this.Y.Equals(other.Y); + } +} diff --git a/ImageSharp/Primitives/Rational.cs b/ImageSharp/Primitives/Rational.cs new file mode 100644 index 0000000..5f475d2 --- /dev/null +++ b/ImageSharp/Primitives/Rational.cs @@ -0,0 +1,171 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Globalization; + +namespace SixLabors.ImageSharp { + /// + /// Represents a number that can be expressed as a fraction. + /// + /// + /// This is a very simplified implementation of a rational number designed for use with metadata only. + /// + public readonly struct Rational : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The to create the rational from. + public Rational(uint value) + : this(value, 1) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The number above the line in a vulgar fraction showing how many of the parts indicated by the denominator are taken. + /// The number below the line in a vulgar fraction; a divisor. + public Rational(uint numerator, uint denominator) + : this(numerator, denominator, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The number above the line in a vulgar fraction showing how many of the parts indicated by the denominator are taken. + /// The number below the line in a vulgar fraction; a divisor. + /// Specified if the rational should be simplified. + public Rational(uint numerator, uint denominator, bool simplify) + { + if (simplify) + { + LongRational rational = new LongRational(numerator, denominator).Simplify(); + + this.Numerator = (uint)rational.Numerator; + this.Denominator = (uint)rational.Denominator; + } + else + { + this.Numerator = numerator; + this.Denominator = denominator; + } + } + + /// + /// Initializes a new instance of the struct. + /// + /// The to create the instance from. + public Rational(double value) + : this(value, false) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The to create the instance from. + /// Whether to use the best possible precision when parsing the value. + public Rational(double value, bool bestPrecision) + { + LongRational rational = LongRational.FromDouble(Math.Abs(value), bestPrecision); + + this.Numerator = (uint)rational.Numerator; + this.Denominator = (uint)rational.Denominator; + } + + /// + /// Gets the numerator of a number. + /// + public uint Numerator { get; } + + /// + /// Gets the denominator of a number. + /// + public uint Denominator { get; } + + /// + /// Determines whether the specified instances are considered equal. + /// + /// The first to compare. + /// The second to compare. + /// The + public static bool operator ==(Rational left, Rational right) => left.Equals(right); + + /// + /// Determines whether the specified instances are not considered equal. + /// + /// The first to compare. + /// The second to compare. + /// The + public static bool operator !=(Rational left, Rational right) => !left.Equals(right); + + /// + /// Converts the specified to an instance of this type. + /// + /// The to convert to an instance of this type. + /// + /// The . + /// + public static Rational FromDouble(double value) => new(value, false); + + /// + /// Converts the specified to an instance of this type. + /// + /// The to convert to an instance of this type. + /// Whether to use the best possible precision when parsing the value. + /// + /// The . + /// + public static Rational FromDouble(double value, bool bestPrecision) => new(value, bestPrecision); + + /// + public override bool Equals(object? obj) => obj is Rational other && this.Equals(other); + + /// + public bool Equals(Rational other) + => this.Numerator == other.Numerator && this.Denominator == other.Denominator; + + /// + public override int GetHashCode() + { + LongRational self = new(this.Numerator, this.Denominator); + return self.GetHashCode(); + } + + /// + /// Converts a rational number to the nearest . + /// + /// + /// The . + /// + public double ToDouble() => this.Numerator / (double)this.Denominator; + + /// + /// Converts a rational number to the nearest . + /// + /// + /// The . + /// + public float ToSingle() => this.Numerator / (float)this.Denominator; + + /// + public override string ToString() => this.ToString(CultureInfo.InvariantCulture); + + /// + /// Converts the numeric value of this instance to its equivalent string representation using + /// the specified culture-specific format information. + /// + /// + /// An object that supplies culture-specific formatting information. + /// + /// The + public string ToString(IFormatProvider provider) + { + LongRational rational = new(this.Numerator, this.Denominator); + return rational.ToString(provider); + } + } +} diff --git a/ImageSharp/Primitives/Rectangle.cs b/ImageSharp/Primitives/Rectangle.cs new file mode 100644 index 0000000..c1f236e --- /dev/null +++ b/ImageSharp/Primitives/Rectangle.cs @@ -0,0 +1,462 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.ComponentModel; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp { + /// + /// Stores a set of four integers that represent the location and size of a rectangle. + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + public struct Rectangle : IEquatable + { + /// + /// Represents a that has X, Y, Width, and Height values set to zero. + /// + public static readonly Rectangle Empty; + + /// + /// Initializes a new instance of the struct. + /// + /// The horizontal position of the rectangle. + /// The vertical position of the rectangle. + /// The width of the rectangle. + /// The height of the rectangle. + public Rectangle(int x, int y, int width, int height) + { + this.X = x; + this.Y = y; + this.Width = width; + this.Height = height; + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The which specifies the rectangles point in a two-dimensional plane. + /// + /// + /// The which specifies the rectangles height and width. + /// + public Rectangle(Point point, Size size) + { + this.X = point.X; + this.Y = point.Y; + this.Width = size.Width; + this.Height = size.Height; + } + + /// + /// Gets or sets the x-coordinate of this . + /// + public int X { get; set; } + + /// + /// Gets or sets the y-coordinate of this . + /// + public int Y { get; set; } + + /// + /// Gets or sets the width of this . + /// + public int Width { get; set; } + + /// + /// Gets or sets the height of this . + /// + public int Height { get; set; } + + /// + /// Gets or sets the coordinates of the upper-left corner of the rectangular region represented by this . + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public Point Location + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(this.X, this.Y); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + this.X = value.X; + this.Y = value.Y; + } + } + + /// + /// Gets or sets the size of this . + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public Size Size + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(this.Width, this.Height); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + this.Width = value.Width; + this.Height = value.Height; + } + } + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEmpty => this.Equals(Empty); + + /// + /// Gets the y-coordinate of the top edge of this . + /// + public int Top => this.Y; + + /// + /// Gets the x-coordinate of the right edge of this . + /// + public int Right + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => unchecked(this.X + this.Width); + } + + /// + /// Gets the y-coordinate of the bottom edge of this . + /// + public int Bottom + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => unchecked(this.Y + this.Height); + } + + /// + /// Gets the x-coordinate of the left edge of this . + /// + public int Left => this.X; + + /// + /// Creates a with the coordinates of the specified . + /// + /// The rectangle. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator RectangleF(Rectangle rectangle) => new(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + + /// + /// Creates a with the coordinates of the specified . + /// + /// The rectangle. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Vector4(Rectangle rectangle) => new(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Rectangle left, Rectangle right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Rectangle left, Rectangle right) => !left.Equals(right); + + /// + /// Creates a new with the specified location and size. + /// The left coordinate of the rectangle. + /// The top coordinate of the rectangle. + /// The right coordinate of the rectangle. + /// The bottom coordinate of the rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + + // ReSharper disable once InconsistentNaming + public static Rectangle FromLTRB(int left, int top, int right, int bottom) => new(left, top, unchecked(right - left), unchecked(bottom - top)); + + /// + /// Returns the center point of the given . + /// + /// The rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point Center(Rectangle rectangle) => new(rectangle.Left + (rectangle.Width >> 1), rectangle.Top + (rectangle.Height >> 1)); // >> 1 is bit-hack for / 2 + + /// + /// Creates a rectangle that represents the intersection between and + /// . If there is no intersection, an empty rectangle is returned. + /// + /// The first rectangle. + /// The second rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rectangle Intersect(Rectangle a, Rectangle b) + { + int x1 = Math.Max(a.X, b.X); + int x2 = Math.Min(a.Right, b.Right); + int y1 = Math.Max(a.Y, b.Y); + int y2 = Math.Min(a.Bottom, b.Bottom); + + if (x2 >= x1 && y2 >= y1) + { + return new Rectangle(x1, y1, x2 - x1, y2 - y1); + } + + return Empty; + } + + /// + /// Creates a that is inflated by the specified amount. + /// + /// The rectangle. + /// The amount to inflate the width by. + /// The amount to inflate the height by. + /// A new . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rectangle Inflate(Rectangle rectangle, int x, int y) + { + Rectangle r = rectangle; + r.Inflate(x, y); + return r; + } + + /// + /// Converts a to a by performing a ceiling operation on all the coordinates. + /// + /// The rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rectangle Ceiling(RectangleF rectangle) + { + unchecked + { + return new Rectangle( + (int)MathF.Ceiling(rectangle.X), + (int)MathF.Ceiling(rectangle.Y), + (int)MathF.Ceiling(rectangle.Width), + (int)MathF.Ceiling(rectangle.Height)); + } + } + + /// + /// Transforms a rectangle by the given matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// A transformed rectangle. + public static RectangleF Transform(Rectangle rectangle, Matrix3x2 matrix) + => RectangleF.Transform(rectangle, matrix); + + /// + /// Transforms a rectangle by the given 4x4 matrix, applying a projective transform + /// flattened into 2D space. + /// + /// The source rectangle. + /// The transformation matrix. + /// A transformed rectangle. + public static RectangleF Transform(Rectangle rectangle, Matrix4x4 matrix) + => RectangleF.Transform(rectangle, matrix); + + /// + /// Converts a to a by performing a truncate operation on all the coordinates. + /// + /// The rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rectangle Truncate(RectangleF rectangle) + { + unchecked + { + return new Rectangle( + (int)rectangle.X, + (int)rectangle.Y, + (int)rectangle.Width, + (int)rectangle.Height); + } + } + + /// + /// Converts a to a by performing a round operation on all the coordinates. + /// + /// The rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rectangle Round(RectangleF rectangle) + { + unchecked + { + return new Rectangle( + (int)MathF.Round(rectangle.X), + (int)MathF.Round(rectangle.Y), + (int)MathF.Round(rectangle.Width), + (int)MathF.Round(rectangle.Height)); + } + } + + /// + /// Creates a rectangle that represents the union between and . + /// + /// The first rectangle. + /// The second rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rectangle Union(Rectangle a, Rectangle b) + { + int x1 = Math.Min(a.X, b.X); + int x2 = Math.Max(a.Right, b.Right); + int y1 = Math.Min(a.Y, b.Y); + int y2 = Math.Max(a.Bottom, b.Bottom); + + return new Rectangle(x1, y1, x2 - x1, y2 - y1); + } + + /// + /// Deconstructs this rectangle into four integers. + /// + /// The out value for X. + /// The out value for Y. + /// The out value for the width. + /// The out value for the height. + public void Deconstruct(out int x, out int y, out int width, out int height) + { + x = this.X; + y = this.Y; + width = this.Width; + height = this.Height; + } + + /// + /// Creates a Rectangle that represents the intersection between this Rectangle and the . + /// + /// The rectangle. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Intersect(Rectangle rectangle) + { + Rectangle result = Intersect(rectangle, this); + + this.X = result.X; + this.Y = result.Y; + this.Width = result.Width; + this.Height = result.Height; + } + + /// + /// Inflates this by the specified amount. + /// + /// The width. + /// The height. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Inflate(int width, int height) + { + unchecked + { + this.X -= width; + this.Y -= height; + + this.Width += 2 * width; + this.Height += 2 * height; + } + } + + /// + /// Inflates this by the specified amount. + /// + /// The size. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Inflate(Size size) => this.Inflate(size.Width, size.Height); + + /// + /// Determines if the specified point is contained within the rectangular region defined by + /// this . + /// + /// The x-coordinate of the given point. + /// The y-coordinate of the given point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(int x, int y) => this.X <= x && x < this.Right && this.Y <= y && y < this.Bottom; + + /// + /// Determines if the specified point is contained within the rectangular region defined by this . + /// + /// The point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(Point point) => this.Contains(point.X, point.Y); + + /// + /// Determines if the rectangular region represented by is entirely contained + /// within the rectangular region represented by this . + /// + /// The rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(Rectangle rectangle) => + (this.X <= rectangle.X) && (rectangle.Right <= this.Right) && + (this.Y <= rectangle.Y) && (rectangle.Bottom <= this.Bottom); + + /// + /// Determines if the specified intersects the rectangular region defined by + /// this . + /// + /// The other Rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IntersectsWith(Rectangle rectangle) => + (rectangle.X < this.Right) && (this.X < rectangle.Right) && + (rectangle.Y < this.Bottom) && (this.Y < rectangle.Bottom); + + /// + /// Adjusts the location of this rectangle by the specified amount. + /// + /// The point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Offset(Point point) => this.Offset(point.X, point.Y); + + /// + /// Adjusts the location of this rectangle by the specified amount. + /// + /// The amount to offset the x-coordinate. + /// The amount to offset the y-coordinate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Offset(int dx, int dy) + { + unchecked + { + this.X += dx; + this.Y += dy; + } + } + + /// + public override int GetHashCode() => HashCode.Combine(this.X, this.Y, this.Width, this.Height); + + /// + public override string ToString() => $"Rectangle [ X={this.X}, Y={this.Y}, Width={this.Width}, Height={this.Height} ]"; + + /// + public override bool Equals(object? obj) => obj is Rectangle other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Rectangle other) => + this.X.Equals(other.X) && + this.Y.Equals(other.Y) && + this.Width.Equals(other.Width) && + this.Height.Equals(other.Height); + } +} diff --git a/ImageSharp/Primitives/RectangleF.cs b/ImageSharp/Primitives/RectangleF.cs new file mode 100644 index 0000000..2e0f11c --- /dev/null +++ b/ImageSharp/Primitives/RectangleF.cs @@ -0,0 +1,421 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.ComponentModel; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp { + /// + /// Stores a set of four single precision floating points that represent the location and size of a rectangle. + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + public struct RectangleF : IEquatable + { + /// + /// Represents a that has X, Y, Width, and Height values set to zero. + /// + public static readonly RectangleF Empty; + + /// + /// Initializes a new instance of the struct. + /// + /// The horizontal position of the rectangle. + /// The vertical position of the rectangle. + /// The width of the rectangle. + /// The height of the rectangle. + public RectangleF(float x, float y, float width, float height) + { + this.X = x; + this.Y = y; + this.Width = width; + this.Height = height; + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The which specifies the rectangles point in a two-dimensional plane. + /// + /// + /// The which specifies the rectangles height and width. + /// + public RectangleF(PointF point, SizeF size) + { + this.X = point.X; + this.Y = point.Y; + this.Width = size.Width; + this.Height = size.Height; + } + + /// + /// Gets or sets the x-coordinate of this . + /// + public float X { get; set; } + + /// + /// Gets or sets the y-coordinate of this . + /// + public float Y { get; set; } + + /// + /// Gets or sets the width of this . + /// + public float Width { get; set; } + + /// + /// Gets or sets the height of this . + /// + public float Height { get; set; } + + /// + /// Gets or sets the coordinates of the upper-left corner of the rectangular region represented by this . + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public PointF Location + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(this.X, this.Y); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + this.X = value.X; + this.Y = value.Y; + } + } + + /// + /// Gets or sets the size of this . + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public SizeF Size + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(this.Width, this.Height); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + this.Width = value.Width; + this.Height = value.Height; + } + } + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEmpty => (this.Width <= 0) || (this.Height <= 0); + + /// + /// Gets the y-coordinate of the top edge of this . + /// + public float Top => this.Y; + + /// + /// Gets the x-coordinate of the right edge of this . + /// + public float Right + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.X + this.Width; + } + + /// + /// Gets the y-coordinate of the bottom edge of this . + /// + public float Bottom + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.Y + this.Height; + } + + /// + /// Gets the x-coordinate of the left edge of this . + /// + public float Left => this.X; + + /// + /// Creates a with the coordinates of the specified by truncating each coordinate. + /// + /// The rectangle. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator Rectangle(RectangleF rectangle) => Rectangle.Truncate(rectangle); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(RectangleF left, RectangleF right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(RectangleF left, RectangleF right) => !left.Equals(right); + + /// + /// Creates a new with the specified location and size. + /// The left coordinate of the rectangle. + /// The top coordinate of the rectangle. + /// The right coordinate of the rectangle. + /// The bottom coordinate of the rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + + // ReSharper disable once InconsistentNaming + public static RectangleF FromLTRB(float left, float top, float right, float bottom) => new(left, top, right - left, bottom - top); + + /// + /// Returns the center point of the given . + /// + /// The rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Center(RectangleF rectangle) => new(rectangle.Left + (rectangle.Width / 2), rectangle.Top + (rectangle.Height / 2)); + + /// + /// Creates a rectangle that represents the intersection between and + /// . If there is no intersection, an empty rectangle is returned. + /// + /// The first rectangle. + /// The second rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RectangleF Intersect(RectangleF a, RectangleF b) + { + float x1 = MathF.Max(a.X, b.X); + float x2 = MathF.Min(a.Right, b.Right); + float y1 = MathF.Max(a.Y, b.Y); + float y2 = MathF.Min(a.Bottom, b.Bottom); + + if (x2 >= x1 && y2 >= y1) + { + return new RectangleF(x1, y1, x2 - x1, y2 - y1); + } + + return Empty; + } + + /// + /// Creates a that is inflated by the specified amount. + /// + /// The rectangle. + /// The amount to inflate the width by. + /// The amount to inflate the height by. + /// A new . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RectangleF Inflate(RectangleF rectangle, float x, float y) + { + RectangleF r = rectangle; + r.Inflate(x, y); + return r; + } + + /// + /// Transforms a rectangle by the given matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// A transformed . + public static RectangleF Transform(RectangleF rectangle, Matrix3x2 matrix) + { + PointF topLeft = PointF.Transform(rectangle.Location, matrix); + PointF topRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Top), matrix); + PointF bottomLeft = PointF.Transform(new PointF(rectangle.Left, rectangle.Bottom), matrix); + PointF bottomRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Bottom), matrix); + + float left = MathF.Min(MathF.Min(topLeft.X, topRight.X), MathF.Min(bottomLeft.X, bottomRight.X)); + float top = MathF.Min(MathF.Min(topLeft.Y, topRight.Y), MathF.Min(bottomLeft.Y, bottomRight.Y)); + float right = MathF.Max(MathF.Max(topLeft.X, topRight.X), MathF.Max(bottomLeft.X, bottomRight.X)); + float bottom = MathF.Max(MathF.Max(topLeft.Y, topRight.Y), MathF.Max(bottomLeft.Y, bottomRight.Y)); + + return FromLTRB(left, top, right, bottom); + } + + /// + /// Transforms a rectangle by the given 4x4 matrix, applying a projective transform + /// flattened into 2D space. + /// + /// The source rectangle. + /// The transformation matrix. + /// A transformed . + public static RectangleF Transform(RectangleF rectangle, Matrix4x4 matrix) + { + PointF topLeft = PointF.Transform(rectangle.Location, matrix); + PointF topRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Top), matrix); + PointF bottomLeft = PointF.Transform(new PointF(rectangle.Left, rectangle.Bottom), matrix); + PointF bottomRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Bottom), matrix); + + float left = MathF.Min(MathF.Min(topLeft.X, topRight.X), MathF.Min(bottomLeft.X, bottomRight.X)); + float top = MathF.Min(MathF.Min(topLeft.Y, topRight.Y), MathF.Min(bottomLeft.Y, bottomRight.Y)); + float right = MathF.Max(MathF.Max(topLeft.X, topRight.X), MathF.Max(bottomLeft.X, bottomRight.X)); + float bottom = MathF.Max(MathF.Max(topLeft.Y, topRight.Y), MathF.Max(bottomLeft.Y, bottomRight.Y)); + + return FromLTRB(left, top, right, bottom); + } + + /// + /// Creates a rectangle that represents the union between and . + /// + /// The first rectangle. + /// The second rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RectangleF Union(RectangleF a, RectangleF b) + { + float x1 = MathF.Min(a.X, b.X); + float x2 = MathF.Max(a.Right, b.Right); + float y1 = MathF.Min(a.Y, b.Y); + float y2 = MathF.Max(a.Bottom, b.Bottom); + + return new RectangleF(x1, y1, x2 - x1, y2 - y1); + } + + /// + /// Deconstructs this rectangle into four floats. + /// + /// The out value for X. + /// The out value for Y. + /// The out value for the width. + /// The out value for the height. + public void Deconstruct(out float x, out float y, out float width, out float height) + { + x = this.X; + y = this.Y; + width = this.Width; + height = this.Height; + } + + /// + /// Creates a RectangleF that represents the intersection between this RectangleF and the . + /// + /// The rectangle. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Intersect(RectangleF rectangle) + { + RectangleF result = Intersect(rectangle, this); + + this.X = result.X; + this.Y = result.Y; + this.Width = result.Width; + this.Height = result.Height; + } + + /// + /// Inflates this by the specified amount. + /// + /// The width. + /// The height. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Inflate(float width, float height) + { + this.X -= width; + this.Y -= height; + + this.Width += 2 * width; + this.Height += 2 * height; + } + + /// + /// Inflates this by the specified amount. + /// + /// The size. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Inflate(SizeF size) => this.Inflate(size.Width, size.Height); + + /// + /// Determines if the specfied point is contained within the rectangular region defined by + /// this . + /// + /// The x-coordinate of the given point. + /// The y-coordinate of the given point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(float x, float y) => this.X <= x && x < this.Right && this.Y <= y && y < this.Bottom; + + /// + /// Determines if the specified point is contained within the rectangular region defined by this . + /// + /// The point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(PointF point) => this.Contains(point.X, point.Y); + + /// + /// Determines if the rectangular region represented by is entirely contained + /// within the rectangular region represented by this . + /// + /// The rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(RectangleF rectangle) => + (this.X <= rectangle.X) && (rectangle.Right <= this.Right) && + (this.Y <= rectangle.Y) && (rectangle.Bottom <= this.Bottom); + + /// + /// Determines if the specfied intersects the rectangular region defined by + /// this . + /// + /// The other Rectange. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IntersectsWith(RectangleF rectangle) => + (rectangle.X < this.Right) && (this.X < rectangle.Right) && + (rectangle.Y < this.Bottom) && (this.Y < rectangle.Bottom); + + /// + /// Adjusts the location of this rectangle by the specified amount. + /// + /// The point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Offset(PointF point) => this.Offset(point.X, point.Y); + + /// + /// Adjusts the location of this rectangle by the specified amount. + /// + /// The amount to offset the x-coordinate. + /// The amount to offset the y-coordinate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Offset(float dx, float dy) + { + this.X += dx; + this.Y += dy; + } + + /// + public override int GetHashCode() + => HashCode.Combine(this.X, this.Y, this.Width, this.Height); + + /// + public override string ToString() + => $"RectangleF [ X={this.X}, Y={this.Y}, Width={this.Width}, Height={this.Height} ]"; + + /// + public override bool Equals(object? obj) => obj is RectangleF other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(RectangleF other) => + this.X.Equals(other.X) && + this.Y.Equals(other.Y) && + this.Width.Equals(other.Width) && + this.Height.Equals(other.Height); + } +} diff --git a/ImageSharp/Primitives/SignedRational.cs b/ImageSharp/Primitives/SignedRational.cs new file mode 100644 index 0000000..58327cc --- /dev/null +++ b/ImageSharp/Primitives/SignedRational.cs @@ -0,0 +1,189 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Globalization; + +namespace SixLabors.ImageSharp { + /// + /// Represents a number that can be expressed as a fraction. + /// + /// + /// This is a very simplified implementation of a rational number designed for use with metadata only. + /// + public readonly struct SignedRational : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The to create the rational from. + public SignedRational(int value) + : this(value, 1) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The number above the line in a vulgar fraction showing how many of the parts indicated by the denominator are taken. + /// The number below the line in a vulgar fraction; a divisor. + public SignedRational(int numerator, int denominator) + : this(numerator, denominator, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The number above the line in a vulgar fraction showing how many of the parts indicated by the denominator are taken. + /// The number below the line in a vulgar fraction; a divisor. + /// Specified if the rational should be simplified. + public SignedRational(int numerator, int denominator, bool simplify) + { + if (simplify) + { + LongRational rational = new LongRational(numerator, denominator).Simplify(); + + this.Numerator = (int)rational.Numerator; + this.Denominator = (int)rational.Denominator; + } + else + { + this.Numerator = numerator; + this.Denominator = denominator; + } + } + + /// + /// Initializes a new instance of the struct. + /// + /// The to create the instance from. + public SignedRational(double value) + : this(value, false) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The to create the instance from. + /// Whether to use the best possible precision when parsing the value. + public SignedRational(double value, bool bestPrecision) + { + LongRational rational = LongRational.FromDouble(value, bestPrecision); + + this.Numerator = (int)rational.Numerator; + this.Denominator = (int)rational.Denominator; + } + + /// + /// Gets the numerator of a number. + /// + public int Numerator { get; } + + /// + /// Gets the denominator of a number. + /// + public int Denominator { get; } + + /// + /// Determines whether the specified instances are considered equal. + /// + /// The first to compare. + /// The second to compare. + /// The + public static bool operator ==(SignedRational left, SignedRational right) + { + return left.Equals(right); + } + + /// + /// Determines whether the specified instances are not considered equal. + /// + /// The first to compare. + /// The second to compare. + /// The + public static bool operator !=(SignedRational left, SignedRational right) + { + return !left.Equals(right); + } + + /// + /// Converts the specified to an instance of this type. + /// + /// The to convert to an instance of this type. + /// + /// The . + /// + public static SignedRational FromDouble(double value) + { + return new SignedRational(value, false); + } + + /// + /// Converts the specified to an instance of this type. + /// + /// The to convert to an instance of this type. + /// Whether to use the best possible precision when parsing the value. + /// + /// The . + /// + public static SignedRational FromDouble(double value, bool bestPrecision) + { + return new SignedRational(value, bestPrecision); + } + + /// + public override bool Equals(object? obj) + { + return obj is SignedRational other && this.Equals(other); + } + + /// + public bool Equals(SignedRational other) + { + LongRational left = new(this.Numerator, this.Denominator); + LongRational right = new(other.Numerator, other.Denominator); + + return left.Equals(right); + } + + /// + public override int GetHashCode() + { + LongRational self = new(this.Numerator, this.Denominator); + return self.GetHashCode(); + } + + /// + /// Converts a rational number to the nearest . + /// + /// + /// The . + /// + public double ToDouble() + { + return this.Numerator / (double)this.Denominator; + } + + /// + public override string ToString() + { + return this.ToString(CultureInfo.InvariantCulture); + } + + /// + /// Converts the numeric value of this instance to its equivalent string representation using + /// the specified culture-specific format information. + /// + /// + /// An object that supplies culture-specific formatting information. + /// + /// The + public string ToString(IFormatProvider provider) + { + LongRational rational = new(this.Numerator, this.Denominator); + return rational.ToString(provider); + } + } +} diff --git a/ImageSharp/Primitives/Size.cs b/ImageSharp/Primitives/Size.cs new file mode 100644 index 0000000..ae3327a --- /dev/null +++ b/ImageSharp/Primitives/Size.cs @@ -0,0 +1,295 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.ComponentModel; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp { + /// + /// Stores an ordered pair of integers, which specify a height and width. + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + public struct Size : IEquatable + { + /// + /// Represents a that has Width and Height values set to zero. + /// + public static readonly Size Empty; + + /// + /// Initializes a new instance of the struct. + /// + /// The width and height of the size. + public Size(int value) + : this() + { + this.Width = value; + this.Height = value; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The width of the size. + /// The height of the size. + public Size(int width, int height) + { + this.Width = width; + this.Height = height; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The size. + public Size(Size size) + : this() + { + this.Width = size.Width; + this.Height = size.Height; + } + + /// + /// Initializes a new instance of the struct from the given . + /// + /// The point. + public Size(Point point) + { + this.Width = point.X; + this.Height = point.Y; + } + + /// + /// Gets or sets the width of this . + /// + public int Width { get; set; } + + /// + /// Gets or sets the height of this . + /// + public int Height { get; set; } + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEmpty => this.Equals(Empty); + + /// + /// Creates a with the dimensions of the specified . + /// + /// The point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator SizeF(Size size) => new(size.Width, size.Height); + + /// + /// Converts the given into a . + /// + /// The size. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator Point(Size size) => new(size.Width, size.Height); + + /// + /// Computes the sum of adding two sizes. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size operator +(Size left, Size right) => Add(left, right); + + /// + /// Computes the difference left by subtracting one size from another. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size operator -(Size left, Size right) => Subtract(left, right); + + /// + /// Multiplies a by an producing . + /// + /// Multiplier of type . + /// Multiplicand of type . + /// Product of type . + public static Size operator *(int left, Size right) => Multiply(right, left); + + /// + /// Multiplies by an producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type . + public static Size operator *(Size left, int right) => Multiply(left, right); + + /// + /// Divides by an producing . + /// + /// Dividend of type . + /// Divisor of type . + /// Result of type . + public static Size operator /(Size left, int right) => new(unchecked(left.Width / right), unchecked(left.Height / right)); + + /// + /// Multiplies by a producing . + /// + /// Multiplier of type . + /// Multiplicand of type . + /// Product of type . + public static SizeF operator *(float left, Size right) => Multiply(right, left); + + /// + /// Multiplies by a producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type . + public static SizeF operator *(Size left, float right) => Multiply(left, right); + + /// + /// Divides by a producing . + /// + /// Dividend of type . + /// Divisor of type . + /// Result of type . + public static SizeF operator /(Size left, float right) + => new(left.Width / right, left.Height / right); + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Size left, Size right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Size left, Size right) => !left.Equals(right); + + /// + /// Performs vector addition of two objects. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size Add(Size left, Size right) => new(unchecked(left.Width + right.Width), unchecked(left.Height + right.Height)); + + /// + /// Contracts a by another . + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size Subtract(Size left, Size right) => new(unchecked(left.Width - right.Width), unchecked(left.Height - right.Height)); + + /// + /// Converts a to a by performing a ceiling operation on all the dimensions. + /// + /// The size. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size Ceiling(SizeF size) => new(unchecked((int)MathF.Ceiling(size.Width)), unchecked((int)MathF.Ceiling(size.Height))); + + /// + /// Converts a to a by performing a round operation on all the dimensions. + /// + /// The size. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size Round(SizeF size) => new(unchecked((int)MathF.Round(size.Width)), unchecked((int)MathF.Round(size.Height))); + + /// + /// Transforms a size by the given matrix. + /// + /// The source size. + /// The transformation matrix. + /// A transformed size. + public static SizeF Transform(Size size, Matrix3x2 matrix) + { + Vector2 v = Vector2.Transform(new Vector2(size.Width, size.Height), matrix); + + return new SizeF(v.X, v.Y); + } + + /// + /// Converts a to a by performing a round operation on all the dimensions. + /// + /// The size. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size Truncate(SizeF size) => new(unchecked((int)size.Width), unchecked((int)size.Height)); + + /// + /// Deconstructs this size into two integers. + /// + /// The out value for the width. + /// The out value for the height. + public void Deconstruct(out int width, out int height) + { + width = this.Width; + height = this.Height; + } + + /// + public override int GetHashCode() => HashCode.Combine(this.Width, this.Height); + + /// + public override string ToString() => $"Size [ Width={this.Width}, Height={this.Height} ]"; + + /// + public override bool Equals(object? obj) => obj is Size other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Size other) => this.Width.Equals(other.Width) && this.Height.Equals(other.Height); + + /// + /// Multiplies by an producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type . + private static Size Multiply(Size size, int multiplier) => + new(unchecked(size.Width * multiplier), unchecked(size.Height * multiplier)); + + /// + /// Multiplies by a producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type SizeF. + private static SizeF Multiply(Size size, float multiplier) => + new(size.Width * multiplier, size.Height * multiplier); + } +} diff --git a/ImageSharp/Primitives/SizeF.cs b/ImageSharp/Primitives/SizeF.cs new file mode 100644 index 0000000..3c825c9 --- /dev/null +++ b/ImageSharp/Primitives/SizeF.cs @@ -0,0 +1,232 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.ComponentModel; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp { + /// + /// Stores an ordered pair of single precision floating points, which specify a height and width. + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + public struct SizeF : IEquatable + { + /// + /// Represents a that has Width and Height values set to zero. + /// + public static readonly SizeF Empty; + + /// + /// Initializes a new instance of the struct. + /// + /// The width of the size. + /// The height of the size. + public SizeF(float width, float height) + { + this.Width = width; + this.Height = height; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The size. + public SizeF(SizeF size) + : this() + { + this.Width = size.Width; + this.Height = size.Height; + } + + /// + /// Initializes a new instance of the struct from the given . + /// + /// The point. + public SizeF(PointF point) + { + this.Width = point.X; + this.Height = point.Y; + } + + /// + /// Gets or sets the width of this . + /// + public float Width { get; set; } + + /// + /// Gets or sets the height of this . + /// + public float Height { get; set; } + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly bool IsEmpty => this.Equals(Empty); + + /// + /// Creates a with the coordinates of the specified . + /// + /// The point. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Vector2(SizeF point) => new(point.Width, point.Height); + + /// + /// Creates a with the dimensions of the specified by truncating each of the dimensions. + /// + /// The size. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator Size(SizeF size) => new(unchecked((int)size.Width), unchecked((int)size.Height)); + + /// + /// Converts the given into a . + /// + /// The size. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator PointF(SizeF size) => new(size.Width, size.Height); + + /// + /// Computes the sum of adding two sizes. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SizeF operator +(SizeF left, SizeF right) => Add(left, right); + + /// + /// Computes the difference left by subtracting one size from another. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SizeF operator -(SizeF left, SizeF right) => Subtract(left, right); + + /// + /// Multiplies by a producing . + /// + /// Multiplier of type . + /// Multiplicand of type . + /// Product of type . + public static SizeF operator *(float left, SizeF right) => Multiply(right, left); + + /// + /// Multiplies by a producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type . + public static SizeF operator *(SizeF left, float right) => Multiply(left, right); + + /// + /// Divides by a producing . + /// + /// Dividend of type . + /// Divisor of type . + /// Result of type . + public static SizeF operator /(SizeF left, float right) + => new(left.Width / right, left.Height / right); + + /// + /// Compares two objects for equality. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(SizeF left, SizeF right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(SizeF left, SizeF right) => !left.Equals(right); + + /// + /// Performs vector addition of two objects. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SizeF Add(SizeF left, SizeF right) => new(left.Width + right.Width, left.Height + right.Height); + + /// + /// Contracts a by another . + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SizeF Subtract(SizeF left, SizeF right) => new(left.Width - right.Width, left.Height - right.Height); + + /// + /// Transforms a size by the given matrix. + /// + /// The source size. + /// The transformation matrix. + /// A transformed size. + public static SizeF Transform(SizeF size, Matrix3x2 matrix) + { + Vector2 v = Vector2.Transform(new Vector2(size.Width, size.Height), matrix); + + return new SizeF(v.X, v.Y); + } + + /// + /// Deconstructs this size into two floats. + /// + /// The out value for the width. + /// The out value for the height. + public readonly void Deconstruct(out float width, out float height) + { + width = this.Width; + height = this.Height; + } + + /// + public override readonly int GetHashCode() => HashCode.Combine(this.Width, this.Height); + + /// + public override readonly string ToString() => $"SizeF [ Width={this.Width}, Height={this.Height} ]"; + + /// + public override readonly bool Equals(object? obj) => obj is SizeF sizeF && this.Equals(sizeF); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Equals(SizeF other) => this.Width.Equals(other.Width) && this.Height.Equals(other.Height); + + /// + /// Multiplies by a producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type SizeF. + private static SizeF Multiply(SizeF size, float multiplier) => + new(size.Width * multiplier, size.Height * multiplier); + } +} diff --git a/ImageSharp/Primitives/ValueSize.cs b/ImageSharp/Primitives/ValueSize.cs new file mode 100644 index 0000000..db626ab --- /dev/null +++ b/ImageSharp/Primitives/ValueSize.cs @@ -0,0 +1,132 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp { + /// + /// Represents a value in relation to a value on the image. + /// + internal readonly struct ValueSize : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The value. + /// The type. + public ValueSize(float value, ValueSizeType type) + { + if (type != ValueSizeType.Absolute) + { + Guard.MustBeBetweenOrEqualTo(value, 0, 1, nameof(value)); + } + + this.Value = value; + this.Type = type; + } + + /// + /// Enumerates the different value types. + /// + public enum ValueSizeType + { + /// + /// The value is the final return value. + /// + Absolute, + + /// + /// The value is a percentage of the image width. + /// + PercentageOfWidth, + + /// + /// The value is a percentage of the images height. + /// + PercentageOfHeight + } + + /// + /// Gets the value. + /// + public float Value { get; } + + /// + /// Gets the type. + /// + public ValueSizeType Type { get; } + + /// + /// Implicitly converts a float into an absolute value. + /// + /// the value to use as the absolute figure. + public static implicit operator ValueSize(float f) => Absolute(f); + + /// + /// Create a new ValueSize with as a PercentageOfWidth type with value set to percentage. + /// + /// The percentage. + /// a Values size with type PercentageOfWidth + public static ValueSize PercentageOfWidth(float percentage) + { + return new ValueSize(percentage, ValueSizeType.PercentageOfWidth); + } + + /// + /// Create a new ValueSize with as a PercentageOfHeight type with value set to percentage. + /// + /// The percentage. + /// a Values size with type PercentageOfHeight + public static ValueSize PercentageOfHeight(float percentage) + { + return new ValueSize(percentage, ValueSizeType.PercentageOfHeight); + } + + /// + /// Create a new ValueSize with as a Absolute type with value set to value. + /// + /// The value. + /// a Values size with type Absolute. + public static ValueSize Absolute(float value) + { + return new ValueSize(value, ValueSizeType.Absolute); + } + + /// + /// Calculates the specified size. + /// + /// The size. + /// The calculated value. + public float Calculate(Size size) + { + switch (this.Type) + { + case ValueSizeType.PercentageOfWidth: + return this.Value * size.Width; + case ValueSizeType.PercentageOfHeight: + return this.Value * size.Height; + case ValueSizeType.Absolute: + default: + return this.Value; + } + } + + /// + public override string ToString() => $"{this.Value} - {this.Type}"; + + /// + public override bool Equals(object? obj) + { + return obj is ValueSize size && this.Equals(size); + } + + /// + public bool Equals(ValueSize other) + { + return this.Type == other.Type && this.Value.Equals(other.Value); + } + + /// + public override int GetHashCode() => HashCode.Combine(this.Value, this.Type); + } +} diff --git a/ImageSharp/Processing/AdaptiveThresholdExtensions.cs b/ImageSharp/Processing/AdaptiveThresholdExtensions.cs new file mode 100644 index 0000000..7f5d978 --- /dev/null +++ b/ImageSharp/Processing/AdaptiveThresholdExtensions.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Binarization; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Extensions to perform AdaptiveThreshold through Mutator. + /// + public static class AdaptiveThresholdExtensions + { + /// + /// Applies Bradley Adaptive Threshold to the image. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source) + => source.ApplyProcessor(new AdaptiveThresholdProcessor()); + + /// + /// Applies Bradley Adaptive Threshold to the image. + /// + /// The current image processing context. + /// Threshold limit (0.0-1.0) to consider for binarization. + /// The . + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, float thresholdLimit) + => source.ApplyProcessor(new AdaptiveThresholdProcessor(thresholdLimit)); + + /// + /// Applies Bradley Adaptive Threshold to the image. + /// + /// The current image processing context. + /// Upper (white) color for thresholding. + /// Lower (black) color for thresholding. + /// The . + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, Color upper, Color lower) + => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower)); + + /// + /// Applies Bradley Adaptive Threshold to the image. + /// + /// The current image processing context. + /// Upper (white) color for thresholding. + /// Lower (black) color for thresholding. + /// Threshold limit (0.0-1.0) to consider for binarization. + /// The . + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, Color upper, Color lower, float thresholdLimit) + => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower, thresholdLimit)); + + /// + /// Applies Bradley Adaptive Threshold to the image. + /// + /// The current image processing context. + /// Upper (white) color for thresholding. + /// Lower (black) color for thresholding. + /// Rectangle region to apply the processor on. + /// The . + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, Color upper, Color lower, Rectangle rectangle) + => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower), rectangle); + + /// + /// Applies Bradley Adaptive Threshold to the image. + /// + /// The current image processing context. + /// Upper (white) color for thresholding. + /// Lower (black) color for thresholding. + /// Threshold limit (0.0-1.0) to consider for binarization. + /// Rectangle region to apply the processor on. + /// The . + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, Color upper, Color lower, float thresholdLimit, Rectangle rectangle) + => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower, thresholdLimit), rectangle); + } +} diff --git a/ImageSharp/Processing/AffineTransformBuilder.cs b/ImageSharp/Processing/AffineTransformBuilder.cs new file mode 100644 index 0000000..ad7c38e --- /dev/null +++ b/ImageSharp/Processing/AffineTransformBuilder.cs @@ -0,0 +1,383 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Processing { + /// + /// A helper class for constructing instances for use in affine transforms. + /// + public class AffineTransformBuilder + { + private readonly List> transformMatrixFactories = []; + + /// + /// Initializes a new instance of the class. + /// + public AffineTransformBuilder() + { + } + + /// + /// Prepends a rotation matrix using the given rotation angle in degrees + /// and the image center point as rotation center. + /// + /// The amount of rotation, in degrees. + /// The . + public AffineTransformBuilder PrependRotationDegrees(float degrees) + => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); + + /// + /// Prepends a rotation matrix using the given rotation angle in radians + /// and the image center point as rotation center. + /// + /// The amount of rotation, in radians. + /// The . + public AffineTransformBuilder PrependRotationRadians(float radians) + => this.Prepend( + size => TransformUtilities.CreateRotationTransformMatrixRadians(radians, size)); + + /// + /// Prepends a rotation matrix using the given rotation in degrees at the given origin. + /// + /// The amount of rotation, in degrees. + /// The rotation origin point. + /// The . + public AffineTransformBuilder PrependRotationDegrees(float degrees, Vector2 origin) + => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin); + + /// + /// Prepends a rotation matrix using the given rotation in radians at the given origin. + /// + /// The amount of rotation, in radians. + /// The rotation origin point. + /// The . + public AffineTransformBuilder PrependRotationRadians(float radians, Vector2 origin) + => this.PrependMatrix(Matrix3x2.CreateRotation(radians, origin)); + + /// + /// Appends a rotation matrix using the given rotation angle in degrees + /// and the image center point as rotation center. + /// + /// The amount of rotation, in degrees. + /// The . + public AffineTransformBuilder AppendRotationDegrees(float degrees) + => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); + + /// + /// Appends a rotation matrix using the given rotation angle in radians + /// and the image center point as rotation center. + /// + /// The amount of rotation, in radians. + /// The . + public AffineTransformBuilder AppendRotationRadians(float radians) + => this.Append(size => TransformUtilities.CreateRotationTransformMatrixRadians(radians, size)); + + /// + /// Appends a rotation matrix using the given rotation in degrees at the given origin. + /// + /// The amount of rotation, in degrees. + /// The rotation origin point. + /// The . + public AffineTransformBuilder AppendRotationDegrees(float degrees, Vector2 origin) + => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin); + + /// + /// Appends a rotation matrix using the given rotation in radians at the given origin. + /// + /// The amount of rotation, in radians. + /// The rotation origin point. + /// The . + public AffineTransformBuilder AppendRotationRadians(float radians, Vector2 origin) + => this.AppendMatrix(Matrix3x2.CreateRotation(radians, origin)); + + /// + /// Prepends a scale matrix from the given uniform scale. + /// + /// The uniform scale. + /// The . + public AffineTransformBuilder PrependScale(float scale) + => this.PrependMatrix(Matrix3x2.CreateScale(scale)); + + /// + /// Prepends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public AffineTransformBuilder PrependScale(SizeF scale) + => this.PrependScale((Vector2)scale); + + /// + /// Prepends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public AffineTransformBuilder PrependScale(Vector2 scales) + => this.PrependMatrix(Matrix3x2.CreateScale(scales)); + + /// + /// Appends a scale matrix from the given uniform scale. + /// + /// The uniform scale. + /// The . + public AffineTransformBuilder AppendScale(float scale) + => this.AppendMatrix(Matrix3x2.CreateScale(scale)); + + /// + /// Appends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public AffineTransformBuilder AppendScale(SizeF scales) + => this.AppendScale((Vector2)scales); + + /// + /// Appends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public AffineTransformBuilder AppendScale(Vector2 scales) + => this.AppendMatrix(Matrix3x2.CreateScale(scales)); + + /// + /// Prepends a centered skew matrix from the give angles in degrees. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The . + public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) + => this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); + + /// + /// Prepends a centered skew matrix from the give angles in radians. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The . + public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY) + => this.Prepend(size => TransformUtilities.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)); + + /// + /// Prepends a skew matrix using the given angles in degrees at the given origin. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The skew origin point. + /// The . + public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY, Vector2 origin) + => this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin); + + /// + /// Prepends a skew matrix using the given angles in radians at the given origin. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The skew origin point. + /// The . + public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY, Vector2 origin) + => this.PrependMatrix(Matrix3x2.CreateSkew(radiansX, radiansY, origin)); + + /// + /// Appends a centered skew matrix from the give angles in degrees. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The . + public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) + => this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); + + /// + /// Appends a centered skew matrix from the give angles in radians. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The . + public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY) + => this.Append(size => TransformUtilities.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)); + + /// + /// Appends a skew matrix using the given angles in degrees at the given origin. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The skew origin point. + /// The . + public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY, Vector2 origin) + => this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin); + + /// + /// Appends a skew matrix using the given angles in radians at the given origin. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The skew origin point. + /// The . + public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY, Vector2 origin) + => this.AppendMatrix(Matrix3x2.CreateSkew(radiansX, radiansY, origin)); + + /// + /// Prepends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public AffineTransformBuilder PrependTranslation(PointF position) + => this.PrependTranslation((Vector2)position); + + /// + /// Prepends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public AffineTransformBuilder PrependTranslation(Vector2 position) + => this.PrependMatrix(Matrix3x2.CreateTranslation(position)); + + /// + /// Appends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public AffineTransformBuilder AppendTranslation(PointF position) + => this.AppendTranslation((Vector2)position); + + /// + /// Appends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public AffineTransformBuilder AppendTranslation(Vector2 position) + => this.AppendMatrix(Matrix3x2.CreateTranslation(position)); + + /// + /// Prepends a raw matrix. + /// + /// The matrix to prepend. + /// + /// The resultant matrix is degenerate containing one or more values equivalent + /// to or a zero determinant and therefore cannot be used + /// for linear transforms. + /// + /// The . + public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix) + { + CheckDegenerate(matrix); + return this.Prepend(_ => matrix); + } + + /// + /// Appends a raw matrix. + /// + /// The matrix to append. + /// + /// The resultant matrix is degenerate containing one or more values equivalent + /// to or a zero determinant and therefore cannot be used + /// for linear transforms. + /// + /// The . + public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix) + { + CheckDegenerate(matrix); + return this.Append(_ => matrix); + } + + /// + /// Returns the combined matrix for a given source size. + /// + /// The source image size. + /// The . + public Matrix3x2 BuildMatrix(Size sourceSize) + => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize)); + + /// + /// Returns the combined transform matrix for a given source rectangle. + /// + /// The rectangle in the source image. + /// + /// The resultant matrix is degenerate containing one or more values equivalent + /// to or a zero determinant and therefore cannot be used + /// for linear transforms. + /// + /// The . + public Matrix3x2 BuildMatrix(Rectangle sourceRectangle) + { + Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); + Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); + + // Translate the origin matrix to cater for source rectangle offsets. + Matrix3x2 matrix = Matrix3x2.CreateTranslation(-sourceRectangle.Location); + + Size size = sourceRectangle.Size; + + foreach (Func factory in this.transformMatrixFactories) + { + matrix *= factory(size); + } + + CheckDegenerate(matrix); + + return matrix; + } + + /// + /// Returns the size of a rectangle large enough to contain the transformed source rectangle. + /// + /// The rectangle in the source image. + /// + /// The resultant matrix is degenerate containing one or more values equivalent + /// to or a zero determinant and therefore cannot be used + /// for linear transforms. + /// + /// The . + public SizeF GetTransformedSize(Rectangle sourceRectangle) + { + Matrix3x2 matrix = this.BuildMatrix(sourceRectangle); + return GetTransformedSize(sourceRectangle, matrix); + } + + /// + /// Returns the size of a rectangle large enough to contain the transformed source rectangle. + /// + /// The rectangle in the source image. + /// The transformation matrix. + /// + /// The resultant matrix is degenerate containing one or more values equivalent + /// to or a zero determinant and therefore cannot be used + /// for linear transforms. + /// + /// The . + internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix3x2 matrix) + => TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size); + + /// + /// Clears all accumulated transform matrices, resetting the builder to its initial state. + /// + /// The . + public AffineTransformBuilder Clear() + { + this.transformMatrixFactories.Clear(); + return this; + } + + private static void CheckDegenerate(Matrix3x2 matrix) + { + if (TransformUtilities.IsDegenerate(matrix)) + { + throw new DegenerateTransformException("Matrix is degenerate. Check input values."); + } + } + + private AffineTransformBuilder Prepend(Func transformFactory) + { + this.transformMatrixFactories.Insert(0, transformFactory); + return this; + } + + private AffineTransformBuilder Append(Func transformFactory) + { + this.transformMatrixFactories.Add(transformFactory); + return this; + } + } +} diff --git a/ImageSharp/Processing/AnchorPositionMode.cs b/ImageSharp/Processing/AnchorPositionMode.cs new file mode 100644 index 0000000..24d5721 --- /dev/null +++ b/ImageSharp/Processing/AnchorPositionMode.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing { + /// + /// Enumerated anchor positions to apply to resized images. + /// + public enum AnchorPositionMode + { + /// + /// Anchors the position of the image to the center of it's bounding container. + /// + Center, + + /// + /// Anchors the position of the image to the top of it's bounding container. + /// + Top, + + /// + /// Anchors the position of the image to the bottom of it's bounding container. + /// + Bottom, + + /// + /// Anchors the position of the image to the left of it's bounding container. + /// + Left, + + /// + /// Anchors the position of the image to the right of it's bounding container. + /// + Right, + + /// + /// Anchors the position of the image to the top left side of it's bounding container. + /// + TopLeft, + + /// + /// Anchors the position of the image to the top right side of it's bounding container. + /// + TopRight, + + /// + /// Anchors the position of the image to the bottom right side of it's bounding container. + /// + BottomRight, + + /// + /// Anchors the position of the image to the bottom left side of it's bounding container. + /// + BottomLeft + } +} diff --git a/ImageSharp/Processing/BinaryThresholdMode.cs b/ImageSharp/Processing/BinaryThresholdMode.cs new file mode 100644 index 0000000..64cde96 --- /dev/null +++ b/ImageSharp/Processing/BinaryThresholdMode.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing { + /// + /// Selects the value to be compared to threshold. + /// + public enum BinaryThresholdMode + { + /// + /// Compare the color luminance (according to ITU-R Recommendation BT.709). + /// + Luminance = 0, + + /// + /// Compare the HSL saturation of the color. + /// + Saturation = 1, + + /// + /// Compare the maximum of YCbCr chroma value, i.e. Cb and Cr distance from achromatic value. + /// + MaxChroma = 2, + } +} diff --git a/ImageSharp/Processing/ColorBlindnessMode.cs b/ImageSharp/Processing/ColorBlindnessMode.cs new file mode 100644 index 0000000..50de133 --- /dev/null +++ b/ImageSharp/Processing/ColorBlindnessMode.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing { + /// + /// Enumerates the various types of defined color blindness filters. + /// + public enum ColorBlindnessMode + { + /// + /// Partial color desensitivity. + /// + Achromatomaly, + + /// + /// Complete color desensitivity (Monochrome) + /// + Achromatopsia, + + /// + /// Green weak + /// + Deuteranomaly, + + /// + /// Green blind + /// + Deuteranopia, + + /// + /// Red weak + /// + Protanomaly, + + /// + /// Red blind + /// + Protanopia, + + /// + /// Blue weak + /// + Tritanomaly, + + /// + /// Blue blind + /// + Tritanopia + } +} diff --git a/ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs b/ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs new file mode 100644 index 0000000..1eb1efb --- /dev/null +++ b/ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Performs processor application operations on the source image + /// + /// The pixel format + internal class DefaultImageProcessorContext : IInternalImageProcessingContext + where TPixel : unmanaged, IPixel + { + private readonly bool mutate; + private readonly Image source; + private Image? destination; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The source image. + /// Whether to mutate the image. + public DefaultImageProcessorContext(Configuration configuration, Image source, bool mutate) + { + this.Configuration = configuration; + this.mutate = mutate; + this.source = source; + + // Mutate acts upon the source image only. + if (this.mutate) + { + this.destination = source; + } + } + + /// + public Configuration Configuration { get; } + + /// + public IDictionary Properties { get; } = new ConcurrentDictionary(); + + /// + public Image GetResultImage() + { + if (!this.mutate && this.destination is null) + { + // Ensure we have cloned the source if we are not mutating as we might have failed + // to register any processors. + this.destination = this.source.Clone(); + } + + return this.destination!; + } + + /// + public Size GetCurrentSize() => this.GetCurrentBounds().Size; + + /// + public IImageProcessingContext ApplyProcessor(IImageProcessor processor) + => this.ApplyProcessor(processor, this.GetCurrentBounds()); + + /// + public IImageProcessingContext ApplyProcessor(IImageProcessor processor, Rectangle rectangle) + { + if (!this.mutate && this.destination is null) + { + // When cloning an image we can optimize the processing pipeline by avoiding an unnecessary + // interim clone if the first processor in the pipeline is a cloning processor. + if (processor is ICloningImageProcessor cloningImageProcessor) + { + using ICloningImageProcessor pixelProcessor = cloningImageProcessor.CreatePixelSpecificCloningProcessor(this.Configuration, this.source, rectangle); + this.destination = pixelProcessor.CloneAndExecute(); + return this; + } + + // Not a cloning processor? We need to create a clone to operate on. + this.destination = this.source.Clone(); + } + + // Standard processing pipeline. + using (IImageProcessor specificProcessor = processor.CreatePixelSpecificProcessor(this.Configuration, this.destination!, rectangle)) + { + specificProcessor.Execute(); + } + + return this; + } + + private Rectangle GetCurrentBounds() => this.destination?.Bounds ?? this.source.Bounds; + } +} diff --git a/ImageSharp/Processing/Extensions/Binarization/BinaryDitherExtensions.cs b/ImageSharp/Processing/Extensions/Binarization/BinaryDitherExtensions.cs new file mode 100644 index 0000000..1eaa4ce --- /dev/null +++ b/ImageSharp/Processing/Extensions/Binarization/BinaryDitherExtensions.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Dithering; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions to apply binary dithering on an + /// using Mutate/Clone. + /// + public static class BinaryDitherExtensions + { + /// + /// Dithers the image reducing it to two colors using ordered dithering. + /// + /// The current image processing context. + /// The ordered ditherer. + /// The . + public static IImageProcessingContext + BinaryDither(this IImageProcessingContext source, IDither dither) => + BinaryDither(source, dither, Color.White, Color.Black); + + /// + /// Dithers the image reducing it to two colors using ordered dithering. + /// + /// The current image processing context. + /// The ordered ditherer. + /// The color to use for pixels that are above the threshold. + /// The color to use for pixels that are below the threshold + /// The . + public static IImageProcessingContext BinaryDither( + this IImageProcessingContext source, + IDither dither, + Color upperColor, + Color lowerColor) => + source.ApplyProcessor(new PaletteDitherProcessor(dither, new[] { upperColor, lowerColor })); + + /// + /// Dithers the image reducing it to two colors using ordered dithering. + /// + /// The current image processing context. + /// The ordered ditherer. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext BinaryDither( + this IImageProcessingContext source, + IDither dither, + Rectangle rectangle) => + BinaryDither(source, dither, Color.White, Color.Black, rectangle); + + /// + /// Dithers the image reducing it to two colors using ordered dithering. + /// + /// The current image processing context. + /// The ordered ditherer. + /// The color to use for pixels that are above the threshold. + /// The color to use for pixels that are below the threshold + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext BinaryDither( + this IImageProcessingContext source, + IDither dither, + Color upperColor, + Color lowerColor, + Rectangle rectangle) => + source.ApplyProcessor(new PaletteDitherProcessor(dither, new[] { upperColor, lowerColor }), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Binarization/BinaryThresholdExtensions.cs b/ImageSharp/Processing/Extensions/Binarization/BinaryThresholdExtensions.cs new file mode 100644 index 0000000..b90c41c --- /dev/null +++ b/ImageSharp/Processing/Extensions/Binarization/BinaryThresholdExtensions.cs @@ -0,0 +1,143 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Binarization; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extension methods to apply binary thresholding on an + /// using Mutate/Clone. + /// + public static class BinaryThresholdExtensions + { + /// + /// Applies binarization to the image splitting the pixels at the given threshold with + /// Luminance as the color component to be compared to threshold. + /// + /// The current image processing context. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The . + public static IImageProcessingContext BinaryThreshold(this IImageProcessingContext source, float threshold) + => source.ApplyProcessor(new BinaryThresholdProcessor(threshold, BinaryThresholdMode.Luminance)); + + /// + /// Applies binarization to the image splitting the pixels at the given threshold. + /// + /// The current image processing context. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// Selects the value to be compared to threshold. + /// The . + public static IImageProcessingContext BinaryThreshold( + this IImageProcessingContext source, + float threshold, + BinaryThresholdMode mode) + => source.ApplyProcessor(new BinaryThresholdProcessor(threshold, mode)); + + /// + /// Applies binarization to the image splitting the pixels at the given threshold with + /// Luminance as the color component to be compared to threshold. + /// + /// The current image processing context. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext BinaryThreshold( + this IImageProcessingContext source, + float threshold, + Rectangle rectangle) + => source.ApplyProcessor(new BinaryThresholdProcessor(threshold, BinaryThresholdMode.Luminance), rectangle); + + /// + /// Applies binarization to the image splitting the pixels at the given threshold. + /// + /// The current image processing context. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// Selects the value to be compared to threshold. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext BinaryThreshold( + this IImageProcessingContext source, + float threshold, + BinaryThresholdMode mode, + Rectangle rectangle) + => source.ApplyProcessor(new BinaryThresholdProcessor(threshold, mode), rectangle); + + /// + /// Applies binarization to the image splitting the pixels at the given threshold with + /// Luminance as the color component to be compared to threshold. + /// + /// The current image processing context. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The color to use for pixels that are above the threshold. + /// The color to use for pixels that are below the threshold + /// The . + public static IImageProcessingContext BinaryThreshold( + this IImageProcessingContext source, + float threshold, + Color upperColor, + Color lowerColor) + => source.ApplyProcessor(new BinaryThresholdProcessor(threshold, upperColor, lowerColor, BinaryThresholdMode.Luminance)); + + /// + /// Applies binarization to the image splitting the pixels at the given threshold. + /// + /// The current image processing context. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The color to use for pixels that are above the threshold. + /// The color to use for pixels that are below the threshold + /// Selects the value to be compared to threshold. + /// The . + public static IImageProcessingContext BinaryThreshold( + this IImageProcessingContext source, + float threshold, + Color upperColor, + Color lowerColor, + BinaryThresholdMode mode) + => source.ApplyProcessor(new BinaryThresholdProcessor(threshold, upperColor, lowerColor, mode)); + + /// + /// Applies binarization to the image splitting the pixels at the given threshold with + /// Luminance as the color component to be compared to threshold. + /// + /// The current image processing context. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The color to use for pixels that are above the threshold. + /// The color to use for pixels that are below the threshold + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext BinaryThreshold( + this IImageProcessingContext source, + float threshold, + Color upperColor, + Color lowerColor, + Rectangle rectangle) + => source.ApplyProcessor(new BinaryThresholdProcessor(threshold, upperColor, lowerColor, BinaryThresholdMode.Luminance), rectangle); + + /// + /// Applies binarization to the image splitting the pixels at the given threshold. + /// + /// The current image processing context. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The color to use for pixels that are above the threshold. + /// The color to use for pixels that are below the threshold + /// Selects the value to be compared to threshold. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext BinaryThreshold( + this IImageProcessingContext source, + float threshold, + Color upperColor, + Color lowerColor, + BinaryThresholdMode mode, + Rectangle rectangle) => + source.ApplyProcessor(new BinaryThresholdProcessor(threshold, upperColor, lowerColor, mode), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Convolution/BokehBlurExtensions.cs b/ImageSharp/Processing/Extensions/Convolution/BokehBlurExtensions.cs new file mode 100644 index 0000000..b0728d7 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Convolution/BokehBlurExtensions.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Adds bokeh blurring extensions to the type. + /// + public static class BokehBlurExtensions + { + /// + /// Applies a bokeh blur to the image. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext BokehBlur(this IImageProcessingContext source) + => source.ApplyProcessor(new BokehBlurProcessor()); + + /// + /// Applies a bokeh blur to the image. + /// + /// The current image processing context. + /// The 'radius' value representing the size of the area to sample. + /// The 'components' value representing the number of kernels to use to approximate the bokeh effect. + /// The gamma highlight factor to use to emphasize bright spots in the source image + /// The . + public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, int radius, int components, float gamma) + => source.ApplyProcessor(new BokehBlurProcessor(radius, components, gamma)); + + /// + /// Applies a bokeh blur to the image. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, Rectangle rectangle) + => source.ApplyProcessor(new BokehBlurProcessor(), rectangle); + + /// + /// Applies a bokeh blur to the image. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The 'radius' value representing the size of the area to sample. + /// The 'components' value representing the number of kernels to use to approximate the bokeh effect. + /// The gamma highlight factor to use to emphasize bright spots in the source image + /// The . + public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, Rectangle rectangle, int radius, int components, float gamma) + => source.ApplyProcessor(new BokehBlurProcessor(radius, components, gamma), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs b/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs new file mode 100644 index 0000000..39d160c --- /dev/null +++ b/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions methods to apply box blurring to an + /// using Mutate/Clone. + /// + public static class BoxBlurExtensions + { + /// + /// Applies a box blur to the image. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext BoxBlur(this IImageProcessingContext source) + => source.ApplyProcessor(new BoxBlurProcessor()); + + /// + /// Applies a box blur to the image. + /// + /// The current image processing context. + /// The 'radius' value representing the size of the area to sample. + /// The . + public static IImageProcessingContext BoxBlur(this IImageProcessingContext source, int radius) + => source.ApplyProcessor(new BoxBlurProcessor(radius)); + + /// + /// Applies a box blur to the image. + /// + /// The current image processing context. + /// The 'radius' value representing the size of the area to sample. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext BoxBlur(this IImageProcessingContext source, int radius, Rectangle rectangle) + => source.ApplyProcessor(new BoxBlurProcessor(radius), rectangle); + + /// + /// Applies a box blur to the image. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The 'radius' value representing the size of the area to sample. + /// + /// The to use when mapping the pixels outside of the border, in X direction. + /// + /// + /// The to use when mapping the pixels outside of the border, in Y direction. + /// + /// The . + public static IImageProcessingContext BoxBlur( + this IImageProcessingContext source, + Rectangle rectangle, + int radius, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + => source.ApplyProcessor(new BoxBlurProcessor(radius, borderWrapModeX, borderWrapModeY), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Convolution/ConvolutionExtensions.cs b/ImageSharp/Processing/Extensions/Convolution/ConvolutionExtensions.cs new file mode 100644 index 0000000..9062678 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Convolution/ConvolutionExtensions.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Processing.Extensions.Convolution { + /// + /// Defines general convolution extensions to apply on an + /// using Mutate/Clone. + /// + public static class ConvolutionExtensions + { + /// + /// Applies a convolution filter to the image. + /// + /// The current image processing context. + /// The convolution kernel to apply. + /// The . + public static IImageProcessingContext Convolve(this IImageProcessingContext source, DenseMatrix kernelXY) + => Convolve(source, kernelXY, false); + + /// + /// Applies a convolution filter to the image. + /// + /// The current image processing context. + /// The convolution kernel to apply. + /// Whether the convolution filter is applied to alpha as well as the color channels. + /// The . + public static IImageProcessingContext Convolve(this IImageProcessingContext source, DenseMatrix kernelXY, bool preserveAlpha) + => Convolve(source, kernelXY, preserveAlpha, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat); + + /// + /// Applies a convolution filter to the image. + /// + /// The current image processing context. + /// The convolution kernel to apply. + /// Whether the convolution filter is applied to alpha as well as the color channels. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + /// The . + public static IImageProcessingContext Convolve( + this IImageProcessingContext source, + DenseMatrix kernelXY, + bool preserveAlpha, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + => source.ApplyProcessor(new ConvolutionProcessor(kernelXY, preserveAlpha, borderWrapModeX, borderWrapModeY)); + + /// + /// Applies a convolution filter to the image. + /// + /// The current image processing context. + /// The rectangle structure that specifies the portion of the image object to alter. + /// The convolution kernel to apply. + /// The . + public static IImageProcessingContext Convolve(this IImageProcessingContext source, Rectangle rectangle, DenseMatrix kernelXY) + => Convolve(source, rectangle, kernelXY, false); + + /// + /// Applies a convolution filter to the image. + /// + /// The current image processing context. + /// The rectangle structure that specifies the portion of the image object to alter. + /// The convolution kernel to apply. + /// Whether the convolution filter is applied to alpha as well as the color channels. + /// The . + public static IImageProcessingContext Convolve(this IImageProcessingContext source, Rectangle rectangle, DenseMatrix kernelXY, bool preserveAlpha) + => Convolve(source, rectangle, kernelXY, preserveAlpha, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat); + + /// + /// Applies a convolution filter to the image. + /// + /// The current image processing context. + /// The rectangle structure that specifies the portion of the image object to alter. + /// The convolution kernel to apply. + /// Whether the convolution filter is applied to alpha as well as the color channels. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + /// The . + public static IImageProcessingContext Convolve( + this IImageProcessingContext source, + Rectangle rectangle, + DenseMatrix kernelXY, + bool preserveAlpha, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + => source.ApplyProcessor(new ConvolutionProcessor(kernelXY, preserveAlpha, borderWrapModeX, borderWrapModeY), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Convolution/DetectEdgesExtensions.cs b/ImageSharp/Processing/Extensions/Convolution/DetectEdgesExtensions.cs new file mode 100644 index 0000000..7ad0d60 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Convolution/DetectEdgesExtensions.cs @@ -0,0 +1,207 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines edge detection extensions applicable on an using Mutate/Clone. + /// + public static class DetectEdgesExtensions + { + /// + /// Detects any edges within the image. + /// Uses the kernel operating in grayscale mode. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext DetectEdges(this IImageProcessingContext source) + => DetectEdges(source, KnownEdgeDetectorKernels.Sobel); + + /// + /// Detects any edges within the image. + /// Uses the kernel operating in grayscale mode. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext DetectEdges(this IImageProcessingContext source, Rectangle rectangle) + => DetectEdges(source, rectangle, KnownEdgeDetectorKernels.Sobel); + + /// + /// Detects any edges within the image operating in grayscale mode. + /// + /// The current image processing context. + /// The 2D edge detector kernel. + /// The . + public static IImageProcessingContext DetectEdges(this IImageProcessingContext source, EdgeDetector2DKernel kernel) + => DetectEdges(source, kernel, true); + + /// + /// Detects any edges within the image using a . + /// + /// The current image processing context. + /// The 2D edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// + /// The . + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + EdgeDetector2DKernel kernel, + bool grayscale) + => source.ApplyProcessor(new EdgeDetector2DProcessor(kernel, grayscale)); + + /// + /// Detects any edges within the image operating in grayscale mode. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The 2D edge detector kernel. + /// The . + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + Rectangle rectangle, + EdgeDetector2DKernel kernel) + => DetectEdges(source, rectangle, kernel, true); + + /// + /// Detects any edges within the image using a . + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The 2D edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// + /// The . + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + Rectangle rectangle, + EdgeDetector2DKernel kernel, + bool grayscale) + => source.ApplyProcessor(new EdgeDetector2DProcessor(kernel, grayscale), rectangle); + + /// + /// Detects any edges within the image operating in grayscale mode. + /// + /// The current image processing context. + /// The edge detector kernel. + /// The . + public static IImageProcessingContext DetectEdges(this IImageProcessingContext source, EdgeDetectorKernel kernel) + => DetectEdges(source, kernel, true); + + /// + /// Detects any edges within the image using a . + /// + /// The current image processing context. + /// The edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// + /// The . + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + EdgeDetectorKernel kernel, + bool grayscale) + => source.ApplyProcessor(new EdgeDetectorProcessor(kernel, grayscale)); + + /// + /// Detects any edges within the image operating in grayscale mode. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The edge detector kernel. + /// The . + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + Rectangle rectangle, + EdgeDetectorKernel kernel) + => DetectEdges(source, rectangle, kernel, true); + + /// + /// Detects any edges within the image using a . + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// + /// The . + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + Rectangle rectangle, + EdgeDetectorKernel kernel, + bool grayscale) + => source.ApplyProcessor(new EdgeDetectorProcessor(kernel, grayscale), rectangle); + + /// + /// Detects any edges within the image operating in grayscale mode. + /// + /// The current image processing context. + /// The compass edge detector kernel. + /// The . + public static IImageProcessingContext DetectEdges(this IImageProcessingContext source, EdgeDetectorCompassKernel kernel) + => DetectEdges(source, kernel, true); + + /// + /// Detects any edges within the image using a . + /// + /// The current image processing context. + /// The compass edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// + /// The . + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + EdgeDetectorCompassKernel kernel, + bool grayscale) + => source.ApplyProcessor(new EdgeDetectorCompassProcessor(kernel, grayscale)); + + /// + /// Detects any edges within the image operating in grayscale mode. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The compass edge detector kernel. + /// The . + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + Rectangle rectangle, + EdgeDetectorCompassKernel kernel) + => DetectEdges(source, rectangle, kernel, true); + + /// + /// Detects any edges within the image using a . + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The compass edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// + /// The . + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + Rectangle rectangle, + EdgeDetectorCompassKernel kernel, + bool grayscale) + => source.ApplyProcessor(new EdgeDetectorCompassProcessor(kernel, grayscale), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs b/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs new file mode 100644 index 0000000..ddee8a9 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines Gaussian blurring extensions to apply on an + /// using Mutate/Clone. + /// + public static class GaussianBlurExtensions + { + /// + /// Applies a Gaussian blur to the image. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext GaussianBlur(this IImageProcessingContext source) + => source.ApplyProcessor(new GaussianBlurProcessor()); + + /// + /// Applies a Gaussian blur to the image. + /// + /// The current image processing context. + /// The 'sigma' value representing the weight of the blur. + /// The . + public static IImageProcessingContext GaussianBlur(this IImageProcessingContext source, float sigma) + => source.ApplyProcessor(new GaussianBlurProcessor(sigma)); + + /// + /// Applies a Gaussian blur to the image. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The 'sigma' value representing the weight of the blur. + /// The . + public static IImageProcessingContext GaussianBlur( + this IImageProcessingContext source, + Rectangle rectangle, + float sigma) + => source.ApplyProcessor(new GaussianBlurProcessor(sigma), rectangle); + + /// + /// Applies a Gaussian blur to the image. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The 'sigma' value representing the weight of the blur. + /// + /// The to use when mapping the pixels outside of the border, in X direction. + /// + /// + /// The to use when mapping the pixels outside of the border, in Y direction. + /// + /// The . + public static IImageProcessingContext GaussianBlur( + this IImageProcessingContext source, + Rectangle rectangle, + float sigma, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + => source.ApplyProcessor(new GaussianBlurProcessor(sigma, borderWrapModeX, borderWrapModeY), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs b/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs new file mode 100644 index 0000000..a37962c --- /dev/null +++ b/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines Gaussian sharpening extensions to apply on an + /// using Mutate/Clone. + /// + public static class GaussianSharpenExtensions + { + /// + /// Applies a Gaussian sharpening filter to the image. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source) + => source.ApplyProcessor(new GaussianSharpenProcessor()); + + /// + /// Applies a Gaussian sharpening filter to the image. + /// + /// The current image processing context. + /// The 'sigma' value representing the weight of the blur. + /// The . + public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source, float sigma) + => source.ApplyProcessor(new GaussianSharpenProcessor(sigma)); + + /// + /// Applies a Gaussian sharpening filter to the image. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The 'sigma' value representing the weight of the blur. + /// The . + public static IImageProcessingContext GaussianSharpen( + this IImageProcessingContext source, + Rectangle rectangle, + float sigma) => + source.ApplyProcessor(new GaussianSharpenProcessor(sigma), rectangle); + + /// + /// Applies a Gaussian sharpening filter to the image. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The 'sigma' value representing the weight of the blur. + /// + /// The to use when mapping the pixels outside of the border, in X direction. + /// + /// + /// The to use when mapping the pixels outside of the border, in Y direction. + /// + /// The . + public static IImageProcessingContext GaussianSharpen( + this IImageProcessingContext source, + Rectangle rectangle, + float sigma, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + => source.ApplyProcessor(new GaussianSharpenProcessor(sigma, borderWrapModeX, borderWrapModeY), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs b/ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs new file mode 100644 index 0000000..ce4c549 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the applying of the median blur on an + /// using Mutate/Clone. + /// + public static class MedianBlurExtensions + { + /// + /// Applies a median blur on the image. + /// + /// The current image processing context. + /// The radius of the area to find the median for. + /// + /// Whether the filter is applied to alpha as well as the color channels. + /// + /// The . + public static IImageProcessingContext MedianBlur( + this IImageProcessingContext source, + int radius, + bool preserveAlpha) + => source.ApplyProcessor(new MedianBlurProcessor(radius, preserveAlpha)); + + /// + /// Applies a median blur on the image. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The radius of the area to find the median for. + /// + /// Whether the filter is applied to alpha as well as the color channels. + /// + /// The . + public static IImageProcessingContext MedianBlur( + this IImageProcessingContext source, + Rectangle rectangle, + int radius, + bool preserveAlpha) + => source.ApplyProcessor(new MedianBlurProcessor(radius, preserveAlpha), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Dithering/DitherExtensions.cs b/ImageSharp/Processing/Extensions/Dithering/DitherExtensions.cs new file mode 100644 index 0000000..aee8e5f --- /dev/null +++ b/ImageSharp/Processing/Extensions/Dithering/DitherExtensions.cs @@ -0,0 +1,153 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Dithering; +using System; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines dithering extensions to apply on an + /// using Mutate/Clone. + /// + public static class DitherExtensions + { + /// + /// Dithers the image reducing it to a web-safe palette using . + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext Dither(this IImageProcessingContext source) => + Dither(source, KnownDitherings.Bayer8x8); + + /// + /// Dithers the image reducing it to a web-safe palette. + /// + /// The current image processing context. + /// The ordered ditherer. + /// The . + public static IImageProcessingContext Dither( + this IImageProcessingContext source, + IDither dither) => + source.ApplyProcessor(new PaletteDitherProcessor(dither)); + + /// + /// Dithers the image reducing it to a web-safe palette. + /// + /// The current image processing context. + /// The ordered ditherer. + /// The dithering scale used to adjust the amount of dither. + /// The . + public static IImageProcessingContext Dither( + this IImageProcessingContext source, + IDither dither, + float ditherScale) => + source.ApplyProcessor(new PaletteDitherProcessor(dither, ditherScale)); + + /// + /// Dithers the image reducing it to the given palette. + /// + /// The current image processing context. + /// The ordered ditherer. + /// The palette to select substitute colors from. + /// The . + public static IImageProcessingContext Dither( + this IImageProcessingContext source, + IDither dither, + ReadOnlyMemory palette) => + source.ApplyProcessor(new PaletteDitherProcessor(dither, palette)); + + /// + /// Dithers the image reducing it to the given palette. + /// + /// The current image processing context. + /// The ordered ditherer. + /// The dithering scale used to adjust the amount of dither. + /// The palette to select substitute colors from. + /// The . + public static IImageProcessingContext Dither( + this IImageProcessingContext source, + IDither dither, + float ditherScale, + ReadOnlyMemory palette) => + source.ApplyProcessor(new PaletteDitherProcessor(dither, ditherScale, palette)); + + /// + /// Dithers the image reducing it to a web-safe palette using . + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Dither(this IImageProcessingContext source, Rectangle rectangle) => + Dither(source, KnownDitherings.Bayer8x8, rectangle); + + /// + /// Dithers the image reducing it to a web-safe palette. + /// + /// The current image processing context. + /// The ordered ditherer. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Dither( + this IImageProcessingContext source, + IDither dither, + Rectangle rectangle) => + source.ApplyProcessor(new PaletteDitherProcessor(dither), rectangle); + + /// + /// Dithers the image reducing it to a web-safe palette. + /// + /// The current image processing context. + /// The ordered ditherer. + /// The dithering scale used to adjust the amount of dither. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Dither( + this IImageProcessingContext source, + IDither dither, + float ditherScale, + Rectangle rectangle) => + source.ApplyProcessor(new PaletteDitherProcessor(dither, ditherScale), rectangle); + + /// + /// Dithers the image reducing it to the given palette. + /// + /// The current image processing context. + /// The ordered ditherer. + /// The palette to select substitute colors from. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Dither( + this IImageProcessingContext source, + IDither dither, + ReadOnlyMemory palette, + Rectangle rectangle) => + source.ApplyProcessor(new PaletteDitherProcessor(dither, palette), rectangle); + + /// + /// Dithers the image reducing it to the given palette. + /// + /// The current image processing context. + /// The ordered ditherer. + /// The dithering scale used to adjust the amount of dither. + /// The palette to select substitute colors from. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Dither( + this IImageProcessingContext source, + IDither dither, + float ditherScale, + ReadOnlyMemory palette, + Rectangle rectangle) => + source.ApplyProcessor(new PaletteDitherProcessor(dither, ditherScale, palette), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Drawing/DrawImageExtensions.cs b/ImageSharp/Processing/Extensions/Drawing/DrawImageExtensions.cs new file mode 100644 index 0000000..be9028f --- /dev/null +++ b/ImageSharp/Processing/Extensions/Drawing/DrawImageExtensions.cs @@ -0,0 +1,635 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Drawing; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Adds extensions that allow the drawing of images to the type. + /// + public static class DrawImageExtensions + { + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The opacity of the image to draw. Must be between 0 and 1. + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + float opacity) + => DrawImage(source, foreground, opacity, 0); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The opacity of the image to draw. Must be between 0 and 1. + /// + /// The number of times the foreground frames are allowed to loop while applying this operation across successive frames. + /// A value of 0 means loop indefinitely. + /// + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + float opacity, + int foregroundRepeatCount) + { + GraphicsOptions options = source.GetGraphicsOptions(); + return DrawImage(source, foreground, options.ColorBlendingMode, options.AlphaCompositionMode, opacity, foregroundRepeatCount); + } + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The rectangle structure that specifies the portion of the image to draw. + /// The opacity of the image to draw. Must be between 0 and 1. + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Rectangle foregroundRectangle, + float opacity) + => DrawImage(source, foreground, foregroundRectangle, opacity, 0); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The rectangle structure that specifies the portion of the image to draw. + /// The opacity of the image to draw. Must be between 0 and 1. + /// + /// The number of times the foreground frames are allowed to loop while applying this operation across successive frames. + /// A value of 0 means loop indefinitely. + /// + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Rectangle foregroundRectangle, + float opacity, + int foregroundRepeatCount) + { + GraphicsOptions options = source.GetGraphicsOptions(); + return DrawImage(source, foreground, foregroundRectangle, options.ColorBlendingMode, options.AlphaCompositionMode, opacity, foregroundRepeatCount); + } + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The color blending mode. + /// The opacity of the image to draw. Must be between 0 and 1. + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + PixelColorBlendingMode colorBlending, + float opacity) + => DrawImage(source, foreground, colorBlending, opacity, 0); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The color blending mode. + /// The opacity of the image to draw. Must be between 0 and 1. + /// + /// The number of times the foreground frames are allowed to loop while applying this operation across successive frames. + /// A value of 0 means loop indefinitely. + /// + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + PixelColorBlendingMode colorBlending, + float opacity, + int foregroundRepeatCount) + => DrawImage(source, foreground, Point.Empty, colorBlending, opacity, foregroundRepeatCount); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The rectangle structure that specifies the portion of the image to draw. + /// The color blending mode. + /// The opacity of the image to draw. Must be between 0 and 1. + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Rectangle foregroundRectangle, + PixelColorBlendingMode colorBlending, + float opacity) + => DrawImage(source, foreground, foregroundRectangle, colorBlending, opacity, 0); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The rectangle structure that specifies the portion of the image to draw. + /// The color blending mode. + /// The opacity of the image to draw. Must be between 0 and 1. + /// + /// The number of times the foreground frames are allowed to loop while applying this operation across successive frames. + /// A value of 0 means loop indefinitely. + /// + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Rectangle foregroundRectangle, + PixelColorBlendingMode colorBlending, + float opacity, + int foregroundRepeatCount) + => DrawImage(source, foreground, Point.Empty, foregroundRectangle, colorBlending, opacity, foregroundRepeatCount); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The color blending mode. + /// The alpha composition mode. + /// The opacity of the image to draw. Must be between 0 and 1. + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + PixelColorBlendingMode colorBlending, + PixelAlphaCompositionMode alphaComposition, + float opacity) + => DrawImage(source, foreground, colorBlending, alphaComposition, opacity, 0); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The color blending mode. + /// The alpha composition mode. + /// The opacity of the image to draw. Must be between 0 and 1. + /// + /// The number of times the foreground frames are allowed to loop while applying this operation across successive frames. + /// A value of 0 means loop indefinitely. + /// + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + PixelColorBlendingMode colorBlending, + PixelAlphaCompositionMode alphaComposition, + float opacity, + int foregroundRepeatCount) + => DrawImage(source, foreground, Point.Empty, colorBlending, alphaComposition, opacity, foregroundRepeatCount); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The rectangle structure that specifies the portion of the image to draw. + /// The color blending mode. + /// The alpha composition mode. + /// The opacity of the image to draw. Must be between 0 and 1. + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Rectangle foregroundRectangle, + PixelColorBlendingMode colorBlending, + PixelAlphaCompositionMode alphaComposition, + float opacity) + => DrawImage(source, foreground, foregroundRectangle, colorBlending, alphaComposition, opacity, 0); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The rectangle structure that specifies the portion of the image to draw. + /// The color blending mode. + /// The alpha composition mode. + /// The opacity of the image to draw. Must be between 0 and 1. + /// + /// The number of times the foreground frames are allowed to loop while applying this operation across successive frames. + /// A value of 0 means loop indefinitely. + /// + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Rectangle foregroundRectangle, + PixelColorBlendingMode colorBlending, + PixelAlphaCompositionMode alphaComposition, + float opacity, + int foregroundRepeatCount) + => DrawImage(source, foreground, Point.Empty, foregroundRectangle, colorBlending, alphaComposition, opacity, foregroundRepeatCount); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The options, including the blending type and blending amount. + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + GraphicsOptions options) + => DrawImage(source, foreground, options, 0); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The options, including the blending type and blending amount. + /// + /// The number of times the foreground frames are allowed to loop while applying this operation across successive frames. + /// A value of 0 means loop indefinitely. + /// + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + GraphicsOptions options, + int foregroundRepeatCount) + => DrawImage(source, foreground, Point.Empty, options, foregroundRepeatCount); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The rectangle structure that specifies the portion of the image to draw. + /// The options, including the blending type and blending amount. + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Rectangle foregroundRectangle, + GraphicsOptions options) + => DrawImage(source, foreground, foregroundRectangle, options, 0); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The rectangle structure that specifies the portion of the image to draw. + /// The options, including the blending type and blending amount. + /// + /// The number of times the foreground frames are allowed to loop while applying this operation across successive frames. + /// A value of 0 means loop indefinitely. + /// + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Rectangle foregroundRectangle, + GraphicsOptions options, + int foregroundRepeatCount) + => DrawImage(source, foreground, Point.Empty, foregroundRectangle, options, foregroundRepeatCount); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The location on the currently processing image at which to draw. + /// The opacity of the image to draw. Must be between 0 and 1. + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Point backgroundLocation, + float opacity) + => DrawImage(source, foreground, backgroundLocation, opacity, 0); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The location on the currently processing image at which to draw. + /// The opacity of the image to draw. Must be between 0 and 1. + /// + /// The number of times the foreground frames are allowed to loop while applying this operation across successive frames. + /// A value of 0 means loop indefinitely. + /// + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Point backgroundLocation, + float opacity, + int foregroundRepeatCount) + { + GraphicsOptions options = source.GetGraphicsOptions(); + return DrawImage(source, foreground, backgroundLocation, options.ColorBlendingMode, options.AlphaCompositionMode, opacity, foregroundRepeatCount); + } + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The location on the currently processing image at which to draw. + /// The rectangle structure that specifies the portion of the image to draw. + /// The opacity of the image to draw. Must be between 0 and 1. + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Point backgroundLocation, + Rectangle foregroundRectangle, + float opacity) + => DrawImage(source, foreground, backgroundLocation, foregroundRectangle, opacity, 0); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The location on the currently processing image at which to draw. + /// The rectangle structure that specifies the portion of the image to draw. + /// The opacity of the image to draw. Must be between 0 and 1. + /// + /// The number of times the foreground frames are allowed to loop while applying this operation across successive frames. + /// A value of 0 means loop indefinitely. + /// + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Point backgroundLocation, + Rectangle foregroundRectangle, + float opacity, + int foregroundRepeatCount) + { + GraphicsOptions options = source.GetGraphicsOptions(); + return DrawImage(source, foreground, backgroundLocation, foregroundRectangle, options.ColorBlendingMode, options.AlphaCompositionMode, opacity, foregroundRepeatCount); + } + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The location on the currently processing image at which to draw. + /// The color blending to apply. + /// The opacity of the image to draw. Must be between 0 and 1. + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Point backgroundLocation, + PixelColorBlendingMode colorBlending, + float opacity) + => DrawImage(source, foreground, backgroundLocation, colorBlending, opacity, 0); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The location on the currently processing image at which to draw. + /// The color blending to apply. + /// The opacity of the image to draw. Must be between 0 and 1. + /// + /// The number of times the foreground frames are allowed to loop while applying this operation across successive frames. + /// A value of 0 means loop indefinitely. + /// + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Point backgroundLocation, + PixelColorBlendingMode colorBlending, + float opacity, + int foregroundRepeatCount) + => DrawImage(source, foreground, backgroundLocation, colorBlending, source.GetGraphicsOptions().AlphaCompositionMode, opacity, foregroundRepeatCount); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The location on the currently processing image at which to draw. + /// The rectangle structure that specifies the portion of the image to draw. + /// The color blending to apply. + /// The opacity of the image to draw. Must be between 0 and 1. + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Point backgroundLocation, + Rectangle foregroundRectangle, + PixelColorBlendingMode colorBlending, + float opacity) + => DrawImage(source, foreground, backgroundLocation, foregroundRectangle, colorBlending, opacity, 0); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The location on the currently processing image at which to draw. + /// The rectangle structure that specifies the portion of the image to draw. + /// The color blending to apply. + /// The opacity of the image to draw. Must be between 0 and 1. + /// + /// The number of times the foreground frames are allowed to loop while applying this operation across successive frames. + /// A value of 0 means loop indefinitely. + /// + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Point backgroundLocation, + Rectangle foregroundRectangle, + PixelColorBlendingMode colorBlending, + float opacity, + int foregroundRepeatCount) + => DrawImage(source, foreground, backgroundLocation, foregroundRectangle, colorBlending, source.GetGraphicsOptions().AlphaCompositionMode, opacity, foregroundRepeatCount); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The location on the currently processing image at which to draw. + /// The options containing the blend mode and opacity. + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Point backgroundLocation, + GraphicsOptions options) + => DrawImage(source, foreground, backgroundLocation, options, 0); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The location on the currently processing image at which to draw. + /// The options containing the blend mode and opacity. + /// + /// The number of times the foreground frames are allowed to loop while applying this operation across successive frames. + /// A value of 0 means loop indefinitely. + /// + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Point backgroundLocation, + GraphicsOptions options, + int foregroundRepeatCount) + => DrawImage(source, foreground, backgroundLocation, options.ColorBlendingMode, options.AlphaCompositionMode, options.BlendPercentage, foregroundRepeatCount); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The location on the currently processing image at which to draw. + /// The rectangle structure that specifies the portion of the image to draw. + /// The options containing the blend mode and opacity. + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Point backgroundLocation, + Rectangle foregroundRectangle, + GraphicsOptions options) + => DrawImage(source, foreground, backgroundLocation, foregroundRectangle, options, 0); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The location on the currently processing image at which to draw. + /// The rectangle structure that specifies the portion of the image to draw. + /// The options containing the blend mode and opacity. + /// + /// The number of times the foreground frames are allowed to loop while applying this operation across successive frames. + /// A value of 0 means loop indefinitely. + /// + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Point backgroundLocation, + Rectangle foregroundRectangle, + GraphicsOptions options, + int foregroundRepeatCount) + => DrawImage(source, foreground, backgroundLocation, foregroundRectangle, options.ColorBlendingMode, options.AlphaCompositionMode, options.BlendPercentage, foregroundRepeatCount); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The location on the currently processing image at which to draw. + /// The color blending to apply. + /// The alpha composition mode. + /// The opacity of the image to draw. Must be between 0 and 1. + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Point backgroundLocation, + PixelColorBlendingMode colorBlending, + PixelAlphaCompositionMode alphaComposition, + float opacity) + => DrawImage(source, foreground, backgroundLocation, colorBlending, alphaComposition, opacity, 0); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The location on the currently processing image at which to draw. + /// The color blending to apply. + /// The alpha composition mode. + /// The opacity of the image to draw. Must be between 0 and 1. + /// + /// The number of times the foreground frames are allowed to loop while applying this operation across successive frames. + /// A value of 0 means loop indefinitely. + /// + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Point backgroundLocation, + PixelColorBlendingMode colorBlending, + PixelAlphaCompositionMode alphaComposition, + float opacity, + int foregroundRepeatCount) + => source.ApplyProcessor(new DrawImageProcessor(foreground, backgroundLocation, foreground.Bounds, colorBlending, alphaComposition, opacity, foregroundRepeatCount)); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The location on the currently processing image at which to draw. + /// The rectangle structure that specifies the portion of the image to draw. + /// The color blending to apply. + /// The alpha composition mode. + /// The opacity of the image to draw. Must be between 0 and 1. + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Point backgroundLocation, + Rectangle foregroundRectangle, + PixelColorBlendingMode colorBlending, + PixelAlphaCompositionMode alphaComposition, + float opacity) + => DrawImage(source, foreground, backgroundLocation, foregroundRectangle, colorBlending, alphaComposition, opacity, 0); + + /// + /// Draws the given image together with the currently processing image by blending their pixels. + /// + /// The current image processing context. + /// The image to draw on the currently processing image. + /// The location on the currently processing image at which to draw. + /// The rectangle structure that specifies the portion of the image to draw. + /// The color blending to apply. + /// The alpha composition mode. + /// The opacity of the image to draw. Must be between 0 and 1. + /// + /// The number of times the foreground frames are allowed to loop while applying this operation across successive frames. + /// A value of 0 means loop indefinitely. + /// + /// The . + public static IImageProcessingContext DrawImage( + this IImageProcessingContext source, + Image foreground, + Point backgroundLocation, + Rectangle foregroundRectangle, + PixelColorBlendingMode colorBlending, + PixelAlphaCompositionMode alphaComposition, + float opacity, + int foregroundRepeatCount) => + source.ApplyProcessor( + new DrawImageProcessor(foreground, backgroundLocation, foregroundRectangle, colorBlending, alphaComposition, opacity, foregroundRepeatCount), + foregroundRectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Effects/OilPaintExtensions.cs b/ImageSharp/Processing/Extensions/Effects/OilPaintExtensions.cs new file mode 100644 index 0000000..6e7650e --- /dev/null +++ b/ImageSharp/Processing/Extensions/Effects/OilPaintExtensions.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Effects; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines oil painting effect extensions applicable on an + /// using Mutate/Clone. + /// + public static class OilPaintExtensions + { + /// + /// Alters the colors of the image recreating an oil painting effect with levels and brushSize + /// set to 10 and 15 respectively. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext OilPaint(this IImageProcessingContext source) => OilPaint(source, 10, 15); + + /// + /// Alters the colors of the image recreating an oil painting effect with levels and brushSize + /// set to 10 and 15 respectively. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext OilPaint(this IImageProcessingContext source, Rectangle rectangle) => + OilPaint(source, 10, 15, rectangle); + + /// + /// Alters the colors of the image recreating an oil painting effect. + /// + /// The current image processing context. + /// The number of intensity levels. Higher values result in a broader range of color intensities forming part of the result image. + /// The number of neighboring pixels used in calculating each individual pixel value. + /// The . + public static IImageProcessingContext + OilPaint(this IImageProcessingContext source, int levels, int brushSize) => + source.ApplyProcessor(new OilPaintingProcessor(levels, brushSize)); + + /// + /// Alters the colors of the image recreating an oil painting effect. + /// + /// The current image processing context. + /// The number of intensity levels. Higher values result in a broader range of color intensities forming part of the result image. + /// The number of neighboring pixels used in calculating each individual pixel value. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext OilPaint( + this IImageProcessingContext source, + int levels, + int brushSize, + Rectangle rectangle) => + source.ApplyProcessor(new OilPaintingProcessor(levels, brushSize), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Effects/PixelRowDelegateExtensions.cs b/ImageSharp/Processing/Extensions/Effects/PixelRowDelegateExtensions.cs new file mode 100644 index 0000000..7f795bf --- /dev/null +++ b/ImageSharp/Processing/Extensions/Effects/PixelRowDelegateExtensions.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Effects; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extension methods that allow the application of user defined processing delegate to an . + /// + public static class PixelRowDelegateExtensions + { + /// + /// Applies a user defined processing delegate to the image. + /// + /// The current image processing context. + /// The user defined processing delegate to use to modify image rows. + /// The . + public static IImageProcessingContext ProcessPixelRowsAsVector4(this IImageProcessingContext source, PixelRowOperation rowOperation) + => ProcessPixelRowsAsVector4(source, rowOperation, PixelConversionModifiers.None); + + /// + /// Applies a user defined processing delegate to the image. + /// + /// The current image processing context. + /// The user defined processing delegate to use to modify image rows. + /// The to apply during the pixel conversions. + /// The . + public static IImageProcessingContext ProcessPixelRowsAsVector4(this IImageProcessingContext source, PixelRowOperation rowOperation, PixelConversionModifiers modifiers) + => source.ApplyProcessor(new PixelRowDelegateProcessor(rowOperation, modifiers)); + + /// + /// Applies a user defined processing delegate to the image. + /// + /// The current image processing context. + /// The user defined processing delegate to use to modify image rows. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext ProcessPixelRowsAsVector4(this IImageProcessingContext source, PixelRowOperation rowOperation, Rectangle rectangle) + => ProcessPixelRowsAsVector4(source, rowOperation, rectangle, PixelConversionModifiers.None); + + /// + /// Applies a user defined processing delegate to the image. + /// + /// The current image processing context. + /// The user defined processing delegate to use to modify image rows. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The to apply during the pixel conversions. + /// The . + public static IImageProcessingContext ProcessPixelRowsAsVector4(this IImageProcessingContext source, PixelRowOperation rowOperation, Rectangle rectangle, PixelConversionModifiers modifiers) + => source.ApplyProcessor(new PixelRowDelegateProcessor(rowOperation, modifiers), rectangle); + + /// + /// Applies a user defined processing delegate to the image. + /// + /// The current image processing context. + /// The user defined processing delegate to use to modify image rows. + /// The . + public static IImageProcessingContext ProcessPixelRowsAsVector4(this IImageProcessingContext source, PixelRowOperation rowOperation) + => ProcessPixelRowsAsVector4(source, rowOperation, PixelConversionModifiers.None); + + /// + /// Applies a user defined processing delegate to the image. + /// + /// The current image processing context. + /// The user defined processing delegate to use to modify image rows. + /// The to apply during the pixel conversions. + /// The . + public static IImageProcessingContext ProcessPixelRowsAsVector4(this IImageProcessingContext source, PixelRowOperation rowOperation, PixelConversionModifiers modifiers) + => source.ApplyProcessor(new PositionAwarePixelRowDelegateProcessor(rowOperation, modifiers)); + + /// + /// Applies a user defined processing delegate to the image. + /// + /// The current image processing context. + /// The user defined processing delegate to use to modify image rows. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext ProcessPixelRowsAsVector4(this IImageProcessingContext source, PixelRowOperation rowOperation, Rectangle rectangle) + => ProcessPixelRowsAsVector4(source, rowOperation, rectangle, PixelConversionModifiers.None); + + /// + /// Applies a user defined processing delegate to the image. + /// + /// The current image processing context. + /// The user defined processing delegate to use to modify image rows. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The to apply during the pixel conversions. + /// The . + public static IImageProcessingContext ProcessPixelRowsAsVector4(this IImageProcessingContext source, PixelRowOperation rowOperation, Rectangle rectangle, PixelConversionModifiers modifiers) + => source.ApplyProcessor(new PositionAwarePixelRowDelegateProcessor(rowOperation, modifiers), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Effects/PixelateExtensions.cs b/ImageSharp/Processing/Extensions/Effects/PixelateExtensions.cs new file mode 100644 index 0000000..3f1cc25 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Effects/PixelateExtensions.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Effects; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines pixelation effect extensions applicable on an + /// using Mutate/Clone. + /// + public static class PixelateExtensions + { + /// + /// Pixelates an image with the given pixel size. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext Pixelate(this IImageProcessingContext source) => Pixelate(source, 4); + + /// + /// Pixelates an image with the given pixel size. + /// + /// The current image processing context. + /// The size of the pixels. + /// The . + public static IImageProcessingContext Pixelate(this IImageProcessingContext source, int size) => + source.ApplyProcessor(new PixelateProcessor(size)); + + /// + /// Pixelates an image with the given pixel size. + /// + /// The current image processing context. + /// The size of the pixels. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Pixelate( + this IImageProcessingContext source, + int size, + Rectangle rectangle) => + source.ApplyProcessor(new PixelateProcessor(size), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Filters/BlackWhiteExtensions.cs b/ImageSharp/Processing/Extensions/Filters/BlackWhiteExtensions.cs new file mode 100644 index 0000000..d5fd4d9 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Filters/BlackWhiteExtensions.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extension methods that allow the application of black and white toning to an + /// using Mutate/Clone. + /// + public static class BlackWhiteExtensions + { + /// + /// Applies black and white toning to the image. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext BlackWhite(this IImageProcessingContext source) + => source.ApplyProcessor(new BlackWhiteProcessor()); + + /// + /// Applies black and white toning to the image. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext BlackWhite(this IImageProcessingContext source, Rectangle rectangle) + => source.ApplyProcessor(new BlackWhiteProcessor(), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Filters/BrightnessExtensions.cs b/ImageSharp/Processing/Extensions/Filters/BrightnessExtensions.cs new file mode 100644 index 0000000..5bd7ad5 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Filters/BrightnessExtensions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the alteration of the brightness component of an + /// using Mutate/Clone. + /// + public static class BrightnessExtensions + { + /// + /// Alters the brightness component of the image. + /// + /// + /// A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing brighter results. + /// + /// The current image processing context. + /// The proportion of the conversion. Must be greater than or equal to 0. + /// The . + public static IImageProcessingContext Brightness(this IImageProcessingContext source, float amount) + => source.ApplyProcessor(new BrightnessProcessor(amount)); + + /// + /// Alters the brightness component of the image. + /// + /// + /// A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing brighter results. + /// + /// The current image processing context. + /// The proportion of the conversion. Must be greater than or equal to 0. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Brightness(this IImageProcessingContext source, float amount, Rectangle rectangle) + => source.ApplyProcessor(new BrightnessProcessor(amount), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Filters/ColorBlindnessExtensions.cs b/ImageSharp/Processing/Extensions/Filters/ColorBlindnessExtensions.cs new file mode 100644 index 0000000..53eb3fb --- /dev/null +++ b/ImageSharp/Processing/Extensions/Filters/ColorBlindnessExtensions.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors; +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that simulate the effects of various color blindness disorders on an + /// using Mutate/Clone. + /// + public static class ColorBlindnessExtensions + { + /// + /// Applies the given colorblindness simulator to the image. + /// + /// The current image processing context. + /// The type of color blindness simulator to apply. + /// The . + public static IImageProcessingContext ColorBlindness(this IImageProcessingContext source, ColorBlindnessMode colorBlindness) + => source.ApplyProcessor(GetProcessor(colorBlindness)); + + /// + /// Applies the given colorblindness simulator to the image. + /// + /// The current image processing context. + /// The type of color blindness simulator to apply. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext ColorBlindness(this IImageProcessingContext source, ColorBlindnessMode colorBlindnessMode, Rectangle rectangle) + => source.ApplyProcessor(GetProcessor(colorBlindnessMode), rectangle); + + private static IImageProcessor GetProcessor(ColorBlindnessMode colorBlindness) + { + switch (colorBlindness) + { + case ColorBlindnessMode.Achromatomaly: + return new AchromatomalyProcessor(); + case ColorBlindnessMode.Achromatopsia: + return new AchromatopsiaProcessor(); + case ColorBlindnessMode.Deuteranomaly: + return new DeuteranomalyProcessor(); + case ColorBlindnessMode.Deuteranopia: + return new DeuteranopiaProcessor(); + case ColorBlindnessMode.Protanomaly: + return new ProtanomalyProcessor(); + case ColorBlindnessMode.Protanopia: + return new ProtanopiaProcessor(); + case ColorBlindnessMode.Tritanomaly: + return new TritanomalyProcessor(); + default: + return new TritanopiaProcessor(); + } + } + } +} diff --git a/ImageSharp/Processing/Extensions/Filters/ContrastExtensions.cs b/ImageSharp/Processing/Extensions/Filters/ContrastExtensions.cs new file mode 100644 index 0000000..4c26786 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Filters/ContrastExtensions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the alteration of the contrast component of an + /// using Mutate/Clone. + /// + public static class ContrastExtensions + { + /// + /// Alters the contrast component of the image. + /// + /// + /// A value of 0 will create an image that is completely gray. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing results with more contrast. + /// + /// The current image processing context. + /// The proportion of the conversion. Must be greater than or equal to 0. + /// The . + public static IImageProcessingContext Contrast(this IImageProcessingContext source, float amount) + => source.ApplyProcessor(new ContrastProcessor(amount)); + + /// + /// Alters the contrast component of the image. + /// + /// + /// A value of 0 will create an image that is completely gray. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing results with more contrast. + /// + /// The current image processing context. + /// The proportion of the conversion. Must be greater than or equal to 0. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Contrast(this IImageProcessingContext source, float amount, Rectangle rectangle) + => source.ApplyProcessor(new ContrastProcessor(amount), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Filters/FilterExtensions.cs b/ImageSharp/Processing/Extensions/Filters/FilterExtensions.cs new file mode 100644 index 0000000..1341b80 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Filters/FilterExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the application of composable filters to an + /// using Mutate/Clone. + /// + public static class FilterExtensions + { + /// + /// Filters an image by the given color matrix + /// + /// The current image processing context. + /// The filter color matrix + /// The . + public static IImageProcessingContext Filter(this IImageProcessingContext source, ColorMatrix matrix) + => source.ApplyProcessor(new FilterProcessor(matrix)); + + /// + /// Filters an image by the given color matrix + /// + /// The current image processing context. + /// The filter color matrix + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Filter(this IImageProcessingContext source, ColorMatrix matrix, Rectangle rectangle) + => source.ApplyProcessor(new FilterProcessor(matrix), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Filters/GrayscaleExtensions.cs b/ImageSharp/Processing/Extensions/Filters/GrayscaleExtensions.cs new file mode 100644 index 0000000..4cd2122 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Filters/GrayscaleExtensions.cs @@ -0,0 +1,112 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors; +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the application of grayscale toning to an + /// using Mutate/Clone. + /// + public static class GrayscaleExtensions + { + /// + /// Applies grayscale toning to the image. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext Grayscale(this IImageProcessingContext source) + => Grayscale(source, GrayscaleMode.Bt709); + + /// + /// Applies grayscale toning to the image using the given amount. + /// + /// The current image processing context. + /// The proportion of the conversion. Must be between 0 and 1. + /// The . + public static IImageProcessingContext Grayscale(this IImageProcessingContext source, float amount) + => Grayscale(source, GrayscaleMode.Bt709, amount); + + /// + /// Applies grayscale toning to the image with the given . + /// + /// The current image processing context. + /// The formula to apply to perform the operation. + /// The . + public static IImageProcessingContext Grayscale(this IImageProcessingContext source, GrayscaleMode mode) + => Grayscale(source, mode, 1F); + + /// + /// Applies grayscale toning to the image with the given using the given amount. + /// + /// The current image processing context. + /// The formula to apply to perform the operation. + /// The proportion of the conversion. Must be between 0 and 1. + /// The . + public static IImageProcessingContext Grayscale(this IImageProcessingContext source, GrayscaleMode mode, float amount) + { + IImageProcessor processor = mode == GrayscaleMode.Bt709 + ? (IImageProcessor)new GrayscaleBt709Processor(amount) + : new GrayscaleBt601Processor(amount); + + source.ApplyProcessor(processor); + return source; + } + + /// + /// Applies grayscale toning to the image. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Grayscale(this IImageProcessingContext source, Rectangle rectangle) + => Grayscale(source, 1F, rectangle); + + /// + /// Applies grayscale toning to the image using the given amount. + /// + /// The current image processing context. + /// The proportion of the conversion. Must be between 0 and 1. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Grayscale(this IImageProcessingContext source, float amount, Rectangle rectangle) + => Grayscale(source, GrayscaleMode.Bt709, amount, rectangle); + + /// + /// Applies grayscale toning to the image. + /// + /// The current image processing context. + /// The formula to apply to perform the operation. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Grayscale(this IImageProcessingContext source, GrayscaleMode mode, Rectangle rectangle) + => Grayscale(source, mode, 1F, rectangle); + + /// + /// Applies grayscale toning to the image using the given amount. + /// + /// The current image processing context. + /// The formula to apply to perform the operation. + /// The proportion of the conversion. Must be between 0 and 1. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Grayscale(this IImageProcessingContext source, GrayscaleMode mode, float amount, Rectangle rectangle) + { + IImageProcessor processor = mode == GrayscaleMode.Bt709 + ? (IImageProcessor)new GrayscaleBt709Processor(amount) + : new GrayscaleBt601Processor(amount); + + source.ApplyProcessor(processor, rectangle); + return source; + } + } +} diff --git a/ImageSharp/Processing/Extensions/Filters/HueExtensions.cs b/ImageSharp/Processing/Extensions/Filters/HueExtensions.cs new file mode 100644 index 0000000..5d137c3 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Filters/HueExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the alteration of the hue component of an + /// using Mutate/Clone. + /// + public static class HueExtensions + { + /// + /// Alters the hue component of the image. + /// + /// The current image processing context. + /// The rotation angle in degrees to adjust the hue. + /// The . + public static IImageProcessingContext Hue(this IImageProcessingContext source, float degrees) + => source.ApplyProcessor(new HueProcessor(degrees)); + + /// + /// Alters the hue component of the image. + /// + /// The current image processing context. + /// The rotation angle in degrees to adjust the hue. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Hue(this IImageProcessingContext source, float degrees, Rectangle rectangle) + => source.ApplyProcessor(new HueProcessor(degrees), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Filters/InvertExtensions.cs b/ImageSharp/Processing/Extensions/Filters/InvertExtensions.cs new file mode 100644 index 0000000..4d21856 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Filters/InvertExtensions.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the inversion of colors of an + /// using Mutate/Clone. + /// + public static class InvertExtensions + { + /// + /// Inverts the colors of the image. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext Invert(this IImageProcessingContext source) + => source.ApplyProcessor(new InvertProcessor(1F)); + + /// + /// Inverts the colors of the image. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Invert(this IImageProcessingContext source, Rectangle rectangle) + => source.ApplyProcessor(new InvertProcessor(1F), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Filters/KodachromeExtensions.cs b/ImageSharp/Processing/Extensions/Filters/KodachromeExtensions.cs new file mode 100644 index 0000000..5f79a12 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Filters/KodachromeExtensions.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the recreation of an old Kodachrome camera effect on an + /// using Mutate/Clone. + /// + public static class KodachromeExtensions + { + /// + /// Alters the colors of the image recreating an old Kodachrome camera effect. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext Kodachrome(this IImageProcessingContext source) + => source.ApplyProcessor(new KodachromeProcessor()); + + /// + /// Alters the colors of the image recreating an old Kodachrome camera effect. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Kodachrome(this IImageProcessingContext source, Rectangle rectangle) + => source.ApplyProcessor(new KodachromeProcessor(), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Filters/LightnessExtensions.cs b/ImageSharp/Processing/Extensions/Filters/LightnessExtensions.cs new file mode 100644 index 0000000..03a326b --- /dev/null +++ b/ImageSharp/Processing/Extensions/Filters/LightnessExtensions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the alteration of the lightness component of an + /// using Mutate/Clone. + /// + public static class LightnessExtensions + { + /// + /// Alters the lightness component of the image. + /// + /// + /// A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing lighter results. + /// + /// The current image processing context. + /// The proportion of the conversion. Must be greater than or equal to 0. + /// The . + public static IImageProcessingContext Lightness(this IImageProcessingContext source, float amount) + => source.ApplyProcessor(new LightnessProcessor(amount)); + + /// + /// Alters the lightness component of the image. + /// + /// + /// A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing lighter results. + /// + /// The current image processing context. + /// The proportion of the conversion. Must be greater than or equal to 0. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Lightness(this IImageProcessingContext source, float amount, Rectangle rectangle) + => source.ApplyProcessor(new LightnessProcessor(amount), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Filters/LomographExtensions.cs b/ImageSharp/Processing/Extensions/Filters/LomographExtensions.cs new file mode 100644 index 0000000..250a327 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Filters/LomographExtensions.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the recreation of an old Lomograph camera effect on an + /// using Mutate/Clone. + /// + public static class LomographExtensions + { + /// + /// Alters the colors of the image recreating an old Lomograph camera effect. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext Lomograph(this IImageProcessingContext source) + => source.ApplyProcessor(new LomographProcessor(source.GetGraphicsOptions())); + + /// + /// Alters the colors of the image recreating an old Lomograph camera effect. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Lomograph(this IImageProcessingContext source, Rectangle rectangle) + => source.ApplyProcessor(new LomographProcessor(source.GetGraphicsOptions()), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Filters/OpacityExtensions.cs b/ImageSharp/Processing/Extensions/Filters/OpacityExtensions.cs new file mode 100644 index 0000000..48fcf99 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Filters/OpacityExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the alteration of the opacity component of an + /// using Mutate/Clone. + /// + public static class OpacityExtensions + { + /// + /// Multiplies the alpha component of the image. + /// + /// The current image processing context. + /// The proportion of the conversion. Must be between 0 and 1. + /// The . + public static IImageProcessingContext Opacity(this IImageProcessingContext source, float amount) + => source.ApplyProcessor(new OpacityProcessor(amount)); + + /// + /// Multiplies the alpha component of the image. + /// + /// The current image processing context. + /// The proportion of the conversion. Must be between 0 and 1. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Opacity(this IImageProcessingContext source, float amount, Rectangle rectangle) + => source.ApplyProcessor(new OpacityProcessor(amount), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Filters/PolaroidExtensions.cs b/ImageSharp/Processing/Extensions/Filters/PolaroidExtensions.cs new file mode 100644 index 0000000..283375a --- /dev/null +++ b/ImageSharp/Processing/Extensions/Filters/PolaroidExtensions.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the recreation of an old Polaroid camera effect on an + /// using Mutate/Clone. + /// + public static class PolaroidExtensions + { + /// + /// Alters the colors of the image recreating an old Polaroid camera effect. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext Polaroid(this IImageProcessingContext source) + => source.ApplyProcessor(new PolaroidProcessor(source.GetGraphicsOptions())); + + /// + /// Alters the colors of the image recreating an old Polaroid camera effect. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Polaroid(this IImageProcessingContext source, Rectangle rectangle) + => source.ApplyProcessor(new PolaroidProcessor(source.GetGraphicsOptions()), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Filters/SaturateExtensions.cs b/ImageSharp/Processing/Extensions/Filters/SaturateExtensions.cs new file mode 100644 index 0000000..15589fa --- /dev/null +++ b/ImageSharp/Processing/Extensions/Filters/SaturateExtensions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the alteration of the saturation component of an + /// using Mutate/Clone. + /// + public static class SaturateExtensions + { + /// + /// Alters the saturation component of the image. + /// + /// + /// A value of 0 is completely un-saturated. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of amount over 1 are allowed, providing super-saturated results + /// + /// The current image processing context. + /// The proportion of the conversion. Must be greater than or equal to 0. + /// The . + public static IImageProcessingContext Saturate(this IImageProcessingContext source, float amount) + => source.ApplyProcessor(new SaturateProcessor(amount)); + + /// + /// Alters the saturation component of the image. + /// + /// + /// A value of 0 is completely un-saturated. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of amount over 1 are allowed, providing super-saturated results + /// + /// The current image processing context. + /// The proportion of the conversion. Must be greater than or equal to 0. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Saturate(this IImageProcessingContext source, float amount, Rectangle rectangle) + => source.ApplyProcessor(new SaturateProcessor(amount), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Filters/SepiaExtensions.cs b/ImageSharp/Processing/Extensions/Filters/SepiaExtensions.cs new file mode 100644 index 0000000..f72bf5d --- /dev/null +++ b/ImageSharp/Processing/Extensions/Filters/SepiaExtensions.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the application of sepia toning on an + /// using Mutate/Clone. + /// + public static class SepiaExtensions + { + /// + /// Applies sepia toning to the image. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext Sepia(this IImageProcessingContext source) + => Sepia(source, 1F); + + /// + /// Applies sepia toning to the image using the given amount. + /// + /// The current image processing context. + /// The proportion of the conversion. Must be between 0 and 1. + /// The . + public static IImageProcessingContext Sepia(this IImageProcessingContext source, float amount) + => source.ApplyProcessor(new SepiaProcessor(amount)); + + /// + /// Applies sepia toning to the image. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Sepia(this IImageProcessingContext source, Rectangle rectangle) + => Sepia(source, 1F, rectangle); + + /// + /// Applies sepia toning to the image. + /// + /// The current image processing context. + /// The proportion of the conversion. Must be between 0 and 1. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Sepia(this IImageProcessingContext source, float amount, Rectangle rectangle) + => source.ApplyProcessor(new SepiaProcessor(amount), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Normalization/HistogramEqualizationExtensions.cs b/ImageSharp/Processing/Extensions/Normalization/HistogramEqualizationExtensions.cs new file mode 100644 index 0000000..bb90adf --- /dev/null +++ b/ImageSharp/Processing/Extensions/Normalization/HistogramEqualizationExtensions.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Normalization; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extension that allow the adjustment of the contrast of an image via its histogram. + /// + public static class HistogramEqualizationExtensions + { + /// + /// Equalizes the histogram of an image to increases the contrast. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext HistogramEqualization(this IImageProcessingContext source) => + HistogramEqualization(source, new HistogramEqualizationOptions()); + + /// + /// Equalizes the histogram of an image to increases the contrast. + /// + /// The current image processing context. + /// The histogram equalization options to use. + /// The . + public static IImageProcessingContext HistogramEqualization( + this IImageProcessingContext source, + HistogramEqualizationOptions options) => + source.ApplyProcessor(HistogramEqualizationProcessor.FromOptions(options)); + } +} diff --git a/ImageSharp/Processing/Extensions/Overlays/BackgroundColorExtensions.cs b/ImageSharp/Processing/Extensions/Overlays/BackgroundColorExtensions.cs new file mode 100644 index 0000000..fd18df7 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Overlays/BackgroundColorExtensions.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Overlays; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extension methods to replace the background color of an + /// using Mutate/Clone. + /// + public static class BackgroundColorExtensions + { + /// + /// Replaces the background color of image with the given one. + /// + /// The current image processing context. + /// The color to set as the background. + /// The . + public static IImageProcessingContext BackgroundColor(this IImageProcessingContext source, Color color) => + BackgroundColor(source, source.GetGraphicsOptions(), color); + + /// + /// Replaces the background color of image with the given one. + /// + /// The current image processing context. + /// The color to set as the background. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext BackgroundColor( + this IImageProcessingContext source, + Color color, + Rectangle rectangle) => + BackgroundColor(source, source.GetGraphicsOptions(), color, rectangle); + + /// + /// Replaces the background color of image with the given one. + /// + /// The current image processing context. + /// The options effecting pixel blending. + /// The color to set as the background. + /// The . + public static IImageProcessingContext BackgroundColor( + this IImageProcessingContext source, + GraphicsOptions options, + Color color) => + source.ApplyProcessor(new BackgroundColorProcessor(options, color)); + + /// + /// Replaces the background color of image with the given one. + /// + /// The current image processing context. + /// The options effecting pixel blending. + /// The color to set as the background. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext BackgroundColor( + this IImageProcessingContext source, + GraphicsOptions options, + Color color, + Rectangle rectangle) => + source.ApplyProcessor(new BackgroundColorProcessor(options, color), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Overlays/GlowExtensions.cs b/ImageSharp/Processing/Extensions/Overlays/GlowExtensions.cs new file mode 100644 index 0000000..3dbe50a --- /dev/null +++ b/ImageSharp/Processing/Extensions/Overlays/GlowExtensions.cs @@ -0,0 +1,172 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Overlays; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the application of a radial glow on an + /// using Mutate/Clone. + /// + public static class GlowExtensions + { + /// + /// Applies a radial glow effect to an image. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext Glow(this IImageProcessingContext source) => + Glow(source, source.GetGraphicsOptions()); + + /// + /// Applies a radial glow effect to an image. + /// + /// The current image processing context. + /// The color to set as the glow. + /// The . + public static IImageProcessingContext Glow(this IImageProcessingContext source, Color color) + { + return Glow(source, source.GetGraphicsOptions(), color); + } + + /// + /// Applies a radial glow effect to an image. + /// + /// The current image processing context. + /// The the radius. + /// The . + public static IImageProcessingContext Glow(this IImageProcessingContext source, float radius) => + Glow(source, source.GetGraphicsOptions(), radius); + + /// + /// Applies a radial glow effect to an image. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Glow(this IImageProcessingContext source, Rectangle rectangle) => + source.Glow(source.GetGraphicsOptions(), rectangle); + + /// + /// Applies a radial glow effect to an image. + /// + /// The current image processing context. + /// The color to set as the glow. + /// The the radius. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Glow( + this IImageProcessingContext source, + Color color, + float radius, + Rectangle rectangle) => + source.Glow(source.GetGraphicsOptions(), color, ValueSize.Absolute(radius), rectangle); + + /// + /// Applies a radial glow effect to an image. + /// + /// The current image processing context. + /// The options effecting things like blending. + /// The . + public static IImageProcessingContext Glow(this IImageProcessingContext source, GraphicsOptions options) => + source.Glow(options, Color.Black, ValueSize.PercentageOfWidth(0.5f)); + + /// + /// Applies a radial glow effect to an image. + /// + /// The current image processing context. + /// The options effecting things like blending. + /// The color to set as the glow. + /// The . + public static IImageProcessingContext Glow( + this IImageProcessingContext source, + GraphicsOptions options, + Color color) => + source.Glow(options, color, ValueSize.PercentageOfWidth(0.5f)); + + /// + /// Applies a radial glow effect to an image. + /// + /// The current image processing context. + /// The options effecting things like blending. + /// The the radius. + /// The . + public static IImageProcessingContext Glow( + this IImageProcessingContext source, + GraphicsOptions options, + float radius) => + source.Glow(options, Color.Black, ValueSize.Absolute(radius)); + + /// + /// Applies a radial glow effect to an image. + /// + /// The current image processing context. + /// The options effecting things like blending. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Glow( + this IImageProcessingContext source, + GraphicsOptions options, + Rectangle rectangle) => + source.Glow(options, Color.Black, ValueSize.PercentageOfWidth(0.5f), rectangle); + + /// + /// Applies a radial glow effect to an image. + /// + /// The current image processing context. + /// The options effecting things like blending. + /// The color to set as the glow. + /// The the radius. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Glow( + this IImageProcessingContext source, + GraphicsOptions options, + Color color, + float radius, + Rectangle rectangle) => + source.Glow(options, color, ValueSize.Absolute(radius), rectangle); + + /// + /// Applies a radial glow effect to an image. + /// + /// The current image processing context. + /// The options effecting things like blending. + /// The color to set as the glow. + /// The the radius. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + private static IImageProcessingContext Glow( + this IImageProcessingContext source, + GraphicsOptions options, + Color color, + ValueSize radius, + Rectangle rectangle) => + source.ApplyProcessor(new GlowProcessor(options, color, radius), rectangle); + + /// + /// Applies a radial glow effect to an image. + /// + /// The current image processing context. + /// The options effecting things like blending. + /// The color to set as the glow. + /// The the radius. + /// The . + private static IImageProcessingContext Glow( + this IImageProcessingContext source, + GraphicsOptions options, + Color color, + ValueSize radius) => + source.ApplyProcessor(new GlowProcessor(options, color, radius)); + } +} diff --git a/ImageSharp/Processing/Extensions/Overlays/VignetteExtensions.cs b/ImageSharp/Processing/Extensions/Overlays/VignetteExtensions.cs new file mode 100644 index 0000000..2397004 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Overlays/VignetteExtensions.cs @@ -0,0 +1,176 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Overlays; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the application of a radial glow to an + /// using Mutate/Clone. + /// + public static class VignetteExtensions + { + /// + /// Applies a radial vignette effect to an image. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext Vignette(this IImageProcessingContext source) => + Vignette(source, source.GetGraphicsOptions()); + + /// + /// Applies a radial vignette effect to an image. + /// + /// The current image processing context. + /// The color to set as the vignette. + /// The . + public static IImageProcessingContext Vignette(this IImageProcessingContext source, Color color) => + Vignette(source, source.GetGraphicsOptions(), color); + + /// + /// Applies a radial vignette effect to an image. + /// + /// The current image processing context. + /// The the x-radius. + /// The the y-radius. + /// The . + public static IImageProcessingContext Vignette( + this IImageProcessingContext source, + float radiusX, + float radiusY) => + Vignette(source, source.GetGraphicsOptions(), radiusX, radiusY); + + /// + /// Applies a radial vignette effect to an image. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Vignette(this IImageProcessingContext source, Rectangle rectangle) => + Vignette(source, source.GetGraphicsOptions(), rectangle); + + /// + /// Applies a radial vignette effect to an image. + /// + /// The current image processing context. + /// The color to set as the vignette. + /// The the x-radius. + /// The the y-radius. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Vignette( + this IImageProcessingContext source, + Color color, + float radiusX, + float radiusY, + Rectangle rectangle) => + source.Vignette(source.GetGraphicsOptions(), color, radiusX, radiusY, rectangle); + + /// + /// Applies a radial vignette effect to an image. + /// + /// The current image processing context. + /// The options effecting pixel blending. + /// The . + public static IImageProcessingContext Vignette(this IImageProcessingContext source, GraphicsOptions options) => + source.VignetteInternal( + options, + Color.Black, + ValueSize.PercentageOfWidth(.5f), + ValueSize.PercentageOfHeight(.5f)); + + /// + /// Applies a radial vignette effect to an image. + /// + /// The current image processing context. + /// The options effecting pixel blending. + /// The color to set as the vignette. + /// The . + public static IImageProcessingContext Vignette( + this IImageProcessingContext source, + GraphicsOptions options, + Color color) => + source.VignetteInternal( + options, + color, + ValueSize.PercentageOfWidth(.5f), + ValueSize.PercentageOfHeight(.5f)); + + /// + /// Applies a radial vignette effect to an image. + /// + /// The current image processing context. + /// The options effecting pixel blending. + /// The the x-radius. + /// The the y-radius. + /// The . + public static IImageProcessingContext Vignette( + this IImageProcessingContext source, + GraphicsOptions options, + float radiusX, + float radiusY) => + source.VignetteInternal(options, Color.Black, radiusX, radiusY); + + /// + /// Applies a radial vignette effect to an image. + /// + /// The current image processing context. + /// The options effecting pixel blending. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Vignette( + this IImageProcessingContext source, + GraphicsOptions options, + Rectangle rectangle) => + source.VignetteInternal( + options, + Color.Black, + ValueSize.PercentageOfWidth(.5f), + ValueSize.PercentageOfHeight(.5f), + rectangle); + + /// + /// Applies a radial vignette effect to an image. + /// + /// The current image processing context. + /// The options effecting pixel blending. + /// The color to set as the vignette. + /// The the x-radius. + /// The the y-radius. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Vignette( + this IImageProcessingContext source, + GraphicsOptions options, + Color color, + float radiusX, + float radiusY, + Rectangle rectangle) => + source.VignetteInternal(options, color, radiusX, radiusY, rectangle); + + private static IImageProcessingContext VignetteInternal( + this IImageProcessingContext source, + GraphicsOptions options, + Color color, + ValueSize radiusX, + ValueSize radiusY, + Rectangle rectangle) => + source.ApplyProcessor(new VignetteProcessor(options, color, radiusX, radiusY), rectangle); + + private static IImageProcessingContext VignetteInternal( + this IImageProcessingContext source, + GraphicsOptions options, + Color color, + ValueSize radiusX, + ValueSize radiusY) => + source.ApplyProcessor(new VignetteProcessor(options, color, radiusX, radiusY)); + } +} diff --git a/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs b/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs new file mode 100644 index 0000000..725356f --- /dev/null +++ b/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs @@ -0,0 +1,110 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the computation of image integrals on an + /// + public static partial class ProcessingExtensions + { + /// + /// Apply an image integral. + /// + /// The image on which to apply the integral. + /// The type of the pixel. + /// The containing all the sums. + public static Buffer2D CalculateIntegralImage(this Image source) + where TPixel : unmanaged, IPixel + => CalculateIntegralImage(source.Frames.RootFrame); + + /// + /// Apply an image integral. + /// + /// The image on which to apply the integral. + /// The bounds within the image frame to calculate. + /// The type of the pixel. + /// The containing all the sums. + public static Buffer2D CalculateIntegralImage(this Image source, Rectangle bounds) + where TPixel : unmanaged, IPixel + => CalculateIntegralImage(source.Frames.RootFrame, bounds); + + /// + /// Apply an image integral. + /// + /// The image frame on which to apply the integral. + /// The type of the pixel. + /// The containing all the sums. + public static Buffer2D CalculateIntegralImage(this ImageFrame source) + where TPixel : unmanaged, IPixel + => source.CalculateIntegralImage(source.Bounds); + + /// + /// Apply an image integral. + /// + /// The image frame on which to apply the integral. + /// The bounds within the image frame to calculate. + /// The type of the pixel. + /// The containing all the sums. + public static Buffer2D CalculateIntegralImage(this ImageFrame source, Rectangle bounds) + where TPixel : unmanaged, IPixel + { + Configuration configuration = source.Configuration; + + Rectangle interest = Rectangle.Intersect(bounds, source.Bounds); + int startY = interest.Y; + int startX = interest.X; + int endY = interest.Height; + + Buffer2D intImage = configuration.MemoryAllocator.Allocate2D(interest.Width, interest.Height); + ulong sumX0 = 0; + Buffer2D sourceBuffer = source.PixelBuffer; + + using (IMemoryOwner tempRow = configuration.MemoryAllocator.Allocate(interest.Width)) + { + Span tempSpan = tempRow.GetSpan(); + Span sourceRow = sourceBuffer.DangerousGetRowSpan(startY).Slice(startX, tempSpan.Length); + Span destRow = intImage.DangerousGetRowSpan(0); + + PixelOperations.Instance.ToL8(configuration, sourceRow, tempSpan); + + // First row + for (int x = 0; x < tempSpan.Length; x++) + { + sumX0 += tempSpan[x].PackedValue; + destRow[x] = sumX0; + } + + Span previousDestRow = destRow; + + // All other rows + for (int y = 1; y < endY; y++) + { + sourceRow = sourceBuffer.DangerousGetRowSpan(y + startY).Slice(startX, tempSpan.Length); + destRow = intImage.DangerousGetRowSpan(y); + + PixelOperations.Instance.ToL8(configuration, sourceRow, tempSpan); + + // Process first column + sumX0 = tempSpan[0].PackedValue; + destRow[0] = sumX0 + previousDestRow[0]; + + // Process all other colmns + for (int x = 1; x < tempSpan.Length; x++) + { + sumX0 += tempSpan[x].PackedValue; + destRow[x] = sumX0 + previousDestRow[x]; + } + + previousDestRow = destRow; + } + } + + return intImage; + } + } +} diff --git a/ImageSharp/Processing/Extensions/ProcessingExtensions.cs b/ImageSharp/Processing/Extensions/ProcessingExtensions.cs new file mode 100644 index 0000000..d38d07a --- /dev/null +++ b/ImageSharp/Processing/Extensions/ProcessingExtensions.cs @@ -0,0 +1,299 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors; +using System; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Adds extensions that allow the processing of images to the type. + /// + public static partial class ProcessingExtensions + { + /// + /// Mutates the source image by applying the image operation to it. + /// + /// The image to mutate. + /// The operation to perform on the source. + /// The source is null. + /// The operation is null. + /// The source has been disposed. + /// The processing operation failed. + public static void Mutate(this Image source, Action operation) + => Mutate(source, source.Configuration, operation); + + /// + /// Mutates the source image by applying the image operation to it. + /// + /// The image to mutate. + /// The configuration which allows altering default behaviour or extending the library. + /// The operation to perform on the source. + /// The configuration is null. + /// The source is null. + /// The operation is null. + /// The source has been disposed. + /// The processing operation failed. + public static void Mutate(this Image source, Configuration configuration, Action operation) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(source, nameof(source)); + Guard.NotNull(operation, nameof(operation)); + source.EnsureNotDisposed(); + + source.AcceptVisitor(new ProcessingVisitor(configuration, operation, true)); + } + + /// + /// Mutates the source image by applying the image operation to it. + /// + /// The pixel format. + /// The image to mutate. + /// The operation to perform on the source. + /// The source is null. + /// The operation is null. + /// The source has been disposed. + /// The processing operation failed. + public static void Mutate(this Image source, Action operation) + where TPixel : unmanaged, IPixel + => Mutate(source, source.Configuration, operation); + + /// + /// Mutates the source image by applying the image operation to it. + /// + /// The pixel format. + /// The image to mutate. + /// The configuration which allows altering default behaviour or extending the library. + /// The operation to perform on the source. + /// The configuration is null. + /// The source is null. + /// The operation is null. + /// The source has been disposed. + /// The processing operation failed. + public static void Mutate(this Image source, Configuration configuration, Action operation) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(source, nameof(source)); + Guard.NotNull(operation, nameof(operation)); + source.EnsureNotDisposed(); + + IInternalImageProcessingContext operationsRunner + = configuration.ImageOperationsProvider.CreateImageProcessingContext(configuration, source, true); + + operation(operationsRunner); + } + + /// + /// Mutates the source image by applying the operations to it. + /// + /// The pixel format. + /// The image to mutate. + /// The operations to perform on the source. + /// The source is null. + /// The operations are null. + /// The source has been disposed. + /// The processing operation failed. + public static void Mutate(this Image source, params IImageProcessor[] operations) + where TPixel : unmanaged, IPixel + => Mutate(source, source.Configuration, operations); + + /// + /// Mutates the source image by applying the operations to it. + /// + /// The pixel format. + /// The image to mutate. + /// The configuration which allows altering default behaviour or extending the library. + /// The operations to perform on the source. + /// The configuration is null. + /// The source is null. + /// The operations are null. + /// The source has been disposed. + /// The processing operation failed. + public static void Mutate(this Image source, Configuration configuration, params IImageProcessor[] operations) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(source, nameof(source)); + Guard.NotNull(operations, nameof(operations)); + source.EnsureNotDisposed(); + + IInternalImageProcessingContext operationsRunner + = configuration.ImageOperationsProvider.CreateImageProcessingContext(configuration, source, true); + + operationsRunner.ApplyProcessors(operations); + } + + /// + /// Creates a deep clone of the current image. The clone is then mutated by the given operation. + /// + /// The image to clone. + /// The operation to perform on the clone. + /// The new . + /// The source is null. + /// The operation is null. + /// The source has been disposed. + /// The processing operation failed. + public static Image Clone(this Image source, Action operation) + => Clone(source, source.Configuration, operation); + + /// + /// Creates a deep clone of the current image. The clone is then mutated by the given operation. + /// + /// The image to clone. + /// The configuration which allows altering default behaviour or extending the library. + /// The operation to perform on the clone. + /// The configuration is null. + /// The source is null. + /// The operation is null. + /// The source has been disposed. + /// The processing operation failed. + /// The new . + public static Image Clone(this Image source, Configuration configuration, Action operation) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(source, nameof(source)); + Guard.NotNull(operation, nameof(operation)); + source.EnsureNotDisposed(); + + ProcessingVisitor visitor = new(configuration, operation, false); + source.AcceptVisitor(visitor); + return visitor.GetResultImage(); + } + + /// + /// Creates a deep clone of the current image. The clone is then mutated by the given operation. + /// + /// The pixel format. + /// The image to clone. + /// The operation to perform on the clone. + /// The source is null. + /// The operation is null. + /// The source has been disposed. + /// The processing operation failed. + /// The new . + public static Image Clone(this Image source, Action operation) + where TPixel : unmanaged, IPixel + => Clone(source, source.Configuration, operation); + + /// + /// Creates a deep clone of the current image. The clone is then mutated by the given operation. + /// + /// The pixel format. + /// The image to clone. + /// The configuration which allows altering default behaviour or extending the library. + /// The operation to perform on the clone. + /// The configuration is null. + /// The source is null. + /// The operation is null. + /// The source has been disposed. + /// The processing operation failed. + /// The new + public static Image Clone(this Image source, Configuration configuration, Action operation) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(source, nameof(source)); + Guard.NotNull(operation, nameof(operation)); + source.EnsureNotDisposed(); + + IInternalImageProcessingContext operationsRunner + = configuration.ImageOperationsProvider.CreateImageProcessingContext(configuration, source, false); + + operation(operationsRunner); + return operationsRunner.GetResultImage(); + } + + /// + /// Creates a deep clone of the current image. The clone is then mutated by the given operations. + /// + /// The pixel format. + /// The image to clone. + /// The operations to perform on the clone. + /// The source is null. + /// The operations are null. + /// The source has been disposed. + /// The processing operation failed. + /// The new + public static Image Clone(this Image source, params IImageProcessor[] operations) + where TPixel : unmanaged, IPixel + => Clone(source, source.Configuration, operations); + + /// + /// Creates a deep clone of the current image. The clone is then mutated by the given operations. + /// + /// The pixel format. + /// The image to clone. + /// The configuration which allows altering default behaviour or extending the library. + /// The operations to perform on the clone. + /// The configuration is null. + /// The source is null. + /// The operations are null. + /// The source has been disposed. + /// The processing operation failed. + /// The new + public static Image Clone(this Image source, Configuration configuration, params IImageProcessor[] operations) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(source, nameof(source)); + Guard.NotNull(operations, nameof(operations)); + source.EnsureNotDisposed(); + + IInternalImageProcessingContext operationsRunner + = configuration.ImageOperationsProvider.CreateImageProcessingContext(configuration, source, false); + + operationsRunner.ApplyProcessors(operations); + return operationsRunner.GetResultImage(); + } + + /// + /// Applies the given collection against the context + /// + /// The image processing context. + /// The operations to perform on the source. + /// The processing operation failed. + /// The to allow chaining of operations. + public static IImageProcessingContext ApplyProcessors( + this IImageProcessingContext source, + params IImageProcessor[] operations) + { + foreach (IImageProcessor p in operations) + { + source = source.ApplyProcessor(p); + } + + return source; + } + + private class ProcessingVisitor : IImageVisitor + { + private readonly Configuration configuration; + + private readonly Action operation; + + private readonly bool mutate; + + private Image? resultImage; + + public ProcessingVisitor(Configuration configuration, Action operation, bool mutate) + { + this.configuration = configuration; + this.operation = operation; + this.mutate = mutate; + } + + public Image GetResultImage() => this.resultImage!; + + public void Visit(Image image) + where TPixel : unmanaged, IPixel + { + IInternalImageProcessingContext operationsRunner = + this.configuration.ImageOperationsProvider.CreateImageProcessingContext(this.configuration, image, this.mutate); + + this.operation(operationsRunner); + this.resultImage = operationsRunner.GetResultImage(); + } + } + } +} diff --git a/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs b/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs new file mode 100644 index 0000000..e4cbe9f --- /dev/null +++ b/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the application of quantizing algorithms on an + /// using Mutate/Clone. + /// + public static class QuantizeExtensions + { + /// + /// Applies quantization to the image using the . + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext Quantize(this IImageProcessingContext source) => + Quantize(source, KnownQuantizers.Hexadecatree); + + /// + /// Applies quantization to the image. + /// + /// The current image processing context. + /// The quantizer to apply to perform the operation. + /// The . + public static IImageProcessingContext Quantize(this IImageProcessingContext source, IQuantizer quantizer) => + source.ApplyProcessor(new QuantizeProcessor(quantizer)); + + /// + /// Applies quantization to the image using the . + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Quantize(this IImageProcessingContext source, Rectangle rectangle) => + Quantize(source, KnownQuantizers.Hexadecatree, rectangle); + + /// + /// Applies quantization to the image. + /// + /// The current image processing context. + /// The quantizer to apply to perform the operation. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Quantize(this IImageProcessingContext source, IQuantizer quantizer, Rectangle rectangle) => + source.ApplyProcessor(new QuantizeProcessor(quantizer), rectangle); + } +} diff --git a/ImageSharp/Processing/Extensions/Transforms/AutoOrientExtensions.cs b/ImageSharp/Processing/Extensions/Transforms/AutoOrientExtensions.cs new file mode 100644 index 0000000..d735cd4 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Transforms/AutoOrientExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the application of auto-orientation operations to an + /// using Mutate/Clone. + /// + public static class AutoOrientExtensions + { + /// + /// Adjusts an image so that its orientation is suitable for viewing. Adjustments are based on EXIF metadata embedded in the image. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext AutoOrient(this IImageProcessingContext source) + => source.ApplyProcessor(new AutoOrientProcessor()); + } +} diff --git a/ImageSharp/Processing/Extensions/Transforms/CropExtensions.cs b/ImageSharp/Processing/Extensions/Transforms/CropExtensions.cs new file mode 100644 index 0000000..e614ee4 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Transforms/CropExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the application of cropping operations on an + /// using Mutate/Clone. + /// + public static class CropExtensions + { + /// + /// Crops an image to the given width and height. + /// + /// The current image processing context. + /// The target image width. + /// The target image height. + /// The . + public static IImageProcessingContext Crop(this IImageProcessingContext source, int width, int height) => + Crop(source, new Rectangle(0, 0, width, height)); + + /// + /// Crops an image to the given rectangle. + /// + /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to retain. + /// + /// The . + public static IImageProcessingContext Crop(this IImageProcessingContext source, Rectangle cropRectangle) => + source.ApplyProcessor(new CropProcessor(cropRectangle, source.GetCurrentSize())); + } +} diff --git a/ImageSharp/Processing/Extensions/Transforms/EntropyCropExtensions.cs b/ImageSharp/Processing/Extensions/Transforms/EntropyCropExtensions.cs new file mode 100644 index 0000000..139be1f --- /dev/null +++ b/ImageSharp/Processing/Extensions/Transforms/EntropyCropExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the application of entropy cropping operations on an + /// using Mutate/Clone. + /// + public static class EntropyCropExtensions + { + /// + /// Crops an image to the area of greatest entropy using a threshold for entropic density of .5F. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext EntropyCrop(this IImageProcessingContext source) => + source.ApplyProcessor(new EntropyCropProcessor()); + + /// + /// Crops an image to the area of greatest entropy. + /// + /// The current image processing context. + /// The threshold for entropic density. + /// The . + public static IImageProcessingContext EntropyCrop(this IImageProcessingContext source, float threshold) => + source.ApplyProcessor(new EntropyCropProcessor(threshold)); + } +} diff --git a/ImageSharp/Processing/Extensions/Transforms/FlipExtensions.cs b/ImageSharp/Processing/Extensions/Transforms/FlipExtensions.cs new file mode 100644 index 0000000..4989979 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Transforms/FlipExtensions.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the application of flipping operations on an + /// using Mutate/Clone. + /// + public static class FlipExtensions + { + /// + /// Flips an image by the given instructions. + /// + /// The current image processing context. + /// The to perform the flip. + /// The . + public static IImageProcessingContext Flip(this IImageProcessingContext source, FlipMode flipMode) + => source.ApplyProcessor(new FlipProcessor(flipMode)); + } +} diff --git a/ImageSharp/Processing/Extensions/Transforms/PadExtensions.cs b/ImageSharp/Processing/Extensions/Transforms/PadExtensions.cs new file mode 100644 index 0000000..d20a51e --- /dev/null +++ b/ImageSharp/Processing/Extensions/Transforms/PadExtensions.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the application of padding operations on an + /// using Mutate/Clone. + /// + public static class PadExtensions + { + /// + /// Evenly pads an image to fit the new dimensions. + /// + /// The current image processing context. + /// The new width. + /// The new height. + /// The . + public static IImageProcessingContext Pad(this IImageProcessingContext source, int width, int height) + => source.Pad(width, height, default); + + /// + /// Evenly pads an image to fit the new dimensions with the given background color. + /// + /// The current image processing context. + /// The new width. + /// The new height. + /// The background color with which to pad the image. + /// The . + public static IImageProcessingContext Pad(this IImageProcessingContext source, int width, int height, Color color) + { + Size size = source.GetCurrentSize(); + ResizeOptions options = new() + { + // Prevent downsizing. + Size = new Size(Math.Max(width, size.Width), Math.Max(height, size.Height)), + Mode = ResizeMode.BoxPad, + Sampler = KnownResamplers.NearestNeighbor, + PadColor = color + }; + + return source.Resize(options); + } + } +} diff --git a/ImageSharp/Processing/Extensions/Transforms/ResizeExtensions.cs b/ImageSharp/Processing/Extensions/Transforms/ResizeExtensions.cs new file mode 100644 index 0000000..c445726 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Transforms/ResizeExtensions.cs @@ -0,0 +1,175 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the application of resize operations on an + /// using Mutate/Clone. + /// + public static class ResizeExtensions + { + /// + /// Resizes an image to the given . + /// + /// The current image processing context. + /// The target image size. + /// The . + /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image or the nearest possible ratio. + public static IImageProcessingContext Resize(this IImageProcessingContext source, Size size) + => Resize(source, size.Width, size.Height, KnownResamplers.Bicubic, false); + + /// + /// Resizes an image to the given . + /// + /// The current image processing context. + /// The target image size. + /// Whether to compress and expand the image color-space to gamma correct the image during processing. + /// The . + /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image or the nearest possible ratio. + public static IImageProcessingContext Resize(this IImageProcessingContext source, Size size, bool compand) + => Resize(source, size.Width, size.Height, KnownResamplers.Bicubic, compand); + + /// + /// Resizes an image to the given width and height. + /// + /// The current image processing context. + /// The target image width. + /// The target image height. + /// The . + /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image or the nearest possible ratio. + public static IImageProcessingContext Resize(this IImageProcessingContext source, int width, int height) + => Resize(source, width, height, KnownResamplers.Bicubic, false); + + /// + /// Resizes an image to the given width and height. + /// + /// The current image processing context. + /// The target image width. + /// The target image height. + /// Whether to compress and expand the image color-space to gamma correct the image during processing. + /// The . + /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image or the nearest possible ratio. + public static IImageProcessingContext Resize(this IImageProcessingContext source, int width, int height, bool compand) + => Resize(source, width, height, KnownResamplers.Bicubic, compand); + + /// + /// Resizes an image to the given width and height with the given sampler. + /// + /// The current image processing context. + /// The target image width. + /// The target image height. + /// The to perform the resampling. + /// The . + /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image or the nearest possible ratio. + public static IImageProcessingContext Resize(this IImageProcessingContext source, int width, int height, IResampler sampler) + => Resize(source, width, height, sampler, false); + + /// + /// Resizes an image to the given width and height with the given sampler. + /// + /// The current image processing context. + /// The target image size. + /// The to perform the resampling. + /// Whether to compress and expand the image color-space to gamma correct the image during processing. + /// The . + /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image or the nearest possible ratio. + public static IImageProcessingContext Resize(this IImageProcessingContext source, Size size, IResampler sampler, bool compand) + => Resize(source, size.Width, size.Height, sampler, new Rectangle(0, 0, size.Width, size.Height), compand); + + /// + /// Resizes an image to the given width and height with the given sampler. + /// + /// The current image processing context. + /// The target image width. + /// The target image height. + /// The to perform the resampling. + /// Whether to compress and expand the image color-space to gamma correct the image during processing. + /// The . + /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image or the nearest possible ratio. + public static IImageProcessingContext Resize(this IImageProcessingContext source, int width, int height, IResampler sampler, bool compand) + => Resize(source, width, height, sampler, new Rectangle(0, 0, width, height), compand); + + /// + /// Resizes an image to the given width and height with the given sampler and + /// source rectangle. + /// + /// The current image processing context. + /// The target image width. + /// The target image height. + /// The to perform the resampling. + /// + /// The structure that specifies the portion of the image object to draw. + /// + /// + /// The structure that specifies the portion of the target image object to draw to. + /// + /// Whether to compress and expand the image color-space to gamma correct the image during processing. + /// The . + /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image or the nearest possible ratio. + public static IImageProcessingContext Resize( + this IImageProcessingContext source, + int width, + int height, + IResampler sampler, + Rectangle sourceRectangle, + Rectangle targetRectangle, + bool compand) + { + ResizeOptions options = new() + { + Size = new Size(width, height), + Mode = ResizeMode.Manual, + Sampler = sampler, + TargetRectangle = targetRectangle, + Compand = compand + }; + + return source.ApplyProcessor(new ResizeProcessor(options, source.GetCurrentSize()), sourceRectangle); + } + + /// + /// Resizes an image to the given width and height with the given sampler and source rectangle. + /// + /// The current image processing context. + /// The target image width. + /// The target image height. + /// The to perform the resampling. + /// + /// The structure that specifies the portion of the target image object to draw to. + /// + /// Whether to compress and expand the image color-space to gamma correct the image during processing. + /// The . + /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image or the nearest possible ratio. + public static IImageProcessingContext Resize( + this IImageProcessingContext source, + int width, + int height, + IResampler sampler, + Rectangle targetRectangle, + bool compand) + { + ResizeOptions options = new() + { + Size = new Size(width, height), + Mode = ResizeMode.Manual, + Sampler = sampler, + TargetRectangle = targetRectangle, + Compand = compand + }; + + return Resize(source, options); + } + + /// + /// Resizes an image in accordance with the given . + /// + /// The current image processing context. + /// The resize options. + /// The . + /// Passing zero for one of height or width within the resize options will automatically preserve the aspect ratio of the original image or the nearest possible ratio. + public static IImageProcessingContext Resize(this IImageProcessingContext source, ResizeOptions options) + => source.ApplyProcessor(new ResizeProcessor(options, source.GetCurrentSize())); + } +} diff --git a/ImageSharp/Processing/Extensions/Transforms/RotateExtensions.cs b/ImageSharp/Processing/Extensions/Transforms/RotateExtensions.cs new file mode 100644 index 0000000..a579412 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Transforms/RotateExtensions.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the application of rotate operations on an + /// using Mutate/Clone. + /// + public static class RotateExtensions + { + /// + /// Rotates and flips an image by the given instructions. + /// + /// The current image processing context. + /// The to perform the rotation. + /// The . + public static IImageProcessingContext Rotate(this IImageProcessingContext source, RotateMode rotateMode) => + Rotate(source, (float)rotateMode); + + /// + /// Rotates an image by the given angle in degrees. + /// + /// The current image processing context. + /// The angle in degrees to perform the rotation. + /// The . + public static IImageProcessingContext Rotate(this IImageProcessingContext source, float degrees) => + Rotate(source, degrees, KnownResamplers.Bicubic); + + /// + /// Rotates an image by the given angle in degrees using the specified sampling algorithm. + /// + /// The current image processing context. + /// The angle in degrees to perform the rotation. + /// The to perform the resampling. + /// The . + public static IImageProcessingContext Rotate( + this IImageProcessingContext source, + float degrees, + IResampler sampler) => + source.ApplyProcessor(new RotateProcessor(degrees, sampler, source.GetCurrentSize())); + } +} diff --git a/ImageSharp/Processing/Extensions/Transforms/RotateFlipExtensions.cs b/ImageSharp/Processing/Extensions/Transforms/RotateFlipExtensions.cs new file mode 100644 index 0000000..689051f --- /dev/null +++ b/ImageSharp/Processing/Extensions/Transforms/RotateFlipExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the application of rotate-flip operations on an + /// using Mutate/Clone. + /// + public static class RotateFlipExtensions + { + /// + /// Rotates and flips an image by the given instructions. + /// + /// The current image processing context. + /// The to perform the rotation. + /// The to perform the flip. + /// The . + public static IImageProcessingContext RotateFlip(this IImageProcessingContext source, RotateMode rotateMode, FlipMode flipMode) + => source.Rotate(rotateMode).Flip(flipMode); + } +} diff --git a/ImageSharp/Processing/Extensions/Transforms/SkewExtensions.cs b/ImageSharp/Processing/Extensions/Transforms/SkewExtensions.cs new file mode 100644 index 0000000..598bff1 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Transforms/SkewExtensions.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the application of skew operations on an + /// using Mutate/Clone. + /// + public static class SkewExtensions + { + /// + /// Skews an image by the given angles in degrees. + /// + /// The current image processing context. + /// The angle in degrees to perform the skew along the x-axis. + /// The angle in degrees to perform the skew along the y-axis. + /// The . + public static IImageProcessingContext + Skew(this IImageProcessingContext source, float degreesX, float degreesY) => + Skew(source, degreesX, degreesY, KnownResamplers.Bicubic); + + /// + /// Skews an image by the given angles in degrees using the specified sampling algorithm. + /// + /// The current image processing context. + /// The angle in degrees to perform the skew along the x-axis. + /// The angle in degrees to perform the skew along the y-axis. + /// The to perform the resampling. + /// The . + public static IImageProcessingContext Skew( + this IImageProcessingContext source, + float degreesX, + float degreesY, + IResampler sampler) => + source.ApplyProcessor(new SkewProcessor(degreesX, degreesY, sampler, source.GetCurrentSize())); + } +} diff --git a/ImageSharp/Processing/Extensions/Transforms/SwizzleExtensions.cs b/ImageSharp/Processing/Extensions/Transforms/SwizzleExtensions.cs new file mode 100644 index 0000000..dad417c --- /dev/null +++ b/ImageSharp/Processing/Extensions/Transforms/SwizzleExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the application of swizzle operations on an + /// + public static class SwizzleExtensions + { + /// + /// Swizzles an image. + /// + /// The current image processing context. + /// The swizzler function. + /// The swizzler function type. + /// The . + public static IImageProcessingContext Swizzle(this IImageProcessingContext source, TSwizzler swizzler) + where TSwizzler : struct, ISwizzler + => source.ApplyProcessor(new SwizzleProcessor(swizzler)); + } +} diff --git a/ImageSharp/Processing/Extensions/Transforms/TransformExtensions.cs b/ImageSharp/Processing/Extensions/Transforms/TransformExtensions.cs new file mode 100644 index 0000000..4495231 --- /dev/null +++ b/ImageSharp/Processing/Extensions/Transforms/TransformExtensions.cs @@ -0,0 +1,138 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Defines extensions that allow the application of composable transform operations + /// on an using Mutate/Clone. + /// + public static class TransformExtensions + { + /// + /// Performs an affine transform of an image. + /// + /// The current image processing context. + /// The affine transform builder. + /// The . + public static IImageProcessingContext Transform( + this IImageProcessingContext source, + AffineTransformBuilder builder) => + Transform(source, builder, KnownResamplers.Bicubic); + + /// + /// Performs an affine transform of an image using the specified sampling algorithm. + /// + /// The current image processing context. + /// The affine transform builder. + /// The to perform the resampling. + /// The . + public static IImageProcessingContext Transform( + this IImageProcessingContext source, + AffineTransformBuilder builder, + IResampler sampler) => + source.Transform(new Rectangle(Point.Empty, source.GetCurrentSize()), builder, sampler); + + /// + /// Performs an affine transform of an image using the specified sampling algorithm. + /// + /// The current image processing context. + /// The source rectangle + /// The affine transform builder. + /// The to perform the resampling. + /// The . + public static IImageProcessingContext Transform( + this IImageProcessingContext source, + Rectangle sourceRectangle, + AffineTransformBuilder builder, + IResampler sampler) + { + Matrix3x2 transform = builder.BuildMatrix(sourceRectangle); + Size targetDimensions = TransformUtilities.GetTransformedCanvasSize(transform, sourceRectangle.Size); + return source.Transform(sourceRectangle, transform, targetDimensions, sampler); + } + + /// + /// Performs an affine transform of an image using the specified sampling algorithm. + /// + /// The current image processing context. + /// The source rectangle + /// The transformation matrix. + /// The size of the result image. + /// The to perform the resampling. + /// The . + public static IImageProcessingContext Transform( + this IImageProcessingContext source, + Rectangle sourceRectangle, + Matrix3x2 transform, + Size targetDimensions, + IResampler sampler) + => source.ApplyProcessor( + new AffineTransformProcessor(transform, sampler, targetDimensions), + sourceRectangle); + + /// + /// Performs a projective transform of an image. + /// + /// The current image processing context. + /// The affine transform builder. + /// The . + public static IImageProcessingContext Transform( + this IImageProcessingContext source, + ProjectiveTransformBuilder builder) => + Transform(source, builder, KnownResamplers.Bicubic); + + /// + /// Performs a projective transform of an image using the specified sampling algorithm. + /// + /// The current image processing context. + /// The projective transform builder. + /// The to perform the resampling. + /// The . + public static IImageProcessingContext Transform( + this IImageProcessingContext source, + ProjectiveTransformBuilder builder, + IResampler sampler) => + source.Transform(new Rectangle(Point.Empty, source.GetCurrentSize()), builder, sampler); + + /// + /// Performs a projective transform of an image using the specified sampling algorithm. + /// + /// The current image processing context. + /// The source rectangle + /// The projective transform builder. + /// The to perform the resampling. + /// The . + public static IImageProcessingContext Transform( + this IImageProcessingContext source, + Rectangle sourceRectangle, + ProjectiveTransformBuilder builder, + IResampler sampler) + { + Matrix4x4 transform = builder.BuildMatrix(sourceRectangle); + Size targetDimensions = TransformUtilities.GetTransformedCanvasSize(transform, sourceRectangle.Size); + return source.Transform(sourceRectangle, transform, targetDimensions, sampler); + } + + /// + /// Performs a projective transform of an image using the specified sampling algorithm. + /// + /// The current image processing context. + /// The source rectangle + /// The transformation matrix. + /// The size of the result image. + /// The to perform the resampling. + /// The . + public static IImageProcessingContext Transform( + this IImageProcessingContext source, + Rectangle sourceRectangle, + Matrix4x4 transform, + Size targetDimensions, + IResampler sampler) + => source.ApplyProcessor( + new ProjectiveTransformProcessor(transform, sampler, targetDimensions), + sourceRectangle); + } +} diff --git a/ImageSharp/Processing/FlipMode.cs b/ImageSharp/Processing/FlipMode.cs new file mode 100644 index 0000000..48330fe --- /dev/null +++ b/ImageSharp/Processing/FlipMode.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing { + /// + /// Provides enumeration over how a image should be flipped. + /// + public enum FlipMode + { + /// + /// Don't flip the image. + /// + None, + + /// + /// Flip the image horizontally. + /// + Horizontal, + + /// + /// Flip the image vertically. + /// + Vertical, + } +} diff --git a/ImageSharp/Processing/GrayscaleMode.cs b/ImageSharp/Processing/GrayscaleMode.cs new file mode 100644 index 0000000..5f33d77 --- /dev/null +++ b/ImageSharp/Processing/GrayscaleMode.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing { + /// + /// Enumerates the various types of defined grayscale filters. + /// + public enum GrayscaleMode + { + /// + /// ITU-R Recommendation BT.709 + /// + Bt709, + + /// + /// ITU-R Recommendation BT.601 + /// + Bt601 + } +} diff --git a/ImageSharp/Processing/IImageProcessingContext.cs b/ImageSharp/Processing/IImageProcessingContext.cs new file mode 100644 index 0000000..3f6f7d8 --- /dev/null +++ b/ImageSharp/Processing/IImageProcessingContext.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Processing { + /// + /// A pixel-agnostic interface to queue up image operations to apply to an image. + /// + public interface IImageProcessingContext + { + /// + /// Gets the configuration which allows altering default behaviour or extending the library. + /// + Configuration Configuration { get; } + + /// + /// Gets a set of properties for the Image Processing Context. + /// + /// This can be used for storing global settings and defaults to be accessable to processors. + IDictionary Properties { get; } + + /// + /// Gets the image dimensions at the current point in the processing pipeline. + /// + /// The . + Size GetCurrentSize(); + + /// + /// Adds the processor to the current set of image operations to be applied. + /// + /// The processor to apply. + /// The area to apply it to. + /// The current operations class to allow chaining of operations. + IImageProcessingContext ApplyProcessor(IImageProcessor processor, Rectangle rectangle); + + /// + /// Adds the processor to the current set of image operations to be applied. + /// + /// The processor to apply. + /// The current operations class to allow chaining of operations. + IImageProcessingContext ApplyProcessor(IImageProcessor processor); + } +} diff --git a/ImageSharp/Processing/IImageProcessingContextFactory.cs b/ImageSharp/Processing/IImageProcessingContextFactory.cs new file mode 100644 index 0000000..f0270d7 --- /dev/null +++ b/ImageSharp/Processing/IImageProcessingContextFactory.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Represents an interface that will create IInternalImageProcessingContext instances + /// + internal interface IImageProcessingContextFactory + { + /// + /// Called during mutate operations to generate the image operations provider. + /// + /// The pixel format + /// The configuration which allows altering default behaviour or extending the library. + /// The source image. + /// A flag to determine whether image operations are allowed to mutate the source image. + /// A new + IInternalImageProcessingContext CreateImageProcessingContext(Configuration configuration, Image source, bool mutate) + where TPixel : unmanaged, IPixel; + } + + /// + /// The default implementation of + /// + internal class DefaultImageOperationsProviderFactory : IImageProcessingContextFactory + { + /// + public IInternalImageProcessingContext CreateImageProcessingContext(Configuration configuration, Image source, bool mutate) + where TPixel : unmanaged, IPixel + { + return new DefaultImageProcessorContext(configuration, source, mutate); + } + } +} diff --git a/ImageSharp/Processing/IInternalImageProcessingContext{TPixel}.cs b/ImageSharp/Processing/IInternalImageProcessingContext{TPixel}.cs new file mode 100644 index 0000000..2901608 --- /dev/null +++ b/ImageSharp/Processing/IInternalImageProcessingContext{TPixel}.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing { + /// + /// An interface for internal operations we don't want to expose on . + /// + /// The pixel type. + internal interface IInternalImageProcessingContext : IImageProcessingContext + where TPixel : unmanaged, IPixel + { + /// + /// Returns the result image to return by + /// (and other overloads). + /// + /// The current image or a new image depending on whether it is requested to mutate the source image. + Image GetResultImage(); + } +} diff --git a/ImageSharp/Processing/KnownDitherings.cs b/ImageSharp/Processing/KnownDitherings.cs new file mode 100644 index 0000000..78b1a57 --- /dev/null +++ b/ImageSharp/Processing/KnownDitherings.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Dithering; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Contains reusable static instances of known dithering algorithms. + /// + public static class KnownDitherings + { + /// + /// Gets the order ditherer using the 2x2 Bayer dithering matrix + /// + public static IDither Bayer2x2 { get; } = OrderedDither.Bayer2x2; + + /// + /// Gets the order ditherer using the 3x3 dithering matrix + /// + public static IDither Ordered3x3 { get; } = OrderedDither.Ordered3x3; + + /// + /// Gets the order ditherer using the 4x4 Bayer dithering matrix + /// + public static IDither Bayer4x4 { get; } = OrderedDither.Bayer4x4; + + /// + /// Gets the order ditherer using the 8x8 Bayer dithering matrix + /// + public static IDither Bayer8x8 { get; } = OrderedDither.Bayer8x8; + + /// + /// Gets the order ditherer using the 16x16 Bayer dithering matrix + /// + public static IDither Bayer16x16 { get; } = OrderedDither.Bayer16x16; + + /// + /// Gets the error Dither that implements the Atkinson algorithm. + /// + public static IDither Atkinson { get; } = ErrorDither.Atkinson; + + /// + /// Gets the error Dither that implements the Burks algorithm. + /// + public static IDither Burks { get; } = ErrorDither.Burkes; + + /// + /// Gets the error Dither that implements the Floyd-Steinberg algorithm. + /// + public static IDither FloydSteinberg { get; } = ErrorDither.FloydSteinberg; + + /// + /// Gets the error Dither that implements the Jarvis-Judice-Ninke algorithm. + /// + public static IDither JarvisJudiceNinke { get; } = ErrorDither.JarvisJudiceNinke; + + /// + /// Gets the error Dither that implements the Sierra-2 algorithm. + /// + public static IDither Sierra2 { get; } = ErrorDither.Sierra2; + + /// + /// Gets the error Dither that implements the Sierra-3 algorithm. + /// + public static IDither Sierra3 { get; } = ErrorDither.Sierra3; + + /// + /// Gets the error Dither that implements the Sierra-Lite algorithm. + /// + public static IDither SierraLite { get; } = ErrorDither.SierraLite; + + /// + /// Gets the error Dither that implements the Stevenson-Arce algorithm. + /// + public static IDither StevensonArce { get; } = ErrorDither.StevensonArce; + + /// + /// Gets the error Dither that implements the Stucki algorithm. + /// + public static IDither Stucki { get; } = ErrorDither.Stucki; + } +} diff --git a/ImageSharp/Processing/KnownEdgeDetectorKernels.cs b/ImageSharp/Processing/KnownEdgeDetectorKernels.cs new file mode 100644 index 0000000..df18656 --- /dev/null +++ b/ImageSharp/Processing/KnownEdgeDetectorKernels.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Contains reusable static instances of known edge detection kernels. + /// + public static class KnownEdgeDetectorKernels + { + /// + /// Gets the Kayyali edge detector kernel. + /// + public static EdgeDetector2DKernel Kayyali { get; } = EdgeDetector2DKernel.KayyaliKernel; + + /// + /// Gets the Kirsch edge detector kernel. + /// + public static EdgeDetectorCompassKernel Kirsch { get; } = EdgeDetectorCompassKernel.Kirsch; + + /// + /// Gets the Laplacian 3x3 edge detector kernel. + /// + public static EdgeDetectorKernel Laplacian3x3 { get; } = EdgeDetectorKernel.Laplacian3x3; + + /// + /// Gets the Laplacian 5x5 edge detector kernel. + /// + public static EdgeDetectorKernel Laplacian5x5 { get; } = EdgeDetectorKernel.Laplacian5x5; + + /// + /// Gets the Laplacian of Gaussian edge detector kernel. + /// + public static EdgeDetectorKernel LaplacianOfGaussian { get; } = EdgeDetectorKernel.LaplacianOfGaussian; + + /// + /// Gets the Prewitt edge detector kernel. + /// + public static EdgeDetector2DKernel Prewitt { get; } = EdgeDetector2DKernel.PrewittKernel; + + /// + /// Gets the Roberts-Cross edge detector kernel. + /// + public static EdgeDetector2DKernel RobertsCross { get; } = EdgeDetector2DKernel.RobertsCrossKernel; + + /// + /// Gets the Robinson edge detector kernel. + /// + public static EdgeDetectorCompassKernel Robinson { get; } = EdgeDetectorCompassKernel.Robinson; + + /// + /// Gets the Scharr edge detector kernel. + /// + public static EdgeDetector2DKernel Scharr { get; } = EdgeDetector2DKernel.ScharrKernel; + + /// + /// Gets the Sobel edge detector kernel. + /// + public static EdgeDetector2DKernel Sobel { get; } = EdgeDetector2DKernel.SobelKernel; + } +} diff --git a/ImageSharp/Processing/KnownFilterMatrices.cs b/ImageSharp/Processing/KnownFilterMatrices.cs new file mode 100644 index 0000000..67cc455 --- /dev/null +++ b/ImageSharp/Processing/KnownFilterMatrices.cs @@ -0,0 +1,488 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// Many of these matrices are translated from Chromium project where +// SkScalar[] is memory-mapped to a row-major matrix. +// The following translates to our column-major form: +// +// | 0| 1| 2| 3| 4| |0|5|10|15| |M11|M12|M13|M14| +// | 5| 6| 7| 8| 9| |1|6|11|16| |M21|M22|M23|M24| +// |10|11|12|13|14| = |2|7|12|17| = |M31|M32|M33|M34| +// |15|16|17|18|19| |3|8|13|18| |M41|M42|M43|M44| +// |4|9|14|19| |M51|M52|M53|M54| +using System; + +namespace SixLabors.ImageSharp.Processing { + /// + /// A collection of known values for composing filters + /// + public static class KnownFilterMatrices + { + /// + /// Gets a filter recreating Achromatomaly (Color desensitivity) color blindness + /// + public static ColorMatrix AchromatomalyFilter { get; } = new() + { + M11 = .618F, + M12 = .163F, + M13 = .163F, + M21 = .320F, + M22 = .775F, + M23 = .320F, + M31 = .062F, + M32 = .062F, + M33 = .516F, + M44 = 1 + }; + + /// + /// Gets a filter recreating Achromatopsia (Monochrome) color blindness. + /// + public static ColorMatrix AchromatopsiaFilter { get; } = new() + { + M11 = .299F, + M12 = .299F, + M13 = .299F, + M21 = .587F, + M22 = .587F, + M23 = .587F, + M31 = .114F, + M32 = .114F, + M33 = .114F, + M44 = 1F + }; + + /// + /// Gets a filter recreating Deuteranomaly (Green-Weak) color blindness. + /// + public static ColorMatrix DeuteranomalyFilter { get; } = new() + { + M11 = .8F, + M12 = .258F, + M21 = .2F, + M22 = .742F, + M23 = .142F, + M33 = .858F, + M44 = 1F + }; + + /// + /// Gets a filter recreating Deuteranopia (Green-Blind) color blindness. + /// + public static ColorMatrix DeuteranopiaFilter { get; } = new() + { + M11 = .625F, + M12 = .7F, + M21 = .375F, + M22 = .3F, + M23 = .3F, + M33 = .7F, + M44 = 1F + }; + + /// + /// Gets a filter recreating Protanomaly (Red-Weak) color blindness. + /// + public static ColorMatrix ProtanomalyFilter { get; } = new() + { + M11 = .817F, + M12 = .333F, + M21 = .183F, + M22 = .667F, + M23 = .125F, + M33 = .875F, + M44 = 1F + }; + + /// + /// Gets a filter recreating Protanopia (Red-Blind) color blindness. + /// + public static ColorMatrix ProtanopiaFilter { get; } = new() + { + M11 = .567F, + M12 = .558F, + M21 = .433F, + M22 = .442F, + M23 = .242F, + M33 = .758F, + M44 = 1F + }; + + /// + /// Gets a filter recreating Tritanomaly (Blue-Weak) color blindness. + /// + public static ColorMatrix TritanomalyFilter { get; } = new() + { + M11 = .967F, + M21 = .33F, + M22 = .733F, + M23 = .183F, + M32 = .267F, + M33 = .817F, + M44 = 1F + }; + + /// + /// Gets a filter recreating Tritanopia (Blue-Blind) color blindness. + /// + public static ColorMatrix TritanopiaFilter { get; } = new() + { + M11 = .95F, + M21 = .05F, + M22 = .433F, + M23 = .475F, + M32 = .567F, + M33 = .525F, + M44 = 1F + }; + + /// + /// Gets an approximated black and white filter + /// + public static ColorMatrix BlackWhiteFilter { get; } = new() + { + M11 = 1.5F, + M12 = 1.5F, + M13 = 1.5F, + M21 = 1.5F, + M22 = 1.5F, + M23 = 1.5F, + M31 = 1.5F, + M32 = 1.5F, + M33 = 1.5F, + M44 = 1F, + M51 = -1F, + M52 = -1F, + M53 = -1F, + }; + + /// + /// Gets a filter recreating an old Kodachrome camera effect. + /// + public static ColorMatrix KodachromeFilter { get; } = new ColorMatrix + { + M11 = .7297023F, + M22 = .6109577F, + M33 = .597218F, + M44 = 1F, + M51 = .105F, + M52 = .145F, + M53 = .155F, + } + + * CreateSaturateFilter(1.2F) * CreateContrastFilter(1.35F); + + /// + /// Gets a filter recreating an old Lomograph camera effect. + /// + public static ColorMatrix LomographFilter { get; } = new ColorMatrix + { + M11 = 1.5F, + M22 = 1.45F, + M33 = 1.16F, + M44 = 1F, + M51 = -.1F, + M52 = -.02F, + M53 = -.07F, + } + + * CreateSaturateFilter(1.1F) * CreateContrastFilter(1.33F); + + /// + /// Gets a filter recreating an old Polaroid camera effect. + /// + public static ColorMatrix PolaroidFilter { get; } = new() + { + M11 = 1.538F, + M12 = -.062F, + M13 = -.262F, + M21 = -.022F, + M22 = 1.578F, + M23 = -.022F, + M31 = .216F, + M32 = -.16F, + M33 = 1.5831F, + M44 = 1F, + M51 = .02F, + M52 = -.05F, + M53 = -.05F + }; + + /// + /// Create a brightness filter matrix using the given amount. + /// + /// + /// A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing brighter results. + /// + /// The proportion of the conversion. Must be greater than or equal to 0. + /// The + public static ColorMatrix CreateBrightnessFilter(float amount) + { + Guard.MustBeGreaterThanOrEqualTo(amount, 0, nameof(amount)); + + // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc + return new ColorMatrix + { + M11 = amount, + M22 = amount, + M33 = amount, + M44 = 1F + }; + } + + /// + /// Create a contrast filter matrix using the given amount. + /// + /// + /// A value of 0 will create an image that is completely gray. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing results with more contrast. + /// + /// The proportion of the conversion. Must be greater than or equal to 0. + /// The + public static ColorMatrix CreateContrastFilter(float amount) + { + Guard.MustBeGreaterThanOrEqualTo(amount, 0, nameof(amount)); + + // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc + float contrast = (-.5F * amount) + .5F; + + return new ColorMatrix + { + M11 = amount, + M22 = amount, + M33 = amount, + M44 = 1F, + M51 = contrast, + M52 = contrast, + M53 = contrast + }; + } + + /// + /// Create a grayscale filter matrix using the given amount using the formula as specified by ITU-R Recommendation BT.601. + /// + /// + /// The proportion of the conversion. Must be between 0 and 1. + /// The + public static ColorMatrix CreateGrayscaleBt601Filter(float amount) + { + Guard.MustBeBetweenOrEqualTo(amount, 0, 1F, nameof(amount)); + amount = 1F - amount; + + ColorMatrix m = default; + m.M11 = .299F + (.701F * amount); + m.M21 = .587F - (.587F * amount); + m.M31 = 1F - (m.M11 + m.M21); + + m.M12 = .299F - (.299F * amount); + m.M22 = .587F + (.2848F * amount); + m.M32 = 1F - (m.M12 + m.M22); + + m.M13 = .299F - (.299F * amount); + m.M23 = .587F - (.587F * amount); + m.M33 = 1F - (m.M13 + m.M23); + m.M44 = 1F; + + return m; + } + + /// + /// Create a grayscale filter matrix using the given amount using the formula as specified by ITU-R Recommendation BT.709. + /// + /// + /// The proportion of the conversion. Must be between 0 and 1. + /// The + public static ColorMatrix CreateGrayscaleBt709Filter(float amount) + { + Guard.MustBeBetweenOrEqualTo(amount, 0, 1F, nameof(amount)); + amount = 1F - amount; + + // https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc + ColorMatrix m = default; + m.M11 = .2126F + (.7874F * amount); + m.M21 = .7152F - (.7152F * amount); + m.M31 = 1F - (m.M11 + m.M21); + + m.M12 = .2126F - (.2126F * amount); + m.M22 = .7152F + (.2848F * amount); + m.M32 = 1F - (m.M12 + m.M22); + + m.M13 = .2126F - (.2126F * amount); + m.M23 = .7152F - (.7152F * amount); + m.M33 = 1F - (m.M13 + m.M23); + m.M44 = 1F; + + return m; + } + + /// + /// Create a hue filter matrix using the given angle in degrees. + /// + /// The angle of rotation in degrees. + /// The + public static ColorMatrix CreateHueFilter(float degrees) + { + // Wrap the angle round at 360. + degrees %= 360; + + // Make sure it's not negative. + while (degrees < 0) + { + degrees += 360; + } + + float radian = GeometryUtilities.DegreeToRadian(degrees); + float cosRadian = MathF.Cos(radian); + float sinRadian = MathF.Sin(radian); + + // The matrix is set up to preserve the luminance of the image. + // See http://graficaobscura.com/matrix/index.html + // Number are taken from https://msdn.microsoft.com/en-us/library/jj192162(v=vs.85).aspx + return new ColorMatrix + { + M11 = .213F + (cosRadian * .787F) - (sinRadian * .213F), + M21 = .715F - (cosRadian * .715F) - (sinRadian * .715F), + M31 = .072F - (cosRadian * .072F) + (sinRadian * .928F), + + M12 = .213F - (cosRadian * .213F) + (sinRadian * .143F), + M22 = .715F + (cosRadian * .285F) + (sinRadian * .140F), + M32 = .072F - (cosRadian * .072F) - (sinRadian * .283F), + + M13 = .213F - (cosRadian * .213F) - (sinRadian * .787F), + M23 = .715F - (cosRadian * .715F) + (sinRadian * .715F), + M33 = .072F + (cosRadian * .928F) + (sinRadian * .072F), + M44 = 1F + }; + } + + /// + /// Create an invert filter matrix using the given amount. + /// + /// The proportion of the conversion. Must be between 0 and 1. + /// The + public static ColorMatrix CreateInvertFilter(float amount) + { + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + + // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc + float invert = 1F - (2F * amount); + + return new ColorMatrix + { + M11 = invert, + M22 = invert, + M33 = invert, + M44 = 1F, + M51 = amount, + M52 = amount, + M53 = amount, + }; + } + + /// + /// Create an opacity filter matrix using the given amount. + /// + /// The proportion of the conversion. Must be between 0 and 1. + /// The + public static ColorMatrix CreateOpacityFilter(float amount) + { + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + + // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc + return new ColorMatrix + { + M11 = 1F, + M22 = 1F, + M33 = 1F, + M44 = amount + }; + } + + /// + /// Create a saturation filter matrix using the given amount. + /// + /// + /// A value of 0 is completely un-saturated. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of amount over 1 are allowed, providing super-saturated results + /// + /// The proportion of the conversion. Must be greater than or equal to 0. + /// The + public static ColorMatrix CreateSaturateFilter(float amount) + { + Guard.MustBeGreaterThanOrEqualTo(amount, 0, nameof(amount)); + + // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc + ColorMatrix m = default; + m.M11 = .213F + (.787F * amount); + m.M21 = .715F - (.715F * amount); + m.M31 = 1F - (m.M11 + m.M21); + + m.M12 = .213F - (.213F * amount); + m.M22 = .715F + (.285F * amount); + m.M32 = 1F - (m.M12 + m.M22); + + m.M13 = .213F - (.213F * amount); + m.M23 = .715F - (.715F * amount); + m.M33 = 1F - (m.M13 + m.M23); + m.M44 = 1F; + + return m; + } + + /// + /// Create a lightness filter matrix using the given amount. + /// + /// + /// A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing lighter results. + /// + /// The proportion of the conversion. Must be greater than or equal to 0. + /// The + public static ColorMatrix CreateLightnessFilter(float amount) + { + Guard.MustBeGreaterThanOrEqualTo(amount, 0, nameof(amount)); + amount--; + + return new ColorMatrix + { + M11 = 1F, + M22 = 1F, + M33 = 1F, + M44 = 1F, + M51 = amount, + M52 = amount, + M53 = amount + }; + } + + /// + /// Create a sepia filter matrix using the given amount. + /// The formula used matches the svg specification. + /// + /// The proportion of the conversion. Must be between 0 and 1. + /// The + public static ColorMatrix CreateSepiaFilter(float amount) + { + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + amount = 1F - amount; + + // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc + return new ColorMatrix + { + M11 = .393F + (.607F * amount), + M21 = .769F - (.769F * amount), + M31 = .189F - (.189F * amount), + + M12 = .349F - (.349F * amount), + M22 = .686F + (.314F * amount), + M32 = .168F - (.168F * amount), + + M13 = .272F - (.272F * amount), + M23 = .534F - (.534F * amount), + M33 = .131F + (.869F * amount), + M44 = 1F + }; + } + } +} diff --git a/ImageSharp/Processing/KnownQuantizers.cs b/ImageSharp/Processing/KnownQuantizers.cs new file mode 100644 index 0000000..70f3ab6 --- /dev/null +++ b/ImageSharp/Processing/KnownQuantizers.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Contains reusable static instances of known quantizing algorithms. + /// + public static class KnownQuantizers + { + /// + /// Gets the adaptive hexadecatree quantizer. Fast with good quality. + /// + public static IQuantizer Hexadecatree { get; } = new HexadecatreeQuantizer(); + + /// + /// Gets the Xiaolin Wu's Color Quantizer which generates high quality output. + /// + public static IQuantizer Wu { get; } = new WuQuantizer(); + + /// + /// Gets the palette based quantizer consisting of web safe colors as defined in the CSS Color Module Level 4. + /// + public static IQuantizer WebSafe { get; } = new WebSafePaletteQuantizer(); + + /// + /// Gets the palette based quantizer consisting of colors as defined in the original second edition of Werner’s Nomenclature of Colours 1821. + /// The hex codes were collected and defined by Nicholas Rougeux + /// + public static IQuantizer Werner { get; } = new WernerPaletteQuantizer(); + } +} diff --git a/ImageSharp/Processing/KnownResamplers.cs b/ImageSharp/Processing/KnownResamplers.cs new file mode 100644 index 0000000..8afb456 --- /dev/null +++ b/ImageSharp/Processing/KnownResamplers.cs @@ -0,0 +1,97 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Processing { + /// + /// Contains reusable static instances of known resampling algorithms + /// + public static class KnownResamplers + { + /// + /// Gets the Bicubic sampler that implements the bicubic kernel algorithm W(x) + /// + public static IResampler Bicubic { get; } = default(BicubicResampler); + + /// + /// Gets the Box sampler that implements the box algorithm. Similar to nearest neighbor when upscaling. + /// When downscaling the pixels will average, merging pixels together. + /// + public static IResampler Box { get; } = default(BoxResampler); + + /// + /// Gets the Catmull-Rom sampler, a well known standard Cubic Filter often used as a interpolation function + /// + public static IResampler CatmullRom { get; } = CubicResampler.CatmullRom; + + /// + /// Gets the Hermite sampler. A type of smoothed triangular interpolation filter that rounds off strong edges while + /// preserving flat 'color levels' in the original image. + /// + public static IResampler Hermite { get; } = CubicResampler.Hermite; + + /// + /// Gets the Lanczos kernel sampler that implements smooth interpolation with a radius of 2 pixels. + /// This algorithm provides sharpened results when compared to others when downsampling. + /// + public static IResampler Lanczos2 { get; } = LanczosResampler.Lanczos2; + + /// + /// Gets the Lanczos kernel sampler that implements smooth interpolation with a radius of 3 pixels + /// This algorithm provides sharpened results when compared to others when downsampling. + /// + public static IResampler Lanczos3 { get; } = LanczosResampler.Lanczos3; + + /// + /// Gets the Lanczos kernel sampler that implements smooth interpolation with a radius of 5 pixels + /// This algorithm provides sharpened results when compared to others when downsampling. + /// + public static IResampler Lanczos5 { get; } = LanczosResampler.Lanczos5; + + /// + /// Gets the Lanczos kernel sampler that implements smooth interpolation with a radius of 8 pixels + /// This algorithm provides sharpened results when compared to others when downsampling. + /// + public static IResampler Lanczos8 { get; } = LanczosResampler.Lanczos8; + + /// + /// Gets the Mitchell-Netravali sampler. This seperable cubic algorithm yields a very good equilibrium between + /// detail preservation (sharpness) and smoothness. + /// + public static IResampler MitchellNetravali { get; } = CubicResampler.MitchellNetravali; + + /// + /// Gets the Nearest-Neighbour sampler that implements the nearest neighbor algorithm. This uses a very fast, unscaled filter + /// which will select the closest pixel to the new pixels position. + /// + public static IResampler NearestNeighbor { get; } = default(NearestNeighborResampler); + + /// + /// Gets the Robidoux sampler. This algorithm developed by Nicolas Robidoux providing a very good equilibrium between + /// detail preservation (sharpness) and smoothness comparable to . + /// + public static IResampler Robidoux { get; } = CubicResampler.Robidoux; + + /// + /// Gets the Robidoux Sharp sampler. A sharpened form of the sampler + /// + public static IResampler RobidouxSharp { get; } = CubicResampler.RobidouxSharp; + + /// + /// Gets the Spline sampler. A separable cubic algorithm similar to but yielding smoother results. + /// + public static IResampler Spline { get; } = CubicResampler.Spline; + + /// + /// Gets the Triangle sampler, otherwise known as Bilinear. This interpolation algorithm can be used where perfect image transformation + /// with pixel matching is impossible, so that one can calculate and assign appropriate intensity values to pixels + /// + public static IResampler Triangle { get; } = default(TriangleResampler); + + /// + /// Gets the Welch sampler. A high speed algorithm that delivers very sharpened results. + /// + public static IResampler Welch { get; } = default(WelchResampler); + } +} diff --git a/ImageSharp/Processing/PixelRowOperation.cs b/ImageSharp/Processing/PixelRowOperation.cs new file mode 100644 index 0000000..d78e393 --- /dev/null +++ b/ImageSharp/Processing/PixelRowOperation.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Processing { + /// + /// A representing a user defined processing delegate to use to modify image rows. + /// + /// The target row of pixels to process. + /// The , , , and fields map the RGBA channels respectively. + public delegate void PixelRowOperation(Span span); + + /// + /// A representing a user defined processing delegate to use to modify image rows. + /// + /// + /// The type of the parameter of the method that this delegate encapsulates. + /// This type parameter is contravariant.That is, you can use either the type you specified or any type that is less derived. + /// + /// The target row of pixels to process. + /// The parameter of the method that this delegate encapsulates. + /// The , , , and fields map the RGBA channels respectively. + public delegate void PixelRowOperation(Span span, T value); +} diff --git a/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs b/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs new file mode 100644 index 0000000..cba1ca5 --- /dev/null +++ b/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Binarization { + /// + /// Performs Bradley Adaptive Threshold filter against an image. + /// + /// + /// Implements "Adaptive Thresholding Using the Integral Image", + /// see paper: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.420.7883&rep=rep1&type=pdf + /// + public class AdaptiveThresholdProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + public AdaptiveThresholdProcessor() + : this(Color.White, Color.Black, 0.85f) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Threshold limit. + public AdaptiveThresholdProcessor(float thresholdLimit) + : this(Color.White, Color.Black, thresholdLimit) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Color for upper threshold. + /// Color for lower threshold. + public AdaptiveThresholdProcessor(Color upper, Color lower) + : this(upper, lower, 0.85f) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Color for upper threshold. + /// Color for lower threshold. + /// Threshold limit. + public AdaptiveThresholdProcessor(Color upper, Color lower, float thresholdLimit) + { + this.Upper = upper; + this.Lower = lower; + this.ThresholdLimit = thresholdLimit; + } + + /// + /// Gets or sets upper color limit for thresholding. + /// + public Color Upper { get; set; } + + /// + /// Gets or sets lower color limit for threshold. + /// + public Color Lower { get; set; } + + /// + /// Gets or sets the value for threshold limit. + /// + public float ThresholdLimit { get; set; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new AdaptiveThresholdProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs new file mode 100644 index 0000000..c30e32b --- /dev/null +++ b/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs @@ -0,0 +1,129 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Binarization { + /// + /// Performs Bradley Adaptive Threshold filter against an image. + /// + /// The pixel format. + internal class AdaptiveThresholdProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly AdaptiveThresholdProcessor definition; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public AdaptiveThresholdProcessor(Configuration configuration, AdaptiveThresholdProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + => this.definition = definition; + + /// + protected override void OnFrameApply(ImageFrame source) + { + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + + Configuration configuration = this.Configuration; + TPixel upper = this.definition.Upper.ToPixel(); + TPixel lower = this.definition.Lower.ToPixel(); + float thresholdLimit = this.definition.ThresholdLimit; + + // ClusterSize defines the size of cluster to used to check for average. + // Tweaked to support up to 4k wide pixels and not more. 4096 / 16 is 256 thus the '-1' + byte clusterSize = (byte)Math.Clamp(interest.Width / 16F, 0, 255); + + using Buffer2D intImage = source.CalculateIntegralImage(interest); + RowOperation operation = new(configuration, interest, source.PixelBuffer, intImage, upper, lower, thresholdLimit, clusterSize); + ParallelRowIterator.IterateRows( + configuration, + interest, + in operation); + } + + private readonly struct RowOperation : IRowOperation + { + private readonly Configuration configuration; + private readonly Rectangle bounds; + private readonly Buffer2D source; + private readonly Buffer2D intImage; + private readonly TPixel upper; + private readonly TPixel lower; + private readonly float thresholdLimit; + private readonly int startX; + private readonly int startY; + private readonly byte clusterSize; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperation( + Configuration configuration, + Rectangle bounds, + Buffer2D source, + Buffer2D intImage, + TPixel upper, + TPixel lower, + float thresholdLimit, + byte clusterSize) + { + this.configuration = configuration; + this.bounds = bounds; + this.startX = bounds.X; + this.startY = bounds.Y; + this.source = source; + this.intImage = intImage; + this.upper = upper; + this.lower = lower; + this.thresholdLimit = thresholdLimit; + this.clusterSize = clusterSize; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) + => bounds.Width; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y, Span span) + { + Span rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length); + PixelOperations.Instance.ToL8(this.configuration, rowSpan, span); + + int startY = this.startY; + int maxX = this.bounds.Width - 1; + int maxY = this.bounds.Height - 1; + int clusterSize = this.clusterSize; + float thresholdLimit = this.thresholdLimit; + Buffer2D image = this.intImage; + for (int x = 0; x < rowSpan.Length; x++) + { + int x1 = Math.Clamp(x - clusterSize + 1, 0, maxX); + int x2 = Math.Min(x + clusterSize + 1, maxX); + int y1 = Math.Clamp(y - startY - clusterSize + 1, 0, maxY); + int y2 = Math.Min(y - startY + clusterSize + 1, maxY); + + uint count = (uint)((x2 - x1) * (y2 - y1)); + ulong sum = Math.Min(image[x2, y2] - image[x1, y2] - image[x2, y1] + image[x1, y1], ulong.MaxValue); + + if (span[x].PackedValue * count <= sum * thresholdLimit) + { + rowSpan[x] = this.lower; + } + else + { + rowSpan[x] = this.upper; + } + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs b/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs new file mode 100644 index 0000000..f7303b4 --- /dev/null +++ b/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Binarization { + /// + /// Performs simple binary threshold filtering against an image. + /// + public class BinaryThresholdProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The threshold to split the image. Must be between 0 and 1. + /// The color component to be compared to threshold. + public BinaryThresholdProcessor(float threshold, BinaryThresholdMode mode) + : this(threshold, Color.White, Color.Black, mode) + { + } + + /// + /// Initializes a new instance of the class with + /// Luminance as color component to be compared to threshold. + /// + /// The threshold to split the image. Must be between 0 and 1. + public BinaryThresholdProcessor(float threshold) + : this(threshold, Color.White, Color.Black, BinaryThresholdMode.Luminance) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The threshold to split the image. Must be between 0 and 1. + /// The color to use for pixels that are above the threshold. + /// The color to use for pixels that are below the threshold. + /// The color component to be compared to threshold. + public BinaryThresholdProcessor(float threshold, Color upperColor, Color lowerColor, BinaryThresholdMode mode) + { + Guard.MustBeBetweenOrEqualTo(threshold, 0, 1, nameof(threshold)); + this.Threshold = threshold; + this.UpperColor = upperColor; + this.LowerColor = lowerColor; + this.Mode = mode; + } + + /// + /// Initializes a new instance of the class with + /// Luminance as color component to be compared to threshold. + /// + /// The threshold to split the image. Must be between 0 and 1. + /// The color to use for pixels that are above the threshold. + /// The color to use for pixels that are below the threshold. + public BinaryThresholdProcessor(float threshold, Color upperColor, Color lowerColor) + : this(threshold, upperColor, lowerColor, BinaryThresholdMode.Luminance) + { + } + + /// + /// Gets the threshold value. + /// + public float Threshold { get; } + + /// + /// Gets the color to use for pixels that are above the threshold. + /// + public Color UpperColor { get; } + + /// + /// Gets the color to use for pixels that fall below the threshold. + /// + public Color LowerColor { get; } + + /// + /// Gets the defining the value to be compared to threshold. + /// + public BinaryThresholdMode Mode { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new BinaryThresholdProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs new file mode 100644 index 0000000..0395892 --- /dev/null +++ b/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs @@ -0,0 +1,192 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Binarization { + /// + /// Performs simple binary threshold filtering against an image. + /// + /// The pixel format. + internal class BinaryThresholdProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly BinaryThresholdProcessor definition; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public BinaryThresholdProcessor(Configuration configuration, BinaryThresholdProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + => this.definition = definition; + + /// + protected override void OnFrameApply(ImageFrame source) + { + byte threshold = (byte)MathF.Round(this.definition.Threshold * 255F); + TPixel upper = this.definition.UpperColor.ToPixel(); + TPixel lower = this.definition.LowerColor.ToPixel(); + + Rectangle sourceRectangle = this.SourceRectangle; + Configuration configuration = this.Configuration; + + Rectangle interest = Rectangle.Intersect(sourceRectangle, source.Bounds); + RowOperation operation = new( + interest.X, + source.PixelBuffer, + upper, + lower, + threshold, + this.definition.Mode, + configuration); + + ParallelRowIterator.IterateRows( + configuration, + interest, + in operation); + } + + /// + /// A implementing the clone logic for . + /// + private readonly struct RowOperation : IRowOperation + { + private readonly Buffer2D source; + private readonly TPixel upper; + private readonly TPixel lower; + private readonly byte threshold; + private readonly BinaryThresholdMode mode; + private readonly int startX; + private readonly Configuration configuration; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperation( + int startX, + Buffer2D source, + TPixel upper, + TPixel lower, + byte threshold, + BinaryThresholdMode mode, + Configuration configuration) + { + this.startX = startX; + this.source = source; + this.upper = upper; + this.lower = lower; + this.threshold = threshold; + this.mode = mode; + this.configuration = configuration; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) + => bounds.Width; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke(int y, Span span) + { + TPixel upper = this.upper; + TPixel lower = this.lower; + + Span rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length); + PixelOperations.Instance.ToRgb24(this.configuration, rowSpan, span); + + switch (this.mode) + { + case BinaryThresholdMode.Luminance: + { + byte threshold = this.threshold; + for (int x = 0; x < rowSpan.Length; x++) + { + Rgb24 rgb = span[x]; + byte luminance = ColorNumerics.Get8BitBT709Luminance(rgb.R, rgb.G, rgb.B); + ref TPixel color = ref rowSpan[x]; + color = luminance >= threshold ? upper : lower; + } + + break; + } + + case BinaryThresholdMode.Saturation: + { + float threshold = this.threshold / 255F; + for (int x = 0; x < rowSpan.Length; x++) + { + float saturation = GetSaturation(span[x]); + ref TPixel color = ref rowSpan[x]; + color = saturation >= threshold ? upper : lower; + } + + break; + } + + case BinaryThresholdMode.MaxChroma: + { + float threshold = this.threshold * 0.5F; // /2 + for (int x = 0; x < rowSpan.Length; x++) + { + float chroma = GetMaxChroma(span[x]); + ref TPixel color = ref rowSpan[x]; + color = chroma >= threshold ? upper : lower; + } + + break; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float GetSaturation(Rgb24 rgb) + { + // Slimmed down RGB => HSL formula. See HslAndRgbConverter. + const float inv255 = 1 / 255F; + float r = rgb.R * inv255; + float g = rgb.G * inv255; + float b = rgb.B * inv255; + + float max = MathF.Max(r, MathF.Max(g, b)); + float min = MathF.Min(r, MathF.Min(g, b)); + float chroma = max - min; + + if (MathF.Abs(chroma) < Constants.Epsilon) + { + return 0F; + } + + float l = (max + min) * 0.5F; // /2 + + if (l <= .5F) + { + return chroma / (max + min); + } + + return chroma / (2F - max - min); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float GetMaxChroma(Rgb24 rgb) + { + // Slimmed down RGB => YCbCr formula. See YCbCrAndRgbConverter. + float r = rgb.R; + float g = rgb.G; + float b = rgb.B; + const float achromatic = 127.5F; + + float cb = 128F + ((-0.168736F * r) - (0.331264F * g) + (0.5F * b)); + float cr = 128F + ((0.5F * r) - (0.418688F * g) - (0.081312F * b)); + + return MathF.Max(MathF.Abs(cb - achromatic), MathF.Abs(cr - achromatic)); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/CloningImageProcessor.cs b/ImageSharp/Processing/Processors/CloningImageProcessor.cs new file mode 100644 index 0000000..d3a8899 --- /dev/null +++ b/ImageSharp/Processing/Processors/CloningImageProcessor.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors { + /// + /// The base class for all cloning image processors. + /// + public abstract class CloningImageProcessor : ICloningImageProcessor + { + /// + public abstract ICloningImageProcessor CreatePixelSpecificCloningProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel; + + /// + IImageProcessor IImageProcessor.CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + => this.CreatePixelSpecificCloningProcessor(configuration, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs new file mode 100644 index 0000000..f18fe93 --- /dev/null +++ b/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs @@ -0,0 +1,181 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors { + /// + /// The base class for all pixel specific cloning image processors. + /// Allows the application of processing algorithms to the image. + /// The image is cloned before operating upon and the buffers swapped upon completion. + /// + /// The pixel format. + public abstract class CloningImageProcessor : ICloningImageProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + protected CloningImageProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + { + this.Configuration = configuration; + this.Source = source; + this.SourceRectangle = sourceRectangle; + } + + /// + /// Gets The source for the current processor instance. + /// + protected Image Source { get; } + + /// + /// Gets The source area to process for the current processor instance. + /// + protected Rectangle SourceRectangle { get; } + + /// + /// Gets the instance to use when performing operations. + /// + protected Configuration Configuration { get; } + + /// + Image ICloningImageProcessor.CloneAndExecute() + { + Image clone = this.CreateTarget(); + this.CheckFrameCount(this.Source, clone); + + this.BeforeImageApply(clone); + + for (int i = 0; i < this.Source.Frames.Count; i++) + { + ImageFrame sourceFrame = this.Source.Frames[i]; + ImageFrame clonedFrame = clone.Frames[i]; + + this.BeforeFrameApply(sourceFrame, clonedFrame); + this.OnFrameApply(sourceFrame, clonedFrame); + this.AfterFrameApply(sourceFrame, clonedFrame); + } + + this.AfterImageApply(clone); + + return clone; + } + + /// + void IImageProcessor.Execute() + { + // Create an interim clone of the source image to operate on. + // Doing this allows for the application of transforms that will alter + // the dimensions of the image. + Image? clone = default; + try + { + clone = ((ICloningImageProcessor)this).CloneAndExecute(); + + // We now need to move the pixel data/size data and any metadata from the clone to the source. + this.CheckFrameCount(this.Source, clone); + this.Source.SwapOrCopyPixelsBuffersFrom(clone); + this.Source.CopyMetadataFrom(clone); + } + finally + { + // Dispose of the clone now that we have swapped the pixel/size data. + clone?.Dispose(); + } + } + + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Gets the size of the destination image. + /// + /// The . + protected abstract Size GetDestinationSize(); + + /// + /// This method is called before the process is applied to prepare the processor. + /// + /// The cloned/destination image. Cannot be null. + protected virtual void BeforeImageApply(Image destination) + { + } + + /// + /// This method is called before the process is applied to prepare the processor. + /// + /// The source image. Cannot be null. + /// The cloned/destination image. Cannot be null. + protected virtual void BeforeFrameApply(ImageFrame source, ImageFrame destination) + { + } + + /// + /// Applies the process to the specified portion of the specified at the specified location + /// and with the specified size. + /// + /// The source image. Cannot be null. + /// The cloned/destination image. Cannot be null. + protected abstract void OnFrameApply(ImageFrame source, ImageFrame destination); + + /// + /// This method is called after the process is applied to prepare the processor. + /// + /// The source image. Cannot be null. + /// The cloned/destination image. Cannot be null. + protected virtual void AfterFrameApply(ImageFrame source, ImageFrame destination) + => destination.Metadata.AfterFrameApply(source, destination, Matrix4x4.Identity); + + /// + /// This method is called after the process is applied to prepare the processor. + /// + /// The cloned/destination image. Cannot be null. + protected virtual void AfterImageApply(Image destination) + => destination.Metadata.AfterImageApply(destination, Matrix4x4.Identity); + + /// + /// Disposes the object and frees resources for the Garbage Collector. + /// + /// Whether to dispose managed and unmanaged objects. + protected virtual void Dispose(bool disposing) + { + } + + private Image CreateTarget() + { + Image source = this.Source; + Size destinationSize = this.GetDestinationSize(); + + // We will always be creating the clone even for mutate because we may need to resize the canvas. + ImageFrame[] destinationFrames = new ImageFrame[source.Frames.Count]; + for (int i = 0; i < destinationFrames.Length; i++) + { + destinationFrames[i] = new ImageFrame( + this.Configuration, + destinationSize.Width, + destinationSize.Height, + source.Frames[i].Metadata.DeepClone()); + } + + // Use the overload to prevent an extra frame being added. + return new Image(this.Configuration, source.Metadata.DeepClone(), destinationFrames); + } + + private void CheckFrameCount(Image a, Image b) + { + if (a.Frames.Count != b.Frames.Count) + { + throw new ImageProcessingException($"An error occurred when processing the image using {this.GetType().Name}. The processor changed the number of frames."); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor.cs b/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor.cs new file mode 100644 index 0000000..e2e5402 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor.cs @@ -0,0 +1,162 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Applies bokeh blur processing to the image. + /// + public sealed class BokehBlurProcessor : IImageProcessor + { + /// + /// The default radius used by the parameterless constructor. + /// + public const int DefaultRadius = 32; + + /// + /// The default component count used by the parameterless constructor. + /// + public const int DefaultComponents = 2; + + /// + /// The default gamma used by the parameterless constructor. + /// + public const float DefaultGamma = 3F; + + /// + /// Initializes a new instance of the class. + /// + public BokehBlurProcessor() + { + this.Radius = DefaultRadius; + this.Components = DefaultComponents; + this.Gamma = DefaultGamma; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// + /// + /// The number of components to use to approximate the original 2D bokeh blur convolution kernel. + /// + /// + /// The gamma highlight factor to use to further process the image. + /// + public BokehBlurProcessor(int radius, int components, float gamma) + { + Guard.MustBeGreaterThan(radius, 0, nameof(radius)); + Guard.MustBeBetweenOrEqualTo(components, 1, 6, nameof(components)); + Guard.MustBeGreaterThanOrEqualTo(gamma, 1, nameof(gamma)); + + this.Radius = radius; + this.Components = components; + this.Gamma = gamma; + } + + /// + /// Gets the radius. + /// + public int Radius { get; } + + /// + /// Gets the number of components. + /// + public int Components { get; } + + /// + /// Gets the gamma highlight factor to use when applying the effect. + /// + public float Gamma { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new BokehBlurProcessor(configuration, this, source, sourceRectangle); + + /// + /// A implementing the horizontal convolution logic for . + /// + /// + /// This type is located in the non-generic class and not in , where + /// it is actually used, because it does not use any generic parameters internally. Defining in a non-generic class means that there will only + /// ever be a single instantiation of this type for the JIT/AOT compilers to process, instead of having duplicate versions for each pixel type. + /// + internal readonly struct SecondPassConvolutionRowOperation : IRowOperation + { + private readonly Rectangle bounds; + private readonly Buffer2D targetValues; + private readonly Buffer2D sourceValues; + private readonly KernelSamplingMap map; + private readonly Complex64[] kernel; + private readonly float z; + private readonly float w; + + [MethodImpl(InliningOptions.ShortMethod)] + public SecondPassConvolutionRowOperation( + Rectangle bounds, + Buffer2D targetValues, + Buffer2D sourceValues, + KernelSamplingMap map, + Complex64[] kernel, + float z, + float w) + { + this.bounds = bounds; + this.targetValues = targetValues; + this.sourceValues = sourceValues; + this.map = map; + this.kernel = kernel; + this.z = z; + this.w = w; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y) + { + int boundsX = this.bounds.X; + int boundsWidth = this.bounds.Width; + int kernelSize = this.kernel.Length; + + ref int sampleRowBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(this.map.GetRowOffsetSpan()), (uint)((y - this.bounds.Y) * kernelSize)); + + // The target buffer is zeroed initially and then it accumulates the results + // of each partial convolution, so we don't have to clear it here as well + ref Vector4 targetBase = ref this.targetValues.GetElementUnsafe(boundsX, y); + ref Complex64 kernelStart = ref MemoryMarshal.GetArrayDataReference(this.kernel); + ref Complex64 kernelEnd = ref Unsafe.Add(ref kernelStart, (uint)kernelSize); + + while (Unsafe.IsAddressLessThan(ref kernelStart, ref kernelEnd)) + { + // Get the precalculated source sample row for this kernel row and copy to our buffer + ref ComplexVector4 sourceBase = ref this.sourceValues.GetElementUnsafe(0, sampleRowBase); + ref ComplexVector4 sourceEnd = ref Unsafe.Add(ref sourceBase, (uint)boundsWidth); + ref Vector4 targetStart = ref targetBase; + Complex64 factor = kernelStart; + + while (Unsafe.IsAddressLessThan(ref sourceBase, ref sourceEnd)) + { + ComplexVector4 partial = factor * sourceBase; + + targetStart += partial.WeightedSum(this.z, this.w); + + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + targetStart = ref Unsafe.Add(ref targetStart, 1); + } + + kernelStart = ref Unsafe.Add(ref kernelStart, 1); + sampleRowBase = ref Unsafe.Add(ref sampleRowBase, 1); + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs new file mode 100644 index 0000000..30a6ef5 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs @@ -0,0 +1,435 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.ColorProfiles.Companding; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Applies bokeh blur processing to the image. + /// + /// The pixel format. + /// This processor is based on the code from Mike Pound, see github.com/mikepound/convolve. + internal class BokehBlurProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + /// + /// The gamma highlight factor to use when applying the effect + /// + private readonly float gamma; + + /// + /// The size of each complex convolution kernel. + /// + private readonly int kernelSize; + + /// + /// The kernel parameters to use for the current instance (a: X, b: Y, A: Z, B: W) + /// + private readonly Vector4[] kernelParameters; + + /// + /// The kernel components for the current instance + /// + private readonly Complex64[][] kernels; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public BokehBlurProcessor(Configuration configuration, BokehBlurProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.gamma = definition.Gamma; + this.kernelSize = (definition.Radius * 2) + 1; + + // Get the bokeh blur data + BokehBlurKernelData data = BokehBlurKernelDataProvider.GetBokehBlurKernelData( + definition.Radius, + this.kernelSize, + definition.Components); + + this.kernelParameters = data.Parameters; + this.kernels = data.Kernels; + } + + /// + /// Gets the complex kernels to use to apply the blur for the current instance + /// + public IReadOnlyList Kernels => this.kernels; + + /// + /// Gets the kernel parameters used to compute the pixel values from each complex pixel + /// + public IReadOnlyList KernelParameters => this.kernelParameters; + + /// + protected override void OnFrameApply(ImageFrame source) + { + Rectangle sourceRectangle = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + + // Preliminary gamma highlight pass + if (this.gamma == 3F) + { + ApplyGamma3ExposureRowOperation gammaOperation = new(sourceRectangle, source.PixelBuffer, this.Configuration); + ParallelRowIterator.IterateRows( + this.Configuration, + sourceRectangle, + in gammaOperation); + } + else + { + ApplyGammaExposureRowOperation gammaOperation = new(sourceRectangle, source.PixelBuffer, this.Configuration, this.gamma); + ParallelRowIterator.IterateRows( + this.Configuration, + sourceRectangle, + in gammaOperation); + } + + // Create a 0-filled buffer to use to store the result of the component convolutions + using Buffer2D processingBuffer = this.Configuration.MemoryAllocator.Allocate2D(source.Size, AllocationOptions.Clean); + + // Perform the 1D convolutions on all the kernel components and accumulate the results + this.OnFrameApplyCore(source, sourceRectangle, this.Configuration, processingBuffer); + + // Apply the inverse gamma exposure pass, and write the final pixel data + if (this.gamma == 3F) + { + ApplyInverseGamma3ExposureRowOperation operation = new(sourceRectangle, source.PixelBuffer, processingBuffer, this.Configuration); + ParallelRowIterator.IterateRows( + this.Configuration, + sourceRectangle, + in operation); + } + else + { + ApplyInverseGammaExposureRowOperation operation = new(sourceRectangle, source.PixelBuffer, processingBuffer, this.Configuration, this.gamma); + ParallelRowIterator.IterateRows( + this.Configuration, + sourceRectangle, + in operation); + } + } + + /// + /// Computes and aggregates the convolution for each complex kernel component in the processor. + /// + /// The source image. Cannot be null. + /// The structure that specifies the portion of the image object to draw. + /// The configuration. + /// The buffer with the raw pixel data to use to aggregate the results of each convolution. + private void OnFrameApplyCore( + ImageFrame source, + Rectangle sourceRectangle, + Configuration configuration, + Buffer2D processingBuffer) + { + // Allocate the buffer with the intermediate convolution results + using Buffer2D firstPassBuffer = configuration.MemoryAllocator.Allocate2D(source.Size); + + // Unlike in the standard 2 pass convolution processor, we use a rectangle of 1x the interest width + // to speedup the actual convolution, by applying bulk pixel conversion and clamping calculation. + // The second half of the buffer will just target the temporary buffer of complex pixel values. + // This is needed because the bokeh blur operates as TPixel -> complex -> TPixel, so we cannot + // convert back to standard pixels after each separate 1D convolution pass. Like in the gaussian + // blur though, we preallocate and compute the kernel sampling maps before processing each complex + // component, to avoid recomputing the same sampling map once per convolution pass. Since we are + // doing two 1D convolutions with the same kernel, we can use a single kernel sampling map as if + // we were using a 2D kernel with each dimension being the same as the length of our kernel, and + // use the two sampling offset spans resulting from this same map. This saves some extra work. + using KernelSamplingMap mapXY = new(configuration.MemoryAllocator); + + mapXY.BuildSamplingOffsetMap(this.kernelSize, this.kernelSize, sourceRectangle); + + ref Complex64[] baseRef = ref MemoryMarshal.GetReference(this.kernels.AsSpan()); + ref Vector4 paramsRef = ref MemoryMarshal.GetReference(this.kernelParameters.AsSpan()); + + // Perform two 1D convolutions for each component in the current instance + for (int i = 0; i < this.kernels.Length; i++) + { + // Compute the resulting complex buffer for the current component + Complex64[] kernel = Unsafe.Add(ref baseRef, (uint)i); + Vector4 parameters = Unsafe.Add(ref paramsRef, (uint)i); + + // Horizontal convolution + FirstPassConvolutionRowOperation horizontalOperation = new( + sourceRectangle, + firstPassBuffer, + source.PixelBuffer, + mapXY, + kernel, + configuration); + + ParallelRowIterator.IterateRows( + configuration, + sourceRectangle, + in horizontalOperation); + + // Vertical 1D convolutions to accumulate the partial results on the target buffer + BokehBlurProcessor.SecondPassConvolutionRowOperation verticalOperation = new( + sourceRectangle, + processingBuffer, + firstPassBuffer, + mapXY, + kernel, + parameters.Z, + parameters.W); + + ParallelRowIterator.IterateRows( + configuration, + sourceRectangle, + in verticalOperation); + } + } + + /// + /// A implementing the vertical convolution logic for . + /// + private readonly struct FirstPassConvolutionRowOperation : IRowOperation + { + private readonly Rectangle bounds; + private readonly Buffer2D targetValues; + private readonly Buffer2D sourcePixels; + private readonly KernelSamplingMap map; + private readonly Complex64[] kernel; + private readonly Configuration configuration; + + [MethodImpl(InliningOptions.ShortMethod)] + public FirstPassConvolutionRowOperation( + Rectangle bounds, + Buffer2D targetValues, + Buffer2D sourcePixels, + KernelSamplingMap map, + Complex64[] kernel, + Configuration configuration) + { + this.bounds = bounds; + this.targetValues = targetValues; + this.sourcePixels = sourcePixels; + this.map = map; + this.kernel = kernel; + this.configuration = configuration; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) + => bounds.Width; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y, Span span) + { + int boundsX = this.bounds.X; + int boundsWidth = this.bounds.Width; + int kernelSize = this.kernel.Length; + + // Clear the target buffer for each row run + Span targetBuffer = this.targetValues.DangerousGetRowSpan(y); + targetBuffer.Clear(); + + // Execute the bulk pixel format conversion for the current row + Span sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, span); + + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(span); + ref ComplexVector4 targetStart = ref MemoryMarshal.GetReference(targetBuffer); + ref ComplexVector4 targetEnd = ref Unsafe.Add(ref targetStart, (uint)span.Length); + ref Complex64 kernelBase = ref MemoryMarshal.GetArrayDataReference(this.kernel); + ref Complex64 kernelEnd = ref Unsafe.Add(ref kernelBase, (uint)kernelSize); + ref int sampleColumnBase = ref MemoryMarshal.GetReference(this.map.GetColumnOffsetSpan()); + + while (Unsafe.IsAddressLessThan(ref targetStart, ref targetEnd)) + { + ref Complex64 kernelStart = ref kernelBase; + ref int sampleColumnStart = ref sampleColumnBase; + + while (Unsafe.IsAddressLessThan(ref kernelStart, ref kernelEnd)) + { + Vector4 sample = Unsafe.Add(ref sourceBase, (uint)(sampleColumnStart - boundsX)); + + targetStart.Sum(kernelStart * sample); + + kernelStart = ref Unsafe.Add(ref kernelStart, 1); + sampleColumnStart = ref Unsafe.Add(ref sampleColumnStart, 1); + } + + // Shift the base column sampling reference by one row at the end of each outer + // iteration so that the inner tight loop indexing can skip the multiplication + sampleColumnBase = ref Unsafe.Add(ref sampleColumnBase, (uint)kernelSize); + targetStart = ref Unsafe.Add(ref targetStart, 1); + } + } + } + + /// + /// A implementing the gamma exposure logic for . + /// + private readonly struct ApplyGammaExposureRowOperation : IRowOperation + { + private readonly Rectangle bounds; + private readonly Buffer2D targetPixels; + private readonly Configuration configuration; + private readonly float gamma; + + [MethodImpl(InliningOptions.ShortMethod)] + public ApplyGammaExposureRowOperation( + Rectangle bounds, + Buffer2D targetPixels, + Configuration configuration, + float gamma) + { + this.bounds = bounds; + this.targetPixels = targetPixels; + this.configuration = configuration; + this.gamma = gamma; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) + => bounds.Width; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y, Span span) + { + Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y)[this.bounds.X..]; + PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan[..span.Length], span, PixelConversionModifiers.Premultiply); + + // Input is premultiplied [0,1] so the LUT is safe here. + GammaCompanding.Expand(span[..this.bounds.Width], this.gamma); + + PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan); + } + } + + /// + /// A implementing the 3F gamma exposure logic for . + /// + private readonly struct ApplyGamma3ExposureRowOperation : IRowOperation + { + private readonly Rectangle bounds; + private readonly Buffer2D targetPixels; + private readonly Configuration configuration; + + [MethodImpl(InliningOptions.ShortMethod)] + public ApplyGamma3ExposureRowOperation( + Rectangle bounds, + Buffer2D targetPixels, + Configuration configuration) + { + this.bounds = bounds; + this.targetPixels = targetPixels; + this.configuration = configuration; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) + => bounds.Width; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y, Span span) + { + Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y)[this.bounds.X..]; + + PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan[..span.Length], span, PixelConversionModifiers.Premultiply); + + Numerics.CubePowOnXYZ(span); + + PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan); + } + } + + /// + /// A implementing the inverse gamma exposure logic for . + /// + private readonly struct ApplyInverseGammaExposureRowOperation : IRowOperation + { + private readonly Rectangle bounds; + private readonly Buffer2D targetPixels; + private readonly Buffer2D sourceValues; + private readonly Configuration configuration; + private readonly float gamma; + + [MethodImpl(InliningOptions.ShortMethod)] + public ApplyInverseGammaExposureRowOperation( + Rectangle bounds, + Buffer2D targetPixels, + Buffer2D sourceValues, + Configuration configuration, + float gamma) + { + this.bounds = bounds; + this.targetPixels = targetPixels; + this.sourceValues = sourceValues; + this.configuration = configuration; + this.gamma = gamma; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y) + { + Span targetPixelSpan = this.targetPixels.DangerousGetRowSpan(y)[this.bounds.X..]; + Span sourceRowSpan = this.sourceValues.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); + + Numerics.Clamp(MemoryMarshal.Cast(sourceRowSpan), 0, 1F); + GammaCompanding.Compress(sourceRowSpan, this.gamma); + + PixelOperations.Instance.FromVector4Destructive(this.configuration, sourceRowSpan, targetPixelSpan, PixelConversionModifiers.Premultiply); + } + } + + /// + /// A implementing the inverse 3F gamma exposure logic for . + /// + private readonly struct ApplyInverseGamma3ExposureRowOperation : IRowOperation + { + private readonly Rectangle bounds; + private readonly Buffer2D targetPixels; + private readonly Buffer2D sourceValues; + private readonly Configuration configuration; + + [MethodImpl(InliningOptions.ShortMethod)] + public ApplyInverseGamma3ExposureRowOperation( + Rectangle bounds, + Buffer2D targetPixels, + Buffer2D sourceValues, + Configuration configuration) + { + this.bounds = bounds; + this.targetPixels = targetPixels; + this.sourceValues = sourceValues; + this.configuration = configuration; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y) + { + Span sourceRowSpan = this.sourceValues.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); + + Numerics.Clamp(MemoryMarshal.Cast(sourceRowSpan), 0, 1F); + Numerics.CubeRootOnXYZ(sourceRowSpan); + + Span targetPixelSpan = this.targetPixels.DangerousGetRowSpan(y)[this.bounds.X..]; + + PixelOperations.Instance.FromVector4Destructive(this.configuration, sourceRowSpan, targetPixelSpan, PixelConversionModifiers.Premultiply); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs b/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs new file mode 100644 index 0000000..981a9df --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Wrapping mode for the border pixels in convolution processing. + /// + public enum BorderWrappingMode : byte + { + /// Repeat the border pixel value: aaaaaa|abcdefgh|hhhhhhh + Repeat = 0, + + /// Take values from the opposite edge: cdefgh|abcdefgh|abcdefg + Wrap = 1, + + /// Mirror the last few border values: fedcba|abcdefgh|hgfedcb + /// This Mode is similar to , but here the very border pixel is repeated. + Mirror = 2, + + /// Bounce off the border: fedcb|abcdefgh|gfedcb + /// This Mode is similar to , but here the very border pixel is not repeated. + Bounce = 3 + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor.cs b/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor.cs new file mode 100644 index 0000000..b276e0c --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Defines a box blur processor of a given radius. + /// + public sealed class BoxBlurProcessor : IImageProcessor + { + /// + /// The default radius used by the parameterless constructor. + /// + public const int DefaultRadius = 7; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public BoxBlurProcessor(int radius, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) + { + this.Radius = radius; + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// + public BoxBlurProcessor(int radius) + : this(radius, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat) + { + } + + /// + /// Initializes a new instance of the class. + /// + public BoxBlurProcessor() + : this(DefaultRadius) + { + } + + /// + /// Gets the Radius. + /// + public int Radius { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new BoxBlurProcessor(configuration, this, source, sourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor{TPixel}.cs new file mode 100644 index 0000000..00826c9 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor{TPixel}.cs @@ -0,0 +1,90 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Applies box blur processing to the image. + /// + /// The pixel format. + internal class BoxBlurProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public BoxBlurProcessor(Configuration configuration, BoxBlurProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + int kernelSize = (definition.Radius * 2) + 1; + this.Kernel = CreateBoxKernel(kernelSize); + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public BoxBlurProcessor( + Configuration configuration, + BoxBlurProcessor definition, + Image source, + Rectangle sourceRectangle, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + : base(configuration, source, sourceRectangle) + { + int kernelSize = (definition.Radius * 2) + 1; + this.Kernel = CreateBoxKernel(kernelSize); + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; + } + + /// + /// Gets the 1D convolution kernel. + /// + public float[] Kernel { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + + /// + protected override void OnFrameApply(ImageFrame source) + { + using Convolution2PassProcessor processor = new(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); + + processor.Apply(source); + } + + /// + /// Create a 1 dimensional Box kernel. + /// + /// The maximum size of the kernel in either direction. + /// The . + private static float[] CreateBoxKernel(int kernelSize) + { + float[] kernel = new float[kernelSize]; + + kernel.AsSpan().Fill(1F / kernelSize); + + return kernel; + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs new file mode 100644 index 0000000..9e84e2c --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs @@ -0,0 +1,97 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Defines a processor that uses two one-dimensional matrices to perform convolution against an image. + /// + /// The pixel format. + internal class Convolution2DProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The horizontal gradient operator. + /// The vertical gradient operator. + /// Whether the convolution filter is applied to alpha as well as the color channels. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public Convolution2DProcessor( + Configuration configuration, + in DenseMatrix kernelX, + in DenseMatrix kernelY, + bool preserveAlpha, + Image source, + Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + Guard.IsTrue(kernelX.Size.Equals(kernelY.Size), $"{nameof(kernelX)} {nameof(kernelY)}", "Kernel sizes must be the same."); + this.KernelX = kernelX; + this.KernelY = kernelY; + this.PreserveAlpha = preserveAlpha; + } + + /// + /// Gets the horizontal convolution kernel. + /// + public DenseMatrix KernelX { get; } + + /// + /// Gets the vertical convolution kernel. + /// + public DenseMatrix KernelY { get; } + + /// + /// Gets a value indicating whether the convolution filter is applied to alpha as well as the color channels. + /// + public bool PreserveAlpha { get; } + + /// + protected override void OnFrameApply(ImageFrame source) + { + MemoryAllocator allocator = this.Configuration.MemoryAllocator; + using Buffer2D targetPixels = allocator.Allocate2D(source.Width, source.Height); + + source.CopyTo(targetPixels); + + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + + using (KernelSamplingMap map = new(allocator)) + { + // Since the kernel sizes are identical we can use a single map. + map.BuildSamplingOffsetMap(this.KernelY, interest); + + Convolution2DRowOperation operation = new( + interest, + targetPixels, + source.PixelBuffer, + map, + this.KernelY, + this.KernelX, + this.Configuration, + this.PreserveAlpha); + + // Convolution is memory-bandwidth-bound with low arithmetic intensity. + // Parallelization degrades performance due to cache line contention from + // overlapping source row reads. See #3111. + using IMemoryOwner buffer = allocator.Allocate(operation.GetRequiredBufferLength(interest)); + Span span = buffer.Memory.Span; + + for (int y = interest.Top; y < interest.Bottom; y++) + { + operation.Invoke(y, span); + } + } + + Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Convolution2DRowOperation{TPixel}.cs b/ImageSharp/Processing/Processors/Convolution/Convolution2DRowOperation{TPixel}.cs new file mode 100644 index 0000000..6c84fbe --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Convolution2DRowOperation{TPixel}.cs @@ -0,0 +1,197 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// A implementing the logic for 2D convolution. + /// + internal readonly struct Convolution2DRowOperation : IRowOperation + where TPixel : unmanaged, IPixel + { + private readonly Rectangle bounds; + private readonly Buffer2D targetPixels; + private readonly Buffer2D sourcePixels; + private readonly KernelSamplingMap map; + private readonly DenseMatrix kernelMatrixY; + private readonly DenseMatrix kernelMatrixX; + private readonly Configuration configuration; + private readonly bool preserveAlpha; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Convolution2DRowOperation( + Rectangle bounds, + Buffer2D targetPixels, + Buffer2D sourcePixels, + KernelSamplingMap map, + DenseMatrix kernelMatrixY, + DenseMatrix kernelMatrixX, + Configuration configuration, + bool preserveAlpha) + { + this.bounds = bounds; + this.targetPixels = targetPixels; + this.sourcePixels = sourcePixels; + this.map = map; + this.kernelMatrixY = kernelMatrixY; + this.kernelMatrixX = kernelMatrixX; + this.configuration = configuration; + this.preserveAlpha = preserveAlpha; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) + => 3 * bounds.Width; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke(int y, Span span) + { + if (this.preserveAlpha) + { + this.Convolve3(y, span); + } + else + { + this.Convolve4(y, span); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Convolve3(int y, Span span) + { + // Span is 3x bounds. + int boundsX = this.bounds.X; + int boundsWidth = this.bounds.Width; + Span sourceBuffer = span[..boundsWidth]; + Span targetYBuffer = span.Slice(boundsWidth, boundsWidth); + Span targetXBuffer = span.Slice(boundsWidth * 2, boundsWidth); + + Convolution2DState state = new(in this.kernelMatrixY, in this.kernelMatrixX, this.map); + ref int sampleRowBase = ref state.GetSampleRow((uint)(y - this.bounds.Y)); + + // Clear the target buffers for each row run. + targetYBuffer.Clear(); + targetXBuffer.Clear(); + ref Vector4 targetBaseY = ref MemoryMarshal.GetReference(targetYBuffer); + ref Vector4 targetBaseX = ref MemoryMarshal.GetReference(targetXBuffer); + + ReadOnlyKernel kernelY = state.KernelY; + ReadOnlyKernel kernelX = state.KernelX; + Span sourceRow; + for (uint kY = 0; kY < kernelY.Rows; kY++) + { + // Get the precalculated source sample row for this kernel row and copy to our buffer. + int sampleY = Unsafe.Add(ref sampleRowBase, kY); + sourceRow = this.sourcePixels.DangerousGetRowSpan(sampleY).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); + + for (uint x = 0; x < (uint)sourceBuffer.Length; x++) + { + ref int sampleColumnBase = ref state.GetSampleColumn(x); + ref Vector4 targetY = ref Unsafe.Add(ref targetBaseY, x); + ref Vector4 targetX = ref Unsafe.Add(ref targetBaseX, x); + + for (uint kX = 0; kX < kernelY.Columns; kX++) + { + int sampleX = Unsafe.Add(ref sampleColumnBase, kX) - boundsX; + Vector4 sample = Unsafe.Add(ref sourceBase, (uint)sampleX); + targetY += kernelX[kY, kX] * sample; + targetX += kernelY[kY, kX] * sample; + } + } + } + + // Now we need to combine the values and copy the original alpha values + // from the source row. + sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + for (nuint x = 0; x < (uint)sourceRow.Length; x++) + { + ref Vector4 target = ref Unsafe.Add(ref targetBaseY, x); + Vector4 vectorY = target; + Vector4 vectorX = Unsafe.Add(ref targetBaseX, x); + + target = Vector4.SquareRoot((vectorX * vectorX) + (vectorY * vectorY)); + target.W = Unsafe.Add(ref MemoryMarshal.GetReference(sourceBuffer), x).W; + } + + Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetYBuffer, targetRowSpan); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Convolve4(int y, Span span) + { + // Span is 3x bounds. + int boundsX = this.bounds.X; + int boundsWidth = this.bounds.Width; + Span sourceBuffer = span[..boundsWidth]; + Span targetYBuffer = span.Slice(boundsWidth, boundsWidth); + Span targetXBuffer = span.Slice(boundsWidth * 2, boundsWidth); + + Convolution2DState state = new(in this.kernelMatrixY, in this.kernelMatrixX, this.map); + ref int sampleRowBase = ref state.GetSampleRow((uint)(y - this.bounds.Y)); + + // Clear the target buffers for each row run. + targetYBuffer.Clear(); + targetXBuffer.Clear(); + ref Vector4 targetBaseY = ref MemoryMarshal.GetReference(targetYBuffer); + ref Vector4 targetBaseX = ref MemoryMarshal.GetReference(targetXBuffer); + + ReadOnlyKernel kernelY = state.KernelY; + ReadOnlyKernel kernelX = state.KernelX; + for (uint kY = 0; kY < kernelY.Rows; kY++) + { + // Get the precalculated source sample row for this kernel row and copy to our buffer. + int sampleY = Unsafe.Add(ref sampleRowBase, kY); + Span sourceRow = this.sourcePixels.DangerousGetRowSpan(sampleY).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + Numerics.Premultiply(sourceBuffer); + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); + + for (uint x = 0; x < (uint)sourceBuffer.Length; x++) + { + ref int sampleColumnBase = ref state.GetSampleColumn(x); + ref Vector4 targetY = ref Unsafe.Add(ref targetBaseY, x); + ref Vector4 targetX = ref Unsafe.Add(ref targetBaseX, x); + + for (uint kX = 0; kX < kernelY.Columns; kX++) + { + int sampleX = Unsafe.Add(ref sampleColumnBase, kX) - boundsX; + Vector4 sample = Unsafe.Add(ref sourceBase, sampleX); + targetY += kernelX[kY, kX] * sample; + targetX += kernelY[kY, kX] * sample; + } + } + } + + // Now we need to combine the values + for (nuint x = 0; x < (uint)targetYBuffer.Length; x++) + { + ref Vector4 target = ref Unsafe.Add(ref targetBaseY, x); + Vector4 vectorY = target; + Vector4 vectorX = Unsafe.Add(ref targetBaseX, x); + + target = Vector4.SquareRoot((vectorX * vectorX) + (vectorY * vectorY)); + } + + Numerics.UnPremultiply(targetYBuffer); + + Span targetRow = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetYBuffer, targetRow); + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Convolution2DState.cs b/ImageSharp/Processing/Processors/Convolution/Convolution2DState.cs new file mode 100644 index 0000000..2af4b67 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Convolution2DState.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// A stack only struct used for reducing reference indirection during 2D convolution operations. + /// + internal readonly ref struct Convolution2DState + { + private readonly Span rowOffsetMap; + private readonly Span columnOffsetMap; + private readonly uint kernelHeight; + private readonly uint kernelWidth; + + public Convolution2DState( + in DenseMatrix kernelY, + in DenseMatrix kernelX, + KernelSamplingMap map) + { + // We check the kernels are the same size upstream. + this.KernelY = new ReadOnlyKernel(kernelY); + this.KernelX = new ReadOnlyKernel(kernelX); + this.kernelHeight = (uint)kernelY.Rows; + this.kernelWidth = (uint)kernelY.Columns; + this.rowOffsetMap = map.GetRowOffsetSpan(); + this.columnOffsetMap = map.GetColumnOffsetSpan(); + } + + public readonly ReadOnlyKernel KernelY + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + public readonly ReadOnlyKernel KernelX + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly ref int GetSampleRow(uint row) + => ref Unsafe.Add(ref MemoryMarshal.GetReference(this.rowOffsetMap), row * this.kernelHeight); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly ref int GetSampleColumn(uint column) + => ref Unsafe.Add(ref MemoryMarshal.GetReference(this.columnOffsetMap), column * this.kernelWidth); + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs new file mode 100644 index 0000000..5a775f8 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs @@ -0,0 +1,476 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Defines a processor that uses two one-dimensional matrices to perform two-pass convolution against an image. + /// + /// The pixel format. + internal class Convolution2PassProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The 1D convolution kernel. + /// Whether the convolution filter is applied to alpha as well as the color channels. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public Convolution2PassProcessor( + Configuration configuration, + float[] kernel, + bool preserveAlpha, + Image source, + Rectangle sourceRectangle, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + : this(configuration, kernel, kernel, preserveAlpha, source, sourceRectangle, borderWrapModeX, borderWrapModeY) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The 1D convolution kernel. X Direction + /// The 1D convolution kernel. Y Direction + /// Whether the convolution filter is applied to alpha as well as the color channels. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public Convolution2PassProcessor( + Configuration configuration, + float[] kernelX, + float[] kernelY, + bool preserveAlpha, + Image source, + Rectangle sourceRectangle, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + : base(configuration, source, sourceRectangle) + { + this.KernelX = kernelX; + this.KernelY = kernelY; + this.PreserveAlpha = preserveAlpha; + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; + } + + /// + /// Gets the convolution kernel. X direction. + /// + public float[] KernelX { get; } + + /// + /// Gets the convolution kernel. Y direction. + /// + public float[] KernelY { get; } + + /// + /// Gets a value indicating whether the convolution filter is applied to alpha as well as the color channels. + /// + public bool PreserveAlpha { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + + /// + protected override void OnFrameApply(ImageFrame source) + { + using Buffer2D firstPassPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Size); + + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + + // We can create a single sampling map with the size as if we were using the non separated 2D kernel + // the two 1D kernels represent, and reuse it across both convolution steps, like in the bokeh blur. + using KernelSamplingMap mapXY = new(this.Configuration.MemoryAllocator); + + mapXY.BuildSamplingOffsetMap(this.KernelX.Length, this.KernelX.Length, interest, this.BorderWrapModeX, this.BorderWrapModeY); + + // Horizontal convolution + HorizontalConvolutionRowOperation horizontalOperation = new( + interest, + firstPassPixels, + source.PixelBuffer, + mapXY, + this.KernelX, + this.Configuration, + this.PreserveAlpha); + + ParallelRowIterator.IterateRows( + this.Configuration, + interest, + in horizontalOperation); + + // Vertical convolution + VerticalConvolutionRowOperation verticalOperation = new( + interest, + source.PixelBuffer, + firstPassPixels, + mapXY, + this.KernelY, + this.Configuration, + this.PreserveAlpha); + + ParallelRowIterator.IterateRows( + this.Configuration, + interest, + in verticalOperation); + } + + /// + /// A implementing the logic for the horizontal 1D convolution. + /// + internal readonly struct HorizontalConvolutionRowOperation : IRowOperation + { + private readonly Rectangle bounds; + private readonly Buffer2D targetPixels; + private readonly Buffer2D sourcePixels; + private readonly KernelSamplingMap map; + private readonly float[] kernel; + private readonly Configuration configuration; + private readonly bool preserveAlpha; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HorizontalConvolutionRowOperation( + Rectangle bounds, + Buffer2D targetPixels, + Buffer2D sourcePixels, + KernelSamplingMap map, + float[] kernel, + Configuration configuration, + bool preserveAlpha) + { + this.bounds = bounds; + this.targetPixels = targetPixels; + this.sourcePixels = sourcePixels; + this.map = map; + this.kernel = kernel; + this.configuration = configuration; + this.preserveAlpha = preserveAlpha; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetRequiredBufferLength(Rectangle bounds) + => 2 * bounds.Width; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke(int y, Span span) + { + if (this.preserveAlpha) + { + this.Convolve3(y, span); + } + else + { + this.Convolve4(y, span); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Convolve3(int y, Span span) + { + // Span is 2x bounds. + int boundsX = this.bounds.X; + int boundsWidth = this.bounds.Width; + int kernelSize = this.kernel.Length; + + Span sourceBuffer = span[..this.bounds.Width]; + Span targetBuffer = span[this.bounds.Width..]; + + // Clear the target buffer for each row run. + targetBuffer.Clear(); + + // Get the precalculated source sample row for this kernel row and copy to our buffer. + Span sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); + ref Vector4 targetStart = ref MemoryMarshal.GetReference(targetBuffer); + ref Vector4 targetEnd = ref Unsafe.Add(ref targetStart, (uint)sourceBuffer.Length); + ref float kernelBase = ref MemoryMarshal.GetArrayDataReference(this.kernel); + ref float kernelEnd = ref Unsafe.Add(ref kernelBase, (uint)kernelSize); + ref int sampleColumnBase = ref MemoryMarshal.GetReference(this.map.GetColumnOffsetSpan()); + + while (Unsafe.IsAddressLessThan(ref targetStart, ref targetEnd)) + { + ref float kernelStart = ref kernelBase; + ref int sampleColumnStart = ref sampleColumnBase; + + while (Unsafe.IsAddressLessThan(ref kernelStart, ref kernelEnd)) + { + Vector4 sample = Unsafe.Add(ref sourceBase, (uint)(sampleColumnStart - boundsX)); + + targetStart += kernelStart * sample; + + kernelStart = ref Unsafe.Add(ref kernelStart, 1); + sampleColumnStart = ref Unsafe.Add(ref sampleColumnStart, 1); + } + + targetStart = ref Unsafe.Add(ref targetStart, 1); + sampleColumnBase = ref Unsafe.Add(ref sampleColumnBase, (uint)kernelSize); + } + + // Now we need to copy the original alpha values from the source row. + sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + targetStart = ref MemoryMarshal.GetReference(targetBuffer); + + while (Unsafe.IsAddressLessThan(ref targetStart, ref targetEnd)) + { + targetStart.W = sourceBase.W; + + targetStart = ref Unsafe.Add(ref targetStart, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + Span targetRow = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Convolve4(int y, Span span) + { + // Span is 2x bounds. + int boundsX = this.bounds.X; + int boundsWidth = this.bounds.Width; + int kernelSize = this.kernel.Length; + + Span sourceBuffer = span[..this.bounds.Width]; + Span targetBuffer = span[this.bounds.Width..]; + + // Clear the target buffer for each row run. + targetBuffer.Clear(); + + // Get the precalculated source sample row for this kernel row and copy to our buffer. + Span sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + Numerics.Premultiply(sourceBuffer); + + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); + ref Vector4 targetStart = ref MemoryMarshal.GetReference(targetBuffer); + ref Vector4 targetEnd = ref Unsafe.Add(ref targetStart, (uint)sourceBuffer.Length); + ref float kernelBase = ref MemoryMarshal.GetArrayDataReference(this.kernel); + ref float kernelEnd = ref Unsafe.Add(ref kernelBase, (uint)kernelSize); + ref int sampleColumnBase = ref MemoryMarshal.GetReference(this.map.GetColumnOffsetSpan()); + + while (Unsafe.IsAddressLessThan(ref targetStart, ref targetEnd)) + { + ref float kernelStart = ref kernelBase; + ref int sampleColumnStart = ref sampleColumnBase; + + while (Unsafe.IsAddressLessThan(ref kernelStart, ref kernelEnd)) + { + Vector4 sample = Unsafe.Add(ref sourceBase, (uint)(sampleColumnStart - boundsX)); + + targetStart += kernelStart * sample; + + kernelStart = ref Unsafe.Add(ref kernelStart, 1); + sampleColumnStart = ref Unsafe.Add(ref sampleColumnStart, 1); + } + + targetStart = ref Unsafe.Add(ref targetStart, 1); + sampleColumnBase = ref Unsafe.Add(ref sampleColumnBase, (uint)kernelSize); + } + + Numerics.UnPremultiply(targetBuffer); + + Span targetRow = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow); + } + } + + /// + /// A implementing the logic for the vertical 1D convolution. + /// + internal readonly struct VerticalConvolutionRowOperation : IRowOperation + { + private readonly Rectangle bounds; + private readonly Buffer2D targetPixels; + private readonly Buffer2D sourcePixels; + private readonly KernelSamplingMap map; + private readonly float[] kernel; + private readonly Configuration configuration; + private readonly bool preserveAlpha; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public VerticalConvolutionRowOperation( + Rectangle bounds, + Buffer2D targetPixels, + Buffer2D sourcePixels, + KernelSamplingMap map, + float[] kernel, + Configuration configuration, + bool preserveAlpha) + { + this.bounds = bounds; + this.targetPixels = targetPixels; + this.sourcePixels = sourcePixels; + this.map = map; + this.kernel = kernel; + this.configuration = configuration; + this.preserveAlpha = preserveAlpha; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetRequiredBufferLength(Rectangle bounds) + => 2 * bounds.Width; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke(int y, Span span) + { + if (this.preserveAlpha) + { + this.Convolve3(y, span); + } + else + { + this.Convolve4(y, span); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Convolve3(int y, Span span) + { + // Span is 2x bounds. + int boundsX = this.bounds.X; + int boundsWidth = this.bounds.Width; + int kernelSize = this.kernel.Length; + + Span sourceBuffer = span[..this.bounds.Width]; + Span targetBuffer = span[this.bounds.Width..]; + + ref int sampleRowBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(this.map.GetRowOffsetSpan()), (uint)((y - this.bounds.Y) * kernelSize)); + + // Clear the target buffer for each row run. + targetBuffer.Clear(); + + ref Vector4 targetBase = ref MemoryMarshal.GetReference(targetBuffer); + ref float kernelStart = ref MemoryMarshal.GetArrayDataReference(this.kernel); + ref float kernelEnd = ref Unsafe.Add(ref kernelStart, (uint)kernelSize); + + Span sourceRow; + while (Unsafe.IsAddressLessThan(ref kernelStart, ref kernelEnd)) + { + // Get the precalculated source sample row for this kernel row and copy to our buffer. + sourceRow = this.sourcePixels.DangerousGetRowSpan(sampleRowBase).Slice(boundsX, boundsWidth); + + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); + ref Vector4 sourceEnd = ref Unsafe.Add(ref sourceBase, (uint)sourceBuffer.Length); + ref Vector4 targetStart = ref targetBase; + float factor = kernelStart; + + while (Unsafe.IsAddressLessThan(ref sourceBase, ref sourceEnd)) + { + targetStart += factor * sourceBase; + + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + targetStart = ref Unsafe.Add(ref targetStart, 1); + } + + kernelStart = ref Unsafe.Add(ref kernelStart, 1); + sampleRowBase = ref Unsafe.Add(ref sampleRowBase, 1); + } + + // Now we need to copy the original alpha values from the source row. + sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + { + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); + ref Vector4 sourceEnd = ref Unsafe.Add(ref sourceBase, (uint)sourceBuffer.Length); + + while (Unsafe.IsAddressLessThan(ref sourceBase, ref sourceEnd)) + { + targetBase.W = sourceBase.W; + + targetBase = ref Unsafe.Add(ref targetBase, 1); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + } + + Span targetRow = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Convolve4(int y, Span span) + { + // Span is 2x bounds. + int boundsX = this.bounds.X; + int boundsWidth = this.bounds.Width; + int kernelSize = this.kernel.Length; + + Span sourceBuffer = span[..this.bounds.Width]; + Span targetBuffer = span[this.bounds.Width..]; + + ref int sampleRowBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(this.map.GetRowOffsetSpan()), (uint)((y - this.bounds.Y) * kernelSize)); + + // Clear the target buffer for each row run. + targetBuffer.Clear(); + + ref Vector4 targetBase = ref MemoryMarshal.GetReference(targetBuffer); + ref float kernelStart = ref MemoryMarshal.GetArrayDataReference(this.kernel); + ref float kernelEnd = ref Unsafe.Add(ref kernelStart, (uint)kernelSize); + + Span sourceRow; + while (Unsafe.IsAddressLessThan(ref kernelStart, ref kernelEnd)) + { + // Get the precalculated source sample row for this kernel row and copy to our buffer. + sourceRow = this.sourcePixels.DangerousGetRowSpan(sampleRowBase).Slice(boundsX, boundsWidth); + + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + Numerics.Premultiply(sourceBuffer); + + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); + ref Vector4 sourceEnd = ref Unsafe.Add(ref sourceBase, (uint)sourceBuffer.Length); + ref Vector4 targetStart = ref targetBase; + float factor = kernelStart; + + while (Unsafe.IsAddressLessThan(ref sourceBase, ref sourceEnd)) + { + targetStart += factor * sourceBase; + + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + targetStart = ref Unsafe.Add(ref targetStart, 1); + } + + kernelStart = ref Unsafe.Add(ref kernelStart, 1); + sampleRowBase = ref Unsafe.Add(ref sampleRowBase, 1); + } + + Numerics.UnPremultiply(targetBuffer); + + Span targetRow = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs b/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs new file mode 100644 index 0000000..479dc2f --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Defines a processor that uses a 2 dimensional matrix to perform convolution against an image. + /// + public class ConvolutionProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The 2d gradient operator. + /// Whether the convolution filter is applied to alpha as well as the color channels. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public ConvolutionProcessor( + in DenseMatrix kernelXY, + bool preserveAlpha, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + { + this.KernelXY = kernelXY; + this.PreserveAlpha = preserveAlpha; + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; + } + + /// + /// Gets the 2d convolution kernel. + /// + public DenseMatrix KernelXY { get; } + + /// + /// Gets a value indicating whether the convolution filter is applied to alpha as well as the color channels. + /// + public bool PreserveAlpha { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, + IPixel + { + if (this.KernelXY.TryGetLinearlySeparableComponents(out float[]? kernelX, out float[]? kernelY)) + { + return new Convolution2PassProcessor( + configuration, + kernelX, + kernelY, + this.PreserveAlpha, + source, + sourceRectangle, + this.BorderWrapModeX, + this.BorderWrapModeY); + } + + return new ConvolutionProcessor( + configuration, + this.KernelXY, + this.PreserveAlpha, + source, + sourceRectangle, + this.BorderWrapModeX, + this.BorderWrapModeY); + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessorHelpers.cs b/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessorHelpers.cs new file mode 100644 index 0000000..73de62c --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessorHelpers.cs @@ -0,0 +1,189 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + internal static class ConvolutionProcessorHelpers + { + /// + /// Kernel radius is calculated using the minimum viable value. + /// See . + /// + /// The weight of the blur. + internal static int GetDefaultGaussianRadius(float sigma) + => (int)MathF.Ceiling(sigma * 3); + + /// + /// Create a 1 dimensional Gaussian kernel using the Gaussian G(x) function. + /// + /// The convolution kernel. + /// The kernel size. + /// The weight of the blur. + internal static float[] CreateGaussianBlurKernel(int size, float weight) + { + float[] kernel = new float[size]; + + float sum = 0F; + float midpoint = (size - 1) / 2F; + + for (int i = 0; i < size; i++) + { + float x = i - midpoint; + float gx = Numerics.Gaussian(x, weight); + sum += gx; + kernel[i] = gx; + } + + // Normalize kernel so that the sum of all weights equals 1 + for (int i = 0; i < size; i++) + { + kernel[i] /= sum; + } + + return kernel; + } + + /// + /// Create a 1 dimensional Gaussian kernel using the Gaussian G(x) function + /// + /// The convolution kernel. + /// The kernel size. + /// The weight of the blur. + internal static float[] CreateGaussianSharpenKernel(int size, float weight) + { + float[] kernel = new float[size]; + + float sum = 0; + + float midpoint = (size - 1) / 2F; + for (int i = 0; i < size; i++) + { + float x = i - midpoint; + float gx = Numerics.Gaussian(x, weight); + sum += gx; + kernel[i] = gx; + } + + // Invert the kernel for sharpening. + int midpointRounded = (int)midpoint; + for (int i = 0; i < size; i++) + { + if (i == midpointRounded) + { + // Calculate central value + kernel[i] = (2F * sum) - kernel[i]; + } + else + { + // invert value + kernel[i] = -kernel[i]; + } + } + + // Normalize kernel so that the sum of all weights equals 1 + for (int i = 0; i < size; i++) + { + kernel[i] /= sum; + } + + return kernel; + } + + /// + /// Checks whether or not a given NxM matrix is linearly separable, and if so, it extracts the separable components. + /// These would be two 1D vectors, of size N and of size M. + /// This algorithm runs in O(NM). + /// + /// The input 2D matrix to analyze. + /// The resulting 1D row vector, if possible. + /// The resulting 1D column vector, if possible. + /// Whether or not was linearly separable. + public static bool TryGetLinearlySeparableComponents(this DenseMatrix matrix, [NotNullWhen(true)] out float[]? row, [NotNullWhen(true)] out float[]? column) + { + int height = matrix.Rows; + int width = matrix.Columns; + + float[] tempX = new float[width]; + float[] tempY = new float[height]; + + // This algorithm checks whether the input matrix is linearly separable and extracts two + // 1D components if possible. Note that for a given NxM matrix that is linearly separable, + // there exists an infinite number of possible solutions to the system of linear equations + // representing the possible 1D components that can produce the input matrix as a product. + // Let's assume we have a 3x3 input matrix to describe the logic. We have the following: + // + // | m11, m12, m13 | | c1 | + // M = | m21, m22, m23 |, and we want to find: R = | r1, r2, r3 | and C = | c2 |. + // | m31, m32, m33 | | c3 | + // + // We essentially get the following system of linear equations to solve: + // + // / a11 = r1c1 + // | a12 = r2c1 + // | a13 = r3c1 + // | a21 = r1c2 a11 a12 a13 a11 a12 a13 + // / a22 = r2c2, which gives us: ----- = ----- = ----- and ----- = ----- = -----. + // \ a23 = r3c2 a21 a22 a23 a31 a32 a33 + // | a31 = r1c3 + // | a32 = r2c3 + // \ a33 = r3c3 + // + // As we said, there are infinite solutions to this problem (provided the input matrix is in + // fact linearly separable), but we can look at the equalities above to find a way to define + // one specific solution that is very easy to calculate (and that is equivalent to all others + // anyway). In particular, we can see that in order for it to be linearly separable, the matrix + // needs to have each row linearly dependent on each other. That is, its rank is just 1. This + // means that we can express the whole matrix as a function of one row vector (any of the rows + // in the matrix), and a column vector that represents the ratio of each element in a given column + // j with the corresponding j-th item in the reference row. This same procedure extends naturally + // to lineary separable 2D matrices of any size, too. So we end up with the following generalized + // solution for a matrix M of size NxN (or MxN, that works too) and the R and C vectors: + // + // | m11, m12, m13, ..., m1N | | m11/m11 | + // | m21, m22, m23, ..., m2N | | m21/m11 | + // M = | m31, m32, m33, ..., m3N |, R = | m11, m12, m13, ..., m1N |, C = | m31/m11 |. + // | ... ... ... ... ... | | ... | + // | mN1, mN2, mN3, ..., mNN | | mN1/m11 | + // + // So what this algorithm does is just the following: + // 1) It calculates the C[i] value for each i-th row. + // 2) It checks that every j-th item in the row respects C[i] = M[i, j] / M[0, j]. If this is + // not true for any j-th item in any i-th row, then the matrix is not linearly separable. + // 3) It sets items in R and C to the values detailed above if the validation passed. + for (int y = 1; y < height; y++) + { + float ratio = matrix[y, 0] / matrix[0, 0]; + + for (int x = 1; x < width; x++) + { + if (Math.Abs(ratio - (matrix[y, x] / matrix[0, x])) > 0.0001f) + { + row = null; + column = null; + + return false; + } + } + + tempY[y] = ratio; + } + + // The first row is used as a reference, to the ratio is just 1 + tempY[0] = 1; + + // The row component is simply the reference row in the input matrix. + // In this case, we're just using the first one for simplicity. + for (int x = 0; x < width; x++) + { + tempX[x] = matrix[0, x]; + } + + row = tempX; + column = tempY; + + return true; + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs new file mode 100644 index 0000000..e6effd4 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs @@ -0,0 +1,239 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Defines a processor that uses a 2 dimensional matrix to perform convolution against an image. + /// + /// The pixel format. + internal class ConvolutionProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The 2d gradient operator. + /// Whether the convolution filter is applied to alpha as well as the color channels. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public ConvolutionProcessor( + Configuration configuration, + in DenseMatrix kernelXY, + bool preserveAlpha, + Image source, + Rectangle sourceRectangle) + : this(configuration, kernelXY, preserveAlpha, source, sourceRectangle, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The 2d gradient operator. + /// Whether the convolution filter is applied to alpha as well as the color channels. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public ConvolutionProcessor( + Configuration configuration, + in DenseMatrix kernelXY, + bool preserveAlpha, + Image source, + Rectangle sourceRectangle, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + : base(configuration, source, sourceRectangle) + { + this.KernelXY = kernelXY; + this.PreserveAlpha = preserveAlpha; + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; + } + + /// + /// Gets the 2d convolution kernel. + /// + public DenseMatrix KernelXY { get; } + + /// + /// Gets a value indicating whether the convolution filter is applied to alpha as well as the color channels. + /// + public bool PreserveAlpha { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + + /// + protected override void OnFrameApply(ImageFrame source) + { + MemoryAllocator allocator = this.Configuration.MemoryAllocator; + using Buffer2D targetPixels = allocator.Allocate2D(source.Size); + + source.CopyTo(targetPixels); + + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + + using (KernelSamplingMap map = new(allocator)) + { + map.BuildSamplingOffsetMap(this.KernelXY.Rows, this.KernelXY.Columns, interest, this.BorderWrapModeX, this.BorderWrapModeY); + + RowOperation operation = new(interest, targetPixels, source.PixelBuffer, map, this.KernelXY, this.Configuration, this.PreserveAlpha); + ParallelRowIterator.IterateRows( + this.Configuration, + interest, + in operation); + } + + Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); + } + + /// + /// A implementing the convolution logic for . + /// + private readonly struct RowOperation : IRowOperation + { + private readonly Rectangle bounds; + private readonly Buffer2D targetPixels; + private readonly Buffer2D sourcePixels; + private readonly KernelSamplingMap map; + private readonly DenseMatrix kernel; + private readonly Configuration configuration; + private readonly bool preserveAlpha; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperation( + Rectangle bounds, + Buffer2D targetPixels, + Buffer2D sourcePixels, + KernelSamplingMap map, + DenseMatrix kernel, + Configuration configuration, + bool preserveAlpha) + { + this.bounds = bounds; + this.targetPixels = targetPixels; + this.sourcePixels = sourcePixels; + this.map = map; + this.kernel = kernel; + this.configuration = configuration; + this.preserveAlpha = preserveAlpha; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) + => 2 * bounds.Width; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y, Span span) + { + // Span is 2x bounds. + int boundsX = this.bounds.X; + int boundsWidth = this.bounds.Width; + Span sourceBuffer = span[..this.bounds.Width]; + Span targetBuffer = span[this.bounds.Width..]; + + ref Vector4 targetRowRef = ref MemoryMarshal.GetReference(span); + Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); + + ConvolutionState state = new(in this.kernel, this.map); + int row = y - this.bounds.Y; + ref int sampleRowBase = ref state.GetSampleRow((uint)row); + + if (this.preserveAlpha) + { + // Clear the target buffer for each row run. + targetBuffer.Clear(); + ref Vector4 targetBase = ref MemoryMarshal.GetReference(targetBuffer); + + Span sourceRow; + for (uint kY = 0; kY < state.Kernel.Rows; kY++) + { + // Get the precalculated source sample row for this kernel row and copy to our buffer. + int offsetY = Unsafe.Add(ref sampleRowBase, kY); + sourceRow = this.sourcePixels.DangerousGetRowSpan(offsetY).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); + + for (uint x = 0; x < (uint)sourceBuffer.Length; x++) + { + ref int sampleColumnBase = ref state.GetSampleColumn(x); + ref Vector4 target = ref Unsafe.Add(ref targetBase, x); + + for (uint kX = 0; kX < state.Kernel.Columns; kX++) + { + int offsetX = Unsafe.Add(ref sampleColumnBase, kX) - boundsX; + Vector4 sample = Unsafe.Add(ref sourceBase, (uint)offsetX); + target += state.Kernel[kY, kX] * sample; + } + } + } + + // Now we need to copy the original alpha values from the source row. + sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + for (nuint x = 0; x < (uint)sourceRow.Length; x++) + { + ref Vector4 target = ref Unsafe.Add(ref targetBase, x); + target.W = Unsafe.Add(ref MemoryMarshal.GetReference(sourceBuffer), x).W; + } + } + else + { + // Clear the target buffer for each row run. + targetBuffer.Clear(); + ref Vector4 targetBase = ref MemoryMarshal.GetReference(targetBuffer); + + for (uint kY = 0; kY < state.Kernel.Rows; kY++) + { + // Get the precalculated source sample row for this kernel row and copy to our buffer. + int offsetY = Unsafe.Add(ref sampleRowBase, kY); + Span sourceRow = this.sourcePixels.DangerousGetRowSpan(offsetY).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + Numerics.Premultiply(sourceBuffer); + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); + + for (uint x = 0; x < (uint)sourceBuffer.Length; x++) + { + ref int sampleColumnBase = ref state.GetSampleColumn(x); + ref Vector4 target = ref Unsafe.Add(ref targetBase, x); + + for (uint kX = 0; kX < state.Kernel.Columns; kX++) + { + int offsetX = Unsafe.Add(ref sampleColumnBase, kX) - boundsX; + Vector4 sample = Unsafe.Add(ref sourceBase, (uint)offsetX); + target += state.Kernel[kY, kX] * sample; + } + } + } + + Numerics.UnPremultiply(targetBuffer); + } + + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRowSpan); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/ConvolutionState.cs b/ImageSharp/Processing/Processors/Convolution/ConvolutionState.cs new file mode 100644 index 0000000..cbf3262 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/ConvolutionState.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// A stack only struct used for reducing reference indirection during convolution operations. + /// + internal readonly ref struct ConvolutionState + { + private readonly Span rowOffsetMap; + private readonly Span columnOffsetMap; + private readonly uint kernelHeight; + private readonly uint kernelWidth; + + public ConvolutionState( + in DenseMatrix kernel, + KernelSamplingMap map) + { + this.Kernel = new ReadOnlyKernel(kernel); + this.kernelHeight = (uint)kernel.Rows; + this.kernelWidth = (uint)kernel.Columns; + this.rowOffsetMap = map.GetRowOffsetSpan(); + this.columnOffsetMap = map.GetColumnOffsetSpan(); + } + + public readonly ReadOnlyKernel Kernel + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly ref int GetSampleRow(uint row) + => ref Unsafe.Add(ref MemoryMarshal.GetReference(this.rowOffsetMap), row * this.kernelHeight); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly ref int GetSampleColumn(uint column) + => ref Unsafe.Add(ref MemoryMarshal.GetReference(this.columnOffsetMap), column * this.kernelWidth); + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/EdgeDetector2DProcessor.cs b/ImageSharp/Processing/Processors/Convolution/EdgeDetector2DProcessor.cs new file mode 100644 index 0000000..5172404 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/EdgeDetector2DProcessor.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Defines edge detection using the two 1D gradient operators. + /// + public sealed class EdgeDetector2DProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The 2D edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// + public EdgeDetector2DProcessor(EdgeDetector2DKernel kernel, bool grayscale) + { + this.Kernel = kernel; + this.Grayscale = grayscale; + } + + /// + /// Gets the 2D edge detector kernel. + /// + public EdgeDetector2DKernel Kernel { get; } + + /// + /// Gets a value indicating whether to convert the image to grayscale before performing + /// edge detection. + /// + public bool Grayscale { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new EdgeDetector2DProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/EdgeDetector2DProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Convolution/EdgeDetector2DProcessor{TPixel}.cs new file mode 100644 index 0000000..6dd711a --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/EdgeDetector2DProcessor{TPixel}.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Defines a processor that detects edges within an image using two one-dimensional matrices. + /// + /// The pixel format. + internal class EdgeDetector2DProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly DenseMatrix kernelX; + private readonly DenseMatrix kernelY; + private readonly bool grayscale; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public EdgeDetector2DProcessor( + Configuration configuration, + EdgeDetector2DProcessor definition, + Image source, + Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.kernelX = definition.Kernel.KernelX; + this.kernelY = definition.Kernel.KernelY; + this.grayscale = definition.Grayscale; + } + + /// + protected override void BeforeImageApply() + { + using (IImageProcessor opaque = new OpaqueProcessor(this.Configuration, this.Source, this.SourceRectangle)) + { + opaque.Execute(); + } + + if (this.grayscale) + { + new GrayscaleBt709Processor(1F).Execute(this.Configuration, this.Source, this.SourceRectangle); + } + + base.BeforeImageApply(); + } + + /// + protected override void OnFrameApply(ImageFrame source) + { + using Convolution2DProcessor processor = new( + this.Configuration, + in this.kernelX, + in this.kernelY, + true, + this.Source, + this.SourceRectangle); + + processor.Apply(source); + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor.cs b/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor.cs new file mode 100644 index 0000000..33d45f3 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Defines edge detection using eight gradient operators. + /// + public sealed class EdgeDetectorCompassProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// + public EdgeDetectorCompassProcessor(EdgeDetectorCompassKernel kernel, bool grayscale) + { + this.Kernel = kernel; + this.Grayscale = grayscale; + } + + /// + /// Gets the edge detector kernel. + /// + public EdgeDetectorCompassKernel Kernel { get; } + + /// + /// Gets a value indicating whether to convert the image to grayscale before performing + /// edge detection. + /// + public bool Grayscale { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new EdgeDetectorCompassProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs new file mode 100644 index 0000000..8407015 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs @@ -0,0 +1,132 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Defines a processor that detects edges within an image using a eight two dimensional matrices. + /// + /// The pixel format. + internal class EdgeDetectorCompassProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly DenseMatrix[] kernels; + private readonly bool grayscale; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + internal EdgeDetectorCompassProcessor( + Configuration configuration, + EdgeDetectorCompassProcessor definition, + Image source, + Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.grayscale = definition.Grayscale; + this.kernels = definition.Kernel.Flatten(); + } + + /// + protected override void BeforeImageApply() + { + using (IImageProcessor opaque = new OpaqueProcessor(this.Configuration, this.Source, this.SourceRectangle)) + { + opaque.Execute(); + } + + if (this.grayscale) + { + new GrayscaleBt709Processor(1F).Execute(this.Configuration, this.Source, this.SourceRectangle); + } + + base.BeforeImageApply(); + } + + /// + protected override void OnFrameApply(ImageFrame source) + { + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + + // We need a clean copy for each pass to start from + using ImageFrame cleanCopy = source.Clone(); + + using (ConvolutionProcessor processor = new(this.Configuration, in this.kernels[0], true, this.Source, interest)) + { + processor.Apply(source); + } + + if (this.kernels.Length == 1) + { + return; + } + + // Additional runs + for (int i = 1; i < this.kernels.Length; i++) + { + using ImageFrame pass = cleanCopy.Clone(); + + using (ConvolutionProcessor processor = new(this.Configuration, in this.kernels[i], true, this.Source, interest)) + { + processor.Apply(pass); + } + + RowOperation operation = new(source.PixelBuffer, pass.PixelBuffer, interest); + ParallelRowIterator.IterateRows( + this.Configuration, + interest, + in operation); + } + } + + /// + /// A implementing the convolution logic for . + /// + private readonly struct RowOperation : IRowOperation + { + private readonly Buffer2D targetPixels; + private readonly Buffer2D passPixels; + private readonly uint minX; + private readonly uint maxX; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperation( + Buffer2D targetPixels, + Buffer2D passPixels, + Rectangle bounds) + { + this.targetPixels = targetPixels; + this.passPixels = passPixels; + this.minX = (uint)bounds.X; + this.maxX = (uint)bounds.Right; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y) + { + ref TPixel passPixelsBase = ref MemoryMarshal.GetReference(this.passPixels.DangerousGetRowSpan(y)); + ref TPixel targetPixelsBase = ref MemoryMarshal.GetReference(this.targetPixels.DangerousGetRowSpan(y)); + + for (nuint x = this.minX; x < this.maxX; x++) + { + // Grab the max components of the two pixels + ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, x); + ref TPixel currentTargetPixel = ref Unsafe.Add(ref targetPixelsBase, x); + currentTargetPixel = TPixel.FromVector4(Vector4.Max(currentPassPixel.ToVector4(), currentTargetPixel.ToVector4())); + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor.cs b/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor.cs new file mode 100644 index 0000000..9b72191 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Defines edge detection using a single 2D gradient operator. + /// + public sealed class EdgeDetectorProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// + public EdgeDetectorProcessor(EdgeDetectorKernel kernel, bool grayscale) + { + this.Kernel = kernel; + this.Grayscale = grayscale; + } + + /// + /// Gets the edge detector kernel. + /// + public EdgeDetectorKernel Kernel { get; } + + /// + /// Gets a value indicating whether to convert the image to grayscale before performing + /// edge detection. + /// + public bool Grayscale { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new EdgeDetectorProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor{TPixel}.cs new file mode 100644 index 0000000..a2abdcd --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor{TPixel}.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Filters; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Defines a processor that detects edges within an image using a single two dimensional matrix. + /// + /// The pixel format. + internal class EdgeDetectorProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly bool grayscale; + private readonly DenseMatrix kernelXY; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The target area to process for the current processor instance. + public EdgeDetectorProcessor( + Configuration configuration, + EdgeDetectorProcessor definition, + Image source, + Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.kernelXY = definition.Kernel.KernelXY; + this.grayscale = definition.Grayscale; + } + + /// + protected override void BeforeImageApply() + { + using (IImageProcessor opaque = new OpaqueProcessor(this.Configuration, this.Source, this.SourceRectangle)) + { + opaque.Execute(); + } + + if (this.grayscale) + { + new GrayscaleBt709Processor(1F).Execute(this.Configuration, this.Source, this.SourceRectangle); + } + + base.BeforeImageApply(); + } + + /// + protected override void OnFrameApply(ImageFrame source) + { + using ConvolutionProcessor processor = new(this.Configuration, in this.kernelXY, true, this.Source, this.SourceRectangle); + processor.Apply(source); + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor.cs b/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor.cs new file mode 100644 index 0000000..117f894 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor.cs @@ -0,0 +1,120 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Defines Gaussian blur by a (Sigma, Radius) pair. + /// + public sealed class GaussianBlurProcessor : IImageProcessor + { + /// + /// The default value for . + /// + public const float DefaultSigma = 3f; + + /// + /// Initializes a new instance of the class. + /// + public GaussianBlurProcessor() + : this(DefaultSigma, ConvolutionProcessorHelpers.GetDefaultGaussianRadius(DefaultSigma)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The 'sigma' value representing the weight of the blur. + public GaussianBlurProcessor(float sigma) + : this(sigma, ConvolutionProcessorHelpers.GetDefaultGaussianRadius(sigma)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The 'sigma' value representing the weight of the blur. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public GaussianBlurProcessor(float sigma, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) + : this(sigma, ConvolutionProcessorHelpers.GetDefaultGaussianRadius(sigma), borderWrapModeX, borderWrapModeY) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// + public GaussianBlurProcessor(int radius) + : this(radius / 3F, radius) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'sigma' value representing the weight of the blur. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// This should be at least twice the sigma value. + /// + public GaussianBlurProcessor(float sigma, int radius) + : this(sigma, radius, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'sigma' value representing the weight of the blur. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// This should be at least twice the sigma value. + /// + /// + /// The to use when mapping the pixels outside of the border, in X direction. + /// + /// + /// The to use when mapping the pixels outside of the border, in Y direction. + /// + public GaussianBlurProcessor(float sigma, int radius, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) + { + this.Sigma = sigma; + this.Radius = radius; + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; + } + + /// + /// Gets the sigma value representing the weight of the blur + /// + public float Sigma { get; } + + /// + /// Gets the radius defining the size of the area to sample. + /// + public int Radius { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new GaussianBlurProcessor(configuration, this, source, sourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs new file mode 100644 index 0000000..e6584f3 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Applies Gaussian blur processing to an image. + /// + /// The pixel format. + internal class GaussianBlurProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public GaussianBlurProcessor( + Configuration configuration, + GaussianBlurProcessor definition, + Image source, + Rectangle sourceRectangle, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + : base(configuration, source, sourceRectangle) + { + int kernelSize = (definition.Radius * 2) + 1; + this.Kernel = ConvolutionProcessorHelpers.CreateGaussianBlurKernel(kernelSize, definition.Sigma); + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; + } + + /// + /// Gets the 1D convolution kernel. + /// + public float[] Kernel { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + + /// + protected override void OnFrameApply(ImageFrame source) + { + using Convolution2PassProcessor processor = new(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); + + processor.Apply(source); + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor.cs b/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor.cs new file mode 100644 index 0000000..a4fc3fa --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor.cs @@ -0,0 +1,120 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Defines Gaussian sharpening by a (Sigma, Radius) pair. + /// + public sealed class GaussianSharpenProcessor : IImageProcessor + { + /// + /// The default value for . + /// + public const float DefaultSigma = 3f; + + /// + /// Initializes a new instance of the class. + /// + public GaussianSharpenProcessor() + : this(DefaultSigma, ConvolutionProcessorHelpers.GetDefaultGaussianRadius(DefaultSigma)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The 'sigma' value representing the weight of the blur. + public GaussianSharpenProcessor(float sigma) + : this(sigma, ConvolutionProcessorHelpers.GetDefaultGaussianRadius(sigma)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The 'sigma' value representing the weight of the blur. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public GaussianSharpenProcessor(float sigma, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) + : this(sigma, ConvolutionProcessorHelpers.GetDefaultGaussianRadius(sigma), borderWrapModeX, borderWrapModeY) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// + public GaussianSharpenProcessor(int radius) + : this(radius / 3F, radius) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'sigma' value representing the weight of the blur. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// This should be at least twice the sigma value. + /// + public GaussianSharpenProcessor(float sigma, int radius) + : this(sigma, radius, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'sigma' value representing the weight of the blur. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// This should be at least twice the sigma value. + /// + /// + /// The to use when mapping the pixels outside of the border, in X direction. + /// + /// + /// The to use when mapping the pixels outside of the border, in Y direction. + /// + public GaussianSharpenProcessor(float sigma, int radius, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) + { + this.Sigma = sigma; + this.Radius = radius; + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; + } + + /// + /// Gets the sigma value representing the weight of the blur + /// + public float Sigma { get; } + + /// + /// Gets the radius defining the size of the area to sample. + /// + public int Radius { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new GaussianSharpenProcessor(configuration, this, source, sourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs new file mode 100644 index 0000000..6a43dcc --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Applies Gaussian sharpening processing to the image. + /// + /// The pixel format. + internal class GaussianSharpenProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public GaussianSharpenProcessor( + Configuration configuration, + GaussianSharpenProcessor definition, + Image source, + Rectangle sourceRectangle, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + : base(configuration, source, sourceRectangle) + { + int kernelSize = (definition.Radius * 2) + 1; + this.Kernel = ConvolutionProcessorHelpers.CreateGaussianSharpenKernel(kernelSize, definition.Sigma); + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; + } + + /// + /// Gets the 1D convolution kernel. + /// + public float[] Kernel { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + + /// + protected override void OnFrameApply(ImageFrame source) + { + using Convolution2PassProcessor processor = new(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); + + processor.Apply(source); + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Kernel.cs b/ImageSharp/Processing/Processors/Convolution/Kernel.cs new file mode 100644 index 0000000..ab9352e --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Kernel.cs @@ -0,0 +1,98 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// A stack only, readonly, kernel matrix that can be indexed without + /// bounds checks when compiled in release mode. + /// + /// The type of each element in the kernel. + internal readonly ref struct Kernel + where T : struct, IEquatable + { + private readonly Span values; + + public Kernel(DenseMatrix matrix) + { + this.Columns = matrix.Columns; + this.Rows = matrix.Rows; + this.values = matrix.Span; + } + + public int Columns + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + public int Rows + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + public ReadOnlySpan Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.values; + } + + public T this[int row, int column] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + this.CheckCoordinates(row, column); + ref T vBase = ref MemoryMarshal.GetReference(this.values); + return Unsafe.Add(ref vBase, (uint)((row * this.Columns) + column)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + this.CheckCoordinates(row, column); + ref T vBase = ref MemoryMarshal.GetReference(this.values); + Unsafe.Add(ref vBase, (uint)((row * this.Columns) + column)) = value; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetValue(int index, T value) + { + this.CheckIndex(index); + ref T vBase = ref MemoryMarshal.GetReference(this.values); + Unsafe.Add(ref vBase, (uint)index) = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() => this.values.Clear(); + + [Conditional("DEBUG")] + private void CheckCoordinates(int row, int column) + { + if (row < 0 || row >= this.Rows) + { + throw new ArgumentOutOfRangeException(nameof(row), row, $"{row} is outside the matrix bounds."); + } + + if (column < 0 || column >= this.Columns) + { + throw new ArgumentOutOfRangeException(nameof(column), column, $"{column} is outside the matrix bounds."); + } + } + + [Conditional("DEBUG")] + private void CheckIndex(int index) + { + if (index < 0 || index >= this.values.Length) + { + throw new ArgumentOutOfRangeException(nameof(index), index, $"{index} is outside the matrix bounds."); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs b/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs new file mode 100644 index 0000000..7eaab7d --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs @@ -0,0 +1,184 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Provides a map of the convolution kernel sampling offsets. + /// + internal sealed class KernelSamplingMap : IDisposable + { + private readonly MemoryAllocator allocator; + private bool isDisposed; + private IMemoryOwner? yOffsets; + private IMemoryOwner? xOffsets; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + public KernelSamplingMap(MemoryAllocator allocator) => this.allocator = allocator; + + /// + /// Builds a map of the sampling offsets for the kernel clamped by the given bounds. + /// + /// The convolution kernel. + /// The source bounds. + public void BuildSamplingOffsetMap(DenseMatrix kernel, Rectangle bounds) + => this.BuildSamplingOffsetMap(kernel.Rows, kernel.Columns, bounds, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat); + + /// + /// Builds a map of the sampling offsets for the kernel clamped by the given bounds. + /// + /// The height (number of rows) of the convolution kernel to use. + /// The width (number of columns) of the convolution kernel to use. + /// The source bounds. + public void BuildSamplingOffsetMap(int kernelHeight, int kernelWidth, Rectangle bounds) + => this.BuildSamplingOffsetMap(kernelHeight, kernelWidth, bounds, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat); + + /// + /// Builds a map of the sampling offsets for the kernel clamped by the given bounds. + /// + /// The height (number of rows) of the convolution kernel to use. + /// The width (number of columns) of the convolution kernel to use. + /// The source bounds. + /// The wrapping mode on the horizontal borders. + /// The wrapping mode on the vertical borders. + public void BuildSamplingOffsetMap(int kernelHeight, int kernelWidth, Rectangle bounds, BorderWrappingMode xBorderMode, BorderWrappingMode yBorderMode) + { + this.yOffsets = this.allocator.Allocate(bounds.Height * kernelHeight); + this.xOffsets = this.allocator.Allocate(bounds.Width * kernelWidth); + + int minY = bounds.Y; + int maxY = bounds.Bottom - 1; + int minX = bounds.X; + int maxX = bounds.Right - 1; + + BuildOffsets(this.yOffsets, bounds.Height, kernelHeight, minY, maxY, yBorderMode); + BuildOffsets(this.xOffsets, bounds.Width, kernelWidth, minX, maxX, xBorderMode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetRowOffsetSpan() => this.yOffsets!.GetSpan(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetColumnOffsetSpan() => this.xOffsets!.GetSpan(); + + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.yOffsets?.Dispose(); + this.xOffsets?.Dispose(); + + this.isDisposed = true; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void BuildOffsets(IMemoryOwner offsets, int boundsSize, int kernelSize, int min, int max, BorderWrappingMode borderMode) + { + int radius = kernelSize >> 1; + Span span = offsets.GetSpan(); + ref int spanBase = ref MemoryMarshal.GetReference(span); + for (int chunk = 0; chunk < boundsSize; chunk++) + { + int chunkBase = chunk * kernelSize; + for (int i = 0; i < kernelSize; i++) + { + Unsafe.Add(ref spanBase, (uint)(chunkBase + i)) = chunk + i + min - radius; + } + } + + CorrectBorder(span, kernelSize, min, max, borderMode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void CorrectBorder(Span span, int kernelSize, int min, int max, BorderWrappingMode borderMode) + { + int affectedSize = (kernelSize >> 1) * kernelSize; + ref int spanBase = ref MemoryMarshal.GetReference(span); + if (affectedSize > 0) + { + switch (borderMode) + { + case BorderWrappingMode.Repeat: + Numerics.Clamp(span[..affectedSize], min, max); + Numerics.Clamp(span[^affectedSize..], min, max); + break; + case BorderWrappingMode.Mirror: + int min2dec = min + min - 1; + for (int i = 0; i < affectedSize; i++) + { + int value = span[i]; + if (value < min) + { + span[i] = min2dec - value; + } + } + + int max2inc = max + max + 1; + for (int i = span.Length - affectedSize; i < span.Length; i++) + { + int value = span[i]; + if (value > max) + { + span[i] = max2inc - value; + } + } + + break; + case BorderWrappingMode.Bounce: + int min2 = min + min; + for (int i = 0; i < affectedSize; i++) + { + int value = span[i]; + if (value < min) + { + span[i] = min2 - value; + } + } + + int max2 = max + max; + for (int i = span.Length - affectedSize; i < span.Length; i++) + { + int value = span[i]; + if (value > max) + { + span[i] = max2 - value; + } + } + + break; + case BorderWrappingMode.Wrap: + int diff = max - min + 1; + for (int i = 0; i < affectedSize; i++) + { + int value = span[i]; + if (value < min) + { + span[i] = diff + value; + } + } + + for (int i = span.Length - affectedSize; i < span.Length; i++) + { + int value = span[i]; + if (value > max) + { + span[i] = value - diff; + } + } + + break; + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetector2DKernel.cs b/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetector2DKernel.cs new file mode 100644 index 0000000..1741c51 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetector2DKernel.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Represents an edge detection convolution kernel consisting of two 1D gradient operators. + /// + public readonly struct EdgeDetector2DKernel : IEquatable + { + /// + /// An edge detection kernel containing two Kayyali operators. + /// + public static readonly EdgeDetector2DKernel KayyaliKernel = new(KayyaliKernels.KayyaliX, KayyaliKernels.KayyaliY); + + /// + /// An edge detection kernel containing two Prewitt operators. + /// . + /// + public static readonly EdgeDetector2DKernel PrewittKernel = new(PrewittKernels.PrewittX, PrewittKernels.PrewittY); + + /// + /// An edge detection kernel containing two Roberts-Cross operators. + /// . + /// + public static readonly EdgeDetector2DKernel RobertsCrossKernel = new(RobertsCrossKernels.RobertsCrossX, RobertsCrossKernels.RobertsCrossY); + + /// + /// An edge detection kernel containing two Scharr operators. + /// + public static readonly EdgeDetector2DKernel ScharrKernel = new(ScharrKernels.ScharrX, ScharrKernels.ScharrY); + + /// + /// An edge detection kernel containing two Sobel operators. + /// . + /// + public static readonly EdgeDetector2DKernel SobelKernel = new(SobelKernels.SobelX, SobelKernels.SobelY); + + /// + /// Initializes a new instance of the struct. + /// + /// The horizontal gradient operator. + /// The vertical gradient operator. + public EdgeDetector2DKernel(DenseMatrix kernelX, DenseMatrix kernelY) + { + Guard.IsTrue( + kernelX.Size.Equals(kernelY.Size), + $"{nameof(kernelX)} {nameof(kernelY)}", + "Kernel sizes must be the same."); + + this.KernelX = kernelX; + this.KernelY = kernelY; + } + + /// + /// Gets the horizontal gradient operator. + /// + public DenseMatrix KernelX { get; } + + /// + /// Gets the vertical gradient operator. + /// + public DenseMatrix KernelY { get; } + + /// + /// Checks whether two structures are equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is equal to the parameter; + /// otherwise, false. + /// + public static bool operator ==(EdgeDetector2DKernel left, EdgeDetector2DKernel right) + => left.Equals(right); + + /// + /// Checks whether two structures are equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is not equal to the parameter; + /// otherwise, false. + /// + public static bool operator !=(EdgeDetector2DKernel left, EdgeDetector2DKernel right) + => !(left == right); + + /// + public override bool Equals(object? obj) + => obj is EdgeDetector2DKernel kernel && this.Equals(kernel); + + /// + public bool Equals(EdgeDetector2DKernel other) + => this.KernelX.Equals(other.KernelX) + && this.KernelY.Equals(other.KernelY); + + /// + public override int GetHashCode() => HashCode.Combine(this.KernelX, this.KernelY); + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetectorCompassKernel.cs b/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetectorCompassKernel.cs new file mode 100644 index 0000000..4539b51 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetectorCompassKernel.cs @@ -0,0 +1,161 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Represents an edge detection convolution kernel consisting of eight gradient operators. + /// + public readonly struct EdgeDetectorCompassKernel : IEquatable + { + /// + /// An edge detection kenel comprised of Kirsch gradient operators. + /// . + /// + public static readonly EdgeDetectorCompassKernel Kirsch = + new( + KirschKernels.North, + KirschKernels.NorthWest, + KirschKernels.West, + KirschKernels.SouthWest, + KirschKernels.South, + KirschKernels.SouthEast, + KirschKernels.East, + KirschKernels.NorthEast); + + /// + /// An edge detection kenel comprised of Robinson gradient operators. + /// + /// + public static readonly EdgeDetectorCompassKernel Robinson = + new( + RobinsonKernels.North, + RobinsonKernels.NorthWest, + RobinsonKernels.West, + RobinsonKernels.SouthWest, + RobinsonKernels.South, + RobinsonKernels.SouthEast, + RobinsonKernels.East, + RobinsonKernels.NorthEast); + + /// + /// Initializes a new instance of the struct. + /// + /// The north gradient operator. + /// The north-west gradient operator. + /// The west gradient operator. + /// The south-west gradient operator. + /// The south gradient operator. + /// The south-east gradient operator. + /// The east gradient operator. + /// The north-east gradient operator. + public EdgeDetectorCompassKernel( + DenseMatrix north, + DenseMatrix northWest, + DenseMatrix west, + DenseMatrix southWest, + DenseMatrix south, + DenseMatrix southEast, + DenseMatrix east, + DenseMatrix northEast) + { + this.North = north; + this.NorthWest = northWest; + this.West = west; + this.SouthWest = southWest; + this.South = south; + this.SouthEast = southEast; + this.East = east; + this.NorthEast = northEast; + } + + /// + /// Gets the North gradient operator. + /// + public DenseMatrix North { get; } + + /// + /// Gets the NorthWest gradient operator. + /// + public DenseMatrix NorthWest { get; } + + /// + /// Gets the West gradient operator. + /// + public DenseMatrix West { get; } + + /// + /// Gets the SouthWest gradient operator. + /// + public DenseMatrix SouthWest { get; } + + /// + /// Gets the South gradient operator. + /// + public DenseMatrix South { get; } + + /// + /// Gets the SouthEast gradient operator. + /// + public DenseMatrix SouthEast { get; } + + /// + /// Gets the East gradient operator. + /// + public DenseMatrix East { get; } + + /// + /// Gets the NorthEast gradient operator. + /// + public DenseMatrix NorthEast { get; } + + /// + /// Checks whether two structures are equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is equal to the parameter; + /// otherwise, false. + /// + public static bool operator ==(EdgeDetectorCompassKernel left, EdgeDetectorCompassKernel right) + => left.Equals(right); + + /// + /// Checks whether two structures are equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is not equal to the parameter; + /// otherwise, false. + /// + public static bool operator !=(EdgeDetectorCompassKernel left, EdgeDetectorCompassKernel right) + => !(left == right); + + /// + public override bool Equals(object? obj) => obj is EdgeDetectorCompassKernel kernel && this.Equals(kernel); + + /// + public bool Equals(EdgeDetectorCompassKernel other) => this.North.Equals(other.North) && this.NorthWest.Equals(other.NorthWest) && this.West.Equals(other.West) && this.SouthWest.Equals(other.SouthWest) && this.South.Equals(other.South) && this.SouthEast.Equals(other.SouthEast) && this.East.Equals(other.East) && this.NorthEast.Equals(other.NorthEast); + + /// + public override int GetHashCode() + => HashCode.Combine( + this.North, + this.NorthWest, + this.West, + this.SouthWest, + this.South, + this.SouthEast, + this.East, + this.NorthEast); + + internal DenseMatrix[] Flatten() => + [ + this.North, this.NorthWest, this.West, this.SouthWest, + this.South, this.SouthEast, this.East, this.NorthEast + ]; + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetectorKernel.cs b/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetectorKernel.cs new file mode 100644 index 0000000..7a0ad5e --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetectorKernel.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Represents an edge detection convolution kernel consisting of a single 2D gradient operator. + /// + public readonly struct EdgeDetectorKernel : IEquatable + { + /// + /// An edge detection kernel containing a 3x3 Laplacian operator. + /// + /// + public static readonly EdgeDetectorKernel Laplacian3x3 = new(LaplacianKernels.Laplacian3x3); + + /// + /// An edge detection kernel containing a 5x5 Laplacian operator. + /// + /// + public static readonly EdgeDetectorKernel Laplacian5x5 = new(LaplacianKernels.Laplacian5x5); + + /// + /// An edge detection kernel containing a Laplacian of Gaussian operator. + /// . + /// + public static readonly EdgeDetectorKernel LaplacianOfGaussian = new(LaplacianKernels.LaplacianOfGaussianXY); + + /// + /// Initializes a new instance of the struct. + /// + /// The 2D gradient operator. + public EdgeDetectorKernel(DenseMatrix kernelXY) + => this.KernelXY = kernelXY; + + /// + /// Gets the 2D gradient operator. + /// + public DenseMatrix KernelXY { get; } + + /// + /// Checks whether two structures are equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is equal to the parameter; + /// otherwise, false. + /// + public static bool operator ==(EdgeDetectorKernel left, EdgeDetectorKernel right) + => left.Equals(right); + + /// + /// Checks whether two structures are equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is not equal to the parameter; + /// otherwise, false. + /// + public static bool operator !=(EdgeDetectorKernel left, EdgeDetectorKernel right) + => !(left == right); + + /// + public override bool Equals(object? obj) + => obj is EdgeDetectorKernel kernel && this.Equals(kernel); + + /// + public bool Equals(EdgeDetectorKernel other) + => this.KernelXY.Equals(other.KernelXY); + + /// + public override int GetHashCode() => this.KernelXY.GetHashCode(); + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/KayyaliKernels.cs b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/KayyaliKernels.cs new file mode 100644 index 0000000..6b96b09 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/KayyaliKernels.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Contains the kernels used for Kayyali edge detection + /// + internal static class KayyaliKernels + { + /// + /// Gets the horizontal gradient operator. + /// + public static DenseMatrix KayyaliX => + new float[,] + { + { 6, 0, -6 }, + { 0, 0, 0 }, + { -6, 0, 6 } + }; + + /// + /// Gets the vertical gradient operator. + /// + public static DenseMatrix KayyaliY => + new float[,] + { + { -6, 0, 6 }, + { 0, 0, 0 }, + { 6, 0, -6 } + }; + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/KirschKernels.cs b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/KirschKernels.cs new file mode 100644 index 0000000..801e1fb --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/KirschKernels.cs @@ -0,0 +1,99 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Contains the eight matrices used for Kirsch edge detection. + /// . + /// + internal static class KirschKernels + { + /// + /// Gets the North gradient operator + /// + public static DenseMatrix North => + new float[,] + { + { 5, 5, 5 }, + { -3, 0, -3 }, + { -3, -3, -3 } + }; + + /// + /// Gets the NorthWest gradient operator + /// + public static DenseMatrix NorthWest => + new float[,] + { + { 5, 5, -3 }, + { 5, 0, -3 }, + { -3, -3, -3 } + }; + + /// + /// Gets the West gradient operator + /// + public static DenseMatrix West => + new float[,] + { + { 5, -3, -3 }, + { 5, 0, -3 }, + { 5, -3, -3 } + }; + + /// + /// Gets the SouthWest gradient operator + /// + public static DenseMatrix SouthWest => + new float[,] + { + { -3, -3, -3 }, + { 5, 0, -3 }, + { 5, 5, -3 } + }; + + /// + /// Gets the South gradient operator + /// + public static DenseMatrix South => + new float[,] + { + { -3, -3, -3 }, + { -3, 0, -3 }, + { 5, 5, 5 } + }; + + /// + /// Gets the SouthEast gradient operator + /// + public static DenseMatrix SouthEast => + new float[,] + { + { -3, -3, -3 }, + { -3, 0, 5 }, + { -3, 5, 5 } + }; + + /// + /// Gets the East gradient operator + /// + public static DenseMatrix East => + new float[,] + { + { -3, -3, 5 }, + { -3, 0, 5 }, + { -3, -3, 5 } + }; + + /// + /// Gets the NorthEast gradient operator + /// + public static DenseMatrix NorthEast => + new float[,] + { + { -3, 5, 5 }, + { -3, 0, 5 }, + { -3, -3, -3 } + }; + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/LaplacianKernelFactory.cs b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/LaplacianKernelFactory.cs new file mode 100644 index 0000000..d484ff3 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/LaplacianKernelFactory.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// A factory for creating Laplacian kernel matrices. + /// + internal static class LaplacianKernelFactory + { + /// + /// Creates a Laplacian matrix, 2nd derivative, of an arbitrary length. + /// + /// + /// The length of the matrix sides + /// The + public static DenseMatrix CreateKernel(uint length) + { + Guard.MustBeGreaterThanOrEqualTo(length, 3u, nameof(length)); + Guard.IsFalse(length % 2 == 0, nameof(length), "The kernel length must be an odd number."); + + DenseMatrix kernel = new((int)length); + kernel.Fill(-1); + + int mid = (int)(length / 2); + kernel[mid, mid] = (length * length) - 1; + + return kernel; + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/LaplacianKernels.cs b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/LaplacianKernels.cs new file mode 100644 index 0000000..0ffb30b --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/LaplacianKernels.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Contains Laplacian kernels of different sizes. + /// + /// . + /// + internal static class LaplacianKernels + { + /// + /// Gets the 3x3 Laplacian kernel + /// + public static DenseMatrix Laplacian3x3 => LaplacianKernelFactory.CreateKernel(3); + + /// + /// Gets the 5x5 Laplacian kernel + /// + public static DenseMatrix Laplacian5x5 => LaplacianKernelFactory.CreateKernel(5); + + /// + /// Gets the Laplacian of Gaussian kernel. + /// + public static DenseMatrix LaplacianOfGaussianXY => + new float[,] + { + { 0, 0, -1, 0, 0 }, + { 0, -1, -2, -1, 0 }, + { -1, -2, 16, -2, -1 }, + { 0, -1, -2, -1, 0 }, + { 0, 0, -1, 0, 0 } + }; + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/PrewittKernels.cs b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/PrewittKernels.cs new file mode 100644 index 0000000..f2b8bdd --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/PrewittKernels.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Contains the kernels used for Prewitt edge detection + /// + internal static class PrewittKernels + { + /// + /// Gets the horizontal gradient operator. + /// + public static DenseMatrix PrewittX => + new float[,] + { + { -1, 0, 1 }, + { -1, 0, 1 }, + { -1, 0, 1 } + }; + + /// + /// Gets the vertical gradient operator. + /// + public static DenseMatrix PrewittY => + new float[,] + { + { 1, 1, 1 }, + { 0, 0, 0 }, + { -1, -1, -1 } + }; + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/RobertsCrossKernels.cs b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/RobertsCrossKernels.cs new file mode 100644 index 0000000..c20a961 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/RobertsCrossKernels.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Contains the kernels used for RobertsCross edge detection + /// + internal static class RobertsCrossKernels + { + /// + /// Gets the horizontal gradient operator. + /// + public static DenseMatrix RobertsCrossX => + new float[,] + { + { 1, 0 }, + { 0, -1 } + }; + + /// + /// Gets the vertical gradient operator. + /// + public static DenseMatrix RobertsCrossY => + new float[,] + { + { 0, 1 }, + { -1, 0 } + }; + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/RobinsonKernels.cs b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/RobinsonKernels.cs new file mode 100644 index 0000000..88243de --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/RobinsonKernels.cs @@ -0,0 +1,99 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Contains the kernels used for Robinson edge detection. + /// + /// + internal static class RobinsonKernels + { + /// + /// Gets the North gradient operator + /// + public static DenseMatrix North => + new float[,] + { + { 1, 2, 1 }, + { 0, 0, 0 }, + { -1, -2, -1 } + }; + + /// + /// Gets the NorthWest gradient operator + /// + public static DenseMatrix NorthWest => + new float[,] + { + { 2, 1, 0 }, + { 1, 0, -1 }, + { 0, -1, -2 } + }; + + /// + /// Gets the West gradient operator + /// + public static DenseMatrix West => + new float[,] + { + { 1, 0, -1 }, + { 2, 0, -2 }, + { 1, 0, -1 } + }; + + /// + /// Gets the SouthWest gradient operator + /// + public static DenseMatrix SouthWest => + new float[,] + { + { 0, -1, -2 }, + { 1, 0, -1 }, + { 2, 1, 0 } + }; + + /// + /// Gets the South gradient operator + /// + public static DenseMatrix South => + new float[,] + { + { -1, -2, -1 }, + { 0, 0, 0 }, + { 1, 2, 1 } + }; + + /// + /// Gets the SouthEast gradient operator + /// + public static DenseMatrix SouthEast => + new float[,] + { + { -2, -1, 0 }, + { -1, 0, 1 }, + { 0, 1, 2 } + }; + + /// + /// Gets the East gradient operator + /// + public static DenseMatrix East => + new float[,] + { + { -1, 0, 1 }, + { -2, 0, 2 }, + { -1, 0, 1 } + }; + + /// + /// Gets the NorthEast gradient operator + /// + public static DenseMatrix NorthEast => + new float[,] + { + { 0, 1, 2 }, + { -1, 0, 1 }, + { -2, -1, 0 } + }; + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/ScharrKernels.cs b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/ScharrKernels.cs new file mode 100644 index 0000000..f52d81e --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/ScharrKernels.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Contains the kernels used for Scharr edge detection + /// + internal static class ScharrKernels + { + /// + /// Gets the horizontal gradient operator. + /// + public static DenseMatrix ScharrX => + new float[,] + { + { -3, 0, 3 }, + { -10, 0, 10 }, + { -3, 0, 3 } + }; + + /// + /// Gets the vertical gradient operator. + /// + public static DenseMatrix ScharrY => + new float[,] + { + { 3, 10, 3 }, + { 0, 0, 0 }, + { -3, -10, -3 } + }; + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/SobelKernels.cs b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/SobelKernels.cs new file mode 100644 index 0000000..4413f74 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/SobelKernels.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Contains the kernels used for Sobel edge detection + /// + internal static class SobelKernels + { + /// + /// Gets the horizontal gradient operator. + /// + public static DenseMatrix SobelX => + new float[,] + { + { -1, 0, 1 }, + { -2, 0, 2 }, + { -1, 0, 1 } + }; + + /// + /// Gets the vertical gradient operator. + /// + public static DenseMatrix SobelY => + new float[,] + { + { -1, -2, -1 }, + { 0, 0, 0 }, + { 1, 2, 1 } + }; + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor.cs b/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor.cs new file mode 100644 index 0000000..287b583 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Applies an median filter. + /// + public sealed class MedianBlurProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'radius' value representing the size of the area to filter over. + /// + /// + /// Whether the filter is applied to alpha as well as the color channels. + /// + public MedianBlurProcessor(int radius, bool preserveAlpha) + { + this.Radius = radius; + this.PreserveAlpha = preserveAlpha; + } + + /// + /// Gets the size of the area to find the median of. + /// + public int Radius { get; } + + /// + /// Gets a value indicating whether the filter is applied to alpha as well as the color channels. + /// + public bool PreserveAlpha { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new MedianBlurProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs new file mode 100644 index 0000000..dd8d372 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Applies an median filter. + /// + /// The type of pixel format. + internal sealed class MedianBlurProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly MedianBlurProcessor definition; + + public MedianBlurProcessor(Configuration configuration, MedianBlurProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) => this.definition = definition; + + protected override void OnFrameApply(ImageFrame source) + { + int kernelSize = (2 * this.definition.Radius) + 1; + + MemoryAllocator allocator = this.Configuration.MemoryAllocator; + using Buffer2D targetPixels = allocator.Allocate2D(source.Width, source.Height); + + source.CopyTo(targetPixels); + + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + + using KernelSamplingMap map = new(this.Configuration.MemoryAllocator); + map.BuildSamplingOffsetMap(kernelSize, kernelSize, interest, this.definition.BorderWrapModeX, this.definition.BorderWrapModeY); + + MedianRowOperation operation = new( + interest, + targetPixels, + source.PixelBuffer, + map, + kernelSize, + this.Configuration, + this.definition.PreserveAlpha); + + ParallelRowIterator.IterateRows, Vector4>( + this.Configuration, + interest, + in operation); + + Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/MedianConvolutionState.cs b/ImageSharp/Processing/Processors/Convolution/MedianConvolutionState.cs new file mode 100644 index 0000000..ca0115a --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/MedianConvolutionState.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// A stack only struct used for reducing reference indirection during convolution operations. + /// + internal readonly ref struct MedianConvolutionState + { + private readonly Span rowOffsetMap; + private readonly Span columnOffsetMap; + private readonly int kernelHeight; + private readonly int kernelWidth; + + public MedianConvolutionState( + in DenseMatrix kernel, + KernelSamplingMap map) + { + this.Kernel = new Kernel(kernel); + this.kernelHeight = kernel.Rows; + this.kernelWidth = kernel.Columns; + this.rowOffsetMap = map.GetRowOffsetSpan(); + this.columnOffsetMap = map.GetColumnOffsetSpan(); + } + + public readonly Kernel Kernel + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly ref int GetSampleRow(int row) + => ref Unsafe.Add(ref MemoryMarshal.GetReference(this.rowOffsetMap), (uint)(row * this.kernelHeight)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly ref int GetSampleColumn(int column) + => ref Unsafe.Add(ref MemoryMarshal.GetReference(this.columnOffsetMap), (uint)(column * this.kernelWidth)); + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs b/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs new file mode 100644 index 0000000..034c6d8 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs @@ -0,0 +1,182 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// Applies an median filter. + /// + /// The type of pixel format. + internal readonly struct MedianRowOperation : IRowOperation + where TPixel : unmanaged, IPixel + { + private readonly int yChannelStart; + private readonly int zChannelStart; + private readonly int wChannelStart; + private readonly Configuration configuration; + private readonly Rectangle bounds; + private readonly Buffer2D targetPixels; + private readonly Buffer2D sourcePixels; + private readonly KernelSamplingMap map; + private readonly int kernelSize; + private readonly bool preserveAlpha; + + public MedianRowOperation(Rectangle bounds, Buffer2D targetPixels, Buffer2D sourcePixels, KernelSamplingMap map, int kernelSize, Configuration configuration, bool preserveAlpha) + { + this.bounds = bounds; + this.configuration = configuration; + this.targetPixels = targetPixels; + this.sourcePixels = sourcePixels; + this.map = map; + this.kernelSize = kernelSize; + this.preserveAlpha = preserveAlpha; + int kernelCount = this.kernelSize * this.kernelSize; + this.yChannelStart = kernelCount; + this.zChannelStart = this.yChannelStart + kernelCount; + this.wChannelStart = this.zChannelStart + kernelCount; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) + => (2 * this.kernelSize * this.kernelSize) + bounds.Width + (this.kernelSize * bounds.Width); + + public void Invoke(int y, Span span) + { + // Span has kernelSize^2 twice, then bound width followed by kernelsize * bounds width. + int boundsX = this.bounds.X; + int boundsWidth = this.bounds.Width; + int kernelCount = this.kernelSize * this.kernelSize; + Span kernelBuffer = span[..kernelCount]; + Span channelVectorBuffer = span.Slice(kernelCount, kernelCount); + Span sourceVectorBuffer = span.Slice(kernelCount << 1, this.kernelSize * boundsWidth); + Span targetBuffer = span.Slice((kernelCount << 1) + sourceVectorBuffer.Length, boundsWidth); + + // Stack 4 channels of floats in the space of Vector4's. + Span channelBuffer = MemoryMarshal.Cast(channelVectorBuffer); + Span xChannel = channelBuffer[..kernelCount]; + Span yChannel = channelBuffer.Slice(this.yChannelStart, kernelCount); + Span zChannel = channelBuffer.Slice(this.zChannelStart, kernelCount); + + DenseMatrix kernel = new(this.kernelSize, this.kernelSize, kernelBuffer); + + int row = y - this.bounds.Y; + MedianConvolutionState state = new(in kernel, this.map); + ref int sampleRowBase = ref state.GetSampleRow(row); + ref Vector4 targetBase = ref MemoryMarshal.GetReference(targetBuffer); + + // First convert the required source rows to Vector4. + for (int i = 0; i < this.kernelSize; i++) + { + int currentYIndex = Unsafe.Add(ref sampleRowBase, (uint)i); + Span sourceRow = this.sourcePixels.DangerousGetRowSpan(currentYIndex).Slice(boundsX, boundsWidth); + Span sourceVectorRow = sourceVectorBuffer.Slice(i * boundsWidth, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceVectorRow); + } + + if (this.preserveAlpha) + { + for (int x = 0; x < boundsWidth; x++) + { + int index = 0; + ref int sampleColumnBase = ref state.GetSampleColumn(x); + ref Vector4 target = ref Unsafe.Add(ref targetBase, (uint)x); + for (int kY = 0; kY < state.Kernel.Rows; kY++) + { + Span sourceRow = sourceVectorBuffer[(kY * boundsWidth)..]; + ref Vector4 sourceRowBase = ref MemoryMarshal.GetReference(sourceRow); + for (int kX = 0; kX < state.Kernel.Columns; kX++) + { + int currentXIndex = Unsafe.Add(ref sampleColumnBase, (uint)kX) - boundsX; + Vector4 pixel = Unsafe.Add(ref sourceRowBase, (uint)currentXIndex); + state.Kernel.SetValue(index, pixel); + index++; + } + } + + target = FindMedian3(state.Kernel.Span, xChannel, yChannel, zChannel); + } + } + else + { + Span wChannel = channelBuffer.Slice(this.wChannelStart, kernelCount); + for (int x = 0; x < boundsWidth; x++) + { + int index = 0; + ref int sampleColumnBase = ref state.GetSampleColumn(x); + ref Vector4 target = ref Unsafe.Add(ref targetBase, (uint)x); + for (int kY = 0; kY < state.Kernel.Rows; kY++) + { + Span sourceRow = sourceVectorBuffer[(kY * boundsWidth)..]; + ref Vector4 sourceRowBase = ref MemoryMarshal.GetReference(sourceRow); + for (int kX = 0; kX < state.Kernel.Columns; kX++) + { + int currentXIndex = Unsafe.Add(ref sampleColumnBase, (uint)kX) - boundsX; + Vector4 pixel = Unsafe.Add(ref sourceRowBase, (uint)currentXIndex); + state.Kernel.SetValue(index, pixel); + index++; + } + } + + target = FindMedian4(state.Kernel.Span, xChannel, yChannel, zChannel, wChannel); + } + } + + Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRowSpan); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector4 FindMedian3(ReadOnlySpan kernelSpan, Span xChannel, Span yChannel, Span zChannel) + { + int halfLength = (kernelSpan.Length + 1) >> 1; + + // Split color channels + for (int i = 0; i < xChannel.Length; i++) + { + xChannel[i] = kernelSpan[i].X; + yChannel[i] = kernelSpan[i].Y; + zChannel[i] = kernelSpan[i].Z; + } + + // Sort each channel serarately. + xChannel.Sort(); + yChannel.Sort(); + zChannel.Sort(); + + // Taking the W value from the source pixels, where the middle index in the kernelSpan is by definition the resulting pixel. + // This will preserve the alpha value. + return new Vector4(xChannel[halfLength], yChannel[halfLength], zChannel[halfLength], kernelSpan[halfLength].W); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector4 FindMedian4(ReadOnlySpan kernelSpan, Span xChannel, Span yChannel, Span zChannel, Span wChannel) + { + int halfLength = (kernelSpan.Length + 1) >> 1; + + // Split color channels + for (int i = 0; i < xChannel.Length; i++) + { + xChannel[i] = kernelSpan[i].X; + yChannel[i] = kernelSpan[i].Y; + zChannel[i] = kernelSpan[i].Z; + wChannel[i] = kernelSpan[i].W; + } + + // Sort each channel serarately. + xChannel.Sort(); + yChannel.Sort(); + zChannel.Sort(); + wChannel.Sort(); + + return new Vector4(xChannel[halfLength], yChannel[halfLength], zChannel[halfLength], wChannel[halfLength]); + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelData.cs b/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelData.cs new file mode 100644 index 0000000..964563e --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelData.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters { + /// + /// A that contains data about a set of bokeh blur kernels + /// + internal readonly struct BokehBlurKernelData + { + /// + /// The kernel parameters to use for the current set of complex kernels + /// + public readonly Vector4[] Parameters; + + /// + /// The kernel components to apply the bokeh blur effect + /// + public readonly Complex64[][] Kernels; + + /// + /// Initializes a new instance of the struct. + /// + /// The kernel parameters + /// The complex kernel components + public BokehBlurKernelData(Vector4[] parameters, Complex64[][] kernels) + { + this.Parameters = parameters; + this.Kernels = kernels; + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs b/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs new file mode 100644 index 0000000..6e69fa5 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs @@ -0,0 +1,222 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Concurrent; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters { + /// + /// Provides parameters to be used in the . + /// + internal static class BokehBlurKernelDataProvider + { + /// + /// The mapping of initialized complex kernels and parameters, to speed up the initialization of new instances + /// + private static readonly ConcurrentDictionary Cache = new(); + + /// + /// Gets the kernel scales to adjust the component values in each kernel + /// + private static float[] KernelScales { get; } = [1.4f, 1.2f, 1.2f, 1.2f, 1.2f, 1.2f]; + + /// + /// Gets the available bokeh blur kernel parameters + /// + private static Vector4[][] KernelComponents { get; } = + [ + + // 1 component + [new Vector4(0.862325f, 1.624835f, 0.767583f, 1.862321f)], + + // 2 components + [ + new Vector4(0.886528f, 5.268909f, 0.411259f, -0.548794f), + new Vector4(1.960518f, 1.558213f, 0.513282f, 4.56111f) + ], + + // 3 components + [ + new Vector4(2.17649f, 5.043495f, 1.621035f, -2.105439f), + new Vector4(1.019306f, 9.027613f, -0.28086f, -0.162882f), + new Vector4(2.81511f, 1.597273f, -0.366471f, 10.300301f) + ], + + // 4 components + [ + new Vector4(4.338459f, 1.553635f, -5.767909f, 46.164397f), + new Vector4(3.839993f, 4.693183f, 9.795391f, -15.227561f), + new Vector4(2.791880f, 8.178137f, -3.048324f, 0.302959f), + new Vector4(1.342190f, 12.328289f, 0.010001f, 0.244650f) + ], + + // 5 components + [ + new Vector4(4.892608f, 1.685979f, -22.356787f, 85.91246f), + new Vector4(4.71187f, 4.998496f, 35.918936f, -28.875618f), + new Vector4(4.052795f, 8.244168f, -13.212253f, -1.578428f), + new Vector4(2.929212f, 11.900859f, 0.507991f, 1.816328f), + new Vector4(1.512961f, 16.116382f, 0.138051f, -0.01f) + ], + + // 6 components + [ + new Vector4(5.143778f, 2.079813f, -82.326596f, 111.231024f), + new Vector4(5.612426f, 6.153387f, 113.878661f, 58.004879f), + new Vector4(5.982921f, 9.802895f, 39.479083f, -162.028887f), + new Vector4(6.505167f, 11.059237f, -71.286026f, 95.027069f), + new Vector4(3.869579f, 14.81052f, 1.405746f, -3.704914f), + new Vector4(2.201904f, 19.032909f, -0.152784f, -0.107988f) + ] + ]; + + /// + /// Gets the bokeh blur kernel data for the specified parameters. + /// + /// The value representing the size of the area to sample. + /// The size of each kernel to compute. + /// The number of components to use to approximate the original 2D bokeh blur convolution kernel. + /// A instance with the kernel data for the current parameters. + public static BokehBlurKernelData GetBokehBlurKernelData( + int radius, + int kernelSize, + int componentsCount) + { + // Reuse the initialized values from the cache, if possible + BokehBlurParameters parameters = new(radius, componentsCount); + if (!Cache.TryGetValue(parameters, out BokehBlurKernelData info)) + { + // Initialize the complex kernels and parameters with the current arguments + (Vector4[] kernelParameters, float kernelsScale) = GetParameters(componentsCount); + Complex64[][] kernels = CreateComplexKernels(kernelParameters, radius, kernelSize, kernelsScale); + NormalizeKernels(kernels, kernelParameters); + + // Store them in the cache for future use + info = new BokehBlurKernelData(kernelParameters, kernels); + Cache.TryAdd(parameters, info); + } + + return info; + } + + /// + /// Gets the kernel parameters and scaling factor for the current count value in the current instance + /// + private static (Vector4[] Parameters, float Scale) GetParameters(int componentsCount) + { + // Prepare the kernel components + int index = Math.Max(0, Math.Min(componentsCount - 1, KernelComponents.Length)); + + return (KernelComponents[index], KernelScales[index]); + } + + /// + /// Creates the collection of complex 1D kernels with the specified parameters + /// + /// The parameters to use to normalize the kernels + /// The value representing the size of the area to sample. + /// The size of each kernel to compute. + /// The scale factor for each kernel. + private static Complex64[][] CreateComplexKernels( + Vector4[] kernelParameters, + int radius, + int kernelSize, + float kernelsScale) + { + Complex64[][] kernels = new Complex64[kernelParameters.Length][]; + ref Vector4 baseRef = ref MemoryMarshal.GetReference(kernelParameters.AsSpan()); + for (int i = 0; i < kernelParameters.Length; i++) + { + ref Vector4 paramsRef = ref Unsafe.Add(ref baseRef, (uint)i); + kernels[i] = CreateComplex1DKernel(radius, kernelSize, kernelsScale, paramsRef.X, paramsRef.Y); + } + + return kernels; + } + + /// + /// Creates a complex 1D kernel with the specified parameters + /// + /// The value representing the size of the area to sample. + /// The size of each kernel to compute. + /// The scale factor for each kernel. + /// The exponential parameter for each complex component + /// The angle component for each complex component + private static Complex64[] CreateComplex1DKernel( + int radius, + int kernelSize, + float kernelsScale, + float a, + float b) + { + Complex64[] kernel = new Complex64[kernelSize]; + ref Complex64 baseRef = ref MemoryMarshal.GetReference(kernel.AsSpan()); + int r = radius, n = -r; + + for (int i = 0; i < kernelSize; i++, n++) + { + // Incrementally compute the range values + float value = n * kernelsScale * (1f / r); + value *= value; + + // Fill in the complex kernel values + Unsafe.Add(ref baseRef, (uint)i) = new Complex64( + MathF.Exp(-a * value) * MathF.Cos(b * value), + MathF.Exp(-a * value) * MathF.Sin(b * value)); + } + + return kernel; + } + + /// + /// Normalizes the kernels with respect to A * real + B * imaginary + /// + /// The current convolution kernels to normalize + /// The parameters to use to normalize the kernels + private static void NormalizeKernels(Complex64[][] kernels, Vector4[] kernelParameters) + { + // Calculate the complex weighted sum + float total = 0; + Span kernelsSpan = kernels.AsSpan(); + ref Complex64[] baseKernelsRef = ref MemoryMarshal.GetReference(kernelsSpan); + ref Vector4 baseParamsRef = ref MemoryMarshal.GetReference(kernelParameters.AsSpan()); + + for (int i = 0; i < kernelParameters.Length; i++) + { + ref Complex64[] kernelRef = ref Unsafe.Add(ref baseKernelsRef, (uint)i); + int length = kernelRef.Length; + ref Complex64 valueRef = ref MemoryMarshal.GetArrayDataReference(kernelRef); + ref Vector4 paramsRef = ref Unsafe.Add(ref baseParamsRef, (uint)i); + + for (int j = 0; j < length; j++) + { + for (int k = 0; k < length; k++) + { + ref Complex64 jRef = ref Unsafe.Add(ref valueRef, (uint)j); + ref Complex64 kRef = ref Unsafe.Add(ref valueRef, (uint)k); + total += + (paramsRef.Z * ((jRef.Real * kRef.Real) - (jRef.Imaginary * kRef.Imaginary))) + + (paramsRef.W * ((jRef.Real * kRef.Imaginary) + (jRef.Imaginary * kRef.Real))); + } + } + } + + // Normalize the kernels + float scalar = 1f / MathF.Sqrt(total); + for (int i = 0; i < kernelsSpan.Length; i++) + { + ref Complex64[] kernelsRef = ref Unsafe.Add(ref baseKernelsRef, (uint)i); + int length = kernelsRef.Length; + ref Complex64 valueRef = ref MemoryMarshal.GetArrayDataReference(kernelsRef); + + for (int j = 0; j < length; j++) + { + Unsafe.Add(ref valueRef, (uint)j) *= scalar; + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurParameters.cs b/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurParameters.cs new file mode 100644 index 0000000..de305bf --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurParameters.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters { + /// + /// A that contains parameters to apply a bokeh blur filter + /// + internal readonly struct BokehBlurParameters : IEquatable + { + /// + /// The size of the convolution kernel to use when applying the bokeh blur + /// + public readonly int Radius; + + /// + /// The number of complex components to use to approximate the bokeh kernel + /// + public readonly int Components; + + /// + /// Initializes a new instance of the struct. + /// + /// The size of the kernel + /// The number of kernel components + public BokehBlurParameters(int radius, int components) + { + this.Radius = radius; + this.Components = components; + } + + /// + public bool Equals(BokehBlurParameters other) + { + return this.Radius.Equals(other.Radius) && this.Components.Equals(other.Components); + } + + /// + public override bool Equals(object? obj) => obj is BokehBlurParameters other && this.Equals(other); + + /// + public override int GetHashCode() + { + unchecked + { + return (this.Radius.GetHashCode() * 397) ^ this.Components.GetHashCode(); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Convolution/ReadOnlyKernel.cs b/ImageSharp/Processing/Processors/Convolution/ReadOnlyKernel.cs new file mode 100644 index 0000000..c32abc1 --- /dev/null +++ b/ImageSharp/Processing/Processors/Convolution/ReadOnlyKernel.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution { + /// + /// A stack only, readonly, kernel matrix that can be indexed without + /// bounds checks when compiled in release mode. + /// + internal readonly ref struct ReadOnlyKernel + { + private readonly ReadOnlySpan values; + + public ReadOnlyKernel(DenseMatrix matrix) + { + this.Columns = (uint)matrix.Columns; + this.Rows = (uint)matrix.Rows; + this.values = matrix.Span; + } + + public uint Columns + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + public uint Rows + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + public float this[uint row, uint column] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + this.CheckCoordinates(row, column); + ref float vBase = ref MemoryMarshal.GetReference(this.values); + return Unsafe.Add(ref vBase, (row * this.Columns) + column); + } + } + + [Conditional("DEBUG")] + private void CheckCoordinates(uint row, uint column) + { + if (row >= this.Rows) + { + throw new ArgumentOutOfRangeException(nameof(row), row, $"{row} is outwith the matrix bounds."); + } + + if (column >= this.Columns) + { + throw new ArgumentOutOfRangeException(nameof(column), column, $"{column} is outwith the matrix bounds."); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Dithering/DHALF.TXT b/ImageSharp/Processing/Processors/Dithering/DHALF.TXT new file mode 100644 index 0000000..ec6ba3d --- /dev/null +++ b/ImageSharp/Processing/Processors/Dithering/DHALF.TXT @@ -0,0 +1,1331 @@ +DHALF.TXT +June 20, 1991 + +Original name: DITHER.TXT +Original date: January 2, 1989 + + +===================================== +ORIGINAL FOREWORD BY LEE CROCKER + +What follows is everything you ever wanted to know (for the time being) +about digital halftoning, or dithering. I'm sure it will be out of date as +soon as it is released, but it does serve to collect data from a wide +variety of sources into a single document, and should save you considerable +searching time. + +Numbers in brackets (e.g. [4] or [12]) are references. A list of these +works appears at the end of this document. + +Because this document describes ideas and algorithms which are constantly +changing, I expect that it may have many editions, additions, and +corrections before it gets to you. I will list my name below as original +author, but I do not wish to deter others from adding their own thoughts and +discoveries. This is not copyrighted in any way, and was created solely +for the purpose of organizing my own knowledge on the subject, and sharing +this with others. Please distribute it to anyone who might be interested. + +If you add anything to this document, please feel free to include your name +below as a contributor or as a reference. I would particularly like to see +additions to the "Other books of interest" section. Please keep the text in +this simple format: no margins, no pagination, no lines longer than 79 +characters, and no non-ASCII or non-printing characters other than a CR/LF +pair at the end of each line. It is intended that this be read on as many +different machines as possible. + + +Original Author: + + Lee Daniel Crocker [73407,2030] + + +Contributors: + + Paul Boulay [72117,446] + + Mike Morra [76703,4051] + + +===================================== +COMMENTS BY MIKE MORRA + +I first entered the world of imaging in the fall of 1990 when my employer, +Epson America Inc., began shipping the ES-300C color flatbed scanner. +Suddenly, here I was, a field systems analyst who had worked almost +exclusively with printers and PCs, thrust into a new and arcane world of +look-up tables and dithering and color reduction and .GIF files! I realized +right away that I had a lot of catching up to do (and it needed to be done +quickly), so I began to frequent the CompuServe Information Service's +Graphics Support Forum on a very regular basis. + +Lee Crocker's excellent paper called DITHER.TXT was one of the first pieces +of information that I came across, and it went a very long way toward +answering a lot of questions that I'd had about the subject of dithering. +It also provided me with the names of other essential reference works upon +which Lee had based his paper, and I immediately began an eager search for +these other references. + +In the course of my self-study, however, I found that DITHER.TXT does +presume the reader's familiarity with some fundamental imaging concepts, +which meant that I needed to do a little "cramming." I get the impression +that Lee was directing his paper more toward graphics programmers than to +complete neophytes like me. I decided that I would rewrite and append to +DITHER.TXT and try to incorporate some of the more elementary information +that I'd absorbed along the way. In doing so, I hope that it will make it +even more comprehensive, and thus even more useful to first-time users. + +I elected to rename the revised file and chose the name DHALF.TXT in homage +to the term "digital halftoning," as used in Robert Ulichney's splendid +reference work. Notwithstanding, this paper is still very much Lee's +original work, and I certainly do not propose that I have created something +new and original here. It is also quite possible that in changing the +presentation of some of the material therein, I may have unwittingly +corrupted Lee's original intent and delivery, and this was also not my +intention. + +Accordingly, I've submitted this paper to the Graphics Support Forum as a +draft work only, at least for the time being. Quite honestly, I don't know +whether it would be appropriate as a replacement to DITHER.TXT, or as a +second, distinct document. Too, I may very well have misconstrued or +misinterpreted some factual information in my revision. As such, I welcome +criticism and comment from all the original authors and contributors, and +any readers, with the hope that their feedback will help me to address these +issues. + +If this revision it is received favorably, I will submit it to the public +domain; if it is met with brickbats (for whatever reason), I will withdraw +it. Whatever the outcome, though, it will at least represent a very +rewarding learning experience on my part! + +With the unselfish help of many of the denizens of the Graphics Support +Forum, I was ultimately able to thrash out (in my own mind) the answers to +my questions that I needed. I'd like to publicly thank the whole Forum +community in general for putting up with my unending barrage of questions +and inquiries over the past few months . In particular, I would thank +John Swenson, Chris Young, and (of course) Lee Crocker for their invaluable +assistance. + +Mike Morra [76703,4051] +June 20, 1991 + + +===================================== +What is Digital Halftoning? + +Throughout much of the course of computer imaging technology, experimenters +and users have been challenged with attempting to acceptably render +digitized images on display devices which were incapable of reproducing the +full spectrum of intensities or colors present in the source image. The +challenge is even more pronounced in today's world of personal computing +because of the technology gap between image generation and image rendering +equipment. + +Today, we now have affordable 24-bit image scanners which can generate +nearly true-to-life scans having as many as 256 shades of gray, or in excess +of 16.7 million colors. Mainstream display technology, however, still lags +behind with 16- and 256-color VGA/SVGA video monitors and printers with +binary (black/white) "marking engines" as the norm. Without specialized +techniques for color reduction -- the process of finding the "best fit" of +the display device's available gray shades and/or colors -- the imaging +experimenter would be plagued with blotchy, noisy, off-color images. + +(As of this writing, "true color" 24-bit video display devices, capable of +reproducing all of the color/intensity information in the source image, are +now beginning to migrate downward into the PC environment, but they exact a +premium in cost and processor power which many users are loathe to pay. So- +called "high-color" video displays -- typically 16-bit, with 32,768-color +capability -- are moving into the mainstream, but color reduction techniques +would still be required with these devices.) + +The science of digital halftoning (more commonly referred to as dithering, +or spatial dithering) is one of the techniques used to achieve satisfactory +image rendering and color reduction. Initially, it was principally +associated with the rendering of continuous-tone (grayscale) images on +"binary" (i.e. 1-bit) video displays which could only display full black or +full white pixels, or on printers which could produce only full black spots +on a printed page. Indeed, Ulichney [3] gives a definition of digital +halftoning as "... any algorithmic process which creates the illusion of +continuous-tone images from the judicious arrangement of binary picture +elements." + +Ulichney's study, as well as the earlier literature on the subject (and this +paper itself), discusses the process mostly in this context. Since we in +the PC world are still saddled primarily with black/white marking engines in +our hardcopy devices, this binary interpretation of digital halftoning is +still very pertinent. However, as we will see later in this discussion, the +concept can also be extended to include display devices (typically video +monitors) which support limited grayscale or color palettes. Accordingly, +we can broaden the traditional definition of digital halftoning to refer to +rendering an image on any display device which is unable to show the entire +range of colors or gray shades that are contained in the source image. + + +===================================== +Intensity/Color Resolution + +The concept of resolution is essential to the understanding of digital +halftoning. Resolution can be defined as "fineness" and is used to +describe the level of detail in a digitally sampled signal. + +Typically, when we hear the term "resolution" applied to images, we think of +what's known as "spatial resolution," which is the basic sampling rate for +the image. It describes the fineness of the "dots" (pixels or ink/toner +spots) which comprise the image, i.e. how many of them are present along +each horizontal and vertical inch. However, we can also speak of "intensity +resolution" or "color resolution," which describes the fineness of detail +available at each spot, i.e. the number of different gray shades or colors +in the image. (I will go back and forth between the two terms depending on +the type of image being discussed, but the reader should be aware that the +concepts are analogous to each other.) + +As you might expect, the higher the resolution of a digital sample, the +better it can reproduce high frequency detail in the particular domain +described by that resolution. A VGA display, for example, has a relatively +good spatial resolution of 640 x 480 and a relatively poor color resolution +of 8 bits (256 colors). By comparison, an NTSC color television receiver +has a spatial resolution of approximately 350 x 525 and an excellent, nearly +infinite color resolution. Thus, images rendered on a VGA screen will be +quite sharp, but rather blotchy in color. The same image displayed on the +television receiver will not be as crisp, but will have much more accurate +color rendition. + +It is often possible to "trade" one kind of resolution for another. If your +display device has a higher spatial resolution than the image you are trying +to reproduce, it can show a very good image even if its color resolution is +less. This is what most of us know as "dithering" and is the subject of +this paper. (The other tradeoff, i.e., trading color resolution for spatial +resolution, is called "anti-aliasing," and is not discussed here.) + + +For the following discussions I will assume that we are given a grayscale +image with 256 shades of gray, which are assigned intensity values from 0 +(black) through 255 (white), and that we are trying to reproduce it on a +black and white output device, e.g. something like an Epson impact dotmatrix +printer, or an HP LaserJet laser printer. Most of these methods can be +extended in obvious ways to deal with displays that have more than two +levels (but still fewer than the source image), or to color images. Where +such extension is not obvious, or where better results can be obtained, I +will go into more detail. + + +===================================== +Fixed Thresholding + +A good place to start is with the example of performing a simple (or fixed) +thresholding operation on our grayscale image in order to display it on our +black and white device. This is accomplished by establishing a demarcation +point, or threshold, at the 50% gray level. Each dot of the source image is +compared against this threshold value: if it is darker than the value, the +device plots it black, and if it's lighter, the device plots it white. + +What happens to the image during this operation? Well, some detail +survives, but our perception of gray levels is completely gone. This means +that a lot of the image content is obliterated. Take an area of the image +which is made up of various gray shades in the range of 60-90%. After fixed +thresholding, all of those shades (being darker than the 50% gray threshold) +will be mapped to solid black. So much for variations of intensity. + +Another portion of the image might show an object with an increasing, +diffused shadow across one of its surfaces, with gray shades in the range of +20-70%. This gradual variation in intensity will be lost in fixed +thresholding, giving way to two separate areas (one white, one black) and a +distinct, visible boundary between them. The situation where a transition +from one intensity or shade to another is very conspicuous is known as +contouring. + + +===================================== +Artifacts + +Phenomena like contouring, which are not present in the source image but +produced by the digital signal processing, are called artifacts. The most +common type of artifact is the Moire' pattern. If you display or print an +image of several lines, closely spaced and radiating from a single point, +you will see what appear to be flower-like patterns. These are not part of +the original image but are an illusion produced by the jaggedness of the +display. We will encounter and discuss other forms of artifacts later in +this paper. + + +===================================== +Error Noise + +Returning to our fixed-thresholded (and badly-rendered) image, how could we +document what has taken place to make this image so inaccurate? Expressing +it in technical terms, a relatively large amount of error "noise" is present +in the fixed-thresholded image. The error value is the difference between +the image's original intensity at a given dot and the intensity of the +displayed dot. Obviously, very dark values like 1 or 2 (which are almost +full black) incur very small errors when they are rendered as a 0 value +(black) dot. On the other hand, a gross error is incurred when a 129 value +dot (a medium gray) is displayed at 255 value (white), for instance. + +Simply put, digital halftoning redistributes this "noise energy" in a way +which makes it less visible. This brings up an important concept: digital +halftoning does not INCREASE the noise energy. In some of the literature, +reference is made to the "addition of dither noise," which might give this +impression. This is not the case, however: effective digital halftoning +acts upon the low-frequency component of the error noise (the component +which contributes to graininess) and scatters it in higher-frequency +components where it is not as obvious. + + +===================================== +Classes of digital halftoning algorithms + +The algorithms we will discuss in this paper can be subdivided into four +categories: + + 1. Random dither + 2. Patterning + 3. Ordered dither + 4. Error-diffusion halftoning + +Each of these methods is generally better than those listed before it, but +other considerations such as processing time, memory constraints, etc. may +weigh in favor of one of the simpler methods. + +To convert any of the first three methods into color, simply apply the +algorithm separately for each primary color and mix the resulting values. +This assumes that you have at least eight output colors: black, red, green, +blue, cyan, magenta, yellow, and white. Though this will work for error +diffusion as well, there are better methods which will be discussed in more +detail later. + + +===================================== +Random dither + +Random dithering could be termed the "bubblesort" of digital halftoning +algorithms. It was the first attempt (documented as far back as 1951) to +correct the contouring produced by fixed thresholding, and it has +traditionally been referenced for comparison in most studies of digital +halftoning. In fact, the name "ordered dither" (which will be discussed +later) was chosen to contrast random dither. + +While it is not really acceptable as a production method, it is very simple +to describe and implement. For each dot in our grayscale image, we generate +a random number in the range 0 - 255: if the random number is greater than +the image value at that dot, the display device plots the dot white; +otherwise, it plots it black. That's it. + +This generates a picture with a lot of "white noise", which looks like TV +picture "snow". Although inaccurate and grainy, the image is free from +artifacts. Interestingly enough, this digital halftoning method is useful +in reproducing very low-frequency images, where the absence of artifacts is +more important than noise. For example, a whole screen containing a +gradient of all levels from black to white would actually look best with a +random dither. With this image, other digital halftoning algorithms would +produce significant artifacts like diagonal patterns (in ordered dithering) +and clustering (in error diffusion halftones). + +I should mention, of course, that unless your computer has a hardware-based +random number generator (and most don't), there may be some artifacts from +the random number generation algorithm itself. For efficiency, you can take +the random number generator "out of the loop" by generating a list of random +numbers beforehand for use in the dither. Make sure that the list is larger +than the number of dots in the image or you may get artifacts from the reuse +of numbers. The worst case would be if the size of your list of random +numbers is a multiple or near-multiple of the horizontal size of the image; +in this case, unwanted vertical or diagonal lines will appear. + +As unattractive as it is, random dithering can actually be related to a +pleasing, centuries-old art know as mezzotinting (the name itself is an +Italianized derivative of the English "halftone"). In a mezzotint, the +skilled craftsman worked a soft metal (usually copper) printing plate, and +roughened or ground the dark regions of the image by hand and in a seemingly +random fashion. Analyzing it in scientific terms (which would surely insult +any mezzotinting artisan who might read this!) the pattern created is not +very regular or periodic at all, but the absence of low frequency noise +leads to a very attractive image without much graininess. A similar process +is still in use today, in the form of modern gravure printing. + + +===================================== +"Classical" halftoning + +Let's take a short departure from the digital domain and look at the +traditional or "classical" printing technique of halftoning. This technique +is over a century old, dating back to the weaving of silk pictures in the +mid 1800's. Modern halftone printing was invented in the late 1800's, and +halftones of that period are even today considered to be attractive +renditions of their subjects. + +Essentially, halftoning involves the printing of dots of different sizes in +an ordered and closely spaced pattern in order to simulate various +intensities. The early halftoning artisans realized that when we view a +very small area at normal viewing distances, our eyes perform a blending or +smoothing function on the fine detail within that area. As a result, we +perceive only the overall intensity of the area. This is known as spatial +integration. + +Although the tools of halftoning (the "screens" and screening process used +to generate the varying dots of the printed image) have undergone +improvements throughout the years, the fundamental principles remain +unchanged. This includes the 45-degree "screen angle" of the lines of dots, +which was known even to the earliest halftone artisans as giving more +pleasing images than dot lines running horizontally and vertically. + + +===================================== +Patterning + +This was the first digital technique to pay homage to the classical +halftone. It takes advantage of the fact that the spatial resolution of +display devices had improved to the point where one could trade some of it +for better intensity resolution. Like random dither, it is also a simple +concept, but is much more effective. + +For each possible value in the image, we create and display a pattern of +pixels (which can be either video pixels or printer "spots") that +approximates that value. Remembering the concept of spatial integration, if +we choose the appropriate patterns we can simulate the appearance of various +intensity levels -- even though our display can only generate a limited set +of intensities. + +For example, consider a 3 x 3 pattern. It can have one of 512 different +arrangements of pixels: however, in terms of intensity, not all of them are +unique. Since the number of black pixels in the pattern determines the +darkness of the pattern, we really have only 10 discrete intensity patterns +(including the all-white pattern), each one having one more black pixel than +the previous one. + +But which 10 patterns? Well, we can eliminate, right off the bat, patterns +like: + + --- X-- --X X-- + XXX or -X- or -X- or X-- + --- --X X-- X-- + + +because if they were repeated over a large area (a common occurrence in many +images [1]) they would create vertical, horizontal, or diagonal lines. +Also, studies [1] have shown that the patterns should form a "growth +sequence:" once a pixel is intensified for a particular value, it should +remain intensified for all subsequent values. In this fashion, each pattern +is a superset of the previous one; this similarity between adjacent +intensity patterns minimizes any contouring artifacts. + +Here is a good pattern for a 3-by-3 matrix which subscribes to the rules set +forth above: + + + --- --- --- -X- -XX -XX -XX -XX XXX XXX + --- -X- -XX -XX -XX -XX XXX XXX XXX XXX + --- --- --- --- --- -X- -X- XX- XX- XXX + + +This pattern matrix effectively simulates a screened halftone with dots of +various sizes. In large areas of constant value, the repetitive pattern +formed will be mostly artifact-free. + +No doubt, the reader will realize that applying this patterning process to +our image will triple its size in each direction. Because of this, +patterning can only be used where the display's spatial resolution is much +greater than that of the image. + +Another limitation of patterning is that the effective spatial resolution is +decreased, since a multiple-pixel "cell" is used to simulate the single, +larger halftone dot. The more intensity resolution we want, the larger the +halftone cell used and, by extension, the lower the spatial resolution. + +In the above example, using 3 x 3 patterning, we are able to simulate 10 +intensity levels (not a very good rendering) but we must reduce the spatial +resolution to 1/3 of the original figure. To get 64 intensity levels (a +very acceptable rendering), we would have to go to an 8 x 8 pattern and an +eight-fold decrease in spatial resolution. And to get the full 256 levels +of intensity in our source image, we would need a 16 x 16 pattern and would +incur a 16-fold reduction in spatial resolution. Because of this size +distortion of the image, and with the development of more effective digital +halftoning methods, patterning is only infrequently used today. + +To extend this method to color images, we would use patterns of colored +pixels to represent shades not directly printable by the hardware. For +example, if your hardware is capable of printing only red, green, blue, and +black (the minimal case for color dithering), other colors can be +represented with 2 x 2 patterns of these four: + + + Yellow = R G Cyan = G B Magenta = R B Gray = R G + G R B G B R B K + + +(B here represents blue, K is black). In this particular example, there are +a total of 31 such distinct patterns which can be used; their enumeration is +left "as an exercise for the reader" (don't you hate books that do that?). + + +===================================== +Clustered vs. dispersed patterns + +The pattern diagrammed above is called a "clustered" pattern, so called +because as new pixels are intensified in each pattern, they are placed +adjacent to the already-intensified pixels. Clustered-dot patterns were +used on many of the early display devices which could not render individual +pixels very distinctly, e.g. printing presses or other printers which smear +the printed spots slightly (a condition known as dot gain), or video +monitors which introduce some blurriness to the pixels. Clustered-dot +groupings tend to hide the effect of dot gain, but also produce a somewhat +grainy image. + +As video and hardcopy display technology improved, newer devices (such as +electrophotographic laser printers and high-res video displays) were better +able to accurately place and size their pixels. Further research showed +that, especially with larger patterns, the dispersed (non-clustered) layout +was more pleasing. Here is one such pattern: + + + --- X-- X-- X-- X-X X-X X-X XXX XXX XXX + --- --- --- --X --X X-X X-X X-X XXX XXX + --- --- -X- -X- -X- -X- XX- XX- XX- XXX + + + +Since clustering is not used, dispersed-dot patterns produce less grainy +images. + + +===================================== +Ordered dither + +While patterning was an important step toward the digital reproduction of +the classic halftone, its main shortcoming was the spatial enlargement (and +corresponding reduction in resolution) of the image. Ordered dither +represents a major improvement in digital halftoning where this spatial +distortion was eliminated and the image could then be rendered in its +original size. + +Obviously, in order to accomplish this, each dot in the source image must be +mapped to a pixel on the display device on a one-to-one basis. Accordingly, +the patterning concept was redefined so that instead of plotting the whole +pattern for each image dot, THE IMAGE DOT IS MAPPED ONLY TO ONE PIXEL IN THE +PATTERN. Returning to our example of a 3 x 3 pattern, this means that we +would be mapping NINE image dots into this pattern. + +The simplest way to do this in programming is to map the X and Y coordinates +of each image dot into the pixel (X mod 3, Y mod 3) in the pattern. + +Returning to our two patterns (clustered and dispersed) as defined earlier, +we can derive an effective mathematical algorithm that can be used to plot +the correct pixel patterns. Because each of the patterns above is a +superset of the previous, we can express the patterns in a compact array +form as the order of pixels added: + + + 8 3 4 1 7 4 + 6 1 2 and 5 8 3 + 7 5 9 6 2 9 + + +Then we can simply use the value in the array as a threshold. If the value +of the original image dot (scaled into the 0-9 range) is less than the +number in the corresponding cell of the matrix, we plot that pixel black; +otherwise, we plot it white. Note that in large areas of constant value, we +will get repetitions of the pattern just as we did with patterning. + +As before, clustered patterns should be used for those display devices which +blur the pixels. In fact, the clustered-dot ordered dither is the process +used by most newspapers, and in the computer imaging world the term +"halftoning" has come to refer to this method if not otherwise qualified. + + +As noted earlier, the dispersed-dot method (where the display hardware +allows) is preferred in order to decrease the graininess of the displayed +images. Bayer [2] has shown that for matrices of orders which are powers of +two there is an optimal pattern of dispersed dots which results in the +pattern noise being as high-frequency as possible. The pattern for a 2x2 +and 4x4 matrices are as follows: + + +1 3 1 9 3 11 These patterns (and their rotations +4 2 13 5 15 7 and reflections) are optimal for a + 4 12 2 10 dispersed-dot ordered dither. + 16 8 14 6 + + +Ulichney [3] shows a recursive technique can be used to generate the larger +patterns. (To fully reproduce our 256-level image, we would need to use an +8x8 pattern.) + +The Bayer ordered dither is in very common use and is easily identified by +the cross-hatch pattern artifacts it produces in the resulting display. +This artifacting is the major drawback of an otherwise powerful and very +fast technique. + + +===================================== +Dithering with "blue noise" + +Up to this point in our discussion, we have (with the exception of dithering +with white noise) discussed digital halftoning schemes which rely on the +application of some fairly regular mathematical processes in order to +redistribute the error noise of the image. Unfortunately, the regularity of +these algorithms leads to different kinds of artifacting which detracts from +the rendered image. In addition, these images all tend to reflect the +display device's row-and-column dot pattern to some extent, and this further +contributes to the "mechanical" character of the output image. + +Dithering with white noise, on the other hand, introduces enough randomness +to suppress the artifacting and the gridlike appearance, but the low- +frequency component of this noise introduces graininess. + +Obviously, what is needed is a method which falls somewhere in the middle of +these two extremes. In theoretical terms, if we could take white noise and +remove its low-frequency content, this would be an ideal way to disperse the +error content of our image. Many of the digital halftoning developers, +making an analogy to the audio world, refer to this concept as dithering +with blue noise. (In audio theory, "pink noise," which is often used as a +diagnostic and testing tool, is white noise from which some level of high- +frequency content has been filtered.) + +Alas, while an audio-frequency analog low-pass filter is a relatively simple +device to construct and operate, implementing a digital high-pass filter in +program code -- and one which operates efficiently enough so as not to +degrade display response time -- is no trivial task. + + +===================================== +Error-diffusion halftoning + +After considerable research, it was found that a set of techniques known as +error diffusion (also termed error dispersion or error distribution) +accomplished this quite effectively. In fact, error diffusion generates the +best results of any of the digital halftoning methods described here. Much +of the low-frequency noise component is suppressed, producing images with +very little grain. Error-diffusion halftones also display a very pleasing +randomness, without the visual sensation of rows and columns of dots; this +effect is known as the "grid defiance illusion." + +As in other areas of life, though, there ain't no such thing as a free +lunch. Error diffusion is, by nature, the slowest method of digital +halftoning. In fact, there are several variants of this technique, and the +better they get, the slower they are. However, one will realize a very +significant improvement in the quality of the processed images which easily +justifies the time and computational power required. + +Error diffusion is very simple to describe. For each point in our image, we +first find the closest intensity (or color) available. We then calculate +the difference between the image value at that point and that nearest +available intensity/color: this difference is our error value. Now we +divide up the error value and distribute it to some of the neighboring image +areas which we have not visited (or processed) yet. When we get to these +later dots, we add in the portions of error values which were distributed +there from the preceding dots, and clip the cumulative value to an allowed +range if needed. This new, modified value now becomes the image value that +we use for processing this point. + +If we are dithering our sample grayscale image for output to a black-and- +white device, the "find closest intensity/color" operation is just a simple +thresholding (the closest intensity is going to be either black or white). +In color imaging -- for instance, color-reducing a 24-bit true color Targa +file to an 8-bit, mapped GIF file -- this involves matching the input color +to the closest available hardware color. Depending on how the display +hardware manages its intensity/color palette, this matching process can be a +difficult task. (This is covered in more detail in the "Color issues" +section later in this paper.) + +Up till now, all other methods of digital halftoning were point operations, +where any adjustments that were made to a given dot had no effect on any of +the surrounding dots. With error diffusion, we are doing a "neighborhood +operation." Dispersing the error value over a larger area is the key to the +success of these methods. + +The different ways of dividing up the error can be expressed as patterns +called filters. In the following sections, I will list a number of the most +commonly-used filters and some info on each. + + +===================================== +The Floyd-Steinberg filter + +This is where it all began, with Floyd and Steinberg's [4] pioneering +research in 1975. The filter can be diagrammed thus: + + + * 7 + 3 5 1 (1/16) + + +In this (and all subsequent) filter diagrams, the "*" represents the pixel +currently being scanning, and the neighboring numbers (called weights) +represent the portion of the error distributed to the pixel in that +position. The expression in parentheses is the divisor used to break up the +error weights. In the Floyd-Steinberg filter, each pixel "communicates" +with 4 "neighbors." The pixel immediately to the right gets 7/16 of the +error value, the pixel directly below gets 5/16 of the error, and the +diagonally adjacent pixels get 3/16 and 1/16. + +The weighting shown is for the traditional left-to-right scanning of the +image. If the line were scanned right-to-left (more about this later), this +pattern would be reversed. In either case, the weights calculated for the +subsequent line must be held by the program, usually in an array of some +sort, until that line is visited later. + +Floyd and Steinberg carefully chose this filter so that it would produce a +checkerboard pattern in areas with intensity of 1/2 (or 128, in our sample +image). It is also fairly easy to execute in programming code, since the +division by 16 is accomplished by simple, fast bit-shifting instructions +(this is the case whenever the divisor is a power of 2). + + +===================================== +The "false" Floyd-Steinberg filter + +Occasionally, you will see the following filter erroneously called the +Floyd-Steinberg filter: + + + * 3 + 3 2 (1/8) + + +The output from this filter is nowhere near as good as that from the real +Floyd-Steinberg filter. There aren't enough weights to the dispersion, +which means that the error value isn't distributed finely enough. With the +entire image scanned left-to-right, the artifacting produced would be +totally unacceptable. + +Much better results would be obtained by using an alternating, or +serpentine, raster scan: processing the first line left-to-right, the next +line right-to-left, and so on (reversing the filter pattern appropriately). +Serpentine scanning -- which can be used with any of the error-diffusion +filters detailed here -- introduces an additional perturbation which +contributes more randomness to the resultant halftone. Even with serpentine +scanning, however, this filter would need additional perturbations (see +below) to give acceptable results. + + +===================================== +The Jarvis, Judice, and Ninke filter + +If the false Floyd-Steinberg filter fails because the error isn't +distributed well enough, then it follows that a filter with a wider +distribution would be better. This is exactly what Jarvis, Judice, and +Ninke [6] did in 1976 with their filter: + + + * 7 5 + 3 5 7 5 3 + 1 3 5 3 1 (1/48) + + +While producing nicer output than Floyd-Steinberg, this filter is much +slower to implement. With the divisor of 48, we can no longer use bit- +shifting to calculate the weights but must invoke actual DIV (divide) +processor instructions. This is further exacerbated by the fact that the +filter must communicate with 12 neighbors; three times as many in the Floyd- +Steinberg filter. Furthermore, with the errors distributed over three +lines, this means that the program must keep two forward error arrays, which +requires extra memory and time for processing. + + +===================================== +The Stucki filter + +P. Stucki [7] offered a rework of the Jarvis, Judice, and Ninke filter in +1981: + + + * 8 4 + 2 4 8 4 2 + 1 2 4 2 1 (1/42) + + +Once again, division by 42 is quite slow to calculate (requiring DIVs). +However, after the initial 8/42 is calculated, some time can be saved by +producing the remaining fractions by shifts. The Stucki filter has been +observed to give very clean, sharp output, which helps to offset the slow +processing time. + + +===================================== +The Burkes filter + +Daniel Burkes [5] of TerraVision undertook to improve upon the Stucki filter +in 1988: + + + * 8 4 The Burkes filter + 2 4 8 4 2 (1/32) + + +Notice that this is just a simplification of the Stucki filter with the +bottom row removed. The main improvement is that the divisor is now 32, +which allows the error values to be calculated using shifts once more, and +the number of neighbors communicated with has been reduced to seven. +Furthermore, the removal of one row reduces the memory requirements of the +filter by eliminating the second forward array which would otherwise be +needed. + + +===================================== +The Sierra filters + +In 1989, Frankie Sierra came out with his three-line filter: + + + * 5 3 The Sierra3 filter + 2 4 5 4 2 + 2 3 2 (1/32) + + +A year later, Sierra followed up with a two-line modification: + + + * 4 3 The Sierra2 filter + 1 2 3 2 1 (1/16) + + +and a very simple "Filter Lite," as he calls it: + + + * 2 The Sierra-2-4A filter + 1 1 (1/4) + + +Even this very simple filter, according to Sierra, produces better results +than the original Floyd-Steinberg filter. + + +===================================== +Miscellaneous filters + +Many image processing software packages offer one or more of the filters +listed above as dithering options. In nearly every case, the Floyd- +Steinberg filter (or a variant thereof) is included. The Bayer ordered +dither is sometimes offered, although the Floyd-Steinberg filter will do a +better job in essentially the same processing time. Higher-quality filters +like Burkes or Stucki are usually also present. + +All of the filters described above are used on display devices which have +"square pixels." This is to say that the display lays out the pixels in +rows and columns, aligned horizontally and vertically and spaced equally in +both directions. This applies to the commonly-used video modes in VGA and +SVGA: 640 x 480, 800 x 600, and 1024 x 768, with a 4:3 "aspect ratio." It +would also include HP-compatible and PostScript desktop laser printers using +300dpi marking engines. + +Some displays may use "rectangular pixels," where the horizontal and +vertical spacings are unequal. This would include various EGA and CGA video +modes and other specialized video displays, and most dot-matrix printers. +In many cases, the filters described earlier will do a decent job on +rectangular pixel grids, but an optimized filter would be preferred. +Slinkman [10] describes one such filter for his 640 x 240 monochrome display +with a 1:2 aspect ratio. + +In other cases, video displays might use a "hexagonal grid" of pixels, where +rows of pixels are offset or staggered, in much the same fashion used on +broadcast television receivers. This is illustrated below: + + + . . . . . . . . . . . . . . . . . . . . . + . . . . . . . . . . . . . . . . . . . . + . . . . . . . . . . . . . . . . . . . . . + . . . . . . . . . . . . . . . . . . . . + . . . . . . . . . . . . . . . . . . . . . + square/rectangular hexagonal + + +Hexagonal grids are given a very thorough treatment by Ulichney, should you +be interested in further information. + +While technically not an error-diffusion filter, a method proposed by Gozum +[11] offers color resolutions in excess of 256 colors by plotting red, +green, and blue pixel "triplets" or triads to simulate an "interlaced" +television display (sacrificing some horizontal resolution in the process). +Again, I would refer interested readers to his document for more +information. + + +===================================== +Special considerations + +The speed disadvantages of the more complex filters can be eliminated +somewhat by performing the divisions beforehand and using lookup tables +instead of doing the math inside the loop. This makes it harder to use +various filters in the same program, but the speed benefits are enormous. + +It is critical with all of these algorithms that when error values are added +to neighboring pixels, the resultant summed values must be truncated to fit +within the limits of hardware. Otherwise, an area of very intense color may +cause streaks into an adjacent area of less intense color. + +This truncation is known as "clipping," and is analogous to the audio +world's concept of the same name. As in the case of an audio amplifier, +clipping adds undesired noise to the data. Unlike the audio world, however, +the visual clipping performed in error-diffusion halftoning is acceptable +since it is not nearly so offensive as the color streaking that would occur +otherwise. It is mainly for this reason that the larger filters work better +-- they split the errors up more finely and produce less clipping noise. + +With all of these filters, it is also important to ensure that the sum of +the distributed error values is equal to the original error value. This is +most easily accomplished by subtracting each fraction, as it is calculated, +from the whole error value, and using the final remainder as the last +fraction. + + +===================================== +Further perturbations + +As alluded to earlier, there are various techniques for the reduction of +digital artifacts, most of which involve using a little randomness to +lightly "perturb" a regular algorithm (particularly the simpler ones). It +could be said that random dither takes this concept to the extreme. + +Serpentine scanning is one of these techniques, as noted earlier. Other +techniques include the addition of small amounts of white noise, or +randomizing the positions of the error weights (essentially, using a +constantly-varying pattern). As you might imagine, any of these methods +incur a penalty in processing time. + +Indeed, some of the above filters (particularly the simpler ones) can be +greatly improved by skewing the weights with a little randomness [3]. + + +===================================== +Nearest available color + +Calculating the nearest available intensity is trivial with a monochrome +image; calculating the nearest available color in a color image requires +more work. + +A table of RGB values of all available colors must be scanned sequentially +for each input pixel to find the closest. The "distance" formula most often +used is a simple pythagorean "least squares". The difference for each color +is squared, and the three squares added to produce the distance value. This +value is equivalent to the square of the distance between the points in RGB- +space. It is not necessary to compute the square root of this value because +we are not interested in the actual distance, only in which is smallest. +The square root function is a monotonic increasing function and does not +affect the order of its operands. If the total number of colors with which +you are dealing is small, this part of the algorithm can be replaced by a +lookup table as well. + +When your hardware allows you to select the available colors, very good +results can be achieved by selecting colors from the image itself. You must +reserve at least 8 colors for the primaries, secondaries, black, and white +for best results. If you do not know the colors in your image ahead of +time, or if you are going to use the same map to dither several different +images, you will have to fill your color map with a good range of colors. +This can be done either by assigning a certain number of bits to each +primary and computing all combinations, or by a smoother distribution as +suggested by Heckbert [8]. + +An alternate method of color selection, based on a tetrahedral color space, +has been proposed by Crawford [12]. His algorithm has been optimized for +either dispersed-dot ordered dither or Floyd-Steinberg error diffusion with +serpentine scan. + + +===================================== +Hardware halftoning + +In some cases, image scanning hardware may be able to digitally halftone and +dither the image "on the fly" as it is being scanned. The data produced by +the "raw" scan is then already in a 1- or 2-bit/pixel format. While this +feature would probably be unsuitable for cases where the image would need +further processing (see the "Loss of image information" section below), it +is very useful where the operator wants to generate a final image, ready for +printing or displaying, with little or no subsequent processing. + +As an example, the Epson ES-300C color scanner (and its European equivalent, +the Epson GT-6000) offers three internal halftone modes. One is a standard +"halftone" algorithm, i.e. a clustered-dot ordered dither. The other two +are error-diffusion filters (one "sharp," the other "soft") which are +proprietary Epson-developed filters. + + +===================================== +Loss of image information incurred by digital halftoning + +It is important to emphasize here that digital halftoning is a ONE-WAY +operation. Once an image has been halftoned or dithered, although it may +look like a good reproduction of the original, INFORMATION IS PERMANENTLY +LOST. Many image processing functions fail on dithered images; in fact, you +would not want to dither an image which had already been dithered to some +extent. + +For these reasons, digital halftoning must be considered primarily as a way +TO PRODUCE AN IMAGE ON HARDWARE THAT WOULD OTHERWISE BE INCAPABLE OF +DISPLAYING IT. This would hold true wherever a grayscale or color image +needs to be rendered on a bilevel display device. In this situation, one +would almost never want to store the dithered image. + +On the other hand, when color images are dithered for display on color +displays with a lower color resolution, the dithered images are more useful. +In fact, the bulk of today's scanned-image GIF files which abound on +electronic BBSs and information services are 8-bit (256 color), colormapped +and dithered files created from 24-bit true-color scans. Only rarely are +the 24-bit files exchanged, because of the huge amount of data contained in +them. + +In some cases, these mapped GIF files may be further processed with special +paint/processing utilities, with very respectable results. However, the +previous warning still applies: one can never obtain the same image fidelity +when operating on the mapped GIF file as they could if they were operating +on the true-color image file. + +Generally speaking, digital halftoning and dithering should be the last +stage in producing a physical display from a digitally stored image. The +data representing an image should always be kept in full detail in case you +should want to reprocess it in any way. As affordable display technology +improves, the day may soon come where you might possess the hardware to +allow you to use all of the original image information without the need for +digital halftoning or color reduction. + + +===================================== +Sample code + +Despite my best efforts in expository writing, nothing explains an algorithm +better than real code. With that in mind, presented here are a few programs +which implement some of the concepts presented in this paper. + + +1) This code (in the C programming language) dithers a 256-level + monochrome image onto a black-and-white display with the Bayer ordered + dither. + +/* Bayer-method ordered dither. The array line[] contains the intensity +** values for the line being processed. As you can see, the ordered +** dither is much simpler than the error dispersion dither. It is also +** many times faster, but it is not as accurate and produces cross-hatch +** patterns on the output. +*/ + +unsigned char line[WIDTH]; + +int pattern[8][8] = { + { 0, 32, 8, 40, 2, 34, 10, 42}, /* 8x8 Bayer ordered dithering */ + {48, 16, 56, 24, 50, 18, 58, 26}, /* pattern. Each input pixel */ + {12, 44, 4, 36, 14, 46, 6, 38}, /* is scaled to the 0..63 range */ + {60, 28, 52, 20, 62, 30, 54, 22}, /* before looking in this table */ + { 3, 35, 11, 43, 1, 33, 9, 41}, /* to determine the action. */ + {51, 19, 59, 27, 49, 17, 57, 25}, + {15, 47, 7, 39, 13, 45, 5, 37}, + {63, 31, 55, 23, 61, 29, 53, 21} }; + +int getline(); /* Function to read line[] from image */ + /* file; must return EOF when done. */ +putdot(int x, int y); /* Plot white dot at given x, y. */ + +dither() +{ + int x, y; + + while (getline() != EOF) { + for (x=0; x> 2; /* Scale value to 0..63 range */ + + if (c > pattern[x & 7][y & 7]) putdot(x, y); + } + ++y; + } +} + + +2) This program (also written in C) dithers a color image onto an 8-color + display by error-diffusion using the Burkes filter. + +/* Burkes filter error diffusion dithering algorithm in color. The array +** line[][] contains the RGB values for the current line being processed; +** line[0][x] = red, line[1][x] = green, line[2][x] = blue. +*/ + +unsigned char line[3][WIDTH]; +unsigned char colormap[3][COLORS] = { + 0, 0, 0, /* Black This color map should be replaced */ + 255, 0, 0, /* Red by one available on your hardware */ + 0, 255, 0, /* Green */ + 0, 0, 255, /* Blue */ + 255, 255, 0, /* Yellow */ + 255, 0, 255, /* Magenta */ + 0, 255, 255, /* Cyan */ + 255, 255, 255 }; /* White */ + +int getline(); /* Function to read line[][] from image */ + /* file; must return EOF when done. */ +putdot(int x, int y, int c); /* Plot dot of given color at given x, y. */ + +dither() +{ + static int ed[3][WIDTH] = {0}; /* Errors distributed down, i.e., */ + /* to the next line. */ + int x, y, h, c, nc, v, /* Working variables */ + e[4], /* Error parts (7/8,1/8,5/8,3/8). */ + ef[3]; /* Error distributed forward. */ + long dist, sdist; /* Used for least-squares match. */ + + for (x=0; x 255) v = 255; /* and clip. */ + line[c][x] = v; + } + + sdist = 255L * 255L * 255L + 1L; /* Compute the color */ + for (c=0; c> 1; /* half of v, e[1..4] */ + e[1] = (7 * h) >> 3; /* will be filled */ + e[2] = h - e[1]; /* with the Floyd and */ + h = v - h; /* Steinberg weights. */ + e[3] = (5 * h) >> 3; + e[4] = h = e[3]; + + ef[c] = e[1]; /* Distribute errors. */ + if (x < WIDTH-1) ed[c][x+1] = e[2]; + if (x == 0) ed[c][x] = e[3]; else ed[c][x] += e[3]; + if (x > 0) ed[c][x-1] += e[4]; + } + } + ++y; + } +} + + +3) This program (in somewhat incomplete, very inefficient pseudo-C) + implements error diffusion dithering with the Floyd and Steinberg + filter. It is not efficiently coded, but its purpose is to show the + method, which I believe it does. + +/* Floyd/Steinberg error diffusion dithering algorithm in color. The array +** line[][] contains the RGB values for the current line being processed; +** line[0][x] = red, line[1][x] = green, line[2][x] = blue. It uses the +** external functions getline() and putdot(), whose purpose should be easy +** to see from the code. +*/ + +unsigned char line[3][WIDTH]; +unsigned char colormap[3][COLORS] = { + 0, 0, 0, /* Black This color map should be replaced */ + 255, 0, 0, /* Red by one available on your hardware. */ + 0, 255, 0, /* Green It may contain any number of colors */ + 0, 0, 255, /* Blue as long as the constant COLORS is */ + 255, 255, 0, /* Yellow set correctly. */ + 255, 0, 255, /* Magenta */ + 0, 255, 255, /* Cyan */ + 255, 255, 255 }; /* White */ + +int getline(); /* Function to read line[] from image file; */ + /* must return EOF when done. */ +putdot(int x, int y, int c); /* Plot dot of color c at location x, y. */ + +dither() +{ + static int ed[3][WIDTH] = {0}; /* Errors distributed down, i.e., */ + /* to the next line. */ + int x, y, h, c, nc, v, /* Working variables */ + e[4], /* Error parts (7/8,1/8,5/8,3/8). */ + ef[3]; /* Error distributed forward. */ + long dist, sdist; /* Used for least-squares match. */ + + for (x=0; x 255) v = 255; /* and clip. */ + line[c][x] = v; + } + + sdist = 255L * 255L * 255L + 1L; /* Compute the color */ + for (c=0; c> 1; /* half of v, e[1..4] */ + e[1] = (7 * h) >> 3; /* will be filled */ + e[2] = h - e[1]; /* with the Floyd and */ + h = v - h; /* Steinberg weights. */ + e[3] = (5 * h) >> 3; + e[4] = h = e[3]; + + ef[c] = e[1]; /* Distribute errors. */ + if (x < WIDTH-1) ed[c][x+1] = e[2]; + if (x == 0) ed[c][x] = e[3]; else ed[c][x] += e[3]; + if (x > 0) ed[c][x-1] += e[4]; + } + } /* next x */ + + ++y; + } /* next y */ +} + + +===================================== +Bibliography + +[1] Foley, J.D. and A. van Dam, Fundamentals of Interactive Computer + Graphics, Addison-Wesley, Reading, MA, 1982. + + This is a standard reference for many graphic techniques which has + not declined with age. Highly recommended. This edition is out + of print but can be found in many university and engineering + libraries. NOTE: This book has been updated and rewritten, and + this new version is currently in print as: + + Foley, J.D., A. van Dam, S.K. Feiner, and J.F. Hughes; Computer + Graphics: Principles and Practice. Addison-Wesley, Reading, MA, 1990. + + This rewrite omits some of the more technical data of the 1982 + edition, but has been updated to include information on error- + diffusion and the Floyd-Steinberg filter. Currently on computer + bookstore shelves and rather expensive (around $75 list price). + +[2] Bayer, B.E., "An Optimum Method for Two-Level Rendition of Continuous + Tone Pictures," IEEE International Conference on Communications, + Conference Records, 1973, pp. 26-11 to 26-15. + + A short article proving the optimality of Bayer's pattern in the + dispersed-dot ordered dither. + +[3] Ulichney, R., Digital Halftoning, The MIT Press, Cambridge, MA, 1987. + + This is the best book I know of for describing the various black + and white dithering methods. It has clear explanations (a little + higher math may come in handy) and wonderful illustrations. It + does not contain any code, but don't let that keep you from + getting this book. Computer Literacy normally carries it but the + title is often sold out. + + [MFM note: I can't describe how much information I got from this + book! Several different writers have praised this reference to + the skies, and I can only concur. Some of it went right over my + head -- it's heavenly for someone who is thrilled by Fourier + analysis -- but the rest of it is a clear and excellent treatment + of the subject. I had to request it on an interlibrary loan, but + it was worth the two weeks' wait and the 25 cents it cost me for + the search. University or engineering libraries would be your + best bet, as would technical bookstores.] + +[4] Floyd, R.W. and L. Steinberg, "An Adaptive Algorithm for Spatial Gray + Scale." SID 1975, International Symposium Digest of Technical Papers, + vol 1975m, pp. 36-37. + + Short article in which Floyd and Steinberg introduce their filter. + +[5] Daniel Burkes is unpublished, but can be reached at this address: + + Daniel Burkes + TerraVision, Inc. + 2351 College Station Road, Suite 563 + Athens, GA 30305 + + or via CIS at UID# 72077,356. The Burkes error filter was submitted to + the public domain on September 15, 1988 in an unpublished document, + "Presentation of the Burkes error filter for use in preparing + continuous-tone images for presentation on bi-level devices." The file + BURKES.ARC, in LIB 15 (Publications) of the CIS Graphics Support Forum, + contains this document as well as sample images. + +[6] Jarvis, J.F., C.N. Judice, and W.H. Ninke, "A Survey of Techniques for + the Display of Continuous Tone Pictures on Bi-Level Displays," Computer + Graphics and Image Processing, vol. 5, pp. 13-40, 1976. + +[7] Stucki, P., "MECCA - a multiple-error correcting computation algorithm + for bilevel image hardcopy reproduction." Research Report RZ1060, IBM + Research Laboratory, Zurich, Switzerland, 1981. + +[8] Heckbert, P. "Color Image Quantization for Frame Buffer Display." + Computer Graphics (SIGGRAPH 82), vol. 16, pp. 297-307, 1982. + +[9] Frankie Sierra is unpublished, but can be reached via CIS at UID# + 76356,2254. Pictorial presentations of his filters can be found in LIB + 17 (Developer's Den) of the CIS Graphics Support Forum as the files + DITER1.GIF, DITER2.GIF, DITER6.GIF, DITER7.GIF, DITER8.GIF, and + DITER9.GIF. + +[10] J.F.R. "Frank" Slinkman is unpublished, but can be reached via CIS at + UID# 72411,650. The file NUDTHR.ARC in LIB 17 (Developer's Den) of the + CIS Graphics Support Forum contains his document "New Dithering Method + for Non-Square Pixels" as well as sample images and encoding program. + +[11] Lawrence Gozum is unpublished, but can be reached via CIS at UID# + 73437,2372. His document "Notes of IDTVGA Dithering Method" can be + found in LIB 17 (Developer's Den) of the CIS Graphics Support Forum as + the file IDTVGA.TXT. + +[12] Robert M. Crawford is unpublished, but can be reached via CIS at UID# + 76356,741. The file DGIF.ZIP in LIB 17 (Developer's Den) of the CIS + Graphics Support Forum contains documentation, sample images, and demo + program. + + +======================================================================== +Other works of interest: + +Knuth, D.E., "Digital Halftones by Dot Diffusion." ACM Transactions on +Graphics, Vol. 6, No. 4, October 1987, pp 245-273. + + Surveys the various methods available for mapping grayscale images to + B&W for high-quality phototypesetting and laser printer reproduction. + Presents an algorithm for smooth dot diffusion. (With 22 references.) + +Newman, W.M. and R.F.S. Sproull, Principles of Interactive Computer +Graphics, 2nd edition, McGraw-Hill, New York, 1979. + + Similar to Foley and van Dam in scope and content. + +Rogers, D.F., Procedural Elements for Computer Graphics, McGraw-Hill, New +York, 1985. + + More of a conceptual treatment of the subject -- for something with + more programming code, see the following work. Alas, the author errs + in his discussion of the Floyd-Steinberg filter and uses the "false" + filter pattern discussed earlier. + +Rogers, D.F. and J. A. Adams, Mathematical Elements for Computer Graphics, +McGraw-Hill, New York, 1976. + + A good detailed discussion of producing graphic images on a computer. + Plenty of sample code. + +Kuto, S., "Continuous Color Presentation Using a Low-Cost Ink Jet Printer," +Proc. Computer Graphics Tokyo 84, 24-27 April, 1984, Tokyo, Japan. + +Mitchell, W.J., R.S. Liggett, and T. Kvan, The Art of Computer Graphics +Programming, Van Nostrand Reinhold Co., New York, 1987. + +Pavlidis, T., Algorithms for Graphics and Image Processing, Computer Science +Press, Rockville, MD, 1982. + diff --git a/ImageSharp/Processing/Processors/Dithering/DITHER.TXT b/ImageSharp/Processing/Processors/Dithering/DITHER.TXT new file mode 100644 index 0000000..1f49fd6 --- /dev/null +++ b/ImageSharp/Processing/Processors/Dithering/DITHER.TXT @@ -0,0 +1,547 @@ +DITHER.TXT + +What follows is everything you ever wanted to know (for the time being) about +dithering. I'm sure it will be out of date as soon as it is released, but it +does serve to collect data from a wide variety of sources into a single +document, and should save you considerable searching time. + +Numbers in brackets (like this [0]) are references. A list of these works +appears at the end of this document. + +Because this document describes ideas and algorithms which are constantly +changing, I expect that it may have many editions, additions, and corrections +before it gets to you. I will list my name below as original author, but I +do not wish to deter others from adding their own thoughts and discoveries. +This is not copyrighted in any way, and was created solely for the purpose of +organizing my own knowledge on the subject, and sharing this with others. +Please distribute it to anyonw who might be interested. + +If you add anything to this document, please feel free to include your name +below as a contributor or as a reference. I would particularly like to see +additions to the "Other books of interest" section. Please keep the text in +this simple format: no margins, no pagination, no lines longer that 79 +characters, and no non-ASCII or non-printing characters other than a CR/LF +pair at the end of each line. It is intended that this be read on as many +different machines as possible. + +Original Author: + +Lee Crocker I can be reached in the CompuServe Graphics +1380 Jewett Ave Support Forum (GO PICS) with ID # 73407,2030. +Pittsburg, CA 94565 + +Contributors: + +======================================================================== +What is Dithering? + +Dithering, also called Halftoning or Color Reduction, is the process of +rendering an image on a display device with fewer colors than are in the +image. The number of different colors in an image or on a device I will call +its Color Resolution. The term "resolution" means "fineness" and is used to +describe the level of detail in a digitally sampled signal. It is used most +often in referring to the Spatial Resolution, which is the basic sampling +rate for a digitized image. + +Spatial resolution describes the fineness of the "dots" used in an image. +Color resolution describes the fineness of detail available at each dot. The +higher the resolution of a digital sample, the better it can reproduce high +frequency detail. A compact disc, for example, has a temporal (time) +resolution of 44,000 samples per second, and a dynamic (volume) resolution of +16 bits (0..65535). It can therefore reproduce sounds with a vast dynamic +range (from barely audible to ear-splitting) with great detail, but it has +problems with very high-frequency sounds, like violins and piccolos. + +It is often possible to "trade" one kind of resolution for another. If your +display device has a higher spatial resolution than the image you are trying +to reproduce, it can show a very good image even if its color resolution is +less. This is what we will call "dithering" and is the subject of this +paper. The other tradeoff, i.e., trading color resolution for spatial +resolution, is called "anti-aliasing" and is not discussed here. + +It is important to emphasize here that dithering is a one-way operation. +Once an image has been dithered, although it may look like a good +reproduction of the original, information is permanently lost. Many image +processing functions fail on dithered images. For these reasons, dithering +must be considered only as a way to produce an image on hardware that would +otherwise be incapable of displaying it. The data representing an image +should always be kept in full detail. + + +======================================================================== +Classes of dithering algorithms + +The classes of dithering algorithms we will discuss here are these: + +1. Random +2. Pattern +3. Ordered +4. Error dispersion + +Each of these methods is generally better than those listed before it, but +other considerations such as processing time, memory constraints, etc. may +weigh in favor of one of the simpler methods. + +For the following discussions I will assume that we are given an image with +256 shades of gray (0=black..255=white) that we are trying to reproduce on a +black and white ouput device. Most of these methods can be extended in +obvious ways to deal with displays that have more than two levels but fewer +than the image, or to color images. Where such extension is not obvious, or +where better results can be obtained, I will go into more detail. + +To convert any of the first three methods into color, simply apply the +algorithm separately for each primary and mix the resulting values. This +assumes that you have at least eight output colors: black, red, green, blue, +cyan, magenta, yellow, and white. Though this will work for error dispersion +as well, there are better methods in this case. + + +======================================================================== +Random dither + +This is the bubblesort of dithering algorithms. It is not really acceptable +as a production method, but it is very simple to describe and implement. For +each value in the image, simply generate a random number 1..256; if it is +geater than the image value at that point, plot the point white, otherwise +plot it black. That's it. This generates a picture with a lot of "white +noise", which looks like TV picture "snow". Though the image produced is +very inaccurate and noisy, it is free from "artifacts" which are phenomena +produced by digital signal processing. + +The most common type of artifact is the Moire pattern (Contributors: please +resist the urge to put an accent on the "e", as no portable character set +exists for this). If you draw several lines close together radiating from a +single point on a computer display, you will see what appear to be flower- +like patterns. These patterns are not part of the original idea of lines, +but are an illusion produced by the jaggedness of the display. + +Many techniques exist for the reduction of digital artifacts like these, most +of which involve using a little randomness to "perturb" a regular algorithm a +little. Random dither obviously takes this to extreme. + +I should mention, of course, that unless your computer has a hardware-based +random number generator (and most don't) there may be some artifacts from the +random number generation algorithm itself. + +While random dither adds a lot of high-frequency noise to a picture, it is +useful in reproducing very low-frequency images where the absence of +artifacts is more important than noise. For example, a whole screen +containing a gradient of all levels from black to white would actually look +best with a random dither. In this case, ordered dithering would produce +diagonal patterns, and error dispersion would produce clustering. + +For efficiency, you can take the random number generator "out of the loop" by +generating a list of random numbers beforehand for use in the dither. Make +sure that the list is larger than the number of pixels in the image or you +may get artifacts from the reuse of numbers. The worst case would be if the +size of your list of random numbers is a multiple or near-multiple of the +horizontal size of the image, in which case unwanted vertical or diagonal +lines will appear. + + +======================================================================== +Pattern dither + +This is also a simple concept, but much more effective than random dither. +For each possible value in the image, create a pattern of dots that +approximates that value. For instance, a 3-by-3 block of dots can have one +of 512 patterns, but for our purposes, there are only 10; the number of black +dots in the pattern determines the darkness of the pattern. + +Which 10 patterns do we choose? Obviously, we need the all-white and all- +black patterns. We can eliminate those patterns which would create vertical +or horizontal lines if repeated over a large area because many images have +such regions of similar value [1]. It has been shown [1] that patterns for +adjacent colors should be similar to reduce an artifact called "contouring", +or visible edges between regions of adjacent values. One easy way to assure +this is to make each pattern a superset of the previous. Here are two good +sets of patterns for a 3-by-3 matrix: + + --- --- --- -X- -XX -XX -XX -XX XXX XXX + --- -X- -XX -XX -XX -XX XXX XXX XXX XXX + --- --- --- --- --- -X- -X- XX- XX- XXX +or + --- X-- X-- X-- X-X X-X X-X XXX XXX XXX + --- --- --- --X --X X-X X-X X-X XXX XXX + --- --- -X- -X- -X- -X- XX- XX- XX- XXX + +The first set of patterns above are "clustered" in that as new dots are added +to each pattern, they are added next to dots already there. The second set +is "dispersed" as the dots are spread out more. This distinction is more +important on larger patterns. Dispersed-dot patterns produce less grainy +images, but require that the output device render each dot distinctly. When +this is not the case, as with a printing press which smears the dots a +little, clustered patterns are better. + +For each pixel in the image we now print the pattern which is closest to its +value. This will triple the size of the image in each direction, so this +method can only be used where the display spatial resolution is much greater +than that of the image. + +We can exploit the fact that most images have large areas of similar value to +reduce our need for extra spatial resolution. Instead of plotting a whole +pattern for each pixel, map each pixel in the image to a dot in the pattern +an only plot the corresponding dot for each pixel. + +The simplest way to do this is to map the X and Y coordinates of each pixel +into the dot (X mod 3, Y mod 3) in the pattern. Large areas of constant +value will come out as repetitions of the pattern as before. + +To extend this method to color images, we must use patterns of colored dots +to represent shades not directly printable by the hardware. For example, if +your hardware is capable of printing only red, green, blue, and black (the +minimal case for color dithering), other colors can be represented with +patterns of these four: + + Yellow = R G Cyan = G B Magenta = R B Gray = R G + G R B G B R B K + +(B here represents blue, K is black). There are a total of 31 such distinct +patterns which can be used; I will leave their enumeration "as an exercise +for the reader" (don't you hate books that do that?). + + +======================================================================== +Ordered dither + +Because each of the patterns above is a superset of the previous, we can +express the patterns in compact form as the order of dots added: + + 8 3 4 and 1 7 4 + 6 1 2 5 8 3 + 7 5 9 6 2 9 + +Then we can simply use the value in the array as a threshhold. If the value +of the pixel (scaled into the 0-9 range) is less than the number in the +corresponding cell of the matrix, plot that pixel black, otherwise, plot it +white. This process is called ordered dither. As before, clustered patterns +should be used for devices which blur dots. In fact, the clustered pattern +ordered dither is the process used by most newspapers, and the term +halftoning refers to this method if not otherwise qualified. + +Bayer [2] has shown that for matrices of orders which are powers of two there +is an optimal pattern of dispersed dots which results in the pattern noise +being as high-frequency as possible. The pattern for a 2x2 and 4x4 matrices +are as follows: + + 1 3 1 9 3 11 These patterns (and their rotations + 4 2 13 5 15 7 and reflections) are optimal for a + 4 12 2 10 dispersed-pattern ordered dither. + 16 8 14 6 + +Ulichney [3] shows a recursive technique can be used to generate the larger +patterns. To fully reproduce our 256-level image, we would need to use the +8x8 pattern. + +Bayer's method is in very common use and is easily identified by the cross- +hatch pattern artifacts it produces in the resulting display. This +artifacting is the major drawback of the technique wich is otherwise very +fast and powerful. Ordered dithering also performs very badly on images +which have already been dithered to some extent. As stated earlier, +dithering should be the last stage in producing a physical display from a +digitally stored image. The dithered image should never be stored itself. + + +======================================================================== +Error dispersion + +This technique generates the best results of any method here, and is +naturally the slowest. In fact, there are many variants of this technique as +well, and the better they get, the slower they are. + +Error dispersion is very simple to describe: for each point in the image, +first find the closest color available. Calculate the difference between the +value in the image and the color you have. Now divide up these error values +and distribute them over the neighboring pixels which you have not visited +yet. When you get to these later pixels, just add the errors distributed +from the earlier ones, clip the values to the allowed range if needed, then +continue as above. + +If you are dithering a grayscale image for output to a black-and-white +device, the "find closest color" is just a simle threshholding operation. In +color, it involves matching the input color to the closest available hardware +color, which can be difficult depending on the hardware palette. + +There are many ways to distribute the errors and many ways to scan the +image, but I will deal here with only a few. The two basic ways to scan the +image are with a normal left-to-right, top-to-bottom raster, or with an +alternating left-to-right then right-to-left raster. The latter method +generally produces fewer artifacts and can be used with all the error +diffusion patterns discussed below. + +The different ways of dividing up the error can be expressed as patterns +(called filters, for reasons too boring to go into here). + + X 7 This is the Floyd and Steinberg [4] + 3 5 1 error diffusion filter. + +In this filter, the X represents the pixel you are currently scanning, and +the numbers (called weights, for equally boring reasons) represent the +proportion of the error distributed to the pixel in that position. Here, the +pixel immediately to the right gets 7/16 of the error (the divisor is 16 +because the weights add to 16), the pixel directly below gets 5/16 of the +error, and the diagonally adjacent pixels get 3/16 and 1/16. When scanning a +line right-to-left, this pattern is reversed. This pattern was chosen +carefully so that it would produce a checkerboard pattern in areas with +intensity of 1/2 (or 128 in our image). It is also fairly easy to calculate +when the division by 16 is replaced by shifts. + +Another filter in common use, but not recommended: + + X 3 A simpler filter. + 3 2 + +This is often erroneously called the Floyd-Steinberg filter, but it does not +produce as good results. An alternating raster scan of the image is +necessary with this filter to reduce artifacts. Additional perturbations of +the formula are frequently necessary also. + +Burke [5] suggests the following filter: + + X 8 4 The Burke filter. + 2 4 8 4 2 + +Notice that this is just a simplification of the Stucki filter (below) with +the bottom row removed. The main improvement is that the divisor is now 32, +which makes calculating the errors faster, and the removal of one row +reduces the memory requirements of the method. + +This is also fairly easy to calculate and produces better results than Floyd +and Steinberg. Jarvis, Judice, and Ninke [6] use the following: + + X 7 5 The Jarvis, et al. pattern. + 3 5 7 5 3 + 1 3 5 3 1 + +The divisor here is 48, which is a little more expensive to calculate, and +the errors are distributed over three lines, requiring extra memory and time +for processing. Probably the best filter is from Stucki [7]: + + X 8 4 The Stucki pattern. + 2 4 8 4 2 + 1 2 4 2 1 + +This one takes a division by 42 for each pixel and is therefore slow if math +is done inside the loop. After the initial 8/42 is calculated, some time can +be saved by producing the remaining fractions by shifts. + +The speed advantages of the simpler filters can be eliminated somewhat by +performing the divisions beforehand and using lookup tables instead of per- +forming math inside the loop. This makes it harder to use various filters +in the same program, but the speed benefits are enormous. + +It is critical with all of these algorithms that when error values are added +to neighboring pixels, the values must be truncated to fit within the limits +of hardware, otherwise and area of very intense color may cause streaks into +an adjacent area of less intense color. This truncation adds noise to the +image anagous to clipping in an audio amplifier, but it is not nearly so +offensive as the streaking. It is mainly for this reason that the larger +filters work better--they split the errors up more finely and produce less of +this clipping noise. + +With all of these filters, it is also important to ensure that the errors +you distribute properly add to the original error value. This is easiest to +accomplish by subtracting each fraction from the whole error as it is +calculated, and using the final remainder as the last fraction. + +Some of these methods (particularly the simpler ones) can be greatly improved +by skewing the weights with a little randomness [3]. + +Calculating the "nearest available color" is trivial with a monochrome image; +with color images it requires more work. A table of RGB values of all +available colors must be scanned sequentially for each input pixel to find +the closest. The "distance" formula most often used is a simple pythagorean +"least squares". The difference for each color is squared, and the three +squares added to produce the distance value. This value is equivalent to the +square of the distance between the points in RGB-space. It is not necessary +to compute the square root of this value because we are not interested in the +actual distance, only in which is smallest. The square root function is a +monotonic increasing function and does not affect the order of its operands. +If the total number of colors with which you are dealing is small, this part +of the algorithm can be replaced by a lookup table as well. + +When your hardware allows you to select the available colors, very good +results can be achieved by selecting colors from the image itself. You must +reserve at least 8 colors for the primaries, secondaries, black, and white +for best results. If you do not know the colors in your image ahead of time, +or if you are going to use the same map to dither several different images, +you will have to fill your color map with a good range of colors. This can +be done either by assigning a certain number of bits to each primary and +computing all combinations, or by a smoother distribution as suggested by +Heckbert [8]. + + +======================================================================== +Sample code + +Despite my best efforts in expository writing, nothing explains an algorithm +better than real code. With that in mind, presented here below is an +algorithm (in somewhat incomplete, very inefficient pseudo-C) which +implements error diffusion dithering with the Floyd and Steinberg filter. It +is not efficiently coded, but its purpose is to show the method, which I +believe it does. + +/* Floyd/Steinberg error diffusion dithering algorithm in color. The array +** line[][] contains the RGB values for the current line being processed; +** line[0][x] = red, line[1][x] = green, line[2][x] = blue. It uses the +** external functions getline() and putdot(), whose pupose should be easy +** to see from the code. +*/ + +unsigned char line[3][WIDTH]; +unsigned char colormap[3][COLORS] = { + 0, 0, 0, /* Black This color map should be replaced */ + 255, 0, 0, /* Red by one available on your hardware. */ + 0, 255, 0, /* Green It may contain any number of colors */ + 0, 0, 255, /* Blue as long as the constant COLORS is */ + 255, 255, 0, /* Yellow set correctly. */ + 255, 0, 255, /* Magenta */ + 0, 255, 255, /* Cyan */ + 255, 255, 255 }; /* White */ + +int getline(); /* Function to read line[] from image file; */ + /* must return EOF when done. */ +putdot(int x, int y, int c); /* Plot dot of color c at location x, y. */ + +dither() +{ + static int ed[3][WIDTH] = {0}; /* Errors distributed down, i.e., */ + /* to the next line. */ + int x, y, h, c, nc, v, /* Working variables */ + e[4], /* Error parts (7/8,1/8,5/8,3/8). */ + ef[3]; /* Error distributed forward. */ + long dist, sdist; /* Used for least-squares match. */ + + for (x=0; x 255) v = 255; /* and clip. */ + line[c][x] = v; + } + + sdist = 255L * 255L * 255L + 1L; /* Compute the color */ + for (c=0; c> 1; /* half of v, e[1..4] */ + e[1] = (7 * h) >> 3; /* will be filled */ + e[2] = h - e[1]; /* with the Floyd and */ + h = v - h; /* Steinberg weights. */ + e[3] = (5 * h) >> 3; + e[4] = h = e[3]; + + ef[c] = e[1]; /* Distribute errors. */ + if (x < WIDTH-1) ed[c][x+1] = e[2]; + if (x == 0) ed[c][x] = e[3]; else ed[c][x] += e[3]; + if (x > 0) ed[c][x-1] += e[4]; + } + } /* next x */ + + ++y; + } /* next y */ +} + + +======================================================================== +Bibliography + +[1] Foley, J. D. and Andries Van Dam (1982) + Fundamentals of Interactive Computer Graphics. Reading, MA: Addisson + Wesley. + + This is a standard reference for many graphic techniques which has not + declined with age. Highly recommended. + +[2] Bayer, B. E. (1973) + "An Optimum Method for Two-Level Rendition of Continuous Tone Pictures," + IEEE International Conference on Communications, Conference Records, pp. + 26-11 to 26-15. + + A short article proving the optimality of Bayer's pattern in the + dispersed-dot ordered dither. + +[3] Ulichney, R. (1987) + Digital Halftoning. Cambridge, MA: The MIT Press. + + This is the best book I know of for describing the various black and + white dithering methods. It has clear explanations (a little higher math + may come in handy) and wonderful illustrations. It does not contain any + code, but don't let that keep you from getting this book. Computer + Literacy carries it but is often sold out. + +[4] Floyd, R.W. and L. Steinberg (1975) + "An Adaptive Algorithm for Spatial Gray Scale." SID International + Symposium Digest of Technical Papers, vol 1975m, pp. 36-37. + + Short article in which Floyd and Steinberg introduce their filter. + +[5] Daniel Burkes is unpublished, but can be reached at this address: + + Daniel Burkes + TerraVision Inc. + 2351 College Station Road Suite 563 + Athens, GA 30305 + + or via CompuServe's Graphics Support Forum, ID # 72077,356. + +[6] Jarvis, J. F., C. N. Judice, and W. H. Ninke (1976) + "A Survey of Techniques for the Display of Continuous Tone Pictures on + Bi-Level Displays." Computer Graphics and Image Processing, vol. 5, pp. + 13-40. + +[7] Stucki, P. (1981) + "MECCA - a multiple-error correcting computation algorithm for bilevel + image hardcopy reproduction." Research Report RZ1060, IBM Research + Laboratory, Zurich, Switzerland. + +[8] Heckbert, Paul (9182) + "Color Image Quantization for Frame Buffer Display." Computer Graphics + (SIGGRAPH 82), vol. 16, pp. 297-307. + + +======================================================================== +Other works of interest: + +Newman, William M., and Robert F. S. Sproull (1979) +Principles of Interactive Computer Graphics. 2nd edition. New York: +McGraw-Hill. + +Rogers, David F. (1985) +Procedural Elements for Computer Graphics. New York: McGraw-Hill. + +Rogers, David F., and J. A. Adams (1976) +Mathematical Elements for Computer Graphics. New York: McGraw-Hill. + + +======================================================================== +About CompuServe Graphics Support Forum: + +CompuServe Information Service is a service of the H&R Block companies +providing computer users with electronic mail, teleconferencing, and many +other telecommunications services. Call 800-848-8199 for more information. + +The Graphics Support Forum is dedicated to helping its users get the most out +of their computers' graphics capabilities. It has a small staff and a large +number of "Developers" who create images and software on all types of +machines from Apple IIs to Sun workstations. While on CompuServe, type GO +PICS from any "!" prompt to gain access to the forum. \ No newline at end of file diff --git a/ImageSharp/Processing/Processors/Dithering/ErrorDither.KnownTypes.cs b/ImageSharp/Processing/Processors/Dithering/ErrorDither.KnownTypes.cs new file mode 100644 index 0000000..ced8615 --- /dev/null +++ b/ImageSharp/Processing/Processors/Dithering/ErrorDither.KnownTypes.cs @@ -0,0 +1,187 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering { + /// + /// An error diffusion dithering implementation. + /// + public readonly partial struct ErrorDither + { + /// + /// Applies error diffusion based dithering using the Atkinson image dithering algorithm. + /// + public static readonly ErrorDither Atkinson = CreateAtkinson(); + + /// + /// Applies error diffusion based dithering using the Burks image dithering algorithm. + /// + public static readonly ErrorDither Burkes = CreateBurks(); + + /// + /// Applies error diffusion based dithering using the Floyd–Steinberg image dithering algorithm. + /// + public static readonly ErrorDither FloydSteinberg = CreateFloydSteinberg(); + + /// + /// Applies error diffusion based dithering using the Jarvis, Judice, Ninke image dithering algorithm. + /// + public static readonly ErrorDither JarvisJudiceNinke = CreateJarvisJudiceNinke(); + + /// + /// Applies error diffusion based dithering using the Sierra2 image dithering algorithm. + /// + public static readonly ErrorDither Sierra2 = CreateSierra2(); + + /// + /// Applies error diffusion based dithering using the Sierra3 image dithering algorithm. + /// + public static readonly ErrorDither Sierra3 = CreateSierra3(); + + /// + /// Applies error diffusion based dithering using the Sierra Lite image dithering algorithm. + /// + public static readonly ErrorDither SierraLite = CreateSierraLite(); + + /// + /// Applies error diffusion based dithering using the Stevenson-Arce image dithering algorithm. + /// + public static readonly ErrorDither StevensonArce = CreateStevensonArce(); + + /// + /// Applies error diffusion based dithering using the Stucki image dithering algorithm. + /// + public static readonly ErrorDither Stucki = CreateStucki(); + + private static ErrorDither CreateAtkinson() + { + const float divisor = 8F; + const int offset = 1; + + float[,] matrix = + { + { 0, 0, 1 / divisor, 1 / divisor }, + { 1 / divisor, 1 / divisor, 1 / divisor, 0 }, + { 0, 1 / divisor, 0, 0 } + }; + + return new ErrorDither(matrix, offset); + } + + private static ErrorDither CreateBurks() + { + const float divisor = 32F; + const int offset = 2; + + float[,] matrix = + { + { 0, 0, 0, 8 / divisor, 4 / divisor }, + { 2 / divisor, 4 / divisor, 8 / divisor, 4 / divisor, 2 / divisor } + }; + + return new ErrorDither(matrix, offset); + } + + private static ErrorDither CreateFloydSteinberg() + { + const float divisor = 16F; + const int offset = 1; + + float[,] matrix = + { + { 0, 0, 7 / divisor }, + { 3 / divisor, 5 / divisor, 1 / divisor } + }; + + return new ErrorDither(matrix, offset); + } + + private static ErrorDither CreateJarvisJudiceNinke() + { + const float divisor = 48F; + const int offset = 2; + + float[,] matrix = + { + { 0, 0, 0, 7 / divisor, 5 / divisor }, + { 3 / divisor, 5 / divisor, 7 / divisor, 5 / divisor, 3 / divisor }, + { 1 / divisor, 3 / divisor, 5 / divisor, 3 / divisor, 1 / divisor } + }; + + return new ErrorDither(matrix, offset); + } + + private static ErrorDither CreateSierra2() + { + const float divisor = 16F; + const int offset = 2; + + float[,] matrix = + { + { 0, 0, 0, 4 / divisor, 3 / divisor }, + { 1 / divisor, 2 / divisor, 3 / divisor, 2 / divisor, 1 / divisor } + }; + + return new ErrorDither(matrix, offset); + } + + private static ErrorDither CreateSierra3() + { + const float divisor = 32F; + const int offset = 2; + + float[,] matrix = + { + { 0, 0, 0, 5 / divisor, 3 / divisor }, + { 2 / divisor, 4 / divisor, 5 / divisor, 4 / divisor, 2 / divisor }, + { 0, 2 / divisor, 3 / divisor, 2 / divisor, 0 } + }; + + return new ErrorDither(matrix, offset); + } + + private static ErrorDither CreateSierraLite() + { + const float divisor = 4F; + const int offset = 1; + + float[,] matrix = + { + { 0, 0, 2 / divisor }, + { 1 / divisor, 1 / divisor, 0 } + }; + + return new ErrorDither(matrix, offset); + } + + private static ErrorDither CreateStevensonArce() + { + const float divisor = 200F; + const int offset = 3; + + float[,] matrix = + { + { 0, 0, 0, 0, 0, 32 / divisor, 0 }, + { 12 / divisor, 0, 26 / divisor, 0, 30 / divisor, 0, 16 / divisor }, + { 0, 12 / divisor, 0, 26 / divisor, 0, 12 / divisor, 0 }, + { 5 / divisor, 0, 12 / divisor, 0, 12 / divisor, 0, 5 / divisor } + }; + + return new ErrorDither(matrix, offset); + } + + private static ErrorDither CreateStucki() + { + const float divisor = 42F; + const int offset = 2; + + float[,] matrix = + { + { 0, 0, 0, 8 / divisor, 4 / divisor }, + { 2 / divisor, 4 / divisor, 8 / divisor, 4 / divisor, 2 / divisor }, + { 1 / divisor, 2 / divisor, 4 / divisor, 2 / divisor, 1 / divisor } + }; + + return new ErrorDither(matrix, offset); + } + } +} diff --git a/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs new file mode 100644 index 0000000..1573173 --- /dev/null +++ b/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -0,0 +1,242 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering { + /// + /// An error diffusion dithering implementation. + /// + /// + public readonly partial struct ErrorDither : IDither, IEquatable, IEquatable + { + private readonly int offset; + private readonly DenseMatrix matrix; + + /// + /// Initializes a new instance of the struct. + /// + /// The diffusion matrix. + /// The starting offset within the matrix. + [MethodImpl(InliningOptions.ShortMethod)] + public ErrorDither(in DenseMatrix matrix, int offset) + { + Guard.MustBeGreaterThan(offset, 0, nameof(offset)); + + this.matrix = matrix; + this.offset = offset; + } + + /// + /// Compares the two instances to determine whether they are equal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator ==(IDither left, ErrorDither right) + => right == left; + + /// + /// Compares the two instances to determine whether they are unequal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator !=(IDither left, ErrorDither right) + => !(right == left); + + /// + /// Compares the two instances to determine whether they are equal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator ==(ErrorDither left, IDither right) + => left.Equals(right); + + /// + /// Compares the two instances to determine whether they are unequal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator !=(ErrorDither left, IDither right) + => !(left == right); + + /// + /// Compares the two instances to determine whether they are equal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator ==(ErrorDither left, ErrorDither right) + => left.Equals(right); + + /// + /// Compares the two instances to determine whether they are unequal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator !=(ErrorDither left, ErrorDither right) + => !(left == right); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void ApplyQuantizationDither( + ref TFrameQuantizer quantizer, + ImageFrame source, + IndexedImageFrame destination, + Rectangle bounds) + where TFrameQuantizer : struct, IQuantizer + where TPixel : unmanaged, IPixel + { + if (this == default) + { + ThrowDefaultInstance(); + } + + int offsetY = bounds.Top; + int offsetX = bounds.Left; + float scale = quantizer.Options.DitherScale; + Buffer2D sourceBuffer = source.PixelBuffer; + + for (int y = 0; y < destination.Height; y++) + { + ReadOnlySpan sourceRow = sourceBuffer.DangerousGetRowSpan(y + offsetY); + Span destinationRow = destination.GetWritablePixelRowSpanUnsafe(y); + + for (int x = 0; x < destinationRow.Length; x++) + { + TPixel sourcePixel = sourceRow[x + offsetX]; + destinationRow[x] = quantizer.GetQuantizedColor(sourcePixel, out TPixel transformed); + this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); + } + } + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void ApplyPaletteDither( + in TPaletteDitherImageProcessor processor, + ImageFrame source, + Rectangle bounds) + where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor + where TPixel : unmanaged, IPixel + { + if (this == default) + { + ThrowDefaultInstance(); + } + + Buffer2D sourceBuffer = source.PixelBuffer; + float scale = processor.DitherScale; + for (int y = bounds.Top; y < bounds.Bottom; y++) + { + ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(sourceBuffer.DangerousGetRowSpan(y)); + for (int x = bounds.Left; x < bounds.Right; x++) + { + ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, (uint)x); + TPixel transformed = Unsafe.AsRef(in processor).GetPaletteColor(sourcePixel); + this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); + sourcePixel = transformed; + } + } + } + + // Internal for AOT + [MethodImpl(InliningOptions.ShortMethod)] + internal TPixel Dither( + ImageFrame image, + Rectangle bounds, + TPixel source, + TPixel transformed, + int x, + int y, + float scale) + where TPixel : unmanaged, IPixel + { + // Equal? Break out as there's no error to pass. + if (source.Equals(transformed)) + { + return transformed; + } + + // Calculate the error + Vector4 error = (source.ToVector4() - transformed.ToVector4()) * scale; + + int offset = this.offset; + DenseMatrix matrix = this.matrix; + Buffer2D imageBuffer = image.PixelBuffer; + + // Loop through and distribute the error amongst neighboring pixels. + for (int row = 0, targetY = y; row < matrix.Rows; row++, targetY++) + { + if (targetY >= bounds.Bottom) + { + continue; + } + + Span rowSpan = imageBuffer.DangerousGetRowSpan(targetY); + + for (int col = 0; col < matrix.Columns; col++) + { + int targetX = x + (col - offset); + if (targetX < bounds.Left || targetX >= bounds.Right) + { + continue; + } + + float coefficient = matrix[row, col]; + if (coefficient == 0) + { + continue; + } + + ref TPixel pixel = ref rowSpan[targetX]; + Vector4 result = pixel.ToVector4(); + + // Do not diffuse error into fully transparent pixels. They carry no visible color + // (a decoder shows whatever is behind them), so perturbing them is meaningless and, + // for indexed transparency, nudges them off the exact transparent color so they are + // matched to the nearest opaque palette entry instead of being kept transparent. + if (result.W <= 0) + { + continue; + } + + result += error * coefficient; + pixel = TPixel.FromVector4(result); + } + } + + return transformed; + } + + /// + public override bool Equals(object? obj) + => obj is ErrorDither dither && this.Equals(dither); + + /// + public bool Equals(ErrorDither other) + => this.offset == other.offset && this.matrix.Equals(other.matrix); + + /// + public bool Equals(IDither? other) + => this.Equals((object?)other); + + /// + public override int GetHashCode() + => HashCode.Combine(this.offset, this.matrix); + + [MethodImpl(InliningOptions.ColdPath)] + private static void ThrowDefaultInstance() + => throw new ImageProcessingException("Cannot use the default value type instance to dither."); + } +} diff --git a/ImageSharp/Processing/Processors/Dithering/IDither.cs b/ImageSharp/Processing/Processors/Dithering/IDither.cs new file mode 100644 index 0000000..5691835 --- /dev/null +++ b/ImageSharp/Processing/Processors/Dithering/IDither.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering { + /// + /// Defines the contract for types that apply dithering to images. + /// + public interface IDither + { + /// + /// Transforms the quantized image frame applying a dither matrix. + /// This method should be treated as destructive, altering the input pixels. + /// + /// The type of frame quantizer. + /// The pixel format. + /// The frame quantizer. + /// The source image. + /// The destination quantized frame. + /// The region of interest bounds. + public void ApplyQuantizationDither( + ref TFrameQuantizer quantizer, + ImageFrame source, + IndexedImageFrame destination, + Rectangle bounds) + where TFrameQuantizer : struct, IQuantizer + where TPixel : unmanaged, IPixel; + + /// + /// Transforms the image frame applying a dither matrix. + /// This method should be treated as destructive, altering the input pixels. + /// + /// The type of palette dithering processor. + /// The pixel format. + /// The palette dithering processor. + /// The source image. + /// The region of interest bounds. + public void ApplyPaletteDither( + in TPaletteDitherImageProcessor processor, + ImageFrame source, + Rectangle bounds) + where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor + where TPixel : unmanaged, IPixel; + } +} diff --git a/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs new file mode 100644 index 0000000..5a02f2c --- /dev/null +++ b/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering { + /// + /// Implements an algorithm to alter the pixels of an image via palette dithering. + /// + /// The pixel format. + public interface IPaletteDitherImageProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Gets the configuration instance to use when performing operations. + /// + public Configuration Configuration { get; } + + /// + /// Gets the dithering palette. + /// + public ReadOnlyMemory Palette { get; } + + /// + /// Gets the dithering scale used to adjust the amount of dither. Range 0..1. + /// + public float DitherScale { get; } + + /// + /// Returns the color from the dithering palette corresponding to the given color. + /// + /// The color to match. + /// The match. + public TPixel GetPaletteColor(TPixel color); + } +} diff --git a/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs b/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs new file mode 100644 index 0000000..ff794ad --- /dev/null +++ b/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering { + /// + /// An ordered dithering matrix with equal sides of arbitrary length + /// + public readonly partial struct OrderedDither + { + /// + /// Applies order dithering using the 2x2 Bayer dithering matrix. + /// + public static readonly OrderedDither Bayer2x2 = new(2); + + /// + /// Applies order dithering using the 4x4 Bayer dithering matrix. + /// + public static readonly OrderedDither Bayer4x4 = new(4); + + /// + /// Applies order dithering using the 8x8 Bayer dithering matrix. + /// + public static readonly OrderedDither Bayer8x8 = new(8); + + /// + /// Applies order dithering using the 16x16 Bayer dithering matrix. + /// + public static readonly OrderedDither Bayer16x16 = new(16); + + /// + /// Applies order dithering using the 3x3 ordered dithering matrix. + /// + public static readonly OrderedDither Ordered3x3 = new(3); + } +} diff --git a/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs new file mode 100644 index 0000000..81f0950 --- /dev/null +++ b/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -0,0 +1,229 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering { + /// + /// An ordered dithering matrix with equal sides of arbitrary length + /// + public readonly partial struct OrderedDither : IDither, IEquatable, IEquatable + { + private readonly DenseMatrix thresholdMatrix; + private readonly int modulusX; + private readonly int modulusY; + + /// + /// Initializes a new instance of the struct. + /// + /// The length of the matrix sides + [MethodImpl(InliningOptions.ShortMethod)] + public OrderedDither(uint length) + { + Guard.MustBeGreaterThan(length, 0, nameof(length)); + + DenseMatrix ditherMatrix = OrderedDitherFactory.CreateDitherMatrix(length); + + // Create a new matrix to run against, that pre-thresholds the values. + // We don't want to adjust the original matrix generation code as that + // creates known, easy to test values. + // https://en.wikipedia.org/wiki/Ordered_dithering#Algorithm + DenseMatrix thresholdMatrix = new((int)length); + float m2 = length * length; + for (int y = 0; y < length; y++) + { + for (int x = 0; x < length; x++) + { + thresholdMatrix[y, x] = ((ditherMatrix[y, x] + 1) / m2) - .5F; + } + } + + this.modulusX = ditherMatrix.Columns; + this.modulusY = ditherMatrix.Rows; + this.thresholdMatrix = thresholdMatrix; + } + + /// + /// Compares the two instances to determine whether they are equal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator ==(IDither left, OrderedDither right) + => right == left; + + /// + /// Compares the two instances to determine whether they are unequal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator !=(IDither left, OrderedDither right) + => !(right == left); + + /// + /// Compares the two instances to determine whether they are equal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator ==(OrderedDither left, IDither right) + => left.Equals(right); + + /// + /// Compares the two instances to determine whether they are unequal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator !=(OrderedDither left, IDither right) + => !(left == right); + + /// + /// Compares the two instances to determine whether they are equal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator ==(OrderedDither left, OrderedDither right) + => left.Equals(right); + + /// + /// Compares the two instances to determine whether they are unequal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator !=(OrderedDither left, OrderedDither right) + => !(left == right); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void ApplyQuantizationDither( + ref TFrameQuantizer quantizer, + ImageFrame source, + IndexedImageFrame destination, + Rectangle bounds) + where TFrameQuantizer : struct, IQuantizer + where TPixel : unmanaged, IPixel + { + if (this == default) + { + ThrowDefaultInstance(); + } + + int spread = CalculatePaletteSpread(destination.Palette.Length); + float scale = quantizer.Options.DitherScale; + Buffer2D sourceBuffer = source.PixelBuffer; + + for (int y = bounds.Top; y < bounds.Bottom; y++) + { + ReadOnlySpan sourceRow = sourceBuffer.DangerousGetRowSpan(y).Slice(bounds.X, bounds.Width); + Span destRow = destination.GetWritablePixelRowSpanUnsafe(y - bounds.Y)[..sourceRow.Length]; + + for (int x = 0; x < sourceRow.Length; x++) + { + TPixel dithered = this.Dither(sourceRow[x], x, y, spread, scale); + destRow[x] = quantizer.GetQuantizedColor(dithered, out TPixel _); + } + } + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void ApplyPaletteDither( + in TPaletteDitherImageProcessor processor, + ImageFrame source, + Rectangle bounds) + where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor + where TPixel : unmanaged, IPixel + { + if (this == default) + { + ThrowDefaultInstance(); + } + + int spread = CalculatePaletteSpread(processor.Palette.Length); + float scale = processor.DitherScale; + Buffer2D sourceBuffer = source.PixelBuffer; + + for (int y = bounds.Top; y < bounds.Bottom; y++) + { + Span row = sourceBuffer.DangerousGetRowSpan(y).Slice(bounds.X, bounds.Width); + + for (int x = 0; x < row.Length; x++) + { + ref TPixel sourcePixel = ref row[x]; + TPixel dithered = this.Dither(sourcePixel, x, y, spread, scale); + sourcePixel = processor.GetPaletteColor(dithered); + } + } + } + + // Spread assumes an even colorspace distribution and precision. + // TODO: Cubed root is currently used to represent 3 color channels + // but we should introduce something to PixelTypeInfo. + // https://bisqwit.iki.fi/story/howto/dither/jy/ + // https://en.wikipedia.org/wiki/Ordered_dithering#Algorithm + internal static int CalculatePaletteSpread(int colors) + => (int)(255 / Math.Max(1, Math.Pow(colors, 1.0 / 3) - 1)); + + [MethodImpl(InliningOptions.ShortMethod)] + internal TPixel Dither( + TPixel source, + int x, + int y, + int spread, + float scale) + where TPixel : unmanaged, IPixel + { + Rgba32 rgba = source.ToRgba32(); + + // Leave fully transparent pixels untouched. They carry no visible color (a decoder shows + // whatever is behind them), so perturbing them is meaningless and, for indexed transparency, + // nudges them off the exact transparent color so they are matched to the nearest opaque + // palette entry instead of being kept transparent. + if (rgba.A == 0) + { + return source; + } + + Unsafe.SkipInit(out Rgba32 attempt); + + float factor = spread * this.thresholdMatrix[y % this.modulusY, x % this.modulusX] * scale; + + attempt.R = (byte)Numerics.Clamp(rgba.R + factor, byte.MinValue, byte.MaxValue); + attempt.G = (byte)Numerics.Clamp(rgba.G + factor, byte.MinValue, byte.MaxValue); + attempt.B = (byte)Numerics.Clamp(rgba.B + factor, byte.MinValue, byte.MaxValue); + attempt.A = (byte)Numerics.Clamp(rgba.A + factor, byte.MinValue, byte.MaxValue); + + return TPixel.FromRgba32(attempt); + } + + /// + public override bool Equals(object? obj) + => obj is OrderedDither dither && this.Equals(dither); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool Equals(OrderedDither other) + => this.thresholdMatrix.Equals(other.thresholdMatrix) && this.modulusX == other.modulusX && this.modulusY == other.modulusY; + + /// + public bool Equals(IDither? other) + => this.Equals((object?)other); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public override int GetHashCode() + => HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY); + + [MethodImpl(InliningOptions.ColdPath)] + private static void ThrowDefaultInstance() + => throw new ImageProcessingException("Cannot use the default value type instance to dither."); + } +} diff --git a/ImageSharp/Processing/Processors/Dithering/OrderedDitherFactory.cs b/ImageSharp/Processing/Processors/Dithering/OrderedDitherFactory.cs new file mode 100644 index 0000000..919257d --- /dev/null +++ b/ImageSharp/Processing/Processors/Dithering/OrderedDitherFactory.cs @@ -0,0 +1,92 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering { + /// + /// A factory for creating ordered dither matrices. + /// + internal static class OrderedDitherFactory + { + /// + /// Creates an ordered dithering matrix with equal sides of arbitrary length. + /// + /// + /// The length of the matrix sides + /// The + public static DenseMatrix CreateDitherMatrix(uint length) + { + // Calculate the the logarithm of length to the base 2 + uint exponent = 0; + uint bayerLength; + do + { + exponent++; + bayerLength = (uint)(1 << (int)exponent); + } + while (length > bayerLength); + + // Create our Bayer matrix that matches the given exponent and dimensions + DenseMatrix matrix = new((int)length); + uint i = 0; + for (int y = 0; y < length; y++) + { + for (int x = 0; x < length; x++) + { + matrix[y, x] = Bayer(i / length, i % length, exponent); + i++; + } + } + + // If the user requested a matrix with a non-power-of-2 length e.g. 3x3 and we used 4x4 algorithm, + // we need to convert the numbers so that the resulting range is un-gapped. + // We generated: We saved: We compress the number range: + // 0 8 2 10 0 8 2 0 5 2 + // 12 4 14 6 12 4 14 7 4 8 + // 3 11 1 9 3 11 1 3 6 1 + // 15 7 13 5 + uint maxValue = bayerLength * bayerLength; + uint missing = 0; + for (uint v = 0; v < maxValue; ++v) + { + bool found = false; + for (int y = 0; y < length; ++y) + { + for (int x = 0; x < length; x++) + { + if (matrix[y, x] == v) + { + matrix[y, x] -= missing; + found = true; + break; + } + } + } + + if (!found) + { + ++missing; + } + } + + return matrix; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Bayer(uint x, uint y, uint order) + { + uint result = 0; + for (uint i = 0; i < order; ++i) + { + uint xOddXorYOdd = (x & 1) ^ (y & 1); + uint xOdd = x & 1; + result = ((result << 1 | xOddXorYOdd) << 1) | xOdd; + x >>= 1; + y >>= 1; + } + + return result; + } + } +} diff --git a/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs b/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs new file mode 100644 index 0000000..5137489 --- /dev/null +++ b/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; +using System; + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering { + /// + /// Allows the consumption a palette to dither an image. + /// + public sealed class PaletteDitherProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The ordered ditherer. + public PaletteDitherProcessor(IDither dither) + : this(dither, QuantizerConstants.MaxDitherScale) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The ordered ditherer. + /// The dithering scale used to adjust the amount of dither. + public PaletteDitherProcessor(IDither dither, float ditherScale) + : this(dither, ditherScale, Color.WebSafePalette) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The dithering algorithm. + /// The palette to select substitute colors from. + public PaletteDitherProcessor(IDither dither, ReadOnlyMemory palette) + : this(dither, QuantizerConstants.MaxDitherScale, palette) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The dithering algorithm. + /// The dithering scale used to adjust the amount of dither. + /// The palette to select substitute colors from. + public PaletteDitherProcessor(IDither dither, float ditherScale, ReadOnlyMemory palette) + { + Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); + Guard.NotNull(dither, nameof(dither)); + this.Dither = dither; + this.DitherScale = Numerics.Clamp(ditherScale, QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale); + this.Palette = palette; + } + + /// + /// Gets the dithering algorithm to apply to the output image. + /// + public IDither Dither { get; } + + /// + /// Gets the dithering scale used to adjust the amount of dither. Range 0..1. + /// + public float DitherScale { get; } + + /// + /// Gets the palette to select substitute colors from. + /// + public ReadOnlyMemory Palette { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new PaletteDitherProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs new file mode 100644 index 0000000..5821ee3 --- /dev/null +++ b/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering { + /// + /// Allows the consumption a palette to dither an image. + /// + /// The pixel format. + internal sealed class PaletteDitherProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly DitherProcessor ditherProcessor; + private readonly IDither dither; + private IMemoryOwner? paletteOwner; + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public PaletteDitherProcessor(Configuration configuration, PaletteDitherProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.dither = definition.Dither; + + ReadOnlySpan sourcePalette = definition.Palette.Span; + this.paletteOwner = this.Configuration.MemoryAllocator.Allocate(sourcePalette.Length); + Color.ToPixel(sourcePalette, this.paletteOwner.Memory.Span); + + this.ditherProcessor = new DitherProcessor( + this.Configuration, + this.paletteOwner.Memory, + definition.DitherScale); + } + + /// + protected override void OnFrameApply(ImageFrame source) + { + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + this.dither.ApplyPaletteDither(in this.ditherProcessor, source, interest); + } + + /// + protected override void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + this.isDisposed = true; + if (disposing) + { + this.paletteOwner?.Dispose(); + this.ditherProcessor.Dispose(); + } + + this.paletteOwner = null; + base.Dispose(disposing); + } + + /// + /// Used to allow inlining of calls to + /// . + /// + /// Internal for AOT + [SuppressMessage( + "Design", + "CA1001:Types that own disposable fields should be disposable", + Justification = "https://github.com/dotnet/roslyn-analyzers/issues/6151")] + internal readonly struct DitherProcessor : IPaletteDitherImageProcessor, IDisposable + { + private readonly PixelMap pixelMap; + + [MethodImpl(InliningOptions.ShortMethod)] + public DitherProcessor( + Configuration configuration, + ReadOnlyMemory palette, + float ditherScale) + { + this.Configuration = configuration; + this.pixelMap = PixelMapFactory.Create(configuration, palette, ColorMatchingMode.Coarse); + this.Palette = palette; + this.DitherScale = ditherScale; + } + + public Configuration Configuration { get; } + + public ReadOnlyMemory Palette { get; } + + public float DitherScale { get; } + + [MethodImpl(InliningOptions.ShortMethod)] + public TPixel GetPaletteColor(TPixel color) + { + this.pixelMap.GetClosestColor(color, out TPixel match); + return match; + } + + public void Dispose() => this.pixelMap.Dispose(); + } + } +} diff --git a/ImageSharp/Processing/Processors/Dithering/error_diffusion.txt b/ImageSharp/Processing/Processors/Dithering/error_diffusion.txt new file mode 100644 index 0000000..27dea8a --- /dev/null +++ b/ImageSharp/Processing/Processors/Dithering/error_diffusion.txt @@ -0,0 +1,61 @@ +Reference: +http://bisqwit.iki.fi/jutut/kuvat/ordered_dither/error_diffusion.txt + +List of error diffusion schemes. + +Quantization error of *current* pixel is added to the pixels +on the right and below according to the formulas below. +This works nicely for most static pictures, but causes +an avalanche of jittering artifacts if used in animation. + +Floyd-Steinberg: + + * 7 + 3 5 1 / 16 + +Jarvis-Judice-Ninke: + + * 7 5 + 3 5 7 5 3 + 1 3 5 3 1 / 48 + +Stucki: + + * 8 4 + 2 4 8 4 2 + 1 2 4 2 1 / 42 + +Burkes: + + * 8 4 + 2 4 8 4 2 / 32 + + +Sierra3: + + * 5 3 + 2 4 5 4 2 + 2 3 2 / 32 + +Sierra2: + + * 4 3 + 1 2 3 2 1 / 16 + +Sierra-2-4A: + + * 2 + 1 1 / 4 + +Stevenson-Arce: + + * . 32 + 12 . 26 . 30 . 16 + . 12 . 26 . 12 . + 5 . 12 . 12 . 5 / 200 + +Atkinson: + + * 1 1 / 8 + 1 1 1 + 1 diff --git a/ImageSharp/Processing/Processors/Dithering/optimal-parallel-error-diffusion-dithering.pdf b/ImageSharp/Processing/Processors/Dithering/optimal-parallel-error-diffusion-dithering.pdf new file mode 100644 index 0000000..42fb22c Binary files /dev/null and b/ImageSharp/Processing/Processors/Dithering/optimal-parallel-error-diffusion-dithering.pdf differ diff --git a/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor.cs b/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor.cs new file mode 100644 index 0000000..e50f6e7 --- /dev/null +++ b/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor.cs @@ -0,0 +1,138 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Drawing { + /// + /// Combines two images together by blending the pixels. + /// + public class DrawImageProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The image to blend. + /// The location to draw the foreground image on the background. + /// The blending mode to use when drawing the image. + /// The Alpha blending mode to use when drawing the image. + /// The opacity of the image to blend. + /// The number of times the foreground frames are allowed to loop. 0 means infinitely. + public DrawImageProcessor( + Image foreground, + Point backgroundLocation, + PixelColorBlendingMode colorBlendingMode, + PixelAlphaCompositionMode alphaCompositionMode, + float opacity, + int foregroundRepeatCount) + : this(foreground, backgroundLocation, foreground.Bounds, colorBlendingMode, alphaCompositionMode, opacity, foregroundRepeatCount) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The image to blend. + /// The location to draw the foreground image on the background. + /// The rectangular portion of the foreground image to draw. + /// The blending mode to use when drawing the image. + /// The Alpha blending mode to use when drawing the image. + /// The opacity of the image to blend. + /// The number of times the foreground frames are allowed to loop. 0 means infinitely. + public DrawImageProcessor( + Image foreground, + Point backgroundLocation, + Rectangle foregroundRectangle, + PixelColorBlendingMode colorBlendingMode, + PixelAlphaCompositionMode alphaCompositionMode, + float opacity, + int foregroundRepeatCount) + { + this.ForeGround = foreground; + this.BackgroundLocation = backgroundLocation; + this.ForegroundRectangle = foregroundRectangle; + this.ColorBlendingMode = colorBlendingMode; + this.AlphaCompositionMode = alphaCompositionMode; + this.Opacity = opacity; + this.ForegroundRepeatCount = foregroundRepeatCount; + } + + /// + /// Gets the image to blend. + /// + public Image ForeGround { get; } + + /// + /// Gets the location to draw the foreground image on the background. + /// + public Point BackgroundLocation { get; } + + /// + /// Gets the rectangular portion of the foreground image to draw. + /// + public Rectangle ForegroundRectangle { get; } + + /// + /// Gets the blending mode to use when drawing the image. + /// + public PixelColorBlendingMode ColorBlendingMode { get; } + + /// + /// Gets the Alpha blending mode to use when drawing the image. + /// + public PixelAlphaCompositionMode AlphaCompositionMode { get; } + + /// + /// Gets the opacity of the image to blend. + /// + public float Opacity { get; } + + /// + /// Gets the number of times the foreground frames are allowed to loop. 0 means infinitely. + /// + public int ForegroundRepeatCount { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixelBg : unmanaged, IPixel + { + ProcessorFactoryVisitor visitor = new(configuration, this, source); + this.ForeGround.AcceptVisitor(visitor); + return visitor.Result!; + } + + private class ProcessorFactoryVisitor : IImageVisitor + where TPixelBg : unmanaged, IPixel + { + private readonly Configuration configuration; + private readonly DrawImageProcessor definition; + private readonly Image source; + + public ProcessorFactoryVisitor( + Configuration configuration, + DrawImageProcessor definition, + Image source) + { + this.configuration = configuration; + this.definition = definition; + this.source = source; + } + + public IImageProcessor? Result { get; private set; } + + public void Visit(Image image) + where TPixelFg : unmanaged, IPixel + => this.Result = new DrawImageProcessor( + this.configuration, + image, + this.source, + this.definition.BackgroundLocation, + this.definition.ForegroundRectangle, + this.definition.ColorBlendingMode, + this.definition.AlphaCompositionMode, + this.definition.Opacity, + this.definition.ForegroundRepeatCount); + } + } +} diff --git a/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs b/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs new file mode 100644 index 0000000..be5376d --- /dev/null +++ b/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs @@ -0,0 +1,211 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Drawing { + /// + /// Combines two images together by blending the pixels. + /// + /// The pixel format of destination image. + /// The pixel format of source image. + internal class DrawImageProcessor : ImageProcessor + where TPixelBg : unmanaged, IPixel + where TPixelFg : unmanaged, IPixel + { + /// + /// Counts how many times has been called for this processor instance. + /// Used to select the current foreground frame. + /// + private int foregroundFrameCounter; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The foreground to blend with the currently processing image. + /// The source for the current processor instance. + /// The location to draw the blended image. + /// The source area to process for the current processor instance. + /// The blending mode to use when drawing the image. + /// The alpha blending mode to use when drawing the image. + /// The opacity of the image to blend. Must be between 0 and 1. + /// + /// The number of times the foreground frames are allowed to loop while applying this processor across successive frames. + /// A value of 0 means loop indefinitely. + /// + public DrawImageProcessor( + Configuration configuration, + Image foregroundImage, + Image backgroundImage, + Point backgroundLocation, + Rectangle foregroundRectangle, + PixelColorBlendingMode colorBlendingMode, + PixelAlphaCompositionMode alphaCompositionMode, + float opacity, + int foregroundRepeatCount) + : base(configuration, backgroundImage, backgroundImage.Bounds) + { + Guard.MustBeGreaterThanOrEqualTo(foregroundRepeatCount, 0, nameof(foregroundRepeatCount)); + Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity)); + + this.ForegroundImage = foregroundImage; + this.ForegroundRectangle = foregroundRectangle; + this.Opacity = opacity; + this.Blender = PixelOperations.Instance.GetPixelBlender(colorBlendingMode, alphaCompositionMode); + this.BackgroundLocation = backgroundLocation; + this.ForegroundRepeatCount = foregroundRepeatCount; + } + + /// + /// Gets the image to blend + /// + public Image ForegroundImage { get; } + + /// + /// Gets the rectangular portion of the foreground image to draw. + /// + public Rectangle ForegroundRectangle { get; } + + /// + /// Gets the opacity of the image to blend + /// + public float Opacity { get; } + + /// + /// Gets the pixel blender + /// + public PixelBlender Blender { get; } + + /// + /// Gets the location to draw the blended image + /// + public Point BackgroundLocation { get; } + + /// + /// Gets the number of times the foreground frames are allowed to loop while applying this processor across + /// successive frames. A value of 0 means loop indefinitely. + /// + public int ForegroundRepeatCount { get; } + + /// + protected override void OnFrameApply(ImageFrame source) + { + // Align the bounds so that both the source and targets are the same width and height for blending. + // We ensure that negative locations are subtracted from both bounds so that foreground images can partially overlap. + Rectangle foregroundRectangle = this.ForegroundRectangle; + + // Sanitize the location so that we don't try and sample outside the image. + int left = this.BackgroundLocation.X; + int top = this.BackgroundLocation.Y; + + if (this.BackgroundLocation.X < 0) + { + foregroundRectangle.Width += this.BackgroundLocation.X; + foregroundRectangle.X -= this.BackgroundLocation.X; + left = 0; + } + + if (this.BackgroundLocation.Y < 0) + { + foregroundRectangle.Height += this.BackgroundLocation.Y; + foregroundRectangle.Y -= this.BackgroundLocation.Y; + top = 0; + } + + // Clamp the height/width to the available space left to prevent overflowing + foregroundRectangle.Width = Math.Min(source.Width - left, foregroundRectangle.Width); + foregroundRectangle.Height = Math.Min(source.Height - top, foregroundRectangle.Height); + foregroundRectangle = Rectangle.Intersect(foregroundRectangle, this.ForegroundImage.Bounds); + + int width = foregroundRectangle.Width; + int height = foregroundRectangle.Height; + if (width <= 0 || height <= 0) + { + // Nothing to do, return. + return; + } + + // Sanitize the dimensions so that we don't try and sample outside the image. + Rectangle backgroundRectangle = Rectangle.Intersect(new Rectangle(left, top, width, height), this.SourceRectangle); + Configuration configuration = this.Configuration; + int currentFrameIndex = this.foregroundFrameCounter % this.ForegroundImage.Frames.Count; + + RowOperation operation = + new( + configuration, + source.PixelBuffer, + this.ForegroundImage.Frames[currentFrameIndex].PixelBuffer, + backgroundRectangle, + foregroundRectangle, + this.Blender, + this.Opacity); + + ParallelRowIterator.IterateRows( + configuration, + new Rectangle(0, 0, foregroundRectangle.Width, foregroundRectangle.Height), + in operation); + + // The repeat count only affects how the foreground frame advances across successive background frames. + // When exhausted, the selected foreground frame stops advancing. + if (this.ForegroundRepeatCount is 0 || this.foregroundFrameCounter / this.ForegroundImage.Frames.Count < this.ForegroundRepeatCount) + { + this.foregroundFrameCounter++; + } + } + + /// + /// A implementing the draw logic for . + /// + private readonly struct RowOperation : IRowOperation + { + private readonly Buffer2D background; + private readonly Buffer2D foreground; + private readonly PixelBlender blender; + private readonly Configuration configuration; + private readonly Rectangle foregroundRectangle; + private readonly Rectangle backgroundRectangle; + private readonly float opacity; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperation( + Configuration configuration, + Buffer2D background, + Buffer2D foreground, + Rectangle backgroundRectangle, + Rectangle foregroundRectangle, + PixelBlender blender, + float opacity) + { + this.configuration = configuration; + this.background = background; + this.foreground = foreground; + this.backgroundRectangle = backgroundRectangle; + this.foregroundRectangle = foregroundRectangle; + this.blender = blender; + this.opacity = opacity; + } + + /// + public int GetRequiredBufferLength(Rectangle bounds) + + // By using a dedicated vector span we can avoid per-row pool allocations in PixelBlender.Blend + // We need 3 Vector4 values per pixel to store the background, foreground, and result pixels for blending. + => 3 * bounds.Width; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y, Span span) + { + Span background = this.background.DangerousGetRowSpan(y + this.backgroundRectangle.Top).Slice(this.backgroundRectangle.Left, this.backgroundRectangle.Width); + Span foreground = this.foreground.DangerousGetRowSpan(y + this.foregroundRectangle.Top).Slice(this.foregroundRectangle.Left, this.foregroundRectangle.Width); + this.blender.Blend(this.configuration, background, background, foreground, this.opacity, span); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Effects/IPixelRowDelegate.cs b/ImageSharp/Processing/Processors/Effects/IPixelRowDelegate.cs new file mode 100644 index 0000000..27eaaae --- /dev/null +++ b/ImageSharp/Processing/Processors/Effects/IPixelRowDelegate.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Processing.Processors.Effects { + /// + /// An used by the row delegates for a given instance + /// + public interface IPixelRowDelegate + { + /// + /// Applies the current pixel row delegate to a target row of preprocessed pixels. + /// + /// The target row of pixels to process. + /// The initial horizontal and vertical offset for the input pixels to process. + void Invoke(Span span, Point offset); + } +} diff --git a/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor.cs b/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor.cs new file mode 100644 index 0000000..49dce30 --- /dev/null +++ b/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Effects { + /// + /// Defines an oil painting effect. + /// + public sealed class OilPaintingProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The number of intensity levels. Higher values result in a broader range of color intensities forming part of the result image. + /// + /// + /// The number of neighboring pixels used in calculating each individual pixel value. + /// + public OilPaintingProcessor(int levels, int brushSize) + { + Guard.MustBeGreaterThan(levels, 0, nameof(levels)); + Guard.MustBeGreaterThan(brushSize, 0, nameof(brushSize)); + + this.Levels = levels; + this.BrushSize = brushSize; + } + + /// + /// Gets the number of intensity levels. + /// + public int Levels { get; } + + /// + /// Gets the brush size. + /// + public int BrushSize { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new OilPaintingProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs new file mode 100644 index 0000000..b059209 --- /dev/null +++ b/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs @@ -0,0 +1,188 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Effects { + /// + /// Applies oil painting effect processing to the image. + /// + /// Adapted from by Dewald Esterhuizen. + /// The pixel format. + internal class OilPaintingProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly OilPaintingProcessor definition; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public OilPaintingProcessor(Configuration configuration, OilPaintingProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + => this.definition = definition; + + /// + protected override void OnFrameApply(ImageFrame source) + { + int levels = Math.Clamp(this.definition.Levels, 1, 255); + int brushSize = Math.Clamp(this.definition.BrushSize, 1, Math.Min(source.Width, source.Height)); + + using Buffer2D targetPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Size); + + source.CopyTo(targetPixels); + + RowIntervalOperation operation = new(this.SourceRectangle, targetPixels, source.PixelBuffer, this.Configuration, brushSize >> 1, levels); + try + { + ParallelRowIterator.IterateRowIntervals( + this.Configuration, + this.SourceRectangle, + in operation); + } + catch (Exception ex) + { + throw new ImageProcessingException("The OilPaintProcessor failed. The most likely reason is that a pixel component was outside of its' allowed range.", ex); + } + + Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); + } + + /// + /// A implementing the convolution logic for . + /// + private readonly struct RowIntervalOperation : IRowIntervalOperation + { + private readonly Rectangle bounds; + private readonly Buffer2D targetPixels; + private readonly Buffer2D source; + private readonly Configuration configuration; + private readonly int radius; + private readonly int levels; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowIntervalOperation( + Rectangle bounds, + Buffer2D targetPixels, + Buffer2D source, + Configuration configuration, + int radius, + int levels) + { + this.bounds = bounds; + this.targetPixels = targetPixels; + this.source = source; + this.configuration = configuration; + this.radius = radius; + this.levels = levels; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(in RowInterval rows) + { + int maxY = this.bounds.Bottom - 1; + int maxX = this.bounds.Right - 1; + + /* Allocate the two temporary Vector4 buffers, one for the source row and one for the target row. + * The ParallelHelper.IterateRowsWithTempBuffers overload is not used in this case because + * the two allocated buffers have a length equal to the width of the source image, + * and not just equal to the width of the target rectangle to process. + * Furthermore, there are two buffers being allocated in this case, so using that overload would + * have still required the explicit allocation of the secondary buffer. + * Similarly, one temporary float buffer is also allocated from the pool, and that is used + * to create the target bins for all the color channels being processed. + * This buffer is only rented once outside of the main processing loop, and its contents + * are cleared for each loop iteration, to avoid the repeated allocation for each processed pixel. */ + using IMemoryOwner sourceRowBuffer = this.configuration.MemoryAllocator.Allocate(this.source.Width); + using IMemoryOwner targetRowBuffer = this.configuration.MemoryAllocator.Allocate(this.source.Width); + using IMemoryOwner bins = this.configuration.MemoryAllocator.Allocate(this.levels * 4); + + Span sourceRowVector4Span = sourceRowBuffer.Memory.Span; + Span sourceRowAreaVector4Span = sourceRowVector4Span.Slice(this.bounds.X, this.bounds.Width); + + Span targetRowVector4Span = targetRowBuffer.Memory.Span; + Span targetRowAreaVector4Span = targetRowVector4Span.Slice(this.bounds.X, this.bounds.Width); + + Span binsSpan = bins.GetSpan(); + Span intensityBinsSpan = MemoryMarshal.Cast(binsSpan); + Span redBinSpan = binsSpan[this.levels..]; + Span blueBinSpan = redBinSpan[this.levels..]; + Span greenBinSpan = blueBinSpan[this.levels..]; + + for (int y = rows.Min; y < rows.Max; y++) + { + Span sourceRowPixelSpan = this.source.DangerousGetRowSpan(y); + Span sourceRowAreaPixelSpan = sourceRowPixelSpan.Slice(this.bounds.X, this.bounds.Width); + + PixelOperations.Instance.ToVector4(this.configuration, sourceRowAreaPixelSpan, sourceRowAreaVector4Span, PixelConversionModifiers.Scale); + + for (int x = this.bounds.X; x < this.bounds.Right; x++) + { + int maxIntensity = 0; + int maxIndex = 0; + + // Clear the current shared buffer before processing each target pixel + bins.Memory.Span.Clear(); + + for (int fy = 0; fy <= this.radius; fy++) + { + int fyr = fy - this.radius; + int offsetY = y + fyr; + offsetY = Numerics.Clamp(offsetY, 0, maxY); + + Span sourceOffsetRow = this.source.DangerousGetRowSpan(offsetY); + + for (int fx = 0; fx <= this.radius; fx++) + { + int fxr = fx - this.radius; + int offsetX = x + fxr; + offsetX = Numerics.Clamp(offsetX, 0, maxX); + + Vector4 vector = sourceOffsetRow[offsetX].ToScaledVector4(); + + float sourceRed = vector.X; + float sourceBlue = vector.Z; + float sourceGreen = vector.Y; + + int currentIntensity = (int)MathF.Round((sourceBlue + sourceGreen + sourceRed) / 3F * (this.levels - 1)); + + intensityBinsSpan[currentIntensity]++; + redBinSpan[currentIntensity] += sourceRed; + blueBinSpan[currentIntensity] += sourceBlue; + greenBinSpan[currentIntensity] += sourceGreen; + + if (intensityBinsSpan[currentIntensity] > maxIntensity) + { + maxIntensity = intensityBinsSpan[currentIntensity]; + maxIndex = currentIntensity; + } + } + + float red = redBinSpan[maxIndex] / maxIntensity; + float blue = blueBinSpan[maxIndex] / maxIntensity; + float green = greenBinSpan[maxIndex] / maxIntensity; + float alpha = sourceRowVector4Span[x].W; + + targetRowVector4Span[x] = new Vector4(red, green, blue, alpha); + } + } + + Span targetRowAreaPixelSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); + + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetRowAreaVector4Span, targetRowAreaPixelSpan, PixelConversionModifiers.Scale); + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor.cs b/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor.cs new file mode 100644 index 0000000..69a5fe4 --- /dev/null +++ b/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Effects { + /// + /// Applies a user defined row processing delegate to the image. + /// + internal sealed class PixelRowDelegateProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The user defined, row processing delegate. + /// The to apply during the pixel conversions. + public PixelRowDelegateProcessor(PixelRowOperation pixelRowOperation, PixelConversionModifiers modifiers) + { + this.PixelRowOperation = pixelRowOperation; + this.Modifiers = modifiers; + } + + /// + /// Gets the user defined row processing delegate to the image. + /// + public PixelRowOperation PixelRowOperation { get; } + + /// + /// Gets the to apply during the pixel conversions. + /// + public PixelConversionModifiers Modifiers { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new PixelRowDelegateProcessor( + new PixelRowDelegate(this.PixelRowOperation), + configuration, + this.Modifiers, + source, + sourceRectangle); + + /// + /// A implementing the row processing logic for . + /// + public readonly struct PixelRowDelegate : IPixelRowDelegate + { + private readonly PixelRowOperation pixelRowOperation; + + [MethodImpl(InliningOptions.ShortMethod)] + public PixelRowDelegate(PixelRowOperation pixelRowOperation) + => this.pixelRowOperation = pixelRowOperation; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(Span span, Point offset) => this.pixelRowOperation(span); + } + } +} diff --git a/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs b/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs new file mode 100644 index 0000000..cdd989f --- /dev/null +++ b/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs @@ -0,0 +1,105 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Effects { + /// + /// The base class for all processors that accept a user defined row processing delegate. + /// + /// The pixel format. + /// The row processor type. + internal sealed class PixelRowDelegateProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + where TDelegate : struct, IPixelRowDelegate + { + private readonly TDelegate rowDelegate; + + /// + /// The to apply during the pixel conversions. + /// + private readonly PixelConversionModifiers modifiers; + + /// + /// Initializes a new instance of the class. + /// + /// The row processor to use to process each pixel row + /// The configuration which allows altering default behaviour or extending the library. + /// The to apply during the pixel conversions. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public PixelRowDelegateProcessor( + in TDelegate rowDelegate, + Configuration configuration, + PixelConversionModifiers modifiers, + Image source, + Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.rowDelegate = rowDelegate; + this.modifiers = modifiers; + } + + /// + protected override void OnFrameApply(ImageFrame source) + { + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + RowOperation operation = new(interest.X, source.PixelBuffer, this.Configuration, this.modifiers, this.rowDelegate); + + ParallelRowIterator.IterateRows( + this.Configuration, + interest, + in operation); + } + + /// + /// A implementing the convolution logic for . + /// + private readonly struct RowOperation : IRowOperation + { + private readonly int startX; + private readonly Buffer2D source; + private readonly Configuration configuration; + private readonly PixelConversionModifiers modifiers; + private readonly TDelegate rowProcessor; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperation( + int startX, + Buffer2D source, + Configuration configuration, + PixelConversionModifiers modifiers, + in TDelegate rowProcessor) + { + this.startX = startX; + this.source = source; + this.configuration = configuration; + this.modifiers = modifiers; + this.rowProcessor = rowProcessor; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) + => bounds.Width; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y, Span span) + { + Span rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length); + PixelOperations.Instance.ToVector4(this.configuration, rowSpan, span, this.modifiers); + + // Run the user defined pixel shader to the current row of pixels + Unsafe.AsRef(in this.rowProcessor).Invoke(span, new Point(this.startX, y)); + + PixelOperations.Instance.FromVector4Destructive(this.configuration, span, rowSpan, this.modifiers); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Effects/PixelateProcessor.cs b/ImageSharp/Processing/Processors/Effects/PixelateProcessor.cs new file mode 100644 index 0000000..48c455d --- /dev/null +++ b/ImageSharp/Processing/Processors/Effects/PixelateProcessor.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Effects { + /// + /// Defines a pixelation effect of a given size. + /// + public sealed class PixelateProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The size of the pixels. Must be greater than 0. + /// + /// is less than 0 or equal to 0. + /// + public PixelateProcessor(int size) + { + Guard.MustBeGreaterThan(size, 0, nameof(size)); + this.Size = size; + } + + /// + /// Gets or the pixel size. + /// + public int Size { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new PixelateProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs new file mode 100644 index 0000000..f13c6f3 --- /dev/null +++ b/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Effects { + /// + /// Applies a pixelation effect processing to the image. + /// + /// The pixel format. + internal class PixelateProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly PixelateProcessor definition; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The . + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public PixelateProcessor(Configuration configuration, PixelateProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + => this.definition = definition; + + private int Size => this.definition.Size; + + /// + protected override void OnFrameApply(ImageFrame source) + { + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + int size = this.Size; + + Guard.MustBeBetweenOrEqualTo(size, 0, interest.Width, nameof(size)); + Guard.MustBeBetweenOrEqualTo(size, 0, interest.Height, nameof(size)); + + // Get the range on the y-plane to choose from. + // TODO: It would be nice to be able to pool this somehow but neither Memory nor Span + // implement IEnumerable. + IEnumerable range = EnumerableExtensions.SteppedRange(interest.Y, i => i < interest.Bottom, size); + Parallel.ForEach( + range, + this.Configuration.GetParallelOptions(), + new RowOperation(interest, size, source.PixelBuffer).Invoke); + } + + private readonly struct RowOperation + { + private readonly int minX; + private readonly int maxX; + private readonly int maxXIndex; + private readonly int maxY; + private readonly int maxYIndex; + private readonly int size; + private readonly int radius; + private readonly Buffer2D source; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperation( + Rectangle bounds, + int size, + Buffer2D source) + { + this.minX = bounds.X; + this.maxX = bounds.Right; + this.maxXIndex = bounds.Right - 1; + this.maxY = bounds.Bottom; + this.maxYIndex = bounds.Bottom - 1; + this.size = size; + this.radius = size >> 1; + this.source = source; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y) + { + Span rowSpan = this.source.DangerousGetRowSpan(Math.Min(y + this.radius, this.maxYIndex)); + + for (int x = this.minX; x < this.maxX; x += this.size) + { + // Get the pixel color in the centre of the soon to be pixelated area. + TPixel pixel = rowSpan[Math.Min(x + this.radius, this.maxXIndex)]; + + // For each pixel in the pixelate size, set it to the centre color. + for (int oY = y; oY < y + this.size && oY < this.maxY; oY++) + { + for (int oX = x; oX < x + this.size && oX < this.maxX; oX++) + { + this.source[oX, oY] = pixel; + } + } + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Effects/PositionAwarePixelRowDelegateProcessor.cs b/ImageSharp/Processing/Processors/Effects/PositionAwarePixelRowDelegateProcessor.cs new file mode 100644 index 0000000..60f909f --- /dev/null +++ b/ImageSharp/Processing/Processors/Effects/PositionAwarePixelRowDelegateProcessor.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Effects { + /// + /// Applies a user defined, position aware, row processing delegate to the image. + /// + internal sealed class PositionAwarePixelRowDelegateProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The user defined, position aware, row processing delegate. + /// The to apply during the pixel conversions. + public PositionAwarePixelRowDelegateProcessor(PixelRowOperation pixelRowOperation, PixelConversionModifiers modifiers) + { + this.PixelRowOperation = pixelRowOperation; + this.Modifiers = modifiers; + } + + /// + /// Gets the user defined, position aware, row processing delegate. + /// + public PixelRowOperation PixelRowOperation { get; } + + /// + /// Gets the to apply during the pixel conversions. + /// + public PixelConversionModifiers Modifiers { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + { + return new PixelRowDelegateProcessor( + new PixelRowDelegate(this.PixelRowOperation), + configuration, + this.Modifiers, + source, + sourceRectangle); + } + + /// + /// A implementing the row processing logic for . + /// + public readonly struct PixelRowDelegate : IPixelRowDelegate + { + private readonly PixelRowOperation pixelRowOperation; + + [MethodImpl(InliningOptions.ShortMethod)] + public PixelRowDelegate(PixelRowOperation pixelRowOperation) + { + this.pixelRowOperation = pixelRowOperation; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(Span span, Point offset) => this.pixelRowOperation(span, offset); + } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/AchromatomalyProcessor.cs b/ImageSharp/Processing/Processors/Filters/AchromatomalyProcessor.cs new file mode 100644 index 0000000..f798a56 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/AchromatomalyProcessor.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Converts the colors of the image recreating Achromatomaly (Color desensitivity) color blindness. + /// + public sealed class AchromatomalyProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + public AchromatomalyProcessor() + : base(KnownFilterMatrices.AchromatomalyFilter) + { + } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/AchromatopsiaProcessor.cs b/ImageSharp/Processing/Processors/Filters/AchromatopsiaProcessor.cs new file mode 100644 index 0000000..16b3571 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/AchromatopsiaProcessor.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Converts the colors of the image recreating Achromatopsia (Monochrome) color blindness. + /// + public sealed class AchromatopsiaProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + public AchromatopsiaProcessor() + : base(KnownFilterMatrices.AchromatopsiaFilter) + { + } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/BlackWhiteProcessor.cs b/ImageSharp/Processing/Processors/Filters/BlackWhiteProcessor.cs new file mode 100644 index 0000000..67fa9c4 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/BlackWhiteProcessor.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Applies a black and white filter matrix to the image. + /// + public sealed class BlackWhiteProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + public BlackWhiteProcessor() + : base(KnownFilterMatrices.BlackWhiteFilter) + { + } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/BrightnessProcessor.cs b/ImageSharp/Processing/Processors/Filters/BrightnessProcessor.cs new file mode 100644 index 0000000..276c6c1 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/BrightnessProcessor.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Applies a brightness filter matrix using the given amount. + /// + public sealed class BrightnessProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// + /// A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing brighter results. + /// + /// The proportion of the conversion. Must be greater than or equal to 0. + public BrightnessProcessor(float amount) + : base(KnownFilterMatrices.CreateBrightnessFilter(amount)) + { + this.Amount = amount; + } + + /// + /// Gets the proportion of the conversion + /// + public float Amount { get; } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/ContrastProcessor.cs b/ImageSharp/Processing/Processors/Filters/ContrastProcessor.cs new file mode 100644 index 0000000..6fe0a75 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/ContrastProcessor.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Applies a contrast filter matrix using the given amount. + /// + public sealed class ContrastProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// + /// A value of 0 will create an image that is completely gray. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing results with more contrast. + /// + /// The proportion of the conversion. Must be greater than or equal to 0. + public ContrastProcessor(float amount) + : base(KnownFilterMatrices.CreateContrastFilter(amount)) + { + this.Amount = amount; + } + + /// + /// Gets the proportion of the conversion. + /// + public float Amount { get; } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/DeuteranomalyProcessor.cs b/ImageSharp/Processing/Processors/Filters/DeuteranomalyProcessor.cs new file mode 100644 index 0000000..2fc5835 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/DeuteranomalyProcessor.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Converts the colors of the image recreating Deuteranomaly (Green-Weak) color blindness. + /// + public sealed class DeuteranomalyProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + public DeuteranomalyProcessor() + : base(KnownFilterMatrices.DeuteranomalyFilter) + { + } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/DeuteranopiaProcessor.cs b/ImageSharp/Processing/Processors/Filters/DeuteranopiaProcessor.cs new file mode 100644 index 0000000..71c6ff0 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/DeuteranopiaProcessor.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Converts the colors of the image recreating Deuteranopia (Green-Blind) color blindness. + /// + public sealed class DeuteranopiaProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + public DeuteranopiaProcessor() + : base(KnownFilterMatrices.DeuteranopiaFilter) + { + } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs b/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs new file mode 100644 index 0000000..2261f74 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Defines a free-form color filter by a . + /// + public class FilterProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The matrix used to apply the image filter + public FilterProcessor(ColorMatrix matrix) => this.Matrix = matrix; + + /// + /// Gets the used to apply the image filter. + /// + public ColorMatrix Matrix { get; } + + /// + public virtual IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new FilterProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs new file mode 100644 index 0000000..3c79cbf --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Provides methods that accept a matrix to apply free-form filters to images. + /// + /// The pixel format. + internal class FilterProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly FilterProcessor definition; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The . + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public FilterProcessor(Configuration configuration, FilterProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + => this.definition = definition; + + /// + protected override void OnFrameApply(ImageFrame source) + { + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + RowOperation operation = new(interest.X, source.PixelBuffer, this.definition.Matrix, this.Configuration); + + ParallelRowIterator.IterateRows( + this.Configuration, + interest, + in operation); + } + + /// + /// A implementing the convolution logic for . + /// + private readonly struct RowOperation : IRowOperation + { + private readonly int startX; + private readonly Buffer2D source; + private readonly ColorMatrix matrix; + private readonly Configuration configuration; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperation( + int startX, + Buffer2D source, + ColorMatrix matrix, + Configuration configuration) + { + this.startX = startX; + this.source = source; + this.matrix = matrix; + this.configuration = configuration; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) + => bounds.Width; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y, Span span) + { + Span rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length); + PixelOperations.Instance.ToVector4(this.configuration, rowSpan, span, PixelConversionModifiers.Scale); + + ColorNumerics.Transform(span, ref Unsafe.AsRef(in this.matrix)); + + PixelOperations.Instance.FromVector4Destructive(this.configuration, span, rowSpan, PixelConversionModifiers.Scale); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/GrayscaleBt601Processor.cs b/ImageSharp/Processing/Processors/Filters/GrayscaleBt601Processor.cs new file mode 100644 index 0000000..ec0252f --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/GrayscaleBt601Processor.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Applies a grayscale filter matrix using the given amount and the formula as specified by ITU-R Recommendation BT.601 + /// + public sealed class GrayscaleBt601Processor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The proportion of the conversion. Must be between 0 and 1. + public GrayscaleBt601Processor(float amount) + : base(KnownFilterMatrices.CreateGrayscaleBt601Filter(amount)) + { + this.Amount = amount; + } + + /// + /// Gets the proportion of the conversion + /// + public float Amount { get; } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/GrayscaleBt709Processor.cs b/ImageSharp/Processing/Processors/Filters/GrayscaleBt709Processor.cs new file mode 100644 index 0000000..944a5a5 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/GrayscaleBt709Processor.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Applies a grayscale filter matrix using the given amount and the formula as specified by ITU-R Recommendation BT.709 + /// + public sealed class GrayscaleBt709Processor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The proportion of the conversion. Must be between 0 and 1. + public GrayscaleBt709Processor(float amount) + : base(KnownFilterMatrices.CreateGrayscaleBt709Filter(amount)) + { + this.Amount = amount; + } + + /// + /// Gets the proportion of the conversion. + /// + public float Amount { get; } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/HueProcessor.cs b/ImageSharp/Processing/Processors/Filters/HueProcessor.cs new file mode 100644 index 0000000..b527ccd --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/HueProcessor.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Applies a hue filter matrix using the given angle of rotation in degrees + /// + public sealed class HueProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The angle of rotation in degrees + public HueProcessor(float degrees) + : base(KnownFilterMatrices.CreateHueFilter(degrees)) + { + this.Degrees = degrees; + } + + /// + /// Gets the angle of rotation in degrees + /// + public float Degrees { get; } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/InvertProcessor.cs b/ImageSharp/Processing/Processors/Filters/InvertProcessor.cs new file mode 100644 index 0000000..c17ebc6 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/InvertProcessor.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Applies a filter matrix that inverts the colors of an image + /// + public sealed class InvertProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The proportion of the conversion. Must be between 0 and 1. + public InvertProcessor(float amount) + : base(KnownFilterMatrices.CreateInvertFilter(amount)) + { + this.Amount = amount; + } + + /// + /// Gets the proportion of the conversion + /// + public float Amount { get; } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/KodachromeProcessor.cs b/ImageSharp/Processing/Processors/Filters/KodachromeProcessor.cs new file mode 100644 index 0000000..d8dbb4b --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/KodachromeProcessor.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Applies a filter matrix recreating an old Kodachrome camera effect matrix to the image + /// + public sealed class KodachromeProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + public KodachromeProcessor() + : base(KnownFilterMatrices.KodachromeFilter) + { + } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/LightnessProcessor.cs b/ImageSharp/Processing/Processors/Filters/LightnessProcessor.cs new file mode 100644 index 0000000..49fabf7 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/LightnessProcessor.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Applies a lightness filter matrix using the given amount. + /// + public sealed class LightnessProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// + /// A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing lighter results. + /// + /// The proportion of the conversion. Must be greater than or equal to 0. + public LightnessProcessor(float amount) + : base(KnownFilterMatrices.CreateLightnessFilter(amount)) + { + this.Amount = amount; + } + + /// + /// Gets the proportion of the conversion + /// + public float Amount { get; } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/LomographProcessor.cs b/ImageSharp/Processing/Processors/Filters/LomographProcessor.cs new file mode 100644 index 0000000..baa7498 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/LomographProcessor.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Converts the colors of the image recreating an old Lomograph effect. + /// + public sealed class LomographProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// Graphics options to use within the processor. + public LomographProcessor(GraphicsOptions graphicsOptions) + : base(KnownFilterMatrices.LomographFilter) + { + this.GraphicsOptions = graphicsOptions; + } + + /// + /// Gets the options effecting blending and composition + /// + public GraphicsOptions GraphicsOptions { get; } + + /// + public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) => + new LomographProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Filters/LomographProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Filters/LomographProcessor{TPixel}.cs new file mode 100644 index 0000000..a602589 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/LomographProcessor{TPixel}.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Overlays; + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Converts the colors of the image recreating an old Lomograph effect. + /// + internal class LomographProcessor : FilterProcessor + where TPixel : unmanaged, IPixel + { + private static readonly Color VeryDarkGreen = Color.FromPixel(new Rgba32(0, 10, 0, 255)); + private readonly LomographProcessor definition; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public LomographProcessor(Configuration configuration, LomographProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, definition, source, sourceRectangle) + { + this.definition = definition; + } + + /// + protected override void AfterImageApply() + { + new VignetteProcessor(this.definition.GraphicsOptions, VeryDarkGreen).Execute(this.Configuration, this.Source, this.SourceRectangle); + base.AfterImageApply(); + } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/OpacityProcessor.cs b/ImageSharp/Processing/Processors/Filters/OpacityProcessor.cs new file mode 100644 index 0000000..1cecd54 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/OpacityProcessor.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Applies an opacity filter matrix using the given amount. + /// + public sealed class OpacityProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The proportion of the conversion. Must be between 0 and 1. + public OpacityProcessor(float amount) + : base(KnownFilterMatrices.CreateOpacityFilter(amount)) + { + this.Amount = amount; + } + + /// + /// Gets the proportion of the conversion. + /// + public float Amount { get; } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs new file mode 100644 index 0000000..14fefc1 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + internal sealed class OpaqueProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + public OpaqueProcessor( + Configuration configuration, + Image source, + Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + } + + protected override void OnFrameApply(ImageFrame source) + { + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + + OpaqueRowOperation operation = new(this.Configuration, source.PixelBuffer, interest); + ParallelRowIterator.IterateRows(this.Configuration, interest, in operation); + } + + private readonly struct OpaqueRowOperation : IRowOperation + { + private readonly Configuration configuration; + private readonly Buffer2D target; + private readonly Rectangle bounds; + + [MethodImpl(InliningOptions.ShortMethod)] + public OpaqueRowOperation( + Configuration configuration, + Buffer2D target, + Rectangle bounds) + { + this.configuration = configuration; + this.target = target; + this.bounds = bounds; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) + => bounds.Width; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y, Span span) + { + Span targetRowSpan = this.target.DangerousGetRowSpan(y)[this.bounds.X..]; + PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan[..span.Length], span, PixelConversionModifiers.Scale); + ref Vector4 baseRef = ref MemoryMarshal.GetReference(span); + + for (int x = 0; x < this.bounds.Width; x++) + { + ref Vector4 v = ref Unsafe.Add(ref baseRef, (uint)x); + v.W = 1F; + } + + PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan, PixelConversionModifiers.Scale); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/PolaroidProcessor.cs b/ImageSharp/Processing/Processors/Filters/PolaroidProcessor.cs new file mode 100644 index 0000000..9ef0c99 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/PolaroidProcessor.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Converts the colors of the image recreating an old Polaroid effect. + /// + public sealed class PolaroidProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// Graphics options to use within the processor. + public PolaroidProcessor(GraphicsOptions graphicsOptions) + : base(KnownFilterMatrices.PolaroidFilter) + { + this.GraphicsOptions = graphicsOptions; + } + + /// + /// Gets the options effecting blending and composition + /// + public GraphicsOptions GraphicsOptions { get; } + + /// + public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) => + new PolaroidProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Filters/PolaroidProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Filters/PolaroidProcessor{TPixel}.cs new file mode 100644 index 0000000..2a74059 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/PolaroidProcessor{TPixel}.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Overlays; + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Converts the colors of the image recreating an old Polaroid effect. + /// + internal class PolaroidProcessor : FilterProcessor + where TPixel : unmanaged, IPixel + { + private static readonly Color LightOrange = Color.FromPixel(new Rgba32(255, 153, 102, 128)); + private static readonly Color VeryDarkOrange = Color.FromPixel(new Rgb24(102, 34, 0)); + private readonly PolaroidProcessor definition; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public PolaroidProcessor(Configuration configuration, PolaroidProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, definition, source, sourceRectangle) + { + this.definition = definition; + } + + /// + protected override void AfterImageApply() + { + new VignetteProcessor(this.definition.GraphicsOptions, VeryDarkOrange).Execute(this.Configuration, this.Source, this.SourceRectangle); + new GlowProcessor(this.definition.GraphicsOptions, LightOrange, this.Source.Width / 4F).Execute(this.Configuration, this.Source, this.SourceRectangle); + base.AfterImageApply(); + } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/ProtanomalyProcessor.cs b/ImageSharp/Processing/Processors/Filters/ProtanomalyProcessor.cs new file mode 100644 index 0000000..b2c1f77 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/ProtanomalyProcessor.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Converts the colors of the image recreating Protanomaly (Red-Weak) color blindness. + /// + public sealed class ProtanomalyProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + public ProtanomalyProcessor() + : base(KnownFilterMatrices.ProtanomalyFilter) + { + } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/ProtanopiaProcessor.cs b/ImageSharp/Processing/Processors/Filters/ProtanopiaProcessor.cs new file mode 100644 index 0000000..9c85681 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/ProtanopiaProcessor.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Converts the colors of the image recreating Protanopia (Red-Blind) color blindness. + /// + public sealed class ProtanopiaProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + public ProtanopiaProcessor() + : base(KnownFilterMatrices.ProtanopiaFilter) + { + } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/README.md b/ImageSharp/Processing/Processors/Filters/README.md new file mode 100644 index 0000000..209f3b6 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/README.md @@ -0,0 +1,4 @@ +Color blindness matrices adapted from and tested against: + +http://web.archive.org/web/20090413045433/http://nofunc.org/Color_Matrix_Library +http://www.color-blindness.com/coblis-color-blindness-simulator/ \ No newline at end of file diff --git a/ImageSharp/Processing/Processors/Filters/SaturateProcessor.cs b/ImageSharp/Processing/Processors/Filters/SaturateProcessor.cs new file mode 100644 index 0000000..1499fb5 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/SaturateProcessor.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Applies a saturation filter matrix using the given amount. + /// + public sealed class SaturateProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// + /// A value of 0 is completely un-saturated. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of amount over 1 are allowed, providing super-saturated results + /// + /// The proportion of the conversion. Must be greater than or equal to 0. + public SaturateProcessor(float amount) + : base(KnownFilterMatrices.CreateSaturateFilter(amount)) + { + this.Amount = amount; + } + + /// + /// Gets the proportion of the conversion + /// + public float Amount { get; } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/SepiaProcessor.cs b/ImageSharp/Processing/Processors/Filters/SepiaProcessor.cs new file mode 100644 index 0000000..d6cd7f5 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/SepiaProcessor.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Applies a sepia filter matrix using the given amount. + /// + public sealed class SepiaProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The proportion of the conversion. Must be between 0 and 1. + public SepiaProcessor(float amount) + : base(KnownFilterMatrices.CreateSepiaFilter(amount)) + { + this.Amount = amount; + } + + /// + /// Gets the proportion of the conversion + /// + public float Amount { get; } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/TritanomalyProcessor.cs b/ImageSharp/Processing/Processors/Filters/TritanomalyProcessor.cs new file mode 100644 index 0000000..025d79c --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/TritanomalyProcessor.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Converts the colors of the image recreating Tritanomaly (Blue-Weak) color blindness. + /// + public sealed class TritanomalyProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + public TritanomalyProcessor() + : base(KnownFilterMatrices.TritanomalyFilter) + { + } + } +} diff --git a/ImageSharp/Processing/Processors/Filters/TritanopiaProcessor.cs b/ImageSharp/Processing/Processors/Filters/TritanopiaProcessor.cs new file mode 100644 index 0000000..72e82c4 --- /dev/null +++ b/ImageSharp/Processing/Processors/Filters/TritanopiaProcessor.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Filters { + /// + /// Converts the colors of the image recreating Tritanopia (Blue-Blind) color blindness. + /// + public sealed class TritanopiaProcessor : FilterProcessor + { + /// + /// Initializes a new instance of the class. + /// + public TritanopiaProcessor() + : base(KnownFilterMatrices.TritanopiaFilter) + { + } + } +} diff --git a/ImageSharp/Processing/Processors/ICloningImageProcessor.cs b/ImageSharp/Processing/Processors/ICloningImageProcessor.cs new file mode 100644 index 0000000..99dec18 --- /dev/null +++ b/ImageSharp/Processing/Processors/ICloningImageProcessor.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors { + /// + /// Defines an algorithm to alter the pixels of a cloned image. + /// + public interface ICloningImageProcessor : IImageProcessor + { + /// + /// Creates a pixel specific that is capable of executing + /// the processing algorithm on an . + /// + /// The pixel type. + /// The configuration which allows altering default behaviour or extending the library. + /// The source image. Cannot be null. + /// + /// The structure that specifies the portion of the image object to draw. + /// + /// The + ICloningImageProcessor CreatePixelSpecificCloningProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel; + } +} diff --git a/ImageSharp/Processing/Processors/ICloningImageProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/ICloningImageProcessor{TPixel}.cs new file mode 100644 index 0000000..6c1d2d0 --- /dev/null +++ b/ImageSharp/Processing/Processors/ICloningImageProcessor{TPixel}.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors { + /// + /// Implements an algorithm to alter the pixels of a cloned image. + /// + /// The pixel format. + public interface ICloningImageProcessor : IImageProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Clones the specified and executes the process against the clone. + /// + /// The . + Image CloneAndExecute(); + } +} diff --git a/ImageSharp/Processing/Processors/IImageProcessor.cs b/ImageSharp/Processing/Processors/IImageProcessor.cs new file mode 100644 index 0000000..c84e3c8 --- /dev/null +++ b/ImageSharp/Processing/Processors/IImageProcessor.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors { + /// + /// Defines an algorithm to alter the pixels of an image. + /// Non-generic implementations are responsible for: + /// 1. Encapsulating the parameters of the algorithm. + /// 2. Creating the generic instance to execute the algorithm. + /// + public interface IImageProcessor + { + /// + /// Creates a pixel specific that is capable of executing + /// the processing algorithm on an . + /// + /// The pixel type. + /// The configuration which allows altering default behaviour or extending the library. + /// The source image. Cannot be null. + /// + /// The structure that specifies the portion of the image object to draw. + /// + /// The + IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel; + } +} diff --git a/ImageSharp/Processing/Processors/IImageProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/IImageProcessor{TPixel}.cs new file mode 100644 index 0000000..bb80456 --- /dev/null +++ b/ImageSharp/Processing/Processors/IImageProcessor{TPixel}.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Processing.Processors { + /// + /// Implements an algorithm to alter the pixels of an image. + /// + /// The pixel format. + public interface IImageProcessor : IDisposable + where TPixel : unmanaged, IPixel + { + /// + /// Executes the process against the specified . + /// + void Execute(); + } +} diff --git a/ImageSharp/Processing/Processors/ImageProcessorExtensions.cs b/ImageSharp/Processing/Processors/ImageProcessorExtensions.cs new file mode 100644 index 0000000..970afd5 --- /dev/null +++ b/ImageSharp/Processing/Processors/ImageProcessorExtensions.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors { + internal static class ImageProcessorExtensions + { + /// + /// Executes the processor against the given source image and rectangle bounds. + /// + /// The processor. + /// The configuration which allows altering default behaviour or extending the library. + /// The source image. + /// The source bounds. + public static void Execute(this IImageProcessor processor, Configuration configuration, Image source, Rectangle sourceRectangle) + => source.AcceptVisitor(new ExecuteVisitor(configuration, processor, sourceRectangle)); + + private class ExecuteVisitor : IImageVisitor + { + private readonly Configuration configuration; + private readonly IImageProcessor processor; + private readonly Rectangle sourceRectangle; + + public ExecuteVisitor(Configuration configuration, IImageProcessor processor, Rectangle sourceRectangle) + { + this.configuration = configuration; + this.processor = processor; + this.sourceRectangle = sourceRectangle; + } + + public void Visit(Image image) + where TPixel : unmanaged, IPixel + { + using (IImageProcessor processorImpl = this.processor.CreatePixelSpecificProcessor(this.configuration, image, this.sourceRectangle)) + { + processorImpl.Execute(); + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/ImageProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/ImageProcessor{TPixel}.cs new file mode 100644 index 0000000..6d45b15 --- /dev/null +++ b/ImageSharp/Processing/Processors/ImageProcessor{TPixel}.cs @@ -0,0 +1,119 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors { + /// + /// The base class for all pixel specific image processors. + /// Allows the application of processing algorithms to the image. + /// + /// The pixel format. + public abstract class ImageProcessor : IImageProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + protected ImageProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + { + this.Configuration = configuration; + this.Source = source; + this.SourceRectangle = sourceRectangle; + } + + /// + /// Gets The source for the current processor instance. + /// + protected Image Source { get; } + + /// + /// Gets The source area to process for the current processor instance. + /// + protected Rectangle SourceRectangle { get; } + + /// + /// Gets the instance to use when performing operations. + /// + protected Configuration Configuration { get; } + + /// + void IImageProcessor.Execute() + { + this.BeforeImageApply(); + + foreach (ImageFrame sourceFrame in this.Source.Frames) + { + this.Apply(sourceFrame); + } + + this.AfterImageApply(); + } + + /// + /// Applies the processor to a single image frame. + /// + /// the source image. + public void Apply(ImageFrame source) + { + this.BeforeFrameApply(source); + this.OnFrameApply(source); + this.AfterFrameApply(source); + } + + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// This method is called before the process is applied to prepare the processor. + /// + protected virtual void BeforeImageApply() + { + } + + /// + /// This method is called before the process is applied to prepare the processor. + /// + /// The source image. Cannot be null. + protected virtual void BeforeFrameApply(ImageFrame source) + { + } + + /// + /// Applies the process to the specified portion of the specified at the specified location + /// and with the specified size. + /// + /// The source image. Cannot be null. + protected abstract void OnFrameApply(ImageFrame source); + + /// + /// This method is called after the process is applied to each frame. + /// + /// The source image. Cannot be null. + protected virtual void AfterFrameApply(ImageFrame source) + => source.Metadata.AfterFrameApply(source, source, Matrix4x4.Identity); + + /// + /// This method is called after the process is applied to the complete image. + /// + protected virtual void AfterImageApply() + => this.Source.Metadata.AfterImageApply(this.Source, Matrix4x4.Identity); + + /// + /// Disposes the object and frees resources for the Garbage Collector. + /// + /// Whether to dispose managed and unmanaged objects. + protected virtual void Dispose(bool disposing) + { + } + } +} diff --git a/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor.cs b/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor.cs new file mode 100644 index 0000000..4e1b039 --- /dev/null +++ b/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization { + /// + /// Applies an adaptive histogram equalization to the image. The image is split up in tiles. For each tile a cumulative distribution function (cdf) is calculated. + /// To calculate the final equalized pixel value, the cdf value of four adjacent tiles will be interpolated. + /// + public class AdaptiveHistogramEqualizationProcessor : HistogramEqualizationProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// Indicating whether to clip the histogram bins at a specific value. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// The number of tiles the image is split into (horizontal and vertically). Minimum value is 2. Maximum value is 100. + public AdaptiveHistogramEqualizationProcessor( + int luminanceLevels, + bool clipHistogram, + int clipLimit, + int numberOfTiles) + : base(luminanceLevels, clipHistogram, clipLimit) => this.NumberOfTiles = numberOfTiles; + + /// + /// Gets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization. + /// + public int NumberOfTiles { get; } + + /// + public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + => new AdaptiveHistogramEqualizationProcessor( + configuration, + this.LuminanceLevels, + this.ClipHistogram, + this.ClipLimit, + this.NumberOfTiles, + source, + sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs new file mode 100644 index 0000000..7e2992d --- /dev/null +++ b/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs @@ -0,0 +1,636 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization { + /// + /// Applies an adaptive histogram equalization to the image. The image is split up in tiles. For each tile a cumulative distribution function (cdf) is calculated. + /// To calculate the final equalized pixel value, the cdf value of four adjacent tiles will be interpolated. + /// + /// The pixel format. + internal class AdaptiveHistogramEqualizationProcessor : HistogramEqualizationProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// Indicating whether to clip the histogram bins at a specific value. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// The number of tiles the image is split into (horizontal and vertically). Minimum value is 2. Maximum value is 100. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public AdaptiveHistogramEqualizationProcessor( + Configuration configuration, + int luminanceLevels, + bool clipHistogram, + int clipLimit, + int tiles, + Image source, + Rectangle sourceRectangle) + : base(configuration, luminanceLevels, clipHistogram, clipLimit, source, sourceRectangle) + { + Guard.MustBeGreaterThanOrEqualTo(tiles, 2, nameof(tiles)); + Guard.MustBeLessThanOrEqualTo(tiles, 100, nameof(tiles)); + + this.Tiles = tiles; + } + + /// + /// Gets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization. + /// + private int Tiles { get; } + + /// + protected override void OnFrameApply(ImageFrame source) + { + int sourceWidth = source.Width; + int sourceHeight = source.Height; + int tileWidth = (int)MathF.Ceiling(sourceWidth / (float)this.Tiles); + int tileHeight = (int)MathF.Ceiling(sourceHeight / (float)this.Tiles); + int tileCount = this.Tiles; + int halfTileWidth = (int)((uint)tileWidth / 2); + int halfTileHeight = (int)((uint)tileHeight / 2); + int luminanceLevels = this.LuminanceLevels; + + // The image is split up into tiles. For each tile the cumulative distribution function will be calculated. + using (CdfTileData cdfData = new(this.Configuration, sourceWidth, sourceHeight, this.Tiles, this.Tiles, tileWidth, tileHeight, luminanceLevels)) + { + cdfData.CalculateLookupTables(source, this); + + List<(int Y, int CdfY)> tileYStartPositions = []; + int cdfY = 0; + int yStart = halfTileHeight; + for (int tile = 0; tile < tileCount - 1; tile++) + { + tileYStartPositions.Add((yStart, cdfY)); + cdfY++; + yStart += tileHeight; + } + + RowIntervalOperation operation = new(cdfData, tileYStartPositions, tileWidth, tileHeight, tileCount, halfTileWidth, luminanceLevels, source.PixelBuffer); + ParallelRowIterator.IterateRowIntervals( + this.Configuration, + new Rectangle(0, 0, sourceWidth, tileYStartPositions.Count), + in operation); + + // Fix left column + ProcessBorderColumn(source.PixelBuffer, cdfData, 0, sourceHeight, this.Tiles, tileHeight, xStart: 0, xEnd: halfTileWidth, luminanceLevels); + + // Fix right column + int rightBorderStartX = ((this.Tiles - 1) * tileWidth) + halfTileWidth; + ProcessBorderColumn(source.PixelBuffer, cdfData, this.Tiles - 1, sourceHeight, this.Tiles, tileHeight, xStart: rightBorderStartX, xEnd: sourceWidth, luminanceLevels); + + // Fix top row + ProcessBorderRow(source.PixelBuffer, cdfData, 0, sourceWidth, this.Tiles, tileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); + + // Fix bottom row + int bottomBorderStartY = ((this.Tiles - 1) * tileHeight) + halfTileHeight; + ProcessBorderRow(source.PixelBuffer, cdfData, this.Tiles - 1, sourceWidth, this.Tiles, tileWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); + + // Left top corner + ProcessCornerTile(source.PixelBuffer, cdfData, 0, 0, xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); + + // Left bottom corner + ProcessCornerTile(source.PixelBuffer, cdfData, 0, this.Tiles - 1, xStart: 0, xEnd: halfTileWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); + + // Right top corner + ProcessCornerTile(source.PixelBuffer, cdfData, this.Tiles - 1, 0, xStart: rightBorderStartX, xEnd: sourceWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); + + // Right bottom corner + ProcessCornerTile(source.PixelBuffer, cdfData, this.Tiles - 1, this.Tiles - 1, xStart: rightBorderStartX, xEnd: sourceWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); + } + } + + /// + /// Processes the part of a corner tile which was previously left out. It consists of 1 / 4 of a tile and does not need interpolation. + /// + /// The source image. + /// The lookup table to remap the grey values. + /// The x-position in the CDF lookup map. + /// The y-position in the CDF lookup map. + /// X start position. + /// X end position. + /// Y start position. + /// Y end position. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// + private static void ProcessCornerTile( + Buffer2D source, + CdfTileData cdfData, + int cdfX, + int cdfY, + int xStart, + int xEnd, + int yStart, + int yEnd, + int luminanceLevels) + { + for (int dy = yStart; dy < yEnd; dy++) + { + Span rowSpan = source.DangerousGetRowSpan(dy); + for (int dx = xStart; dx < xEnd; dx++) + { + ref TPixel pixel = ref rowSpan[dx]; + float luminanceEqualized = cdfData.RemapGreyValue(cdfX, cdfY, GetLuminance(pixel, luminanceLevels)); + pixel = TPixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); + } + } + } + + /// + /// Processes a border column of the image which is half the size of the tile width. + /// + /// The source image. + /// The pre-computed lookup tables to remap the grey values for each tiles. + /// The X index of the lookup table to use. + /// The source image height. + /// The number of vertical tiles. + /// The height of a tile. + /// X start position in the image. + /// X end position of the image. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// + private static void ProcessBorderColumn( + Buffer2D source, + CdfTileData cdfData, + int cdfX, + int sourceHeight, + int tileCount, + int tileHeight, + int xStart, + int xEnd, + int luminanceLevels) + { + int halfTileHeight = (int)((uint)tileHeight / 2); + + int cdfY = 0; + int y = halfTileHeight; + for (int tile = 0; tile < tileCount - 1; tile++) + { + int yLimit = Math.Min(y + tileHeight, sourceHeight - 1); + int tileY = 0; + for (int dy = y; dy < yLimit; dy++) + { + Span rowSpan = source.DangerousGetRowSpan(dy); + for (int dx = xStart; dx < xEnd; dx++) + { + ref TPixel pixel = ref rowSpan[dx]; + float luminanceEqualized = InterpolateBetweenTwoTiles(pixel, cdfData, cdfX, cdfY, cdfX, cdfY + 1, tileY, tileHeight, luminanceLevels); + pixel = TPixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); + } + + tileY++; + } + + cdfY++; + y += tileHeight; + } + } + + /// + /// Processes a border row of the image which is half of the size of the tile height. + /// + /// The source image. + /// The pre-computed lookup tables to remap the grey values for each tiles. + /// The Y index of the lookup table to use. + /// The source image width. + /// The number of horizontal tiles. + /// The width of a tile. + /// Y start position in the image. + /// Y end position of the image. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// + private static void ProcessBorderRow( + Buffer2D source, + CdfTileData cdfData, + int cdfY, + int sourceWidth, + int tileCount, + int tileWidth, + int yStart, + int yEnd, + int luminanceLevels) + { + int halfTileWidth = (int)((uint)tileWidth / 2); + + int cdfX = 0; + int x = halfTileWidth; + for (int tile = 0; tile < tileCount - 1; tile++) + { + for (int dy = yStart; dy < yEnd; dy++) + { + Span rowSpan = source.DangerousGetRowSpan(dy); + int tileX = 0; + int xLimit = Math.Min(x + tileWidth, sourceWidth - 1); + for (int dx = x; dx < xLimit; dx++) + { + ref TPixel pixel = ref rowSpan[dx]; + float luminanceEqualized = InterpolateBetweenTwoTiles(pixel, cdfData, cdfX, cdfY, cdfX + 1, cdfY, tileX, tileWidth, luminanceLevels); + pixel = TPixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); + tileX++; + } + } + + cdfX++; + x += tileWidth; + } + } + + /// + /// Bilinear interpolation between four adjacent tiles. + /// + /// The pixel to remap the grey value from. + /// The pre-computed lookup tables to remap the grey values for each tiles. + /// The number of tiles in the x-direction. + /// The number of tiles in the y-direction. + /// X position inside the tile. + /// Y position inside the tile. + /// X index of the top left lookup table to use. + /// Y index of the top left lookup table to use. + /// Width of one tile in pixels. + /// Height of one tile in pixels. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// + /// A re-mapped grey value. + [MethodImpl(InliningOptions.ShortMethod)] + private static float InterpolateBetweenFourTiles( + TPixel sourcePixel, + CdfTileData cdfData, + int tileCountX, + int tileCountY, + int tileX, + int tileY, + int cdfX, + int cdfY, + int tileWidth, + int tileHeight, + int luminanceLevels) + { + int luminance = GetLuminance(sourcePixel, luminanceLevels); + float tx = tileX / (float)(tileWidth - 1); + float ty = tileY / (float)(tileHeight - 1); + + int yTop = cdfY; + int yBottom = Math.Min(tileCountY - 1, yTop + 1); + int xLeft = cdfX; + int xRight = Math.Min(tileCountX - 1, xLeft + 1); + + float cdfLeftTopLuminance = cdfData.RemapGreyValue(xLeft, yTop, luminance); + float cdfRightTopLuminance = cdfData.RemapGreyValue(xRight, yTop, luminance); + float cdfLeftBottomLuminance = cdfData.RemapGreyValue(xLeft, yBottom, luminance); + float cdfRightBottomLuminance = cdfData.RemapGreyValue(xRight, yBottom, luminance); + return BilinearInterpolation(tx, ty, cdfLeftTopLuminance, cdfRightTopLuminance, cdfLeftBottomLuminance, cdfRightBottomLuminance); + } + + /// + /// Linear interpolation between two tiles. + /// + /// The pixel to remap the grey value from. + /// The CDF lookup map. + /// X position inside the first tile. + /// Y position inside the first tile. + /// X position inside the second tile. + /// Y position inside the second tile. + /// Position inside the tile. + /// Width of the tile. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// + /// A re-mapped grey value. + [MethodImpl(InliningOptions.ShortMethod)] + private static float InterpolateBetweenTwoTiles( + TPixel sourcePixel, + CdfTileData cdfData, + int tileX1, + int tileY1, + int tileX2, + int tileY2, + int tilePos, + int tileWidth, + int luminanceLevels) + { + int luminance = GetLuminance(sourcePixel, luminanceLevels); + float tx = tilePos / (float)(tileWidth - 1); + + float cdfLuminance1 = cdfData.RemapGreyValue(tileX1, tileY1, luminance); + float cdfLuminance2 = cdfData.RemapGreyValue(tileX2, tileY2, luminance); + return LinearInterpolation(cdfLuminance1, cdfLuminance2, tx); + } + + /// + /// Bilinear interpolation between four tiles. + /// + /// The interpolation value in x direction in the range of [0, 1]. + /// The interpolation value in y direction in the range of [0, 1]. + /// Luminance from top left tile. + /// Luminance from right top tile. + /// Luminance from left bottom tile. + /// Luminance from right bottom tile. + /// Interpolated Luminance. + [MethodImpl(InliningOptions.ShortMethod)] + private static float BilinearInterpolation(float tx, float ty, float lt, float rt, float lb, float rb) + => LinearInterpolation(LinearInterpolation(lt, rt, tx), LinearInterpolation(lb, rb, tx), ty); + + /// + /// Linear interpolation between two grey values. + /// + /// The left value. + /// The right value. + /// The interpolation value between the two values in the range of [0, 1]. + /// The interpolated value. + [MethodImpl(InliningOptions.ShortMethod)] + private static float LinearInterpolation(float left, float right, float t) + => left + ((right - left) * t); + + private readonly struct RowIntervalOperation : IRowIntervalOperation + { + private readonly CdfTileData cdfData; + private readonly List<(int Y, int CdfY)> tileYStartPositions; + private readonly int tileWidth; + private readonly int tileHeight; + private readonly int tileCount; + private readonly int halfTileWidth; + private readonly int luminanceLevels; + private readonly Buffer2D source; + private readonly int sourceWidth; + private readonly int sourceHeight; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowIntervalOperation( + CdfTileData cdfData, + List<(int Y, int CdfY)> tileYStartPositions, + int tileWidth, + int tileHeight, + int tileCount, + int halfTileWidth, + int luminanceLevels, + Buffer2D source) + { + this.cdfData = cdfData; + this.tileYStartPositions = tileYStartPositions; + this.tileWidth = tileWidth; + this.tileHeight = tileHeight; + this.tileCount = tileCount; + this.halfTileWidth = halfTileWidth; + this.luminanceLevels = luminanceLevels; + this.source = source; + this.sourceWidth = source.Width; + this.sourceHeight = source.Height; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(in RowInterval rows) + { + for (int index = rows.Min; index < rows.Max; index++) + { + (int y, int cdfY) = this.tileYStartPositions[index]; + int cdfX = 0; + int x = this.halfTileWidth; + for (int tile = 0; tile < this.tileCount - 1; tile++) + { + int tileY = 0; + int yEnd = Math.Min(y + this.tileHeight, this.sourceHeight); + int xEnd = Math.Min(x + this.tileWidth, this.sourceWidth); + for (int dy = y; dy < yEnd; dy++) + { + Span rowSpan = this.source.DangerousGetRowSpan(dy); + int tileX = 0; + for (int dx = x; dx < xEnd; dx++) + { + ref TPixel pixel = ref rowSpan[dx]; + float luminanceEqualized = InterpolateBetweenFourTiles( + pixel, + this.cdfData, + this.tileCount, + this.tileCount, + tileX, + tileY, + cdfX, + cdfY, + this.tileWidth, + this.tileHeight, + this.luminanceLevels); + + pixel = TPixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); + tileX++; + } + + tileY++; + } + + cdfX++; + x += this.tileWidth; + } + } + } + } + + /// + /// Contains the results of the cumulative distribution function for all tiles. + /// + private sealed class CdfTileData : IDisposable + { + private readonly Configuration configuration; + private readonly MemoryAllocator memoryAllocator; + + /// + /// Used for storing the minimum value for each CDF entry. + /// + private readonly Buffer2D cdfMinBuffer2D; + + /// + /// Used for storing the LUT for each CDF entry. + /// + private readonly Buffer2D cdfLutBuffer2D; + private readonly int pixelsInTile; + private readonly int sourceWidth; + private readonly int tileWidth; + private readonly int tileHeight; + private readonly int luminanceLevels; + private readonly List<(int Y, int CdfY)> tileYStartPositions; + + public CdfTileData( + Configuration configuration, + int sourceWidth, + int sourceHeight, + int tileCountX, + int tileCountY, + int tileWidth, + int tileHeight, + int luminanceLevels) + { + this.configuration = configuration; + this.memoryAllocator = configuration.MemoryAllocator; + this.luminanceLevels = luminanceLevels; + this.cdfMinBuffer2D = this.memoryAllocator.Allocate2D(tileCountX, tileCountY); + this.cdfLutBuffer2D = this.memoryAllocator.Allocate2D(tileCountX * luminanceLevels, tileCountY); + this.sourceWidth = sourceWidth; + this.tileWidth = tileWidth; + this.tileHeight = tileHeight; + this.pixelsInTile = tileWidth * tileHeight; + + // Calculate the start positions and rent buffers. + this.tileYStartPositions = []; + int cdfY = 0; + for (int y = 0; y < sourceHeight; y += tileHeight) + { + this.tileYStartPositions.Add((y, cdfY)); + cdfY++; + } + } + + public void CalculateLookupTables(ImageFrame source, HistogramEqualizationProcessor processor) + { + RowIntervalOperation operation = new( + processor, + this.memoryAllocator, + this.cdfMinBuffer2D, + this.cdfLutBuffer2D, + this.tileYStartPositions, + this.tileWidth, + this.tileHeight, + this.luminanceLevels, + source.PixelBuffer); + + ParallelRowIterator.IterateRowIntervals( + this.configuration, + new Rectangle(0, 0, this.sourceWidth, this.tileYStartPositions.Count), + in operation); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public Span GetCdfLutSpan(int tileX, int tileY) => this.cdfLutBuffer2D.DangerousGetRowSpan(tileY).Slice(tileX * this.luminanceLevels, this.luminanceLevels); + + /// + /// Remaps the grey value with the cdf. + /// + /// The tiles x-position. + /// The tiles y-position. + /// The original luminance. + /// The remapped luminance. + [MethodImpl(InliningOptions.ShortMethod)] + public float RemapGreyValue(int tilesX, int tilesY, int luminance) + { + int cdfMin = this.cdfMinBuffer2D[tilesX, tilesY]; + Span cdfSpan = this.GetCdfLutSpan(tilesX, tilesY); + return (this.pixelsInTile - cdfMin) == 0 + ? cdfSpan[luminance] / this.pixelsInTile + : cdfSpan[luminance] / (float)(this.pixelsInTile - cdfMin); + } + + public void Dispose() + { + this.cdfMinBuffer2D.Dispose(); + this.cdfLutBuffer2D.Dispose(); + } + + private readonly struct RowIntervalOperation : IRowIntervalOperation + { + private readonly HistogramEqualizationProcessor processor; + private readonly MemoryAllocator allocator; + private readonly Buffer2D cdfMinBuffer2D; + private readonly Buffer2D cdfLutBuffer2D; + private readonly List<(int Y, int CdfY)> tileYStartPositions; + private readonly int tileWidth; + private readonly int tileHeight; + private readonly int luminanceLevels; + private readonly Buffer2D source; + private readonly int sourceWidth; + private readonly int sourceHeight; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowIntervalOperation( + HistogramEqualizationProcessor processor, + MemoryAllocator allocator, + Buffer2D cdfMinBuffer2D, + Buffer2D cdfLutBuffer2D, + List<(int Y, int CdfY)> tileYStartPositions, + int tileWidth, + int tileHeight, + int luminanceLevels, + Buffer2D source) + { + this.processor = processor; + this.allocator = allocator; + this.cdfMinBuffer2D = cdfMinBuffer2D; + this.cdfLutBuffer2D = cdfLutBuffer2D; + this.tileYStartPositions = tileYStartPositions; + this.tileWidth = tileWidth; + this.tileHeight = tileHeight; + this.luminanceLevels = luminanceLevels; + this.source = source; + this.sourceWidth = source.Width; + this.sourceHeight = source.Height; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(in RowInterval rows) + { + for (int index = rows.Min; index < rows.Max; index++) + { + int cdfX = 0; + int cdfY = this.tileYStartPositions[index].CdfY; + int y = this.tileYStartPositions[index].Y; + int endY = Math.Min(y + this.tileHeight, this.sourceHeight); + Span cdfMinSpan = this.cdfMinBuffer2D.DangerousGetRowSpan(cdfY); + cdfMinSpan.Clear(); + + using IMemoryOwner histogramBuffer = this.allocator.Allocate(this.luminanceLevels); + Span histogram = histogramBuffer.GetSpan(); + ref int histogramBase = ref MemoryMarshal.GetReference(histogram); + + for (int x = 0; x < this.sourceWidth; x += this.tileWidth) + { + histogram.Clear(); + Span cdfLutSpan = this.cdfLutBuffer2D.DangerousGetRowSpan(index).Slice(cdfX * this.luminanceLevels, this.luminanceLevels); + ref int cdfBase = ref MemoryMarshal.GetReference(cdfLutSpan); + + int xlimit = Math.Min(x + this.tileWidth, this.sourceWidth); + for (int dy = y; dy < endY; dy++) + { + Span rowSpan = this.source.DangerousGetRowSpan(dy); + for (int dx = x; dx < xlimit; dx++) + { + int luminance = GetLuminance(rowSpan[dx], this.luminanceLevels); + histogram[luminance]++; + } + } + + if (this.processor.ClipHistogramEnabled) + { + this.processor.ClipHistogram(histogram, this.processor.ClipLimit); + } + + cdfMinSpan[cdfX] += CalculateCdf(ref cdfBase, ref histogramBase, histogram.Length - 1); + + cdfX++; + } + } + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor.cs b/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor.cs new file mode 100644 index 0000000..160776c --- /dev/null +++ b/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization { + /// + /// Applies an adaptive histogram equalization to the image using an sliding window approach. + /// + public class AdaptiveHistogramEqualizationSlidingWindowProcessor : HistogramEqualizationProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// Indicating whether to clip the histogram bins at a specific value. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// The number of tiles the image is split into (horizontal and vertically). Minimum value is 2. Maximum value is 100. + public AdaptiveHistogramEqualizationSlidingWindowProcessor( + int luminanceLevels, + bool clipHistogram, + int clipLimit, + int numberOfTiles) + : base(luminanceLevels, clipHistogram, clipLimit) + { + this.NumberOfTiles = numberOfTiles; + } + + /// + /// Gets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization. + /// + public int NumberOfTiles { get; } + + /// + public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + => new AdaptiveHistogramEqualizationSlidingWindowProcessor( + configuration, + this.LuminanceLevels, + this.ClipHistogram, + this.ClipLimit, + this.NumberOfTiles, + source, + sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs new file mode 100644 index 0000000..94fc8da --- /dev/null +++ b/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs @@ -0,0 +1,439 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization { + /// + /// Applies an adaptive histogram equalization to the image using an sliding window approach. + /// + /// The pixel format. + internal class AdaptiveHistogramEqualizationSlidingWindowProcessor : HistogramEqualizationProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// Indicating whether to clip the histogram bins at a specific value. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// The number of tiles the image is split into (horizontal and vertically). Minimum value is 2. Maximum value is 100. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public AdaptiveHistogramEqualizationSlidingWindowProcessor( + Configuration configuration, + int luminanceLevels, + bool clipHistogram, + int clipLimit, + int tiles, + Image source, + Rectangle sourceRectangle) + : base(configuration, luminanceLevels, clipHistogram, clipLimit, source, sourceRectangle) + { + Guard.MustBeGreaterThanOrEqualTo(tiles, 2, nameof(tiles)); + Guard.MustBeLessThanOrEqualTo(tiles, 100, nameof(tiles)); + + this.Tiles = tiles; + } + + /// + /// Gets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization. + /// + private int Tiles { get; } + + /// + protected override void OnFrameApply(ImageFrame source) + { + MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator; + + ParallelOptions parallelOptions = new() + { MaxDegreeOfParallelism = this.Configuration.MaxDegreeOfParallelism }; + int tileWidth = source.Width / this.Tiles; + int tileHeight = tileWidth; + int pixelInTile = tileWidth * tileHeight; + int halfTileHeight = (int)((uint)tileHeight / 2); + int halfTileWidth = halfTileHeight; + SlidingWindowInfos slidingWindowInfos = new(tileWidth, tileHeight, halfTileWidth, halfTileHeight, pixelInTile); + + // TODO: If the process was able to be switched to operate in parallel rows instead of columns + // then we could take advantage of batching and allocate per-row buffers only once per batch. + using Buffer2D targetPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Width, source.Height); + + // Process the inner tiles, which do not require to check the borders. + SlidingWindowOperation innerOperation = new( + this.Configuration, + this, + source, + memoryAllocator, + targetPixels, + slidingWindowInfos, + yStart: halfTileHeight, + yEnd: source.Height - halfTileHeight, + useFastPath: true); + + Parallel.For( + halfTileWidth, + source.Width - halfTileWidth, + parallelOptions, + innerOperation.Invoke); + + // Process the left border of the image. + SlidingWindowOperation leftBorderOperation = new( + this.Configuration, + this, + source, + memoryAllocator, + targetPixels, + slidingWindowInfos, + yStart: 0, + yEnd: source.Height, + useFastPath: false); + + Parallel.For( + 0, + halfTileWidth, + parallelOptions, + leftBorderOperation.Invoke); + + // Process the right border of the image. + SlidingWindowOperation rightBorderOperation = new( + this.Configuration, + this, + source, + memoryAllocator, + targetPixels, + slidingWindowInfos, + yStart: 0, + yEnd: source.Height, + useFastPath: false); + + Parallel.For( + source.Width - halfTileWidth, + source.Width, + parallelOptions, + rightBorderOperation.Invoke); + + // Process the top border of the image. + SlidingWindowOperation topBorderOperation = new( + this.Configuration, + this, + source, + memoryAllocator, + targetPixels, + slidingWindowInfos, + yStart: 0, + yEnd: halfTileHeight, + useFastPath: false); + + Parallel.For( + halfTileWidth, + source.Width - halfTileWidth, + parallelOptions, + topBorderOperation.Invoke); + + // Process the bottom border of the image. + SlidingWindowOperation bottomBorderOperation = new( + this.Configuration, + this, + source, + memoryAllocator, + targetPixels, + slidingWindowInfos, + yStart: source.Height - halfTileHeight, + yEnd: source.Height, + useFastPath: false); + + Parallel.For( + halfTileWidth, + source.Width - halfTileWidth, + parallelOptions, + bottomBorderOperation.Invoke); + + Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); + } + + /// + /// Get the a pixel row at a given position with a length of the tile width. Mirrors pixels which exceeds the edges. + /// + /// The source image. + /// Pre-allocated pixel row span of the size of a the tile width. + /// The x position. + /// The y position. + /// The width in pixels of a tile. + /// The configuration. + private static void CopyPixelRow( + ImageFrame source, + Span rowPixels, + int x, + int y, + int tileWidth, + Configuration configuration) + { + if (y < 0) + { + y = Numerics.Abs(y); + } + else if (y >= source.Height) + { + int diff = y - source.Height; + y = source.Height - diff - 1; + } + + // Special cases for the left and the right border where DangerousGetRowSpan can not be used. + if (x < 0) + { + rowPixels.Clear(); + int idx = 0; + for (int dx = x; dx < x + tileWidth; dx++) + { + rowPixels[idx] = source[Numerics.Abs(dx), y].ToVector4(); + idx++; + } + + return; + } + else if (x + tileWidth > source.Width) + { + rowPixels.Clear(); + int idx = 0; + for (int dx = x; dx < x + tileWidth; dx++) + { + if (dx >= source.Width) + { + int diff = dx - source.Width; + rowPixels[idx] = source[dx - diff - 1, y].ToVector4(); + } + else + { + rowPixels[idx] = source[dx, y].ToVector4(); + } + + idx++; + } + + return; + } + + CopyPixelRowFast(source.PixelBuffer, rowPixels, x, y, tileWidth, configuration); + } + + /// + /// Get the a pixel row at a given position with a length of the tile width. + /// + /// The source image. + /// Pre-allocated pixel row span of the size of a the tile width. + /// The x position. + /// The y position. + /// The width in pixels of a tile. + /// The configuration. + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyPixelRowFast( + Buffer2D source, + Span rowPixels, + int x, + int y, + int tileWidth, + Configuration configuration) + => PixelOperations.Instance.ToVector4(configuration, source.DangerousGetRowSpan(y).Slice(start: x, length: tileWidth), rowPixels); + + /// + /// Adds a column of grey values to the histogram. + /// + /// The reference to the span of grey values to add. + /// The reference to the histogram span. + /// The number of different luminance levels. + /// The grey values span length. + [MethodImpl(InliningOptions.ShortMethod)] + private static void AddPixelsToHistogram(ref Vector4 greyValuesBase, ref int histogramBase, int luminanceLevels, int length) + { + for (nuint idx = 0; idx < (uint)length; idx++) + { + int luminance = ColorNumerics.GetBT709Luminance(ref Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); + Unsafe.Add(ref histogramBase, (uint)luminance)++; + } + } + + /// + /// Removes a column of grey values from the histogram. + /// + /// The reference to the span of grey values to remove. + /// The reference to the histogram span. + /// The number of different luminance levels. + /// The grey values span length. + [MethodImpl(InliningOptions.ShortMethod)] + private static void RemovePixelsFromHistogram(ref Vector4 greyValuesBase, ref int histogramBase, int luminanceLevels, int length) + { + for (nuint idx = 0; idx < (uint)length; idx++) + { + int luminance = ColorNumerics.GetBT709Luminance(ref Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); + Unsafe.Add(ref histogramBase, (uint)luminance)--; + } + } + + /// + /// Applies the sliding window equalization to one column of the image. The window is moved from top to bottom. + /// Moving the window one pixel down requires to remove one row from the top of the window from the histogram and + /// adding a new row at the bottom. + /// + private readonly struct SlidingWindowOperation + { + private readonly Configuration configuration; + private readonly AdaptiveHistogramEqualizationSlidingWindowProcessor processor; + private readonly ImageFrame source; + private readonly MemoryAllocator memoryAllocator; + private readonly Buffer2D targetPixels; + private readonly SlidingWindowInfos swInfos; + private readonly int yStart; + private readonly int yEnd; + private readonly bool useFastPath; + + /// + /// Initializes a new instance of the struct. + /// + /// The configuration. + /// The histogram processor. + /// The source image. + /// The memory allocator. + /// The target pixels. + /// about the sliding window dimensions. + /// The y start position. + /// The y end position. + /// if set to true the borders of the image will not be checked. + [MethodImpl(InliningOptions.ShortMethod)] + public SlidingWindowOperation( + Configuration configuration, + AdaptiveHistogramEqualizationSlidingWindowProcessor processor, + ImageFrame source, + MemoryAllocator memoryAllocator, + Buffer2D targetPixels, + SlidingWindowInfos swInfos, + int yStart, + int yEnd, + bool useFastPath) + { + this.configuration = configuration; + this.processor = processor; + this.source = source; + this.memoryAllocator = memoryAllocator; + this.targetPixels = targetPixels; + this.swInfos = swInfos; + this.yStart = yStart; + this.yEnd = yEnd; + this.useFastPath = useFastPath; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int x) + { + using (IMemoryOwner histogramBuffer = this.memoryAllocator.Allocate(this.processor.LuminanceLevels, AllocationOptions.Clean)) + using (IMemoryOwner histogramBufferCopy = this.memoryAllocator.Allocate(this.processor.LuminanceLevels, AllocationOptions.Clean)) + using (IMemoryOwner cdfBuffer = this.memoryAllocator.Allocate(this.processor.LuminanceLevels, AllocationOptions.Clean)) + using (IMemoryOwner pixelRowBuffer = this.memoryAllocator.Allocate(this.swInfos.TileWidth, AllocationOptions.Clean)) + { + Span histogram = histogramBuffer.GetSpan(); + ref int histogramBase = ref MemoryMarshal.GetReference(histogram); + + Span histogramCopy = histogramBufferCopy.GetSpan(); + ref int histogramCopyBase = ref MemoryMarshal.GetReference(histogramCopy); + + ref int cdfBase = ref MemoryMarshal.GetReference(cdfBuffer.GetSpan()); + + Span pixelRow = pixelRowBuffer.GetSpan(); + ref Vector4 pixelRowBase = ref MemoryMarshal.GetReference(pixelRow); + + // Build the initial histogram of grayscale values. + for (int dy = this.yStart - this.swInfos.HalfTileHeight; dy < this.yStart + this.swInfos.HalfTileHeight; dy++) + { + if (this.useFastPath) + { + CopyPixelRowFast(this.source.PixelBuffer, pixelRow, x - this.swInfos.HalfTileWidth, dy, this.swInfos.TileWidth, this.configuration); + } + else + { + CopyPixelRow(this.source, pixelRow, x - this.swInfos.HalfTileWidth, dy, this.swInfos.TileWidth, this.configuration); + } + + AddPixelsToHistogram(ref pixelRowBase, ref histogramBase, this.processor.LuminanceLevels, pixelRow.Length); + } + + for (int y = this.yStart; y < this.yEnd; y++) + { + if (this.processor.ClipHistogramEnabled) + { + // Clipping the histogram, but doing it on a copy to keep the original un-clipped values for the next iteration. + histogram.CopyTo(histogramCopy); + this.processor.ClipHistogram(histogramCopy, this.processor.ClipLimit); + } + + // Calculate the cumulative distribution function, which will map each input pixel in the current tile to a new value. + int cdfMin = this.processor.ClipHistogramEnabled + ? CalculateCdf(ref cdfBase, ref histogramCopyBase, histogram.Length - 1) + : CalculateCdf(ref cdfBase, ref histogramBase, histogram.Length - 1); + + float numberOfPixelsMinusCdfMin = this.swInfos.PixelInTile - cdfMin; + + // Map the current pixel to the new equalized value. + int luminance = GetLuminance(this.source[x, y], this.processor.LuminanceLevels); + float luminanceEqualized = Unsafe.Add(ref cdfBase, (uint)luminance) / numberOfPixelsMinusCdfMin; + this.targetPixels[x, y] = TPixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, this.source[x, y].ToVector4().W)); + + // Remove top most row from the histogram, mirroring rows which exceeds the borders. + if (this.useFastPath) + { + CopyPixelRowFast(this.source.PixelBuffer, pixelRow, x - this.swInfos.HalfTileWidth, y - this.swInfos.HalfTileWidth, this.swInfos.TileWidth, this.configuration); + } + else + { + CopyPixelRow(this.source, pixelRow, x - this.swInfos.HalfTileWidth, y - this.swInfos.HalfTileWidth, this.swInfos.TileWidth, this.configuration); + } + + RemovePixelsFromHistogram(ref pixelRowBase, ref histogramBase, this.processor.LuminanceLevels, pixelRow.Length); + + // Add new bottom row to the histogram, mirroring rows which exceeds the borders. + if (this.useFastPath) + { + CopyPixelRowFast(this.source.PixelBuffer, pixelRow, x - this.swInfos.HalfTileWidth, y + this.swInfos.HalfTileWidth, this.swInfos.TileWidth, this.configuration); + } + else + { + CopyPixelRow(this.source, pixelRow, x - this.swInfos.HalfTileWidth, y + this.swInfos.HalfTileWidth, this.swInfos.TileWidth, this.configuration); + } + + AddPixelsToHistogram(ref pixelRowBase, ref histogramBase, this.processor.LuminanceLevels, pixelRow.Length); + } + } + } + } + + private class SlidingWindowInfos + { + public SlidingWindowInfos(int tileWidth, int tileHeight, int halfTileWidth, int halfTileHeight, int pixelInTile) + { + this.TileWidth = tileWidth; + this.TileHeight = tileHeight; + this.HalfTileWidth = halfTileWidth; + this.HalfTileHeight = halfTileHeight; + this.PixelInTile = pixelInTile; + } + + public int TileWidth { get; } + + public int TileHeight { get; } + + public int PixelInTile { get; } + + public int HalfTileWidth { get; } + + public int HalfTileHeight { get; } + } + } +} diff --git a/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs b/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs new file mode 100644 index 0000000..903fab8 --- /dev/null +++ b/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization { + /// + /// Applies a luminance histogram equilization to the image. + /// + public class AutoLevelProcessor : HistogramEqualizationProcessor + { + /// + /// Initializes a new instance of the class. + /// It uses the exact minimum and maximum values found in the luminance channel, as the BlackPoint and WhitePoint to linearly stretch the colors + /// (and histogram) of the image. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// Indicating whether to clip the histogram bins at a specific value. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// Whether to apply a synchronized luminance value to each color channel. + public AutoLevelProcessor( + int luminanceLevels, + bool clipHistogram, + int clipLimit, + bool syncChannels) + : base(luminanceLevels, clipHistogram, clipLimit) + { + this.SyncChannels = syncChannels; + } + + /// + /// Gets a value indicating whether to apply a synchronized luminance value to each color channel. + /// + public bool SyncChannels { get; } + + /// + public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + => new AutoLevelProcessor( + configuration, + this.LuminanceLevels, + this.ClipHistogram, + this.ClipLimit, + this.SyncChannels, + source, + sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs new file mode 100644 index 0000000..49147e7 --- /dev/null +++ b/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs @@ -0,0 +1,224 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization { + /// + /// Applies a luminance histogram equalization to the image. + /// + /// The pixel format. + internal class AutoLevelProcessor : HistogramEqualizationProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// + /// Indicating whether to clip the histogram bins at a specific value. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// Whether to apply a synchronized luminance value to each color channel. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public AutoLevelProcessor( + Configuration configuration, + int luminanceLevels, + bool clipHistogram, + int clipLimit, + bool syncChannels, + Image source, + Rectangle sourceRectangle) + : base(configuration, luminanceLevels, clipHistogram, clipLimit, source, sourceRectangle) + => this.SyncChannels = syncChannels; + + /// + /// Gets a value indicating whether to apply a synchronized luminance value to each color channel. + /// + private bool SyncChannels { get; } + + /// + protected override void OnFrameApply(ImageFrame source) + { + MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator; + int numberOfPixels = source.Width * source.Height; + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + + using IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean); + + // Build the histogram of the grayscale levels. + GrayscaleLevelsRowOperation grayscaleOperation = new(this.Configuration, interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels); + ParallelRowIterator.IterateRows, Vector4>( + this.Configuration, + interest, + in grayscaleOperation); + + Span histogram = histogramBuffer.GetSpan(); + if (this.ClipHistogramEnabled) + { + this.ClipHistogram(histogram, this.ClipLimit); + } + + using IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean); + + // Calculate the cumulative distribution function, which will map each input pixel to a new value. + int cdfMin = CalculateCdf( + ref MemoryMarshal.GetReference(cdfBuffer.GetSpan()), + ref MemoryMarshal.GetReference(histogram), + histogram.Length - 1); + + float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin; + + if (this.SyncChannels) + { + SynchronizedChannelsRowOperation cdfOperation = new(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin); + ParallelRowIterator.IterateRows( + this.Configuration, + interest, + in cdfOperation); + } + else + { + SeperateChannelsRowOperation cdfOperation = new(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin); + ParallelRowIterator.IterateRows( + this.Configuration, + interest, + in cdfOperation); + } + } + + /// + /// A implementing the cdf logic for synchronized color channels. + /// + private readonly struct SynchronizedChannelsRowOperation : IRowOperation + { + private readonly Configuration configuration; + private readonly Rectangle bounds; + private readonly IMemoryOwner cdfBuffer; + private readonly Buffer2D source; + private readonly int luminanceLevels; + private readonly float numberOfPixelsMinusCdfMin; + + [MethodImpl(InliningOptions.ShortMethod)] + public SynchronizedChannelsRowOperation( + Configuration configuration, + Rectangle bounds, + IMemoryOwner cdfBuffer, + Buffer2D source, + int luminanceLevels, + float numberOfPixelsMinusCdfMin) + { + this.configuration = configuration; + this.bounds = bounds; + this.cdfBuffer = cdfBuffer; + this.source = source; + this.luminanceLevels = luminanceLevels; + this.numberOfPixelsMinusCdfMin = numberOfPixelsMinusCdfMin; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) => bounds.Width; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y, Span span) + { + Span vectorBuffer = span[..this.bounds.Width]; + ref Vector4 vectorRef = ref MemoryMarshal.GetReference(vectorBuffer); + ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan()); + PixelAccessor sourceAccess = new(this.source); + int levels = this.luminanceLevels; + float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin; + + Span pixelRow = sourceAccess.GetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); + PixelOperations.Instance.ToVector4(this.configuration, pixelRow, vectorBuffer); + + for (int x = 0; x < this.bounds.Width; x++) + { + Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x); + int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels); + float scaledLuminance = Unsafe.Add(ref cdfBase, (uint)luminance) / noOfPixelsMinusCdfMin; + float scalingFactor = scaledLuminance * levels / luminance; + Unsafe.Add(ref vectorRef, (uint)x) = new Vector4(scalingFactor * vector.X, scalingFactor * vector.Y, scalingFactor * vector.Z, vector.W); + } + + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorBuffer, pixelRow); + } + } + + /// + /// A implementing the cdf logic for separate color channels. + /// + private readonly struct SeperateChannelsRowOperation : IRowOperation + { + private readonly Configuration configuration; + private readonly Rectangle bounds; + private readonly IMemoryOwner cdfBuffer; + private readonly Buffer2D source; + private readonly int luminanceLevels; + private readonly float numberOfPixelsMinusCdfMin; + + [MethodImpl(InliningOptions.ShortMethod)] + public SeperateChannelsRowOperation( + Configuration configuration, + Rectangle bounds, + IMemoryOwner cdfBuffer, + Buffer2D source, + int luminanceLevels, + float numberOfPixelsMinusCdfMin) + { + this.configuration = configuration; + this.bounds = bounds; + this.cdfBuffer = cdfBuffer; + this.source = source; + this.luminanceLevels = luminanceLevels; + this.numberOfPixelsMinusCdfMin = numberOfPixelsMinusCdfMin; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) => bounds.Width; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y, Span span) + { + Span vectorBuffer = span[..this.bounds.Width]; + ref Vector4 vectorRef = ref MemoryMarshal.GetReference(vectorBuffer); + ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan()); + PixelAccessor sourceAccess = new(this.source); + int levelsMinusOne = this.luminanceLevels - 1; + float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin; + + Span pixelRow = sourceAccess.GetRowSpan(y); + PixelOperations.Instance.ToVector4(this.configuration, pixelRow, vectorBuffer); + + for (int x = 0; x < this.bounds.Width; x++) + { + Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x) * levelsMinusOne; + + uint originalX = (uint)MathF.Round(vector.X); + float scaledX = Unsafe.Add(ref cdfBase, originalX) / noOfPixelsMinusCdfMin; + uint originalY = (uint)MathF.Round(vector.Y); + float scaledY = Unsafe.Add(ref cdfBase, originalY) / noOfPixelsMinusCdfMin; + uint originalZ = (uint)MathF.Round(vector.Z); + float scaledZ = Unsafe.Add(ref cdfBase, originalZ) / noOfPixelsMinusCdfMin; + Unsafe.Add(ref vectorRef, (uint)x) = new Vector4(scaledX, scaledY, scaledZ, vector.W); + } + + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorBuffer, pixelRow); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs b/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs new file mode 100644 index 0000000..3bfb12f --- /dev/null +++ b/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization { + /// + /// Defines a global histogram equalization applicable to an . + /// + public class GlobalHistogramEqualizationProcessor : HistogramEqualizationProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The number of luminance levels. + /// A value indicating whether to clip the histogram bins at a specific value. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + public GlobalHistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, int clipLimit) + : base(luminanceLevels, clipHistogram, clipLimit) + { + } + + /// + public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + => new GlobalHistogramEqualizationProcessor( + configuration, + this.LuminanceLevels, + this.ClipHistogram, + this.ClipLimit, + source, + sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs new file mode 100644 index 0000000..7af8cab --- /dev/null +++ b/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs @@ -0,0 +1,142 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization { + /// + /// Applies a global histogram equalization to the image. + /// + /// The pixel format. + internal class GlobalHistogramEqualizationProcessor : HistogramEqualizationProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// + /// Indicating whether to clip the histogram bins at a specific value. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public GlobalHistogramEqualizationProcessor( + Configuration configuration, + int luminanceLevels, + bool clipHistogram, + int clipLimit, + Image source, + Rectangle sourceRectangle) + : base(configuration, luminanceLevels, clipHistogram, clipLimit, source, sourceRectangle) + { + } + + /// + protected override void OnFrameApply(ImageFrame source) + { + MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator; + int numberOfPixels = source.Width * source.Height; + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + + using IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean); + + // Build the histogram of the grayscale levels. + GrayscaleLevelsRowOperation grayscaleOperation = new(this.Configuration, interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels); + ParallelRowIterator.IterateRows, Vector4>( + this.Configuration, + interest, + in grayscaleOperation); + + Span histogram = histogramBuffer.GetSpan(); + if (this.ClipHistogramEnabled) + { + this.ClipHistogram(histogram, this.ClipLimit); + } + + using IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean); + + // Calculate the cumulative distribution function, which will map each input pixel to a new value. + int cdfMin = CalculateCdf( + ref MemoryMarshal.GetReference(cdfBuffer.GetSpan()), + ref MemoryMarshal.GetReference(histogram), + histogram.Length - 1); + + float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin; + + // Apply the cdf to each pixel of the image + CdfApplicationRowOperation cdfOperation = new(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin); + ParallelRowIterator.IterateRows( + this.Configuration, + interest, + in cdfOperation); + } + + /// + /// A implementing the cdf application levels logic for . + /// + private readonly struct CdfApplicationRowOperation : IRowOperation + { + private readonly Configuration configuration; + private readonly Rectangle bounds; + private readonly IMemoryOwner cdfBuffer; + private readonly Buffer2D source; + private readonly int luminanceLevels; + private readonly float numberOfPixelsMinusCdfMin; + + [MethodImpl(InliningOptions.ShortMethod)] + public CdfApplicationRowOperation( + Configuration configuration, + Rectangle bounds, + IMemoryOwner cdfBuffer, + Buffer2D source, + int luminanceLevels, + float numberOfPixelsMinusCdfMin) + { + this.configuration = configuration; + this.bounds = bounds; + this.cdfBuffer = cdfBuffer; + this.source = source; + this.luminanceLevels = luminanceLevels; + this.numberOfPixelsMinusCdfMin = numberOfPixelsMinusCdfMin; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) => bounds.Width; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y, Span span) + { + Span vectorBuffer = span[..this.bounds.Width]; + ref Vector4 vectorRef = ref MemoryMarshal.GetReference(vectorBuffer); + ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan()); + int levels = this.luminanceLevels; + float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin; + + Span pixelRow = this.source.DangerousGetRowSpan(y); + PixelOperations.Instance.ToVector4(this.configuration, pixelRow, vectorBuffer); + + for (int x = 0; x < this.bounds.Width; x++) + { + Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x); + int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels); + float luminanceEqualized = Unsafe.Add(ref cdfBase, (uint)luminance) / noOfPixelsMinusCdfMin; + Unsafe.Add(ref vectorRef, (uint)x) = new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, vector.W); + } + + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorBuffer, pixelRow); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs b/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs new file mode 100644 index 0000000..202fe51 --- /dev/null +++ b/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization { + /// + /// A implementing the grayscale levels logic as . + /// + internal readonly struct GrayscaleLevelsRowOperation : IRowOperation + where TPixel : unmanaged, IPixel + { + private readonly Configuration configuration; + private readonly Rectangle bounds; + private readonly IMemoryOwner histogramBuffer; + private readonly Buffer2D source; + private readonly int luminanceLevels; + + [MethodImpl(InliningOptions.ShortMethod)] + public GrayscaleLevelsRowOperation( + Configuration configuration, + Rectangle bounds, + IMemoryOwner histogramBuffer, + Buffer2D source, + int luminanceLevels) + { + this.configuration = configuration; + this.bounds = bounds; + this.histogramBuffer = histogramBuffer; + this.source = source; + this.luminanceLevels = luminanceLevels; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) => bounds.Width; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y, Span span) + { + Span vectorBuffer = span.Slice(0, this.bounds.Width); + ref Vector4 vectorRef = ref MemoryMarshal.GetReference(vectorBuffer); + ref int histogramBase = ref MemoryMarshal.GetReference(this.histogramBuffer.GetSpan()); + int levels = this.luminanceLevels; + + Span pixelRow = this.source.DangerousGetRowSpan(y); + PixelOperations.Instance.ToVector4(this.configuration, pixelRow, vectorBuffer); + + for (int x = 0; x < this.bounds.Width; x++) + { + Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x); + int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels); + Interlocked.Increment(ref Unsafe.Add(ref histogramBase, (uint)luminance)); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs b/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs new file mode 100644 index 0000000..1531e8d --- /dev/null +++ b/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization { + /// + /// Enumerates the different types of defined histogram equalization methods. + /// + public enum HistogramEqualizationMethod : int + { + /// + /// A global histogram equalization. + /// + Global, + + /// + /// Adaptive histogram equalization using a tile interpolation approach. + /// + AdaptiveTileInterpolation, + + /// + /// Adaptive histogram equalization using sliding window. Slower then the tile interpolation mode, but can yield to better results. + /// + AdaptiveSlidingWindow, + + /// + /// Adjusts the brightness levels of a particular image by scaling the + /// minimum and maximum values to the full brightness range. + /// + AutoLevel + } +} diff --git a/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs b/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs new file mode 100644 index 0000000..3aabb2a --- /dev/null +++ b/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization { + /// + /// Data container providing the different options for the histogram equalization. + /// + public class HistogramEqualizationOptions + { + /// + /// Gets or sets the histogram equalization method to use. Defaults to global histogram equalization. + /// + public HistogramEqualizationMethod Method { get; set; } = HistogramEqualizationMethod.Global; + + /// + /// Gets or sets the number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// Defaults to 256. + /// + public int LuminanceLevels { get; set; } = 256; + + /// + /// Gets or sets a value indicating whether to clip the histogram bins at a specific value. + /// It is recommended to use clipping when the AdaptiveTileInterpolation method is used, to suppress artifacts which can occur on the borders of the tiles. + /// Defaults to false. + /// + public bool ClipHistogram { get; set; } + + /// + /// Gets or sets the histogram clip limit. Adaptive histogram equalization may cause noise to be amplified in near constant + /// regions. To reduce this problem, histogram bins which exceed a given limit will be capped at this value. The exceeding values + /// will be redistributed equally to all other bins. The clipLimit depends on the size of the tiles the image is split into + /// and therefore the image size itself. + /// Defaults to 350. + /// + /// For more information, see also: https://en.wikipedia.org/wiki/Adaptive_histogram_equalization#Contrast_Limited_AHE + public int ClipLimit { get; set; } = 350; + + /// + /// Gets or sets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization. + /// Defaults to 8. + /// + public int NumberOfTiles { get; set; } = 8; + + /// + /// Gets or sets a value indicating whether to synchronize the scaling factor over all color channels. + /// This parameter is only applicable to AutoLevel and is ignored for all others. + /// Defaults to true. + /// + public bool SyncChannels { get; set; } = true; + } +} diff --git a/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs new file mode 100644 index 0000000..88eea46 --- /dev/null +++ b/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization { + /// + /// Defines a processor that normalizes the histogram of an image. + /// + public abstract class HistogramEqualizationProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// Indicates, if histogram bins should be clipped. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, int clipLimit) + { + this.LuminanceLevels = luminanceLevels; + this.ClipHistogram = clipHistogram; + this.ClipLimit = clipLimit; + } + + /// + /// Gets the number of luminance levels. + /// + public int LuminanceLevels { get; } + + /// + /// Gets a value indicating whether to clip the histogram bins at a specific value. + /// + public bool ClipHistogram { get; } + + /// + /// Gets the histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// + public int ClipLimit { get; } + + /// + public abstract IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel; + + /// + /// Creates the that implements the algorithm + /// defined by the given . + /// + /// The . + /// The . + public static HistogramEqualizationProcessor FromOptions(HistogramEqualizationOptions options) => options.Method switch + { + HistogramEqualizationMethod.Global + => new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit), + + HistogramEqualizationMethod.AdaptiveTileInterpolation + => new AdaptiveHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.NumberOfTiles), + + HistogramEqualizationMethod.AdaptiveSlidingWindow + => new AdaptiveHistogramEqualizationSlidingWindowProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.NumberOfTiles), + + HistogramEqualizationMethod.AutoLevel + => new AutoLevelProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.SyncChannels), + + _ => new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit), + }; + } +} diff --git a/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs new file mode 100644 index 0000000..fc5ba4b --- /dev/null +++ b/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs @@ -0,0 +1,149 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization { + /// + /// Defines a processor that normalizes the histogram of an image. + /// + /// The pixel format. + internal abstract class HistogramEqualizationProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly float luminanceLevelsFloat; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// Indicates, if histogram bins should be clipped. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + protected HistogramEqualizationProcessor( + Configuration configuration, + int luminanceLevels, + bool clipHistogram, + int clipLimit, + Image source, + Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + Guard.MustBeGreaterThan(luminanceLevels, 0, nameof(luminanceLevels)); + Guard.MustBeGreaterThan(clipLimit, 1, nameof(clipLimit)); + + this.LuminanceLevels = luminanceLevels; + this.luminanceLevelsFloat = luminanceLevels; + this.ClipHistogramEnabled = clipHistogram; + this.ClipLimit = clipLimit; + } + + /// + /// Gets the number of luminance levels. + /// + public int LuminanceLevels { get; } + + /// + /// Gets a value indicating whether to clip the histogram bins at a specific value. + /// + public bool ClipHistogramEnabled { get; } + + /// + /// Gets the histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// + public int ClipLimit { get; } + + /// + /// Calculates the cumulative distribution function. + /// + /// The reference to the array holding the cdf. + /// The reference to the histogram of the input image. + /// Index of the maximum of the histogram. + /// The first none zero value of the cdf. + public static int CalculateCdf(ref int cdfBase, ref int histogramBase, int maxIdx) + { + int histSum = 0; + int cdfMin = 0; + bool cdfMinFound = false; + + for (nuint i = 0; i <= (uint)maxIdx; i++) + { + histSum += Unsafe.Add(ref histogramBase, i); + if (!cdfMinFound && histSum != 0) + { + cdfMin = histSum; + cdfMinFound = true; + } + + // Creating the lookup table: subtracting cdf min, so we do not need to do that inside the for loop. + Unsafe.Add(ref cdfBase, i) = Math.Max(0, histSum - cdfMin); + } + + return cdfMin; + } + + /// + /// AHE tends to over amplify the contrast in near-constant regions of the image, since the histogram in such regions is highly concentrated. + /// Clipping the histogram is meant to reduce this effect, by cutting of histogram bin's which exceed a certain amount and redistribute + /// the values over the clip limit to all other bins equally. + /// + /// The histogram to apply the clipping. + /// Histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + public void ClipHistogram(Span histogram, int clipLimit) + { + int sumOverClip = 0; + ref int histogramBase = ref MemoryMarshal.GetReference(histogram); + + for (nuint i = 0; i < (uint)histogram.Length; i++) + { + ref int histogramLevel = ref Unsafe.Add(ref histogramBase, i); + if (histogramLevel > clipLimit) + { + sumOverClip += histogramLevel - clipLimit; + histogramLevel = clipLimit; + } + } + + // Redistribute the clipped pixels over all bins of the histogram. + int addToEachBin = sumOverClip > 0 ? (int)MathF.Floor(sumOverClip / this.luminanceLevelsFloat) : 0; + if (addToEachBin > 0) + { + for (nuint i = 0; i < (uint)histogram.Length; i++) + { + Unsafe.Add(ref histogramBase, i) += addToEachBin; + } + } + + int residual = sumOverClip - (addToEachBin * this.LuminanceLevels); + if (residual != 0) + { + uint residualStep = (uint)Math.Max(this.LuminanceLevels / residual, 1); + for (nuint i = 0; i < (uint)this.LuminanceLevels && residual > 0; i += residualStep, residual--) + { + ref int histogramLevel = ref Unsafe.Add(ref histogramBase, i); + histogramLevel++; + } + } + } + + /// + /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. + /// + /// The pixel to get the luminance from + /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images) + [MethodImpl(InliningOptions.ShortMethod)] + public static int GetLuminance(TPixel sourcePixel, int luminanceLevels) + { + // TODO: We need a bulk per span equivalent. + Vector4 vector = sourcePixel.ToVector4(); + return ColorNumerics.GetBT709Luminance(ref vector, luminanceLevels); + } + } +} diff --git a/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor.cs b/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor.cs new file mode 100644 index 0000000..488e687 --- /dev/null +++ b/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Overlays { + /// + /// Defines a processing operation to replace the background color of an . + /// + public sealed class BackgroundColorProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The options defining blending algorithm and amount. + /// The to set the background color to. + public BackgroundColorProcessor(GraphicsOptions options, Color color) + { + this.Color = color; + this.GraphicsOptions = options; + } + + /// + /// Gets the Graphics options to alter how processor is applied. + /// + public GraphicsOptions GraphicsOptions { get; } + + /// + /// Gets the background color value. + /// + public Color Color { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new BackgroundColorProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs new file mode 100644 index 0000000..546974d --- /dev/null +++ b/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Overlays { + /// + /// Sets the background color of the image. + /// + /// The pixel format. + internal class BackgroundColorProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly BackgroundColorProcessor definition; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public BackgroundColorProcessor(Configuration configuration, BackgroundColorProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + => this.definition = definition; + + /// + protected override void OnFrameApply(ImageFrame source) + { + TPixel color = this.definition.Color.ToPixel(); + GraphicsOptions graphicsOptions = this.definition.GraphicsOptions; + + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + + Configuration configuration = this.Configuration; + MemoryAllocator memoryAllocator = configuration.MemoryAllocator; + + using IMemoryOwner colors = memoryAllocator.Allocate(interest.Width); + using IMemoryOwner amount = memoryAllocator.Allocate(interest.Width); + + colors.GetSpan().Fill(color); + amount.GetSpan().Fill(graphicsOptions.BlendPercentage); + + PixelBlender blender = PixelOperations.Instance.GetPixelBlender(graphicsOptions); + + RowOperation operation = new(configuration, interest, blender, amount, colors, source.PixelBuffer); + ParallelRowIterator.IterateRows( + configuration, + interest, + in operation); + } + + private readonly struct RowOperation : IRowOperation + { + private readonly Configuration configuration; + private readonly Rectangle bounds; + private readonly PixelBlender blender; + private readonly IMemoryOwner amount; + private readonly IMemoryOwner colors; + private readonly Buffer2D source; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperation( + Configuration configuration, + Rectangle bounds, + PixelBlender blender, + IMemoryOwner amount, + IMemoryOwner colors, + Buffer2D source) + { + this.configuration = configuration; + this.bounds = bounds; + this.blender = blender; + this.amount = amount; + this.colors = colors; + this.source = source; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y) + { + Span destination = + this.source.DangerousGetRowSpan(y) + .Slice(this.bounds.X, this.bounds.Width); + + // Switch color & destination in the 2nd and 3rd places because we are + // applying the target color under the current one. + this.blender.Blend( + this.configuration, + destination, + this.colors.GetSpan(), + destination, + this.amount.GetSpan()); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs b/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs new file mode 100644 index 0000000..9776390 --- /dev/null +++ b/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Overlays { + /// + /// Defines a radial glow effect applicable to an . + /// + public sealed class GlowProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The options effecting blending and composition. + /// The color or the glow. + public GlowProcessor(GraphicsOptions options, Color color) + : this(options, color, 0) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The options effecting blending and composition. + /// The color or the glow. + /// The radius of the glow. + internal GlowProcessor(GraphicsOptions options, Color color, ValueSize radius) + { + this.GlowColor = color; + this.Radius = radius; + this.GraphicsOptions = options; + } + + /// + /// Gets the options effecting blending and composition. + /// + public GraphicsOptions GraphicsOptions { get; } + + /// + /// Gets the glow color to apply. + /// + public Color GlowColor { get; } + + /// + /// Gets the the radius. + /// + internal ValueSize Radius { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new GlowProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs new file mode 100644 index 0000000..06be112 --- /dev/null +++ b/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs @@ -0,0 +1,123 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Overlays { + /// + /// An that applies a radial glow effect an . + /// + /// The pixel format. + internal class GlowProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly PixelBlender blender; + private readonly GlowProcessor definition; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public GlowProcessor(Configuration configuration, GlowProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.definition = definition; + this.blender = PixelOperations.Instance.GetPixelBlender(definition.GraphicsOptions); + } + + /// + protected override void OnFrameApply(ImageFrame source) + { + TPixel glowColor = this.definition.GlowColor.ToPixel(); + float blendPercent = this.definition.GraphicsOptions.BlendPercentage; + + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + + Vector2 center = Rectangle.Center(interest); + float finalRadius = this.definition.Radius.Calculate(interest.Size); + float maxDistance = finalRadius > 0 + ? MathF.Min(finalRadius, interest.Width * .5F) + : interest.Width * .5F; + + Configuration configuration = this.Configuration; + MemoryAllocator allocator = configuration.MemoryAllocator; + + using IMemoryOwner rowColors = allocator.Allocate(interest.Width); + rowColors.GetSpan().Fill(glowColor); + + RowOperation operation = new(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source.PixelBuffer); + ParallelRowIterator.IterateRows( + configuration, + interest, + in operation); + } + + private readonly struct RowOperation : IRowOperation + { + private readonly Configuration configuration; + private readonly Rectangle bounds; + private readonly PixelBlender blender; + private readonly Vector2 center; + private readonly float maxDistance; + private readonly float blendPercent; + private readonly IMemoryOwner colors; + private readonly Buffer2D source; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperation( + Configuration configuration, + Rectangle bounds, + IMemoryOwner colors, + PixelBlender blender, + Vector2 center, + float maxDistance, + float blendPercent, + Buffer2D source) + { + this.configuration = configuration; + this.bounds = bounds; + this.colors = colors; + this.blender = blender; + this.center = center; + this.maxDistance = maxDistance; + this.blendPercent = blendPercent; + this.source = source; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) + => bounds.Width; + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y, Span span) + { + Span colorSpan = this.colors.GetSpan(); + + for (int i = 0; i < this.bounds.Width; i++) + { + float distance = Vector2.Distance(this.center, new Vector2(i + this.bounds.X, y)); + span[i] = Numerics.Clamp(this.blendPercent * (1 - (.95F * (distance / this.maxDistance))), 0, 1F); + } + + Span destination = this.source.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); + + this.blender.Blend( + this.configuration, + destination, + destination, + colorSpan, + span); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs b/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs new file mode 100644 index 0000000..d522ed8 --- /dev/null +++ b/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Overlays { + /// + /// Defines a radial vignette effect applicable to an . + /// + public sealed class VignetteProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The options effecting blending and composition. + /// The color of the vignette. + public VignetteProcessor(GraphicsOptions options, Color color) + { + this.VignetteColor = color; + this.GraphicsOptions = options; + } + + /// + /// Initializes a new instance of the class. + /// + /// The options effecting blending and composition. + /// The color of the vignette. + /// The x-radius. + /// The y-radius. + internal VignetteProcessor(GraphicsOptions options, Color color, ValueSize radiusX, ValueSize radiusY) + { + this.VignetteColor = color; + this.RadiusX = radiusX; + this.RadiusY = radiusY; + this.GraphicsOptions = options; + } + + /// + /// Gets the options effecting blending and composition + /// + public GraphicsOptions GraphicsOptions { get; } + + /// + /// Gets the vignette color to apply. + /// + public Color VignetteColor { get; } + + /// + /// Gets the the x-radius. + /// + internal ValueSize RadiusX { get; } + + /// + /// Gets the the y-radius. + /// + internal ValueSize RadiusY { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new VignetteProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs new file mode 100644 index 0000000..9e1260a --- /dev/null +++ b/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs @@ -0,0 +1,131 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Overlays { + /// + /// An that applies a radial vignette effect to an . + /// + /// The pixel format. + internal class VignetteProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly PixelBlender blender; + private readonly VignetteProcessor definition; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public VignetteProcessor(Configuration configuration, VignetteProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.definition = definition; + this.blender = PixelOperations.Instance.GetPixelBlender(definition.GraphicsOptions); + } + + /// + protected override void OnFrameApply(ImageFrame source) + { + TPixel vignetteColor = this.definition.VignetteColor.ToPixel(); + float blendPercent = this.definition.GraphicsOptions.BlendPercentage; + + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + + Vector2 center = Rectangle.Center(interest); + float finalRadiusX = this.definition.RadiusX.Calculate(interest.Size); + float finalRadiusY = this.definition.RadiusY.Calculate(interest.Size); + + float rX = finalRadiusX > 0 + ? MathF.Min(finalRadiusX, interest.Width * .5F) + : interest.Width * .5F; + + float rY = finalRadiusY > 0 + ? MathF.Min(finalRadiusY, interest.Height * .5F) + : interest.Height * .5F; + + float maxDistance = MathF.Sqrt((rX * rX) + (rY * rY)); + + Configuration configuration = this.Configuration; + MemoryAllocator allocator = configuration.MemoryAllocator; + + using IMemoryOwner rowColors = allocator.Allocate(interest.Width); + rowColors.GetSpan().Fill(vignetteColor); + + RowOperation operation = new(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source.PixelBuffer); + ParallelRowIterator.IterateRows( + configuration, + interest, + in operation); + } + + private readonly struct RowOperation : IRowOperation + { + private readonly Configuration configuration; + private readonly Rectangle bounds; + private readonly PixelBlender blender; + private readonly Vector2 center; + private readonly float maxDistance; + private readonly float blendPercent; + private readonly IMemoryOwner colors; + private readonly Buffer2D source; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperation( + Configuration configuration, + Rectangle bounds, + IMemoryOwner colors, + PixelBlender blender, + Vector2 center, + float maxDistance, + float blendPercent, + Buffer2D source) + { + this.configuration = configuration; + this.bounds = bounds; + this.colors = colors; + this.blender = blender; + this.center = center; + this.maxDistance = maxDistance; + this.blendPercent = blendPercent; + this.source = source; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) + => bounds.Width; + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y, Span span) + { + Span colorSpan = this.colors.GetSpan(); + + for (int i = 0; i < this.bounds.Width; i++) + { + float distance = Vector2.Distance(this.center, new Vector2(i + this.bounds.X, y)); + span[i] = Numerics.Clamp(this.blendPercent * (.9F * (distance / this.maxDistance)), 0, 1F); + } + + Span destination = this.source.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); + + this.blender.Blend( + this.configuration, + destination, + destination, + colorSpan, + span); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs b/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs new file mode 100644 index 0000000..4bf5f29 --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// Defines the precision level used when matching colors during quantization. + /// + public enum ColorMatchingMode + { + /// + /// Uses a coarse caching strategy optimized for performance at the expense of exact matches. + /// This provides the fastest matching but may yield approximate results. + /// + Coarse, + + /// + /// Performs exact color matching using a bounded exact-match cache with eviction. + /// This preserves exact color matching while accelerating repeated colors. + /// + Exact + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/DefaultPixelSamplingStrategy.cs b/ImageSharp/Processing/Processors/Quantization/DefaultPixelSamplingStrategy.cs new file mode 100644 index 0000000..a964e70 --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/DefaultPixelSamplingStrategy.cs @@ -0,0 +1,158 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// A pixel sampling strategy that enumerates a limited amount of rows from different frames, + /// if the total number of pixels is over a threshold. + /// + public class DefaultPixelSamplingStrategy : IPixelSamplingStrategy + { + // TODO: This value shall be determined by benchmarking. + // A smaller value should likely work well, providing better perf. + private const int DefaultMaximumPixels = 4096 * 4096; + + /// + /// Initializes a new instance of the class. + /// + public DefaultPixelSamplingStrategy() + : this(DefaultMaximumPixels, 0.1) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The maximum number of pixels to process. + /// always scan at least this portion of total pixels within the image. + public DefaultPixelSamplingStrategy(int maximumPixels, double minimumScanRatio) + { + Guard.MustBeGreaterThan(maximumPixels, 0, nameof(maximumPixels)); + this.MaximumPixels = maximumPixels; + this.MinimumScanRatio = minimumScanRatio; + } + + /// + /// Gets the maximum number of pixels to process. (The threshold.) + /// + public long MaximumPixels { get; } + + /// + /// Gets a value indicating: always scan at least this portion of total pixels within the image. + /// The default is 0.1 (10%). + /// + public double MinimumScanRatio { get; } + + /// + public IEnumerable> EnumeratePixelRegions(Image image) + where TPixel : unmanaged, IPixel + { + long maximumPixels = Math.Min(this.MaximumPixels, (long)image.Width * image.Height * image.Frames.Count); + long maxNumberOfRows = maximumPixels / image.Width; + long totalNumberOfRows = (long)image.Height * image.Frames.Count; + + if (totalNumberOfRows <= maxNumberOfRows) + { + // Enumerate all pixels + foreach (ImageFrame frame in image.Frames) + { + yield return frame.PixelBuffer.GetRegion(); + } + } + else + { + double r = maxNumberOfRows / (double)totalNumberOfRows; + + // Use a rough approximation to make sure we don't leave out large contiguous regions: + if (maxNumberOfRows > 200) + { + r = Math.Round(r, 2); + } + else + { + r = Math.Round(r, 1); + } + + r = Math.Max(this.MinimumScanRatio, r); // always visit the minimum defined portion of the image. + + Rational ratio = new(r); + + int denom = (int)ratio.Denominator; + int num = (int)ratio.Numerator; + DebugGuard.MustBeGreaterThan(denom, 0, "Denominator must be greater than zero."); + + for (int pos = 0; pos < totalNumberOfRows; pos++) + { + int subPos = (int)((uint)pos % (uint)denom); + if (subPos < num) + { + yield return GetRow(pos); + } + } + + Buffer2DRegion GetRow(int pos) + { + int frameIdx = pos / image.Height; + int y = pos % image.Height; + return image.Frames[frameIdx].PixelBuffer.GetRegion(0, y, image.Width, 1); + } + } + } + + /// + public IEnumerable> EnumeratePixelRegions(ImageFrame frame) + where TPixel : unmanaged, IPixel + { + long maximumPixels = Math.Min(this.MaximumPixels, (long)frame.Width * frame.Height); + long maxNumberOfRows = maximumPixels / frame.Width; + long totalNumberOfRows = frame.Height; + + if (totalNumberOfRows <= maxNumberOfRows) + { + yield return frame.PixelBuffer.GetRegion(); + } + else + { + double r = maxNumberOfRows / (double)totalNumberOfRows; + + // Use a rough approximation to make sure we don't leave out large contiguous regions: + if (maxNumberOfRows > 200) + { + r = Math.Round(r, 2); + } + else + { + r = Math.Round(r, 1); + } + + r = Math.Max(this.MinimumScanRatio, r); // always visit the minimum defined portion of the image. + + Rational ratio = new(r); + + int denom = (int)ratio.Denominator; + int num = (int)ratio.Numerator; + DebugGuard.MustBeGreaterThan(denom, 0, "Denominator must be greater than zero."); + + for (int pos = 0; pos < totalNumberOfRows; pos++) + { + int subPos = (int)((uint)pos % (uint)denom); + if (subPos < num) + { + yield return GetRow(pos); + } + } + + Buffer2DRegion GetRow(int pos) + { + int y = pos % frame.Height; + return frame.PixelBuffer.GetRegion(0, y, frame.Width, 1); + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs b/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs new file mode 100644 index 0000000..f152a9b --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs @@ -0,0 +1,261 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// Gets the closest color to the supplied color based upon the Euclidean distance. + /// + /// The pixel format. + /// The cache type. + /// + /// This class is not thread safe and should not be accessed in parallel. + /// Doing so will result in non-idempotent results. + /// + internal sealed class EuclideanPixelMap : PixelMap + where TPixel : unmanaged, IPixel + where TCache : struct, IColorIndexCache + { + private Rgba32[] rgbaPalette; + + // Do not make readonly. It's a mutable struct. +#pragma warning disable IDE0044 // Add readonly modifier + private TCache cache; +#pragma warning restore IDE0044 // Add readonly modifier + + private readonly Configuration configuration; + + /// + /// Initializes a new instance of the class. + /// + /// Specifies the settings and resources for the pixel map's operations. + /// Defines the color palette used for pixel mapping. + public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory palette) + { + this.configuration = configuration; + this.Palette = palette; + this.rgbaPalette = new Rgba32[palette.Length]; + this.cache = TCache.Create(configuration.MemoryAllocator); + PixelOperations.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette); + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public override int GetClosestColor(TPixel color, out TPixel match) + { + ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.Palette.Span); + Rgba32 rgba = color.ToRgba32(); + + if (this.cache.TryGetValue(rgba, out short index)) + { + match = Unsafe.Add(ref paletteRef, (ushort)index); + return index; + } + + return this.GetClosestColorSlow(rgba, ref paletteRef, out match); + } + + /// + public override void Clear(ReadOnlyMemory palette) + { + this.Palette = palette; + this.rgbaPalette = new Rgba32[palette.Length]; + PixelOperations.Instance.ToRgba32(this.configuration, this.Palette.Span, this.rgbaPalette); + this.cache.Clear(); + } + + [MethodImpl(InliningOptions.ColdPath)] + private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match) + { + ReadOnlySpan rgbaPalette = this.rgbaPalette; + ref Rgba32 rgbaPaletteRef = ref MemoryMarshal.GetReference(rgbaPalette); + int index = 0; + int leastDistance = int.MaxValue; + int i = 0; + + if (Vector128.IsHardwareAccelerated && rgbaPalette.Length >= 4) + { + // Duplicate the query color so one 128-bit register can be subtracted from + // two packed RGBA candidates at a time after widening. + Vector128 pixel = Vector128.Create( + rgba.R, + rgba.G, + rgba.B, + rgba.A, + rgba.R, + rgba.G, + rgba.B, + rgba.A); + + int vectorizedLength = rgbaPalette.Length & ~0x03; + + for (; i < vectorizedLength; i += 4) + { + // Load four packed Rgba32 values (16 bytes) and widen them into two vectors: + // [c0.r, c0.g, c0.b, c0.a, c1.r, ...] and [c2.r, c2.g, c2.b, c2.a, c3.r, ...]. + Vector128 packed = Vector128.LoadUnsafe(ref Unsafe.As(ref Unsafe.Add(ref rgbaPaletteRef, i))); + Vector128 lowerDiff = Vector128.WidenLower(packed).AsInt16() - pixel; + Vector128 upperDiff = Vector128.WidenUpper(packed).AsInt16() - pixel; + + // MultiplyAddAdjacent collapses channel squares into RG + BA partial sums, + // so each pair of int lanes still corresponds to one candidate color. + Vector128 lowerPairs = Vector128_.MultiplyAddAdjacent(lowerDiff, lowerDiff); + Vector128 upperPairs = Vector128_.MultiplyAddAdjacent(upperDiff, upperDiff); + + // Sum the two partials for candidates i and i + 1. + ref int lowerRef = ref Unsafe.As, int>(ref lowerPairs); + int distance = lowerRef + Unsafe.Add(ref lowerRef, 1); + if (distance < leastDistance) + { + index = i; + leastDistance = distance; + if (distance == 0) + { + goto Found; + } + } + + distance = Unsafe.Add(ref lowerRef, 2) + Unsafe.Add(ref lowerRef, 3); + if (distance < leastDistance) + { + index = i + 1; + leastDistance = distance; + if (distance == 0) + { + goto Found; + } + } + + // Sum the two partials for candidates i + 2 and i + 3. + ref int upperRef = ref Unsafe.As, int>(ref upperPairs); + distance = upperRef + Unsafe.Add(ref upperRef, 1); + if (distance < leastDistance) + { + index = i + 2; + leastDistance = distance; + if (distance == 0) + { + goto Found; + } + } + + distance = Unsafe.Add(ref upperRef, 2) + Unsafe.Add(ref upperRef, 3); + if (distance < leastDistance) + { + index = i + 3; + leastDistance = distance; + if (distance == 0) + { + goto Found; + } + } + } + } + + for (; i < rgbaPalette.Length; i++) + { + int distance = DistanceSquared(rgba, Unsafe.Add(ref rgbaPaletteRef, i)); + if (distance < leastDistance) + { + index = i; + leastDistance = distance; + if (distance == 0) + { + goto Found; + } + } + } + + Found: + + // Now I have the index, pop it into the cache for next time + _ = this.cache.TryAdd(rgba, (short)index); + match = Unsafe.Add(ref paletteRef, (uint)index); + + return index; + } + + /// + /// Returns the Euclidean distance squared between two specified points. + /// + /// The first point. + /// The second point. + /// The distance squared. + [MethodImpl(InliningOptions.ShortMethod)] + private static int DistanceSquared(Rgba32 a, Rgba32 b) + { + int deltaR = a.R - b.R; + int deltaG = a.G - b.G; + int deltaB = a.B - b.B; + int deltaA = a.A - b.A; + return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA); + } + + /// + public override void Dispose() => this.cache.Dispose(); + } + + /// + /// Represents a map of colors to indices. + /// + /// The pixel format. + internal abstract class PixelMap : IDisposable + where TPixel : unmanaged, IPixel + { + /// + /// Gets the color palette of this . + /// + public ReadOnlyMemory Palette { get; private protected set; } + + /// + /// Returns the closest color in the palette and the index of that pixel. + /// + /// The color to match. + /// The matched color. + /// + /// The index. + /// + public abstract int GetClosestColor(TPixel color, out TPixel match); + + /// + /// Clears the map, resetting it to use the given palette. + /// + /// The color palette to map from. + public abstract void Clear(ReadOnlyMemory palette); + + /// + public abstract void Dispose(); + } + + /// + /// A factory for creating instances. + /// + internal static class PixelMapFactory + { + /// + /// Creates a new instance. + /// + /// The pixel format. + /// The configuration. + /// The color palette to map from. + /// The color matching mode. + /// + /// The . + /// + public static PixelMap Create( + Configuration configuration, + ReadOnlyMemory palette, + ColorMatchingMode colorMatchingMode) + where TPixel : unmanaged, IPixel => colorMatchingMode switch + { + ColorMatchingMode.Exact => new EuclideanPixelMap(configuration, palette), + _ => new EuclideanPixelMap(configuration, palette), + }; + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/ExtensivePixelSamplingStrategy.cs b/ImageSharp/Processing/Processors/Quantization/ExtensivePixelSamplingStrategy.cs new file mode 100644 index 0000000..736cc21 --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/ExtensivePixelSamplingStrategy.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// A pixel sampling strategy that enumerates all pixels. + /// + public class ExtensivePixelSamplingStrategy : IPixelSamplingStrategy + { + /// + public IEnumerable> EnumeratePixelRegions(Image image) + where TPixel : unmanaged, IPixel + { + foreach (ImageFrame frame in image.Frames) + { + yield return frame.PixelBuffer.GetRegion(); + } + } + + /// + public IEnumerable> EnumeratePixelRegions(ImageFrame frame) + where TPixel : unmanaged, IPixel + { + yield return frame.PixelBuffer.GetRegion(); + } + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer.cs b/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer.cs new file mode 100644 index 0000000..a0bffef --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// Quantizes images by grouping colors in an adaptive 16-way tree and reducing those groups into a palette. + /// + /// + /// Each level routes colors using one bit of RGB and, when useful, one bit of alpha. Fully opaque mid-tone colors + /// use RGB-only routing so more branch resolution is spent on visible color detail, while transparent, dark, and + /// light colors use alpha-aware routing so opacity changes can form their own palette buckets. + /// + public class HexadecatreeQuantizer : IQuantizer + { + /// + /// Initializes a new instance of the class + /// using the default . + /// + public HexadecatreeQuantizer() + : this(new QuantizerOptions()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The quantizer options that control palette size, dithering, and transparency behavior. + public HexadecatreeQuantizer(QuantizerOptions options) + { + Guard.NotNull(options, nameof(options)); + this.Options = options; + } + + /// + public QuantizerOptions Options { get; } + + /// + public IQuantizer CreatePixelSpecificQuantizer(Configuration configuration) + where TPixel : unmanaged, IPixel + => this.CreatePixelSpecificQuantizer(configuration, this.Options); + + /// + public IQuantizer CreatePixelSpecificQuantizer(Configuration configuration, QuantizerOptions options) + where TPixel : unmanaged, IPixel + => new HexadecatreeQuantizer(configuration, options); + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer{TPixel}.cs b/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer{TPixel}.cs new file mode 100644 index 0000000..b2a5cce --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer{TPixel}.cs @@ -0,0 +1,686 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// Quantizes an image by building an adaptive 16-way color tree and reducing it to the requested palette size. + /// + /// + /// + /// Each level routes colors using one bit of RGB and, when useful, one bit of alpha, giving the tree up to 16 children + /// per node and letting transparency participate directly in palette construction. + /// + /// + /// Fully opaque mid-tone colors use RGB-only routing so more branch resolution is spent on visible color detail. + /// Transparent, dark, and light colors use alpha-aware routing so opacity changes can form distinct palette buckets. + /// + /// + /// The pixel format. +#pragma warning disable CA1001 // Types that own disposable fields should be disposable + // See https://github.com/dotnet/roslyn-analyzers/issues/6151 + public struct HexadecatreeQuantizer : IQuantizer +#pragma warning restore CA1001 // Types that own disposable fields should be disposable + where TPixel : unmanaged, IPixel + { + private readonly int maxColors; + private readonly int bitDepth; + private readonly Hexadecatree tree; + private readonly IMemoryOwner paletteOwner; + private ReadOnlyMemory palette; + private PixelMap? pixelMap; + private readonly bool isDithering; + private bool isDisposed; + + /// + /// Initializes a new instance of the struct. + /// + /// The configuration that provides memory allocation and pixel conversion services. + /// The quantizer options that control palette size, dithering, and transparency behavior. + [MethodImpl(InliningOptions.ShortMethod)] + public HexadecatreeQuantizer(Configuration configuration, QuantizerOptions options) + { + this.Configuration = configuration; + this.Options = options; + + this.maxColors = this.Options.MaxColors; + this.bitDepth = Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.maxColors), 1, 8); + this.tree = new Hexadecatree(configuration, this.bitDepth, this.maxColors, this.Options.TransparencyThreshold); + this.paletteOwner = configuration.MemoryAllocator.Allocate(this.maxColors, AllocationOptions.Clean); + this.pixelMap = default; + this.palette = default; + this.isDithering = this.Options.Dither is not null; + this.isDisposed = false; + } + + /// + public Configuration Configuration { get; } + + /// + public QuantizerOptions Options { get; } + + /// + public ReadOnlyMemory Palette + { + get + { + if (this.palette.IsEmpty) + { + this.ResolvePalette(); + QuantizerUtilities.CheckPaletteState(in this.palette); + } + + return this.palette; + } + } + + /// + public readonly void AddPaletteColors(in Buffer2DRegion pixelRegion) + { + PixelRowDelegate pixelRowDelegate = new(this.tree); + QuantizerUtilities.AddPaletteColors, TPixel, Rgba32, PixelRowDelegate>( + ref Unsafe.AsRef(in this), + in pixelRegion, + in pixelRowDelegate); + } + + /// + /// Materializes the final palette from the accumulated tree and prepares the dither lookup map when needed. + /// + private void ResolvePalette() + { + short paletteIndex = 0; + Span paletteSpan = this.paletteOwner.GetSpan(); + + this.tree.Palettize(paletteSpan, ref paletteIndex); + ReadOnlyMemory result = this.paletteOwner.Memory[..paletteSpan.Length]; + + if (this.isDithering) + { + // Dithered colors often no longer land on a color that was seen during palette construction, + // so the quantization pass switches to nearest-palette matching once the palette is finalized. + this.pixelMap = PixelMapFactory.Create(this.Configuration, result, this.Options.ColorMatchingMode); + } + + this.palette = result; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public readonly IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + => QuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(in this), source, bounds); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public readonly byte GetQuantizedColor(TPixel color, out TPixel match) + { + if (this.isDithering) + { + // Dithering introduces adjusted colors that were never inserted into the tree, so tree lookup + // is only reliable for the non-dithered path. + return (byte)this.pixelMap!.GetClosestColor(color, out match); + } + + ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.palette.Span); + int index = this.tree.GetPaletteIndex(color); + match = Unsafe.Add(ref paletteRef, (nuint)index); + return (byte)index; + } + + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.isDisposed = true; + this.paletteOwner.Dispose(); + this.pixelMap?.Dispose(); + this.pixelMap = null; + this.tree.Dispose(); + } + } + + /// + /// Forwards source rows into the tree without creating an intermediate buffer. + /// + private readonly struct PixelRowDelegate : IQuantizingPixelRowDelegate + { + private readonly Hexadecatree tree; + + /// + /// Initializes a new instance of the struct. + /// + /// The destination tree that should accumulate each visited row. + public PixelRowDelegate(Hexadecatree tree) => this.tree = tree; + + /// + public void Invoke(ReadOnlySpan row, int rowIndex) => this.tree.AddColors(row); + } + + /// + /// Stores the adaptive 16-way partition tree used to accumulate colors and emit palette entries. + /// + /// + /// The tree uses a fixed node arena for predictable allocation behavior, keeps per-level reducible node lists so + /// deeper buckets can be merged until the palette fits, and caches the previously inserted leaf so repeated colors + /// can be accumulated cheaply. + /// + internal sealed class Hexadecatree : IDisposable + { + // Pooled buffer for OctreeNodes. + private readonly IMemoryOwner nodesOwner; + + // One reducible-node head per level. + // Each entry stores a node index, or -1 when that level currently + // has no reducible nodes. + private readonly short[] reducibleNodes; + + // Maximum number of allowable colors. + private readonly int maxColors; + + // Maximum significant bits. + private readonly int maxColorBits; + + // The threshold for transparent colors. + private readonly int transparencyThreshold255; + + // Instead of a reference to the root, we store the index of the root node. + // Index 0 is reserved for the root. + private readonly short rootIndex; + + // Running index for node allocation. Start at 1 so that index 0 is reserved for the root. + private short nextNode = 1; + + // Previously quantized node (index; -1 if none) and its color. + private int previousNode; + private Rgba32 previousColor; + + // Free list for reclaimed node indices. + private readonly Stack freeIndices = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The configuration that provides the backing memory allocator. + /// The number of levels to descend before forcing leaves. + /// The maximum number of palette entries the reduced tree may retain. + /// The alpha threshold below which generated palette entries become fully transparent. + public Hexadecatree( + Configuration configuration, + int maxColorBits, + int maxColors, + float transparencyThreshold) + { + this.maxColorBits = maxColorBits; + this.maxColors = maxColors; + this.transparencyThreshold255 = (int)(transparencyThreshold * 255F); + this.Leaves = 0; + this.previousNode = -1; + this.previousColor = default; + + // Allocate a conservative buffer for nodes. + const int capacity = 4096; + this.nodesOwner = configuration.MemoryAllocator.Allocate(capacity, AllocationOptions.Clean); + + // Create the reducible nodes array (one per level 0 .. maxColorBits-1). + this.reducibleNodes = new short[this.maxColorBits]; + this.reducibleNodes.AsSpan().Fill(-1); + + // Reserve index 0 for the root. + this.rootIndex = 0; + ref Node root = ref this.Nodes[this.rootIndex]; + root.Initialize(0, this.maxColorBits, this, this.rootIndex); + } + + /// + /// Gets or sets the number of leaf nodes currently representing palette buckets. + /// + public int Leaves { get; set; } + + /// + /// Gets the underlying node arena. + /// + internal Span Nodes => this.nodesOwner.Memory.Span; + + /// + /// Adds a row of colors to the tree. + /// + /// The colors to accumulate. + public void AddColors(ReadOnlySpan row) + { + for (int x = 0; x < row.Length; x++) + { + this.AddColor(row[x]); + } + } + + /// + /// Adds a single color sample to the tree. + /// + /// The color to accumulate. + private void AddColor(Rgba32 color) + { + // Once the node arena is full and there are no recycled slots available, keep collapsing + // reducible leaves until the tree is small enough to make forward progress again. + if (this.nextNode >= this.Nodes.Length && this.freeIndices.Count == 0) + { + while (this.Leaves > this.maxColors) + { + this.Reduce(); + } + } + + // Scanlines often contain long runs of the same color. Caching the previous leaf lets those + // repeats skip the tree walk and just bump the accumulated sums in place. + if (this.previousColor.Equals(color)) + { + if (this.previousNode == -1) + { + this.previousColor = color; + Node.AddColor(this.rootIndex, color, this.maxColorBits, 0, this); + } + else + { + Node.Increment(this.previousNode, color, this); + } + } + else + { + this.previousColor = color; + Node.AddColor(this.rootIndex, color, this.maxColorBits, 0, this); + } + } + + /// + /// Reduces the tree to the requested palette size and emits the final palette entries. + /// + /// The destination palette span. + /// The running palette index. + public void Palettize(Span palette, ref short paletteIndex) + { + while (this.Leaves > this.maxColors) + { + this.Reduce(); + } + + this.Nodes[this.rootIndex].ConstructPalette(this, palette, ref paletteIndex); + } + + /// + /// Gets the palette index selected by the tree for the supplied color. + /// + /// The color to resolve. + /// The palette index represented by the best matching leaf in the reduced tree. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetPaletteIndex(TPixel color) + => this.Nodes[this.rootIndex].GetPaletteIndex(color.ToRgba32(), 0, this); + + /// + /// Records the most recently touched leaf so repeated colors can bypass another descent. + /// + /// The leaf node index. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void TrackPrevious(int nodeIndex) + => this.previousNode = nodeIndex; + + /// + /// Collapses the deepest currently reducible node into a single leaf. + /// + private void Reduce() + { + int index = this.maxColorBits - 1; + while ((index > 0) && (this.reducibleNodes[index] == -1)) + { + index--; + } + + ref Node node = ref this.Nodes[this.reducibleNodes[index]]; + this.reducibleNodes[index] = node.NextReducibleIndex; + node.Reduce(this); + + // If the last inserted leaf was merged away, the next repeated color must walk the tree again. + this.previousNode = -1; + } + + /// + /// Allocates a node index from the free list or from the unused tail of the arena. + /// + /// The allocated node index, or -1 if no node can be allocated. + internal short AllocateNode() + { + if (this.freeIndices.Count > 0) + { + return this.freeIndices.Pop(); + } + + if (this.nextNode >= this.Nodes.Length) + { + return -1; + } + + short newIndex = this.nextNode; + this.nextNode++; + return newIndex; + } + + /// + /// Returns a node index to the free list. + /// + /// The node index to recycle. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void FreeNode(short index) + { + this.freeIndices.Push(index); + this.Leaves--; + } + + /// + public void Dispose() => this.nodesOwner.Dispose(); + + /// + /// Represents one node in the hexadecatree node arena. + /// + [StructLayout(LayoutKind.Sequential)] + internal struct Node + { + public bool Leaf; + public long PixelCount; + public long Red; + public long Green; + public long Blue; + public long Alpha; + public short PaletteIndex; + public short NextReducibleIndex; + private InlineArray16 children; + + /// + /// Gets the 16 child slots for this node. + /// + [UnscopedRef] + public Span Children => this.children; + + /// + /// Initializes a node either as a leaf or as a reducible interior node. + /// + /// The depth of the node being initialized. + /// The maximum tree depth. + /// The owning tree. + /// The node index in the arena. + public void Initialize(int level, int colorBits, Hexadecatree tree, short index) + { + this.Leaf = level == colorBits; + this.Red = 0; + this.Green = 0; + this.Blue = 0; + this.Alpha = 0; + this.PixelCount = 0; + this.PaletteIndex = 0; + this.NextReducibleIndex = -1; + this.Children.Fill(-1); + + if (this.Leaf) + { + tree.Leaves++; + } + else + { + // Track reducible nodes per level so palette reduction can always collapse the deepest + // buckets first without scanning the entire arena. + this.NextReducibleIndex = tree.reducibleNodes[level]; + tree.reducibleNodes[level] = index; + } + } + + /// + /// Descends the tree for the supplied color, allocating nodes as needed until a leaf is reached. + /// + /// The current node index. + /// The color being accumulated. + /// The maximum tree depth. + /// The current depth. + /// The owning tree. + public static void AddColor(int nodeIndex, Rgba32 color, int colorBits, int level, Hexadecatree tree) + { + ref Node node = ref tree.Nodes[nodeIndex]; + if (node.Leaf) + { + Increment(nodeIndex, color, tree); + tree.TrackPrevious(nodeIndex); + return; + } + + int index = GetColorIndex(color, level); + Span children = node.Children; + short childIndex = children[index]; + + if (childIndex == -1) + { + childIndex = tree.AllocateNode(); + if (childIndex == -1) + { + // If the arena is exhausted and no node can be reclaimed yet, fall back to + // accumulating into the current node instead of failing the insert outright. + Increment(nodeIndex, color, tree); + tree.TrackPrevious(nodeIndex); + return; + } + + ref Node child = ref tree.Nodes[childIndex]; + child.Initialize(level + 1, colorBits, tree, childIndex); + children[index] = childIndex; + } + + // Keep descending until we reach the leaf bucket that should accumulate this sample. + AddColor(childIndex, color, colorBits, level + 1, tree); + } + + /// + /// Adds the supplied color sample to an existing node's running sums. + /// + /// The node index to update. + /// The color sample being accumulated. + /// The owning tree. + public static void Increment(int nodeIndex, Rgba32 color, Hexadecatree tree) + { + ref Node node = ref tree.Nodes[nodeIndex]; + node.PixelCount++; + node.Red += color.R; + node.Green += color.G; + node.Blue += color.B; + node.Alpha += color.A; + } + + /// + /// Merges all child nodes into this node and turns it into a leaf. + /// + /// The owning tree. + public void Reduce(Hexadecatree tree) + { + // If already a leaf, do nothing. + if (this.Leaf) + { + return; + } + + // Allocation fallback can accumulate samples on an interior node. Seed the merge + // with this node's own sums so reduction preserves those samples with its children. + long pixelCount = this.PixelCount; + long sumRed = this.Red; + long sumGreen = this.Green; + long sumBlue = this.Blue; + long sumAlpha = this.Alpha; + Span children = this.Children; + + for (int i = 0; i < children.Length; i++) + { + short childIndex = children[i]; + if (childIndex != -1) + { + ref Node child = ref tree.Nodes[childIndex]; + long pixels = child.PixelCount; + sumRed += child.Red; + sumGreen += child.Green; + sumBlue += child.Blue; + sumAlpha += child.Alpha; + pixelCount += pixels; + + children[i] = -1; + tree.FreeNode(childIndex); + } + } + + if (pixelCount > 0) + { + this.Red = sumRed; + this.Green = sumGreen; + this.Blue = sumBlue; + this.Alpha = sumAlpha; + this.PixelCount = pixelCount; + } + else + { + this.Red = this.Green = this.Blue = this.Alpha = 0; + this.PixelCount = 0; + } + + this.Leaf = true; + tree.Leaves++; + } + + /// + /// Traverses the reduced tree and emits one palette color per leaf. + /// + /// The owning tree. + /// The destination palette span. + /// The running palette index. + public void ConstructPalette(Hexadecatree tree, Span palette, ref short paletteIndex) + { + if (this.Leaf) + { + Vector4 sum = new(this.Red, this.Green, this.Blue, this.Alpha); + Vector4 offset = new(this.PixelCount >> 1); + Vector4 vector = Vector4.Clamp( + (sum + offset) / this.PixelCount, + Vector4.Zero, + new Vector4(255)); + + if (vector.W < tree.transparencyThreshold255) + { + vector = Vector4.Zero; + } + + palette[paletteIndex] = TPixel.FromRgba32(new Rgba32((byte)vector.X, (byte)vector.Y, (byte)vector.Z, (byte)vector.W)); + this.PaletteIndex = paletteIndex++; + } + else + { + Span children = this.Children; + for (int i = 0; i < children.Length; i++) + { + int childIndex = children[i]; + if (childIndex != -1) + { + tree.Nodes[childIndex].ConstructPalette(tree, palette, ref paletteIndex); + } + } + } + } + + /// + /// Resolves the palette index represented by this node for the supplied color. + /// + /// The color to resolve. + /// The current tree depth. + /// The owning tree. + /// The palette index for the best reachable leaf, or -1 if no leaf can be reached. + public int GetPaletteIndex(Rgba32 color, int level, Hexadecatree tree) + { + if (this.Leaf) + { + return this.PaletteIndex; + } + + int colorIndex = GetColorIndex(color, level); + Span children = this.Children; + int childIndex = children[colorIndex]; + if (childIndex != -1) + { + return tree.Nodes[childIndex].GetPaletteIndex(color, level + 1, tree); + } + + // After reductions the exact branch can disappear, so fall back to the first reachable descendant leaf. + for (int i = 0; i < children.Length; i++) + { + childIndex = children[i]; + if (childIndex != -1) + { + int childPaletteIndex = tree.Nodes[childIndex].GetPaletteIndex(color, level + 1, tree); + if (childPaletteIndex != -1) + { + return childPaletteIndex; + } + } + } + + return -1; + } + + /// + /// Computes the child slot for a color at the supplied tree level. + /// + /// The color being routed. + /// The tree depth whose bit plane should be sampled. + /// The child slot index for the color at the supplied level. + /// + /// For fully opaque mid-tone colors the tree ignores alpha and routes on RGB only, preserving more branch + /// resolution for visible color detail. For transparent, dark, and light colors it includes alpha as the + /// most significant routing bit so opacity changes can form their own branches. + /// + public static int GetColorIndex(Rgba32 color, int level) + { + // Sample one bit plane per level, starting at the most significant bit and moving downward. + int shift = 7 - level; + byte mask = (byte)(1 << shift); + + // Use BT.709 luminance as a cheap brightness estimate for deciding whether alpha carries + // useful information at this level for fully opaque colors. + int luminance = ColorNumerics.Get8BitBT709Luminance(color.R, color.G, color.B); + + // Scale the brightness thresholds with depth so deeper levels become stricter about when + // to spend a branch bit on alpha instead of RGB detail. + int darkThreshold = 128 >> level; + int lightThreshold = 255 - (128 >> level); + + if (color.A == 255 && luminance > darkThreshold && luminance < lightThreshold) + { + // Fully opaque mid-tone colors route on RGB only, which preserves more visible color + // resolution because alpha would contribute no extra separation here. + int rBits = ((color.R & mask) >> shift) << 2; + int gBits = ((color.G & mask) >> shift) << 1; + int bBits = (color.B & mask) >> shift; + return rBits | gBits | bBits; + } + else + { + // Transparent, dark, and light colors include alpha as the high routing bit so opacity + // changes can form distinct buckets alongside RGB differences. + int aBits = ((color.A & mask) >> shift) << 3; + int rBits = ((color.R & mask) >> shift) << 2; + int gBits = ((color.G & mask) >> shift) << 1; + int bBits = (color.B & mask) >> shift; + return aBits | rBits | gBits | bBits; + } + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs b/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs new file mode 100644 index 0000000..cf7a3ac --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs @@ -0,0 +1,397 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// Represents a cache used for efficiently retrieving palette indices for colors. + /// + internal interface IColorIndexCache : IDisposable + { + /// + /// Adds a color to the cache. + /// + /// The color to add. + /// The index of the color in the palette. + /// + /// if the color was added; otherwise, . + /// + public bool TryAdd(Rgba32 color, short value); + + /// + /// Gets the index of the color in the palette. + /// + /// The color to get the index for. + /// The index of the color in the palette. + /// + /// if the color is in the palette; otherwise, . + /// + public bool TryGetValue(Rgba32 color, out short value); + + /// + /// Clears the cache. + /// + public void Clear(); + } + + /// + /// Represents a cache used for efficiently retrieving palette indices for colors. + /// + /// The type of the cache. + internal interface IColorIndexCache : IColorIndexCache + where T : struct, IColorIndexCache + { + /// + /// Creates a new instance of the cache. + /// + /// The memory allocator to use. + /// + /// The new instance of the cache. + /// + public static abstract T Create(MemoryAllocator allocator); + } + + /// + /// + /// CoarseCache is a fast, low-memory lookup structure for caching palette indices associated with RGBA values, + /// using a quantized representation of 5,5,5,6 (RGB: 5 bits each, Alpha: 6 bits). + /// + /// + /// The cache quantizes the RGB channels to 5 bits each, resulting in 32 levels per channel and a total of 32³ = 32,768 buckets. + /// Each bucket is represented by an , which holds a small, inline array of alpha entries. + /// Each alpha entry stores the alpha value quantized to 6 bits (0–63) along with a palette index (a 16-bit value). + /// + /// + /// Performance Characteristics: + /// - Lookup: O(1) for computing the bucket index from the RGB channels, plus a small constant time (up to 8 iterations) + /// to search through the alpha entries in the bucket. + /// - Insertion: O(1) for bucket index computation and a quick linear search over a very small (fixed) number of entries. + /// + /// + /// Memory Characteristics: + /// - The cache consists of 32,768 buckets. + /// - Each is implemented using an inline array with a capacity of 8 entries. + /// - Each bucket occupies approximately 1 byte (Count) + (8 entries × 3 bytes each) ≈ 25 bytes. + /// - Overall, the buckets occupy roughly 32,768 × 25 bytes = 819,200 bytes (≈ 800 KB). + /// + /// + /// This design provides nearly constant-time lookup and insertion with minimal memory usage, + /// making it ideal for applications such as color distance caching in images with a limited palette (up to 256 entries). + /// + /// + internal unsafe struct CoarseCache : IColorIndexCache + { + // Use 5 bits per channel for R, G, and B: 32 levels each. + // Total buckets = 32^3 = 32768. + private const int RgbBits = 5; + private const int RgbShift = 8 - RgbBits; // 3 + private const int BucketCount = 1 << (RgbBits * 3); // 32768 + private readonly IMemoryOwner bucketsOwner; + private readonly AlphaBucket* buckets; + private MemoryHandle bucketHandle; + + private CoarseCache(MemoryAllocator allocator) + { + this.bucketsOwner = allocator.Allocate(BucketCount, AllocationOptions.Clean); + this.bucketHandle = this.bucketsOwner.Memory.Pin(); + this.buckets = (AlphaBucket*)this.bucketHandle.Pointer; + } + + /// + public static CoarseCache Create(MemoryAllocator allocator) => new(allocator); + + /// + public readonly bool TryAdd(Rgba32 color, short paletteIndex) + { + int bucketIndex = GetBucketIndex(color.R, color.G, color.B); + byte quantAlpha = QuantizeAlpha(color.A); + this.buckets[bucketIndex].Add(quantAlpha, paletteIndex); + return true; + } + + /// + public readonly bool TryGetValue(Rgba32 color, out short paletteIndex) + { + int bucketIndex = GetBucketIndex(color.R, color.G, color.B); + byte quantAlpha = QuantizeAlpha(color.A); + return this.buckets[bucketIndex].TryGetValue(quantAlpha, out paletteIndex); + } + + /// + public readonly void Clear() + { + Span bucketsSpan = this.bucketsOwner.GetSpan(); + bucketsSpan.Clear(); + } + + /// + public void Dispose() + { + this.bucketHandle.Dispose(); + this.bucketsOwner.Dispose(); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetBucketIndex(byte r, byte g, byte b) + { + int qr = r >> RgbShift; + int qg = g >> RgbShift; + int qb = b >> RgbShift; + + // Combine the quantized channels into a single index. + return (qr << (RgbBits << 1)) | (qg << RgbBits) | qb; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static byte QuantizeAlpha(byte a) => (byte)(a >> 2); + + public struct AlphaEntry + { + // Store the alpha value quantized to 6 bits (0..63). + public byte QuantizedAlpha; + public short PaletteIndex; + } + + public struct AlphaBucket + { + // Fixed capacity for alpha entries in this bucket. + // We choose a capacity of 8 for several reasons: + // + // 1. The alpha channel is quantized to 6 bits, so there are 64 possible distinct values. + // In the worst-case, a given RGB bucket might encounter up to 64 different alpha values. + // + // 2. However, in practice (based on probability theory and typical image data), + // the number of unique alpha values that actually occur for a given quantized RGB + // bucket is usually very small. If you randomly sample 8 values out of 64, + // the probability that these samples are all unique is high if the distribution + // of alpha values is skewed or if only a few alpha values are used. + // + // 3. Statistically, for many real-world images, most RGB buckets will have only a couple + // of unique alpha values. Allocating 8 slots per bucket provides a good trade-off: + // it captures the common-case scenario while keeping overall memory usage low. + // + // 4. Even if more than 8 unique alpha values occur in a bucket, + // our design overwrites the first entry. This behavior gives us some "wriggle room" + // while preserving the most frequently encountered or most recent values. + public const int Capacity = 8; + public byte Count; + private InlineArray8 entries; + + [MethodImpl(InliningOptions.ShortMethod)] + public bool TryGetValue(byte quantizedAlpha, out short paletteIndex) + { + for (int i = 0; i < this.Count; i++) + { + ref AlphaEntry entry = ref this.entries[i]; + if (entry.QuantizedAlpha == quantizedAlpha) + { + paletteIndex = entry.PaletteIndex; + return true; + } + } + + paletteIndex = -1; + return false; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Add(byte quantizedAlpha, short paletteIndex) + { + // Check for an existing entry with the same quantized alpha. + for (int i = 0; i < this.Count; i++) + { + ref AlphaEntry entry = ref this.entries[i]; + if (entry.QuantizedAlpha == quantizedAlpha) + { + // Update palette index if found. + entry.PaletteIndex = paletteIndex; + return; + } + } + + // If there's room, add a new entry. + if (this.Count < Capacity) + { + ref AlphaEntry newEntry = ref this.entries[this.Count]; + newEntry.QuantizedAlpha = quantizedAlpha; + newEntry.PaletteIndex = paletteIndex; + this.Count++; + } + else + { + // Bucket is full. Overwrite the first entry to give us some wriggle room. + this.entries[0].QuantizedAlpha = quantizedAlpha; + this.entries[0].PaletteIndex = paletteIndex; + } + } + } + } + + /// + /// A fixed-size exact-match cache that stores packed RGBA keys with 4-way set associativity. + /// + /// + /// The cache holds 512 total entries split across 128 sets. Entries are evicted within a set + /// using round-robin replacement, but cached values are returned only when the full packed RGBA + /// key matches, preserving exact quantization results with predictable memory usage. + /// The overall memory usage is approximately 4–5 KB. Both lookup and insertion operations are, + /// on average, O(1) since each lookup probes at most four candidate entries within the selected set. + /// This guarantees highly efficient and predictable performance for small, fixed-size color palettes. + /// + internal unsafe struct AccurateCache : IColorIndexCache + { + public const int Capacity = 512; + private const int Ways = 4; + private const int SetCount = Capacity / Ways; + private const int SetMask = SetCount - 1; + + private readonly IMemoryOwner keysOwner; + private MemoryHandle keysHandle; + private uint* keys; + + private readonly IMemoryOwner valuesOwner; + private MemoryHandle valuesHandle; + private ushort* values; + + private readonly IMemoryOwner nextVictimOwner; + private MemoryHandle nextVictimHandle; + private byte* nextVictim; + + private AccurateCache(MemoryAllocator allocator) + { + this.keysOwner = allocator.Allocate(Capacity, AllocationOptions.Clean); + this.keysHandle = this.keysOwner.Memory.Pin(); + this.keys = (uint*)this.keysHandle.Pointer; + + this.valuesOwner = allocator.Allocate(Capacity, AllocationOptions.Clean); + this.valuesHandle = this.valuesOwner.Memory.Pin(); + this.values = (ushort*)this.valuesHandle.Pointer; + + this.nextVictimOwner = allocator.Allocate(SetCount, AllocationOptions.Clean); + this.nextVictimHandle = this.nextVictimOwner.Memory.Pin(); + this.nextVictim = (byte*)this.nextVictimHandle.Pointer; + } + + /// + public static AccurateCache Create(MemoryAllocator allocator) => new(allocator); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool TryAdd(Rgba32 color, short value) + { + uint key = color.PackedValue; + int set = GetSetIndex(key); + int start = set * Ways; + int empty = -1; + + uint* keys = this.keys; + ushort* values = this.values; + ushort storedValue = (ushort)(value + 1); + + for (int i = start; i < start + Ways; i++) + { + ushort candidate = values[i]; + if (candidate == 0) + { + empty = i; + continue; + } + + if (keys[i] == key) + { + values[i] = storedValue; + return true; + } + } + + int slot = empty >= 0 ? empty : start + this.nextVictim[set]; + keys[slot] = key; + values[slot] = storedValue; + + if (empty < 0) + { + this.nextVictim[set] = (byte)((this.nextVictim[set] + 1) & (Ways - 1)); + } + + return true; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public readonly bool TryGetValue(Rgba32 color, out short value) + { + uint key = color.PackedValue; + int start = GetSetIndex(key) * Ways; + + uint* keys = this.keys; + ushort* values = this.values; + + for (int i = start; i < start + Ways; i++) + { + ushort candidate = values[i]; + if (candidate != 0 && keys[i] == key) + { + value = (short)(candidate - 1); + return true; + } + } + + value = -1; + return false; + } + + /// + /// Clears the cache. + /// + public readonly void Clear() + { + this.valuesOwner.GetSpan().Clear(); + this.nextVictimOwner.GetSpan().Clear(); + } + + public void Dispose() + { + this.keysHandle.Dispose(); + this.keysOwner.Dispose(); + this.valuesHandle.Dispose(); + this.valuesOwner.Dispose(); + this.nextVictimHandle.Dispose(); + this.nextVictimOwner.Dispose(); + this.keys = null; + this.values = null; + this.nextVictim = null; + } + + /// + /// Maps a packed RGBA key to one of the cache sets used by . + /// + /// The packed key. + /// The zero-based set index for the key. + /// + /// + /// The cache is 4-way set-associative, so this hash only needs to choose one of + /// sets before probing up to four candidate entries. + /// + /// + /// is laid out as R | (G << 8) | (B << 16) | (A << 24). + /// The XOR-fold mixes neighboring bytes into the low bits, and the final mask selects the + /// set. With the current 128-set layout that makes the selected set effectively depend on + /// the low 7 bits of R ^ G ^ B. Alpha still participates in the later exact key + /// comparison, but not in set selection. + /// + /// + /// Collisions are expected and acceptable here. Correctness comes from the full packed-key + /// comparison during probing; this hash only aims to spread keys cheaply enough that each + /// access touches at most one 4-entry set. + /// + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetSetIndex(uint key) + => (int)(((key >> 16) ^ (key >> 8) ^ key) & SetMask); + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/IPixelSamplingStrategy.cs b/ImageSharp/Processing/Processors/Quantization/IPixelSamplingStrategy.cs new file mode 100644 index 0000000..00f064a --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/IPixelSamplingStrategy.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// Provides an abstraction to enumerate pixel regions for sampling within . + /// + public interface IPixelSamplingStrategy + { + /// + /// Enumerates pixel regions for all frames within the image as . + /// + /// The image. + /// The pixel type. + /// An enumeration of pixel regions. + IEnumerable> EnumeratePixelRegions(Image image) + where TPixel : unmanaged, IPixel; + + /// + /// Enumerates pixel regions within a single image frame as . + /// + /// The image frame. + /// The pixel type. + /// An enumeration of pixel regions. + IEnumerable> EnumeratePixelRegions(ImageFrame frame) + where TPixel : unmanaged, IPixel; + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs b/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs new file mode 100644 index 0000000..8ea0c74 --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// Provides methods for allowing quantization of images pixels with configurable dithering. + /// + public interface IQuantizer + { + /// + /// Gets the quantizer options defining quantization rules. + /// + public QuantizerOptions Options { get; } + + /// + /// Creates the generic frame quantizer. + /// + /// The to configure internal operations. + /// The pixel format. + /// The . + public IQuantizer CreatePixelSpecificQuantizer(Configuration configuration) + where TPixel : unmanaged, IPixel; + + /// + /// Creates the generic frame quantizer. + /// + /// The pixel format. + /// The to configure internal operations. + /// The options to create the quantizer with. + /// The . + public IQuantizer CreatePixelSpecificQuantizer(Configuration configuration, QuantizerOptions options) + where TPixel : unmanaged, IPixel; + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/IQuantizer{TPixel}.cs b/ImageSharp/Processing/Processors/Quantization/IQuantizer{TPixel}.cs new file mode 100644 index 0000000..f2071b1 --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/IQuantizer{TPixel}.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// Provides methods to allow the execution of the quantization process on an image frame. + /// + /// The pixel format. + public interface IQuantizer : IDisposable + where TPixel : unmanaged, IPixel + { + /// + /// Gets the configuration. + /// + public Configuration Configuration { get; } + + /// + /// Gets the quantizer options defining quantization rules. + /// + public QuantizerOptions Options { get; } + + /// + /// Gets the quantized color palette. + /// + /// + /// The palette has not been built via . + /// + public ReadOnlyMemory Palette { get; } + + /// + /// Adds colors to the quantized palette from the given pixel source. + /// + /// The of source pixels to register. + public void AddPaletteColors(in Buffer2DRegion pixelRegion); + + /// + /// Quantizes an image frame and return the resulting output pixels. + /// + /// The source image frame to quantize. + /// The bounds within the frame to quantize. + /// + /// A representing a quantized version of the source frame pixels. + /// + /// + /// Only executes the second (quantization) step. The palette has to be built by calling . + /// To run both steps, use . + /// + public IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds); + + /// + /// Returns the index and color from the quantized palette corresponding to the given color. + /// + /// The color to match. + /// The matched color. + /// The index. + public byte GetQuantizedColor(TPixel color, out TPixel match); + + // TODO: Enable bulk operations. + // void GetQuantizedColors(ReadOnlySpan colors, ReadOnlySpan palette, Span indices, Span matches); + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/IQuantizingPixelRowDelegate{TPixel}.cs b/ImageSharp/Processing/Processors/Quantization/IQuantizingPixelRowDelegate{TPixel}.cs new file mode 100644 index 0000000..19b2301 --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/IQuantizingPixelRowDelegate{TPixel}.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// Defines a delegate for processing a row of pixels in an image for quantization. + /// + /// Represents a pixel type that can be processed in a quantizing operation. + internal interface IQuantizingPixelRowDelegate + where TPixel : unmanaged, IPixel + { + /// + /// Processes a row of pixels for quantization. + /// + /// The row of pixels to process. + /// The index of the row being processed. + public void Invoke(ReadOnlySpan row, int rowIndex); + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs new file mode 100644 index 0000000..8cb9460 --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// Allows the quantization of images pixels using color palettes. + /// + public class PaletteQuantizer : IQuantizer + { + private readonly ReadOnlyMemory colorPalette; + private readonly int transparencyIndex; + private readonly Color transparentColor; + + /// + /// Initializes a new instance of the class. + /// + /// The color palette. + public PaletteQuantizer(ReadOnlyMemory palette) + : this(palette, new QuantizerOptions()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The color palette to use. + /// The quantizer options defining quantization rules. + public PaletteQuantizer(ReadOnlyMemory palette, QuantizerOptions options) + : this(palette, options, -1, default) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The color palette to use. + /// The quantizer options defining quantization rules. + /// The index of the color in the palette that should be considered as transparent. + /// The color that should be considered as transparent. + internal PaletteQuantizer( + ReadOnlyMemory palette, + QuantizerOptions options, + int transparencyIndex, + Color transparentColor) + { + Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); + Guard.NotNull(options, nameof(options)); + + this.colorPalette = palette; + this.Options = options; + this.transparencyIndex = transparencyIndex; + this.transparentColor = transparentColor; + } + + /// + public QuantizerOptions Options { get; } + + /// + public IQuantizer CreatePixelSpecificQuantizer(Configuration configuration) + where TPixel : unmanaged, IPixel + => this.CreatePixelSpecificQuantizer(configuration, this.Options); + + /// + public IQuantizer CreatePixelSpecificQuantizer(Configuration configuration, QuantizerOptions options) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(options, nameof(options)); + + // If the palette is larger than the max colors then we need to trim it down. + // treat the buffer as FILO. + TPixel[] palette = new TPixel[Math.Min(options.MaxColors, this.colorPalette.Length)]; + Color.ToPixel(this.colorPalette.Span[..palette.Length], palette.AsSpan()); + return new PaletteQuantizer(configuration, options, palette, this.transparencyIndex, this.transparentColor.ToPixel()); + } + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs b/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs new file mode 100644 index 0000000..262e8e1 --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs @@ -0,0 +1,108 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// Encapsulates methods to create a quantized image based upon the given palette. + /// + /// + /// The pixel format. + [SuppressMessage( + "Design", + "CA1001:Types that own disposable fields should be disposable", + Justification = "https://github.com/dotnet/roslyn-analyzers/issues/6151")] + internal struct PaletteQuantizer : IQuantizer + where TPixel : unmanaged, IPixel + { + private readonly PixelMap pixelMap; + private int transparencyIndex; + private TPixel transparentColor; + + /// + /// Initializes a new instance of the struct. + /// + /// The configuration which allows altering default behavior or extending the library. + /// The quantizer options defining quantization rules. + /// The palette to use. + [MethodImpl(InliningOptions.ShortMethod)] + public PaletteQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory palette) + : this(configuration, options, palette, -1, default) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(options, nameof(options)); + } + + /// + /// Initializes a new instance of the struct. + /// + /// The configuration which allows altering default behavior or extending the library. + /// The quantizer options defining quantization rules. + /// The palette to use. + /// The index of the color in the palette that should be considered as transparent. + /// The color that should be considered as transparent. + public PaletteQuantizer( + Configuration configuration, + QuantizerOptions options, + ReadOnlyMemory palette, + int transparencyIndex, + TPixel transparentColor) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(options, nameof(options)); + + this.Configuration = configuration; + this.Options = options; + this.pixelMap = PixelMapFactory.Create(this.Configuration, palette, options.ColorMatchingMode); + this.transparencyIndex = transparencyIndex; + this.transparentColor = transparentColor; + } + + /// + public Configuration Configuration { get; } + + /// + public QuantizerOptions Options { get; } + + /// + public readonly ReadOnlyMemory Palette => this.pixelMap.Palette; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public readonly void AddPaletteColors(in Buffer2DRegion pixelRegion) + { + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public readonly IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + => QuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(in this), source, bounds); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public readonly byte GetQuantizedColor(TPixel color, out TPixel match) + { + if (this.transparencyIndex >= 0 && color.Equals(this.transparentColor)) + { + match = this.transparentColor; + return (byte)this.transparencyIndex; + } + + return (byte)this.pixelMap.GetClosestColor(color, out match); + } + + public void SetTransparencyIndex(int transparencyIndex, TPixel transparentColor) + { + this.transparencyIndex = transparencyIndex; + this.transparentColor = transparentColor; + } + + /// + public readonly void Dispose() => this.pixelMap.Dispose(); + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor.cs b/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor.cs new file mode 100644 index 0000000..2108656 --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// Defines quantization processing for images to reduce the number of colors used in the image palette. + /// + public class QuantizeProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The quantizer used to reduce the color palette. + public QuantizeProcessor(IQuantizer quantizer) + => this.Quantizer = quantizer; + + /// + /// Gets the quantizer. + /// + public IQuantizer Quantizer { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new QuantizeProcessor(configuration, this.Quantizer, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs new file mode 100644 index 0000000..237e722 --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// Enables the quantization of images to reduce the number of colors used in the image palette. + /// + /// The pixel format. + internal class QuantizeProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly IQuantizer quantizer; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The quantizer used to reduce the color palette. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public QuantizeProcessor(Configuration configuration, IQuantizer quantizer, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + Guard.NotNull(quantizer, nameof(quantizer)); + this.quantizer = quantizer; + } + + /// + protected override void OnFrameApply(ImageFrame source) + { + Rectangle interest = Rectangle.Intersect(source.Bounds, this.SourceRectangle); + + Configuration configuration = this.Configuration; + using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration); + using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(source, interest); + + ReadOnlySpan paletteSpan = quantized.Palette.Span; + int offsetY = interest.Top; + int offsetX = interest.Left; + Buffer2D sourceBuffer = source.PixelBuffer; + + for (int y = 0; y < quantized.Height; y++) + { + ReadOnlySpan quantizedRow = quantized.DangerousGetRowSpan(y); + Span row = sourceBuffer.DangerousGetRowSpan(y + offsetY); + + for (int x = 0; x < quantized.Width; x++) + { + row[x + offsetX] = paletteSpan[quantizedRow[x]]; + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/QuantizerConstants.cs b/ImageSharp/Processing/Processors/Quantization/QuantizerConstants.cs new file mode 100644 index 0000000..38b9ef9 --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/QuantizerConstants.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Dithering; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// Contains color quantization specific constants. + /// + public static class QuantizerConstants + { + /// + /// The minimum number of colors to use when quantizing an image. + /// + public const int MinColors = 1; + + /// + /// The maximum number of colors to use when quantizing an image. + /// + public const int MaxColors = 256; + + /// + /// The minimum dithering scale used to adjust the amount of dither. + /// + public const float MinDitherScale = 0; + + /// + /// The maximum dithering scale used to adjust the amount of dither. + /// + public const float MaxDitherScale = 1F; + + /// + /// The default threshold at which to consider a pixel transparent. + /// + public const float DefaultTransparencyThreshold = 64 / 255F; + + /// + /// The minimum threshold at which to consider a pixel transparent. + /// + public const float MinTransparencyThreshold = 0F; + + /// + /// The maximum threshold at which to consider a pixel transparent. + /// + public const float MaxTransparencyThreshold = 1F; + + /// + /// Gets the default dithering algorithm to use. + /// + public static IDither DefaultDither { get; } = KnownDitherings.FloydSteinberg; + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs b/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs new file mode 100644 index 0000000..9dca97a --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs @@ -0,0 +1,92 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Processing.Processors.Dithering; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// Defines options for quantization. + /// + public class QuantizerOptions : IDeepCloneable + { +#pragma warning disable IDE0032 // Use auto property + private float ditherScale = QuantizerConstants.MaxDitherScale; + private int maxColors = QuantizerConstants.MaxColors; + private float threshold = QuantizerConstants.DefaultTransparencyThreshold; +#pragma warning restore IDE0032 // Use auto property + + /// + /// Initializes a new instance of the class. + /// + public QuantizerOptions() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The options to clone. + private QuantizerOptions(QuantizerOptions options) + { + this.Dither = options.Dither; + this.DitherScale = options.DitherScale; + this.MaxColors = options.MaxColors; + this.TransparencyThreshold = options.TransparencyThreshold; + this.ColorMatchingMode = options.ColorMatchingMode; + this.TransparentColorMode = options.TransparentColorMode; + } + + /// + /// Gets or sets the algorithm to apply to the output image. + /// Defaults to ; set to for no dithering. + /// + public IDither? Dither { get; set; } = QuantizerConstants.DefaultDither; + + /// + /// Gets or sets the dithering scale used to adjust the amount of dither. Range 0..1. + /// Defaults to . + /// + public float DitherScale + { + get => this.ditherScale; + set => this.ditherScale = Numerics.Clamp(value, QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale); + } + + /// + /// Gets or sets the maximum number of colors to hold in the color palette. Range 0..256. + /// Defaults to . + /// + public int MaxColors + { + get => this.maxColors; + set => this.maxColors = Numerics.Clamp(value, QuantizerConstants.MinColors, QuantizerConstants.MaxColors); + } + + /// + /// Gets or sets the color matching mode used for matching pixel values to palette colors. + /// Defaults to . + /// + public ColorMatchingMode ColorMatchingMode { get; set; } = ColorMatchingMode.Coarse; + + /// + /// Gets or sets the threshold at which to consider a pixel transparent. Range 0..1. + /// Defaults to . + /// + public float TransparencyThreshold + { + get => this.threshold; + set => this.threshold = Numerics.Clamp(value, QuantizerConstants.MinTransparencyThreshold, QuantizerConstants.MaxTransparencyThreshold); + } + + /// + /// Gets or sets the transparent color mode used for handling transparent colors + /// when not using thresholding. + /// Defaults to . + /// + public TransparentColorMode TransparentColorMode { get; set; } = TransparentColorMode.Preserve; + + /// + public QuantizerOptions DeepClone() => new(this); + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs b/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs new file mode 100644 index 0000000..84e9166 --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs @@ -0,0 +1,447 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Dithering; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// Contains utility methods for instances. + /// + public static class QuantizerUtilities + { + /// + /// Performs a deep clone the instance and optionally mutates the clone. + /// + /// The instance to clone. + /// An optional delegate to mutate the cloned instance. + /// The cloned instance. + public static QuantizerOptions DeepClone(this QuantizerOptions options, Action? mutate) + { + QuantizerOptions clone = options.DeepClone(); + mutate?.Invoke(clone); + return clone; + } + + /// + /// Determines if transparent pixels can be replaced based on the specified color mode and pixel type. + /// + /// The type of the pixel. + /// The alpha threshold used to determine if a pixel is transparent. + /// Returns true if transparent pixels can be replaced; otherwise, false. + public static bool ShouldReplacePixelsByAlphaThreshold(float threshold) + where TPixel : unmanaged, IPixel + => threshold > 0 && TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Unassociated; + + /// + /// Replaces pixels in a span with fully transparent pixels based on an alpha threshold. + /// + /// A span of color vectors that will be checked for transparency and potentially modified. + /// The alpha threshold used to determine if a pixel is transparent. + public static void ReplacePixelsByAlphaThreshold(Span source, float threshold) + { + if (Vector512.IsHardwareAccelerated && source.Length >= 4) + { + Vector512 threshold512 = Vector512.Create(threshold); + Span> source512 = MemoryMarshal.Cast>(source); + for (int i = 0; i < source512.Length; i++) + { + ref Vector512 v = ref source512[i]; + + // Do `vector < threshold` + Vector512 mask = Vector512.LessThan(v, threshold512); + + // Replicate the result for W to all elements (is AllBitsSet if the W was less than threshold and Zero otherwise) + mask = Vector512.Shuffle(mask, Vector512.Create(3, 3, 3, 3, 7, 7, 7, 7, 11, 11, 11, 11, 15, 15, 15, 15)); + + // Use the mask to select the replacement vector + // (replacement & mask) | (v512 & ~mask) + v = Vector512.ConditionalSelect(mask, Vector512.Zero, v); + } + + int m = Numerics.Modulo4(source.Length); + if (m != 0) + { + for (int i = source.Length - m; i < source.Length; i++) + { + if (source[i].W < threshold) + { + source[i] = Vector4.Zero; + } + } + } + } + else if (Vector256.IsHardwareAccelerated && source.Length >= 2) + { + Vector256 threshold256 = Vector256.Create(threshold); + Span> source256 = MemoryMarshal.Cast>(source); + for (int i = 0; i < source256.Length; i++) + { + ref Vector256 v = ref source256[i]; + + // Do `vector < threshold` + Vector256 mask = Vector256.LessThan(v, threshold256); + + // Replicate the result for W to all elements (is AllBitsSet if the W was less than threshold and Zero otherwise) + mask = Vector256.Shuffle(mask, Vector256.Create(3, 3, 3, 3, 7, 7, 7, 7)); + + // Use the mask to select the replacement vector + // (replacement & mask) | (v256 & ~mask) + v = Vector256.ConditionalSelect(mask, Vector256.Zero, v); + } + + int m = Numerics.Modulo2(source.Length); + if (m != 0) + { + for (int i = source.Length - m; i < source.Length; i++) + { + if (source[i].W < threshold) + { + source[i] = Vector4.Zero; + } + } + } + } + else if (Vector128.IsHardwareAccelerated) + { + Vector128 threshold128 = Vector128.Create(threshold); + + for (int i = 0; i < source.Length; i++) + { + ref Vector4 v = ref source[i]; + Vector128 v128 = v.AsVector128(); + + // Do `vector < threshold` + Vector128 mask = Vector128.LessThan(v128, threshold128); + + // Replicate the result for W to all elements (is AllBitsSet if the W was less than threshold and Zero otherwise) + mask = Vector128.Shuffle(mask, Vector128.Create(3, 3, 3, 3)); + + // Use the mask to select the replacement vector + // (replacement & mask) | (v128 & ~mask) + v = Vector128.ConditionalSelect(mask, Vector128.Zero, v128).AsVector4(); + } + } + else + { + for (int i = 0; i < source.Length; i++) + { + if (source[i].W < threshold) + { + source[i] = Vector4.Zero; + } + } + } + } + + /// + /// Helper method for throwing an exception when a frame quantizer palette has + /// been requested but not built yet. + /// + /// The pixel format. + /// The frame quantizer palette. + /// + /// The palette has not been built via + /// + [MethodImpl(InliningOptions.ColdPath)] + public static void CheckPaletteState(in ReadOnlyMemory palette) + where TPixel : unmanaged, IPixel + { + if (palette.IsEmpty) + { + throw new InvalidOperationException("Frame Quantizer palette has not been built."); + } + } + + /// + /// Execute both steps of the quantization. + /// + /// The pixel specific quantizer. + /// The source image frame to quantize. + /// The bounds within the frame to quantize. + /// The pixel type. + /// + /// A representing a quantized version of the source frame pixels. + /// + public static IndexedImageFrame BuildPaletteAndQuantizeFrame( + this IQuantizer quantizer, + ImageFrame source, + Rectangle bounds) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(quantizer, nameof(quantizer)); + Guard.NotNull(source, nameof(source)); + + Rectangle interest = Rectangle.Intersect(source.Bounds, bounds); + Buffer2DRegion region = source.PixelBuffer.GetRegion(interest); + + quantizer.AddPaletteColors(in region); + return quantizer.QuantizeFrame(source, bounds); + } + + /// + /// Quantizes an image frame and return the resulting output pixels. + /// + /// The type of frame quantizer. + /// The pixel format. + /// The pixel specific quantizer. + /// The source image frame to quantize. + /// The bounds within the frame to quantize. + /// + /// A representing a quantized version of the source frame pixels. + /// + public static IndexedImageFrame QuantizeFrame( + ref TFrameQuantizer quantizer, + ImageFrame source, + Rectangle bounds) + where TFrameQuantizer : struct, IQuantizer + where TPixel : unmanaged, IPixel + { + Guard.NotNull(source, nameof(source)); + Rectangle interest = Rectangle.Intersect(source.Bounds, bounds); + + IndexedImageFrame destination = new( + quantizer.Configuration, + interest.Width, + interest.Height, + quantizer.Palette); + + if (quantizer.Options.Dither is null) + { + SecondPass(ref quantizer, source, destination, interest); + } + else + { + // We clone the image as we don't want to alter the original via error diffusion based dithering. + using ImageFrame clone = source.Clone(); + SecondPass(ref quantizer, clone, destination, interest); + } + + return destination; + } + + /// + /// Adds colors to the quantized palette from the given pixel regions. + /// + /// The pixel format. + /// The pixel specific quantizer. + /// The pixel sampling strategy. + /// The source image to sample from. + public static void BuildPalette( + this IQuantizer quantizer, + IPixelSamplingStrategy pixelSamplingStrategy, + Image source) + where TPixel : unmanaged, IPixel + { + foreach (Buffer2DRegion region in pixelSamplingStrategy.EnumeratePixelRegions(source)) + { + quantizer.AddPaletteColors(in region); + } + } + + /// + /// Adds colors to the quantized palette from the given pixel regions. + /// + /// The pixel format. + /// The pixel specific quantizer. + /// The pixel sampling strategy. + /// The source image frame to sample from. + public static void BuildPalette( + this IQuantizer quantizer, + IPixelSamplingStrategy pixelSamplingStrategy, + ImageFrame source) + where TPixel : unmanaged, IPixel + { + foreach (Buffer2DRegion region in pixelSamplingStrategy.EnumeratePixelRegions(source)) + { + quantizer.AddPaletteColors(in region); + } + } + + internal static void AddPaletteColors( + ref TFrameQuantizer quantizer, + in Buffer2DRegion source, + in TDelegate rowDelegate) + where TFrameQuantizer : struct, IQuantizer + where TPixel : unmanaged, IPixel + where TPixel2 : unmanaged, IPixel + where TDelegate : struct, IQuantizingPixelRowDelegate + { + Configuration configuration = quantizer.Configuration; + float threshold = quantizer.Options.TransparencyThreshold; + TransparentColorMode mode = quantizer.Options.TransparentColorMode; + + using IMemoryOwner delegateRowOwner = configuration.MemoryAllocator.Allocate(source.Width); + Span delegateRow = delegateRowOwner.Memory.Span; + + bool replaceByThreshold = ShouldReplacePixelsByAlphaThreshold(threshold); + bool replaceTransparent = EncodingUtilities.ShouldReplaceTransparentPixels(mode); + + if (replaceByThreshold || replaceTransparent) + { + using IMemoryOwner vectorRowOwner = configuration.MemoryAllocator.Allocate(source.Width); + Span vectorRow = vectorRowOwner.Memory.Span; + + if (replaceByThreshold) + { + for (int y = 0; y < source.Height; y++) + { + Span sourceRow = source.DangerousGetRowSpan(y); + PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale); + + ReplacePixelsByAlphaThreshold(vectorRow, threshold); + + PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, delegateRow, PixelConversionModifiers.Scale); + rowDelegate.Invoke(delegateRow, y); + } + } + else + { + for (int y = 0; y < source.Height; y++) + { + Span sourceRow = source.DangerousGetRowSpan(y); + PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale); + + EncodingUtilities.ReplaceTransparentPixels(vectorRow); + + PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, delegateRow, PixelConversionModifiers.Scale); + rowDelegate.Invoke(delegateRow, y); + } + } + } + else + { + for (int y = 0; y < source.Height; y++) + { + Span sourceRow = source.DangerousGetRowSpan(y); + PixelOperations.Instance.To(configuration, sourceRow, delegateRow); + rowDelegate.Invoke(delegateRow, y); + } + } + } + + private static void SecondPass( + ref TFrameQuantizer quantizer, + ImageFrame source, + IndexedImageFrame destination, + Rectangle bounds) + where TFrameQuantizer : struct, IQuantizer + where TPixel : unmanaged, IPixel + { + float threshold = quantizer.Options.TransparencyThreshold; + bool replaceByThreshold = ShouldReplacePixelsByAlphaThreshold(threshold); + + TransparentColorMode mode = quantizer.Options.TransparentColorMode; + bool replaceTransparent = EncodingUtilities.ShouldReplaceTransparentPixels(mode); + + IDither? dither = quantizer.Options.Dither; + Buffer2D sourceBuffer = source.PixelBuffer; + Buffer2DRegion region = sourceBuffer.GetRegion(bounds); + + Configuration configuration = quantizer.Configuration; + using IMemoryOwner vectorOwner = configuration.MemoryAllocator.Allocate(region.Width); + Span vectorRow = vectorOwner.Memory.Span; + + if (dither is null) + { + using IMemoryOwner quantizingRowOwner = configuration.MemoryAllocator.Allocate(region.Width); + Span quantizingRow = quantizingRowOwner.Memory.Span; + + // This is NOT a clone so we DO NOT write back to the source. + if (replaceByThreshold || replaceTransparent) + { + if (replaceByThreshold) + { + for (int y = 0; y < region.Height; y++) + { + Span sourceRow = region.DangerousGetRowSpan(y); + PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale); + + ReplacePixelsByAlphaThreshold(vectorRow, threshold); + + PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, quantizingRow, PixelConversionModifiers.Scale); + + Span destinationRow = destination.GetWritablePixelRowSpanUnsafe(y); + for (int x = 0; x < destinationRow.Length; x++) + { + destinationRow[x] = quantizer.GetQuantizedColor(quantizingRow[x], out TPixel _); + } + } + } + else + { + for (int y = 0; y < region.Height; y++) + { + Span sourceRow = region.DangerousGetRowSpan(y); + PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale); + + EncodingUtilities.ReplaceTransparentPixels(vectorRow); + + PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, quantizingRow, PixelConversionModifiers.Scale); + + Span destinationRow = destination.GetWritablePixelRowSpanUnsafe(y); + for (int x = 0; x < destinationRow.Length; x++) + { + destinationRow[x] = quantizer.GetQuantizedColor(quantizingRow[x], out TPixel _); + } + } + } + + return; + } + + for (int y = 0; y < region.Height; y++) + { + ReadOnlySpan sourceRow = region.DangerousGetRowSpan(y); + Span destinationRow = destination.GetWritablePixelRowSpanUnsafe(y); + + for (int x = 0; x < destinationRow.Length; x++) + { + destinationRow[x] = quantizer.GetQuantizedColor(sourceRow[x], out TPixel _); + } + } + + return; + } + + // This is a clone so we write back to the source. + if (replaceByThreshold || replaceTransparent) + { + if (replaceByThreshold) + { + for (int y = 0; y < region.Height; y++) + { + Span sourceRow = region.DangerousGetRowSpan(y); + PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale); + + ReplacePixelsByAlphaThreshold(vectorRow, threshold); + + PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, sourceRow, PixelConversionModifiers.Scale); + } + } + else + { + for (int y = 0; y < region.Height; y++) + { + Span sourceRow = region.DangerousGetRowSpan(y); + PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale); + + EncodingUtilities.ReplaceTransparentPixels(vectorRow); + + PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, sourceRow, PixelConversionModifiers.Scale); + } + } + } + + dither.ApplyQuantizationDither(ref quantizer, source, destination, bounds); + } + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs b/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs new file mode 100644 index 0000000..a793fb7 --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// A palette quantizer consisting of web safe colors as defined in the CSS Color Module Level 4. + /// + public sealed class WebSafePaletteQuantizer : PaletteQuantizer + { + /// + /// Initializes a new instance of the class. + /// + public WebSafePaletteQuantizer() + : this(new QuantizerOptions()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The quantizer options defining quantization rules. + public WebSafePaletteQuantizer(QuantizerOptions options) + : base(Color.WebSafePalette, options) + { + } + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs b/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs new file mode 100644 index 0000000..e780c02 --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// A palette quantizer consisting of colors as defined in the original second edition of Werner’s Nomenclature of Colours 1821. + /// The hex codes were collected and defined by Nicholas Rougeux + /// + public sealed class WernerPaletteQuantizer : PaletteQuantizer + { + /// + /// Initializes a new instance of the class. + /// + public WernerPaletteQuantizer() + : this(new QuantizerOptions()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The quantizer options defining quantization rules. + public WernerPaletteQuantizer(QuantizerOptions options) + : base(Color.WernerPalette, options) + { + } + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs b/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs new file mode 100644 index 0000000..08a666c --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// Allows the quantization of images pixels using Xiaolin Wu's Color Quantizer + /// + public class WuQuantizer : IQuantizer + { + /// + /// Initializes a new instance of the class + /// using the default . + /// + public WuQuantizer() + : this(new QuantizerOptions()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The quantizer options defining quantization rules. + public WuQuantizer(QuantizerOptions options) + { + Guard.NotNull(options, nameof(options)); + this.Options = options; + } + + /// + public QuantizerOptions Options { get; } + + /// + public IQuantizer CreatePixelSpecificQuantizer(Configuration configuration) + where TPixel : unmanaged, IPixel + => this.CreatePixelSpecificQuantizer(configuration, this.Options); + + /// + public IQuantizer CreatePixelSpecificQuantizer(Configuration configuration, QuantizerOptions options) + where TPixel : unmanaged, IPixel + => new WuQuantizer(configuration, options); + } +} diff --git a/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs b/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs new file mode 100644 index 0000000..b66b05a --- /dev/null +++ b/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs @@ -0,0 +1,893 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization { + /// + /// An implementation of Wu's color quantizer with alpha channel. + /// + /// + /// + /// Based on C Implementation of Xiaolin Wu's Color Quantizer (v. 2) + /// (see Graphics Gems volume II, pages 126-133) + /// (). + /// + /// + /// This adaptation is based on the excellent JeremyAnsel.ColorQuant by Jérémy Ansel + /// + /// + /// + /// Algorithm: Greedy orthogonal bipartition of RGB space for variance minimization aided by inclusion-exclusion tricks. + /// For speed no nearest neighbor search is done. Slightly better performance can be expected by more sophisticated + /// but more expensive versions. + /// + /// + /// The pixel format. + [SuppressMessage( + "Design", + "CA1001:Types that own disposable fields should be disposable", + Justification = "https://github.com/dotnet/roslyn-analyzers/issues/6151")] + internal struct WuQuantizer : IQuantizer + where TPixel : unmanaged, IPixel + { + private readonly MemoryAllocator memoryAllocator; + + // The following two variables determine the amount of bits to preserve when calculating the histogram. + // Reducing the value of these numbers the granularity of the color maps produced, making it much faster + // and using much less memory but potentially less accurate. Current results are very good though! + private const int IndexBits = 5; + private const int IndexAlphaBits = 5; + private const int IndexCount = (1 << IndexBits) + 1; + private const int IndexAlphaCount = (1 << IndexAlphaBits) + 1; + private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount; + + private readonly IMemoryOwner momentsOwner; + private readonly IMemoryOwner tagsOwner; + private readonly IMemoryOwner paletteOwner; + private ReadOnlyMemory palette; + private int maxColors; + private readonly Box[] colorCube; + private PixelMap? pixelMap; + private readonly bool isDithering; + private bool isDisposed; + + /// + /// Initializes a new instance of the struct. + /// + /// The configuration which allows altering default behavior or extending the library. + /// The quantizer options defining quantization rules. + [MethodImpl(InliningOptions.ShortMethod)] + public WuQuantizer(Configuration configuration, QuantizerOptions options) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(options, nameof(options)); + + this.Configuration = configuration; + this.Options = options; + this.maxColors = this.Options.MaxColors; + this.memoryAllocator = this.Configuration.MemoryAllocator; + this.momentsOwner = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); + this.tagsOwner = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); + this.paletteOwner = this.memoryAllocator.Allocate(this.maxColors, AllocationOptions.Clean); + this.colorCube = new Box[this.maxColors]; + this.isDisposed = false; + this.pixelMap = default; + this.palette = default; + this.isDithering = this.Options.Dither is not null; + } + + /// + public Configuration Configuration { get; } + + /// + public QuantizerOptions Options { get; } + + /// + public ReadOnlyMemory Palette + { + get + { + if (this.palette.IsEmpty) + { + this.ResolvePalette(); + QuantizerUtilities.CheckPaletteState(in this.palette); + } + + return this.palette; + } + } + + /// + public readonly void AddPaletteColors(in Buffer2DRegion pixelRegion) + { + PixelRowDelegate pixelRowDelegate = new(ref Unsafe.AsRef(in this)); + QuantizerUtilities.AddPaletteColors, TPixel, Rgba32, PixelRowDelegate>( + ref Unsafe.AsRef(in this), + in pixelRegion, + in pixelRowDelegate); + } + + /// + /// Once all histogram data has been accumulated, this method computes the moments, + /// splits the color cube, and resolves the final palette from the accumulated histogram. + /// + private void ResolvePalette() + { + // Calculate the cumulative moments from the accumulated histogram. + this.Get3DMoments(this.memoryAllocator); + + // Partition the histogram into color cubes. + this.BuildCube(); + + // Compute the palette colors from the resolved cubes. + Span paletteSpan = this.paletteOwner.GetSpan()[..this.maxColors]; + ReadOnlySpan momentsSpan = this.momentsOwner.GetSpan(); + + float transparencyThreshold = this.Options.TransparencyThreshold; + for (int k = 0; k < paletteSpan.Length; k++) + { + this.Mark(ref this.colorCube[k], (byte)k); + Moment moment = Volume(ref this.colorCube[k], momentsSpan); + if (moment.Weight > 0) + { + Vector4 normalized = moment.Normalize(); + if (normalized.W < transparencyThreshold) + { + normalized = Vector4.Zero; + } + + paletteSpan[k] = TPixel.FromScaledVector4(normalized); + } + } + + // Update the palette to the new computed colors. + this.palette = this.paletteOwner.Memory[..paletteSpan.Length]; + + // Create the pixel map if dithering is enabled. + if (this.isDithering && this.pixelMap is null) + { + this.pixelMap = PixelMapFactory.Create(this.Configuration, this.palette, this.Options.ColorMatchingMode); + } + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public readonly IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + => QuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(in this), source, bounds); + + /// + public readonly byte GetQuantizedColor(TPixel color, out TPixel match) + { + // Due to the addition of new colors by dithering that are not part of the original histogram, + // the color cube might not match the correct color. + // In this case, we must use the pixel map to get the closest color. + if (this.isDithering) + { + return (byte)this.pixelMap!.GetClosestColor(color, out match); + } + + Rgba32 rgba = color.ToRgba32(); + + const int shift = 8 - IndexBits; + int r = rgba.R >> shift; + int g = rgba.G >> shift; + int b = rgba.B >> shift; + int a = rgba.A >> (8 - IndexAlphaBits); + + ReadOnlySpan tagSpan = this.tagsOwner.GetSpan(); + byte index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)]; + ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.palette.Span); + match = Unsafe.Add(ref paletteRef, (nuint)index); + return index; + } + + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.isDisposed = true; + this.momentsOwner?.Dispose(); + this.tagsOwner?.Dispose(); + this.paletteOwner?.Dispose(); + this.pixelMap?.Dispose(); + this.pixelMap = null; + } + } + + /// + /// Gets the index of the given color in the palette. + /// + /// The red value. + /// The green value. + /// The blue value. + /// The alpha value. + /// The index. + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetPaletteIndex(int r, int g, int b, int a) + => (r << ((IndexBits * 2) + IndexAlphaBits)) + + (r << (IndexBits + IndexAlphaBits + 1)) + + (g << (IndexBits + IndexAlphaBits)) + + (r << (IndexBits * 2)) + + (r << (IndexBits + 1)) + + (g << IndexBits) + + ((r + g + b) << IndexAlphaBits) + + r + g + b + a; + + /// + /// Computes sum over a box of any given statistic. + /// + /// The cube. + /// The moment. + /// The result. + private static Moment Volume(ref Box cube, ReadOnlySpan moments) + => moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMax)] + - moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] + - moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMax)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; + + /// + /// Computes part of Volume(cube, moment) that doesn't depend on RMax, GMax, BMax, or AMax (depending on direction). + /// + /// The cube. + /// The direction. + /// The moment. + /// The result. + /// Invalid direction. + private static Moment Bottom(ref Box cube, int direction, ReadOnlySpan moments) + => direction switch + { + // Red + 3 => -moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)], + + // Green + 2 => -moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMax)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)], + + // Blue + 1 => -moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)], + + // Alpha + 0 => -moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)], + _ => throw new ArgumentOutOfRangeException(nameof(direction)), + }; + + /// + /// Computes remainder of Volume(cube, moment), substituting position for RMax, GMax, BMax, or AMax (depending on direction). + /// + /// The cube. + /// The direction. + /// The position. + /// The moment. + /// The result. + /// Invalid direction. + private static Moment Top(ref Box cube, int direction, int position, ReadOnlySpan moments) + => direction switch + { + // Red + 3 => moments[GetPaletteIndex(position, cube.GMax, cube.BMax, cube.AMax)] + - moments[GetPaletteIndex(position, cube.GMax, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(position, cube.GMax, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(position, cube.GMax, cube.BMin, cube.AMin)] + - moments[GetPaletteIndex(position, cube.GMin, cube.BMax, cube.AMax)] + + moments[GetPaletteIndex(position, cube.GMin, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(position, cube.GMin, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(position, cube.GMin, cube.BMin, cube.AMin)], + + // Green + 2 => moments[GetPaletteIndex(cube.RMax, position, cube.BMax, cube.AMax)] + - moments[GetPaletteIndex(cube.RMax, position, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(cube.RMax, position, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(cube.RMax, position, cube.BMin, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, position, cube.BMax, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, position, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, position, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, position, cube.BMin, cube.AMin)], + + // Blue + 1 => moments[GetPaletteIndex(cube.RMax, cube.GMax, position, cube.AMax)] + - moments[GetPaletteIndex(cube.RMax, cube.GMax, position, cube.AMin)] + - moments[GetPaletteIndex(cube.RMax, cube.GMin, position, cube.AMax)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, position, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMax, position, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, position, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, position, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, position, cube.AMin)], + + // Alpha + 0 => moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, position)] + - moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, position)] + - moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, position)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, position)] + - moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, position)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, position)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, position)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, position)], + _ => throw new ArgumentOutOfRangeException(nameof(direction)), + }; + + /// + /// Builds a 3-D color histogram of counts, r/g/b, c^2. + /// + /// The source pixel data. + private readonly void Build3DHistogram(ReadOnlySpan pixels) + { + Span moments = this.momentsOwner.GetSpan(); + for (int x = 0; x < pixels.Length; x++) + { + Rgba32 rgba = pixels[x]; + int r = (rgba.R >> (8 - IndexBits)) + 1; + int g = (rgba.G >> (8 - IndexBits)) + 1; + int b = (rgba.B >> (8 - IndexBits)) + 1; + int a = (rgba.A >> (8 - IndexAlphaBits)) + 1; + + moments[GetPaletteIndex(r, g, b, a)] += rgba; + } + } + + /// + /// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box. + /// + /// The memory allocator used for allocating buffers. + private readonly void Get3DMoments(MemoryAllocator allocator) + { + using IMemoryOwner volume = allocator.Allocate(IndexCount * IndexAlphaCount); + using IMemoryOwner area = allocator.Allocate(IndexAlphaCount); + + Span momentSpan = this.momentsOwner.GetSpan(); + Span volumeSpan = volume.GetSpan(); + Span areaSpan = area.GetSpan(); + const int indexBits2 = IndexBits * 2; + const int indexAndAlphaBits = IndexBits + IndexAlphaBits; + const int indexBitsAndAlphaBits1 = IndexBits + IndexAlphaBits + 1; + int baseIndex = GetPaletteIndex(1, 0, 0, 0); + + for (int r = 1; r < IndexCount; r++) + { + // Currently, RyuJIT hoists the invariants of multi-level nested loop only to the + // immediate outer loop. See https://github.com/dotnet/runtime/issues/61420 + // To ensure the calculation doesn't happen repeatedly, hoist some of the calculations + // in the form of ind1* manually. + int ind1R = (r << (indexBits2 + IndexAlphaBits)) + + (r << indexBitsAndAlphaBits1) + + (r << indexBits2) + + (r << (IndexBits + 1)) + + r; + + volumeSpan.Clear(); + + for (int g = 1; g < IndexCount; g++) + { + int ind1G = ind1R + + (g << indexAndAlphaBits) + + (g << IndexBits) + + g; + int r_g = r + g; + + areaSpan.Clear(); + + for (int b = 1; b < IndexCount; b++) + { + int ind1B = ind1G + + ((r_g + b) << IndexAlphaBits) + + b; + + Moment line = default; + int bIndexAlphaOffset = b * IndexAlphaCount; + for (int a = 1; a < IndexAlphaCount; a++) + { + int ind1 = ind1B + a; + + line += momentSpan[ind1]; + + areaSpan[a] += line; + + int inv = bIndexAlphaOffset + a; + volumeSpan[inv] += areaSpan[a]; + + int ind2 = ind1 - baseIndex; + momentSpan[ind1] = momentSpan[ind2] + volumeSpan[inv]; + } + } + } + } + } + + /// + /// Computes the weighted variance of a box cube. + /// + /// The cube. + /// The . + private readonly double Variance(ref Box cube) + { + ReadOnlySpan momentSpan = this.momentsOwner.GetSpan(); + + Moment volume = Volume(ref cube, momentSpan); + Moment variance = + momentSpan[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMax)] + - momentSpan[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMin)] + - momentSpan[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMax)] + + momentSpan[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] + - momentSpan[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMax)] + + momentSpan[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] + + momentSpan[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] + - momentSpan[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] + - momentSpan[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMax)] + + momentSpan[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] + + momentSpan[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] + - momentSpan[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] + + momentSpan[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] + - momentSpan[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] + - momentSpan[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] + + momentSpan[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; + + Vector4 vector = new(volume.R, volume.G, volume.B, volume.A); + return variance.Moment2 - (Vector4.Dot(vector, vector) / volume.Weight); + } + + /// + /// We want to minimize the sum of the variances of two sub-boxes. + /// The sum(c^2) terms can be ignored since their sum over both sub-boxes + /// is the same (the sum for the whole box) no matter where we split. + /// The remaining terms have a minus sign in the variance formula, + /// so we drop the minus sign and maximize the sum of the two terms. + /// + /// The cube. + /// The direction. + /// The first position. + /// The last position. + /// The cutting point. + /// The whole moment. + /// The . + private readonly float Maximize(ref Box cube, int direction, int first, int last, out int cut, Moment whole) + { + ReadOnlySpan momentSpan = this.momentsOwner.GetSpan(); + Moment bottom = Bottom(ref cube, direction, momentSpan); + + float max = 0F; + cut = -1; + + for (int i = first; i < last; i++) + { + Moment half = bottom + Top(ref cube, direction, i, momentSpan); + + if (half.Weight == 0) + { + continue; + } + + Vector4 vector = new(half.R, half.G, half.B, half.A); + float temp = Vector4.Dot(vector, vector) / half.Weight; + + half = whole - half; + + if (half.Weight == 0) + { + continue; + } + + vector = new Vector4(half.R, half.G, half.B, half.A); + temp += Vector4.Dot(vector, vector) / half.Weight; + + if (temp > max) + { + max = temp; + cut = i; + } + } + + return max; + } + + /// + /// Cuts a box. + /// + /// The first set. + /// The second set. + /// Returns a value indicating whether the box has been split. + private readonly bool Cut(ref Box set1, ref Box set2) + { + ReadOnlySpan momentSpan = this.momentsOwner.GetSpan(); + Moment whole = Volume(ref set1, momentSpan); + + float maxR = this.Maximize(ref set1, 3, set1.RMin + 1, set1.RMax, out int cutR, whole); + float maxG = this.Maximize(ref set1, 2, set1.GMin + 1, set1.GMax, out int cutG, whole); + float maxB = this.Maximize(ref set1, 1, set1.BMin + 1, set1.BMax, out int cutB, whole); + float maxA = this.Maximize(ref set1, 0, set1.AMin + 1, set1.AMax, out int cutA, whole); + + int dir; + + if ((maxR >= maxG) && (maxR >= maxB) && (maxR >= maxA)) + { + dir = 3; + + if (cutR < 0) + { + return false; + } + } + else if ((maxG >= maxR) && (maxG >= maxB) && (maxG >= maxA)) + { + dir = 2; + } + else if ((maxB >= maxR) && (maxB >= maxG) && (maxB >= maxA)) + { + dir = 1; + } + else + { + dir = 0; + } + + set2.RMax = set1.RMax; + set2.GMax = set1.GMax; + set2.BMax = set1.BMax; + set2.AMax = set1.AMax; + + switch (dir) + { + // Red + case 3: + set2.RMin = set1.RMax = cutR; + set2.GMin = set1.GMin; + set2.BMin = set1.BMin; + set2.AMin = set1.AMin; + break; + + // Green + case 2: + set2.GMin = set1.GMax = cutG; + set2.RMin = set1.RMin; + set2.BMin = set1.BMin; + set2.AMin = set1.AMin; + break; + + // Blue + case 1: + set2.BMin = set1.BMax = cutB; + set2.RMin = set1.RMin; + set2.GMin = set1.GMin; + set2.AMin = set1.AMin; + break; + + // Alpha + case 0: + set2.AMin = set1.AMax = cutA; + set2.RMin = set1.RMin; + set2.GMin = set1.GMin; + set2.BMin = set1.BMin; + break; + } + + set1.Volume = (set1.RMax - set1.RMin) * (set1.GMax - set1.GMin) * (set1.BMax - set1.BMin) * (set1.AMax - set1.AMin); + set2.Volume = (set2.RMax - set2.RMin) * (set2.GMax - set2.GMin) * (set2.BMax - set2.BMin) * (set2.AMax - set2.AMin); + + return true; + } + + /// + /// Marks a color space tag. + /// + /// The cube. + /// A label. + private readonly void Mark(ref Box cube, byte label) + { + Span tagSpan = this.tagsOwner.GetSpan(); + + for (int r = cube.RMin + 1; r <= cube.RMax; r++) + { + // Currently, RyuJIT hoists the invariants of multi-level nested loop only to the + // immediate outer loop. See https://github.com/dotnet/runtime/issues/61420 + // To ensure the calculation doesn't happen repeatedly, hoist some of the calculations + // in the form of ind1* manually. + int ind1R = (r << ((IndexBits * 2) + IndexAlphaBits)) + + (r << (IndexBits + IndexAlphaBits + 1)) + + (r << (IndexBits * 2)) + + (r << (IndexBits + 1)) + + r; + + for (int g = cube.GMin + 1; g <= cube.GMax; g++) + { + int ind1G = ind1R + + (g << (IndexBits + IndexAlphaBits)) + + (g << IndexBits) + + g; + int r_g = r + g; + + for (int b = cube.BMin + 1; b <= cube.BMax; b++) + { + int ind1B = ind1G + + ((r_g + b) << IndexAlphaBits) + + b; + + for (int a = cube.AMin + 1; a <= cube.AMax; a++) + { + int index = ind1B + a; + + tagSpan[index] = label; + } + } + } + } + } + + /// + /// Builds the cube. + /// + private void BuildCube() + { + // Store the volume variance. + using IMemoryOwner vvOwner = this.Configuration.MemoryAllocator.Allocate(this.maxColors); + Span vv = vvOwner.GetSpan(); + + ref Box cube = ref MemoryMarshal.GetArrayDataReference(this.colorCube); + cube.RMin = cube.GMin = cube.BMin = cube.AMin = 0; + cube.RMax = cube.GMax = cube.BMax = IndexCount - 1; + cube.AMax = IndexAlphaCount - 1; + + int next = 0; + + for (int i = 1; i < this.maxColors; i++) + { + ref Box nextCube = ref this.colorCube[next]; + ref Box currentCube = ref this.colorCube[i]; + if (this.Cut(ref nextCube, ref currentCube)) + { + vv[next] = nextCube.Volume > 1 ? this.Variance(ref nextCube) : 0D; + vv[i] = currentCube.Volume > 1 ? this.Variance(ref currentCube) : 0D; + } + else + { + vv[next] = 0D; + i--; + } + + next = 0; + + double temp = vv[0]; + for (int k = 1; k <= i; k++) + { + if (vv[k] > temp) + { + temp = vv[k]; + next = k; + } + } + + if (temp <= 0D) + { + this.maxColors = i + 1; + break; + } + } + } + + private struct Moment + { + /// + /// Moment of r*P(c). + /// + public long R; + + /// + /// Moment of g*P(c). + /// + public long G; + + /// + /// Moment of b*P(c). + /// + public long B; + + /// + /// Moment of a*P(c). + /// + public long A; + + /// + /// Moment of P(c). + /// + public long Weight; + + /// + /// Moment of c^2*P(c). + /// + public double Moment2; + + [MethodImpl(InliningOptions.ShortMethod)] + public static Moment operator +(Moment x, Moment y) + { + x.R += y.R; + x.G += y.G; + x.B += y.B; + x.A += y.A; + x.Weight += y.Weight; + x.Moment2 += y.Moment2; + return x; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static Moment operator -(Moment x, Moment y) + { + x.R -= y.R; + x.G -= y.G; + x.B -= y.B; + x.A -= y.A; + x.Weight -= y.Weight; + x.Moment2 -= y.Moment2; + return x; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static Moment operator -(Moment x) + { + x.R = -x.R; + x.G = -x.G; + x.B = -x.B; + x.A = -x.A; + x.Weight = -x.Weight; + x.Moment2 = -x.Moment2; + return x; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static Moment operator +(Moment x, Rgba32 y) + { + x.R += y.R; + x.G += y.G; + x.B += y.B; + x.A += y.A; + x.Weight++; + + Vector4 vector = new(y.R, y.G, y.B, y.A); + x.Moment2 += Vector4.Dot(vector, vector); + + return x; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public readonly Vector4 Normalize() + => new Vector4(this.R, this.G, this.B, this.A) / this.Weight / 255F; + } + + /// + /// Represents a box color cube. + /// + private struct Box : IEquatable + { + /// + /// Gets or sets the min red value, exclusive. + /// + public int RMin; + + /// + /// Gets or sets the max red value, inclusive. + /// + public int RMax; + + /// + /// Gets or sets the min green value, exclusive. + /// + public int GMin; + + /// + /// Gets or sets the max green value, inclusive. + /// + public int GMax; + + /// + /// Gets or sets the min blue value, exclusive. + /// + public int BMin; + + /// + /// Gets or sets the max blue value, inclusive. + /// + public int BMax; + + /// + /// Gets or sets the min alpha value, exclusive. + /// + public int AMin; + + /// + /// Gets or sets the max alpha value, inclusive. + /// + public int AMax; + + /// + /// Gets or sets the volume. + /// + public int Volume; + + /// + public override readonly bool Equals(object? obj) + => obj is Box box + && this.Equals(box); + + /// + public readonly bool Equals(Box other) => + this.RMin == other.RMin + && this.RMax == other.RMax + && this.GMin == other.GMin + && this.GMax == other.GMax + && this.BMin == other.BMin + && this.BMax == other.BMax + && this.AMin == other.AMin + && this.AMax == other.AMax + && this.Volume == other.Volume; + + /// + public override readonly int GetHashCode() + { + HashCode hash = default; + hash.Add(this.RMin); + hash.Add(this.RMax); + hash.Add(this.GMin); + hash.Add(this.GMax); + hash.Add(this.BMin); + hash.Add(this.BMax); + hash.Add(this.AMin); + hash.Add(this.AMax); + hash.Add(this.Volume); + return hash.ToHashCode(); + } + } + + private readonly struct PixelRowDelegate : IQuantizingPixelRowDelegate + { + private readonly WuQuantizer quantizer; + + public PixelRowDelegate(ref WuQuantizer quantizer) => this.quantizer = quantizer; + + public void Invoke(ReadOnlySpan row, int rowIndex) => this.quantizer.Build3DHistogram(row); + } + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs b/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs new file mode 100644 index 0000000..8b7b3b2 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Defines a crop operation on an image. + /// + public sealed class CropProcessor : CloningImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The target cropped rectangle. + /// The source image size. + public CropProcessor(Rectangle cropRectangle, Size sourceSize) + { + // Check bounds here and throw if we are passed a rectangle exceeding our source bounds. + Guard.IsTrue( + new Rectangle(Point.Empty, sourceSize).Contains(cropRectangle), + nameof(cropRectangle), + "Crop rectangle should be smaller than the source bounds."); + + this.CropRectangle = cropRectangle; + } + + /// + /// Gets the width. + /// + public Rectangle CropRectangle { get; } + + /// + public override ICloningImageProcessor CreatePixelSpecificCloningProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + => new CropProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs new file mode 100644 index 0000000..2099a1f --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs @@ -0,0 +1,105 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Provides methods to allow the cropping of an image. + /// + /// The pixel format. + internal class CropProcessor : TransformProcessor + where TPixel : unmanaged, IPixel + { + private readonly Rectangle cropRectangle; + private readonly Matrix4x4 transformMatrix; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The . + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public CropProcessor(Configuration configuration, CropProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.cropRectangle = definition.CropRectangle; + + // Calculate the transform matrix from the crop operation to allow us + // to update any metadata that represents pixel coordinates in the source image. + this.transformMatrix = new ProjectiveTransformBuilder() + .AppendTranslation(new PointF(-this.cropRectangle.X, -this.cropRectangle.Y)) + .BuildMatrix(sourceRectangle); + } + + /// + protected override Size GetDestinationSize() => new(this.cropRectangle.Width, this.cropRectangle.Height); + + /// + protected override Matrix4x4 GetTransformMatrix() => this.transformMatrix; + + /// + protected override void OnFrameApply(ImageFrame source, ImageFrame destination) + { + // Handle crop dimensions identical to the original + if (source.Width == destination.Width + && source.Height == destination.Height + && this.SourceRectangle == this.cropRectangle) + { + // the cloned will be blank here copy all the pixel data over + source.PixelBuffer.CopyTo(destination.PixelBuffer); + return; + } + + Rectangle bounds = this.cropRectangle; + + // Copying is too cheap to benefit from parallelization; + // the overhead exceeds the work per task. See #3111. + RowOperation operation = new(bounds, source.PixelBuffer, destination.PixelBuffer); + + for (int y = bounds.Top; y < bounds.Bottom; y++) + { + operation.Invoke(y); + } + } + + /// + /// A implementing the processor logic for . + /// + private readonly struct RowOperation : IRowOperation + { + private readonly Rectangle bounds; + private readonly Buffer2D source; + private readonly Buffer2D destination; + + /// + /// Initializes a new instance of the struct. + /// + /// The target processing bounds for the current instance. + /// The source for the current instance. + /// The destination for the current instance. + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperation(Rectangle bounds, Buffer2D source, Buffer2D destination) + { + this.bounds = bounds; + this.source = source; + this.destination = destination; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y) + { + Span sourceRow = this.source.DangerousGetRowSpan(y)[this.bounds.Left..]; + Span targetRow = this.destination.DangerousGetRowSpan(y - this.bounds.Top); + sourceRow[..this.bounds.Width].CopyTo(targetRow); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/DegenerateTransformException.cs b/ImageSharp/Processing/Processors/Transforms/DegenerateTransformException.cs new file mode 100644 index 0000000..514244c --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/DegenerateTransformException.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Represents an error that occurs during a transform operation. + /// + public sealed class DegenerateTransformException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public DegenerateTransformException() + { + } + + /// + /// Initializes a new instance of the class + /// with a specified error message. + /// + /// The message that describes the error. + public DegenerateTransformException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class + /// with a specified error message and a reference to the inner exception that is + /// the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. + public DegenerateTransformException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor.cs b/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor.cs new file mode 100644 index 0000000..d7f6857 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Defines cropping operation that preserves areas of highest entropy. + /// + public sealed class EntropyCropProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + public EntropyCropProcessor() + : this(.5F) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The threshold to split the image. Must be between 0 and 1. + /// + /// is less than 0 or is greater than 1. + /// + public EntropyCropProcessor(float threshold) + { + Guard.MustBeBetweenOrEqualTo(threshold, 0, 1F, nameof(threshold)); + this.Threshold = threshold; + } + + /// + /// Gets the entropy threshold value. + /// + public float Threshold { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new EntropyCropProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs new file mode 100644 index 0000000..7984113 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs @@ -0,0 +1,178 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Binarization; +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Provides methods to allow the cropping of an image to preserve areas of highest entropy. + /// + /// The pixel format. + internal class EntropyCropProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly EntropyCropProcessor definition; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The . + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public EntropyCropProcessor(Configuration configuration, EntropyCropProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + => this.definition = definition; + + /// + protected override void BeforeImageApply() + { + Rectangle rectangle; + + // TODO: This is clunky. We should add behavior enum to ExtractFrame. + // All frames have be the same size so we only need to calculate the correct dimensions for the first frame + using (Image temp = new(this.Configuration, this.Source.Metadata.DeepClone(), [this.Source.Frames.RootFrame.Clone()])) + { + Configuration configuration = this.Source.Configuration; + + // Detect the edges. + new EdgeDetector2DProcessor(KnownEdgeDetectorKernels.Sobel, false).Execute(this.Configuration, temp, this.SourceRectangle); + + // Apply threshold binarization filter. + new BinaryThresholdProcessor(this.definition.Threshold).Execute(this.Configuration, temp, this.SourceRectangle); + + // Search for the first white pixels + rectangle = GetFilteredBoundingRectangle(temp.Frames.RootFrame, 0); + } + + new CropProcessor(rectangle, this.Source.Size).Execute(this.Configuration, this.Source, this.SourceRectangle); + + base.BeforeImageApply(); + } + + /// + protected override void OnFrameApply(ImageFrame source) + { + // All processing happens at the image level within BeforeImageApply(); + } + + /// + /// Gets the bounding from the given points. + /// + /// + /// The designating the top left position. + /// + /// + /// The designating the bottom right position. + /// + /// + /// The bounding . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Rectangle GetBoundingRectangle(Point topLeft, Point bottomRight) + => new( + topLeft.X, + topLeft.Y, + bottomRight.X - topLeft.X, + bottomRight.Y - topLeft.Y); + + /// + /// Finds the bounding rectangle based on the first instance of any color component other + /// than the given one. + /// + /// The to search within. + /// The color component value to remove. + /// The channel to test against. + /// + /// The . + /// + private static Rectangle GetFilteredBoundingRectangle(ImageFrame bitmap, float componentValue, RgbaComponent channel = RgbaComponent.B) + { + int width = bitmap.Width; + int height = bitmap.Height; + Point topLeft = default; + Point bottomRight = default; + Func, int, int, float, bool> delegateFunc = channel switch + { + RgbaComponent.R => (pixels, x, y, b) => MathF.Abs(pixels[x, y].ToVector4().X - b) > Constants.Epsilon, + RgbaComponent.G => (pixels, x, y, b) => MathF.Abs(pixels[x, y].ToVector4().Y - b) > Constants.Epsilon, + RgbaComponent.B => (pixels, x, y, b) => MathF.Abs(pixels[x, y].ToVector4().Z - b) > Constants.Epsilon, + _ => (pixels, x, y, b) => MathF.Abs(pixels[x, y].ToVector4().W - b) > Constants.Epsilon, + }; + int GetMinY(ImageFrame pixels) + { + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + if (delegateFunc(pixels, x, y, componentValue)) + { + return y; + } + } + } + + return 0; + } + + int GetMaxY(ImageFrame pixels) + { + for (int y = height - 1; y > -1; y--) + { + for (int x = 0; x < width; x++) + { + if (delegateFunc(pixels, x, y, componentValue)) + { + return y; + } + } + } + + return height; + } + + int GetMinX(ImageFrame pixels) + { + for (int x = 0; x < width; x++) + { + for (int y = 0; y < height; y++) + { + if (delegateFunc(pixels, x, y, componentValue)) + { + return x; + } + } + } + + return 0; + } + + int GetMaxX(ImageFrame pixels) + { + for (int x = width - 1; x > -1; x--) + { + for (int y = 0; y < height; y++) + { + if (delegateFunc(pixels, x, y, componentValue)) + { + return x; + } + } + } + + return width; + } + + topLeft.Y = GetMinY(bitmap); + topLeft.X = GetMinX(bitmap); + bottomRight.Y = Numerics.Clamp(GetMaxY(bitmap) + 1, 0, height); + bottomRight.X = Numerics.Clamp(GetMaxX(bitmap) + 1, 0, width); + + return GetBoundingRectangle(topLeft, bottomRight); + } + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/IResampler.cs b/ImageSharp/Processing/Processors/Transforms/IResampler.cs new file mode 100644 index 0000000..9772d6a --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/IResampler.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Encapsulates an interpolation algorithm for resampling images. + /// + public interface IResampler + { + /// + /// Gets the radius in which to sample pixels. + /// + float Radius { get; } + + /// + /// Gets the result of the interpolation algorithm. + /// + /// The value to process. + /// + /// The + /// + float GetValue(float x); + + /// + /// Applies a transformation upon an image. + /// + /// The pixel format. + /// The transforming image processor. + void ApplyTransform(IResamplingTransformImageProcessor processor) + where TPixel : unmanaged, IPixel; + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/IResamplingTransformImageProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Transforms/IResamplingTransformImageProcessor{TPixel}.cs new file mode 100644 index 0000000..5bd0215 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/IResamplingTransformImageProcessor{TPixel}.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Implements an algorithm to alter the pixels of an image via resampling transforms. + /// + /// The pixel format. + public interface IResamplingTransformImageProcessor : IImageProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Applies a resampling transform with the given sampler. + /// + /// The type of sampler. + /// The sampler to use. + void ApplyTransform(in TResampler sampler) + where TResampler : struct, IResampler; + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/ISwizzler.cs b/ImageSharp/Processing/Processors/Transforms/ISwizzler.cs new file mode 100644 index 0000000..17d3f30 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/ISwizzler.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Encapsulate an algorithm to swizzle pixels in an image. + /// + public interface ISwizzler + { + /// + /// Gets the size of the image after transformation. + /// + public Size DestinationSize { get; } + + /// + /// Applies the swizzle transformation to a given point. + /// + /// Point to transform. + /// The transformed point. + public Point Transform(Point point); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor.cs b/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor.cs new file mode 100644 index 0000000..98f7abf --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Defines an affine transformation applicable on an . + /// + public class AffineTransformProcessor : CloningImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The transform matrix. + /// The sampler to perform the transform operation. + /// The target dimensions. + public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size targetDimensions) + { + Guard.NotNull(sampler, nameof(sampler)); + Guard.MustBeValueType(sampler); + + if (TransformUtilities.IsDegenerate(matrix)) + { + throw new DegenerateTransformException("Matrix is degenerate. Check input values."); + } + + this.Sampler = sampler; + this.TransformMatrix = matrix; + this.DestinationSize = targetDimensions; + } + + /// + /// Gets the sampler to perform interpolation of the transform operation. + /// + public IResampler Sampler { get; } + + /// + /// Gets the matrix used to supply the affine transform. + /// + public Matrix3x2 TransformMatrix { get; } + + /// + /// Gets the destination size to constrain the transformed image to. + /// + public Size DestinationSize { get; } + + /// + public override ICloningImageProcessor CreatePixelSpecificCloningProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + => new AffineTransformProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs new file mode 100644 index 0000000..f1e1bc4 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs @@ -0,0 +1,257 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Provides the base methods to perform affine transforms on an image. + /// + /// The pixel format. + internal class AffineTransformProcessor : TransformProcessor, IResamplingTransformImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly Size destinationSize; + private readonly Matrix3x2 transformMatrix; + private readonly Matrix4x4 transformMatrix4x4; + private readonly IResampler resampler; + private ImageFrame? source; + private ImageFrame? destination; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public AffineTransformProcessor(Configuration configuration, AffineTransformProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.destinationSize = definition.DestinationSize; + this.transformMatrix = definition.TransformMatrix; + this.transformMatrix4x4 = new(this.transformMatrix); + this.resampler = definition.Sampler; + } + + protected override Size GetDestinationSize() => this.destinationSize; + + /// + protected override void OnFrameApply(ImageFrame source, ImageFrame destination) + { + this.source = source; + this.destination = destination; + this.resampler.ApplyTransform(this); + } + + /// + protected override Matrix4x4 GetTransformMatrix() => this.transformMatrix4x4; + + /// + public void ApplyTransform(in TResampler sampler) + where TResampler : struct, IResampler + { + Configuration configuration = this.Configuration; + ImageFrame source = this.source!; + ImageFrame destination = this.destination!; + Matrix3x2 matrix = this.transformMatrix; + + // Handle transforms that result in output identical to the original. + // Degenerate matrices are already handled in the upstream definition. + if (matrix.Equals(Matrix3x2.Identity)) + { + // The clone will be blank here copy all the pixel data over + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds); + Buffer2DRegion sourceBuffer = source.PixelBuffer.GetRegion(interest); + Buffer2DRegion destinationBuffer = destination.PixelBuffer.GetRegion(interest); + for (int y = 0; y < sourceBuffer.Height; y++) + { + sourceBuffer.DangerousGetRowSpan(y).CopyTo(destinationBuffer.DangerousGetRowSpan(y)); + } + + return; + } + + // All matrices are defined in normalized coordinate space so we need to convert to pixel space. + // After normalization we need to invert the matrix for correct sampling. + matrix = TransformUtilities.NormalizeToPixel(matrix); + Matrix3x2.Invert(matrix, out matrix); + + if (sampler is NearestNeighborResampler) + { + NNAffineOperation nnOperation = new( + source.PixelBuffer, + Rectangle.Intersect(this.SourceRectangle, source.Bounds), + destination.PixelBuffer, + matrix); + + ParallelRowIterator.IterateRows( + configuration, + destination.Bounds, + in nnOperation); + + return; + } + + AffineOperation operation = new( + configuration, + source.PixelBuffer, + Rectangle.Intersect(this.SourceRectangle, source.Bounds), + destination.PixelBuffer, + in sampler, + matrix); + + ParallelRowIterator.IterateRowIntervals, Vector4>( + configuration, + destination.Bounds, + in operation); + } + + private readonly struct NNAffineOperation : IRowOperation + { + private readonly Buffer2D source; + private readonly Buffer2D destination; + private readonly Rectangle bounds; + private readonly Matrix3x2 matrix; + + [MethodImpl(InliningOptions.ShortMethod)] + public NNAffineOperation( + Buffer2D source, + Rectangle bounds, + Buffer2D destination, + Matrix3x2 matrix) + { + this.source = source; + this.bounds = bounds; + this.destination = destination; + this.matrix = matrix; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y) + { + Span destinationRowSpan = this.destination.DangerousGetRowSpan(y); + + for (int x = 0; x < destinationRowSpan.Length; x++) + { + Vector2 point = Vector2.Transform(new Vector2(x, y), this.matrix); + int px = (int)MathF.Round(point.X); + int py = (int)MathF.Round(point.Y); + + if (this.bounds.Contains(px, py)) + { + destinationRowSpan[x] = this.source.GetElementUnsafe(px, py); + } + } + } + } + + private readonly struct AffineOperation : IRowIntervalOperation + where TResampler : struct, IResampler + { + private readonly Configuration configuration; + private readonly Buffer2D source; + private readonly Rectangle bounds; + private readonly Buffer2D destination; + private readonly TResampler sampler; + private readonly Matrix3x2 matrix; + private readonly float yRadius; + private readonly float xRadius; + + [MethodImpl(InliningOptions.ShortMethod)] + public AffineOperation( + Configuration configuration, + Buffer2D source, + Rectangle bounds, + Buffer2D destination, + in TResampler sampler, + Matrix3x2 matrix) + { + this.configuration = configuration; + this.source = source; + this.bounds = bounds; + this.destination = destination; + this.sampler = sampler; + this.matrix = matrix; + + this.yRadius = LinearTransformUtility.GetSamplingRadius(in sampler, source.Height, destination.Height); + this.xRadius = LinearTransformUtility.GetSamplingRadius(in sampler, source.Width, destination.Width); + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) + => bounds.Width; + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(in RowInterval rows, Span span) + { + Matrix3x2 matrix = this.matrix; + TResampler sampler = this.sampler; + float yRadius = this.yRadius; + float xRadius = this.xRadius; + int minY = this.bounds.Y; + int maxY = this.bounds.Bottom - 1; + int minX = this.bounds.X; + int maxX = this.bounds.Right - 1; + + for (int y = rows.Min; y < rows.Max; y++) + { + Span destinationRowSpan = this.destination.DangerousGetRowSpan(y); + PixelOperations.Instance.ToVector4( + this.configuration, + destinationRowSpan, + span, + PixelConversionModifiers.Scale); + + for (int x = 0; x < span.Length; x++) + { + Vector2 point = Vector2.Transform(new Vector2(x, y), matrix); + float pY = point.Y; + float pX = point.X; + + int top = LinearTransformUtility.GetRangeStart(yRadius, pY, minY, maxY); + int bottom = LinearTransformUtility.GetRangeEnd(yRadius, pY, minY, maxY); + int left = LinearTransformUtility.GetRangeStart(xRadius, pX, minX, maxX); + int right = LinearTransformUtility.GetRangeEnd(xRadius, pX, minX, maxX); + + if (bottom == top || right == left) + { + continue; + } + + Vector4 sum = Vector4.Zero; + for (int yK = top; yK <= bottom; yK++) + { + Span sourceRowSpan = this.source.DangerousGetRowSpan(yK); + float yWeight = sampler.GetValue(yK - pY); + + for (int xK = left; xK <= right; xK++) + { + float xWeight = sampler.GetValue(xK - pX); + + Vector4 current = sourceRowSpan[xK].ToScaledVector4(); + Numerics.Premultiply(ref current); + sum += current * xWeight * yWeight; + } + } + + span[x] = sum; + } + + Numerics.UnPremultiply(span); + PixelOperations.Instance.FromVector4Destructive( + this.configuration, + span, + destinationRowSpan, + PixelConversionModifiers.Scale); + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor.cs b/ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor.cs new file mode 100644 index 0000000..396c9e6 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Adjusts an image so that its orientation is suitable for viewing. Adjustments are based on EXIF metadata embedded in the image. + /// + public sealed class AutoOrientProcessor : IImageProcessor + { + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new AutoOrientProcessor(configuration, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor{TPixel}.cs new file mode 100644 index 0000000..96e5610 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor{TPixel}.cs @@ -0,0 +1,112 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; +using System; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Adjusts an image so that its orientation is suitable for viewing. Adjustments are based on EXIF metadata embedded in the image. + /// + /// The pixel format. + internal class AutoOrientProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public AutoOrientProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + } + + /// + protected override void BeforeImageApply() + { + ushort orientation = GetExifOrientation(this.Source); + Size size = this.SourceRectangle.Size; + switch (orientation) + { + case ExifOrientationMode.TopRight: + new FlipProcessor(FlipMode.Horizontal).Execute(this.Configuration, this.Source, this.SourceRectangle); + break; + + case ExifOrientationMode.BottomRight: + new RotateProcessor((int)RotateMode.Rotate180, size).Execute(this.Configuration, this.Source, this.SourceRectangle); + break; + + case ExifOrientationMode.BottomLeft: + new FlipProcessor(FlipMode.Vertical).Execute(this.Configuration, this.Source, this.SourceRectangle); + break; + + case ExifOrientationMode.LeftTop: + new RotateProcessor((int)RotateMode.Rotate90, size).Execute(this.Configuration, this.Source, this.SourceRectangle); + new FlipProcessor(FlipMode.Horizontal).Execute(this.Configuration, this.Source, this.SourceRectangle); + break; + + case ExifOrientationMode.RightTop: + new RotateProcessor((int)RotateMode.Rotate90, size).Execute(this.Configuration, this.Source, this.SourceRectangle); + break; + + case ExifOrientationMode.RightBottom: + new FlipProcessor(FlipMode.Vertical).Execute(this.Configuration, this.Source, this.SourceRectangle); + new RotateProcessor((int)RotateMode.Rotate270, size).Execute(this.Configuration, this.Source, this.SourceRectangle); + break; + + case ExifOrientationMode.LeftBottom: + new RotateProcessor((int)RotateMode.Rotate270, size).Execute(this.Configuration, this.Source, this.SourceRectangle); + break; + + case ExifOrientationMode.Unknown: + case ExifOrientationMode.TopLeft: + default: + break; + } + + base.BeforeImageApply(); + } + + /// + protected override void OnFrameApply(ImageFrame sourceBase) + { + // All processing happens at the image level within BeforeImageApply(); + } + + /// + /// Returns the current EXIF orientation + /// + /// The image to auto rotate. + /// The + private static ushort GetExifOrientation(Image source) + { + if (source.Metadata.ExifProfile is null) + { + return ExifOrientationMode.Unknown; + } + + if (!source.Metadata.ExifProfile.TryGetValue(ExifTag.Orientation, out IExifValue? value)) + { + return ExifOrientationMode.Unknown; + } + + ushort orientation; + if (value.DataType == ExifDataType.Short) + { + orientation = value.Value; + } + else + { + orientation = Convert.ToUInt16(value.Value); + source.Metadata.ExifProfile.RemoveValue(ExifTag.Orientation); + } + + source.Metadata.ExifProfile.SetValue(ExifTag.Orientation, ExifOrientationMode.TopLeft); + + return orientation; + } + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor.cs b/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor.cs new file mode 100644 index 0000000..0bee574 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Defines a flipping around the center point of the image. + /// + public sealed class FlipProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The used to perform flipping. + public FlipProcessor(FlipMode flipMode) => this.FlipMode = flipMode; + + /// + /// Gets the used to perform flipping. + /// + public FlipMode FlipMode { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new FlipProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs new file mode 100644 index 0000000..7c5ee78 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs @@ -0,0 +1,131 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Provides methods that allow the flipping of an image around its center point. + /// + /// The pixel format. + internal class FlipProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly FlipProcessor definition; + private readonly Matrix4x4 transformMatrix; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behavior or extending the library. + /// The . + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public FlipProcessor(Configuration configuration, FlipProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.definition = definition; + + // Calculate the transform matrix from the flip operation to allow us + // to update any metadata that represents pixel coordinates in the source image. + ProjectiveTransformBuilder builder = new(); + switch (this.definition.FlipMode) + { + // No default needed as we have already set the pixels. + case FlipMode.Vertical: + + // Flip vertically by scaling the Y axis by -1 and translating the Y coordinate. + builder.AppendScale(new Vector2(1, -1)) + .AppendTranslation(new PointF(0, this.SourceRectangle.Height - 1)); + break; + case FlipMode.Horizontal: + + // Flip horizontally by scaling the X axis by -1 and translating the X coordinate. + builder.AppendScale(new Vector2(-1, 1)) + .AppendTranslation(new PointF(this.SourceRectangle.Width - 1, 0)); + break; + default: + this.transformMatrix = Matrix4x4.Identity; + return; + } + + this.transformMatrix = builder.BuildMatrix(sourceRectangle); + } + + /// + protected override void OnFrameApply(ImageFrame source) + { + switch (this.definition.FlipMode) + { + // No default needed as we have already set the pixels. + case FlipMode.Vertical: + FlipX(source.PixelBuffer, this.Configuration); + break; + case FlipMode.Horizontal: + FlipY(source, this.Configuration); + break; + } + } + + /// + protected override void AfterFrameApply(ImageFrame source) + => source.Metadata.AfterFrameApply(source, source, this.transformMatrix); + + /// + protected override void AfterImageApply() + => this.Source.Metadata.AfterImageApply(this.Source, this.transformMatrix); + + /// + /// Swaps the image at the X-axis, which goes horizontally through the middle at half the height of the image. + /// + /// The source image to apply the process to. + /// The configuration. + private static void FlipX(Buffer2D source, Configuration configuration) + { + int height = source.Height; + using IMemoryOwner tempBuffer = configuration.MemoryAllocator.Allocate(source.Width); + Span temp = tempBuffer.Memory.Span; + + for (int yTop = 0; yTop < (int)((uint)height / 2); yTop++) + { + int yBottom = height - yTop - 1; + Span topRow = source.DangerousGetRowSpan(yBottom); + Span bottomRow = source.DangerousGetRowSpan(yTop); + topRow.CopyTo(temp); + bottomRow.CopyTo(topRow); + temp.CopyTo(bottomRow); + } + } + + /// + /// Swaps the image at the Y-axis, which goes vertically through the middle at half of the width of the image. + /// + /// The source image to apply the process to. + /// The configuration. + private static void FlipY(ImageFrame source, Configuration configuration) + { + RowOperation operation = new(source.PixelBuffer); + ParallelRowIterator.IterateRows( + configuration, + source.Bounds, + in operation); + } + + private readonly struct RowOperation : IRowOperation + { + private readonly Buffer2D source; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperation(Buffer2D source) => this.source = source; + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y) => this.source.DangerousGetRowSpan(y).Reverse(); + } + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Linear/GaussianEliminationSolver.cs b/ImageSharp/Processing/Processors/Transforms/Linear/GaussianEliminationSolver.cs new file mode 100644 index 0000000..9251ed7 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Linear/GaussianEliminationSolver.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms.Linear { + /// + /// Represents a solver for systems of linear equations using the Gaussian Elimination method. + /// This class applies Gaussian Elimination to transform the matrix into row echelon form and then performs back substitution to find the solution vector. + /// This implementation is based on: + /// + internal static class GaussianEliminationSolver + { + /// + /// Solves the system of linear equations represented by the given matrix and result vector using Gaussian Elimination. + /// + /// The square matrix representing the coefficients of the linear equations. + /// The vector representing the constants on the right-hand side of the linear equations. + /// Thrown if the matrix is singular and cannot be solved. + /// + /// The matrix passed to this method must be a square matrix. + /// If the matrix is singular (i.e., has no unique solution), an will be thrown. + /// + public static void Solve(double[][] matrix, double[] result) + { + TransformToRowEchelonForm(matrix, result); + ApplyBackSubstitution(matrix, result); + } + + private static void TransformToRowEchelonForm(double[][] matrix, double[] result) + { + int colCount = matrix.Length; + int rowCount = matrix[0].Length; + int pivotRow = 0; + for (int pivotCol = 0; pivotCol < colCount; pivotCol++) + { + double maxValue = double.Abs(matrix[pivotRow][pivotCol]); + int maxIndex = pivotRow; + for (int r = pivotRow + 1; r < rowCount; r++) + { + double value = double.Abs(matrix[r][pivotCol]); + if (value > maxValue) + { + maxIndex = r; + maxValue = value; + } + } + + if (matrix[maxIndex][pivotCol] == 0) + { + throw new NotSupportedException("Matrix is singular and cannot be solve"); + } + + (matrix[pivotRow], matrix[maxIndex]) = (matrix[maxIndex], matrix[pivotRow]); + (result[pivotRow], result[maxIndex]) = (result[maxIndex], result[pivotRow]); + + for (int r = pivotRow + 1; r < rowCount; r++) + { + double fraction = matrix[r][pivotCol] / matrix[pivotRow][pivotCol]; + for (int c = pivotCol + 1; c < colCount; c++) + { + matrix[r][c] -= matrix[pivotRow][c] * fraction; + } + + result[r] -= result[pivotRow] * fraction; + matrix[r][pivotCol] = 0; + } + + pivotRow++; + } + } + + private static void ApplyBackSubstitution(double[][] matrix, double[] result) + { + int rowCount = matrix[0].Length; + + for (int row = rowCount - 1; row >= 0; row--) + { + result[row] /= matrix[row][row]; + + for (int r = 0; r < row; r++) + { + result[r] -= result[row] * matrix[r][row]; + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs b/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs new file mode 100644 index 0000000..d1b8dc5 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Utility methods for linear transforms. + /// + internal static class LinearTransformUtility + { + /// + /// Returns the sampling radius for the given sampler and dimensions. + /// + /// The type of resampler. + /// The resampler sampler. + /// The source size. + /// The destination size. + /// The . + [MethodImpl(InliningOptions.ShortMethod)] + public static float GetSamplingRadius(in TResampler sampler, int sourceSize, int destinationSize) + where TResampler : struct, IResampler + { + float scale = (float)sourceSize / destinationSize; + + if (scale < 1F) + { + scale = 1F; + } + + return MathF.Ceiling(sampler.Radius * scale); + } + + /// + /// Gets the start position (inclusive) for a sampling range given + /// the radius, center position and max constraint. + /// + /// The radius. + /// The center position. + /// The min allowed amount. + /// The max allowed amount. + /// The . + [MethodImpl(InliningOptions.ShortMethod)] + public static int GetRangeStart(float radius, float center, int min, int max) + => Numerics.Clamp((int)MathF.Floor(center - radius), min, max); + + /// + /// Gets the end position (inclusive) for a sampling range given + /// the radius, center position and max constraint. + /// + /// The radius. + /// The center position. + /// The min allowed amount. + /// The max allowed amount. + /// The . + [MethodImpl(InliningOptions.ShortMethod)] + public static int GetRangeEnd(float radius, float center, int min, int max) + => Numerics.Clamp((int)MathF.Ceiling(center + radius), min, max); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor.cs b/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor.cs new file mode 100644 index 0000000..177e50a --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Defines a projective transformation applicable to an . + /// + public sealed class ProjectiveTransformProcessor : CloningImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The transform matrix. + /// The sampler to perform the transform operation. + /// The target dimensions. + public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size targetDimensions) + { + Guard.NotNull(sampler, nameof(sampler)); + Guard.MustBeValueType(sampler); + + if (TransformUtilities.IsDegenerate(matrix)) + { + throw new DegenerateTransformException("Matrix is degenerate. Check input values."); + } + + this.Sampler = sampler; + this.TransformMatrix = matrix; + this.DestinationSize = targetDimensions; + } + + /// + /// Gets the sampler to perform interpolation of the transform operation. + /// + public IResampler Sampler { get; } + + /// + /// Gets the matrix used to supply the projective transform. + /// + public Matrix4x4 TransformMatrix { get; } + + /// + /// Gets the destination size to constrain the transformed image to. + /// + public Size DestinationSize { get; } + + /// + public override ICloningImageProcessor CreatePixelSpecificCloningProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + => new ProjectiveTransformProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs new file mode 100644 index 0000000..2349e44 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs @@ -0,0 +1,255 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Provides the base methods to perform non-affine transforms on an image. + /// + /// The pixel format. + internal class ProjectiveTransformProcessor : TransformProcessor, IResamplingTransformImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly Size destinationSize; + private readonly IResampler resampler; + private readonly Matrix4x4 transformMatrix; + private ImageFrame? source; + private ImageFrame? destination; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public ProjectiveTransformProcessor(Configuration configuration, ProjectiveTransformProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.destinationSize = definition.DestinationSize; + this.transformMatrix = definition.TransformMatrix; + this.resampler = definition.Sampler; + } + + protected override Size GetDestinationSize() => this.destinationSize; + + /// + protected override void OnFrameApply(ImageFrame source, ImageFrame destination) + { + this.source = source; + this.destination = destination; + this.resampler.ApplyTransform(this); + } + + /// + protected override Matrix4x4 GetTransformMatrix() => this.transformMatrix; + + /// + public void ApplyTransform(in TResampler sampler) + where TResampler : struct, IResampler + { + Configuration configuration = this.Configuration; + ImageFrame source = this.source!; + ImageFrame destination = this.destination!; + Matrix4x4 matrix = this.transformMatrix; + + // Handle transforms that result in output identical to the original. + // Degenerate matrices are already handled in the upstream definition. + if (matrix.Equals(Matrix4x4.Identity)) + { + // The clone will be blank here copy all the pixel data over + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds); + Buffer2DRegion sourceBuffer = source.PixelBuffer.GetRegion(interest); + Buffer2DRegion destinationBuffer = destination.PixelBuffer.GetRegion(interest); + for (int y = 0; y < sourceBuffer.Height; y++) + { + sourceBuffer.DangerousGetRowSpan(y).CopyTo(destinationBuffer.DangerousGetRowSpan(y)); + } + + return; + } + + // All matrices are defined in normalized coordinate space so we need to convert to pixel space. + // After normalization we need to invert the matrix for correct sampling. + matrix = TransformUtilities.NormalizeToPixel(matrix); + Matrix4x4.Invert(matrix, out matrix); + + if (sampler is NearestNeighborResampler) + { + NNProjectiveOperation nnOperation = new( + source.PixelBuffer, + Rectangle.Intersect(this.SourceRectangle, source.Bounds), + destination.PixelBuffer, + matrix); + + ParallelRowIterator.IterateRows( + configuration, + destination.Bounds, + in nnOperation); + + return; + } + + ProjectiveOperation operation = new( + configuration, + source.PixelBuffer, + Rectangle.Intersect(this.SourceRectangle, source.Bounds), + destination.PixelBuffer, + in sampler, + matrix); + + ParallelRowIterator.IterateRowIntervals, Vector4>( + configuration, + destination.Bounds, + in operation); + } + + private readonly struct NNProjectiveOperation : IRowOperation + { + private readonly Buffer2D source; + private readonly Buffer2D destination; + private readonly Rectangle bounds; + private readonly Matrix4x4 matrix; + + [MethodImpl(InliningOptions.ShortMethod)] + public NNProjectiveOperation( + Buffer2D source, + Rectangle bounds, + Buffer2D destination, + Matrix4x4 matrix) + { + this.source = source; + this.bounds = bounds; + this.destination = destination; + this.matrix = matrix; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y) + { + Span destinationRowSpan = this.destination.DangerousGetRowSpan(y); + + for (int x = 0; x < destinationRowSpan.Length; x++) + { + Vector2 point = TransformUtilities.ProjectiveTransform2D(x, y, this.matrix); + int px = (int)MathF.Round(point.X); + int py = (int)MathF.Round(point.Y); + + if (this.bounds.Contains(px, py)) + { + destinationRowSpan[x] = this.source.GetElementUnsafe(px, py); + } + } + } + } + + private readonly struct ProjectiveOperation : IRowIntervalOperation + where TResampler : struct, IResampler + { + private readonly Configuration configuration; + private readonly Buffer2D source; + private readonly Rectangle bounds; + private readonly Buffer2D destination; + private readonly TResampler sampler; + private readonly Matrix4x4 matrix; + private readonly float yRadius; + private readonly float xRadius; + + [MethodImpl(InliningOptions.ShortMethod)] + public ProjectiveOperation( + Configuration configuration, + Buffer2D source, + Rectangle bounds, + Buffer2D destination, + in TResampler sampler, + Matrix4x4 matrix) + { + this.configuration = configuration; + this.source = source; + this.bounds = bounds; + this.destination = destination; + this.sampler = sampler; + this.matrix = matrix; + + this.yRadius = LinearTransformUtility.GetSamplingRadius(in sampler, bounds.Height, destination.Height); + this.xRadius = LinearTransformUtility.GetSamplingRadius(in sampler, bounds.Width, destination.Width); + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int GetRequiredBufferLength(Rectangle bounds) + => bounds.Width; + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(in RowInterval rows, Span span) + { + Matrix4x4 matrix = this.matrix; + TResampler sampler = this.sampler; + float yRadius = this.yRadius; + float xRadius = this.xRadius; + int minY = this.bounds.Y; + int maxY = this.bounds.Bottom - 1; + int minX = this.bounds.X; + int maxX = this.bounds.Right - 1; + + for (int y = rows.Min; y < rows.Max; y++) + { + Span destinationRowSpan = this.destination.DangerousGetRowSpan(y); + PixelOperations.Instance.ToVector4( + this.configuration, + destinationRowSpan, + span, + PixelConversionModifiers.Scale); + + for (int x = 0; x < span.Length; x++) + { + Vector2 point = TransformUtilities.ProjectiveTransform2D(x, y, matrix); + float pY = point.Y; + float pX = point.X; + + int top = LinearTransformUtility.GetRangeStart(yRadius, pY, minY, maxY); + int bottom = LinearTransformUtility.GetRangeEnd(yRadius, pY, minY, maxY); + int left = LinearTransformUtility.GetRangeStart(xRadius, pX, minX, maxX); + int right = LinearTransformUtility.GetRangeEnd(xRadius, pX, minX, maxX); + + if (bottom <= top || right <= left) + { + continue; + } + + Vector4 sum = Vector4.Zero; + for (int yK = top; yK <= bottom; yK++) + { + Span sourceRowSpan = this.source.DangerousGetRowSpan(yK); + float yWeight = sampler.GetValue(yK - pY); + + for (int xK = left; xK <= right; xK++) + { + float xWeight = sampler.GetValue(xK - pX); + + Vector4 current = sourceRowSpan[xK].ToScaledVector4(); + Numerics.Premultiply(ref current); + sum += current * xWeight * yWeight; + } + } + + span[x] = sum; + } + + Numerics.UnPremultiply(span); + PixelOperations.Instance.FromVector4Destructive( + this.configuration, + span, + destinationRowSpan, + PixelConversionModifiers.Scale); + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs b/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs new file mode 100644 index 0000000..5f58819 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Defines a rotation applicable to an . + /// + public sealed class RotateProcessor : AffineTransformProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The angle of rotation in degrees. + /// The source image size + public RotateProcessor(float degrees, Size sourceSize) + : this(degrees, KnownResamplers.Bicubic, sourceSize) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The angle of rotation in degrees. + /// The sampler to perform the rotating operation. + /// The source image size + public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) + : this( + TransformUtilities.CreateRotationTransformMatrixDegrees(degrees, sourceSize), + sampler, + sourceSize) + => this.Degrees = degrees; + + // Helper constructor + private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize) + : base(rotationMatrix, sampler, TransformUtilities.GetTransformedCanvasSize(rotationMatrix, sourceSize)) + { + } + + /// + /// Gets the angle of rotation in degrees. + /// + public float Degrees { get; } + + /// + public override ICloningImageProcessor CreatePixelSpecificCloningProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + => new RotateProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs new file mode 100644 index 0000000..84f3987 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs @@ -0,0 +1,285 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Provides methods that allow the rotating of images. + /// + /// The pixel format. + internal class RotateProcessor : AffineTransformProcessor + where TPixel : unmanaged, IPixel + { + private readonly float degrees; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public RotateProcessor(Configuration configuration, RotateProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, definition, source, sourceRectangle) + => this.degrees = definition.Degrees; + + /// + protected override void OnFrameApply(ImageFrame source, ImageFrame destination) + { + if (this.OptimizedApply(source, destination, this.Configuration)) + { + return; + } + + base.OnFrameApply(source, destination); + } + + /// + protected override void AfterImageApply(Image destination) + { + ExifProfile? profile = destination.Metadata.ExifProfile; + if (profile is null) + { + return; + } + + if (MathF.Abs(WrapDegrees(this.degrees)) < Constants.Epsilon) + { + // No need to do anything so return. + return; + } + + profile.RemoveValue(ExifTag.Orientation); + + base.AfterImageApply(destination); + } + + /// + /// Wraps a given angle in degrees so that it falls withing the 0-360 degree range + /// + /// The angle of rotation in degrees. + /// The . + private static float WrapDegrees(float degrees) + { + degrees %= 360; + + while (degrees < 0) + { + degrees += 360; + } + + return degrees; + } + + /// + /// Rotates the images with an optimized method when the angle is 90, 180 or 270 degrees. + /// + /// The source image. + /// The destination image. + /// The configuration. + /// + /// The + /// + private bool OptimizedApply( + ImageFrame source, + ImageFrame destination, + Configuration configuration) + { + // Wrap the degrees to keep within 0-360 so we can apply optimizations when possible. + float degrees = WrapDegrees(this.degrees); + + if (MathF.Abs(degrees) < Constants.Epsilon) + { + // The destination will be blank here so copy all the pixel data over + source.PixelBuffer.CopyTo(destination.PixelBuffer); + return true; + } + + if (MathF.Abs(degrees - 90) < Constants.Epsilon) + { + Rotate90(source, destination, configuration); + return true; + } + + if (MathF.Abs(degrees - 180) < Constants.Epsilon) + { + Rotate180(source, destination, configuration); + return true; + } + + if (MathF.Abs(degrees - 270) < Constants.Epsilon) + { + Rotate270(source, destination, configuration); + return true; + } + + return false; + } + + /// + /// Rotates the image 180 degrees clockwise at the centre point. + /// + /// The source image. + /// The destination image. + /// The configuration. + private static void Rotate180(ImageFrame source, ImageFrame destination, Configuration configuration) + { + Rotate180RowOperation operation = new(source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer); + ParallelRowIterator.IterateRows( + configuration, + source.Bounds, + in operation); + } + + /// + /// Rotates the image 270 degrees clockwise at the center point. + /// + /// The source image. + /// The destination image. + /// The configuration. + private static void Rotate270(ImageFrame source, ImageFrame destination, Configuration configuration) + { + Rotate270RowIntervalOperation operation = new(destination.Bounds, source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer); + ParallelRowIterator.IterateRowIntervals( + configuration, + source.Bounds, + in operation); + } + + /// + /// Rotates the image 90 degrees clockwise at the center point. + /// + /// The source image. + /// The destination image. + /// The configuration. + private static void Rotate90(ImageFrame source, ImageFrame destination, Configuration configuration) + { + Rotate90RowOperation operation = new(destination.Bounds, source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer); + ParallelRowIterator.IterateRows( + configuration, + source.Bounds, + in operation); + } + + private readonly struct Rotate180RowOperation : IRowOperation + { + private readonly int width; + private readonly int height; + private readonly Buffer2D source; + private readonly Buffer2D destination; + + [MethodImpl(InliningOptions.ShortMethod)] + public Rotate180RowOperation( + int width, + int height, + Buffer2D source, + Buffer2D destination) + { + this.width = width; + this.height = height; + this.source = source; + this.destination = destination; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y) + { + Span sourceRow = this.source.DangerousGetRowSpan(y); + Span targetRow = this.destination.DangerousGetRowSpan(this.height - y - 1); + + for (int x = 0; x < this.width; x++) + { + targetRow[this.width - x - 1] = sourceRow[x]; + } + } + } + + private readonly struct Rotate270RowIntervalOperation : IRowIntervalOperation + { + private readonly Rectangle bounds; + private readonly int width; + private readonly int height; + private readonly Buffer2D source; + private readonly Buffer2D destination; + + [MethodImpl(InliningOptions.ShortMethod)] + public Rotate270RowIntervalOperation( + Rectangle bounds, + int width, + int height, + Buffer2D source, + Buffer2D destination) + { + this.bounds = bounds; + this.width = width; + this.height = height; + this.source = source; + this.destination = destination; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(in RowInterval rows) + { + for (int y = rows.Min; y < rows.Max; y++) + { + Span sourceRow = this.source.DangerousGetRowSpan(y); + for (int x = 0; x < this.width; x++) + { + int newX = this.height - y - 1; + newX = this.height - newX - 1; + int newY = this.width - x - 1; + + if (this.bounds.Contains(newX, newY)) + { + this.destination[newX, newY] = sourceRow[x]; + } + } + } + } + } + + private readonly struct Rotate90RowOperation : IRowOperation + { + private readonly Rectangle bounds; + private readonly int width; + private readonly int height; + private readonly Buffer2D source; + private readonly Buffer2D destination; + + [MethodImpl(InliningOptions.ShortMethod)] + public Rotate90RowOperation( + Rectangle bounds, + int width, + int height, + Buffer2D source, + Buffer2D destination) + { + this.bounds = bounds; + this.width = width; + this.height = height; + this.source = source; + this.destination = destination; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y) + { + Span sourceRow = this.source.DangerousGetRowSpan(y); + int newX = this.height - y - 1; + for (int x = 0; x < this.width; x++) + { + if (this.bounds.Contains(newX, x)) + { + this.destination[newX, x] = sourceRow[x]; + } + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs b/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs new file mode 100644 index 0000000..8c16f7d --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Defines a skew transformation applicable to an . + /// + public sealed class SkewProcessor : AffineTransformProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The angle in degrees to perform the skew along the x-axis. + /// The angle in degrees to perform the skew along the y-axis. + /// The source image size + public SkewProcessor(float degreesX, float degreesY, Size sourceSize) + : this(degreesX, degreesY, KnownResamplers.Bicubic, sourceSize) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The angle in degrees to perform the skew along the x-axis. + /// The angle in degrees to perform the skew along the y-axis. + /// The sampler to perform the skew operation. + /// The source image size + public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize) + : this( + TransformUtilities.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize), + sampler, + sourceSize) + { + this.DegreesX = degreesX; + this.DegreesY = degreesY; + } + + // Helper constructor: + private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize) + : base(skewMatrix, sampler, TransformUtilities.GetTransformedCanvasSize(skewMatrix, sourceSize)) + { + } + + /// + /// Gets the angle of rotation along the x-axis in degrees. + /// + public float DegreesX { get; } + + /// + /// Gets the angle of rotation along the y-axis in degrees. + /// + public float DegreesY { get; } + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Resamplers/BicubicResampler.cs b/ImageSharp/Processing/Processors/Transforms/Resamplers/BicubicResampler.cs new file mode 100644 index 0000000..37969ca --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Resamplers/BicubicResampler.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// The function implements the bicubic kernel algorithm W(x) as described on + /// Wikipedia + /// A commonly used algorithm within image processing that preserves sharpness better than triangle interpolation. + /// + public readonly struct BicubicResampler : IResampler + { + /// + public float Radius => 2; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public float GetValue(float x) + { + if (x < 0F) + { + x = -x; + } + + // Given the coefficient "a" as -0.5F. + if (x <= 1F) + { + // Below simplified result = ((a + 2F) * (x * x * x)) - ((a + 3F) * (x * x)) + 1; + return (((1.5F * x) - 2.5F) * x * x) + 1; + } + else if (x < 2F) + { + // Below simplified result = (a * (x * x * x)) - ((5F * a) * (x * x)) + ((8F * a) * x) - (4F * a); + return (((((-0.5F * x) + 2.5F) * x) - 4) * x) + 2; + } + + return 0; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void ApplyTransform(IResamplingTransformImageProcessor processor) + where TPixel : unmanaged, IPixel + => processor.ApplyTransform(in this); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Resamplers/BoxResampler.cs b/ImageSharp/Processing/Processors/Transforms/Resamplers/BoxResampler.cs new file mode 100644 index 0000000..ce7057a --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Resamplers/BoxResampler.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// The function implements the box algorithm. Similar to nearest neighbor when upscaling. + /// When downscaling the pixels will average, merging together. + /// + public readonly struct BoxResampler : IResampler + { + /// + public float Radius => 0.5F; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public float GetValue(float x) + { + if (x > -0.5F && x <= 0.5F) + { + return 1; + } + + return 0; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void ApplyTransform(IResamplingTransformImageProcessor processor) + where TPixel : unmanaged, IPixel + => processor.ApplyTransform(in this); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Resamplers/CubicResampler.cs b/ImageSharp/Processing/Processors/Transforms/Resamplers/CubicResampler.cs new file mode 100644 index 0000000..7c1bfd1 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Resamplers/CubicResampler.cs @@ -0,0 +1,111 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Cubic filters contain a collection of different filters of varying B-Spline and + /// Cardinal values. With these two values you can generate any smoothly fitting + /// (continuious first derivative) piece-wise cubic filter. + /// + /// + /// + public readonly struct CubicResampler : IResampler + { + private readonly float bspline; + private readonly float cardinal; + + /// + /// The Catmull-Rom filter is a well known standard Cubic Filter often used as a interpolation function. + /// This filter produces a reasonably sharp edge, but without a the pronounced gradient change on large + /// scale image enlargements that a 'Lagrange' filter can produce. + /// + public static readonly CubicResampler CatmullRom = new(2, 0, .5F); + + /// + /// The Hermite filter is type of smoothed triangular interpolation Filter, + /// This filter rounds off strong edges while preserving flat 'color levels' in the original image. + /// + public static readonly CubicResampler Hermite = new(2, 0, 0); + + /// + /// The function implements the Mitchell-Netravali algorithm as described on + /// Wikipedia + /// + public static readonly CubicResampler MitchellNetravali = new(2, .3333333F, .3333333F); + + /// + /// The function implements the Robidoux algorithm. + /// + /// + public static readonly CubicResampler Robidoux = new(2, .37821575509399867F, .31089212245300067F); + + /// + /// The function implements the Robidoux Sharp algorithm. + /// + /// + public static readonly CubicResampler RobidouxSharp = new(2, .2620145123990142F, .3689927438004929F); + + /// + /// The function implements the spline algorithm. + /// + /// + /// + /// The function implements the Robidoux Sharp algorithm. + /// + /// + public static readonly CubicResampler Spline = new(2, 1, 0); + + /// + /// Initializes a new instance of the struct. + /// + /// The sampling radius. + /// The B-Spline value. + /// The Cardinal cubic value. + public CubicResampler(float radius, float bspline, float cardinal) + { + this.Radius = radius; + this.bspline = bspline; + this.cardinal = cardinal; + } + + /// + public float Radius { get; } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public float GetValue(float x) + { + float b = this.bspline; + float c = this.cardinal; + + if (x < 0F) + { + x = -x; + } + + float temp = x * x; + if (x < 1F) + { + x = ((12 - (9 * b) - (6 * c)) * (x * temp)) + ((-18 + (12 * b) + (6 * c)) * temp) + (6 - (2 * b)); + return x / 6F; + } + + if (x < 2F) + { + x = ((-b - (6 * c)) * (x * temp)) + (((6 * b) + (30 * c)) * temp) + (((-12 * b) - (48 * c)) * x) + ((8 * b) + (24 * c)); + return x / 6F; + } + + return 0F; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void ApplyTransform(IResamplingTransformImageProcessor processor) + where TPixel : unmanaged, IPixel + => processor.ApplyTransform(in this); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Resamplers/LanczosResampler.cs b/ImageSharp/Processing/Processors/Transforms/Resamplers/LanczosResampler.cs new file mode 100644 index 0000000..ce53026 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Resamplers/LanczosResampler.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// The function implements the Lanczos kernel algorithm as described on + /// Wikipedia. + /// + public readonly struct LanczosResampler : IResampler + { + /// + /// Implements the Lanczos kernel algorithm with a radius of 2. + /// + public static readonly LanczosResampler Lanczos2 = new(2); + + /// + /// Implements the Lanczos kernel algorithm with a radius of 3. + /// + public static readonly LanczosResampler Lanczos3 = new(3); + + /// + /// Implements the Lanczos kernel algorithm with a radius of 5. + /// + public static readonly LanczosResampler Lanczos5 = new(5); + + /// + /// Implements the Lanczos kernel algorithm with a radius of 8. + /// + public static readonly LanczosResampler Lanczos8 = new(8); + + /// + /// Initializes a new instance of the struct. + /// + /// The sampling radius. + public LanczosResampler(float radius) => this.Radius = radius; + + /// + public float Radius { get; } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public float GetValue(float x) + { + if (x < 0F) + { + x = -x; + } + + float radius = this.Radius; + if (x < radius) + { + return Numerics.SinC(x) * Numerics.SinC(x / radius); + } + + return 0F; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void ApplyTransform(IResamplingTransformImageProcessor processor) + where TPixel : unmanaged, IPixel + => processor.ApplyTransform(in this); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Resamplers/NearestNeighborResampler.cs b/ImageSharp/Processing/Processors/Transforms/Resamplers/NearestNeighborResampler.cs new file mode 100644 index 0000000..d402ca6 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Resamplers/NearestNeighborResampler.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// The function implements the nearest neighbor algorithm. This uses an unscaled filter + /// which will select the closest pixel to the new pixels position. + /// + public readonly struct NearestNeighborResampler : IResampler + { + /// + public float Radius => 1; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public float GetValue(float x) => x; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void ApplyTransform(IResamplingTransformImageProcessor processor) + where TPixel : unmanaged, IPixel + => processor.ApplyTransform(in this); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Resamplers/TriangleResampler.cs b/ImageSharp/Processing/Processors/Transforms/Resamplers/TriangleResampler.cs new file mode 100644 index 0000000..de8efc3 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Resamplers/TriangleResampler.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// The function implements the triangle (bilinear) algorithm. + /// Bilinear interpolation can be used where perfect image transformation with pixel matching is impossible, + /// so that one can calculate and assign appropriate intensity values to pixels. + /// + public readonly struct TriangleResampler : IResampler + { + /// + public float Radius => 1; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public float GetValue(float x) + { + if (x < 0F) + { + x = -x; + } + + if (x < 1F) + { + return 1F - x; + } + + return 0F; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void ApplyTransform(IResamplingTransformImageProcessor processor) + where TPixel : unmanaged, IPixel + => processor.ApplyTransform(in this); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Resamplers/WelchResampler.cs b/ImageSharp/Processing/Processors/Transforms/Resamplers/WelchResampler.cs new file mode 100644 index 0000000..d2ed29d --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Resamplers/WelchResampler.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// The function implements the welch algorithm. + /// + /// + public readonly struct WelchResampler : IResampler + { + /// + public float Radius => 3; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public float GetValue(float x) + { + if (x < 0F) + { + x = -x; + } + + if (x < 3F) + { + return Numerics.SinC(x) * (1F - (x * x / 9F)); + } + + return 0F; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void ApplyTransform(IResamplingTransformImageProcessor processor) + where TPixel : unmanaged, IPixel + => processor.ApplyTransform(in this); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs b/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs new file mode 100644 index 0000000..120b9e7 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs @@ -0,0 +1,430 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Provides methods to help calculate the target rectangle when resizing using the + /// enumeration. + /// + internal static class ResizeHelper + { + public static unsafe int CalculateResizeWorkerHeightInWindowBands( + int windowBandHeight, + int width, + int sizeLimitHintInBytes) + { + int sizeLimitHint = sizeLimitHintInBytes / sizeof(Vector4); + int sizeOfOneWindow = windowBandHeight * width; + return Math.Max(2, sizeLimitHint / sizeOfOneWindow); + } + + /// + /// Calculates the target location and bounds to perform the resize operation against. + /// + /// The source image size. + /// The resize options. + /// + /// The tuple representing the location and the bounds + /// + public static (Size Size, Rectangle Rectangle) CalculateTargetLocationAndBounds(Size sourceSize, ResizeOptions options) + { + int width = options.Size.Width; + int height = options.Size.Height; + + if (width <= 0 && height <= 0) + { + ThrowInvalid($"Target width {width} and height {height} must be greater than zero."); + } + + // Ensure target size is populated across both dimensions. + // These dimensions are used to calculate the final dimensions determined by the mode algorithm. + // If only one of the incoming dimensions is 0, it will be modified here to maintain aspect ratio. + // If it is not possible to keep aspect ratio, make sure at least the minimum is is kept. + const int Min = 1; + if (width == 0 && height > 0) + { + width = (int)MathF.Max(Min, MathF.Round(sourceSize.Width * height / (float)sourceSize.Height)); + } + + if (height == 0 && width > 0) + { + height = (int)MathF.Max(Min, MathF.Round(sourceSize.Height * width / (float)sourceSize.Width)); + } + + switch (options.Mode) + { + case ResizeMode.Crop: + return CalculateCropRectangle(sourceSize, options, width, height); + case ResizeMode.Pad: + return CalculatePadRectangle(sourceSize, options, width, height); + case ResizeMode.BoxPad: + return CalculateBoxPadRectangle(sourceSize, options, width, height); + case ResizeMode.Max: + return CalculateMaxRectangle(sourceSize, width, height); + case ResizeMode.Min: + return CalculateMinRectangle(sourceSize, width, height); + case ResizeMode.Manual: + return CalculateManualRectangle(options, width, height); + + // case ResizeMode.Stretch: + default: + return (new Size(Sanitize(width), Sanitize(height)), new Rectangle(0, 0, Sanitize(width), Sanitize(height))); + } + } + + private static (Size Size, Rectangle Rectangle) CalculateBoxPadRectangle( + Size source, + ResizeOptions options, + int width, + int height) + { + int sourceWidth = source.Width; + int sourceHeight = source.Height; + + // Fractional variants for preserving aspect ratio. + float percentHeight = MathF.Abs(height / (float)sourceHeight); + float percentWidth = MathF.Abs(width / (float)sourceWidth); + + int boxPadHeight = height > 0 ? height : (int)MathF.Round(sourceHeight * percentWidth); + int boxPadWidth = width > 0 ? width : (int)MathF.Round(sourceWidth * percentHeight); + + // Only calculate if upscaling. + if (sourceWidth < boxPadWidth && sourceHeight < boxPadHeight) + { + int targetX; + int targetY; + int targetWidth = sourceWidth; + int targetHeight = sourceHeight; + width = boxPadWidth; + height = boxPadHeight; + + switch (options.Position) + { + case AnchorPositionMode.Left: + targetY = (int)((uint)(height - sourceHeight) / 2); + targetX = 0; + break; + case AnchorPositionMode.Right: + targetY = (int)((uint)(height - sourceHeight) / 2); + targetX = width - sourceWidth; + break; + case AnchorPositionMode.TopRight: + targetY = 0; + targetX = width - sourceWidth; + break; + case AnchorPositionMode.Top: + targetY = 0; + targetX = (int)((uint)(width - sourceWidth) / 2); + break; + case AnchorPositionMode.TopLeft: + targetY = 0; + targetX = 0; + break; + case AnchorPositionMode.BottomRight: + targetY = height - sourceHeight; + targetX = width - sourceWidth; + break; + case AnchorPositionMode.Bottom: + targetY = height - sourceHeight; + targetX = (int)((uint)(width - sourceWidth) / 2); + break; + case AnchorPositionMode.BottomLeft: + targetY = height - sourceHeight; + targetX = 0; + break; + default: + targetY = (int)((uint)(height - sourceHeight) / 2); + targetX = (int)((uint)(width - sourceWidth) / 2); + break; + } + + // Target image width and height can be different to the rectangle width and height. + return (new Size(Sanitize(width), Sanitize(height)), new Rectangle(targetX, targetY, Sanitize(targetWidth), Sanitize(targetHeight))); + } + + // Switch to pad mode to downscale and calculate from there. + return CalculatePadRectangle(source, options, width, height); + } + + private static (Size Size, Rectangle Rectangle) CalculateCropRectangle( + Size source, + ResizeOptions options, + int width, + int height) + { + float ratio; + int sourceWidth = source.Width; + int sourceHeight = source.Height; + + int targetX = 0; + int targetY = 0; + int targetWidth = width; + int targetHeight = height; + + // Fractional variants for preserving aspect ratio. + float percentHeight = MathF.Abs(height / (float)sourceHeight); + float percentWidth = MathF.Abs(width / (float)sourceWidth); + + if (percentHeight < percentWidth) + { + ratio = percentWidth; + + if (options.CenterCoordinates.HasValue) + { + float center = -(ratio * sourceHeight) * options.CenterCoordinates.Value.Y; + targetY = (int)MathF.Round(center + (height / 2F)); + + if (targetY > 0) + { + targetY = 0; + } + + if (targetY < (int)MathF.Round(height - (sourceHeight * ratio))) + { + targetY = (int)MathF.Round(height - (sourceHeight * ratio)); + } + } + else + { + switch (options.Position) + { + case AnchorPositionMode.Top: + case AnchorPositionMode.TopLeft: + case AnchorPositionMode.TopRight: + targetY = 0; + break; + case AnchorPositionMode.Bottom: + case AnchorPositionMode.BottomLeft: + case AnchorPositionMode.BottomRight: + targetY = (int)MathF.Round(height - (sourceHeight * ratio)); + break; + default: + targetY = (int)MathF.Round((height - (sourceHeight * ratio)) / 2F); + break; + } + } + + targetHeight = (int)MathF.Ceiling(sourceHeight * percentWidth); + } + else + { + ratio = percentHeight; + + if (options.CenterCoordinates.HasValue) + { + float center = -(ratio * sourceWidth) * options.CenterCoordinates.Value.X; + targetX = (int)MathF.Round(center + (width / 2F)); + + if (targetX > 0) + { + targetX = 0; + } + + if (targetX < (int)MathF.Round(width - (sourceWidth * ratio))) + { + targetX = (int)MathF.Round(width - (sourceWidth * ratio)); + } + } + else + { + switch (options.Position) + { + case AnchorPositionMode.Left: + case AnchorPositionMode.TopLeft: + case AnchorPositionMode.BottomLeft: + targetX = 0; + break; + case AnchorPositionMode.Right: + case AnchorPositionMode.TopRight: + case AnchorPositionMode.BottomRight: + targetX = (int)MathF.Round(width - (sourceWidth * ratio)); + break; + default: + targetX = (int)MathF.Round((width - (sourceWidth * ratio)) / 2F); + break; + } + } + + targetWidth = (int)MathF.Ceiling(sourceWidth * percentHeight); + } + + // Target image width and height can be different to the rectangle width and height. + return (new Size(Sanitize(width), Sanitize(height)), new Rectangle(targetX, targetY, Sanitize(targetWidth), Sanitize(targetHeight))); + } + + private static (Size Size, Rectangle Rectangle) CalculateMaxRectangle( + Size source, + int width, + int height) + { + int targetWidth = width; + int targetHeight = height; + + // Fractional variants for preserving aspect ratio. + float percentHeight = MathF.Abs(height / (float)source.Height); + float percentWidth = MathF.Abs(width / (float)source.Width); + + // Integers must be cast to floats to get needed precision + float ratio = height / (float)width; + float sourceRatio = source.Height / (float)source.Width; + + if (sourceRatio < ratio) + { + targetHeight = (int)MathF.Round(source.Height * percentWidth); + } + else + { + targetWidth = (int)MathF.Round(source.Width * percentHeight); + } + + // Replace the size to match the rectangle. + return (new Size(Sanitize(targetWidth), Sanitize(targetHeight)), new Rectangle(0, 0, Sanitize(targetWidth), Sanitize(targetHeight))); + } + + private static (Size Size, Rectangle Rectangle) CalculateMinRectangle( + Size source, + int width, + int height) + { + int sourceWidth = source.Width; + int sourceHeight = source.Height; + int targetWidth = width; + int targetHeight = height; + + // Don't upscale + if (width > sourceWidth || height > sourceHeight) + { + return (new Size(sourceWidth, sourceHeight), new Rectangle(0, 0, sourceWidth, sourceHeight)); + } + + // Find the shortest distance to go. + int widthDiff = sourceWidth - width; + int heightDiff = sourceHeight - height; + + if (widthDiff < heightDiff) + { + float sourceRatio = (float)sourceHeight / sourceWidth; + targetHeight = (int)MathF.Round(width * sourceRatio); + } + else if (widthDiff > heightDiff) + { + float sourceRatioInverse = (float)sourceWidth / sourceHeight; + targetWidth = (int)MathF.Round(height * sourceRatioInverse); + } + else + { + if (height > width) + { + float percentWidth = MathF.Abs(width / (float)sourceWidth); + targetHeight = (int)MathF.Round(sourceHeight * percentWidth); + } + else + { + float percentHeight = MathF.Abs(height / (float)sourceHeight); + targetWidth = (int)MathF.Round(sourceWidth * percentHeight); + } + } + + // Replace the size to match the rectangle. + return (new Size(Sanitize(targetWidth), Sanitize(targetHeight)), new Rectangle(0, 0, Sanitize(targetWidth), Sanitize(targetHeight))); + } + + private static (Size Size, Rectangle Rectangle) CalculatePadRectangle( + Size sourceSize, + ResizeOptions options, + int width, + int height) + { + float ratio; + int sourceWidth = sourceSize.Width; + int sourceHeight = sourceSize.Height; + + int targetX = 0; + int targetY = 0; + int targetWidth = width; + int targetHeight = height; + + // Fractional variants for preserving aspect ratio. + float percentHeight = MathF.Abs(height / (float)sourceHeight); + float percentWidth = MathF.Abs(width / (float)sourceWidth); + + if (percentHeight < percentWidth) + { + ratio = percentHeight; + targetWidth = (int)MathF.Round(sourceWidth * percentHeight); + + switch (options.Position) + { + case AnchorPositionMode.Left: + case AnchorPositionMode.TopLeft: + case AnchorPositionMode.BottomLeft: + targetX = 0; + break; + case AnchorPositionMode.Right: + case AnchorPositionMode.TopRight: + case AnchorPositionMode.BottomRight: + targetX = (int)MathF.Round(width - (sourceWidth * ratio)); + break; + default: + targetX = (int)MathF.Round((width - (sourceWidth * ratio)) / 2F); + break; + } + } + else + { + ratio = percentWidth; + targetHeight = (int)MathF.Round(sourceHeight * percentWidth); + + switch (options.Position) + { + case AnchorPositionMode.Top: + case AnchorPositionMode.TopLeft: + case AnchorPositionMode.TopRight: + targetY = 0; + break; + case AnchorPositionMode.Bottom: + case AnchorPositionMode.BottomLeft: + case AnchorPositionMode.BottomRight: + targetY = (int)MathF.Round(height - (sourceHeight * ratio)); + break; + default: + targetY = (int)MathF.Round((height - (sourceHeight * ratio)) / 2F); + break; + } + } + + // Target image width and height can be different to the rectangle width and height. + return (new Size(Sanitize(width), Sanitize(height)), new Rectangle(targetX, targetY, Sanitize(targetWidth), Sanitize(targetHeight))); + } + + private static (Size Size, Rectangle Rectangle) CalculateManualRectangle( + ResizeOptions options, + int width, + int height) + { + if (!options.TargetRectangle.HasValue) + { + ThrowInvalid("Manual resizing requires a target location and size."); + } + + Rectangle targetRectangle = options.TargetRectangle.Value; + + int targetX = targetRectangle.X; + int targetY = targetRectangle.Y; + int targetWidth = targetRectangle.Width > 0 ? targetRectangle.Width : width; + int targetHeight = targetRectangle.Height > 0 ? targetRectangle.Height : height; + + // Target image width and height can be different to the rectangle width and height. + return (new Size(Sanitize(width), Sanitize(height)), new Rectangle(targetX, targetY, Sanitize(targetWidth), Sanitize(targetHeight))); + } + + [DoesNotReturn] + private static void ThrowInvalid(string message) => throw new InvalidOperationException(message); + + private static int Sanitize(int input) => Math.Max(1, input); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs b/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs new file mode 100644 index 0000000..6353b0a --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs @@ -0,0 +1,188 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Points to a collection of weights allocated in . + /// + internal readonly unsafe struct ResizeKernel + { + /// + /// The buffer with the convolution factors. + /// Note that when FMA is supported, this is of size 4x that reported in . + /// + private readonly float* bufferPtr; + + /// + /// Initializes a new instance of the struct. + /// + /// The starting index for the destination row. + /// The pointer to the buffer with the convolution factors. + /// The length of the kernel. + [MethodImpl(InliningOptions.ShortMethod)] + internal ResizeKernel(int startIndex, float* bufferPtr, int length) + { + this.StartIndex = startIndex; + this.bufferPtr = bufferPtr; + this.Length = length; + } + + /// + /// Gets a value indicating whether vectorization is supported. + /// + public static bool IsHardwareAccelerated + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Vector256.IsHardwareAccelerated; + } + + /// + /// Gets the start index for the destination row. + /// + public int StartIndex + { + [MethodImpl(InliningOptions.ShortMethod)] + get; + } + + /// + /// Gets the length of the kernel. + /// + public int Length + { + [MethodImpl(InliningOptions.ShortMethod)] + get; + } + + /// + /// Gets the span representing the portion of the that this window covers. + /// + /// The . + /// + public Span Values + { + [MethodImpl(InliningOptions.ShortMethod)] + get + { + if (Vector256.IsHardwareAccelerated) + { + return new(this.bufferPtr, this.Length * 4); + } + + return new(this.bufferPtr, this.Length); + } + } + + /// + /// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this instance. + /// + /// The input span of vectors + /// The weighted sum + [MethodImpl(InliningOptions.ShortMethod)] + public Vector4 Convolve(Span rowSpan) + => this.ConvolveCore(ref rowSpan[this.StartIndex]); + + [MethodImpl(InliningOptions.ShortMethod)] + public Vector4 ConvolveCore(ref Vector4 rowStartRef) + { + if (IsHardwareAccelerated) + { + float* bufferStart = this.bufferPtr; + ref Vector4 rowEndRef = ref Unsafe.Add(ref rowStartRef, this.Length & ~3); + Vector256 result256_0 = Vector256.Zero; + Vector256 result256_1 = Vector256.Zero; + + while (Unsafe.IsAddressLessThan(ref rowStartRef, ref rowEndRef)) + { + Vector256 pixels256_0 = Unsafe.As>(ref rowStartRef); + Vector256 pixels256_1 = Unsafe.As>(ref Unsafe.Add(ref rowStartRef, (nuint)2)); + + result256_0 = Vector256_.MultiplyAdd(result256_0, Vector256.Load(bufferStart), pixels256_0); + result256_1 = Vector256_.MultiplyAdd(result256_1, Vector256.Load(bufferStart + 8), pixels256_1); + + bufferStart += 16; + rowStartRef = ref Unsafe.Add(ref rowStartRef, (nuint)4); + } + + result256_0 += result256_1; + + if ((this.Length & 3) >= 2) + { + Vector256 pixels256_0 = Unsafe.As>(ref rowStartRef); + result256_0 = Vector256_.MultiplyAdd(result256_0, Vector256.Load(bufferStart), pixels256_0); + + bufferStart += 8; + rowStartRef = ref Unsafe.Add(ref rowStartRef, (nuint)2); + } + + Vector128 result128 = result256_0.GetLower() + result256_0.GetUpper(); + + if ((this.Length & 1) != 0) + { + Vector128 pixels128 = Unsafe.As>(ref rowStartRef); + result128 = Vector128_.MultiplyAdd(result128, Vector128.Load(bufferStart), pixels128); + } + + return result128.AsVector4(); + } + else + { + // Destination color components + Vector4 result = Vector4.Zero; + float* bufferStart = this.bufferPtr; + float* bufferEnd = this.bufferPtr + this.Length; + + while (bufferStart < bufferEnd) + { + // Vector4 v = offsetedRowSpan[i]; + result += rowStartRef * *bufferStart; + + bufferStart++; + rowStartRef = ref Unsafe.Add(ref rowStartRef, (nuint)1); + } + + return result; + } + } + + /// + /// Copy the contents of altering + /// to the value . + /// + /// The new value for . + [MethodImpl(InliningOptions.ShortMethod)] + internal ResizeKernel AlterLeftValue(int left) + => new(left, this.bufferPtr, this.Length); + + internal void FillOrCopyAndExpand(Span values) + { + DebugGuard.IsTrue(values.Length == this.Length, nameof(values), "ResizeKernel.Fill: values.Length != this.Length!"); + + if (IsHardwareAccelerated) + { + Vector4* bufferStart = (Vector4*)this.bufferPtr; + ref float valuesStart = ref MemoryMarshal.GetReference(values); + ref float valuesEnd = ref Unsafe.Add(ref valuesStart, values.Length); + + while (Unsafe.IsAddressLessThan(ref valuesStart, ref valuesEnd)) + { + *bufferStart = new Vector4(valuesStart); + + bufferStart++; + valuesStart = ref Unsafe.Add(ref valuesStart, (nuint)1); + } + } + else + { + values.CopyTo(this.Values); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs b/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs new file mode 100644 index 0000000..b43011b --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + internal partial class ResizeKernelMap + { + /// + /// Memory-optimized where repeating rows are stored only once. + /// + private sealed class PeriodicKernelMap : ResizeKernelMap + { + private readonly int period; + + private readonly int cornerInterval; + + private readonly int sourcePeriod; + + public PeriodicKernelMap( + MemoryAllocator memoryAllocator, + int sourceLength, + int destinationLength, + double ratio, + double scale, + int radius, + int period, + int cornerInterval, + int sourcePeriod) + : base( + memoryAllocator, + sourceLength, + destinationLength, + (cornerInterval * 2) + period, + ratio, + scale, + radius) + { + this.cornerInterval = cornerInterval; + this.period = period; + this.sourcePeriod = sourcePeriod; + } + + internal override string Info => base.Info + $"|period:{this.period}|cornerInterval:{this.cornerInterval}"; + + protected internal override void Initialize(in TResampler sampler) + { + // Build top corner data + one period of the mosaic data: + int startOfFirstRepeatedMosaic = this.cornerInterval + this.period; + + for (int i = 0; i < startOfFirstRepeatedMosaic; i++) + { + this.kernels[i] = this.BuildKernel(in sampler, i, i); + } + + // Copy the mosaics: + int bottomStartDest = this.DestinationLength - this.cornerInterval; + for (int i = startOfFirstRepeatedMosaic; i < bottomStartDest; i++) + { + ResizeKernel kernel = this.kernels[i - this.period]; + + // Shift the kernel start index by the source-side period so the same weights align to the + // next repeated sampling window in the source image. + this.kernels[i] = kernel.AlterLeftValue(kernel.StartIndex + this.sourcePeriod); + } + + // Build bottom corner data: + int bottomStartData = this.cornerInterval + this.period; + for (int i = 0; i < this.cornerInterval; i++) + { + this.kernels[bottomStartDest + i] = this.BuildKernel(in sampler, bottomStartDest + i, bottomStartData + i); + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs new file mode 100644 index 0000000..f396357 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs @@ -0,0 +1,280 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Provides resize kernel values from an optimized contiguous memory region. + /// + internal partial class ResizeKernelMap : IDisposable + { + private static readonly TolerantMath TolerantMath = TolerantMath.Default; + + private readonly int sourceLength; + + private readonly double ratio; + + private readonly double scale; + + private readonly int radius; + + private readonly MemoryHandle pinHandle; + + private readonly Buffer2D data; + + private readonly ResizeKernel[] kernels; + + private bool isDisposed; + + // To avoid both GC allocations, and MemoryAllocator ceremony: + private readonly float[] tempValues; + + private ResizeKernelMap( + MemoryAllocator memoryAllocator, + int sourceLength, + int destinationLength, + int bufferHeight, + double ratio, + double scale, + int radius) + { + this.ratio = ratio; + this.scale = scale; + this.radius = radius; + this.sourceLength = sourceLength; + this.DestinationLength = destinationLength; + this.MaxDiameter = (radius * 2) + 1; + + int diameter = ResizeKernel.IsHardwareAccelerated ? this.MaxDiameter * 4 : this.MaxDiameter; + this.data = memoryAllocator.Allocate2D(diameter, bufferHeight, preferContiguosImageBuffers: true); + this.pinHandle = this.data.DangerousGetSingleMemory().Pin(); + this.kernels = new ResizeKernel[destinationLength]; + this.tempValues = new float[this.MaxDiameter]; + } + + /// + /// Gets the length of the destination row/column + /// + public int DestinationLength { get; } + + /// + /// Gets the maximum diameter of the kernels. + /// + public int MaxDiameter { get; } + + /// + /// Gets a string of information to help debugging + /// + internal virtual string Info => + $"radius:{this.radius}|sourceSize:{this.sourceLength}|destinationSize:{this.DestinationLength}|ratio:{this.ratio}|scale:{this.scale}"; + + /// + /// Disposes instance releasing it's backing buffer. + /// + public void Dispose() + => this.Dispose(true); + + /// + /// Disposes the object and frees resources for the Garbage Collector. + /// + /// Whether to dispose of managed and unmanaged objects. + protected virtual void Dispose(bool disposing) + { + if (!this.isDisposed) + { + this.isDisposed = true; + + if (disposing) + { + this.pinHandle.Dispose(); + this.data.Dispose(); + } + } + } + + /// + /// Returns a for an index value between 0 and DestinationSize - 1. + /// + [MethodImpl(InliningOptions.ShortMethod)] + internal ref ResizeKernel GetKernel(nuint destIdx) => ref this.kernels[(int)destIdx]; + + /// + /// Returns a read-only span of over the underlying kernel data. + /// + [MethodImpl(InliningOptions.ShortMethod)] + internal ReadOnlySpan GetKernelSpan() => this.kernels; + + /// + /// Computes the weights to apply at each pixel when resizing. + /// + /// The type of sampler. + /// The + /// The destination size + /// The source size + /// The to use for buffer allocations + /// The + public static ResizeKernelMap Calculate( + in TResampler sampler, + int destinationSize, + int sourceSize, + MemoryAllocator memoryAllocator) + where TResampler : struct, IResampler + { + double ratio = (double)sourceSize / destinationSize; + double scale = ratio; + + if (scale < 1) + { + scale = 1; + } + + int radius = (int)TolerantMath.Ceiling(scale * sampler.Radius); + + // 'ratio' is a rational number. + // Multiplying it by destSize/GCD(sourceSize, destinationSize) yields an integer, so every `period` rows + // the destination-space sampling centers repeat their fractional alignment. `period` is the repeat length + // in destination rows, while `sourcePeriod` is the corresponding integer offset in source pixels that + // must be added to the kernel's left index when we reuse the same weights from a previous period. + int gcd = Numerics.GreatestCommonDivisor(sourceSize, destinationSize); + int period = destinationSize / gcd; + int sourcePeriod = sourceSize / gcd; + + // the center position at i == 0: + double center0 = (ratio - 1) * 0.5; + double firstNonNegativeLeftVal = (radius - center0 - 1) / ratio; + + // The number of rows building a "stairway" at the top and the bottom of the kernel map + // corresponding to the corners of the image. + // If we do not normalize the kernel values, these rows also fit the periodic logic, + // however, it's just simpler to calculate them separately. + int cornerInterval = (int)TolerantMath.Ceiling(firstNonNegativeLeftVal); + + // If firstNonNegativeLeftVal was an integral value, we need firstNonNegativeLeftVal+1 + // instead of Ceiling: + if (TolerantMath.AreEqual(firstNonNegativeLeftVal, cornerInterval)) + { + cornerInterval++; + } + + // If 'cornerInterval' is too big compared to 'period', we can't apply the periodic optimization. + // If we don't have at least 2 periods, we go with the basic implementation: + bool hasAtLeast2Periods = 2 * (cornerInterval + period) < destinationSize; + + ResizeKernelMap result = hasAtLeast2Periods + ? new PeriodicKernelMap( + memoryAllocator, + sourceSize, + destinationSize, + ratio, + scale, + radius, + period, + cornerInterval, + sourcePeriod) + : new ResizeKernelMap( + memoryAllocator, + sourceSize, + destinationSize, + destinationSize, + ratio, + scale, + radius); + + result.Initialize(in sampler); + + return result; + } + + /// + /// Initializes the kernel map. + /// + protected internal virtual void Initialize(in TResampler sampler) + where TResampler : struct, IResampler + { + for (int i = 0; i < this.DestinationLength; i++) + { + this.kernels[i] = this.BuildKernel(in sampler, i, i); + } + } + + /// + /// Builds a for the row (in ) + /// referencing the data at row within , + /// so the data reusable by other data rows. + /// + private ResizeKernel BuildKernel(in TResampler sampler, int destRowIndex, int dataRowIndex) + where TResampler : struct, IResampler + { + double center = ((destRowIndex + .5) * this.ratio) - .5; + double scale = this.scale; + + // Keep inside bounds. + int left = (int)TolerantMath.Ceiling(center - this.radius); + if (left < 0) + { + left = 0; + } + + int right = (int)TolerantMath.Floor(center + this.radius); + if (right > this.sourceLength - 1) + { + right = this.sourceLength - 1; + } + + ResizeKernel kernel = this.CreateKernel(dataRowIndex, left, right); + Span kernelValues = this.tempValues.AsSpan(0, kernel.Length); + ref float kernelStart = ref MemoryMarshal.GetReference(kernelValues); + float sum = 0; + + for (int j = left; j <= right; j++) + { + float value = sampler.GetValue((float)((j - center) / scale)); + sum += value; + kernelStart = value; + kernelStart = ref Unsafe.Add(ref kernelStart, 1); + } + + // Normalize, best to do it here rather than in the pixel loop later on. + if (sum > 0) + { + Numerics.Normalize(kernelValues, sum); + } + + kernel.FillOrCopyAndExpand(kernelValues); + + return kernel; + } + + /// + /// Returns a referencing values of + /// at row . + /// + private unsafe ResizeKernel CreateKernel(int dataRowIndex, int left, int right) + { + int length = right - left + 1; + this.ValidateSizesForCreateKernel(length, dataRowIndex, left, right); + + Span rowSpan = this.data.DangerousGetRowSpan(dataRowIndex); + + ref float rowReference = ref MemoryMarshal.GetReference(rowSpan); + float* rowPtr = (float*)Unsafe.AsPointer(ref rowReference); + return new ResizeKernel(left, rowPtr, length); + } + + [Conditional("DEBUG")] + private void ValidateSizesForCreateKernel(int length, int dataRowIndex, int left, int right) + { + if (length > this.data.Width) + { + throw new InvalidOperationException( + $"Error in KernelMap.CreateKernel({dataRowIndex},{left},{right}): left > this.data.Width"); + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs b/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs new file mode 100644 index 0000000..973353e --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Defines an image resizing operation with the given and dimensional parameters. + /// + public class ResizeProcessor : CloningImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The resize options. + /// The source image size. + public ResizeProcessor(ResizeOptions options, Size sourceSize) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(options.Sampler, nameof(options.Sampler)); + Guard.MustBeValueType(options.Sampler); + + (Size size, Rectangle rectangle) = ResizeHelper.CalculateTargetLocationAndBounds(sourceSize, options); + + this.Options = options; + this.DestinationWidth = size.Width; + this.DestinationHeight = size.Height; + this.DestinationRectangle = rectangle; + } + + /// + /// Gets the destination width. + /// + public int DestinationWidth { get; } + + /// + /// Gets the destination height. + /// + public int DestinationHeight { get; } + + /// + /// Gets the resize rectangle. + /// + public Rectangle DestinationRectangle { get; } + + /// + /// Gets the resize options. + /// + public ResizeOptions Options { get; } + + /// + public override ICloningImageProcessor CreatePixelSpecificCloningProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + => new ResizeProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs new file mode 100644 index 0000000..d787da6 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs @@ -0,0 +1,290 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Implements resizing of images using various resamplers. + /// + /// The pixel format. + internal class ResizeProcessor : TransformProcessor, IResamplingTransformImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly ResizeOptions options; + private readonly int destinationWidth; + private readonly int destinationHeight; + private readonly IResampler resampler; + private readonly Rectangle destinationRectangle; + private Image? destination; + private readonly Matrix4x4 transformMatrix; + + public ResizeProcessor(Configuration configuration, ResizeProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.destinationWidth = definition.DestinationWidth; + this.destinationHeight = definition.DestinationHeight; + this.destinationRectangle = definition.DestinationRectangle; + this.options = definition.Options; + this.resampler = definition.Options.Sampler; + + // Calculate the transform matrix from the resize operation to allow us + // to update any metadata that represents pixel coordinates in the source image. + Vector2 scale = new( + this.destinationRectangle.Width / (float)this.SourceRectangle.Width, + this.destinationRectangle.Height / (float)this.SourceRectangle.Height); + + this.transformMatrix = new ProjectiveTransformBuilder() + .AppendScale(scale) + .AppendTranslation((PointF)this.destinationRectangle.Location) + .BuildMatrix(sourceRectangle); + } + + /// + protected override Size GetDestinationSize() => new(this.destinationWidth, this.destinationHeight); + + /// + protected override void BeforeImageApply(Image destination) + { + this.destination = destination; + this.resampler.ApplyTransform(this); + + base.BeforeImageApply(destination); + } + + /// + protected override void OnFrameApply(ImageFrame source, ImageFrame destination) + { + // Everything happens in BeforeImageApply. + } + + /// + protected override Matrix4x4 GetTransformMatrix() => this.transformMatrix; + + public void ApplyTransform(in TResampler sampler) + where TResampler : struct, IResampler + { + Configuration configuration = this.Configuration; + Image source = this.Source; + Image destination = this.destination!; + Rectangle sourceRectangle = this.SourceRectangle; + Rectangle destinationRectangle = this.destinationRectangle; + bool compand = this.options.Compand; + bool premultiplyAlpha = this.options.PremultiplyAlpha; + TPixel fillColor = this.options.PadColor.ToPixel(); + bool shouldFill = (this.options.Mode == ResizeMode.BoxPad || this.options.Mode == ResizeMode.Pad) + && this.options.PadColor != default; + + // Handle resize dimensions identical to the original + if (source.Width == destination.Width + && source.Height == destination.Height + && sourceRectangle == destinationRectangle) + { + for (int i = 0; i < source.Frames.Count; i++) + { + ImageFrame sourceFrame = source.Frames[i]; + ImageFrame destinationFrame = destination.Frames[i]; + + // The cloned will be blank here copy all the pixel data over + sourceFrame.PixelBuffer.CopyTo(destinationFrame.PixelBuffer); + } + + return; + } + + Rectangle interest = Rectangle.Intersect(destinationRectangle, destination.Bounds); + + if (sampler is NearestNeighborResampler) + { + for (int i = 0; i < source.Frames.Count; i++) + { + ImageFrame sourceFrame = source.Frames[i]; + ImageFrame destinationFrame = destination.Frames[i]; + + if (shouldFill) + { + destinationFrame.Clear(fillColor); + } + + ApplyNNResizeFrameTransform( + configuration, + sourceFrame, + destinationFrame, + sourceRectangle, + destinationRectangle, + interest); + } + + return; + } + + // Since all image frame dimensions have to be the same we can calculate + // the kernel maps and reuse for all frames. + MemoryAllocator allocator = configuration.MemoryAllocator; + using ResizeKernelMap horizontalKernelMap = ResizeKernelMap.Calculate( + in sampler, + destinationRectangle.Width, + sourceRectangle.Width, + allocator); + + using ResizeKernelMap verticalKernelMap = ResizeKernelMap.Calculate( + in sampler, + destinationRectangle.Height, + sourceRectangle.Height, + allocator); + + for (int i = 0; i < source.Frames.Count; i++) + { + ImageFrame sourceFrame = source.Frames[i]; + ImageFrame destinationFrame = destination.Frames[i]; + + if (shouldFill) + { + destinationFrame.Clear(fillColor); + } + + ApplyResizeFrameTransform( + configuration, + sourceFrame, + destinationFrame, + horizontalKernelMap, + verticalKernelMap, + sourceRectangle, + destinationRectangle, + interest, + compand, + premultiplyAlpha); + } + } + + private static void ApplyNNResizeFrameTransform( + Configuration configuration, + ImageFrame source, + ImageFrame destination, + Rectangle sourceRectangle, + Rectangle destinationRectangle, + Rectangle interest) + { + // Scaling factors + float widthFactor = sourceRectangle.Width / (float)destinationRectangle.Width; + float heightFactor = sourceRectangle.Height / (float)destinationRectangle.Height; + + NNRowOperation operation = new( + sourceRectangle, + destinationRectangle, + interest, + widthFactor, + heightFactor, + source.PixelBuffer, + destination.PixelBuffer); + + ParallelRowIterator.IterateRows( + configuration, + interest, + in operation); + } + + private static PixelConversionModifiers GetModifiers(bool compand, bool premultiplyAlpha) + { + if (premultiplyAlpha) + { + return PixelConversionModifiers.Premultiply.ApplyCompanding(compand); + } + + return PixelConversionModifiers.None.ApplyCompanding(compand); + } + + private static void ApplyResizeFrameTransform( + Configuration configuration, + ImageFrame source, + ImageFrame destination, + ResizeKernelMap horizontalKernelMap, + ResizeKernelMap verticalKernelMap, + Rectangle sourceRectangle, + Rectangle destinationRectangle, + Rectangle interest, + bool compand, + bool premultiplyAlpha) + { + PixelAlphaRepresentation? alphaRepresentation = PixelOperations.Instance.GetPixelTypeInfo().AlphaRepresentation; + + // Premultiply only if alpha representation is unknown or Unassociated: + bool needsPremultiplication = alphaRepresentation == null || alphaRepresentation.Value == PixelAlphaRepresentation.Unassociated; + premultiplyAlpha &= needsPremultiplication; + PixelConversionModifiers conversionModifiers = GetModifiers(compand, premultiplyAlpha); + + Buffer2DRegion sourceRegion = source.PixelBuffer.GetRegion(sourceRectangle); + + // To reintroduce parallel processing, we would launch multiple workers + // for different row intervals of the image. + using ResizeWorker worker = new( + configuration, + sourceRegion, + conversionModifiers, + horizontalKernelMap, + verticalKernelMap, + interest, + destinationRectangle.Location); + worker.Initialize(); + + RowInterval workingInterval = new(interest.Top, interest.Bottom); + worker.FillDestinationPixels(workingInterval, destination.PixelBuffer); + } + + private readonly struct NNRowOperation : IRowOperation + { + private readonly Rectangle sourceBounds; + private readonly Rectangle destinationBounds; + private readonly Rectangle interest; + private readonly float widthFactor; + private readonly float heightFactor; + private readonly Buffer2D source; + private readonly Buffer2D destination; + + [MethodImpl(InliningOptions.ShortMethod)] + public NNRowOperation( + Rectangle sourceBounds, + Rectangle destinationBounds, + Rectangle interest, + float widthFactor, + float heightFactor, + Buffer2D source, + Buffer2D destination) + { + this.sourceBounds = sourceBounds; + this.destinationBounds = destinationBounds; + this.interest = interest; + this.widthFactor = widthFactor; + this.heightFactor = heightFactor; + this.source = source; + this.destination = destination; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y) + { + int sourceX = this.sourceBounds.X; + int sourceY = this.sourceBounds.Y; + int destOriginX = this.destinationBounds.X; + int destOriginY = this.destinationBounds.Y; + int destLeft = this.interest.Left; + int destRight = this.interest.Right; + + // Y coordinates of source points + Span sourceRow = this.source.DangerousGetRowSpan((int)(((y - destOriginY) * this.heightFactor) + sourceY)); + Span targetRow = this.destination.DangerousGetRowSpan(y); + + for (int x = destLeft; x < destRight; x++) + { + // X coordinates of source points + targetRow[x] = sourceRow[(int)(((x - destOriginX) * this.widthFactor) + sourceX)]; + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs new file mode 100644 index 0000000..f78c188 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -0,0 +1,253 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Implements the resize algorithm using a sliding window of size + /// maximized by . + /// The height of the window is a multiple of the vertical kernel's maximum diameter. + /// When sliding the window, the contents of the bottom window band are copied to the new top band. + /// For more details, and visual explanation, see "ResizeWorker.pptx". + /// + internal sealed class ResizeWorker : IDisposable + where TPixel : unmanaged, IPixel + { + private readonly Buffer2D transposedFirstPassBuffer; + + private readonly Configuration configuration; + + private readonly PixelConversionModifiers conversionModifiers; + + private readonly ResizeKernelMap horizontalKernelMap; + + private readonly Buffer2DRegion source; + + private readonly Rectangle sourceRectangle; + + private readonly IMemoryOwner tempRowBuffer; + + private readonly IMemoryOwner tempColumnBuffer; + + private readonly ResizeKernelMap verticalKernelMap; + + private readonly Rectangle targetWorkingRect; + + private readonly Point targetOrigin; + + private readonly int windowBandHeight; + + private readonly int workerHeight; + + private RowInterval currentWindow; + + public ResizeWorker( + Configuration configuration, + Buffer2DRegion source, + PixelConversionModifiers conversionModifiers, + ResizeKernelMap horizontalKernelMap, + ResizeKernelMap verticalKernelMap, + Rectangle targetWorkingRect, + Point targetOrigin) + { + this.configuration = configuration; + this.source = source; + this.sourceRectangle = source.Bounds; + this.conversionModifiers = conversionModifiers; + this.horizontalKernelMap = horizontalKernelMap; + this.verticalKernelMap = verticalKernelMap; + this.targetWorkingRect = targetWorkingRect; + this.targetOrigin = targetOrigin; + + this.windowBandHeight = verticalKernelMap.MaxDiameter; + + // We need to make sure the working buffer is contiguous: + int workingBufferLimitHintInBytes = Math.Min( + configuration.WorkingBufferSizeHintInBytes, + configuration.MemoryAllocator.GetBufferCapacityInBytes()); + + int numberOfWindowBands = ResizeHelper.CalculateResizeWorkerHeightInWindowBands( + this.windowBandHeight, + targetWorkingRect.Width, + workingBufferLimitHintInBytes); + + this.workerHeight = Math.Min(this.sourceRectangle.Height, numberOfWindowBands * this.windowBandHeight); + + this.transposedFirstPassBuffer = configuration.MemoryAllocator.Allocate2D( + this.workerHeight, + targetWorkingRect.Width, + preferContiguosImageBuffers: true, + options: AllocationOptions.Clean); + + this.tempRowBuffer = configuration.MemoryAllocator.Allocate(this.sourceRectangle.Width); + this.tempColumnBuffer = configuration.MemoryAllocator.Allocate(targetWorkingRect.Width); + + this.currentWindow = new RowInterval(0, this.workerHeight); + } + + public void Dispose() + { + this.transposedFirstPassBuffer.Dispose(); + this.tempRowBuffer.Dispose(); + this.tempColumnBuffer.Dispose(); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public Span GetColumnSpan(int x, int startY) + => this.transposedFirstPassBuffer.DangerousGetRowSpan(x)[(startY - this.currentWindow.Min)..]; + + public void Initialize() + => this.CalculateFirstPassValues(this.currentWindow); + + public void FillDestinationPixels(RowInterval rowInterval, Buffer2D destination) + { + Span tempColSpan = this.tempColumnBuffer.GetSpan(); + + // When creating transposedFirstPassBuffer, we made sure it's contiguous. + Span transposedFirstPassBufferSpan = this.transposedFirstPassBuffer.DangerousGetSingleSpan(); + + int left = this.targetWorkingRect.Left; + int width = this.targetWorkingRect.Width; + nuint widthCount = (uint)width; + + // Normalize destination-space Y to kernel indices using uint arithmetic. + // This relies on the contract that processing addresses are normalized (cropping/padding handled by targetOrigin). + int targetOriginY = this.targetOrigin.Y; + + // Hoist invariant calculations outside the loop. + int currentWindowMax = this.currentWindow.Max; + int currentWindowMin = this.currentWindow.Min; + nuint workerHeight = (uint)this.workerHeight; + nuint workerHeight2 = workerHeight * 2; + + // Ref-walk the kernel table to avoid bounds checks in the tight loop. + ReadOnlySpan vKernels = this.verticalKernelMap.GetKernelSpan(); + ref ResizeKernel vKernelBase = ref MemoryMarshal.GetReference(vKernels); + + ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempColSpan); + + for (int y = rowInterval.Min; y < rowInterval.Max; y++) + { + // Normalize destination-space Y to an unsigned kernel index. + uint vIdx = (uint)(y - targetOriginY); + ref ResizeKernel kernel = ref Unsafe.Add(ref vKernelBase, (nint)vIdx); + + // Slide the working window when the kernel would read beyond the current cached region. + int kernelEnd = kernel.StartIndex + kernel.Length; + while (kernelEnd > currentWindowMax) + { + this.Slide(); + currentWindowMax = this.currentWindow.Max; + currentWindowMin = this.currentWindow.Min; + } + + int top = kernel.StartIndex - currentWindowMin; + ref Vector4 colRef0 = ref transposedFirstPassBufferSpan[top]; + + // Unroll by 2 and advance column refs via arithmetic to reduce inner-loop overhead. + nuint i = 0; + for (; i + 1 < widthCount; i += 2) + { + ref Vector4 colRef1 = ref Unsafe.Add(ref colRef0, workerHeight); + + Unsafe.Add(ref tempRowBase, i) = kernel.ConvolveCore(ref colRef0); + Unsafe.Add(ref tempRowBase, i + 1) = kernel.ConvolveCore(ref colRef1); + + colRef0 = ref Unsafe.Add(ref colRef0, workerHeight2); + } + + if (i < widthCount) + { + Unsafe.Add(ref tempRowBase, i) = kernel.ConvolveCore(ref colRef0); + } + + Span targetRowSpan = destination.DangerousGetRowSpan(y).Slice(left, width); + + PixelOperations.Instance.FromVector4Destructive(this.configuration, tempColSpan, targetRowSpan, this.conversionModifiers); + } + } + + private void Slide() + { + int minY = this.currentWindow.Max - this.windowBandHeight; + int maxY = Math.Min(minY + this.workerHeight, this.sourceRectangle.Height); + + // Copy previous bottom band to the new top: + // (rows <--> columns, because the buffer is transposed) + this.transposedFirstPassBuffer.DangerousCopyColumns( + this.workerHeight - this.windowBandHeight, + 0, + this.windowBandHeight); + + this.currentWindow = new RowInterval(minY, maxY); + + // Calculate the remainder: + this.CalculateFirstPassValues(this.currentWindow.Slice(this.windowBandHeight)); + } + + private void CalculateFirstPassValues(RowInterval calculationInterval) + { + Span tempRowSpan = this.tempRowBuffer.GetSpan(); + Span transposedFirstPassBufferSpan = this.transposedFirstPassBuffer.DangerousGetSingleSpan(); + + nuint left = (uint)this.targetWorkingRect.Left; + nuint right = (uint)this.targetWorkingRect.Right; + nuint widthCount = right - left; + + // Normalize destination-space X to kernel indices using uint arithmetic. + // This relies on the contract that processing addresses are normalized (cropping/padding handled by targetOrigin). + nuint targetOriginX = (uint)this.targetOrigin.X; + + nuint workerHeight = (uint)this.workerHeight; + int currentWindowMin = this.currentWindow.Min; + + // Ref-walk the kernel table to avoid bounds checks in the tight loop. + ReadOnlySpan hKernels = this.horizontalKernelMap.GetKernelSpan(); + ref ResizeKernel hKernelBase = ref MemoryMarshal.GetReference(hKernels); + + for (int y = calculationInterval.Min; y < calculationInterval.Max; y++) + { + Span sourceRow = this.source.DangerousGetRowSpan(y); + + PixelOperations.Instance.ToVector4( + this.configuration, + sourceRow, + tempRowSpan, + this.conversionModifiers); + + ref Vector4 firstPassBaseRef = ref transposedFirstPassBufferSpan[y - currentWindowMin]; + + // Unroll by 2 to reduce loop and kernel lookup overhead. + nuint x = left; + nuint z = 0; + + for (; z + 1 < widthCount; x += 2, z += 2) + { + nuint hIdx0 = (uint)(x - targetOriginX); + nuint hIdx1 = (uint)((x + 1) - targetOriginX); + + ref ResizeKernel kernel0 = ref Unsafe.Add(ref hKernelBase, (nint)hIdx0); + ref ResizeKernel kernel1 = ref Unsafe.Add(ref hKernelBase, (nint)hIdx1); + + Unsafe.Add(ref firstPassBaseRef, z * workerHeight) = kernel0.Convolve(tempRowSpan); + Unsafe.Add(ref firstPassBaseRef, (z + 1) * workerHeight) = kernel1.Convolve(tempRowSpan); + } + + if (z < widthCount) + { + nuint hIdx = (uint)(x - targetOriginX); + ref ResizeKernel kernel = ref Unsafe.Add(ref hKernelBase, (nint)hIdx); + + Unsafe.Add(ref firstPassBaseRef, z * workerHeight) = kernel.Convolve(tempRowSpan); + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.pptx b/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.pptx new file mode 100644 index 0000000..2489591 Binary files /dev/null and b/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.pptx differ diff --git a/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler,TPixel}.cs b/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler,TPixel}.cs new file mode 100644 index 0000000..977aaff --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler,TPixel}.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + internal class SwizzleProcessor : TransformProcessor + where TSwizzler : struct, ISwizzler + where TPixel : unmanaged, IPixel + { + private readonly TSwizzler swizzler; + private readonly Size destinationSize; + private readonly Matrix4x4 transformMatrix; + + public SwizzleProcessor(Configuration configuration, TSwizzler swizzler, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.swizzler = swizzler; + this.destinationSize = swizzler.DestinationSize; + + // Calculate the transform matrix from the swizzle operation to allow us + // to update any metadata that represents pixel coordinates in the source image. + this.transformMatrix = new ProjectiveTransformBuilder() + .AppendMatrix(TransformUtilities.GetSwizzlerMatrix(swizzler, sourceRectangle)) + .BuildMatrix(sourceRectangle); + } + + /// + protected override Size GetDestinationSize() => this.destinationSize; + + /// + protected override Matrix4x4 GetTransformMatrix() => this.transformMatrix; + + /// + protected override void OnFrameApply(ImageFrame source, ImageFrame destination) + { + Point p = default; + Point newPoint; + Buffer2D sourceBuffer = source.PixelBuffer; + for (p.Y = 0; p.Y < source.Height; p.Y++) + { + Span rowSpan = sourceBuffer.DangerousGetRowSpan(p.Y); + for (p.X = 0; p.X < source.Width; p.X++) + { + newPoint = this.swizzler.Transform(p); + destination[newPoint.X, newPoint.Y] = rowSpan[p.X]; + } + } + } + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler}.cs b/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler}.cs new file mode 100644 index 0000000..e289c59 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler}.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Defines a swizzle operation on an image. + /// + /// The swizzle function type. + public sealed class SwizzleProcessor : IImageProcessor + where TSwizzler : struct, ISwizzler + { + /// + /// Initializes a new instance of the class. + /// + /// The swizzler operation. + public SwizzleProcessor(TSwizzler swizzler) => this.Swizzler = swizzler; + + /// + /// Gets the swizzler operation. + /// + public TSwizzler Swizzler { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new SwizzleProcessor(configuration, this.Swizzler, source, sourceRectangle); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs b/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs new file mode 100644 index 0000000..7806f22 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// The base class for all transform processors. Any processor that changes the dimensions of the image should inherit from this. + /// + /// The pixel format. + internal abstract class TransformProcessor : CloningImageProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + protected TransformProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + } + + /// + /// Gets the transform matrix that will be applied to the image. + /// + /// + /// The that represents the transformation to be applied to the image. + /// + protected abstract Matrix4x4 GetTransformMatrix(); + + /// + protected override void AfterFrameApply(ImageFrame source, ImageFrame destination) + => destination.Metadata.AfterFrameApply(source, destination, this.GetTransformMatrix()); + + /// + protected override void AfterImageApply(Image destination) + => destination.Metadata.AfterImageApply(destination, this.GetTransformMatrix()); + } +} diff --git a/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs b/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs new file mode 100644 index 0000000..9f68bc8 --- /dev/null +++ b/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs @@ -0,0 +1,685 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Processing.Processors.Transforms.Linear; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + /// + /// Contains utility methods for working with transforms. + /// + internal static class TransformUtilities + { + /// + /// Returns a value that indicates whether the specified matrix is degenerate + /// containing one or more values equivalent to or a + /// zero determinant and therefore cannot be used for linear transforms. + /// + /// The transform matrix. + public static bool IsDegenerate(Matrix3x2 matrix) + => IsNaN(matrix) || IsZero(matrix.GetDeterminant()); + + /// + /// Returns a value that indicates whether the specified matrix is degenerate + /// containing one or more values equivalent to or a + /// zero determinant and therefore cannot be used for linear transforms. + /// + /// The transform matrix. + public static bool IsDegenerate(Matrix4x4 matrix) + => IsNaN(matrix) || IsZero(matrix.GetDeterminant()); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsZero(float a) + => a > -Constants.EpsilonSquared && a < Constants.EpsilonSquared; + + /// + /// Returns a value that indicates whether the specified matrix contains any values + /// that are not a number . + /// + /// The transform matrix. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNaN(Matrix3x2 matrix) + => float.IsNaN(matrix.M11) || float.IsNaN(matrix.M12) + || float.IsNaN(matrix.M21) || float.IsNaN(matrix.M22) + || float.IsNaN(matrix.M31) || float.IsNaN(matrix.M32); + + /// + /// Returns a value that indicates whether the specified matrix contains any values + /// that are not a number . + /// + /// The transform matrix. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNaN(Matrix4x4 matrix) + => float.IsNaN(matrix.M11) || float.IsNaN(matrix.M12) || float.IsNaN(matrix.M13) || float.IsNaN(matrix.M14) + || float.IsNaN(matrix.M21) || float.IsNaN(matrix.M22) || float.IsNaN(matrix.M23) || float.IsNaN(matrix.M24) + || float.IsNaN(matrix.M31) || float.IsNaN(matrix.M32) || float.IsNaN(matrix.M33) || float.IsNaN(matrix.M34) + || float.IsNaN(matrix.M41) || float.IsNaN(matrix.M42) || float.IsNaN(matrix.M43) || float.IsNaN(matrix.M44); + + /// + /// Applies the projective transform against the given coordinates flattened into the 2D space. + /// + /// The "x" vector coordinate. + /// The "y" vector coordinate. + /// The transform matrix. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 ProjectiveTransform2D(float x, float y, Matrix4x4 matrix) + { + // Transforms the 2D point (x, y) as the homogeneous coordinate (x, y, 0, 1) and + // performs the perspective divide (X/W, Y/W) to project back into Cartesian 2D space. + // + // For affine matrices (M14=0, M24=0, M34=0, M44=1) W is always 1 and the divide + // is a no-op, producing the same result as Vector2.Transform(v, Matrix4x4).AsVector2() + // (the approach used by .NET 10+). + // + // For projective matrices (taper, quad distortion) W varies per point and the divide + // is essential for correct perspective mapping. W <= 0 means the point has crossed the + // vanishing line of the projection; clamping to epsilon avoids division by zero or + // negative values that would flip/mirror the output. + const float epsilon = 0.0000001F; + Vector4 v4 = Vector4.Transform(new Vector4(x, y, 0, 1F), matrix); + return new Vector2(v4.X, v4.Y) / MathF.Max(v4.W, epsilon); + } + + /// + /// Creates a centered rotation transform matrix using the given rotation in degrees and the original source size. + /// + /// The amount of rotation, in degrees. + /// The source image size. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size) + => CreateRotationTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degrees), size); + + /// + /// Creates a centered rotation transform matrix using the given rotation in radians and the original source size. + /// + /// The amount of rotation, in radians. + /// The source image size. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size) + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size); + + /// + /// Creates a centered skew transform matrix from the give angles in degrees and the original source size. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The source image size. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size) + => CreateSkewTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), size); + + /// + /// Creates a centered skew transform matrix from the give angles in radians and the original source size. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The source image size. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size) + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size); + + /// + /// Gets the centered transform matrix based upon the source rectangle. + /// + /// The transformation matrix. + /// The source image size. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size size) + { + // 1) Unbounded size. + SizeF ts = GetRawTransformedSize(matrix, size); + + // 2) Invert the content transform for screen->world. + Matrix3x2.Invert(matrix, out Matrix3x2 inv); + + // 3) Translate target (canvas) so its center is at the origin, + // translate source so its center is at the origin, then undo the content transform. + Matrix3x2 toTarget = Matrix3x2.CreateTranslation(new Vector2(-ts.Width, -ts.Height) * 0.5f); + Matrix3x2 toSource = Matrix3x2.CreateTranslation(new Vector2(size.Width, size.Height) * 0.5f); + + // 4) World->screen. + Matrix3x2.Invert(toTarget * inv * toSource, out Matrix3x2 centered); + + return centered; + } + + /// + /// Creates a matrix that performs a tapering projective transform. + /// + /// + /// The rectangular size of the image being transformed. + /// An enumeration that indicates the side of the rectangle that tapers. + /// An enumeration that indicates on which corners to taper the rectangle. + /// The amount to taper. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide side, TaperCorner corner, float fraction) + { + Matrix4x4 matrix = Matrix4x4.Identity; + + /* + * SkMatrix is laid out in the following manner: + * + * [ ScaleX SkewY Persp0 ] + * [ SkewX ScaleY Persp1 ] + * [ TransX TransY Persp2 ] + * + * When converting from Matrix4x4 to SkMatrix, the third row and + * column is dropped. When converting from SkMatrix to Matrix4x4 + * the third row and column remain as identity: + * + * [ a b c ] [ a b 0 c ] + * [ d e f ] -> [ d e 0 f ] + * [ g h i ] [ 0 0 1 0 ] + * [ g h 0 i ] + */ + switch (side) + { + case TaperSide.Left: + matrix.M11 = fraction; + matrix.M22 = fraction; + matrix.M14 = (fraction - 1) / size.Width; + + switch (corner) + { + case TaperCorner.RightOrBottom: + break; + + case TaperCorner.LeftOrTop: + matrix.M12 = size.Height * matrix.M14; + matrix.M42 = size.Height * (1 - fraction); + break; + + case TaperCorner.Both: + matrix.M12 = size.Height * .5F * matrix.M14; + matrix.M42 = size.Height * (1 - fraction) / 2; + break; + } + + break; + + case TaperSide.Top: + matrix.M11 = fraction; + matrix.M22 = fraction; + matrix.M24 = (fraction - 1) / size.Height; + + switch (corner) + { + case TaperCorner.RightOrBottom: + break; + + case TaperCorner.LeftOrTop: + matrix.M21 = size.Width * matrix.M24; + matrix.M41 = size.Width * (1 - fraction); + break; + + case TaperCorner.Both: + matrix.M21 = size.Width * .5F * matrix.M24; + matrix.M41 = size.Width * (1 - fraction) * .5F; + break; + } + + break; + + case TaperSide.Right: + matrix.M11 = 1 / fraction; + matrix.M14 = (1 - fraction) / (size.Width * fraction); + + switch (corner) + { + case TaperCorner.RightOrBottom: + break; + + case TaperCorner.LeftOrTop: + matrix.M12 = size.Height * matrix.M14; + break; + + case TaperCorner.Both: + matrix.M12 = size.Height * .5F * matrix.M14; + break; + } + + break; + + case TaperSide.Bottom: + matrix.M22 = 1 / fraction; + matrix.M24 = (1 - fraction) / (size.Height * fraction); + + switch (corner) + { + case TaperCorner.RightOrBottom: + break; + + case TaperCorner.LeftOrTop: + matrix.M21 = size.Width * matrix.M24; + break; + + case TaperCorner.Both: + matrix.M21 = size.Width * .5F * matrix.M24; + break; + } + + break; + } + + return matrix; + } + + /// + /// Computes the projection matrix for a quad distortion transformation. + /// + /// The source rectangle. + /// The top-left point of the distorted quad. + /// The top-right point of the distorted quad. + /// The bottom-right point of the distorted quad. + /// The bottom-left point of the distorted quad. + /// The computed projection matrix for the quad distortion. + /// + /// This method is based on the algorithm described in the following article: + /// + /// + public static Matrix4x4 CreateQuadDistortionMatrix( + Rectangle rectangle, + PointF topLeft, + PointF topRight, + PointF bottomRight, + PointF bottomLeft) + { + PointF p1 = new(rectangle.X, rectangle.Y); + PointF p2 = new(rectangle.X + rectangle.Width, rectangle.Y); + PointF p3 = new(rectangle.X + rectangle.Width, rectangle.Y + rectangle.Height); + PointF p4 = new(rectangle.X, rectangle.Y + rectangle.Height); + + PointF q1 = topLeft; + PointF q2 = topRight; + PointF q3 = bottomRight; + PointF q4 = bottomLeft; + + double[][] matrixData = + [ + [p1.X, p1.Y, 1, 0, 0, 0, -p1.X * q1.X, -p1.Y * q1.X], + [0, 0, 0, p1.X, p1.Y, 1, -p1.X * q1.Y, -p1.Y * q1.Y], + [p2.X, p2.Y, 1, 0, 0, 0, -p2.X * q2.X, -p2.Y * q2.X], + [0, 0, 0, p2.X, p2.Y, 1, -p2.X * q2.Y, -p2.Y * q2.Y], + [p3.X, p3.Y, 1, 0, 0, 0, -p3.X * q3.X, -p3.Y * q3.X], + [0, 0, 0, p3.X, p3.Y, 1, -p3.X * q3.Y, -p3.Y * q3.Y], + [p4.X, p4.Y, 1, 0, 0, 0, -p4.X * q4.X, -p4.Y * q4.X], + [0, 0, 0, p4.X, p4.Y, 1, -p4.X * q4.Y, -p4.Y * q4.Y], + ]; + + double[] b = + [ + q1.X, + q1.Y, + q2.X, + q2.Y, + q3.X, + q3.Y, + q4.X, + q4.Y, + ]; + + GaussianEliminationSolver.Solve(matrixData, b); + +#pragma warning disable SA1117 + Matrix4x4 projectionMatrix = new( + (float)b[0], (float)b[3], 0, (float)b[6], + (float)b[1], (float)b[4], 0, (float)b[7], + 0, 0, 1, 0, + (float)b[2], (float)b[5], 0, 1); +#pragma warning restore SA1117 + + return projectionMatrix; + } + + /// + /// Calculates the size of a destination canvas large enough to contain + /// the fully transformed source content, including any translation offsets. + /// + /// The transformation matrix. + /// The original source size. + /// + /// A representing the dimensions of the destination + /// canvas required to fully contain the transformed source, including + /// any positive or negative translation offsets. + /// + /// + /// + /// This method ensures that the transformed content remains fully visible + /// on the destination canvas by expanding its size to include translations + /// in all directions. + /// + /// + /// It behaves identically to calling + /// with + /// preserveCanvas set to . + /// + /// + /// The resulting canvas size represents the total area required to display + /// the transformed image without clipping, not merely the geometric bounds + /// of the transformed source. + /// + /// + public static Size GetTransformedCanvasSize(Matrix3x2 matrix, Size size) + => Size.Ceiling(GetTransformedSize(matrix, size, true)); + + /// + /// Calculates the size of a destination canvas large enough to contain + /// the fully transformed source content, including any translation offsets. + /// + /// The transformation matrix. + /// The original source size. + /// + /// A representing the dimensions of the destination + /// canvas required to fully contain the transformed source, including + /// any positive or negative translation offsets. + /// + /// + /// + /// This method ensures that the transformed content remains fully visible + /// on the destination canvas by expanding its size to include translations + /// in all directions. + /// + /// + /// It behaves identically to calling + /// with + /// preserveCanvas set to . + /// + /// + /// The resulting canvas size represents the total area required to display + /// the transformed image without clipping, not merely the geometric bounds + /// of the transformed source. + /// + /// + public static Size GetTransformedCanvasSize(Matrix4x4 matrix, Size size) + => Size.Ceiling(GetTransformedSize(matrix, size, true)); + + /// + /// Returns the size relative to the source for the given transformation matrix. + /// + /// The transformation matrix. + /// The original source size. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SizeF GetRawTransformedSize(Matrix4x4 matrix, Size size) + => GetTransformedSize(matrix, size, false); + + /// + /// Returns the size of the transformed source. When is true, + /// the size is expanded to include translation so the full moved content remains visible. + /// + /// The transformation matrix. + /// The original source size. + /// + /// If , expand the size to account for translation (left/up as well as right/down). + /// If , return only the transformed span without translation expansion. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static SizeF GetTransformedSize(Matrix4x4 matrix, Size size, bool preserveCanvas) + { + Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); + + if (matrix.IsIdentity || matrix.Equals(default)) + { + return size; + } + + if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size), matrix, out RectangleF bounds)) + { + return preserveCanvas ? GetPreserveCanvasSize(bounds) : bounds.Size; + } + + return size; + } + + /// + /// Attempts to derive a 4x4 projective transform matrix that approximates the behavior of an . + /// + /// + /// The swizzler to use for the transformation. + /// + /// + /// The source rectangle that defines the area to be transformed. + /// + /// + /// The type of the swizzler, which must implement . + /// + public static Matrix4x4 GetSwizzlerMatrix(T swizzler, Rectangle sourceRectangle) + where T : struct, ISwizzler + => CreateQuadDistortionMatrix( + sourceRectangle, + swizzler.Transform(new Point(sourceRectangle.Left, sourceRectangle.Top)), + swizzler.Transform(new Point(sourceRectangle.Right, sourceRectangle.Top)), + swizzler.Transform(new Point(sourceRectangle.Right, sourceRectangle.Bottom)), + swizzler.Transform(new Point(sourceRectangle.Left, sourceRectangle.Bottom))); + + /// + /// Returns the size relative to the source for the given transformation matrix. + /// + /// The transformation matrix. + /// The original source size. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SizeF GetRawTransformedSize(Matrix3x2 matrix, Size size) + => GetTransformedSize(matrix, size, false); + + /// + /// Returns the size of the transformed source. When is true, + /// the size is expanded to include translation so the full moved content remains visible. + /// + /// The transformation matrix. + /// The original source size. + /// + /// If , expand the size to account for translation (left/up as well as right/down). + /// If , return only the transformed span without translation expansion. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static SizeF GetTransformedSize(Matrix3x2 matrix, Size size, bool preserveCanvas) + { + Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); + + if (matrix.IsIdentity || matrix.Equals(default)) + { + return size; + } + + if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size), matrix, out RectangleF bounds)) + { + return preserveCanvas ? GetPreserveCanvasSize(bounds) : bounds.Size; + } + + return size; + } + + /// + /// Returns the rectangle relative to the source for the given transformation matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// The resulting bounding rectangle. + /// + /// if the transformation was successful; otherwise, . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix3x2 matrix, out RectangleF bounds) + { + if (matrix.IsIdentity || rectangle.Equals(default)) + { + bounds = default; + return false; + } + + Vector2 tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix); + Vector2 tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); + Vector2 bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); + Vector2 br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix); + + bounds = GetBoundingRectangle(tl, tr, bl, br); + return true; + } + + /// + /// Returns the rectangle relative to the source for the given transformation matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// The resulting bounding rectangle. + /// + /// if the transformation was successful; otherwise, . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix4x4 matrix, out RectangleF bounds) + { + if (matrix.IsIdentity || rectangle.Equals(default)) + { + bounds = default; + return false; + } + + Vector2 tl = ProjectiveTransform2D(rectangle.Left, rectangle.Top, matrix); + Vector2 tr = ProjectiveTransform2D(rectangle.Right, rectangle.Top, matrix); + Vector2 bl = ProjectiveTransform2D(rectangle.Left, rectangle.Bottom, matrix); + Vector2 br = ProjectiveTransform2D(rectangle.Right, rectangle.Bottom, matrix); + + bounds = GetBoundingRectangle(tl, tr, bl, br); + return true; + } + + /// + /// Calculates the size of a destination canvas large enough to contain the full + /// transformed content of a source rectangle while preserving any translation offsets. + /// + /// + /// The representing the transformed bounds of the source content + /// in destination (output) space. + /// + /// + /// A that describes the canvas dimensions required to fully + /// contain the transformed content while accounting for any positive or negative translation. + /// + /// + /// + /// This method expands the output canvas to ensure that translated content remains visible. + /// + /// + /// If the transformation produces a positive translation, the method extends the canvas + /// on the positive side (right or bottom). + /// If the transformation produces a negative translation (the content moves left or up), + /// the method extends the canvas on the negative side to include that offset. + /// + /// + /// The result is equivalent to taking the union of: + /// + /// + /// The original, untransformed rectangle at the origin [0..Width] × [0..Height]. + /// + /// + /// The translated rectangle defined by . + /// + /// + /// This ensures the entire translated image fits within the resulting canvas, + /// without trimming any portion caused by translation. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static SizeF GetPreserveCanvasSize(RectangleF rectangle) + { + // Compute the required height. + // If the top is negative, expand upward by that amount (rectangle.Bottom already includes height). + // Otherwise, take the larger of the transformed height or the bottom offset. + float height = rectangle.Top < 0 + ? rectangle.Bottom + : MathF.Max(rectangle.Height, rectangle.Bottom); + + // Compute the required width. + // If the left is negative, expand leftward by that amount (rectangle.Right already includes width). + // Otherwise, take the larger of the transformed width or the right offset. + float width = rectangle.Left < 0 + ? rectangle.Right + : MathF.Max(rectangle.Width, rectangle.Right); + + // Guard: if translation exceeds or cancels dimensions, + // ensure non-zero positive size using the base rectangle dimensions. + if (height <= 0) + { + height = rectangle.Height; + } + + if (width <= 0) + { + width = rectangle.Width; + } + + // Return the final size that preserves the full visible region of the transformed content. + return new SizeF(width, height); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static RectangleF GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br) + { + float left = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X))); + float top = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y))); + float right = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X))); + float bottom = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y))); + + return RectangleF.FromLTRB(left, top, right, bottom); + } + + /// + /// Normalizes an affine 2D matrix so that it operates in pixel space. + /// Applies the row-vector conjugation T(+0.5,+0.5) * M * T(-0.5,-0.5) + /// to align the transform with pixel centers. + /// + /// The affine matrix. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix3x2 NormalizeToPixel(Matrix3x2 matrix) + { + const float dx = 0.5f, dy = 0.5f; + + matrix.M31 += (-dx) + ((dx * matrix.M11) + (dy * matrix.M21)); + matrix.M32 += (-dy) + ((dx * matrix.M12) + (dy * matrix.M22)); + return matrix; + } + + /// + /// Normalizes a projective 4×4 matrix so that it operates in pixel space. + /// Applies the row-vector conjugation T(+0.5,+0.5,0) * M * T(-0.5,-0.5,0) + /// to align the transform with pixel centers. + /// + /// The projective matrix. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix4x4 NormalizeToPixel(Matrix4x4 matrix) + { + const float dx = 0.5f, dy = 0.5f; + + // Fast path: affine (no perspective) + if (matrix.M14 == 0f && matrix.M24 == 0f && matrix.M34 == 0f && matrix.M44 == 1f) + { + // t' = t + (-d + d·L) + matrix.M41 += (-dx) + ((dx * matrix.M11) + (dy * matrix.M21)); + matrix.M42 += (-dy) + ((dx * matrix.M12) + (dy * matrix.M22)); + return matrix; + } + + Matrix4x4 tPos = Matrix4x4.Identity; + tPos.M41 = dx; + tPos.M42 = dy; + Matrix4x4 tNeg = Matrix4x4.Identity; + tNeg.M41 = -dx; + tNeg.M42 = -dy; + return tPos * matrix * tNeg; + } + } +} diff --git a/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/ImageSharp/Processing/ProjectiveTransformBuilder.cs new file mode 100644 index 0000000..af4d150 --- /dev/null +++ b/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -0,0 +1,431 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Processing { + /// + /// A helper class for constructing instances for use in projective transforms. + /// + public class ProjectiveTransformBuilder + { + private readonly List> transformMatrixFactories = []; + + /// + /// Initializes a new instance of the class. + /// + public ProjectiveTransformBuilder() + { + } + + /// + /// Prepends a matrix that performs a tapering projective transform. + /// + /// An enumeration that indicates the side of the rectangle that tapers. + /// An enumeration that indicates on which corners to taper the rectangle. + /// The amount to taper. + /// The . + public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corner, float fraction) + => this.Prepend(size => TransformUtilities.CreateTaperMatrix(size, side, corner, fraction)); + + /// + /// Appends a matrix that performs a tapering projective transform. + /// + /// An enumeration that indicates the side of the rectangle that tapers. + /// An enumeration that indicates on which corners to taper the rectangle. + /// The amount to taper. + /// The . + public ProjectiveTransformBuilder AppendTaper(TaperSide side, TaperCorner corner, float fraction) + => this.Append(size => TransformUtilities.CreateTaperMatrix(size, side, corner, fraction)); + + /// + /// Prepends a centered rotation matrix using the given rotation in degrees. + /// + /// The amount of rotation, in degrees. + /// The . + public ProjectiveTransformBuilder PrependRotationDegrees(float degrees) + => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); + + /// + /// Prepends a centered rotation matrix using the given rotation in radians. + /// + /// The amount of rotation, in radians. + /// The . + public ProjectiveTransformBuilder PrependRotationRadians(float radians) + => this.Prepend(size => new Matrix4x4(TransformUtilities.CreateRotationTransformMatrixRadians(radians, size))); + + /// + /// Prepends a centered rotation matrix using the given rotation in degrees at the given origin. + /// + /// The amount of rotation, in radians. + /// The rotation origin point. + /// The . + internal ProjectiveTransformBuilder PrependRotationDegrees(float degrees, Vector2 origin) + => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin); + + /// + /// Prepends a centered rotation matrix using the given rotation in radians at the given origin. + /// + /// The amount of rotation, in radians. + /// The rotation origin point. + /// The . + internal ProjectiveTransformBuilder PrependRotationRadians(float radians, Vector2 origin) + => this.PrependMatrix( + Matrix4x4.CreateRotationZ(radians, new Vector3(origin, 0))); + + /// + /// Appends a centered rotation matrix using the given rotation in degrees. + /// + /// The amount of rotation, in degrees. + /// The . + public ProjectiveTransformBuilder AppendRotationDegrees(float degrees) + => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); + + /// + /// Appends a centered rotation matrix using the given rotation in radians. + /// + /// The amount of rotation, in radians. + /// The . + public ProjectiveTransformBuilder AppendRotationRadians(float radians) + => this.Append(size => new Matrix4x4(TransformUtilities.CreateRotationTransformMatrixRadians(radians, size))); + + /// + /// Appends a centered rotation matrix using the given rotation in degrees at the given origin. + /// + /// The amount of rotation, in radians. + /// The rotation origin point. + /// The . + internal ProjectiveTransformBuilder AppendRotationDegrees(float degrees, Vector2 origin) + => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin); + + /// + /// Appends a centered rotation matrix using the given rotation in radians at the given origin. + /// + /// The amount of rotation, in radians. + /// The rotation origin point. + /// The . + internal ProjectiveTransformBuilder AppendRotationRadians(float radians, Vector2 origin) + => this.AppendMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(origin, 0))); + + /// + /// Prepends a scale matrix from the given uniform scale. + /// + /// The uniform scale. + /// The . + public ProjectiveTransformBuilder PrependScale(float scale) + => this.PrependMatrix(Matrix4x4.CreateScale(scale)); + + /// + /// Prepends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public ProjectiveTransformBuilder PrependScale(SizeF scale) + => this.PrependScale((Vector2)scale); + + /// + /// Prepends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public ProjectiveTransformBuilder PrependScale(Vector2 scales) + => this.PrependMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1F))); + + /// + /// Appends a scale matrix from the given uniform scale. + /// + /// The uniform scale. + /// The . + public ProjectiveTransformBuilder AppendScale(float scale) + => this.AppendMatrix(Matrix4x4.CreateScale(scale)); + + /// + /// Appends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public ProjectiveTransformBuilder AppendScale(SizeF scales) + => this.AppendScale((Vector2)scales); + + /// + /// Appends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public ProjectiveTransformBuilder AppendScale(Vector2 scales) + => this.AppendMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1F))); + + /// + /// Prepends a centered skew matrix from the give angles in degrees. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The . + internal ProjectiveTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) + => this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); + + /// + /// Prepends a centered skew matrix from the give angles in radians. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The . + public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY) + => this.Prepend(size => new Matrix4x4(TransformUtilities.CreateSkewTransformMatrixRadians(radiansX, radiansY, size))); + + /// + /// Prepends a skew matrix using the given angles in degrees at the given origin. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The skew origin point. + /// The . + public ProjectiveTransformBuilder PrependSkewDegrees(float degreesX, float degreesY, Vector2 origin) + => this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin); + + /// + /// Prepends a skew matrix using the given angles in radians at the given origin. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The skew origin point. + /// The . + public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY, Vector2 origin) + => this.PrependMatrix(new Matrix4x4(Matrix3x2.CreateSkew(radiansX, radiansY, origin))); + + /// + /// Appends a centered skew matrix from the give angles in degrees. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The . + internal ProjectiveTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) + => this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); + + /// + /// Appends a centered skew matrix from the give angles in radians. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The . + public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY) + => this.Append(size => new Matrix4x4(TransformUtilities.CreateSkewTransformMatrixRadians(radiansX, radiansY, size))); + + /// + /// Appends a skew matrix using the given angles in degrees at the given origin. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The skew origin point. + /// The . + public ProjectiveTransformBuilder AppendSkewDegrees(float degreesX, float degreesY, Vector2 origin) + => this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin); + + /// + /// Appends a skew matrix using the given angles in radians at the given origin. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The skew origin point. + /// The . + public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY, Vector2 origin) + => this.AppendMatrix(new Matrix4x4(Matrix3x2.CreateSkew(radiansX, radiansY, origin))); + + /// + /// Prepends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public ProjectiveTransformBuilder PrependTranslation(PointF position) + => this.PrependTranslation((Vector2)position); + + /// + /// Prepends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public ProjectiveTransformBuilder PrependTranslation(Vector2 position) + => this.PrependMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0))); + + /// + /// Appends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public ProjectiveTransformBuilder AppendTranslation(PointF position) + => this.AppendTranslation((Vector2)position); + + /// + /// Appends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public ProjectiveTransformBuilder AppendTranslation(Vector2 position) + => this.AppendMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0))); + + /// + /// Prepends a quad distortion matrix using the specified corner points. + /// + /// The top-left corner point of the distorted quad. + /// The top-right corner point of the distorted quad. + /// The bottom-right corner point of the distorted quad. + /// The bottom-left corner point of the distorted quad. + /// The . + public ProjectiveTransformBuilder PrependQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft) + => this.Prepend(size => TransformUtilities.CreateQuadDistortionMatrix( + new Rectangle(Point.Empty, size), + topLeft, + topRight, + bottomRight, + bottomLeft)); + + /// + /// Appends a quad distortion matrix using the specified corner points. + /// + /// The top-left corner point of the distorted quad. + /// The top-right corner point of the distorted quad. + /// The bottom-right corner point of the distorted quad. + /// The bottom-left corner point of the distorted quad. + /// The . + public ProjectiveTransformBuilder AppendQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft) + => this.Append(size => TransformUtilities.CreateQuadDistortionMatrix( + new Rectangle(Point.Empty, size), + topLeft, + topRight, + bottomRight, + bottomLeft)); + + /// + /// Prepends a raw matrix. + /// + /// The matrix to prepend. + /// + /// The resultant matrix is degenerate containing one or more values equivalent + /// to or a zero determinant and therefore cannot be used + /// for linear transforms. + /// + /// The . + public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix) + { + CheckDegenerate(matrix); + return this.Prepend(_ => matrix); + } + + /// + /// Appends a raw matrix. + /// + /// The matrix to append. + /// + /// The resultant matrix is degenerate containing one or more values equivalent + /// to or a zero determinant and therefore cannot be used + /// for linear transforms. + /// + /// The . + public ProjectiveTransformBuilder AppendMatrix(Matrix4x4 matrix) + { + CheckDegenerate(matrix); + return this.Append(_ => matrix); + } + + /// + /// Returns the combined matrix for a given source size. + /// + /// The source image size. + /// The . + public Matrix4x4 BuildMatrix(Size sourceSize) + => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize)); + + /// + /// Returns the combined matrix for a given source rectangle. + /// + /// The rectangle in the source image. + /// + /// The resultant matrix is degenerate containing one or more values equivalent + /// to or a zero determinant and therefore cannot be used + /// for linear transforms. + /// + /// The . + public Matrix4x4 BuildMatrix(Rectangle sourceRectangle) + { + Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); + Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); + + // Translate the origin matrix to cater for source rectangle offsets. + Matrix4x4 matrix = Matrix4x4.CreateTranslation(new Vector3(-sourceRectangle.Location, 0)); + + Size size = sourceRectangle.Size; + + foreach (Func factory in this.transformMatrixFactories) + { + matrix *= factory(size); + } + + CheckDegenerate(matrix); + + return matrix; + } + + /// + /// Returns the size of a rectangle large enough to contain the transformed source rectangle. + /// + /// The rectangle in the source image. + /// + /// The resultant matrix is degenerate containing one or more values equivalent + /// to or a zero determinant and therefore cannot be used + /// for linear transforms. + /// + /// The . + public SizeF GetTransformedSize(Rectangle sourceRectangle) + { + Matrix4x4 matrix = this.BuildMatrix(sourceRectangle); + return GetTransformedSize(sourceRectangle, matrix); + } + + /// + /// Returns the size of a rectangle large enough to contain the transformed source rectangle. + /// + /// The rectangle in the source image. + /// The transformation matrix. + /// + /// The resultant matrix is degenerate containing one or more values equivalent + /// to or a zero determinant and therefore cannot be used + /// for linear transforms. + /// + /// The . + internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix4x4 matrix) + => TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size); + + /// + /// Clears all accumulated transform matrices, resetting the builder to its initial state. + /// + /// The . + public ProjectiveTransformBuilder Clear() + { + this.transformMatrixFactories.Clear(); + return this; + } + + private static void CheckDegenerate(Matrix4x4 matrix) + { + if (TransformUtilities.IsDegenerate(matrix)) + { + throw new DegenerateTransformException("Matrix is degenerate. Check input values."); + } + } + + private ProjectiveTransformBuilder Prepend(Func transformFactory) + { + this.transformMatrixFactories.Insert(0, transformFactory); + return this; + } + + private ProjectiveTransformBuilder Append(Func transformFactory) + { + this.transformMatrixFactories.Add(transformFactory); + return this; + } + } +} diff --git a/ImageSharp/Processing/ResizeMode.cs b/ImageSharp/Processing/ResizeMode.cs new file mode 100644 index 0000000..1bfcf15 --- /dev/null +++ b/ImageSharp/Processing/ResizeMode.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing { + /// + /// Provides enumeration over how the image should be resized. + /// + public enum ResizeMode + { + /// + /// Crops the resized image to fit the bounds of its container. + /// + Crop, + + /// + /// Pads the resized image to fit the bounds of its container. + /// If only one dimension is passed, will maintain the original aspect ratio. + /// + Pad, + + /// + /// Pads the image to fit the bound of the container without resizing the + /// original source. + /// When downscaling, performs the same functionality as + /// + BoxPad, + + /// + /// Constrains the resized image to fit the bounds of its container maintaining + /// the original aspect ratio. + /// + Max, + + /// + /// Resizes the image until the shortest side reaches the set given dimension. + /// Upscaling is disabled in this mode and the original image will be returned + /// if attempted. + /// + Min, + + /// + /// Stretches the resized image to fit the bounds of its container. + /// + Stretch, + + /// + /// The target location and size of the resized image has been manually set. + /// + Manual + } +} diff --git a/ImageSharp/Processing/ResizeOptions.cs b/ImageSharp/Processing/ResizeOptions.cs new file mode 100644 index 0000000..586b576 --- /dev/null +++ b/ImageSharp/Processing/ResizeOptions.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Processing { + /// + /// The resize options for resizing images against certain modes. + /// + public class ResizeOptions + { + /// + /// Gets or sets the resize mode. + /// + public ResizeMode Mode { get; set; } = ResizeMode.Crop; + + /// + /// Gets or sets the anchor position. + /// + public AnchorPositionMode Position { get; set; } = AnchorPositionMode.Center; + + /// + /// Gets or sets the center coordinates. + /// + public PointF? CenterCoordinates { get; set; } + + /// + /// Gets or sets the target size. + /// + public Size Size { get; set; } + + /// + /// Gets or sets the sampler to perform the resize operation. + /// + public IResampler Sampler { get; set; } = KnownResamplers.Bicubic; + + /// + /// Gets or sets a value indicating whether to compress + /// or expand individual pixel colors the value on processing. + /// + public bool Compand { get; set; } + + /// + /// Gets or sets the target rectangle to resize into. + /// + public Rectangle? TargetRectangle { get; set; } + + /// + /// Gets or sets a value indicating whether to premultiply + /// the alpha (if it exists) during the resize operation. + /// + public bool PremultiplyAlpha { get; set; } = true; + + /// + /// Gets or sets the color to use as a background when padding an image. + /// + public Color PadColor { get; set; } + } +} diff --git a/ImageSharp/Processing/RotateMode.cs b/ImageSharp/Processing/RotateMode.cs new file mode 100644 index 0000000..9c18127 --- /dev/null +++ b/ImageSharp/Processing/RotateMode.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing { + /// + /// Provides enumeration over how the image should be rotated. + /// + public enum RotateMode + { + /// + /// Do not rotate the image. + /// + None, + + /// + /// Rotate the image by 90 degrees clockwise. + /// + Rotate90 = 90, + + /// + /// Rotate the image by 180 degrees clockwise. + /// + Rotate180 = 180, + + /// + /// Rotate the image by 270 degrees clockwise. + /// + Rotate270 = 270 + } +} diff --git a/ImageSharp/Processing/TaperCorner.cs b/ImageSharp/Processing/TaperCorner.cs new file mode 100644 index 0000000..f9071bb --- /dev/null +++ b/ImageSharp/Processing/TaperCorner.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing { + /// + /// Enumerates the various options which determine how to taper corners + /// + public enum TaperCorner + { + /// + /// Taper the left or top corner + /// + LeftOrTop, + + /// + /// Taper the right or bottom corner + /// + RightOrBottom, + + /// + /// Taper the both sets of corners + /// + Both + } +} diff --git a/ImageSharp/Processing/TaperSide.cs b/ImageSharp/Processing/TaperSide.cs new file mode 100644 index 0000000..f55f276 --- /dev/null +++ b/ImageSharp/Processing/TaperSide.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing { + /// + /// Enumerates the various options which determine which side to taper + /// + public enum TaperSide + { + /// + /// Taper the left side + /// + Left, + + /// + /// Taper the top side + /// + Top, + + /// + /// Taper the right side + /// + Right, + + /// + /// Taper the bottom side + /// + Bottom + } +} diff --git a/ImageSharp/Properties/AssemblyInfo.cs b/ImageSharp/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..334737a --- /dev/null +++ b/ImageSharp/Properties/AssemblyInfo.cs @@ -0,0 +1,9 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// Redundant suppressing of SA1413 for Rider. +[assembly: + System.Diagnostics.CodeAnalysis.SuppressMessage( + "StyleCop.CSharp.MaintainabilityRules", + "SA1413:UseTrailingCommasInMultiLineInitializers", + Justification = "Follows SixLabors.ruleset")] diff --git a/ImageSharp/ReadOrigin.cs b/ImageSharp/ReadOrigin.cs new file mode 100644 index 0000000..fd64a45 --- /dev/null +++ b/ImageSharp/ReadOrigin.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp { + /// + /// Specifies the position in a stream to use for reading. + /// + public enum ReadOrigin + { + /// + /// Specifies the beginning of a stream. + /// + Begin, + + /// + /// Specifies the current position within a stream. + /// + Current + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a68eb67 --- /dev/null +++ b/LICENSE @@ -0,0 +1,43 @@ +Six Labors Split License +Version 1.0, June 2022 +Copyright (c) Six Labors + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, including but not limited to software source + code, documentation source, and configuration files. + + "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including + but not limited to compiled object code, generated documentation, and conversions to other media types. + + "Work" (or "Works") shall mean any Six Labors software made available under the License, as indicated by a + copyright notice that is included in or attached to the work. + + "Direct Package Dependency" shall mean any Work in Source or Object form that is installed directly by You. + + "Transitive Package Dependency" shall mean any Work in Object form that is installed indirectly by a third party + dependency unrelated to Six Labors. + +2. License + + Works in Source or Object form are split licensed and may be licensed under the Apache License, Version 2.0 or a + Six Labors Commercial Use License. + + Licenses are granted based upon You meeting the qualified criteria as stated. Once granted, + You must reference the granted license only in all documentation. + + Works in Source or Object form are licensed to You under the Apache License, Version 2.0 if. + + - You are consuming the Work in for use in software licensed under an Open Source or Source Available license. + - You are consuming the Work as a Transitive Package Dependency. + - You are consuming the Work as a Direct Package Dependency in the capacity of a For-profit company/individual with + less than 1M USD annual gross revenue. + - You are consuming the Work as a Direct Package Dependency in the capacity of a Non-profit organization + or Registered Charity. + + For all other scenarios, Works in Source or Object form are licensed to You under the Six Labors Commercial License + which may be purchased by visiting https://sixlabors.com/pricing/. diff --git a/PolygonClipper/ActiveEdge.cs b/PolygonClipper/ActiveEdge.cs new file mode 100644 index 0000000..288ea42 --- /dev/null +++ b/PolygonClipper/ActiveEdge.cs @@ -0,0 +1,194 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// Represents an edge that is currently active in the sweep-line. + /// + /// + /// The sweep assumes a Y-axis-positive-down coordinate system. "Bottom" and "Top" + /// refer to the lower and upper scanline endpoints (larger and smaller Y respectively). + /// + internal sealed class ActiveEdge + { +#pragma warning disable SA1401 // Hot sweep state uses fields to avoid accessor overhead. + /// + /// The lower endpoint of the edge in scanline order. + /// + public Vertex Bottom; + + /// + /// The upper endpoint of the edge in scanline order. + /// + public Vertex Top; + + /// + /// The X coordinate where the edge intersects the current scanline. + /// + public double CurrentX; + + /// + /// The delta-X per delta-Y for the edge (its scanline slope). + /// + public double Dx; + + /// + /// The winding delta contributed by this edge (+1 or -1). + /// + public int WindDelta; + + /// + /// The accumulated winding count for this edge. + /// + public int WindCount; + + /// + /// The output record this edge is contributing to, if any. + /// + public OutputRecord? OutputRecord; + + /// + /// The previous edge in the Active Edge List (AEL). + /// + public ActiveEdge? PrevInAel; + + /// + /// The next edge in the Active Edge List (AEL). + /// + public ActiveEdge? NextInAel; + + /// + /// The previous edge in the Sorted Edge List (SEL). + /// + public ActiveEdge? PrevInSel; + + /// + /// The next edge in the Sorted Edge List (SEL). + /// + public ActiveEdge? NextInSel; + + /// + /// The temporary link used when sorting intersections. + /// + public ActiveEdge? Jump; + + /// + /// The current top vertex for this edge's bound. + /// + public SweepVertex? VertexTop; + + /// + /// The local minima that spawned this edge. + /// + public LocalMinima LocalMin; + + /// + /// Indicates whether this edge is the left bound of its pair. + /// + public bool IsLeftBound; + + /// + /// The pending join state for this edge. + /// + public JoinWith JoinWith; +#pragma warning restore SA1401 + + /// + /// Gets a value indicating whether this edge currently contributes to output. + /// + public bool IsHot => this.OutputRecord != null; + + /// + /// Gets a value indicating whether the edge is horizontal within tolerance. + /// + public bool IsHorizontal => this.Top.Y == this.Bottom.Y; + + /// + /// Gets a value indicating whether a horizontal edge is heading right. + /// + public bool IsHeadingRightHorizontal => double.IsNegativeInfinity(this.Dx); + + /// + /// Gets a value indicating whether a horizontal edge is heading left. + /// + public bool IsHeadingLeftHorizontal => double.IsPositiveInfinity(this.Dx); + + /// + /// Gets a value indicating whether the current top vertex is a local maxima. + /// + public bool IsMaxima => this.VertexTop != null && this.VertexTop.IsMaxima; + + /// + /// Gets a value indicating whether this edge is the front edge of its output record. + /// + public bool IsFront => this.OutputRecord != null && this == this.OutputRecord.FrontEdge; + + /// + /// Gets the next input vertex along the bound in the winding direction. + /// + public SweepVertex NextVertex => this.WindDelta > 0 ? this.VertexTop!.Next! : this.VertexTop!.Prev!; + + /// + /// Gets the vertex two steps behind the current top, used for turn tests. + /// + public SweepVertex PrevPrevVertex => this.WindDelta > 0 ? this.VertexTop!.Prev!.Prev! : this.VertexTop!.Next!.Next!; + + /// + /// Finds the previous hot edge in the AEL, if any. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ActiveEdge? GetPrevHotEdge() + { + ActiveEdge? prev = this.PrevInAel; + while (prev != null && !prev.IsHot) + { + prev = prev.PrevInAel; + } + + return prev; + } + + /// + /// Calculates the X coordinate where this edge intersects the scanline at . + /// + // This method sits on the hottest path in large self-intersection workloads. + // AggressiveOptimization consistently improves codegen here versus tiered defaults. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static double TopX(ActiveEdge edge, double currentY) + { + if (currentY == edge.Top.Y || edge.Top.X == edge.Bottom.X) + { + return edge.Top.X; + } + + if (currentY == edge.Bottom.Y) + { + return edge.Bottom.X; + } + + return edge.Bottom.X + (edge.Dx * (currentY - edge.Bottom.Y)); + } + + /// + /// Recomputes from the current endpoints. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void UpdateDx() => this.Dx = GetDx(this.Bottom, this.Top); + + /// + /// Computes delta-X per delta-Y, returning infinities for horizontal edges. + /// + private static double GetDx(Vertex pt1, Vertex pt2) + { + double dy = pt2.Y - pt1.Y; + if (dy != 0) + { + return (pt2.X - pt1.X) / dy; + } + + return pt2.X > pt1.X ? double.NegativeInfinity : double.PositiveInfinity; + } + } +} diff --git a/PolygonClipper/ActiveEdgeList.cs b/PolygonClipper/ActiveEdgeList.cs new file mode 100644 index 0000000..2ea41e2 --- /dev/null +++ b/PolygonClipper/ActiveEdgeList.cs @@ -0,0 +1,349 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// Maintains the active edge list (AEL) plus a horizontal edge stack for the sweep. + /// + /// + /// The AEL is ordered left-to-right at the current scanline. As edges are inserted + /// and removed, this list preserves adjacency for intersection processing. + /// The horizontal stack is a lightweight LIFO queue used to process horizontal + /// bounds separately from the main sweep order. + /// + internal sealed class ActiveEdgeList + { + private readonly Stack pool; + private ActiveEdge? horizontalHead; + + /// + /// Initializes a new instance of the class. + /// + public ActiveEdgeList() => this.pool = new Stack(); + + /// + /// Gets the head of the active edge list. + /// + public ActiveEdge? Head { get; private set; } + + /// + /// Gets the number of retained pooled edge objects. + /// + public int RetainedPoolCount => this.pool.Count; + + /// + /// Clears all active edges and returns them to the pool. + /// + public void ClearActiveEdges() + { + while (this.Head != null) + { + this.Remove(this.Head); + } + + this.horizontalHead = null; + } + + /// + /// Resets sweep pointers without clearing the pool. + /// + public void Reset() + { + this.Head = null; + this.horizontalHead = null; + } + + /// + /// Acquires a reusable active edge, allocating if the pool is empty. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ActiveEdge Acquire() + => this.pool.Count == 0 ? new ActiveEdge() : this.pool.Pop(); + + /// + /// Inserts an edge into the active list, maintaining left-to-right order. + /// + /// The edge to insert. + public void InsertLeft(ActiveEdge edge) + { + if (this.Head == null) + { + edge.PrevInAel = null; + edge.NextInAel = null; + this.Head = edge; + return; + } + + if (!IsValidActiveEdgeOrder(this.Head, edge)) + { + edge.PrevInAel = null; + edge.NextInAel = this.Head; + this.Head.PrevInAel = edge; + this.Head = edge; + return; + } + + ActiveEdge edge2 = this.Head; + while (edge2.NextInAel != null && IsValidActiveEdgeOrder(edge2.NextInAel, edge)) + { + edge2 = edge2.NextInAel; + } + + // Keep joined edges adjacent in the active list. + if (edge2.JoinWith == JoinWith.Right) + { + edge2 = edge2.NextInAel!; + } + + edge.NextInAel = edge2.NextInAel; + if (edge2.NextInAel != null) + { + edge2.NextInAel.PrevInAel = edge; + } + + edge.PrevInAel = edge2; + edge2.NextInAel = edge; + } + + /// + /// Inserts a right bound edge immediately after another edge in the active list. + /// + /// The anchor edge. + /// The edge to insert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void InsertRight(ActiveEdge edge, ActiveEdge edge2) + { + edge2.NextInAel = edge.NextInAel; + if (edge.NextInAel != null) + { + edge.NextInAel.PrevInAel = edge2; + } + + edge2.PrevInAel = edge; + edge.NextInAel = edge2; + } + + /// + /// Removes an edge from the active list and returns it to the pool. + /// + /// The edge to remove. + public void Remove(ActiveEdge edge) + { + ActiveEdge? prev = edge.PrevInAel; + ActiveEdge? next = edge.NextInAel; + + if (prev == null && next == null && edge != this.Head) + { + return; + } + + if (prev != null) + { + prev.NextInAel = next; + } + else + { + this.Head = next; + } + + if (next != null) + { + next.PrevInAel = prev; + } + + this.Recycle(edge); + } + + /// + /// Swaps the positions of two adjacent edges in the active list. + /// + /// The left edge. + /// The right edge. + public void SwapPositions(ActiveEdge left, ActiveEdge right) + { + // Precondition: left must be immediately to the left of right. + ActiveEdge? next = right.NextInAel; + if (next != null) + { + next.PrevInAel = left; + } + + ActiveEdge? prev = left.PrevInAel; + if (prev != null) + { + prev.NextInAel = right; + } + + right.PrevInAel = prev; + right.NextInAel = left; + left.PrevInAel = right; + left.NextInAel = next; + if (right.PrevInAel == null) + { + this.Head = right; + } + } + + /// + /// Clears the horizontal edge stack. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ClearHorizontalQueue() => this.horizontalHead = null; + + /// + /// Pushes a horizontal edge onto the processing stack. + /// + /// The horizontal edge to push. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void PushHorizontal(ActiveEdge edge) + { + edge.NextInSel = this.horizontalHead; + this.horizontalHead = edge; + } + + /// + /// Pops the next horizontal edge to process. + /// + /// The next horizontal edge, or . + /// when a horizontal edge was available. + public bool TryPopHorizontal(out ActiveEdge? edge) + { + while (true) + { + edge = this.horizontalHead; + if (edge == null) + { + return false; + } + + ActiveEdge? next = edge.NextInSel; + this.horizontalHead = ReferenceEquals(next, edge) ? null : next; + if (edge.VertexTop != null) + { + return true; + } + } + } + + /// + /// Copies the active list into a sorted list and updates current X values. + /// + /// The scanline top Y coordinate. + /// The head of the sorted list. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public ActiveEdge? CopyToSorted(double topY) + { + ActiveEdge? edge = this.Head; + ActiveEdge? sortedHead = edge; + while (edge != null) + { + edge.PrevInSel = edge.PrevInAel; + edge.NextInSel = edge.NextInAel; + edge.Jump = edge.NextInSel; + + // Joined edges can be split later during intersection processing. + edge.CurrentX = ActiveEdge.TopX(edge, topY); + + // Defer any Y updates; intersection tests use original bounds. + edge = edge.NextInAel; + } + + return sortedHead; + } + + /// + /// Determines whether the newcomer should be inserted after the resident in the active list. + /// + /// The current resident edge. + /// The incoming edge to compare. + /// if the newcomer bedoubles after the resident. + public static bool IsValidActiveEdgeOrder(ActiveEdge resident, ActiveEdge newcomer) + { + if (newcomer.CurrentX != resident.CurrentX) + { + return newcomer.CurrentX > resident.CurrentX; + } + + // Compare turning direction: resident.Top -> newcomer.Bottom -> newcomer.Top. + int d = PolygonUtilities.CrossSign(resident.Top, newcomer.Bottom, newcomer.Top); + if (d != 0) + { + return d < 0; + } + + // For collinear bounds, use the next turn to order them. + if (!resident.IsMaxima && (resident.Top.Y > newcomer.Top.Y)) + { + return PolygonUtilities.CrossSign( + newcomer.Bottom, + resident.Top, + resident.NextVertex.Point) <= 0; + } + + if (!newcomer.IsMaxima && (newcomer.Top.Y > resident.Top.Y)) + { + return PolygonUtilities.CrossSign( + newcomer.Bottom, + newcomer.Top, + newcomer.NextVertex.Point) >= 0; + } + + double y = newcomer.Bottom.Y; + bool newcomerIsLeft = newcomer.IsLeftBound; + + if (resident.Bottom.Y != y || resident.LocalMin.Vertex.Point.Y != y) + { + return newcomer.IsLeftBound; + } + + // Only newly inserted edges reach this branch. + if (resident.IsLeftBound != newcomerIsLeft) + { + return newcomerIsLeft; + } + + if (PolygonUtilities.IsCollinear( + resident.PrevPrevVertex.Point, + resident.Bottom, + resident.Top)) + { + return true; + } + + // Use the alternate bound turn to break the tie. + return (PolygonUtilities.CrossSign( + resident.PrevPrevVertex.Point, + newcomer.Bottom, + newcomer.PrevPrevVertex.Point) > 0) == newcomerIsLeft; + } + + /// + /// Resets and returns an active edge to the reuse pool. + /// + /// The edge to recycle. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Recycle(ActiveEdge edge) + { + // Clear references so pooled edges do not retain objects. + edge.Bottom = default; + edge.Top = default; + edge.Dx = 0.0; + edge.CurrentX = 0; + edge.WindCount = 0; + edge.OutputRecord = null; + edge.PrevInAel = null; + edge.NextInAel = null; + edge.PrevInSel = null; + edge.NextInSel = null; + edge.Jump = null; + edge.VertexTop = null; + edge.LocalMin = default; + edge.IsLeftBound = false; + edge.JoinWith = JoinWith.None; + this.pool.Push(edge); + } + } +} diff --git a/PolygonClipper/ArrayBuilder{T}.cs b/PolygonClipper/ArrayBuilder{T}.cs new file mode 100644 index 0000000..ccef22d --- /dev/null +++ b/PolygonClipper/ArrayBuilder{T}.cs @@ -0,0 +1,180 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// A helper type for avoiding allocations while building arrays. + /// + /// The type of item contained in the array. + internal struct ArrayBuilder + where T : struct + { + private const int DefaultCapacity = 4; + + // Starts out null, initialized on first Add. + private T[]? data; + private int size; + + /// + /// Initializes a new instance of the struct. + /// + /// The initial capacity of the array. + public ArrayBuilder(int capacity) + : this() + { + if (capacity > 0) + { + this.data = new T[capacity]; + } + } + + /// + /// Gets or sets the number of items in the array. + /// + public int Length + { + readonly get => this.size; + + set + { + if (value > 0) + { + this.EnsureCapacity(value); + this.size = value; + } + else + { + this.size = 0; + } + } + } + + /// + /// Gets the backing buffer capacity. + /// + public readonly int Capacity + => this.data?.Length ?? 0; + + /// + /// Returns a reference to specified element of the array. + /// + /// The index of the element to return. + /// The . + /// + /// Thrown when index less than 0 or index greater than or equal to . + /// + public readonly ref T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + DebugGuard.MustBeBetweenOrEqualTo(index, 0, this.size, nameof(index)); + return ref this.data![index]; + } + } + + /// + /// Adds the given item to the array. + /// + /// The item to add. + public void Add(T item) + { + int position = this.size; + T[]? array = this.data; + + if (array != null && (uint)position < (uint)array.Length) + { + this.size = position + 1; + array[position] = item; + } + else + { + this.AddWithResize(item); + } + } + + // Non-inline from Add to improve its code quality as uncommon path + [MethodImpl(MethodImplOptions.NoInlining)] + private void AddWithResize(T item) + { + int size = this.size; + this.Grow(size + 1); + this.size = size + 1; + this.data[size] = item; + } + + /// + /// Remove the last item from the array. + /// + public void RemoveLast() + { + DebugGuard.MustBeGreaterThan(this.size, 0, nameof(this.size)); + this.size--; + } + + /// + /// Clears the array. + /// Allocated memory is left intact for future usage. + /// + public void Clear() => + + // No need to actually clear since we're not allowing reference types. + this.size = 0; + + /// + /// Sorts the active range of items using the specified comparer. + /// + /// The comparer to use, or null for the default comparer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void Sort(IComparer? comparer = null) + { + if (this.size <= 1 || this.data == null) + { + return; + } + + Array.Sort(this.data, 0, this.size, comparer); + } + + private void EnsureCapacity(int min) + { + int length = this.data?.Length ?? 0; + if (length < min) + { + this.Grow(min); + } + } + + [MemberNotNull(nameof(data))] + private void Grow(int capacity) + { + // Same expansion algorithm as List. + int length = this.data?.Length ?? 0; + int newCapacity = length == 0 ? DefaultCapacity : length * 2; + if ((uint)newCapacity > Array.MaxLength) + { + newCapacity = Array.MaxLength; + } + + if (newCapacity < capacity) + { + newCapacity = capacity; + } + + T[] array = new T[newCapacity]; + + if (this.size > 0) + { + Array.Copy(this.data!, array, this.size); + } + + this.data = array; + } + } +} diff --git a/PolygonClipper/BooleanOperation.cs b/PolygonClipper/BooleanOperation.cs new file mode 100644 index 0000000..0dae7e5 --- /dev/null +++ b/PolygonClipper/BooleanOperation.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.PolygonClipper { + /// + /// Specifies the type of boolean operation to perform on polygons. + /// + public enum BooleanOperation + { + /// + /// The intersection operation, which results in the area common to both polygons. + /// + Intersection = 0, + + /// + /// The union operation, which results in the combined area of both polygons. + /// + Union = 1, + + /// + /// The difference operation, which subtracts the clipping polygon from the subject polygon. + /// + Difference = 2, + + /// + /// The exclusive OR (XOR) operation, which results in the area covered by exactly one polygon, + /// excluding the overlapping areas. + /// + Xor = 3 + } +} diff --git a/PolygonClipper/Box2.cs b/PolygonClipper/Box2.cs new file mode 100644 index 0000000..0aaeacb --- /dev/null +++ b/PolygonClipper/Box2.cs @@ -0,0 +1,134 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// Represents a bounding box. + /// + public readonly struct Box2 : IEquatable + { + /// + /// Gets the minimum xy-coordinate. + /// +#pragma warning disable CA1051 // Do not declare visible instance fields + public readonly Vertex Min; + + /// + /// Gets the maximum xy-coordinate. + /// + public readonly Vertex Max; +#pragma warning restore CA1051 // Do not declare visible instance fields + + /// + /// Initializes a new instance of the struct. + /// + /// The xy-coordinate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Box2(in Vertex vector) + : this(vector, vector) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The minimum xy-coordinate. + /// The maximum xy-coordinate. + public Box2(in Vertex min, in Vertex max) + { + this.Min = min; + this.Max = max; + } + + /// + /// Gets an invalid bounds instance. + /// + public static Box2 Invalid { get; } = new( + new Vertex(double.MaxValue, double.MaxValue), + new Vertex(-double.MaxValue, -double.MaxValue)); + + /// + /// Compares two instances for equality. + /// + /// The left object. + /// The right object. + /// true if both boxes are equal; otherwise, false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(in Box2 left, in Box2 right) + => left.Equals(right); + + /// + /// Determines whether two instances are not equal. + /// + /// The left object. + /// The right object. + /// true if the boxes are not equal; otherwise, false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(in Box2 left, in Box2 right) + => !(left == right); + + /// + /// Returns true if the box is empty. + /// + /// if the box is empty; otherwise, . + public bool IsEmpty() => this.Max.X <= this.Min.X || this.Max.Y <= this.Min.Y; + + /// + /// Returns true if the point lies within the box. + /// + /// The point to test. + /// if the point lies within the box; otherwise, . + public bool Contains(in Vertex point) + => point.X > this.Min.X && point.X < this.Max.X && point.Y > this.Min.Y && point.Y < this.Max.Y; + + /// + /// Returns true if the box contains another box. + /// + /// The other box. + /// if the box contains the other box; otherwise, . + public bool Contains(in Box2 bounds) + => bounds.Min.X >= this.Min.X && bounds.Max.X <= this.Max.X && + bounds.Min.Y >= this.Min.Y && bounds.Max.Y <= this.Max.Y; + + /// + /// Returns true if the boxes intersect. + /// + /// The other box. + /// if the boxes intersect; otherwise, . + public bool Intersects(in Box2 bounds) + => Math.Max(this.Min.X, bounds.Min.X) <= Math.Min(this.Max.X, bounds.Max.X) && + Math.Max(this.Min.Y, bounds.Min.Y) <= Math.Min(this.Max.Y, bounds.Max.Y); + + /// + /// Returns the midpoint of the box. + /// + /// The midpoint. + public Vertex MidPoint() => new((this.Min.X + this.Max.X) / 2D, (this.Min.Y + this.Max.Y) / 2D); + + /// + /// Adds another bounding box to this instance. + /// + /// The other box. + /// The summed . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Box2 Add(in Box2 other) + => new(Vertex.Min(this.Min, other.Min), Vertex.Max(this.Max, other.Max)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + => obj is Box2 box + && this.Equals(box); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Box2 other) + => this.Min == other.Min && this.Max == other.Max; + + /// + public override int GetHashCode() => HashCode.Combine(this.Min, this.Max); + } +} diff --git a/PolygonClipper/Buffer{T}.cs b/PolygonClipper/Buffer{T}.cs new file mode 100644 index 0000000..fa7489f --- /dev/null +++ b/PolygonClipper/Buffer{T}.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// An disposable buffer that is backed by an array pool. + /// + /// The type of buffer element. + internal ref struct Buffer + where T : unmanaged + { + private int length; + private readonly byte[] buffer; + private readonly Span span; + private bool isDisposed; + + public Buffer(int length) + { + Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length)); + int itemSizeBytes = Unsafe.SizeOf(); + int bufferSizeInBytes = length * itemSizeBytes; + this.buffer = ArrayPool.Shared.Rent(bufferSizeInBytes); + this.length = length; + + using ByteMemoryManager manager = new(this.buffer); + this.Memory = manager.Memory[..this.length]; + this.span = this.Memory.Span; + + this.isDisposed = false; + } + + public Memory Memory { get; } + + public readonly Span GetSpan() + { + if (this.buffer is null) + { + ThrowObjectDisposedException(); + } + + return this.span; + } + + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + ArrayPool.Shared.Return(this.buffer); + this.length = 0; + this.isDisposed = true; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowObjectDisposedException() => throw new ObjectDisposedException("Buffer"); + } +} diff --git a/PolygonClipper/ByteMemoryManager{T}.cs b/PolygonClipper/ByteMemoryManager{T}.cs new file mode 100644 index 0000000..6c2955a --- /dev/null +++ b/PolygonClipper/ByteMemoryManager{T}.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.PolygonClipper { + /// + /// A custom that can wrap of instances + /// and cast them to be for any arbitrary unmanaged value type. + /// + /// The value type to use when casting the wrapped instance. + internal sealed class ByteMemoryManager : MemoryManager + where T : unmanaged + { + /// + /// The wrapped of instance. + /// + private readonly Memory memory; + + /// + /// Initializes a new instance of the class. + /// + /// The of instance to wrap. + public ByteMemoryManager(Memory memory) => this.memory = memory; + + /// + protected override void Dispose(bool disposing) + { + } + + /// + public override Span GetSpan() => MemoryMarshal.Cast(this.memory.Span); + + /// + public override MemoryHandle Pin(int elementIndex = 0) + + // We need to adjust the offset into the wrapped byte segment, + // as the input index refers to the target-cast memory of T. + // We just have to shift this index by the byte size of T. + => this.memory[(elementIndex * Unsafe.SizeOf())..].Pin(); + + /// + public override void Unpin() + { + } + } +} diff --git a/PolygonClipper/Contour.cs b/PolygonClipper/Contour.cs new file mode 100644 index 0000000..6637575 --- /dev/null +++ b/PolygonClipper/Contour.cs @@ -0,0 +1,282 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// Represents a single polygon ring (outer contour or hole). + /// + /// + /// A contour is treated as implicitly closed: an edge is always considered between the last + /// vertex and the first vertex. A duplicated terminal closing vertex is optional on input + /// but not required. + /// + [DebuggerDisplay("Count = {Count}")] +#pragma warning disable CA1710 // Identifiers should have correct suffix + public sealed class Contour : IReadOnlyCollection +#pragma warning restore CA1710 // Identifiers should have correct suffix + { + private bool hasCachedOrientation; + private bool cachedCounterClockwise; + + /// + /// Set of vertices conforming the external contour + /// + private readonly List vertices = []; + + /// + /// Holes of the contour. They are stored as the indexes of + /// the holes in a polygon class + /// + private readonly List holeIndices = []; + + /// + /// Initializes a new instance of the class. + /// + public Contour() + => this.vertices = []; + + /// + /// Initializes a new instance of the class with a vertex capacity. + /// + /// The initial vertex capacity. + public Contour(int capacity) + => this.vertices = new List(capacity); + + /// + /// Gets the number of stored vertices. + /// + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.vertices.Count; + } + + /// + /// Gets the number of holes. + /// + public int HoleCount => this.holeIndices.Count; + + /// + /// Gets a value indicating whether the contour is external (not a hole). + /// + public bool IsExternal => this.ParentIndex == null; + + /// + /// Gets or sets the index of the parent contour in the polygon if this contour is a hole. + /// + public int? ParentIndex { get; set; } + + /// + /// Gets or sets the depth of the contour. + /// + public int Depth { get; set; } + + /// + /// Gets the vertex at the specified index. + /// + /// The index of the vertex. + /// The at the specified index. + public Vertex this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.vertices[index]; + } + + /// + /// Gets the hole index at the specified position in the contour. + /// + /// The index of the hole. + /// The hole index. + public int GetHoleIndex(int index) => this.holeIndices[index]; + + /// + /// Gets the segment at the specified index of the contour. + /// + /// The index of the segment. + /// The . The final segment wraps from last vertex to first vertex. + internal Segment GetSegment(int index) + => (index == this.Count - 1) + ? new Segment(this.vertices[^1], this.vertices[0]) + : new Segment(this.vertices[index], this.vertices[index + 1]); + + /// + /// Gets the bounding box of the contour. + /// + /// The . + public Box2 GetBoundingBox() + { + if (this.Count == 0) + { + return default; + } + + List points = this.vertices; + Box2 b = new(points[0]); + for (int i = 1; i < points.Count; ++i) + { + b = b.Add(new Box2(points[i])); + } + + return b; + } + + /// + /// Gets a value indicating whether the contour is counterclockwise oriented + /// + /// + /// if the contour is counterclockwise oriented; otherwise . + /// + public bool IsCounterClockwise() + { + if (this.hasCachedOrientation) + { + return this.cachedCounterClockwise; + } + + this.hasCachedOrientation = true; + + double area = 0; + Vertex c; + Vertex c1; + + List points = this.vertices; + for (int i = 0; i < points.Count - 1; i++) + { + c = points[i]; + c1 = points[i + 1]; + area += Vertex.Cross(c, c1); + } + + c = points[^1]; + c1 = points[0]; + area += Vertex.Cross(c, c1); + return this.cachedCounterClockwise = area >= 0; + } + + /// + /// Gets a value indicating whether the contour is clockwise oriented + /// + /// + /// if the contour is clockwise oriented; otherwise . + /// + public bool IsClockwise() => !this.IsCounterClockwise(); + + /// + /// Reverses the orientation of the contour. + /// + public void Reverse() + { + this.vertices.Reverse(); + this.cachedCounterClockwise = !this.cachedCounterClockwise; + } + + /// + /// Sets the contour to clockwise orientation. + /// + public void SetClockwise() + { + if (this.IsCounterClockwise()) + { + this.Reverse(); + } + } + + /// + /// Sets the contour to counterclockwise orientation. + /// + public void SetCounterClockwise() + { + if (this.IsClockwise()) + { + this.Reverse(); + } + } + + /// + /// Translates the contour by the specified x and y values. + /// + /// The x-coordinate offset. + /// The y-coordinate offset. + public void Translate(double x, double y) + { + List points = this.vertices; + for (int i = 0; i < points.Count; i++) + { + points[i] += new Vertex(x, y); + } + } + + /// + /// Adds a vertex to the end of the vertices collection. + /// + /// The vertex to add. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(in Vertex vertex) => this.vertices.Add(vertex); + + /// + /// Removes the vertex at the specified index from the contour. + /// + /// The index of the vertex to remove. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RemoveVertexAt(int index) => this.vertices.RemoveAt(index); + + /// + /// Clears all vertices and holes from the contour. + /// + public void Clear() + { + this.vertices.Clear(); + this.holeIndices.Clear(); + } + + /// + /// Clears all holes from the contour. + /// + public void ClearHoles() => this.holeIndices.Clear(); + + /// + /// Gets the last vertex in the contour. + /// + /// The last in the contour. + public Vertex GetLastVertex() => this.vertices[^1]; + + /// + /// Adds a hole index to the contour. + /// + /// The index of the hole to add. + public void AddHoleIndex(int index) => this.holeIndices.Add(index); + + /// + /// Creates a deep copy of this contour. + /// + /// A detached contour copy. + public Contour DeepClone() + { + Contour clone = new(this.vertices.Count) + { + ParentIndex = this.ParentIndex, + Depth = this.Depth, + hasCachedOrientation = this.hasCachedOrientation, + cachedCounterClockwise = this.cachedCounterClockwise + }; + + clone.vertices.AddRange(this.vertices); + clone.holeIndices.AddRange(this.holeIndices); + + return clone; + } + + /// + public IEnumerator GetEnumerator() + => ((IEnumerable)this.vertices).GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() + => ((IEnumerable)this.vertices).GetEnumerator(); + } +} diff --git a/PolygonClipper/EdgeType.cs b/PolygonClipper/EdgeType.cs new file mode 100644 index 0000000..d429938 --- /dev/null +++ b/PolygonClipper/EdgeType.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.PolygonClipper { + /// + /// Specifies the type of an edge in a boolean operation on polygons. + /// + internal enum EdgeType + { + /// + /// A normal edge that contributes to the resulting polygon. + /// + Normal = 0, + + /// + /// An edge that does not contribute to the resulting polygon. + /// This typically occurs when the edge lies entirely inside another polygon. + /// + NonContributing = 1, + + /// + /// An edge that represents a transition within the same polygon, + /// meaning it does not cross into another polygon. + /// + SameTransition = 2, + + /// + /// An edge that represents a transition between different polygons, + /// meaning it crosses from one polygon to another. + /// + DifferentTransition = 3 + } +} diff --git a/PolygonClipper/FloatExtensions.cs b/PolygonClipper/FloatExtensions.cs new file mode 100644 index 0000000..d0047f7 --- /dev/null +++ b/PolygonClipper/FloatExtensions.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// Provides extension methods for floating-point numbers. + /// + internal static class FloatExtensions + { + /// + /// Returns the next representable double value in the direction of y. + /// + /// + /// The starting floating-point number. + /// The target floating-point number. + /// The next representable value of x towards y. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double NextAfter(this double x, double y) + { + // Special cases + if (double.IsNaN(x) || double.IsNaN(y)) + { + return double.NaN; + } + + if (x == y) + { + return y; + } + + if (double.IsPositiveInfinity(x)) + { + return double.PositiveInfinity; + } + + if (double.IsNegativeInfinity(x)) + { + return double.NegativeInfinity; + } + + // Handle stepping from zero + if (x == 0D) + { + return Math.CopySign(double.Epsilon, y); // Smallest positive subnormal double + } + + // Convert double to raw bits + long bits = BitConverter.DoubleToInt64Bits(x); + + // Adjust bits to get the next representable value + // Moving in the same sign direction + if ((y > x) == (x > 0D)) + { + bits++; + } + else + { + bits--; + } + + // Convert bits back to double + double next = BitConverter.Int64BitsToDouble(bits); + + // Ensure correct handling of signed zeros + if (next == 0D) + { + return Math.CopySign(next, x); + } + + return next; + } + } +} diff --git a/PolygonClipper/IntersectNode.cs b/PolygonClipper/IntersectNode.cs new file mode 100644 index 0000000..c8e56c0 --- /dev/null +++ b/PolygonClipper/IntersectNode.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.PolygonClipper { + /// + /// Represents a pending intersection between two active edges. + /// + /// + /// Intersections are sorted and processed from higher scanlines to lower ones so + /// that edge order in the AEL remains consistent as the sweep descends. + /// + internal readonly struct IntersectNode + { +#pragma warning disable SA1401 // Hot path intersection sorting benefits from field access. + /// + /// Gets the intersection point between and . + /// + public readonly Vertex Point; + + /// + /// Gets the first active edge participating in the intersection. + /// + public readonly ActiveEdge Edge1; + + /// + /// Gets the second active edge participating in the intersection. + /// + public readonly ActiveEdge Edge2; +#pragma warning restore SA1401 + + /// + /// Initializes a new instance of the struct. + /// + internal IntersectNode(Vertex point, ActiveEdge edge1, ActiveEdge edge2) + { + this.Point = point; + this.Edge1 = edge1; + this.Edge2 = edge2; + } + } +} diff --git a/PolygonClipper/JoinWith.cs b/PolygonClipper/JoinWith.cs new file mode 100644 index 0000000..3e17be3 --- /dev/null +++ b/PolygonClipper/JoinWith.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.PolygonClipper { + /// + /// Indicates whether an active edge should be joined with a neighbor after a split. + /// + internal enum JoinWith + { + /// + /// No pending join. + /// + None, + + /// + /// Join with the left neighbor in the AEL. + /// + Left, + + /// + /// Join with the right neighbor in the AEL. + /// + Right + } +} diff --git a/PolygonClipper/LineCap.cs b/PolygonClipper/LineCap.cs new file mode 100644 index 0000000..69ae451 --- /dev/null +++ b/PolygonClipper/LineCap.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.PolygonClipper { + /// + /// Specifies the shape to be used at the ends of open lines or paths when stroking. + /// + public enum LineCap + { + /// + /// The stroke ends exactly at the endpoint. + /// No extension is added beyond the path's end coordinates. + /// + Butt, + + /// + /// The stroke extends beyond the endpoint by half the line width, + /// producing a square edge. + /// + Square, + + /// + /// The stroke ends with a semicircular cap, + /// extending beyond the endpoint by half the line width. + /// + Round + } +} diff --git a/PolygonClipper/LineJoin.cs b/PolygonClipper/LineJoin.cs new file mode 100644 index 0000000..b4b693d --- /dev/null +++ b/PolygonClipper/LineJoin.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.PolygonClipper { + /// + /// Specifies how the connection between two consecutive line segments (a join) + /// is rendered when stroking paths or polygons. + /// + public enum LineJoin + { + /// + /// Joins lines by extending their outer edges until they meet at a sharp corner. + /// If the miter limit is exceeded, the join is truncated at the limit distance. + /// + Miter = 0, + + /// + /// Joins lines by extending their outer edges to form a miter. + /// If the miter limit is exceeded, the join falls back to a bevel. + /// + MiterRevert = 1, + + /// + /// Joins lines by connecting them with a circular arc centered at the join point, + /// producing a smooth, rounded corner. + /// + Round = 2, + + /// + /// Joins lines by connecting the outer corners directly with a straight line, + /// forming a flat edge at the join point. + /// + Bevel = 3, + + /// + /// Joins lines by forming a miter, but if the miter limit is exceeded, + /// the join falls back to a round join instead of a bevel. + /// + MiterRound = 4 + } +} diff --git a/PolygonClipper/LocalMinima.cs b/PolygonClipper/LocalMinima.cs new file mode 100644 index 0000000..0ec659b --- /dev/null +++ b/PolygonClipper/LocalMinima.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; + +namespace SixLabors.PolygonClipper { + /// + /// Describes the lowest vertex of an edge bound for the sweep line. + /// + internal readonly struct LocalMinima : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + internal LocalMinima(SweepVertex vertex) => this.Vertex = vertex; + + /// + /// Gets the vertex associated with this local minima. + /// + internal SweepVertex Vertex { get; } + + public static bool operator ==(LocalMinima lm1, LocalMinima lm2) => lm1.Equals(lm2); + + public static bool operator !=(LocalMinima lm1, LocalMinima lm2) => !(lm1 == lm2); + + public override bool Equals(object? obj) => obj is LocalMinima minima && this.Equals(minima); + + public override int GetHashCode() => this.Vertex.GetHashCode(); + + public bool Equals(LocalMinima other) => ReferenceEquals(this.Vertex, other.Vertex); + } + + /// + /// Orders local minima so higher Y-values are processed first during the sweep. + /// + internal sealed class LocalMinimaComparer : IComparer + { + public int Compare(LocalMinima locMin1, LocalMinima locMin2) + => locMin2.Vertex.Point.Y.CompareTo(locMin1.Vertex.Point.Y); + } +} diff --git a/PolygonClipper/OutputRecord.cs b/PolygonClipper/OutputRecord.cs new file mode 100644 index 0000000..0b90471 --- /dev/null +++ b/PolygonClipper/OutputRecord.cs @@ -0,0 +1,163 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.PolygonClipper { + /// + /// Captures a clipped contour, its topology, and its ownership hierarchy. + /// + internal sealed class OutputRecord + { + /// + /// Gets or sets the stable index assigned when the record is pooled. + /// + public int Index { get; set; } + + /// + /// Gets or sets the number of output points in the contour. + /// + public int OutputPointCount { get; set; } + + /// + /// Gets or sets the containing output record, if any. + /// + public OutputRecord? Owner { get; set; } + + /// + /// Gets or sets the front edge that defines the output orientation. + /// + public ActiveEdge? FrontEdge { get; set; } + + /// + /// Gets or sets the back edge that defines the output orientation. + /// + public ActiveEdge? BackEdge { get; set; } + + /// + /// Gets or sets the circular linked list of output points. + /// + public OutputPoint? Points { get; set; } + + /// + /// Gets or sets the cached bounds for ownership tests. + /// + public Box2 Bounds { get; set; } + + /// + /// Gets or sets the temporary contour used during bounds checks. + /// + public List Path { get; set; } = []; + + /// + /// Gets or sets split indices used to resolve complex self-intersections. + /// + public List? Splits { get; set; } + + /// + /// Gets or sets the cached split ownership used to avoid recursion. + /// + public OutputRecord? RecursiveSplit { get; set; } + } + + /// + /// Represents a vertex in the output contour linked list. + /// + internal sealed class OutputPoint + { +#pragma warning disable SA1401 // Hot output ring traversal uses fields to avoid accessor overhead. + /// + /// The vertex coordinate. + /// + public Vertex Point; + + /// + /// The next point in the linked list. + /// + public OutputPoint? Next; + + /// + /// The previous point in the linked list. + /// + public OutputPoint Prev; + + /// + /// The owning output record. + /// + public OutputRecord OutputRecord; + + /// + /// The horizontal segment reference used for joins. + /// + public HorizontalSegment? HorizontalSegment; +#pragma warning restore SA1401 + + /// + /// Initializes a new instance of the class. + /// + public OutputPoint(Vertex point, OutputRecord outputRecord) + { + this.Point = point; + this.OutputRecord = outputRecord; + this.Next = this; + this.Prev = this; + this.HorizontalSegment = null; + } + } + + /// + /// Captures a pending horizontal segment to be joined. + /// + internal sealed class HorizontalSegment + { + /// + /// Initializes a new instance of the class. + /// + public HorizontalSegment(OutputPoint op) + { + this.LeftPoint = op; + this.RightPoint = null; + this.LeftToRight = true; + } + + /// + /// Gets or sets the left-most point of the segment. + /// + public OutputPoint? LeftPoint { get; set; } + + /// + /// Gets or sets the right-most point of the segment. + /// + public OutputPoint? RightPoint { get; set; } + + /// + /// Gets or sets a value indicating whether the segment runs left-to-right. + /// + public bool LeftToRight { get; set; } + } + + /// + /// Stores a pair of horizontal edges to be joined. + /// + internal sealed class HorizontalJoin + { + /// + /// Initializes a new instance of the class. + /// + public HorizontalJoin(OutputPoint leftToRight, OutputPoint rightToLeft) + { + this.LeftToRight = leftToRight; + this.RightToLeft = rightToLeft; + } + + /// + /// Gets or sets the left-to-right point of the join. + /// + public OutputPoint? LeftToRight { get; set; } + + /// + /// Gets or sets the right-to-left point of the join. + /// + public OutputPoint? RightToLeft { get; set; } + } +} diff --git a/PolygonClipper/PathCommand.cs b/PolygonClipper/PathCommand.cs new file mode 100644 index 0000000..1dbe23e --- /dev/null +++ b/PolygonClipper/PathCommand.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.PolygonClipper { + /// + /// Encodes path commands used by the stroker path-emission pipeline. + /// + [Flags] + internal enum PathCommand : byte + { + /// + /// Marks the end of a command stream. + /// + Stop = 0, + + /// + /// Starts a new contour at the supplied vertex. + /// + MoveTo = 1, + + /// + /// Emits a line segment to the supplied vertex. + /// + LineTo = 2, + + /// + /// Terminates the current contour and applies path flags. + /// + EndPoly = 0x0F, + + /// + /// Bit mask for extracting the command portion of a value. + /// + Mask = 0x0F + } +} diff --git a/PolygonClipper/PathCommandExtensions.cs b/PolygonClipper/PathCommandExtensions.cs new file mode 100644 index 0000000..dcd1f16 --- /dev/null +++ b/PolygonClipper/PathCommandExtensions.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// Provides helper methods for querying values. + /// + internal static class PathCommandExtensions + { + /// + /// Returns whether the command emits a vertex coordinate. + /// + /// The command to evaluate. + /// + /// when the command is a vertex-emitting command; otherwise, . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Vertex(this PathCommand command) => command is >= PathCommand.MoveTo and < PathCommand.EndPoly; + + /// + /// Returns whether the command is . + /// + /// The command to evaluate. + /// if the command is stop; otherwise, . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Stop(this PathCommand command) => command == PathCommand.Stop; + + /// + /// Returns whether the command is . + /// + /// The command to evaluate. + /// if the command is move-to; otherwise, . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool MoveTo(this PathCommand command) => command == PathCommand.MoveTo; + + /// + /// Returns whether the masked command type is . + /// + /// The command to evaluate. + /// if the command type is end-poly; otherwise, . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool EndPoly(this PathCommand command) => (command & PathCommand.Mask) == PathCommand.EndPoly; + + /// + /// Extracts the close-path flag from the command. + /// + /// The command to evaluate. + /// The command value masked with . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetCloseFlag(this PathCommand command) => (int)command & (int)PathFlags.Close; + } +} diff --git a/PolygonClipper/PathFlags.cs b/PolygonClipper/PathFlags.cs new file mode 100644 index 0000000..6745db1 --- /dev/null +++ b/PolygonClipper/PathFlags.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.PolygonClipper { + /// + /// Flags that annotate path-termination commands. + /// + [Flags] + internal enum PathFlags : byte + { + /// + /// No path flags are set. + /// + None = 0, + + /// + /// Marks a counter-clockwise contour orientation. + /// + Ccw = 0x10, + + /// + /// Marks a clockwise contour orientation. + /// + Cw = 0x20, + + /// + /// Marks the contour as closed. + /// + Close = 0x40, + + /// + /// Bit mask for extracting the flag portion of a command value. + /// + Mask = 0xF0 + } +} diff --git a/PolygonClipper/PointInPolygonResult.cs b/PolygonClipper/PointInPolygonResult.cs new file mode 100644 index 0000000..eb0dbaf --- /dev/null +++ b/PolygonClipper/PointInPolygonResult.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.PolygonClipper { + /// + /// Describes the relationship between a point and a polygon. + /// + internal enum PointInPolygonResult + { + /// + /// The point lies on the polygon boundary. + /// + On = 0, + + /// + /// The point lies strictly inside the polygon. + /// + Inside = 1, + + /// + /// The point lies outside the polygon. + /// + Outside = 2 + } +} diff --git a/PolygonClipper/Polygon.cs b/PolygonClipper/Polygon.cs new file mode 100644 index 0000000..ea31085 --- /dev/null +++ b/PolygonClipper/Polygon.cs @@ -0,0 +1,192 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; + +namespace SixLabors.PolygonClipper { + /// + /// Represents a complex polygon. + /// +#pragma warning disable CA1710 // Identifiers should have correct suffix + public sealed class Polygon : IReadOnlyCollection +#pragma warning restore CA1710 // Identifiers should have correct suffix + { + /// + /// The collection of contours that make up the polygon. + /// + private readonly List contours; + + /// + /// Initializes a new instance of the class. + /// + public Polygon() + => this.contours = []; + + /// + /// Initializes a new instance of the class with a contour capacity. + /// + /// The initial contour capacity. + public Polygon(int capacity) + => this.contours = new List(capacity); + + /// + /// Gets the number of contours in the polygon. + /// + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.contours.Count; + } + + /// + /// Gets the total number of vertices across all contours in the polygon. + /// + /// The total vertex count. + public int VertexCount + { + get + { + int count = 0; + for (int i = 0; i < this.contours.Count; i++) + { + count += this.contours[i].Count; + } + + return count; + } + } + + /// + /// Gets the contour at the specified index. + /// + /// The index of the contour. + /// The at the given index. + public Contour this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.contours[index]; + } + + /// + /// Joins another polygon to this instance. + /// + /// The polygon to join. + public void Join(Polygon polygon) + { + int size = this.Count; + for (int i = 0; i < polygon.contours.Count; ++i) + { + Contour contour = polygon.contours[i]; + this.Add(contour); + this.GetLastContour().ClearHoles(); + + for (int j = 0; j < contour.HoleCount; ++j) + { + this.GetLastContour().AddHoleIndex(contour.GetHoleIndex(j) + size); + } + } + } + + /// + /// Gets the bounding box. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Box2 GetBoundingBox() + { + if (this.Count == 0) + { + return default; + } + + Box2 b = this.contours[0].GetBoundingBox(); + for (int i = 1; i < this.Count; i++) + { + b = b.Add(this.contours[i].GetBoundingBox()); + } + + return b; + } + + /// + /// Translates the polygon by the specified x and y values. + /// + /// The x-coordinate offset. + /// The y-coordinate offset. + public void Translate(double x, double y) + { + for (int i = 0; i < this.contours.Count; i++) + { + this.contours[i].Translate(x, y); + } + } + + /// + /// Adds a contour to the end of the contour collection. + /// + /// The contour to add. + public void Add(Contour contour) => this.contours.Add(contour); + + /// + /// Gets the last contour in the polygon. + /// + /// The last in the collection. + public Contour GetLastContour() => this.contours[^1]; + + /// + /// Clears all contours from the polygon. + /// + public void Clear() => this.contours.Clear(); + + /// + /// Creates a deep copy of this polygon and all of its contours. + /// + /// A detached polygon copy. + public Polygon DeepClone() + { + Polygon clone = new(this.contours.Count); + for (int i = 0; i < this.contours.Count; i++) + { + clone.contours.Add(this.contours[i].DeepClone()); + } + + return clone; + } + + /// + public IEnumerator GetEnumerator() + => ((IEnumerable)this.contours).GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() + => ((IEnumerable)this.contours).GetEnumerator(); + + /// + /// Creates a string useful for debugging. + /// + /// The . + public string ToDebugString() + { + StringBuilder stringBuilder = new(); + stringBuilder.AppendLine("["); + + foreach (Contour contour in this.contours) + { + stringBuilder.AppendLine(" ["); + foreach (Vertex vertex in contour) + { + stringBuilder.AppendLine(" new Vertex(" + vertex.X + ", " + vertex.Y + "),"); + } + + stringBuilder.AppendLine(" ],"); + } + + stringBuilder.AppendLine("];"); + + return stringBuilder.ToString(); + } + } +} diff --git a/PolygonClipper/PolygonClipper.cs b/PolygonClipper/PolygonClipper.cs new file mode 100644 index 0000000..440610e --- /dev/null +++ b/PolygonClipper/PolygonClipper.cs @@ -0,0 +1,1234 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.PolygonClipper { + /// + /// Performs boolean operations on polygons. + /// + /// + /// + /// This implementation follows the algorithm described in + /// "A Simple Algorithm for Boolean Operations on Polygons" by Francisco Martínez, + /// Carlos Ogayar, Juan R. Jiménez, and Antonio J. Rueda. + /// It supports intersection, union, difference, and symmetric difference (XOR). + /// + /// + /// It uses a sweep-line with an event queue to process segment intersections and robustly + /// handles special cases, including overlapping edges and trivial non-overlapping inputs. + /// + /// + /// The static boolean methods are the recommended entry points. They route work through + /// an internal thread-local pool of reusable clipper instances and automatically reset + /// temporary state between calls. + /// + /// + /// Instance members are not thread-safe for concurrent use. + /// + /// The high-level workflow has three stages: + /// + /// Preprocessing: Handles trivial operations and prepares segments for processing. + /// Sweeping: Processes events using a priority queue, handling segment insertions and removals. + /// Connecting edges: Constructs the resulting polygon by connecting valid segments. + /// + /// + public class PolygonClipper + { + // Keep a small per-thread hot set of clipper instances. Depth 4 covers common + // burst usage without retaining many heavyweight buffers per thread. + private const int MaxClipperPoolDepth = 4; + + // Upper bound for retained event/status capacities when returning to the pool. + // 131_072 (~128K) keeps normal workloads warm while dropping pathological runs. + private const int MaxRetainedEventCapacityScore = 131_072; + + [ThreadStatic] + private static Stack? clipperPool; + + private readonly SweepEventComparer comparer = new(); + private readonly List unorderedEventQueue = []; + private readonly SweepEventPoolList sortedEvents = []; + private readonly StatusLine statusLine = new(); + + private Polygon? subject; + private Polygon? clipping; + private BooleanOperation operation; + + /// + /// Initializes a new instance of the class. + /// + /// The polygon used as the left-hand operand. + /// The polygon used as the right-hand operand. + /// The boolean operation to execute. + /// + /// This constructor is intended for advanced/manual execution flows. + /// For typical usage, prefer the static methods to take advantage of + /// internal pooling and automatic lifecycle management. + /// + public PolygonClipper(Polygon subject, Polygon clip, BooleanOperation operation) + => this.Configure(subject, clip, operation); + + private PolygonClipper() + { + } + + /// + /// Computes the intersection of two polygons. + /// + /// The polygon used as the left-hand operand. + /// The polygon used as the right-hand operand. + /// A polygon containing regions common to both inputs. + /// Preferred entry point. Uses internal thread-local reusable instances. + public static Polygon Intersection(Polygon subject, Polygon clip) + { + PolygonClipper clipper = Rent(subject, clip, BooleanOperation.Intersection); + try + { + return clipper.Run(); + } + finally + { + Return(clipper); + } + } + + /// + /// Computes the union of two polygons. + /// + /// The polygon used as the left-hand operand. + /// The polygon used as the right-hand operand. + /// A polygon containing regions from either input. + /// Preferred entry point. Uses internal thread-local reusable instances. + public static Polygon Union(Polygon subject, Polygon clip) + { + PolygonClipper clipper = Rent(subject, clip, BooleanOperation.Union); + try + { + return clipper.Run(); + } + finally + { + Return(clipper); + } + } + + /// + /// Computes the difference of two polygons ( minus ). + /// + /// The polygon used as the left-hand operand. + /// The polygon used as the right-hand operand. + /// A polygon containing regions from not covered by . + /// Preferred entry point. Uses internal thread-local reusable instances. + public static Polygon Difference(Polygon subject, Polygon clip) + { + PolygonClipper clipper = Rent(subject, clip, BooleanOperation.Difference); + try + { + return clipper.Run(); + } + finally + { + Return(clipper); + } + } + + /// + /// Computes the symmetric difference (XOR) of two polygons. + /// + /// The polygon used as the left-hand operand. + /// The polygon used as the right-hand operand. + /// A polygon containing regions that belong to exactly one input. + /// Preferred entry point. Uses internal thread-local reusable instances. + public static Polygon Xor(Polygon subject, Polygon clip) + { + PolygonClipper clipper = Rent(subject, clip, BooleanOperation.Xor); + try + { + return clipper.Run(); + } + finally + { + Return(clipper); + } + } + + /// + /// Normalizes a polygon by resolving self-intersections and overlaps. + /// + /// The polygon to process. + /// + /// A new normalized polygon. Output contours are implicitly closed + /// (no duplicated terminal closing vertex is appended). + /// + /// Preferred entry point. Uses internal thread-local reusable builders. + public static Polygon Normalize(Polygon polygon) + => SelfIntersectionRemover.Process(polygon); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static PolygonClipper Rent(Polygon subject, Polygon clip, BooleanOperation operation) + { + Stack? pool = clipperPool; + if (pool != null && pool.Count > 0) + { + PolygonClipper clipper = pool.Pop(); + clipper.Configure(subject, clip, operation); + return clipper; + } + + return new PolygonClipper(subject, clip, operation); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Return(PolygonClipper clipper) + { + clipper.subject = null; + clipper.clipping = null; + clipper.unorderedEventQueue.Clear(); + clipper.sortedEvents.Clear(); + clipper.statusLine.Reset(0); + + // Avoid keeping very large event buffers alive in thread-static pools. + if (clipper.GetRetainedEventCapacityScore() > MaxRetainedEventCapacityScore) + { + return; + } + + Stack pool = clipperPool ??= new Stack(MaxClipperPoolDepth); + if (pool.Count < MaxClipperPoolDepth) + { + pool.Push(clipper); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Configure(Polygon subject, Polygon clip, BooleanOperation operation) + { + this.subject = subject; + this.clipping = clip; + this.operation = operation; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetRetainedEventCapacityScore() + => this.unorderedEventQueue.Capacity + this.sortedEvents.Capacity + this.statusLine.RetainedCapacity; + + /// + /// Executes the configured boolean operation for the current subject and clipping polygons. + /// + /// The operation result. + /// Instance execution is not thread-safe for concurrent use. + public Polygon Run() + { + // Compute bounding boxes for optimization steps 1 and 2 + Polygon subject = this.subject ?? throw new InvalidOperationException("Polygon clipper subject is not configured."); + Polygon clipping = this.clipping ?? throw new InvalidOperationException("Polygon clipper clipping polygon is not configured."); + BooleanOperation operation = this.operation; + + // Check for trivial cases that can be resolved without sweeping + if (TryTrivialOperationForEmptyPolygons(subject, clipping, operation, out Polygon? result)) + { + return result; + } + + // Process all segments in the subject polygon + Vertex min = new(double.PositiveInfinity); + Vertex max = new(double.NegativeInfinity); + + // Estimate the total number of sweep events. + // Each segment contributes two events (left/right endpoints), + // and subdivision during intersection may increase the count, + // so we conservatively double the total vertex count. + int subjectVertexCount = subject.VertexCount; + int clippingVertexCount = clipping.VertexCount; + int eventCount = (subjectVertexCount + clippingVertexCount) * 2; + + SweepEventComparer comparer = this.comparer; + List unorderedEventQueue = this.unorderedEventQueue; + unorderedEventQueue.Clear(); + if (eventCount > unorderedEventQueue.Capacity) + { + unorderedEventQueue.EnsureCapacity(eventCount); + } + + int contourId = 0; + + for (int i = 0; i < subject.Count; i++) + { + Contour contour = subject[i]; + contourId++; + int segmentCount = GetContourSegmentCount(contour); + for (int j = 0; j < segmentCount; j++) + { + ProcessSegment( + contourId, + contour.GetSegment(j), + PolygonType.Subject, + unorderedEventQueue, + comparer, + ref min, + ref max); + } + } + + Box2 subjectBB = new(min, max); + + // Process all segments in the clipping polygon + min = new Vertex(double.PositiveInfinity); + max = new Vertex(double.NegativeInfinity); + for (int i = 0; i < clipping.Count; i++) + { + Contour contour = clipping[i]; + + int segmentCount = GetContourSegmentCount(contour); + for (int j = 0; j < segmentCount; j++) + { + ProcessSegment( + contourId, + contour.GetSegment(j), + PolygonType.Clipping, + unorderedEventQueue, + comparer, + ref min, + ref max); + } + } + + Box2 clippingBB = new(min, max); + if (TryTrivialOperationForNonOverlappingBoundingBoxes(subject, clipping, subjectBB, clippingBB, operation, out result)) + { + return result; + } + + // Sweep line algorithm: process events in the priority queue + StablePriorityQueue eventQueue = new(comparer, unorderedEventQueue); + SweepEventPoolList sortedEvents = this.sortedEvents; + sortedEvents.Clear(); + if (eventCount > sortedEvents.Capacity) + { + sortedEvents.EnsureCapacity(eventCount); + } + + // Heuristic capacity for the sweep line status structure. + // At any given point during the sweep, only a subset of segments + // are active, so we preallocate half the subject's vertex count + // to reduce resizing without overcommitting memory. + StatusLine statusLine = this.statusLine; + statusLine.Reset(subjectVertexCount >> 1); + double subjectMaxX = subjectBB.Max.X; + double minMaxX = Vertex.Min(subjectBB.Max, clippingBB.Max).X; + + SweepEvent? prevEvent; + SweepEvent? nextEvent; + Span workspace = new SweepEvent[4]; + while (eventQueue.Count > 0) + { + SweepEvent sweepEvent = eventQueue.Dequeue(); + sortedEvents.Add(sweepEvent); + + // Optimization: skip further processing if intersection is impossible + if ((operation == BooleanOperation.Intersection && sweepEvent.Point.X > minMaxX) || + (operation == BooleanOperation.Difference && sweepEvent.Point.X > subjectMaxX)) + { + return ConnectEdges(sortedEvents, comparer); + } + + if (sweepEvent.Left) + { + // Insert the event into the status line and get neighbors + int it = statusLine.Add(sweepEvent); + prevEvent = statusLine.Prev(it); + nextEvent = statusLine.Next(it); + + // Compute fields for the current event + ComputeFields(sweepEvent, prevEvent, operation); + + // Check intersection with the next neighbor + if (nextEvent != null) + { + // Check intersection with the next neighbor + if (PossibleIntersection(sweepEvent, nextEvent, eventQueue, workspace) == 2) + { + ComputeFields(sweepEvent, prevEvent, operation); + ComputeFields(nextEvent, sweepEvent, operation); + } + } + + // Check intersection with the previous neighbor + if (prevEvent != null) + { + // Check intersection with the previous neighbor + if (PossibleIntersection(prevEvent, sweepEvent, eventQueue, workspace) == 2) + { + int prevIndex = statusLine.IndexOf(prevEvent); + SweepEvent? prevPrevEvent = statusLine.Prev(prevIndex); + ComputeFields(prevEvent, prevPrevEvent, operation); + ComputeFields(sweepEvent, prevEvent, operation); + } + } + } + else + { + // Remove the event from the status line + sweepEvent = sweepEvent.OtherEvent; + int it = statusLine.IndexOf(sweepEvent); + prevEvent = statusLine.Prev(it); + nextEvent = statusLine.Next(it); + + // Check intersection between neighbors + if (prevEvent != null && nextEvent != null) + { + _ = PossibleIntersection(prevEvent, nextEvent, eventQueue, workspace); + } + + statusLine.RemoveAt(it); + } + } + + // Connect edges after processing all events + return ConnectEdges(sortedEvents, comparer); + } + + /// + /// Gets the number of edges to process for a contour, treating non-closed rings as implicitly closed. + /// + /// The contour to inspect. + /// The number of segments to iterate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetContourSegmentCount(Contour contour) + { + int count = contour.Count; + if (count < 3) + { + return 0; + } + + return contour[0] == contour[^1] ? count - 1 : count; + } + + /// + /// Checks if the boolean operation is trivial due to one polygon having zero contours + /// and sets the result accordingly. + /// + /// The subject polygon. + /// The clipping polygon. + /// The boolean operation being performed. + /// The resulting polygon if the operation is trivial. + /// + /// if the operation results in a trivial case due to zero contours; + /// otherwise, . + /// + private static bool TryTrivialOperationForEmptyPolygons( + Polygon subject, + Polygon clipping, + BooleanOperation operation, + [NotNullWhen(true)] out Polygon? result) + { + result = null; + + if (subject.Count * clipping.Count == 0) + { + if (operation == BooleanOperation.Intersection) + { + result = []; + return true; + } + + if (operation == BooleanOperation.Difference) + { + result = subject.DeepClone(); + return true; + } + + if (operation is BooleanOperation.Union or BooleanOperation.Xor) + { + result = subject.Count == 0 ? clipping.DeepClone() : subject.DeepClone(); + return true; + } + } + + return false; + } + + /// + /// Checks if the boolean operation is trivial due to non-overlapping bounding boxes + /// and sets the result accordingly. + /// + /// The subject polygon. + /// The clipping polygon. + /// The bounding box of the subject polygon. + /// The bounding box of the clipping polygon. + /// The boolean operation being performed. + /// The resulting polygon if the operation is trivial. + /// + /// if the operation results in a trivial case due to non-overlapping + /// bounding boxes; otherwise, . + /// + private static bool TryTrivialOperationForNonOverlappingBoundingBoxes( + Polygon subject, + Polygon clipping, + Box2 subjectBB, + Box2 clippingBB, + BooleanOperation operation, + [NotNullWhen(true)] out Polygon? result) + { + result = null; + + if (subjectBB.Min.X > clippingBB.Max.X || clippingBB.Min.X > subjectBB.Max.X || + subjectBB.Min.Y > clippingBB.Max.Y || clippingBB.Min.Y > subjectBB.Max.Y) + { + if (operation == BooleanOperation.Intersection) + { + result = []; + return true; + } + + // The bounding boxes do not overlap + if (operation == BooleanOperation.Difference) + { + result = subject.DeepClone(); + return true; + } + + if (operation is BooleanOperation.Union or BooleanOperation.Xor) + { + result = new(subject.Count + clipping.Count); + result.Join(subject.DeepClone()); + result.Join(clipping.DeepClone()); + return true; + } + } + + return false; + } + + /// + /// Processes a segment by generating sweep events for its endpoints and adding them to the event queue. + /// + /// The identifier of the contour to which the segment belongs. + /// The segment to process. + /// The polygon type to which the segment belongs. + /// The unordered event queue to add the generated events to. + /// The comparer used to determine the order of sweep events in the queue. + /// The minimum vertex of the bounding box. + /// The maximum vertex of the bounding box. + private static void ProcessSegment( + int contourId, + Segment s, + PolygonType pt, + List eventQueue, + SweepEventComparer comparer, + ref Vertex min, + ref Vertex max) + { + if (s.Source == s.Target) + { + // Skip degenerate zero-length segments. + return; + } + + // Create sweep events for the endpoints of the segment + SweepEvent e1 = new(s.Source, true, pt); + SweepEvent e2 = new(s.Target, true, e1, pt); + e1.OtherEvent = e2; + e1.ContourId = e2.ContourId = contourId; + + // Determine which endpoint is the left endpoint + if (comparer.Compare(e1, e2) < 0) + { + e2.Left = false; + } + else + { + e1.Left = false; + } + + min = Vertex.Min(min, s.Min); + max = Vertex.Max(max, s.Max); + + // Add the events to the event queue + eventQueue.Add(e1); + eventQueue.Add(e2); + } + + /// + /// Computes fields for a given sweep event. + /// + /// The sweep event to compute fields for. + /// The the previous event in the status line. + /// The boolean operation being performed. + private static void ComputeFields(SweepEvent le, SweepEvent? prev, BooleanOperation operation) + { + // Compute inOut and otherInOut fields + if (prev == null) + { + le.InOut = false; + le.OtherInOut = true; + } + else if (le.PolygonType == prev.PolygonType) + { + // Previous line segment in sl belongs to the same polygon that "se" belongs to. + le.InOut = !prev.InOut; + le.OtherInOut = prev.OtherInOut; + } + else + { + // Previous line segment in sl belongs to a different polygon that "se" belongs to. + le.InOut = !prev.OtherInOut; + le.OtherInOut = prev.IsVertical() ? !prev.InOut : prev.InOut; + } + + // Compute PrevInResult field + if (prev != null) + { + le.PrevInResult = (!InResult(prev, operation) || prev.IsVertical()) + ? prev.PrevInResult + : prev; + } + + // Check if the line segment belongs to the Boolean operation + bool inResult = InResult(le, operation); + if (inResult) + { + le.ResultTransition = DetermineResultTransition(le, operation); + } + else + { + le.ResultTransition = ResultTransition.Neutral; + } + } + + /// + /// Determines the result transition state for a given sweep event based on the specified boolean operation. + /// + /// The sweep event to evaluate. + /// The boolean operation being performed (e.g., Intersection, Union, XOR, Difference). + /// + /// A value that represents the transition state of the event: + /// + /// if the event contributes to the result. + /// if the event does not contribute to the result. + /// if the event does not affect the transition but is part of the result. + /// + /// + /// Thrown if the boolean operation is invalid or unsupported. + private static ResultTransition DetermineResultTransition(SweepEvent sweepEvent, BooleanOperation operation) + { + bool thisIn = !sweepEvent.InOut; + bool thatIn = !sweepEvent.OtherInOut; + bool isIn; + + // Determine the "in" state based on the operation + switch (operation) + { + case BooleanOperation.Intersection: + isIn = thisIn && thatIn; + break; + case BooleanOperation.Union: + isIn = thisIn || thatIn; + break; + case BooleanOperation.Xor: + isIn = thisIn ^ thatIn; + break; + case BooleanOperation.Difference: + if (sweepEvent.PolygonType == PolygonType.Subject) + { + isIn = thisIn && !thatIn; + } + else + { + isIn = thatIn && !thisIn; + } + + break; + default: + throw new InvalidOperationException("Invalid boolean operation."); + } + + return isIn ? ResultTransition.Contributing : ResultTransition.NonContributing; + } + + /// + /// Determines if the given sweep event belongs to the result of the boolean operation. + /// + /// The sweep event to check. + /// The boolean operation being performed. + /// if the event belongs to the result; otherwise, . + private static bool InResult(SweepEvent sweepEvent, BooleanOperation operation) + => sweepEvent.EdgeType switch + { + EdgeType.Normal => operation switch + { + BooleanOperation.Intersection => !sweepEvent.OtherInOut, + BooleanOperation.Union => sweepEvent.OtherInOut, + BooleanOperation.Difference => + (sweepEvent.OtherInOut && sweepEvent.PolygonType == PolygonType.Subject) || + (!sweepEvent.OtherInOut && sweepEvent.PolygonType == PolygonType.Clipping), + BooleanOperation.Xor => true, + _ => false, + }, + EdgeType.NonContributing => false, + EdgeType.SameTransition => operation is BooleanOperation.Intersection or BooleanOperation.Union, + EdgeType.DifferentTransition => operation == BooleanOperation.Difference, + _ => false, + }; + + /// + /// Determines the possible intersection of two sweep line segments. + /// + /// The first sweep event representing a line segment. + /// The second sweep event representing a line segment. + /// The event queue to add new events to. + /// + /// A scratch space for temporary storage of sweep events. + /// Must be at least 4 elements long to hold the events for the two segments and their associated other events. + /// + /// + /// An integer indicating the result of the intersection: + /// + /// 0 if no intersection or trivial intersection at endpoints. + /// 1 if the segments intersect at a single point. + /// 2 if the segments overlap and share a left endpoint. + /// 3 if the segments partially overlap or one includes the other. + /// + /// + /// + /// Thrown when the line segments overlap but belong to the same polygon. + /// + private static int PossibleIntersection( + SweepEvent le1, + SweepEvent le2, + StablePriorityQueue eventQueue, + Span workspace) + { + if (le1.OtherEvent == null || le2.OtherEvent == null) + { + // No intersection possible. + return 0; + } + + // Point intersections + int nIntersections = PolygonUtilities.FindIntersection( + le1.GetSegment(), + le2.GetSegment(), + out Vertex ip1, + out Vertex _); // Currently unused but could be used to detect collinear overlapping segments + + if (nIntersections == 0) + { + // No intersection + return 0; + } + + // Ignore intersection if it occurs at the exact left or right endpoint of both segments + if (nIntersections == 1 && + (le1.Point == le2.Point || le1.OtherEvent.Point == le2.OtherEvent.Point)) + { + // Line segments intersect at an endpoint of both line segments + return 0; + } + + // If segments overlap and belong to the same polygon, ignore them + if (nIntersections == 2 && le1.PolygonType == le2.PolygonType) + { + return 0; + } + + // Handle a single intersection point + SweepEventComparer comparer = eventQueue.Comparer; + if (nIntersections == 1) + { + // If the intersection point is not an endpoint of le1 segment. + if (le1.Point != ip1 && le1.OtherEvent.Point != ip1) + { + DivideSegment(le1, ip1, eventQueue, comparer); + } + + // If the intersection point is not an endpoint of le2 segment. + if (le2.Point != ip1 && le2.OtherEvent.Point != ip1) + { + DivideSegment(le2, ip1, eventQueue, comparer); + } + + return 1; + } + + // The line segments associated with le1 and le2 overlap. + bool leftCoincide = le1.Point == le2.Point; + bool rightCoincide = le1.OtherEvent.Point == le2.OtherEvent.Point; + + // Populate the events. + // The working buffer has a length of 4, which is sufficient to hold the events + // for the two segments and their associated other events. + // Events are assigned in a specific order to avoid overwriting shared references. + ref SweepEvent wRef = ref MemoryMarshal.GetReference(workspace); + if (!leftCoincide) + { + if (comparer.Compare(le1, le2) > 0) + { + Unsafe.Add(ref wRef, 0u) = le2; + Unsafe.Add(ref wRef, 1u) = le1; + } + else + { + Unsafe.Add(ref wRef, 0u) = le1; + Unsafe.Add(ref wRef, 1u) = le2; + } + + // Positions 0 and 1 contain the left events of the segments. + // Positions 2 and 3 will contain the right events of the segments. + if (!rightCoincide) + { + Unsafe.Add(ref wRef, 2u) = le1.OtherEvent; + Unsafe.Add(ref wRef, 3u) = le2.OtherEvent; + } + else + { + Unsafe.Add(ref wRef, 2u) = le2.OtherEvent; + Unsafe.Add(ref wRef, 3u) = le1.OtherEvent; + } + } + else if (leftCoincide && !rightCoincide) + { + // Only the right endpoints differ, so we use positions 0 and 1 for their sorted order. + if (comparer.Compare(le1.OtherEvent, le2.OtherEvent) > 0) + { + Unsafe.Add(ref wRef, 0u) = le2.OtherEvent; + Unsafe.Add(ref wRef, 1u) = le1.OtherEvent; + } + else + { + Unsafe.Add(ref wRef, 0u) = le1.OtherEvent; + Unsafe.Add(ref wRef, 1u) = le2.OtherEvent; + } + } + + if (leftCoincide) + { + le2.EdgeType = EdgeType.NonContributing; + le1.EdgeType = (le2.InOut == le1.InOut) + ? EdgeType.SameTransition + : EdgeType.DifferentTransition; + + if (leftCoincide && !rightCoincide) + { + DivideSegment(Unsafe.Add(ref wRef, 1u).OtherEvent, Unsafe.Add(ref wRef, 0u).Point, eventQueue, comparer); + } + + return 2; + } + + if (rightCoincide) + { + // Since leftCoincide is false, the first two workspace slots contain distinct left events. + DivideSegment(Unsafe.Add(ref wRef, 0u), Unsafe.Add(ref wRef, 1u).Point, eventQueue, comparer); + return 3; + } + + // Handle general overlapping case + // At this point: workspace[0,1] = sorted left events, workspace[2,3] = sorted right events. + if (Unsafe.Add(ref wRef, 0u) != Unsafe.Add(ref wRef, 3u).OtherEvent) + { + DivideSegment(Unsafe.Add(ref wRef, 0u), Unsafe.Add(ref wRef, 1u).Point, eventQueue, comparer); + DivideSegment(Unsafe.Add(ref wRef, 1u), Unsafe.Add(ref wRef, 2u).Point, eventQueue, comparer); + return 3; + } + + // One segment fully contains the other + DivideSegment(Unsafe.Add(ref wRef, 0u), Unsafe.Add(ref wRef, 1u).Point, eventQueue, comparer); + DivideSegment(Unsafe.Add(ref wRef, 3u).OtherEvent, Unsafe.Add(ref wRef, 2u).Point, eventQueue, comparer); + return 3; + } + + /// + /// Divides the given segment at the specified point, creating two new segments. + /// + /// The left event representing the segment to divide. + /// The point at which to divide the segment. + /// The event queue to add the new events to. + /// The comparer used to sort the events. + private static void DivideSegment( + SweepEvent le, + Vertex p, + StablePriorityQueue eventQueue, + SweepEventComparer comparer) + { + if (le.OtherEvent == null) + { + return; + } + + SweepEvent re = le.OtherEvent; + + // The idea is to divide the segment based on the given `inter` coordinate as follows: + // + // (se_l)--------(r)(l)--------(re) + // + // Under normal circumstances the resulting events satisfy the conditions: + // + // se_l is before r, and l is before re. + // + // Since the intersection point computation is bounded to the interval [se_l.x, re.x] + // it is impossible for r/l to fall outside the interval. This leaves the corner cases: + // + // 1. r.x == se_l.x and r.y < se_l.y: This corresponds to the case where the first + // sub-segment becomes a perfectly vertical line. The problem is that vertical + // segments always have to be processed from bottom to top consistency. The + // theoretically correct event order would be r first (bottom), se_l later (top). + // However, se_l is the event just being processed, so there is no (easy) way of + // processing r before se_l. The easiest solution to the problem is to avoid it, + // by incrementing inter.x by one ULP. + // 2. l.x == re.x and l.y > re.y: This corresponds to the case where the second + // sub-segment becomes a perfectly vertical line, and because of the bottom-to-top + // convention for vertical segment, the order of l and re must be swapped. + // In this case swapping is not a problem, because both events are in the future. + // + // See also: https://github.com/21re/rust-geo-booleanop/pull/11 + + // Prevent from corner case 1 + if (p.X == le.Point.X && p.Y < le.Point.Y) + { + // The files are different in the two reference repositories but both fail. + p = new Vertex(p.X.NextAfter(double.PositiveInfinity), p.Y); + } + + // Create the right event for the left segment (new right endpoint) + SweepEvent r = new(p, false, le, le.PolygonType); + + // Create the left event for the right segment (new left endpoint) + SweepEvent l = new(p, true, re, le.PolygonType); + + // Assign the same contour ID to maintain connectivity + r.ContourId = l.ContourId = le.ContourId; + + // Corner case 2 can be accounted for by swapping l / se_r + if (comparer.Compare(l, re) > 0) + { + Debug.WriteLine("Rounding error detected: Adjusting left/right flags for event ordering."); + re.Left = true; + l.Left = false; + } + + // Update references to maintain correct linkage + re.OtherEvent = l; + le.OtherEvent = r; + + // Add the new events to the event queue + eventQueue.Enqueue(l); + eventQueue.Enqueue(r); + } + + /// + /// Connects edges in the result polygon by processing the sweep events + /// and constructing contours for the final result. + /// + /// The sorted list of sweep events. + /// The comparer used to sort the events. + /// The resulting . + private static Polygon ConnectEdges(SweepEventPoolList sortedEvents, SweepEventComparer comparer) + { + // Copy the events in the result polygon to resultEvents list + List resultEvents = new(sortedEvents.Count); + for (int i = 0; i < sortedEvents.Count; i++) + { + SweepEvent se = sortedEvents[i]; + if (se.Left && se.InResult) + { + resultEvents.Add(se); + } + else if (!se.Left && se.OtherEvent.InResult) + { + resultEvents.Add(se); + } + } + + // Due to overlapping edges, the resultEvents list may not be completely sorted + bool sorted = false; + while (!sorted) + { + sorted = true; + for (int i = 0; i < resultEvents.Count - 1; i++) + { + if (comparer.Compare(resultEvents[i], resultEvents[i + 1]) > 0) + { + (resultEvents[i], resultEvents[i + 1]) = (resultEvents[i + 1], resultEvents[i]); + sorted = false; + } + } + } + + // Assign positions to events + // The first loop ensures that every event gets its initial position based on its index in the list. + // This must be completed for all events before adjustments are made for right events to avoid inconsistent state. + for (int i = 0; i < resultEvents.Count; i++) + { + resultEvents[i].Pos = i; + } + + // Adjust positions for right events + // The second loop handles swapping positions for right events with their corresponding left events. + // This ensures that the `Pos` values are consistent between paired events after the initial assignment. + for (int i = 0; i < resultEvents.Count; i++) + { + SweepEvent sweepEvent = resultEvents[i]; + if (sweepEvent.Left) + { + (sweepEvent.OtherEvent.Pos, sweepEvent.Pos) = (sweepEvent.Pos, sweepEvent.OtherEvent.Pos); + } + } + + ReadOnlySpan iterationMap = PrecomputeIterationOrder(resultEvents); + + Polygon result = []; + Span processed = new bool[resultEvents.Count]; + for (int i = 0; i < resultEvents.Count; i++) + { + if (processed[i]) + { + continue; + } + + int contourId = result.Count; + Contour contour = InitializeContourFromContext(resultEvents[i], result, contourId); + + int pos = i; + Vertex initial = resultEvents[i].Point; + contour.Add(initial); + + // Main loop to process the contour + do + { + MarkProcessed(resultEvents[pos], processed, pos, contourId); + pos = resultEvents[pos].Pos; + + MarkProcessed(resultEvents[pos], processed, pos, contourId); + + contour.Add(resultEvents[pos].Point); + pos = NextPos(pos, processed, iterationMap, out bool found); + if (!found) + { + break; + } + } + while (resultEvents[pos].Point != initial); + + result.Add(contour); + } + + Polygon polygon = []; + + for (int i = 0; i < result.Count; i++) + { + Contour contour = result[i]; + if (contour.IsExternal) + { + // The exterior ring goes first + polygon.Add(contour); + + // Followed by holes if any + for (int j = 0; j < contour.HoleCount; j++) + { + int holeId = contour.GetHoleIndex(j); + polygon.Add(result[holeId]); + } + } + } + + return polygon; + } + + private static ReadOnlySpan PrecomputeIterationOrder(List data) + { + Span map = new int[data.Count]; + + int i = 0; + while (i < data.Count) + { + SweepEvent xRef = data[i]; + + // Find index range of R events + int rFrom = i; + while (i < data.Count && xRef.Point == data[i].Point && !data[i].Left) + { + i++; + } + + int rUptoExclusive = i; + + // Find index range of L events + int lFrom = i; + while (i < data.Count && xRef.Point == data[i].Point) + { + if (!data[i].Left) + { + throw new InvalidOperationException("Expected left event"); + } + + i++; + } + + int lUptoExclusive = i; + + bool hasREvents = rUptoExclusive > rFrom; + bool hasLEvents = lUptoExclusive > lFrom; + + if (hasREvents) + { + int rUpto = rUptoExclusive - 1; + + // Connect elements in [rFrom, rUpto) to larger index + for (int j = rFrom; j < rUpto; j++) + { + map[j] = j + 1; + } + + // Special handling of *last* element: Connect either the last L event + // or loop back to start of R events (if no L events). + if (hasLEvents) + { + map[rUpto] = lUptoExclusive - 1; + } + else + { + map[rUpto] = rFrom; + } + } + + if (hasLEvents) + { + int lUpto = lUptoExclusive - 1; + + // Connect elements in (lFrom, lUpto] to lower index + for (int j = lFrom + 1; j <= lUpto; j++) + { + map[j] = j - 1; + } + + // Special handling of *first* element: Connect either to the first R event + // or loop back to end of L events (if no R events). + if (hasREvents) + { + map[lFrom] = rFrom; + } + else + { + map[lFrom] = lUpto; + } + } + } + + return map; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void MarkProcessed(SweepEvent sweepEvent, Span processed, int pos, int contourId) + { + processed[pos] = true; + sweepEvent.OutputContourId = contourId; + } + + /// + /// Initializes a contour based on its context in relation to previous events and contours. + /// Implements the 4 cases of parent contours from the Martinez paper (Fig. 4). + /// + /// The current sweep event. + /// The collection of contours processed so far. + /// The ID for the new contour. + /// The initialized . + private static Contour InitializeContourFromContext(SweepEvent sweepEvent, Polygon polygon, int contourId) + { + Contour contour = []; + + // Check if there is a "previous in result" event + if (sweepEvent.PrevInResult != null) + { + SweepEvent prevInResult = sweepEvent.PrevInResult; + + // It is valid to query PrevInResult's outputContourId because it must have already been processed + int lowerContourId = prevInResult.OutputContourId; + ResultTransition lowerResultTransition = prevInResult.ResultTransition; + + if (lowerResultTransition > 0) + { + // We are inside. Check if the lower contour is a hole or an exterior contour. + Contour lowerContour = polygon[lowerContourId]; + + if (lowerContour.ParentIndex != null) + { + // The lower contour is a hole: Connect the new contour as a hole to its parent and use the same depth. + int parentContourId = lowerContour.ParentIndex.Value; + polygon[parentContourId].AddHoleIndex(contourId); + contour.ParentIndex = parentContourId; + contour.Depth = polygon[lowerContourId].Depth; + } + else + { + // The lower contour is an exterior contour: Connect the new contour as a hole and increment depth. + polygon[lowerContourId].AddHoleIndex(contourId); + contour.ParentIndex = lowerContourId; + contour.Depth = polygon[lowerContourId].Depth + 1; + } + } + else + { + // We are outside: This contour is an exterior contour of the same depth. + contour.ParentIndex = null; + contour.Depth = polygon[lowerContourId].Depth; + } + } + else + { + // There is no "previous in result" event: This contour is an exterior contour with depth 0. + contour.ParentIndex = null; + contour.Depth = 0; + } + + return contour; + } + + /// + /// Finds the next unprocessed position in the result events, either forward or backward, + /// starting from the given position. + /// + /// The current position in the result events. + /// A list indicating whether each event at the corresponding index has been processed. + /// A precomputed map that indicates the next position to check for unprocessed events. + /// A boolean indicating whether an unprocessed event was found. + /// The index of the next unprocessed position. + /// + /// This method searches forward from the current position until it finds an unprocessed event with + /// a different point or reaches the end of the list. If no such event is found, it searches backward + /// until it finds an unprocessed event. + /// + private static int NextPos( + int pos, + ReadOnlySpan processed, + ReadOnlySpan iterationMap, + out bool found) + { + int startPos = pos; + + while (true) + { + pos = iterationMap[pos]; + if (pos == startPos) + { + // Entire group is already processed? + found = false; + return int.MinValue; + } + + if (!processed[pos]) + { + found = true; + return pos; + } + } + } + } +} diff --git a/PolygonClipper/PolygonClipper.csproj b/PolygonClipper/PolygonClipper.csproj new file mode 100644 index 0000000..03959c7 --- /dev/null +++ b/PolygonClipper/PolygonClipper.csproj @@ -0,0 +1,38 @@ + + + +net10.0 + SixLabors.PolygonClipper + SixLabors.PolygonClipper + SixLabors.PolygonClipper + SixLabors.PolygonClipper + sixlabors.polygonclipper.128.png + LICENSE + https://github.com/SixLabors/PolygonClipper/ + $(RepositoryUrl) + + + Debug;Release + true + + + + + true + enable + + + + + 1.0 + + + + + + + + + + + diff --git a/PolygonClipper/PolygonStroker.cs b/PolygonClipper/PolygonStroker.cs new file mode 100644 index 0000000..9d6b361 --- /dev/null +++ b/PolygonClipper/PolygonStroker.cs @@ -0,0 +1,1250 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { +#pragma warning disable SA1201 // Elements should appear in the correct order + + /// + /// Generates polygonal stroke geometry for contours with configurable joins and caps. + /// + /// + /// This type performs two phases: + /// + /// Expand each source contour into one or two stroke-side outlines with joins/caps. + /// + /// Optionally resolve generated overlaps/self-intersections using + /// with positive fill semantics. + /// + /// + /// The emitted contours are implicitly closed (first vertex is not duplicated at the end). + /// + /// The static method is the recommended + /// entry point. It routes calls through an internal thread-local pool of reusable stroker + /// instances and automatically resets temporary state between calls. + /// + /// Instance members are not thread-safe for concurrent use. + /// + public sealed class PolygonStroker + { + // Numerical tolerances used while collapsing near-duplicate source points and + // while testing near-parallel line intersections. + private const double VertexDistanceEpsilon = 1E-14D; + private const double IntersectionEpsilon = 1E-30D; + private const double Pi = Math.PI; + private const double PiMul2 = Math.PI * 2D; + + // The inner miter limit used to clamp joins on acute interior angles. + private const double InnerMiterLimit = 1.01D; + + // Keep at most 2 warm instances per option-set (one active shape and one spare) + // to reduce churn without retaining many rarely reused configurations. + private const int MaxPooledStrokersPerOptions = 2; + + // Discard oversized scratch buffers so a single pathological stroke does not pin + // large arrays in thread-local pools for the lifetime of the thread. + private const int MaxRetainedScratchBytes = 256 * 1024; + + private static readonly StrokeOptions DefaultStrokeOptions = new(); + + [ThreadStatic] + private static Dictionary>? strokersByOptions; + + // Scratch buffers reused across contours to keep per-call allocations down. + private ArrayBuilder outVertices = new(1); + private ArrayBuilder srcVertices = new(16); + + // Streaming-state fields used by the Accumulate() state machine. + private int closed; + private int outVertex; + private Status prevStatus; + private int srcVertex; + private Status status; + private double strokeWidth = 0.5D; + private double widthAbs = 0.5D; + private double widthEps = 0.5D / 1024D; + private int widthSign = 1; + + /// + /// Initializes a new instance of the class with the specified stroke options. + /// + /// The stroke options. + /// Thrown when is null. + /// + /// This constructor is intended for advanced/manual usage. + /// For typical call patterns, prefer the static + /// method to use internal pooling automatically. + /// + public PolygonStroker(StrokeOptions options) + { + ArgumentNullException.ThrowIfNull(options); + this.NormalizeOutput = options.NormalizeOutput; + this.LineJoin = options.LineJoin; + this.LineCap = options.LineCap; + this.MiterLimit = options.MiterLimit; + this.ArcDetailScale = options.ArcDetailScale; + } + + /// + /// Internal state machine used by to stream stroked output vertices. + /// + private enum Status + { + /// Initial setup and input normalization. + Initial, + + /// Ready to emit the first command for the contour. + Ready, + + /// Emit start-cap vertices for open contours. + Cap1, + + /// Emit end-cap vertices for open contours. + Cap2, + + /// Emit joins for the first stroke side. + Outline1, + + /// Switch from first side to second side for closed paths. + CloseFirst, + + /// Emit joins for the second stroke side. + Outline2, + + /// Flush buffered vertices from the current join/cap computation. + OutVertices, + + /// Emit end-poly marker for first stroke side. + EndPoly1, + + /// Emit end-poly marker for second stroke side. + EndPoly2, + + /// Stop emitting commands. + Stop + } + + private readonly struct StrokeOptionsKey : IEquatable + { + public StrokeOptionsKey(StrokeOptions options) + { + this.NormalizeOutput = options.NormalizeOutput; + this.LineJoin = options.LineJoin; + this.LineCap = options.LineCap; + this.MiterLimit = options.MiterLimit; + this.ArcDetailScale = options.ArcDetailScale; + } + + public bool NormalizeOutput { get; } + + public LineJoin LineJoin { get; } + + public LineCap LineCap { get; } + + public double MiterLimit { get; } + + public double ArcDetailScale { get; } + + public bool Equals(StrokeOptionsKey other) + => this.NormalizeOutput == other.NormalizeOutput && + this.LineJoin == other.LineJoin && + this.LineCap == other.LineCap && + this.MiterLimit == other.MiterLimit && + this.ArcDetailScale == other.ArcDetailScale; + + public override bool Equals(object? obj) => obj is StrokeOptionsKey other && this.Equals(other); + + public override int GetHashCode() + => HashCode.Combine( + this.NormalizeOutput, + this.LineJoin, + this.LineCap, + this.MiterLimit, + this.ArcDetailScale); + } + + /// + /// Strokes with using optional + /// . + /// + /// Input polygon to stroke. + /// Stroke width. + /// + /// Stroke options controlling joins, caps and approximation behavior. + /// When null, default are used. + /// + /// The stroked polygon contours. + /// Thrown when is null. + /// Preferred entry point. Uses internal thread-local reusable instances. + public static Polygon Stroke(Polygon polygon, double width, StrokeOptions? options = null) + { + StrokeOptions effectiveOptions = options ?? DefaultStrokeOptions; + StrokeOptionsKey key = new(effectiveOptions); + PolygonStroker stroker = Rent(key, effectiveOptions); + try + { + stroker.Width = width; + return stroker.Stroke(polygon); + } + finally + { + Return(key, stroker); + } + } + + /// + /// Strokes using this instance's configured options and width. + /// + /// Input polygon to stroke. + /// The stroked polygon contours. + /// Thrown when is null. + /// Instance execution is not thread-safe for concurrent use. + public Polygon Stroke(Polygon polygon) + { + ArgumentNullException.ThrowIfNull(polygon); + if (polygon.Count == 0) + { + return []; + } + + Polygon allContours = new(Math.Max(2, polygon.Count * 2)); + for (int i = 0; i < polygon.Count; i++) + { + Contour contour = polygon[i]; + + // Close explicit or near-seam contours to avoid tiny stitch gaps, + // but keep clearly open polylines open so caps are emitted. + bool isClosed = IsContourClosedForEmission(contour, this.widthAbs * 2D); + Polygon stroked = this.ProcessPathToPolygon(contour, isClosed); + if (stroked.Count > 0) + { + allContours.Join(stroked); + } + } + + if (allContours.Count == 0) + { + return []; + } + + if (!this.NormalizeOutput) + { + return allContours; + } + + // Stroker emission already follows positive-fill assumptions, so skip + // extra input-orientation normalization and only resolve overlaps. + return SelfIntersectionRemover.Process(allContours, normalizeInputForPositiveFill: false); + } + + /// + /// Strokes after setting the current stroke width. + /// + /// Input polygon to stroke. + /// Stroke width. + /// The stroked polygon contours. + /// Instance execution is not thread-safe for concurrent use. + public Polygon Stroke(Polygon polygon, double width) + { + this.Width = width; + return this.Stroke(polygon); + } + + /// + /// Gets the miter limit used to clamp outer miter joins. + /// + public double MiterLimit { get; } + + /// + /// Gets the tessellation detail scale used for round joins and round caps. + /// Higher values produce more vertices and smoother curves. + /// + public double ArcDetailScale { get; } + + /// + /// Gets the outer line join style used for stroking corners. + /// + public LineJoin LineJoin { get; } + + /// + /// Gets the line cap style used for open path ends. + /// + public LineCap LineCap { get; } + + /// + /// Gets a value indicating whether generated contours should be normalized by resolving + /// self-intersections and overlaps. + /// + public bool NormalizeOutput { get; } + + /// + /// Gets or sets the stroke width. + /// + /// + /// Positive values produce conventional outward stroking. Negative values are supported + /// and flip the side orientation while preserving magnitude. + /// + public double Width + { + get => this.strokeWidth * 2D; + set + { + this.strokeWidth = value * 0.5D; + if (this.strokeWidth < 0D) + { + this.widthAbs = -this.strokeWidth; + this.widthSign = -1; + } + else + { + this.widthAbs = this.strokeWidth; + this.widthSign = 1; + } + + this.widthEps = this.strokeWidth / 1024D; + } + } + + /// + /// Converts a single contour into stroked polygon contours. + /// + /// The source contour. + /// Whether the contour should be emitted as closed. + /// The generated stroked contour set for this input contour. + private Polygon ProcessPathToPolygon(Contour contour, bool isClosed) + { + ArgumentNullException.ThrowIfNull(contour); + + int pointCount = contour.Count; + if (pointCount < 2) + { + return []; + } + + bool hasExplicitClosure = pointCount > 1 && contour[0] == contour[^1]; + if (isClosed && hasExplicitClosure) + { + // Keep one implicit closure path in the stroker state machine. + // Duplicate terminal vertices are re-added at final contour emission. + pointCount--; + } + + if (pointCount < 2) + { + return []; + } + + if (pointCount == 2) + { + Vertex p0 = contour[0]; + Vertex p1 = contour[1]; + if (Vertex.DistanceSquared(p0, p1) <= VertexDistanceEpsilon * VertexDistanceEpsilon) + { + // Degenerate segment behaves like a stroked point. + return [this.GeneratePointCap(p0.X, p0.Y)]; + } + } + + this.Reset(); + for (int i = 0; i < pointCount; i++) + { + Vertex point = contour[i]; + this.Add(point.X, point.Y, PathCommand.LineTo); + } + + if (isClosed) + { + this.ClosePath(); + } + + Polygon result = new(isClosed ? 2 : 1); + this.FinishPath(result); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static PolygonStroker Rent(in StrokeOptionsKey key, StrokeOptions options) + { + Dictionary>? pools = strokersByOptions; + if (pools != null && + pools.TryGetValue(key, out Stack? pool) && + pool.Count > 0) + { + return pool.Pop(); + } + + return new PolygonStroker(options); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Return(in StrokeOptionsKey key, PolygonStroker stroker) + { + stroker.ResetForReuse(); + + // Pool only compact instances; large retained buffers are intentionally dropped. + if (stroker.GetRetainedScratchBytes() > MaxRetainedScratchBytes) + { + return; + } + + Dictionary> pools = strokersByOptions ??= []; + if (!pools.TryGetValue(key, out Stack? pool)) + { + pool = new Stack(MaxPooledStrokersPerOptions); + pools[key] = pool; + } + + if (pool.Count < MaxPooledStrokersPerOptions) + { + pool.Push(stroker); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetRetainedScratchBytes() + => (this.outVertices.Capacity * Unsafe.SizeOf()) + + (this.srcVertices.Capacity * Unsafe.SizeOf()); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ResetForReuse() + { + this.outVertices.Clear(); + this.srcVertices.Clear(); + this.closed = 0; + this.outVertex = 0; + this.prevStatus = Status.Initial; + this.srcVertex = 0; + this.status = Status.Initial; + } + + /// + /// Returns whether a contour should be treated as closed when emitting stroke geometry. + /// + /// The contour to inspect. + /// Current stroke width. + /// if the contour should be treated as closed; otherwise . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsContourClosedForEmission(Contour contour, double strokeWidth) + { + int count = contour.Count; + if (count < 3) + { + return false; + } + + if (contour[0] == contour[^1]) + { + return true; + } + + Vertex delta = contour[0] - contour[^1]; + double closeThreshold = Math.Max(strokeWidth, 1E-3D); + return delta.LengthSquared() <= closeThreshold * closeThreshold; + } + + /// + /// Marks the current path as closed before finishing the outline. + /// + private void ClosePath() + { + this.closed = (int)PathFlags.Close; + this.status = Status.Initial; + } + + /// + /// Resets the stroker state for reuse. + /// + private void Reset() + { + // Reuse builders to avoid per-contour allocations. + this.srcVertices.Clear(); + this.outVertices.Clear(); + this.srcVertex = 0; + this.outVertex = 0; + this.closed = 0; + this.status = Status.Initial; + } + + /// + /// Consumes commands from and materializes final contour lists. + /// + /// Destination polygon that receives generated contours. + private void FinishPath(Polygon result) + { + Vertex current = default; + Vertex lastPoint = default; + bool hasLastPoint = false; + Contour? currentContour = null; + PathCommand command; + + while (!(command = this.Accumulate(ref current)).Stop()) + { + if (command.MoveTo()) + { + // Start a new contour. Commit any previous contour that is already complete. + if (currentContour is { Count: >= 3 }) + { + result.Add(currentContour); + } + + currentContour = new Contour(16); + hasLastPoint = false; + } + + if (command.Vertex()) + { + currentContour ??= new Contour(16); + + // Drop immediate duplicate vertices to avoid zero-length segments + // entering the intersection-removal pass. + if (!hasLastPoint || current != lastPoint) + { + currentContour.Add(current); + lastPoint = current; + hasLastPoint = true; + } + } + + if (command.EndPoly()) + { + if (currentContour is { Count: >= 3 }) + { + result.Add(currentContour); + } + + currentContour = null; + hasLastPoint = false; + } + } + + if (currentContour is { Count: >= 3 }) + { + result.Add(currentContour); + } + } + + /// + /// Adds a path command and coordinate into the source stream. + /// + /// X coordinate. + /// Y coordinate. + /// Path command associated with the coordinate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Add(double x, double y, PathCommand cmd) + { + this.status = Status.Initial; + if (cmd.MoveTo()) + { + // MoveTo starts a new source contour. + if (this.srcVertices.Length != 0) + { + this.srcVertices.RemoveLast(); + } + + this.Add(x, y); + } + else if (cmd.Vertex()) + { + this.Add(x, y); + } + else + { + // Non-vertex command updates close flags. + this.closed = cmd.GetCloseFlag(); + } + } + + /// + /// Appends a source vertex, collapsing trailing duplicates when needed. + /// + /// X coordinate. + /// Y coordinate. + /// Cached edge length hint. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Add(double x, double y, double distance = 0D) + { + if (this.srcVertices.Length > 1) + { + ref StrokeVertexDistance vd1 = ref this.srcVertices[^2]; + ref StrokeVertexDistance vd2 = ref this.srcVertices[^1]; + bool ret = vd1.Measure(vd2); + if (!ret && this.srcVertices.Length != 0) + { + // If the previous segment collapses, remove the duplicate tail. + this.srcVertices.RemoveLast(); + } + } + + this.srcVertices.Add(new StrokeVertexDistance(x, y, distance)); + } + + /// + /// Streams stroke output as path commands/vertices from the current source contour. + /// + /// Receives the emitted vertex when a vertex command is returned. + /// The next path command. + private PathCommand Accumulate(ref Vertex point) + { + ref ArrayBuilder src = ref this.srcVertices; + PathCommand cmd = PathCommand.LineTo; + while (!cmd.Stop()) + { + switch (this.status) + { + case Status.Initial: + // Normalize degenerate tail/head duplicates before any join math. + this.CloseVertexPath(this.closed != 0); + + if (src.Length < 3) + { + // Very short contours cannot be treated as closed reliably. + this.closed = 0; + } + + this.status = Status.Ready; + break; + + case Status.Ready: + // Require enough vertices for either open (2) or closed (3+) processing. + if (src.Length < 2 + (this.closed != 0 ? 1 : 0)) + { + cmd = PathCommand.Stop; + break; + } + + this.status = this.closed != 0 ? Status.Outline1 : Status.Cap1; + cmd = PathCommand.MoveTo; + this.srcVertex = 0; + this.outVertex = 0; + break; + + case Status.Cap1: + // Open path: emit start cap first. + ref StrokeVertexDistance start = ref src[0]; + ref StrokeVertexDistance startNext = ref src[1]; + this.CalcCap(ref start, ref startNext, start.Distance); + this.srcVertex = 1; + this.prevStatus = Status.Outline1; + this.status = Status.OutVertices; + this.outVertex = 0; + break; + + case Status.Cap2: + // Open path: emit terminal cap before reversing through side 2. + int lastIndex = src.Length - 1; + ref StrokeVertexDistance end = ref src[lastIndex]; + ref StrokeVertexDistance endPrev = ref src[lastIndex - 1]; + this.CalcCap(ref end, ref endPrev, endPrev.Distance); + this.prevStatus = Status.Outline2; + this.status = Status.OutVertices; + this.outVertex = 0; + break; + + case Status.Outline1: + int srcLength = src.Length; + if (this.closed != 0) + { + if (this.srcVertex >= srcLength) + { + // Closed path switches to second side through an explicit endpoly. + this.prevStatus = Status.CloseFirst; + this.status = Status.EndPoly1; + break; + } + } + else if (this.srcVertex >= srcLength - 1) + { + this.status = Status.Cap2; + break; + } + + // Emit join vertices for side 1 (forward traversal). + int index = this.srcVertex; + int prevIndex = index == 0 ? srcLength - 1 : index - 1; + int nextIndex = index + 1 == srcLength ? 0 : index + 1; + + ref StrokeVertexDistance prev = ref src[prevIndex]; + ref StrokeVertexDistance curr = ref src[index]; + ref StrokeVertexDistance next = ref src[nextIndex]; + this.CalcJoin( + ref prev, + ref curr, + ref next, + prev.Distance, + curr.Distance); + + this.srcVertex++; + this.prevStatus = this.status; + this.status = Status.OutVertices; + this.outVertex = 0; + break; + + case Status.CloseFirst: + // Start second side as a new contour command stream. + cmd = PathCommand.MoveTo; + this.status = Status.Outline2; + break; + + case Status.Outline2: + int srcLength2 = src.Length; + if (this.srcVertex <= (this.closed == 0 ? 1 : 0)) + { + this.status = Status.EndPoly2; + this.prevStatus = Status.Stop; + break; + } + + this.srcVertex--; + + // Emit join vertices for side 2 (reverse traversal). + int reverseIndex = this.srcVertex; + int reverseNextIndex = reverseIndex + 1 == srcLength2 ? 0 : reverseIndex + 1; + int reversePrevIndex = reverseIndex == 0 ? srcLength2 - 1 : reverseIndex - 1; + + ref StrokeVertexDistance reverseNext = ref src[reverseNextIndex]; + ref StrokeVertexDistance reverseCurr = ref src[reverseIndex]; + ref StrokeVertexDistance reversePrev = ref src[reversePrevIndex]; + this.CalcJoin( + ref reverseNext, + ref reverseCurr, + ref reversePrev, + reverseCurr.Distance, + reversePrev.Distance); + + this.prevStatus = this.status; + this.status = Status.OutVertices; + this.outVertex = 0; + break; + + case Status.OutVertices: + if (this.outVertex >= this.outVertices.Length) + { + // Re-enter previous phase once buffered join/cap points are flushed. + this.status = this.prevStatus; + } + else + { + point = this.outVertices[this.outVertex++]; + return cmd; + } + + break; + + case Status.EndPoly1: + this.status = this.prevStatus; + + // First side is emitted counter-clockwise. + return PathCommand.EndPoly | (PathCommand)(PathFlags.Close | PathFlags.Ccw); + + case Status.EndPoly2: + this.status = this.prevStatus; + + // Second side is emitted clockwise. + return PathCommand.EndPoly | (PathCommand)(PathFlags.Close | PathFlags.Cw); + + case Status.Stop: + cmd = PathCommand.Stop; + break; + } + } + + return cmd; + } + + /// + /// Removes duplicate tail/head points and optionally enforces closed-loop source topology. + /// + /// Whether closing normalization should be applied. + private void CloseVertexPath(bool close) + { + // Collapse duplicated trailing points while preserving a measured segment. + while (this.srcVertices.Length > 1) + { + ref StrokeVertexDistance vd1 = ref this.srcVertices[^2]; + ref StrokeVertexDistance vd2 = ref this.srcVertices[^1]; + bool ret = vd1.Measure(vd2); + + if (ret) + { + break; + } + + StrokeVertexDistance tail = this.srcVertices[^1]; + if (this.srcVertices.Length != 0) + { + this.srcVertices.RemoveLast(); + } + + if (this.srcVertices.Length != 0) + { + this.srcVertices.RemoveLast(); + } + + this.Add(tail.X, tail.Y, tail.Distance); + } + + if (!close) + { + return; + } + + // For closed paths, also remove zero-length seam between final and initial points. + while (this.srcVertices.Length > 1) + { + ref StrokeVertexDistance vd1 = ref this.srcVertices[^1]; + ref StrokeVertexDistance vd2 = ref this.srcVertices[0]; + bool ret = vd1.Measure(vd2); + + if (ret) + { + break; + } + + if (this.srcVertices.Length != 0) + { + this.srcVertices.RemoveLast(); + } + } + } + + /// + /// Emits interpolated arc vertices between two offset vectors around a join center. + /// + /// Join center X. + /// Join center Y. + /// First offset vector X. + /// First offset vector Y. + /// Second offset vector X. + /// Second offset vector Y. + private void CalcArc(double x, double y, double dx1, double dy1, double dx2, double dy2) + { + double strokeWidth = this.strokeWidth; + double a1 = Math.Atan2(dy1 * this.widthSign, dx1 * this.widthSign); + double a2 = Math.Atan2(dy2 * this.widthSign, dx2 * this.widthSign); + + // Derive angular step from arc detail scale and stroke radius. + double da = Math.Acos(this.widthAbs / (this.widthAbs + (0.125D / this.ArcDetailScale))) * 2D; + this.AddPoint(x + dx1, y + dy1); + + if (this.widthSign > 0) + { + if (a1 > a2) + { + a2 += PiMul2; + } + + // Sweep forward for positive widths. + int n = (int)((a2 - a1) / da); + da = (a2 - a1) / (n + 1); + a1 += da; + for (int i = 0; i < n; i++) + { + this.AddPoint(x + (Math.Cos(a1) * strokeWidth), y + (Math.Sin(a1) * strokeWidth)); + a1 += da; + } + } + else + { + if (a1 < a2) + { + a2 -= PiMul2; + } + + // Sweep backward for negative widths. + int n = (int)((a1 - a2) / da); + da = (a1 - a2) / (n + 1); + a1 -= da; + for (int i = 0; i < n; i++) + { + this.AddPoint(x + (Math.Cos(a1) * strokeWidth), y + (Math.Sin(a1) * strokeWidth)); + a1 -= da; + } + } + + this.AddPoint(x + dx2, y + dy2); + } + + /// + /// Emits miter/revert/round join geometry, including fallback behavior when intersection is unstable. + /// + /// Previous source vertex. + /// Current source vertex. + /// Next source vertex. + /// First offset vector X. + /// First offset vector Y. + /// Second offset vector X. + /// Second offset vector Y. + /// Requested line join mode. + /// Miter limit in stroke-width units. + /// Distance of bevel midpoint from join center. + private void CalcMiter( + ref StrokeVertexDistance v0, + ref StrokeVertexDistance v1, + ref StrokeVertexDistance v2, + double dx1, + double dy1, + double dx2, + double dy2, + LineJoin lineJoin, + double miterLimit, + double bevelDistance) + { + Vertex p0 = new(v0.X, v0.Y); + Vertex p1 = new(v1.X, v1.Y); + Vertex p2 = new(v2.X, v2.Y); + Vertex offset1 = new(dx1, -dy1); + Vertex offset2 = new(dx2, -dy2); + + double xi = v1.X; + double yi = v1.Y; + double intersectionDistance = 1D; + double limit = this.widthAbs * miterLimit; + bool miterLimitExceeded = true; + bool intersectionFailed = true; + + // Intersect the two offset support lines to obtain the geometric miter apex. + if (TryCalcIntersection( + p0 + offset1, + p1 + offset1, + p1 + offset2, + p2 + offset2, + out Vertex intersection)) + { + xi = intersection.X; + yi = intersection.Y; + intersectionDistance = Vertex.Distance(p1, intersection); + if (intersectionDistance <= limit) + { + this.AddPoint(xi, yi); + miterLimitExceeded = false; + } + + intersectionFailed = false; + } + else + { + // If lines are parallel/near-parallel, probe a fallback candidate. + double x2 = v1.X + dx1; + double y2 = v1.Y - dy1; + Vertex probe = new(x2, y2); + if ((CrossProduct(v0, v1, probe) < 0D) == + (CrossProduct(v1, v2, probe) < 0D)) + { + this.AddPoint(v1.X + dx1, v1.Y - dy1); + miterLimitExceeded = false; + } + } + + if (!miterLimitExceeded) + { + return; + } + + // Join-style-specific overflow behavior when the true miter exceeds limit. + switch (lineJoin) + { + case LineJoin.MiterRevert: + this.AddPoint(v1.X + dx1, v1.Y - dy1); + this.AddPoint(v1.X + dx2, v1.Y - dy2); + break; + + case LineJoin.MiterRound: + this.CalcArc(v1.X, v1.Y, dx1, -dy1, dx2, -dy2); + break; + + default: + if (intersectionFailed) + { + // No reliable apex: project a clipped bevel using local tangent/perpendicular vectors. + miterLimit *= this.widthSign; + this.AddPoint(v1.X + dx1 + (dy1 * miterLimit), v1.Y - dy1 + (dx1 * miterLimit)); + this.AddPoint(v1.X + dx2 - (dy2 * miterLimit), v1.Y - dy2 - (dx2 * miterLimit)); + } + else + { + // Blend from bevel corners toward true intersection to honor miter limit. + double x1 = v1.X + dx1; + double y1 = v1.Y - dy1; + double x2 = v1.X + dx2; + double y2 = v1.Y - dy2; + intersectionDistance = (limit - bevelDistance) / (intersectionDistance - bevelDistance); + this.AddPoint(x1 + ((xi - x1) * intersectionDistance), y1 + ((yi - y1) * intersectionDistance)); + this.AddPoint(x2 + ((xi - x2) * intersectionDistance), y2 + ((yi - y2) * intersectionDistance)); + } + + break; + } + } + + /// + /// Emits cap geometry for an open contour endpoint. + /// + /// Cap anchor vertex. + /// Adjacent source vertex used to determine tangent direction. + /// Length of the incident segment. + private void CalcCap(ref StrokeVertexDistance v0, ref StrokeVertexDistance v1, double len) + { + this.outVertices.Clear(); + double strokeWidth = this.strokeWidth; + if (len < VertexDistanceEpsilon) + { + this.AddPoint(v0.X, v0.Y); + this.AddPoint(v1.X, v1.Y); + return; + } + + double dx1 = (v1.Y - v0.Y) / len; + double dy1 = (v1.X - v0.X) / len; + double dx2 = 0D; + double dy2 = 0D; + + dx1 *= strokeWidth; + dy1 *= strokeWidth; + + if (this.LineCap != LineCap.Round) + { + if (this.LineCap == LineCap.Square) + { + // Square caps extend half-width in tangent direction. + dx2 = dy1 * this.widthSign; + dy2 = dx1 * this.widthSign; + } + + this.AddPoint(v0.X - dx1 - dx2, v0.Y + dy1 - dy2); + this.AddPoint(v0.X + dx1 - dx2, v0.Y - dy1 - dy2); + } + else + { + // Round cap emitted as half-circle arc around endpoint. + double da = Math.Acos(this.widthAbs / (this.widthAbs + (0.125D / this.ArcDetailScale))) * 2D; + int n = (int)(Pi / da); + da = Pi / (n + 1); + + this.AddPoint(v0.X - dx1, v0.Y + dy1); + if (this.widthSign > 0) + { + double a1 = Math.Atan2(dy1, -dx1) + da; + for (int i = 0; i < n; i++) + { + this.AddPoint(v0.X + (Math.Cos(a1) * strokeWidth), v0.Y + (Math.Sin(a1) * strokeWidth)); + a1 += da; + } + } + else + { + double a1 = Math.Atan2(-dy1, dx1) - da; + for (int i = 0; i < n; i++) + { + this.AddPoint(v0.X + (Math.Cos(a1) * strokeWidth), v0.Y + (Math.Sin(a1) * strokeWidth)); + a1 -= da; + } + } + + this.AddPoint(v0.X + dx1, v0.Y - dy1); + } + } + + /// + /// Emits join geometry for a source vertex using configured inner/outer join rules. + /// + /// Previous source vertex. + /// Current source vertex. + /// Next source vertex. + /// Length of segment v0-v1. + /// Length of segment v1-v2. + private void CalcJoin(ref StrokeVertexDistance v0, ref StrokeVertexDistance v1, ref StrokeVertexDistance v2, double len1, double len2) + { + const double eps = VertexDistanceEpsilon; + double strokeWidth = this.strokeWidth; + double widthAbs = this.widthAbs; + if (len1 < eps || len2 < eps) + { + this.outVertices.Clear(); + + // Degenerate neighborhood: use best available segment direction for both offsets. + double l1 = len1 >= eps ? len1 : len2; + double l2 = len2 >= eps ? len2 : len1; + double invL1 = strokeWidth / l1; + double invL2 = strokeWidth / l2; + + Vertex p0 = new(v0.X, v0.Y); + Vertex p1 = new(v1.X, v1.Y); + Vertex p2 = new(v2.X, v2.Y); + Vertex seg1 = p1 - p0; + Vertex seg2 = p2 - p1; + + double offX1 = seg1.Y * invL1; + double offY1 = seg1.X * invL1; + double offX2 = seg2.Y * invL2; + double offY2 = seg2.X * invL2; + + this.AddPoint(v1.X + offX1, v1.Y - offY1); + this.AddPoint(v1.X + offX2, v1.Y - offY2); + return; + } + + Vertex v0Vertex = new(v0.X, v0.Y); + Vertex v1Vertex = new(v1.X, v1.Y); + Vertex v2Vertex = new(v2.X, v2.Y); + Vertex segForward = v1Vertex - v0Vertex; + Vertex segNext = v2Vertex - v1Vertex; + double invLen1 = strokeWidth / len1; + double invLen2 = strokeWidth / len2; + double dx1 = segForward.Y * invLen1; + double dy1 = segForward.X * invLen1; + double dx2 = segNext.Y * invLen2; + double dy2 = segNext.X * invLen2; + this.outVertices.Clear(); + + // Cross-product sign classifies whether we are on an inner corner or outer corner + // relative to stroke direction. + double cp = Vertex.Cross(segNext, segForward); + if (Math.Abs(cp) > double.Epsilon && (cp > 0D) == (strokeWidth > 0D)) + { + double limit = Math.Min(len1, len2) / widthAbs; + if (limit < InnerMiterLimit) + { + limit = InnerMiterLimit; + } + + this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, LineJoin.MiterRevert, limit, 0D); + } + else + { + // Outer join path. + Vertex averageOffset = new Vertex(dx1 + dx2, dy1 + dy2) * 0.5D; + double bevelDistance = averageOffset.Length(); + + if (this.LineJoin is LineJoin.Round or LineJoin.Bevel && + this.ArcDetailScale * (this.widthAbs - bevelDistance) < this.widthEps) + { + // Near-collinear optimization: collapse to single intersection point when possible. + Vertex outerOffset1 = new(dx1, -dy1); + Vertex outerOffset2 = new(dx2, -dy2); + if (TryCalcIntersection( + v0Vertex + outerOffset1, + v1Vertex + outerOffset1, + v1Vertex + outerOffset2, + v2Vertex + outerOffset2, + out Vertex intersection)) + { + this.AddPoint(intersection.X, intersection.Y); + } + else + { + this.AddPoint(v1.X + dx1, v1.Y - dy1); + } + + return; + } + + switch (this.LineJoin) + { + case LineJoin.Miter: + case LineJoin.MiterRevert: + case LineJoin.MiterRound: + this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, this.LineJoin, this.MiterLimit, bevelDistance); + break; + + case LineJoin.Round: + this.CalcArc(v1.X, v1.Y, dx1, -dy1, dx2, -dy2); + break; + + default: + this.AddPoint(v1.X + dx1, v1.Y - dy1); + this.AddPoint(v1.X + dx2, v1.Y - dy2); + break; + } + } + } + + /// + /// Appends a computed output vertex to the current join/cap vertex buffer. + /// + /// X coordinate. + /// Y coordinate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddPoint(double x, double y) => this.outVertices.Add(new Vertex(x, y)); + + /// + /// Creates cap geometry for a single-point contour. + /// + /// Point X. + /// Point Y. + /// An implicitly closed contour representing the cap footprint. + private Contour GeneratePointCap(double x, double y) + { + if (this.LineCap == LineCap.Round) + { + // Emit a full circle when a contour collapses to a point. + double da = Math.Acos(this.widthAbs / (this.widthAbs + (0.125D / this.ArcDetailScale))) * 2D; + int n = Math.Max(4, (int)(PiMul2 / da)); + double angleStep = PiMul2 / n; + + Contour result = new(n); + for (int i = 0; i < n; i++) + { + double angle = i * angleStep; + result.Add(new Vertex( + x + (Math.Cos(angle) * this.strokeWidth), + y + (Math.Sin(angle) * this.strokeWidth))); + } + + return result; + } + + double w = this.strokeWidth; + Contour square = + [ + new Vertex(x - w, y - w), + new Vertex(x + w, y - w), + new Vertex(x + w, y + w), + new Vertex(x - w, y + w) + ]; + return square; + } + + /// + /// Computes the oriented area/cross-product used for turn classification. + /// + /// First segment start. + /// First segment end. + /// Third point. + /// Signed cross product value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double CrossProduct(in StrokeVertexDistance a, in StrokeVertexDistance b, in Vertex point) + => ((point.X - b.X) * (b.Y - a.Y)) - ((point.Y - b.Y) * (b.X - a.X)); + + /// + /// Computes line intersection for two infinite lines defined by segment endpoints. + /// + /// First line start. + /// First line end. + /// Second line start. + /// Second line end. + /// Receives the intersection point when available. + /// if lines intersect robustly; otherwise . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryCalcIntersection(in Vertex a, in Vertex b, in Vertex c, in Vertex d, out Vertex intersection) + { + Vertex ab = b - a; + Vertex cd = d - c; + double denominator = Vertex.Cross(ab, cd); + if (Math.Abs(denominator) < IntersectionEpsilon) + { + // Parallel or numerically unstable near-parallel lines. + intersection = default; + return false; + } + + double t = Vertex.Cross(c - a, cd) / denominator; + intersection = a + (ab * t); + return true; + } + } + +#pragma warning restore SA1201 // Elements should appear in the correct order +} diff --git a/PolygonClipper/PolygonType.cs b/PolygonClipper/PolygonType.cs new file mode 100644 index 0000000..fcfeaa6 --- /dev/null +++ b/PolygonClipper/PolygonType.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.PolygonClipper { + /// + /// Specifies the type of a polygon in a boolean operation. + /// + internal enum PolygonType + { + /// + /// Represents the subject polygon in a boolean operation. + /// + Subject = 0, + + /// + /// Represents the clipping polygon in a boolean operation. + /// + Clipping = 1 + } +} diff --git a/PolygonClipper/PolygonUtilities.cs b/PolygonClipper/PolygonUtilities.cs new file mode 100644 index 0000000..15fff0e --- /dev/null +++ b/PolygonClipper/PolygonUtilities.cs @@ -0,0 +1,860 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// Provides utility methods for performing geometric calculations related to polygons, such as calculating signed areas + /// and finding intersections of line segments. + /// + internal static class PolygonUtilities + { + /// + /// Returns the signed area of a triangle. + /// + /// The first point. + /// The second point. + /// The third point. + /// The area. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double SignedArea(in Vertex p0, in Vertex p1, in Vertex p2) + => Vertex.Cross(p0 - p2, p1 - p2); + + /// + /// Finds the intersection of two line segments, constraining results to their intersection bounding box. + /// + /// The first segment. + /// The second segment. + /// The first intersection point. + /// The second intersection point (if overlap occurs). + /// + /// An indicating the number of intersection points: + /// - Returns 0 if there is no intersection. + /// - Returns 1 if the segments intersect at a single point. + /// - Returns 2 if the segments overlap. + /// + public static int FindIntersection(in Segment seg0, in Segment seg1, out Vertex pi0, out Vertex pi1) + { + pi0 = default; + pi1 = default; + + if (!TryGetIntersectionBoundingBox(seg0.Source, seg0.Target, seg1.Source, seg1.Target, out Box2? bbox)) + { + return 0; + } + + int interResult = FindIntersectionImpl(seg0, seg1, out pi0, out pi1); + + if (interResult == 1) + { + pi0 = ConstrainToBoundingBox(pi0, bbox.Value); + } + else if (interResult == 2) + { + pi0 = ConstrainToBoundingBox(pi0, bbox.Value); + pi1 = ConstrainToBoundingBox(pi1, bbox.Value); + } + + return interResult; + } + + /// + /// Finds the intersection of two line segments. + /// + /// The first line segment. + /// The second line segment. + /// + /// The first intersection point (if any). If the segments intersect at a single point, this will contain the intersection point. + /// If the segments overlap, this will contain the start of the overlapping segment. + /// + /// + /// The second intersection point (if any). If the segments overlap, this will contain the end of the overlapping segment. + /// + /// + /// An indicating the number of intersection points: + /// - Returns 0 if there is no intersection. + /// - Returns 1 if the segments intersect at a single point. + /// - Returns 2 if the segments overlap. + /// + private static int FindIntersectionImpl(in Segment seg0, in Segment seg1, out Vertex pi0, out Vertex pi1) + { + pi0 = default; + pi1 = default; + + Vertex a1 = seg0.Source; + Vertex a2 = seg1.Source; + + Vertex va = seg0.Target - a1; + Vertex vb = seg1.Target - a2; + Vertex e = a2 - a1; + + double kross = Vertex.Cross(va, vb); + double sqrKross = kross * kross; + double sqrLenA = Vertex.Dot(va, va); + + if (sqrKross > 0) + { + // Lines of the segments are not parallel + double s = Vertex.Cross(e, vb) / kross; + if (s is < 0 or > 1) + { + return 0; + } + + double t = Vertex.Cross(e, va) / kross; + if (t is < 0 or > 1) + { + return 0; + } + + // If s or t is exactly 0 or 1, the intersection is on an endpoint + if (s is 0 or 1) + { + // On an endpoint of line segment a + pi0 = MidPoint(a1, s, va); + return 1; + } + + if (t is 0 or 1) + { + // On an endpoint of line segment b + pi0 = MidPoint(a2, t, vb); + return 1; + } + + // Intersection of lines is a point on each segment + pi0 = a1 + (s * va); + return 1; + } + + // Lines are parallel; check if they are collinear + kross = Vertex.Cross(e, va); + sqrKross = kross * kross; + if (sqrKross > 0) + { + // Lines of the segments are different + return 0; + } + + // Segments are collinear, check for overlap + double sa = Vertex.Dot(va, e) / sqrLenA; + double sb = sa + (Vertex.Dot(va, vb) / sqrLenA); + double smin = Math.Min(sa, sb); + double smax = Math.Max(sa, sb); + + if (smin <= 1 && smax >= 0) + { + if (smin == 1) + { + pi0 = MidPoint(a1, smin, va); + return 1; + } + + if (smax == 0) + { + pi0 = MidPoint(a1, smax, va); + return 1; + } + + pi0 = MidPoint(a1, Math.Max(smin, 0), va); + pi1 = MidPoint(a1, Math.Min(smax, 1), va); + return 2; + } + + return 0; + } + + /// + /// Computes the bounding box of the intersection area of two line segments. + /// + /// The first point of the first segment. + /// The second point of the first segment. + /// The first point of the second segment. + /// The second point of the second segment. + /// The intersection bounding box if one exists, otherwise null. + /// + /// if the segments intersect; otherwise, . + /// + private static bool TryGetIntersectionBoundingBox( + in Vertex a1, + in Vertex a2, + in Vertex b1, + in Vertex b2, + [NotNullWhen(true)] out Box2? result) + { + Vertex minA = Vertex.Min(a1, a2); + Vertex maxA = Vertex.Max(a1, a2); + Vertex minB = Vertex.Min(b1, b2); + Vertex maxB = Vertex.Max(b1, b2); + + Vertex interMin = Vertex.Max(minA, minB); + Vertex interMax = Vertex.Min(maxA, maxB); + + if (interMin.X <= interMax.X && interMin.Y <= interMax.Y) + { + result = new Box2(interMin, interMax); + return true; + } + + result = null; + return false; + } + + /// + /// Constrains a point to the given bounding box. + /// + /// The point to constrain. + /// The bounding box. + /// The constrained point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vertex ConstrainToBoundingBox(in Vertex p, in Box2 bbox) + => Vertex.Min(Vertex.Max(p, bbox.Min), bbox.Max); + + /// + /// Computes the point at a given fractional distance adouble a directed line segment. + /// + /// The starting vertex of the segment. + /// The scalar factor representing the fractional distance adouble the segment. + /// The direction vector of the segment. + /// The interpolated vertex at the given fractional distance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vertex MidPoint(in Vertex p, double s, in Vertex d) => p + (s * d); + + /// + /// Returns the dot product of the vectors AB and BC. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Dot(in Vertex a, in Vertex b, in Vertex c) + => Vertex.Dot(b - a, c - b); + + /// + /// Returns the cross product of the vectors AB and BC. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Cross(in Vertex a, in Vertex b, in Vertex c) + => Vertex.Cross(b - a, c - b); + + /// + /// Returns the sign of the cross product of the vectors AB and BC. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CrossSign(in Vertex a, in Vertex b, in Vertex c) + { + double crossValueInt = Cross(a, b, c); + if (crossValueInt == 0) + { + return 0; + } + + return crossValueInt > 0 ? 1 : -1; + } + + /// + /// Returns true when three vertices are collinear. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsCollinear(in Vertex a, in Vertex shared, in Vertex b) + => CrossSign(a, shared, b) == 0; + + /// + /// Computes the signed area of a contour. + /// + public static double Area(List path) + { + int count = path.Count; + if (count < 3) + { + return 0D; + } + + double area = 0; + Vertex prev = path[count - 1]; + for (int i = 0; i < count; i++) + { + Vertex current = path[i]; + area += (prev.Y + current.Y) * (prev.X - current.X); + prev = current; + } + + return area * 0.5D; + } + + /// + /// Computes the squared perpendicular distance from a point to a line segment. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double PerpendicularDistanceSquared(in Vertex point, in Vertex line1, in Vertex line2) + { + Vertex toPoint = point - line1; + Vertex direction = line2 - line1; + double lengthSquared = Vertex.Dot(direction, direction); + if (lengthSquared == 0D) + { + return 0D; + } + + double cross = Vertex.Cross(toPoint, direction); + return (cross * cross) / lengthSquared; + } + + /// + /// Finds the intersection of two line segments, including endpoints. + /// + public static bool TryGetLineIntersection( + in Vertex a1, + in Vertex a2, + in Vertex b1, + in Vertex b2, + out Vertex intersection) + { + double dy1 = a2.Y - a1.Y; + double dx1 = a2.X - a1.X; + double dy2 = b2.Y - b1.Y; + double dx2 = b2.X - b1.X; + double det = (dy1 * dx2) - (dy2 * dx1); + if (det == 0D) + { + intersection = default; + return false; + } + + double t = (((a1.X - b1.X) * dy2) - ((a1.Y - b1.Y) * dx2)) / det; + if (t <= 0D) + { + intersection = a1; + return true; + } + + if (t >= 1D) + { + intersection = a2; + return true; + } + + intersection = new Vertex(a1.X + (t * dx1), a1.Y + (t * dy1)); + + return true; + } + + /// + /// Projects a point onto a segment and returns the closest point. + /// + public static Vertex ClosestPointOnSegment(in Vertex point, in Vertex seg1, in Vertex seg2) + { + if (seg1 == seg2) + { + return seg1; + } + + double dx = seg2.X - seg1.X; + double dy = seg2.Y - seg1.Y; + double q = (((point.X - seg1.X) * dx) + ((point.Y - seg1.Y) * dy)) / ((dx * dx) + (dy * dy)); + + // Clamp to segment bounds so we always return the closest point on the finite segment. + q = Math.Clamp(q, 0D, 1D); + return new Vertex(seg1.X + (q * dx), seg1.Y + (q * dy)); + } + + /// + /// Returns true when two segments intersect. + /// + public static bool SegmentsIntersect(in Vertex a1, in Vertex a2, in Vertex b1, in Vertex b2, bool inclusive = false) + { + // Uses cross-product tests to solve a1 + d1 * t == b1 + d2 * u. + // cp is the denominator (cross of directions); cp == 0 means parallel/collinear. + Vertex d1 = a2 - a1; + Vertex d2 = b2 - b1; + double cp = Vertex.Cross(d2, d1); + if (cp == 0) + { + return false; + } + + if (inclusive) + { + // Inclusive mode allows intersections at endpoints. + double t = Vertex.Cross(a1 - b1, d2); + if (t == 0) + { + return true; + } + + if (t > 0) + { + if (cp < 0 || t > cp) + { + // t outside [0, cp] once sign is normalized. + return false; + } + } + else if (cp > 0 || t < cp) + { + return false; + } + + t = Vertex.Cross(a1 - b1, d1); + if (t == 0) + { + return true; + } + + if (t > 0) + { + // t within bounds for the second segment. + return cp > 0 && t <= cp; + } + + return cp < 0 && t >= cp; + } + + // Exclusive mode requires the intersection to be strictly inside both segments. + double t2 = Vertex.Cross(a1 - b1, d2); + if (t2 == 0) + { + return false; + } + + if (t2 > 0) + { + if (cp < 0 || t2 >= cp) + { + // Reject if t2 is outside the open interval. + return false; + } + } + else if (cp > 0 || t2 <= cp) + { + return false; + } + + t2 = Vertex.Cross(a1 - b1, d1); + if (t2 == 0) + { + return false; + } + + if (t2 > 0) + { + // Both parameters are inside open intervals. + return cp > 0 && t2 < cp; + } + + return cp < 0 && t2 > cp; + } + + /// + /// Computes the bounding box of a contour. + /// + public static Box2 GetBounds(List path) + { + if (path.Count == 0) + { + return default; + } + + double minX = double.MaxValue; + double minY = double.MaxValue; + double maxX = double.MinValue; + double maxY = double.MinValue; + + for (int i = 0; i < path.Count; i++) + { + Vertex pt = path[i]; + if (pt.X < minX) + { + minX = pt.X; + } + + if (pt.X > maxX) + { + maxX = pt.X; + } + + if (pt.Y < minY) + { + minY = pt.Y; + } + + if (pt.Y > maxY) + { + maxY = pt.Y; + } + } + + if (minX == double.MaxValue) + { + return default; + } + + return new Box2(new Vertex(minX, minY), new Vertex(maxX, maxY)); + } + + /// + /// Returns the midpoint of a contour's bounding box. + /// + private static Vertex GetBoundsMidPoint(List path) => GetBounds(path).MidPoint(); + + /// + /// Determines whether a point is inside a contour. + /// + public static PointInPolygonResult PointInPolygon(in Vertex point, List polygon) + { + int len = polygon.Count; + int start = 0; + if (len < 3) + { + return PointInPolygonResult.Outside; + } + + while (start < len && polygon[start].Y == point.Y) + { + start++; + } + + if (start == len) + { + return PointInPolygonResult.Outside; + } + + bool isAbove = polygon[start].Y < point.Y; + bool startingAbove = isAbove; + int val = 0; + int i = start + 1; + int end = len; + while (true) + { + if (i == end) + { + if (end == 0 || start == 0) + { + break; + } + + end = start; + i = 0; + } + + if (isAbove) + { + while (i < end && polygon[i].Y < point.Y) + { + i++; + } + } + else + { + while (i < end && polygon[i].Y > point.Y) + { + i++; + } + } + + if (i == end) + { + continue; + } + + Vertex curr = polygon[i]; + Vertex prev = i > 0 ? polygon[i - 1] : polygon[len - 1]; + + if (curr.Y == point.Y) + { + if (curr.X == point.X || + (curr.Y == prev.Y && ((point.X < prev.X) != (point.X < curr.X)))) + { + return PointInPolygonResult.On; + } + + i++; + if (i == start) + { + break; + } + + continue; + } + + if (point.X < curr.X && point.X < prev.X) + { + // no-op + } + else if (point.X > prev.X && point.X > curr.X) + { + val = 1 - val; + } + else + { + int cps2 = CrossSign(prev, curr, point); + if (cps2 == 0) + { + return PointInPolygonResult.On; + } + + if ((cps2 < 0) == isAbove) + { + val = 1 - val; + } + } + + isAbove = !isAbove; + i++; + } + + if (isAbove == startingAbove) + { + return val == 0 ? PointInPolygonResult.Outside : PointInPolygonResult.Inside; + } + + if (i == len) + { + i = 0; + } + + int cps = i == 0 + ? CrossSign(polygon[len - 1], polygon[0], point) + : CrossSign(polygon[i - 1], polygon[i], point); + + if (cps == 0) + { + return PointInPolygonResult.On; + } + + if ((cps < 0) == isAbove) + { + val = 1 - val; + } + + return val == 0 ? PointInPolygonResult.Outside : PointInPolygonResult.Inside; + } + + /// + /// Returns true if the outer contour contains the inner contour. + /// + private static bool PathContainsPath(List inner, List outer) + { + PointInPolygonResult pip = PointInPolygonResult.On; + for (int i = 0; i < inner.Count; i++) + { + switch (PointInPolygon(inner[i], outer)) + { + case PointInPolygonResult.Outside: + if (pip == PointInPolygonResult.Outside) + { + return false; + } + + pip = PointInPolygonResult.Outside; + break; + case PointInPolygonResult.Inside: + if (pip == PointInPolygonResult.Inside) + { + return true; + } + + pip = PointInPolygonResult.Inside; + break; + default: + break; + } + } + + Vertex midpoint = GetBoundsMidPoint(inner); + return PointInPolygon(midpoint, outer) != PointInPolygonResult.Outside; + } + + /// + /// Returns true if the outer contour contains the inner contour. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Path2ContainsPath1(List inner, List outer) => PathContainsPath(inner, outer); + + /// + /// Finds the intersection of two line segments, constraining results to their intersection bounding box. + /// + /// The first point of the first segment. + /// The second point of the first segment. + /// The first point of the second segment. + /// The second point of the second segment. + /// The first intersection point. + /// The second intersection point (if overlap occurs). + /// + /// An indicating the number of intersection points: + /// - Returns 0 if there is no intersection. + /// - Returns 1 if the segments intersect at a single point. + /// - Returns 2 if the segments overlap. + /// + public static int FindIntersection(in Vertex a1, in Vertex a2, in Vertex b1, in Vertex b2, out Vertex pi0, out Vertex pi1) + { + pi0 = default; + pi1 = default; + + if (!TryGetIntersectionBoundingBox(a1, a2, b1, b2, out Box2 bbox)) + { + return 0; + } + + int interResult = FindIntersectionImpl(a1, a2, b1, b2, out pi0, out pi1); + + if (interResult == 1) + { + pi0 = ConstrainToBoundingBox(pi0, bbox); + } + else if (interResult == 2) + { + pi0 = ConstrainToBoundingBox(pi0, bbox); + pi1 = ConstrainToBoundingBox(pi1, bbox); + } + + return interResult; + } + + /// + /// Finds the intersection of two line segments. + /// + /// The first point of the first segment. + /// The second point of the first segment. + /// The first point of the second segment. + /// The second point of the second segment. + /// + /// The first intersection point (if any). If the segments intersect at a single point, this will contain the intersection point. + /// If the segments overlap, this will contain the start of the overlapping segment. + /// + /// + /// The second intersection point (if any). If the segments overlap, this will contain the end of the overlapping segment. + /// + /// + /// An indicating the number of intersection points: + /// - Returns 0 if there is no intersection. + /// - Returns 1 if the segments intersect at a single point. + /// - Returns 2 if the segments overlap. + /// + private static int FindIntersectionImpl(in Vertex a1, in Vertex a2, in Vertex b1, in Vertex b2, out Vertex pi0, out Vertex pi1) + { + pi0 = default; + pi1 = default; + + Vertex va = a2 - a1; + Vertex vb = b2 - b1; + Vertex e = b1 - a1; + double kross = Vertex.Cross(va, vb); + double sqrKross = kross * kross; + double sqrLenA = Vertex.Dot(va, va); + + if (sqrKross > 0D) + { + // Lines of the segments are not parallel. + double s = Vertex.Cross(e, vb) / kross; + if (s is < 0D or > 1D) + { + return 0; + } + + double t = Vertex.Cross(e, va) / kross; + if (t is < 0D or > 1D) + { + return 0; + } + + // If s or t is exactly 0 or 1, the intersection is on an endpoint. + if (s is 0D or 1D) + { + // On an endpoint of segment a. + pi0 = MidPoint(a1, s, va); + return 1; + } + + if (t is 0D or 1D) + { + // On an endpoint of segment b. + pi0 = MidPoint(a2, t, vb); + return 1; + } + + // Intersection of lines is a point on each segment. + pi0 = MidPoint(a1, s, va); + return 1; + } + + // Lines are parallel; check if they are collinear. + kross = Vertex.Cross(e, va); + sqrKross = kross * kross; + if (sqrKross > 0D) + { + // Parallel but not collinear. + return 0; + } + + if (sqrLenA == 0D) + { + return 0; + } + + // Segments are collinear, check 1D overlap in segment-a parameter space. + double sa = Vertex.Dot(va, e) / sqrLenA; + double sb = sa + (Vertex.Dot(va, vb) / sqrLenA); + double smin = Math.Min(sa, sb); + double smax = Math.Max(sa, sb); + + if (smin <= 1D && smax >= 0D) + { + if (smin == 1D) + { + pi0 = MidPoint(a1, smin, va); + return 1; + } + + if (smax == 0D) + { + pi0 = MidPoint(a1, smax, va); + return 1; + } + + pi0 = MidPoint(a1, Math.Max(smin, 0D), va); + pi1 = MidPoint(a1, Math.Min(smax, 1D), va); + return pi0 == pi1 ? 1 : 2; + } + + return 0; + } + + /// + /// Computes the bounding box of the intersection area of two line segments. + /// + /// The first point of the first segment. + /// The second point of the first segment. + /// The first point of the second segment. + /// The second point of the second segment. + /// The intersection bounding box if one exists, otherwise null. + /// + /// if the segments intersect; otherwise, . + /// + private static bool TryGetIntersectionBoundingBox( + in Vertex a1, + in Vertex a2, + in Vertex b1, + in Vertex b2, + out Box2 result) + { + Vertex minA = Vertex.Min(a1, a2); + Vertex maxA = Vertex.Max(a1, a2); + Vertex minB = Vertex.Min(b1, b2); + Vertex maxB = Vertex.Max(b1, b2); + + Vertex interMin = Vertex.Max(minA, minB); + Vertex interMax = Vertex.Min(maxA, maxB); + + if (interMin.X <= interMax.X && interMin.Y <= interMax.Y) + { + result = new Box2(interMin, interMax); + return true; + } + + result = default; + return false; + } + } +} diff --git a/PolygonClipper/ResultTransition.cs b/PolygonClipper/ResultTransition.cs new file mode 100644 index 0000000..5aa3d09 --- /dev/null +++ b/PolygonClipper/ResultTransition.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.PolygonClipper { + /// + /// Represents the result transition for a sweep event. + /// + public enum ResultTransition + { + /// + /// The event does not contribute to the result. + /// + NonContributing = -1, + + /// + /// The event transitions within the result. + /// + Neutral = 0, + + /// + /// The event contributes to the result. + /// + Contributing = 1 + } +} diff --git a/PolygonClipper/ScanlineSchedule.cs b/PolygonClipper/ScanlineSchedule.cs new file mode 100644 index 0000000..84d3fef --- /dev/null +++ b/PolygonClipper/ScanlineSchedule.cs @@ -0,0 +1,158 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// Manages scanline ordering and local minima scheduling for the sweep line. + /// + /// + /// This type keeps local minima in sorted order and seeds scanlines from their Y coordinates. + /// It also provides ordered scanline pop/insert operations used during the sweep. + /// + internal sealed class ScanlineSchedule + { + private static readonly LocalMinimaComparer LocalMinimaComparerInstance = new(); + + private ArrayBuilder localMinima; + private readonly List scanlines; + private int localMinimaIndex; + private bool isLocalMinimaSorted; + + /// + /// Initializes a new instance of the class. + /// + public ScanlineSchedule() + { + this.localMinima = new ArrayBuilder(16); + this.scanlines = []; + } + + /// + /// Gets the number of registered local minima. + /// + public int LocalMinimaCount => this.localMinima.Length; + + /// + /// Gets a retained-capacity score used to decide pooling reuse. + /// + public int RetainedCapacityScore => this.localMinima.Capacity + this.scanlines.Capacity; + + /// + /// Adds a local minima to the schedule. + /// + /// The minima to append. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddLocalMinima(in LocalMinima localMinima) + { + this.localMinima.Add(localMinima); + this.isLocalMinimaSorted = false; + } + + /// + /// Marks the local minima list as unsorted. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MarkDirty() => this.isLocalMinimaSorted = false; + + /// + /// Clears all minima and scanline state. + /// + public void Clear() + { + this.localMinima.Clear(); + this.scanlines.Clear(); + this.localMinimaIndex = 0; + this.isLocalMinimaSorted = false; + } + + /// + /// Clears scanlines while keeping local minima intact. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ClearScanlines() => this.scanlines.Clear(); + + /// + /// Sorts minima (if needed) and seeds the scanline list. + /// + public void Reset() + { + if (!this.isLocalMinimaSorted) + { + this.localMinima.Sort(LocalMinimaComparerInstance); + this.isLocalMinimaSorted = true; + } + + this.scanlines.Clear(); + int localMinimaCount = this.localMinima.Length; + this.scanlines.EnsureCapacity(localMinimaCount); + for (int i = localMinimaCount - 1; i >= 0; i--) + { + this.scanlines.Add(this.localMinima[i].Vertex.Point.Y); + } + + this.localMinimaIndex = 0; + } + + /// + /// Determines whether the next local minima is on the given scanline. + /// + /// The scanline Y coordinate. + /// if a minima exists at this Y. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasLocalMinimaAtY(double y) + => this.localMinimaIndex < this.localMinima.Length && + this.localMinima[this.localMinimaIndex].Vertex.Point.Y == y; + + /// + /// Pops the next local minima from the schedule. + /// + /// The next local minima. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LocalMinima PopLocalMinima() => this.localMinima[this.localMinimaIndex++]; + + /// + /// Inserts a scanline value into the ordered list. + /// + /// The scanline Y coordinate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void InsertScanline(double y) + { + int index = this.scanlines.BinarySearch(y); + if (index >= 0) + { + return; + } + + index = ~index; + this.scanlines.Insert(index, y); + } + + /// + /// Pops the next scanline from the schedule. + /// + /// The popped scanline value. + /// when a scanline was available. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryPopScanline(out double y) + { + int count = this.scanlines.Count - 1; + if (count < 0) + { + y = 0; + return false; + } + + y = this.scanlines[count]; + this.scanlines.RemoveAt(count--); + while (count >= 0 && y == this.scanlines[count]) + { + this.scanlines.RemoveAt(count--); + } + + return true; + } + } +} diff --git a/PolygonClipper/Segment.cs b/PolygonClipper/Segment.cs new file mode 100644 index 0000000..4a4ec79 --- /dev/null +++ b/PolygonClipper/Segment.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// Represents a line segment on a plane. + /// + internal readonly struct Segment : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The segment source. + /// The segment target. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Segment(in Vertex source, in Vertex target) + { + this.Source = source; + this.Target = target; + this.Min = Vertex.Min(source, target); + this.Max = Vertex.Max(source, target); + } + + /// + /// Gets the segment source vector. + /// + public Vertex Source { get; } + + /// + /// Gets the segment target vector. + /// + public Vertex Target { get; } + + /// + /// Gets the point of the segment with lexicographically smallest coordinate. + /// + public Vertex Min { get; } + + /// + /// Gets the point of the segment with lexicographically largest coordinate. + /// + public Vertex Max { get; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(in Segment left, in Segment right) + => left.Equals(right); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(in Segment left, in Segment right) + => !(left == right); + + /// + /// Gets a value indicating whether the segment is degenerate. + /// + /// + /// if the segment is degenerate; otherwise . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsDegenerate() => this.Source.Equals(this.Target); + + /// + /// Gets a value indicating whether the segment is vertical. + /// + /// + /// if the segment is vertical; otherwise . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsVertical() => this.Source.X == this.Target.X; + + /// + /// Changes the segment orientation. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Segment Reverse() + => new(this.Target, this.Source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + => obj is Segment segment && this.Equals(segment); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Segment other) + => this.Source.Equals(other.Source) && this.Target.Equals(other.Target); + + /// + public override int GetHashCode() + => HashCode.Combine(this.Source, this.Target); + } +} diff --git a/PolygonClipper/SegmentComparer.cs b/PolygonClipper/SegmentComparer.cs new file mode 100644 index 0000000..7b71e67 --- /dev/null +++ b/PolygonClipper/SegmentComparer.cs @@ -0,0 +1,158 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// Allows the comparison of segments for sorting. + /// + internal sealed class SegmentComparer : IComparer, IComparer + { + /// + public int Compare(SweepEvent? x, SweepEvent? y) + { + // If the events are the same, return 0 (no order difference) + if (ReferenceEquals(x, y)) + { + return 0; + } + + if (x == null) + { + return -1; + } + + if (y == null) + { + return 1; + } + + SweepEvent perhapsInversedX, perhapsInversedY; + bool inversed; + + if (x.IsBefore(y)) + { + perhapsInversedX = x; + perhapsInversedY = y; + inversed = false; + } + else + { + perhapsInversedX = y; + perhapsInversedY = x; + inversed = true; + } + + // Check if the segments are collinear by comparing their signed areas + double area1 = PolygonUtilities.SignedArea(perhapsInversedX.Point, perhapsInversedX.OtherEvent.Point, perhapsInversedY.Point); + double area2 = PolygonUtilities.SignedArea(perhapsInversedX.Point, perhapsInversedX.OtherEvent.Point, perhapsInversedY.OtherEvent.Point); + + if (area1 != 0 || area2 != 0) + { + // Segments are not collinear + // If they share their left endpoint, use the right endpoint to sort + if (perhapsInversedX.Point == perhapsInversedY.Point) + { + return LessIf(perhapsInversedX.IsBelow(perhapsInversedY.OtherEvent.Point), inversed); + } + + // Different left endpoints: use the y-coordinate to sort if x-coordinates are the same + if (perhapsInversedX.Point.X == perhapsInversedY.Point.X) + { + return LessIf(perhapsInversedX.Point.Y < perhapsInversedY.Point.Y, inversed); + } + + // If `x` and `y` lie on the same side of the reference segment, + // no intersection check is necessary. + if ((area1 > 0) == (area2 > 0)) + { + return LessIf(area1 > 0, inversed); + } + + // If `x` lies on the reference segment, compare based on `y`. + if (area1 == 0) + { + return LessIf(area2 > 0, inversed); + } + + // Form segments from the events. + Segment seg0 = new(perhapsInversedX.Point, perhapsInversedX.OtherEvent.Point); + Segment seg1 = new(perhapsInversedY.Point, perhapsInversedY.OtherEvent.Point); + + // Call the provided intersection method. + int interResult = PolygonUtilities.FindIntersection(seg0, seg1, out Vertex pi0, out Vertex _); + + if (interResult == 0) + { + // No unique intersection found: decide based on area1. + return LessIf(area1 > 0, inversed); + } + else if (interResult == 1) + { + // Unique intersection found. + if (pi0 == y.Point) + { + return LessIf(area2 > 0, inversed); + } + + return LessIf(area1 > 0, inversed); + } + + // If interResult is neither 0 nor 1, fall through to collinear logic. + } + + // Collinear branch – mimicking the Rust logic: + if (perhapsInversedX.PolygonType == perhapsInversedY.PolygonType) + { + // Both segments belong to the same polygon. + if (perhapsInversedX.Point == perhapsInversedY.Point) + { + // When left endpoints are identical, order by contour id. + return LessIf(perhapsInversedX.ContourId < perhapsInversedY.ContourId, inversed); + } + + // If left endpoints differ, the Rust version simply returns "less" (i.e. the one inserted earlier). + // Here we mimic that by always returning -1. + return LessIf(true, inversed); + } + + // Segments are collinear but belong to different polygons. + return LessIf(perhapsInversedX.PolygonType == PolygonType.Subject, inversed); + } + + /// + public int Compare(object? x, object? y) + { + if (x == null) + { + return -1; + } + + if (y == null) + { + return 1; + } + + if (x is SweepEvent a && y is SweepEvent b) + { + return this.Compare(a, b); + } + + throw new ArgumentException("Both arguments must be of type SweepEvent.", nameof(x)); + } + + /// + /// Converts a boolean comparison result to an ordering value. + /// Returns -1 if the condition is true, 1 if false. + /// + /// The boolean condition to evaluate. + /// Should the result be inversed. + /// -1 if condition is true, 1 if false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int LessIf(bool condition, bool inversed = false) => condition ^ inversed ? -1 : 1; + } +} diff --git a/PolygonClipper/SelfIntersectionRemover.cs b/PolygonClipper/SelfIntersectionRemover.cs new file mode 100644 index 0000000..bbc3b5d --- /dev/null +++ b/PolygonClipper/SelfIntersectionRemover.cs @@ -0,0 +1,1352 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// Provides functionality to remove self-intersections from polygons using a sweep line algorithm. + /// + /// + /// + /// This class implements a sweep line algorithm that resolves self-intersections + /// and normalizes contours for positive winding output. + /// + /// + /// The algorithm works in three phases: + /// + /// + /// + /// Intersection Detection: Uses a sweep line to find all points where segments + /// intersect each other (both self-intersections and cross-contour intersections). + /// + /// + /// Segment Splitting: Divides segments at intersection points, creating new + /// vertices where crossings occur. + /// + /// + /// Boundary Extraction: Keeps only edges that + /// form the boundary between filled and unfilled regions. + /// + /// + /// + internal static class SelfIntersectionRemover + { + // Mirror PolygonClipper pooling policy: keep a tiny hot set per thread. + private const int MaxOutputBuilderPoolDepth = 4; + + // Retain builders only while their pooled internal capacity stays in a + // bounded range. Oversized builders are dropped after heavy/pathological inputs. + private const int MaxRetainedOutputBuilderCapacityScore = 131_072; + + [ThreadStatic] + private static Stack? outputBuilderPool; + + /// + /// Processes a polygon to remove self-intersections. + /// + /// The polygon to process. + /// + /// A new with self-intersections resolved and contours + /// normalized for positive winding fill semantics. + /// + public static Polygon Process(Polygon polygon) + => Process(polygon, normalizeInputForPositiveFill: true); + + /// + /// Processes a polygon to remove self-intersections. + /// + /// The polygon to process. + /// + /// Whether input contours should be normalized before sweep execution. + /// + /// The self-intersection-removed polygon. + internal static Polygon Process(Polygon polygon, bool normalizeInputForPositiveFill) + { + if (polygon.Count == 0) + { + return []; + } + + List> subject = BuildSubjectPaths(polygon, normalizeInputForPositiveFill); + if (normalizeInputForPositiveFill) + { + GetLowestPathInfo(subject, out int lowestPathIdx, out bool isNegativeArea); + if (lowestPathIdx >= 0 && isNegativeArea) + { + ReverseContours(subject); + } + } + + OutputBuilder builder = RentOutputBuilder(); + try + { + return UnionWithClipper(subject, polygon.Count, builder); + } + finally + { + ReturnOutputBuilder(builder); + } + } + + /// + /// Executes a union using the internal clipper. + /// + /// The quantized subject contours to union. + /// The initial contour capacity for the output polygon. + /// The reusable output builder instance. + /// A polygon containing the unioned contours. + private static Polygon UnionWithClipper( + List> subject, + int resultCapacity, + OutputBuilder builder) + { + builder.ResetForReuse(); + builder.PreserveCollinear = true; + builder.AddSubject(subject); + return builder.Execute(resultCapacity); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ReverseContours(List> subject) + { + for (int i = 0; i < subject.Count; i++) + { + subject[i].Reverse(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static OutputBuilder RentOutputBuilder() + { + Stack? pool = outputBuilderPool; + if (pool != null && pool.Count > 0) + { + return pool.Pop(); + } + + return new OutputBuilder(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ReturnOutputBuilder(OutputBuilder builder) + { + builder.ResetForReuse(); + + // Drop oversized builders to prevent long-lived thread-local memory spikes. + if (builder.RetainedCapacityScore > MaxRetainedOutputBuilderCapacityScore) + { + return; + } + + Stack pool = outputBuilderPool ??= new Stack(MaxOutputBuilderPoolDepth); + if (pool.Count < MaxOutputBuilderPoolDepth) + { + pool.Push(builder); + } + } + + /// + /// Determines the lowest point across all paths and whether its contour area is negative. + /// + /// The paths to examine. + /// The index of the path containing the lowest point. + /// True when the lowest path has negative area. + private static void GetLowestPathInfo(List> paths, out int lowestPathIdx, out bool isNegativeArea) + { + lowestPathIdx = -1; + isNegativeArea = false; + + if (paths.Count == 0) + { + return; + } + + Vertex lowestPoint = default; + bool hasPoint = false; + for (int i = 0; i < paths.Count; i++) + { + List path = paths[i]; + if (path.Count == 0) + { + continue; + } + + Vertex candidate = GetLowestPoint(path); + if (!hasPoint || candidate.Y > lowestPoint.Y || (candidate.Y == lowestPoint.Y && candidate.X < lowestPoint.X)) + { + lowestPoint = candidate; + lowestPathIdx = i; + hasPoint = true; + } + } + + if (lowestPathIdx >= 0) + { + isNegativeArea = GetSignedArea(paths[lowestPathIdx]) < 0D; + } + } + + private static Vertex GetLowestPoint(List path) + { + int count = path.Count; + int lastIndex = count - 1; + if (count > 1 && path[0] == path[^1]) + { + lastIndex = count - 2; + } + + Vertex lowest = path[0]; + for (int i = 1; i <= lastIndex; i++) + { + Vertex candidate = path[i]; + if (candidate.Y > lowest.Y || (candidate.Y == lowest.Y && candidate.X < lowest.X)) + { + lowest = candidate; + } + } + + return lowest; + } + + private static int GetDepth(int index, ReadOnlySpan parentIndices) + { + int depth = 0; + int current = parentIndices[index]; + while (current >= 0) + { + depth++; + current = parentIndices[current]; + } + + return depth; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vertex GetContourTestPoint(List contour) + { + if (contour.Count == 0) + { + return default; + } + + Vertex first = contour[0]; + if (contour.Count > 1 && first == contour[^1]) + { + return contour[1]; + } + + return first; + } + + private static double GetSignedArea(List contour) + { + int count = contour.Count; + if (count == 0) + { + return 0D; + } + + double area = 0; + Vertex current = contour[0]; + for (int i = 1; i < count; i++) + { + Vertex next = contour[i]; + area += Vertex.Cross(current, next); + current = next; + } + + area += Vertex.Cross(current, contour[0]); + return area * 0.5D; + } + + private static bool HasSelfIntersection(List contour) + { + int vertexCount = contour.Count - 1; + if (vertexCount < 4) + { + return false; + } + + for (int i = 0; i < vertexCount; i++) + { + Vertex segA1 = contour[i]; + Vertex segA2 = contour[i + 1]; + if (segA1 == segA2) + { + continue; + } + + for (int j = i + 1; j < vertexCount; j++) + { + if (j == i || j == i + 1 || (i == 0 && j == vertexCount - 1)) + { + continue; + } + + Vertex segB1 = contour[j]; + Vertex segB2 = contour[j + 1]; + if (segB1 == segB2) + { + continue; + } + + if (PolygonUtilities.SegmentsIntersect(segA1, segA2, segB1, segB2, true) || + (PolygonUtilities.IsCollinear(segA1, segA2, segB1) && + PolygonUtilities.IsCollinear(segA1, segA2, segB2) && + SegmentsOverlap(segA1, segA2, segB1, segB2))) + { + return true; + } + } + } + + return false; + } + + private static bool ContoursIntersect( + List left, + List right, + in Box2 leftBounds, + in Box2 rightBounds) + { + if (!leftBounds.Intersects(rightBounds)) + { + return false; + } + + int leftCount = left.Count - 1; + int rightCount = right.Count - 1; + for (int i = 0; i < leftCount; i++) + { + Vertex leftSeg1 = left[i]; + Vertex leftSeg2 = left[i + 1]; + if (leftSeg1 == leftSeg2) + { + continue; + } + + for (int j = 0; j < rightCount; j++) + { + Vertex rightSeg1 = right[j]; + Vertex rightSeg2 = right[j + 1]; + if (rightSeg1 == rightSeg2) + { + continue; + } + + if (PolygonUtilities.SegmentsIntersect(leftSeg1, leftSeg2, rightSeg1, rightSeg2, true) || + (PolygonUtilities.IsCollinear(leftSeg1, leftSeg2, rightSeg1) && + PolygonUtilities.IsCollinear(leftSeg1, leftSeg2, rightSeg2) && + SegmentsOverlap(leftSeg1, leftSeg2, rightSeg1, rightSeg2))) + { + return true; + } + } + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool SegmentsOverlap(in Vertex a1, in Vertex a2, in Vertex b1, in Vertex b2) + { + Vertex aMin = Vertex.Min(a1, a2); + Vertex aMax = Vertex.Max(a1, a2); + Vertex bMin = Vertex.Min(b1, b2); + Vertex bMax = Vertex.Max(b1, b2); + + return aMax.X >= bMin.X && + bMax.X >= aMin.X && + aMax.Y >= bMin.Y && + bMax.Y >= aMin.Y; + } + + /// + /// Builds subject paths from a polygon. + /// + /// The polygon to convert. + /// Whether input contour orientation should be normalized. + /// A list of fixed-precision vertex paths ready for clipping. + private static List> BuildSubjectPaths(Polygon polygon, bool normalizeForPositiveFill) + { + List> subject = new(polygon.Count); + List? sourceIndices = normalizeForPositiveFill ? new List(polygon.Count) : null; + for (int i = 0; i < polygon.Count; i++) + { + Contour contour = polygon[i]; + if (contour.Count == 0) + { + continue; + } + + bool isClosed = contour.Count > 1 && contour[0] == contour[^1]; + int capacity = contour.Count + (isClosed ? 0 : 1); + List path = new(capacity); + + CopyContourVertices(contour, isClosed, path); + subject.Add(path); + sourceIndices?.Add(i); + } + + if (normalizeForPositiveFill) + { + ApplyPositiveFillOrientation(polygon, subject, sourceIndices!); + } + + return subject; + } + + private static void CopyContourVertices(Contour source, bool isClosed, List destination) + { + for (int i = 0; i < source.Count; i++) + { + destination.Add(source[i]); + } + + if (!isClosed && destination.Count > 1 && destination[^1] != destination[0]) + { + destination.Add(destination[0]); + } + } + + private static void ApplyPositiveFillOrientation( + Polygon source, + List> subject, + List sourceIndices) + { + bool[]? reverseFlags = BuildPositiveFillReversalFlags(source, subject, sourceIndices); + if (reverseFlags == null) + { + return; + } + + for (int i = 0; i < reverseFlags.Length; i++) + { + if (reverseFlags[i]) + { + subject[i].Reverse(); + } + } + } + + private static bool[]? BuildPositiveFillReversalFlags( + Polygon source, + List> subject, + List sourceIndices) + { + int count = subject.Count; + if (count == 0) + { + return null; + } + + if (count == 1) + { + return GetSignedArea(subject[0]) < 0D ? [true] : null; + } + + using Buffer parentIndicesBuffer = new(count); + Span parentIndices = parentIndicesBuffer.GetSpan(); + parentIndices.Fill(-1); + + using Buffer signedAreasBuffer = new(count); + Span signedAreas = signedAreasBuffer.GetSpan(); + + bool hasHierarchy = false; + bool hasSignedAreas = false; + for (int i = 0; i < count; i++) + { + Contour contour = source[sourceIndices[i]]; + if (contour.ParentIndex == null && contour.HoleCount <= 0) + { + continue; + } + + hasHierarchy = true; + break; + } + + if (hasHierarchy) + { + int sourceCount = source.Count; + using Buffer sourceToSubjectBuffer = new(sourceCount); + Span sourceToSubject = sourceToSubjectBuffer.GetSpan(); + sourceToSubject.Fill(-1); + + for (int i = 0; i < sourceIndices.Count; i++) + { + sourceToSubject[sourceIndices[i]] = i; + } + + for (int i = 0; i < count; i++) + { + int sourceIndex = sourceIndices[i]; + int parentIndex = source[sourceIndex].ParentIndex ?? -1; + parentIndices[i] = parentIndex >= 0 && parentIndex < sourceCount + ? sourceToSubject[parentIndex] + : -1; + } + } + else + { + using Buffer boundsBuffer = new(count); + Span bounds = boundsBuffer.GetSpan(); + using Buffer absAreasBuffer = new(count); + Span absAreas = absAreasBuffer.GetSpan(); + + for (int i = 0; i < count; i++) + { + List contour = subject[i]; + bounds[i] = PolygonUtilities.GetBounds(contour); + double signedArea = GetSignedArea(contour); + signedAreas[i] = signedArea; + absAreas[i] = Math.Abs(signedArea); + + if (HasSelfIntersection(contour)) + { + // Avoid reorienting inputs that are already self-intersecting. + return null; + } + } + + for (int i = 0; i < count; i++) + { + for (int j = i + 1; j < count; j++) + { + if (ContoursIntersect(subject[i], subject[j], bounds[i], bounds[j])) + { + // Overlapping contours can change semantics when reoriented. + return null; + } + } + } + + hasSignedAreas = true; + + for (int i = 0; i < count; i++) + { + List contour = subject[i]; + if (contour.Count == 0) + { + continue; + } + + Vertex testPoint = GetContourTestPoint(contour); + double smallestArea = double.PositiveInfinity; + int parentIndex = -1; + for (int j = 0; j < count; j++) + { + if (i == j || !bounds[j].Contains(testPoint)) + { + continue; + } + + if (PolygonUtilities.PointInPolygon(testPoint, subject[j]) != PointInPolygonResult.Inside) + { + continue; + } + + if (absAreas[j] < smallestArea) + { + smallestArea = absAreas[j]; + parentIndex = j; + } + } + + parentIndices[i] = parentIndex; + } + } + + bool[] reverseFlags = new bool[count]; + bool needsReversal = false; + for (int i = 0; i < count; i++) + { + List contour = subject[i]; + if (contour.Count == 0) + { + continue; + } + + int depth = GetDepth(i, parentIndices); + bool shouldBeCounterClockwise = (depth & 1) == 0; + double signedArea = hasSignedAreas ? signedAreas[i] : GetSignedArea(contour); + bool isCounterClockwise = signedArea >= 0D; + if (isCounterClockwise != shouldBeCounterClockwise) + { + reverseFlags[i] = true; + needsReversal = true; + } + } + + return needsReversal ? reverseFlags : null; + } + + private sealed class OutputBuilder + { + // Clipper integer-space tolerances converted to double-space equivalents + // of ClipperD(6). + private const double NearPointDelta = 2E-6D; + private const double MinimumSplitArea = 2E-12D; + private const double SignificantTriangleArea = 1E-12D; + + private readonly SelfIntersectionSweepLine sweepLine; + private bool buildHierarchy; + + /// + /// Initializes a new instance of the class. + /// + public OutputBuilder() => this.sweepLine = new SelfIntersectionSweepLine(); + + /// + /// Gets or sets a value indicating whether collinear output points are preserved. + /// + public bool PreserveCollinear + { + get => this.sweepLine.PreserveCollinear; + set => this.sweepLine.PreserveCollinear = value; + } + + /// + /// Gets or sets a value indicating whether the output orientation is reversed. + /// + public bool ReverseSolution { get; set; } + + /// + /// Gets a retained-capacity score used by caller-side pooling policy. + /// + public int RetainedCapacityScore => this.sweepLine.RetainedCapacityScore; + + /// + /// Clears all cached input and output data. + /// + public void Clear() => this.sweepLine.Clear(); + + /// + /// Resets mutable state so this instance can be safely reused. + /// + public void ResetForReuse() + { + this.buildHierarchy = false; + this.ReverseSolution = false; + this.PreserveCollinear = true; + this.Clear(); + } + + /// + /// Adds subject contours to the sweep-line clipper. + /// + /// The subject contours to add. + public void AddSubject(List> paths) => this.sweepLine.AddSubject(paths); + + /// + /// Determines whether two points are within a tight tolerance. + /// + /// The first point. + /// The second point. + /// if the points are nearly coincident. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ArePointsVeryClose(in Vertex firstPoint, in Vertex secondPoint) + { + Vertex delta = Vertex.Abs(firstPoint - secondPoint); + return delta.X < NearPointDelta && delta.Y < NearPointDelta; + } + + /// + /// Tests whether an output ring collapses to a very small triangle. + /// + /// A point on the ring. + /// if the triangle is degenerate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsVerySmallTriangle(OutputPoint outputPoint) => outputPoint.Next!.Next == outputPoint.Prev && + (ArePointsVeryClose(outputPoint.Prev.Point, outputPoint.Next.Point) || + ArePointsVeryClose(outputPoint.Point, outputPoint.Next.Point) || + ArePointsVeryClose(outputPoint.Point, outputPoint.Prev.Point)); + + /// + /// Validates that an output ring is a non-degenerate closed loop. + /// + /// A point on the ring. + /// if the ring is valid. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsValidClosedPath(OutputPoint? outputPoint) => outputPoint != null && outputPoint.Next != outputPoint && + (outputPoint.Next != outputPoint.Prev || !IsVerySmallTriangle(outputPoint)); + + /// + /// Removes an output point from the ring and returns the next point. + /// + /// The output point to remove. + /// The next output point in the ring, or . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static OutputPoint? RecycleOutputPoint(OutputPoint outputPoint) + { + OutputPoint? result = outputPoint.Next == outputPoint ? null : outputPoint.Next; + outputPoint.Prev.Next = outputPoint.Next; + outputPoint.Next!.Prev = outputPoint.Prev; + + return result; + } + + /// + /// Creates a new output record with the next stable index. + /// + /// The created output record. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OutputRecord CreateOutputRecord() + { + int idx = this.sweepLine.OutputRecords.Count; + OutputRecord result = this.sweepLine.OutputRecords.Add(); + result.Index = idx; + return result; + } + + /// + /// Duplicates an output point and inserts it before or after the original. + /// + /// The point to duplicate. + /// Whether to insert after the original. + /// The newly inserted output point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OutputPoint DuplicateOutputPoint(OutputPoint outputPoint, bool insertAfter) + { + OutputPoint result = this.sweepLine.OutputPoints.Add(outputPoint.Point, outputPoint.OutputRecord); + if (insertAfter) + { + result.Next = outputPoint.Next; + result.Next!.Prev = result; + result.Prev = outputPoint; + outputPoint.Next = result; + } + else + { + result.Prev = outputPoint.Prev; + result.Prev.Next = result; + result.Next = outputPoint; + outputPoint.Prev = result; + } + + return result; + } + + /// + /// Removes collinear points and resolves self-intersections in an output record. + /// + /// The output record to clean. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CleanCollinearEdges(OutputRecord? outputRecord) + { + outputRecord = SelfIntersectionSweepLine.ResolveOutputRecord(outputRecord); + + if (outputRecord == null) + { + return; + } + + if (!IsValidClosedPath(outputRecord.Points)) + { + outputRecord.Points = null; + return; + } + + OutputPoint startOp = outputRecord.Points!; + OutputPoint? outputPoint2 = startOp; + while (true) + { + // Preserve immediate A-B-A return spikes. The flat-ring fix injects + // these intentionally (touch -> tip -> touch), so we keep apex B even + // when collinear to avoid collapsing expected boundary detail. + bool isReturnSpikeApex = outputPoint2!.Prev.Point == outputPoint2.Next!.Point && + outputPoint2.Point != outputPoint2.Prev.Point; + if (PolygonUtilities.IsCollinear(outputPoint2!.Prev.Point, outputPoint2.Point, outputPoint2.Next!.Point) && + (outputPoint2.Point == outputPoint2.Prev.Point || + outputPoint2.Point == outputPoint2.Next.Point || + (!this.PreserveCollinear && !isReturnSpikeApex) || + (PolygonUtilities.Dot(outputPoint2.Prev.Point, outputPoint2.Point, outputPoint2.Next.Point) < 0 && + !isReturnSpikeApex))) + { + if (outputPoint2 == outputRecord.Points) + { + outputRecord.Points = outputPoint2.Prev; + } + + outputPoint2 = RecycleOutputPoint(outputPoint2); + if (!IsValidClosedPath(outputPoint2)) + { + outputRecord.Points = null; + return; + } + + startOp = outputPoint2!; + continue; + } + + outputPoint2 = outputPoint2.Next; + if (outputPoint2 == startOp) + { + break; + } + } + + this.FixSelfIntersections(outputRecord); + } + + /// + /// Splits an output record at a self-intersection. + /// + /// The record being split. + /// The output point where the split occurs. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SplitOutputRecord(OutputRecord outputRecord, OutputPoint splitOp) + { + // The segments (splitOp.Prev, splitOp) and (splitOp.Next, splitOp.Next.Next) intersect. + OutputPoint prevOp = splitOp.Prev; + OutputPoint nextNextOp = splitOp.Next!.Next!; + outputRecord.Points = prevOp; + + PolygonUtilities.TryGetLineIntersection( + prevOp.Point, splitOp.Point, splitOp.Next.Point, nextNextOp.Point, out Vertex intersectionPoint); + + double area1 = SelfIntersectionSweepLine.ComputeSignedArea(prevOp); + double absArea1 = Math.Abs(area1); + + if (absArea1 < MinimumSplitArea) + { + outputRecord.Points = null; + return; + } + + double area2 = AreaTriangle(intersectionPoint, splitOp.Point, splitOp.Next.Point); + double absArea2 = Math.Abs(area2); + + // Remove the crossing segment and insert the intersection point. + if (intersectionPoint == prevOp.Point || intersectionPoint == nextNextOp.Point) + { + nextNextOp.Prev = prevOp; + prevOp.Next = nextNextOp; + } + else + { + OutputPoint newOp2 = this.sweepLine.OutputPoints.Add(intersectionPoint, outputRecord); + newOp2.Prev = prevOp; + newOp2.Next = nextNextOp; + nextNextOp.Prev = newOp2; + prevOp.Next = newOp2; + } + + // Note: area1 is the path's signed area *before* splitting, whereas area2 is + // the signed area of the triangle containing splitOp & splitOp.Next. + // So the only way for these areas to have the same sign is if + // the split triangle is larger than the path containing prevOp or + // if there's more than one self-intersection. + if (!(absArea2 > SignificantTriangleArea) || + (!(absArea2 > absArea1) && + ((area2 > 0) != (area1 > 0)))) + { + return; + } + + OutputRecord newOutputRecord = this.CreateOutputRecord(); + newOutputRecord.Owner = outputRecord.Owner; + splitOp.OutputRecord = newOutputRecord; + splitOp.Next.OutputRecord = newOutputRecord; + + OutputPoint newOp = this.sweepLine.OutputPoints.Add(intersectionPoint, newOutputRecord); + newOp.Prev = splitOp.Next; + newOp.Next = splitOp; + newOutputRecord.Points = newOp; + splitOp.Prev = newOp; + splitOp.Next.Next = newOp; + + if (!this.buildHierarchy) + { + return; + } + + if (SelfIntersectionSweepLine.IsPathInsidePath(prevOp, newOp)) + { + newOutputRecord.Splits ??= []; + newOutputRecord.Splits.Add(outputRecord.Index); + } + else + { + outputRecord.Splits ??= []; + outputRecord.Splits.Add(newOutputRecord.Index); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double AreaTriangle(in Vertex pt1, in Vertex pt2, in Vertex pt3) + => ((pt3.Y + pt1.Y) * (pt3.X - pt1.X)) + + ((pt1.Y + pt2.Y) * (pt1.X - pt2.X)) + + ((pt2.Y + pt3.Y) * (pt2.X - pt3.X)); + + /// + /// Resolves self-intersections within an output record. + /// + /// The output record to inspect. + private void FixSelfIntersections(OutputRecord outputRecord) + { + OutputPoint outputPoint2 = outputRecord.Points!; + if (outputPoint2.Prev == outputPoint2.Next!.Next) + { + // Triangles cannot self-intersect. + return; + } + + while (true) + { + if (PolygonUtilities.SegmentsIntersect( + outputPoint2!.Prev.Point, + outputPoint2.Point, + outputPoint2.Next!.Point, + outputPoint2.Next.Next!.Point)) + { + if (PolygonUtilities.SegmentsIntersect( + outputPoint2.Prev.Point, + outputPoint2.Point, + outputPoint2.Next.Next!.Point, + outputPoint2.Next.Next.Next!.Point)) + { + // Adjacent intersections (micro self-intersection). + outputPoint2 = this.DuplicateOutputPoint(outputPoint2, false); + outputPoint2.Point = outputPoint2.Next!.Next!.Next!.Point; + outputPoint2 = outputPoint2.Next; + } + else + { + if (outputPoint2 == outputRecord.Points || outputPoint2.Next == outputRecord.Points) + { + outputRecord.Points = outputRecord.Points.Prev; + } + + this.SplitOutputRecord(outputRecord, outputPoint2); + if (outputRecord.Points == null) + { + return; + } + + outputPoint2 = outputRecord.Points; + + // Triangles cannot self-intersect. + if (outputPoint2.Prev == outputPoint2.Next!.Next) + { + break; + } + + continue; + } + } + + outputPoint2 = outputPoint2.Next!; + if (outputPoint2 == outputRecord.Points) + { + break; + } + } + } + + /// + /// Builds a lightweight path from an output ring. + /// + /// A point on the output ring. + /// Whether to reverse point order. + /// The destination contour. + /// if a valid path was built. + private static bool BuildPath(OutputPoint? outputPoint, bool reverse, List path) + { + if (outputPoint == null || outputPoint.Next == outputPoint || outputPoint.Next == outputPoint.Prev) + { + return false; + } + + path.Clear(); + + Vertex lastPoint; + OutputPoint currentPoint; + if (reverse) + { + lastPoint = outputPoint.Point; + currentPoint = outputPoint.Prev; + } + else + { + outputPoint = outputPoint.Next!; + lastPoint = outputPoint.Point; + currentPoint = outputPoint.Next!; + } + + path.Add(lastPoint); + + while (currentPoint != outputPoint) + { + if (currentPoint.Point != lastPoint) + { + lastPoint = currentPoint.Point; + path.Add(lastPoint); + } + + currentPoint = reverse ? currentPoint.Prev : currentPoint.Next!; + } + + return path.Count != 3 || !IsVerySmallTriangle(currentPoint); + } + + /// + /// Builds a contour from an output ring. + /// + /// A point on the output ring. + /// Whether to reverse point order. + /// The destination contour. + /// if a valid contour was built. + private static bool BuildContour(OutputPoint? outputPoint, bool reverse, Contour contour) + { + if (outputPoint == null || outputPoint.Next == outputPoint || outputPoint.Next == outputPoint.Prev) + { + return false; + } + + contour.Clear(); + + Vertex lastPoint; + OutputPoint currentPoint; + if (reverse) + { + lastPoint = outputPoint.Point; + currentPoint = outputPoint.Prev; + } + else + { + outputPoint = outputPoint.Next!; + lastPoint = outputPoint.Point; + currentPoint = outputPoint.Next!; + } + + contour.Add(lastPoint); + + while (currentPoint != outputPoint) + { + Vertex current = currentPoint.Point; + if (current != lastPoint) + { + lastPoint = current; + contour.Add(lastPoint); + } + + currentPoint = reverse ? currentPoint.Prev : currentPoint.Next!; + } + + if (contour.Count == 3 && IsVerySmallTriangle(currentPoint)) + { + contour.Clear(); + return false; + } + + if (contour.Count < 3) + { + contour.Clear(); + return false; + } + + return true; + } + + /// + /// Ensures an output record has bounds populated and valid geometry. + /// + /// The output record to check. + /// if bounds are available. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool CheckOutputBounds(OutputRecord outputRecord) + { + if (outputRecord.Points == null) + { + return false; + } + + if (!outputRecord.Bounds.IsEmpty()) + { + return true; + } + + this.CleanCollinearEdges(outputRecord); + if (outputRecord.Points == null) + { + return false; + } + + if (outputRecord.OutputPointCount > 0) + { + outputRecord.Path.EnsureCapacity(outputRecord.OutputPointCount); + } + + if (!BuildPath(outputRecord.Points, this.ReverseSolution, outputRecord.Path)) + { + return false; + } + + outputRecord.Bounds = PolygonUtilities.GetBounds(outputRecord.Path); + return true; + } + + /// + /// Determines ownership for split output records. + /// + /// The output record whose owner is being resolved. + /// The split indices to evaluate. + /// if ownership could be resolved. + private bool CheckSplitOwner(OutputRecord outputRecord, List? splits) + { + // Use indexing because splits can be modified during iteration (Issue #1029). + for (int i = 0; i < splits!.Count; i++) + { + OutputRecord? splitRecord = this.sweepLine.OutputRecords[splits[i]]; + if (splitRecord.Points == null && splitRecord.Splits != null && + this.CheckSplitOwner(outputRecord, splitRecord.Splits)) + { + // Issue #942. + return true; + } + + splitRecord = SelfIntersectionSweepLine.ResolveOutputRecord(splitRecord); + if (splitRecord == null || splitRecord == outputRecord || splitRecord.RecursiveSplit == outputRecord) + { + continue; + } + + // Issue #599. + splitRecord.RecursiveSplit = outputRecord; + + if (splitRecord.Splits != null && this.CheckSplitOwner(outputRecord, splitRecord.Splits)) + { + return true; + } + + if (!this.CheckOutputBounds(splitRecord) || + !splitRecord.Bounds.Contains(outputRecord.Bounds) || + !SelfIntersectionSweepLine.IsPathInsidePath(outputRecord.Points!, splitRecord.Points!)) + { + continue; + } + + // splitRecord is owned by outputRecord (Issue #957). + if (!SelfIntersectionSweepLine.IsOwnerValid(outputRecord, splitRecord)) + { + splitRecord.Owner = outputRecord.Owner; + } + + // Found in splitRecord. + outputRecord.Owner = splitRecord; + return true; + } + + return false; + } + + /// + /// Resolves the owning output record for hierarchy construction. + /// + /// The output record to resolve. + private void ResolveOutputOwner(OutputRecord outputRecord) + { + if (outputRecord.Bounds.IsEmpty()) + { + return; + } + + while (outputRecord.Owner != null) + { + if (outputRecord.Owner.Splits != null && + this.CheckSplitOwner(outputRecord, outputRecord.Owner.Splits)) + { + break; + } + + if (outputRecord.Owner.Points != null && this.CheckOutputBounds(outputRecord.Owner) && + SelfIntersectionSweepLine.IsPathInsidePath(outputRecord.Points!, outputRecord.Owner.Points!)) + { + break; + } + + outputRecord.Owner = outputRecord.Owner.Owner; + } + } + + /// + /// Builds and returns a hierarchical polygon from output records. + /// + /// The initial contour capacity for the output polygon. + /// The built polygon. + private Polygon BuildPolygon(int resultCapacity) + { + int validClosedCount = 0; + int i = 0; + + // First pass: validate bounds and resolve owners. + // Complexity is O(N) over current output records, but N can grow during + // the pass because CheckOutputBounds may split/fix paths and append records. + while (i < this.sweepLine.OutputRecords.Count) + { + OutputRecord outputRecord = this.sweepLine.OutputRecords[i++]; + if (outputRecord.Points == null) + { + continue; + } + + if (this.CheckOutputBounds(outputRecord)) + { + this.ResolveOutputOwner(outputRecord); + validClosedCount++; + } + } + + if (validClosedCount == 0) + { + return new Polygon(resultCapacity); + } + + int outputRecordCount = this.sweepLine.OutputRecords.Count; + Polygon polygon = new(Math.Max(resultCapacity, validClosedCount)); + using Buffer contourIndexBuffer = new(outputRecordCount); + Span contourIndexByOutputRecord = contourIndexBuffer.GetSpan(); + contourIndexByOutputRecord.Fill(-1); + + // Second pass: build contours and map OutputRecord.Index -> contour index. + // This avoids a dictionary allocation and keeps lookups O(1). + for (int index = 0; index < outputRecordCount; index++) + { + OutputRecord outputRecord = this.sweepLine.OutputRecords[index]; + if (outputRecord.Points == null || outputRecord.Bounds.IsEmpty()) + { + continue; + } + + int estimatedCapacity = outputRecord.OutputPointCount > 0 + ? outputRecord.OutputPointCount + : 0; + Contour contour = estimatedCapacity > 0 ? new Contour(estimatedCapacity) : []; + if (!BuildContour(outputRecord.Points, this.ReverseSolution, contour)) + { + continue; + } + + int contourIndex = polygon.Count; + polygon.Add(contour); + int outputRecordIndex = outputRecord.Index; + if ((uint)outputRecordIndex < (uint)contourIndexByOutputRecord.Length) + { + contourIndexByOutputRecord[outputRecordIndex] = contourIndex; + } + } + + if (polygon.Count == 0) + { + return polygon; + } + + for (int index = 0; index < polygon.Count; index++) + { + Contour contour = polygon[index]; + contour.ParentIndex = null; + contour.Depth = 0; + contour.ClearHoles(); + } + + // Third pass: map owner links to parent contour indices. + for (int index = 0; index < outputRecordCount; index++) + { + OutputRecord outputRecord = this.sweepLine.OutputRecords[index]; + int outputRecordIndex = outputRecord.Index; + if ((uint)outputRecordIndex >= (uint)contourIndexByOutputRecord.Length) + { + continue; + } + + int contourIndex = contourIndexByOutputRecord[outputRecordIndex]; + if (contourIndex < 0) + { + continue; + } + + OutputRecord? owner = outputRecord.Owner; + while (owner != null) + { + int ownerIndex = owner.Index; + if ((uint)ownerIndex < (uint)contourIndexByOutputRecord.Length) + { + int parentIndex = contourIndexByOutputRecord[ownerIndex]; + if (parentIndex >= 0) + { + polygon[contourIndex].ParentIndex = parentIndex; + break; + } + } + + owner = owner.Owner; + } + } + + // Fourth pass: depth is owner-chain length within the emitted contour set. + for (int index = 0; index < outputRecordCount; index++) + { + OutputRecord outputRecord = this.sweepLine.OutputRecords[index]; + int outputRecordIndex = outputRecord.Index; + if ((uint)outputRecordIndex >= (uint)contourIndexByOutputRecord.Length) + { + continue; + } + + int contourIndex = contourIndexByOutputRecord[outputRecordIndex]; + if (contourIndex < 0) + { + continue; + } + + // Depth is the number of owning contours in the chain. + int depth = 0; + OutputRecord? owner = outputRecord.Owner; + while (owner != null) + { + int ownerIndex = owner.Index; + if ((uint)ownerIndex < (uint)contourIndexByOutputRecord.Length && + contourIndexByOutputRecord[ownerIndex] >= 0) + { + depth++; + } + + owner = owner.Owner; + } + + polygon[contourIndex].Depth = depth; + } + + for (int index = 0; index < polygon.Count; index++) + { + Contour contour = polygon[index]; + if (contour.ParentIndex != null) + { + // Map parent links to hole indices for quick traversal. + polygon[contour.ParentIndex.Value].AddHoleIndex(index); + } + } + + return polygon; + } + + /// + /// Executes the union and returns a hierarchical polygon. + /// + /// The initial contour capacity for the output polygon. + /// The resulting polygon. + public Polygon Execute(int resultCapacity) + { + this.buildHierarchy = true; + bool succeeded = this.sweepLine.Execute(true); + Polygon result = succeeded ? this.BuildPolygon(resultCapacity) : []; + this.sweepLine.ClearSolutionData(); + return result; + } + } + } +} diff --git a/PolygonClipper/SelfIntersectionSweepLine.cs b/PolygonClipper/SelfIntersectionSweepLine.cs new file mode 100644 index 0000000..677e5ff --- /dev/null +++ b/PolygonClipper/SelfIntersectionSweepLine.cs @@ -0,0 +1,2611 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// Sweep-line union clipper specialized for self-intersection removal. + /// + /// + /// This clipper consumes subject-only paths and applies positive winding fill + /// semantics to compute the union. It reuses pooled data structures to keep allocations low. + /// + internal sealed class SelfIntersectionSweepLine + { + // Clipper's integer constants are calibrated for scaled coordinates. + // This port operates directly in double-space, so thresholds must be + // converted to the equivalent ClipperD(6) magnitudes. + private const double JoinExtremaDelta = 2E-6D; + private const double JoinPerpendicularDistanceSquaredTolerance = 2.5E-13D; + private const int HorizontalLoopFailSafeLimit = 100_000; + + private readonly ActiveEdgeList activeEdges; + private readonly ScanlineSchedule scanlineSchedule; + private readonly List intersectionList; + private readonly VertexPoolList vertexList; + private readonly List horizontalSegments; + private readonly HorizontalJoinPoolList horizontalJoins; + private double currentScanlineBottomY; + private bool buildHierarchy; + private bool succeeded; + + /// + /// Initializes a new instance of the class. + /// + public SelfIntersectionSweepLine() + { + this.activeEdges = new ActiveEdgeList(); + this.scanlineSchedule = new ScanlineSchedule(); + this.intersectionList = []; + this.vertexList = []; + this.OutputRecords = []; + this.horizontalSegments = []; + this.horizontalJoins = []; + this.OutputPoints = []; + this.PreserveCollinear = true; + } + + /// + /// Gets or sets a value indicating whether collinear output points are preserved. + /// + public bool PreserveCollinear { get; set; } + + /// + /// Gets the pooled output records produced by the sweep. + /// + public OutputRecordPoolList OutputRecords { get; } + + /// + /// Gets the pooled output points produced by the sweep. + /// + public OutputPointPoolList OutputPoints { get; } + + /// + /// Gets a retained-capacity score used by caller-side pooling policy. + /// + public int RetainedCapacityScore => + this.scanlineSchedule.RetainedCapacityScore + + this.intersectionList.Capacity + + this.vertexList.Capacity + + this.horizontalSegments.Capacity + + this.horizontalJoins.Capacity + + this.OutputRecords.Capacity + + this.OutputPoints.Capacity + + this.activeEdges.RetainedPoolCount; + + /// + /// Swaps two active edge references. + /// + /// The first active edge. + /// The second active edge. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SwapActiveEdges(ref ActiveEdge edge1, ref ActiveEdge edge2) => (edge2, edge1) = (edge1, edge2); + + /// + /// Locates the active edge that shares the same maxima vertex. + /// + /// The active edge being matched. + /// The paired maxima edge, or if none exists in the active list. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ActiveEdge? FindMaximaPair(ActiveEdge edge) + { + ActiveEdge? edge2 = edge.NextInAel; + while (edge2 != null) + { + if (edge2.VertexTop == edge.VertexTop) + { + // Matched the companion maxima edge. + return edge2; + } + + edge2 = edge2.NextInAel; + } + + return null; + } + + /// + /// Returns the maxima vertex on the current Y scanline for the edge. + /// + /// The active edge to inspect. + /// The maxima vertex at the current Y, or if not a maxima. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static SweepVertex? GetMaximaVertexAtCurrentY(ActiveEdge edge) + { + SweepVertex? result = edge.VertexTop; + if (result == null) + { + return null; + } + + SweepVertex start = result; + + // Horizontal plateaus at the top can have multiple same-Y vertices. + // Follow the plateau in winding direction to find the effective scanline + // endpoint candidate in O(k), where k is plateau length. + if (edge.WindDelta > 0) + { + while (result.Next!.Point.Y == result.Point.Y) + { + SweepVertex next = result.Next; + if (next == start) + { + break; + } + + result = next; + } + } + else + { + while (result.Prev!.Point.Y == result.Point.Y) + { + SweepVertex prev = result.Prev; + if (prev == start) + { + break; + } + + result = prev; + } + } + + // If the traversed endpoint is not flagged maxima but the start vertex is + // a maxima on the same scanline, prefer the explicit maxima marker. + if (!result.IsMaxima && start.IsMaxima && start.Point.Y == result.Point.Y) + { + result = start; + } + + if (!result.IsMaxima) + { + // No maxima at the current scanline. + result = null; + } + + return result; + } + + /// + /// Assigns the output record edges that define the front and back sides. + /// + /// The output record to update. + /// The edge used for the front side. + /// The edge used for the back side. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetOutputSides(OutputRecord outputRecord, ActiveEdge startEdge, ActiveEdge endEdge) + { + outputRecord.FrontEdge = startEdge; + outputRecord.BackEdge = endEdge; + } + + /// + /// Swaps output record ownership between two active edges. + /// + /// The first active edge. + /// The second active edge. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SwapOutputRecords(ActiveEdge edge1, ActiveEdge edge2) + { + // At least one edge already owns an output record. + OutputRecord? outputRecord1 = edge1.OutputRecord; + OutputRecord? outputRecord2 = edge2.OutputRecord; + if (outputRecord1 == outputRecord2) + { + ActiveEdge? edge = outputRecord1!.FrontEdge; + outputRecord1.FrontEdge = outputRecord1.BackEdge; + outputRecord1.BackEdge = edge; + return; + } + + if (outputRecord1 != null) + { + if (edge1 == outputRecord1.FrontEdge) + { + outputRecord1.FrontEdge = edge2; + } + else + { + outputRecord1.BackEdge = edge2; + } + } + + if (outputRecord2 != null) + { + if (edge2 == outputRecord2.FrontEdge) + { + outputRecord2.FrontEdge = edge1; + } + else + { + outputRecord2.BackEdge = edge1; + } + } + + edge1.OutputRecord = outputRecord2; + edge2.OutputRecord = outputRecord1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsTwoVertexFlatRingEdge(ActiveEdge edge) => + + // Degenerate "ring" used by issue-style tests: two opposing horizontal + // edges around a single local minimum (A-B-A). This is an O(1) shape check. + edge.IsHorizontal && edge.LocalMin.Vertex.Prev == edge.LocalMin.Vertex.Next; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vertex GetFlatRingTip(ActiveEdge flatEdge, in Vertex touchPoint) + { + // If touch is at an endpoint, the tip is the opposite endpoint. + if (touchPoint == flatEdge.Bottom) + { + return flatEdge.Top; + } + + if (touchPoint == flatEdge.Top) + { + return flatEdge.Bottom; + } + + // Otherwise choose the endpoint farther from the touch in X. + // This gives a stable spike apex for touch->tip->touch emission. + double bottomDx = Math.Abs(touchPoint.X - flatEdge.Bottom.X); + double topDx = Math.Abs(touchPoint.X - flatEdge.Top.X); + return bottomDx > topDx ? flatEdge.Bottom : flatEdge.Top; + } + + /// + /// Assigns an output record's owner while preventing cyclic ownership. + /// + /// The output record to update. + /// The candidate owner. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetOutputOwner(OutputRecord outputRecord, OutputRecord newOwner) + { + // Precondition: newOwner is never null. + while (newOwner.Owner != null && newOwner.Owner.Points == null) + { + newOwner.Owner = newOwner.Owner.Owner; + } + + // Avoid cycles: ensure outputRecord is not already an ancestor of newOwner. + OutputRecord? tmp = newOwner; + while (tmp != null && tmp != outputRecord) + { + tmp = tmp.Owner; + } + + if (tmp != null) + { + newOwner.Owner = outputRecord.Owner; + } + + outputRecord.Owner = newOwner; + } + + /// + /// Computes the signed area of a closed output ring. + /// + /// A point on the output ring. + /// The signed area of the ring. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double ComputeSignedArea(OutputPoint outputPoint) + { + // https://en.wikipedia.org/wiki/Shoelace_formula + double signedArea = 0.0; + OutputPoint outputPoint2 = outputPoint; + do + { + signedArea += Vertex.Cross(outputPoint2.Prev.Point, outputPoint2.Point); + outputPoint2 = outputPoint2.Next!; + } + while (outputPoint2 != outputPoint); + return signedArea * 0.5; + } + + /// + /// Resolves a non-null output record that still owns geometry. + /// + /// The candidate output record. + /// The resolved output record, or if none remains. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static OutputRecord? ResolveOutputRecord(OutputRecord? outputRecord) + { + while (outputRecord != null && outputRecord.Points == null) + { + outputRecord = outputRecord.Owner; + } + + return outputRecord; + } + + /// + /// Validates that an output record is not owned by a descendant. + /// + /// The output record to validate. + /// The owner candidate. + /// when the ownership chain is valid. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsOwnerValid(OutputRecord? outputRecord, OutputRecord? testOwner) + { + while (testOwner != null && testOwner != outputRecord) + { + testOwner = testOwner.Owner; + } + + return testOwner == null; + } + + /// + /// Clears output record links from a hot edge. + /// + /// The active edge to detach. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DetachOutputRecord(ActiveEdge edge) + { + OutputRecord? outputRecord = edge.OutputRecord; + if (outputRecord == null) + { + return; + } + + outputRecord.FrontEdge!.OutputRecord = null; + outputRecord.BackEdge!.OutputRecord = null; + outputRecord.FrontEdge = null; + outputRecord.BackEdge = null; + } + + /// + /// Determines whether an edge is the front edge of its output record. + /// + /// The active edge to query. + /// when the edge is the front edge. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsOutputRecordAscending(ActiveEdge hotEdge) => hotEdge == hotEdge.OutputRecord!.FrontEdge; + + /// + /// Checks whether the two edges in an intersection node are adjacent in the active list. + /// + /// The intersection node to inspect. + /// if the edges are adjacent in the active list. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool AreEdgesAdjacentInActiveList(in IntersectNode intersectionNode) + => (intersectionNode.Edge1.NextInAel == intersectionNode.Edge2) || (intersectionNode.Edge1.PrevInAel == intersectionNode.Edge2); + + /// + /// Clears solution-only data while preserving the input vertices and minima list. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ClearSolutionData() + { + this.activeEdges.ClearActiveEdges(); + this.scanlineSchedule.ClearScanlines(); + this.ClearIntersectionNodes(); + this.OutputRecords.Clear(); + this.horizontalSegments.Clear(); + this.horizontalJoins.Clear(); + this.OutputPoints.Clear(); + } + + /// + /// Clears all clipper state, including cached input data. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + this.ClearSolutionData(); + this.scanlineSchedule.Clear(); + this.vertexList.Clear(); + } + + /// + /// Resets scanline state and sorts local minima before an execution pass. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ResetState() + { + this.scanlineSchedule.Reset(); + this.currentScanlineBottomY = 0; + this.activeEdges.Reset(); + this.succeeded = true; + } + + /// + /// Adds subject contours for the union operation. + /// + /// The subject contours to add. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddSubject(List> paths) + { + this.scanlineSchedule.MarkDirty(); + this.AddPathsToVertexList(paths); + } + + /// + /// Registers a local minima vertex once for the sweep. + /// + /// The vertex that marks a local minima. + /// The schedule collecting minima. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void RegisterLocalMinima(SweepVertex vertex, ScanlineSchedule scanlineSchedule) + { + // Guard against registering the same vertex twice. + if ((vertex.Flags & VertexFlags.LocalMin) != VertexFlags.None) + { + return; + } + + vertex.Flags |= VertexFlags.LocalMin; + scanlineSchedule.AddLocalMinima(new LocalMinima(vertex)); + } + + /// + /// Builds circular vertex lists and captures local minima/maxima for the sweep. + /// + /// The subject contours to process. + private void AddPathsToVertexList(List> paths) + { + int totalVertCnt = 0; + foreach (List path in paths) + { + totalVertCnt += path.Count; + } + + // Pre-size the pool to avoid growth during vertex creation. + this.vertexList.EnsureCapacity(this.vertexList.Count + totalVertCnt); + + foreach (List path in paths) + { + SweepVertex? v0 = null; + SweepVertex? prevVertex = null; + SweepVertex? currVertex; + foreach (Vertex point in path) + { + if (v0 == null) + { + v0 = this.vertexList.Add(point, VertexFlags.None, null); + prevVertex = v0; + continue; + } + + if (prevVertex!.Point != point) + { + currVertex = this.vertexList.Add(point, VertexFlags.None, prevVertex); + prevVertex.Next = currVertex; + prevVertex = currVertex; + } + } + + if (v0 == null || prevVertex?.Prev == null) + { + continue; + } + + if (prevVertex.Point == v0.Point) + { + prevVertex = prevVertex.Prev; + } + + prevVertex.Next = v0; + v0.Prev = prevVertex; + if (prevVertex.Next == prevVertex) + { + continue; + } + + // Non-degenerate closed ring. + prevVertex = v0.Prev; + while (prevVertex != v0 && prevVertex!.Point.Y == v0.Point.Y) + { + prevVertex = prevVertex.Prev; + } + + if (prevVertex == v0) + { + // Flat closed rings still contribute when they touch other contours. + if (!RegisterFlatRingExtrema(v0, this.scanlineSchedule)) + { + continue; + } + + continue; + } + + bool goingUp = prevVertex.Point.Y > v0.Point.Y; + + bool goingUp0 = goingUp; + prevVertex = v0; + currVertex = v0.Next; + while (currVertex != v0) + { + if (currVertex!.Point.Y > prevVertex.Point.Y && goingUp) + { + prevVertex.Flags |= VertexFlags.LocalMax; + goingUp = false; + } + else if (currVertex.Point.Y < prevVertex.Point.Y && !goingUp) + { + goingUp = true; + RegisterLocalMinima(prevVertex, this.scanlineSchedule); + } + + prevVertex = currVertex; + currVertex = currVertex.Next; + } + + if (goingUp != goingUp0) + { + if (goingUp0) + { + RegisterLocalMinima(prevVertex, this.scanlineSchedule); + } + else + { + prevVertex.Flags |= VertexFlags.LocalMax; + } + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool RegisterFlatRingExtrema(SweepVertex start, ScanlineSchedule scanlineSchedule) + { + // For a fully flat closed ring, derive synthetic extrema by scanning once + // for left/right-most vertices: O(m) in ring vertex count. + SweepVertex leftMost = start; + SweepVertex rightMost = start; + SweepVertex current = start.Next!; + while (current != start) + { + if (current.Point.X < leftMost.Point.X) + { + leftMost = current; + } + + if (current.Point.X > rightMost.Point.X) + { + rightMost = current; + } + + current = current.Next!; + } + + if (leftMost == rightMost) + { + return false; + } + + rightMost.Flags |= VertexFlags.LocalMax; + RegisterLocalMinima(leftMost, scanlineSchedule); + return true; + } + + /// + /// Determines whether a closed edge contributes to the union result. + /// + /// The edge to test. + /// if the edge contributes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsContributingClosedEdge(ActiveEdge edge) => edge.WindCount == 1; + + /// + /// Updates the winding count for a closed path edge. + /// + /// The active edge to update. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetWindingCountForClosedEdge(ActiveEdge edge) + { + // Winding counts apply to regions, not edges. The edge wind count tracks the + // higher of the two adjacent region counts. Adjacent regions differ by one. + ActiveEdge? edge2 = edge.PrevInAel; + + if (edge2 == null) + { + edge.WindCount = edge.WindDelta; + } + else + { + // If edge2's wind count follows its wind delta, + // the filled region is to the right of edge2 (so edge is inside). Neither value is 0. + if (edge2.WindCount * edge2.WindDelta < 0) + { + // Opposite signs: edge lies outside edge2's region. + if (Math.Abs(edge2.WindCount) > 1) + { + // Outside this polygon but still inside another. + if (edge2.WindDelta * edge.WindDelta < 0) + { + // Reversing direction; keep the same winding count. + edge.WindCount = edge2.WindCount; + } + else + { + // Otherwise step the winding count toward zero. + edge.WindCount = edge2.WindCount + edge.WindDelta; + } + } + else + { + // Outside all polygons; reset to the edge's own winding. + edge.WindCount = edge.WindDelta; + } + } + else + { + // Same sign: edge lies inside edge2's region. + if (edge2.WindDelta * edge.WindDelta < 0) + { + // Reversing direction; keep the same winding count. + edge.WindCount = edge2.WindCount; + } + else + { + // Otherwise step the winding count away from zero. + edge.WindCount = edge2.WindCount + edge.WindDelta; + } + } + } + } + + /// + /// Inserts any local minima that occur at the current scanline into the active list. + /// + /// The current scanline Y coordinate. + private void InsertLocalMinimaIntoActiveList(double botY) + { + // Insert all minima on the current scanline. + // Horizontal minima use the previous vertex as the descending bound. + while (this.scanlineSchedule.HasLocalMinimaAtY(botY)) + { + LocalMinima localMinima = this.scanlineSchedule.PopLocalMinima(); + ActiveEdge leftBound = this.activeEdges.Acquire(); + leftBound.Bottom = localMinima.Vertex.Point; + leftBound.CurrentX = localMinima.Vertex.Point.X; + leftBound.WindDelta = -1; + leftBound.VertexTop = localMinima.Vertex.Prev; + leftBound.Top = localMinima.Vertex.Prev!.Point; + leftBound.OutputRecord = null; + leftBound.LocalMin = localMinima; + leftBound.UpdateDx(); + + ActiveEdge rightBound = this.activeEdges.Acquire(); + rightBound.Bottom = localMinima.Vertex.Point; + rightBound.CurrentX = localMinima.Vertex.Point.X; + rightBound.WindDelta = 1; + + // Ascending bound. + rightBound.VertexTop = localMinima.Vertex.Next; + rightBound.Top = localMinima.Vertex.Next!.Point; + rightBound.OutputRecord = null; + rightBound.LocalMin = localMinima; + rightBound.UpdateDx(); + + // leftBound starts descending and rightBound ascending. + // Swap them if their geometric ordering is inverted. + if (leftBound.IsHorizontal) + { + if (leftBound.IsHeadingRightHorizontal) + { + SwapActiveEdges(ref leftBound, ref rightBound); + } + } + else if (rightBound.IsHorizontal) + { + if (rightBound.IsHeadingLeftHorizontal) + { + SwapActiveEdges(ref leftBound, ref rightBound); + } + } + else if (leftBound.Dx < rightBound.Dx) + { + SwapActiveEdges(ref leftBound, ref rightBound); + } + + bool contributing; + leftBound.IsLeftBound = true; + this.activeEdges.InsertLeft(leftBound); + + SetWindingCountForClosedEdge(leftBound); + contributing = IsContributingClosedEdge(leftBound); + if (leftBound.IsHorizontal && + rightBound.IsHorizontal && + leftBound.LocalMin.Vertex.Prev == leftBound.LocalMin.Vertex.Next) + { + contributing = false; + } + + rightBound.WindCount = leftBound.WindCount; + ActiveEdgeList.InsertRight(leftBound, rightBound); + + if (contributing) + { + _ = this.AddLocalMinimumOutput(leftBound, rightBound, leftBound.Bottom, true); + if (!leftBound.IsHorizontal) + { + this.CheckJoinLeft(leftBound, leftBound.Bottom); + } + } + + while (rightBound.NextInAel != null && + ActiveEdgeList.IsValidActiveEdgeOrder(rightBound.NextInAel, rightBound)) + { + this.IntersectActiveEdges(rightBound, rightBound.NextInAel, rightBound.Bottom); + this.activeEdges.SwapPositions(rightBound, rightBound.NextInAel); + } + + if (rightBound.IsHorizontal) + { + this.activeEdges.PushHorizontal(rightBound); + } + else + { + this.CheckJoinRight(rightBound, rightBound.Bottom); + this.scanlineSchedule.InsertScanline(rightBound.Top.Y); + } + + if (leftBound.IsHorizontal) + { + this.activeEdges.PushHorizontal(leftBound); + } + else + { + this.scanlineSchedule.InsertScanline(leftBound.Top.Y); + } + } + } + + /// + /// Creates a new output record at a local minimum. + /// + /// The first bound edge. + /// The second bound edge. + /// The local minimum point. + /// Whether this output is created for a split. + /// The created output point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OutputPoint AddLocalMinimumOutput(ActiveEdge edge1, ActiveEdge edge2, Vertex point, bool isNew = false) + { + OutputRecord outputRecord = this.CreateOutputRecord(); + edge1.OutputRecord = outputRecord; + edge2.OutputRecord = outputRecord; + + ActiveEdge? prevHotEdge = edge1.GetPrevHotEdge(); + + // WindDelta reflects input winding, not output orientation. + // Output orientation is driven by which edge is assigned as the front (ascending) edge. + if (prevHotEdge != null) + { + if (this.buildHierarchy) + { + SetOutputOwner(outputRecord, prevHotEdge.OutputRecord!); + } + + outputRecord.Owner = prevHotEdge.OutputRecord; + if (IsOutputRecordAscending(prevHotEdge) == isNew) + { + SetOutputSides(outputRecord, edge2, edge1); + } + else + { + SetOutputSides(outputRecord, edge1, edge2); + } + } + else + { + outputRecord.Owner = null; + if (isNew) + { + SetOutputSides(outputRecord, edge1, edge2); + } + else + { + SetOutputSides(outputRecord, edge2, edge1); + } + } + + OutputPoint outputPoint = this.OutputPoints.Add(point, outputRecord); + outputRecord.Points = outputPoint; + return outputPoint; + } + + /// + /// Joins two output records when a local maximum is encountered. + /// + /// The first active edge. + /// The second active edge. + /// The local maximum point. + /// The last output point, or when no output remains. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OutputPoint? AddLocalMaximumOutput(ActiveEdge edge1, ActiveEdge edge2, Vertex point) + { + if (IsJoined(edge1)) + { + this.SplitEdge(edge1, point); + } + + if (IsJoined(edge2)) + { + this.SplitEdge(edge2, point); + } + + if (edge1.IsFront == edge2.IsFront) + { + bool hasTwoVertexFlatEdge = + (edge1.IsHorizontal && edge1.NextVertex == edge1.LocalMin.Vertex) || + (edge2.IsHorizontal && edge2.NextVertex == edge2.LocalMin.Vertex); + if (hasTwoVertexFlatEdge) + { + OutputPoint outputPoint = this.AddOutputPoint(edge1, point); + _ = this.AddOutputPoint(edge2, point); + SwapOutputRecords(edge1, edge2); + return outputPoint; + } + + if (edge1.IsHorizontal && edge2.IsHorizontal) + { + return this.AddOutputPoint(edge1, point); + } + + this.succeeded = false; + return null; + } + + OutputPoint result = this.AddOutputPoint(edge1, point); + if (edge1.OutputRecord == edge2.OutputRecord) + { + OutputRecord outputRecord = edge1.OutputRecord!; + outputRecord.Points = result; + + if (this.buildHierarchy) + { + ActiveEdge? e = edge1.GetPrevHotEdge(); + if (e == null) + { + outputRecord.Owner = null; + } + else + { + SetOutputOwner(outputRecord, e.OutputRecord!); + } + + // Owner assignment here is provisional and will be resolved later. + } + + DetachOutputRecord(edge1); + } + + // Join in index order to preserve output orientation. + else if (edge1.OutputRecord!.Index < edge2.OutputRecord!.Index) + { + JoinOutputRecords(edge1, edge2); + } + else + { + JoinOutputRecords(edge2, edge1); + } + + return result; + } + + /// + /// Merges the output paths from two active edges into a single record. + /// + /// The primary edge to keep. + /// The secondary edge to merge. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void JoinOutputRecords(ActiveEdge edge1, ActiveEdge edge2) + { + // Append edge2's path onto edge1's path, then discard edge2's path pointers. + // The joining ends rarely share coordinates, so pointer swaps are safe. + OutputPoint p1Start = edge1.OutputRecord!.Points!; + OutputPoint p2Start = edge2.OutputRecord!.Points!; + OutputPoint p1End = p1Start.Next!; + OutputPoint p2End = p2Start.Next!; + if (edge1.IsFront) + { + p2End.Prev = p1Start; + p1Start.Next = p2End; + p2Start.Next = p1End; + p1End.Prev = p2Start; + edge1.OutputRecord!.Points = p2Start; + + edge1.OutputRecord!.FrontEdge = edge2.OutputRecord!.FrontEdge; + if (edge1.OutputRecord!.FrontEdge != null) + { + edge1.OutputRecord!.FrontEdge!.OutputRecord = edge1.OutputRecord; + } + } + else + { + p1End.Prev = p2Start; + p2Start.Next = p1End; + p1Start.Next = p2End; + p2End.Prev = p1Start; + + edge1.OutputRecord!.BackEdge = edge2.OutputRecord!.BackEdge; + if (edge1.OutputRecord!.BackEdge != null) + { + edge1.OutputRecord!.BackEdge!.OutputRecord = edge1.OutputRecord; + } + } + + // After joining, edge2's output record contains no vertices. + edge2.OutputRecord!.FrontEdge = null; + edge2.OutputRecord!.BackEdge = null; + edge2.OutputRecord!.Points = null; + edge1.OutputRecord!.OutputPointCount += edge2.OutputRecord!.OutputPointCount; + SetOutputOwner(edge2.OutputRecord, edge1.OutputRecord); + + // and edge1 and edge2 are maxima and are about to be dropped from the Actives list. + edge1.OutputRecord = null; + edge2.OutputRecord = null; + } + + /// + /// Adds an output point to the front or back of the current output record. + /// + /// The active edge that owns the output. + /// The point to add. + /// The output point that was added or reused. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OutputPoint AddOutputPoint(ActiveEdge edge, Vertex point) + { + // outputRecord.Points is a circular list; Points is the front point and + // Points.Next is the back point for this output record. + OutputRecord outputRecord = edge.OutputRecord!; + bool toFront = edge.IsFront; + OutputPoint opFront = outputRecord.Points!; + OutputPoint opBack = opFront.Next!; + + switch (toFront) + { + case true when point == opFront.Point: + return opFront; + case false when point == opBack.Point: + return opBack; + } + + OutputPoint newOp = this.OutputPoints.Add(point, outputRecord); + opBack.Prev = newOp; + newOp.Prev = opFront; + newOp.Next = opBack; + opFront.Next = newOp; + if (toFront) + { + outputRecord.Points = newOp; + } + + return newOp; + } + + /// + /// Creates a new output record and assigns the next index. + /// + /// The created output record. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OutputRecord CreateOutputRecord() + { + int idx = this.OutputRecords.Count; + OutputRecord result = this.OutputRecords.Add(); + result.Index = idx; + return result; + } + + /// + /// Advances the active edge to the next vertex in the scanbeam. + /// + /// The active edge to update. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UpdateEdgeInActiveList(ActiveEdge edge) + { + edge.Bottom = edge.Top; + edge.VertexTop = edge.NextVertex; + edge.Top = edge.VertexTop.Point; + edge.CurrentX = edge.Bottom.X; + edge.UpdateDx(); + + if (IsJoined(edge)) + { + // Split joined edges before advancing to avoid missing intersections. + this.SplitEdge(edge, edge.Bottom); + } + + if (edge.IsHorizontal) + { + TrimHorizontal(edge, this.PreserveCollinear); + + return; + } + + this.scanlineSchedule.InsertScanline(edge.Top.Y); + + this.CheckJoinLeft(edge, edge.Bottom); + + // Issue #500: check join on the right bound at the bottom point. + this.CheckJoinRight(edge, edge.Bottom, true); + } + + /// + /// Handles an intersection between two active edges at a given point. + /// + /// The first intersecting edge. + /// The second intersecting edge. + /// The intersection point. + private void IntersectActiveEdges(ActiveEdge edge1, ActiveEdge edge2, Vertex point) + { + if (IsJoined(edge1)) + { + this.SplitEdge(edge1, point); + } + + if (IsJoined(edge2)) + { + this.SplitEdge(edge2, point); + } + + // Update winding counts for both edges. + if (edge1.WindCount + edge2.WindDelta == 0) + { + edge1.WindCount = -edge1.WindCount; + } + else + { + edge1.WindCount += edge2.WindDelta; + } + + if (edge2.WindCount - edge1.WindDelta == 0) + { + edge2.WindCount = -edge2.WindCount; + } + else + { + edge2.WindCount -= edge1.WindDelta; + } + + int oldE1WindCount = edge1.WindCount; + int oldE2WindCount = edge2.WindCount; + + bool e1WindCountIs0or1 = oldE1WindCount is 0 or 1; + bool e2WindCountIs0or1 = oldE2WindCount is 0 or 1; + bool edge1IsTwoVertexFlatRing = IsTwoVertexFlatRingEdge(edge1); + bool edge2IsTwoVertexFlatRing = IsTwoVertexFlatRingEdge(edge2); + + if ((!edge1.IsHot && !e1WindCountIs0or1 && !edge1IsTwoVertexFlatRing) || + (!edge2.IsHot && !e2WindCountIs0or1 && !edge2IsTwoVertexFlatRing)) + { + return; + } + + if (edge1IsTwoVertexFlatRing || edge2IsTwoVertexFlatRing) + { + if (edge1.IsHot ^ edge2.IsHot) + { + ActiveEdge hotEdge = edge1.IsHot ? edge1 : edge2; + ActiveEdge flatEdge = edge1IsTwoVertexFlatRing ? edge1 : edge2; + if (!flatEdge.IsHot) + { + // Keep the fix in-sweep: inject touch->tip->touch directly into the + // hot output chain in O(1), avoiding any post-process contour scans. + Vertex tip = GetFlatRingTip(flatEdge, point); + _ = this.AddOutputPoint(hotEdge, point); + _ = this.AddOutputPoint(hotEdge, tip); + _ = this.AddOutputPoint(hotEdge, point); + return; + } + } + } + + // Emit output based on hot edges and winding state. + + // If both edges are hot, treat as maxima or crossing. + if (edge1.IsHot && edge2.IsHot) + { + if ((oldE1WindCount != 0 && oldE1WindCount != 1) || (oldE2WindCount != 0 && oldE2WindCount != 1)) + { + _ = this.AddLocalMaximumOutput(edge1, edge2, point); + } + else if (edge1.IsFront || (edge1.OutputRecord == edge2.OutputRecord)) + { + // this 'else if' condition isn't strictly needed but + // it's sensible to split polygons that only touch at + // a common vertex (not at common edges). + _ = this.AddLocalMaximumOutput(edge1, edge2, point); + } + else + { + // Treat as a crossing; emit and swap output records. + _ = this.AddOutputPoint(edge1, point); + SwapOutputRecords(edge1, edge2); + } + } + + // If only one edge is hot, emit and swap. + else if (edge1.IsHot) + { + _ = this.AddOutputPoint(edge1, point); + SwapOutputRecords(edge1, edge2); + } + else if (edge2.IsHot) + { + _ = this.AddOutputPoint(edge2, point); + SwapOutputRecords(edge1, edge2); + } + + // If both edges are cold, only minima with winding=1 start output. + else + { + if (oldE1WindCount == 1 && oldE2WindCount == 1) + { + _ = this.AddLocalMinimumOutput(edge1, edge2, point); + } + } + } + + /// + /// Executes the sweep-line union. + /// + private void ExecuteInternal() + { + this.ResetState(); + if (!this.scanlineSchedule.TryPopScanline(out double y)) + { + return; + } + + // Process each scanbeam: insert local minima, handle horizontals, resolve intersections, + // then advance to the next scanline. + while (this.succeeded) + { + this.InsertLocalMinimaIntoActiveList(y); + ActiveEdge? edge; + while (this.activeEdges.TryPopHorizontal(out edge)) + { + this.ProcessHorizontal(edge!); + } + + if (this.horizontalSegments.Count > 0) + { + this.ConvertHorizontalSegmentsToJoins(); + this.horizontalSegments.Clear(); + } + + // Advance to the next scanbeam. + this.currentScanlineBottomY = y; + if (!this.scanlineSchedule.TryPopScanline(out y)) + { + // y is now the new top of the scanbeam. + break; + } + + this.ProcessIntersections(y); + this.ProcessScanbeamTop(y); + while (this.activeEdges.TryPopHorizontal(out edge)) + { + this.ProcessHorizontal(edge!); + } + } + + if (this.succeeded) + { + this.ProcessHorizontalJoins(); + } + } + + /// + /// Executes the sweep-line union, leaving output records populated for conversion. + /// + /// Whether hierarchy-sensitive output ownership is required. + /// if the sweep completed successfully. + public bool Execute(bool buildHierarchy) + { + this.buildHierarchy = buildHierarchy; + try + { + this.ExecuteInternal(); + } + catch + { + this.succeeded = false; + } + + return this.succeeded; + } + + /// + /// Builds and processes edge intersections for the current scanbeam. + /// + /// The scanbeam top Y coordinate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ProcessIntersections(double topY) + { + if (!this.BuildIntersectionList(topY)) + { + return; + } + + this.ProcessIntersectionList(); + this.ClearIntersectionNodes(); + } + + /// + /// Clears the list of pending intersection nodes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ClearIntersectionNodes() => this.intersectionList.Clear(); + + /// + /// Adds a new intersection node between two edges at the current scanbeam. + /// + /// The first edge. + /// The second edge. + /// The scanbeam top Y coordinate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddIntersectionNode(ActiveEdge edge1, ActiveEdge edge2, double topY) + { + if (!PolygonUtilities.TryGetLineIntersection( + edge1.Bottom, edge1.Top, edge2.Bottom, edge2.Top, out Vertex intersectionPoint)) + { + intersectionPoint = new Vertex(edge1.CurrentX, topY); + } + + // Prevent vertical segment ordering violations: if the intersection creates a perfectly + // vertical split where the new split point has the same X but lower Y than the bottom, + // nudge X to the next representable value. Vertical segments must be processed bottom-to-top, + // but the current bottom event is already being processed, so we cannot reorder. Moving X by + // one ULP ensures the split point sorts after the bottom event. + // See: https://github.com/21re/rust-geo-booleanop/pull/11 + if (intersectionPoint.X == edge1.Bottom.X && intersectionPoint.Y < edge1.Bottom.Y) + { + intersectionPoint = new Vertex(intersectionPoint.X.NextAfter(double.PositiveInfinity), intersectionPoint.Y); + } + else if (intersectionPoint.X == edge2.Bottom.X && intersectionPoint.Y < edge2.Bottom.Y) + { + intersectionPoint = new Vertex(intersectionPoint.X.NextAfter(double.PositiveInfinity), intersectionPoint.Y); + } + + // Clamp intersections that drift outside the scanbeam due to numeric error. + if (intersectionPoint.Y > this.currentScanlineBottomY || intersectionPoint.Y < topY) + { + double absDx1 = Math.Abs(edge1.Dx); + double absDx2 = Math.Abs(edge2.Dx); + + // dx is dX/dY, so large magnitudes mean the edge is nearly horizontal (dY is tiny). + // Using TopX with a clamped Y can amplify floating-point error in that case, so we + // fall back to closest-point clamping when |dx| > 100 (about 0.57 degrees from horizontal). + // This threshold keeps near-horizontal intersections stable without scaling the input. + switch (absDx1 > 100) + { + case true when absDx2 > 100: + { + intersectionPoint = absDx1 > absDx2 + ? PolygonUtilities.ClosestPointOnSegment(intersectionPoint, edge1.Bottom, edge1.Top) + : PolygonUtilities.ClosestPointOnSegment(intersectionPoint, edge2.Bottom, edge2.Top); + + break; + } + + case true: + intersectionPoint = PolygonUtilities.ClosestPointOnSegment(intersectionPoint, edge1.Bottom, edge1.Top); + break; + default: + { + if (absDx2 > 100) + { + intersectionPoint = PolygonUtilities.ClosestPointOnSegment(intersectionPoint, edge2.Bottom, edge2.Top); + } + else + { + double targetY = intersectionPoint.Y < topY ? topY : this.currentScanlineBottomY; + double targetX = absDx1 < absDx2 ? ActiveEdge.TopX(edge1, targetY) : ActiveEdge.TopX(edge2, targetY); + intersectionPoint = new Vertex(targetX, targetY); + } + + break; + } + } + } + + IntersectNode node = new(intersectionPoint, edge1, edge2); + this.intersectionList.Add(node); + } + + /// + /// Extracts an edge from the sorted edge list. + /// + /// The edge to extract. + /// The next edge after the extracted one. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ActiveEdge? ExtractFromSortedEdges(ActiveEdge edge) + { + ActiveEdge? res = edge.NextInSel; + if (res != null) + { + res.PrevInSel = edge.PrevInSel; + } + + edge.PrevInSel!.NextInSel = res; + return res; + } + + /// + /// Inserts an edge before another edge in the sorted edge list. + /// + /// The edge to insert. + /// The reference edge. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InsertBeforeInSortedEdges(ActiveEdge edge1, ActiveEdge edge2) + { + edge1.PrevInSel = edge2.PrevInSel; + if (edge1.PrevInSel != null) + { + edge1.PrevInSel.NextInSel = edge1; + } + + edge1.NextInSel = edge2; + edge2.PrevInSel = edge1; + } + + /// + /// Builds the list of intersections required to sort edges at the top of the scanbeam. + /// + /// The scanbeam top Y coordinate. + /// if any intersections were found. + private bool BuildIntersectionList(double topY) + { + if (this.activeEdges.Head?.NextInAel == null) + { + return false; + } + + // Compute edge positions at the top of the scanbeam to derive required intersections. + ActiveEdge? sortedHead = this.activeEdges.CopyToSorted(topY); + + // Find intersections via a stable merge sort so only adjacent edges intersect. + // Nodes are stored for ProcessIntersectionList. See https://stackoverflow.com/a/46319131/359538. + ActiveEdge? left = sortedHead; + + while (left!.Jump != null) + { + ActiveEdge? prevBase = null; + while (left?.Jump != null) + { + ActiveEdge? currBase = left; + ActiveEdge? right = left.Jump; + ActiveEdge? lEnd = right; + ActiveEdge? rEnd = right.Jump; + left.Jump = rEnd; + while (left != lEnd && right != rEnd) + { + if (right!.CurrentX < left!.CurrentX) + { + ActiveEdge? tmp = right.PrevInSel!; + while (true) + { + this.AddIntersectionNode(tmp, right, topY); + if (tmp == left) + { + break; + } + + tmp = tmp.PrevInSel!; + } + + tmp = right; + right = ExtractFromSortedEdges(tmp); + lEnd = right; + InsertBeforeInSortedEdges(tmp, left); + if (left != currBase) + { + continue; + } + + currBase = tmp; + currBase.Jump = rEnd; + if (prevBase == null) + { + sortedHead = currBase; + } + else + { + prevBase.Jump = currBase; + } + } + else + { + left = left.NextInSel; + } + } + + prevBase = currBase; + left = rEnd; + } + + left = sortedHead; + } + + return this.intersectionList.Count > 0; + } + + /// + /// Processes the intersection list in bottom-up order, swapping edges and generating output. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ProcessIntersectionList() + { + // Intersections must be processed bottom-up, and only between adjacent edges. + + // Sort so intersections proceed from bottom to top. + this.intersectionList.Sort(default(IntersectNodeComparer)); + + // Reorder as needed to ensure intersecting edges are adjacent. + for (int i = 0; i < this.intersectionList.Count; ++i) + { + if (!AreEdgesAdjacentInActiveList(this.intersectionList[i])) + { + int j = i + 1; + while (!AreEdgesAdjacentInActiveList(this.intersectionList[j])) + { + j++; + } + + // Swap into adjacency. + (this.intersectionList[j], this.intersectionList[i]) = + (this.intersectionList[i], this.intersectionList[j]); + } + + IntersectNode node = this.intersectionList[i]; + this.IntersectActiveEdges(node.Edge1, node.Edge2, node.Point); + this.activeEdges.SwapPositions(node.Edge1, node.Edge2); + + node.Edge1.CurrentX = node.Point.X; + node.Edge2.CurrentX = node.Point.X; + this.CheckJoinLeft(node.Edge2, node.Point, true); + this.CheckJoinRight(node.Edge1, node.Point, true); + } + } + + /// + /// Resolves left-to-right direction and bounds for a horizontal edge. + /// + /// The horizontal edge. + /// The maxima vertex for the horizontal span. + /// The left bound X value. + /// The right bound X value. + /// when the edge is left-to-right. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ResetHorizontalDirection( + ActiveEdge horizontalEdge, + SweepVertex? vertexMax, + out double leftX, + out double rightX) + { + if (horizontalEdge.Bottom.X == horizontalEdge.Top.X) + { + // Degenerate horizontal edge (zero length). + leftX = horizontalEdge.CurrentX; + rightX = horizontalEdge.CurrentX; + ActiveEdge? edge = horizontalEdge.NextInAel; + while (edge != null && edge.VertexTop != vertexMax) + { + edge = edge.NextInAel; + } + + return edge != null; + } + + if (horizontalEdge.CurrentX < horizontalEdge.Top.X) + { + leftX = horizontalEdge.CurrentX; + rightX = horizontalEdge.Top.X; + return true; + } + + // Right to left. + leftX = horizontalEdge.Top.X; + rightX = horizontalEdge.CurrentX; + return false; + } + + /// + /// Trims collinear points from a horizontal edge. + /// + /// The horizontal edge. + /// Whether collinear points are preserved. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void TrimHorizontal(ActiveEdge horizontalEdge, bool preserveCollinear) + { + bool wasTrimmed = false; + Vertex point = horizontalEdge.NextVertex.Point; + + while (point.Y == horizontalEdge.Top.Y) + { + // Always trim 180-degree spikes in closed paths; otherwise stop when preserving collinear. + if (preserveCollinear && + (point.X < horizontalEdge.Top.X) != (horizontalEdge.Bottom.X < horizontalEdge.Top.X)) + { + break; + } + + horizontalEdge.VertexTop = horizontalEdge.NextVertex; + horizontalEdge.Top = point; + wasTrimmed = true; + if (horizontalEdge.IsMaxima) + { + break; + } + + point = horizontalEdge.NextVertex.Point; + } + + if (wasTrimmed) + { + // Recompute slope after trimming. + horizontalEdge.UpdateDx(); + } + } + + /// + /// Adds a horizontal segment for later join processing. + /// + /// The output point that anchors the segment. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddHorizontalSegment(OutputPoint outputPoint) + => this.horizontalSegments.Add(new HorizontalSegment(outputPoint)); + + /// + /// Returns the last output point for a hot edge. + /// + /// The hot edge to inspect. + /// The last output point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static OutputPoint GetLastOutputPoint(ActiveEdge hotEdge) + { + OutputRecord outputRecord = hotEdge.OutputRecord!; + return (hotEdge == outputRecord.FrontEdge) ? + outputRecord.Points! : outputRecord.Points!.Next!; + } + + /// + /// Processes a horizontal edge and resolves any intersections adouble the scanline. + /// + /// The horizontal edge to process. + private void ProcessHorizontal(ActiveEdge horizontalEdge) + /******************************************************************************* + * Notes: Horizontal edges (HEs) at scanline intersections (i.e. at the top or * + * bottom of a scanbeam) are processed as if layered. The order in which HEs * + * are processed doesn't matter. HEs intersect with the bottom vertices of * + * other HEs[#] and with non-horizontal edges [*]. Once these intersections * + * are completed, intermediate HEs are 'promoted' to the next edge in their * + * bounds, and they in turn may be intersected[%] by other HEs. * + * * + * eg: 3 horizontals at a scanline: / | / / * + * | / | (HE3)o ========%========== o * + * o ======= o(HE2) / | / / * + * o ============#=========*======*========#=========o (HE1) * + * / | / | / * + *******************************************************************************/ + { + double y = horizontalEdge.Bottom.Y; + + SweepVertex? vertexMax = GetMaximaVertexAtCurrentY(horizontalEdge); + + bool isLeftToRight = + ResetHorizontalDirection(horizontalEdge, vertexMax, out double leftX, out double rightX); + + ActiveEdge? immediatePair = horizontalEdge.NextInAel; + if (immediatePair == null || + immediatePair.LocalMin.Vertex != horizontalEdge.LocalMin.Vertex || + !immediatePair.IsHorizontal) + { + immediatePair = horizontalEdge.PrevInAel; + } + + if (!horizontalEdge.IsHot && + IsTwoVertexFlatRingEdge(horizontalEdge) && + immediatePair != null && + immediatePair.LocalMin.Vertex == horizontalEdge.LocalMin.Vertex && + immediatePair.IsHorizontal) + { + // Fast path for degenerate flat rings. Complexity is O(K) where K is the + // number of active edges crossing this horizontal span at the scanline. + // No extra contour collections are built. + ActiveEdge? scan = isLeftToRight ? horizontalEdge.NextInAel : horizontalEdge.PrevInAel; + if (scan == immediatePair) + { + scan = isLeftToRight ? immediatePair.NextInAel : immediatePair.PrevInAel; + } + + while (scan != null) + { + if ((isLeftToRight && scan.CurrentX > rightX) || + (!isLeftToRight && scan.CurrentX < leftX)) + { + break; + } + + if (!scan.IsHorizontal) + { + Vertex point = new(scan.CurrentX, y); + if (isLeftToRight) + { + this.IntersectActiveEdges(horizontalEdge, scan, point); + } + else + { + this.IntersectActiveEdges(scan, horizontalEdge, point); + } + } + + scan = isLeftToRight ? scan.NextInAel : scan.PrevInAel; + } + + this.activeEdges.Remove(immediatePair); + this.activeEdges.Remove(horizontalEdge); + return; + } + + if (horizontalEdge.IsHot) + { + OutputPoint outputPoint = this.AddOutputPoint(horizontalEdge, new Vertex(horizontalEdge.CurrentX, y)); + this.AddHorizontalSegment(outputPoint); + } + + int horizontalLoopGuard = 0; + while (true) + { + if (++horizontalLoopGuard > HorizontalLoopFailSafeLimit) + { + // Fail-safe for corrupted links: bail out instead of throwing/hanging. + return; + } + + // Traverse consecutive horizontal edges on this scanline. + ActiveEdge? edge = isLeftToRight ? horizontalEdge.NextInAel : horizontalEdge.PrevInAel; + + int edgeLoopGuard = 0; + while (edge != null) + { + if (++edgeLoopGuard > HorizontalLoopFailSafeLimit) + { + // Fail-safe for corrupted links: bail out instead of throwing/hanging. + return; + } + + if (edge.VertexTop == vertexMax) + { + // Handle the maxima pair before processing other intersections. + if (horizontalEdge.IsHot && IsJoined(edge)) + { + this.SplitEdge(edge, edge.Top); + } + + if (horizontalEdge.IsHot) + { + while (horizontalEdge.VertexTop != vertexMax) + { + _ = this.AddOutputPoint(horizontalEdge, horizontalEdge.Top); + this.UpdateEdgeInActiveList(horizontalEdge); + } + + if (isLeftToRight) + { + _ = this.AddLocalMaximumOutput(horizontalEdge, edge, horizontalEdge.Top); + } + else + { + _ = this.AddLocalMaximumOutput(edge, horizontalEdge, horizontalEdge.Top); + } + } + + this.activeEdges.Remove(edge); + this.activeEdges.Remove(horizontalEdge); + return; + } + + // If this horizontal is a maxima, keep going until its pair is reached; + // otherwise check for break conditions. + Vertex point; + if (vertexMax != horizontalEdge.VertexTop) + { + // Stop once the edge moves beyond the horizontal span. + if ((isLeftToRight && edge.CurrentX > rightX) || + (!isLeftToRight && edge.CurrentX < leftX)) + { + break; + } + + if (edge.CurrentX == horizontalEdge.Top.X && !edge.IsHorizontal) + { + point = horizontalEdge.NextVertex.Point; + + // At the horizontal end, stop only when the outslope overtakes the edge + // (greater when heading right, smaller when heading left). + if ((isLeftToRight && (ActiveEdge.TopX(edge, point.Y) >= point.X)) || + (!isLeftToRight && (ActiveEdge.TopX(edge, point.Y) <= point.X))) + { + break; + } + } + } + + point = new Vertex(edge.CurrentX, y); + + if (isLeftToRight) + { + this.IntersectActiveEdges(horizontalEdge, edge, point); + this.activeEdges.SwapPositions(horizontalEdge, edge); + this.CheckJoinLeft(edge, point); + horizontalEdge.CurrentX = edge.CurrentX; + edge = horizontalEdge.NextInAel; + } + else + { + this.IntersectActiveEdges(edge, horizontalEdge, point); + this.activeEdges.SwapPositions(edge, horizontalEdge); + this.CheckJoinRight(edge, point); + horizontalEdge.CurrentX = edge.CurrentX; + edge = horizontalEdge.PrevInAel; + } + + if (horizontalEdge.IsHot) + { + this.AddHorizontalSegment(GetLastOutputPoint(horizontalEdge)); + } + } + + // Stop once no more consecutive horizontals remain. + if (horizontalEdge.NextVertex.Point.Y != horizontalEdge.Top.Y) + { + break; + } + + // Advance to the next horizontal segment in the bound. + if (horizontalEdge.IsHot) + { + _ = this.AddOutputPoint(horizontalEdge, horizontalEdge.Top); + } + + this.UpdateEdgeInActiveList(horizontalEdge); + + isLeftToRight = ResetHorizontalDirection( + horizontalEdge, + vertexMax, + out leftX, + out rightX); + } + + // Finished this horizontal chain. + if (horizontalEdge.IsHot) + { + OutputPoint outputPoint = this.AddOutputPoint(horizontalEdge, horizontalEdge.Top); + this.AddHorizontalSegment(outputPoint); + } + + // Advance past the final intermediate horizontal. + this.UpdateEdgeInActiveList(horizontalEdge); + } + + /// + /// Processes edges that reach the top of the scanbeam, updating or removing them. + /// + /// The scanbeam top Y coordinate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ProcessScanbeamTop(double y) + { + this.activeEdges.ClearHorizontalQueue(); + ActiveEdge? edge = this.activeEdges.Head; + while (edge != null) + { + // Edge is never horizontal at this point. + if (edge.Top.Y == y) + { + edge.CurrentX = edge.Top.X; + if (edge.IsMaxima) + { + // Maxima reached; finalize this bound. + edge = this.ProcessMaxima(edge); + continue; + } + + // Intermediate vertex on the bound. + if (edge.IsHot) + { + _ = this.AddOutputPoint(edge, edge.Top); + } + + this.UpdateEdgeInActiveList(edge); + + // Queue horizontals for dedicated processing. + if (edge.IsHorizontal) + { + this.activeEdges.PushHorizontal(edge); + } + } + + // Edge continues through the scanbeam. + else + { + edge.CurrentX = ActiveEdge.TopX(edge, y); + } + + edge = edge.NextInAel; + } + } + + /// + /// Handles a maxima event for the active edge. + /// + /// The active edge at the maxima. + /// The next edge to continue scanning from. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ActiveEdge? ProcessMaxima(ActiveEdge edge) + { + ActiveEdge? prevEdge = edge.PrevInAel; + ActiveEdge? nextEdge = edge.NextInAel; + + ActiveEdge? maxPair = FindMaximaPair(edge); + if (maxPair == null) + { + // Horizontal maxima pair is handled in horizontal processing. + return nextEdge; + } + + if (IsJoined(edge)) + { + this.SplitEdge(edge, edge.Top); + } + + if (IsJoined(maxPair)) + { + this.SplitEdge(maxPair, maxPair.Top); + } + + // Only non-horizontal maxima reach here. + // Process edges between the maxima pair. + while (nextEdge != maxPair) + { + this.IntersectActiveEdges(edge, nextEdge!, edge.Top); + this.activeEdges.SwapPositions(edge, nextEdge!); + nextEdge = edge.NextInAel; + } + + // At this point edge.NextInAel == maxPair. + if (edge.IsHot) + { + _ = this.AddLocalMaximumOutput(edge, maxPair, edge.Top); + } + + this.activeEdges.Remove(edge); + this.activeEdges.Remove(maxPair); + return prevEdge != null ? prevEdge.NextInAel : this.activeEdges.Head; + } + + /// + /// Tests whether an edge is currently joined to a neighbor. + /// + /// The edge to inspect. + /// if the edge is joined. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsJoined(ActiveEdge edge) => edge.JoinWith != JoinWith.None; + + /// + /// Splits a joined edge at the specified point. + /// + /// The edge to split. + /// The split point. + private void SplitEdge(ActiveEdge edge, Vertex point) + { + if (edge.JoinWith == JoinWith.Right) + { + edge.JoinWith = JoinWith.None; + edge.NextInAel!.JoinWith = JoinWith.None; + _ = this.AddLocalMinimumOutput(edge, edge.NextInAel, point, true); + } + else + { + edge.JoinWith = JoinWith.None; + edge.PrevInAel!.JoinWith = JoinWith.None; + _ = this.AddLocalMinimumOutput(edge.PrevInAel, edge, point, true); + } + } + + /// + /// Attempts to join the current edge with its left neighbor. + /// + /// The active edge being evaluated. + /// The candidate join point. + /// Whether to check the current X for proximity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CheckJoinLeft( + ActiveEdge edge, + Vertex point, + bool checkCurrX = false) + { + ActiveEdge? prev = edge.PrevInAel; + if (prev == null || + !edge.IsHot || !prev.IsHot || + edge.IsHorizontal || prev.IsHorizontal) + { + return; + } + + // Reject joins that are too close to extrema (Issue #490). + if ((point.Y < edge.Top.Y + JoinExtremaDelta || point.Y < prev.Top.Y + JoinExtremaDelta) && + ((edge.Bottom.Y > point.Y) || (prev.Bottom.Y > point.Y))) + { + // Issue #490. + return; + } + + if (checkCurrX) + { + if (PolygonUtilities.PerpendicularDistanceSquared(point, prev.Bottom, prev.Top) > + JoinPerpendicularDistanceSquaredTolerance) + { + return; + } + } + else if (edge.CurrentX != prev.CurrentX) + { + return; + } + + if (!PolygonUtilities.IsCollinear(edge.Top, point, prev.Top)) + { + return; + } + + if (edge.OutputRecord!.Index == prev.OutputRecord!.Index) + { + _ = this.AddLocalMaximumOutput(prev, edge, point); + } + else if (edge.OutputRecord!.Index < prev.OutputRecord!.Index) + { + JoinOutputRecords(edge, prev); + } + else + { + JoinOutputRecords(prev, edge); + } + + prev.JoinWith = JoinWith.Right; + edge.JoinWith = JoinWith.Left; + } + + /// + /// Attempts to join the current edge with its right neighbor. + /// + /// The active edge being evaluated. + /// The candidate join point. + /// Whether to check the current X for proximity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CheckJoinRight( + ActiveEdge edge, + Vertex point, + bool checkCurrX = false) + { + ActiveEdge? next = edge.NextInAel; + if (next == null || + !edge.IsHot || !next.IsHot || + edge.IsHorizontal || next.IsHorizontal) + { + return; + } + + // Reject joins that are too close to extrema (Issue #490). + if ((point.Y < edge.Top.Y + JoinExtremaDelta || point.Y < next.Top.Y + JoinExtremaDelta) && + ((edge.Bottom.Y > point.Y) || (next.Bottom.Y > point.Y))) + { + // Issue #490. + return; + } + + if (checkCurrX) + { + if (PolygonUtilities.PerpendicularDistanceSquared(point, next.Bottom, next.Top) > + JoinPerpendicularDistanceSquaredTolerance) + { + return; + } + } + else if (edge.CurrentX != next.CurrentX) + { + return; + } + + if (!PolygonUtilities.IsCollinear(edge.Top, point, next.Top)) + { + return; + } + + if (edge.OutputRecord!.Index == next.OutputRecord!.Index) + { + _ = this.AddLocalMaximumOutput(edge, next, point); + } + else if (edge.OutputRecord!.Index < next.OutputRecord!.Index) + { + JoinOutputRecords(edge, next); + } + else + { + JoinOutputRecords(next, edge); + } + + edge.JoinWith = JoinWith.Right; + next.JoinWith = JoinWith.Left; + } + + /// + /// Ensures all output points in a record reference the correct owner. + /// + /// The output record to normalize. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void FixOutputRecordPoints(OutputRecord outputRecord) + { + OutputPoint outputPoint = outputRecord.Points!; + do + { + outputPoint.OutputRecord = outputRecord; + outputPoint = outputPoint.Next!; + } + while (outputPoint != outputRecord.Points); + } + + /// + /// Determines the left/right ordering of a horizontal segment. + /// + /// The segment to update. + /// The previous output point. + /// The next output point. + /// if the segment has non-zero length. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool SetHorizontalSegmentHeadingForward(HorizontalSegment horizontalSegment, OutputPoint prevPoint, OutputPoint nextPoint) + { + if (prevPoint.Point.X == nextPoint.Point.X) + { + return false; + } + + if (prevPoint.Point.X < nextPoint.Point.X) + { + horizontalSegment.LeftPoint = prevPoint; + horizontalSegment.RightPoint = nextPoint; + horizontalSegment.LeftToRight = true; + } + else + { + horizontalSegment.LeftPoint = nextPoint; + horizontalSegment.RightPoint = prevPoint; + horizontalSegment.LeftToRight = false; + } + + return true; + } + + /// + /// Normalizes a horizontal segment and sets its left/right pointers. + /// + /// The segment to update. + /// if the segment remains valid after normalization. + private static bool UpdateHorizontalSegment(HorizontalSegment horizontalSegment) + { + OutputPoint outputPoint = horizontalSegment.LeftPoint!; + OutputRecord outputRecord = ResolveOutputRecord(outputPoint.OutputRecord)!; + bool outputRecordHasEdges = outputRecord.FrontEdge != null; + double currentY = outputPoint.Point.Y; + OutputPoint prevPoint = outputPoint, nextPoint = outputPoint; + if (outputRecordHasEdges) + { + OutputPoint opA = outputRecord.Points!, opZ = opA.Next!; + while (prevPoint != opZ && prevPoint.Prev.Point.Y == currentY) + { + prevPoint = prevPoint.Prev; + } + + while (nextPoint != opA && nextPoint.Next!.Point.Y == currentY) + { + nextPoint = nextPoint.Next; + } + } + else + { + while (prevPoint.Prev != nextPoint && prevPoint.Prev.Point.Y == currentY) + { + prevPoint = prevPoint.Prev; + } + + while (nextPoint.Next != prevPoint && nextPoint.Next!.Point.Y == currentY) + { + nextPoint = nextPoint.Next; + } + } + + bool result = + SetHorizontalSegmentHeadingForward(horizontalSegment, prevPoint, nextPoint) && + horizontalSegment.LeftPoint!.HorizontalSegment == null; + + if (result) + { + horizontalSegment.LeftPoint!.HorizontalSegment = horizontalSegment; + } + else + { + // Mark invalid so sorting pushes it to the end. + horizontalSegment.RightPoint = null; + } + + return result; + } + + /// + /// Duplicates an output point and inserts it before or after the original. + /// + /// The point to duplicate. + /// Whether to insert after the original. + /// The newly inserted output point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OutputPoint DuplicateOutputPoint(OutputPoint outputPoint, bool insertAfter) + { + OutputPoint result = this.OutputPoints.Add(outputPoint.Point, outputPoint.OutputRecord); + if (insertAfter) + { + result.Next = outputPoint.Next; + result.Next!.Prev = result; + result.Prev = outputPoint; + outputPoint.Next = result; + } + else + { + result.Prev = outputPoint.Prev; + result.Prev.Next = result; + result.Next = outputPoint; + outputPoint.Prev = result; + } + + return result; + } + + /// + /// Sorts horizontal segments by their X extents. + /// + /// The first segment. + /// The second segment. + /// A comparison result for sorting. + private static int CompareHorizontalSegments(HorizontalSegment? segment1, HorizontalSegment? segment2) + { + if (segment1 == null || segment2 == null) + { + return 0; + } + + if (segment1.RightPoint == null) + { + return segment2.RightPoint == null ? 0 : 1; + } + + if (segment2.RightPoint == null) + { + return -1; + } + + return segment1.LeftPoint!.Point.X.CompareTo(segment2.LeftPoint!.Point.X); + } + + /// + /// Converts horizontal segments into join candidates for post-processing. + /// + private void ConvertHorizontalSegmentsToJoins() + { + int k = 0; + foreach (HorizontalSegment horizontalSegment in this.horizontalSegments) + { + if (UpdateHorizontalSegment(horizontalSegment)) + { + k++; + } + } + + if (k < 2) + { + return; + } + + this.horizontalSegments.Sort(CompareHorizontalSegments); + + for (int i = 0; i < k - 1; i++) + { + HorizontalSegment segment1 = this.horizontalSegments[i]; + + // Find overlapping segments to generate join candidates. + for (int j = i + 1; j < k; j++) + { + HorizontalSegment segment2 = this.horizontalSegments[j]; + if ((segment2.LeftPoint!.Point.X >= segment1.RightPoint!.Point.X) || + (segment2.LeftToRight == segment1.LeftToRight) || + (segment2.RightPoint!.Point.X <= segment1.LeftPoint!.Point.X)) + { + continue; + } + + double currentY = segment1.LeftPoint.Point.Y; + if (segment1.LeftToRight) + { + while (segment1.LeftPoint.Next!.Point.Y == currentY && + segment1.LeftPoint.Next.Point.X <= segment2.LeftPoint.Point.X) + { + segment1.LeftPoint = segment1.LeftPoint.Next; + } + + while (segment2.LeftPoint.Prev.Point.Y == currentY && + segment2.LeftPoint.Prev.Point.X <= segment1.LeftPoint.Point.X) + { + segment2.LeftPoint = segment2.LeftPoint.Prev; + } + + _ = this.horizontalJoins.Add( + this.DuplicateOutputPoint(segment1.LeftPoint, true), + this.DuplicateOutputPoint(segment2.LeftPoint, false)); + } + else + { + while (segment1.LeftPoint.Prev.Point.Y == currentY && + segment1.LeftPoint.Prev.Point.X <= segment2.LeftPoint.Point.X) + { + segment1.LeftPoint = segment1.LeftPoint.Prev; + } + + while (segment2.LeftPoint.Next!.Point.Y == currentY && + segment2.LeftPoint.Next.Point.X <= segment1.LeftPoint.Point.X) + { + segment2.LeftPoint = segment2.LeftPoint.Next; + } + + _ = this.horizontalJoins.Add( + this.DuplicateOutputPoint(segment2.LeftPoint, true), + this.DuplicateOutputPoint(segment1.LeftPoint, false)); + } + } + } + } + + /// + /// Builds a cleaned contour by removing redundant collinear points. + /// + /// A point on the output ring. + /// A contour with redundant points removed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static List BuildCleanContour(OutputPoint outputPoint) + { + List result = []; + OutputPoint outputPoint2 = outputPoint; + while (outputPoint2.Next != outputPoint && + ((outputPoint2.Point.X == outputPoint2.Next!.Point.X && + outputPoint2.Point.X == outputPoint2.Prev.Point.X) || + (outputPoint2.Point.Y == outputPoint2.Next.Point.Y && + outputPoint2.Point.Y == outputPoint2.Prev.Point.Y))) + { + outputPoint2 = outputPoint2.Next; + } + + result.Add(outputPoint2.Point); + OutputPoint prevOp = outputPoint2; + outputPoint2 = outputPoint2.Next; + while (outputPoint2 != outputPoint) + { + if ((outputPoint2.Point.X != outputPoint2.Next!.Point.X || outputPoint2.Point.X != prevOp.Point.X) && + (outputPoint2.Point.Y != outputPoint2.Next.Point.Y || outputPoint2.Point.Y != prevOp.Point.Y)) + { + result.Add(outputPoint2.Point); + prevOp = outputPoint2; + } + + outputPoint2 = outputPoint2.Next; + } + + return result; + } + + /// + /// Classifies a point against an output polygon. + /// + /// The point to test. + /// A point on the polygon ring. + /// The point-in-polygon classification. + private static PointInPolygonResult PointInOutputPolygon(Vertex point, OutputPoint outputPoint) + { + if (outputPoint == outputPoint.Next || outputPoint.Prev == outputPoint.Next) + { + return PointInPolygonResult.Outside; + } + + OutputPoint outputPoint2 = outputPoint; + do + { + if (outputPoint.Point.Y != point.Y) + { + break; + } + + outputPoint = outputPoint.Next!; + } + while (outputPoint != outputPoint2); + + // Degenerate ring. + if (outputPoint.Point.Y == point.Y) + { + return PointInPolygonResult.Outside; + } + + // Point is strictly above or below the starting Y. + bool isAbove = outputPoint.Point.Y < point.Y, startingAbove = isAbove; + int val = 0; + + outputPoint2 = outputPoint.Next!; + while (outputPoint2 != outputPoint) + { + if (isAbove) + { + while (outputPoint2 != outputPoint && outputPoint2.Point.Y < point.Y) + { + outputPoint2 = outputPoint2.Next!; + } + } + else + { + while (outputPoint2 != outputPoint && outputPoint2.Point.Y > point.Y) + { + outputPoint2 = outputPoint2.Next!; + } + } + + if (outputPoint2 == outputPoint) + { + break; + } + + // The scanline must touch or cross point.Y an even number of times. + // Handle horizontal touches explicitly. + if (outputPoint2.Point.Y == point.Y) + { + if (outputPoint2.Point.X == point.X || (outputPoint2.Point.Y == outputPoint2.Prev.Point.Y && + (point.X < outputPoint2.Prev.Point.X) != (point.X < outputPoint2.Point.X))) + { + return PointInPolygonResult.On; + } + + outputPoint2 = outputPoint2.Next!; + if (outputPoint2 == outputPoint) + { + break; + } + + continue; + } + + if (outputPoint2.Point.X <= point.X || outputPoint2.Prev.Point.X <= point.X) + { + if (outputPoint2.Prev.Point.X < point.X && outputPoint2.Point.X < point.X) + { + // Toggle parity. + val = 1 - val; + } + else + { + int d = PolygonUtilities.CrossSign(outputPoint2.Prev.Point, outputPoint2.Point, point); + if (d == 0) + { + return PointInPolygonResult.On; + } + + if ((d < 0) == isAbove) + { + val = 1 - val; + } + } + } + + isAbove = !isAbove; + outputPoint2 = outputPoint2.Next!; + } + + if (isAbove == startingAbove) + { + return val == 0 ? PointInPolygonResult.Outside : PointInPolygonResult.Inside; + } + + { + int d = PolygonUtilities.CrossSign(outputPoint2.Prev.Point, outputPoint2.Point, point); + if (d == 0) + { + return PointInPolygonResult.On; + } + + if ((d < 0) == isAbove) + { + val = 1 - val; + } + } + + return val == 0 ? PointInPolygonResult.Outside : PointInPolygonResult.Inside; + } + + /// + /// Determines whether one output ring lies inside another. + /// + /// A point on the candidate inner ring. + /// A point on the candidate outer ring. + /// if the first ring is inside the second. + public static bool IsPathInsidePath(OutputPoint outputPoint1, OutputPoint outputPoint2) + { + // Allow for rounding error; don't decide based solely on the first vertex. + PointInPolygonResult pip = PointInPolygonResult.On; + OutputPoint outputPoint = outputPoint1; + do + { + switch (PointInOutputPolygon(outputPoint.Point, outputPoint2)) + { + case PointInPolygonResult.Outside: + if (pip == PointInPolygonResult.Outside) + { + return false; + } + + pip = PointInPolygonResult.Outside; + break; + case PointInPolygonResult.Inside: + if (pip == PointInPolygonResult.Inside) + { + return true; + } + + pip = PointInPolygonResult.Inside; + break; + default: + break; + } + + outputPoint = outputPoint.Next!; + } + while (outputPoint != outputPoint1); + + // Result is unclear, so try again using cleaned paths (Issue #973). + return PolygonUtilities.Path2ContainsPath1(BuildCleanContour(outputPoint1), BuildCleanContour(outputPoint2)); + } + + /// + /// Moves split ownership from one output record to another. + /// + /// The output record to move from. + /// The output record to move to. + private static void MoveOutputSplits(OutputRecord sourceRecord, OutputRecord targetRecord) + { + if (sourceRecord.Splits == null) + { + return; + } + + targetRecord.Splits ??= []; + foreach (int i in sourceRecord.Splits) + { + if (i != targetRecord.Index) + { + targetRecord.Splits.Add(i); + } + } + + sourceRecord.Splits = null; + } + + /// + /// Processes horizontal joins captured during the sweep. + /// + private void ProcessHorizontalJoins() + { + foreach (HorizontalJoin join in this.horizontalJoins) + { + OutputRecord outputRecord1 = ResolveOutputRecord(join.LeftToRight!.OutputRecord)!; + OutputRecord outputRecord2 = ResolveOutputRecord(join.RightToLeft!.OutputRecord)!; + + OutputPoint op1b = join.LeftToRight.Next!; + OutputPoint op2b = join.RightToLeft.Prev; + join.LeftToRight.Next = join.RightToLeft; + join.RightToLeft.Prev = join.LeftToRight; + op1b.Prev = op2b; + op2b.Next = op1b; + + // This join may split a single output record. + if (outputRecord1 == outputRecord2) + { + outputRecord2 = this.CreateOutputRecord(); + outputRecord2.Points = op1b; + FixOutputRecordPoints(outputRecord2); + + // If outputRecord1.Points moved to outputRecord2, update outputRecord1.Points. + if (outputRecord1.Points!.OutputRecord == outputRecord2) + { + outputRecord1.Points = join.LeftToRight; + outputRecord1.Points.OutputRecord = outputRecord1; + } + + // Issue references: #498, #520, #584, #576, #618 + if (this.buildHierarchy) + { + if (IsPathInsidePath(outputRecord1.Points, outputRecord2.Points)) + { + // swap outputRecord1's and outputRecord2's points + (outputRecord2.Points, outputRecord1.Points) = (outputRecord1.Points, outputRecord2.Points); + FixOutputRecordPoints(outputRecord1); + FixOutputRecordPoints(outputRecord2); + + // outputRecord2 is now inside outputRecord1 + outputRecord2.Owner = outputRecord1; + } + else if (IsPathInsidePath(outputRecord2.Points, outputRecord1.Points)) + { + outputRecord2.Owner = outputRecord1; + } + else + { + outputRecord2.Owner = outputRecord1.Owner; + } + + outputRecord1.Splits ??= []; + outputRecord1.Splits.Add(outputRecord2.Index); + } + else + { + outputRecord2.Owner = outputRecord1; + } + } + else + { + outputRecord2.Points = null; + if (this.buildHierarchy) + { + SetOutputOwner(outputRecord2, outputRecord1); + + // Issue #618. + MoveOutputSplits(outputRecord2, outputRecord1); + } + else + { + outputRecord2.Owner = outputRecord1; + } + } + } + } + + /// + /// Sorts intersection nodes from top to bottom, then left to right. + /// + internal struct IntersectNodeComparer : IComparer + { + /// + /// Compares two intersection nodes for sorting. + /// + /// The first node. + /// The second node. + /// A comparison result for sorting. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly int Compare(IntersectNode a, IntersectNode b) + { + double deltaY = a.Point.Y - b.Point.Y; + if (deltaY != 0) + { + return deltaY > 0 ? -1 : 1; + } + + double deltaX = a.Point.X - b.Point.X; + if (deltaX == 0) + { + return 0; + } + + return deltaX < 0 ? -1 : 1; + } + } + } +} diff --git a/PolygonClipper/StablePriorityQueue{T,TComparer}.cs b/PolygonClipper/StablePriorityQueue{T,TComparer}.cs new file mode 100644 index 0000000..e4a1c05 --- /dev/null +++ b/PolygonClipper/StablePriorityQueue{T,TComparer}.cs @@ -0,0 +1,220 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.PolygonClipper { + /// + /// Represents a stable priority queue that maintains the order of items with the same priority. + /// + /// The type of elements in the priority queue. + /// The type of comparer used to determine the priority of the elements. + [DebuggerDisplay("Count = {Count}")] + internal sealed class StablePriorityQueue + where TComparer : IComparer + { + private const int Log2Arity = 2; + private const int DefaultCapacity = 16; + private readonly List heap; + + /// + /// Initializes a new instance of the class with a specified comparer. + /// + /// The comparer to determine the priority of the elements. + public StablePriorityQueue(TComparer comparer) + : this(comparer, DefaultCapacity) + { + } + + /// + /// Initializes a new instance of the class with a specified comparer. + /// + /// The comparer to determine the priority of the elements. + /// The initial capacity of the priority queue. + public StablePriorityQueue(TComparer comparer, int capacity) + { + this.Comparer = comparer ?? throw new ArgumentNullException(nameof(comparer)); + this.heap = new List(capacity > 0 ? capacity : DefaultCapacity); + } + + /// + /// Initializes a new instance of the class + /// with a specified comparer and an initial collection of unordered elements. + /// The heap property is established in linear time. + /// + /// The comparer to determine the priority of the elements. + /// + /// The initial collection of elements to heapify. + /// Note: The collection is modified to establish the heap property. + /// + public StablePriorityQueue(TComparer comparer, List items) + { + this.Comparer = comparer ?? throw new ArgumentNullException(nameof(comparer)); + this.heap = items ?? throw new ArgumentNullException(nameof(items)); + this.Heapify(this.heap); + } + + /// + /// Gets the number of elements in the priority queue. + /// + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.heap.Count; + } + + /// + /// Gets the comparer used to determine the priority of the elements. + /// + public TComparer Comparer { get; } + + /// + /// Adds an item to the priority queue, maintaining the heap property. + /// + /// The item to add. + public void Enqueue(T item) + { + List data = this.heap; + data.Add(item); + this.Up((uint)data.Count - 1, data); + } + + /// + /// Removes and returns the item with the highest priority (lowest value) from the priority queue. + /// + /// The item with the highest priority. + /// Thrown if the priority queue is empty. + public T Dequeue() + { + List data = this.heap; + int count = data.Count; + ThrowIfEmpty(count); + ref T dRef = ref MemoryMarshal.GetReference(CollectionsMarshal.AsSpan(data)); + + int maxIndex = count - 1; + T top = Unsafe.Add(ref dRef, 0u); + T bottom = Unsafe.Add(ref dRef, (uint)maxIndex); + data.RemoveAt(maxIndex); + + if (--count > 0) + { + Unsafe.Add(ref dRef, 0u) = bottom; + this.Down(0u, data); + } + + return top; + } + + /// + /// Returns the item with the highest priority (lowest value) without removing it. + /// + /// The item with the highest priority. + /// Thrown if the priority queue is empty. + public T Peek() + { + ThrowIfEmpty(this.Count); + return this.heap[0]; + } + + /// + /// Restores the min-heap property by moving the item at the specified index upward + /// through the heap until it is in the correct position. This is called after insertion. + /// + /// The index of the newly added item to sift upward. + /// The heap to operate on. + private void Up(uint index, List heap) + { + ref T dRef = ref MemoryMarshal.GetReference(CollectionsMarshal.AsSpan(heap)); + T item = Unsafe.Add(ref dRef, index); + TComparer comparer = this.Comparer; + + while (index > 0) + { + uint parent = (index - 1u) >> Log2Arity; + T current = Unsafe.Add(ref dRef, parent); + if (comparer.Compare(item, current) >= 0) + { + break; + } + + Unsafe.Add(ref dRef, index) = current; + index = parent; + } + + Unsafe.Add(ref dRef, index) = item; + } + + /// + /// Restores the min-heap property by moving the item at the specified index downward + /// through the heap until it is in the correct position. This is called after removal of the root. + /// + /// The index of the item to sift downward (typically the root). + /// The heap to operate on. + private void Down(uint index, List heap) + { + Span data = CollectionsMarshal.AsSpan(heap); + ref T dRef = ref MemoryMarshal.GetReference(data); + + uint length = (uint)data.Length; + T item = Unsafe.Add(ref dRef, index); + TComparer comparer = this.Comparer; + + while ((index << Log2Arity) + 1u < length) + { + uint firstChild = (index << Log2Arity) + 1u; + uint bestChild = firstChild; + uint maxChild = Math.Min(firstChild + (1u << Log2Arity), length); + + for (uint i = firstChild + 1u; i < maxChild; i++) + { + if (comparer.Compare(Unsafe.Add(ref dRef, i), Unsafe.Add(ref dRef, bestChild)) < 0) + { + bestChild = i; + } + } + + if (comparer.Compare(Unsafe.Add(ref dRef, bestChild), item) >= 0) + { + break; + } + + Unsafe.Add(ref dRef, index) = Unsafe.Add(ref dRef, bestChild); + index = bestChild; + } + + Unsafe.Add(ref dRef, index) = item; + } + + /// + /// Heapifies the given list to establish the min-heap property. + /// + /// The list to heapify. + private void Heapify(List heap) + { + int count = heap.Count; + if (count <= 1) + { + return; + } + + int lastParent = (count - 2) >> Log2Arity; + for (int i = lastParent; i >= 0; i--) + { + this.Down((uint)i, heap); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowIfEmpty(int count) + { + if (count == 0) + { + throw new InvalidOperationException("Queue is empty."); + } + } + } +} diff --git a/PolygonClipper/StatusLine.cs b/PolygonClipper/StatusLine.cs new file mode 100644 index 0000000..82a34c1 --- /dev/null +++ b/PolygonClipper/StatusLine.cs @@ -0,0 +1,216 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// Represents a status line for the sweep line algorithm, maintaining a sorted collection of sweep events. + /// + /// Performance Characteristics: + /// - **Insertion**: O(n) in the worst case. The operation consists of: + /// 1. A binary search (O(log n)) to determine the correct insertion point. + /// 2. A shift operation to move subsequent elements in the list (O(k)), where k is the number of elements + /// after the insertion index. In the worst case, this can approach O(n). + /// - **Removal**: O(n) in the worst case. After finding the index of the element to remove, subsequent + /// elements in the list need to be shifted (O(k)), where k is the number of elements after the removed index. + /// - **Next/Previous Access**: O(1) after the index is known, as the list provides constant-time indexing. + /// + /// The implementation ensures efficient neighbor traversal (next/previous) at O(1), making it suitable for + /// algorithms where neighboring elements are accessed frequently. The use of `BinarySearch` minimizes the cost + /// of insertion/removal compared to naive search-based approaches. + /// + [DebuggerDisplay("Count = {Count}")] + internal sealed class StatusLine + { + private const int DefaultCapacity = 16; + private readonly List sortedEvents; + private readonly SegmentComparer comparer = new(); + + public StatusLine() + : this(DefaultCapacity) + { + } + + public StatusLine(int capacity) + => this.sortedEvents = new List(capacity > 0 ? capacity : DefaultCapacity); + + /// + /// Gets the number of events in the status line. + /// + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.sortedEvents.Count; + } + + /// + /// Gets the minimum sweep event in the status line (first in sort order). + /// + public SweepEvent Min + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.sortedEvents[0]; + } + + /// + /// Gets the maximum sweep event in the status line (last in sort order). + /// + public SweepEvent Max + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.sortedEvents[^1]; + } + + /// + /// Gets the retained list capacity. + /// + public int RetainedCapacity => this.sortedEvents.Capacity; + + /// + /// Gets the event at the specified index. + /// + /// The index of the event. + /// The sweep event at the given index. + public SweepEvent this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.sortedEvents[index]; + } + + /// + /// Clears active events and ensures the desired capacity. + /// + /// Desired minimum capacity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset(int capacity) + { + this.sortedEvents.Clear(); + if (capacity > this.sortedEvents.Capacity) + { + this.sortedEvents.EnsureCapacity(capacity); + } + } + + /// + /// Adds a sweep event into the status line, maintaining sorted order. + /// + /// The sweep event to insert. + /// The index where the event was inserted. + public int Add(SweepEvent e) + { + int index = this.sortedEvents.BinarySearch(e, this.comparer); + if (index < 0) + { + index = ~index; // Get the correct insertion point + } + + this.sortedEvents.Insert(index, e); + e.PosSL = index; + return index; + } + + /// + /// Removes a sweep event from the status line. + /// + /// The index of the event to remove. + /// + /// Thrown if is less than 0 or greater than or equal to the number of events. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RemoveAt(int index) + => this.sortedEvents.RemoveAt(index); + + /// + /// Finds the current index of a sweep event in the status line. + /// + /// The event to locate. + /// The index of the event, or -1 if it is not present. + public int IndexOf(SweepEvent e) + { + List events = this.sortedEvents; + int count = events.Count; + int hint = e.PosSL; + + if ((uint)hint < (uint)count && ReferenceEquals(events[hint], e)) + { + return hint; + } + + int index = events.BinarySearch(e, this.comparer); + if (index >= 0) + { + if (ReferenceEquals(events[index], e)) + { + e.PosSL = index; + return index; + } + + // BinarySearch can return any comparer-equal slot. Scan local ties by reference. + for (int i = index - 1; i >= 0 && this.comparer.Compare(events[i], e) == 0; i--) + { + if (ReferenceEquals(events[i], e)) + { + e.PosSL = i; + return i; + } + } + + for (int i = index + 1; i < count && this.comparer.Compare(events[i], e) == 0; i++) + { + if (ReferenceEquals(events[i], e)) + { + e.PosSL = i; + return i; + } + } + } + + // Fail-safe reference lookup for correctness if comparer order is temporarily unstable. + for (int i = 0; i < count; i++) + { + if (ReferenceEquals(events[i], e)) + { + e.PosSL = i; + return i; + } + } + + return -1; + } + + /// + /// Gets the next sweep event relative to the given index. + /// + /// The reference index. + /// The next sweep event, or null if none exists. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SweepEvent? Next(int index) + { + if (index >= 0 && index < this.sortedEvents.Count - 1) + { + return this.sortedEvents[index + 1]; + } + + return null; + } + + /// + /// Gets the previous sweep event relative to the given index. + /// + /// The reference index. + /// The previous sweep event, or null if none exists. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SweepEvent? Prev(int index) + { + if (index > 0 && index < this.sortedEvents.Count) + { + return this.sortedEvents[index - 1]; + } + + return null; + } + } +} diff --git a/PolygonClipper/StrokeOptions.cs b/PolygonClipper/StrokeOptions.cs new file mode 100644 index 0000000..9ab4be0 --- /dev/null +++ b/PolygonClipper/StrokeOptions.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.PolygonClipper { + /// + /// Provides configuration options for geometric stroke generation. + /// + public sealed class StrokeOptions : IEquatable + { + /// + /// Gets or sets a value indicating whether stroked contours should be normalized by + /// resolving self-intersections and overlaps before returning. + /// + /// + /// Defaults to for maximum throughput. + /// When disabled, callers should rasterize with a non-zero winding fill rule. + /// + public bool NormalizeOutput { get; set; } + + /// + /// Gets or sets the miter limit used to clamp outer miter joins. + /// + public double MiterLimit { get; set; } = 4D; + + /// + /// Gets or sets the tessellation detail scale for round joins and round caps. + /// Higher values produce more vertices (smoother curves, more work). + /// Lower values produce fewer vertices. + /// + public double ArcDetailScale { get; set; } = 1D; + + /// + /// Gets or sets the outer line join style used for stroking corners. + /// + public LineJoin LineJoin { get; set; } = LineJoin.Bevel; + + /// + /// Gets or sets the line cap style used for open path ends. + /// + public LineCap LineCap { get; set; } = LineCap.Butt; + + /// + public override bool Equals(object? obj) => this.Equals(obj as StrokeOptions); + + /// + public bool Equals(StrokeOptions? other) + => other is not null && + this.NormalizeOutput == other.NormalizeOutput && + this.MiterLimit == other.MiterLimit && + this.ArcDetailScale == other.ArcDetailScale && + this.LineJoin == other.LineJoin && + this.LineCap == other.LineCap; + + /// + public override int GetHashCode() + => HashCode.Combine( + this.NormalizeOutput, + this.MiterLimit, + this.ArcDetailScale, + this.LineJoin, + this.LineCap); + } +} diff --git a/PolygonClipper/StrokeVertexDistance.cs b/PolygonClipper/StrokeVertexDistance.cs new file mode 100644 index 0000000..34f04b5 --- /dev/null +++ b/PolygonClipper/StrokeVertexDistance.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// Represents a stroke-processing vertex with a cached outgoing segment length. + /// + /// + /// This is an internal mutable value used by while + /// normalizing source contours and computing joins/caps. + /// + internal struct StrokeVertexDistance + { + private const double VertexDistanceEpsilon = 1E-14D; + private const double Dd = 1D / VertexDistanceEpsilon; + + /// + /// The X-coordinate. + /// + public double X; + + /// + /// The Y-coordinate. + /// + public double Y; + + /// + /// Cached distance to another vertex measured by . + /// + public double Distance; + + /// + /// Initializes a new instance of the struct. + /// + /// The X-coordinate. + /// The Y-coordinate. + /// Initial cached distance value. + public StrokeVertexDistance(double x, double y, double distance) + { + this.X = x; + this.Y = y; + this.Distance = distance; + } + + /// + /// Measures the Euclidean distance from this vertex to and stores it in . + /// + /// The vertex to measure to. + /// + /// when the measured distance is greater than the internal epsilon; + /// otherwise . + /// + /// + /// When points are closer than epsilon, is set to a large sentinel value + /// to avoid divide-by-near-zero behavior in downstream stroker math. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Measure(in StrokeVertexDistance vd) + { + bool ret = (this.Distance = Vertex.Distance(new Vertex(this.X, this.Y), new Vertex(vd.X, vd.Y))) > VertexDistanceEpsilon; + if (!ret) + { + this.Distance = Dd; + } + + return ret; + } + } +} diff --git a/PolygonClipper/SweepEvent.cs b/PolygonClipper/SweepEvent.cs new file mode 100644 index 0000000..a26c0cd --- /dev/null +++ b/PolygonClipper/SweepEvent.cs @@ -0,0 +1,205 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#nullable disable + +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// Represents a sweep. + /// + internal sealed class SweepEvent + { + /// + /// Initializes a new instance of the class. + /// + /// The point associated with the event. + /// Whether the point is the left endpoint of the segment. + /// The event associated with the other endpoint of the segment. + /// The polygon type to which the segment belongs. + /// The type of the edge. Default is . + public SweepEvent( + Vertex point, + bool left, + SweepEvent otherEvent, + PolygonType polygonType = PolygonType.Subject, + EdgeType edgeType = EdgeType.Normal) + { + this.Point = point; + this.Left = left; + this.OtherEvent = otherEvent; + this.PolygonType = polygonType; + this.EdgeType = edgeType; + } + + /// + /// Initializes a new instance of the class. + /// + /// The point associated with the event. + /// Whether the point is the left endpoint of the segment. + /// The polygon type to which the segment belongs. + public SweepEvent(Vertex point, bool left, PolygonType polygonType = PolygonType.Subject) + { + this.Point = point; + this.Left = left; + this.PolygonType = polygonType; + this.EdgeType = EdgeType.Normal; + } + + /// + /// Initializes a new instance of the class. + /// + /// The point associated with the event. + /// Whether the point is the left endpoint of the segment. + /// The ID of the contour to which the event belongs. + public SweepEvent(Vertex point, bool left, int contourId) + { + this.Point = point; + this.Left = left; + this.ContourId = contourId; + this.PolygonType = PolygonType.Subject; + this.EdgeType = EdgeType.Normal; + } + + /// + /// Gets the point associated with the event. + /// + public Vertex Point { get; } + + /// + /// Gets or sets a value indicating whether the point is the + /// left (source) endpoint of the segment (p, other->p). + /// + public bool Left { get; set; } + + /// + /// Gets or sets the ID of the contour to which the event belongs. + /// + public int ContourId { get; set; } + + /// + /// Gets index of the polygon to which the associated segment belongs to; + /// + public PolygonType PolygonType { get; } + + /// + /// Gets or sets the type of the edge. + /// + public EdgeType EdgeType { get; set; } + + /// + /// Gets or sets the event associated to the other endpoint of the segment. + /// + public SweepEvent OtherEvent { get; set; } + + /// + /// Gets or sets a value indicating whether the segment (p, other->p) represent an + /// inside-outside transition in the polygon for a vertical ray from (p.x, -infinite) + /// that crosses the segment. + /// + public bool InOut { get; set; } + + /// + /// Gets or sets a value indicating whether the inOut transition for the segment from + /// the other polygon preceding this segment in the sweep line. + /// + public bool OtherInOut { get; set; } + + /// + /// Gets or sets the sorted sweep events. Only used in "left" events. + /// Position of the event (segment) in SL (status line). + /// + public int PosSL { get; set; } + + /// + /// Gets or sets the previous segment in the sweep line belonging to the result of the + /// boolean operation. + /// + public SweepEvent PrevInResult { get; set; } + + /// + /// Gets or sets the transition state of the event in the result. + /// + public ResultTransition ResultTransition { get; set; } + + /// + /// Gets a value indicating whether the event contributes to the result. + /// + public bool InResult => this.ResultTransition != ResultTransition.Neutral; + + /// + /// Gets or sets the position of the event in the sorted events. + /// + public int Pos { get; set; } + + /// + /// Gets or sets a value indicating whether the event is a result in-out transition. + /// + public bool ResultInOut { get; set; } + + /// + /// Gets or sets the output contour ID associated with this contour. + /// + public int OutputContourId { get; set; } + + /// + /// Is the line segment (point, otherEvent->point) below point p. + /// + /// The point to check against. + /// + /// if the line segment is below the point; otherwise . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsBelow(in Vertex p) + => this.Left + ? PolygonUtilities.SignedArea(this.Point, this.OtherEvent.Point, p) > 0D + : PolygonUtilities.SignedArea(this.OtherEvent.Point, this.Point, p) > 0D; + + /// + /// Is the line segment (point, otherEvent->point) above point p. + /// + /// The point to check against. + /// + /// if the line segment is above the point; otherwise . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsAbove(in Vertex p) => !this.IsBelow(p); + + /// + /// Is the line segment (point, otherEvent->point) a vertical line segment. + /// + /// + /// if the line segment is vertical; otherwise . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsVertical() => this.Point.X == this.OtherEvent.Point.X; + + /// + /// Determines if this sweep event comes before another sweep event. + /// + /// The other sweep event to compare with. + /// + /// if this event comes before the other; otherwise . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsBefore(SweepEvent other) + { + // Compare by x-coordinate first + if (this.Point.X != other.Point.X) + { + return this.Point.X < other.Point.X; + } + + // If x-coordinates are equal, compare by y-coordinate + return this.Point.Y < other.Point.Y; + } + + /// + /// Returns the segment associated with the sweep event. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Segment GetSegment() => new(this.Point, this.OtherEvent.Point); + } +} diff --git a/PolygonClipper/SweepEventComparer.cs b/PolygonClipper/SweepEventComparer.cs new file mode 100644 index 0000000..c7bdb82 --- /dev/null +++ b/PolygonClipper/SweepEventComparer.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace SixLabors.PolygonClipper { + /// + /// Compares two instances for sorting in the event queue. + /// + internal sealed class SweepEventComparer : IComparer, IComparer + { + /// + public int Compare(SweepEvent? x, SweepEvent? y) + { + if (x == null) + { + return -1; + } + + if (y == null) + { + return 1; + } + + // Compare by x-coordinate + if (x.Point.X > y.Point.X) + { + return 1; + } + + if (x.Point.X < y.Point.X) + { + return -1; + } + + // Compare by y-coordinate when x-coordinates are the same + if (x.Point.Y != y.Point.Y) + { + return x.Point.Y > y.Point.Y ? 1 : -1; + } + + // Compare left vs. right endpoint + if (x.Left != y.Left) + { + return x.Left ? 1 : -1; + } + + // Compare collinearity using signed area + double area = PolygonUtilities.SignedArea(x.Point, x.OtherEvent.Point, y.OtherEvent.Point); + if (area != 0) + { + return x.IsBelow(y.OtherEvent.Point) ? -1 : 1; + } + + // Compare by polygon type: subject polygons have higher priority + return x.PolygonType != PolygonType.Subject && y.PolygonType == PolygonType.Subject ? 1 : -1; + } + + /// + public int Compare(object? x, object? y) + { + if (x is SweepEvent a && y is SweepEvent b) + { + return this.Compare(a, b); + } + + throw new ArgumentException("Both arguments must be of type SweepEvent.", nameof(x)); + } + } +} diff --git a/PolygonClipper/SweepVertex.cs b/PolygonClipper/SweepVertex.cs new file mode 100644 index 0000000..080add9 --- /dev/null +++ b/PolygonClipper/SweepVertex.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.PolygonClipper { + /// + /// Represents a vertex in the input contour linked list used by the sweep. + /// + /// + /// Vertices are linked in a circular doubly linked list (see and + /// ) so the sweep can traverse ascending/descending bounds and + /// detect local minima/maxima efficiently. + /// + internal sealed class SweepVertex + { +#pragma warning disable SA1401 // Hot sweep vertex state uses fields to avoid accessor overhead. + /// + /// The vertex position. + /// + public Vertex Point; + + /// + /// The next vertex in the contour. + /// + public SweepVertex? Next; + + /// + /// The previous vertex in the contour. + /// + public SweepVertex? Prev; + + /// + /// Flags describing sweep-related classification. + /// + public VertexFlags Flags; +#pragma warning restore SA1401 + + /// + /// Initializes a new instance of the class. + /// + public SweepVertex(Vertex point, VertexFlags flags, SweepVertex? prev) + { + this.Point = point; + this.Flags = flags; + this.Next = null; + this.Prev = prev; + } + + /// + /// Gets a value indicating whether this vertex is marked as a local maxima. + /// + public bool IsMaxima => (this.Flags & VertexFlags.LocalMax) != 0; + } +} diff --git a/PolygonClipper/Vertex.cs b/PolygonClipper/Vertex.cs new file mode 100644 index 0000000..bd6b16e --- /dev/null +++ b/PolygonClipper/Vertex.cs @@ -0,0 +1,248 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.PolygonClipper { + /// + /// Represents a two-dimensional vertex with X and Y coordinates. + /// + public readonly struct Vertex : IEquatable + { + /// + /// Gets the X-coordinate of the vertex. + /// +#pragma warning disable CA1051 // Do not declare visible instance fields + public readonly double X; + + /// + /// Gets the Y-coordinate of the vertex. + /// + public readonly double Y; +#pragma warning restore CA1051 // Do not declare visible instance fields + + /// + /// Initializes a new instance of the struct. + /// + /// The X and Y coordinates of the vertex. + public Vertex(double xy) + { + this.X = xy; + this.Y = xy; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The X-coordinate of the vertex. + /// The Y-coordinate of the vertex. + public Vertex(double x, double y) + { + this.X = x; + this.Y = y; + } + + /// + /// Adds two vectors together. + /// + /// The first vector to add. + /// The second vector to add. + /// The summed vector. + /// The method defines the addition operation for objects. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vertex operator +(in Vertex left, in Vertex right) + => AsVertexUnsafe(AsVector128Unsafe(left) + AsVector128Unsafe(right)); + + /// + /// Subtracts the second vector from the first. + /// + /// The first vector. + /// The second vector. + /// The vector that results from subtracting from . + /// The method defines the subtraction operation for objects. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vertex operator -(in Vertex left, in Vertex right) + => AsVertexUnsafe(AsVector128Unsafe(left) - AsVector128Unsafe(right)); + + /// + /// Returns a new vector whose values are the product of each pair of elements in two specified vertices. + /// + /// The first vertex. + /// The second vertex. + /// The element-wise product vertex. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vertex operator *(in Vertex left, in Vertex right) => AsVertexUnsafe(AsVector128Unsafe(left) * AsVector128Unsafe(right)); + + /// + /// Multiplies the specified vertex by the specified scalar value. + /// + /// The first vertex. + /// The scalar value. + /// The scaled vertex. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vertex operator *(in Vertex left, double right) => AsVertexUnsafe(AsVector128Unsafe(left) * right); + + /// + /// Multiplies the specified vertex by the specified scalar value. + /// + /// The first vertex. + /// The scalar value. + /// The scaled vertex. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vertex operator *(double left, in Vertex right) => right * left; + + /// + /// Divides the first vertex by the second. + /// + /// The first vertex. + /// The second vertex. + /// The vertex that results from dividing by . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vertex operator /(in Vertex left, in Vertex right) + => AsVertexUnsafe(AsVector128Unsafe(left) / AsVector128Unsafe(right)); + + /// + /// Divides the specified vertex by a specified scalar value. + /// + /// The first vertex. + /// The scalar value. + /// The result of the division. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vertex operator /(in Vertex left, double right) + => AsVertexUnsafe(AsVector128Unsafe(left) / right); + + /// + /// Divides the specified vertex by the specified scalar value. + /// + /// The first vertex. + /// The scalar value. + /// The result of the division. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vertex operator /(double left, in Vertex right) => right / left; + + /// + /// Determines whether two vertices are equal. + /// + /// The first vertex. + /// The second vertex. + /// if the vertices are equal; otherwise, . + public static bool operator ==(in Vertex left, in Vertex right) => left.Equals(right); + + /// + /// Determines whether two vertices are not equal. + /// + /// The first vertex. + /// The second vertex. + /// if the vertices are not equal; otherwise, . + public static bool operator !=(in Vertex left, in Vertex right) => !left.Equals(right); + + /// + /// Returns the dot product of two vertices. + /// + /// The first vertex. + /// The second vertex. + /// The dot product. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Dot(in Vertex a, in Vertex b) + { + Vector128 a128 = AsVector128Unsafe(a); + Vector128 b128 = AsVector128Unsafe(b); + return Vector128.Dot(a128, b128); + } + + /// + /// Returns the cross product of two vertices. + /// + /// The first vertex. + /// The second vertex. + /// The cross product. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Cross(in Vertex a, in Vertex b) + => (a.X * b.Y) - (a.Y * b.X); + + /// Computes the Euclidean distance between the two given vertices. + /// The first vertex. + /// The second vertex. + /// The distance. + public static double Distance(in Vertex a, in Vertex b) + => double.Sqrt(DistanceSquared(a, b)); + + /// Returns the Euclidean distance squared between two specified vertices. + /// The first vertex. + /// The second vertex. + /// The distance squared. + public static double DistanceSquared(in Vertex a, in Vertex b) + => (a - b).LengthSquared(); + + /// + /// Returns the length of the vertex. + /// + /// The vertex's length. + /// + public double Length() + => double.Sqrt(this.LengthSquared()); + + /// Returns the length of the vertex squared. + /// The vertex's length squared. + /// This operation offers better performance than a call to the method. + /// + public double LengthSquared() + => Dot(this, this); + + /// + /// Returns a vertex whose elements are the minimum of each of the pairs of elements in two specified vertices. + /// + /// The first vertex. + /// The second vertex. + /// The minimized . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vertex Min(in Vertex a, in Vertex b) + => AsVertexUnsafe(Vector128.Min(AsVector128Unsafe(a), AsVector128Unsafe(b))); + + /// + /// Returns a vertex whose elements are the maximum of each of the pairs of elements in two specified vertices. + /// + /// The first vertex. + /// The second vertex. + /// The maximized . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vertex Max(in Vertex a, in Vertex b) + => AsVertexUnsafe(Vector128.Max(AsVector128Unsafe(a), AsVector128Unsafe(b))); + + /// + /// Computes the absolute value of each element in a specified vertex. + /// + /// The vertex that will have its absolute value computed. + /// + /// A vertex with the absolute value of each of the elements in . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vertex Abs(in Vertex value) + => AsVertexUnsafe(Vector128.Abs(AsVector128Unsafe(value))); + + /// + public bool Equals(Vertex other) + => this.X == other.X && this.Y == other.Y; + + /// + public override bool Equals(object? obj) => + obj is Vertex vertex && this.Equals(vertex); + + /// + public override int GetHashCode() => HashCode.Combine(this.X, this.Y); + + /// + public override string ToString() => $"Vertex [ X={this.X}, Y={this.Y} ]"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 AsVector128Unsafe(in Vertex value) + => Unsafe.BitCast>(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vertex AsVertexUnsafe(Vector128 value) + => Unsafe.BitCast, Vertex>(value); + } +} diff --git a/PolygonClipper/VertexFlags.cs b/PolygonClipper/VertexFlags.cs new file mode 100644 index 0000000..d626f96 --- /dev/null +++ b/PolygonClipper/VertexFlags.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.PolygonClipper { + /// + /// Classifies sweep vertices by local-extrema role during bound construction. + /// + /// + /// The self-intersection sweep decomposes each contour into monotonic bounds that + /// start at local minima and terminate at local maxima. These flags annotate + /// each with that role. + /// + [Flags] + internal enum VertexFlags + { + /// + /// No extrema role is assigned. + /// + None = 0, + + /// + /// Marks a local maximum vertex (bound endpoint). + /// + LocalMax = 1 << 0, + + /// + /// Marks a local minimum vertex (bound start). + /// + LocalMin = 1 << 1 + } +} diff --git a/PolygonClipper/VertexPoolList.cs b/PolygonClipper/VertexPoolList.cs new file mode 100644 index 0000000..84276c2 --- /dev/null +++ b/PolygonClipper/VertexPoolList.cs @@ -0,0 +1,342 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.PolygonClipper { + /// + /// Pool-backed list of clip vertices reused across clipping operations. + /// + internal sealed class VertexPoolList : PooledList + { + /// + /// Adds or reuses a clip vertex initialized with the given data. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SweepVertex Add(Vertex point, VertexFlags flags, SweepVertex? prev) + { + this.TryGrow(); + SweepVertex poolVertex = this.Items[this.Size]; + if (poolVertex == null) + { + poolVertex = new SweepVertex(point, flags, prev); + this.Items[this.Size] = poolVertex; + } + else + { + // Reset pooled state so linked lists are rebuilt safely. + poolVertex.Point = point; + poolVertex.Flags = flags; + poolVertex.Prev = prev; + poolVertex.Next = null; + } + + this.Size++; + return poolVertex; + } + } + + /// + /// Pool-backed list of output points allocated during clipping. + /// + internal sealed class OutputPointPoolList : PooledList + { + /// + /// Adds or reuses an output point and increments the owning record count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public OutputPoint Add(Vertex pt, OutputRecord outputRecord) + { + this.TryGrow(); + OutputPoint pooledPoint = this.Items[this.Size]; + if (pooledPoint == null) + { + pooledPoint = new OutputPoint(pt, outputRecord); + this.Items[this.Size] = pooledPoint; + } + else + { + pooledPoint.Point = pt; + pooledPoint.OutputRecord = outputRecord; + pooledPoint.Next = pooledPoint; + pooledPoint.Prev = pooledPoint; + pooledPoint.HorizontalSegment = null; + } + + this.Size++; + outputRecord.OutputPointCount++; + return pooledPoint; + } + } + + /// + /// Pool-backed list of output records that preserves per-record state between runs. + /// + internal sealed class OutputRecordPoolList : PooledList + { + private static readonly List Tombstone = []; + + /// + /// Adds or reuses an output record with cleared state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public OutputRecord Add() + { + this.TryGrow(); + OutputRecord outputRecord = this.Items[this.Size]; + if (outputRecord == null) + { + outputRecord = new OutputRecord(); + this.Items[this.Size] = outputRecord; + } + else + { + outputRecord.Index = 0; + outputRecord.OutputPointCount = 0; + outputRecord.Owner = null; + outputRecord.FrontEdge = null; + outputRecord.BackEdge = null; + outputRecord.Points = null; + outputRecord.Bounds = default; + outputRecord.Path.Clear(); + outputRecord.Splits?.Clear(); + outputRecord.RecursiveSplit = null; + } + + this.Size++; + return outputRecord; + } + + public override void Clear() + { + base.Clear(); + for (int i = 0; i < this.Items.Length; i++) + { + OutputRecord outputRecord = this.Items[i]; + if (outputRecord == null || outputRecord.Path == Tombstone) + { + break; + } + + // Mark paths so pooled records are not accidentally reused without reset. + outputRecord.Path = Tombstone; + outputRecord.Owner = null; + outputRecord.FrontEdge = null; + outputRecord.BackEdge = null; + outputRecord.Points = null; + outputRecord.RecursiveSplit = null; + } + } + } + + /// + /// Pool-backed list of horizontal joins used during sweep processing. + /// + internal sealed class HorizontalJoinPoolList : PooledList + { + /// + /// Adds or reuses a horizontal join entry. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HorizontalJoin Add(OutputPoint ltor, OutputPoint rtol) + { + this.TryGrow(); + HorizontalJoin hJoin = this.Items[this.Size]; + if (hJoin == null) + { + hJoin = new HorizontalJoin(ltor, rtol); + this.Items[this.Size] = hJoin; + } + else + { + hJoin.LeftToRight = ltor; + hJoin.RightToLeft = rtol; + } + + this.Size++; + return hJoin; + } + } + + /// + /// Pool-backed list of sweep events reused between clipping runs. + /// + internal sealed class SweepEventPoolList : PooledList + { + /// + /// Adds a sweep event to the active range. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(SweepEvent sweepEvent) + { + this.TryGrow(); + this.Items[this.Size] = sweepEvent; + this.Size++; + } + } + + /// + /// Base class for pool-backed lists with stable indexing and reuse. + /// + /// + /// These lists are append-only during a run and reset via to + /// reuse previously allocated storage and object instances. The internal array + /// can grow but never shrinks, so callers should treat + /// as a long-lived pool size. Elements are only valid in the range + /// [0, Count); indices remain stable for the lifetime of a run, which allows + /// pooled nodes to store indices instead of references when needed. + /// + internal abstract class PooledList : IReadOnlyList + where T : class + { + private const int DefaultCapacity = 4; + + /// + /// Initializes a new instance of the class. + /// + protected PooledList() => this.Items = []; + + /// + /// Gets the number of items that have been added during the current run. + /// + public int Count => this.Size; + + /// + /// Gets the backing array used for pooled storage. + /// + protected T[] Items { get; private set; } + + /// + /// Gets or sets the number of active items in the pool. + /// + protected int Size { get; set; } + + /// + /// Gets the current capacity of the pooled storage. + /// + public int Capacity + { + get => this.Items.Length; + private set + { + if (value <= this.Items.Length) + { + return; + } + + int target = (int)BitOperations.RoundUpToPowerOf2((uint)value); + T[] newItems = new T[target]; + if (this.Size > 0) + { + Array.Copy(this.Items, newItems, this.Size); + } + + this.Items = newItems; + } + } + + /// + /// Gets the item at the specified index within the active range. + /// + public T this[int index] + { + get + { + DebugGuard.MustBeLessThan((uint)index, (uint)this.Size, nameof(index)); + return this.Items[index]; + } + } + + /// + /// Ensures the pool can hold at least items. + /// + public void EnsureCapacity(int capacity) => this.Capacity = capacity; + + /// + /// Resets the active count to zero without clearing the backing array. + /// + public virtual void Clear() => this.Size = 0; + + /// + /// Gets a struct enumerator over the active items. + /// + public PooledListEnumerator GetEnumerator() => new(this); + + /// + IEnumerator IEnumerable.GetEnumerator() => new PooledListEnumerator(this); + + /// + IEnumerator IEnumerable.GetEnumerator() => new PooledListEnumerator(this); + + /// + /// Grows the pool by at least one slot, doubling capacity when needed. + /// + protected void TryGrow() + { + int newSize = this.Size + 1; + if (newSize <= this.Items.Length) + { + return; + } + + int newCapacity = this.Items.Length == 0 ? DefaultCapacity : this.Items.Length * 2; + this.Capacity = newCapacity; + } + + /// + /// Struct enumerator for iterating active items without allocations. + /// + internal struct PooledListEnumerator : IEnumerator + where TItem : class + { + private readonly PooledList list; + private int index; + private TItem? current; + + public PooledListEnumerator(PooledList list) + { + this.list = list; + this.index = 0; + this.current = null; + } + + public readonly TItem Current => this.current!; + + readonly object IEnumerator.Current => this.current!; + + public readonly void Dispose() + { + } + + public bool MoveNext() + { + int count = this.list.Size; + if ((uint)this.index < (uint)count) + { + this.current = this.list[this.index]; + this.index++; + return true; + } + + return this.MoveNextRare(count); + } + + private bool MoveNextRare(int count) + { + this.index = count + 1; + this.current = null; + return false; + } + + public void Reset() + { + this.index = 0; + this.current = null; + } + } + } +} diff --git a/SharedInfrastructure.projitems b/SharedInfrastructure.projitems new file mode 100644 index 0000000..c9f9f71 --- /dev/null +++ b/SharedInfrastructure.projitems @@ -0,0 +1,22 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 68a8cc40-6aed-4e96-b524-31b1158fdeea + + + SixLabors + + + + + + + + + + + + + \ No newline at end of file diff --git a/SixLabors.Fonts/ArrayBuilder{T}.cs b/SixLabors.Fonts/ArrayBuilder{T}.cs new file mode 100644 index 0000000..80c30e1 --- /dev/null +++ b/SixLabors.Fonts/ArrayBuilder{T}.cs @@ -0,0 +1,193 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts { + /// + /// A helper type for avoiding allocations while building arrays. + /// + /// The type of item contained in the array. + internal struct ArrayBuilder + where T : struct + { + private const int DefaultCapacity = 4; + private const int MaxCoreClrArrayLength = 0x7FeFFFFF; + + // Starts out null, initialized on first Add. + private T[]? data; + private int size; + + /// + /// Initializes a new instance of the struct. + /// + /// The initial capacity of the array. + public ArrayBuilder(int capacity) + : this() + { + Guard.MustBeGreaterThanOrEqualTo(capacity, 0, nameof(capacity)); + + this.data = new T[capacity]; + } + + /// + /// Gets or sets the number of items in the array. + /// + public int Length + { + readonly get => this.size; + + set + { + if (value != this.size) + { + if (value > 0) + { + this.EnsureCapacity(value); + this.size = value; + } + else + { + this.size = 0; + } + } + } + } + + /// + /// Returns a reference to specified element of the array. + /// + /// The index of the element to return. + /// The . + /// + /// Thrown when index less than 0 or index greater than or equal to . + /// + public readonly ref T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + DebugGuard.MustBeBetweenOrEqualTo(index, 0, this.size, nameof(index)); + return ref this.data![index]; + } + } + + /// + /// Adds the given item to the array. + /// + /// The item to add. + public void Add(T item) + { + int position = this.size; + + // Expand the array. + this.Length++; + this.data![position] = item; + } + + /// + /// Appends a given number of empty items to the array returning + /// the items as a slice. + /// + /// The number of items in the slice. + /// Whether to clear the new slice, Defaults to . + /// The . + public ArraySlice Add(int length, bool clear = true) + { + int position = this.size; + + // Expand the array. + this.Length += length; + + ArraySlice slice = this.AsSlice(position, this.Length - position); + if (clear) + { + slice.Span.Clear(); + } + + return slice; + } + + /// + /// Appends the slice to the array copying the data across. + /// + /// The array slice. + /// The . + public ArraySlice Add(in ReadOnlyArraySlice value) + { + int position = this.size; + + // Expand the array. + this.Length += value.Length; + + ArraySlice slice = this.AsSlice(position, this.Length - position); + value.CopyTo(slice); + + return slice; + } + + /// + /// Clears the array. + /// Allocated memory is left intact for future usage. + /// + public void Clear() => + + // No need to actually clear since we're not allowing reference types. + this.size = 0; + + private void EnsureCapacity(int min) + { + int length = this.data?.Length ?? 0; + if (length < min) + { + // Same expansion algorithm as List. + uint newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u; + if (newCapacity > MaxCoreClrArrayLength) + { + newCapacity = MaxCoreClrArrayLength; + } + + if (newCapacity < min) + { + newCapacity = (uint)min; + } + + var array = new T[newCapacity]; + + if (this.size > 0) + { + Array.Copy(this.data!, array, this.size); + } + + this.data = array; + } + } + + /// + /// Returns the current state of the array as a slice. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ArraySlice AsSlice() => this.AsSlice(this.Length); + + /// + /// Returns the current state of the array as a slice. + /// + /// The number of items in the slice. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly ArraySlice AsSlice(int length) + => new(this.data!, 0, length); + + /// + /// Returns the current state of the array as a slice. + /// + /// The index at which to begin the slice. + /// The number of items in the slice. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly ArraySlice AsSlice(int start, int length) + => new(this.data!, start, length); + } +} diff --git a/SixLabors.Fonts/ArraySlice{T}.cs b/SixLabors.Fonts/ArraySlice{T}.cs new file mode 100644 index 0000000..520c900 --- /dev/null +++ b/SixLabors.Fonts/ArraySlice{T}.cs @@ -0,0 +1,209 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.Fonts { + /// + /// ArraySlice represents a contiguous region of arbitrary memory similar + /// to and though constrained + /// to arrays. + /// Unlike , it is not a byref-like type. + /// + /// The type of item contained in the slice. + internal readonly struct ArraySlice : IEnumerable, IEnumerable + where T : struct + { + private readonly T[] data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data buffer. + public ArraySlice(T[] data) + : this(data, 0, data.Length) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data buffer. + /// The offset position in the underlying buffer this slice was created from. + /// The number of items in the slice. + public ArraySlice(T[] data, int start, int length) + { + DebugGuard.MustBeGreaterThanOrEqualTo(start, 0, nameof(start)); + DebugGuard.MustBeLessThanOrEqualTo(length, data.Length, nameof(length)); + DebugGuard.MustBeLessThanOrEqualTo(start + length, data.Length, nameof(this.data)); + + this.data = data; + this.Start = start; + this.Length = length; + } + + /// + /// Gets an empty + /// + public static ArraySlice Empty => new(Array.Empty()); + + /// + /// Gets the offset position in the underlying buffer this slice was created from. + /// + public int Start { get; } + + /// + /// Gets the number of items in the slice. + /// + public int Length { get; } + + /// + /// Gets a representing this slice. + /// + public Span Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(this.data, this.Start, this.Length); + } + + /// + /// Returns a reference to specified element of the slice. + /// + /// The index of the element to return. + /// The . + /// + /// Thrown when index less than 0 or index greater than or equal to . + /// + public ref T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + DebugGuard.MustBeBetweenOrEqualTo(index, 0, this.Length, nameof(index)); + ref T b = ref MemoryMarshal.GetReference(this.Span); + return ref Unsafe.Add(ref b, index); + } + } + + /// + /// Defines an implicit conversion of a to a + /// + public static implicit operator ReadOnlyArraySlice(ArraySlice slice) + => new(slice.data, slice.Start, slice.Length); + + /// + /// Defines an implicit conversion of an array to a + /// + public static implicit operator ArraySlice(T[] array) + => new(array, 0, array.Length); + + /// + /// Copies the contents of this slice into destination span. If the source + /// and destinations overlap, this method behaves as if the original values in + /// a temporary location before the destination is overwritten. + /// + /// The slice to copy items into. + /// + /// Thrown when the destination slice is shorter than the source Span. + /// + public void CopyTo(ArraySlice destination) + => this.Span.CopyTo(destination.Span); + + /// + /// Fills the contents of this slice with the given value. + /// + public void Fill(T value) => this.Span.Fill(value); + + /// + /// Forms a slice out of the given slice, beginning at 'start', of given length + /// + /// The index at which to begin this slice. + /// The desired length for the slice (exclusive). + /// + /// Thrown when the specified or end index is not in range (<0 or >Length). + /// + public ArraySlice Slice(int start, int length) + => new(this.data, start, length); + + /// + public IEnumerator GetEnumerator() => new Enumerator(this); + + /// + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); + + public struct Enumerator : IEnumerator + { + private readonly T[]? array; + private readonly int start; + private readonly int end; // cache Start + Length, since it's a little slow + private int current; + + internal Enumerator(ArraySlice slice) + { + DebugGuard.NotNull(slice.data, nameof(slice.data)); + DebugGuard.MustBeGreaterThanOrEqualTo(slice.Start, 0, nameof(slice.Start)); + DebugGuard.MustBeGreaterThanOrEqualTo(slice.Length, 0, nameof(slice.Length)); + + DebugGuard.MustBeLessThanOrEqualTo( + slice.Start + slice.Length, + slice.data.Length, + nameof(slice.data.Length)); + + this.array = slice.data; + this.start = slice.Start; + this.end = slice.Start + slice.Length; + this.current = slice.Start - 1; + } + + /// + public readonly T Current + { + get + { + if (this.current < this.start) + { + ThrowEnumNotStarted(); + } + + if (this.current >= this.end) + { + ThrowEnumEnded(); + } + + return this.array![this.current]; + } + } + + object? IEnumerator.Current => this.Current; + + /// + public bool MoveNext() + { + if (this.current < this.end) + { + this.current++; + return this.current < this.end; + } + + return false; + } + + /// + void IEnumerator.Reset() => this.current = this.start - 1; + + public readonly void Dispose() + { + } + + private static void ThrowEnumNotStarted() + => throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); + + private static void ThrowEnumEnded() + => throw new InvalidOperationException("Enumeration already finished."); + } + } +} diff --git a/SixLabors.Fonts/BigEndianBinaryReader.cs b/SixLabors.Fonts/BigEndianBinaryReader.cs new file mode 100644 index 0000000..4784f6e --- /dev/null +++ b/SixLabors.Fonts/BigEndianBinaryReader.cs @@ -0,0 +1,513 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; + +namespace SixLabors.Fonts { + /// + /// + /// A binary reader that reads in big-endian format. + /// + /// + /// This reader captures the stream position at construction time as startOfStream. + /// All offset values read from OpenType tables (via , + /// , etc.) are raw values relative to wherever the spec says + /// they originate (typically the start of the containing table). + /// + /// + /// When seeking with using , the + /// startOfStream is automatically added to the supplied offset. This means + /// table-relative offsets can be passed directly to without manually + /// adding the table's absolute position. Do not add the table start + /// yourself — that would double-count and land at the wrong position. + /// + /// + /// In contrast, . always returns the + /// absolute position within the underlying stream and is unaffected by + /// startOfStream. + /// + /// + [DebuggerDisplay("Start: {StartOfStream}, Position: {BaseStream.Position}")] + internal sealed class BigEndianBinaryReader : IDisposable + { + /// + /// Buffer used for temporary storage before conversion into primitives. + /// + private readonly byte[] buffer = new byte[16]; + private readonly bool leaveOpen; + + /// + /// Initializes a new instance of the class. + /// The current position of is captured as startOfStream + /// and used as the origin for all subsequent calls with + /// . + /// + /// Stream to read data from. + /// If , the stream is not disposed when this reader is disposed. + public BigEndianBinaryReader(Stream stream, bool leaveOpen) + { + this.BaseStream = stream; + this.StartOfStream = stream.Position; + this.leaveOpen = leaveOpen; + } + + /// + /// Gets the underlying stream of the EndianBinaryReader. + /// Note that on this stream is always the + /// absolute position and is not adjusted by + /// startOfStream. Avoid using BaseStream.Position to compute + /// offsets for — use raw offsets from , + /// , etc. instead. + /// + public Stream BaseStream { get; } + + /// + /// Gets the absolute stream position captured at construction time. + /// This is the origin for all seeks. + /// + public long StartOfStream { get; } + + /// + /// Seeks within the stream. + /// When is , startOfStream + /// is automatically added to , so callers should pass + /// table-relative offsets directly (e.g. values read from + /// or ). Do not add the table's absolute + /// position — that would double-count. + /// + /// Offset to seek to, relative to . + /// Origin of seek operation. + public void Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + offset += this.StartOfStream; + } + + _ = this.BaseStream.Seek(offset, origin); + } + + /// + /// Reads a single byte from the stream. + /// + /// The byte read + public byte ReadByte() + { + this.ReadInternal(this.buffer, 1); + return this.buffer[0]; + } + + /// + /// Reads a single byte from the stream and reinterprets it as the specified enum type. + /// + /// The enum type whose underlying type must be a single byte. + /// The enum value. + public TEnum ReadByte() + where TEnum : struct, Enum + { + _ = TryConvert(this.ReadByte(), out TEnum value); + return value; + } + + /// + /// Reads a single signed byte from the stream. + /// + /// The byte read + public sbyte ReadSByte() + { + this.ReadInternal(this.buffer, 1); + return unchecked((sbyte)this.buffer[0]); + } + + /// + /// Reads a 2.14 fixed-point number from the stream. + /// 2 bytes are read and divided by 16384 to produce a value in the range [-2, +2). + /// + /// The fixed-point value as a . + public float ReadF2Dot14() + { + const float f2Dot14ToFloat = 16384F; + return this.ReadInt16() / f2Dot14ToFloat; + } + + /// + /// Reads a 16-bit signed integer from the stream, using the bit converter + /// for this reader. 2 bytes are read. + /// + /// The 16-bit integer read + public short ReadInt16() + { + this.ReadInternal(this.buffer, 2); + + return BinaryPrimitives.ReadInt16BigEndian(this.buffer); + } + + /// + /// Reads a 16-bit integer from the stream and reinterprets it as the specified enum type. + /// + /// The enum type whose underlying type must be 16 bits. + /// The enum value. + public TEnum ReadInt16() + where TEnum : struct, Enum + { + _ = TryConvert(this.ReadUInt16(), out TEnum value); + return value; + } + + /// + /// Reads a signed 16-bit integer in big-endian order, representing an FWORD value from the current stream position. + /// + /// A 16-bit signed integer read from the stream, interpreted as an FWORD value. + public short ReadFWORD() => this.ReadInt16(); + + /// + /// Reads an array of FWORD (signed 16-bit) values from the stream. + /// + /// The number of values to read. + /// An array of 16-bit signed integers. + public short[] ReadFWORDArray(int length) => this.ReadInt16Array(length); + + /// + /// Reads an unsigned 16-bit integer (UFWORD) from the current stream and advances the position by two bytes. + /// + /// An unsigned 16-bit integer read from the current stream. + public ushort ReadUFWORD() => this.ReadUInt16(); + + /// + /// Reads a 32-bit fixed-point number from the underlying data source and returns it as a single-precision + /// floating-point value. + /// + /// A representing the fixed-point value read from the data source. + public float ReadFixed() + { + this.ReadInternal(this.buffer, 4); + return BinaryPrimitives.ReadInt32BigEndian(this.buffer) / 65536F; + } + + /// + /// Reads a 4-byte signed integer from the current stream. + /// + /// The 32-bit signed integer read from the stream. + public int ReadInt32() + { + this.ReadInternal(this.buffer, 4); + + return BinaryPrimitives.ReadInt32BigEndian(this.buffer); + } + + /// + /// Reads a 64-bit signed integer from the stream. + /// 8 bytes are read. + /// + /// The 64-bit integer read. + public long ReadInt64() + { + this.ReadInternal(this.buffer, 8); + + return BinaryPrimitives.ReadInt64BigEndian(this.buffer); + } + + /// + /// Reads a 16-bit unsigned integer from the stream. + /// 2 bytes are read. + /// + /// The 16-bit unsigned integer read. + public ushort ReadUInt16() + { + this.ReadInternal(this.buffer, 2); + + return BinaryPrimitives.ReadUInt16BigEndian(this.buffer); + } + + /// + /// Reads a 16-bit unsigned integer from the stream representing an offset position. + /// 2 bytes are read. The returned value is the raw offset as stored in the font file + /// (typically relative to the start of the containing table). Pass it directly to + /// with — do not add the table's + /// absolute position. + /// + /// The 16-bit unsigned integer read. + public ushort ReadOffset16() => this.ReadUInt16(); + + /// + /// Reads a 16-bit unsigned integer from the stream and reinterprets it as the specified enum type. + /// + /// The enum type whose underlying type must be 16 bits. + /// The enum value. + public TEnum ReadUInt16() + where TEnum : struct, Enum + { + _ = TryConvert(this.ReadUInt16(), out TEnum value); + return value; + } + + /// + /// Reads an array of 16-bit unsigned integers from the stream. + /// + /// The number of values to read. + /// An array of 16-bit unsigned integers. + public ushort[] ReadUInt16Array(int length) + { + ushort[] data = new ushort[length]; + for (int i = 0; i < length; i++) + { + data[i] = this.ReadUInt16(); + } + + return data; + } + + /// + /// Reads array of 16-bit unsigned integers from the stream to the buffer. + /// + /// The buffer to read to. + public void ReadUInt16Array(Span buffer) + { + for (int i = 0; i < buffer.Length; i++) + { + buffer[i] = this.ReadUInt16(); + } + } + + /// + /// Reads an array of 32-bit unsigned integers from the stream. + /// + /// The number of values to read. + /// An array of 32-bit unsigned integers. + public uint[] ReadUInt32Array(int length) + { + uint[] data = new uint[length]; + for (int i = 0; i < length; i++) + { + data[i] = this.ReadUInt32(); + } + + return data; + } + + /// + /// Reads an array of 8-bit unsigned integers (bytes) from the stream. + /// + /// The number of bytes to read. + /// A byte array of the requested length. + public byte[] ReadUInt8Array(int length) + { + byte[] data = new byte[length]; + + this.ReadInternal(data, length); + + return data; + } + + /// + /// Reads an array of 16-bit signed integers from the stream. + /// + /// The number of values to read. + /// An array of 16-bit signed integers. + public short[] ReadInt16Array(int length) + { + short[] data = new short[length]; + for (int i = 0; i < length; i++) + { + data[i] = this.ReadInt16(); + } + + return data; + } + + /// + /// Reads an array of 16-bit signed integers from the stream to the buffer. + /// + /// The buffer to read to. + public void ReadInt16Array(Span buffer) + { + for (int i = 0; i < buffer.Length; i++) + { + buffer[i] = this.ReadInt16(); + } + } + + /// + /// Reads a 8-bit unsigned integer from the stream, using the bit converter + /// for this reader. 1 bytes are read. + /// + /// The 8-bit unsigned integer read. + public byte ReadUInt8() + { + this.ReadInternal(this.buffer, 1); + return this.buffer[0]; + } + + /// + /// Reads a 24-bit unsigned integer from the stream, using the bit converter + /// for this reader. 3 bytes are read. + /// + /// The 24-bit unsigned integer read. + public uint ReadUInt24() + { + byte highByte = this.ReadByte(); + return (uint)((highByte << 16) | this.ReadUInt16()); + } + + /// + /// Reads a 24-bit unsigned integer from the stream representing an offset position. + /// 3 bytes are read. The returned value is the raw offset as stored in the font file + /// (typically relative to the start of the containing table). Pass it directly to + /// with — do not add the table's + /// absolute position. + /// + /// The 24-bit unsigned integer read. + public uint ReadOffset24() => this.ReadUInt24(); + + /// + /// Reads a 32-bit unsigned integer from the stream, using the bit converter + /// for this reader. 4 bytes are read. + /// + /// The 32-bit unsigned integer read. + public uint ReadUInt32() + { + this.ReadInternal(this.buffer, 4); + + return BinaryPrimitives.ReadUInt32BigEndian(this.buffer); + } + + /// + /// Reads a 32-bit unsigned integer from the stream representing an offset position. + /// 4 bytes are read. The returned value is the raw offset as stored in the font file + /// (typically relative to the start of the containing table). Pass it directly to + /// with — do not add the table's + /// absolute position. + /// + /// The 32-bit unsigned integer read. + public uint ReadOffset32() => this.ReadUInt32(); + + /// + /// Reads the specified number of bytes, returning them in a new byte array. + /// If not enough bytes are available before the end of the stream, this + /// method will return what is available. + /// + /// The number of bytes to read. + /// The bytes read. + public byte[] ReadBytes(int count) + { + byte[] ret = new byte[count]; + int index = 0; + while (index < count) + { + int read = this.BaseStream.Read(ret, index, count - index); + + // Stream has finished half way through. That's fine, return what we've got. + if (read == 0) + { + byte[] copy = new byte[index]; + Buffer.BlockCopy(ret, 0, copy, 0, index); + return copy; + } + + index += read; + } + + return ret; + } + + /// + /// Reads a string of a specific length, which specifies the number of bytes + /// to read from the stream. These bytes are then converted into a string with + /// the encoding for this reader. + /// + /// The bytes to read. + /// The encoding. + /// + /// The string read from the stream. + /// + public string ReadString(int bytesToRead, Encoding encoding) + { + byte[] data = new byte[bytesToRead]; + this.ReadInternal(data, bytesToRead); + return encoding.GetString(data, 0, data.Length); + } + + /// + /// Reads a 4-byte OpenType tag from the stream as a UTF-8 string. + /// + /// A 4-character string representing the tag (e.g. "glyf", "GPOS"). + public string ReadTag() + { + this.ReadInternal(this.buffer, 4); + + return Encoding.UTF8.GetString(this.buffer, 0, 4); + } + + /// + /// Reads an offset consuming the given number of bytes (1–4). + /// The returned value is the raw offset as stored in the font file + /// (typically relative to the start of the containing table). Pass it directly to + /// with — do not add the table's + /// absolute position. + /// + /// The offset size in bytes (1, 2, 3, or 4). + /// The 32-bit signed integer representing the offset. + /// Thrown when is not 1–4. + public int ReadOffset(int size) + => size switch + { + 1 => this.ReadByte(), + 2 => (this.ReadByte() << 8) | (this.ReadByte() << 0), + 3 => (this.ReadByte() << 16) | (this.ReadByte() << 8) | (this.ReadByte() << 0), + 4 => (this.ReadByte() << 24) | (this.ReadByte() << 16) | (this.ReadByte() << 8) | (this.ReadByte() << 0), + _ => throw new InvalidOperationException(), + }; + + /// + /// Reads the given number of bytes from the stream, throwing an exception + /// if they can't all be read. + /// + /// Buffer to read into. + /// Number of bytes to read. + /// The end of the stream was reached before reading could complete. + private void ReadInternal(byte[] data, int size) + { + int index = 0; + + while (index < size) + { + int read = this.BaseStream.Read(data, index, size - index); + if (read == 0) + { + throw new EndOfStreamException($"End of stream reached with {size - index} byte{(size - index == 1 ? "s" : string.Empty)} left to read."); + } + + index += read; + } + } + + /// + public void Dispose() + { + if (!this.leaveOpen) + { + this.BaseStream?.Dispose(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryConvert(T input, out TEnum value) + where T : struct, IConvertible, IFormattable, IComparable + where TEnum : struct, Enum + { + if (Unsafe.SizeOf() == Unsafe.SizeOf()) + { + value = Unsafe.As(ref input); + return true; + } + + value = default; + return false; + } + } +} diff --git a/SixLabors.Fonts/Bounds.cs b/SixLabors.Fonts/Bounds.cs new file mode 100644 index 0000000..632ba58 --- /dev/null +++ b/SixLabors.Fonts/Bounds.cs @@ -0,0 +1,93 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.Fonts.Tables.TrueType.Glyphs; + +namespace SixLabors.Fonts { + internal readonly struct Bounds : IEquatable + { + public static Bounds Empty; + + public Bounds(Vector2 min, Vector2 max) + { + this.Min = Vector2.Min(min, max); + this.Max = Vector2.Max(min, max); + } + + public Bounds(float minX, float minY, float maxX, float maxY) + : this(new Vector2(minX, minY), new Vector2(maxX, maxY)) + { + } + + public Vector2 Min { get; } + + public Vector2 Max { get; } + + public static bool operator ==(Bounds left, Bounds right) => left.Equals(right); + + public static bool operator !=(Bounds left, Bounds right) => !(left == right); + + public Vector2 Size() => this.Max - this.Min; + + public static Bounds Load(BigEndianBinaryReader reader) + { + short minX = reader.ReadInt16(); + short minY = reader.ReadInt16(); + short maxX = reader.ReadInt16(); + short maxY = reader.ReadInt16(); + + return new Bounds(minX, minY, maxX, maxY); + } + + public static Bounds Load(IList controlPoints) + { + if (controlPoints is null || controlPoints.Count == 0) + { + return Empty; + } + + float xMin = float.MaxValue; + float yMin = float.MaxValue; + float xMax = float.MinValue; + float yMax = float.MinValue; + + for (int i = 0; i < controlPoints.Count; i++) + { + Vector2 p = controlPoints[i].Point; + if (p.X < xMin) + { + xMin = p.X; + } + + if (p.X > xMax) + { + xMax = p.X; + } + + if (p.Y < yMin) + { + yMin = p.Y; + } + + if (p.Y > yMax) + { + yMax = p.Y; + } + } + + return new Bounds(xMin, yMin, xMax, yMax); + } + + public static Bounds Transform(in Bounds bounds, Matrix3x2 matrix) + => new(Vector2.Transform(bounds.Min, matrix), Vector2.Transform(bounds.Max, matrix)); + + public override bool Equals(object? obj) => obj is Bounds bounds && this.Equals(bounds); + + public bool Equals(Bounds other) => this.Min.Equals(other.Min) && this.Max.Equals(other.Max); + + public override int GetHashCode() => HashCode.Combine(this.Min, this.Max); + } +} diff --git a/SixLabors.Fonts/Buffer{T}.cs b/SixLabors.Fonts/Buffer{T}.cs new file mode 100644 index 0000000..91ad029 --- /dev/null +++ b/SixLabors.Fonts/Buffer{T}.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts { + /// + /// An disposable buffer that is backed by an array pool. + /// + /// The type of buffer element. + internal ref struct Buffer + where T : unmanaged + { + private int length; + private readonly byte[] buffer; + private readonly Span span; + private bool isDisposed; + + public Buffer(int length) + : this(length, clear: false) + { + } + + public Buffer(int length, bool clear) + { + Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length)); + int itemSizeBytes = Unsafe.SizeOf(); + int bufferSizeInBytes = length * itemSizeBytes; + this.buffer = ArrayPool.Shared.Rent(bufferSizeInBytes); + this.length = length; + + using ByteMemoryManager manager = new(this.buffer); + this.Memory = manager.Memory[..this.length]; + this.span = this.Memory.Span; + + if (clear) + { + this.span.Clear(); + } + + this.isDisposed = false; + } + + public Memory Memory { get; } + + public readonly Span GetSpan() + { + if (this.buffer is null) + { + ThrowObjectDisposedException(); + } + + return this.span; + } + + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + ArrayPool.Shared.Return(this.buffer); + this.length = 0; + this.isDisposed = true; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowObjectDisposedException() => throw new ObjectDisposedException("Buffer"); + } +} diff --git a/SixLabors.Fonts/ByteMemoryManager{T}.cs b/SixLabors.Fonts/ByteMemoryManager{T}.cs new file mode 100644 index 0000000..0a00234 --- /dev/null +++ b/SixLabors.Fonts/ByteMemoryManager{T}.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.Fonts { + /// + /// A custom that can wrap of instances + /// and cast them to be for any arbitrary unmanaged value type. + /// + /// The value type to use when casting the wrapped instance. + internal sealed class ByteMemoryManager : MemoryManager + where T : unmanaged + { + /// + /// The wrapped of instance. + /// + private readonly Memory memory; + + /// + /// Initializes a new instance of the class. + /// + /// The of instance to wrap. + public ByteMemoryManager(Memory memory) => this.memory = memory; + + /// + protected override void Dispose(bool disposing) + { + } + + /// + public override Span GetSpan() => MemoryMarshal.Cast(this.memory.Span); + + /// + public override MemoryHandle Pin(int elementIndex = 0) + + // We need to adjust the offset into the wrapped byte segment, + // as the input index refers to the target-cast memory of T. + // We just have to shift this index by the byte size of T. + => this.memory.Slice(elementIndex * Unsafe.SizeOf()).Pin(); + + /// + public override void Unpin() + { + } + } +} diff --git a/SixLabors.Fonts/CaretMovement.cs b/SixLabors.Fonts/CaretMovement.cs new file mode 100644 index 0000000..7c35b42 --- /dev/null +++ b/SixLabors.Fonts/CaretMovement.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Specifies a caret movement operation within laid-out text. + /// + public enum CaretMovement + { + /// + /// Move to the previous grapheme insertion position. + /// + Previous, + + /// + /// Move to the next grapheme insertion position. + /// + Next, + + /// + /// Move to the previous Unicode word boundary. + /// + PreviousWord, + + /// + /// Move to the next Unicode word boundary. + /// + NextWord, + + /// + /// Move to the start of the current line. + /// + LineStart, + + /// + /// Move to the end of the current line. + /// + LineEnd, + + /// + /// Move to the start of the laid-out text. + /// + TextStart, + + /// + /// Move to the end of the laid-out text. + /// + TextEnd, + + /// + /// Move to the previous visual line. + /// + LineUp, + + /// + /// Move to the next visual line. + /// + LineDown + } +} diff --git a/SixLabors.Fonts/CaretPlacement.cs b/SixLabors.Fonts/CaretPlacement.cs new file mode 100644 index 0000000..93d2559 --- /dev/null +++ b/SixLabors.Fonts/CaretPlacement.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Specifies an absolute caret placement within a laid-out text scope. + /// + public enum CaretPlacement + { + /// + /// Place the caret at the start of the laid-out text scope. + /// + Start, + + /// + /// Place the caret at the end of the laid-out text scope. + /// + End + } +} diff --git a/SixLabors.Fonts/CaretPosition.cs b/SixLabors.Fonts/CaretPosition.cs new file mode 100644 index 0000000..d4801fc --- /dev/null +++ b/SixLabors.Fonts/CaretPosition.cs @@ -0,0 +1,91 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts { + /// + /// Represents a caret line in laid-out text. + /// + public readonly struct CaretPosition + { + /// + /// Initializes a new instance of the struct. + /// + /// The zero-based line index. + /// The grapheme insertion index in the original text. + /// The UTF-16 index in the original text. + /// The caret start point in pixel units. + /// The caret end point in pixel units. + /// Whether the caret has a second visual position. + /// The secondary caret start point in pixel units. + /// The secondary caret end point in pixel units. + /// The position to preserve when moving between visual lines. + internal CaretPosition( + int lineIndex, + int graphemeIndex, + int stringIndex, + Vector2 start, + Vector2 end, + bool hasSecondary, + Vector2 secondaryStart, + Vector2 secondaryEnd, + float lineNavigationPosition) + { + this.LineIndex = lineIndex; + this.GraphemeIndex = graphemeIndex; + this.StringIndex = stringIndex; + this.Start = start; + this.End = end; + this.HasSecondary = hasSecondary; + this.SecondaryStart = secondaryStart; + this.SecondaryEnd = secondaryEnd; + this.LineNavigationPosition = lineNavigationPosition; + } + + /// + /// Gets the zero-based line index. + /// + public int LineIndex { get; } + + /// + /// Gets the zero-based grapheme index in the original text. + /// + public int GraphemeIndex { get; } + + /// + /// Gets the zero-based UTF-16 code unit index in the original text. + /// + public int StringIndex { get; } + + /// + /// Gets the caret start point in pixel units. + /// + public Vector2 Start { get; } + + /// + /// Gets the caret end point in pixel units. + /// + public Vector2 End { get; } + + /// + /// Gets a value indicating whether a second visual caret position is available. + /// + public bool HasSecondary { get; } + + /// + /// Gets the secondary caret start point in pixel units. + /// + public Vector2 SecondaryStart { get; } + + /// + /// Gets the secondary caret end point in pixel units. + /// + public Vector2 SecondaryEnd { get; } + + /// + /// Gets the position to preserve when moving between visual lines. + /// + internal float LineNavigationPosition { get; } + } +} diff --git a/SixLabors.Fonts/ClipQuad.cs b/SixLabors.Fonts/ClipQuad.cs new file mode 100644 index 0000000..981daf6 --- /dev/null +++ b/SixLabors.Fonts/ClipQuad.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.Fonts { + /// + /// Represents a rectangular clipping region as a convex quadrilateral. + /// Allows for transformation by rotation, skew, or non-uniform scaling, + /// resulting in non-axis-aligned edges. + /// + public readonly struct ClipQuad + { + /// + /// Initializes a new instance of the struct. + /// + /// The top-left corner of the quadrilateral. + /// The top-right corner of the quadrilateral. + /// The bottom-right corner of the quadrilateral. + /// The bottom-left corner of the quadrilateral. + public ClipQuad(Vector2 topLeft, Vector2 topRight, Vector2 bottomRight, Vector2 bottomLeft) + { + this.TopLeft = topLeft; + this.TopRight = topRight; + this.BottomRight = bottomRight; + this.BottomLeft = bottomLeft; + } + + /// + /// Gets the top-left corner of the quadrilateral. + /// + public Vector2 TopLeft { get; } + + /// + /// Gets the top-right corner of the quadrilateral. + /// + public Vector2 TopRight { get; } + + /// + /// Gets the bottom-right corner of the quadrilateral. + /// + public Vector2 BottomRight { get; } + + /// + /// Gets the bottom-left corner of the quadrilateral. + /// + public Vector2 BottomLeft { get; } + + /// + /// Creates a from an axis-aligned and an optional transform. + /// + /// The bounds representing the untransformed rectangular area. + /// An optional transform to apply. If omitted, no transform is applied. + /// A representing the transformed rectangle. + internal static ClipQuad FromBounds(in Bounds bounds, in Matrix3x2 transform) + { + Vector2 tl = Vector2.Transform(bounds.Min, transform); + Vector2 tr = Vector2.Transform(new Vector2(bounds.Max.X, bounds.Min.Y), transform); + Vector2 br = Vector2.Transform(bounds.Max, transform); + Vector2 bl = Vector2.Transform(new Vector2(bounds.Min.X, bounds.Max.Y), transform); + return new ClipQuad(tl, tr, br, bl); + } + + /// + /// Determines whether the quadrilateral is axis-aligned within a small tolerance. + /// + /// The tolerance for comparing parallel edges, typically a small epsilon. + /// + /// if opposite edges are parallel and of equal length; otherwise, . + /// + public bool IsAxisAligned(float tolerance = 1E-4F) + { + Vector2 top = this.TopRight - this.TopLeft; + Vector2 bottom = this.BottomRight - this.BottomLeft; + Vector2 left = this.BottomLeft - this.TopLeft; + Vector2 right = this.BottomRight - this.TopRight; + + bool horizontalParallel = MathF.Abs(Vector2.Dot(Vector2.Normalize(top), Vector2.Normalize(bottom)) - 1F) < tolerance; + bool verticalParallel = MathF.Abs(Vector2.Dot(Vector2.Normalize(left), Vector2.Normalize(right)) - 1F) < tolerance; + + return horizontalParallel && verticalParallel; + } + } +} diff --git a/SixLabors.Fonts/ColorFontSupport.cs b/SixLabors.Fonts/ColorFontSupport.cs new file mode 100644 index 0000000..0f73d06 --- /dev/null +++ b/SixLabors.Fonts/ColorFontSupport.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts { + /// + /// Specifies which color font formats are enabled for layout and rendering. + /// + /// + /// This enumeration allows a renderer to select which OpenType color font + /// technologies to honor when processing glyph runs. Multiple formats may be + /// enabled simultaneously. + /// + [Flags] + public enum ColorFontSupport + { + /// + /// Disable color font rendering entirely. All glyphs will be drawn as monochrome outlines. + /// + None = 0, + + /// + /// Enable rendering of COLR version 0 color glyphs (layered solid colors defined by COLR/CPAL tables). + /// + ColrV0 = 1, + + /// + /// Enable rendering of COLR version 1 color glyphs (paint graph-based color glyphs with gradients and transforms). + /// + ColrV1 = 2, + + /// + /// Enable rendering of color glyphs stored as SVG documents in the OpenType SVG table. + /// + Svg = 4 + } +} diff --git a/SixLabors.Fonts/DecorationPositioningMode.cs b/SixLabors.Fonts/DecorationPositioningMode.cs new file mode 100644 index 0000000..77dc951 --- /dev/null +++ b/SixLabors.Fonts/DecorationPositioningMode.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Defines how text decorations (underline, overline, strikethrough) are positioned relative to font metrics. + /// + public enum DecorationPositioningMode + { + /// + /// Uses the primary (base) font's metrics for the entire run or line, + /// ensuring a consistent decoration position across mixed fonts and scripts. + /// Matches typical browser behavior. + /// + PrimaryFont = 0, + + /// + /// Uses each glyph's own font metrics to position its decoration. + /// Decoration positions may vary between glyphs and fallback fonts within the same line. + /// Matches typical Microsoft Word behavior. + /// + GlyphFont = 1, + } +} diff --git a/SixLabors.Fonts/Exceptions/FontException.cs b/SixLabors.Fonts/Exceptions/FontException.cs new file mode 100644 index 0000000..7cc29ba --- /dev/null +++ b/SixLabors.Fonts/Exceptions/FontException.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts { + /// + /// Base class for exceptions thrown by this library. + /// + /// + public class FontException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public FontException(string message) + : base(message) + { + } + } +} diff --git a/SixLabors.Fonts/Exceptions/FontFamilyNotFoundException.cs b/SixLabors.Fonts/Exceptions/FontFamilyNotFoundException.cs new file mode 100644 index 0000000..d17b91b --- /dev/null +++ b/SixLabors.Fonts/Exceptions/FontFamilyNotFoundException.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SixLabors.Fonts { + /// + /// Exception for detailing missing font families. + /// + /// + public class FontFamilyNotFoundException : FontException + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the missing font family. + public FontFamilyNotFoundException(string family) + : this(family, Array.Empty()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the missing font family. + /// + /// The collection of directories that were searched for the font family. + /// Pass an empty collection if font families were not searched in directories. + /// + public FontFamilyNotFoundException(string family, IReadOnlyCollection searchDirectories) + : base(GetMessage(family, searchDirectories)) + { + this.FontFamily = family; + this.SearchDirectories = searchDirectories; + } + + /// + /// Gets the name of the font family that was not found. + /// + public string FontFamily { get; } + + /// + /// Gets the collection of directories that were unsuccessfully searched for the font family. + /// + /// + /// If the exception did not originate from the then this property will be empty. + /// + public IReadOnlyCollection SearchDirectories { get; } + + private static string GetMessage(string family, IReadOnlyCollection searchDirectories) + { + if (searchDirectories.Count == 0) + { + return $"The \"{family}\" font family could not be found"; + } + + if (searchDirectories.Count == 1) + { + return $"The \"{family}\" font family could not be found in the following directory: {searchDirectories.First()}"; + } + + return $"The \"{family}\" font family could not be found in the following directories:{Environment.NewLine}{string.Join(Environment.NewLine, searchDirectories.Select(e => $" * {e}"))}"; + } + } +} diff --git a/SixLabors.Fonts/Exceptions/FontsThrowHelper.cs b/SixLabors.Fonts/Exceptions/FontsThrowHelper.cs new file mode 100644 index 0000000..ca08252 --- /dev/null +++ b/SixLabors.Fonts/Exceptions/FontsThrowHelper.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// Helper methods to throw exceptions + /// + internal static class FontsThrowHelper + { + /// + /// Throws an . + /// + [MethodImpl(MethodImplOptions.NoInlining)] + public static T ThrowGlyphMissingException(CodePoint codePoint) + => throw new GlyphMissingException(codePoint); + + /// + /// Throws an . + /// + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowDefaultInstance() + => throw new FontException("Cannot use the default value type instance to create a font."); + } +} diff --git a/SixLabors.Fonts/Exceptions/GlyphMissingException.cs b/SixLabors.Fonts/Exceptions/GlyphMissingException.cs new file mode 100644 index 0000000..d52c5c9 --- /dev/null +++ b/SixLabors.Fonts/Exceptions/GlyphMissingException.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// Exception for detailing missing font families. + /// + /// + public class GlyphMissingException : FontException + { + /// + /// Initializes a new instance of the class. + /// + /// The code point for the glyph we where unable to find. + public GlyphMissingException(CodePoint codePoint) + : base($"Cannot find a glyph for the code point '{codePoint.ToDebuggerDisplay()}'") + { + } + } +} diff --git a/SixLabors.Fonts/Exceptions/InvalidFontFileException.cs b/SixLabors.Fonts/Exceptions/InvalidFontFileException.cs new file mode 100644 index 0000000..8c4e40c --- /dev/null +++ b/SixLabors.Fonts/Exceptions/InvalidFontFileException.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts { + /// + /// Exception font loading can throw if it encounters invalid data during font loading. + /// + /// + public class InvalidFontFileException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public InvalidFontFileException(string message) + : base(message) + { + } + } +} diff --git a/SixLabors.Fonts/Exceptions/InvalidFontTableException.cs b/SixLabors.Fonts/Exceptions/InvalidFontTableException.cs new file mode 100644 index 0000000..a1abaca --- /dev/null +++ b/SixLabors.Fonts/Exceptions/InvalidFontTableException.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Exception font loading can throw if it encounters invalid data during font loading. + /// + /// + public class InvalidFontTableException : InvalidFontFileException + { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The table. + public InvalidFontTableException(string message, string table) + : base(message) + => this.Table = table; + + /// + /// Gets the table where the error originated. + /// + public string Table { get; } + } +} diff --git a/SixLabors.Fonts/Exceptions/MissingFontTableException.cs b/SixLabors.Fonts/Exceptions/MissingFontTableException.cs new file mode 100644 index 0000000..65fc4c2 --- /dev/null +++ b/SixLabors.Fonts/Exceptions/MissingFontTableException.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Exception font loading can throw if it finds a required table is missing during font loading. + /// + /// + public class MissingFontTableException : InvalidFontFileException + { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The table. + public MissingFontTableException(string message, string table) + : base(message) + => this.Table = table; + + /// + /// Gets the table where the error originated. + /// + public string Table { get; } + } +} diff --git a/SixLabors.Fonts/FileFontMetrics.cs b/SixLabors.Fonts/FileFontMetrics.cs new file mode 100644 index 0000000..30bfcc8 --- /dev/null +++ b/SixLabors.Fonts/FileFontMetrics.cs @@ -0,0 +1,208 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Numerics; +using SixLabors.Fonts.Tables; +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// + /// Represents a font face with metrics, which is a set of glyphs with a specific style (regular, italic, bold etc). + /// + /// The font source is a filesystem path. + /// + internal sealed class FileFontMetrics : FontMetrics + { + private readonly Lazy fontMetrics; + + public FileFontMetrics(string path) + : this(path, 0) + { + } + + public FileFontMetrics(string path, long offset) + : this(FontDescription.LoadDescription(path), path, offset) + { + } + + internal FileFontMetrics(FontDescription description, string path, long offset) + { + this.Description = description; + this.Path = path; + this.fontMetrics = new Lazy(() => StreamFontMetrics.LoadFont(path, offset), true); + } + + /// + public override FontDescription Description { get; } + + /// + /// Gets the filesystem path to the font face source. + /// + public string Path { get; } + + /// + /// Gets the underlying that this file-backed instance delegates to. + /// + internal StreamFontMetrics StreamFontMetrics => this.fontMetrics.Value; + + /// + public override ushort UnitsPerEm => this.fontMetrics.Value.UnitsPerEm; + + /// + public override float ScaleFactor => this.fontMetrics.Value.ScaleFactor; + + /// + public override HorizontalMetrics HorizontalMetrics => this.fontMetrics.Value.HorizontalMetrics; + + /// + public override VerticalMetrics VerticalMetrics => this.fontMetrics.Value.VerticalMetrics; + + /// + public override short SubscriptXSize => this.fontMetrics.Value.SubscriptXSize; + + /// + public override short SubscriptYSize => this.fontMetrics.Value.SubscriptYSize; + + /// + public override short SubscriptXOffset => this.fontMetrics.Value.SubscriptXOffset; + + /// + public override short SubscriptYOffset => this.fontMetrics.Value.SubscriptYOffset; + + /// + public override short SuperscriptXSize => this.fontMetrics.Value.SuperscriptXSize; + + /// + public override short SuperscriptYSize => this.fontMetrics.Value.SuperscriptYSize; + + /// + public override short SuperscriptXOffset => this.fontMetrics.Value.SuperscriptXOffset; + + /// + public override short SuperscriptYOffset => this.fontMetrics.Value.SuperscriptYOffset; + + /// + public override short StrikeoutSize => this.fontMetrics.Value.StrikeoutSize; + + /// + public override short StrikeoutPosition => this.fontMetrics.Value.StrikeoutPosition; + + /// + public override short UnderlinePosition => this.fontMetrics.Value.UnderlinePosition; + + /// + public override short UnderlineThickness => this.fontMetrics.Value.UnderlineThickness; + + /// + public override float ItalicAngle => this.fontMetrics.Value.ItalicAngle; + + /// + internal override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) + => this.fontMetrics.Value.TryGetGlyphId(codePoint, out glyphId); + + /// + internal override bool TryGetGlyphId( + CodePoint codePoint, + CodePoint? nextCodePoint, + out ushort glyphId, + out bool skipNextCodePoint) + => this.fontMetrics.Value.TryGetGlyphId(codePoint, nextCodePoint, out glyphId, out skipNextCodePoint); + + /// + internal override bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) + => this.fontMetrics.Value.TryGetCodePoint(glyphId, out codePoint); + + /// + internal override bool TryGetGlyphClass(ushort glyphId, [NotNullWhen(true)] out GlyphClassDef? glyphClass) + => this.fontMetrics.Value.TryGetGlyphClass(glyphId, out glyphClass); + + /// + internal override bool TryGetMarkAttachmentClass(ushort glyphId, [NotNullWhen(true)] out GlyphClassDef? markAttachmentClass) + => this.fontMetrics.Value.TryGetMarkAttachmentClass(glyphId, out markAttachmentClass); + + /// + public override bool TryGetVariationAxes(out ReadOnlyMemory variationAxes) + => this.fontMetrics.Value.TryGetVariationAxes(out variationAxes); + + /// + internal override bool IsInMarkFilteringSet(ushort markGlyphSetIndex, ushort glyphId) + => this.fontMetrics.Value.IsInMarkFilteringSet(markGlyphSetIndex, glyphId); + + /// + public override bool TryGetGlyphMetrics( + CodePoint codePoint, + TextAttributes textAttributes, + TextDecorations textDecorations, + LayoutMode layoutMode, + ColorFontSupport support, + [NotNullWhen(true)] out FontGlyphMetrics? metrics) + => this.fontMetrics.Value.TryGetGlyphMetrics(codePoint, textAttributes, textDecorations, layoutMode, support, out metrics); + + /// + internal override FontGlyphMetrics GetGlyphMetrics( + CodePoint codePoint, + ushort glyphId, + TextAttributes textAttributes, + TextDecorations textDecorations, + LayoutMode layoutMode, + ColorFontSupport support) + => this.fontMetrics.Value.GetGlyphMetrics(codePoint, glyphId, textAttributes, textDecorations, layoutMode, support); + + /// + public override ReadOnlyMemory GetAvailableCodePoints() + => this.fontMetrics.Value.GetAvailableCodePoints(); + + /// + internal override bool TryGetGSubTable([NotNullWhen(true)] out GSubTable? gSubTable) + => this.fontMetrics.Value.TryGetGSubTable(out gSubTable); + + /// + internal override void ApplySubstitution(GlyphSubstitutionCollection collection) + => this.fontMetrics.Value.ApplySubstitution(collection); + + /// + internal override bool TryGetKerningOffset(ushort currentId, ushort nextId, out Vector2 vector) + => this.fontMetrics.Value.TryGetKerningOffset(currentId, nextId, out vector); + + /// + internal override void UpdatePositions(GlyphPositioningCollection collection) + => this.fontMetrics.Value.UpdatePositions(collection); + + /// + internal override float GetGDefVariationDelta(uint packedVariationIndex) + => this.fontMetrics.Value.GetGDefVariationDelta(packedVariationIndex); + + /// + internal override ReadOnlySpan GetNormalizedCoordinates() + => this.fontMetrics.Value.GetNormalizedCoordinates(); + + /// + /// Reads a from the specified stream. + /// + /// The file path. + /// A read-only memory region containing the font metrics. + public static ReadOnlyMemory LoadFontCollection(string path) + { + using FileStream fs = File.OpenRead(path); + long startPos = fs.Position; + using BigEndianBinaryReader reader = new(fs, true); + TtcHeader ttcHeader = TtcHeader.Read(reader); + FileFontMetrics[] fonts = new FileFontMetrics[(int)ttcHeader.NumFonts]; + + for (int i = 0; i < ttcHeader.NumFonts; ++i) + { + fs.Position = startPos + ttcHeader.OffsetTable[i]; + FontDescription description = FontDescription.LoadDescription(fs); + fonts[i] = new FileFontMetrics(description, path, ttcHeader.OffsetTable[i]); + } + + return fonts; + } + } +} diff --git a/SixLabors.Fonts/Font.cs b/SixLabors.Fonts/Font.cs new file mode 100644 index 0000000..8c7eb12 --- /dev/null +++ b/SixLabors.Fonts/Font.cs @@ -0,0 +1,375 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// Defines a particular format for text, including font face, size, and style attributes. + /// This class cannot be inherited. + /// + public sealed class Font + { + private readonly FontVariation[] variations; + private readonly Lazy metrics; + private readonly Lazy fontName; + + /// + /// Initializes a new instance of the class. + /// + /// The font family. + /// The size of the font in PT units. + public Font(FontFamily family, float size) + : this(family, size, FontStyle.Regular) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The font family. + /// The size of the font in PT units. + /// The font style. + public Font(FontFamily family, float size, FontStyle style) + { + if (family == default) + { + throw new ArgumentException("Cannot use the default value type instance to create a font.", nameof(family)); + } + + this.Family = family; + this.RequestedStyle = style; + this.Size = size; + this.variations = []; + this.metrics = new Lazy(this.LoadInstanceInternal, true); + this.fontName = new Lazy(this.LoadFontName, true); + } + + /// + /// Initializes a new instance of the class. + /// + /// The prototype. + /// The font style. + public Font(Font prototype, FontStyle style) + : this(prototype?.Family ?? throw new ArgumentNullException(nameof(prototype)), prototype.Size, style) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The prototype. + /// The size of the font in PT units. + /// The font style. + public Font(Font prototype, float size, FontStyle style) + : this(prototype?.Family ?? throw new ArgumentNullException(nameof(prototype)), size, style) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The prototype. + /// The size of the font in PT units. + public Font(Font prototype, float size) + : this(prototype.Family, size, prototype.RequestedStyle) + { + } + + /// + /// Initializes a new instance of the class with the specified variation axis settings. + /// + /// The prototype font providing family, size, and style. + /// The variation axis settings to apply. + public Font(Font prototype, params FontVariation[] variations) + { + Guard.NotNull(prototype, nameof(prototype)); + Guard.NotNull(variations, nameof(variations)); + + this.Family = prototype.Family; + this.RequestedStyle = prototype.RequestedStyle; + this.Size = prototype.Size; + this.variations = variations; + this.metrics = new Lazy(this.LoadInstanceInternal, true); + this.fontName = new Lazy(this.LoadFontName, true); + } + + /// + /// Gets the family. + /// + public FontFamily Family { get; } + + /// + /// Gets the name. + /// + public string Name => this.fontName.Value; + + /// + /// Gets the size of the font in PT units. + /// + public float Size { get; } + + /// + /// Gets the font metrics. + /// + /// Font instance not found. + public FontMetrics FontMetrics => this.metrics.Value ?? throw new FontException("Font instance not found."); + + /// + /// Gets a value indicating whether this is bold. + /// + public bool IsBold => (this.FontMetrics.Description.Style & FontStyle.Bold) == FontStyle.Bold; + + /// + /// Gets a value indicating whether this is italic. + /// + public bool IsItalic => (this.FontMetrics.Description.Style & FontStyle.Italic) == FontStyle.Italic; + + /// + /// Gets the variation axis settings applied to this font. + /// + public ReadOnlySpan Variations => this.variations; + + /// + /// Gets the requested style. + /// + internal FontStyle RequestedStyle { get; } + + /// + /// Gets the filesystem path to the font family source. + /// + /// + /// When this method returns, contains the filesystem path to the font family source, + /// if the path exists; otherwise, the default value for the type of the path parameter. + /// This parameter is passed uninitialized. + /// + /// + /// if the was created via a filesystem path; otherwise, . + /// + public bool TryGetPath([NotNullWhen(true)] out string? path) + { + if (this == default) + { + FontsThrowHelper.ThrowDefaultInstance(); + } + + if (this.FontMetrics is FileFontMetrics fileMetrics) + { + path = fileMetrics.Path; + return true; + } + + path = null; + return false; + } + + /// + /// Gets the glyph for the given codepoint. + /// + /// The code point of the character. + /// + /// When this method returns, contains the glyph for the given codepoint if the glyph + /// is found; otherwise the default value. This parameter is passed uninitialized. + /// + /// + /// if the face contains glyphs for the specified codepoint; otherwise, . + /// + public bool TryGetGlyphs(CodePoint codePoint, [NotNullWhen(true)] out Glyph? glyph) + => this.TryGetGlyphs(codePoint, TextAttributes.None, ColorFontSupport.None, out glyph); + + /// + /// Gets the glyph for the given codepoint. + /// + /// The code point of the character. + /// Options for enabling color font support during layout and rendering. + /// + /// When this method returns, contains the glyphs for the given codepoint and color support if the glyph + /// is found; otherwise the default value. This parameter is passed uninitialized. + /// + /// + /// if the face contains glyphs for the specified codepoint; otherwise, . + /// + public bool TryGetGlyphs(CodePoint codePoint, ColorFontSupport support, [NotNullWhen(true)] out Glyph? glyph) + => this.TryGetGlyphs(codePoint, TextAttributes.None, support, out glyph); + + /// + /// Gets the glyph for the given codepoint. + /// + /// The code point of the character. + /// The text attributes to apply to the glyphs. + /// Options for enabling color font support during layout and rendering. + /// + /// When this method returns, contains the glyph for the given codepoint, attributes, and color support if the glyph + /// is found; otherwise the default value. This parameter is passed uninitialized. + /// + /// + /// if the face contains glyphs for the specified codepoint; otherwise, . + /// + public bool TryGetGlyphs( + CodePoint codePoint, + TextAttributes textAttributes, + ColorFontSupport support, + [NotNullWhen(true)] out Glyph? glyph) + => this.TryGetGlyph(codePoint, textAttributes, TextDecorations.None, LayoutMode.HorizontalTopBottom, support, out glyph); + + /// + /// Gets the glyph for the given codepoint. + /// + /// The code point of the character. + /// The text attributes to apply to the glyphs. + /// The layout mode to apply to the glyphs. + /// Options for enabling color font support during layout and rendering. + /// + /// When this method returns, contains the glyph for the given codepoint, attributes, and color support if the glyph + /// is found; otherwise the default value. This parameter is passed uninitialized. + /// + /// + /// if the face contains glyphs for the specified codepoint; otherwise, . + /// + public bool TryGetGlyph( + CodePoint codePoint, + TextAttributes textAttributes, + LayoutMode layoutMode, + ColorFontSupport support, + [NotNullWhen(true)] out Glyph? glyph) + => this.TryGetGlyph(codePoint, textAttributes, TextDecorations.None, layoutMode, support, out glyph); + + /// + /// Gets the glyph for the given codepoint. + /// + /// The code point of the character. + /// The text attributes to apply to the glyphs. + /// The text decorations to apply to the glyphs. + /// The layout mode to apply to the glyphs. + /// Options for enabling color font support during layout and rendering. + /// + /// When this method returns, contains the glyph for the given codepoint, attributes, and color support if the glyph + /// is found; otherwise the default value. This parameter is passed uninitialized. + /// + /// + /// if the face contains glyphs for the specified codepoint; otherwise, . + /// + public bool TryGetGlyph( + CodePoint codePoint, + TextAttributes textAttributes, + TextDecorations textDecorations, + LayoutMode layoutMode, + ColorFontSupport support, + [NotNullWhen(true)] out Glyph? glyph) + { + TextRun textRun = new() { Start = 0, End = 1, Font = this, TextAttributes = textAttributes, TextDecorations = textDecorations }; + if (this.FontMetrics.TryGetGlyphMetrics(codePoint, textAttributes, textDecorations, layoutMode, support, out FontGlyphMetrics? metrics)) + { + glyph = new(metrics.CloneForRendering(textRun), this.Size); + return true; + } + + glyph = null; + return false; + } + + /// + /// Gets the amount, in px units, the glyph should be offset if it is followed by + /// the glyph. + /// + /// The current glyph. + /// The next glyph. + /// The DPI (Dots Per Inch) to render/measure the kerning offset at. + /// + /// When this method returns, contains the offset, in font units, that should be applied to the + /// glyph, if the offset is found; otherwise the default vector value. + /// This parameter is passed uninitialized. + /// + /// + /// if the face contains and offset for the glyph combination; otherwise, . + /// + public bool TryGetKerningOffset(Glyph current, Glyph next, float dpi, out Vector2 vector) + { + if (this.FontMetrics.TryGetKerningOffset(current.GlyphMetrics.GlyphId, next.GlyphMetrics.GlyphId, out vector)) + { + // Scale the result + Vector2 scale = new Vector2(this.Size * dpi) / next.GlyphMetrics.ScaleFactor; + vector *= scale; + return true; + } + + return false; + } + + private string LoadFontName() + => this.metrics.Value?.Description.FontName(this.Family.Culture) ?? string.Empty; + + private FontMetrics? LoadInstanceInternal() + { + FontMetrics? metrics = this.ResolveBaseMetrics(); + if (metrics is null) + { + return null; + } + + // If variations are specified and the base metrics supports them, create a variation instance. + if (this.variations.Length > 0) + { + StreamFontMetrics? streamMetrics = metrics switch + { + StreamFontMetrics s => s, + FileFontMetrics f => f.StreamFontMetrics, + _ => null + }; + + if (streamMetrics is not null) + { + return streamMetrics.CreateVariationInstance(this.variations); + } + } + + return metrics; + } + + private FontMetrics? ResolveBaseMetrics() + { + if (this.Family.TryGetMetrics(this.RequestedStyle, out FontMetrics? metrics)) + { + return metrics; + } + + if ((this.RequestedStyle & FontStyle.Italic) == FontStyle.Italic) + { + // Can't find style requested and they want one that's at least partial italic. + // Try the regular italic. + if (this.Family.TryGetMetrics(FontStyle.Italic, out metrics)) + { + return metrics; + } + } + + if ((this.RequestedStyle & FontStyle.Bold) == FontStyle.Bold) + { + // Can't find style requested and they want one that's at least partial bold. + // Try the regular bold. + if (this.Family.TryGetMetrics(FontStyle.Bold, out metrics)) + { + return metrics; + } + } + + // Can't find style requested so let's just try returning the default. + ReadOnlySpan styles = this.Family.GetAvailableStyles().Span; + FontStyle defaultStyle = styles[0]; + foreach (FontStyle style in styles) + { + if (style == FontStyle.Regular) + { + defaultStyle = FontStyle.Regular; + break; + } + } + + this.Family.TryGetMetrics(defaultStyle, out metrics); + return metrics; + } + } +} diff --git a/SixLabors.Fonts/FontCollection.cs b/SixLabors.Fonts/FontCollection.cs new file mode 100644 index 0000000..7c07c7e --- /dev/null +++ b/SixLabors.Fonts/FontCollection.cs @@ -0,0 +1,303 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using SixLabors.Fonts.Tables; + +namespace SixLabors.Fonts { + /// + /// Represents a collection of font families. + /// + public sealed class FontCollection : IFontCollection, IFontMetricsCollection + { + private readonly HashSet searchDirectories = []; + private readonly HashSet metricsCollection = []; + + /// + /// Initializes a new instance of the class. + /// + public FontCollection() + : this(Array.Empty()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The collection of directories used to search for font families. + /// + /// Use this constructor instead of the parameterless constructor if the fonts added to that collection + /// are actually added after searching inside physical file system directories. The message of the + /// will include the searched directories. + /// + internal FontCollection(IReadOnlyCollection searchDirectories) + { + Guard.NotNull(searchDirectories, nameof(searchDirectories)); + foreach (string? dir in searchDirectories) + { + this.searchDirectories.Add(dir); + } + } + + /// + public IEnumerable Families => this.FamiliesByCultureImpl(CultureInfo.InvariantCulture); + + /// + public FontFamily Add(string path) + => this.Add(path, out _); + + /// + public FontFamily Add(string path, out FontDescription description) + => this.AddImpl(path, CultureInfo.InvariantCulture, out description); + + /// + public FontFamily Add(Stream stream) + => this.Add(stream, out _); + + /// + public FontFamily Add(Stream stream, out FontDescription description) + => this.AddImpl(stream, CultureInfo.InvariantCulture, out description); + + /// + public ReadOnlyMemory AddCollection(string path) + => this.AddCollection(path, out _); + + /// + public ReadOnlyMemory AddCollection(string path, out ReadOnlyMemory descriptions) + => this.AddCollectionImpl(path, CultureInfo.InvariantCulture, out descriptions); + + /// + public ReadOnlyMemory AddCollection(Stream stream) + => this.AddCollection(stream, out _); + + /// + public ReadOnlyMemory AddCollection(Stream stream, out ReadOnlyMemory descriptions) + => this.AddCollectionImpl(stream, CultureInfo.InvariantCulture, out descriptions); + + /// + public FontFamily Get(string name) + => this.GetByCulture(name, CultureInfo.InvariantCulture); + + /// + public bool TryGet(string name, out FontFamily family) + => this.TryGetByCulture(name, CultureInfo.InvariantCulture, out family); + + /// + public FontFamily AddWithCulture(string path, CultureInfo culture) + => this.AddImpl(path, culture, out _); + + /// + public FontFamily AddWithCulture(string path, CultureInfo culture, out FontDescription description) + => this.AddImpl(path, culture, out description); + + /// + public FontFamily AddWithCulture(Stream stream, CultureInfo culture) + => this.AddImpl(stream, culture, out _); + + /// + public FontFamily AddWithCulture(Stream stream, CultureInfo culture, out FontDescription description) + => this.AddImpl(stream, culture, out description); + + /// + public ReadOnlyMemory AddCollection(string path, CultureInfo culture) + => this.AddCollection(path, culture, out _); + + /// + public ReadOnlyMemory AddCollection( + string path, + CultureInfo culture, + out ReadOnlyMemory descriptions) + => this.AddCollectionImpl(path, culture, out descriptions); + + /// + public ReadOnlyMemory AddCollection(Stream stream, CultureInfo culture) + => this.AddCollection(stream, culture, out _); + + /// + public ReadOnlyMemory AddCollection( + Stream stream, + CultureInfo culture, + out ReadOnlyMemory descriptions) + => this.AddCollectionImpl(stream, culture, out descriptions); + + /// + public IEnumerable GetByCulture(CultureInfo culture) + => this.FamiliesByCultureImpl(culture); + + /// + public FontFamily GetByCulture(string name, CultureInfo culture) + => this.GetImpl(name, culture); + + /// + public bool TryGetByCulture(string name, CultureInfo culture, out FontFamily family) + => this.TryGetImpl(name, culture, out family); + + /// + FontFamily IFontMetricsCollection.AddMetrics(FontMetrics metrics, CultureInfo culture) + { + ((IFontMetricsCollection)this).AddMetrics(metrics); + return new FontFamily(metrics.Description.FontFamily(culture), this, culture); + } + + /// + void IFontMetricsCollection.AddMetrics(FontMetrics metrics) + { + Guard.NotNull(metrics, nameof(metrics)); + + if (metrics.Description is null) + { + throw new ArgumentException($"{nameof(FontMetrics)} must have a Description.", nameof(metrics)); + } + + lock (this.metricsCollection) + { + this.metricsCollection.Add(metrics); + } + } + + /// + bool IReadOnlyFontMetricsCollection.TryGetMetrics(string name, CultureInfo culture, FontStyle style, [NotNullWhen(true)] out FontMetrics? metrics) + { + metrics = ((IReadOnlyFontMetricsCollection)this).GetAllMetrics(name, culture) + .FirstOrDefault(x => x.Description.Style == style); + + return metrics != null; + } + + /// + IEnumerable IReadOnlyFontMetricsCollection.GetAllMetrics(string name, CultureInfo culture) + { + Guard.NotNull(name, nameof(name)); + StringComparer comparer = StringComparerHelpers.GetCaseInsensitiveStringComparer(culture); + + return this.metricsCollection + .Where(x => comparer.Equals(x.Description.FontFamily(culture), name)) + .ToArray(); + } + + /// + ReadOnlyMemory IReadOnlyFontMetricsCollection.GetAllStyles(string name, CultureInfo culture) + => ((IReadOnlyFontMetricsCollection)this).GetAllMetrics(name, culture).Select(x => x.Description.Style).ToArray(); + + /// + IEnumerator IReadOnlyFontMetricsCollection.GetEnumerator() + => this.metricsCollection.GetEnumerator(); + + internal void AddSearchDirectories(IEnumerable directories) + { + foreach (string? directory in directories) + { + this.searchDirectories.Add(directory); + } + } + + private FontFamily AddImpl(string path, CultureInfo culture, out FontDescription description) + { + FileFontMetrics instance = new(path); + description = instance.Description; + return ((IFontMetricsCollection)this).AddMetrics(instance, culture); + } + + private FontFamily AddImpl(Stream stream, CultureInfo culture, out FontDescription description) + { + StreamFontMetrics metrics = StreamFontMetrics.LoadFont(stream); + description = metrics.Description; + + return ((IFontMetricsCollection)this).AddMetrics(metrics, culture); + } + + private ReadOnlyMemory AddCollectionImpl( + string path, + CultureInfo culture, + out ReadOnlyMemory descriptions) + { + ReadOnlyMemory fontMetrics = FileFontMetrics.LoadFontCollection(path); + ReadOnlySpan fonts = fontMetrics.Span; + + FontDescription[] description = new FontDescription[fonts.Length]; + FontFamily[] families = new FontFamily[fonts.Length]; + int familyCount = 0; + for (int i = 0; i < fonts.Length; i++) + { + description[i] = fonts[i].Description; + FontFamily family = ((IFontMetricsCollection)this).AddMetrics(fonts[i], culture); + + if (!families.AsSpan(0, familyCount).Contains(family)) + { + families[familyCount++] = family; + } + } + + descriptions = description; + return new ReadOnlyMemory(families, 0, familyCount); + } + + private ReadOnlyMemory AddCollectionImpl( + Stream stream, + CultureInfo culture, + out ReadOnlyMemory descriptions) + { + long startPos = stream.Position; + using BigEndianBinaryReader reader = new(stream, true); + TtcHeader ttcHeader = TtcHeader.Read(reader); + FontDescription[] result = new FontDescription[(int)ttcHeader.NumFonts]; + FontFamily[] installedFamilies = new FontFamily[(int)ttcHeader.NumFonts]; + int familyCount = 0; + for (int i = 0; i < ttcHeader.NumFonts; ++i) + { + stream.Position = startPos + ttcHeader.OffsetTable[i]; + StreamFontMetrics instance = StreamFontMetrics.LoadFont(stream); + FontFamily family = ((IFontMetricsCollection)this).AddMetrics(instance, culture); + result[i] = instance.Description; + + if (!installedFamilies.AsSpan(0, familyCount).Contains(family)) + { + installedFamilies[familyCount++] = family; + } + } + + descriptions = result; + return new ReadOnlyMemory(installedFamilies, 0, familyCount); + } + + private FontFamily[] FamiliesByCultureImpl(CultureInfo culture) + => [.. this.metricsCollection + .Select(x => x.Description.FontFamily(culture)) + .Distinct() + .Select(x => new FontFamily(x, this, culture))]; + + private bool TryGetImpl(string name, CultureInfo culture, out FontFamily family) + { + Guard.NotNull(name, nameof(name)); + StringComparer comparer = StringComparerHelpers.GetCaseInsensitiveStringComparer(culture); + + string? match = this.metricsCollection + .Select(x => x.Description.FontFamily(culture)) + .FirstOrDefault(x => comparer.Equals(name, x)); + + if (match != null) + { + family = new FontFamily(match, this, culture); + return true; + } + + family = default; + return false; + } + + private FontFamily GetImpl(string name, CultureInfo culture) + { + if (this.TryGetImpl(name, culture, out FontFamily family)) + { + return family; + } + + throw new FontFamilyNotFoundException(name, this.searchDirectories); + } + } +} diff --git a/SixLabors.Fonts/FontCollectionExtensions.cs b/SixLabors.Fonts/FontCollectionExtensions.cs new file mode 100644 index 0000000..b6f36ed --- /dev/null +++ b/SixLabors.Fonts/FontCollectionExtensions.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts { + /// + /// Extension methods for . + /// + public static class FontCollectionExtensions + { + /// + /// Adds the fonts from the collection to this . + /// + /// The font collection. + /// The containing the system fonts. + public static FontCollection AddSystemFonts(this FontCollection collection) + { + // This cast is safe because our underlying SystemFontCollection implements + // both interfaces separately. + foreach (FontMetrics metric in (IReadOnlyFontMetricsCollection)SystemFonts.Collection) + { + ((IFontMetricsCollection)collection).AddMetrics(metric); + } + + collection.AddSearchDirectories(SystemFonts.Collection.SearchDirectories); + + return collection; + } + + /// + /// Adds the fonts from the collection to this . + /// + /// The font collection. + /// The delegate that defines the conditions of to add into the font collection. + /// The containing the system fonts. + public static FontCollection AddSystemFonts(this FontCollection collection, Predicate match) + { + bool isMatch = false; + foreach (FontMetrics metric in (IReadOnlyFontMetricsCollection)SystemFonts.Collection) + { + bool currentMatch = match(metric); + isMatch |= currentMatch; + if (currentMatch) + { + ((IFontMetricsCollection)collection).AddMetrics(metric); + } + } + + if (isMatch) + { + collection.AddSearchDirectories(SystemFonts.Collection.SearchDirectories); + } + + return collection; + } + } +} diff --git a/SixLabors.Fonts/FontDescription.cs b/SixLabors.Fonts/FontDescription.cs new file mode 100644 index 0000000..21e089f --- /dev/null +++ b/SixLabors.Fonts/FontDescription.cs @@ -0,0 +1,202 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Globalization; +using System.IO; +using SixLabors.Fonts.Tables; +using SixLabors.Fonts.Tables.General; +using SixLabors.Fonts.Tables.General.Name; +using SixLabors.Fonts.WellKnownIds; + +namespace SixLabors.Fonts { + /// + /// Provides basic descriptive metadata for the font. + /// + public class FontDescription + { + private readonly NameTable nameTable; + + /// + /// Initializes a new instance of the class. + /// + /// The name table. + /// The os2 table. + /// The head table. + internal FontDescription(NameTable nameTable, OS2Table? os2, HeadTable? head) + { + this.nameTable = nameTable; + this.Style = ConvertStyle(os2, head); + + this.FontNameInvariantCulture = this.FontName(CultureInfo.InvariantCulture); + this.FontFamilyInvariantCulture = this.FontFamily(CultureInfo.InvariantCulture); + this.FontSubFamilyNameInvariantCulture = this.FontSubFamilyName(CultureInfo.InvariantCulture); + } + + /// + /// Gets the style. + /// + public FontStyle Style { get; } + + /// + /// Gets the name of the font in the invariant culture. + /// + public string FontNameInvariantCulture { get; } + + /// + /// Gets the name of the font family in the invariant culture. + /// + public string FontFamilyInvariantCulture { get; } + + /// + /// Gets the font sub family in the invariant culture. + /// + public string FontSubFamilyNameInvariantCulture { get; } + + /// + /// Gets the name of the font. + /// + /// The culture to load metadata in. + /// The font name. + public string FontName(CultureInfo culture) => this.nameTable.FontName(culture); + + /// + /// Gets the name of the font family. + /// + /// The culture to load metadata in. + /// The font family name. + public string FontFamily(CultureInfo culture) => this.nameTable.FontFamilyName(culture); + + /// + /// Gets the font sub family. + /// + /// The culture to load metadata in. + /// The font sub family name. + public string FontSubFamilyName(CultureInfo culture) => this.nameTable.FontSubFamilyName(culture); + + /// + /// Gets the name matching the given culture and id. + /// If is passed this method will return the first name matching the id. + /// + /// The culture to load metadata in. + /// The name id to match. + /// The name. + public string GetNameById(CultureInfo culture, KnownNameIds nameId) => this.nameTable.GetNameById(culture, nameId); + + /// + /// Reads a from the specified stream. + /// + /// The file path. + /// a . + public static FontDescription LoadDescription(string path) + { + Guard.NotNullOrWhiteSpace(path, nameof(path)); + + using FileStream fs = File.OpenRead(path); + using var reader = new FontReader(fs); + return LoadDescription(reader); + } + + /// + /// Reads a from the specified stream. + /// + /// The stream. + /// a . + public static FontDescription LoadDescription(Stream stream) + { + Guard.NotNull(stream, nameof(stream)); + + // Only read the name tables. + using var reader = new FontReader(stream); + + return LoadDescription(reader); + } + + /// + /// Reads a from the specified stream. + /// + /// The reader. + /// + /// a . + /// + internal static FontDescription LoadDescription(FontReader reader) + { + DebugGuard.NotNull(reader, nameof(reader)); + + // NOTE: These fields are read in their optimized order + // https://docs.microsoft.com/en-gb/typography/opentype/spec/recom#optimized-table-ordering + HeadTable? head = reader.TryGetTable(); + OS2Table? os2 = reader.TryGetTable(); + NameTable nameTable = reader.GetTable(); + + return new FontDescription(nameTable, os2, head); + } + + /// + /// Reads all the s from the file at the specified path (typically a .ttc file like simsun.ttc). + /// + /// The file path. + /// A read-only memory region containing the font descriptions. + public static ReadOnlyMemory LoadFontCollectionDescriptions(string path) + { + Guard.NotNullOrWhiteSpace(path, nameof(path)); + + using FileStream fs = File.OpenRead(path); + return LoadFontCollectionDescriptions(fs); + } + + /// + /// Reads all the s from the specified stream (typically a .ttc file like simsun.ttc). + /// + /// The stream to read the font collection from. + /// A read-only memory region containing the font descriptions. + public static ReadOnlyMemory LoadFontCollectionDescriptions(Stream stream) + { + long startPos = stream.Position; + using var reader = new BigEndianBinaryReader(stream, true); + var ttcHeader = TtcHeader.Read(reader); + + var result = new FontDescription[(int)ttcHeader.NumFonts]; + for (int i = 0; i < ttcHeader.NumFonts; ++i) + { + stream.Position = startPos + ttcHeader.OffsetTable[i]; + using var fontReader = new FontReader(stream); + result[i] = LoadDescription(fontReader); + } + + return result; + } + + private static FontStyle ConvertStyle(OS2Table? os2, HeadTable? head) + { + FontStyle style = FontStyle.Regular; + + if (os2 != null) + { + if ((os2.FontStyle & OS2Table.FontStyleSelection.BOLD) == OS2Table.FontStyleSelection.BOLD) + { + style |= FontStyle.Bold; + } + + if ((os2.FontStyle & OS2Table.FontStyleSelection.ITALIC) == OS2Table.FontStyleSelection.ITALIC) + { + style |= FontStyle.Italic; + } + } + else if (head != null) + { + if ((head.MacStyle & HeadTable.HeadMacStyle.Bold) == HeadTable.HeadMacStyle.Bold) + { + style |= FontStyle.Bold; + } + + if ((head.MacStyle & HeadTable.HeadMacStyle.Italic) == HeadTable.HeadMacStyle.Italic) + { + style |= FontStyle.Italic; + } + } + + return style; + } + } +} diff --git a/SixLabors.Fonts/FontFamily.cs b/SixLabors.Fonts/FontFamily.cs new file mode 100644 index 0000000..27d0244 --- /dev/null +++ b/SixLabors.Fonts/FontFamily.cs @@ -0,0 +1,231 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace SixLabors.Fonts { + /// + /// Defines a group of type faces having a similar basic design and certain + /// variations in styles. + /// + public struct FontFamily : IEquatable + { + private readonly IReadOnlyFontMetricsCollection collection; + + /// + /// Initializes a new instance of the struct. + /// + /// The name. + /// The collection. + /// The culture the family was extracted against + internal FontFamily(string name, IReadOnlyFontMetricsCollection collection, CultureInfo culture) + { + Guard.NotNull(collection, nameof(collection)); + + this.collection = collection; + this.Name = name; + this.Culture = culture; + } + + /// + /// Gets the name. + /// + public string Name { get; } + + /// + /// Gets the culture this instance was extracted against. + /// + public CultureInfo Culture { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// if the current left is equal to the + /// parameter; otherwise, . + /// + public static bool operator ==(FontFamily left, FontFamily right) + => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// if the current left is unequal to the + /// parameter; otherwise, . + /// + public static bool operator !=(FontFamily left, FontFamily right) + => !(left == right); + + /// + /// Create a new instance of the for the named font family with regular styling. + /// + /// The size of the font in PT units. + /// The new . + public readonly Font CreateFont(float size) + { + if (this == default) + { + FontsThrowHelper.ThrowDefaultInstance(); + } + + return new Font(this, size); + } + + /// + /// Create a new instance of the for the named font family. + /// + /// The size of the font in PT units. + /// The font style. + /// The new . + public readonly Font CreateFont(float size, FontStyle style) + { + if (this == default) + { + FontsThrowHelper.ThrowDefaultInstance(); + } + + return new Font(this, size, style); + } + + /// + /// Create a new instance of the for the named font family with regular styling + /// and the specified variation axis settings. + /// + /// The size of the font in PT units. + /// The variation axis settings to apply. + /// The new . + public readonly Font CreateFont(float size, params FontVariation[] variations) + { + if (this == default) + { + FontsThrowHelper.ThrowDefaultInstance(); + } + + Font baseFont = new(this, size); + return variations.Length > 0 ? new Font(baseFont, variations) : baseFont; + } + + /// + /// Create a new instance of the for the named font family with the specified + /// style and variation axis settings. + /// + /// The size of the font in PT units. + /// The font style. + /// The variation axis settings to apply. + /// The new . + public readonly Font CreateFont(float size, FontStyle style, params FontVariation[] variations) + { + if (this == default) + { + FontsThrowHelper.ThrowDefaultInstance(); + } + + Font baseFont = new(this, size, style); + return variations.Length > 0 ? new Font(baseFont, variations) : baseFont; + } + + /// + /// Gets the collection of that are currently available. + /// + /// A read-only memory region containing the available font styles. + public readonly ReadOnlyMemory GetAvailableStyles() + { + if (this == default) + { + FontsThrowHelper.ThrowDefaultInstance(); + } + + return this.collection.GetAllStyles(this.Name, this.Culture); + } + + /// + /// Gets the collection of filesystem paths to the font family sources. + /// + /// + /// When this method returns, contains the filesystem paths to the font family sources, + /// if the path exists; otherwise, an empty memory region. + /// This parameter is passed uninitialized. + /// + /// + /// if the was created via filesystem paths; otherwise, . + /// + public bool TryGetPaths(out ReadOnlyMemory paths) + { + if (this == default) + { + FontsThrowHelper.ThrowDefaultInstance(); + } + + ReadOnlySpan styles = this.GetAvailableStyles().Span; + string[]? filePaths = null; + int pathCount = 0; + + foreach (FontStyle style in styles) + { + if (this.collection.TryGetMetrics(this.Name, this.Culture, style, out FontMetrics? metrics) + && metrics is FileFontMetrics fileMetrics) + { + filePaths ??= new string[styles.Length]; + filePaths[pathCount++] = fileMetrics.Path; + } + } + + paths = pathCount > 0 + ? new ReadOnlyMemory(filePaths!, 0, pathCount) + : ReadOnlyMemory.Empty; + + return !paths.IsEmpty; + } + + /// + /// Gets the specified font metrics matching the given font style. + /// + /// The font style to use when searching for a match. + /// + /// When this method returns, contains the metrics associated with the specified name, + /// if the name is found; otherwise, the default value for the type of the metrics parameter. + /// This parameter is passed uninitialized. + /// + /// + /// if the contains font metrics + /// with the specified name; otherwise, . + /// + public readonly bool TryGetMetrics(FontStyle style, [NotNullWhen(true)] out FontMetrics? metrics) + { + if (this == default) + { + FontsThrowHelper.ThrowDefaultInstance(); + } + + return this.collection.TryGetMetrics(this.Name, this.Culture, style, out metrics); + } + + /// + public override bool Equals(object? obj) + => obj is FontFamily family && this.Equals(family); + + /// + public readonly bool Equals(FontFamily other) + { + StringComparer comparer = StringComparerHelpers.GetCaseInsensitiveStringComparer(this.Culture); + return comparer.Equals(this.Name, other.Name) + && EqualityComparer.Default.Equals(this.Culture, other.Culture) + && EqualityComparer.Default.Equals(this.collection, other.collection); + } + + /// + public override readonly int GetHashCode() + => HashCode.Combine(this.collection, this.Name, this.Culture); + + /// + public override readonly string ToString() => this.Name; + } +} diff --git a/SixLabors.Fonts/FontGlyphMetrics.cs b/SixLabors.Fonts/FontGlyphMetrics.cs new file mode 100644 index 0000000..9712ab1 --- /dev/null +++ b/SixLabors.Fonts/FontGlyphMetrics.cs @@ -0,0 +1,535 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Tables.General; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// Represents a glyph metric from a particular font face. + /// + public abstract class FontGlyphMetrics + { + private static readonly Vector2 YInverter = new(1, -1); + + internal FontGlyphMetrics( + StreamFontMetrics font, + ushort glyphId, + CodePoint codePoint, + Bounds bounds, + ushort advanceWidth, + ushort advanceHeight, + short leftSideBearing, + short topSideBearing, + ushort unitsPerEM, + TextAttributes textAttributes, + TextDecorations textDecorations, + GlyphType glyphType) + { + this.FontMetrics = font; + this.GlyphId = glyphId; + this.CodePoint = codePoint; + this.Bounds = bounds; + this.Width = bounds.Max.X - bounds.Min.X; + this.Height = bounds.Max.Y - bounds.Min.Y; + this.UnitsPerEm = unitsPerEM; + this.AdvanceWidth = advanceWidth; + this.AdvanceHeight = advanceHeight; + this.LeftSideBearing = leftSideBearing; + this.RightSideBearing = (short)(this.AdvanceWidth - this.LeftSideBearing - this.Width); + this.TopSideBearing = topSideBearing; + this.BottomSideBearing = (short)(this.AdvanceHeight - this.TopSideBearing - this.Height); + this.TextAttributes = textAttributes; + this.TextDecorations = textDecorations; + this.GlyphType = glyphType; + + Vector2 offset = Vector2.Zero; + Vector2 scaleFactor = new(unitsPerEM * 72F); + + if ((textAttributes & TextAttributes.Subscript) == TextAttributes.Subscript) + { + float units = this.UnitsPerEm; + scaleFactor /= new Vector2(font.SubscriptXSize / units, font.SubscriptYSize / units); + offset = new(font.SubscriptXOffset, font.SubscriptYOffset < 0 ? font.SubscriptYOffset : -font.SubscriptYOffset); + } + else if ((textAttributes & TextAttributes.Superscript) == TextAttributes.Superscript) + { + float units = this.UnitsPerEm; + scaleFactor /= new Vector2(font.SuperscriptXSize / units, font.SuperscriptYSize / units); + offset = new(font.SuperscriptXOffset, font.SuperscriptYOffset < 0 ? -font.SuperscriptYOffset : font.SuperscriptYOffset); + } + + this.ScaleFactor = scaleFactor; + this.Offset = offset; + } + + internal FontGlyphMetrics( + StreamFontMetrics font, + ushort glyphId, + CodePoint codePoint, + Bounds bounds, + ushort advanceWidth, + ushort advanceHeight, + short leftSideBearing, + short topSideBearing, + ushort unitsPerEM, + Vector2 offset, + Vector2 scaleFactor, + TextRun textRun, + GlyphType glyphType) + { + // This is used during cloning. Ensure anything that could be changed is copied. + this.FontMetrics = font; + this.GlyphId = glyphId; + this.CodePoint = codePoint; + this.Bounds = new Bounds(bounds.Min, bounds.Max); + this.Width = bounds.Max.X - bounds.Min.X; + this.Height = bounds.Max.Y - bounds.Min.Y; + this.UnitsPerEm = unitsPerEM; + this.AdvanceWidth = advanceWidth; + this.AdvanceHeight = advanceHeight; + this.LeftSideBearing = leftSideBearing; + this.RightSideBearing = (short)(this.AdvanceWidth - this.LeftSideBearing - this.Width); + this.TopSideBearing = topSideBearing; + this.BottomSideBearing = (short)(this.AdvanceHeight - this.TopSideBearing - this.Height); + this.TextAttributes = textRun.TextAttributes; + this.TextDecorations = textRun.TextDecorations; + this.GlyphType = glyphType; + this.ScaleFactor = scaleFactor; + this.Offset = offset; + this.TextRun = textRun; + } + + /// + /// Gets the font metrics. + /// + internal StreamFontMetrics FontMetrics { get; } + + /// + /// Gets the Unicode codepoint of the glyph. + /// + public CodePoint CodePoint { get; } + + /// + /// Gets the advance width for horizontal layout, expressed in font units. + /// + public ushort AdvanceWidth { get; private set; } + + /// + /// Gets the advance height for vertical layout, expressed in font units. + /// + public ushort AdvanceHeight { get; private set; } + + /// + /// Gets the left side bearing for horizontal layout, expressed in font units. + /// + public short LeftSideBearing { get; } + + /// + /// Gets the right side bearing for horizontal layout, expressed in font units. + /// + public short RightSideBearing { get; } + + /// + /// Gets the top side bearing for vertical layout, expressed in font units. + /// + public short TopSideBearing { get; } + + /// + /// Gets the bottom side bearing for vertical layout, expressed in font units. + /// + public short BottomSideBearing { get; } + + /// + /// Gets the bounds, expressed in font units. + /// + internal Bounds Bounds { get; } + + /// + /// Gets the width, expressed in font units. + /// + public float Width { get; } + + /// + /// Gets the height, expressed in font units. + /// + public float Height { get; } + + /// + /// Gets the glyph type. + /// + public GlyphType GlyphType { get; } + + /// + public ushort UnitsPerEm { get; } + + /// + /// Gets the id of the glyph within the font tables. + /// + public ushort GlyphId { get; } + + /// + /// Gets the scale factor that is applied to all glyphs in this face. + /// Normally calculated as 72 * so that 1pt = 1px + /// unless the glyph has that apply scaling adjustment. + /// + public Vector2 ScaleFactor { get; } + + /// + /// Gets or sets the offset in font design units. + /// + internal Vector2 Offset { get; set; } + + /// + /// Gets the text run that the glyph belongs to. + /// + internal TextRun TextRun { get; } = null!; + + /// + /// Gets the text attributes applied to the glyph. + /// + public TextAttributes TextAttributes { get; } + + /// + /// Gets the text decorations applied to the glyph. + /// + public TextDecorations TextDecorations { get; } + + /// + /// Performs a semi-deep clone (FontMetrics are not cloned) for rendering + /// This allows caching the original in the font metrics. + /// + /// The current text run this glyph belongs to. + /// The new . + internal abstract FontGlyphMetrics CloneForRendering(TextRun textRun); + + /// + /// Apply an offset to the glyph. + /// + /// The x-offset. + /// The y-offset. + internal void ApplyOffset(short x, short y) + => this.Offset = Vector2.Transform(this.Offset, Matrix3x2.CreateTranslation(x, y)); + + /// + /// Applies an advance to the glyph. + /// + /// The x-advance. + /// The y-advance. + internal void ApplyAdvance(short x, short y) + { + this.AdvanceWidth = (ushort)(this.AdvanceWidth + x); + + // AdvanceHeight values grow downward but font-space grows upward, hence negation + this.AdvanceHeight = (ushort)(this.AdvanceHeight - y); + } + + /// + /// Sets a new advance width. + /// + /// The x-advance. + internal void SetAdvanceWidth(ushort x) => this.AdvanceWidth = x; + + /// + /// Sets a new advance height. + /// + /// The y-advance. + internal void SetAdvanceHeight(ushort y) => this.AdvanceHeight = y; + + /// + /// Calculates the glyph bounding box in device-space (Y-down) coordinates, + /// given the layout mode, render origin, and scaled point size. + /// + /// + /// Steps: + /// 1) Select glyph bounds (or synthesize from advances if empty). + /// 2) Apply rotation if the layout mode is vertical-rotated. + /// 3) Convert from Y-up to Y-down coordinates. + /// 4) Scale and translate to device space using the specified origin. + /// + /// The glyph layout mode (horizontal, vertical, or vertical rotated). + /// The render-space origin in pixels. + /// The scaled point size, mapped to pixels by the caller. + /// + /// A representing the glyph bounds in device space. + /// + internal FontRectangle GetBoundingBox(GlyphLayoutMode mode, Vector2 origin, float scaledPointSize) + { + Vector2 scale = new(scaledPointSize / this.ScaleFactor.X, scaledPointSize / this.ScaleFactor.Y); + Bounds b = this.Bounds; + + // 1) Substitute fallback bounds if the glyph has no outline. + if (b.Equals(Bounds.Empty)) + { + if (mode == GlyphLayoutMode.Vertical) + { + // For vertical layout, set Y-up min = -AdvanceHeight to 0 so Y-down is 0..+AdvanceHeight. + b = new Bounds(0f, -this.AdvanceHeight, 0f, 0f); + } + else + { + // For horizontal layout, just use advance width. + b = new Bounds(0f, 0f, this.AdvanceWidth, 0f); + } + } + + // 2) Rotate for vertical rotated layout. + Vector2 offsetUp = this.Offset; + if (mode == GlyphLayoutMode.VerticalRotated) + { + Matrix3x2 rot = Matrix3x2.CreateRotation(-MathF.PI / 2F); + b = Bounds.Transform(in b, rot); + offsetUp = Vector2.Transform(offsetUp, rot); + } + + // 3) Flip Y to convert to device-space (Y-down). + Vector2 minDown = b.Min * YInverter; + Vector2 maxDown = b.Max * YInverter; + Vector2 offsetDown = offsetUp * YInverter; + + // Normalize bounds after flipping. + float minX = MathF.Min(minDown.X, maxDown.X); + float maxX = MathF.Max(minDown.X, maxDown.X); + float minY = MathF.Min(minDown.Y, maxDown.Y); + float maxY = MathF.Max(minDown.Y, maxDown.Y); + + // 4) Apply scaling and origin translation. + Vector2 size = new(maxX - minX, maxY - minY); + size *= scale; + Vector2 location = origin + ((new Vector2(minX, minY) + offsetDown) * scale); + + return new FontRectangle(location.X, location.Y, size.X, size.Y); + } + + /// + /// Renders the glyph to the render surface in font units relative to a bottom left origin at (0,0) + /// + /// The surface renderer. + /// The index of the grapheme this glyph is part of. + /// The origin used to render the glyph outline. + /// The origin used to render text decorations. + /// The glyph layout mode to render using. + /// The options used to influence the rendering of this glyph. + internal abstract void RenderTo( + IGlyphRenderer renderer, + int graphemeIndex, + Vector2 glyphOrigin, + Vector2 decorationOrigin, + GlyphLayoutMode mode, + TextOptions options); + + /// + /// Renders text decorations, such as underline, strikeout, and overline, for the current glyph to the specified + /// glyph renderer at the given location and layout mode. + /// + /// When rendering in vertical layout modes, decoration positions are synthesized to match common + /// typographic conventions. The renderer may override which decorations are enabled. Overline thickness is derived + /// from underline metrics if not explicitly specified. + /// The glyph renderer that receives the decoration drawing commands. + /// The position, in device-independent coordinates, where the decorations should be rendered relative to the glyph. + /// The layout mode that determines the orientation and positioning of the decorations (e.g., horizontal, vertical, + /// or vertical rotated). + /// The transformation matrix applied to the decoration coordinates before rendering. + /// The scaled pixels-per-em value used to adjust decoration size and positioning for the current rendering context. + /// Additional text rendering options that may influence decoration appearance or behavior. + protected void RenderDecorationsTo( + IGlyphRenderer renderer, + Vector2 location, + GlyphLayoutMode mode, + Matrix3x2 transform, + float scaledPPEM, + TextOptions options) + { + bool perGlyph = options.DecorationPositioningMode == DecorationPositioningMode.GlyphFont; + FontMetrics fontMetrics = perGlyph + ? this.FontMetrics + : options.Font.FontMetrics; + + // The scale factor for the decoration length is treated separately from other factors + // as it is used to scale the length of the decoration line. + // This must always be derived from the glyph's own scale factor to ensure correct length. + Vector2 lengthScaleFactor = this.ScaleFactor; + + // These factors determine horizontal and vertical scaling and offset for the decorations. + // and are either per-glyph or derived from the common font metrics. + Vector2 scaleFactor; + Vector2 offset; + if (perGlyph) + { + // Use the pre-calculated values from this glyph. + scaleFactor = this.ScaleFactor; + offset = this.Offset; + } + else + { + // To ensure that we share the scaling when sharing font metrics we need to + // recalculate the offset and scale factor here using the common font metrics. + scaleFactor = new(fontMetrics.UnitsPerEm * 72F); + offset = Vector2.Zero; + if ((this.TextAttributes & TextAttributes.Subscript) == TextAttributes.Subscript) + { + float units = this.UnitsPerEm; + scaleFactor /= new Vector2(fontMetrics.SubscriptXSize / units, fontMetrics.SubscriptYSize / units); + offset = new(fontMetrics.SubscriptXOffset, fontMetrics.SubscriptYOffset < 0 ? fontMetrics.SubscriptYOffset : -fontMetrics.SubscriptYOffset); + } + else if ((this.TextAttributes & TextAttributes.Superscript) == TextAttributes.Superscript) + { + float units = this.UnitsPerEm; + scaleFactor /= new Vector2(fontMetrics.SuperscriptXSize / units, fontMetrics.SuperscriptYSize / units); + offset = new(fontMetrics.SuperscriptXOffset, fontMetrics.SuperscriptYOffset < 0 ? -fontMetrics.SuperscriptYOffset : fontMetrics.SuperscriptYOffset); + } + } + + bool isVerticalLayout = mode is GlyphLayoutMode.Vertical or GlyphLayoutMode.VerticalRotated; + (Vector2 Start, Vector2 End, float Thickness) GetEnds(TextDecorations decorations, float thickness, float decoratorPosition) + { + // For vertical layout we need to draw a vertical line. + if (isVerticalLayout) + { + float length = mode == GlyphLayoutMode.VerticalRotated ? this.AdvanceWidth : this.AdvanceHeight; + if (length == 0) + { + return (Vector2.Zero, Vector2.Zero, 0); + } + + Vector2 lengthScale = new Vector2(scaledPPEM) / lengthScaleFactor; + Vector2 scale = new Vector2(scaledPPEM) / scaleFactor; + + // Undo the vertical offset applied when laying out the text. + Vector2 scaledOffset = (offset + new Vector2(decoratorPosition, 0)) * scale; + + length *= lengthScale.Y; + thickness *= scale.X; + + Vector2 tl = new(scaledOffset.X, scaledOffset.Y); + Vector2 tr = new(scaledOffset.X + thickness, scaledOffset.Y); + Vector2 bl = new(scaledOffset.X, scaledOffset.Y + length); + + thickness = tr.X - tl.X; + + // Horizontally offset the line to the correct horizontal position + // based upon which side drawing occurs of the line. + float m = decorations switch + { + TextDecorations.Strikeout => .5F, + TextDecorations.Overline => 3, + _ => 1, + }; + + // Account for any future pixel clamping. + scaledOffset = new Vector2(thickness * m, 0) + location; + tl += scaledOffset; + bl += scaledOffset; + + return (tl, bl, thickness); + } + else + { + float length = this.AdvanceWidth; + if (length == 0) + { + return (Vector2.Zero, Vector2.Zero, 0); + } + + Vector2 lengthScale = new Vector2(scaledPPEM) / lengthScaleFactor; + Vector2 scale = new Vector2(scaledPPEM) / scaleFactor; + Vector2 scaledOffset = (offset + new Vector2(0, decoratorPosition)) * scale; + + length *= lengthScale.X; + thickness *= scale.Y; + + Vector2 tl = new(scaledOffset.X, scaledOffset.Y); + Vector2 tr = new(scaledOffset.X + length, scaledOffset.Y); + Vector2 bl = new(scaledOffset.X, scaledOffset.Y + thickness); + + thickness = bl.Y - tl.Y; + tl = (Vector2.Transform(tl, transform) * YInverter) + location; + tr = (Vector2.Transform(tr, transform) * YInverter) + location; + + return (tl, tr, thickness); + } + } + + void SetDecoration(TextDecorations decorations, float thickness, float position) + { + (Vector2 start, Vector2 end, float calcThickness) = GetEnds(decorations, thickness, position); + if (calcThickness != 0) + { + renderer.SetDecoration(decorations, start, end, calcThickness); + } + } + + // Allow the renderer to override the decorations to attach. + // When rendering glyphs vertically we use synthesized positions based upon comparisons with Pango/browsers. + // We deviate from browsers in a few ways: + // - When rendering rotated glyphs and use the default values because it fits the glyphs better. + // - We include the adjusted scale for subscript and superscript glyphs. + // - We make no attempt to adjust the underline position along a text line to render at the same position. + TextDecorations decorations = renderer.EnabledDecorations(); + bool synthesized = mode == GlyphLayoutMode.Vertical; + if ((decorations & TextDecorations.Underline) == TextDecorations.Underline) + { + SetDecoration(TextDecorations.Underline, fontMetrics.UnderlineThickness, synthesized ? Math.Abs(fontMetrics.UnderlinePosition) : fontMetrics.UnderlinePosition); + } + + if ((decorations & TextDecorations.Strikeout) == TextDecorations.Strikeout) + { + SetDecoration(TextDecorations.Strikeout, fontMetrics.StrikeoutSize, synthesized ? fontMetrics.UnitsPerEm * .5F : fontMetrics.StrikeoutPosition); + } + + if ((decorations & TextDecorations.Overline) == TextDecorations.Overline) + { + // There's no built in metrics for overline thickness so use underline. + SetDecoration(TextDecorations.Overline, fontMetrics.UnderlineThickness, fontMetrics.UnitsPerEm - fontMetrics.UnderlinePosition); + } + } + + /// + /// Gets a value indicating whether the specified code point should be skipped when rendering. + /// + /// The code point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected internal static bool ShouldSkipGlyphRendering(CodePoint codePoint) + => UnicodeUtility.ShouldNotBeRendered(codePoint); + + /// + /// Returns the size to render/measure the glyph based on the given size and resolution in px units. + /// + /// The font size in pt units. + /// The DPI (Dots Per Inch) to render/measure the glyph at + /// The . + internal float GetScaledSize(float pointSize, float dpi) + { + float scaledPPEM = dpi * pointSize; + bool forcePPEMToInt = (this.FontMetrics.HeadFlags & HeadTable.HeadFlags.ForcePPEMToInt) != 0; + + if (forcePPEMToInt) + { + scaledPPEM = MathF.Round(scaledPPEM); + } + + return scaledPPEM; + } + + /// + /// Gets the rotation matrix for the glyph based on the layout mode. + /// + /// The glyph layout mode. + /// The. + internal static Matrix3x2 GetRotationMatrix(GlyphLayoutMode mode) + { + if (mode == GlyphLayoutMode.VerticalRotated) + { + // Rotate 90 degrees clockwise. + return Matrix3x2.CreateRotation(-MathF.PI / 2F); + } + + return Matrix3x2.Identity; + } + } +} diff --git a/SixLabors.Fonts/FontMetrics.cs b/SixLabors.Fonts/FontMetrics.cs new file mode 100644 index 0000000..7b0c888 --- /dev/null +++ b/SixLabors.Fonts/FontMetrics.cs @@ -0,0 +1,299 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// Represents a font face with metrics, which is a set of glyphs with a specific style (regular, italic, bold etc). + /// + public abstract class FontMetrics + { + internal FontMetrics() + { + } + + /// + /// Gets the basic description of the face. + /// + public abstract FontDescription Description { get; } + + /// + /// Gets the number of font units per EM square for this face. + /// + public abstract ushort UnitsPerEm { get; } + + /// + /// Gets the scale factor that is applied to all glyphs in this face. + /// Calculated as 72 * so that 1pt = 1px. + /// + public abstract float ScaleFactor { get; } + + /// + /// Gets the metrics specific to horizontal text. + /// + public abstract HorizontalMetrics HorizontalMetrics { get; } + + /// + /// Gets the metrics specific to vertical text. + /// + public abstract VerticalMetrics VerticalMetrics { get; } + + /// + /// Gets the recommended horizontal size in font design units for subscripts for this font. + /// + public abstract short SubscriptXSize { get; } + + /// + /// Gets the recommended vertical size in font design units for subscripts for this font. + /// + public abstract short SubscriptYSize { get; } + + /// + /// Gets the recommended horizontal offset in font design units for subscripts for this font. + /// + public abstract short SubscriptXOffset { get; } + + /// + /// Gets the recommended vertical offset in font design units for subscripts for this font. + /// + public abstract short SubscriptYOffset { get; } + + /// + /// Gets the recommended horizontal size in font design units for superscripts for this font. + /// + public abstract short SuperscriptXSize { get; } + + /// + /// Gets the recommended vertical size in font design units for superscripts for this font. + /// + public abstract short SuperscriptYSize { get; } + + /// + /// Gets the recommended horizontal offset in font design units for superscripts for this font. + /// + public abstract short SuperscriptXOffset { get; } + + /// + /// Gets the recommended vertical offset in font design units for superscripts for this font. + /// + public abstract short SuperscriptYOffset { get; } + + /// + /// Gets thickness of the strikeout stroke in font design units. + /// + public abstract short StrikeoutSize { get; } + + /// + /// Gets the position of the top of the strikeout stroke relative to the baseline in font design units. + /// + public abstract short StrikeoutPosition { get; } + + /// + /// Gets the suggested distance of the top of the underline from the baseline (negative values indicate below baseline). + /// + public abstract short UnderlinePosition { get; } + + /// + /// Gets the suggested values for the underline thickness. In general, the underline thickness should match the thickness of + /// the underscore character (U+005F LOW LINE), and should also match the strikeout thickness, which is specified in the OS/2 table. + /// + public abstract short UnderlineThickness { get; } + + /// + /// Gets the italic angle in counter-clockwise degrees from the vertical. Zero for upright text, negative for text that leans to the right (forward). + /// + public abstract float ItalicAngle { get; } + + /// + /// Gets the specified glyph id matching the codepoint. + /// + /// The codepoint. + /// + /// When this method returns, contains the glyph id associated with the specified codepoint, + /// if the codepoint is found; otherwise, 0. + /// This parameter is passed uninitialized. + /// + /// + /// if the face contains a glyph for the specified codepoint; otherwise, . + /// + internal abstract bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId); + + /// + /// Gets the specified glyph id matching the codepoint pair. + /// + /// The codepoint. + /// The next codepoint. Can be null. + /// + /// When this method returns, contains the glyph id associated with the specified codepoint, + /// if the codepoint is found; otherwise, 0. + /// This parameter is passed uninitialized. + /// + /// + /// When this method return, contains a value indicating whether the next codepoint should be skipped. + /// + /// + /// if the face contains a glyph for the specified codepoint; otherwise, . + /// + internal abstract bool TryGetGlyphId(CodePoint codePoint, CodePoint? nextCodePoint, out ushort glyphId, out bool skipNextCodePoint); + + /// + /// Gets the specified glyph id matching the codepoint. + /// + /// The glyph identifier. + /// + /// When this method returns, contains the codepoint associated with the specified glyph id, + /// if the glyph id is found; otherwise, default. + /// + /// + /// if the face contains a codepoint for the specified glyph id; otherwise, . + /// + internal abstract bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint); + + /// + /// Tries to get the glyph class for a given glyph id. + /// The font needs to have a GDEF table defined. + /// + /// The glyph identifier. + /// The glyph class. + /// true, if the glyph class could be retrieved. + internal abstract bool TryGetGlyphClass(ushort glyphId, [NotNullWhen(true)] out GlyphClassDef? glyphClass); + + /// + /// Tries to get the mark attachment class for a given glyph id. + /// The font needs to have a GDEF table defined. + /// + /// The glyph identifier. + /// The mark attachment class. + /// true, if the mark attachment class could be retrieved. + internal abstract bool TryGetMarkAttachmentClass(ushort glyphId, [NotNullWhen(true)] out GlyphClassDef? markAttachmentClass); + + /// + /// Tries to get the variation axes that this font supports. + /// The font needs to have a fvar table. + /// + /// A read-only memory region containing the variation axes. + /// True, if fvar table is present. + public abstract bool TryGetVariationAxes(out ReadOnlyMemory variationAxes); + + /// + /// Returns a value indicating whether the specified glyph is in the given mark filtering set. + /// The font needs to have a GDEF table defined. + /// + /// The mark glyph set index. + /// The glyph identifier. + /// + /// true, if the glyph is in the mark filtering set. + /// + internal abstract bool IsInMarkFilteringSet(ushort markGlyphSetIndex, ushort glyphId); + + /// + /// Gets the glyph metrics for a given code point. + /// + /// The Unicode code point to get the glyph for. + /// The text attributes applied to the glyph. + /// The text decorations applied to the glyph. + /// The layout mode applied to the glyph. + /// Options for enabling color font support during layout and rendering. + /// + /// When this method returns, contains the metrics for the given codepoint and color support if the metrics + /// are found; otherwise the default value. This parameter is passed uninitialized. + /// + /// + /// if the face contains glyph metrics for the specified codepoint; otherwise, . + /// + public abstract bool TryGetGlyphMetrics( + CodePoint codePoint, + TextAttributes textAttributes, + TextDecorations textDecorations, + LayoutMode layoutMode, + ColorFontSupport support, + [NotNullWhen(true)] out FontGlyphMetrics? metrics); + + /// + /// Gets the unicode codepoints for which a glyph exists in the font. + /// + /// A read-only memory region containing the available codepoints. + public abstract ReadOnlyMemory GetAvailableCodePoints(); + + /// + /// Gets the glyph metrics for a given code point and glyph id. + /// + /// The Unicode codepoint. + /// + /// The previously matched or substituted glyph id for the codepoint in the face. + /// If this value equals 0 the default fallback metrics are returned. + /// + /// The text attributes applied to the glyph. + /// The text decorations applied to the glyph. + /// The layout mode applied to the glyph. + /// Options for enabling color font support during layout and rendering. + /// The font glyph metrics. + internal abstract FontGlyphMetrics GetGlyphMetrics( + CodePoint codePoint, + ushort glyphId, + TextAttributes textAttributes, + TextDecorations textDecorations, + LayoutMode layoutMode, + ColorFontSupport colorSupport); + + /// + /// Tries to get the GSUB table. + /// + /// The GSUB table. + /// true, if the glyph class could be retrieved. + internal abstract bool TryGetGSubTable([NotNullWhen(true)] out GSubTable? gSubTable); + + /// + /// Applies any available substitutions to the collection of glyphs. + /// + /// The glyph substitution collection. + internal abstract void ApplySubstitution(GlyphSubstitutionCollection collection); + + /// + /// Gets the amount, in font units, the glyph should be offset if it is followed by + /// the glyph. + /// + /// The current glyph id. + /// The next glyph id. + /// + /// When this method returns, contains the offset, in font units, that should be applied to the + /// glyph, if the offset is found; otherwise the default vector value. + /// This parameter is passed uninitialized. + /// + /// + /// if the face contains and offset for the glyph combination; otherwise, . + /// + internal abstract bool TryGetKerningOffset(ushort currentId, ushort nextId, out Vector2 vector); + + /// + /// Applies any available positioning updates to the collection of glyphs. + /// + /// The glyph positioning collection. + internal abstract void UpdatePositions(GlyphPositioningCollection collection); + + /// + /// Computes a GPOS/GSUB variation delta for the given packed VariationIndex. + /// The delta is computed using the GDEF ItemVariationStore and the current + /// variation coordinates from the GlyphVariationProcessor. + /// + /// + /// The packed VariationIndex: (outerIndex << 16) | innerIndex. + /// A value of 0 returns 0. + /// + /// The delta value in design units, or 0 if no variation data is available. + internal abstract float GetGDefVariationDelta(uint packedVariationIndex); + + /// + /// Gets the normalized variation coordinates for this font instance. + /// Returns an empty span for non-variable fonts or fonts at default coordinates. + /// + /// The normalized coordinates, or an empty span. + internal abstract ReadOnlySpan GetNormalizedCoordinates(); + } +} diff --git a/SixLabors.Fonts/FontReader.cs b/SixLabors.Fonts/FontReader.cs new file mode 100644 index 0000000..1e4f8c2 --- /dev/null +++ b/SixLabors.Fonts/FontReader.cs @@ -0,0 +1,218 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Compression; +using SixLabors.Fonts.Tables; +using SixLabors.Fonts.Tables.Woff; + +namespace SixLabors.Fonts { + internal sealed class FontReader : IDisposable + { + private readonly Stream stream; + private readonly Dictionary loadedTables = new(); + private readonly TableLoader loader; + + private readonly bool isOwnedStream; + private bool isDisposed; + + internal FontReader(Stream stream, TableLoader loader) + { + this.loader = loader; + + Func loadHeader = TableHeader.Read; + + this.stream = stream; + using var reader = new BigEndianBinaryReader(stream, true); + + // we should immediately read the table header to learn which tables we have and what order they are in + uint version = reader.ReadUInt32(); + ushort tableCount; + if (version == 0x774F4646) + { + // This is a woff file. + this.TableFormat = TableFormat.Woff; + + // WOFFHeader + // UInt32 | signature | 0x774F4646 'wOFF' + // UInt32 | flavor | The "sfnt version" of the input font. + // UInt32 | length | Total size of the WOFF file. + // UInt16 | numTables | Number of entries in directory of font tables. + // UInt16 | reserved | Reserved; set to zero. + // UInt32 | totalSfntSize | Total size needed for the uncompressed font data, including the sfnt header, directory, and font tables(including padding). + // UInt16 | majorVersion | Major version of the WOFF file. + // UInt16 | minorVersion | Minor version of the WOFF file. + // UInt32 | metaOffset | Offset to metadata block, from beginning of WOFF file. + // UInt32 | metaLength | Length of compressed metadata block. + // UInt32 | metaOrigLength | Uncompressed size of metadata block. + // UInt32 | privOffset | Offset to private data block, from beginning of WOFF file. + // UInt32 | privLength | Length of private data block. + uint flavor = reader.ReadUInt32(); + this.OutlineType = (OutlineType)flavor; + uint length = reader.ReadUInt32(); + tableCount = reader.ReadUInt16(); + ushort reserved = reader.ReadUInt16(); + uint totalSfntSize = reader.ReadUInt32(); + ushort majorVersion = reader.ReadUInt16(); + ushort minorVersion = reader.ReadUInt16(); + uint metaOffset = reader.ReadUInt32(); + uint metaLength = reader.ReadUInt32(); + uint metaOrigLength = reader.ReadUInt32(); + uint privOffset = reader.ReadUInt32(); + uint privLength = reader.ReadUInt32(); + this.CompressedTableData = true; + loadHeader = WoffTableHeader.Read; + } + else if (version == 0x774F4632) + { + // This is a woff2 file. + this.TableFormat = TableFormat.Woff2; + + uint flavor = reader.ReadUInt32(); + this.OutlineType = (OutlineType)flavor; + uint length = reader.ReadUInt32(); + tableCount = reader.ReadUInt16(); + ushort reserved = reader.ReadUInt16(); + uint totalSfntSize = reader.ReadUInt32(); + uint totalCompressedSize = reader.ReadUInt32(); + ushort majorVersion = reader.ReadUInt16(); + ushort minorVersion = reader.ReadUInt16(); + uint metaOffset = reader.ReadUInt32(); + uint metaLength = reader.ReadUInt32(); + uint metaOrigLength = reader.ReadUInt32(); + uint privOffset = reader.ReadUInt32(); + uint privLength = reader.ReadUInt32(); + this.CompressedTableData = true; + this.Headers = Woff2Utils.ReadWoff2Headers(reader, tableCount); + + this.isOwnedStream = true; + + byte[] compressedBuffer = reader.ReadBytes((int)totalCompressedSize); + var decompressedStream = new MemoryStream(); + using var input = new MemoryStream(compressedBuffer); + using var decompressor = new BrotliStream(input, CompressionMode.Decompress); + decompressor.CopyTo(decompressedStream); + decompressedStream.Position = 0; + this.stream = decompressedStream; + return; + } + else + { + // This is a standard *.otf file (this is named the Offset Table). + this.TableFormat = TableFormat.Otf; + + this.OutlineType = (OutlineType)version; + tableCount = reader.ReadUInt16(); + ushort searchRange = reader.ReadUInt16(); + ushort entrySelector = reader.ReadUInt16(); + ushort rangeShift = reader.ReadUInt16(); + this.CompressedTableData = false; + } + + var headers = new Dictionary(tableCount); + for (int i = 0; i < tableCount; i++) + { + TableHeader tbl = loadHeader(reader); + headers[tbl.Tag] = tbl; + } + + this.Headers = new ReadOnlyDictionary(headers); + } + + public FontReader(Stream stream) + : this(stream, TableLoader.Default) + { + } + + public TableFormat TableFormat { get; } + + public IReadOnlyDictionary Headers { get; } + + public bool CompressedTableData { get; } + + public OutlineType OutlineType { get; } + + public TTableType? TryGetTable() + where TTableType : Table + { + if (this.loadedTables.TryGetValue(typeof(TTableType), out Table? table)) + { + return (TTableType)table; + } + + TTableType? loadedTable = this.loader.Load(this); + if (loadedTable is null) + { + return null; + } + + table = loadedTable; + this.loadedTables.Add(typeof(TTableType), loadedTable); + + return (TTableType)table; + } + + public TTableType GetTable() + where TTableType : Table + { + TTableType? tbl = this.TryGetTable(); + + if (tbl is null) + { + string tag = this.loader.GetTag(); + throw new MissingFontTableException($"Table '{tag}' is missing", tag!); + } + + return tbl; + } + + public TableHeader? GetHeader(string tag) + => this.Headers.TryGetValue(tag, out TableHeader? header) + ? header + : null; + + public BigEndianBinaryReader GetReaderAtTablePosition(string tableName) + { + if (!this.TryGetReaderAtTablePosition(tableName, out BigEndianBinaryReader? reader)) + { + throw new InvalidFontTableException($"Unable to find table {tableName}", tableName); + } + + return reader!; + } + + public bool TryGetReaderAtTablePosition(string tableName, [NotNullWhen(returnValue: true)] out BigEndianBinaryReader? reader) + => this.TryGetReaderAtTablePosition(tableName, out reader, out _); + + public bool TryGetReaderAtTablePosition(string tableName, [NotNullWhen(returnValue: true)] out BigEndianBinaryReader? reader, [NotNullWhen(returnValue: true)] out TableHeader? header) + { + header = this.GetHeader(tableName); + if (header == null) + { + reader = null; + return false; + } + + reader = header?.CreateReader(this.stream); + return reader != null; + } + + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + if (this.isOwnedStream) + { + this.stream.Dispose(); + this.isDisposed = true; + } + } + } +} diff --git a/SixLabors.Fonts/FontRectangle.cs b/SixLabors.Fonts/FontRectangle.cs new file mode 100644 index 0000000..dd772bb --- /dev/null +++ b/SixLabors.Fonts/FontRectangle.cs @@ -0,0 +1,370 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.ComponentModel; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts { + /// + /// Stores a set of four single precision floating points that represent the location and size of a rectangle. + /// + public readonly struct FontRectangle : IEquatable + { + /// + /// Represents a that has X, Y, Width, and Height values set to zero. + /// + public static readonly FontRectangle Empty; + + /// + /// Initializes a new instance of the struct. + /// + /// The horizontal position of the rectangle. + /// The vertical position of the rectangle. + /// The width of the rectangle. + /// The height of the rectangle. + public FontRectangle(float x, float y, float width, float height) + { + this.X = x; + this.Y = y; + this.Width = width; + this.Height = height; + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The which specifies the rectangles point in a two-dimensional plane. + /// + /// + /// The which specifies the rectangles height and width. + /// + public FontRectangle(Vector2 point, Vector2 size) + : this(point.X, point.Y, size.X, size.Y) + { + } + + /// + /// Initializes a new instance of the structure using the specified bounding box. + /// + /// The bounding box that defines the position and size of the rectangle. + internal FontRectangle(in Bounds bound) + { + this.X = bound.Min.X; + this.Y = bound.Min.Y; + Vector2 size = bound.Max - bound.Min; + this.Width = size.X; + this.Height = size.Y; + } + + /// + /// Gets the x-coordinate of this . + /// + public float X { get; } + + /// + /// Gets the y-coordinate of this . + /// + public float Y { get; } + + /// + /// Gets the width of this . + /// + public float Width { get; } + + /// + /// Gets the height of this . + /// + public float Height { get; } + + /// + /// Gets the coordinates of the upper-left corner of the rectangular region represented by this . + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly Vector2 Location + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(this.X, this.Y); + } + + /// + /// Gets the size of this . + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly Vector2 Size + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(this.Width, this.Height); + } + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly bool IsEmpty => (this.Width <= 0) || (this.Height <= 0); + + /// + /// Gets the y-coordinate of the top edge of this . + /// + public readonly float Top => this.Y; + + /// + /// Gets the x-coordinate of the right edge of this . + /// + public float Right + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.X + this.Width; + } + + /// + /// Gets the y-coordinate of the bottom edge of this . + /// + public float Bottom + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.Y + this.Height; + } + + /// + /// Gets the x-coordinate of the left edge of this . + /// + public float Left => this.X; + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(in FontRectangle left, in FontRectangle right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(in FontRectangle left, in FontRectangle right) => !left.Equals(right); + + /// + /// Creates a new with the specified location and size. + /// The left coordinate of the rectangle. + /// The top coordinate of the rectangle. + /// The right coordinate of the rectangle. + /// The bottom coordinate of the rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + + // ReSharper disable once InconsistentNaming + public static FontRectangle FromLTRB(float left, float top, float right, float bottom) => new(left, top, right - left, bottom - top); + + /// + /// Returns the center point of the given . + /// + /// The rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Center(in FontRectangle rectangle) => new(rectangle.Left + (rectangle.Width / 2), rectangle.Top + (rectangle.Height / 2)); + + /// + /// Creates a rectangle that represents the intersection between and + /// . If there is no intersection, an empty rectangle is returned. + /// + /// The first rectangle. + /// The second rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FontRectangle Intersect(in FontRectangle a, in FontRectangle b) + { + float x1 = MathF.Max(a.X, b.X); + float x2 = MathF.Min(a.Right, b.Right); + float y1 = MathF.Max(a.Y, b.Y); + float y2 = MathF.Min(a.Bottom, b.Bottom); + + if (x2 >= x1 && y2 >= y1) + { + return new FontRectangle(x1, y1, x2 - x1, y2 - y1); + } + + return Empty; + } + + /// + /// Creates a new from the given + /// that is inflated by the specified amount. + /// + /// The rectangle. + /// The amount to inflate the width by. + /// The amount to inflate the height by. + /// A new . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FontRectangle Inflate(in FontRectangle rectangle, float x, float y) + => rectangle.Inflate(x, y); + + /// + /// Creates a new by transforming the given rectangle by the given matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// A transformed . + public static FontRectangle Transform(in FontRectangle rectangle, Matrix3x2 matrix) + { + Vector2 bottomRight = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix); + Vector2 topLeft = Vector2.Transform(rectangle.Location, matrix); + Vector2 size = bottomRight - topLeft; + + return new FontRectangle(topLeft, size); + } + + /// + /// Creates a rectangle that represents the union between and . + /// + /// The first rectangle. + /// The second rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FontRectangle Union(in FontRectangle a, in FontRectangle b) + { + float x1 = MathF.Min(a.X, b.X); + float x2 = MathF.Max(a.Right, b.Right); + float y1 = MathF.Min(a.Y, b.Y); + float y2 = MathF.Max(a.Bottom, b.Bottom); + + return new FontRectangle(x1, y1, x2 - x1, y2 - y1); + } + + /// + /// Deconstructs this rectangle into four floats. + /// + /// The out value for X. + /// The out value for Y. + /// The out value for the width. + /// The out value for the height. + public void Deconstruct(out float x, out float y, out float width, out float height) + { + x = this.X; + y = this.Y; + width = this.Width; + height = this.Height; + } + + /// + /// Creates a FontRectangle that represents the intersection between this FontRectangle and the . + /// + /// The rectangle. + /// New representing the intersections between the two rectangles. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FontRectangle Intersect(in FontRectangle rectangle) + => Intersect(rectangle, this); + + /// + /// Creates a new inflated by the specified amount. + /// + /// The width. + /// The height. + /// New representing the inflated rectangle + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FontRectangle Inflate(float width, float height) + => new( + this.X - width, + this.Y - height, + this.Width + (2 * width), + this.Height + (2 * height)); + + /// + /// Creates a new inflated by the specified amount. + /// + /// The size. + /// New representing the inflated rectangle + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FontRectangle Inflate(Vector2 size) => this.Inflate(size.X, size.Y); + + /// + /// Determines if the specified point is contained within the rectangular region defined by + /// this . + /// + /// The x-coordinate of the given point. + /// The y-coordinate of the given point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(float x, float y) => this.X <= x && x < this.Right && this.Y <= y && y < this.Bottom; + + /// + /// Determines if the specified point is contained within the rectangular region defined by this . + /// + /// The point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(Vector2 point) => this.Contains(point.X, point.Y); + + /// + /// Determines if the rectangular region represented by is entirely contained + /// within the rectangular region represented by this . + /// + /// The rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(in FontRectangle rectangle) => + (this.X <= rectangle.X) && (rectangle.Right <= this.Right) && + (this.Y <= rectangle.Y) && (rectangle.Bottom <= this.Bottom); + + /// + /// Determines if the specified intersects the rectangular region defined by + /// this . + /// + /// The other rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IntersectsWith(in FontRectangle rectangle) => + (rectangle.X < this.Right) && (this.X < rectangle.Right) && + (rectangle.Y < this.Bottom) && (this.Y < rectangle.Bottom); + + /// + /// Adjusts the location of this rectangle by the specified amount. + /// + /// The point. + /// New representing the offset rectangle. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FontRectangle Offset(Vector2 point) => this.Offset(point.X, point.Y); + + /// + /// Adjusts the location of this rectangle by the specified amount. + /// + /// The amount to offset the x-coordinate. + /// The amount to offset the y-coordinate. + /// New representing the inflated rectangle. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FontRectangle Offset(float dx, float dy) + => new(this.X + dx, this.Y + dy, this.Width, this.Height); + + /// + public override int GetHashCode() + => HashCode.Combine(this.X, this.Y, this.Width, this.Height); + + /// + public override string ToString() + => $"FontRectangle [ X={this.X}, Y={this.Y}, Width={this.Width}, Height={this.Height} ]"; + + /// + public override bool Equals(object? obj) + => obj is FontRectangle other + && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(FontRectangle other) + => this.X.Equals(other.X) + && this.Y.Equals(other.Y) + && this.Width.Equals(other.Width) + && this.Height.Equals(other.Height); + } +} diff --git a/SixLabors.Fonts/FontStyle.cs b/SixLabors.Fonts/FontStyle.cs new file mode 100644 index 0000000..d6a0d27 --- /dev/null +++ b/SixLabors.Fonts/FontStyle.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts { + /// + /// The font styles + /// + [Flags] + public enum FontStyle + { + /// + /// Regular + /// + Regular = 0, + + /// + /// Bold + /// + Bold = 1, + + /// + /// Italic + /// + Italic = 2, + + /// + /// Bold and Italic + /// + BoldItalic = 3, + + // TODO: Not yet supported + // Underline = 4, + // Strikeout = 8 + } +} diff --git a/SixLabors.Fonts/FontVariation.cs b/SixLabors.Fonts/FontVariation.cs new file mode 100644 index 0000000..2a512f5 --- /dev/null +++ b/SixLabors.Fonts/FontVariation.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; + +namespace SixLabors.Fonts { + /// + /// Represents a single variation axis setting for a variable font, + /// consisting of a four-character tag and a value. + /// + /// + /// Follows CSS font-variation-settings semantics. + /// Values are clamped to the axis range defined in the font's fvar table. + /// + [DebuggerDisplay("Tag: {Tag}, Value: {Value}")] + public readonly struct FontVariation + { + /// + /// Initializes a new instance of the struct. + /// + /// The four-character axis tag (e.g. "wght", "wdth", "opsz"). + /// The axis value in design-space units. + public FontVariation(string tag, float value) + { + Guard.NotNullOrWhiteSpace(tag, nameof(tag)); + if (tag.Length != 4) + { + throw new ArgumentException("Variation axis tag must be exactly 4 characters.", nameof(tag)); + } + + this.Tag = tag; + this.Value = value; + } + + /// + /// Gets the four-character axis tag identifying the design variation. + /// + public string Tag { get; } + + /// + /// Gets the axis value in design-space units. + /// + public float Value { get; } + } +} diff --git a/SixLabors.Fonts/Glyph.cs b/SixLabors.Fonts/Glyph.cs new file mode 100644 index 0000000..85be58e --- /dev/null +++ b/SixLabors.Fonts/Glyph.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.Fonts.Rendering; + +namespace SixLabors.Fonts { + /// + /// Represents a font-specific glyph at the point size used for layout and rendering. + /// + public readonly struct Glyph + { + private readonly float pointSize; + + internal Glyph(FontGlyphMetrics glyphMetrics, float pointSize) + { + this.GlyphMetrics = glyphMetrics; + this.pointSize = pointSize; + } + + /// + /// Gets the font metrics for this glyph. + /// + public FontGlyphMetrics GlyphMetrics { get; } + + /// + /// Calculates the rendered glyph bounds for the specified layout mode and origin. + /// + /// The glyph layout mode to measure with. + /// The glyph origin to calculate the bounds from. + /// The DPI to measure the glyph at. + /// The rendered glyph bounds. + public FontRectangle BoundingBox(GlyphLayoutMode mode, Vector2 glyphOrigin, float dpi) + => this.GlyphMetrics.GetBoundingBox(mode, glyphOrigin, this.pointSize * dpi); + + /// + /// Renders the glyph to the render surface. + /// + /// The target render surface. + /// The index of the grapheme this glyph is part of. + /// The origin used to render the glyph outline. + /// The origin used to render text decorations. + /// The glyph layout mode to render using. + /// The options to render using. + internal void RenderTo( + IGlyphRenderer surface, + int graphemeIndex, + Vector2 glyphOrigin, + Vector2 decorationOrigin, + GlyphLayoutMode mode, + TextOptions options) + => this.GlyphMetrics.RenderTo(surface, graphemeIndex, glyphOrigin, decorationOrigin, mode, options); + } +} diff --git a/SixLabors.Fonts/GlyphColor.KnownColors.cs b/SixLabors.Fonts/GlyphColor.KnownColors.cs new file mode 100644 index 0000000..5126caa --- /dev/null +++ b/SixLabors.Fonts/GlyphColor.KnownColors.cs @@ -0,0 +1,915 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; + +namespace SixLabors.Fonts { + /// + /// Contains static named color values. + /// + /// + public readonly partial struct GlyphColor + { + private static readonly Lazy> NamedGlyphColorsLookupLazy = new(CreateNamedGlyphColorsLookup, true); + + /// + /// Represents a matching the W3C definition that has an hex value of #F0F8FF. + /// + public static readonly GlyphColor AliceBlue = new(240, 248, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FAEBD7. + /// + public static readonly GlyphColor AntiqueWhite = new(250, 235, 215, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #00FFFF. + /// + public static readonly GlyphColor Aqua = new(0, 255, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #7FFFD4. + /// + public static readonly GlyphColor Aquamarine = new(127, 255, 212, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F0FFFF. + /// + public static readonly GlyphColor Azure = new(240, 255, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F5F5DC. + /// + public static readonly GlyphColor Beige = new(245, 245, 220, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFE4C4. + /// + public static readonly GlyphColor Bisque = new(255, 228, 196, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #000000. + /// + public static readonly GlyphColor Black = new(0, 0, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFEBCD. + /// + public static readonly GlyphColor BlanchedAlmond = new(255, 235, 205, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #0000FF. + /// + public static readonly GlyphColor Blue = new(0, 0, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #8A2BE2. + /// + public static readonly GlyphColor BlueViolet = new(138, 43, 226, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #A52A2A. + /// + public static readonly GlyphColor Brown = new(165, 42, 42, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #DEB887. + /// + public static readonly GlyphColor BurlyWood = new(222, 184, 135, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #5F9EA0. + /// + public static readonly GlyphColor CadetBlue = new(95, 158, 160, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #7FFF00. + /// + public static readonly GlyphColor Chartreuse = new(127, 255, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #D2691E. + /// + public static readonly GlyphColor Chocolate = new(210, 105, 30, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF7F50. + /// + public static readonly GlyphColor Coral = new(255, 127, 80, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #6495ED. + /// + public static readonly GlyphColor CornflowerBlue = new(100, 149, 237, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFF8DC. + /// + public static readonly GlyphColor Cornsilk = new(255, 248, 220, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #DC143C. + /// + public static readonly GlyphColor Crimson = new(220, 20, 60, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #00FFFF. + /// + public static readonly GlyphColor Cyan = Aqua; + + /// + /// Represents a matching the W3C definition that has an hex value of #00008B. + /// + public static readonly GlyphColor DarkBlue = new(0, 0, 139, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #008B8B. + /// + public static readonly GlyphColor DarkCyan = new(0, 139, 139, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #B8860B. + /// + public static readonly GlyphColor DarkGoldenrod = new(184, 134, 11, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #A9A9A9. + /// + public static readonly GlyphColor DarkGray = new(169, 169, 169, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #006400. + /// + public static readonly GlyphColor DarkGreen = new(0, 100, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #A9A9A9. + /// + public static readonly GlyphColor DarkGrey = DarkGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #BDB76B. + /// + public static readonly GlyphColor DarkKhaki = new(189, 183, 107, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #8B008B. + /// + public static readonly GlyphColor DarkMagenta = new(139, 0, 139, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #556B2F. + /// + public static readonly GlyphColor DarkOliveGreen = new(85, 107, 47, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF8C00. + /// + public static readonly GlyphColor DarkOrange = new(255, 140, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #9932CC. + /// + public static readonly GlyphColor DarkOrchid = new(153, 50, 204, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #8B0000. + /// + public static readonly GlyphColor DarkRed = new(139, 0, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #E9967A. + /// + public static readonly GlyphColor DarkSalmon = new(233, 150, 122, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #8FBC8F. + /// + public static readonly GlyphColor DarkSeaGreen = new(143, 188, 143, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #483D8B. + /// + public static readonly GlyphColor DarkSlateBlue = new(72, 61, 139, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #2F4F4F. + /// + public static readonly GlyphColor DarkSlateGray = new(47, 79, 79, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #2F4F4F. + /// + public static readonly GlyphColor DarkSlateGrey = DarkSlateGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #00CED1. + /// + public static readonly GlyphColor DarkTurquoise = new(0, 206, 209, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #9400D3. + /// + public static readonly GlyphColor DarkViolet = new(148, 0, 211, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF1493. + /// + public static readonly GlyphColor DeepPink = new(255, 20, 147, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #00BFFF. + /// + public static readonly GlyphColor DeepSkyBlue = new(0, 191, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #696969. + /// + public static readonly GlyphColor DimGray = new(105, 105, 105, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #696969. + /// + public static readonly GlyphColor DimGrey = DimGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #1E90FF. + /// + public static readonly GlyphColor DodgerBlue = new(30, 144, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #B22222. + /// + public static readonly GlyphColor Firebrick = new(178, 34, 34, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFAF0. + /// + public static readonly GlyphColor FloralWhite = new(255, 250, 240, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #228B22. + /// + public static readonly GlyphColor ForestGreen = new(34, 139, 34, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF00FF. + /// + public static readonly GlyphColor Fuchsia = new(255, 0, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #DCDCDC. + /// + public static readonly GlyphColor Gainsboro = new(220, 220, 220, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F8F8FF. + /// + public static readonly GlyphColor GhostWhite = new(248, 248, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFD700. + /// + public static readonly GlyphColor Gold = new(255, 215, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #DAA520. + /// + public static readonly GlyphColor Goldenrod = new(218, 165, 32, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #808080. + /// + public static readonly GlyphColor Gray = new(128, 128, 128, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #008000. + /// + public static readonly GlyphColor Green = new(0, 128, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #ADFF2F. + /// + public static readonly GlyphColor GreenYellow = new(173, 255, 47, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #808080. + /// + public static readonly GlyphColor Grey = Gray; + + /// + /// Represents a matching the W3C definition that has an hex value of #F0FFF0. + /// + public static readonly GlyphColor Honeydew = new(240, 255, 240, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF69B4. + /// + public static readonly GlyphColor HotPink = new(255, 105, 180, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #CD5C5C. + /// + public static readonly GlyphColor IndianRed = new(205, 92, 92, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #4B0082. + /// + public static readonly GlyphColor Indigo = new(75, 0, 130, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFFF0. + /// + public static readonly GlyphColor Ivory = new(255, 255, 240, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F0E68C. + /// + public static readonly GlyphColor Khaki = new(240, 230, 140, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #E6E6FA. + /// + public static readonly GlyphColor Lavender = new(230, 230, 250, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFF0F5. + /// + public static readonly GlyphColor LavenderBlush = new(255, 240, 245, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #7CFC00. + /// + public static readonly GlyphColor LawnGreen = new(124, 252, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFACD. + /// + public static readonly GlyphColor LemonChiffon = new(255, 250, 205, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #ADD8E6. + /// + public static readonly GlyphColor LightBlue = new(173, 216, 230, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F08080. + /// + public static readonly GlyphColor LightCoral = new(240, 128, 128, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #E0FFFF. + /// + public static readonly GlyphColor LightCyan = new(224, 255, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FAFAD2. + /// + public static readonly GlyphColor LightGoldenrodYellow = new(250, 250, 210, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #D3D3D3. + /// + public static readonly GlyphColor LightGray = new(211, 211, 211, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #90EE90. + /// + public static readonly GlyphColor LightGreen = new(144, 238, 144, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #D3D3D3. + /// + public static readonly GlyphColor LightGrey = LightGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #FFB6C1. + /// + public static readonly GlyphColor LightPink = new(255, 182, 193, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFA07A. + /// + public static readonly GlyphColor LightSalmon = new(255, 160, 122, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #20B2AA. + /// + public static readonly GlyphColor LightSeaGreen = new(32, 178, 170, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #87CEFA. + /// + public static readonly GlyphColor LightSkyBlue = new(135, 206, 250, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #778899. + /// + public static readonly GlyphColor LightSlateGray = new(119, 136, 153, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #778899. + /// + public static readonly GlyphColor LightSlateGrey = LightSlateGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #B0C4DE. + /// + public static readonly GlyphColor LightSteelBlue = new(176, 196, 222, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFFE0. + /// + public static readonly GlyphColor LightYellow = new(255, 255, 224, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #00FF00. + /// + public static readonly GlyphColor Lime = new(0, 255, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #32CD32. + /// + public static readonly GlyphColor LimeGreen = new(50, 205, 50, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FAF0E6. + /// + public static readonly GlyphColor Linen = new(250, 240, 230, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF00FF. + /// + public static readonly GlyphColor Magenta = Fuchsia; + + /// + /// Represents a matching the W3C definition that has an hex value of #800000. + /// + public static readonly GlyphColor Maroon = new(128, 0, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #66CDAA. + /// + public static readonly GlyphColor MediumAquamarine = new(102, 205, 170, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #0000CD. + /// + public static readonly GlyphColor MediumBlue = new(0, 0, 205, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #BA55D3. + /// + public static readonly GlyphColor MediumOrchid = new(186, 85, 211, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #9370DB. + /// + public static readonly GlyphColor MediumPurple = new(147, 112, 219, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #3CB371. + /// + public static readonly GlyphColor MediumSeaGreen = new(60, 179, 113, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #7B68EE. + /// + public static readonly GlyphColor MediumSlateBlue = new(123, 104, 238, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #00FA9A. + /// + public static readonly GlyphColor MediumSpringGreen = new(0, 250, 154, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #48D1CC. + /// + public static readonly GlyphColor MediumTurquoise = new(72, 209, 204, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #C71585. + /// + public static readonly GlyphColor MediumVioletRed = new(199, 21, 133, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #191970. + /// + public static readonly GlyphColor MidnightBlue = new(25, 25, 112, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F5FFFA. + /// + public static readonly GlyphColor MintCream = new(245, 255, 250, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFE4E1. + /// + public static readonly GlyphColor MistyRose = new(255, 228, 225, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFE4B5. + /// + public static readonly GlyphColor Moccasin = new(255, 228, 181, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFDEAD. + /// + public static readonly GlyphColor NavajoWhite = new(255, 222, 173, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #000080. + /// + public static readonly GlyphColor Navy = new(0, 0, 128, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FDF5E6. + /// + public static readonly GlyphColor OldLace = new(253, 245, 230, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #808000. + /// + public static readonly GlyphColor Olive = new(128, 128, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #6B8E23. + /// + public static readonly GlyphColor OliveDrab = new(107, 142, 35, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFA500. + /// + public static readonly GlyphColor Orange = new(255, 165, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF4500. + /// + public static readonly GlyphColor OrangeRed = new(255, 69, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #DA70D6. + /// + public static readonly GlyphColor Orchid = new(218, 112, 214, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #EEE8AA. + /// + public static readonly GlyphColor PaleGoldenrod = new(238, 232, 170, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #98FB98. + /// + public static readonly GlyphColor PaleGreen = new(152, 251, 152, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #AFEEEE. + /// + public static readonly GlyphColor PaleTurquoise = new(175, 238, 238, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #DB7093. + /// + public static readonly GlyphColor PaleVioletRed = new(219, 112, 147, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFEFD5. + /// + public static readonly GlyphColor PapayaWhip = new(255, 239, 213, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFDAB9. + /// + public static readonly GlyphColor PeachPuff = new(255, 218, 185, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #CD853F. + /// + public static readonly GlyphColor Peru = new(205, 133, 63, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFC0CB. + /// + public static readonly GlyphColor Pink = new(255, 192, 203, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #DDA0DD. + /// + public static readonly GlyphColor Plum = new(221, 160, 221, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #B0E0E6. + /// + public static readonly GlyphColor PowderBlue = new(176, 224, 230, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #800080. + /// + public static readonly GlyphColor Purple = new(128, 0, 128, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #663399. + /// + public static readonly GlyphColor RebeccaPurple = new(102, 51, 153, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF0000. + /// + public static readonly GlyphColor Red = new(255, 0, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #BC8F8F. + /// + public static readonly GlyphColor RosyBrown = new(188, 143, 143, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #4169E1. + /// + public static readonly GlyphColor RoyalBlue = new(65, 105, 225, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #8B4513. + /// + public static readonly GlyphColor SaddleBrown = new(139, 69, 19, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FA8072. + /// + public static readonly GlyphColor Salmon = new(250, 128, 114, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F4A460. + /// + public static readonly GlyphColor SandyBrown = new(244, 164, 96, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #2E8B57. + /// + public static readonly GlyphColor SeaGreen = new(46, 139, 87, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFF5EE. + /// + public static readonly GlyphColor SeaShell = new(255, 245, 238, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #A0522D. + /// + public static readonly GlyphColor Sienna = new(160, 82, 45, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #C0C0C0. + /// + public static readonly GlyphColor Silver = new(192, 192, 192, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #87CEEB. + /// + public static readonly GlyphColor SkyBlue = new(135, 206, 235, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #6A5ACD. + /// + public static readonly GlyphColor SlateBlue = new(106, 90, 205, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #708090. + /// + public static readonly GlyphColor SlateGray = new(112, 128, 144, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #708090. + /// + public static readonly GlyphColor SlateGrey = SlateGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFAFA. + /// + public static readonly GlyphColor Snow = new(255, 250, 250, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #00FF7F. + /// + public static readonly GlyphColor SpringGreen = new(0, 255, 127, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #4682B4. + /// + public static readonly GlyphColor SteelBlue = new(70, 130, 180, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #D2B48C. + /// + public static readonly GlyphColor Tan = new(210, 180, 140, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #008080. + /// + public static readonly GlyphColor Teal = new(0, 128, 128, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #D8BFD8. + /// + public static readonly GlyphColor Thistle = new(216, 191, 216, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF6347. + /// + public static readonly GlyphColor Tomato = new(255, 99, 71, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #00000000. + /// + public static readonly GlyphColor Transparent = new(0, 0, 0, 0); + + /// + /// Represents a matching the W3C definition that has an hex value of #40E0D0. + /// + public static readonly GlyphColor Turquoise = new(64, 224, 208, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #EE82EE. + /// + public static readonly GlyphColor Violet = new(238, 130, 238, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F5DEB3. + /// + public static readonly GlyphColor Wheat = new(245, 222, 179, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFFFF. + /// + public static readonly GlyphColor White = new(255, 255, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F5F5F5. + /// + public static readonly GlyphColor WhiteSmoke = new(245, 245, 245, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFF00. + /// + public static readonly GlyphColor Yellow = new(255, 255, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #9ACD32. + /// + public static readonly GlyphColor YellowGreen = new(154, 205, 50, 255); + + private static Dictionary CreateNamedGlyphColorsLookup() + => new(StringComparer.OrdinalIgnoreCase) + { + { nameof(AliceBlue), AliceBlue }, + { nameof(AntiqueWhite), AntiqueWhite }, + { nameof(Aqua), Aqua }, + { nameof(Aquamarine), Aquamarine }, + { nameof(Azure), Azure }, + { nameof(Beige), Beige }, + { nameof(Bisque), Bisque }, + { nameof(Black), Black }, + { nameof(BlanchedAlmond), BlanchedAlmond }, + { nameof(Blue), Blue }, + { nameof(BlueViolet), BlueViolet }, + { nameof(Brown), Brown }, + { nameof(BurlyWood), BurlyWood }, + { nameof(CadetBlue), CadetBlue }, + { nameof(Chartreuse), Chartreuse }, + { nameof(Chocolate), Chocolate }, + { nameof(Coral), Coral }, + { nameof(CornflowerBlue), CornflowerBlue }, + { nameof(Cornsilk), Cornsilk }, + { nameof(Crimson), Crimson }, + { nameof(Cyan), Cyan }, + { nameof(DarkBlue), DarkBlue }, + { nameof(DarkCyan), DarkCyan }, + { nameof(DarkGoldenrod), DarkGoldenrod }, + { nameof(DarkGray), DarkGray }, + { nameof(DarkGreen), DarkGreen }, + { nameof(DarkGrey), DarkGrey }, + { nameof(DarkKhaki), DarkKhaki }, + { nameof(DarkMagenta), DarkMagenta }, + { nameof(DarkOliveGreen), DarkOliveGreen }, + { nameof(DarkOrange), DarkOrange }, + { nameof(DarkOrchid), DarkOrchid }, + { nameof(DarkRed), DarkRed }, + { nameof(DarkSalmon), DarkSalmon }, + { nameof(DarkSeaGreen), DarkSeaGreen }, + { nameof(DarkSlateBlue), DarkSlateBlue }, + { nameof(DarkSlateGray), DarkSlateGray }, + { nameof(DarkSlateGrey), DarkSlateGrey }, + { nameof(DarkTurquoise), DarkTurquoise }, + { nameof(DarkViolet), DarkViolet }, + { nameof(DeepPink), DeepPink }, + { nameof(DeepSkyBlue), DeepSkyBlue }, + { nameof(DimGray), DimGray }, + { nameof(DimGrey), DimGrey }, + { nameof(DodgerBlue), DodgerBlue }, + { nameof(Firebrick), Firebrick }, + { nameof(FloralWhite), FloralWhite }, + { nameof(ForestGreen), ForestGreen }, + { nameof(Fuchsia), Fuchsia }, + { nameof(Gainsboro), Gainsboro }, + { nameof(GhostWhite), GhostWhite }, + { nameof(Gold), Gold }, + { nameof(Goldenrod), Goldenrod }, + { nameof(Gray), Gray }, + { nameof(Green), Green }, + { nameof(GreenYellow), GreenYellow }, + { nameof(Grey), Grey }, + { nameof(Honeydew), Honeydew }, + { nameof(HotPink), HotPink }, + { nameof(IndianRed), IndianRed }, + { nameof(Indigo), Indigo }, + { nameof(Ivory), Ivory }, + { nameof(Khaki), Khaki }, + { nameof(Lavender), Lavender }, + { nameof(LavenderBlush), LavenderBlush }, + { nameof(LawnGreen), LawnGreen }, + { nameof(LemonChiffon), LemonChiffon }, + { nameof(LightBlue), LightBlue }, + { nameof(LightCoral), LightCoral }, + { nameof(LightCyan), LightCyan }, + { nameof(LightGoldenrodYellow), LightGoldenrodYellow }, + { nameof(LightGray), LightGray }, + { nameof(LightGreen), LightGreen }, + { nameof(LightGrey), LightGrey }, + { nameof(LightPink), LightPink }, + { nameof(LightSalmon), LightSalmon }, + { nameof(LightSeaGreen), LightSeaGreen }, + { nameof(LightSkyBlue), LightSkyBlue }, + { nameof(LightSlateGray), LightSlateGray }, + { nameof(LightSlateGrey), LightSlateGrey }, + { nameof(LightSteelBlue), LightSteelBlue }, + { nameof(LightYellow), LightYellow }, + { nameof(Lime), Lime }, + { nameof(LimeGreen), LimeGreen }, + { nameof(Linen), Linen }, + { nameof(Magenta), Magenta }, + { nameof(Maroon), Maroon }, + { nameof(MediumAquamarine), MediumAquamarine }, + { nameof(MediumBlue), MediumBlue }, + { nameof(MediumOrchid), MediumOrchid }, + { nameof(MediumPurple), MediumPurple }, + { nameof(MediumSeaGreen), MediumSeaGreen }, + { nameof(MediumSlateBlue), MediumSlateBlue }, + { nameof(MediumSpringGreen), MediumSpringGreen }, + { nameof(MediumTurquoise), MediumTurquoise }, + { nameof(MediumVioletRed), MediumVioletRed }, + { nameof(MidnightBlue), MidnightBlue }, + { nameof(MintCream), MintCream }, + { nameof(MistyRose), MistyRose }, + { nameof(Moccasin), Moccasin }, + { nameof(NavajoWhite), NavajoWhite }, + { nameof(Navy), Navy }, + { nameof(OldLace), OldLace }, + { nameof(Olive), Olive }, + { nameof(OliveDrab), OliveDrab }, + { nameof(Orange), Orange }, + { nameof(OrangeRed), OrangeRed }, + { nameof(Orchid), Orchid }, + { nameof(PaleGoldenrod), PaleGoldenrod }, + { nameof(PaleGreen), PaleGreen }, + { nameof(PaleTurquoise), PaleTurquoise }, + { nameof(PaleVioletRed), PaleVioletRed }, + { nameof(PapayaWhip), PapayaWhip }, + { nameof(PeachPuff), PeachPuff }, + { nameof(Peru), Peru }, + { nameof(Pink), Pink }, + { nameof(Plum), Plum }, + { nameof(PowderBlue), PowderBlue }, + { nameof(Purple), Purple }, + { nameof(RebeccaPurple), RebeccaPurple }, + { nameof(Red), Red }, + { nameof(RosyBrown), RosyBrown }, + { nameof(RoyalBlue), RoyalBlue }, + { nameof(SaddleBrown), SaddleBrown }, + { nameof(Salmon), Salmon }, + { nameof(SandyBrown), SandyBrown }, + { nameof(SeaGreen), SeaGreen }, + { nameof(SeaShell), SeaShell }, + { nameof(Sienna), Sienna }, + { nameof(Silver), Silver }, + { nameof(SkyBlue), SkyBlue }, + { nameof(SlateBlue), SlateBlue }, + { nameof(SlateGray), SlateGray }, + { nameof(SlateGrey), SlateGrey }, + { nameof(Snow), Snow }, + { nameof(SpringGreen), SpringGreen }, + { nameof(SteelBlue), SteelBlue }, + { nameof(Tan), Tan }, + { nameof(Teal), Teal }, + { nameof(Thistle), Thistle }, + { nameof(Tomato), Tomato }, + { nameof(Transparent), Transparent }, + { nameof(Turquoise), Turquoise }, + { nameof(Violet), Violet }, + { nameof(Wheat), Wheat }, + { nameof(White), White }, + { nameof(WhiteSmoke), WhiteSmoke }, + { nameof(Yellow), Yellow }, + { nameof(YellowGreen), YellowGreen } + }; + } +} diff --git a/SixLabors.Fonts/GlyphColor.cs b/SixLabors.Fonts/GlyphColor.cs new file mode 100644 index 0000000..8555561 --- /dev/null +++ b/SixLabors.Fonts/GlyphColor.cs @@ -0,0 +1,252 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts { + /// + /// Provides access to the color details for the current glyph. + /// + public readonly partial struct GlyphColor : IEquatable + { + internal GlyphColor(byte red, byte green, byte blue, byte alpha) + { + this.R = red; + this.G = green; + this.B = blue; + this.A = alpha; + } + + /// + /// Gets the red component + /// + public readonly byte R { get; } + + /// + /// Gets the green component + /// + public readonly byte G { get; } + + /// + /// Gets the blue component + /// + public readonly byte B { get; } + + /// + /// Gets the alpha component + /// + public readonly byte A { get; } + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + public static bool operator ==(GlyphColor left, GlyphColor right) + => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + public static bool operator !=(GlyphColor left, GlyphColor right) + => !left.Equals(right); + + /// + public override bool Equals(object? obj) + => obj is GlyphColor p && this.Equals(p); + + /// + /// Compares the for equality to this color. + /// + /// + /// The other to compare to. + /// + /// + /// True if the current color is equal to the parameter; otherwise, false. + /// + public bool Equals(GlyphColor other) + => other.R == this.R + && other.G == this.G + && other.B == this.B + && other.A == this.A; + + /// + public override int GetHashCode() + => HashCode.Combine( + this.R, + this.G, + this.B, + this.A); + + /// + /// Gets the hexadecimal string representation of the color instance in the format RRGGBBAA. + /// + /// + /// The hexadecimal representation of the combined color components. + /// + /// + /// When this method returns, contains the equivalent of the hexadecimal input. + /// + /// + /// if the parsing was successful; otherwise, . + /// + public static bool TryParseHex(string? value, [NotNullWhen(true)] out GlyphColor result) + { + result = default; + + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + ReadOnlySpan hex = value.AsSpan(); + + if (hex[0] != '#') + { + return false; + } + + hex = hex[1..]; + + byte a = 255, r, g, b; + + switch (hex.Length) + { + case 8: + if (!TryParseByte(hex[0], hex[1], out r) || + !TryParseByte(hex[2], hex[3], out g) || + !TryParseByte(hex[4], hex[5], out b) || + !TryParseByte(hex[6], hex[7], out a)) + { + return false; + } + + break; + + case 6: + if (!TryParseByte(hex[0], hex[1], out r) || + !TryParseByte(hex[2], hex[3], out g) || + !TryParseByte(hex[4], hex[5], out b)) + { + return false; + } + + break; + + case 4: + if (!TryExpand(hex[0], out r) || + !TryExpand(hex[1], out g) || + !TryExpand(hex[2], out b) || + !TryExpand(hex[3], out a)) + { + return false; + } + + break; + + case 3: + if (!TryExpand(hex[0], out r) || + !TryExpand(hex[1], out g) || + !TryExpand(hex[2], out b)) + { + return false; + } + + break; + + default: + return false; + } + + result = new GlyphColor(r, g, b, a); + return true; + } + + /// + /// Attempts to parse the specified name into a corresponding named glyph color. + /// + /// The name of the glyph color to parse. + /// + /// When this method returns, contains the parsed value if the parse operation succeeded; + /// otherwise, contains the default value. + /// + /// + /// if the parsing was successful; otherwise, . + /// + public static bool TryParseNamed(string? name, [NotNullWhen(true)] out GlyphColor result) + { + result = default; + if (string.IsNullOrWhiteSpace(name)) + { + return false; + } + + return NamedGlyphColorsLookupLazy.Value.TryGetValue(name, out result); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryParseByte(char hi, char lo, out byte value) + { + if (TryConvertHexCharToByte(hi, out byte high) && TryConvertHexCharToByte(lo, out byte low)) + { + value = (byte)((high << 4) | low); + return true; + } + + value = 0; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryExpand(char c, out byte value) + { + if (TryConvertHexCharToByte(c, out byte nibble)) + { + value = (byte)((nibble << 4) | nibble); + return true; + } + + value = 0; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryConvertHexCharToByte(char c, out byte value) + { + if ((uint)(c - '0') <= 9) + { + value = (byte)(c - '0'); + return true; + } + + char lower = (char)(c | 0x20); // Normalize to lowercase + + if ((uint)(lower - 'a') <= 5) + { + value = (byte)(lower - 'a' + 10); + return true; + } + + value = 0; + return false; + } + } +} diff --git a/SixLabors.Fonts/GlyphLayout.cs b/SixLabors.Fonts/GlyphLayout.cs new file mode 100644 index 0000000..53201cb --- /dev/null +++ b/SixLabors.Fonts/GlyphLayout.cs @@ -0,0 +1,192 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// Represents the layout positions of a glyph entry emitted from a laid-out . + /// + internal readonly struct GlyphLayout + { + internal GlyphLayout( + Glyph glyph, + Font font, + Vector2 advanceOrigin, + Vector2 glyphOrigin, + Vector2 decorationOrigin, + float advanceWidth, + float advanceHeight, + GlyphLayoutMode layoutMode, + int bidiLevel, + bool isStartOfLine, + int graphemeIndex, + int stringIndex) + { + this.Glyph = glyph; + this.Font = font; + this.CodePoint = glyph.GlyphMetrics.CodePoint; + this.AdvanceOrigin = advanceOrigin; + this.GlyphOrigin = glyphOrigin; + this.DecorationOrigin = decorationOrigin; + this.AdvanceX = advanceWidth; + this.AdvanceY = advanceHeight; + this.LayoutMode = layoutMode; + this.BidiLevel = bidiLevel; + this.IsStartOfLine = isStartOfLine; + this.GraphemeIndex = graphemeIndex; + this.StringIndex = stringIndex; + } + + /// + /// Gets the font-specific glyph for this laid-out glyph entry. + /// + public Glyph Glyph { get; } + + /// + /// Gets the font used to shape and render this laid-out glyph entry. + /// + public Font Font { get; } + + /// + /// Gets the code point represented by this glyph. + /// + public CodePoint CodePoint { get; } + + /// + /// Gets the origin of the logical advance box in DPI-normalized layout units. + /// + /// + /// Multiply by the target DPI to convert to device pixels. + /// + public Vector2 AdvanceOrigin { get; } + + /// + /// Gets the origin used to render the glyph outline in DPI-normalized layout units. + /// + /// + /// Multiply by the target DPI to convert to device pixels. + /// + public Vector2 GlyphOrigin { get; } + + /// + /// Gets the origin used to render text decorations in DPI-normalized layout units. + /// + /// + /// Multiply by the target DPI to convert to device pixels. + /// + public Vector2 DecorationOrigin { get; } + + /// + /// Gets the advance in the x direction in DPI-normalized layout units. + /// + /// + /// Multiply by the target DPI to convert to device pixels. + /// + public float AdvanceX { get; } + + /// + /// Gets the advance in the y direction in DPI-normalized layout units. + /// + /// + /// Multiply by the target DPI to convert to device pixels. + /// + public float AdvanceY { get; } + + /// + /// Gets the glyph layout mode. + /// + public GlyphLayoutMode LayoutMode { get; } + + /// + /// Gets the resolved bidi embedding level. + /// + internal int BidiLevel { get; } + + /// + /// Gets a value indicating whether this glyph is the first glyph on a new line. + /// + public bool IsStartOfLine { get; } + + /// + /// Gets the zero-based grapheme index in the original text. + /// + public int GraphemeIndex { get; } + + /// + /// Gets the zero-based UTF-16 code unit index in the original text. + /// + public int StringIndex { get; } + + /// + /// Gets a value indicating whether the glyph represents a whitespace character. + /// + /// The . + public bool IsWhiteSpace() => UnicodeUtility.ShouldRenderWhiteSpaceOnly(this.CodePoint); + + /// + /// Measures the positioned logical advance rectangle in pixel units. + /// + /// The target DPI. + /// The measured advance rectangle. + internal FontRectangle MeasureAdvance(float dpi) + => new( + this.AdvanceOrigin.X * dpi, + this.AdvanceOrigin.Y * dpi, + this.AdvanceX * dpi, + this.AdvanceY * dpi); + + /// + /// Measures the rendered glyph bounds in pixel units. + /// + /// The target DPI. + /// The measured rendered bounds. + internal FontRectangle MeasureBounds(float dpi) + { + // Same logic as in GlyphMetrics.RenderTo. + Vector2 glyphOrigin = this.GlyphOrigin * dpi; + FontRectangle box = this.Glyph.BoundingBox(this.LayoutMode, glyphOrigin, dpi); + + // Whitespace uses the layout advance because it occupies measurable + // text space even though the renderer suppresses its outline. + if (this.IsWhiteSpace()) + { + if (this.LayoutMode == GlyphLayoutMode.Vertical) + { + return new FontRectangle( + box.X, + box.Y, + box.Width, + this.AdvanceY * dpi); + } + + if (this.LayoutMode == GlyphLayoutMode.VerticalRotated) + { + return new FontRectangle( + box.X, + box.Y, + 0, + this.AdvanceY * dpi); + } + + return new FontRectangle( + box.X, + box.Y, + this.AdvanceX * dpi, + box.Height); + } + + return box; + } + + /// + public override string ToString() + { + string s = this.IsStartOfLine ? "@ " : string.Empty; + string ws = this.IsWhiteSpace() ? "!" : string.Empty; + Vector2 l = this.GlyphOrigin; + return $"{s}{ws}{this.CodePoint.ToDebuggerDisplay()} {l.X},{l.Y} {this.AdvanceX}x{this.AdvanceY}"; + } + } +} diff --git a/SixLabors.Fonts/GlyphLayoutData.cs b/SixLabors.Fonts/GlyphLayoutData.cs new file mode 100644 index 0000000..f57cd61 --- /dev/null +++ b/SixLabors.Fonts/GlyphLayoutData.cs @@ -0,0 +1,147 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// Per-codepoint shaping data stored inside a . + /// Each entry corresponds to a single codepoint — complex scripts may map one grapheme to + /// multiple entries (tracked via ). + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + internal struct GlyphLayoutData + { + internal const int NoHyphenationMarker = -1; + + /// + /// Initializes a new instance of the struct. + /// + /// The shaped glyph metrics for this codepoint. + /// The font used to shape and render this entry. + /// The point size at which the glyph is rendered. + /// The scaled advance of this entry. + /// The scaled line height contributed by this entry. + /// The scaled typographic ascender. + /// The scaled typographic descender. + /// The symmetric metrics delta applied during line-box construction. + /// The minimum scaled Y (topmost ink) across . + /// The resolved bidi run this entry belongs to. + /// The grapheme index in the source text. + /// Whether this is the last codepoint in its grapheme cluster. + /// The codepoint index in the source text. + /// The index of this codepoint within its grapheme cluster. + /// Whether the entry participates in a transformed vertical layout. + /// Whether the entry was produced by Unicode decomposition. + /// The UTF-16 character index in the source string. + /// The marker index to use if this entry becomes a selected soft-hyphen break. + public GlyphLayoutData( + IReadOnlyList metrics, + Font font, + float pointSize, + float scaledAdvance, + float scaledLineHeight, + float scaledAscender, + float scaledDescender, + float scaledDelta, + float scaledMinY, + BidiRun bidiRun, + int graphemeIndex, + bool isLastInGrapheme, + int codePointIndex, + int graphemeCodePointIndex, + bool isTransformed, + bool isDecomposed, + int stringIndex, + int hyphenationMarkerIndex = NoHyphenationMarker) + { + this.Metrics = metrics; + this.Font = font; + this.PointSize = pointSize; + this.ScaledAdvance = scaledAdvance; + this.ScaledLineHeight = scaledLineHeight; + this.ScaledAscender = scaledAscender; + this.ScaledDescender = scaledDescender; + this.ScaledDelta = scaledDelta; + this.ScaledMinY = scaledMinY; + this.BidiRun = bidiRun; + this.GraphemeIndex = graphemeIndex; + this.IsLastInGrapheme = isLastInGrapheme; + this.CodePointIndex = codePointIndex; + this.GraphemeCodePointIndex = graphemeCodePointIndex; + this.IsTransformed = isTransformed; + this.IsDecomposed = isDecomposed; + this.StringIndex = stringIndex; + this.HyphenationMarkerIndex = hyphenationMarkerIndex; + } + + /// Gets the source codepoint for this entry. + public readonly CodePoint CodePoint => this.Metrics[0].CodePoint; + + /// Gets the shaped glyph metrics produced for this codepoint (one codepoint may map to several glyphs). + public IReadOnlyList Metrics { get; } + + /// Gets the font used to shape and render this entry. + public Font Font { get; } + + /// Gets the point size at which this entry is rendered. + public float PointSize { get; } + + /// Gets or sets the scaled advance of this entry (mutated by justification). + public float ScaledAdvance { get; set; } + + /// Gets the scaled line height contributed by this entry, before line-spacing is applied. + public float ScaledLineHeight { get; } + + /// Gets the scaled typographic ascender. + public float ScaledAscender { get; } + + /// Gets the scaled typographic descender. + public float ScaledDescender { get; } + + /// Gets the symmetric ascender/descender delta applied during line-box construction. + public float ScaledDelta { get; } + + /// Gets the smallest (most negative) scaled Y across . + public float ScaledMinY { get; } + + /// Gets the resolved bidi run this entry belongs to. + public BidiRun BidiRun { get; } + + /// Gets the text direction derived from . + public readonly TextDirection TextDirection => (TextDirection)this.BidiRun.Direction; + + /// Gets the zero-based grapheme index in the original text. + public int GraphemeIndex { get; } + + /// Gets or sets a value indicating whether this is the last entry in its grapheme cluster. + public bool IsLastInGrapheme { get; set; } + + /// Gets the index of this codepoint within its grapheme cluster (0-based). + public int GraphemeCodePointIndex { get; } + + /// Gets the codepoint index in the source text. + public int CodePointIndex { get; } + + /// Gets a value indicating whether the entry participates in a transformed vertical layout. + public bool IsTransformed { get; } + + /// Gets a value indicating whether the entry was produced by Unicode decomposition. + public bool IsDecomposed { get; } + + /// Gets the zero-based UTF-16 code unit index in the original text. + public int StringIndex { get; } + + /// Gets the marker index to use if this entry becomes a selected soft-hyphen break. + public int HyphenationMarkerIndex { get; } + + /// Gets a value indicating whether the codepoint is a line-break character. + public readonly bool IsNewLine => CodePoint.IsNewLine(this.CodePoint); + + private readonly string DebuggerDisplay => FormattableString + .Invariant($"{this.CodePoint.ToDebuggerDisplay()} : {this.TextDirection} : {this.CodePointIndex}, level: {this.BidiRun.Level}"); + } +} diff --git a/SixLabors.Fonts/GlyphLayoutMode.cs b/SixLabors.Fonts/GlyphLayoutMode.cs new file mode 100644 index 0000000..c8df29c --- /dev/null +++ b/SixLabors.Fonts/GlyphLayoutMode.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Provides enumeration for the various layout mode of an individual glyph within a body of text. + /// + public enum GlyphLayoutMode + { + /// + /// Horizontal. + /// + Horizontal, + + /// + /// Vertical. + /// + Vertical, + + /// + /// Rotated 90 degrees clockwise. + /// + VerticalRotated + } +} diff --git a/SixLabors.Fonts/GlyphMetrics.cs b/SixLabors.Fonts/GlyphMetrics.cs new file mode 100644 index 0000000..28c4063 --- /dev/null +++ b/SixLabors.Fonts/GlyphMetrics.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// Represents one laid-out glyph entry in final layout order. + /// + public readonly struct GlyphMetrics + { + /// + /// Initializes a new instance of the struct. + /// + /// The Unicode code point represented by the glyph entry. + /// The positioned logical advance rectangle for the glyph entry in pixel units. + /// The rendered rectangle for the glyph entry in pixel units. + /// The union of the positioned logical advance rectangle and rendered rectangle in pixel units. + /// The font used to shape and render the glyph entry. + /// The grapheme index in the original text. + /// The UTF-16 index in the original text where the glyph entry begins. + internal GlyphMetrics( + CodePoint codePoint, + in FontRectangle advance, + in FontRectangle bounds, + in FontRectangle renderableBounds, + Font font, + int graphemeIndex, + int stringIndex) + { + this.CodePoint = codePoint; + this.Advance = advance; + this.Bounds = bounds; + this.RenderableBounds = renderableBounds; + this.Font = font; + this.GraphemeIndex = graphemeIndex; + this.StringIndex = stringIndex; + } + + /// + /// Gets the Unicode code point represented by the glyph entry. + /// + public CodePoint CodePoint { get; } + + /// + /// Gets the positioned logical advance rectangle for the glyph entry in pixel units. + /// + public FontRectangle Advance { get; } + + /// + /// Gets the rendered rectangle for the glyph entry in pixel units. + /// + public FontRectangle Bounds { get; } + + /// + /// Gets the union of the positioned logical advance rectangle and rendered rectangle in pixel units. + /// + public FontRectangle RenderableBounds { get; } + + /// + /// Gets the font used to shape and render the glyph entry. + /// + public Font Font { get; } + + /// + /// Gets the zero-based grapheme index in the original text. + /// + public int GraphemeIndex { get; } + + /// + /// Gets the zero-based UTF-16 code unit index in the original text. + /// + public int StringIndex { get; } + + /// + public override string ToString() + => $"CodePoint: {this.CodePoint}, Advance: {this.Advance}, Bounds: {this.Bounds}, RenderableBounds: {this.RenderableBounds}."; + } +} diff --git a/SixLabors.Fonts/GlyphPositioningCollection.cs b/SixLabors.Fonts/GlyphPositioningCollection.cs new file mode 100644 index 0000000..e1eaf0f --- /dev/null +++ b/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -0,0 +1,459 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// Represents a collection of glyph metrics that are mapped to input codepoints. + /// + internal sealed class GlyphPositioningCollection : IGlyphShapingCollection + { + /// + /// Contains a map the index of a map within the collection, non-sequential codepoint offsets, and their glyph ids, point size, and mtrics. + /// + private readonly List glyphs = []; + + /// + /// Initializes a new instance of the class. + /// + /// The text options. + public GlyphPositioningCollection(TextOptions textOptions) => this.TextOptions = textOptions; + + /// + public int Count => this.glyphs.Count; + + /// + public TextOptions TextOptions { get; } + + /// + public GlyphShapingData this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.glyphs[index].Data; + } + + /// + public void AddShapingFeature(int index, TagEntry feature) + { + GlyphShapingData data = this.glyphs[index].Data; + data.Features.Add(feature); + if (feature.Enabled) + { + data.EnabledFeatureTags.Add(feature.Tag); + } + } + + /// + public void EnableShapingFeature(int index, Tag feature) + { + GlyphShapingData data = this.glyphs[index].Data; + List features = data.Features; + for (int i = 0; i < features.Count; i++) + { + TagEntry tagEntry = features[i]; + if (tagEntry.Tag == feature) + { + tagEntry.Enabled = true; + features[i] = tagEntry; + data.EnabledFeatureTags.Add(feature); + break; + } + } + } + + /// + public void DisableShapingFeature(int index, Tag feature) + { + GlyphShapingData data = this.glyphs[index].Data; + List features = data.Features; + for (int i = 0; i < features.Count; i++) + { + TagEntry tagEntry = features[i]; + if (tagEntry.Tag == feature) + { + tagEntry.Enabled = false; + features[i] = tagEntry; + data.EnabledFeatureTags.Remove(feature); + break; + } + } + } + + /// + /// Gets the glyph metrics at the given codepoint offset. + /// + /// The zero-based index within the input codepoint collection. + /// + /// The index within the glyph list to start searching from. Updated to the position of the match + /// so that subsequent calls with increasing offsets avoid rescanning from the beginning. + /// + /// The font size in PT units of the font containing this glyph. + /// Whether the glyph is the result of a substitution. + /// Whether the glyph is the result of a vertical substitution. + /// Whether the glyph is the result of a decomposition substitution. + /// + /// When this method returns, contains the glyph metrics associated with the specified offset, + /// if the value is found; otherwise, the default value for the type of the metrics parameter. + /// This parameter is passed uninitialized. + /// + /// The metrics. + public bool TryGetGlyphMetricsAtOffset( + int offset, + ref int startIndex, + out float pointSize, + out bool isSubstituted, + out bool isVerticalSubstitution, + out bool isDecomposed, + [NotNullWhen(true)] out IReadOnlyList? data) + { + List match = []; + pointSize = 0; + isSubstituted = false; + isVerticalSubstitution = false; + isDecomposed = false; + + Tag vert = KnownFeatureTags.VerticalAlternates; + Tag vrt2 = KnownFeatureTags.VerticalAlternatesAndRotation; + Tag vrtr = KnownFeatureTags.VerticalAlternatesForRotation; + + for (int i = startIndex; i < this.glyphs.Count; i++) + { + if (this.glyphs[i].Offset == offset) + { + if (match.Count == 0) + { + startIndex = i; + } + + GlyphPositioningData glyph = this.glyphs[i]; + if (!glyph.Data.IsPlaceholder) + { + isSubstituted = glyph.Data.IsSubstituted; + isDecomposed = glyph.Data.IsDecomposed; + + foreach (Tag feature in glyph.Data.AppliedFeatures) + { + isVerticalSubstitution |= feature == vert; + isVerticalSubstitution |= feature == vrt2; + isVerticalSubstitution |= feature == vrtr; + } + + pointSize = glyph.PointSize; + } + + match.Add(glyph); + } + else if (match.Count > 0) + { + // Offsets, though non-sequential, are sorted, so we can stop searching. + break; + } + } + + data = match; + return match.Count > 0; + } + + /// + /// Updates the collection of glyph ids to the metrics collection to overwrite any glyphs that have been previously + /// identified as fallbacks. + /// + /// The font face with metrics. + /// The glyph substitution collection. + /// if the metrics collection does not contain any fallbacks; otherwise . + public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) + { + FontMetrics fontMetrics = font.FontMetrics; + LayoutMode layoutMode = this.TextOptions.LayoutMode; + ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; + bool hasFallBacks = false; + List orphans = []; + + Tag vert = KnownFeatureTags.VerticalAlternates; + Tag vrt2 = KnownFeatureTags.VerticalAlternatesAndRotation; + Tag vrtr = KnownFeatureTags.VerticalAlternatesForRotation; + + for (int i = 0; i < this.glyphs.Count; i++) + { + GlyphPositioningData current = this.glyphs[i]; + if (current.Metrics.GlyphType != GlyphType.Fallback) + { + // We've already got the correct glyph. + continue; + } + + int offset = current.Offset; + float pointSize = current.PointSize; + if (collection.TryGetGlyphShapingDataAtOffset(offset, out IReadOnlyList? data)) + { + int replacementCount = 0; + for (int j = 0; j < data.Count; j++) + { + GlyphShapingData shape = data[j]; + ushort id = shape.GlyphId; + CodePoint codePoint = shape.CodePoint; + + // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to + // cache the original in the font metrics and only update our collection. + TextAttributes textAttributes = shape.TextRun.TextAttributes; + TextDecorations textDecorations = shape.TextRun.TextDecorations; + + bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode); + foreach (Tag feature in shape.AppliedFeatures) + { + isVertical |= feature == vert; + isVertical |= feature == vrt2; + isVertical |= feature == vrtr; + } + + FontGlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); + { + // If the glyphs are fallbacks we don't want them as + // we've already captured them on the first run. + if (metrics.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) + { + hasFallBacks = true; + } + } + + if (metrics.GlyphType != GlyphType.Fallback) + { + if (replacementCount == 0) + { + // There should only be a single fallback glyph at this position from the previous collection. + this.glyphs.RemoveAt(i); + } + + // We only want a single dimensional advance for positioning. + GlyphShapingBounds bounds = isVertical + ? new(0, 0, 0, metrics.AdvanceHeight) + : new(0, 0, metrics.AdvanceWidth, 0); + + // Track the number of inserted glyphs at the offset so we can correctly increment our position. + this.glyphs.Insert(i += replacementCount, new(offset, new(shape, true) { Bounds = bounds }, font, pointSize, metrics.CloneForRendering(shape.TextRun))); + replacementCount++; + } + } + } + else + { + // If a font had glyphs but a follow up font also has them and can substitute. e.g ligatures + // then we end up with orphaned fallbacks. We need to remove them. + orphans.Add(i); + } + } + + // Remove any orphans. + for (int i = orphans.Count - 1; i >= 0; i--) + { + this.glyphs.RemoveAt(orphans[i]); + } + + return !hasFallBacks; + } + + /// + /// Adds the collection of glyph ids to the metrics collection. + /// identified as fallbacks. + /// + /// The font face with metrics. + /// The glyph substitution collection. + /// if the metrics collection does not contain any fallbacks; otherwise . + public bool TryAdd(Font font, GlyphSubstitutionCollection collection) + { + bool hasFallBacks = false; + FontMetrics fontMetrics = font.FontMetrics; + LayoutMode layoutMode = this.TextOptions.LayoutMode; + ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; + + Tag vert = KnownFeatureTags.VerticalAlternates; + Tag vrt2 = KnownFeatureTags.VerticalAlternatesAndRotation; + Tag vrtr = KnownFeatureTags.VerticalAlternatesForRotation; + + for (int i = 0; i < collection.Count; i++) + { + GlyphShapingData data = collection.GetGlyphShapingData(i, out int offset); + CodePoint codePoint = data.CodePoint; + ushort id = data.GlyphId; + + if (data.IsPlaceholder) + { + // Placeholders are synthetic glyphs: they need layout metrics but must not + // go through font glyph lookup, fallback resolution, or GPOS positioning. + StreamFontMetrics streamFontMetrics = fontMetrics is FileFontMetrics fileFontMetrics + ? fileFontMetrics.StreamFontMetrics + : (StreamFontMetrics)fontMetrics; + + FontGlyphMetrics placeholderMetrics = new PlaceholderGlyphMetrics( + streamFontMetrics, + data.TextRun.Placeholder.GetValueOrDefault(), + font.Size, + this.TextOptions.Dpi, + data.TextRun); + + GlyphShapingBounds placeholderBounds = layoutMode.IsVertical() + ? new(0, 0, 0, placeholderMetrics.AdvanceHeight) + : new(0, 0, placeholderMetrics.AdvanceWidth, 0); + + GlyphShapingData placeholderData = new(data, true) + { + Bounds = placeholderBounds, + IsPositioned = true + }; + + this.glyphs.Add(new(offset, placeholderData, font, font.Size, placeholderMetrics)); + continue; + } + + // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to + // cache the original in the font metrics and only update our collection. + TextAttributes textAttributes = data.TextRun.TextAttributes; + TextDecorations textDecorations = data.TextRun.TextDecorations; + + bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode); + foreach (Tag feature in data.AppliedFeatures) + { + isVertical |= feature == vert; + isVertical |= feature == vrt2; + isVertical |= feature == vrtr; + } + + FontGlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); + + if (metrics.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) + { + hasFallBacks = true; + } + + // We only want a single dimensional advance for positioning. + GlyphShapingBounds bounds = isVertical + ? new(0, 0, 0, metrics.AdvanceHeight) + : new(0, 0, metrics.AdvanceWidth, 0); + + this.glyphs.Add(new(offset, new(data, true) { Bounds = bounds }, font, font.Size, metrics.CloneForRendering(data.TextRun))); + } + + return !hasFallBacks; + } + + /// + /// Updates the position of the glyph at the specified index. + /// + /// The font metrics. + /// The zero-based index of the element. + public void UpdatePosition(FontMetrics fontMetrics, int index) + { + GlyphShapingData data = this[index]; + bool isDirtyXY = data.Bounds.IsDirtyXY; + bool isDirtyWH = data.Bounds.IsDirtyWH; + if (!isDirtyXY && !isDirtyWH) + { + // No change required but the glyph has been processed. + data.IsPositioned = true; + return; + } + + ushort glyphId = data.GlyphId; + FontGlyphMetrics m = this.glyphs[index].Metrics; + + if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics) + { + if (isDirtyXY) + { + m.ApplyOffset((short)data.Bounds.X, (short)data.Bounds.Y); + data.IsPositioned = true; + } + + if (isDirtyWH) + { + m.SetAdvanceWidth((ushort)data.Bounds.Width); + m.SetAdvanceHeight((ushort)data.Bounds.Height); + data.IsPositioned = true; + } + } + } + + /// + /// Updates the advanced metrics of the glyphs at the given index and id, + /// adding dx and dy to the current advance. + /// + /// The font face with metrics. + /// The zero-based index of the element. + /// The id of the glyph to offset. + /// The delta x-advance. + /// The delta y-advance. + public void Advance(FontMetrics fontMetrics, int index, ushort glyphId, short dx, short dy) + { + LayoutMode layoutMode = this.TextOptions.LayoutMode; + Tag vert = KnownFeatureTags.VerticalAlternates; + Tag vrt2 = KnownFeatureTags.VerticalAlternatesAndRotation; + Tag vrtr = KnownFeatureTags.VerticalAlternatesForRotation; + + GlyphPositioningData glyph = this.glyphs[index]; + FontGlyphMetrics m = glyph.Metrics; + + if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics) + { + bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(m.CodePoint, layoutMode); + + foreach (Tag feature in glyph.Data.AppliedFeatures) + { + isVertical |= feature == vert; + isVertical |= feature == vrt2; + isVertical |= feature == vrtr; + } + + m.ApplyAdvance(dx, isVertical ? dy : (short)0); + } + } + + /// + /// Returns a value indicating whether the element at the given index should be processed. + /// + /// The font face with metrics. + /// The zero-based index of the elements to position. + /// if the element should be processed; otherwise, . + public bool ShouldProcess(FontMetrics fontMetrics, int index) + { + GlyphPositioningData data = this.glyphs[index]; + if (data.Data.IsPositioned) + { + return false; + } + + return data.Metrics.FontMetrics == fontMetrics; + } + + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class GlyphPositioningData + { + public GlyphPositioningData(int offset, GlyphShapingData data, Font font, float pointSize, FontGlyphMetrics metrics) + { + this.Offset = offset; + this.Data = data; + this.Font = font; + this.PointSize = pointSize; + this.Metrics = metrics; + } + + public int Offset { get; set; } + + public GlyphShapingData Data { get; set; } + + public Font Font { get; set; } + + public float PointSize { get; set; } + + public FontGlyphMetrics Metrics { get; set; } + + private string DebuggerDisplay => FormattableString.Invariant($"Offset: {this.Offset}, Data: {this.Data.ToDebuggerDisplay()}"); + } + } +} diff --git a/SixLabors.Fonts/GlyphShapingBounds.cs b/SixLabors.Fonts/GlyphShapingBounds.cs new file mode 100644 index 0000000..97ae6c2 --- /dev/null +++ b/SixLabors.Fonts/GlyphShapingBounds.cs @@ -0,0 +1,81 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; + +namespace SixLabors.Fonts { + /// + /// Represents the shaped bounds of a glyph. + /// Uses a class over a struct for ease of use. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + internal class GlyphShapingBounds + { + private int x; + private int y; + private int width; + private int height; + + public GlyphShapingBounds(int x, int y, int width, int height) + { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.IsDirtyXY = false; + this.IsDirtyWH = false; + } + + public int X + { + get => this.x; + + set + { + this.x = value; + this.IsDirtyXY = true; + } + } + + public int Y + { + get => this.y; + + set + { + this.y = value; + this.IsDirtyXY = true; + } + } + + public int Width + { + get => this.width; + + set + { + this.width = value; + this.IsDirtyWH = true; + } + } + + public int Height + { + get => this.height; + + set + { + this.height = value; + this.IsDirtyWH = true; + } + } + + public bool IsDirtyXY { get; private set; } + + public bool IsDirtyWH { get; private set; } + + private string DebuggerDisplay + => FormattableString.Invariant($"{this.X} : {this.Y} : {this.Width} : {this.Height} : {this.IsDirtyXY} : {this.IsDirtyWH}"); + } +} diff --git a/SixLabors.Fonts/GlyphShapingClass.cs b/SixLabors.Fonts/GlyphShapingClass.cs new file mode 100644 index 0000000..a3c10de --- /dev/null +++ b/SixLabors.Fonts/GlyphShapingClass.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + internal readonly struct GlyphShapingClass + { + public GlyphShapingClass(bool isMark, bool isBase, bool isLigature, ushort markAttachmentType) + { + this.IsMark = isMark; + this.IsBase = isBase; + this.IsLigature = isLigature; + this.MarkAttachmentType = markAttachmentType; + } + + public bool IsMark { get; } + + public bool IsBase { get; } + + public bool IsLigature { get; } + + public ushort MarkAttachmentType { get; } + } +} diff --git a/SixLabors.Fonts/GlyphShapingData.cs b/SixLabors.Fonts/GlyphShapingData.cs new file mode 100644 index 0000000..37fbe22 --- /dev/null +++ b/SixLabors.Fonts/GlyphShapingData.cs @@ -0,0 +1,269 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Unicode; +using static SixLabors.Fonts.Unicode.Resources.IndicShapingData; + +namespace SixLabors.Fonts { + /// + /// Contains supplementary data that allows the shaping of glyphs. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + internal class GlyphShapingData + { + private ushort glyphId; + + /// + /// Initializes a new instance of the class. + /// + /// The text run. + public GlyphShapingData(TextRun textRun) => this.TextRun = textRun; + + /// + /// Initializes a new instance of the class. + /// + /// The data to copy properties from. + /// Whether to clear features. + public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) + { + this.GlyphId = data.GlyphId; + this.CodePoint = data.CodePoint; + this.CodePointCount = data.CodePointCount; + this.Direction = data.Direction; + this.TextRun = data.TextRun; + this.LigatureId = data.LigatureId; + this.IsLigated = data.IsLigated; + this.LigatureComponent = data.LigatureComponent; + this.MarkAttachment = data.MarkAttachment; + this.CursiveAttachment = data.CursiveAttachment; + this.IsSubstituted = data.IsSubstituted; + this.IsDecomposed = data.IsDecomposed; + this.IsPlaceholder = data.IsPlaceholder; + this.BidiRun = data.BidiRun; + this.IsPositioned = data.IsPositioned; + this.IsKerned = data.IsKerned; + + if (data.UniversalShapingEngineInfo != null) + { + this.UniversalShapingEngineInfo = new( + data.UniversalShapingEngineInfo.Category, + data.UniversalShapingEngineInfo.SyllableType, + data.UniversalShapingEngineInfo.Syllable); + } + + if (data.IndicShapingEngineInfo != null) + { + this.IndicShapingEngineInfo = new( + data.IndicShapingEngineInfo.Category, + data.IndicShapingEngineInfo.Position, + data.IndicShapingEngineInfo.SyllableType, + data.IndicShapingEngineInfo.Syllable); + } + + if (!clearFeatures) + { + this.Features.AddRange(data.Features); + foreach (Tag tag in data.EnabledFeatureTags) + { + this.EnabledFeatureTags.Add(tag); + } + } + + foreach (Tag feature in data.AppliedFeatures) + { + this.AppliedFeatures.Add(feature); + } + + this.Bounds = data.Bounds; + this.CachedShapingClass = data.CachedShapingClass; + this.ShapingClassCacheKey = data.ShapingClassCacheKey; + } + + /// + /// Gets or sets the glyph id. Setting this value invalidates the cached shaping class. + /// + public ushort GlyphId + { + get => this.glyphId; + set + { + if (this.glyphId != value) + { + this.glyphId = value; + this.ShapingClassCacheKey = -1; + } + } + } + + /// + /// Gets or sets the cached glyph shaping class, avoiding repeated GDEF lookups. + /// + internal GlyphShapingClass CachedShapingClass { get; set; } + + /// + /// Gets or sets the cache key for . + /// A value of -1 indicates the cache is invalid. Valid entries store the glyph id. + /// + internal int ShapingClassCacheKey { get; set; } = -1; + + /// + /// Gets or sets the leading codepoint. + /// + public CodePoint CodePoint { get; set; } + + /// + /// Gets or sets the codepoint count represented by this glyph. + /// + public int CodePointCount { get; set; } = 1; + + /// + /// Gets or sets the text direction. + /// + public TextDirection Direction { get; set; } + + /// + /// Gets or sets the text run this glyph belongs to. + /// + public TextRun TextRun { get; set; } + + /// + /// Gets or sets the id of any ligature this glyph is a member of. + /// + public int LigatureId { get; set; } + + /// + /// Gets or sets a value indicating whether the glyph is ligated. + /// + public bool IsLigated { get; set; } + + /// + /// Gets or sets the ligature component index of the glyph. + /// + public int LigatureComponent { get; set; } = -1; + + /// + /// Gets or sets the index of any mark attachment. + /// + public int MarkAttachment { get; set; } = -1; + + /// + /// Gets or sets the index of any cursive attachment. + /// + public int CursiveAttachment { get; set; } = -1; + + /// + /// Gets or sets the collection of features. + /// + public List Features { get; set; } = []; + + /// + /// Gets the set of feature tags that are currently enabled, maintained + /// in sync with for O(1) lookup. + /// + internal HashSet EnabledFeatureTags { get; } = []; + + /// + /// Gets or sets the collection of applied features. + /// + public HashSet AppliedFeatures { get; set; } = []; + + /// + /// Gets or sets the shaping bounds. + /// + public GlyphShapingBounds Bounds { get; set; } = new(0, 0, 0, 0); + + /// + /// Gets or sets a value indicating whether this glyph is the result of a substitution. + /// + public bool IsSubstituted { get; set; } + + /// + /// Gets or sets a value indicating whether this glyph is the result of a decomposition substitution + /// + public bool IsDecomposed { get; set; } + + /// + /// Gets or sets a value indicating whether this glyph represents an inline placeholder. + /// + public bool IsPlaceholder { get; set; } + + /// + /// Gets or sets the bidi run assigned to an inline placeholder. + /// + public BidiRun BidiRun { get; set; } + + /// + /// Gets or sets a value indicating whether this glyph has been positioned. + /// + public bool IsPositioned { get; set; } + + /// + /// Gets or sets a value indicating whether this glyph has been kerned. + /// + public bool IsKerned { get; set; } + + /// + /// Gets or sets the universal shaping information. + /// + public UniversalShapingEngineInfo? UniversalShapingEngineInfo { get; set; } + + /// + /// Gets or sets the Indic shaping information. + /// + public IndicShapingEngineInfo? IndicShapingEngineInfo { get; set; } + + private string DebuggerDisplay + => FormattableString + .Invariant($" {this.GlyphId} : {this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : {this.TextRun.TextAttributes} : {this.LigatureId} : {this.LigatureComponent} : {this.IsDecomposed}"); + + internal string ToDebuggerDisplay() => this.DebuggerDisplay; + } + + /// + /// Represents information required for universal shaping. + /// + internal class UniversalShapingEngineInfo + { + public UniversalShapingEngineInfo(string category, string syllableType, int syllable) + { + this.Category = category; + this.SyllableType = syllableType; + this.Syllable = syllable; + } + + public string Category { get; set; } + + public string SyllableType { get; set; } + + public int Syllable { get; set; } + } + + internal class IndicShapingEngineInfo + { + public IndicShapingEngineInfo( + Categories category, + Positions position, + string syllableType, + int syllable) + { + this.Category = category; + this.Position = position; + this.SyllableType = syllableType; + this.Syllable = syllable; + } + + public Categories Category { get; set; } + + public MyanmarCategories MyanmarCategory => (MyanmarCategories)this.Category; + + public Positions Position { get; set; } + + public string SyllableType { get; set; } + + public int Syllable { get; set; } + } +} diff --git a/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/SixLabors.Fonts/GlyphSubstitutionCollection.cs new file mode 100644 index 0000000..7843a47 --- /dev/null +++ b/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -0,0 +1,464 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// Represents a collection of glyph indices that are mapped to input codepoints. + /// + internal sealed class GlyphSubstitutionCollection : IGlyphShapingCollection + { + /// + /// Contains a map the index of a map within the collection, non-sequential codepoint offsets, and their glyph ids. + /// + private readonly List glyphs = []; + + /// + /// Initializes a new instance of the class. + /// + /// The text options. + public GlyphSubstitutionCollection(TextOptions textOptions) => this.TextOptions = textOptions; + + /// + /// Gets the number of glyphs ids contained in the collection. + /// This may be more or less than original input codepoint count (due to substitution process). + /// + public int Count => this.glyphs.Count; + + /// + public TextOptions TextOptions { get; } + + /// + /// Gets or sets the running id of any ligature glyphs contained withing this collection are a member of. + /// + public int LigatureId { get; set; } = 1; + + /// + public GlyphShapingData this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.glyphs[index].Data; + } + + /// + /// Gets the shaping data at the specified position. + /// + /// The zero-based index of the elements to get. + /// The zero-based index within the input codepoint collection. + /// The . + internal GlyphShapingData GetGlyphShapingData(int index, out int offset) + { + OffsetGlyphDataPair pair = this.glyphs[index]; + offset = pair.Offset; + return pair.Data; + } + + /// + public void AddShapingFeature(int index, TagEntry feature) + { + GlyphShapingData data = this.glyphs[index].Data; + data.Features.Add(feature); + if (feature.Enabled) + { + data.EnabledFeatureTags.Add(feature.Tag); + } + } + + /// + public void EnableShapingFeature(int index, Tag feature) + { + GlyphShapingData data = this.glyphs[index].Data; + List features = data.Features; + for (int i = 0; i < features.Count; i++) + { + TagEntry tagEntry = features[i]; + if (tagEntry.Tag == feature) + { + tagEntry.Enabled = true; + features[i] = tagEntry; + data.EnabledFeatureTags.Add(feature); + break; + } + } + } + + /// + public void DisableShapingFeature(int index, Tag feature) + { + GlyphShapingData data = this.glyphs[index].Data; + List features = data.Features; + for (int i = 0; i < features.Count; i++) + { + TagEntry tagEntry = features[i]; + if (tagEntry.Tag == feature) + { + tagEntry.Enabled = false; + features[i] = tagEntry; + data.EnabledFeatureTags.Remove(feature); + break; + } + } + } + + /// + /// Adds a clone of the glyph shaping data to the collection at the specified offset. + /// + /// The data. + /// The zero-based index within the input codepoint collection. + public void AddGlyph(GlyphShapingData data, int offset) + => this.glyphs.Add(new(offset, new(data, false))); + + /// + /// Adds the glyph id and the codepoint it represents to the collection. + /// + /// The id of the glyph to add. + /// The codepoint the glyph represents. + /// The resolved text direction for the codepoint. + /// The text run this glyph belongs to. + /// The zero-based index within the input codepoint collection. + public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection direction, TextRun textRun, int offset) + => this.glyphs.Add(new(offset, new(textRun) + { + CodePoint = codePoint, + Direction = direction, + GlyphId = glyphId, + })); + + /// + /// Adds an atomic inline placeholder to the collection. + /// + /// The object replacement codepoint used for Unicode processing. + /// The resolved bidi run for the placeholder. + /// The text run this placeholder belongs to. + /// The zero-based index within the input codepoint collection. + public void AddPlaceholder(CodePoint codePoint, BidiRun bidiRun, TextRun textRun, int offset) + => this.glyphs.Add(new(offset, new(textRun) + { + CodePoint = codePoint, + Direction = (TextDirection)bidiRun.Direction, + GlyphId = 0, + IsPlaceholder = true, + BidiRun = bidiRun, + })); + + /// + /// Moves the specified glyph to the specified position. + /// + /// The index to move from. + /// The index to move to. + public void MoveGlyph(int fromIndex, int toIndex) + { + if (fromIndex == toIndex) + { + return; + } + + GlyphShapingData data = this[fromIndex]; + if (fromIndex > toIndex) + { + // Move item to the right + for (int i = fromIndex; i > toIndex; i--) + { + this.glyphs[i].Data = this.glyphs[i - 1].Data; + } + } + else + { + // Move item to the left + for (int i = fromIndex; i < toIndex; i++) + { + this.glyphs[i].Data = this.glyphs[i + 1].Data; + } + } + + this.glyphs[toIndex].Data = data; + } + + /// + /// Reverses the order of elements in the specified range of the collection. + /// + /// + /// The range is interpreted as half-open, from (inclusive) + /// to (exclusive). + /// + /// Both indices are clamped to the valid range [0, ]. + /// If the resulting range contains fewer than two elements, the method performs no action. + /// The method does not throw if either index is equal to ; in such + /// cases the range is considered valid but may be empty. + /// + /// + /// The zero-based index at which to start reversing (inclusive). This value should be + /// greater than or equal to 0. Values greater than are treated as + /// . + /// + /// + /// The zero-based index at which to stop reversing (exclusive). This value should be + /// greater than or equal to . Values greater than + /// are treated as . + /// + public void ReverseRange(int startIndex, int endIndex) + { + int s = Math.Min(startIndex, this.Count); + int e = Math.Min(endIndex, this.Count); + + if (e < s + 2) + { + return; + } + + this.glyphs.Reverse(s, e - s); + } + + /// + /// Performs a stable sort of the glyphs by the comparison delegate starting at the specified index. + /// Only the references are reordered; offsets remain in place. + /// + /// The start index. + /// The end index. + /// The comparison delegate. + public void Sort(int startIndex, int endIndex, Comparison comparer) + { + // Stable insertion sort using adjacent swaps of Data references. + // The sorted ranges are typically small (syllable clusters of 2-10 glyphs), + // so insertion sort is optimal and avoids allocations. Adjacent swaps + // replace the previous MoveGlyph approach which shifted all intermediate elements. + List glyphs = this.glyphs; + for (int i = startIndex + 1; i < endIndex; i++) + { + int j = i; + while (j > startIndex && comparer(glyphs[j - 1].Data, glyphs[j].Data) > 0) + { + // Swap Data references between adjacent slots. + (glyphs[j].Data, glyphs[j - 1].Data) = (glyphs[j - 1].Data, glyphs[j].Data); + j--; + } + } + } + + /// + /// Removes all elements from the collection. + /// + public void Clear() + { + this.glyphs.Clear(); + this.LigatureId = 1; + } + + /// + /// Gets the specified glyph ids matching the given codepoint offset. + /// + /// The zero-based index within the input codepoint collection. + /// + /// When this method returns, contains the shaping data associated with the specified offset, + /// if the value is found; otherwise, the default value for the type of the data parameter. + /// This parameter is passed uninitialized. + /// + /// + /// if the contains glyph ids + /// for the specified offset; otherwise, . + /// + public bool TryGetGlyphShapingDataAtOffset(int offset, [NotNullWhen(true)] out IReadOnlyList? data) + { + List match = []; + for (int i = 0; i < this.glyphs.Count; i++) + { + if (this.glyphs[i].Offset == offset) + { + match.Add(this.glyphs[i].Data); + } + else if (match.Count > 0) + { + // Offsets, though non-sequential, are sorted, so we can stop searching. + break; + } + } + + data = match; + return match.Count > 0; + } + + /// + /// Performs a 1:1 replacement of a glyph id at the given position. + /// + /// The zero-based index of the element to replace. + /// The replacement glyph id. + /// The feature to apply to the glyph at the specified index. + public void Replace(int index, ushort glyphId, Tag feature) + { + GlyphShapingData current = this.glyphs[index].Data; + current.GlyphId = glyphId; + current.LigatureId = 0; + current.LigatureComponent = -1; + current.MarkAttachment = -1; + current.CursiveAttachment = -1; + current.IsSubstituted = true; + current.AppliedFeatures.Add(feature); + } + + /// + /// Performs a 1:1 replacement of a glyph id at the given position while removing a series of glyph ids at the given positions within the sequence. + /// + /// The zero-based index of the element to replace. + /// The indices at which to remove elements. + /// The replacement glyph id. + /// The ligature id. + /// The feature to apply to the glyph at the specified index. + public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, int ligatureId, Tag feature) + { + // Remove the glyphs at each index. + int codePointCount = 0; + CodePoint codePoint = default; + for (int i = removalIndices.Length - 1; i >= 0; i--) + { + int match = removalIndices[i]; + codePointCount += this.glyphs[match].Data.CodePointCount; + CodePoint currentCodePoint = this.glyphs[match].Data.CodePoint; + if (!UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) || UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint)) + { + if (!CodePoint.IsZeroWidthJoiner(currentCodePoint) && !CodePoint.IsZeroWidthNonJoiner(currentCodePoint)) + { + codePoint = currentCodePoint; + } + } + + this.glyphs.RemoveAt(match); + } + + // Assign our new id at the index. + GlyphShapingData current = this.glyphs[index].Data; + if (codePoint != default) + { + current.CodePoint = codePoint; + } + + current.CodePointCount += codePointCount; + current.GlyphId = glyphId; + current.LigatureId = ligatureId; + current.IsLigated = true; + current.LigatureComponent = -1; + current.MarkAttachment = -1; + current.CursiveAttachment = -1; + current.IsSubstituted = true; + current.AppliedFeatures.Add(feature); + } + + /// + /// Performs a 1:1 replacement of a glyph id at the given position while removing a series of glyph ids. + /// + /// The zero-based index of the element to replace. + /// The number of glyphs to remove. + /// The replacement glyph id. + /// The feature to apply to the glyph at the specified index. + public void Replace(int index, int count, ushort glyphId, Tag feature) + { + // Remove the glyphs at each index. + int codePointCount = 0; + CodePoint codePoint = default; + for (int i = count; i > 0; i--) + { + int match = index + i; + codePointCount += this.glyphs[match].Data.CodePointCount; + CodePoint currentCodePoint = this.glyphs[match].Data.CodePoint; + if (!UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) || UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint)) + { + if (!CodePoint.IsZeroWidthJoiner(currentCodePoint) && !CodePoint.IsZeroWidthNonJoiner(currentCodePoint)) + { + codePoint = currentCodePoint; + } + } + + this.glyphs.RemoveAt(match); + } + + // Assign our new id at the index. + GlyphShapingData current = this.glyphs[index].Data; + if (codePoint != default) + { + current.CodePoint = codePoint; + } + + current.CodePointCount += codePointCount; + current.GlyphId = glyphId; + current.LigatureId = 0; + current.LigatureComponent = -1; + current.MarkAttachment = -1; + current.CursiveAttachment = -1; + current.IsSubstituted = true; + current.AppliedFeatures.Add(feature); + } + + /// + /// Replaces a single glyph id with a collection of glyph ids. + /// + /// The zero-based index of the element to replace. + /// The collection of replacement glyph ids. + /// The feature to apply to the glyph at the specified index. + public void Replace(int index, ReadOnlySpan glyphIds, Tag feature) + { + if (glyphIds.Length > 0) + { + OffsetGlyphDataPair pair = this.glyphs[index]; + GlyphShapingData current = pair.Data; + current.GlyphId = glyphIds[0]; + current.LigatureComponent = 0; + current.MarkAttachment = -1; + current.CursiveAttachment = -1; + current.IsSubstituted = true; + current.IsDecomposed = true; + + // Add additional glyphs from the rest of the sequence. + if (glyphIds.Length > 1) + { + glyphIds = glyphIds[1..]; + for (int i = 0; i < glyphIds.Length; i++) + { + GlyphShapingData data = new(current, false) + { + GlyphId = glyphIds[i], + LigatureComponent = i + 1 + }; + + data.AppliedFeatures.Add(feature); + + this.glyphs.Insert(++index, new(pair.Offset, data)); + } + } + } + else + { + // Spec disallows removal of glyphs in this manner but it's common enough practice to allow it. + // https://github.com/MicrosoftDocs/typography-issues/issues/673 + this.glyphs.RemoveAt(index); + } + } + + public void Insert(int index, GlyphShapingData data) + { + OffsetGlyphDataPair pair = this.glyphs[index]; + this.glyphs.Insert(index, new(pair.Offset, data)); + } + + [DebuggerDisplay("{DebuggerDisplay,nq}")] + private class OffsetGlyphDataPair + { + public OffsetGlyphDataPair(int offset, GlyphShapingData data) + { + this.Offset = offset; + this.Data = data; + } + + public int Offset { get; set; } + + public GlyphShapingData Data { get; set; } + + private string DebuggerDisplay => FormattableString.Invariant($"Offset: {this.Offset}, Data: {this.Data.ToDebuggerDisplay()}"); + } + } +} diff --git a/SixLabors.Fonts/GlyphType.cs b/SixLabors.Fonts/GlyphType.cs new file mode 100644 index 0000000..cffb363 --- /dev/null +++ b/SixLabors.Fonts/GlyphType.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Represents the various versions of a glyph records. + /// + public enum GlyphType + { + /// + /// This is a fall back glyph due to a missing code point. + /// + Fallback, + + /// + /// This is a standard glyph to be drawn in the style the user defines. + /// + Standard, + + /// + /// This is a multi-layer colored glyph (emoji). + /// + Painted, + + /// + /// This is an atomic inline placeholder supplied by the caller. + /// + Placeholder + } +} diff --git a/SixLabors.Fonts/GraphemeMetrics.cs b/SixLabors.Fonts/GraphemeMetrics.cs new file mode 100644 index 0000000..ba8241a --- /dev/null +++ b/SixLabors.Fonts/GraphemeMetrics.cs @@ -0,0 +1,81 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Represents one coalesced grapheme in final layout order. + /// + public readonly struct GraphemeMetrics + { + /// + /// Initializes a new instance of the struct. + /// + /// The positioned logical advance rectangle for the grapheme in pixel units. + /// The rendered glyph bounds for the grapheme in pixel units. + /// The union of the positioned logical advance bounds and rendered glyph bounds in pixel units. + /// The font used to shape and render the grapheme. + /// The grapheme index in the original text. + /// The UTF-16 index in the original text where the grapheme begins. + /// The resolved bidi embedding level. + /// Whether the grapheme represents a line break. + internal GraphemeMetrics( + FontRectangle advance, + FontRectangle bounds, + FontRectangle renderableBounds, + Font font, + int graphemeIndex, + int stringIndex, + int bidiLevel, + bool isLineBreak) + { + this.Advance = advance; + this.Bounds = bounds; + this.RenderableBounds = renderableBounds; + this.Font = font; + this.GraphemeIndex = graphemeIndex; + this.StringIndex = stringIndex; + this.BidiLevel = bidiLevel; + this.IsLineBreak = isLineBreak; + } + + /// + /// Gets the positioned logical advance rectangle for the grapheme in pixel units. + /// + public FontRectangle Advance { get; } + + /// + /// Gets the rendered glyph bounds for the grapheme in pixel units. + /// + public FontRectangle Bounds { get; } + + /// + /// Gets the union of the positioned logical advance bounds and rendered glyph bounds in pixel units. + /// + public FontRectangle RenderableBounds { get; } + + /// + /// Gets the font used to shape and render the grapheme. + /// + public Font Font { get; } + + /// + /// Gets the zero-based grapheme index in the original text. + /// + public int GraphemeIndex { get; } + + /// + /// Gets the zero-based UTF-16 code unit index in the original text. + /// + public int StringIndex { get; } + + /// + /// Gets the resolved bidi embedding level. + /// + internal int BidiLevel { get; } + + /// + /// Gets a value indicating whether this grapheme represents a line break. + /// + public bool IsLineBreak { get; } + } +} diff --git a/SixLabors.Fonts/HintingMode.cs b/SixLabors.Fonts/HintingMode.cs new file mode 100644 index 0000000..5af83aa --- /dev/null +++ b/SixLabors.Fonts/HintingMode.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Defines modes to determine how to apply hinting. The use of mathematical instructions + /// to adjust the display of an outline font so that it lines up with a rasterized grid. + /// + public enum HintingMode + { + /// + /// Do not hint the glyphs. + /// + None, + + /// + /// Hint the glyph using standard configuration. + /// + Standard + } +} diff --git a/SixLabors.Fonts/HorizontalAlignment.cs b/SixLabors.Fonts/HorizontalAlignment.cs new file mode 100644 index 0000000..3775dcd --- /dev/null +++ b/SixLabors.Fonts/HorizontalAlignment.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Horizontal alignment modes. + /// + public enum HorizontalAlignment + { + /// + /// Aligns text from the left. + /// + Left = 0, + + /// + /// Aligns text from the right. + /// + Right = 1, + + /// + /// Aligns text from the center. + /// + Center = 2 + } +} diff --git a/SixLabors.Fonts/HorizontalMetrics.cs b/SixLabors.Fonts/HorizontalMetrics.cs new file mode 100644 index 0000000..bfda95b --- /dev/null +++ b/SixLabors.Fonts/HorizontalMetrics.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Represent the metrics of a font face specific to horizontal text. + /// + public class HorizontalMetrics : IMetricsHeader + { + /// + public short Ascender { get; internal set; } + + /// + public short Descender { get; internal set; } + + /// + public short LineGap { get; internal set; } + + /// + public short LineHeight { get; internal set; } + + /// + public short AdvanceWidthMax { get; internal set; } + + /// + public short AdvanceHeightMax { get; internal set; } + } +} diff --git a/SixLabors.Fonts/IFontCollection.cs b/SixLabors.Fonts/IFontCollection.cs new file mode 100644 index 0000000..f8db5cf --- /dev/null +++ b/SixLabors.Fonts/IFontCollection.cs @@ -0,0 +1,149 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Globalization; +using System.IO; + +namespace SixLabors.Fonts { + /// + /// A readable and writable collection of fonts. + /// + /// + public interface IFontCollection : IReadOnlyFontCollection + { + /// + /// Adds a font to the collection. + /// + /// The filesystem path to the font file. + /// The new . + public FontFamily Add(string path); + + /// + /// Adds a font to the collection. + /// + /// The filesystem path to the font file. + /// The description of the added font. + /// The new . + public FontFamily Add(string path, out FontDescription description); + + /// + /// Adds a font to the collection. + /// + /// The font stream. + /// The new . + public FontFamily Add(Stream stream); + + /// + /// Adds a font to the collection. + /// + /// The font stream. + /// The description of the added font. + /// The new . + public FontFamily Add(Stream stream, out FontDescription description); + + /// + /// Adds a true type font collection (.ttc). + /// + /// The font collection path. + /// A read-only memory region containing the new values. + public ReadOnlyMemory AddCollection(string path); + + /// + /// Adds a true type font collection (.ttc). + /// + /// The font collection path. + /// The descriptions of the added fonts. + /// A read-only memory region containing the new values. + public ReadOnlyMemory AddCollection(string path, out ReadOnlyMemory descriptions); + + /// + /// Adds a true type font collection (.ttc). + /// + /// The font stream. + /// A read-only memory region containing the new values. + public ReadOnlyMemory AddCollection(Stream stream); + + /// + /// Adds a true type font collection (.ttc). + /// + /// The font stream. + /// The descriptions of the added fonts. + /// A read-only memory region containing the new values. + public ReadOnlyMemory AddCollection(Stream stream, out ReadOnlyMemory descriptions); + + /// + /// Adds a font to the collection. + /// + /// The filesystem path to the font file. + /// The culture of the font to add. + /// The new . + public FontFamily AddWithCulture(string path, CultureInfo culture); + + /// + /// Adds a font to the collection. + /// + /// The filesystem path to the font file. + /// The culture of the font to add. + /// The description of the added font. + /// The new . + public FontFamily AddWithCulture(string path, CultureInfo culture, out FontDescription description); + + /// + /// Adds a font to the collection. + /// + /// The font stream. + /// The culture of the font to add. + /// The new . + public FontFamily AddWithCulture(Stream stream, CultureInfo culture); + + /// + /// Adds a font to the collection. + /// + /// The font stream. + /// The culture of the font to add. + /// The description of the added font. + /// The new . + public FontFamily AddWithCulture(Stream stream, CultureInfo culture, out FontDescription description); + + /// + /// Adds a true type font collection (.ttc). + /// + /// The font collection path. + /// The culture of the fonts to add. + /// A read-only memory region containing the new values. + public ReadOnlyMemory AddCollection(string path, CultureInfo culture); + + /// + /// Adds a true type font collection (.ttc). + /// + /// The font collection path. + /// The culture of the fonts to add. + /// The descriptions of the added fonts. + /// A read-only memory region containing the new values. + public ReadOnlyMemory AddCollection( + string path, + CultureInfo culture, + out ReadOnlyMemory descriptions); + + /// + /// Adds a true type font collection (.ttc). + /// + /// The font stream. + /// The culture of the fonts to add. + /// A read-only memory region containing the new values. + public ReadOnlyMemory AddCollection(Stream stream, CultureInfo culture); + + /// + /// Adds a true type font collection (.ttc). + /// + /// The font stream. + /// The culture of the fonts to add. + /// The descriptions of the added fonts. + /// A read-only memory region containing the new values. + public ReadOnlyMemory AddCollection( + Stream stream, + CultureInfo culture, + out ReadOnlyMemory descriptions); + } +} diff --git a/SixLabors.Fonts/IFontMetricsCollection.cs b/SixLabors.Fonts/IFontMetricsCollection.cs new file mode 100644 index 0000000..a831abb --- /dev/null +++ b/SixLabors.Fonts/IFontMetricsCollection.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; + +namespace SixLabors.Fonts { + /// + /// Represents a collection of + /// + internal interface IFontMetricsCollection : IReadOnlyFontMetricsCollection + { + /// + /// Adds the font metrics and culture to the . + /// + /// The font metrics to add. + /// The culture of the font metrics to add. + /// The new . + public FontFamily AddMetrics(FontMetrics metrics, CultureInfo culture); + + /// + /// Adds the font metrics to the . + /// + /// The font metrics to add. + public void AddMetrics(FontMetrics metrics); + } +} diff --git a/SixLabors.Fonts/IGlyphShapingCollection.cs b/SixLabors.Fonts/IGlyphShapingCollection.cs new file mode 100644 index 0000000..9a70afd --- /dev/null +++ b/SixLabors.Fonts/IGlyphShapingCollection.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic; + +namespace SixLabors.Fonts { + /// + /// Defines the contract for glyph shaping collections. + /// + internal interface IGlyphShapingCollection + { + /// + /// Gets the collection count. + /// + public int Count { get; } + + /// + /// Gets the text options used by this collection. + /// + public TextOptions TextOptions { get; } + + /// + /// Gets the glyph shaping data at the specified index. + /// + /// The zero-based index of the elements to get. + /// The . + public GlyphShapingData this[int index] { get; } + + /// + /// Adds the shaping feature to the collection which should be applied to the glyph at a specified index. + /// + /// The zero-based index of the element. + /// The feature to apply. + public void AddShapingFeature(int index, TagEntry feature); + + /// + /// Enables a previously added shaping feature. + /// + /// The zero-based index of the element. + /// The feature to enable. + public void EnableShapingFeature(int index, Tag feature); + + /// + /// Disables a previously added shaping feature. + /// + /// The zero-based index of the element. + /// The feature to disable. + public void DisableShapingFeature(int index, Tag feature); + } +} diff --git a/SixLabors.Fonts/IMetricsHeader.cs b/SixLabors.Fonts/IMetricsHeader.cs new file mode 100644 index 0000000..7a96d9a --- /dev/null +++ b/SixLabors.Fonts/IMetricsHeader.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Defines the contract for the metrics header of a font face. + /// + public interface IMetricsHeader + { + /// + /// Gets the typographic ascender of the face, expressed in font units. + /// + public short Ascender { get; } + + /// + /// Gets the typographic descender of the face, expressed in font units. + /// + public short Descender { get; } + + /// + /// Gets the typographic line gap of the face, expressed in font units. + /// This field should be combined with the and + /// values to determine default line spacing. + /// + public short LineGap { get; } + + /// + /// Gets the typographic line spacing of the face, expressed in font units. + /// + public short LineHeight { get; } + + /// + /// Gets the maximum advance width, in font units, for all glyphs in this face. + /// + public short AdvanceWidthMax { get; } + + /// + /// Gets the maximum advance height, in font units, for all glyphs in this + /// face.This is only relevant for vertical layouts, and is set to for + /// fonts that do not provide vertical metrics. + /// + public short AdvanceHeightMax { get; } + } +} diff --git a/SixLabors.Fonts/IO/ZlibInflateStream.cs b/SixLabors.Fonts/IO/ZlibInflateStream.cs new file mode 100644 index 0000000..6b4803a --- /dev/null +++ b/SixLabors.Fonts/IO/ZlibInflateStream.cs @@ -0,0 +1,212 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; +using System.IO.Compression; + +namespace SixLabors.Fonts.IO { + internal sealed class ZlibInflateStream : Stream + { + private long position; + + /// + /// The raw stream containing the uncompressed image data. + /// + private readonly Stream rawStream; + + /// + /// A value indicating whether this instance of the given entity has been disposed. + /// + /// if this instance has been disposed; otherwise, . + /// + /// If the entity is disposed, it must not be disposed a second + /// time. The isDisposed field is set the first time the entity + /// is disposed. If the isDisposed field is true, then the Dispose() + /// method will not dispose again. This help not to prolong the entity's + /// life in the Garbage Collector. + /// + private bool isDisposed; + + /// + /// The read crc data. + /// + private byte[]? crcRead; + + /// + /// The stream responsible for decompressing the input stream. + /// + private DeflateStream? deflateStream; + + /// + /// Initializes a new instance of the class. + /// + /// The stream. + /// + /// Thrown if the compression method is incorrect. + /// + public ZlibInflateStream(Stream stream) + { + // The DICT dictionary identifier identifying the used dictionary. + + // The preset dictionary. + bool fdict; + this.rawStream = stream; + + // Read the zlib header : http://tools.ietf.org/html/rfc1950 + // CMF(Compression Method and flags) + // This byte is divided into a 4 - bit compression method and a + // 4-bit information field depending on the compression method. + // bits 0 to 3 CM Compression method + // bits 4 to 7 CINFO Compression info + // + // 0 1 + // +---+---+ + // |CMF|FLG| + // +---+---+ + int cmf = this.rawStream.ReadByte(); + int flag = this.rawStream.ReadByte(); + if (cmf == -1 || flag == -1) + { + return; + } + + if ((cmf & 0x0f) != 8) + { + throw new IOException($"Bad compression method for ZLIB header: cmf={cmf}"); + } + + // CINFO is the base-2 logarithm of the LZ77 window size, minus eight. + // int cinfo = ((cmf & (0xf0)) >> 8); + fdict = (flag & 32) != 0; + + if (fdict) + { + // The DICT dictionary identifier identifying the used dictionary. + byte[] dictId = new byte[4]; + + for (int i = 0; i < 4; i++) + { + // We consume but don't use this. + dictId[i] = (byte)this.rawStream.ReadByte(); + } + } + + // Initialize the deflate Stream. + this.deflateStream = new DeflateStream(this.rawStream, CompressionMode.Decompress, true); + } + + /// + public override bool CanRead => true; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => false; + + /// + public override long Length => throw new NotSupportedException(); + + /// + public override long Position + { + get => this.position; + + set => throw new NotSupportedException(); + } + + /// + public override void Flush() + => this.deflateStream?.Flush(); + + /// + public override int Read(byte[] buffer, int offset, int count) + { + ObjectDisposedException.ThrowIf(this.deflateStream is null, this.GetType()); + + // We don't check CRC on reading + int read = this.deflateStream.Read(buffer, offset, count); + if (read < 1 && this.crcRead is null) + { + // The deflater has ended. We try to read the next 4 bytes from raw stream (crc) + this.crcRead = new byte[4]; + for (int i = 0; i < 4; i++) + { + // we don't really check/use this + this.crcRead[i] = (byte)this.rawStream.ReadByte(); + } + } + + this.position += read; + return read; + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + origin = SeekOrigin.Current; + offset -= this.position; + } + + if (origin == SeekOrigin.Current && offset >= 0) + { + // consume bytes + for (int i = 0; i < offset; i++) + { + this.ReadByte(); + } + + return this.position; + } + + throw new NotSupportedException("can only seek forwards"); + } + + /// + public override void SetLength(long value) + => throw new NotSupportedException(); + + /// + public override void Write(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + + /// + protected override void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + if (disposing) + { + // dispose managed resources + if (this.deflateStream != null) + { + this.deflateStream.Dispose(); + this.deflateStream = null; + + if (this.crcRead is null) + { + // Consume the trailing 4 bytes + this.crcRead = new byte[4]; + for (int i = 0; i < 4; i++) + { + this.crcRead[i] = (byte)this.rawStream.ReadByte(); + } + } + } + } + + base.Dispose(disposing); + + // Call the appropriate methods to clean up + // unmanaged resources here. + // Note disposing is done. + this.isDisposed = true; + } + } +} diff --git a/SixLabors.Fonts/IReadOnlySystemFontCollection.cs b/SixLabors.Fonts/IReadOnlySystemFontCollection.cs new file mode 100644 index 0000000..ee719be --- /dev/null +++ b/SixLabors.Fonts/IReadOnlySystemFontCollection.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.Fonts { + /// + /// Represents a readonly collection of Operating System fonts. + /// + public interface IReadOnlySystemFontCollection : IReadOnlyFontCollection + { + /// + /// + /// Gets the collection of Operating System directories that were searched for font families. + /// + /// + public IEnumerable SearchDirectories { get; } + } +} diff --git a/SixLabors.Fonts/IReadonlyFontCollection.cs b/SixLabors.Fonts/IReadonlyFontCollection.cs new file mode 100644 index 0000000..db06723 --- /dev/null +++ b/SixLabors.Fonts/IReadonlyFontCollection.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Globalization; + +namespace SixLabors.Fonts { + /// + /// Represents a readonly collection of fonts. + /// + public interface IReadOnlyFontCollection + { + /// + /// Gets the collection of in this + /// using the invariant culture. + /// + public IEnumerable Families { get; } + + /// + /// Gets the specified font family matching the invariant culture and font family name. + /// + /// The font family name. + /// The first matching the given name. + /// is + /// The collection contains no matches. + public FontFamily Get(string name); + + /// + /// Gets the specified font family matching the invariant culture and font family name. + /// + /// The font family name. + /// + /// When this method returns, contains the family associated with the specified name, + /// if the name is found; otherwise, the default value for the type of the family parameter. + /// This parameter is passed uninitialized. + /// + /// + /// if the contains a family + /// with the specified name; otherwise, . + /// + /// is + public bool TryGet(string name, out FontFamily family); + + /// + /// Gets the collection of in this + /// using the given culture. + /// + /// The culture of the families to return. + /// The . + public IEnumerable GetByCulture(CultureInfo culture); + + /// + /// Gets the specified font family matching the given culture and font family name. + /// + /// The font family name. + /// The culture to use when searching for a match. + /// The first matching the given name. + /// is + /// The collection contains no matches. + public FontFamily GetByCulture(string name, CultureInfo culture); + + /// + /// Gets the specified font family matching the given culture and font family name. + /// + /// The font family name. + /// The culture to use when searching for a match. + /// + /// When this method returns, contains the family associated with the specified name, + /// if the name is found; otherwise, the default value for the type of the family parameter. + /// This parameter is passed uninitialized. + /// + /// + /// if the contains a family + /// with the specified name; otherwise, . + /// + /// is + public bool TryGetByCulture(string name, CultureInfo culture, out FontFamily family); + } +} diff --git a/SixLabors.Fonts/IReadonlyFontMetricsCollection.cs b/SixLabors.Fonts/IReadonlyFontMetricsCollection.cs new file mode 100644 index 0000000..48dc03c --- /dev/null +++ b/SixLabors.Fonts/IReadonlyFontMetricsCollection.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace SixLabors.Fonts { + /// + /// Represents a readonly collection of font metrics. + /// The interface uses compiler pattern matching to provide enumeration capabilities. + /// + internal interface IReadOnlyFontMetricsCollection + { + /// + /// Gets the specified font metrics matching the given culture and font family name. + /// + /// The font family name. + /// The culture to use when searching for a match. + /// The font style to use when searching for a match. + /// + /// When this method returns, contains the metrics associated with the specified name, + /// if the name is found; otherwise, the default value for the type of the family parameter. + /// This parameter is passed uninitialized. + /// + /// + /// if the contains font metrics + /// with the specified name; otherwise, . + /// + /// is + public bool TryGetMetrics(string name, CultureInfo culture, FontStyle style, [NotNullWhen(true)] out FontMetrics? metrics); + + /// + /// Gets the collection of available font metrics for a given culture and font family name. + /// + /// The font family name. + /// The culture to use when searching for a match. + /// The . + /// is + public IEnumerable GetAllMetrics(string name, CultureInfo culture); + + /// + /// Gets the collection of available font styles for a given culture and font family name. + /// + /// The font family name. + /// The culture to use when searching for a match. + /// A read-only memory region containing the available font styles. + /// is + public ReadOnlyMemory GetAllStyles(string name, CultureInfo culture); + + /// + public IEnumerator GetEnumerator(); + } +} diff --git a/SixLabors.Fonts/KerningMode.cs b/SixLabors.Fonts/KerningMode.cs new file mode 100644 index 0000000..69f95f1 --- /dev/null +++ b/SixLabors.Fonts/KerningMode.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Kerning is the contextual adjustment of inter-glyph spacing. + /// This property controls metric kerning, kerning that utilizes adjustment data contained in the font. + /// + public enum KerningMode + { + /// + /// Specifies that kerning is applied. + /// + Standard, + + /// + /// Specifies that kerning is not applied. + /// + None, + + /// + /// Specifies that kerning is applied at the discretion of the layout engine. + /// + Auto, + } +} diff --git a/SixLabors.Fonts/KnownVariationAxes.cs b/SixLabors.Fonts/KnownVariationAxes.cs new file mode 100644 index 0000000..5073d7e --- /dev/null +++ b/SixLabors.Fonts/KnownVariationAxes.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Defines the registered design-variation axis tags for variable fonts. + /// These tags are used with to control font design axes. + /// + /// + public static class KnownVariationAxes + { + /// + /// Italic axis ('ital'). Controls the italic angle of the font. + /// Value range: 0 (upright) to 1 (italic). + /// + /// + public const string Italic = "ital"; + + /// + /// Optical size axis ('opsz'). Adjusts the design for a specific text size in points. + /// Typical range: 6 to 144. Larger values optimize for display use; smaller for body text. + /// + /// + public const string OpticalSize = "opsz"; + + /// + /// Slant axis ('slnt'). Controls the slant angle of upright glyphs in degrees. + /// Typical range: -90 to 90. Negative values slant to the right (the common direction). + /// + /// + public const string Slant = "slnt"; + + /// + /// Width axis ('wdth'). Controls the relative width of the font as a percentage of normal. + /// Typical range: 75 (condensed) to 125 (expanded). 100 represents the normal width. + /// + /// + public const string Width = "wdth"; + + /// + /// Weight axis ('wght'). Controls the weight (boldness) of the font. + /// Range: 1 to 1000. Common values: 100 (Thin), 300 (Light), 400 (Regular), + /// 700 (Bold), 900 (Black). + /// + /// + public const string Weight = "wght"; + } +} diff --git a/SixLabors.Fonts/LayoutMode.cs b/SixLabors.Fonts/LayoutMode.cs new file mode 100644 index 0000000..521e9a5 --- /dev/null +++ b/SixLabors.Fonts/LayoutMode.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts { + /// + /// Defines modes to determine the layout direction of text. + /// + [Flags] + public enum LayoutMode + { + /// + /// Text is laid out horizontally from top to bottom. + /// + HorizontalTopBottom = 0, + + /// + /// Text is laid out horizontally from bottom to top. + /// + HorizontalBottomTop = 1 << 0, + + /// + /// Text is laid out vertically from left to right. + /// + VerticalLeftRight = 1 << 1, + + /// + /// Text is laid out vertically from right to left. + /// + VerticalRightLeft = 1 << 2, + + /// + /// Text is laid out vertically from left to right. Horizontal glyphs are rotated 90 degrees clockwise. + /// + VerticalMixedLeftRight = 1 << 3, + + /// + /// Text is laid out vertically from right to left. Horizontal glyphs are rotated 90 degrees clockwise. + /// + VerticalMixedRightLeft = 1 << 4, + } +} diff --git a/SixLabors.Fonts/LayoutModeExtensions.cs b/SixLabors.Fonts/LayoutModeExtensions.cs new file mode 100644 index 0000000..7027202 --- /dev/null +++ b/SixLabors.Fonts/LayoutModeExtensions.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts { + /// + /// Extensions to . + /// + public static class LayoutModeExtensions + { + /// + /// Gets a value indicating whether the layout mode is horizontal. + /// + /// The layout mode. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHorizontal(this LayoutMode mode) + => mode is LayoutMode.HorizontalTopBottom or LayoutMode.HorizontalBottomTop; + + /// + /// Gets a value indicating whether the layout mode is vertical. + /// + /// The layout mode. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsVertical(this LayoutMode mode) + => mode is LayoutMode.VerticalLeftRight or LayoutMode.VerticalRightLeft; + + /// + /// Gets a value indicating whether the layout mode is vertical-mixed only. + /// + /// The layout mode. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsVerticalMixed(this LayoutMode mode) + => mode is LayoutMode.VerticalMixedLeftRight or LayoutMode.VerticalMixedRightLeft; + } +} diff --git a/SixLabors.Fonts/LineLayout.cs b/SixLabors.Fonts/LineLayout.cs new file mode 100644 index 0000000..1656b3b --- /dev/null +++ b/SixLabors.Fonts/LineLayout.cs @@ -0,0 +1,205 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.Fonts.Rendering; + +namespace SixLabors.Fonts { + /// + /// Represents one laid-out line from a . + /// + public sealed class LineLayout + { + private readonly TextBox textBox; + private readonly TextOptions options; + private readonly float wrappingLength; + private readonly int lineIndex; + private readonly LayoutMode layoutMode; + private readonly ReadOnlyMemory graphemeMetrics; + private readonly ReadOnlyMemory wordMetrics; + private GlyphMetrics[]? glyphMetrics; + + internal LineLayout( + TextBox textBox, + TextOptions options, + float wrappingLength, + int lineIndex, + in LineMetrics metrics, + ReadOnlyMemory graphemeMetrics, + ReadOnlyMemory wordMetrics) + { + this.textBox = textBox; + this.options = options; + this.wrappingLength = wrappingLength; + this.lineIndex = lineIndex; + this.layoutMode = options.LayoutMode; + this.LineMetrics = metrics; + this.graphemeMetrics = graphemeMetrics; + this.wordMetrics = wordMetrics; + } + + /// + /// Gets the measured line metrics. + /// + public LineMetrics LineMetrics { get; } + + /// + /// Gets the grapheme metrics entries for this line in final layout order. + /// + public ReadOnlySpan GraphemeMetrics => this.graphemeMetrics.Span; + + /// + /// Hit tests the supplied point against this line's grapheme advance bounds. + /// + /// The point in pixel units. + /// The hit-tested grapheme position. + public TextHit HitTest(Vector2 point) + => TextInteraction.HitTestLine(this.lineIndex, this.GraphemeMetrics, point, this.layoutMode); + + /// + /// Gets the caret position for the supplied hit. + /// + /// The hit-tested grapheme position. + /// The caret position in pixel units. + public CaretPosition GetCaretPosition(TextHit hit) + => TextInteraction.GetCaretPositionLine( + this.lineIndex, + this.LineMetrics, + this.GraphemeMetrics, + hit.GraphemeInsertionIndex, + this.layoutMode); + + /// + /// Gets an absolute caret position in the laid-out line. + /// + /// The absolute caret placement. + /// The caret position in pixel units. + public CaretPosition GetCaret(CaretPlacement placement) + => TextInteraction.GetCaretLine( + this.lineIndex, + this.LineMetrics, + this.GraphemeMetrics, + placement, + this.layoutMode, + this.textBox.TextDirection()); + + /// + /// Moves the supplied caret by the requested operation within this line. + /// + /// The current caret position. + /// The movement operation. + /// The moved caret position in pixel units. + public CaretPosition MoveCaret(CaretPosition caret, CaretMovement movement) + => TextInteraction.MoveCaretLine( + this.lineIndex, + this.LineMetrics, + this.GraphemeMetrics, + this.wordMetrics.Span, + caret, + movement, + this.layoutMode, + this.textBox.TextDirection()); + + /// + /// Gets the word metrics for the word-boundary segment containing the supplied hit-tested grapheme position. + /// + /// The hit-tested grapheme position. + /// The word metrics containing the hit grapheme. + public WordMetrics GetWordMetrics(TextHit hit) + => TextInteraction.GetWordMetrics(this.wordMetrics.Span, hit.GraphemeIndex); + + /// + /// Gets the word metrics for the word-boundary segment containing the supplied caret position. + /// + /// The caret position. + /// The word metrics containing the caret's grapheme insertion index. + public WordMetrics GetWordMetrics(CaretPosition caret) + => TextInteraction.GetWordMetrics(this.wordMetrics.Span, caret.GraphemeIndex); + + /// + /// Gets selection bounds between two hit-tested grapheme positions. + /// + /// The fixed selection endpoint. + /// The active selection endpoint. + /// A read-only memory region containing the selection bounds in visual order and pixel units. + public ReadOnlyMemory GetSelectionBounds(TextHit anchor, TextHit focus) + => TextInteraction.GetSelectionBoundsLine( + this.LineMetrics, + this.GraphemeMetrics, + anchor.GraphemeInsertionIndex, + focus.GraphemeInsertionIndex, + this.layoutMode); + + /// + /// Gets selection bounds between two caret positions. + /// + /// The fixed selection endpoint. + /// The active selection endpoint. + /// A read-only memory region containing the selection bounds in visual order and pixel units. + public ReadOnlyMemory GetSelectionBounds(CaretPosition anchor, CaretPosition focus) + => TextInteraction.GetSelectionBoundsLine( + this.LineMetrics, + this.GraphemeMetrics, + anchor.GraphemeIndex, + focus.GraphemeIndex, + this.layoutMode); + + /// + /// Gets line-local selection bounds for the supplied grapheme metrics. + /// + /// The grapheme metrics to select. + /// A read-only memory region containing the selection bounds in visual order and pixel units. + public ReadOnlyMemory GetSelectionBounds(GraphemeMetrics metrics) + => TextInteraction.GetSelectionBoundsLine( + this.LineMetrics, + metrics, + this.layoutMode); + + /// + /// Gets line-local selection bounds for the supplied word metrics. + /// + /// The word metrics to select. + /// A read-only memory region containing the selection bounds in visual order and pixel units. + public ReadOnlyMemory GetSelectionBounds(WordMetrics metrics) + => TextInteraction.GetSelectionBoundsLine( + this.LineMetrics, + this.GraphemeMetrics, + metrics.GraphemeStart, + metrics.GraphemeEnd, + this.layoutMode); + + /// + public ReadOnlyMemory GetGlyphMetrics() + => this.glyphMetrics ??= TextBlock.GetGlyphMetricsArray( + this.textBox, + this.options, + this.wrappingLength, + this.lineIndex); + + /// + /// Renders this line to the supplied glyph renderer. + /// + /// The target renderer. + public void RenderTo(IGlyphRenderer renderer) + { + FontRectangle bounds = FontRectangle.Empty; + ReadOnlySpan glyphMetrics = this.GetGlyphMetrics().Span; + + for (int i = 0; i < glyphMetrics.Length; i++) + { + bounds = i == 0 + ? glyphMetrics[i].Bounds + : FontRectangle.Union(bounds, glyphMetrics[i].Bounds); + } + + TextBlock.RenderTo( + renderer, + this.textBox, + this.options, + this.wrappingLength, + bounds, + this.lineIndex); + } + } +} diff --git a/SixLabors.Fonts/LineLayoutEnumerator.cs b/SixLabors.Fonts/LineLayoutEnumerator.cs new file mode 100644 index 0000000..d1d1361 --- /dev/null +++ b/SixLabors.Fonts/LineLayoutEnumerator.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Walks a one laid-out line at a time. + /// + /// + /// Each produced line is positioned independently, without cumulative offsets from earlier or later lines. + /// + public sealed class LineLayoutEnumerator + { + private readonly TextBlock textBlock; + private readonly TextLineBreakEnumerator lineEnumerator; + private readonly TextDirection textDirection; + private readonly bool suppressLayout; + private LineLayout? current; + + /// + /// Initializes a new instance of the class. + /// + /// The prepared text block to enumerate. + internal LineLayoutEnumerator(TextBlock textBlock) + { + this.textBlock = textBlock; + this.lineEnumerator = new(textBlock.LogicalLine, textBlock.Options); + this.textDirection = TextLayout.GetTextDirection(textBlock.LogicalLine, textBlock.Options); + this.suppressLayout = textBlock.Options.MaxLines == 0; + } + + /// + /// Gets the current line layout. + /// + public LineLayout Current => this.current!; + + /// + /// Advances to the next line using the supplied wrapping length. + /// + /// + /// The wrapping length applies only to the line being produced by this call. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// when a line was produced. + public bool MoveNext(float wrappingLength) + { + if (this.suppressLayout) + { + return false; + } + + if (!this.lineEnumerator.MoveNext(wrappingLength)) + { + return false; + } + + // The walker lays out each produced line independently so callers can + // place variable-width lines into custom columns, shapes, or virtualized + // surfaces without inheriting block-level line offsets. + this.current = this.textBlock.GetLineLayout( + this.lineEnumerator.Current, + wrappingLength, + this.textDirection); + + return true; + } + } +} diff --git a/SixLabors.Fonts/LineMetrics.cs b/SixLabors.Fonts/LineMetrics.cs new file mode 100644 index 0000000..f1968f7 --- /dev/null +++ b/SixLabors.Fonts/LineMetrics.cs @@ -0,0 +1,110 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts { + /// + /// Encapsulates measured metrics for a single laid-out text line. + /// + public readonly struct LineMetrics + { + /// + /// Initializes a new instance of the struct. + /// + /// The ascender line position within the line box. + /// The baseline position within the line box. + /// The descender line position within the line box. + /// The total line-box size for this line. + /// The logical line box start position in pixel units. + /// The logical line box extent in pixel units. + /// The UTF-16 index in the original text where this line begins. + /// The grapheme index in the original text where this line begins. + /// The number of graphemes in the line. + /// The offset of this line's first grapheme metrics entry. + internal LineMetrics( + float ascender, + float baseline, + float descender, + float lineHeight, + Vector2 start, + Vector2 extent, + int stringIndex, + int graphemeIndex, + int graphemeCount, + int graphemeOffset) + { + this.Ascender = ascender; + this.Baseline = baseline; + this.Descender = descender; + this.LineHeight = lineHeight; + this.Start = start; + this.Extent = extent; + this.StringIndex = stringIndex; + this.GraphemeIndex = graphemeIndex; + this.GraphemeCount = graphemeCount; + this.GraphemeOffset = graphemeOffset; + } + + /// + /// Gets the ascender line position within the line box. + /// + /// + /// This is a position value (not a baseline-relative distance). + /// Use this value to draw the ascender guide line relative to the current line origin. + /// + public float Ascender { get; } + + /// + /// Gets the baseline position within the line box. + /// + /// + /// Use this value as the guide-line position for drawing a baseline relative to the current line origin. + /// + public float Baseline { get; } + + /// + /// Gets the descender line position within the line box. + /// + /// + /// This is a position value (not a baseline-relative distance). + /// Use this value to draw the descender guide line relative to the current line origin. + /// + public float Descender { get; } + + /// + /// Gets the total line-box size for this line. + /// + public float LineHeight { get; } + + /// + /// Gets the logical line box start position in pixel units. + /// + public Vector2 Start { get; } + + /// + /// Gets the logical line box extent in pixel units. + /// + public Vector2 Extent { get; } + + /// + /// Gets the zero-based UTF-16 code unit index in the original text. + /// + public int StringIndex { get; } + + /// + /// Gets the zero-based grapheme index in the original text. + /// + public int GraphemeIndex { get; } + + /// + /// Gets the number of graphemes in the line. + /// + public int GraphemeCount { get; } + + /// + /// Gets the offset of this line's first entry in the flattened grapheme metrics buffer. + /// + internal int GraphemeOffset { get; } + } +} diff --git a/SixLabors.Fonts/LogicalTextLine.cs b/SixLabors.Fonts/LogicalTextLine.cs new file mode 100644 index 0000000..a65215a --- /dev/null +++ b/SixLabors.Fonts/LogicalTextLine.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using System.Collections.Generic; + +namespace SixLabors.Fonts { + /// + /// Contains a composed logical text line and its width-independent line break opportunities. + /// + internal readonly struct LogicalTextLine + { + /// + /// Initializes a new instance of the struct. + /// + /// The composed logical text line. + /// The collected line break opportunities. + /// The collected word-boundary segment runs. + /// The visible hyphenation markers created for soft hyphen entries. + public LogicalTextLine( + TextLine textLine, + List lineBreaks, + List wordSegments, + List hyphenationMarkers) + { + this.TextLine = textLine; + this.LineBreaks = lineBreaks; + this.WordSegments = wordSegments; + this.HyphenationMarkers = hyphenationMarkers; + } + + /// + /// Gets the composed logical text line. + /// + public TextLine TextLine { get; } + + /// + /// Gets the collected line break opportunities. + /// + public List LineBreaks { get; } + + /// + /// Gets the collected word-boundary segment runs. + /// + public List WordSegments { get; } + + /// + /// Gets the visible hyphenation markers created for soft hyphen entries. + /// + public List HyphenationMarkers { get; } + } +} diff --git a/SixLabors.Fonts/MappedArraySlice{T}.cs b/SixLabors.Fonts/MappedArraySlice{T}.cs new file mode 100644 index 0000000..000a63e --- /dev/null +++ b/SixLabors.Fonts/MappedArraySlice{T}.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts { + /// + /// Provides a mapped view of an underlying slice, selecting arbitrary indices + /// from the source array. + /// + /// The type of item contained in the underlying array. + internal readonly struct MappedArraySlice + where T : struct + { + private readonly ArraySlice data; + private readonly ArraySlice map; + + /// + /// Initializes a new instance of the struct. + /// + /// The data slice. + /// The map slice. + public MappedArraySlice(in ArraySlice data, in ArraySlice map) + { + Guard.MustBeGreaterThanOrEqualTo(data.Length, map.Length, nameof(map)); + + this.data = data; + this.map = map; + } + + /// + /// Gets the number of items in the map. + /// + public int Length => this.map.Length; + + /// + /// Returns a reference to specified element of the slice. + /// + /// The index of the element to return. + /// The . + /// + /// Thrown when index less than 0 or index greater than or equal to . + /// + public readonly ref T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref this.data[this.map[index]]; + } + } +} diff --git a/SixLabors.Fonts/Native/CFStringEncoding.cs b/SixLabors.Fonts/Native/CFStringEncoding.cs new file mode 100644 index 0000000..46dfc96 --- /dev/null +++ b/SixLabors.Fonts/Native/CFStringEncoding.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.Fonts.Native { + /// + /// An integer type for constants used to specify supported string encodings in various CFString functions. + /// + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Verbatim constants from the macOS SDK")] + internal enum CFStringEncoding : uint + { + /// + /// An encoding constant that identifies the UTF 8 encoding. + /// + kCFStringEncodingUTF8 = 0x08000100, + + /// + /// An encoding constant that identifies kTextEncodingUnicodeDefault + kUnicodeUTF16LEFormat encoding. This constant specifies little-endian byte order. + /// + kCFStringEncodingUTF16LE = 0x14000100, + } +} diff --git a/SixLabors.Fonts/Native/CFURLPathStyle.cs b/SixLabors.Fonts/Native/CFURLPathStyle.cs new file mode 100644 index 0000000..2239ef8 --- /dev/null +++ b/SixLabors.Fonts/Native/CFURLPathStyle.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.Fonts.Native { + /// + /// Options you can use to determine how CFURL functions parse a file system path name. + /// + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Verbatim constants from the macOS SDK")] + internal enum CFURLPathStyle : long + { + /// + /// Indicates a POSIX style path name. Components are slash delimited. A leading slash indicates an absolute path; a trailing slash is not significant. + /// + kCFURLPOSIXPathStyle = 0, + } +} diff --git a/SixLabors.Fonts/Native/CoreFoundation.cs b/SixLabors.Fonts/Native/CoreFoundation.cs new file mode 100644 index 0000000..1463183 --- /dev/null +++ b/SixLabors.Fonts/Native/CoreFoundation.cs @@ -0,0 +1,125 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.InteropServices; + +namespace SixLabors.Fonts.Native { + // ReSharper disable InconsistentNaming + internal static class CoreFoundation + { + private const string CoreFoundationFramework = "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"; + + /// + /// Returns the number of values currently in an array. + /// + /// The array to examine. + /// The number of values in . + [DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "SYSLIB1054:Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time", Justification = ".NET7 Only")] + public static extern long CFArrayGetCount(IntPtr theArray); + + /// + /// Returns the type identifier for the CFArray opaque type. + /// + /// The type identifier for the CFArray opaque type. + /// CFMutableArray objects have the same type identifier as CFArray objects. + [DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "SYSLIB1054:Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time", Justification = ".NET7 Only")] + public static extern ulong CFArrayGetTypeID(); + + /// + /// Retrieves a value at a given index. + /// + /// The array to examine. + /// The index of the value to retrieve. If the index is outside the index space of (0 to N-1 inclusive where N is the count of ), the behavior is undefined. + /// The value at the index in . If the return value is a Core Foundation Object, ownership follows The Get Rule. + [DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "SYSLIB1054:Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time", Justification = ".NET7 Only")] + public static extern IntPtr CFArrayGetValueAtIndex(IntPtr theArray, long idx); + + /// + /// Returns the unique identifier of an opaque type to which a Core Foundation object belongs. + /// + /// The CFType object to examine. + /// A value of type CFTypeID that identifies the opaque type of . + /// + /// This function returns a value that uniquely identifies the opaque type of any Core Foundation object. + /// You can compare this value with the known CFTypeID identifier obtained with a "GetTypeID" function specific to a type, for example CFDateGetTypeID. + /// These values might change from release to release or platform to platform. + /// + [DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "SYSLIB1054:Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time", Justification = ".NET7 Only")] + public static extern ulong CFGetTypeID(IntPtr cf); + + /// + /// Returns the number (in terms of UTF-16 code pairs) of Unicode characters in a string. + /// + /// The string to examine. + /// The number (in terms of UTF-16 code pairs) of characters stored in . + [DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "SYSLIB1054:Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time", Justification = ".NET7 Only")] + public static extern long CFStringGetLength(IntPtr theString); + + /// + /// Copies the character contents of a string to a local C string buffer after converting the characters to a given encoding. + /// + /// The string whose contents you wish to access. + /// + /// The C string buffer into which to copy the string. On return, the buffer contains the converted characters. If there is an error in conversion, the buffer contains only partial results. + /// The buffer must be large enough to contain the converted characters and a NUL terminator. For example, if the string is Toby, the buffer must be at least 5 bytes long. + /// + /// The length of in bytes. + /// The string encoding to which the character contents of should be converted. The encoding must specify an 8-bit encoding. + /// upon success or if the conversion fails or the provided buffer is too small. + /// This function is useful when you need your own copy of a string’s character data as a C string. You also typically call it as a "backup" when a prior call to the function fails. + [DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "SYSLIB1054:Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time", Justification = ".NET7 Only")] + public static extern bool CFStringGetCString(IntPtr theString, byte[] buffer, long bufferSize, CFStringEncoding encoding); + + /// + /// Quickly obtains a pointer to a C-string buffer containing the characters of a string in a given encoding. + /// + /// The string whose contents you wish to access. + /// The string encoding to which the character contents of should be converted. The encoding must specify an 8-bit encoding. + /// A pointer to a C string or NULL if the internal storage of does not allow this to be returned efficiently. + /// + /// + /// This function either returns the requested pointer immediately, with no memory allocations and no copying, in constant time, or returns NULL. If the latter is the result, call an alternative function such as the function to extract the characters. + /// + /// + /// Whether or not this function returns a valid pointer or NULL depends on many factors, all of which depend on how the string was created and its properties. In addition, the function result might change between different releases and on different platforms. So do not count on receiving a non-NULL result from this function under any circumstances. + /// + /// + [DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "SYSLIB1054:Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time", Justification = ".NET7 Only")] + public static extern IntPtr CFStringGetCStringPtr(IntPtr theString, CFStringEncoding encoding); + + /// + /// Releases a Core Foundation object. + /// + /// A CFType object to release. This value must not be NULL. + [DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "SYSLIB1054:Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time", Justification = ".NET7 Only")] + public static extern void CFRelease(IntPtr cf); + + /// + /// Returns the path portion of a given URL. + /// + /// The CFURL object whose path you want to obtain. + /// The operating system path style to be used to create the path. See for a list of possible values. + /// The URL's path in the format specified by . Ownership follows the create rule. See The Create Rule. + /// This function returns the URL's path as a file system path for a given path style. + [DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "SYSLIB1054:Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time", Justification = ".NET7 Only")] + public static extern IntPtr CFURLCopyFileSystemPath(IntPtr anURL, CFURLPathStyle pathStyle); + + /// + /// Returns the type identifier for the CFURL opaque type. + /// + /// The type identifier for the CFURL opaque type. + [DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "SYSLIB1054:Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time", Justification = ".NET7 Only")] + public static extern ulong CFURLGetTypeID(); + } +} diff --git a/SixLabors.Fonts/Native/CoreText.cs b/SixLabors.Fonts/Native/CoreText.cs new file mode 100644 index 0000000..2fb1d47 --- /dev/null +++ b/SixLabors.Fonts/Native/CoreText.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.InteropServices; + +namespace SixLabors.Fonts.Native { + internal static class CoreText + { + private const string CoreTextFramework = "/System/Library/Frameworks/CoreText.framework/Versions/A/CoreText"; + + /// + /// Returns an array of font URLs. + /// + /// This function returns a retained reference to a CFArray of CFURLRef objects representing the URLs of the available fonts, or NULL on error. The caller is responsible for releasing the array. + [DllImport(CoreTextFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "SYSLIB1054:Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time", Justification = ".NET7 Only")] + public static extern IntPtr CTFontManagerCopyAvailableFontURLs(); + } +} diff --git a/SixLabors.Fonts/Native/MacSystemFontsEnumerator.cs b/SixLabors.Fonts/Native/MacSystemFontsEnumerator.cs new file mode 100644 index 0000000..786dfb9 --- /dev/null +++ b/SixLabors.Fonts/Native/MacSystemFontsEnumerator.cs @@ -0,0 +1,93 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using static SixLabors.Fonts.Native.CoreFoundation; +using static SixLabors.Fonts.Native.CoreText; + +namespace SixLabors.Fonts.Native { + /// + /// An enumerator that enumerates over available macOS system fonts. + /// The enumerated strings are the absolute paths to the font files. + /// + /// + /// Internally, it calls the native CoreText's method to retrieve + /// the list of fonts so using this class must be guarded by RuntimeInformation.IsOSPlatform(OSPlatform.OSX). + /// + internal sealed class MacSystemFontsEnumerator : IEnumerable, IEnumerator + { + private static readonly ArrayPool BytePool = ArrayPool.Shared; + + private readonly IntPtr fontUrls; + private readonly bool releaseFontUrls; + private int fontIndex; + + public MacSystemFontsEnumerator() + : this(CTFontManagerCopyAvailableFontURLs(), releaseFontUrls: true, fontIndex: 0) + { + } + + private MacSystemFontsEnumerator(IntPtr fontUrls, bool releaseFontUrls, int fontIndex) + { + if (fontUrls == IntPtr.Zero) + { + throw new ArgumentException($"The {nameof(fontUrls)} must not be NULL.", nameof(fontUrls)); + } + + this.fontUrls = fontUrls; + this.releaseFontUrls = releaseFontUrls; + this.fontIndex = fontIndex; + + this.Current = null!; + } + + public string Current { get; private set; } + + object IEnumerator.Current => this.Current; + + public bool MoveNext() + { + Debug.Assert(CFGetTypeID(this.fontUrls) == CFArrayGetTypeID(), "The fontUrls array must be a CFArrayRef"); + if (this.fontIndex < CFArrayGetCount(this.fontUrls)) + { + IntPtr fontUrl = CFArrayGetValueAtIndex(this.fontUrls, this.fontIndex); + Debug.Assert(CFGetTypeID(fontUrl) == CFURLGetTypeID(), "The elements of the fontUrls array must be a CFURLRef"); + IntPtr fontPath = CFURLCopyFileSystemPath(fontUrl, CFURLPathStyle.kCFURLPOSIXPathStyle); + + int fontPathLength = (int)CFStringGetLength(fontPath); + int fontPathBufferSize = (fontPathLength + 1) * 2; // +1 for the NULL byte and *2 for UTF-16 + byte[] fontPathBuffer = BytePool.Rent(fontPathBufferSize); + CFStringGetCString(fontPath, fontPathBuffer, fontPathBufferSize, CFStringEncoding.kCFStringEncodingUTF16LE); + this.Current = Encoding.Unicode.GetString(fontPathBuffer, 0, fontPathBufferSize - 2); // -2 for the UTF-16 NULL + BytePool.Return(fontPathBuffer); + + CFRelease(fontPath); + + this.fontIndex++; + + return true; + } + + return false; + } + + public void Reset() => this.fontIndex = 0; + + public void Dispose() + { + if (this.releaseFontUrls) + { + CFRelease(this.fontUrls); + } + } + + public IEnumerator GetEnumerator() => new MacSystemFontsEnumerator(this.fontUrls, releaseFontUrls: false, this.fontIndex); + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + } +} diff --git a/SixLabors.Fonts/ObjectPool{T}.cs b/SixLabors.Fonts/ObjectPool{T}.cs new file mode 100644 index 0000000..a633d91 --- /dev/null +++ b/SixLabors.Fonts/ObjectPool{T}.cs @@ -0,0 +1,129 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Concurrent; +using System.Threading; + +namespace SixLabors.Fonts { + /// + /// A pool for reusing objects of type . + /// + /// The type to pool objects for. + /// + /// This implementation keeps a cache of retained objects. + /// This means that if objects are returned when the pool has already reached "maximumRetained" objects they will be available to be Garbage Collected. + /// + internal sealed class ObjectPool + where T : class + { + private readonly Func createFunc; + private readonly Func returnFunc; + private readonly int maxCapacity; + private int numItems; + + private readonly ConcurrentQueue items = new(); + private T? fastItem; + + /// + /// Initializes a new instance of the class. + /// + /// The pooling policy to use. + public ObjectPool(IPooledObjectPolicy policy) + : this(policy, Environment.ProcessorCount * 2) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The pooling policy to use. + /// The maximum number of objects to retain in the pool. + public ObjectPool(IPooledObjectPolicy policy, int maximumRetained) + { + // cache the target interface methods, to avoid interface lookup overhead + this.createFunc = policy.Create; + this.returnFunc = policy.Return; + this.maxCapacity = maximumRetained - 1; // -1 to account for fastItem + } + + /// + /// Gets an object from the pool if one is available, otherwise creates one. + /// + /// A . + public T Get() + { + T? item = this.fastItem; + if (item == null || Interlocked.CompareExchange(ref this.fastItem, null, item) != item) + { + if (this.items.TryDequeue(out item)) + { + _ = Interlocked.Decrement(ref this.numItems); + return item; + } + + // no object available, so go get a brand new one + return this.createFunc(); + } + + return item; + } + + /// + /// Return an object to the pool. + /// + /// The object to add to the pool. + public void Return(T obj) => this.ReturnCore(obj); + + /// + /// Returns an object to the pool. + /// + /// true if the object was returned to the pool + private bool ReturnCore(T obj) + { + if (!this.returnFunc(obj)) + { + // policy says to drop this object + return false; + } + + if (this.fastItem != null || Interlocked.CompareExchange(ref this.fastItem, obj, null) != null) + { + if (Interlocked.Increment(ref this.numItems) <= this.maxCapacity) + { + this.items.Enqueue(obj); + return true; + } + + // no room, clean up the count and drop the object on the floor + _ = Interlocked.Decrement(ref this.numItems); + return false; + } + + return true; + } + } + + /// + /// Represents a policy for managing pooled objects. + /// + /// The type of object which is being pooled. +#pragma warning disable SA1201 // Elements should appear in the correct order + internal interface IPooledObjectPolicy +#pragma warning restore SA1201 // Elements should appear in the correct order + where T : notnull + { + /// + /// Create a . + /// + /// The which was created. + public T Create(); + + /// + /// Runs some processing when an object was returned to the pool. Can be used to reset the state of an object and indicate if the object should be returned to the pool. + /// + /// The object to return to the pool. + /// if the object should be returned to the pool. if it's not possible/desirable for the pool to keep the object. + public bool Return(T obj); + } +} diff --git a/SixLabors.Fonts/OutlineType.cs b/SixLabors.Fonts/OutlineType.cs new file mode 100644 index 0000000..3f5566f --- /dev/null +++ b/SixLabors.Fonts/OutlineType.cs @@ -0,0 +1,10 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + internal enum OutlineType : uint + { + TrueType = 0x00010000, + CFF = 0x4F54544F + } +} diff --git a/SixLabors.Fonts/PlaceholderGlyphMetrics.cs b/SixLabors.Fonts/PlaceholderGlyphMetrics.cs new file mode 100644 index 0000000..3d09f37 --- /dev/null +++ b/SixLabors.Fonts/PlaceholderGlyphMetrics.cs @@ -0,0 +1,166 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// Represents synthetic glyph metrics for an atomic inline placeholder. + /// + internal sealed class PlaceholderGlyphMetrics : FontGlyphMetrics + { + private readonly TextPlaceholder placeholder; + private readonly float pointSize; + private readonly float dpi; + + /// + /// Initializes a new instance of the class. + /// + /// The font metrics used for shared line metrics and decoration settings. + /// The placeholder dimensions and alignment settings. + /// The point size used for layout. + /// The resolution used to convert placeholder pixels into layout units. + /// The text run this placeholder belongs to. + internal PlaceholderGlyphMetrics( + StreamFontMetrics font, + TextPlaceholder placeholder, + float pointSize, + float dpi, + TextRun textRun) + : base( + font, + 0, + CodePoint.ObjectReplacementChar, + GetBounds(placeholder, pointSize, dpi, font), + ToGlyphUnits(placeholder.Width, pointSize, dpi, font.ScaleFactor), + ToGlyphUnits(placeholder.Height, pointSize, dpi, font.ScaleFactor), + 0, + 0, + font.UnitsPerEm, + Vector2.Zero, + new Vector2(font.ScaleFactor), + textRun, + GlyphType.Placeholder) + { + this.placeholder = placeholder; + this.pointSize = pointSize; + this.dpi = dpi; + } + + /// + internal override FontGlyphMetrics CloneForRendering(TextRun textRun) + => new PlaceholderGlyphMetrics( + this.FontMetrics, + this.placeholder, + this.pointSize, + this.dpi, + textRun); + + /// + internal override void RenderTo( + IGlyphRenderer renderer, + int graphemeIndex, + Vector2 glyphOrigin, + Vector2 decorationOrigin, + GlyphLayoutMode mode, + TextOptions options) + { + // Placeholders reserve layout space only; the caller owns the object rendering. + } + + /// + /// Converts the placeholder box into glyph bounds in the same synthetic font-unit space as its advances. + /// + /// The placeholder dimensions and baseline offset. + /// The point size used for layout. + /// The resolution used to convert placeholder pixels into layout units. + /// The font metrics used by glyph layout. + /// The placeholder bounds expressed in synthetic font units. + private static Bounds GetBounds(TextPlaceholder placeholder, float pointSize, float dpi, StreamFontMetrics font) + { + float scaleFactor = font.ScaleFactor; + float width = ToGlyphUnitsFloat(placeholder.Width, pointSize, dpi, scaleFactor); + float height = ToGlyphUnitsFloat(placeholder.Height, pointSize, dpi, scaleFactor); + float baselineOffset = ToGlyphUnitsFloat(placeholder.BaselineOffset, pointSize, dpi, scaleFactor); + float lineHeight = font.UnitsPerEm; + float metricsDelta = (font.HorizontalMetrics.LineHeight - lineHeight) * .5F; + float ascender = font.HorizontalMetrics.Ascender - metricsDelta; + float descender = Math.Abs(font.HorizontalMetrics.Descender) - metricsDelta; + float coreHeight = ascender + descender + (2 * metricsDelta); + float extra = lineHeight - coreHeight; + + // Top/middle/bottom align against the surrounding run font's normal + // line box, expressed relative to the text baseline in Y-up font units. + float lineTop = ascender + metricsDelta + (extra * .5F); + float lineBottom = lineTop - lineHeight; + float top = baselineOffset; + float bottom = baselineOffset - height; + + switch (placeholder.Alignment) + { + case TextPlaceholderAlignment.AboveBaseline: + top = height; + bottom = 0; + break; + + case TextPlaceholderAlignment.BelowBaseline: + top = 0; + bottom = -height; + break; + + case TextPlaceholderAlignment.Top: + top = lineTop; + bottom = top - height; + break; + + case TextPlaceholderAlignment.Bottom: + // Top, middle, and bottom align against the full line-height + // box, not just the ascender/descender band. + bottom = lineBottom; + top = bottom + height; + break; + + case TextPlaceholderAlignment.Middle: + float center = (lineTop + lineBottom) * .5F; + top = center + (height * .5F); + bottom = center - (height * .5F); + break; + + default: + top = baselineOffset; + bottom = baselineOffset - height; + break; + } + + // Placeholder bounds are authored in device pixels and converted into + // synthetic font units so the normal glyph scaling path maps them back + // to device-space size while preserving the requested baseline alignment. + return new Bounds(0, top, width, bottom); + } + + /// + /// Converts a placeholder pixel measurement into glyph units for the current layout scale. + /// + /// The placeholder measurement in pixels. + /// The point size used for layout. + /// The resolution used to convert placeholder pixels into layout units. + /// The font scale factor used by glyph layout. + /// The measurement expressed in synthetic font units. + private static ushort ToGlyphUnits(float pixels, float pointSize, float dpi, float scaleFactor) + => (ushort)MathF.Round(ToGlyphUnitsFloat(pixels, pointSize, dpi, scaleFactor)); + + /// + /// Converts a placeholder pixel measurement into fractional glyph units for bounds placement. + /// + /// The placeholder measurement in pixels. + /// The point size used for layout. + /// The resolution used to convert placeholder pixels into layout units. + /// The font scale factor used by glyph layout. + /// The measurement expressed in synthetic font units. + private static float ToGlyphUnitsFloat(float pixels, float pointSize, float dpi, float scaleFactor) + => pixels * scaleFactor / (pointSize * dpi); + } +} diff --git a/SixLabors.Fonts/PreparedTextLayoutDesign.md b/SixLabors.Fonts/PreparedTextLayoutDesign.md new file mode 100644 index 0000000..164b558 --- /dev/null +++ b/SixLabors.Fonts/PreparedTextLayoutDesign.md @@ -0,0 +1,534 @@ +# Text Measurement and Interaction APIs + +This document describes the public measurement and selection surface for laid-out +text. The intent is that callers can measure, render, hit-test, place carets, +and draw selections without reimplementing bidi, grapheme, hard-break, or layout +mode rules outside the library. + +All positional metrics exposed by these APIs are in pixel units. + +## API Layers + +There are four layers: + +- `TextMeasurer`: one-shot convenience APIs for measuring a string. +- `TextBlock`: prepared text that can be measured or rendered repeatedly. +- `TextMetrics`: the full measurement result for one laid-out text block. +- `LineLayout`: one laid-out line with line-local measurement and interaction APIs. + +Use `TextMeasurer` for simple one-off work. Use `TextBlock` when the same text +will be measured, rendered, wrapped, or inspected more than once. + +## One-Shot Measurement + +`TextMeasurer` is the shortest path from text and options to measurements. +`TextOptions.WrappingLength` controls wrapping for these methods. + +```csharp +TextOptions options = new(font) +{ + Origin = new Vector2(20, 30), + + // TextMeasurer reads WrappingLength from TextOptions. + WrappingLength = 320 +}; + +TextMetrics metrics = TextMeasurer.Measure(text, options); +FontRectangle advance = TextMeasurer.MeasureAdvance(text, options); +FontRectangle bounds = TextMeasurer.MeasureBounds(text, options); +FontRectangle renderableBounds = TextMeasurer.MeasureRenderableBounds(text, options); +``` + +The aggregate rectangles answer different questions: + +- `MeasureAdvance`: the logical line-box advance of the text. +- `MeasureBounds`: the rendered glyph bounds. +- `MeasureRenderableBounds`: the union of logical advance and rendered glyph bounds. + +Use `MeasureAdvance` for layout flow. Use `MeasureBounds` for tight ink bounds. +Use `MeasureRenderableBounds` when both typographic advance and rendered glyph +overshoot must fit. + +## Prepared Measurement + +`TextBlock` prepares the wrapping-independent text work once. Pass the wrapping +length to each operation. `TextOptions.WrappingLength` is ignored by the +constructor. + +```csharp +TextBlock block = new(text, options); + +// Each operation supplies the wrapping length; the constructor does not. +TextMetrics narrow = block.Measure(240); +TextMetrics wide = block.Measure(480); + +FontRectangle narrowBounds = block.MeasureBounds(240); +FontRectangle wideBounds = block.MeasureBounds(480); +``` + +Use `-1` as the wrapping length to disable wrapping. + +```csharp +// -1 disables wrapping for TextBlock operations. +TextMetrics unwrapped = block.Measure(-1); +``` + +`TextBlock` also exposes direct detail APIs when a full `TextMetrics` object is +not needed: + +```csharp +ReadOnlyMemory lines = block.GetLineMetrics(320); +ReadOnlyMemory graphemes = block.GetGraphemeMetrics(320); +ReadOnlyMemory words = block.GetWordMetrics(320); +ReadOnlyMemory glyphs = block.GetGlyphMetrics(320); +``` + +Method-returned measurement collections use `ReadOnlyMemory` because they are +snapshots that callers may store with their own layout state. Owner-backed +properties, such as `TextMetrics.LineMetrics` and `LineLayout.GraphemeMetrics`, +use `ReadOnlySpan` because the owner object already controls the lifetime. + +## TextMetrics + +`TextMetrics` is the result to keep when callers need several measurements from +the same laid-out text. + +```csharp +TextMetrics metrics = TextMeasurer.Measure(text, options); + +// These aggregate measurements answer different layout and rendering questions. +FontRectangle advance = metrics.Advance; +FontRectangle bounds = metrics.Bounds; +FontRectangle renderableBounds = metrics.RenderableBounds; +int lineCount = metrics.LineCount; + +ReadOnlySpan lines = metrics.LineMetrics; +ReadOnlySpan graphemes = metrics.GraphemeMetrics; +ReadOnlySpan words = metrics.WordMetrics; +``` + +The line and grapheme collections are in final layout order. That matters for +bidi text and reverse line-order layout modes: source order and visual order can +be different. + +`WordMetrics` are in source order because word-boundary navigation is a logical +text operation. Selection and caret APIs convert those logical metrics back into +visual geometry when needed. + +## Line Metrics + +`LineMetrics` describes one laid-out line. + +```csharp +foreach (LineMetrics line in metrics.LineMetrics) +{ + // Start and Extent describe the positioned line box. + Vector2 start = line.Start; + Vector2 extent = line.Extent; + float baseline = line.Baseline; +} +``` + +`Start` and `Extent` describe the positioned line box in pixel units. Selection +and caret APIs use the line box for the cross-axis size, which matches normal +text editor and browser behavior: selecting mixed font sizes on the same line +paints a consistent line-height rectangle rather than one rectangle per glyph +height. + +`StringIndex`, `GraphemeIndex`, and `GraphemeCount` describe the source text +range owned by the line. `GraphemeCount` is not a glyph count. + +## Grapheme Metrics + +Use `GraphemeMetrics` for text interaction: hit testing, caret positioning, +range selection, and UI overlays. + +```csharp +foreach (GraphemeMetrics grapheme in metrics.GraphemeMetrics) +{ + // Use Advance for interaction and Bounds for rendered ink. + FontRectangle advance = grapheme.Advance; + FontRectangle bounds = grapheme.Bounds; + FontRectangle renderableBounds = grapheme.RenderableBounds; + bool isLineBreak = grapheme.IsLineBreak; +} +``` + +The rectangles answer different questions: + +- `Advance`: the positioned logical advance rectangle for the grapheme. +- `Bounds`: the rendered glyph bounds for the grapheme. +- `RenderableBounds`: the union of advance and rendered glyph bounds. + +Use `Advance` for hit targets, carets, and selection geometry. Ink bounds can be +empty, overhang the advance, or exclude whitespace, so they are not a reliable +interaction target. + +`IsLineBreak` identifies hard-break graphemes that remain in the laid-out +metrics. Hard breaks at the end of non-empty lines are trimmed with other +trailing breaking whitespace; hard breaks that own blank lines remain because +they provide the line geometry for selection and caret behavior. + +## Word Metrics + +`WordMetrics` describes one Unicode word-boundary segment from UAX #29. + +```csharp +foreach (WordMetrics word in metrics.WordMetrics) +{ + FontRectangle advance = word.Advance; + FontRectangle bounds = word.Bounds; + FontRectangle renderableBounds = word.RenderableBounds; + int graphemeStart = word.GraphemeStart; + int graphemeEnd = word.GraphemeEnd; + int stringStart = word.StringStart; + int stringEnd = word.StringEnd; +} +``` + +`Advance`, `Bounds`, and `RenderableBounds` have the same meanings as the +equivalent `GraphemeMetrics` rectangles, but accumulated across the +word-boundary segment. Whitespace segments keep their positioned bounds; they +are not discarded just because they are separators. + +All `Start` values on `WordMetrics` are inclusive. All `End` values are exclusive. +`GraphemeStart` and `GraphemeEnd` are grapheme insertion indices. `StringStart` +and `StringEnd` are UTF-16 indices into the original text. + +Unicode word-boundary segments include separators. For example, `can't stop` +contains three segments: + +```text +can't +[space] +stop +``` + +This keeps the raw API aligned with the Unicode standard. Higher-level editor +commands can choose whether to stop on separator boundaries or skip over them. + +## Glyph Metrics + +Glyph detail APIs expose laid-out glyph entries. + +```csharp +ReadOnlyMemory glyphs = metrics.GetGlyphMetrics(); + +foreach (GlyphMetrics glyph in glyphs.Span) +{ + FontRectangle advance = glyph.Advance; + FontRectangle bounds = glyph.Bounds; + FontRectangle renderableBounds = glyph.RenderableBounds; + CodePoint codePoint = glyph.CodePoint; +} +``` + +Use glyph detail for rendering diagnostics, glyph-level visualization, or +advanced inspection. Do not use glyph entries as character or caret positions: +ligatures, decomposition, fallback, emoji, and combining marks mean one +grapheme can map to multiple glyph entries, and multiple source characters can +map to one visual glyph sequence. + +## Per-Line Layout + +`TextBlock.GetLineLayouts` returns line objects when callers want line-local +inspection or interaction. + +```csharp +TextBlock block = new(text, options); +ReadOnlyMemory layout = block.GetLineLayouts(320); + +foreach (LineLayout line in layout.Span) +{ + // LineLayout exposes the slice of grapheme metrics owned by this line. + LineMetrics lineMetrics = line.LineMetrics; + ReadOnlySpan lineGraphemes = line.GraphemeMetrics; +} +``` + +`LineLayout` mirrors the interaction and glyph-detail surface for a single line: + +```csharp +TextHit hit = line.HitTest(point); + +// Passing the hit keeps trailing-edge and bidi handling inside the library. +CaretPosition caret = line.GetCaretPosition(hit); +CaretPosition next = line.MoveCaret(caret, CaretMovement.Next); +WordMetrics word = line.GetWordMetrics(hit); +ReadOnlyMemory selection = line.GetSelectionBounds(caret, next); +ReadOnlyMemory wordSelection = line.GetSelectionBounds(word); +ReadOnlyMemory glyphs = line.GetGlyphMetrics(); +``` + +Use the full `TextMetrics` interaction methods for selections that can cross +line boundaries. Use `LineLayout` when the caller already knows interaction is +line-local. + +## Hit Testing + +Hit testing maps a point to the nearest grapheme and side. + +```csharp +TextHit hit = metrics.HitTest(mousePosition); + +int lineIndex = hit.LineIndex; +int graphemeIndex = hit.GraphemeIndex; +// Use this value for carets and selection endpoints. +int insertionIndex = hit.GraphemeInsertionIndex; +``` + +`GraphemeIndex` identifies the hit grapheme. `GraphemeInsertionIndex` identifies +the logical caret position represented by the hit. For left-to-right text, the +trailing side is usually `GraphemeIndex + 1`. For right-to-left text, the +physical side is reversed, but callers do not need to apply that rule. Use +`GraphemeInsertionIndex` or pass the `TextHit` directly to caret and selection +APIs. + +For word selection, pass the hit directly to `GetWordMetrics`. This uses the +grapheme that was hit, so clicking the trailing side of the final grapheme in a +word still selects that word rather than the following separator segment. + +```csharp +TextHit hit = metrics.HitTest(mousePosition); +WordMetrics word = metrics.GetWordMetrics(hit); +ReadOnlyMemory selection = metrics.GetSelectionBounds(word); +``` + +## Caret Positioning + +Caret APIs return positioned caret lines in pixel units. A caret is also the +navigation token for keyboard/editor interaction. + +```csharp +TextHit hit = metrics.HitTest(mousePosition); + +// The hit overload applies the correct grapheme insertion index. +CaretPosition caret = metrics.GetCaretPosition(hit); + +DrawCaret(caret.Start, caret.End); + +if (caret.HasSecondary) +{ + DrawSecondaryCaret(caret.SecondaryStart, caret.SecondaryEnd); +} +``` + +Use absolute placement when initializing a keyboard caret without a pointer hit. + +```csharp +CaretPosition caret = metrics.GetCaret(CaretPlacement.Start); +``` + +At bidi boundaries, one logical insertion position can have two visual edges. +`CaretPosition` exposes the secondary edge so editor-style callers can choose how +to present or navigate that boundary without recomputing bidi affinity. + +## Caret Movement + +`MoveCaret` applies editor-style movement to a caret and returns the new caret. + +```csharp +CaretPosition caret = metrics.GetCaret(CaretPlacement.Start); + +// Previous and Next move through logical grapheme insertion positions. +caret = metrics.MoveCaret(caret, CaretMovement.Next); + +// PreviousWord and NextWord move through Unicode word boundaries. +caret = metrics.MoveCaret(caret, CaretMovement.NextWord); + +// LineStart and LineEnd are the Home/End-style line movement operations. +caret = metrics.MoveCaret(caret, CaretMovement.LineEnd); + +// TextStart and TextEnd are the whole-block equivalents. +caret = metrics.MoveCaret(caret, CaretMovement.TextStart); +``` + +`LineUp` and `LineDown` move to adjacent visual lines while preserving the +caret's requested position on the line. + +```csharp +CaretPosition firstLineEnd = metrics.GetCaret(CaretPlacement.Start); +firstLineEnd = metrics.MoveCaret(firstLineEnd, CaretMovement.LineEnd); + +// Repeated LineDown keeps the original line position even when an intermediate +// line is shorter and the visible caret has to clamp to that line's end. +CaretPosition middleLine = metrics.MoveCaret(firstLineEnd, CaretMovement.LineDown); +CaretPosition finalLine = metrics.MoveCaret(middleLine, CaretMovement.LineDown); +``` + +This preserves normal rich-text editor behavior: moving down through a short line +does not permanently lose the user's original horizontal or vertical line +position. + +## Selection Bounds + +Selection APIs return rectangles in visual order and pixel units. The result is +`ReadOnlyMemory` so callers can store it with selection state and +use `.Span` when drawing. + +For pointer selection, use the hit overload. This keeps bidi and trailing-edge +logic inside the library. + +```csharp +TextHit anchor = metrics.HitTest(mouseDown); +TextHit focus = metrics.HitTest(mouseMove); + +// The hit overload converts both endpoints to logical insertion indices. +ReadOnlyMemory selection = metrics.GetSelectionBounds(anchor, focus); + +foreach (FontRectangle rectangle in selection.Span) +{ + FillSelectionRectangle(rectangle); +} +``` + +For keyboard selection, keep an anchor caret and move the focus caret. + +```csharp +CaretPosition anchor = metrics.GetCaret(CaretPlacement.Start); +CaretPosition focus = anchor; + +// Shift+Right-style behavior updates only the focus caret. +focus = metrics.MoveCaret(focus, CaretMovement.Next); + +ReadOnlyMemory selection = metrics.GetSelectionBounds(anchor, focus); +``` + +For word selection, use the word metrics overload. + +```csharp +TextHit hit = metrics.HitTest(doubleClickPosition); +WordMetrics word = metrics.GetWordMetrics(hit); + +ReadOnlyMemory selection = metrics.GetSelectionBounds(word); +``` + +Do not sort, union, or merge the returned rectangles unless the UI explicitly +wants a different visual. A single logical selection can be visually +discontinuous inside one line when it crosses bidi runs. Returning multiple +rectangles allows browser-style selection where the unselected visual gap stays +unpainted. + +## Bidi Drag Selection + +Consider a line whose source text is: + +```text +Tall שלום عرب +``` + +In a left-to-right paragraph, the right-to-left run can paint with Arabic before +Hebrew. When a user drags from the left edge of `Tall` toward the Hebrew word, +the selection can become visually split: + +```text +[Tall ] عرب [שלום] +``` + +Application code should not manually decide which physical edge of the Hebrew +glyph means "before" or "after". The correct flow is: + +```csharp +TextHit anchor = metrics.HitTest(mouseDown); +TextHit focus = metrics.HitTest(mouseMove); + +// Bidi split selection is represented by the returned rectangle list. +ReadOnlyMemory rectangles = metrics.GetSelectionBounds(anchor, focus); +``` + +The hit-test result carries the logical insertion index. The selection result is +already split into the visual rectangles that should be painted. + +## Hard Line Breaks + +Hard line breaks that end non-empty lines are trimmed with trailing breaking +whitespace. Hard line breaks that own blank lines remain as graphemes for source +ranges, hit testing, caret movement, and selection painting. + +For text with two hard breaks in the middle: + +```text +Tall عرب שלום + +Small مرحبا שלום +``` + +Full selection should paint three visual rows: the first text line, the blank +line, and the second text line. The line break that ends a non-empty line should +not add a separate painted box; the line break that owns the blank line should. + +Consumers should not special-case this. Draw the rectangles returned by +`GetSelectionBounds`. Consumers that inspect individual graphemes can use +`IsLineBreak` to identify the blank-line hard breaks that remain in the metrics. + +## Recommended Workflows + +For one-off measuring: + +```csharp +// One-shot path for a single layout result. +TextMetrics metrics = TextMeasurer.Measure(text, options); +``` + +For repeated wrapping or rendering: + +```csharp +TextBlock block = new(text, options); + +// Reuse the prepared text for each requested wrapping length. +TextMetrics narrow = block.Measure(240); +TextMetrics wide = block.Measure(480); +block.RenderTo(renderer, 480); +``` + +For text editor interaction: + +```csharp +TextMetrics metrics = block.Measure(wrappingLength); + +TextHit anchor = metrics.HitTest(mouseDown); +TextHit focus = metrics.HitTest(mouseMove); + +// Use hit-based overloads so interaction follows the laid-out bidi result. +CaretPosition caret = metrics.GetCaretPosition(focus); +ReadOnlyMemory selection = metrics.GetSelectionBounds(anchor, focus); +``` + +For keyboard navigation and selection: + +```csharp +TextMetrics metrics = block.Measure(wrappingLength); +CaretPosition caret = metrics.GetCaret(CaretPlacement.Start); +CaretPosition anchor = caret; + +// The movement operation owns grapheme, line, and hard-break navigation rules. +caret = metrics.MoveCaret(caret, CaretMovement.LineDown); +caret = metrics.MoveCaret(caret, CaretMovement.NextWord); +ReadOnlyMemory selection = metrics.GetSelectionBounds(anchor, caret); +``` + +For per-line UI: + +```csharp +ReadOnlyMemory lines = block.GetLineLayouts(wrappingLength); + +foreach (LineLayout line in lines.Span) +{ + ReadOnlySpan graphemes = line.GraphemeMetrics; + ReadOnlyMemory glyphs = line.GetGlyphMetrics(); +} +``` + +## Design Principles + +- The library owns bidi, grapheme, hard-break, wrapping, and layout-mode rules. +- Callers should pass points, hits, or logical ranges and draw the returned geometry. +- Caret movement should flow through `MoveCaret`, not caller-side grapheme arithmetic. +- Word selection should flow through `GetWordMetrics`, not caller-side Unicode boundary logic. +- Grapheme metrics are the text interaction unit. +- Word metrics describe logical source segments and their positioned geometry; + selection bounds are the visual geometry. +- Glyph metrics are rendering-detail data, not caret or character data. +- Selection rectangles are visual geometry, not a single logical union. +- Per-line selection uses line-box height so selection remains visually stable + across mixed fonts and font sizes. diff --git a/SixLabors.Fonts/ReadOnlyArraySlice{T}.cs b/SixLabors.Fonts/ReadOnlyArraySlice{T}.cs new file mode 100644 index 0000000..01840fc --- /dev/null +++ b/SixLabors.Fonts/ReadOnlyArraySlice{T}.cs @@ -0,0 +1,199 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.Fonts { + /// + /// ReadOnlyArraySlice represents a contiguous region of arbitrary memory similar + /// to and though constrained + /// to arrays. + /// Unlike , it is not a byref-like type. + /// + /// The type of item contained in the slice. + internal readonly struct ReadOnlyArraySlice : IEnumerable + where T : struct + { + private readonly T[] data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data buffer. + public ReadOnlyArraySlice(T[] data) + : this(data, 0, data.Length) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data buffer. + /// The offset position in the underlying buffer this slice was created from. + /// The number of items in the slice. + public ReadOnlyArraySlice(T[] data, int start, int length) + { + DebugGuard.MustBeGreaterThanOrEqualTo(start, 0, nameof(start)); + DebugGuard.MustBeLessThanOrEqualTo(length, data.Length, nameof(length)); + DebugGuard.MustBeLessThanOrEqualTo(start + length, data.Length, nameof(this.data)); + + this.data = data; + this.Start = start; + this.Length = length; + } + + /// + /// Gets an empty + /// + public static ReadOnlyArraySlice Empty => new(Array.Empty()); + + /// + /// Gets the offset position in the underlying buffer this slice was created from. + /// + public int Start { get; } + + /// + /// Gets the number of items in the slice. + /// + public int Length { get; } + + /// + /// Gets a representing this slice. + /// + public ReadOnlySpan Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(this.data, this.Start, this.Length); + } + + /// + /// Returns a reference to specified element of the slice. + /// + /// The index of the element to return. + /// The . + /// + /// Thrown when index less than 0 or index greater than or equal to . + /// + public readonly T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + DebugGuard.MustBeBetweenOrEqualTo(index, 0, this.Length, nameof(index)); + ref T b = ref MemoryMarshal.GetReference(this.Span); + return Unsafe.Add(ref b, index); + } + } + + /// + /// Defines an implicit conversion of an array to a + /// + /// The input array. + public static implicit operator ReadOnlyArraySlice(T[] array) + => new(array, 0, array.Length); + + /// + /// Copies the contents of this slice into destination span. If the source + /// and destinations overlap, this method behaves as if the original values in + /// a temporary location before the destination is overwritten. + /// + /// The slice to copy items into. + /// + /// Thrown when the destination slice is shorter than the source Span. + /// + public void CopyTo(ArraySlice destination) + => this.Span.CopyTo(destination.Span); + + /// + /// Forms a slice out of the given slice, beginning at 'start', of given length + /// + /// The index at which to begin this slice. + /// The desired length for the slice (exclusive). + /// + /// Thrown when the specified or end index is not in range (<0 or >Length). + /// + public ReadOnlyArraySlice Slice(int start, int length) + => new(this.data, start, length); + + /// + public IEnumerator GetEnumerator() => new Enumerator(this); + + /// + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); + + public struct Enumerator : IEnumerator + { + private readonly T[]? array; + private readonly int start; + private readonly int end; // cache Start + Length, since it's a little slow + private int current; + + internal Enumerator(ReadOnlyArraySlice slice) + { + DebugGuard.NotNull(slice.data, nameof(slice.data)); + DebugGuard.MustBeGreaterThanOrEqualTo(slice.Start, 0, nameof(slice.Start)); + DebugGuard.MustBeGreaterThanOrEqualTo(slice.Length, 0, nameof(slice.Length)); + + DebugGuard.MustBeLessThanOrEqualTo( + slice.Start + slice.Length, + slice.data.Length, + nameof(slice.data.Length)); + + this.array = slice.data; + this.start = slice.Start; + this.end = slice.Start + slice.Length; + this.current = slice.Start - 1; + } + + /// + public readonly T Current + { + get + { + if (this.current < this.start) + { + ThrowEnumNotStarted(); + } + + if (this.current >= this.end) + { + ThrowEnumEnded(); + } + + return this.array![this.current]; + } + } + + object? IEnumerator.Current => this.Current; + + /// + public bool MoveNext() + { + if (this.current < this.end) + { + this.current++; + return this.current < this.end; + } + + return false; + } + + /// + void IEnumerator.Reset() => this.current = this.start - 1; + + public readonly void Dispose() + { + } + + private static void ThrowEnumNotStarted() + => throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); + + private static void ThrowEnumEnded() + => throw new InvalidOperationException("Enumeration already finished."); + } + } +} diff --git a/SixLabors.Fonts/ReadOnlyMappedArraySlice{T}.cs b/SixLabors.Fonts/ReadOnlyMappedArraySlice{T}.cs new file mode 100644 index 0000000..4730e1f --- /dev/null +++ b/SixLabors.Fonts/ReadOnlyMappedArraySlice{T}.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts { + /// + /// Provides a readonly mapped view of an underlying slice, selecting arbitrary indices + /// from the source array. + /// + /// The type of item contained in the underlying array. + internal readonly struct ReadonlyMappedArraySlice + where T : struct + { + private readonly ReadOnlyArraySlice data; + private readonly ReadOnlyArraySlice map; + + /// + /// Initializes a new instance of the struct. + /// + /// The data slice. + /// The map slice. + public ReadonlyMappedArraySlice(in ReadOnlyArraySlice data, in ReadOnlyArraySlice map) + { + Guard.MustBeGreaterThanOrEqualTo(data.Length, map.Length, nameof(map)); + + this.data = data; + this.map = map; + } + + /// + /// Gets the number of items in the map. + /// + public int Length => this.map.Length; + + /// + /// Returns a reference to specified element of the slice. + /// + /// The index of the element to return. + /// The . + /// + /// Thrown when index less than 0 or index greater than or equal to . + /// + public readonly T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.data[this.map[index]]; + } + } +} diff --git a/SixLabors.Fonts/Rendering/CompositeMode.cs b/SixLabors.Fonts/Rendering/CompositeMode.cs new file mode 100644 index 0000000..e180dd3 --- /dev/null +++ b/SixLabors.Fonts/Rendering/CompositeMode.cs @@ -0,0 +1,183 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Rendering { + /// + /// Defines compositing and blending operations used when combining source and destination colors. + /// + /// Values 0–12 correspond to standard Porter–Duff compositing modes. These determine how source and + /// destination alpha interact to produce transparency. The remaining values (13–27) correspond to + /// separable and non-separable blend modes used in modern graphics systems such as ImageSharp. + /// + /// + public enum CompositeMode + { + // --- Porter–Duff compositing modes --- + + /// + /// Clears both the source and destination. + /// The output is fully transparent regardless of the input colors. + /// + Clear = 0, + + /// + /// Replaces the destination entirely with the source. + /// The destination pixels are ignored. + /// + Src = 1, + + /// + /// Keeps the destination as-is and ignores the source. + /// Equivalent to no drawing operation. + /// + Dest = 2, + + /// + /// Draws the source over the destination using standard alpha compositing. + /// The source appears on top and the destination shows through transparent areas. + /// + SrcOver = 3, + + /// + /// Draws the destination over the source. + /// The destination appears on top and the source shows through transparent areas. + /// + DestOver = 4, + + /// + /// Shows the source only where it overlaps the destination. + /// The destination’s alpha acts as a mask for the source. + /// + SrcIn = 5, + + /// + /// Shows the destination only where it overlaps the source. + /// The source’s alpha acts as a mask for the destination. + /// + DestIn = 6, + + /// + /// Shows the source only where it does not overlap the destination. + /// Produces the inverse of . + /// + SrcOut = 7, + + /// + /// Shows the destination only where it does not overlap the source. + /// Produces the inverse of . + /// + DestOut = 8, + + /// + /// Draws the source over the destination but only within the destination’s alpha region. + /// Outside that region, the destination remains unchanged. + /// + SrcAtop = 9, + + /// + /// Draws the destination over the source but only within the source’s alpha region. + /// Outside that region, the source is visible. + /// + DestAtop = 10, + + /// + /// Exclusive OR. + /// Shows the source and destination only where they do not overlap. + /// Overlapping regions become transparent. + /// + Xor = 11, + + /// + /// Adds the source and destination color values. + /// Alpha is also added, producing a brightening effect. + /// + Plus = 12, + + // --- Separable and non-separable blend modes --- + + /// + /// Combines colors using an inverse multiply. + /// Formula: 1 − (1 − S) × (1 − D). + /// Produces a lighter result similar to photographic screen exposure. + /// + Screen = 13, + + /// + /// Multiplies or screens colors depending on destination lightness. + /// Preserves highlights and shadows while mixing source and destination tones. + /// + Overlay = 14, + + /// + /// Chooses the darker of source and destination values per color channel. + /// + Darken = 15, + + /// + /// Chooses the lighter of source and destination values per color channel. + /// + Lighten = 16, + + /// + /// Brightens the destination to reflect the source. + /// Formula: D / (1 − S). + /// + ColorDodge = 17, + + /// + /// Darkens the destination to reflect the source. + /// Formula: 1 − (1 − D) / S. + /// + ColorBurn = 18, + + /// + /// Applies overlay logic using the source’s lightness. + /// Used for strong highlight and shadow effects. + /// + HardLight = 19, + + /// + /// Similar to , but with reduced contrast. + /// Produces a softer transition between tones. + /// + SoftLight = 20, + + /// + /// Subtracts darker colors from lighter ones to highlight differences. + /// Often used for comparison or edge detection effects. + /// + Difference = 21, + + /// + /// Similar to , but with reduced contrast. + /// Midtones are preserved, producing a lower-contrast difference. + /// + Exclusion = 22, + + /// + /// Multiplies source and destination colors. + /// Always results in a darker composite. + /// + Multiply = 23, + + /// + /// Combines the hue of the source with the saturation and luminosity of the destination. + /// + Hue = 24, + + /// + /// Combines the saturation of the source with the hue and luminosity of the destination. + /// + Saturation = 25, + + /// + /// Combines the hue and saturation of the source with the luminosity of the destination. + /// + Color = 26, + + /// + /// Combines the luminosity of the source with the hue and saturation of the destination. + /// + Luminosity = 27 + } +} diff --git a/SixLabors.Fonts/Rendering/FillRule.cs b/SixLabors.Fonts/Rendering/FillRule.cs new file mode 100644 index 0000000..b172d47 --- /dev/null +++ b/SixLabors.Fonts/Rendering/FillRule.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Rendering { + /// + /// Specifies the fill rule for path rasterization. + /// + public enum FillRule + { + /// + /// Non-zero winding rule. + /// + NonZero = 0, + + /// + /// Even-odd rule. + /// + EvenOdd = 1, + } +} diff --git a/SixLabors.Fonts/Rendering/GlyphRendererExtensions.cs b/SixLabors.Fonts/Rendering/GlyphRendererExtensions.cs new file mode 100644 index 0000000..50a7c3c --- /dev/null +++ b/SixLabors.Fonts/Rendering/GlyphRendererExtensions.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Rendering { + /// + /// A surface that can have a glyph rendered to it as a series of actions. + /// + public static class GlyphRendererExtensions + { + /// + /// Renders the text. + /// + /// The target renderer surface. + /// The text. + /// The options. + /// Returns the original + public static IGlyphRenderer Render(this IGlyphRenderer renderer, ReadOnlySpan text, TextOptions options) + { + new TextRenderer(renderer).RenderText(text, options); + return renderer; + } + } +} diff --git a/SixLabors.Fonts/Rendering/GlyphRendererParameters.cs b/SixLabors.Fonts/Rendering/GlyphRendererParameters.cs new file mode 100644 index 0000000..2d6733c --- /dev/null +++ b/SixLabors.Fonts/Rendering/GlyphRendererParameters.cs @@ -0,0 +1,164 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; +using System.Globalization; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Rendering { + /// + /// The combined set of properties that uniquely identify the glyph that is to be rendered + /// at a particular size and dpi. + /// + [DebuggerDisplay("GlyphId = {GlyphId}, CodePoint = {CodePoint}, PointSize = {PointSize}, Dpi = {Dpi}")] + public readonly struct GlyphRendererParameters : IEquatable + { + internal GlyphRendererParameters( + FontGlyphMetrics metrics, + TextRun textRun, + float pointSize, + float dpi, + GlyphLayoutMode layoutMode, + int graphemeIndex) + { + this.Font = metrics.FontMetrics.Description.FontNameInvariantCulture?.ToUpper(CultureInfo.InvariantCulture) ?? string.Empty; + this.FontStyle = metrics.FontMetrics.Description.Style; + this.GlyphId = metrics.GlyphId; + this.GraphemeIndex = graphemeIndex; + this.PointSize = pointSize; + this.Dpi = dpi; + this.GlyphType = metrics.GlyphType; + this.TextRun = textRun; + this.CodePoint = metrics.CodePoint; + this.LayoutMode = layoutMode; + } + + /// + /// Gets the name of the Font this glyph belongs to. + /// + public string Font { get; } + + /// + /// Gets the type of this glyph. + /// + public GlyphType GlyphType { get; } + + /// + /// Gets the style of the font this glyph belongs to. + /// + public FontStyle FontStyle { get; } + + /// + /// Gets the id of the glyph within the font tables. + /// + public ushort GlyphId { get; } + + /// + /// Gets the id of the composite glyph if the is ; + /// + public ushort CompositeGlyphId { get; } + + /// + /// Gets the zero-based grapheme index in the original text. + /// + public int GraphemeIndex { get; } + + /// + /// Gets the codepoint represented by this glyph. + /// + public CodePoint CodePoint { get; } + + /// + /// Gets the rendered point size. + /// + public float PointSize { get; } + + /// + /// Gets the dots-per-inch the glyph is to be rendered at. + /// + public float Dpi { get; } + + /// + /// Gets the layout mode applied to the glyph. + /// + public GlyphLayoutMode LayoutMode { get; } + + /// + /// Gets the text run that this glyph belongs to. + /// + public TextRun TextRun { get; } + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + public static bool operator ==(GlyphRendererParameters left, GlyphRendererParameters right) + => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + public static bool operator !=(GlyphRendererParameters left, GlyphRendererParameters right) + => !left.Equals(right); + + /// + public bool Equals(GlyphRendererParameters other) + => other.PointSize == this.PointSize + && other.FontStyle == this.FontStyle + && other.Dpi == this.Dpi + && other.GlyphId == this.GlyphId + && other.CompositeGlyphId == this.CompositeGlyphId + && this.GraphemeIndex == other.GraphemeIndex + && other.GlyphType == this.GlyphType + && other.TextRun.TextAttributes == this.TextRun.TextAttributes + && other.TextRun.TextDecorations == this.TextRun.TextDecorations + && other.LayoutMode == this.LayoutMode + && ((other.Font is null && this.Font is null) + || (other.Font?.Equals(this.Font, StringComparison.OrdinalIgnoreCase) == true)); + + /// + public override bool Equals(object? obj) + => obj is GlyphRendererParameters parameters && this.Equals(parameters); + + /// + public override int GetHashCode() + { + int a = HashCode.Combine( + this.Font, + this.PointSize, + this.GlyphId, + this.GlyphType, + this.FontStyle); + + int b = HashCode.Combine( + this.Dpi, + this.TextRun.TextAttributes, + this.TextRun.TextDecorations, + this.LayoutMode); + + int c = HashCode.Combine( + this.CompositeGlyphId, + this.GraphemeIndex); + + return HashCode.Combine(a, b, c); + } + } +} diff --git a/SixLabors.Fonts/Rendering/GradientStop.cs b/SixLabors.Fonts/Rendering/GradientStop.cs new file mode 100644 index 0000000..87c5b70 --- /dev/null +++ b/SixLabors.Fonts/Rendering/GradientStop.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Rendering { + /// + /// Defines a color stop for gradient paints. + /// Offsets must be clamped to the range [0, 1] by the interpreter. + /// Colors are direct RGBA and must not reference palettes. + /// + public readonly struct GradientStop + { + /// + /// Initializes a new instance of the struct. + /// + /// The stop position in the range [0, 1]. + /// The color at the stop. + public GradientStop(float offset, GlyphColor color) + { + this.Offset = offset; + this.Color = color; + } + + /// + /// Gets the stop position in the range [0, 1]. + /// + public float Offset { get; } + + /// + /// Gets the color at the stop (direct RGBA). + /// + public GlyphColor Color { get; } + } +} diff --git a/SixLabors.Fonts/Rendering/GradientUnits.cs b/SixLabors.Fonts/Rendering/GradientUnits.cs new file mode 100644 index 0000000..ce1bc05 --- /dev/null +++ b/SixLabors.Fonts/Rendering/GradientUnits.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Rendering { + /// + /// Coordinate system to interpret gradient geometry. + /// + public enum GradientUnits + { + /// + /// Coordinates are normalized to the painted geometry's bounds ([0, 1] in X and Y). + /// The renderer will map these to the actual path bounds at paint time. + /// + ObjectBoundingBox = 0, + + /// + /// Coordinates are absolute in the same space as the already-transformed geometry. + /// Interpreters must pre-apply any gradient transforms before creating the paint. + /// + UserSpaceOnUse = 1, + } +} diff --git a/SixLabors.Fonts/Rendering/IGlyphRenderer.cs b/SixLabors.Fonts/Rendering/IGlyphRenderer.cs new file mode 100644 index 0000000..d45f3b9 --- /dev/null +++ b/SixLabors.Fonts/Rendering/IGlyphRenderer.cs @@ -0,0 +1,135 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts.Rendering { + /// + /// A surface that can have a glyph rendered to it as a series of actions. + /// + public interface IGlyphRenderer + { + /// + /// Called before any glyphs have been rendered. + /// + /// The rectangle within the text will be rendered. + public void BeginText(in FontRectangle bounds); + + /// + /// Called once all glyphs have completed rendering. + /// + public void EndText(); + + /// + /// Begins the glyph. + /// + /// The bounds the glyph will be rendered at and at what size. + /// + /// The set of parameters that uniquely represents a version of a glyph at particular font size, font family, font style and DPI. + /// + /// + /// Returns if the glyph should be rendered otherwise it returns . + /// + public bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters); + + /// + /// Ends the glyph. + /// + public void EndGlyph(); + + /// + /// Begins a new painted layer with the specified paint and fill rule. + /// All geometry commands issued after this call belong to the layer until is called. + /// + /// The paint definition. + /// The fill rule to use when rasterizing this layer. + /// The optional clip bounds to apply when rasterizing this layer. + public void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds); + + /// + /// Ends the current painted layer. + /// + public void EndLayer(); + + /// + /// Begins the figure. + /// + public void BeginFigure(); + + /// + /// Sets a new start point to draw lines from. + /// + /// The point. + public void MoveTo(Vector2 point); + + /// + /// Draw a straight line connecting the previous point to . + /// + /// The point. + public void LineTo(Vector2 point); + + /// + /// Draw a quadratic bezier curve connecting the previous point to . + /// + /// The second control point. + /// The point. + public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point); + + /// + /// Draw a cubic bezier curve connecting the previous point to . + /// + /// The second control point. + /// The third control point. + /// The point. + public void CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point); + + /// + /// + /// Adds an elliptical arc to the current figure. The arc curves from the last point to , + /// choosing one of four possible routes: clockwise or counterclockwise, and smaller or larger. + /// + /// + /// The arc sweep is always less than 360 degrees. The method appends a line + /// to the last point if either radii are zero, or if last point is equal to . + /// In addition the method scales the radii to fit last point and if both + /// are greater than zero but too small to describe an arc. + /// + /// + /// The x-radius of the ellipsis. + /// The y-radius of the ellipsis. + /// The rotation along the X-axis; measured in degrees clockwise. + /// + /// The large arc flag, and is if an arc spanning less than or equal to 180 degrees + /// is chosen, or if an arc spanning greater than 180 degrees is chosen. + /// + /// + /// The sweep flag, and is if the line joining center to arc sweeps through decreasing + /// angles, or if it sweeps through increasing angles. + /// + /// The end point of the arc. + public void ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, Vector2 point); + + /// + /// Ends the figure. + /// + public void EndFigure(); + + /// + /// Provides a callback to enable custom logic to request decoration details. + /// A custom might use alternative triggers to determine what decorations it needs access to. + /// + /// The text decorations the render wants render info for. + public TextDecorations EnabledDecorations(); + + /// + /// Sets the details of a text decoration to be rendered. + /// This only gets called if the decoration type was requested via + /// and after the glyph has been rendered via and . + /// + /// The type of decoration these details correspond to. + /// The start position from where to draw the decorations from. + /// The end position from where to draw the decorations to. + /// The thickness to draw the decoration. + public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness); + } +} diff --git a/SixLabors.Fonts/Rendering/IPaintedGlyphSource.cs b/SixLabors.Fonts/Rendering/IPaintedGlyphSource.cs new file mode 100644 index 0000000..ffd3963 --- /dev/null +++ b/SixLabors.Fonts/Rendering/IPaintedGlyphSource.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Rendering { + /// + /// Supplies painted glyphs (layers + commands + paints) and canvas metadata for a glyph id. + /// Interpreters (e.g., COLR v1, OT-SVG) implement this interface. + /// + internal interface IPaintedGlyphSource + { + /// + /// Attempts to get a painted glyph and its canvas metadata. + /// + /// The glyph id. + /// The painted glyph. + /// The canvas metadata. + /// if the glyph is available; otherwise . + public bool TryGetPaintedGlyph(ushort glyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas); + } +} diff --git a/SixLabors.Fonts/Rendering/Paint.cs b/SixLabors.Fonts/Rendering/Paint.cs new file mode 100644 index 0000000..45d1569 --- /dev/null +++ b/SixLabors.Fonts/Rendering/Paint.cs @@ -0,0 +1,165 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts.Rendering { + /// + /// Base type for normalized paint definitions that can be used by any renderer. + /// Glyph sources must pre-apply all relevant transforms and resolve any palette + /// or format-specific constructs before creating a paint instance. + /// + public abstract class Paint + { + /// + /// Gets the per-layer opacity multiplier in the range [0, 1]. + /// Renderers should multiply this value into the alpha channel of the final brush. + /// + public float Opacity { get; init; } = 1f; + + /// + /// Gets or sets an optional transform to apply to the paint. + /// Used to pre-apply gradientTransform in SVG or equivalent. + /// + internal Matrix3x2 Transform { get; set; } + + /// + /// Gets the composite mode to use when applying this paint over existing content. + /// + public CompositeMode CompositeMode { get; init; } = CompositeMode.SrcOver; + } + + /// + /// Solid color paint (direct RGBA). Interpreters must resolve palettes to RGBA. + /// Compatible with OT-SVG solid fills and COLR v1 PaintSolid after CPAL resolution. + /// + public sealed class SolidPaint : Paint + { + /// + /// Gets the color to use for the fill. Alpha is respected and further multiplied by . + /// + public GlyphColor Color { get; init; } + } + + /// + /// Linear gradient paint. + /// + public sealed class LinearGradientPaint : Paint + { + /// + /// Gets the coordinate system for and . + /// + internal GradientUnits Units { get; init; } + + /// + /// Gets the gradient start point. Normalized if is . + /// + public Vector2 P0 { get; init; } + + /// + /// Gets the gradient end point. Normalized if is . + /// + public Vector2 P1 { get; init; } + + /// + /// Gets the rotation point for the gradient. Normalized if is . + /// + public Vector2? P2 { get; init; } + + /// + /// Gets the spread method applied when sampling outside the [0, 1] range. + /// + public SpreadMethod Spread { get; init; } = SpreadMethod.Pad; + + /// + /// Gets the ordered gradient stops (ascending by ). + /// + public GradientStop[] Stops { get; init; } = []; + } + + /// + /// Represents a radial gradient paint defined by two circles. + /// The first circle is centered at with radius . + /// The second circle is centered at with radius . + /// The color transition is computed between these two circles. + /// Compatible with two-circle radial gradients used by HTML Canvas and OpenType COLR v1. + /// + public sealed class RadialGradientPaint : Paint + { + /// + /// Gets the coordinate system for , , + /// , and . + /// + internal GradientUnits Units { get; init; } + + /// + /// Gets the center of the starting circle of the gradient. + /// + public Vector2 Center0 { get; init; } + + /// + /// Gets the radius of the starting circle of the gradient. + /// If is , + /// the radius is normalized to the bounds. + /// + public float Radius0 { get; init; } + + /// + /// Gets the center of the ending circle of the gradient. + /// + public Vector2 Center1 { get; init; } + + /// + /// Gets the radius of the ending circle of the gradient. + /// If is , + /// the radius is normalized to the bounds. + /// + public float Radius1 { get; init; } + + /// + /// Gets the spread method applied when sampling outside the [0, 1] range. + /// + public SpreadMethod Spread { get; init; } = SpreadMethod.Pad; + + /// + /// Gets the ordered gradient stops, ascending by . + /// + public GradientStop[] Stops { get; init; } = []; + } + + /// + /// Sweep (conic) gradient paint. Angles are expressed in degrees in the renderer's y-down space. + /// + public sealed class SweepGradientPaint : Paint + { + /// + /// Gets the coordinate system for . Sweep gradients are typically user-space. + /// + internal GradientUnits Units { get; init; } = GradientUnits.UserSpaceOnUse; + + /// + /// Gets the center of the sweep gradient. + /// + public Vector2 Center { get; init; } + + /// + /// Gets the start angle in degrees. + /// + public float StartAngle { get; init; } + + /// + /// Gets the end angle in degrees. + /// + public float EndAngle { get; init; } + + /// + /// Gets the spread method applied when sampling outside the [0, 1] range. + /// + public SpreadMethod Spread { get; init; } = SpreadMethod.Pad; + + /// + /// Gets the ordered gradient stops (ascending by ). + /// + public GradientStop[] Stops { get; init; } = []; + } +} diff --git a/SixLabors.Fonts/Rendering/PaintedCanvasMetadata.cs b/SixLabors.Fonts/Rendering/PaintedCanvasMetadata.cs new file mode 100644 index 0000000..e09bf32 --- /dev/null +++ b/SixLabors.Fonts/Rendering/PaintedCanvasMetadata.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts.Rendering { + /// + /// Canvas metadata describing the document-space coordinate system for a painted glyph. + /// + internal readonly struct PaintedCanvasMetadata + { + /// + /// Initializes a new instance of the struct. + /// + /// The viewBox rectangle (minX, minY, width, height). + /// True if the source coordinate system is y-down; false if y-up. + /// An optional root transform in document-space. + public PaintedCanvasMetadata(FontRectangle viewBox, bool isYDown, Matrix3x2 rootTransform) + { + this.HasViewBox = viewBox != FontRectangle.Empty; + this.ViewBox = viewBox; + this.IsYDown = isYDown; + this.RootTransform = rootTransform; + } + + /// + /// Gets a value indicating whether a root viewBox is present. + /// + public bool HasViewBox { get; } + + /// + /// Gets the viewBox. + /// + public FontRectangle ViewBox { get; } + + /// + /// Gets a value indicating whether the source coordinate system is y-down. + /// + public bool IsYDown { get; } + + /// + /// Gets the root transform in document-space. + /// + public Matrix3x2 RootTransform { get; } + } +} diff --git a/SixLabors.Fonts/Rendering/PaintedGlyph.cs b/SixLabors.Fonts/Rendering/PaintedGlyph.cs new file mode 100644 index 0000000..19a122c --- /dev/null +++ b/SixLabors.Fonts/Rendering/PaintedGlyph.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.Fonts.Rendering { + /// + /// A glyph fully decomposed into painted layers ready for rendering. + /// + internal readonly struct PaintedGlyph + { + /// + /// Initializes a new instance of the struct. + /// + /// The painted layers. + public PaintedGlyph(List layers) => this.Layers = layers; + + /// + /// Gets the layers for this glyph. + /// + public IReadOnlyList Layers { get; } + + /// + /// Gets a value indicating whether this glyph has no layers. + /// + public bool IsEmpty => this.Layers.Count == 0; + + /// + /// Gets an empty glyph instance. + /// + public static PaintedGlyph Empty => new([]); + } +} diff --git a/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs b/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs new file mode 100644 index 0000000..d0d4cfd --- /dev/null +++ b/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs @@ -0,0 +1,581 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Rendering { + /// + /// Provides painted (layered) glyph rendering for color formats such as COLR v1 and OT-SVG. + /// Geometry and paints are supplied in document-space by an interpreter; all layout transforms + /// (UPEM mapping, DPI/point-size scaling, rotation, final placement) are applied here. + /// + public sealed class PaintedGlyphMetrics : FontGlyphMetrics + { + private readonly IPaintedGlyphSource source; + + /// + /// Initializes a new instance of the class. + /// + /// The font metrics. + /// The glyph identifier. + /// The code point. + /// The painted glyph source. + /// The design-space bounds for the glyph. + /// The advance width. + /// The advance height. + /// The left side bearing. + /// The top side bearing. + /// Units per EM. + /// Text attributes. + /// Text decorations. + internal PaintedGlyphMetrics( + StreamFontMetrics font, + ushort glyphId, + CodePoint codePoint, + IPaintedGlyphSource source, + Bounds bounds, + ushort advanceWidth, + ushort advanceHeight, + short leftSideBearing, + short topSideBearing, + ushort unitsPerEM, + TextAttributes textAttributes, + TextDecorations textDecorations) + : base( + font, + glyphId, + codePoint, + bounds, + advanceWidth, + advanceHeight, + leftSideBearing, + topSideBearing, + unitsPerEM, + textAttributes, + textDecorations, + GlyphType.Painted) + => this.source = source; + + /// + /// Initializes a new instance of the class for rendering with overrides. + /// + internal PaintedGlyphMetrics( + StreamFontMetrics font, + ushort glyphId, + CodePoint codePoint, + IPaintedGlyphSource source, + Bounds bounds, + ushort advanceWidth, + ushort advanceHeight, + short leftSideBearing, + short topSideBearing, + ushort unitsPerEM, + Vector2 offset, + Vector2 scaleFactor, + TextRun textRun) + : base( + font, + glyphId, + codePoint, + bounds, + advanceWidth, + advanceHeight, + leftSideBearing, + topSideBearing, + unitsPerEM, + offset, + scaleFactor, + textRun, + GlyphType.Painted) + => this.source = source; + + /// + internal override FontGlyphMetrics CloneForRendering(TextRun textRun) + => new PaintedGlyphMetrics( + this.FontMetrics, + this.GlyphId, + this.CodePoint, + this.source, + this.Bounds, + this.AdvanceWidth, + this.AdvanceHeight, + this.LeftSideBearing, + this.TopSideBearing, + this.UnitsPerEm, + this.Offset, + this.ScaleFactor, + textRun); + + /// + internal override void RenderTo( + IGlyphRenderer renderer, + int graphemeIndex, + Vector2 glyphOrigin, + Vector2 decorationOrigin, + GlyphLayoutMode mode, + TextOptions options) + { + if (ShouldSkipGlyphRendering(this.CodePoint)) + { + return; + } + + float pointSize = this.TextRun.Font?.Size ?? options.Font.Size; + float dpi = options.Dpi; + + // Device-space placement. + glyphOrigin *= dpi; + decorationOrigin *= dpi; + + float scaledPpem = this.GetScaledSize(pointSize, dpi); + Vector2 scale = new Vector2(scaledPpem) / this.ScaleFactor; // uniform + + Matrix3x2 rotation = GetRotationMatrix(mode); + + // Layout similarity: uniform scale then rotation; translation added below. + Matrix3x2 layout = Matrix3x2.CreateScale(scale); + layout *= rotation; + layout.Translation = (this.Offset * scale) + glyphOrigin; + + // Bounds in device space for BeginGlyph. + FontRectangle box = this.GetBoundingBox(mode, glyphOrigin, scaledPpem); + GlyphRendererParameters parameters = new(this, this.TextRun, pointSize, dpi, mode, graphemeIndex); + + if (renderer.BeginGlyph(in box, in parameters)) + { + if (!UnicodeUtility.ShouldRenderWhiteSpaceOnly(this.CodePoint) + && this.source.TryGetPaintedGlyph(this.GlyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas)) + { + // Source-to-UPEM: viewBox mapping (uniform "meet"), optional y-flip, optional root transform. + Matrix3x2 s2u = ComputeSourceToUpem(canvas, this.UnitsPerEm); + + // Full transform from source doc-space to device space. + Matrix3x2 total = s2u * layout; + + // Stream layers and commands with correct transforms. + StreamPaintedGlyph(glyph, in box, renderer, total); + } + + renderer.EndGlyph(); + this.RenderDecorationsTo(renderer, decorationOrigin, mode, rotation, scaledPpem, options); + } + } + + /// + /// Computes the mapping from the interpreter's document-space to UPEM font space. + /// Enforces a uniform 'meet' scale from the root viewBox (if present) and flips Y + /// only if the source is y-up. + /// + private static Matrix3x2 ComputeSourceToUpem(in PaintedCanvasMetadata canvas, ushort upem) + { + Matrix3x2 m = Matrix3x2.Identity; + + // Root transform (doc-space). Apply first if provided. + if (!canvas.RootTransform.IsIdentity) + { + m *= canvas.RootTransform; + } + + // Translate viewBox min to origin, then uniform scale to UPEM using "meet". + if (canvas.HasViewBox) + { + Matrix3x2 t = Matrix3x2.CreateTranslation(-canvas.ViewBox.X, -canvas.ViewBox.Y); + + float sx = upem / Math.Max(canvas.ViewBox.Width, 1e-6f); + float sy = upem / Math.Max(canvas.ViewBox.Height, 1e-6f); + float s = MathF.Min(sx, sy); + + Matrix3x2 sUni = Matrix3x2.CreateScale(s); + + m = m * t * sUni; + } + + // Coordinate system orientation. + if (!canvas.IsYDown) + { + // Flip Y around the origin; placement happens in layout. + m *= Matrix3x2.CreateScale(1f, -1f); + } + + return m; + } + + /// + /// Streams the painted glyph to the renderer, transforming geometry and userSpaceOnUse paints. + /// + /// The painted glyph. + /// The device-space bounds of the glyph. + /// The glyph renderer. + /// The full device-space transform to apply. + private static void StreamPaintedGlyph( + in PaintedGlyph glyph, + in FontRectangle bounds, + IGlyphRenderer renderer, + Matrix3x2 xform) + { + IReadOnlyList layers = glyph.Layers; + for (int i = 0; i < layers.Count; i++) + { + PaintedLayer layer = layers[i]; + + // pre-applied transforms (element/group) + Matrix3x2 layerXform = layer.Transform * xform; + + // Clip bounds in device space (if any). + ClipQuad? clipBounds = layer.ClipBounds.HasValue + ? ClipQuad.FromBounds(layer.ClipBounds.Value, layerXform) + : null; + + // Similarity decomposition for arc radii/angle/sweep adjustment (from layer). + Similarity sim = Similarity.FromMatrix(layerXform); + + // Transform userSpaceOnUse paints into device space; keep ObjectBoundingBox normalized. + Paint? paint = TransformPaint(layer.Paint, in bounds, layerXform); + + renderer.BeginLayer(paint, layer.FillRule, clipBounds); + + bool open = false; + IReadOnlyList cmds = layer.Path; + + for (int j = 0; j < cmds.Count; j++) + { + PathCommand c = cmds[j]; + switch (c.Verb) + { + case PathVerb.MoveTo: + { + if (!open) + { + renderer.BeginFigure(); + open = true; + } + + renderer.MoveTo(Vector2.Transform(c.EndPoint, layerXform)); + break; + } + + case PathVerb.LineTo: + { + renderer.LineTo(Vector2.Transform(c.EndPoint, layerXform)); + break; + } + + case PathVerb.QuadraticTo: + { + renderer.QuadraticBezierTo( + Vector2.Transform(c.ControlPoint1, layerXform), + Vector2.Transform(c.EndPoint, layerXform)); + break; + } + + case PathVerb.CubicTo: + { + renderer.CubicBezierTo( + Vector2.Transform(c.ControlPoint1, layerXform), + Vector2.Transform(c.ControlPoint2, layerXform), + Vector2.Transform(c.EndPoint, layerXform)); + break; + } + + case PathVerb.ArcTo: + { + // Adjust radii by the scale component of the transform; + // angle/sweep by the similarity component; + // endpoint is fully transformed. + float rx = c.RadiusX * layerXform.M11; + float ry = c.RadiusY * layerXform.M12; + float ang = c.RotationDegrees + sim.RotationDegrees; + bool sweep = sim.Reflection ? !c.Sweep : c.Sweep; + + renderer.ArcTo(rx, ry, ang, c.LargeArc, sweep, Vector2.Transform(c.EndPoint, layerXform)); + break; + } + + case PathVerb.ClosePath: + { + if (open) + { + renderer.EndFigure(); + open = false; + } + + break; + } + } + } + + if (open) + { + renderer.EndFigure(); + } + + renderer.EndLayer(); + } + } + + /// + /// Converts a into device-space geometry for the target layer, + /// removing (baking in) any paint-local transforms. Geometry path commands have already + /// been transformed elsewhere; this method only resolves paint geometry (start/end points, + /// centers, radii, angles) into device space so the renderer can construct brushes directly. + /// + /// Rules: + /// + /// UserSpaceOnUse: Apply in user space, then apply + /// to obtain device-space positions. Emit device-space values. + /// ObjectBoundingBox: Apply in normalized [0..1] box space, + /// then denormalize to device space using . Emit device-space values. + /// Color stops (ratios) remain normalized in [0..1] and are passed through unchanged. + /// All returned paints have identity and are suitable for direct + /// consumption by Drawing brushes (e.g. LinearGradientBrush expects device-space points). + /// + /// + /// + /// The source paint, or . + /// The device-space axis-aligned bounding box of the current layer’s geometry. + /// + /// The full device-space transform applied to this layer’s geometry (e.g., layer * s2u * layout). + /// Used to push UserSpaceOnUse paints into device space. ObjectBoundingBox paints are denormalized + /// using instead. + /// + /// + /// A paint expressed in device-space with identity transform, or + /// if the input was . + /// + private static Paint? TransformPaint( + Paint? paint, + in FontRectangle layerBounds, + Matrix3x2 layerXform) + { + if (paint is null) + { + return null; + } + + switch (paint) + { + case SolidPaint s: + { + return s; + } + + case LinearGradientPaint lg: + { + Vector2 p0; + Vector2 p1; + Vector2? p2; + + if (lg.Units == GradientUnits.UserSpaceOnUse) + { + // USOU: transform directly to device space. + Matrix3x2 paintXForm = lg.Transform * layerXform; + p0 = Vector2.Transform(lg.P0, paintXForm); + p1 = Vector2.Transform(lg.P1, paintXForm); + p2 = lg.P2.HasValue ? Vector2.Transform(lg.P2.Value, paintXForm) : null; + } + else + { + // OBB: transform in normalized [0..1] space, then denormalize to device via layer bounds. + Vector2 n0 = Vector2.Transform(lg.P0, lg.Transform); + Vector2 n1 = Vector2.Transform(lg.P1, lg.Transform); + Vector2? n2 = lg.P2.HasValue ? Vector2.Transform(lg.P2.Value, lg.Transform) : null; + + p0 = Vector2.Transform(DenormalizePoint(n0, layerBounds), layerXform); + p1 = Vector2.Transform(DenormalizePoint(n1, layerBounds), layerXform); + p2 = n2.HasValue ? Vector2.Transform(DenormalizePoint(n2.Value, layerBounds), layerXform) : null; + } + + return new LinearGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + P0 = p0, + P1 = p1, + P2 = p2, + Spread = lg.Spread, + Stops = lg.Stops, + Opacity = lg.Opacity, + Transform = Matrix3x2.Identity + }; + } + + case RadialGradientPaint rg: + { + Vector2 c0; + Vector2 c1; + float r0; + float r1; + + if (rg.Units == GradientUnits.UserSpaceOnUse) + { + // USOU: transform directly to device space. + Matrix3x2 paintXForm = rg.Transform * layerXform; + + // Centers get full layer transform. + c0 = Vector2.Transform(rg.Center0, paintXForm); + c1 = Vector2.Transform(rg.Center1, paintXForm); + + // Radii scale by uniform similarity only. + Similarity compSim = Similarity.FromMatrix(paintXForm); + r0 = rg.Radius0 * compSim.Scale; + r1 = rg.Radius1 * compSim.Scale; + } + else + { + // OBB: transform in normalized [0..1] space, then denormalize to device via layer bounds. + Vector2 nc0 = Vector2.Transform(rg.Center0, rg.Transform); + Vector2 nc1 = Vector2.Transform(rg.Center1, rg.Transform); + + c0 = Vector2.Transform(DenormalizePoint(nc0, layerBounds), layerXform); + c1 = Vector2.Transform(DenormalizePoint(nc1, layerBounds), layerXform); + + // Radii scale by total similarity (paint * layer). + Matrix3x2 paintXForm = rg.Transform * layerXform; + Similarity compSim = Similarity.FromMatrix(paintXForm); + r0 = rg.Radius0 * compSim.Scale; + r1 = rg.Radius1 * compSim.Scale; + } + + return new RadialGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + Center0 = c0, + Radius0 = r0, + Center1 = c1, + Radius1 = r1, + Spread = rg.Spread, + Stops = rg.Stops, + Opacity = rg.Opacity, + Transform = Matrix3x2.Identity + }; + } + + case SweepGradientPaint sg: + { + Vector2 center; + float start = sg.StartAngle; + float end = sg.EndAngle; + + if (sg.Units == GradientUnits.UserSpaceOnUse) + { + // USOU: transform directly to device space. + Matrix3x2 paintXForm = sg.Transform * layerXform; + + // Center gets full layer transform. + center = Vector2.Transform(sg.Center, paintXForm); + + // Angles adjust by similarity rotation and reflection only. + Similarity compSim = Similarity.FromMatrix(paintXForm); + start += compSim.RotationDegrees; + end += compSim.RotationDegrees; + if (compSim.Reflection) + { + (start, end) = (end, start); + } + } + else + { + // OBB: transform in normalized [0..1] space, then denormalize to device via layer bounds. + Vector2 nc = Vector2.Transform(sg.Center, sg.Transform); + center = Vector2.Transform(DenormalizePoint(nc, layerBounds), layerXform); + + // Angles adjust by total similarity (paint * layer). + Matrix3x2 paintXForm = sg.Transform * layerXform; + Similarity compSim = Similarity.FromMatrix(paintXForm); + start += compSim.RotationDegrees; + end += compSim.RotationDegrees; + if (compSim.Reflection) + { + (start, end) = (end, start); + } + } + + return new SweepGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + Center = center, + StartAngle = start, + EndAngle = end, + Spread = sg.Spread, + Stops = sg.Stops, + Opacity = sg.Opacity, + Transform = Matrix3x2.Identity + }; + } + + default: + { + return paint; + } + } + + static Vector2 DenormalizePoint(Vector2 p, in FontRectangle bounds) + => new(bounds.X + (p.X * bounds.Width), bounds.Y + (p.Y * bounds.Height)); + } + + /// + /// Represents the similarity component of a 2D affine transformation. + /// + /// + /// A similarity transformation is an affine transform that preserves an object's shape and angles, + /// allowing only uniform scaling, rotation, and optional reflection. This structure isolates those + /// properties from a general so that dependent operations such as arc or + /// gradient adjustment can apply proportional transformations correctly. + /// + private readonly struct Similarity + { + private Similarity(float scale, float rotationDeg, bool reflection, bool isSimilarity) + { + this.Scale = scale; + this.RotationDegrees = rotationDeg; + this.Reflection = reflection; + this.IsSimilarity = isSimilarity; + } + + /// + /// Gets the length of the first column. + /// + public float Scale { get; } + + /// + /// Gets the rotation in degrees. + /// + public float RotationDegrees { get; } + + /// + /// Gets a value indicating whether this matrix includes a reflection. + public bool Reflection { get; } + + /// + /// Gets a value indicating whether this matrix is a similarity transform. + /// True if columns are orthogonal and equal length within tolerance. + /// + public bool IsSimilarity { get; } + + public static Similarity FromMatrix(in Matrix3x2 m) + { + float a = m.M11, b = m.M12, c = m.M21, d = m.M22; + + // scale = |X column| + float sx = MathF.Sqrt((a * a) + (b * b)); + + // rotation from X column + float rotDeg = MathF.Atan2(b, a) * (180f / MathF.PI); + + // reflection from determinant + bool refl = ((a * d) - (b * c)) < 0f; + + // similarity test: columns orthogonal and same length + float dot = (a * c) + (b * d); + float sy = MathF.Sqrt((c * c) + (d * d)); + const float eps = 1e-4f; + bool ortho = MathF.Abs(dot) <= eps; + bool equal = MathF.Abs(sx - sy) <= eps; + + return new Similarity(sx, rotDeg, refl, ortho && equal && sx > 0f); + } + } + } +} diff --git a/SixLabors.Fonts/Rendering/PaintedLayer.cs b/SixLabors.Fonts/Rendering/PaintedLayer.cs new file mode 100644 index 0000000..7acb97c --- /dev/null +++ b/SixLabors.Fonts/Rendering/PaintedLayer.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Numerics; + +namespace SixLabors.Fonts.Rendering { + /// + /// A single painted layer comprising a paint, a fill rule, and a path stream. + /// All coordinates must be pre-transformed in Fonts prior to construction. + /// + internal readonly struct PaintedLayer + { + /// + /// Initializes a new instance of the struct. + /// + /// The paint definition. + /// The fill rule. + /// The transform applied to all path coordinates. + /// An optional clip bounds to apply when rasterizing this layer. + /// The path command stream for this layer. + public PaintedLayer( + Paint? paint, + FillRule fillRule, + Matrix3x2 transform, + Bounds? clipBounds, + IReadOnlyList path) + { + this.Paint = paint; + this.FillRule = fillRule; + this.Transform = transform; + this.ClipBounds = clipBounds; + this.Path = path; + } + + /// + /// Gets the paint definition for this layer. + /// + public Paint? Paint { get; } + + /// + /// Gets the fill rule for rasterization. + /// + public FillRule FillRule { get; } + + /// + /// Gets the transform applied to all path coordinates. + /// + public Matrix3x2 Transform { get; } + + public Bounds? ClipBounds { get; } + + /// + /// Gets the path stream for this layer. + /// + public IReadOnlyList Path { get; } + } +} diff --git a/SixLabors.Fonts/Rendering/PathCommand.cs b/SixLabors.Fonts/Rendering/PathCommand.cs new file mode 100644 index 0000000..ac48c0a --- /dev/null +++ b/SixLabors.Fonts/Rendering/PathCommand.cs @@ -0,0 +1,151 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts.Rendering { + /// + /// A single path command with all coordinates already transformed into final render space (y-down). + /// For arc commands, radii and flags must be pre-adjusted by the interpreter. + /// + internal readonly struct PathCommand + { + /// + /// Initializes a new instance of the struct. + /// + /// The command verb. + /// The end point for the command. + /// The first control point, if used by the verb. + /// The second control point, if used by the verb. + /// The x-radius for . + /// The y-radius for . + /// The x-axis rotation in degrees for . + /// The large-arc flag for . + /// The sweep flag for . + public PathCommand( + PathVerb verb, + Vector2 endPoint, + Vector2 controlPoint1, + Vector2 controlPoint2, + float radiusX, + float radiusY, + float rotationDegrees, + bool largeArc, + bool sweep) + { + this.Verb = verb; + this.EndPoint = endPoint; + this.ControlPoint1 = controlPoint1; + this.ControlPoint2 = controlPoint2; + this.RadiusX = radiusX; + this.RadiusY = radiusY; + this.RotationDegrees = rotationDegrees; + this.LargeArc = largeArc; + this.Sweep = sweep; + } + + /// + /// Gets the command verb. + /// + public PathVerb Verb { get; } + + /// + /// Gets the end point for the command. + /// For and this is the target point. + /// For curves and arcs it is the end point of the segment. + /// + public Vector2 EndPoint { get; } + + /// + /// Gets the first control point (quadratic control or cubic control 1). + /// Not used for , , or . + /// + public Vector2 ControlPoint1 { get; } + + /// + /// Gets the second control point (cubic control 2). + /// Only used for . + /// + public Vector2 ControlPoint2 { get; } + + /// + /// Gets the x-radius for . + /// + public float RadiusX { get; } + + /// + /// Gets the y-radius for . + /// + public float RadiusY { get; } + + /// + /// Gets the rotation of the arc's x-axis in degrees for . + /// + public float RotationDegrees { get; } + + /// + /// Gets a value indicating whether the large-arc flag is set for . + /// + public bool LargeArc { get; } + + /// + /// Gets a value indicating whether the sweep flag is set for . + /// + public bool Sweep { get; } + + /// + /// Creates a command. + /// + /// The destination point. + /// The command. + public static PathCommand MoveTo(Vector2 point) + => new(PathVerb.MoveTo, point, Vector2.Zero, Vector2.Zero, 0f, 0f, 0f, false, false); + + /// + /// Creates a command. + /// + /// The destination point. + /// The command. + public static PathCommand LineTo(Vector2 point) + => new(PathVerb.LineTo, point, Vector2.Zero, Vector2.Zero, 0f, 0f, 0f, false, false); + + /// + /// Creates a command. + /// + /// The control point. + /// The end point. + /// The command. + public static PathCommand QuadraticTo(Vector2 control, Vector2 end) + => new(PathVerb.QuadraticTo, end, control, Vector2.Zero, 0f, 0f, 0f, false, false); + + /// + /// Creates a command. + /// + /// The first control point. + /// The second control point. + /// The end point. + /// The command. + public static PathCommand CubicTo(Vector2 control1, Vector2 control2, Vector2 end) + => new(PathVerb.CubicTo, end, control1, control2, 0f, 0f, 0f, false, false); + + /// + /// Creates an command. + /// + /// The x-radius of the ellipse. + /// The y-radius of the ellipse. + /// The rotation of the ellipse's x-axis in degrees. + /// The large-arc flag. + /// The sweep flag. + /// The end point. + /// The command. + public static PathCommand ArcTo(float radiusX, float radiusY, float rotationDegrees, bool largeArc, bool sweep, Vector2 end) + => new(PathVerb.ArcTo, end, Vector2.Zero, Vector2.Zero, radiusX, radiusY, rotationDegrees, largeArc, sweep); + + /// + /// Creates a command. + /// + /// The command. + public static PathCommand Close() + => new(PathVerb.ClosePath, Vector2.Zero, Vector2.Zero, Vector2.Zero, 0f, 0f, 0f, false, false); + } +} diff --git a/SixLabors.Fonts/Rendering/PathVerb.cs b/SixLabors.Fonts/Rendering/PathVerb.cs new file mode 100644 index 0000000..e016841 --- /dev/null +++ b/SixLabors.Fonts/Rendering/PathVerb.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Rendering { + /// + /// Path verb identifying the command type. + /// + internal enum PathVerb : byte + { + /// + /// Moves the current point without drawing. + /// + MoveTo = 0, + + /// + /// Draws a straight line from the current point to the end point. + /// + LineTo = 1, + + /// + /// Draws a quadratic Bézier from the current point to the end point using a single control point. + /// + QuadraticTo = 2, + + /// + /// Draws a cubic Bézier from the current point to the end point using two control points. + /// + CubicTo = 3, + + /// + /// Draws an elliptical arc from the current point to the end point. + /// + ArcTo = 4, + + /// + /// Closes the current subpath. + /// + ClosePath = 5, + } +} diff --git a/SixLabors.Fonts/Rendering/SpreadMethod.cs b/SixLabors.Fonts/Rendering/SpreadMethod.cs new file mode 100644 index 0000000..442b904 --- /dev/null +++ b/SixLabors.Fonts/Rendering/SpreadMethod.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Rendering { + /// + /// Specifies how a gradient should extend beyond the [0, 1] range. + /// + public enum SpreadMethod + { + /// + /// Clamp to the end colors (pad). + /// + Pad = 0, + + /// + /// Mirror the gradient (reflect). + /// + Reflect = 1, + + /// + /// Repeat the gradient (tile). + /// + Repeat = 2, + } +} diff --git a/SixLabors.Fonts/Rendering/TextRenderer.cs b/SixLabors.Fonts/Rendering/TextRenderer.cs new file mode 100644 index 0000000..ec1c541 --- /dev/null +++ b/SixLabors.Fonts/Rendering/TextRenderer.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Rendering { + /// + /// Encapsulates logic for laying out and then rendering text to a surface. + /// + public class TextRenderer + { + private readonly IGlyphRenderer renderer; + + /// + /// Initializes a new instance of the class. + /// + /// The renderer. + public TextRenderer(IGlyphRenderer renderer) => this.renderer = renderer; + + /// + /// Renders the text to the . + /// + /// The target renderer. + /// The text to render. + /// The text options. controls wrapping; use -1 to disable wrapping. + public static void RenderTextTo(IGlyphRenderer renderer, ReadOnlySpan text, TextOptions options) + => new TextRenderer(renderer).RenderText(text, options); + + /// + /// Renders the text to the . + /// + /// The target renderer. + /// The text to render. + /// The text options. controls wrapping; use -1 to disable wrapping. + public static void RenderTextTo(IGlyphRenderer renderer, string text, TextOptions options) + => new TextRenderer(renderer).RenderText(text, options); + + /// + /// Renders the text to the configured renderer. + /// + /// The text to render. + /// The text options. controls wrapping; use -1 to disable wrapping. + public void RenderText(string text, TextOptions options) + => this.RenderText(text.AsSpan(), options); + + /// + /// Renders the text to the configured renderer. + /// + /// The text to render. + /// The text options. controls wrapping; use -1 to disable wrapping. + public void RenderText(ReadOnlySpan text, TextOptions options) + { + TextBlock block = new(text, options); + block.RenderTo(this.renderer, options.WrappingLength); + } + } +} diff --git a/SixLabors.Fonts/ShapedText.cs b/SixLabors.Fonts/ShapedText.cs new file mode 100644 index 0000000..37d1cf0 --- /dev/null +++ b/SixLabors.Fonts/ShapedText.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using System.Collections.Generic; + +namespace SixLabors.Fonts { + /// + /// Contains the width-independent result of shaping text before logical line composition. + /// + internal readonly struct ShapedText + { + /// + /// Initializes a new instance of the struct. + /// + /// The positioned glyph shaping collection. + /// The resolved bidi runs covering the shaped text. + /// The code point to bidi-run mapping built during shaping. + /// The layout mode used while shaping. + public ShapedText( + GlyphPositioningCollection positionings, + BidiRun[] bidiRuns, + Dictionary bidiMap, + LayoutMode layoutMode) + { + this.Positionings = positionings; + this.BidiRuns = bidiRuns; + this.BidiMap = bidiMap; + this.LayoutMode = layoutMode; + } + + /// + /// Gets the positioned glyph shaping collection. + /// + public GlyphPositioningCollection Positionings { get; } + + /// + /// Gets the resolved bidi runs covering the shaped text. + /// + public BidiRun[] BidiRuns { get; } + + /// + /// Gets the code point to bidi-run mapping built during shaping. + /// + public Dictionary BidiMap { get; } + + /// + /// Gets the layout mode used while shaping. + /// + public LayoutMode LayoutMode { get; } + } +} diff --git a/SixLabors.Fonts/SixLabors.Fonts.csproj b/SixLabors.Fonts/SixLabors.Fonts.csproj new file mode 100644 index 0000000..987a535 --- /dev/null +++ b/SixLabors.Fonts/SixLabors.Fonts.csproj @@ -0,0 +1,48 @@ + + + +net10.0 + SixLabors.Fonts + SixLabors.Fonts + SixLabors.Fonts + SixLabors.Fonts + sixlabors.fonts.128.png + LICENSE + https://github.com/SixLabors/Fonts/ + $(RepositoryUrl) + font;truetype;opentype;woff;woff2 + A cross-platform library for loading and laying out fonts for processing and measuring; written in C# + + 3.0.0.0 + + + + + enable + Nullable + + + $(NoWarn);IL2050; + + + + + 3.0 + true + + + + + + + + + + + + + diff --git a/SixLabors.Fonts/StreamFontMetrics.Cff.cs b/SixLabors.Fonts/StreamFontMetrics.Cff.cs new file mode 100644 index 0000000..0da8aa8 --- /dev/null +++ b/SixLabors.Fonts/StreamFontMetrics.Cff.cs @@ -0,0 +1,193 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; +using SixLabors.Fonts.Tables.Cff; +using SixLabors.Fonts.Tables.General; +using SixLabors.Fonts.Tables.General.Colr; +using SixLabors.Fonts.Tables.General.Kern; +using SixLabors.Fonts.Tables.General.Name; +using SixLabors.Fonts.Tables.General.Post; +using SixLabors.Fonts.Tables.General.Svg; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// Contains CFF specific methods. + /// + internal partial class StreamFontMetrics + { + private static StreamFontMetrics LoadCompactFont(FontReader reader) + { + // Load using recommended order for best performance. + // https://learn.microsoft.com/en-gb/typography/opentype/spec/recom#optimized-table-ordering + // 'head', 'hhea', 'maxp', OS/2, 'name', 'cmap', 'post', 'CFF ' / 'CFF2' + HeadTable head = reader.GetTable(); + HorizontalHeadTable hhea = reader.GetTable(); + MaximumProfileTable maxp = reader.GetTable(); + OS2Table os2 = reader.GetTable(); + NameTable name = reader.GetTable(); + CMapTable cmap = reader.GetTable(); + PostTable post = reader.GetTable(); + ICffTable? cff = + (reader.TryGetTable() ?? (ICffTable?)reader.TryGetTable()) + ?? throw new InvalidFontFileException("Missing required CFF table."); + + // TODO: VORG + HorizontalMetricsTable htmx = reader.GetTable(); + VerticalHeadTable? vhea = reader.TryGetTable(); + VerticalMetricsTable? vmtx = null; + if (vhea is not null) + { + vmtx = reader.TryGetTable(); + } + + KerningTable? kern = reader.TryGetTable(); + + GlyphDefinitionTable? gdef = reader.TryGetTable(); + GSubTable? gSub = reader.TryGetTable(); + GPosTable? gPos = reader.TryGetTable(); + + ColrTable? colr = reader.TryGetTable(); + CpalTable? cpal = reader.TryGetTable(); + SvgTable? svg = reader.TryGetTable(); + + // Variations related tables. + FVarTable? fVar = reader.TryGetTable(); + AVarTable? aVar = reader.TryGetTable(); + GVarTable? gVar = reader.TryGetTable(); + HVarTable? hVar = reader.TryGetTable(); + VVarTable? vVar = reader.TryGetTable(); + MVarTable? mVar = reader.TryGetTable(); + + GlyphVariationProcessor? glyphVariationProcessor = null; + if (cff.ItemVariationStore != null) + { + if (fVar is null) + { + throw new InvalidFontFileException("missing fvar table required for glyph variations processing"); + } + + glyphVariationProcessor = new GlyphVariationProcessor(cff.ItemVariationStore, fVar, aVar, gVar, hVar, vVar, mVar); + } + + CompactFontTables tables = new(cmap, head, hhea, htmx, maxp, name, os2, post, cff) + { + Kern = kern, + Vhea = vhea, + Vmtx = vmtx, + Gdef = gdef, + GSub = gSub, + GPos = gPos, + Colr = colr, + Cpal = cpal, + FVar = fVar, + AVar = aVar, + GVar = gVar, + HVar = hVar, + VVar = vVar, + MVar = mVar, + Svg = svg + }; + + return new StreamFontMetrics(tables, glyphVariationProcessor); + } + + private FontGlyphMetrics CreateCffGlyphMetrics( + in CodePoint codePoint, + ushort glyphId, + GlyphType glyphType, + TextAttributes textAttributes, + TextDecorations textDecorations, + ColorFontSupport colorSupport, + bool isVerticalLayout, + ushort paletteIndex = 0) + { + // TODO: When do we require and how do we use the palette index? + CompactFontTables tables = this.compactFontTables!; + ICffTable cff = tables.Cff; + HorizontalMetricsTable htmx = tables.Htmx; + VerticalMetricsTable? vtmx = tables.Vmtx; + FVarTable? fVar = tables.FVar; + AVarTable? aVar = tables.AVar; + GVarTable? gVar = tables.GVar; + + CffGlyphData vector = cff.GetGlyph(glyphId); + vector.FVar = fVar; + vector.AVar = aVar; + vector.GVar = gVar; + Bounds bounds = vector.GetBounds(); + + // Apply the CFF FontMatrix to transform bounds from charstring space to design units. + if (vector.FontMatrix is double[] fm) + { + float upm = this.UnitsPerEm; + Vector2 fmScale = new((float)(fm[0] * upm), (float)(fm[3] * upm)); + bounds = new Bounds(bounds.Min * fmScale, bounds.Max * fmScale); + } + + ushort advanceWidth = htmx.GetAdvancedWidth(glyphId); + short lsb = htmx.GetLeftSideBearing(glyphId); + + // Apply HVAR advance width adjustment if available. + if (this.GlyphVariationProcessor is not null) + { + advanceWidth = (ushort)(advanceWidth + MathF.Round(this.GlyphVariationProcessor.AdvanceAdjustment(glyphId))); + } + + IMetricsHeader metrics = isVerticalLayout ? this.VerticalMetrics : this.HorizontalMetrics; + ushort advancedHeight = (ushort)(metrics.Ascender - metrics.Descender); + short tsb = (short)(metrics.Ascender - bounds.Max.Y); + if (vtmx != null) + { + advancedHeight = vtmx.GetAdvancedHeight(glyphId); + tsb = vtmx.GetTopSideBearing(glyphId); + } + + // Apply VVAR advance height adjustment if available. + if (this.GlyphVariationProcessor is not null) + { + advancedHeight = (ushort)(advancedHeight + MathF.Round(this.GlyphVariationProcessor.VerticalAdvanceAdjustment(glyphId))); + } + + // TODO: Support CFF based COLR glyphs. + // This requires parsing the CFF charstrings to extract the glyph vectors. + SvgTable? svg = tables.Svg; + if ((colorSupport & ColorFontSupport.Svg) == ColorFontSupport.Svg && svg?.ContainsGlyph(glyphId) == true) + { + return new PaintedGlyphMetrics( + this, + glyphId, + codePoint, + this.GetOrCreateSvgGlyphSource(svg), + bounds, + advanceWidth, + advancedHeight, + lsb, + tsb, + this.UnitsPerEm, + textAttributes, + textDecorations); + } + + return new CffGlyphMetrics( + this, + glyphId, + codePoint, + vector, + bounds, + advanceWidth, + advancedHeight, + lsb, + tsb, + this.UnitsPerEm, + textAttributes, + textDecorations, + glyphType); + } + } +} diff --git a/SixLabors.Fonts/StreamFontMetrics.TrueType.cs b/SixLabors.Fonts/StreamFontMetrics.TrueType.cs new file mode 100644 index 0000000..c0932df --- /dev/null +++ b/SixLabors.Fonts/StreamFontMetrics.TrueType.cs @@ -0,0 +1,325 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; +using SixLabors.Fonts.Tables.General; +using SixLabors.Fonts.Tables.General.Colr; +using SixLabors.Fonts.Tables.General.Kern; +using SixLabors.Fonts.Tables.General.Name; +using SixLabors.Fonts.Tables.General.Post; +using SixLabors.Fonts.Tables.General.Svg; +using SixLabors.Fonts.Tables.TrueType; +using SixLabors.Fonts.Tables.TrueType.Glyphs; +using SixLabors.Fonts.Tables.TrueType.Hinting; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// Contains TrueType specific methods. + /// + internal partial class StreamFontMetrics + { + // Bounded pool of interpreters shared across threads. + // Size tied to logical CPU count. + private readonly ObjectPool? interpreterPool; + + private TrueTypeInterpreter CreateInterpreter() + { + TrueTypeFontTables tables = this.trueTypeFontTables!; + MaximumProfileTable maxp = tables.Maxp; + + TrueTypeInterpreter interpreter = new( + maxp.MaxStackElements, + maxp.MaxStorage, + maxp.MaxFunctionDefs, + maxp.MaxInstructionDefs, + maxp.MaxTwilightPoints); + + FpgmTable? fpgm = tables.Fpgm; + if (fpgm is not null) + { + interpreter.InitializeFunctionDefs(fpgm.Instructions); + } + + return interpreter; + } + + internal void ApplyTrueTypeHinting(HintingMode hintingMode, FontGlyphMetrics metrics, ref GlyphVector glyphVector, Vector2 scaleXY, float pixelSize) + { + if (hintingMode == HintingMode.None || this.outlineType != OutlineType.TrueType) + { + return; + } + + if (this.trueTypeFontTables is null || this.interpreterPool is null) + { + return; + } + + TrueTypeFontTables tables = this.trueTypeFontTables; + TrueTypeInterpreter interpreter = this.interpreterPool.Get(); + + try + { + CvtTable? cvt = tables.Cvt; + PrepTable? prep = tables.Prep; + float hintingScaleFactor = pixelSize / this.UnitsPerEm; + + // Apply cvar deltas to CVT values for variable fonts before hinting. + short[]? cvtValues = cvt?.ControlValues; + if (cvtValues is not null && this.GlyphVariationProcessor is not null) + { + cvtValues = this.GlyphVariationProcessor.ApplyCvtDeltas(cvtValues) ?? cvtValues; + } + + // Provide normalized axis coordinates for the GETVARIATION opcode. + interpreter.SetNormalizedAxisCoordinates(this.GlyphVariationProcessor?.NormalizedCoordinates); + + interpreter.SetControlValueTable(cvtValues, hintingScaleFactor, pixelSize, prep?.Instructions); + + Bounds bounds = glyphVector.Bounds; + + Vector2 pp1 = new(MathF.Round(bounds.Min.X - (metrics.LeftSideBearing * scaleXY.X)), 0); + Vector2 pp2 = new(MathF.Round(pp1.X + (metrics.AdvanceWidth * scaleXY.X)), 0); + Vector2 pp3 = new(0, MathF.Round(bounds.Max.Y + (metrics.TopSideBearing * scaleXY.Y))); + Vector2 pp4 = new(0, MathF.Round(pp3.Y - (metrics.AdvanceHeight * scaleXY.Y))); + + GlyphVector.Hint(hintingMode, ref glyphVector, interpreter, pp1, pp2, pp3, pp4); + } + finally + { + this.interpreterPool.Return(interpreter); + } + } + + private static StreamFontMetrics LoadTrueTypeFont(FontReader reader) + { + // Load using recommended order for best performance. + // https://learn.microsoft.com/en-gb/typography/opentype/spec/recom#optimized-table-ordering + // 'head', 'hhea', 'maxp', OS/2, 'hmtx', LTSH, VDMX, 'hdmx', 'cmap', 'fpgm', 'prep', 'cvt ', 'loca', 'glyf', 'kern', 'name', 'post', 'gasp', PCLT, DSIG + HeadTable head = reader.GetTable(); + HorizontalHeadTable hhea = reader.GetTable(); + MaximumProfileTable maxp = reader.GetTable(); + OS2Table os2 = reader.GetTable(); + HorizontalMetricsTable htmx = reader.GetTable(); + CMapTable cmap = reader.GetTable(); + FpgmTable? fpgm = reader.TryGetTable(); + PrepTable? prep = reader.TryGetTable(); + CvtTable? cvt = reader.TryGetTable(); + IndexLocationTable loca = reader.GetTable(); + GlyphTable glyf = reader.GetTable(); + KerningTable? kern = reader.TryGetTable(); + NameTable name = reader.GetTable(); + PostTable post = reader.GetTable(); + + VerticalHeadTable? vhea = reader.TryGetTable(); + VerticalMetricsTable? vmtx = null; + if (vhea is not null) + { + vmtx = reader.TryGetTable(); + } + + GlyphDefinitionTable? gdef = reader.TryGetTable(); + GSubTable? gSub = reader.TryGetTable(); + GPosTable? gPos = reader.TryGetTable(); + + FVarTable? fvar = reader.TryGetTable(); + AVarTable? avar = reader.TryGetTable(); + GVarTable? gvar = reader.TryGetTable(); + HVarTable? hvar = reader.TryGetTable(); + VVarTable? vvar = reader.TryGetTable(); + MVarTable? mvar = reader.TryGetTable(); + + // cvar depends on axisCount from fvar, so it cannot be auto-loaded via TryGetTable. + CVarTable? cvar = null; + if (fvar is not null) + { + cvar = CVarTable.Load(reader, fvar.AxisCount); + } + + ColrTable? colr = reader.TryGetTable(); + CpalTable? cpal = reader.TryGetTable(); + + SvgTable? svg = reader.TryGetTable(); + + TrueTypeFontTables tables = new(cmap, head, hhea, htmx, maxp, name, os2, post, glyf, loca) + { + Fpgm = fpgm, + Prep = prep, + Cvt = cvt, + Kern = kern, + Vhea = vhea, + Vmtx = vmtx, + Gdef = gdef, + GSub = gSub, + GPos = gPos, + Colr = colr, + Cpal = cpal, + Fvar = fvar, + Gvar = gvar, + Hvar = hvar, + Vvar = vvar, + Mvar = mvar, + Avar = avar, + Svg = svg, + Cvar = cvar + }; + + GlyphVariationProcessor? glyphVariationProcessor = null; + if (fvar != null) + { + // Use the item variation store from HVAR or VVAR if available (for metrics variations). + // A variable font may have gvar without HVAR/VVAR (using phantom points for metrics instead). + ItemVariationStore? itemVariationStore = hvar?.ItemVariationStore ?? vvar?.ItemVariationStore; + glyphVariationProcessor = new GlyphVariationProcessor(itemVariationStore, fvar, avar, gvar, hvar, vvar, mvar, cvar); + } + + return new StreamFontMetrics(tables, glyphVariationProcessor); + } + + private FontGlyphMetrics CreateTrueTypeGlyphMetrics( + in CodePoint codePoint, + ushort glyphId, + GlyphType glyphType, + TextAttributes textAttributes, + TextDecorations textDecorations, + ColorFontSupport colorSupport, + bool isVerticalLayout, + ushort paletteIndex = 0) + { + // TODO: When do we require and how do we use the palette index? + TrueTypeFontTables tables = this.trueTypeFontTables!; + GlyphTable glyf = tables.Glyf; + HorizontalMetricsTable htmx = tables.Htmx; + VerticalMetricsTable? vtmx = tables.Vmtx; + + GlyphVector vector = glyf.GetGlyph(glyphId); + + // Apply gvar deltas to the glyph outline if a variation processor is present. + // Clone first so we don't mutate the shared glyph cache. + if (this.GlyphVariationProcessor is not null) + { + vector = GlyphVector.DeepClone(vector); + this.GlyphVariationProcessor.TransformPoints(glyphId, ref vector); + } + + Bounds bounds = vector.Bounds; + + ushort advanceWidth = htmx.GetAdvancedWidth(glyphId); + short lsb = htmx.GetLeftSideBearing(glyphId); + + // Apply HVAR advance width adjustment if available. + if (this.GlyphVariationProcessor is not null) + { + advanceWidth = (ushort)(advanceWidth + MathF.Round(this.GlyphVariationProcessor.AdvanceAdjustment(glyphId))); + } + + IMetricsHeader metrics = isVerticalLayout ? this.VerticalMetrics : this.HorizontalMetrics; + ushort advancedHeight = (ushort)(metrics.Ascender - metrics.Descender); + short tsb = (short)(metrics.Ascender - bounds.Max.Y); + if (vtmx != null) + { + advancedHeight = vtmx.GetAdvancedHeight(glyphId); + tsb = vtmx.GetTopSideBearing(glyphId); + } + + // Apply VVAR advance height adjustment if available. + if (this.GlyphVariationProcessor is not null) + { + advancedHeight = (ushort)(advancedHeight + MathF.Round(this.GlyphVariationProcessor.VerticalAdvanceAdjustment(glyphId))); + } + + ColrTable? colr = tables.Colr; + if ((colorSupport & ColorFontSupport.ColrV1) == ColorFontSupport.ColrV1 && colr?.ContainsColorV1Glyph(glyphId) == true) + { + CpalTable? cpal = tables.Cpal; + ColrV1GlyphSource glyphSource = new(colr, cpal, i => glyf.GetGlyph(i), this.GlyphVariationProcessor); + + return new PaintedGlyphMetrics( + this, + glyphId, + codePoint, + glyphSource, + bounds, + advanceWidth, + advancedHeight, + lsb, + tsb, + this.UnitsPerEm, + textAttributes, + textDecorations); + } + + if ((colorSupport & ColorFontSupport.ColrV0) == ColorFontSupport.ColrV0 && colr?.ContainsColorV0Glyph(glyphId) == true) + { + CpalTable? cpal = tables.Cpal; + ColrV0GlyphSource glyphSource = new(colr, cpal, i => glyf.GetGlyph(i)); + + return new PaintedGlyphMetrics( + this, + glyphId, + codePoint, + glyphSource, + bounds, + advanceWidth, + advancedHeight, + lsb, + tsb, + this.UnitsPerEm, + textAttributes, + textDecorations); + } + + SvgTable? svg = tables.Svg; + if ((colorSupport & ColorFontSupport.Svg) == ColorFontSupport.Svg && svg?.ContainsGlyph(glyphId) == true) + { + return new PaintedGlyphMetrics( + this, + glyphId, + codePoint, + this.GetOrCreateSvgGlyphSource(svg), + bounds, + advanceWidth, + advancedHeight, + lsb, + tsb, + this.UnitsPerEm, + textAttributes, + textDecorations); + } + + return new TrueTypeGlyphMetrics( + this, + glyphId, + codePoint, + vector, + advanceWidth, + advancedHeight, + lsb, + tsb, + this.UnitsPerEm, + textAttributes, + textDecorations, + glyphType); + } + + private sealed class TrueTypeInterpreterPooledObjectPolicy + : IPooledObjectPolicy + { + private readonly StreamFontMetrics owner; + + public TrueTypeInterpreterPooledObjectPolicy(StreamFontMetrics owner) + => this.owner = owner; + + public TrueTypeInterpreter Create() + => this.owner.CreateInterpreter(); + + public bool Return(TrueTypeInterpreter interpreter) + => true; // Always accept returned instances. + } + } +} diff --git a/SixLabors.Fonts/StreamFontMetrics.cs b/SixLabors.Fonts/StreamFontMetrics.cs new file mode 100644 index 0000000..cb2be94 --- /dev/null +++ b/SixLabors.Fonts/StreamFontMetrics.cs @@ -0,0 +1,840 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Numerics; +using SixLabors.Fonts.Tables; +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; +using SixLabors.Fonts.Tables.Cff; +using SixLabors.Fonts.Tables.General; +using SixLabors.Fonts.Tables.General.Kern; +using SixLabors.Fonts.Tables.General.Post; +using SixLabors.Fonts.Tables.General.Svg; +using SixLabors.Fonts.Tables.TrueType; +using SixLabors.Fonts.Tables.TrueType.Hinting; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// + /// Represents a font face with metrics, which is a set of glyphs with a specific style (regular, italic, bold etc). + /// + /// The font source is a stream. + /// + internal partial class StreamFontMetrics : FontMetrics + { + private readonly TrueTypeFontTables? trueTypeFontTables; + private readonly CompactFontTables? compactFontTables; + private readonly OutlineType outlineType; + + // https://docs.microsoft.com/en-us/typography/opentype/spec/otff#font-tables + private readonly ConcurrentDictionary<(int CodePoint, ushort Id, TextAttributes Attributes, ColorFontSupport ColorSupport, bool IsVerticalLayout), FontGlyphMetrics> glyphCache; + private readonly ConcurrentDictionary<(int CodePoint, int NextCodePoint), (bool Success, ushort GlyphId, bool SkipNextCodePoint)> glyphIdCache; + private readonly ConcurrentDictionary codePointCache; + private SvgGlyphSource? svgGlyphSource; + private readonly FontDescription description; + private readonly HorizontalMetrics horizontalMetrics; + private readonly VerticalMetrics verticalMetrics; + private ushort unitsPerEm; + private float scaleFactor; + private short subscriptXSize; + private short subscriptYSize; + private short subscriptXOffset; + private short subscriptYOffset; + private short superscriptXSize; + private short superscriptYSize; + private short superscriptXOffset; + private short superscriptYOffset; + private short strikeoutSize; + private short strikeoutPosition; + private short underlinePosition; + private short underlineThickness; + private float italicAngle; + + /// + /// Initializes a new instance of the class. + /// + /// The True Type font tables. + /// An optional glyph variation processor for handling variable fonts. + internal StreamFontMetrics(TrueTypeFontTables tables, GlyphVariationProcessor? glyphVariationProcessor = null) + { + this.trueTypeFontTables = tables; + this.outlineType = OutlineType.TrueType; + this.description = new FontDescription(tables.Name, tables.Os2, tables.Head); + this.GlyphVariationProcessor = glyphVariationProcessor; + this.glyphIdCache = new(); + this.codePointCache = new(); + this.glyphCache = new(); + + (HorizontalMetrics HorizontalMetrics, VerticalMetrics VerticalMetrics) metrics = this.Initialize(tables); + this.horizontalMetrics = metrics.HorizontalMetrics; + this.verticalMetrics = metrics.VerticalMetrics; + + this.interpreterPool = new ObjectPool(new TrueTypeInterpreterPooledObjectPolicy(this)); + } + + /// + /// Initializes a new instance of the class. + /// + /// The Compact Font tables. + /// An optional glyph variation processor for handling variable fonts. + internal StreamFontMetrics(CompactFontTables tables, GlyphVariationProcessor? glyphVariationProcessor = null) + { + this.compactFontTables = tables; + this.outlineType = OutlineType.CFF; + this.description = new FontDescription(tables.Name, tables.Os2, tables.Head); + this.GlyphVariationProcessor = glyphVariationProcessor; + this.glyphIdCache = new(); + this.codePointCache = new(); + this.glyphCache = new(); + + (HorizontalMetrics HorizontalMetrics, VerticalMetrics VerticalMetrics) metrics = this.Initialize(tables); + this.horizontalMetrics = metrics.HorizontalMetrics; + this.verticalMetrics = metrics.VerticalMetrics; + } + + /// + /// Initializes a new instance of the class as a variation instance, + /// sharing variation-independent caches from the base instance. + /// Only the glyph cache (which depends on variation coordinates) is fresh. + /// + private StreamFontMetrics( + TrueTypeFontTables tables, + GlyphVariationProcessor processor, + ConcurrentDictionary<(int CodePoint, int NextCodePoint), (bool Success, ushort GlyphId, bool SkipNextCodePoint)> sharedGlyphIdCache, + ConcurrentDictionary sharedCodePointCache, + SvgGlyphSource? svgGlyphSource) + { + this.trueTypeFontTables = tables; + this.outlineType = OutlineType.TrueType; + this.description = new FontDescription(tables.Name, tables.Os2, tables.Head); + this.GlyphVariationProcessor = processor; + this.glyphIdCache = sharedGlyphIdCache; + this.codePointCache = sharedCodePointCache; + this.glyphCache = new(); + this.svgGlyphSource = svgGlyphSource; + + (HorizontalMetrics HorizontalMetrics, VerticalMetrics VerticalMetrics) metrics = this.Initialize(tables); + this.horizontalMetrics = metrics.HorizontalMetrics; + this.verticalMetrics = metrics.VerticalMetrics; + + this.interpreterPool = new ObjectPool(new TrueTypeInterpreterPooledObjectPolicy(this)); + } + + /// + /// Initializes a new instance of the class as a variation instance, + /// sharing variation-independent caches from the base instance. + /// Only the glyph cache (which depends on variation coordinates) is fresh. + /// + private StreamFontMetrics( + CompactFontTables tables, + GlyphVariationProcessor processor, + ConcurrentDictionary<(int CodePoint, int NextCodePoint), (bool Success, ushort GlyphId, bool SkipNextCodePoint)> sharedGlyphIdCache, + ConcurrentDictionary sharedCodePointCache, + SvgGlyphSource? svgGlyphSource) + { + this.compactFontTables = tables; + this.outlineType = OutlineType.CFF; + this.description = new FontDescription(tables.Name, tables.Os2, tables.Head); + this.GlyphVariationProcessor = processor; + this.glyphIdCache = sharedGlyphIdCache; + this.codePointCache = sharedCodePointCache; + this.glyphCache = new(); + this.svgGlyphSource = svgGlyphSource; + + (HorizontalMetrics HorizontalMetrics, VerticalMetrics VerticalMetrics) metrics = this.Initialize(tables); + this.horizontalMetrics = metrics.HorizontalMetrics; + this.verticalMetrics = metrics.VerticalMetrics; + } + + public HeadTable.HeadFlags HeadFlags { get; private set; } + + public GlyphVariationProcessor? GlyphVariationProcessor { get; private set; } + + /// + public override FontDescription Description => this.description; + + /// + public override ushort UnitsPerEm => this.unitsPerEm; + + /// + public override float ScaleFactor => this.scaleFactor; + + /// + public override HorizontalMetrics HorizontalMetrics => this.horizontalMetrics; + + /// + public override VerticalMetrics VerticalMetrics => this.verticalMetrics; + + /// + public override short SubscriptXSize => this.subscriptXSize; + + /// + public override short SubscriptYSize => this.subscriptYSize; + + /// + public override short SubscriptXOffset => this.subscriptXOffset; + + /// + public override short SubscriptYOffset => this.subscriptYOffset; + + /// + public override short SuperscriptXSize => this.superscriptXSize; + + /// + public override short SuperscriptYSize => this.superscriptYSize; + + /// + public override short SuperscriptXOffset => this.superscriptXOffset; + + /// + public override short SuperscriptYOffset => this.superscriptYOffset; + + /// + public override short StrikeoutSize => this.strikeoutSize; + + /// + public override short StrikeoutPosition => this.strikeoutPosition; + + /// + public override short UnderlinePosition => this.underlinePosition; + + /// + public override short UnderlineThickness => this.underlineThickness; + + /// + public override float ItalicAngle => this.italicAngle; + + /// + internal override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) + => this.TryGetGlyphId(codePoint, null, out glyphId, out bool _); + + /// + internal override bool TryGetGlyphId(CodePoint codePoint, CodePoint? nextCodePoint, out ushort glyphId, out bool skipNextCodePoint) + { + CMapTable cmap = this.outlineType == OutlineType.TrueType + ? this.trueTypeFontTables!.Cmap + : this.compactFontTables!.Cmap; + + (bool success, ushort id, bool skip) = this.glyphIdCache.GetOrAdd( + (codePoint.Value, nextCodePoint?.Value ?? -1), + static (_, arg) => + { + bool success = arg.cmap.TryGetGlyphId(arg.codePoint, arg.nextCodePoint, out ushort id, out bool skip); + return (success, id, skip); + }, + (cmap, codePoint, nextCodePoint)); + + glyphId = id; + skipNextCodePoint = skip; + return success; + } + + /// + internal override bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) + { + CMapTable cmap = this.outlineType == OutlineType.TrueType + ? this.trueTypeFontTables!.Cmap + : this.compactFontTables!.Cmap; + + (bool success, CodePoint value) = this.codePointCache.GetOrAdd( + glyphId, + static (glyphId, arg) => + { + bool success = arg.TryGetCodePoint(glyphId, out CodePoint codePoint); + return (success, codePoint); + }, + cmap); + + codePoint = value; + return success; + } + + /// + internal override bool TryGetGlyphClass(ushort glyphId, [NotNullWhen(true)] out GlyphClassDef? glyphClass) + { + GlyphDefinitionTable? gdef = this.outlineType == OutlineType.TrueType + ? this.trueTypeFontTables!.Gdef + : this.compactFontTables!.Gdef; + + glyphClass = null; + return gdef is not null && gdef.TryGetGlyphClass(glyphId, out glyphClass); + } + + /// + internal override bool TryGetMarkAttachmentClass(ushort glyphId, [NotNullWhen(true)] out GlyphClassDef? markAttachmentClass) + { + GlyphDefinitionTable? gdef = this.outlineType == OutlineType.TrueType + ? this.trueTypeFontTables!.Gdef + : this.compactFontTables!.Gdef; + + markAttachmentClass = null; + return gdef is not null && gdef.TryGetMarkAttachmentClass(glyphId, out markAttachmentClass); + } + + /// + public override bool TryGetVariationAxes(out ReadOnlyMemory variationAxes) + { + FVarTable? fvar = this.trueTypeFontTables?.Fvar ?? this.compactFontTables?.FVar; + Tables.General.Name.NameTable? names = this.trueTypeFontTables?.Name ?? this.compactFontTables?.Name; + + if (fvar == null) + { + variationAxes = ReadOnlyMemory.Empty; + return false; + } + + VariationAxis[] axes = new VariationAxis[fvar.Axes.Length]; + for (int i = 0; i < fvar.Axes.Length; i++) + { + VariationAxisRecord axis = fvar.Axes[i]; + string name = names != null ? names.GetNameById(CultureInfo.InvariantCulture, axis.AxisNameId) : string.Empty; + axes[i] = new VariationAxis() + { + Tag = axis.Tag, + Min = axis.MinValue, + Max = axis.MaxValue, + Default = axis.DefaultValue, + Name = name + }; + } + + variationAxes = axes; + return true; + } + + /// + internal override bool IsInMarkFilteringSet(ushort markGlyphSetIndex, ushort glyphId) + { + GlyphDefinitionTable? gdef = this.outlineType == OutlineType.TrueType + ? this.trueTypeFontTables!.Gdef + : this.compactFontTables!.Gdef; + + return gdef is not null && gdef.IsInMarkGlyphSet(markGlyphSetIndex, glyphId); + } + + /// + public override bool TryGetGlyphMetrics( + CodePoint codePoint, + TextAttributes textAttributes, + TextDecorations textDecorations, + LayoutMode layoutMode, + ColorFontSupport support, + [NotNullWhen(true)] out FontGlyphMetrics? metrics) + { + // We return metrics for the special glyph representing a missing character, commonly known as .notdef. + this.TryGetGlyphId(codePoint, out ushort glyphId); + metrics = this.GetGlyphMetrics(codePoint, glyphId, textAttributes, textDecorations, layoutMode, support); + return metrics != null; + } + + /// + internal override FontGlyphMetrics GetGlyphMetrics( + CodePoint codePoint, + ushort glyphId, + TextAttributes textAttributes, + TextDecorations textDecorations, + LayoutMode layoutMode, + ColorFontSupport support) + + // We overwrite the cache entry for this type should the attributes change. + => this.glyphCache.GetOrAdd( + CreateCacheKey(in codePoint, glyphId, textAttributes, support, layoutMode), + static (key, arg) => + + arg.Item3.CreateGlyphMetrics( + in arg.codePoint, + key.Id, + key.Id == 0 ? GlyphType.Fallback : GlyphType.Standard, + key.Attributes, + arg.textDecorations, + key.ColorSupport, + key.IsVerticalLayout), + (textDecorations, codePoint, this)); + + /// + public override ReadOnlyMemory GetAvailableCodePoints() + { + CMapTable cmap = this.outlineType == OutlineType.TrueType + ? this.trueTypeFontTables!.Cmap + : this.compactFontTables!.Cmap; + + return cmap.GetAvailableCodePoints(); + } + + /// + internal override bool TryGetGSubTable([NotNullWhen(true)] out GSubTable? gSubTable) + { + gSubTable = this.outlineType == OutlineType.TrueType + ? this.trueTypeFontTables!.GSub + : this.compactFontTables!.GSub; + + return gSubTable is not null; + } + + /// + internal override void ApplySubstitution(GlyphSubstitutionCollection collection) + { + if (this.TryGetGSubTable(out GSubTable? gSubTable)) + { + gSubTable.ApplySubstitution(this, collection); + } + } + + /// + internal override bool TryGetKerningOffset(ushort currentId, ushort nextId, out Vector2 vector) + { + bool isTTF = this.outlineType == OutlineType.TrueType; + KerningTable? kern = isTTF + ? this.trueTypeFontTables!.Kern + : this.compactFontTables!.Kern; + + if (kern is null) + { + vector = default; + return false; + } + + return kern.TryGetKerningOffset(currentId, nextId, out vector); + } + + /// + internal override void UpdatePositions(GlyphPositioningCollection collection) + { + bool isTTF = this.outlineType == OutlineType.TrueType; + GPosTable? gpos = isTTF + ? this.trueTypeFontTables!.GPos + : this.compactFontTables!.GPos; + + bool kerned = false; + KerningMode kerningMode = collection.TextOptions.KerningMode; + + gpos?.TryUpdatePositions(this, collection, out kerned); + + // TODO: I don't think we should disable kerning here. + if (!kerned && kerningMode != KerningMode.None) + { + KerningTable? kern = isTTF + ? this.trueTypeFontTables!.Kern + : this.compactFontTables!.Kern; + + if (kern?.Count > 0) + { + // Set max constraints to prevent OutOfMemoryException or infinite loops from attacks. + int maxCount = AdvancedTypographicUtils.GetMaxAllowableShapingCollectionCount(collection.Count); + for (int index = 0; index < collection.Count - 1; index++) + { + if (index >= maxCount) + { + break; + } + + kern.UpdatePositions(this, collection, index, index + 1); + } + } + } + } + + /// + internal override float GetGDefVariationDelta(uint packedVariationIndex) + { + if (packedVariationIndex == 0 || this.GlyphVariationProcessor is null) + { + return 0; + } + + GlyphDefinitionTable? gdef = this.outlineType == OutlineType.TrueType + ? this.trueTypeFontTables!.Gdef + : this.compactFontTables!.Gdef; + + if (gdef?.ItemVariationStore is null) + { + return 0; + } + + // The packed index encodes two uint16 values: + // - Upper 16 bits: outer index (selects the ItemVariationData subtable) + // - Lower 16 bits: inner index (selects the DeltaSet within that subtable) + int outerIndex = (int)(packedVariationIndex >> 16); + int innerIndex = (int)(packedVariationIndex & 0xFFFF); + return this.GlyphVariationProcessor.Delta(gdef.ItemVariationStore, outerIndex, innerIndex); + } + + /// + internal override ReadOnlySpan GetNormalizedCoordinates() + => this.GlyphVariationProcessor is not null + ? this.GlyphVariationProcessor.NormalizedCoordinates + : []; + + /// + /// Creates a new instance that shares all immutable table data + /// with this instance but uses a new initialized + /// to the specified variation axis settings. + /// + /// The variation axis settings to apply. + /// A new configured for the requested variation. + internal StreamFontMetrics CreateVariationInstance(FontVariation[] variations) + { + FVarTable? fvar = this.outlineType == OutlineType.TrueType + ? this.trueTypeFontTables?.Fvar + : this.compactFontTables?.FVar; + + if (fvar is null) + { + // Not a variable font; return this instance unchanged. + return this; + } + + // Map FontVariation tags to user coordinate array (indexed by fvar axis order). + // Start with default axis values so unspecified axes remain at their defaults. + float[] userCoordinates = new float[fvar.AxisCount]; + for (int i = 0; i < fvar.AxisCount; i++) + { + userCoordinates[i] = fvar.Axes[i].DefaultValue; + } + + for (int v = 0; v < variations.Length; v++) + { + FontVariation variation = variations[v]; + for (int i = 0; i < fvar.AxisCount; i++) + { + if (string.Equals(fvar.Axes[i].Tag, variation.Tag, StringComparison.Ordinal)) + { + userCoordinates[i] = variation.Value; + break; + } + } + } + + // Create a new processor with the user coordinates. Shares all table references. + if (this.outlineType == OutlineType.TrueType) + { + TrueTypeFontTables tables = this.trueTypeFontTables!; + ItemVariationStore? itemVariationStore = tables.Hvar?.ItemVariationStore ?? tables.Vvar?.ItemVariationStore; + GlyphVariationProcessor processor = new( + itemVariationStore, + fvar, + tables.Avar, + tables.Gvar, + tables.Hvar, + tables.Vvar, + tables.Mvar, + tables.Cvar, + userCoordinates); + + return new StreamFontMetrics(tables, processor, this.glyphIdCache, this.codePointCache, this.svgGlyphSource); + } + else + { + CompactFontTables tables = this.compactFontTables!; + ItemVariationStore? itemVariationStore = tables.Cff.ItemVariationStore; + GlyphVariationProcessor processor = new( + itemVariationStore, + fvar, + tables.AVar, + tables.GVar, + tables.HVar, + tables.VVar, + tables.MVar, + userCoordinates: userCoordinates); + + return new StreamFontMetrics(tables, processor, this.glyphIdCache, this.codePointCache, this.svgGlyphSource); + } + } + + /// + /// Reads a from the specified stream. + /// + /// The file path. + /// a . + public static StreamFontMetrics LoadFont(string path) + { + using FileStream fs = File.OpenRead(path); + using FontReader reader = new(fs); + return LoadFont(reader); + } + + /// + /// Reads a from the specified stream. + /// + /// The file path. + /// Position in the stream to read the font from. + /// a . + public static StreamFontMetrics LoadFont(string path, long offset) + { + using FileStream fs = File.OpenRead(path); + fs.Position = offset; + return LoadFont(fs); + } + + /// + /// Reads a from the specified stream. + /// + /// The stream. + /// a . + public static StreamFontMetrics LoadFont(Stream stream) + { + using FontReader reader = new(stream); + return LoadFont(reader); + } + + internal static StreamFontMetrics LoadFont(FontReader reader) + { + if (reader.OutlineType == OutlineType.TrueType) + { + return LoadTrueTypeFont(reader); + } + + return LoadCompactFont(reader); + } + + private (HorizontalMetrics HorizontalMetrics, VerticalMetrics VerticalMetrics) Initialize(T tables) + where T : IFontTables + { + HeadTable head = tables.Head; + HorizontalHeadTable hhea = tables.Hhea; + VerticalHeadTable? vhea = tables.Vhea; + OS2Table os2 = tables.Os2; + PostTable post = tables.Post; + + this.HeadFlags = head.Flags; + this.unitsPerEm = head.UnitsPerEm; + this.scaleFactor = this.unitsPerEm * 72F; // 72 * UnitsPerEm means 1pt = 1px + this.subscriptXSize = os2.SubscriptXSize; + this.subscriptYSize = os2.SubscriptYSize; + this.subscriptXOffset = os2.SubscriptXOffset; + this.subscriptYOffset = os2.SubscriptYOffset; + this.superscriptXSize = os2.SuperscriptXSize; + this.superscriptYSize = os2.SuperscriptYSize; + this.superscriptXOffset = os2.SuperscriptXOffset; + this.superscriptYOffset = os2.SuperscriptYOffset; + this.strikeoutSize = os2.StrikeoutSize; + this.strikeoutPosition = os2.StrikeoutPosition; + this.underlinePosition = post.UnderlinePosition; + this.underlineThickness = post.UnderlineThickness; + this.italicAngle = post.ItalicAngle; + + HorizontalMetrics horizontalMetrics = InitializeHorizontalMetrics(hhea, vhea, os2); + VerticalMetrics verticalMetrics = InitializeVerticalMetrics(horizontalMetrics, vhea); + + // Apply MVAR deltas for the current variation coordinates. + if (this.GlyphVariationProcessor is not null) + { + this.ApplyMVarDeltas(horizontalMetrics, verticalMetrics); + } + + return (horizontalMetrics, verticalMetrics); + } + + private static HorizontalMetrics InitializeHorizontalMetrics(HorizontalHeadTable hhea, VerticalHeadTable? vhea, OS2Table os2) + { + short ascender; + short descender; + short lineGap; + short lineHeight; + short advanceWidthMax; + short advanceHeightMax; + + // https://www.microsoft.com/typography/otspec/recom.htm#tad + // We use the same approach as FreeType for calculating the the global ascender, descender, and + // height of OpenType fonts for consistency. + // + // 1.If the OS/ 2 table exists and the fsSelection bit 7 is set (USE_TYPO_METRICS), trust the font + // and use the Typo* metrics. + // 2.Otherwise, use the HorizontalHeadTable "hhea" table's metrics. + // 3.If they are zero and the OS/ 2 table exists, + // - Use the OS/ 2 table's sTypo* metrics if they are non-zero. + // - Otherwise, use the OS / 2 table's usWin* metrics. + bool useTypoMetrics = (os2.FontStyle & OS2Table.FontStyleSelection.USE_TYPO_METRICS) == OS2Table.FontStyleSelection.USE_TYPO_METRICS; + if (useTypoMetrics) + { + ascender = os2.TypoAscender; + descender = os2.TypoDescender; + lineGap = os2.TypoLineGap; + lineHeight = (short)(ascender - descender + lineGap); + } + else + { + ascender = hhea.Ascender; + descender = hhea.Descender; + lineGap = hhea.LineGap; + lineHeight = (short)(ascender - descender + lineGap); + } + + if (ascender == 0 || descender == 0) + { + if (os2.TypoAscender != 0 || os2.TypoDescender != 0) + { + ascender = os2.TypoAscender; + descender = os2.TypoDescender; + lineGap = os2.TypoLineGap; + lineHeight = (short)(ascender - descender + lineGap); + } + else + { + ascender = (short)os2.WinAscent; + descender = (short)-os2.WinDescent; + lineHeight = (short)(ascender - descender); + } + } + + advanceWidthMax = (short)hhea.AdvanceWidthMax; + advanceHeightMax = vhea == null ? lineHeight : vhea.AdvanceHeightMax; + + return new() + { + Ascender = ascender, + Descender = descender, + LineGap = lineGap, + LineHeight = lineHeight, + AdvanceWidthMax = advanceWidthMax, + AdvanceHeightMax = advanceHeightMax + }; + } + + private static VerticalMetrics InitializeVerticalMetrics(HorizontalMetrics metrics, VerticalHeadTable? vhea) + { + VerticalMetrics verticalMetrics = new() + { + Ascender = metrics.Ascender, + Descender = metrics.Descender, + LineGap = metrics.LineGap, + LineHeight = metrics.LineHeight, + AdvanceWidthMax = metrics.AdvanceWidthMax, + AdvanceHeightMax = metrics.AdvanceHeightMax, + Synthesized = true + }; + + if (vhea is null) + { + return verticalMetrics; + } + + short ascender = vhea.Ascender; + + // Always negative due to the grid orientation. + short descender = (short)(vhea.Descender > 0 ? -vhea.Descender : vhea.Descender); + short lineGap = vhea.LineGap; + short lineHeight = (short)(ascender - descender + lineGap); + + verticalMetrics.Ascender = ascender; + verticalMetrics.Descender = descender; + verticalMetrics.LineGap = lineGap; + verticalMetrics.LineHeight = lineHeight; + verticalMetrics.Synthesized = false; + + return verticalMetrics; + } + + /// + /// Applies MVAR (Metrics Variations) deltas to all font-wide metrics. + /// MVAR adjusts global metrics (ascender, descender, line gap, strikeout, underline, etc.) + /// based on the current variation coordinates. + /// + /// + private void ApplyMVarDeltas(HorizontalMetrics horizontalMetrics, VerticalMetrics verticalMetrics) + { + GlyphVariationProcessor processor = this.GlyphVariationProcessor!; + + // MVAR tags are 4-byte big-endian ASCII values. + // Horizontal metrics from OS/2 or hhea. + horizontalMetrics.Ascender += (short)MathF.Round(processor.GetMVarDelta(MVarTag.HorizontalAscender)); + horizontalMetrics.Descender += (short)MathF.Round(processor.GetMVarDelta(MVarTag.HorizontalDescender)); + horizontalMetrics.LineGap += (short)MathF.Round(processor.GetMVarDelta(MVarTag.HorizontalLineGap)); + horizontalMetrics.LineHeight = (short)(horizontalMetrics.Ascender - horizontalMetrics.Descender + horizontalMetrics.LineGap); + + // Vertical metrics from vhea. + if (!verticalMetrics.Synthesized) + { + verticalMetrics.Ascender += (short)MathF.Round(processor.GetMVarDelta(MVarTag.VerticalAscender)); + verticalMetrics.Descender += (short)MathF.Round(processor.GetMVarDelta(MVarTag.VerticalDescender)); + verticalMetrics.LineGap += (short)MathF.Round(processor.GetMVarDelta(MVarTag.VerticalLineGap)); + verticalMetrics.LineHeight = (short)(verticalMetrics.Ascender - verticalMetrics.Descender + verticalMetrics.LineGap); + } + + // OS/2 subscript metrics. + this.subscriptXSize += (short)MathF.Round(processor.GetMVarDelta(MVarTag.SubscriptXSize)); + this.subscriptYSize += (short)MathF.Round(processor.GetMVarDelta(MVarTag.SubscriptYSize)); + this.subscriptXOffset += (short)MathF.Round(processor.GetMVarDelta(MVarTag.SubscriptXOffset)); + this.subscriptYOffset += (short)MathF.Round(processor.GetMVarDelta(MVarTag.SubscriptYOffset)); + + // OS/2 superscript metrics. + this.superscriptXSize += (short)MathF.Round(processor.GetMVarDelta(MVarTag.SuperscriptXSize)); + this.superscriptYSize += (short)MathF.Round(processor.GetMVarDelta(MVarTag.SuperscriptYSize)); + this.superscriptXOffset += (short)MathF.Round(processor.GetMVarDelta(MVarTag.SuperscriptXOffset)); + this.superscriptYOffset += (short)MathF.Round(processor.GetMVarDelta(MVarTag.SuperscriptYOffset)); + + // OS/2 strikeout metrics. + this.strikeoutSize += (short)MathF.Round(processor.GetMVarDelta(MVarTag.StrikeoutSize)); + this.strikeoutPosition += (short)MathF.Round(processor.GetMVarDelta(MVarTag.StrikeoutPosition)); + + // post underline metrics. + this.underlinePosition += (short)MathF.Round(processor.GetMVarDelta(MVarTag.UnderlinePosition)); + this.underlineThickness += (short)MathF.Round(processor.GetMVarDelta(MVarTag.UnderlineThickness)); + } + + /// + /// Reads a from the specified stream. + /// + /// The file path. + /// A read-only memory region containing the font metrics. + public static ReadOnlyMemory LoadFontCollection(string path) + { + using FileStream fs = File.OpenRead(path); + return LoadFontCollection(fs); + } + + /// + /// Reads a from the specified stream. + /// + /// The stream. + /// A read-only memory region containing the font metrics. + public static ReadOnlyMemory LoadFontCollection(Stream stream) + { + long startPos = stream.Position; + BigEndianBinaryReader reader = new(stream, true); + TtcHeader ttcHeader = TtcHeader.Read(reader); + StreamFontMetrics[] fonts = new StreamFontMetrics[(int)ttcHeader.NumFonts]; + + for (int i = 0; i < ttcHeader.NumFonts; ++i) + { + stream.Position = startPos + ttcHeader.OffsetTable[i]; + fonts[i] = LoadFont(stream); + } + + return fonts; + } + + private static (int CodePoint, ushort Id, TextAttributes Attributes, ColorFontSupport ColorSupport, bool IsVerticalLayout) CreateCacheKey( + in CodePoint codePoint, + ushort glyphId, + TextAttributes textAttributes, + ColorFontSupport colorSupport, + LayoutMode layoutMode) + => (codePoint.Value, glyphId, textAttributes, colorSupport, AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode)); + + private FontGlyphMetrics CreateGlyphMetrics( + in CodePoint codePoint, + ushort glyphId, + GlyphType glyphType, + TextAttributes textAttributes, + TextDecorations textDecorations, + ColorFontSupport colorSupport, + bool isVerticalLayout, + ushort paletteIndex = 0) + => this.outlineType switch + { + OutlineType.TrueType => this.CreateTrueTypeGlyphMetrics(in codePoint, glyphId, glyphType, textAttributes, textDecorations, colorSupport, isVerticalLayout, paletteIndex), + OutlineType.CFF => this.CreateCffGlyphMetrics(in codePoint, glyphId, glyphType, textAttributes, textDecorations, colorSupport, isVerticalLayout, paletteIndex), + _ => throw new NotSupportedException(), + }; + + private SvgGlyphSource GetOrCreateSvgGlyphSource(SvgTable svgTable) + => this.svgGlyphSource ??= new SvgGlyphSource(svgTable); + } +} diff --git a/SixLabors.Fonts/StringComparerHelpers.cs b/SixLabors.Fonts/StringComparerHelpers.cs new file mode 100644 index 0000000..0307813 --- /dev/null +++ b/SixLabors.Fonts/StringComparerHelpers.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Globalization; + +namespace SixLabors.Fonts { + internal static class StringComparerHelpers + { + public static StringComparer GetCaseInsensitiveStringComparer(CultureInfo culture) + { + if (culture != null) + { + return StringComparer.Create(culture, true); + } + + return StringComparer.OrdinalIgnoreCase; + } + } +} diff --git a/SixLabors.Fonts/SystemFontCollection.cs b/SixLabors.Fonts/SystemFontCollection.cs new file mode 100644 index 0000000..d605eee --- /dev/null +++ b/SixLabors.Fonts/SystemFontCollection.cs @@ -0,0 +1,174 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; + +namespace SixLabors.Fonts { + /// + /// Provides a collection of fonts. + /// + internal sealed class SystemFontCollection : IReadOnlySystemFontCollection, IReadOnlyFontMetricsCollection + { + private readonly FontCollection collection; + private readonly IReadOnlyCollection searchDirectories; + + /// + /// Gets the default set of locations we probe for System Fonts. + /// + private static readonly IReadOnlyCollection StandardFontLocations; + + static SystemFontCollection() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + StandardFontLocations = new[] + { + @"%SYSTEMROOT%\Fonts", + @"%APPDATA%\Microsoft\Windows\Fonts", + @"%LOCALAPPDATA%\Microsoft\Windows\Fonts", + }; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + StandardFontLocations = new[] + { + "%HOME%/.fonts/", + "%HOME%/.local/share/fonts/", + "/usr/local/share/fonts/", + "/usr/share/fonts/", + }; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + StandardFontLocations = new[] + { + // As documented on "Mac OS X: Font locations and their purposes" + // https://web.archive.org/web/20191015122508/https://support.apple.com/en-us/HT201722 + "%HOME%/Library/Fonts/", + "/Library/Fonts/", + "/System/Library/Fonts/", + "/Network/Library/Fonts/", + }; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("Android"))) + { + StandardFontLocations = new[] + { + "/system/fonts/" + }; + } + else + { + StandardFontLocations = Array.Empty(); + } + } + + public SystemFontCollection() + { + IEnumerable paths; + Native.MacSystemFontsEnumerator? nativeEnumerator = null; + + bool forceDirectoryEnumeration = AppContext.TryGetSwitch("Switch.SixLabors.Fonts.DoNotUseNativeSystemFontsEnumeration", out bool isEnabled) && isEnabled; + if (!forceDirectoryEnumeration && RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + nativeEnumerator = new Native.MacSystemFontsEnumerator(); + + // The CTFontManagerCopyAvailableFontURLs method might return duplicate paths, hence the call to Distinct() + paths = nativeEnumerator.Distinct(); + + this.searchDirectories = Array.Empty(); + } + else + { + string[] expanded = [.. StandardFontLocations.Select(Environment.ExpandEnvironmentVariables)]; + string[] existingDirectories = [.. expanded.Where(x => Directory.Exists(x))]; + + // We do this to provide a consistent experience with case sensitive file systems. + paths = existingDirectories + .SelectMany(x => Directory.EnumerateFiles(x, "*.*", SearchOption.AllDirectories)) + .Where(x => Path.GetExtension(x).Equals(".ttf", StringComparison.OrdinalIgnoreCase) + || Path.GetExtension(x).Equals(".ttc", StringComparison.OrdinalIgnoreCase) + || Path.GetExtension(x).Equals(".otf", StringComparison.OrdinalIgnoreCase)); + + this.searchDirectories = existingDirectories; + } + + this.collection = CreateSystemFontCollection(paths, this.searchDirectories); + + nativeEnumerator?.Dispose(); + } + + /// + public IEnumerable Families => this.collection.Families; + + /// + public IEnumerable SearchDirectories => this.searchDirectories; + + /// + public FontFamily Get(string name) => this.GetByCulture(name, CultureInfo.InvariantCulture); + + /// + public bool TryGet(string name, out FontFamily family) + => this.collection.TryGet(name, out family); + + /// + public IEnumerable GetByCulture(CultureInfo culture) + => this.collection.GetByCulture(culture); + + /// + public FontFamily GetByCulture(string name, CultureInfo culture) + => this.collection.GetByCulture(name, culture); + + /// + public bool TryGetByCulture(string name, CultureInfo culture, out FontFamily family) + => this.collection.TryGetByCulture(name, culture, out family); + + /// + bool IReadOnlyFontMetricsCollection.TryGetMetrics(string name, CultureInfo culture, FontStyle style, [NotNullWhen(true)] out FontMetrics? metrics) + => ((IReadOnlyFontMetricsCollection)this.collection).TryGetMetrics(name, culture, style, out metrics); + + /// + IEnumerable IReadOnlyFontMetricsCollection.GetAllMetrics(string name, CultureInfo culture) + => ((IReadOnlyFontMetricsCollection)this.collection).GetAllMetrics(name, culture); + + /// + ReadOnlyMemory IReadOnlyFontMetricsCollection.GetAllStyles(string name, CultureInfo culture) + => ((IReadOnlyFontMetricsCollection)this.collection).GetAllStyles(name, culture); + + /// + IEnumerator IReadOnlyFontMetricsCollection.GetEnumerator() + => ((IReadOnlyFontMetricsCollection)this.collection).GetEnumerator(); + + private static FontCollection CreateSystemFontCollection(IEnumerable paths, IReadOnlyCollection searchDirectories) + { + FontCollection collection = new(searchDirectories); + + foreach (string path in paths) + { + try + { + if (path.EndsWith(".ttc", StringComparison.OrdinalIgnoreCase)) + { + _ = collection.AddCollection(path); + } + else + { + _ = collection.Add(path); + } + } + catch + { + // We swallow exceptions installing system fonts as we hold no guarantees about permissions etc. + } + } + + return collection; + } + } +} diff --git a/SixLabors.Fonts/SystemFonts.cs b/SixLabors.Fonts/SystemFonts.cs new file mode 100644 index 0000000..839df6b --- /dev/null +++ b/SixLabors.Fonts/SystemFonts.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace SixLabors.Fonts { + /// + /// Provides a collection of fonts. + /// + public static class SystemFonts + { + private static readonly Lazy LazySystemFonts = new(() => new SystemFontCollection(), true); + + /// + /// Gets the collection containing the globally installed system fonts. + /// + public static IReadOnlySystemFontCollection Collection => LazySystemFonts.Value; + + /// + /// Gets the collection of s installed on current system. + /// + public static IEnumerable Families => Collection.Families; + + /// + public static FontFamily Get(string name) => GetByCulture(name, CultureInfo.InvariantCulture); + + /// + public static bool TryGet(string fontFamily, out FontFamily family) + => Collection.TryGet(fontFamily, out family); + + /// + /// Create a new instance of the for the named font family with regular styling. + /// + /// The font family name. + /// The size of the font in PT units. + /// The new . + public static Font CreateFont(string name, float size) + => Collection.Get(name).CreateFont(size); + + /// + /// Create a new instance of the for the named font family. + /// + /// The font family name. + /// The size of the font in PT units. + /// The font style. + /// The new . + public static Font CreateFont(string name, float size, FontStyle style) + => Collection.Get(name).CreateFont(size, style); + + /// + public static IEnumerable GetByCulture(CultureInfo culture) + => Collection.GetByCulture(culture); + + /// + public static FontFamily GetByCulture(string fontFamily, CultureInfo culture) + => Collection.GetByCulture(fontFamily, culture); + + /// + public static bool TryGetByCulture(string fontFamily, CultureInfo culture, out FontFamily family) + => Collection.TryGetByCulture(fontFamily, culture, out family); + + /// + /// Create a new instance of the for the named font family with regular styling. + /// + /// The font family name. + /// The font culture. + /// The size of the font in PT units. + /// The new . + public static Font CreateFont(string name, CultureInfo culture, float size) + => Collection.GetByCulture(name, culture).CreateFont(size); + + /// + /// Create a new instance of the for the named font family. + /// + /// The font family name. + /// The font culture. + /// The size of the font in PT units. + /// The font style. + /// The new . + public static Font CreateFont(string name, CultureInfo culture, float size, FontStyle style) + => Collection.GetByCulture(name, culture).CreateFont(size, style); + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs new file mode 100644 index 0000000..760ee17 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -0,0 +1,700 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic.GPos; +using SixLabors.Fonts.Unicode; +using System; +using System.Collections.Generic; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// Provides shared utility methods for advanced typographic layout processing in GPOS and GSUB tables. + /// + internal static class AdvancedTypographicUtils + { + /// + /// The maximum context length for sequence matching operations. + /// Used to prevent excessive processing from maliciously crafted fonts. + /// Based on HarfBuzz hb-buffer.hh. + /// + public const int MaxContextLength = 64; + + /// + /// The maximum length factor multiplied by collection count to compute max allowable collection size. + /// + private const int MaxLengthFactor = 64; + + /// + /// The minimum value for the max allowable collection size. + /// + private const int MaxLengthMinimum = 16384; + + /// + /// The maximum operations factor multiplied by collection count to compute max allowable operations. + /// + private const int MaxOperationsFactor = 1024; + + /// + /// The minimum value for the max allowable operations count. + /// + private const int MaxOperationsMinimum = 16384; + + /// + /// The absolute maximum number of shaping characters, set to half of int.MaxValue. + /// + private const int MaxShapingCharsLength = 0x3FFFFFFF; // Half int max. + + /// + /// Defines the direction for sequence matching operations. + /// + internal enum MatchDirection + { + /// + /// Match in the forward direction. + /// + Forward, + + /// + /// Match in the backward direction. + /// + Backward + } + + /// + /// Gets a value indicating whether the glyph represented by the codepoint should be interpreted vertically. + /// + /// The codepoint represented by the glyph. + /// The layout mode. + /// The . + public static bool IsVerticalGlyph(CodePoint codePoint, LayoutMode layoutMode) + { + if (layoutMode.IsVertical()) + { + return true; + } + + bool isVerticalLayout = layoutMode.IsVerticalMixed(); + return isVerticalLayout && CodePoint.GetVerticalOrientationType(codePoint) is VerticalOrientationType.Upright or VerticalOrientationType.TransformUpright; + } + + /// + /// Gets the maximum allowable shaping collection count for the given input length. + /// + /// The input collection length. + /// The maximum allowable count. + public static int GetMaxAllowableShapingCollectionCount(int length) + => (int)Math.Min(Math.Max((long)length * MaxLengthFactor, MaxLengthMinimum), MaxShapingCharsLength); + + /// + /// Gets the maximum allowable shaping operations count for the given input length. + /// + /// The input collection length. + /// The maximum allowable operations count. + public static int GetMaxAllowableShapingOperationsCount(int length) + => (int)Math.Min(Math.Max((long)length * MaxOperationsFactor, MaxOperationsMinimum), MaxShapingCharsLength); + + /// + /// Applies nested lookups from sequence lookup records for GSUB contextual/chaining lookups. + /// + /// The font metrics. + /// The GSUB table. + /// The feature tag being applied. + /// The lookup flags for glyph filtering. + /// The mark filtering set index. + /// The sequence lookup records specifying which lookups to apply at which positions. + /// The glyph substitution collection. + /// The starting index in the collection. + /// The number of glyphs in the input sequence. + /// if the lookups were applied. + public static bool ApplyLookupList( + FontMetrics fontMetrics, + GSubTable table, + Tag feature, + LookupFlags lookupFlags, + ushort markFilteringSet, + SequenceLookupRecord[] records, + GlyphSubstitutionCollection collection, + int index, + int count) + { + SkippingGlyphIterator iterator = new(fontMetrics, collection, index, lookupFlags, markFilteringSet); + int currentCount = collection.Count; + + foreach (SequenceLookupRecord lookupRecord in records) + { + ushort sequenceIndex = lookupRecord.SequenceIndex; + ushort lookupIndex = lookupRecord.LookupListIndex; + iterator.Index = index; + iterator.Increment(sequenceIndex); + GSub.LookupTable lookup = table.LookupList.LookupTables[lookupIndex]; + _ = lookup.TrySubstitution(fontMetrics, table, collection, feature, iterator.Index, count - (iterator.Index - index)); + + // Account for substitutions changing the length of the collection. + if (collection.Count != currentCount) + { + count -= currentCount - collection.Count; + currentCount = collection.Count; + } + } + + return true; + } + + /// + /// Applies nested lookups from sequence lookup records for GPOS contextual/chaining lookups. + /// + /// The font metrics. + /// The GPOS table. + /// The feature tag being applied. + /// The lookup flags for glyph filtering. + /// The mark filtering set index. + /// The sequence lookup records specifying which lookups to apply at which positions. + /// The glyph positioning collection. + /// The starting index in the collection. + /// The number of glyphs in the input sequence. + /// if the lookups were applied. + public static bool ApplyLookupList( + FontMetrics fontMetrics, + GPosTable table, + Tag feature, + LookupFlags lookupFlags, + ushort markFilteringSet, + SequenceLookupRecord[] records, + GlyphPositioningCollection collection, + int index, + int count) + { + SkippingGlyphIterator iterator = new(fontMetrics, collection, index, lookupFlags, markFilteringSet); + foreach (SequenceLookupRecord lookupRecord in records) + { + ushort sequenceIndex = lookupRecord.SequenceIndex; + ushort lookupIndex = lookupRecord.LookupListIndex; + iterator.Index = index; + iterator.Increment(sequenceIndex); + LookupTable lookup = table.LookupList.LookupTables[lookupIndex]; + _ = lookup.TryUpdatePosition(fontMetrics, table, collection, feature, iterator.Index, count - (iterator.Index - index)); + } + + return true; + } + + /// + /// Matches an input glyph sequence by glyph ID, verifying that each glyph has the specified feature enabled. + /// + /// The skipping glyph iterator. + /// The feature tag that must be enabled on matched glyphs. + /// The initial increment from the iterator's current position. + /// The array of glyph IDs to match. + /// A span to store matched glyph indices, or default if not needed. + /// if the entire sequence was matched; otherwise, . + public static bool MatchInputSequence(SkippingGlyphIterator iterator, Tag feature, ushort increment, ushort[] sequence, Span matches) + => Match( + increment, + sequence, + iterator, + (component, data) => + { + if (!ContainsFeatureTag(data.Features, feature)) + { + return false; + } + + return component == data.GlyphId; + }, + matches); + + /// + /// Determines whether the feature list contains the specified feature tag in an enabled state. + /// + /// The list of tag entries to search. + /// The feature tag to find. + /// if the feature is present and enabled; otherwise, . + private static bool ContainsFeatureTag(List featureList, Tag feature) + { + foreach (TagEntry tagEntry in featureList) + { + if (tagEntry.Tag == feature && tagEntry.Enabled) + { + return true; + } + } + + return false; + } + + /// + /// Matches a glyph sequence by glyph ID. + /// + /// The skipping glyph iterator. + /// The initial increment from the iterator's current position. + /// The array of glyph IDs to match. + /// if the entire sequence was matched; otherwise, . + public static bool MatchSequence(SkippingGlyphIterator iterator, int increment, ushort[] sequence) + => Match( + increment, + sequence, + iterator, + (component, data) => component == data.GlyphId, + default); + + /// + /// Matches a glyph sequence by class values using a class definition table. + /// + /// The skipping glyph iterator. + /// The initial increment from the iterator's current position. + /// The array of class values to match. + /// The class definition table used to map glyph IDs to class values. + /// if the entire sequence was matched; otherwise, . + public static bool MatchClassSequence( + SkippingGlyphIterator iterator, + int increment, + ushort[] sequence, + ClassDefinitionTable classDefinitionTable) + => Match( + increment, + sequence, + iterator, + (component, data) => component == classDefinitionTable.ClassIndexOf(data.GlyphId), + default); + + /// + /// Matches a forward glyph sequence using coverage tables. + /// + /// The skipping glyph iterator. + /// The array of coverage tables to match against. + /// The starting index in the collection. + /// The exclusive end index in the collection. + /// if all coverage tables matched; otherwise, . + public static bool MatchCoverageSequence( + SkippingGlyphIterator iterator, + CoverageTable[] coverageTable, + int startIndex, + int endExclusive) + => Match( + iterator, + startIndex, + coverageTable, + MatchDirection.Forward, + endExclusive, + (component, data) => component.CoverageIndexOf(data.GlyphId) >= 0, + default); + + /// + /// Matches a backward (backtrack) glyph sequence using coverage tables. + /// Per the spec, backtrack[0] matches i-1, then i-2, and so on. + /// + /// The skipping glyph iterator. + /// The array of backtrack coverage tables to match against. + /// The starting index in the collection (the first backtrack position). + /// The exclusive end index in the collection. + /// if all backtrack coverage tables matched; otherwise, . + public static bool MatchBacktrackCoverageSequence( + SkippingGlyphIterator iterator, + CoverageTable[] backtrack, + int startIndex, + int endExclusive) + => Match( + iterator, + startIndex, + backtrack, + MatchDirection.Backward, + endExclusive, + (component, data) => component.CoverageIndexOf(data.GlyphId) >= 0, + default); + + /// + /// Applies a chained sequence rule by matching backtrack, input, and lookahead glyph ID sequences. + /// + /// The skipping glyph iterator. + /// The chained sequence rule table to apply. + /// if all sequences matched; otherwise, . + public static bool ApplyChainedSequenceRule(SkippingGlyphIterator iterator, ChainedSequenceRuleTable rule) + { + if (rule.BacktrackSequence.Length > 0 + && !MatchSequence(iterator, -rule.BacktrackSequence.Length, rule.BacktrackSequence)) + { + return false; + } + + if (rule.InputSequence.Length > 0 + && !MatchSequence(iterator, 1, rule.InputSequence)) + { + return false; + } + + if (rule.LookaheadSequence.Length > 0 + && !MatchSequence(iterator, 1 + rule.InputSequence.Length, rule.LookaheadSequence)) + { + return false; + } + + return true; + } + + /// + /// Applies a chained class sequence rule by matching backtrack, input, and lookahead class sequences. + /// + /// The skipping glyph iterator. + /// The chained class sequence rule table to apply. + /// The class definition table for the input sequence. + /// The class definition table for the backtrack sequence. + /// The class definition table for the lookahead sequence. + /// if all sequences matched; otherwise, . + public static bool ApplyChainedClassSequenceRule( + SkippingGlyphIterator iterator, + ChainedClassSequenceRuleTable rule, + ClassDefinitionTable inputClassDefinitionTable, + ClassDefinitionTable backtrackClassDefinitionTable, + ClassDefinitionTable lookaheadClassDefinitionTable) + { + if (rule.BacktrackSequence.Length > 0 + && !MatchClassSequence(iterator, -rule.BacktrackSequence.Length, rule.BacktrackSequence, backtrackClassDefinitionTable)) + { + return false; + } + + if (rule.InputSequence.Length > 0 && + !MatchClassSequence(iterator, 1, rule.InputSequence, inputClassDefinitionTable)) + { + return false; + } + + if (rule.LookaheadSequence.Length > 0 + && !MatchClassSequence(iterator, 1 + rule.InputSequence.Length, rule.LookaheadSequence, lookaheadClassDefinitionTable)) + { + return false; + } + + return true; + } + + /// + /// Checks all coverage tables (backtrack, input, and lookahead) for a chained context Format 3 match. + /// + /// The font metrics. + /// The lookup flags for glyph filtering. + /// The mark filtering set index. + /// The glyph shaping collection. + /// The starting index of the input sequence. + /// The number of glyphs available from the starting index. + /// The array of input coverage tables. + /// The array of backtrack coverage tables. + /// The array of lookahead coverage tables. + /// if all coverages matched; otherwise, . + public static bool CheckAllCoverages( + FontMetrics fontMetrics, + LookupFlags lookupFlags, + ushort markFilteringSet, + IGlyphShapingCollection collection, + int index, + int count, + CoverageTable[] input, + CoverageTable[] backtrack, + CoverageTable[] lookahead) + { + int endExclusive = index + count; + + SkippingGlyphIterator iterator = new(fontMetrics, collection, index, lookupFlags, markFilteringSet); + + // Compute backtrack start using skippy prev(), not index-1. + int backtrackStart = index; + if (backtrack.Length > 0) + { + SkippingGlyphIterator backIt = iterator; + backIt.Index = index; + backtrackStart = backIt.Prev(); // first backtrack glyph (i-1 in skippy space) + } + + if (!MatchBacktrackCoverageSequence(iterator, backtrack, backtrackStart, endExclusive)) + { + return false; + } + + // Input starts at the current glyph position. + if (!MatchCoverageSequence(iterator, input, index, endExclusive)) + { + return false; + } + + // Compute lookahead start by advancing through the input sequence using skippy Next(), + // not by raw index arithmetic. + int lookaheadStart = index; + if (lookahead.Length > 0) + { + SkippingGlyphIterator fwdIt = iterator; + fwdIt.Index = index; + fwdIt.Increment(input.Length); // advance input.Length steps in skippy space + lookaheadStart = fwdIt.Index; + } + + if (!MatchCoverageSequence(iterator, lookahead, lookaheadStart, endExclusive)) + { + return false; + } + + return true; + } + + /// + /// Applies anchor-based positioning for mark-to-base, mark-to-ligature, or mark-to-mark attachment. + /// + /// The font metrics. + /// The glyph positioning collection. + /// The index of the mark glyph in the collection. + /// The anchor table for the base glyph, or if no anchor is defined. + /// The mark record containing the mark anchor table and class. + /// The index of the base glyph in the collection. + /// The feature tag being applied. + public static void ApplyAnchor( + FontMetrics fontMetrics, + GlyphPositioningCollection collection, + int index, + AnchorTable? baseAnchor, + MarkRecord markRecord, + int baseGlyphIndex, + Tag feature) + { + // baseAnchor may be null because OpenType MarkToBase allows NULL anchor offsets + // in BaseArray/BaseRecord. A NULL offset means "this base glyph has no anchor + // for this mark class", and the lookup must be ignored for this mark–base pair. + if (baseAnchor is null) + { + return; + } + + GlyphShapingData baseData = collection[baseGlyphIndex]; + AnchorXY baseXY = baseAnchor.GetAnchor(fontMetrics, baseData, collection); + + GlyphShapingData markData = collection[index]; + AnchorXY markXY = markRecord.MarkAnchorTable.GetAnchor(fontMetrics, markData, collection); + + markData.Bounds.X = baseXY.XCoordinate - markXY.XCoordinate; + markData.Bounds.Y = baseXY.YCoordinate - markXY.YCoordinate; + markData.MarkAttachment = baseGlyphIndex; + markData.AppliedFeatures.Add(feature); + } + + /// + /// Applies a value record's positioning adjustments to a glyph in the collection. + /// + /// The font metrics. + /// The glyph positioning collection. + /// The index of the glyph in the collection. + /// The value record containing positioning adjustments. + /// The feature tag being applied. + public static void ApplyPosition( + FontMetrics fontMetrics, + GlyphPositioningCollection collection, + int index, + ValueRecord record, + Tag feature) + { + GlyphShapingData current = collection[index]; + current.Bounds.Width += record.XAdvance; + current.Bounds.Height += record.YAdvance; + current.Bounds.X += record.XPlacement; + current.Bounds.Y += record.YPlacement; + + // Apply variation deltas from VariationIndex tables (variable fonts). + if (record.HasVariation) + { + current.Bounds.X += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.XPlacementVariation)); + current.Bounds.Y += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.YPlacementVariation)); + current.Bounds.Width += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.XAdvanceVariation)); + current.Bounds.Height += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.YAdvanceVariation)); + } + + current.AppliedFeatures.Add(feature); + } + + /// + /// Determines whether the specified glyph is a mark glyph based on GDEF class or Unicode properties. + /// + /// The font metrics. + /// The glyph identifier. + /// The glyph shaping data. + /// if the glyph is a mark; otherwise, . + public static bool IsMarkGlyph(FontMetrics fontMetrics, ushort glyphId, GlyphShapingData shapingData) + { + if (!fontMetrics.TryGetGlyphClass(glyphId, out GlyphClassDef? glyphClass) && + !CodePoint.IsMark(shapingData.CodePoint)) + { + return false; + } + + if (glyphClass != GlyphClassDef.MarkGlyph) + { + return false; + } + + return true; + } + + /// + /// Gets the glyph shaping class (mark, base, ligature, mark attachment type) for the specified glyph, + /// using GDEF table data if available or falling back to Unicode properties. + /// Results are cached on the instance. + /// + /// The font metrics. + /// The glyph identifier. + /// The glyph shaping data, used for caching and Unicode fallback. + /// The . + public static GlyphShapingClass GetGlyphShapingClass(FontMetrics fontMetrics, ushort glyphId, GlyphShapingData shapingData) + { + // Cache the shaping class on the GlyphShapingData to avoid repeated GDEF lookups. + // The cache key stores the glyph id; -1 means "not cached". + if (shapingData.ShapingClassCacheKey == glyphId) + { + return shapingData.CachedShapingClass; + } + + bool isMark; + bool isBase; + bool isLigature; + ushort markAttachmentType = 0; + if (fontMetrics.TryGetGlyphClass(glyphId, out GlyphClassDef? glyphClass)) + { + isMark = glyphClass == GlyphClassDef.MarkGlyph; + isBase = glyphClass == GlyphClassDef.BaseGlyph; + isLigature = glyphClass == GlyphClassDef.LigatureGlyph; + if (fontMetrics.TryGetMarkAttachmentClass(glyphId, out GlyphClassDef? markAttachmentClass)) + { + markAttachmentType = (ushort)markAttachmentClass; + } + } + else + { + // TODO: We may have to store each codepoint. FontKit checks all. + isMark = CodePoint.IsMark(shapingData.CodePoint); + isBase = !isMark; + isLigature = shapingData.CodePointCount > 1; + } + + GlyphShapingClass result = new(isMark, isBase, isLigature, markAttachmentType); + shapingData.CachedShapingClass = result; + shapingData.ShapingClassCacheKey = glyphId; + return result; + } + + /// + /// Determines whether the specified glyph is in the given mark filtering set. + /// + /// The font metrics. + /// The mark filtering set index. + /// The glyph identifier. + /// if the glyph is in the mark filtering set; otherwise, . + public static bool IsInMarkFilteringSet(FontMetrics fontMetrics, ushort markFilteringSet, ushort glyphId) + => fontMetrics.IsInMarkFilteringSet(markFilteringSet, glyphId); + + /// + /// Matches a sequence of elements against glyphs using an increment-based approach. + /// + /// The type of sequence elements to match. + /// The initial increment from the iterator's current position. + /// The array of elements to match. + /// The skipping glyph iterator. + /// The condition function to test each element against glyph data. + /// A span to store matched glyph indices, or default if not needed. + /// if all elements in the sequence were matched; otherwise, . + private static bool Match( + int increment, + T[] sequence, + SkippingGlyphIterator iterator, + Func condition, + Span matches) + { + int position = iterator.Index; + int offset = iterator.Increment(increment); + IGlyphShapingCollection collection = iterator.Collection; + + if (offset < 0) + { + return false; + } + + int i = 0; + while (i < sequence.Length && i < MaxContextLength && offset < collection.Count) + { + if (!condition(sequence[i], collection[offset])) + { + break; + } + + if (matches.Length == MaxContextLength) + { + matches[i] = iterator.Index; + } + + i++; + offset = iterator.Next(); + } + + iterator.Index = position; + return i == sequence.Length; + } + + /// + /// Matches a sequence of elements against glyphs using a directional (forward/backward) approach. + /// + /// The type of sequence elements to match. + /// The skipping glyph iterator. + /// The starting index in the collection. + /// The array of elements to match. + /// The direction to iterate (forward or backward). + /// The exclusive end index in the collection. + /// The condition function to test each element against glyph data. + /// A span to store matched glyph indices, or default if not needed. + /// if all elements in the sequence were matched; otherwise, . + private static bool Match( + SkippingGlyphIterator iterator, + int startIndex, + T[] sequence, + MatchDirection direction, + int endExclusive, + Func condition, + Span matches) + { + if (sequence.Length == 0) + { + return true; + } + + int saved = iterator.Index; + iterator.Index = startIndex; + + IGlyphShapingCollection collection = iterator.Collection; + int limit = Math.Min(endExclusive, collection.Count); + + for (int i = 0; i < sequence.Length && i < MaxContextLength; i++) + { + if (iterator.Index < 0 || iterator.Index >= limit) + { + iterator.Index = saved; + return false; + } + + GlyphShapingData data = collection[iterator.Index]; + if (!condition(sequence[i], data)) + { + iterator.Index = saved; + return false; + } + + if (matches.Length == MaxContextLength) + { + matches[i] = iterator.Index; + } + + if (i + 1 < sequence.Length) + { + iterator.Index = direction == MatchDirection.Forward + ? iterator.Next() + : iterator.Prev(); + } + } + + iterator.Index = saved; + return true; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/AttachPoint.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/AttachPoint.cs new file mode 100644 index 0000000..c215baa --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/AttachPoint.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// An AttachPoint table contains an array of contour point indices for the attachment points on a single glyph. + /// + /// + internal struct AttachPoint + { + /// + /// The array of contour point indices for this glyph's attachment points. + /// + public ushort[] PointIndices; + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/AttachmentListTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/AttachmentListTable.cs new file mode 100644 index 0000000..5cce4d3 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/AttachmentListTable.cs @@ -0,0 +1,70 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// The Attachment List table (AttachList) identifies all the attachment points defined in the GDEF table + /// and their associated glyphs so a client can quickly access coordinates for each glyph's attachment points. + /// + /// + internal sealed class AttachmentListTable + { + /// + /// Gets or sets the coverage table that defines which glyphs have attachment points. + /// + public CoverageTable? CoverageTable { get; internal set; } + + /// + /// Gets or sets the array of attachment point tables, one per covered glyph, in Coverage Index order. + /// + public AttachPoint[]? AttachPoints { get; internal set; } + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Offset from the beginning of the GDEF table to the AttachList table. + /// The . + public static AttachmentListTable Load(BigEndianBinaryReader reader, long offset) + { + // Attachment Point List Table + // Type | Name | Description + // ----------|--------------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | coverageOffset | Offset to Coverage table -from beginning of AttachList table. + // ----------|--------------------------------|-------------------------------------------------------------------------------------------------------- + // uint16 | glyphCount | Number of glyphs with attachment points. + // ----------|--------------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | attachPointOffsets[glyphCount] | Array of offsets to AttachPoint tables-from beginning of AttachList table-in Coverage Index order. + // ----------|--------------------------------|-------------------------------------------------------------------------------------------------------- + reader.Seek(offset, SeekOrigin.Begin); + + ushort coverageOffset = reader.ReadUInt16(); + ushort glyphCount = reader.ReadUInt16(); + + using Buffer attachPointOffsetsBuffer = new(glyphCount); + Span attachPointOffsets = attachPointOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(attachPointOffsets); + + AttachmentListTable attachmentListTable = new() + { + CoverageTable = CoverageTable.Load(reader, offset + coverageOffset), + AttachPoints = new AttachPoint[glyphCount] + }; + + for (int i = 0; i < glyphCount; ++i) + { + reader.Seek(offset + attachPointOffsets[i], SeekOrigin.Begin); + ushort pointCount = reader.ReadUInt16(); + attachmentListTable.AttachPoints[i] = new AttachPoint() + { + PointIndices = reader.ReadUInt16Array(pointCount) + }; + } + + return attachmentListTable; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/ChainedClassSequenceRuleSetTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/ChainedClassSequenceRuleSetTable.cs new file mode 100644 index 0000000..6b449af --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/ChainedClassSequenceRuleSetTable.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// A ChainedClassSequenceRuleSet table contains an array of ChainedClassSequenceRule tables that define + /// chained context rules for class-based glyph contexts. + /// + /// + internal sealed class ChainedClassSequenceRuleSetTable + { + /// + /// Initializes a new instance of the class. + /// + /// The array of chained class sequence rule tables. + private ChainedClassSequenceRuleSetTable(ChainedClassSequenceRuleTable[] subRules) => this.SubRules = subRules; + + /// + /// Gets the array of chained class sequence rule tables. + /// + public ChainedClassSequenceRuleTable[] SubRules { get; } + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Offset from the beginning of the ChainedClassSequenceRuleSet table. + /// The . + public static ChainedClassSequenceRuleSetTable Load(BigEndianBinaryReader reader, long offset) + { + // ClassSequenceRuleSet + // +----------+----------------------------------------+---------------------------------------+ + // | Type | Name | Description | + // +==========+========================================+=======================================+ + // | uint16 | classSeqRuleCount | Number of ClassSequenceRule tables | + // +----------+----------------------------------------+---------------------------------------+ + // | Offset16 | classSeqRuleOffsets[classSeqRuleCount] | Array of offsets to ClassSequenceRule | + // | | | tables, from beginning of | + // | | | ClassSequenceRuleSet table | + // +----------+----------------------------------------+---------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + ushort seqRuleCount = reader.ReadUInt16(); + + using Buffer seqRuleOffsetsBuffer = new(seqRuleCount); + Span seqRuleOffsets = seqRuleOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(seqRuleOffsets); + + var subRules = new ChainedClassSequenceRuleTable[seqRuleCount]; + for (int i = 0; i < subRules.Length; i++) + { + subRules[i] = ChainedClassSequenceRuleTable.Load(reader, offset + seqRuleOffsets[i]); + } + + return new ChainedClassSequenceRuleSetTable(subRules); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/ChainedClassSequenceRuleTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/ChainedClassSequenceRuleTable.cs new file mode 100644 index 0000000..43e80d9 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/ChainedClassSequenceRuleTable.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// A ChainedClassSequenceRule table describes a chained context rule using glyph class values + /// for backtrack, input, and lookahead sequences. + /// + /// + internal sealed class ChainedClassSequenceRuleTable + { + /// + /// Initializes a new instance of the class. + /// + /// The array of backtrack-sequence classes. + /// The array of input sequence classes, beginning with the second glyph position. + /// The array of lookahead-sequence classes. + /// The array of sequence lookup records. + private ChainedClassSequenceRuleTable( + ushort[] backtrackSequence, + ushort[] inputSequence, + ushort[] lookaheadSequence, + SequenceLookupRecord[] seqLookupRecords) + { + this.BacktrackSequence = backtrackSequence; + this.InputSequence = inputSequence; + this.LookaheadSequence = lookaheadSequence; + this.SequenceLookupRecords = seqLookupRecords; + } + + /// + /// Gets the array of backtrack-sequence classes. + /// + public ushort[] BacktrackSequence { get; } + + /// + /// Gets the array of input sequence classes, beginning with the second glyph position. + /// + public ushort[] InputSequence { get; } + + /// + /// Gets the array of lookahead-sequence classes. + /// + public ushort[] LookaheadSequence { get; } + + /// + /// Gets the array of sequence lookup records specifying actions to be applied. + /// + public SequenceLookupRecord[] SequenceLookupRecords { get; } + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Offset from the beginning of the ChainedClassSequenceRule table. + /// The . + public static ChainedClassSequenceRuleTable Load(BigEndianBinaryReader reader, long offset) + { + // ChainedClassSequenceRule + // +----------------------+----------------------------------------+--------------------------------------------+ + // | Type | Name | Description | + // +======================+========================================+============================================+ + // | uint16 | backtrackGlyphCount | Number of glyphs in the backtrack | + // | | | sequence | + // +----------------------+----------------------------------------+--------------------------------------------+ + // | uint16 | backtrackSequence[backtrackGlyphCount] | Array of backtrack-sequence classes | + // +----------------------+----------------------------------------+--------------------------------------------+ + // | uint16 | inputGlyphCount | Total number of glyphs in the input | + // | | | sequence | + // +----------------------+----------------------------------------+--------------------------------------------+ + // | uint16 | inputSequence[inputGlyphCount - 1] | Array of input sequence classes, beginning | + // | | | with the second glyph position | + // +----------------------+----------------------------------------+--------------------------------------------+ + // | uint16 | lookaheadGlyphCount | Number of glyphs in the lookahead | + // | | | sequence | + // +----------------------+----------------------------------------+--------------------------------------------+ + // | uint16 | lookaheadSequence[lookaheadGlyphCount] | Array of lookahead-sequence classes | + // +----------------------+----------------------------------------+--------------------------------------------+ + // | uint16 | seqLookupCount | Number of SequenceLookupRecords | + // +----------------------+----------------------------------------+--------------------------------------------+ + // | SequenceLookupRecord | seqLookupRecords[seqLookupCount] | Array of SequenceLookupRecords | + // +----------------------+----------------------------------------+--------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + + ushort backtrackGlyphCount = reader.ReadUInt16(); + ushort[] backtrackSequence = reader.ReadUInt16Array(backtrackGlyphCount); + + ushort inputGlyphCount = reader.ReadUInt16(); + ushort[] inputSequence = reader.ReadUInt16Array(inputGlyphCount - 1); + + ushort lookaheadGlyphCount = reader.ReadUInt16(); + ushort[] lookaheadSequence = reader.ReadUInt16Array(lookaheadGlyphCount); + + ushort seqLookupCount = reader.ReadUInt16(); + SequenceLookupRecord[] seqLookupRecords = SequenceLookupRecord.LoadArray(reader, seqLookupCount); + + return new ChainedClassSequenceRuleTable(backtrackSequence, inputSequence, lookaheadSequence, seqLookupRecords); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/ChainedSequenceRuleSetTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/ChainedSequenceRuleSetTable.cs new file mode 100644 index 0000000..b9ba2b8 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/ChainedSequenceRuleSetTable.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// A ChainedSequenceRuleSet table contains an array of ChainedSequenceRule tables that define + /// chained context rules for simple glyph contexts (glyph ID based). + /// + /// + internal sealed class ChainedSequenceRuleSetTable + { + /// + /// Initializes a new instance of the class. + /// + /// The array of chained sequence rule tables. + private ChainedSequenceRuleSetTable(ChainedSequenceRuleTable[] subRules) => this.SequenceRuleTables = subRules; + + /// + /// Gets the array of chained sequence rule tables. + /// + public ChainedSequenceRuleTable[] SequenceRuleTables { get; } + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Offset from the beginning of the ChainedSequenceRuleSet table. + /// The . + public static ChainedSequenceRuleSetTable Load(BigEndianBinaryReader reader, long offset) + { + // ChainedSequenceRuleSet + // +----------+--------------------------------------------+-----------------------------------------+ + // | Type | Name | Description | + // +==========+============================================+=========================================+ + // | uint16 | chainedSeqRuleCount | Number of ChainedSequenceRule tables | + // +----------+--------------------------------------------+-----------------------------------------+ + // | Offset16 | chainedSeqRuleOffsets[chainedSeqRuleCount] | Array of offsets to ChainedSequenceRule | + // | | | tables, from beginning of | + // | | | ChainedSequenceRuleSet table | + // +----------+--------------------------------------------+-----------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + ushort chainedSeqRuleCount = reader.ReadUInt16(); + + using Buffer chainedSeqRuleOffsetsBuffer = new(chainedSeqRuleCount); + Span chainedSeqRuleOffsets = chainedSeqRuleOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(chainedSeqRuleOffsets); + + var chainedSequenceRules = new ChainedSequenceRuleTable[chainedSeqRuleCount]; + for (int i = 0; i < chainedSequenceRules.Length; i++) + { + chainedSequenceRules[i] = ChainedSequenceRuleTable.Load(reader, offset + chainedSeqRuleOffsets[i]); + } + + return new ChainedSequenceRuleSetTable(chainedSequenceRules); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/ChainedSequenceRuleTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/ChainedSequenceRuleTable.cs new file mode 100644 index 0000000..eb69069 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/ChainedSequenceRuleTable.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// A ChainedSequenceRule table describes a chained context rule using glyph IDs + /// for backtrack, input, and lookahead sequences. + /// + /// + internal sealed class ChainedSequenceRuleTable + { + /// + /// Initializes a new instance of the class. + /// + /// The array of backtrack glyph IDs. + /// The array of input glyph IDs, beginning with the second glyph. + /// The array of lookahead glyph IDs. + /// The array of sequence lookup records. + private ChainedSequenceRuleTable( + ushort[] backtrackSequence, + ushort[] inputSequence, + ushort[] lookaheadSequence, + SequenceLookupRecord[] seqLookupRecords) + { + this.BacktrackSequence = backtrackSequence; + this.InputSequence = inputSequence; + this.LookaheadSequence = lookaheadSequence; + this.SequenceLookupRecords = seqLookupRecords; + } + + /// + /// Gets the array of backtrack glyph IDs. + /// + public ushort[] BacktrackSequence { get; } + + /// + /// Gets the array of input glyph IDs, beginning with the second glyph. + /// + public ushort[] InputSequence { get; } + + /// + /// Gets the array of lookahead glyph IDs. + /// + public ushort[] LookaheadSequence { get; } + + /// + /// Gets the sequence lookup records. + /// The seqLookupRecords array lists the sequence lookup records that specify actions to be taken on glyphs at various positions within the input sequence. + /// These do not have to be ordered in sequence position order; they are ordered according to the desired result. + /// All of the sequence lookup records are processed in order, and each applies to the results of the actions indicated by the preceding record. + /// + public SequenceLookupRecord[] SequenceLookupRecords { get; } + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Offset from the beginning of the ChainedSequenceRule table. + /// The . + public static ChainedSequenceRuleTable Load(BigEndianBinaryReader reader, long offset) + { + // ChainedSequenceRule + // +----------------------+----------------------------------------+--------------------------------------------+ + // | Type | Name | Description | + // +======================+========================================+============================================+ + // | uint16 | backtrackGlyphCount | Number of glyphs in the backtrack sequence | + // +----------------------+----------------------------------------+--------------------------------------------+ + // | uint16 | backtrackSequence[backtrackGlyphCount] | Array of backtrack glyph IDs | + // +----------------------+----------------------------------------+--------------------------------------------+ + // | uint16 | inputGlyphCount | Number of glyphs in the input sequence | + // +----------------------+----------------------------------------+--------------------------------------------+ + // | uint16 | inputSequence[inputGlyphCount - 1] | Array of input glyph IDs—start with | + // | | | second glyph | + // +----------------------+----------------------------------------+--------------------------------------------+ + // | uint16 | lookaheadGlyphCount | Number of glyphs in the lookahead sequence | + // +----------------------+----------------------------------------+--------------------------------------------+ + // | uint16 | lookaheadSequence[lookaheadGlyphCount] | Array of lookahead glyph IDs | + // +----------------------+----------------------------------------+--------------------------------------------+ + // | uint16 | seqLookupCount | Number of SequenceLookupRecords | + // +----------------------+----------------------------------------+--------------------------------------------+ + // | SequenceLookupRecord | seqLookupRecords[seqLookupCount] | Array of SequenceLookupRecords | + // +----------------------+----------------------------------------+--------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + ushort backtrackGlyphCount = reader.ReadUInt16(); + ushort[] backtrackSequence = reader.ReadUInt16Array(backtrackGlyphCount); + + ushort inputGlyphCount = reader.ReadUInt16(); + ushort[] inputSequence = reader.ReadUInt16Array(inputGlyphCount - 1); + + ushort lookaheadGlyphCount = reader.ReadUInt16(); + ushort[] lookaheadSequence = reader.ReadUInt16Array(lookaheadGlyphCount); + + ushort seqLookupCount = reader.ReadUInt16(); + SequenceLookupRecord[] seqLookupRecords = SequenceLookupRecord.LoadArray(reader, seqLookupCount); + + return new ChainedSequenceRuleTable(backtrackSequence, inputSequence, lookaheadSequence, seqLookupRecords); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/ClassDefinitionTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/ClassDefinitionTable.cs new file mode 100644 index 0000000..8773c70 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/ClassDefinitionTable.cs @@ -0,0 +1,216 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// In OpenType Layout, index values identify glyphs. For efficiency and ease of representation, a font developer + /// can group glyph indices to form glyph classes. Class assignments vary in meaning from one lookup subtable + /// to another. For example, in the GSUB and GPOS tables, classes are used to describe glyph contexts. + /// GDEF tables also use the idea of glyph classes. + /// + /// + internal abstract class ClassDefinitionTable + { + /// + /// Gets the class id for the given glyph id. + /// Any glyph not included in the range of covered glyph IDs automatically belongs to Class 0. + /// + /// The glyph identifier. + /// The class id. + public abstract int ClassIndexOf(ushort glyphId); + + /// + /// Tries to load a from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Offset from the beginning of the table. If 0, no table is loaded. + /// When this method returns, contains the loaded table if successful. + /// if the table was loaded; otherwise, . + public static bool TryLoad(BigEndianBinaryReader reader, long offset, [NotNullWhen(true)] out ClassDefinitionTable? table) + { + if (offset == 0) + { + table = null; + return false; + } + + reader.Seek(offset, SeekOrigin.Begin); + ushort classFormat = reader.ReadUInt16(); + table = classFormat switch + { + 1 => ClassDefinitionFormat1Table.Load(reader), + 2 => ClassDefinitionFormat2Table.Load(reader), + _ => null + }; + + return table is not null; + } + + /// + /// Loads a from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Offset from the beginning of the table. + /// The . + /// Thrown when the class format is invalid. + public static ClassDefinitionTable Load(BigEndianBinaryReader reader, long offset) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort classFormat = reader.ReadUInt16(); + return classFormat switch + { + 1 => ClassDefinitionFormat1Table.Load(reader), + 2 => ClassDefinitionFormat2Table.Load(reader), + _ => throw new InvalidFontFileException($"Invalid value for 'classFormat' {classFormat}. Should be '1' or '2'.") + }; + } + } + + /// + /// Class Definition Format 1: class assignment is defined by an array of class values + /// indexed by glyph ID minus a start glyph ID. + /// + internal sealed class ClassDefinitionFormat1Table : ClassDefinitionTable + { + private readonly ushort startGlyphId; + private readonly ushort[] classValueArray; + + /// + /// Initializes a new instance of the class. + /// + /// The first glyph ID of the class value array. + /// The array of class values, one per glyph ID. + private ClassDefinitionFormat1Table(ushort startGlyphId, ushort[] classValueArray) + { + this.startGlyphId = startGlyphId; + this.classValueArray = classValueArray; + } + + /// + /// Loads a from the binary reader. + /// The format identifier has already been read. + /// + /// The big endian binary reader. + /// The . + public static ClassDefinitionFormat1Table Load(BigEndianBinaryReader reader) + { + // +--------+-----------------------------+------------------------------------------+ + // | Type | Name | Description | + // +========+=============================+==========================================+ + // | uint16 | classFormat | Format identifier — format = 1 | + // +--------+-----------------------------+------------------------------------------+ + // | uint16 | startGlyphID | First glyph ID of the classValueArray | + // +--------+-----------------------------+------------------------------------------+ + // | uint16 | glyphCount | Size of the classValueArray | + // +--------+-----------------------------+------------------------------------------+ + // | uint16 | classValueArray[glyphCount] | Array of Class Values — one per glyph ID | + // +--------+-----------------------------+------------------------------------------+ + ushort startGlyphId = reader.ReadUInt16(); + ushort glyphCount = reader.ReadUInt16(); + ushort[] classValueArray = reader.ReadUInt16Array(glyphCount); + return new ClassDefinitionFormat1Table(startGlyphId, classValueArray); + } + + /// + public override int ClassIndexOf(ushort glyphId) + { + int i = glyphId - this.startGlyphId; + if (i >= 0 && i < this.classValueArray.Length) + { + return this.classValueArray[i]; + } + + // Any glyph not included in the range of covered glyph IDs automatically belongs to Class 0. + return 0; + } + } + + /// + /// Class Definition Format 2: class assignment is defined by an array of ranges, + /// each mapping a range of glyph IDs to a class value. + /// + internal sealed class ClassDefinitionFormat2Table : ClassDefinitionTable + { + private readonly ClassRangeRecord[] records; + + /// + /// Initializes a new instance of the class. + /// + /// The array of class range records. + private ClassDefinitionFormat2Table(ClassRangeRecord[] records) + => this.records = records; + + /// + /// Loads a from the binary reader. + /// The format identifier has already been read. + /// + /// The big endian binary reader. + /// The . + public static ClassDefinitionFormat2Table Load(BigEndianBinaryReader reader) + { + // +------------------+------------------------------------+-----------------------------------------+ + // | Type | Name | Description | + // +==================+====================================+=========================================+ + // | uint16 | classFormat | Format identifier — format = 2 | + // +------------------+------------------------------------+-----------------------------------------+ + // | uint16 | classRangeCount | Number of ClassRangeRecords | + // +------------------+------------------------------------+-----------------------------------------+ + // | ClassRangeRecord | classRangeRecords[classRangeCount] | Array of ClassRangeRecords — ordered by | + // | | | startGlyphID | + // +------------------+------------------------------------+-----------------------------------------+ + ushort classRangeCount = reader.ReadUInt16(); + ClassRangeRecord[] records = new ClassRangeRecord[classRangeCount]; + for (int i = 0; i < records.Length; ++i) + { + // +--------+--------------+------------------------------------+ + // | Type | Name | Description | + // +========+==============+====================================+ + // | uint16 | startGlyphID | First glyph ID in the range | + // +--------+--------------+------------------------------------+ + // | uint16 | endGlyphID | Last glyph ID in the range | + // +--------+--------------+------------------------------------+ + // | uint16 | class | Applied to all glyphs in the range | + // +--------+--------------+------------------------------------+ + records[i] = new ClassRangeRecord( + reader.ReadUInt16(), + reader.ReadUInt16(), + reader.ReadUInt16()); + } + + return new ClassDefinitionFormat2Table(records); + } + + /// + public override int ClassIndexOf(ushort glyphId) + { + // Records are ordered by StartGlyphId, so use binary search to find the + // candidate range whose StartGlyphId is <= glyphId. + ClassRangeRecord[] records = this.records; + int lo = 0; + int hi = records.Length - 1; + while (lo <= hi) + { + int mid = (int)(((uint)lo + (uint)hi) >> 1); + ClassRangeRecord rec = records[mid]; + if (glyphId < rec.StartGlyphId) + { + hi = mid - 1; + } + else if (glyphId > rec.EndGlyphId) + { + lo = mid + 1; + } + else + { + return rec.Class; + } + } + + // Any glyph not included in the range of covered glyph IDs automatically belongs to Class 0. + return 0; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/ClassRangeRecord.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/ClassRangeRecord.cs new file mode 100644 index 0000000..a0ce735 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/ClassRangeRecord.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// A ClassRangeRecord defines a range of glyph IDs that belong to a specific class. + /// Used in ClassDefinitionTable Format 2. + /// + /// + [DebuggerDisplay("StartGlyphId: {StartGlyphId}, EndGlyphId: {EndGlyphId}, Class: {Class}")] + internal readonly struct ClassRangeRecord + { + /// + /// Initializes a new instance of the struct. + /// + /// The first glyph ID in the range. + /// The last glyph ID in the range. + /// The class value applied to all glyphs in the range. + public ClassRangeRecord(ushort startGlyphId, ushort endGlyphId, ushort glyphClass) + { + this.StartGlyphId = startGlyphId; + this.EndGlyphId = endGlyphId; + this.Class = glyphClass; + } + + /// + /// Gets the first glyph ID in the range. + /// + public ushort StartGlyphId { get; } + + /// + /// Gets the last glyph ID in the range. + /// + public ushort EndGlyphId { get; } + + /// + /// Gets the class value applied to all glyphs in the range. + /// + public ushort Class { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/ClassSequenceRuleSetTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/ClassSequenceRuleSetTable.cs new file mode 100644 index 0000000..a0bad9b --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/ClassSequenceRuleSetTable.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// A ClassSequenceRuleSet table contains an array of ClassSequenceRule tables that define + /// context rules for class-based glyph contexts in Sequence Context Format 2. + /// + /// + internal sealed class ClassSequenceRuleSetTable + { + /// + /// Initializes a new instance of the class. + /// + /// The array of class sequence rule tables. + private ClassSequenceRuleSetTable(ClassSequenceRuleTable[] sequenceRuleTables) + => this.SequenceRuleTables = sequenceRuleTables; + + /// + /// Gets the array of class sequence rule tables. + /// + public ClassSequenceRuleTable[] SequenceRuleTables { get; } + + /// + /// Loads the class sequence rule set table. + /// + /// The big endian binary reader. + /// Offset from beginning of the ClassSequenceRuleSet table. + /// A class sequence rule set table. + public static ClassSequenceRuleSetTable Load(BigEndianBinaryReader reader, long offset) + { + // ClassSequenceRuleSet + // +----------+----------------------------------------+---------------------------------------+ + // | Type | Name | Description | + // +==========+========================================+=======================================+ + // | uint16 | classSeqRuleCount | Number of ClassSequenceRule tables. | + // +----------+----------------------------------------+---------------------------------------+ + // | Offset16 | classSeqRuleOffsets[classSeqRuleCount] | Array of offsets to ClassSequenceRule | + // | | | tables, from beginning of | + // | | | ClassSequenceRuleSet table. | + // +----------+----------------------------------------+---------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + ushort seqRuleCount = reader.ReadUInt16(); + + using Buffer seqRuleOffsetsBuffer = new(seqRuleCount); + Span seqRuleOffsets = seqRuleOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(seqRuleOffsets); + + var subRules = new ClassSequenceRuleTable[seqRuleCount]; + for (int i = 0; i < subRules.Length; i++) + { + subRules[i] = ClassSequenceRuleTable.Load(reader, offset + seqRuleOffsets[i]); + } + + return new ClassSequenceRuleSetTable(subRules); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/ClassSequenceRuleTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/ClassSequenceRuleTable.cs new file mode 100644 index 0000000..2e40942 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/ClassSequenceRuleTable.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// A ClassSequenceRule table describes a context rule using glyph class values. + /// + /// + internal sealed class ClassSequenceRuleTable + { + /// + /// Initializes a new instance of the class. + /// + /// The array of input sequence classes, beginning with the second glyph position. + /// The array of sequence lookup records. + private ClassSequenceRuleTable(ushort[] inputSequence, SequenceLookupRecord[] seqLookupRecords) + { + this.InputSequence = inputSequence; + this.SequenceLookupRecords = seqLookupRecords; + } + + /// + /// Gets the array of input sequence classes, beginning with the second glyph position. + /// + public ushort[] InputSequence { get; } + + /// + /// Gets the array of sequence lookup records specifying actions to be applied. + /// + public SequenceLookupRecord[] SequenceLookupRecords { get; } + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Offset from the beginning of the ClassSequenceRule table. + /// The . + public static ClassSequenceRuleTable Load(BigEndianBinaryReader reader, long offset) + { + // ClassSequenceRule + // +----------------------+----------------------------------+------------------------------------------+ + // | Type | Name | Description | + // +======================+==================================+==========================================+ + // | uint16 | glyphCount | Number of glyphs to be matched | + // +----------------------+----------------------------------+------------------------------------------+ + // | uint16 | seqLookupCount | Number of SequenceLookupRecords | + // +----------------------+----------------------------------+------------------------------------------+ + // | uint16 | inputSequence[glyphCount - 1] | Sequence of classes to be matched to the | + // | | | input glyph sequence, beginning with the | + // | | | second glyph position | + // +----------------------+----------------------------------+------------------------------------------+ + // | SequenceLookupRecord | seqLookupRecords[seqLookupCount] | Array of SequenceLookupRecords | + // +----------------------+----------------------------------+------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + ushort glyphCount = reader.ReadUInt16(); + ushort seqLookupCount = reader.ReadUInt16(); + ushort[] inputSequence = reader.ReadUInt16Array(glyphCount - 1); + SequenceLookupRecord[] seqLookupRecords = SequenceLookupRecord.LoadArray(reader, seqLookupCount); + + return new ClassSequenceRuleTable(inputSequence, seqLookupRecords); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/CoverageRangeRecord.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/CoverageRangeRecord.cs new file mode 100644 index 0000000..3fbd3ce --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/CoverageRangeRecord.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// A CoverageRangeRecord defines a range of glyph IDs and its starting coverage index. + /// Used in CoverageTable Format 2. + /// + /// + [DebuggerDisplay("StartGlyphId: {StartGlyphId}, EndGlyphId: {EndGlyphId}, Index: {Index}")] + internal readonly struct CoverageRangeRecord + { + /// + /// Initializes a new instance of the struct. + /// + /// The first glyph ID in the range. + /// The last glyph ID in the range. + /// The coverage index of the first glyph ID in the range. + public CoverageRangeRecord(ushort startGlyphId, ushort endGlyphId, ushort startCoverageIndex) + { + this.StartGlyphId = startGlyphId; + this.EndGlyphId = endGlyphId; + this.Index = startCoverageIndex; + } + + /// + /// Gets the first glyph ID in the range. + /// + public ushort StartGlyphId { get; } + + /// + /// Gets the last glyph ID in the range. + /// + public ushort EndGlyphId { get; } + + /// + /// Gets the coverage index of the first glyph ID in the range. + /// + public ushort Index { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/CoverageTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/CoverageTable.cs new file mode 100644 index 0000000..3ef1b03 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/CoverageTable.cs @@ -0,0 +1,215 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; +using static SixLabors.Fonts.Tables.AdvancedTypographic.CoverageFormat2Table; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// Each subtable (except an Extension LookupType subtable) in a lookup references a Coverage table (Coverage), + /// which specifies all the glyphs affected by a substitution or positioning operation described in the subtable. + /// The GSUB, GPOS, and GDEF tables rely on this notion of coverage. + /// If a glyph does not appear in a Coverage table, the client can skip that subtable and move + /// immediately to the next subtable. + /// + /// + internal abstract class CoverageTable + { + /// + /// Gets the coverage index for the specified glyph, or -1 if the glyph is not covered. + /// + /// The glyph identifier. + /// The zero-based coverage index, or -1 if not found. + public abstract int CoverageIndexOf(ushort glyphId); + + /// + /// Loads a from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Offset from the beginning of the table. + /// The . + public static CoverageTable Load(BigEndianBinaryReader reader, long offset) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort coverageFormat = reader.ReadUInt16(); + return coverageFormat switch + { + 1 => CoverageFormat1Table.Load(reader), + 2 => CoverageFormat2Table.Load(reader), + + // Harfbuzz (Coverage.hh) treats this as an empty table and does not throw. + // SofiaSans Condensed can trigger this. See https://github.com/SixLabors/Fonts/issues/470 + _ => EmptyCoverageTable.Instance + }; + } + + /// + /// Loads an array of values from the binary reader. + /// + /// The big endian binary reader. + /// The base offset from which coverage offsets are relative. + /// The array of offsets to individual coverage tables. + /// The array of . + public static CoverageTable[] LoadArray(BigEndianBinaryReader reader, long offset, ReadOnlySpan coverageOffsets) + { + CoverageTable[] tables = new CoverageTable[coverageOffsets.Length]; + for (int i = 0; i < tables.Length; i++) + { + tables[i] = Load(reader, offset + coverageOffsets[i]); + } + + return tables; + } + } + + /// + /// Coverage Format 1: individual glyph indices listed in numerical order. + /// + internal sealed class CoverageFormat1Table : CoverageTable + { + private readonly ushort[] glyphArray; + + /// + /// Initializes a new instance of the class. + /// + /// The array of glyph IDs in numerical order. + private CoverageFormat1Table(ushort[] glyphArray) + => this.glyphArray = glyphArray; + + /// + public override int CoverageIndexOf(ushort glyphId) + { + int n = Array.BinarySearch(this.glyphArray, glyphId); + return n < 0 ? -1 : n; + } + + /// + /// Loads a from the binary reader. + /// The format identifier has already been read. + /// + /// The big endian binary reader. + /// The . + public static CoverageFormat1Table Load(BigEndianBinaryReader reader) + { + // +--------+------------------------+-----------------------------------------+ + // | Type | Name | Description | + // +========+========================+=========================================+ + // | uint16 | coverageFormat | Format identifier — format = 1 | + // +--------+------------------------+-----------------------------------------+ + // | uint16 | glyphCount | Number of glyphs in the glyph array | + // +--------+------------------------+-----------------------------------------+ + // | uint16 | glyphArray[glyphCount] | Array of glyph IDs — in numerical order | + // +--------+------------------------+-----------------------------------------+ + ushort glyphCount = reader.ReadUInt16(); + ushort[] glyphArray = reader.ReadUInt16Array(glyphCount); + + return new CoverageFormat1Table(glyphArray); + } + } + + /// + /// Coverage Format 2: ranges of consecutive glyph IDs, ordered by startGlyphID. + /// + internal sealed class CoverageFormat2Table : CoverageTable + { + private readonly CoverageRangeRecord[] records; + + /// + /// Initializes a new instance of the class. + /// + /// The array of coverage range records. + private CoverageFormat2Table(CoverageRangeRecord[] records) + => this.records = records; + + /// + public override int CoverageIndexOf(ushort glyphId) + { + // Records are ordered by StartGlyphId, so use binary search to find the + // candidate range whose StartGlyphId is <= glyphId. + CoverageRangeRecord[] records = this.records; + int lo = 0; + int hi = records.Length - 1; + while (lo <= hi) + { + int mid = (int)(((uint)lo + (uint)hi) >> 1); + CoverageRangeRecord rec = records[mid]; + if (glyphId < rec.StartGlyphId) + { + hi = mid - 1; + } + else if (glyphId > rec.EndGlyphId) + { + lo = mid + 1; + } + else + { + return rec.Index + glyphId - rec.StartGlyphId; + } + } + + return -1; + } + + /// + /// Loads a from the binary reader. + /// The format identifier has already been read. + /// + /// The big endian binary reader. + /// The . + public static CoverageFormat2Table Load(BigEndianBinaryReader reader) + { + // +-------------+--------------------------+--------------------------------------------------+ + // | Type | Name | Description | + // +=============+==========================+==================================================+ + // | uint16 | coverageFormat | Format identifier — format = 2 | + // +-------------+--------------------------+--------------------------------------------------+ + // | uint16 | rangeCount | Number of RangeRecords | + // +-------------+--------------------------+--------------------------------------------------+ + // | RangeRecord | rangeRecords[rangeCount] | Array of glyph ranges — ordered by startGlyphID. | + // +-------------+--------------------------+--------------------------------------------------+ + ushort rangeCount = reader.ReadUInt16(); + CoverageRangeRecord[] records = new CoverageRangeRecord[rangeCount]; + + for (int i = 0; i < records.Length; i++) + { + // +--------+--------------------+-------------------------------------------+ + // | Type | Name | Description | + // +========+====================+===========================================+ + // | uint16 | startGlyphID | First glyph ID in the range | + // +--------+--------------------+-------------------------------------------+ + // | uint16 | endGlyphID | Last glyph ID in the range | + // +--------+--------------------+-------------------------------------------+ + // | uint16 | startCoverageIndex | Coverage Index of first glyph ID in range | + // +--------+--------------------+-------------------------------------------+ + records[i] = new CoverageRangeRecord( + reader.ReadUInt16(), + reader.ReadUInt16(), + reader.ReadUInt16()); + } + + return new CoverageFormat2Table(records); + } + + /// + /// An empty coverage table that never matches any glyph. Used as a fallback for invalid coverage formats. + /// + internal sealed class EmptyCoverageTable : CoverageTable + { + /// + /// Initializes a new instance of the class. + /// + private EmptyCoverageTable() + { + } + + /// + /// Gets the singleton instance of the empty coverage table. + /// + public static EmptyCoverageTable Instance { get; } = new(); + + /// + public override int CoverageIndexOf(ushort glyphId) => -1; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/FeatureListTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/FeatureListTable.cs new file mode 100644 index 0000000..2a0430e --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/FeatureListTable.cs @@ -0,0 +1,163 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// Features provide information about how to use the glyphs in a font to render a script or language. + /// For example, an Arabic font might have a feature for substituting initial glyph forms, and a Kanji font + /// might have a feature for positioning glyphs vertically. All OpenType Layout features define data for + /// glyph substitution, glyph positioning, or both. + /// + /// + /// + internal class FeatureListTable + { + /// + /// Initializes a new instance of the class. + /// + /// The array of feature tables. + private FeatureListTable(FeatureTable[] featureTables) + => this.FeatureTables = featureTables; + + /// + /// Gets the array of feature tables. + /// + public FeatureTable[] FeatureTables { get; } + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Offset from the beginning of the GPOS or GSUB table to the FeatureList table. + /// The . + public static FeatureListTable Load(BigEndianBinaryReader reader, long offset) + { + // FeatureList + // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +===============+==============================+=================================================================================================================+ + // | uint16 | featureCount | Number of FeatureRecords in this table | + // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+ + // | FeatureRecord | featureRecords[featureCount] | Array of FeatureRecords — zero-based (first feature has FeatureIndex = 0), listed alphabetically by feature tag | + // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + + ushort featureCount = reader.ReadUInt16(); + var featureRecords = new FeatureRecord[featureCount]; + for (int i = 0; i < featureRecords.Length; i++) + { + // FeatureRecord + // +----------+---------------+--------------------------------------------------------+ + // | Type | Name | Description | + // +==========+===============+========================================================+ + // | Tag | featureTag | 4-byte feature identification tag | + // +----------+---------------+--------------------------------------------------------+ + // | Offset16 | featureOffset | Offset to Feature table, from beginning of FeatureList | + // +----------+---------------+--------------------------------------------------------+ + uint featureTag = reader.ReadUInt32(); + ushort featureOffset = reader.ReadOffset16(); + featureRecords[i] = new FeatureRecord(featureTag, featureOffset); + } + + // Load the other table features. + // We do this last to avoid excessive seeking. + var featureTables = new FeatureTable[featureCount]; + for (int i = 0; i < featureTables.Length; i++) + { + FeatureRecord featureRecord = featureRecords[i]; + featureTables[i] = FeatureTable.Load(featureRecord.FeatureTag, reader, offset + featureRecord.FeatureOffset); + } + + return new FeatureListTable(featureTables); + } + + /// + /// A FeatureRecord contains a feature tag and its offset to the Feature table. + /// + [DebuggerDisplay("FeatureTag: {FeatureTag}, Offset: {FeatureOffset}")] + private readonly struct FeatureRecord + { + /// + /// Initializes a new instance of the struct. + /// + /// The 4-byte feature identification tag. + /// The offset to the Feature table from the beginning of the FeatureList. + public FeatureRecord(uint featureTag, ushort featureOffset) + { + this.FeatureTag = new Tag(featureTag); + this.FeatureOffset = featureOffset; + } + + /// + /// Gets the 4-byte feature identification tag. + /// + public Tag FeatureTag { get; } + + /// + /// Gets the offset to the Feature table from the beginning of the FeatureList. + /// + public ushort FeatureOffset { get; } + } + } + + /// + /// A Feature table defines a feature with a set of lookup list indices that implement the feature. + /// + /// + [DebuggerDisplay("Tag: {FeatureTag}")] + internal sealed class FeatureTable + { + /// + /// Initializes a new instance of the class. + /// + /// The feature identification tag. + /// The array of indices into the LookupList. + private FeatureTable(Tag featureTag, ushort[] lookupListIndices) + { + this.FeatureTag = featureTag; + this.LookupListIndices = lookupListIndices; + } + + /// + /// Gets the feature identification tag. + /// + public Tag FeatureTag { get; } + + /// + /// Gets the array of indices into the LookupList for this feature. + /// + public ushort[] LookupListIndices { get; } + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The feature identification tag. + /// The big endian binary reader. + /// Offset from the beginning of the Feature table. + /// The . + public static FeatureTable Load(Tag featureTag, BigEndianBinaryReader reader, long offset) + { + // FeatureListTable + // +----------+-------------------------------------+--------------------------------------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+=====================================+==============================================================================================================+ + // | Offset16 | featureParamsOffset | Offset from start of Feature table to FeatureParams table, if defined for the feature and present, else NULL | + // +----------+-------------------------------------+--------------------------------------------------------------------------------------------------------------+ + // | uint16 | lookupIndexCount | Number of LookupList indices for this feature | + // +----------+-------------------------------------+--------------------------------------------------------------------------------------------------------------+ + // | uint16 | lookupListIndices[lookupIndexCount] | Array of indices into the LookupList — zero-based (first lookup is LookupListIndex = 0) | + // +----------+-------------------------------------+--------------------------------------------------------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + + // TODO: How do we use this? + ushort featureParamsOffset = reader.ReadOffset16(); + ushort lookupIndexCount = reader.ReadUInt16(); + + ushort[] lookupListIndices = reader.ReadUInt16Array(lookupIndexCount); + return new FeatureTable(featureTag, lookupListIndices); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/FeatureVariationsTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/FeatureVariationsTable.cs new file mode 100644 index 0000000..b37dd1b --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/FeatureVariationsTable.cs @@ -0,0 +1,387 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// The FeatureVariations table is used in variable fonts to provide alternate sets of + /// feature table lookups for different regions of the variation space. + /// Shared by both GPOS and GSUB tables (version 1.1). + /// + /// + internal sealed class FeatureVariationsTable + { + /// + /// Initializes a new instance of the class. + /// + /// The array of feature variation records. + private FeatureVariationsTable(FeatureVariationRecord[] records) + => this.Records = records; + + /// + /// Gets the array of feature variation records. + /// + public FeatureVariationRecord[] Records { get; } + + /// + /// Loads the FeatureVariations table. + /// + /// The big endian binary reader. + /// Absolute offset to the beginning of the FeatureVariations table. + /// The FeatureListTable, used to resolve feature tags for substitutions. + /// The FeatureVariationsTable, or null if the offset is 0. + public static FeatureVariationsTable? Load(BigEndianBinaryReader reader, long offset, FeatureListTable featureList) + { + if (offset == 0) + { + return null; + } + + // FeatureVariations table + // +----------+------------------------------------------------------+---------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+======================================================+===============================================================+ + // | uint16 | majorVersion | Major version — set to 1 | + // +----------+------------------------------------------------------+---------------------------------------------------------------+ + // | uint16 | minorVersion | Minor version — set to 0 | + // +----------+------------------------------------------------------+---------------------------------------------------------------+ + // | uint32 | featureVariationRecordCount | Number of FeatureVariationRecords | + // +----------+------------------------------------------------------+---------------------------------------------------------------+ + // | FeatureVariationRecord | featureVariationRecords[count] | Array of FeatureVariationRecords | + // +----------+------------------------------------------------------+---------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + + ushort majorVersion = reader.ReadUInt16(); + ushort minorVersion = reader.ReadUInt16(); + uint recordCount = reader.ReadUInt32(); + + // Read all record offsets first, then load data to avoid excessive seeking. + int count = (int)recordCount; + using Buffer conditionSetOffsetsBuffer = new(count); + using Buffer substitutionOffsetsBuffer = new(count); + Span conditionSetOffsets = conditionSetOffsetsBuffer.GetSpan(); + Span substitutionOffsets = substitutionOffsetsBuffer.GetSpan(); + for (int i = 0; i < count; i++) + { + conditionSetOffsets[i] = reader.ReadOffset32(); + substitutionOffsets[i] = reader.ReadOffset32(); + } + + FeatureVariationRecord[] records = new FeatureVariationRecord[count]; + for (int i = 0; i < count; i++) + { + ConditionSetTable conditionSet = ConditionSetTable.Load(reader, offset + conditionSetOffsets[i]); + FeatureTableSubstitutionRecord[] substitutions = LoadFeatureTableSubstitution(reader, offset + substitutionOffsets[i], featureList); + records[i] = new FeatureVariationRecord(conditionSet, substitutions); + } + + return new FeatureVariationsTable(records); + } + + /// + /// Finds the first matching whose conditions are satisfied + /// by the given normalized coordinates, and returns its feature substitutions. + /// Returns null if no record matches or no variation coordinates are available. + /// + /// The normalized variation coordinates. + /// The matching substitution records, or null. + public FeatureTableSubstitutionRecord[]? FindMatchingSubstitutions(ReadOnlySpan normalizedCoords) + { + if (normalizedCoords.IsEmpty) + { + return null; + } + + for (int i = 0; i < this.Records.Length; i++) + { + if (this.Records[i].ConditionSet.Evaluate(normalizedCoords)) + { + return this.Records[i].Substitutions; + } + } + + return null; + } + + /// + /// Loads the FeatureTableSubstitution records from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Absolute offset to the FeatureTableSubstitution table. + /// The FeatureListTable, used to resolve feature tags for substitutions. + /// The array of feature table substitution records. + private static FeatureTableSubstitutionRecord[] LoadFeatureTableSubstitution( + BigEndianBinaryReader reader, + long offset, + FeatureListTable featureList) + { + // FeatureTableSubstitution table + // +----------+------------------------------------------------------+---------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+======================================================+===============================================================+ + // | uint16 | majorVersion | Major version — set to 1 | + // +----------+------------------------------------------------------+---------------------------------------------------------------+ + // | uint16 | minorVersion | Minor version — set to 0 | + // +----------+------------------------------------------------------+---------------------------------------------------------------+ + // | uint16 | substitutionCount | Number of FeatureTableSubstitutionRecords | + // +----------+------------------------------------------------------+---------------------------------------------------------------+ + // | FeatureTableSubstitutionRecord | substitutions[count] | Array of records | + // +----------+------------------------------------------------------+---------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + + ushort majorVersion = reader.ReadUInt16(); + ushort minorVersion = reader.ReadUInt16(); + ushort substitutionCount = reader.ReadUInt16(); + + // Read record headers (featureIndex + offset pairs). + using Buffer featureIndicesBuffer = new(substitutionCount); + using Buffer featureTableOffsetsBuffer = new(substitutionCount); + Span featureIndices = featureIndicesBuffer.GetSpan(); + Span featureTableOffsets = featureTableOffsetsBuffer.GetSpan(); + for (int i = 0; i < substitutionCount; i++) + { + featureIndices[i] = reader.ReadUInt16(); + featureTableOffsets[i] = reader.ReadOffset32(); + } + + // Load each alternate Feature table. + FeatureTableSubstitutionRecord[] records = new FeatureTableSubstitutionRecord[substitutionCount]; + for (int i = 0; i < substitutionCount; i++) + { + ushort featureIndex = featureIndices[i]; + + // Resolve the original feature tag from the FeatureList so the substitute + // carries the same tag. + Tag featureTag = featureIndex < featureList.FeatureTables.Length + ? featureList.FeatureTables[featureIndex].FeatureTag + : default; + + FeatureTable alternateFeatureTable = FeatureTable.Load(featureTag, reader, offset + featureTableOffsets[i]); + records[i] = new FeatureTableSubstitutionRecord(featureIndex, alternateFeatureTable); + } + + return records; + } + } + + /// + /// A set of conditions that must all be true for a FeatureVariationRecord to match. + /// + internal sealed class ConditionSetTable + { + /// + /// Initializes a new instance of the class. + /// + /// The array of condition tables. + private ConditionSetTable(ConditionTable[] conditions) + => this.Conditions = conditions; + + /// + /// Gets the array of condition tables that must all be satisfied. + /// + public ConditionTable[] Conditions { get; } + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Absolute offset to the beginning of the ConditionSet table. + /// The . + public static ConditionSetTable Load(BigEndianBinaryReader reader, long offset) + { + // ConditionSet table + // +----------+----------------------------+------------------------------------------+ + // | Type | Name | Description | + // +==========+============================+==========================================+ + // | uint16 | conditionCount | Number of conditions | + // +----------+----------------------------+------------------------------------------+ + // | Offset32 | conditionOffsets[count] | Offsets to Condition tables, from | + // | | | beginning of ConditionSet table | + // +----------+----------------------------+------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + + ushort conditionCount = reader.ReadUInt16(); + using Buffer conditionOffsetsBuffer = new(conditionCount); + Span conditionOffsets = conditionOffsetsBuffer.GetSpan(); + for (int i = 0; i < conditionCount; i++) + { + conditionOffsets[i] = reader.ReadOffset32(); + } + + ConditionTable[] conditions = new ConditionTable[conditionCount]; + for (int i = 0; i < conditionCount; i++) + { + conditions[i] = ConditionTable.Load(reader, offset + conditionOffsets[i]); + } + + return new ConditionSetTable(conditions); + } + + /// + /// Evaluates whether all conditions in this set are satisfied by the given normalized coordinates. + /// + /// The normalized variation coordinates. + /// True if all conditions match. + public bool Evaluate(ReadOnlySpan normalizedCoords) + { + for (int i = 0; i < this.Conditions.Length; i++) + { + if (!this.Conditions[i].Evaluate(normalizedCoords)) + { + return false; + } + } + + return true; + } + } + +#pragma warning disable SA1201 // Elements should appear in the correct order + + /// + /// A single record in the FeatureVariations table, pairing a condition set with + /// a set of feature table substitutions. + /// + internal readonly struct FeatureVariationRecord + { + /// + /// Initializes a new instance of the struct. + /// + /// The condition set that must be satisfied. + /// The feature table substitutions to apply when conditions are met. + public FeatureVariationRecord(ConditionSetTable conditionSet, FeatureTableSubstitutionRecord[] substitutions) + { + this.ConditionSet = conditionSet; + this.Substitutions = substitutions; + } + + /// + /// Gets the condition set that must be satisfied for this record to apply. + /// + public ConditionSetTable ConditionSet { get; } + + /// + /// Gets the array of feature table substitution records to apply when conditions are met. + /// + public FeatureTableSubstitutionRecord[] Substitutions { get; } + } + + /// + /// A substitution record that maps a feature index to an alternate Feature table. + /// + internal readonly struct FeatureTableSubstitutionRecord + { + /// + /// Initializes a new instance of the struct. + /// + /// The index into the FeatureList of the feature being substituted. + /// The alternate Feature table to use. + public FeatureTableSubstitutionRecord(ushort featureIndex, FeatureTable alternateFeatureTable) + { + this.FeatureIndex = featureIndex; + this.AlternateFeatureTable = alternateFeatureTable; + } + + /// + /// Gets the index into the FeatureList of the feature being substituted. + /// + public ushort FeatureIndex { get; } + + /// + /// Gets the alternate Feature table to use in place of the original. + /// + public FeatureTable AlternateFeatureTable { get; } + } + + /// + /// A condition that checks whether a normalized coordinate for a specific axis + /// falls within a given range. + /// + internal readonly struct ConditionTable + { + /// + /// Initializes a new instance of the struct. + /// + /// The index of the variation axis. + /// The minimum normalized coordinate value. + /// The maximum normalized coordinate value. + public ConditionTable(ushort axisIndex, float filterRangeMinValue, float filterRangeMaxValue) + { + this.AxisIndex = axisIndex; + this.FilterRangeMinValue = filterRangeMinValue; + this.FilterRangeMaxValue = filterRangeMaxValue; + } + + /// + /// Gets the index of the variation axis (into fvar axes array). + /// + public ushort AxisIndex { get; } + + /// + /// Gets the minimum normalized coordinate value for the condition to be true. + /// + public float FilterRangeMinValue { get; } + + /// + /// Gets the maximum normalized coordinate value for the condition to be true. + /// + public float FilterRangeMaxValue { get; } + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Absolute offset to the beginning of the Condition table. + /// The . + public static ConditionTable Load(BigEndianBinaryReader reader, long offset) + { + // Condition table, Format 1 (ConditionAxisRange) + // +----------+----------------------------+------------------------------------------+ + // | Type | Name | Description | + // +==========+============================+==========================================+ + // | uint16 | format | Format = 1 | + // +----------+----------------------------+------------------------------------------+ + // | uint16 | axisIndex | Index of variation axis | + // +----------+----------------------------+------------------------------------------+ + // | F2DOT14 | filterRangeMinValue | Minimum normalized coordinate value | + // +----------+----------------------------+------------------------------------------+ + // | F2DOT14 | filterRangeMaxValue | Maximum normalized coordinate value | + // +----------+----------------------------+------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + + ushort format = reader.ReadUInt16(); + + // Only Format 1 is defined. + if (format != 1) + { + return default; + } + + ushort axisIndex = reader.ReadUInt16(); + float filterRangeMinValue = reader.ReadF2Dot14(); + float filterRangeMaxValue = reader.ReadF2Dot14(); + + return new ConditionTable(axisIndex, filterRangeMinValue, filterRangeMaxValue); + } + + /// + /// Evaluates whether the given normalized coordinates satisfy this condition. + /// + /// The normalized variation coordinates. + /// True if the coordinate for this axis is within the filter range. + public bool Evaluate(ReadOnlySpan normalizedCoords) + { + if (this.AxisIndex >= normalizedCoords.Length) + { + return false; + } + + float coord = normalizedCoords[this.AxisIndex]; + return coord >= this.FilterRangeMinValue && coord <= this.FilterRangeMaxValue; + } + } + +#pragma warning restore SA1201 +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs new file mode 100644 index 0000000..83ead53 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs @@ -0,0 +1,330 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Numerics; +using SixLabors.Fonts.Tables.TrueType; +using SixLabors.Fonts.Tables.TrueType.Glyphs; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// Base class for GPOS Anchor tables that define an attachment point using X and Y coordinates. + /// Anchor tables are used by mark attachment and cursive attachment positioning subtables. + /// + /// + [DebuggerDisplay("X: {XCoordinate}, Y: {YCoordinate}")] + internal abstract class AnchorTable + { + /// + /// Initializes a new instance of the class. + /// + /// The horizontal value, in design units. + /// The vertical value, in design units. + protected AnchorTable(short xCoordinate, short yCoordinate) + { + this.XCoordinate = xCoordinate; + this.YCoordinate = yCoordinate; + } + + /// + /// Gets the horizontal value, in design units. + /// + protected short XCoordinate { get; } + + /// + /// Gets the vertical value, in design units. + /// + protected short YCoordinate { get; } + + /// + /// Gets the resolved anchor coordinates, potentially adjusted for hinting or variation data. + /// + /// The font metrics. + /// The glyph shaping data. + /// The glyph positioning collection. + /// The resolved anchor coordinates. + public abstract AnchorXY GetAnchor(FontMetrics fontMetrics, GlyphShapingData data, GlyphPositioningCollection collection); + + /// + /// Loads the anchor table. + /// + /// The big endian binary reader. + /// The offset to the beginning of the anchor table. + /// The anchor table. + public static AnchorTable Load(BigEndianBinaryReader reader, long offset) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort anchorFormat = reader.ReadUInt16(); + + return anchorFormat switch + { + 1 => AnchorFormat1.Load(reader), + 2 => AnchorFormat2.Load(reader), + 3 => AnchorFormat3.LoadFormat3(reader, offset), + + // Harfbuzz (Anchor.hh) treats this as an empty table and does not throw.. + // NotoSans Regular can trigger this. See https://github.com/SixLabors/Fonts/issues/417 + _ => EmptyAnchorTable.Instance, + }; + } + + /// + /// Anchor Table Format 1: design units only. Simple X, Y coordinate anchor point. + /// + internal sealed class AnchorFormat1 : AnchorTable + { + /// + /// Initializes a new instance of the class. + /// + /// The horizontal value, in design units. + /// The vertical value, in design units. + public AnchorFormat1(short xCoordinate, short yCoordinate) + : base(xCoordinate, yCoordinate) + { + } + + /// + /// Loads the Format 1 anchor table from the reader. + /// + /// The big endian binary reader. + /// The loaded . + public static AnchorFormat1 Load(BigEndianBinaryReader reader) + { + // +--------------+------------------------+------------------------------------------------+ + // | Type | Name | Description | + // +==============+========================+================================================+ + // | uint16 | anchorFormat | Format identifier, = 1 | + // +--------------+------------------------+------------------------------------------------+ + // | int16 | xCoordinate | Horizontal value, in design units. | + // +--------------+------------------------+------------------------------------------------+ + // | int16 | yCoordinate | Vertical value, in design units. | + // +--------------+------------------------+------------------------------------------------+ + short xCoordinate = reader.ReadInt16(); + short yCoordinate = reader.ReadInt16(); + return new AnchorFormat1(xCoordinate, yCoordinate); + } + + /// + public override AnchorXY GetAnchor(FontMetrics fontMetrics, GlyphShapingData data, GlyphPositioningCollection collection) + => new(this.XCoordinate, this.YCoordinate); + } + + /// + /// Anchor Table Format 2: design units plus contour point. + /// Uses a glyph contour point index to determine the anchor position when hinting is enabled. + /// + internal sealed class AnchorFormat2 : AnchorTable + { + private readonly ushort anchorPointIndex; + + /// + /// Initializes a new instance of the class. + /// + /// The horizontal value, in design units. + /// The vertical value, in design units. + /// The index to the glyph contour point. + public AnchorFormat2(short xCoordinate, short yCoordinate, ushort anchorPointIndex) + : base(xCoordinate, yCoordinate) => this.anchorPointIndex = anchorPointIndex; + + /// + /// Loads the Format 2 anchor table from the reader. + /// + /// The big endian binary reader. + /// The loaded . + public static AnchorFormat2 Load(BigEndianBinaryReader reader) + { + // +--------------+------------------------+------------------------------------------------+ + // | Type | Name | Description | + // +==============+========================+================================================+ + // | uint16 | anchorFormat | Format identifier, = 2 | + // +--------------+------------------------+------------------------------------------------+ + // | int16 | xCoordinate | Horizontal value, in design units. | + // +--------------+------------------------+------------------------------------------------+ + // | int16 | yCoordinate | Vertical value, in design units. | + // +--------------+------------------------+------------------------------------------------+ + // | uint16 + anchorPoint | Index to glyph contour point. + + // +--------------+------------------------+------------------------------------------------+ + short xCoordinate = reader.ReadInt16(); + short yCoordinate = reader.ReadInt16(); + ushort anchorPointIndex = reader.ReadUInt16(); + return new AnchorFormat2(xCoordinate, yCoordinate, anchorPointIndex); + } + + /// + public override AnchorXY GetAnchor(FontMetrics fontMetrics, GlyphShapingData data, GlyphPositioningCollection collection) + { + if (collection.TextOptions.HintingMode != HintingMode.None) + { + TextAttributes textAttributes = data.TextRun.TextAttributes; + TextDecorations textDecorations = data.TextRun.TextDecorations; + LayoutMode layoutMode = collection.TextOptions.LayoutMode; + ColorFontSupport colorFontSupport = collection.TextOptions.ColorFontSupport; + if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out FontGlyphMetrics? metrics)) + { + if (metrics is TrueTypeGlyphMetrics ttmetric) + { + IList points = ttmetric.GetOutline().ControlPoints; + if (this.anchorPointIndex < points.Count) + { + Vector2 point = points[this.anchorPointIndex].Point; + return new((short)point.X, (short)point.Y); + } + } + } + } + + return new(this.XCoordinate, this.YCoordinate); + } + } + + /// + /// Anchor Table Format 3: design units plus Device/VariationIndex tables. + /// Supports per-ppem adjustments via Device tables or variable font adjustments via VariationIndex tables. + /// + internal sealed class AnchorFormat3 : AnchorTable + { + private const ushort VariationIndexFormat = 0x8000; + + /// + /// Packed VariationIndex for X: (outerIndex << 16) | innerIndex. 0 = none. + /// + private readonly uint xVariation; + + /// + /// Packed VariationIndex for Y: (outerIndex << 16) | innerIndex. 0 = none. + /// + private readonly uint yVariation; + + /// + /// Initializes a new instance of the class. + /// + /// The horizontal value, in design units. + /// The vertical value, in design units. + /// The packed VariationIndex for X coordinate. + /// The packed VariationIndex for Y coordinate. + public AnchorFormat3(short xCoordinate, short yCoordinate, uint xVariation, uint yVariation) + : base(xCoordinate, yCoordinate) + { + this.xVariation = xVariation; + this.yVariation = yVariation; + } + + /// + /// Loads the Format 3 anchor table from the reader. + /// + /// The big endian binary reader. + /// The absolute stream position of the anchor table start. + /// The loaded . + public static AnchorFormat3 LoadFormat3(BigEndianBinaryReader reader, long anchorBase) + { + // +--------------+------------------------+-----------------------------------------------------------+ + // | Type | Name | Description | + // +==============+========================+===========================================================+ + // | uint16 | anchorFormat | Format identifier, = 3 | + // +--------------+------------------------+-----------------------------------------------------------+ + // | int16 | xCoordinate | Horizontal value, in design units. | + // +--------------+------------------------+-----------------------------------------------------------+ + // | int16 | yCoordinate | Vertical value, in design units. | + // +--------------+------------------------+-----------------------------------------------------------+ + // | Offset16 | xDeviceOffset + Offset to Device table (non-variable font) / | + // | | | VariationIndex table (variable font) for X coordinate, | + // | | | from beginning of Anchor table (may be NULL) | + // +--------------+------------------------+-----------------------------------------------------------+ + // | Offset16 | yDeviceOffset + Offset to Device table (non-variable font) / | + // | | | VariationIndex table (variable font) for Y coordinate, | + // | | | from beginning of Anchor table (may be NULL) | + // +--------------+------------------------+-----------------------------------------------------------+ + short xCoordinate = reader.ReadInt16(); + short yCoordinate = reader.ReadInt16(); + ushort xDeviceOffset = reader.ReadOffset16(); + ushort yDeviceOffset = reader.ReadOffset16(); + + uint xVariation = ResolveVariationIndex(reader, anchorBase, xDeviceOffset); + uint yVariation = ResolveVariationIndex(reader, anchorBase, yDeviceOffset); + + return new AnchorFormat3(xCoordinate, yCoordinate, xVariation, yVariation); + } + + /// + public override AnchorXY GetAnchor(FontMetrics fontMetrics, GlyphShapingData data, GlyphPositioningCollection collection) + { + short x = this.XCoordinate; + short y = this.YCoordinate; + + if (this.xVariation != 0) + { + x += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(this.xVariation)); + } + + if (this.yVariation != 0) + { + y += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(this.yVariation)); + } + + return new(x, y); + } + + /// + /// Reads a Device/VariationIndex table at the given offset and returns a packed VariationIndex + /// if it is a VariationIndex table (deltaFormat == 0x8000), or 0 otherwise. + /// + /// The big endian binary reader. + /// The absolute stream position of the anchor table. + /// The offset to the Device/VariationIndex table from the anchor table base. + /// The packed VariationIndex, or 0 if not applicable. + private static uint ResolveVariationIndex(BigEndianBinaryReader reader, long anchorBase, ushort deviceOffset) + { + if (deviceOffset == 0) + { + return 0; + } + + long savedPosition = reader.BaseStream.Position; + reader.BaseStream.Position = anchorBase + deviceOffset; + + ushort first = reader.ReadUInt16(); + ushort second = reader.ReadUInt16(); + ushort format = reader.ReadUInt16(); + + reader.BaseStream.Position = savedPosition; + + if (format == VariationIndexFormat) + { + return ((uint)first << 16) | second; + } + + // TODO: Device table (per-ppem adjustments) — not yet implemented. + return 0; + } + } + + /// + /// An empty anchor table that always returns (0, 0). Used as a fallback for unrecognized anchor formats. + /// + internal sealed class EmptyAnchorTable : AnchorTable + { + /// + /// Initializes a new instance of the class. + /// + private EmptyAnchorTable() + : base(0, 0) + { + } + + /// + /// Gets the singleton instance of the . + /// + public static EmptyAnchorTable Instance { get; } = new(); + + /// + public override AnchorXY GetAnchor( + FontMetrics fontMetrics, + GlyphShapingData data, + GlyphPositioningCollection collection) + => new(0, 0); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorXY.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorXY.cs new file mode 100644 index 0000000..68748bd --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorXY.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// Represents the anchor coordinates for a given table. + /// + internal readonly struct AnchorXY + { + /// + /// Initializes a new instance of the struct. + /// + /// The horizontal value, in design units. + /// The vertical value, in design units. + public AnchorXY(short x, short y) + { + this.XCoordinate = x; + this.YCoordinate = y; + } + + /// + /// Gets the horizontal value, in design units. + /// + public short XCoordinate { get; } + + /// + /// Gets the vertical value, in design units. + /// + public short YCoordinate { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/BaseArrayTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/BaseArrayTable.cs new file mode 100644 index 0000000..6f0ab6e --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/BaseArrayTable.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// Represents the BaseArray table used in MarkToBase attachment positioning (GPOS LookupType 4). + /// The BaseArray table contains an array of BaseRecords, one for each base glyph, ordered by the base Coverage index. + /// + /// + internal class BaseArrayTable + { + /// + /// Initializes a new instance of the class. + /// + /// The big endian binary reader. + /// The offset to the beginning of the base array table. + /// The class count. + public BaseArrayTable(BigEndianBinaryReader reader, long offset, ushort classCount) + { + // +--------------+------------------------+--------------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==============+========================+======================================================================================+ + // | uint16 | baseCount | Number of BaseRecords | + // +--------------+------------------------+--------------------------------------------------------------------------------------+ + // | BaseRecord | baseRecords[baseCount] | Array of BaseRecords, in order of baseCoverage Index. | + // +--------------+------------------------+--------------------------------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + ushort baseCount = reader.ReadUInt16(); + this.BaseRecords = new BaseRecord[baseCount]; + for (int i = 0; i < baseCount; i++) + { + this.BaseRecords[i] = new BaseRecord(reader, classCount, offset); + } + } + + /// + /// Gets the base records. + /// + public BaseRecord[] BaseRecords { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/BaseRecord.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/BaseRecord.cs new file mode 100644 index 0000000..701805d --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/BaseRecord.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// Represents a BaseRecord in the BaseArray table. Each BaseRecord contains an array of offsets + /// to Anchor tables, one per mark class, that define the attachment points for base glyphs. + /// + /// + internal readonly struct BaseRecord + { + /// + /// Initializes a new instance of the struct. + /// + /// The big endian binary reader. + /// The class count. + /// Offset to the from beginning of BaseArray table. + public BaseRecord(BigEndianBinaryReader reader, ushort classCount, long offset) + { + // +--------------+-----------------------------------+----------------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==============+===================================+========================================================================================+ + // | Offset16 | baseAnchorOffsets[markClassCount] | Array of offsets (one per mark class) to Anchor tables. | + // | | | Offsets are from beginning of BaseArray table, ordered by class (offsets may be NULL). | + // +--------------+-----------------------------------+----------------------------------------------------------------------------------------+ + this.BaseAnchorTables = new AnchorTable[classCount]; + ushort[] baseAnchorOffsets = new ushort[classCount]; + for (int i = 0; i < classCount; i++) + { + baseAnchorOffsets[i] = reader.ReadOffset16(); + } + + long position = reader.BaseStream.Position; + for (int i = 0; i < classCount; i++) + { + if (baseAnchorOffsets[i] is not 0) + { + this.BaseAnchorTables[i] = AnchorTable.Load(reader, offset + baseAnchorOffsets[i]); + } + } + + reader.BaseStream.Position = position; + } + + /// + /// Gets the base anchor tables. + /// + public AnchorTable[] BaseAnchorTables { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/Class1Record.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/Class1Record.cs new file mode 100644 index 0000000..92e5bb8 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/Class1Record.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// Represents a Class1Record used in Pair Adjustment Positioning Format 2 (class-based kerning). + /// Each Class1Record contains an array of Class2Records, one for each class in the second class definition table. + /// + /// + internal sealed class Class1Record + { + /// + /// Initializes a new instance of the class. + /// + /// The array of Class2 records. + private Class1Record(Class2Record[] class2Records) => this.Class2Records = class2Records; + + /// + /// Gets the array of Class2 records, ordered by classes in the second class definition table. + /// + public Class2Record[] Class2Records { get; } + + /// + /// Loads the from the specified reader. + /// + /// The big endian binary reader. + /// The number of classes in the second class definition table. + /// The value format for the first glyph. + /// The value format for the second glyph. + /// The absolute stream position of the parent table for resolving device offsets. + /// The loaded . + public static Class1Record Load(BigEndianBinaryReader reader, int class2Count, ValueFormat valueFormat1, ValueFormat valueFormat2, long parentBase = -1) + { + // +--------------+----------------------------+---------------------------------------------+ + // | Type | Name | Description | + // +==============+============================+=============================================+ + // | Class2Record | class2Records[class2Count] | Array of Class2 records, ordered by classes | + // | | | in classDef2. | + // +--------------+----------------------------+---------------------------------------------+ + var class2Records = new Class2Record[class2Count]; + for (int i = 0; i < class2Records.Length; i++) + { + class2Records[i] = new Class2Record(reader, valueFormat1, valueFormat2, parentBase); + } + + return new Class1Record(class2Records); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/Class2Record.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/Class2Record.cs new file mode 100644 index 0000000..a65b5ae --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/Class2Record.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// Class2Record used in Pair Adjustment Positioning Format 2. + /// A Class2Record consists of two ValueRecords, one for the first glyph in a class pair (valueRecord1) and one for the second glyph (valueRecord2). + /// Note that both fields of a Class2Record are optional: If the PairPos subtable has a value of zero (0) for valueFormat1 or valueFormat2, + /// then the corresponding record (valueRecord1 or valueRecord2) will be empty — that is, not present. + /// + internal readonly struct Class2Record + { + /// + /// Initializes a new instance of the struct. + /// + /// The big endian binary reader. + /// The value format for value record 1. + /// The value format for value record 2. + /// The absolute stream position of the parent table for resolving device offsets. + public Class2Record(BigEndianBinaryReader reader, ValueFormat valueFormat1, ValueFormat valueFormat2, long parentBase = -1) + { + this.ValueRecord1 = new ValueRecord(reader, valueFormat1, parentBase); + this.ValueRecord2 = new ValueRecord(reader, valueFormat2, parentBase); + } + + /// + /// Gets the positioning for the first glyph. + /// + public ValueRecord ValueRecord1 { get; } + + /// + /// Gets the positioning for second glyph. + /// + public ValueRecord ValueRecord2 { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/ComponentRecord.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/ComponentRecord.cs new file mode 100644 index 0000000..f9d8913 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/ComponentRecord.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// In a ComponentRecord, the zero-based ligatureAnchorOffsets array lists offsets to Anchor tables by mark class. + /// If a component does not define an attachment point for a particular class of marks, then the offset to the corresponding Anchor table will be NULL. + /// Example 8 at the end of this chapter shows a MarkLigPosFormat1 subtable used to attach mark accents to a ligature glyph in the Arabic script. + /// + internal class ComponentRecord + { + /// + /// Initializes a new instance of the class. + /// + /// The big endian binary reader. + /// Number of defined mark classes. + /// Offset from beginning of LigatureAttach table. + public ComponentRecord(BigEndianBinaryReader reader, ushort markClassCount, long offset) + { + // +--------------+---------------------------------------+----------------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==============+=======================================+========================================================================================+ + // | Offset16 | ligatureAnchorOffsets[markClassCount] | Array of offsets (one per class) to Anchor tables. Offsets are from | + // | | | beginning of LigatureAttach table, ordered by class (offsets may be NULL). | + // +--------------+---------------------------------------+----------------------------------------------------------------------------------------+ + this.LigatureAnchorTables = new AnchorTable[markClassCount]; + ushort[] ligatureAnchorOffsets = new ushort[markClassCount]; + for (int i = 0; i < markClassCount; i++) + { + ligatureAnchorOffsets[i] = reader.ReadOffset16(); + } + + long position = reader.BaseStream.Position; + for (int i = 0; i < markClassCount; i++) + { + if (ligatureAnchorOffsets[i] is not 0) + { + this.LigatureAnchorTables[i] = AnchorTable.Load(reader, offset + ligatureAnchorOffsets[i]); + } + } + + reader.BaseStream.Position = position; + } + + /// + /// Gets the array of Anchor tables, one per mark class, that define the attachment points for this ligature component. + /// + public AnchorTable[] LigatureAnchorTables { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/EntryExitAnchors.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/EntryExitAnchors.cs new file mode 100644 index 0000000..b684aed --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/EntryExitAnchors.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// Represents the resolved entry and exit anchor tables for a glyph used in cursive attachment positioning (GPOS LookupType 3). + /// + internal sealed class EntryExitAnchors + { + /// + /// Initializes a new instance of the class. + /// + /// The big endian binary reader. + /// The offset to exitAnchor table, from beginning of CursivePos subtable. + /// Offsets to entry and exit Anchor table, from beginning of CursivePos subtable. + public EntryExitAnchors(BigEndianBinaryReader reader, long offset, EntryExitRecord entryExitRecord) + { + this.EntryAnchor = entryExitRecord.EntryAnchorOffset != 0 ? AnchorTable.Load(reader, offset + entryExitRecord.EntryAnchorOffset) : null; + this.ExitAnchor = entryExitRecord.ExitAnchorOffset != 0 ? AnchorTable.Load(reader, offset + entryExitRecord.ExitAnchorOffset) : null; + } + + /// + /// Gets the entry anchor table. + /// + public AnchorTable? EntryAnchor { get; } + + /// + /// Gets the exit anchor table. + /// + public AnchorTable? ExitAnchor { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/EntryExitRecord.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/EntryExitRecord.cs new file mode 100644 index 0000000..a70dc41 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/EntryExitRecord.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// EntryExitRecord sued in Cursive Attachment Positioning Format1. + /// Each EntryExitRecord consists of two offsets: one to an Anchor table that identifies the entry point on the glyph (entryAnchorOffset), + /// and an offset to an Anchor table that identifies the exit point on the glyph (exitAnchorOffset). + /// + internal readonly struct EntryExitRecord + { + /// + /// Initializes a new instance of the struct. + /// + /// The big endian binary reader. + /// The offset to exitAnchor table, from beginning of CursivePos subtable. + public EntryExitRecord(BigEndianBinaryReader reader, long offset) + { + // EntryExitRecord + // +--------------+------------------------+------------------------------------------------+ + // | Type | Name | Description | + // +==============+========================+================================================+ + // | Offset16 | entryAnchorOffset | Offset to entryAnchor table, from beginning of | + // | | | CursivePos subtable (may be NULL). | + // +--------------+------------------------+------------------------------------------------+ + // | Offset16 | exitAnchorOffset | Offset to exitAnchor table, from beginning of | + // | | | CursivePos subtable (may be NULL). | + // +--------------+------------------------+------------------------------------------------+ + this.EntryAnchorOffset = reader.ReadOffset16(); + this.ExitAnchorOffset = reader.ReadOffset16(); + } + + /// + /// Gets the offset to entryAnchor table, from beginning of CursivePos subtable. + /// + public ushort EntryAnchorOffset { get; } + + /// + /// Gets the offset to exitAnchor table, from beginning of CursivePos subtable. + /// + public ushort ExitAnchorOffset { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LigatureArrayTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LigatureArrayTable.cs new file mode 100644 index 0000000..5d6afe2 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LigatureArrayTable.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// The LigatureArray table contains a count (ligatureCount) and an array of offsets (ligatureAttachOffsets) to LigatureAttach tables. + /// The ligatureAttachOffsets array lists the offsets to LigatureAttach tables, one for each ligature glyph listed in the ligatureCoverage table, + /// in the same order as the ligatureCoverage index. + /// + internal class LigatureArrayTable + { + /// + /// Initializes a new instance of the class. + /// + /// The big endian binary reader. + /// The offset to the start of the ligature array table. + /// Number of defined mark classes. + public LigatureArrayTable(BigEndianBinaryReader reader, long offset, ushort markClassCount) + { + // +--------------+--------------------------------------+--------------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==============+======================================+======================================================================================+ + // | uint16 | ligatureCount | Number of LigatureAttach table offsets. | + // +--------------+--------------------------------------+--------------------------------------------------------------------------------------+ + // | Offset16 | ligatureAttachOffsets[ligatureCount] | Array of offsets to LigatureAttach tables. Offsets are from beginning of | + // | | | LigatureArray table, ordered by ligatureCoverage index. | + // +--------------+--------------------------------------+--------------------------------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + ushort ligatureCount = reader.ReadUInt16(); + this.LigatureAttachTables = new LigatureAttachTable[ligatureCount]; + ushort[] ligatureAttachOffsets = new ushort[ligatureCount]; + for (int i = 0; i < ligatureCount; i++) + { + ligatureAttachOffsets[i] = reader.ReadOffset16(); + } + + for (int i = 0; i < ligatureCount; i++) + { + this.LigatureAttachTables[i] = new LigatureAttachTable(reader, markClassCount, offset + ligatureAttachOffsets[i]); + } + } + + /// + /// Gets the array of LigatureAttach tables, one per ligature glyph, in the same order as the ligature Coverage index. + /// + public LigatureAttachTable[] LigatureAttachTables { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LigatureAttachTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LigatureAttachTable.cs new file mode 100644 index 0000000..6a9584e --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LigatureAttachTable.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// Each LigatureAttach table consists of an array (componentRecords) and count (componentCount) of the component glyphs in a ligature. + /// The array stores the ComponentRecords in the same order as the components in the ligature. + /// The order of the records also corresponds to the writing direction — that is, the logical direction — of the text. + /// For text written left to right, the first component is on the left; for text written right to left, the first component is on the right. + /// + internal class LigatureAttachTable + { + /// + /// Initializes a new instance of the class. + /// + /// The big endian binary reader. + /// Number of defined mark classes. + /// Offset from beginning of LigatureAttach table. + public LigatureAttachTable(BigEndianBinaryReader reader, ushort markClassCount, long offset) + { + // +-------------------+---------------------------------+--------------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +===================+=================================+======================================================================================+ + // | uint16 | componentCount | Number of ComponentRecords in this ligature. | + // +-------------------+---------------------------------+--------------------------------------------------------------------------------------+ + // | ComponentRecords | componentRecords[componentCount]| Array of Component records, ordered in writing direction. | + // +-------------------+---------------------------------+--------------------------------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + ushort componentCount = reader.ReadUInt16(); + this.ComponentRecords = new ComponentRecord[componentCount]; + for (int i = 0; i < componentCount; i++) + { + this.ComponentRecords[i] = new ComponentRecord(reader, markClassCount, offset); + } + } + + /// + /// Gets the array of component records for this ligature, ordered in writing direction. + /// + public ComponentRecord[] ComponentRecords { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs new file mode 100644 index 0000000..d0a4825 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs @@ -0,0 +1,268 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// The headers of the GSUB and GPOS tables contain offsets to Lookup List tables (LookupList) for + /// glyph substitution (GSUB table) and glyph positioning (GPOS table). The LookupList table contains + /// an array of offsets to Lookup tables (lookupOffsets). + /// + /// + internal sealed class LookupListTable + { + /// + /// Initializes a new instance of the class. + /// + /// The number of lookups in this table. + /// The array of lookup tables. + private LookupListTable(ushort lookupCount, LookupTable[] lookupTables) + { + this.LookupCount = lookupCount; + this.LookupTables = lookupTables; + } + + /// + /// Gets the number of lookups in this table. + /// + public ushort LookupCount { get; } + + /// + /// Gets the array of lookup tables. + /// + public LookupTable[] LookupTables { get; } + + /// + /// Loads the from the specified reader at the given offset. + /// + /// The big endian binary reader. + /// The offset to the beginning of the lookup list table. + /// The loaded . + public static LookupListTable Load(BigEndianBinaryReader reader, long offset) + { + // +----------+----------------------------+---------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+============================+===============================================================+ + // | uint16 | lookupCount | Number of lookups in this table | + // +----------+----------------------------+---------------------------------------------------------------+ + // | Offset16 | lookupOffsets[lookupCount] | Array of offsets to Lookup tables, from beginning | + // | | | of LookupList — zero based (first lookup is Lookup index = 0) | + // +----------+----------------------------+---------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + + ushort lookupCount = reader.ReadUInt16(); + using Buffer lookupOffsetsBuffer = new(lookupCount); + Span lookupOffsets = lookupOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(lookupOffsets); + + LookupTable[] lookupTables = new LookupTable[lookupCount]; + + for (int i = 0; i < lookupTables.Length; i++) + { + lookupTables[i] = LookupTable.Load(reader, offset + lookupOffsets[i]); + } + + return new LookupListTable(lookupCount, lookupTables); + } + } + + /// + /// A Lookup table (Lookup) defines the specific conditions, type, and results of a substitution + /// or positioning action that is used to implement a feature. For example, a substitution + /// operation requires a list of target glyph indices to be replaced, a list of replacement glyph + /// indices, and a description of the type of substitution action. + /// + /// + internal sealed class LookupTable + { + /// + /// Initializes a new instance of the class. + /// + /// The lookup type, identifying the kind of positioning operation. + /// The lookup qualifiers. + /// The index into the GDEF mark glyph sets structure. + /// The array of lookup subtables. + private LookupTable( + ushort lookupType, + LookupFlags lookupFlags, + ushort markFilteringSet, + LookupSubTable[] lookupSubTables) + { + this.LookupType = lookupType; + this.LookupFlags = lookupFlags; + this.MarkFilteringSet = markFilteringSet; + this.LookupSubTables = lookupSubTables; + } + + /// + /// Gets the lookup type that identifies the kind of positioning operation. + /// + public ushort LookupType { get; } + + /// + /// Gets the lookup qualifiers. + /// + public LookupFlags LookupFlags { get; } + + /// + /// Gets the index into the GDEF mark glyph sets structure. + /// + public ushort MarkFilteringSet { get; } + + /// + /// Gets the array of lookup subtables. + /// + public LookupSubTable[] LookupSubTables { get; } + + /// + /// Loads the from the specified reader at the given offset. + /// + /// The big endian binary reader. + /// The offset to the beginning of the lookup table. + /// The loaded . + public static LookupTable Load(BigEndianBinaryReader reader, long offset) + { + // +----------+--------------------------------+-------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+================================+=============================================================+ + // | uint16 | lookupType | Different enumerations for GSUB and GPOS. | + // +----------+--------------------------------+-------------------------------------------------------------+ + // | uint16 | lookupFlag | Lookup qualifiers . | + // +----------+--------------------------------+-------------------------------------------------------------+ + // | uint16 | subTableCount | Number of subtables for this lookup. | + // +----------+--------------------------------+-------------------------------------------------------------+ + // | Offset16 | subtableOffsets[subTableCount] | Array of offsets to lookup subtables, from beginning of | + // | | | Lookup table. | + // +----------+--------------------------------+-------------------------------------------------------------+ + // | uint16 | markFilteringSet | Index (base 0) into GDEF mark glyph sets structure. | + // | | | This field is only present if the USE_MARK_FILTERING_SET | + // | | | lookup flag is set. | + // +----------+--------------------------------+-------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + + ushort lookupType = reader.ReadUInt16(); + LookupFlags lookupFlags = reader.ReadUInt16(); + ushort subTableCount = reader.ReadUInt16(); + + using Buffer subTableOffsetsBuffer = new(subTableCount); + Span subTableOffsets = subTableOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(subTableOffsets); + + // The fifth bit indicates the presence of a MarkFilteringSet field in the Lookup table. + ushort markFilteringSet = ((lookupFlags & LookupFlags.UseMarkFilteringSet) != 0) + ? reader.ReadUInt16() + : (ushort)0; + + LookupSubTable[] lookupSubTables = new LookupSubTable[subTableCount]; + + for (int i = 0; i < lookupSubTables.Length; i++) + { + lookupSubTables[i] = LoadLookupSubTable(lookupType, lookupFlags, markFilteringSet, reader, offset + subTableOffsets[i]); + } + + return new LookupTable(lookupType, lookupFlags, markFilteringSet, lookupSubTables); + } + + /// + /// Loads the appropriate lookup subtable based on the lookup type. + /// + /// The lookup type identifier. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The loaded . + private static LookupSubTable LoadLookupSubTable(ushort lookupType, LookupFlags lookupFlags, ushort markFilteringSet, BigEndianBinaryReader reader, long offset) + => lookupType switch + { + 1 => LookupType1SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 2 => LookupType2SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 3 => LookupType3SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 4 => LookupType4SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 5 => LookupType5SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 6 => LookupType6SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 7 => LookupType7SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 8 => LookupType8SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 9 => LookupType9SubTable.Load(reader, offset, lookupFlags, markFilteringSet, LoadLookupSubTable), + _ => new NotImplementedSubTable() + }; + + /// + /// Attempts to update the position of glyphs in the collection at the specified index. + /// + /// The font metrics. + /// The GPOS table. + /// The glyph positioning collection. + /// The feature tag. + /// The zero-based index of the glyph to position. + /// The number of glyphs remaining in the sequence. + /// if the position was updated; otherwise, . + public bool TryUpdatePosition( + FontMetrics fontMetrics, + GPosTable table, + GlyphPositioningCollection collection, + Tag feature, + int index, + int count) + { + foreach (LookupSubTable subTable in this.LookupSubTables) + { + // A lookup is finished for a glyph after the client locates the target + // glyph or glyph context and performs a positioning action, if specified. + if (subTable.TryUpdatePosition(fontMetrics, table, collection, feature, index, count)) + { + return true; + } + } + + return false; + } + } + + /// + /// Base class for all GPOS lookup subtables. Each subtable implements a specific type of glyph positioning operation. + /// + internal abstract class LookupSubTable + { + /// + /// Initializes a new instance of the class. + /// + /// The lookup qualifiers. + /// The mark filtering set index. + protected LookupSubTable(LookupFlags lookupFlags, ushort markFilteringSet) + { + this.LookupFlags = lookupFlags; + this.MarkFilteringSet = markFilteringSet; + } + + /// + /// Gets the lookup qualifiers. + /// + public LookupFlags LookupFlags { get; } + + /// + /// Gets the mark filtering set index. + /// + public ushort MarkFilteringSet { get; } + + /// + /// Attempts to update the position of glyphs in the collection at the specified index. + /// + /// The font metrics. + /// The GPOS table. + /// The glyph positioning collection. + /// The feature tag. + /// The zero-based index of the glyph to position. + /// The number of glyphs remaining in the sequence. + /// if the position was updated; otherwise, . + public abstract bool TryUpdatePosition( + FontMetrics fontMetrics, + GPosTable table, + GlyphPositioningCollection collection, + Tag feature, + int index, + int count); + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType1SubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType1SubTable.cs new file mode 100644 index 0000000..25fdacf --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType1SubTable.cs @@ -0,0 +1,212 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// A single adjustment positioning subtable (SinglePos) is used to adjust the placement or advance of a single glyph, + /// such as a subscript or superscript. In addition, a SinglePos subtable is commonly used to implement lookup data for contextual positioning. + /// A SinglePos subtable will have one of two formats: one that applies the same adjustment to a series of glyphs(Format 1), + /// and one that applies a different adjustment for each unique glyph(Format 2). + /// + /// + internal static class LookupType1SubTable + { + /// + /// Loads the single adjustment positioning subtable from the specified reader. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupSubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort posFormat = reader.ReadUInt16(); + + return posFormat switch + { + 1 => LookupType1Format1SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 2 => LookupType1Format2SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + _ => new NotImplementedSubTable(), + }; + } + } + + /// + /// Single Adjustment Positioning Format 1: applies the same positioning value to all glyphs in the Coverage table. + /// + /// + internal sealed class LookupType1Format1SubTable : LookupSubTable + { + private readonly ValueRecord valueRecord; + private readonly CoverageTable coverageTable; + + /// + /// Initializes a new instance of the class. + /// + /// The positioning value record applied to all covered glyphs. + /// The coverage table. + /// The lookup qualifiers. + /// The mark filtering set index. + private LookupType1Format1SubTable(ValueRecord valueRecord, CoverageTable coverageTable, LookupFlags lookupFlags, ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.valueRecord = valueRecord; + this.coverageTable = coverageTable; + } + + /// + /// Loads the Format 1 single adjustment positioning subtable. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupType1Format1SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + // SinglePosFormat1 + // +-------------+----------------+-----------------------------------------------+ + // | Type | Name | Description | + // +=============+================+===============================================+ + // | uint16 | posFormat | Format identifier: format = 1 | + // +-------------+----------------+-----------------------------------------------+ + // | Offset16 | coverageOffset | Offset to Coverage table, from beginning | + // | | | of SinglePos subtable. | + // +-------------+----------------+-----------------------------------------------+ + // | uint16 | valueFormat | Defines the types of data in the ValueRecord. | + // +-------------+----------------+-----------------------------------------------+ + // | ValueRecord | valueRecord | Defines positioning value(s) — applied to | + // | | | all glyphs in the Coverage table. | + // +-------------+----------------+-----------------------------------------------+ + ushort coverageOffset = reader.ReadOffset16(); + ValueFormat valueFormat = reader.ReadUInt16(); + ValueRecord valueRecord = new(reader, valueFormat, offset); + + CoverageTable coverageTable = CoverageTable.Load(reader, offset + coverageOffset); + + return new LookupType1Format1SubTable(valueRecord, coverageTable, lookupFlags, markFilteringSet); + } + + /// + public override bool TryUpdatePosition( + FontMetrics fontMetrics, + GPosTable table, + GlyphPositioningCollection collection, + Tag feature, + int index, + int count) + { + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + int coverage = this.coverageTable.CoverageIndexOf(glyphId); + if (coverage > -1) + { + ValueRecord record = this.valueRecord; + AdvancedTypographicUtils.ApplyPosition(fontMetrics, collection, index, record, feature); + + return true; + } + + return false; + } + } + + /// + /// Single Adjustment Positioning Format 2: applies a unique positioning value to each glyph in the Coverage table. + /// + /// + internal sealed class LookupType1Format2SubTable : LookupSubTable + { + private readonly CoverageTable coverageTable; + private readonly ValueRecord[] valueRecords; + + /// + /// Initializes a new instance of the class. + /// + /// The array of positioning value records, one per covered glyph. + /// The coverage table. + /// The lookup qualifiers. + /// The mark filtering set index. + private LookupType1Format2SubTable(ValueRecord[] valueRecords, CoverageTable coverageTable, LookupFlags lookupFlags, ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.valueRecords = valueRecords; + this.coverageTable = coverageTable; + } + + /// + /// Loads the Format 2 single adjustment positioning subtable. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupType1Format2SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + // SinglePosFormat2 + // +-------------+--------------------------+-----------------------------------------------+ + // | Type | Name | Description | + // +=============+==========================+===============================================+ + // | uint16 | posFormat | Format identifier: format = 2 | + // +-------------+--------------------------+-----------------------------------------------+ + // | Offset16 | coverageOffset | Offset to Coverage table, from beginning | + // | | | of SinglePos subtable. | + // +-------------+--------------------------+-----------------------------------------------+ + // | uint16 | valueFormat | Defines the types of data in the ValueRecords.| + // +-------------+--------------------------+-----------------------------------------------+ + // | uint16 | valueCount | Number of ValueRecords — must equal glyphCount| + // | | | in the Coverage table. | + // | ValueRecord | valueRecords[valueCount] | Array of ValueRecords — positioning values | + // | | | applied to glyphs. | + // +-------------+--------------------------+-----------------------------------------------+ + ushort coverageOffset = reader.ReadOffset16(); + ValueFormat valueFormat = reader.ReadUInt16(); + ushort valueCount = reader.ReadUInt16(); + ValueRecord[] valueRecords = new ValueRecord[valueCount]; + for (int i = 0; i < valueCount; i++) + { + valueRecords[i] = new ValueRecord(reader, valueFormat, offset); + } + + CoverageTable coverageTable = CoverageTable.Load(reader, offset + coverageOffset); + + return new LookupType1Format2SubTable(valueRecords, coverageTable, lookupFlags, markFilteringSet); + } + + /// + public override bool TryUpdatePosition( + FontMetrics fontMetrics, + GPosTable table, + GlyphPositioningCollection collection, + Tag feature, + int index, + int count) + { + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + int coverage = this.coverageTable.CoverageIndexOf(glyphId); + if (coverage > -1 && coverage < this.valueRecords.Length) + { + ValueRecord record = this.valueRecords[coverage]; + AdvancedTypographicUtils.ApplyPosition(fontMetrics, collection, index, record, feature); + + return true; + } + + return false; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType2SubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType2SubTable.cs new file mode 100644 index 0000000..0eedb9b --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType2SubTable.cs @@ -0,0 +1,388 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// A pair adjustment positioning subtable (PairPos) is used to adjust the placement or advances of two glyphs in relation to one another — + /// for instance, to specify kerning data for pairs of glyphs. Compared to a typical kerning table, however, + /// a PairPos subtable offers more flexibility and precise control over glyph positioning. + /// The PairPos subtable can adjust each glyph in a pair independently in both the X and Y directions, + /// and it can explicitly describe the particular type of adjustment applied to each glyph. + /// PairPos subtables can be either of two formats: one that identifies glyphs individually by index(Format 1), and one that identifies glyphs by class (Format 2). + /// + /// + internal static class LookupType2SubTable + { + /// + /// Loads the pair adjustment positioning subtable from the specified reader. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupSubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort posFormat = reader.ReadUInt16(); + + return posFormat switch + { + 1 => LookupType2Format1SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 2 => LookupType2Format2SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + _ => new NotImplementedSubTable(), + }; + } + + /// + /// Pair Adjustment Positioning Format 1: adjustments for glyph pairs identified individually by glyph index. + /// + /// + internal sealed class LookupType2Format1SubTable : LookupSubTable + { + private readonly CoverageTable coverageTable; + private readonly PairSetTable[] pairSets; + + /// + /// Initializes a new instance of the class. + /// + /// The coverage table. + /// The array of pair set tables. + /// The lookup qualifiers. + /// The mark filtering set index. + public LookupType2Format1SubTable(CoverageTable coverageTable, PairSetTable[] pairSets, LookupFlags lookupFlags, ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.coverageTable = coverageTable; + this.pairSets = pairSets; + } + + /// + /// Loads the Format 1 pair adjustment positioning subtable. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupType2Format1SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + // Pair Adjustment Positioning Subtable format 1. + // +-------------+------------------------------+------------------------------------------------+ + // | Type | Name | Description | + // +=============+==============================+================================================+ + // | uint16 | posFormat | Format identifier: format = 1 | + // +-------------+------------------------------+------------------------------------------------+ + // | Offset16 | coverageOffset | Offset to Coverage table, from beginning of | + // | | | PairPos subtable. | + // +-------------+------------------------------+------------------------------------------------+ + // | uint16 | valueFormat1 | Defines the types of data in valueRecord1 — | + // | | | for the first glyph in the pair (may be zero). | + // +-------------+------------------------------+------------------------------------------------+ + // | uint16 | valueFormat2 | Defines the types of data in valueRecord2 — | + // | | | for the second glyph in the pair (may be zero).| + // +-------------+------------------------------+------------------------------------------------+ + // | uint16 | pairSetCount | Number of PairSet tables | + // +-------------+------------------------------+------------------------------------------------+ + // | Offset16 | pairSetOffsets[pairSetCount] | Array of offsets to PairSet tables. | + // | | | Offsets are from beginning of PairPos subtable,| + // | | | ordered by Coverage Index. | + // +-------------+------------------------------+------------------------------------------------+ + ushort coverageOffset = reader.ReadOffset16(); + ValueFormat valueFormat1 = reader.ReadUInt16(); + ValueFormat valueFormat2 = reader.ReadUInt16(); + ushort pairSetCount = reader.ReadUInt16(); + + using Buffer pairSetOffsetsBuffer = new(pairSetCount); + Span pairSetOffsets = pairSetOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(pairSetOffsets); + + PairSetTable[] pairSets = new PairSetTable[pairSetCount]; + for (int i = 0; i < pairSetCount; i++) + { + reader.Seek(offset + pairSetOffsets[i], SeekOrigin.Begin); + long pairSetBase = offset + pairSetOffsets[i]; + pairSets[i] = PairSetTable.Load(reader, pairSetBase, valueFormat1, valueFormat2); + } + + CoverageTable coverageTable = CoverageTable.Load(reader, offset + coverageOffset); + + return new LookupType2Format1SubTable(coverageTable, pairSets, lookupFlags, markFilteringSet); + } + + /// + public override bool TryUpdatePosition( + FontMetrics fontMetrics, + GPosTable table, + GlyphPositioningCollection collection, + Tag feature, + int index, + int count) + { + if (count <= 1) + { + return false; + } + + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + int coverage = this.coverageTable.CoverageIndexOf(glyphId); + if (coverage > -1 && coverage < this.pairSets.Length) + { + PairSetTable pairSet = this.pairSets[coverage]; + ushort glyphId2 = collection[index + 1].GlyphId; + if (glyphId2 == 0) + { + return false; + } + + if (pairSet.TryGetPairValueRecord(glyphId2, out PairValueRecord pairValueRecord)) + { + ValueRecord record1 = pairValueRecord.ValueRecord1; + AdvancedTypographicUtils.ApplyPosition(fontMetrics, collection, index, record1, feature); + + ValueRecord record2 = pairValueRecord.ValueRecord2; + AdvancedTypographicUtils.ApplyPosition(fontMetrics, collection, index + 1, record2, feature); + + return true; + } + } + + return false; + } + + /// + /// Represents a PairSet table containing an array of PairValueRecords, ordered by the glyph ID of the second glyph. + /// + internal sealed class PairSetTable + { + private readonly PairValueRecord[] pairValueRecords; + + /// + /// Initializes a new instance of the class. + /// + /// The array of pair value records. + private PairSetTable(PairValueRecord[] pairValueRecords) + => this.pairValueRecords = pairValueRecords; + + /// + /// Loads the pair set table from the specified reader. + /// + /// The big endian binary reader. + /// The offset to the beginning of the pair set table. + /// The value format for the first glyph. + /// The value format for the second glyph. + /// The loaded . + public static PairSetTable Load(BigEndianBinaryReader reader, long offset, ValueFormat valueFormat1, ValueFormat valueFormat2) + { + // +-----------------+----------------------------------+---------------------------------------+ + // | Type | Name | Description | + // +=================+==================================+=======================================+ + // | uint16 | pairValueCount | Number of PairValueRecords | + // +-----------------+----------------------------------+---------------------------------------+ + // | PairValueRecord | pairValueRecords[pairValueCount] | Array of PairValueRecords, ordered by | + // | | | glyph ID of the second glyph. | + // +-----------------+----------------------------------+---------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + ushort pairValueCount = reader.ReadUInt16(); + PairValueRecord[] pairValueRecords = new PairValueRecord[pairValueCount]; + for (int i = 0; i < pairValueRecords.Length; i++) + { + pairValueRecords[i] = new PairValueRecord(reader, valueFormat1, valueFormat2, offset); + } + + return new PairSetTable(pairValueRecords); + } + + /// + /// Tries to find a for the specified second glyph ID using binary search. + /// + /// The glyph ID of the second glyph in the pair. + /// When this method returns, contains the matching pair value record if found. + /// if a matching record was found; otherwise, . + public bool TryGetPairValueRecord(ushort glyphId, [NotNullWhen(true)] out PairValueRecord pairValueRecord) + { + // Records are ordered by SecondGlyph, so use binary search. + PairValueRecord[] records = this.pairValueRecords; + int lo = 0; + int hi = records.Length - 1; + while (lo <= hi) + { + int mid = (int)(((uint)lo + (uint)hi) >> 1); + ushort midGlyph = records[mid].SecondGlyph; + if (glyphId < midGlyph) + { + hi = mid - 1; + } + else if (glyphId > midGlyph) + { + lo = mid + 1; + } + else + { + pairValueRecord = records[mid]; + return true; + } + } + + pairValueRecord = default; + return false; + } + } + } + + /// + /// Pair Adjustment Positioning Format 2: adjustments for glyph pairs identified by glyph class. + /// + /// + internal sealed class LookupType2Format2SubTable : LookupSubTable + { + private readonly CoverageTable coverageTable; + private readonly Class1Record[] class1Records; + private readonly ClassDefinitionTable classDefinitionTable1; + private readonly ClassDefinitionTable classDefinitionTable2; + + /// + /// Initializes a new instance of the class. + /// + /// The coverage table. + /// The array of Class1 records. + /// The class definition table for the first glyph. + /// The class definition table for the second glyph. + /// The lookup qualifiers. + /// The mark filtering set index. + public LookupType2Format2SubTable( + CoverageTable coverageTable, + Class1Record[] class1Records, + ClassDefinitionTable classDefinitionTable1, + ClassDefinitionTable classDefinitionTable2, + LookupFlags lookupFlags, + ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.coverageTable = coverageTable; + this.class1Records = class1Records; + this.classDefinitionTable1 = classDefinitionTable1; + this.classDefinitionTable2 = classDefinitionTable2; + } + + /// + /// Loads the Format 2 pair adjustment positioning subtable. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupType2Format2SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + // Pair Adjustment Positioning Subtable format 2. + // +-------------+------------------------------+------------------------------------------------+ + // | Type | Name | Description | + // +=============+==============================+================================================+ + // | uint16 | posFormat | Format identifier: format = 2 | + // +-------------+------------------------------+------------------------------------------------+ + // | Offset16 | coverageOffset | Offset to Coverage table, from beginning of | + // | | | PairPos subtable. | + // +-------------+------------------------------+------------------------------------------------+ + // | uint16 | valueFormat1 | Defines the types of data in valueRecord1 — | + // | | | for the first glyph in the pair (may be zero). | + // +-------------+------------------------------+------------------------------------------------+ + // | uint16 | valueFormat2 | Defines the types of data in valueRecord2 — | + // | | | for the second glyph in the pair (may be zero).| + // +-------------+------------------------------+------------------------------------------------+ + // | Offset16 | classDef1Offset | Offset to ClassDef table, from beginning of | + // | | | PairPos subtable — | + // | | | for the first glyph of the pair. | + // +-------------+------------------------------+------------------------------------------------+ + // | Offset16 | classDef2Offset | Offset to ClassDef table, from beginning of | + // | | | PairPos subtable — | + // | | | for the second glyph of the pair. — | + // +-------------+------------------------------+------------------------------------------------+ + // | uint16 | class1Count | Number of classes in classDef1 table — | + // | | | includes Class 0. | + // +-------------+------------------------------+------------------------------------------------+ + // | uint16 | class2Count | Number of classes in classDef2 table — | + // | | | includes Class 0. | + // +-------------+------------------------------+------------------------------------------------+ + // | Class1Record| class1Records[class1Count] | Array of Class1 records, | + // | | | ordered by classes in classDef1. | + // +-------------+------------------------------+------------------------------------------------+ + ushort coverageOffset = reader.ReadOffset16(); + ValueFormat valueFormat1 = reader.ReadUInt16(); + ValueFormat valueFormat2 = reader.ReadUInt16(); + ushort classDef1Offset = reader.ReadOffset16(); + ushort classDef2Offset = reader.ReadOffset16(); + ushort class1Count = reader.ReadUInt16(); + ushort class2Count = reader.ReadUInt16(); + + Class1Record[] class1Records = new Class1Record[class1Count]; + for (int i = 0; i < class1Records.Length; i++) + { + class1Records[i] = Class1Record.Load(reader, class2Count, valueFormat1, valueFormat2, offset); + } + + CoverageTable coverageTable = CoverageTable.Load(reader, offset + coverageOffset); + ClassDefinitionTable classDefTable1 = ClassDefinitionTable.Load(reader, offset + classDef1Offset); + ClassDefinitionTable classDefTable2 = ClassDefinitionTable.Load(reader, offset + classDef2Offset); + + return new LookupType2Format2SubTable(coverageTable, class1Records, classDefTable1, classDefTable2, lookupFlags, markFilteringSet); + } + + /// + public override bool TryUpdatePosition( + FontMetrics fontMetrics, + GPosTable table, + GlyphPositioningCollection collection, + Tag feature, + int index, + int count) + { + if (count <= 1) + { + return false; + } + + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + int coverage = this.coverageTable.CoverageIndexOf(glyphId); + if (coverage > -1) + { + int classDef1 = this.classDefinitionTable1.ClassIndexOf(glyphId); + ushort glyphId2 = collection[index + 1].GlyphId; + if (glyphId2 == 0) + { + return false; + } + + int classDef2 = this.classDefinitionTable2.ClassIndexOf(glyphId2); + + Class1Record class1Record = this.class1Records[classDef1]; + Class2Record class2Record = class1Record.Class2Records[classDef2]; + + ValueRecord record1 = class2Record.ValueRecord1; + AdvancedTypographicUtils.ApplyPosition(fontMetrics, collection, index, record1, feature); + + ValueRecord record2 = class2Record.ValueRecord2; + AdvancedTypographicUtils.ApplyPosition(fontMetrics, collection, index + 1, record2, feature); + + return true; + } + + return false; + } + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs new file mode 100644 index 0000000..7a08fbb --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs @@ -0,0 +1,298 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// Cursive Attachment Positioning Subtable. + /// Some cursive fonts are designed so that adjacent glyphs join when rendered with their default positioning. + /// However, if positioning adjustments are needed to join the glyphs, a cursive attachment positioning (CursivePos) subtable can describe + /// how to connect the glyphs by aligning two anchor points: the designated exit point of a glyph, and the designated entry point of the following glyph. + /// + /// + internal static class LookupType3SubTable + { + /// + /// Loads the cursive attachment positioning subtable from the specified reader. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupSubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort posFormat = reader.ReadUInt16(); + + return posFormat switch + { + 1 => LookupType3Format1SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + _ => new NotImplementedSubTable(), + }; + } + + /// + /// Cursive Attachment Positioning Format 1: connects adjacent glyphs via entry and exit anchor points. + /// + /// + internal sealed class LookupType3Format1SubTable : LookupSubTable + { + private readonly CoverageTable coverageTable; + private readonly EntryExitAnchors[] entryExitAnchors; + + /// + /// Initializes a new instance of the class. + /// + /// The coverage table. + /// The array of entry/exit anchor pairs. + /// The lookup qualifiers. + /// The mark filtering set index. + public LookupType3Format1SubTable( + CoverageTable coverageTable, + EntryExitAnchors[] entryExitAnchors, + LookupFlags lookupFlags, + ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.coverageTable = coverageTable; + this.entryExitAnchors = entryExitAnchors; + } + + /// + /// Loads the Format 1 cursive attachment positioning subtable. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupType3Format1SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + // Cursive Attachment Positioning Format1. + // +--------------------+---------------------------------+------------------------------------------------------+ + // | Type | Name | Description | + // +====================+=================================+======================================================+ + // | uint16 | posFormat | Format identifier: format = 1 | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | Offset16 | coverageOffset | Offset to Coverage table, | + // | | | from beginning of CursivePos subtable. | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | uint16 | entryExitCount | Number of EntryExit records. | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | EntryExitRecord | entryExitRecord[entryExitCount] | Array of EntryExit records, in Coverage index order. | + // +--------------------+---------------------------------+------------------------------------------------------+ + ushort coverageOffset = reader.ReadOffset16(); + ushort entryExitCount = reader.ReadUInt16(); + EntryExitRecord[] entryExitRecords = new EntryExitRecord[entryExitCount]; + for (int i = 0; i < entryExitCount; i++) + { + entryExitRecords[i] = new EntryExitRecord(reader, offset); + } + + EntryExitAnchors[] entryExitAnchors = new EntryExitAnchors[entryExitCount]; + for (int i = 0; i < entryExitCount; i++) + { + entryExitAnchors[i] = new EntryExitAnchors(reader, offset, entryExitRecords[i]); + } + + CoverageTable coverageTable = CoverageTable.Load(reader, offset + coverageOffset); + + return new LookupType3Format1SubTable(coverageTable, entryExitAnchors, lookupFlags, markFilteringSet); + } + + /// + public override bool TryUpdatePosition( + FontMetrics fontMetrics, + GPosTable table, + GlyphPositioningCollection collection, + Tag feature, + int index, + int count) + { + if (count <= 1) + { + return false; + } + + // Implements Cursive Attachment Positioning Subtable: + // https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-3-cursive-attachment-positioning-subtable + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + int nextIndex = index + 1; + ushort nextGlyphId = collection[nextIndex].GlyphId; + if (nextGlyphId == 0) + { + return false; + } + + int coverageNext = this.coverageTable.CoverageIndexOf(nextGlyphId); + if (coverageNext < 0 || coverageNext >= this.entryExitAnchors.Length) + { + return false; + } + + EntryExitAnchors nextRecord = this.entryExitAnchors[coverageNext]; + AnchorTable? entry = nextRecord.EntryAnchor; + if (entry is null) + { + return false; + } + + int coverage = this.coverageTable.CoverageIndexOf(glyphId); + if (coverage < 0 || coverage >= this.entryExitAnchors.Length) + { + return false; + } + + EntryExitAnchors curRecord = this.entryExitAnchors[coverage]; + AnchorTable? exit = curRecord.ExitAnchor; + if (exit is null) + { + return false; + } + + GlyphShapingData current = collection[index]; + GlyphShapingData next = collection[nextIndex]; + + AnchorXY exitXY = exit.GetAnchor(fontMetrics, current, collection); + AnchorXY entryXY = entry.GetAnchor(fontMetrics, next, collection); + + bool isVerticalLayout = AdvancedTypographicUtils.IsVerticalGlyph(current.CodePoint, collection.TextOptions.LayoutMode); + if (!isVerticalLayout) + { + // Horizontal + if (current.Direction == TextDirection.LeftToRight) + { + current.Bounds.Width = exitXY.XCoordinate + current.Bounds.X; + + int delta = entryXY.XCoordinate + next.Bounds.X; + next.Bounds.Width -= delta; + next.Bounds.X -= delta; + } + else + { + int delta = exitXY.XCoordinate + current.Bounds.X; + current.Bounds.Width -= delta; + current.Bounds.X -= delta; + + next.Bounds.Width = entryXY.XCoordinate + next.Bounds.X; + } + } + else + { + // Vertical layout modes advance top-to-bottom; column progression is handled by layout. + current.Bounds.Height = exitXY.YCoordinate + current.Bounds.Y; + + int delta = entryXY.YCoordinate + next.Bounds.Y; + next.Bounds.Height -= delta; + next.Bounds.Y -= delta; + } + + int child = index; + int parent = nextIndex; + int xOffset = entryXY.XCoordinate - exitXY.XCoordinate; + int yOffset = entryXY.YCoordinate - exitXY.YCoordinate; + if ((this.LookupFlags & LookupFlags.RightToLeft) != LookupFlags.RightToLeft) + { + (parent, child) = (child, parent); + + xOffset = -xOffset; + yOffset = -yOffset; + } + + // If child was already connected to someone else, walk through its old + // chain and reverse the link direction, such that the whole tree of its + // previous connection now attaches to new parent.Watch out for case + // where new parent is on the path from old chain... + bool horizontal = !isVerticalLayout; + ReverseCursiveMinorOffset(collection, index, child, horizontal, parent); + + GlyphShapingData c = collection[child]; + c.CursiveAttachment = parent - child; + if (horizontal) + { + c.Bounds.Y = yOffset; + } + else + { + c.Bounds.X = xOffset; + } + + // If parent was attached to child, separate them. + // https://github.com/harfbuzz/harfbuzz/issues/2469 + GlyphShapingData p = collection[parent]; + if (p.CursiveAttachment == -c.CursiveAttachment) + { + p.CursiveAttachment = 0; + + // Bounds.X/Y carry shaping placement offsets here, matching + // HarfBuzz x_offset/y_offset. Clear only the detached parent's minor axis. + if (horizontal) + { + p.Bounds.Y = 0; + } + else + { + p.Bounds.X = 0; + } + } + + return true; + } + + /// + /// Recursively reverses the cursive minor offset chain so that the entire tree + /// of a previous connection attaches to the new parent. + /// + /// The glyph positioning collection. + /// The original glyph position that initiated the chain reversal. + /// The current index in the chain being reversed. + /// Whether the layout is horizontal. + /// The new parent index to stop at. + private static void ReverseCursiveMinorOffset( + GlyphPositioningCollection collection, + int position, + int i, + bool horizontal, + int parent) + { + GlyphShapingData c = collection[i]; + int chain = c.CursiveAttachment; + if (chain <= 0) + { + return; + } + + c.CursiveAttachment = 0; + + int j = i + chain; + + // Stop if we see new parent in the chain. + if (j == parent) + { + return; + } + + ReverseCursiveMinorOffset(collection, position, j, horizontal, parent); + + GlyphShapingData p = collection[j]; + if (horizontal) + { + p.Bounds.Y = -c.Bounds.Y; + } + else + { + p.Bounds.X = -c.Bounds.X; + } + + p.CursiveAttachment = -chain; + } + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs new file mode 100644 index 0000000..7a67661 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs @@ -0,0 +1,167 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// Mark-to-Base Attachment Positioning Subtable. The MarkToBase attachment (MarkBasePos) subtable is used to position combining mark glyphs with respect to base glyphs. + /// For example, the Arabic, Hebrew, and Thai scripts combine vowels, diacritical marks, and tone marks with base glyphs. + /// + /// + internal static class LookupType4SubTable + { + /// + /// Loads the mark-to-base attachment positioning subtable from the specified reader. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupSubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort format = reader.ReadUInt16(); + + return format switch + { + 1 => LookupType4Format1SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + _ => new NotImplementedSubTable(), + }; + } + + /// + /// MarkToBase Attachment Positioning Format 1: positions combining mark glyphs relative to base glyphs. + /// + /// + internal sealed class LookupType4Format1SubTable : LookupSubTable + { + private readonly CoverageTable markCoverage; + private readonly CoverageTable baseCoverage; + private readonly MarkArrayTable markArrayTable; + private readonly BaseArrayTable baseArrayTable; + + /// + /// Initializes a new instance of the class. + /// + /// The mark coverage table. + /// The base glyph coverage table. + /// The mark array table. + /// The base array table. + /// The lookup qualifiers. + /// The mark filtering set index. + public LookupType4Format1SubTable( + CoverageTable markCoverage, + CoverageTable baseCoverage, + MarkArrayTable markArrayTable, + BaseArrayTable baseArrayTable, + LookupFlags lookupFlags, + ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.markCoverage = markCoverage; + this.baseCoverage = baseCoverage; + this.markArrayTable = markArrayTable; + this.baseArrayTable = baseArrayTable; + } + + /// + /// Loads the Format 1 mark-to-base attachment positioning subtable. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupType4Format1SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + // MarkBasePosFormat1 Subtable. + // +--------------------+---------------------------------+------------------------------------------------------+ + // | Type | Name | Description | + // +====================+=================================+======================================================+ + // | uint16 | posFormat | Format identifier: format = 1 | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | Offset16 | markCoverageOffset | Offset to markCoverage table, | + // | | | from beginning of MarkBasePos subtable. | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | Offset16 | baseCoverageOffset | Offset to baseCoverage table, | + // | | | from beginning of MarkBasePos subtable. | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | uint16 | markClassCount | Number of classes defined for marks. | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | Offset16 | markArrayOffset | Offset to MarkArray table, | + // | | | from beginning of MarkBasePos subtable. | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | Offset16 | baseArrayOffset | Offset to BaseArray table, | + // | | | from beginning of MarkBasePos subtable. | + // +--------------------+---------------------------------+------------------------------------------------------+ + ushort markCoverageOffset = reader.ReadOffset16(); + ushort baseCoverageOffset = reader.ReadOffset16(); + ushort markClassCount = reader.ReadUInt16(); + ushort markArrayOffset = reader.ReadOffset16(); + ushort baseArrayOffset = reader.ReadOffset16(); + + CoverageTable markCoverage = CoverageTable.Load(reader, offset + markCoverageOffset); + CoverageTable baseCoverage = CoverageTable.Load(reader, offset + baseCoverageOffset); + MarkArrayTable markArrayTable = new(reader, offset + markArrayOffset); + BaseArrayTable baseArrayTable = new(reader, offset + baseArrayOffset, markClassCount); + + return new LookupType4Format1SubTable(markCoverage, baseCoverage, markArrayTable, baseArrayTable, lookupFlags, markFilteringSet); + } + + /// + public override bool TryUpdatePosition( + FontMetrics fontMetrics, + GPosTable table, + GlyphPositioningCollection collection, + Tag feature, + int index, + int count) + { + // Mark-to-Base Attachment Positioning Subtable. + // Implements: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-4-mark-to-base-attachment-positioning-subtable + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + int markIndex = this.markCoverage.CoverageIndexOf(glyphId); + if (markIndex < 0 || markIndex >= this.markArrayTable.MarkRecords.Length) + { + return false; + } + + // Search backward for a base glyph. + int baseGlyphIndex = index; + while (--baseGlyphIndex >= 0) + { + GlyphShapingData data = collection[baseGlyphIndex]; + if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphId, data) && data.LigatureComponent <= 0) + { + break; + } + } + + if (baseGlyphIndex < 0) + { + return false; + } + + ushort baseGlyphId = collection[baseGlyphIndex].GlyphId; + int baseIndex = this.baseCoverage.CoverageIndexOf(baseGlyphId); + if (baseIndex < 0 || baseIndex >= this.baseArrayTable.BaseRecords.Length) + { + return false; + } + + MarkRecord markRecord = this.markArrayTable.MarkRecords[markIndex]; + AnchorTable baseAnchor = this.baseArrayTable.BaseRecords[baseIndex].BaseAnchorTables[markRecord.MarkClass]; + AdvancedTypographicUtils.ApplyAnchor(fontMetrics, collection, index, baseAnchor, markRecord, baseGlyphIndex, feature); + + return true; + } + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs new file mode 100644 index 0000000..81a860d --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs @@ -0,0 +1,182 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// Mark-to-Ligature Attachment Positioning Subtable. + /// The MarkToLigature attachment (MarkLigPos) subtable is used to position combining mark glyphs with respect to ligature base glyphs. + /// With MarkToBase attachment, described previously, each base glyph has an attachment point defined for each class of marks. + /// MarkToLigature attachment is similar, except that each ligature glyph is defined to have multiple components (in a virtual sense — not actual glyphs), + /// and each component has a separate set of attachment points defined for the different mark classes. + /// + /// + internal static class LookupType5SubTable + { + /// + /// Loads the mark-to-ligature attachment positioning subtable from the specified reader. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupSubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort subTableFormat = reader.ReadUInt16(); + + return subTableFormat switch + { + 1 => LookupType5Format1SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + _ => new NotImplementedSubTable(), + }; + } + + /// + /// MarkToLigature Attachment Positioning Format 1: positions combining mark glyphs relative to ligature glyph components. + /// + /// + internal sealed class LookupType5Format1SubTable : LookupSubTable + { + private readonly CoverageTable markCoverage; + private readonly CoverageTable ligatureCoverage; + private readonly MarkArrayTable markArrayTable; + private readonly LigatureArrayTable ligatureArrayTable; + + /// + /// Initializes a new instance of the class. + /// + /// The mark coverage table. + /// The ligature coverage table. + /// The mark array table. + /// The ligature array table. + /// The lookup qualifiers. + /// The mark filtering set index. + public LookupType5Format1SubTable( + CoverageTable markCoverage, + CoverageTable ligatureCoverage, + MarkArrayTable markArrayTable, + LigatureArrayTable ligatureArrayTable, + LookupFlags lookupFlags, + ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.markCoverage = markCoverage; + this.ligatureCoverage = ligatureCoverage; + this.markArrayTable = markArrayTable; + this.ligatureArrayTable = ligatureArrayTable; + } + + /// + /// Loads the Format 1 mark-to-ligature attachment positioning subtable. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupType5Format1SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + // MarkLigPosFormat1 Subtable. + // +--------------------+---------------------------------+------------------------------------------------------+ + // | Type | Name | Description | + // +====================+=================================+======================================================+ + // | uint16 | posFormat | Format identifier: format = 1 | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | Offset16 | markCoverageOffset | Offset to markCoverage table, | + // | | | from beginning of MarkLigPos subtable. | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | Offset16 | ligatureCoverageOffset | Offset to ligatureCoverage table, | + // | | | from beginning of MarkLigPos subtable. | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | uint16 | markClassCount | Number of defined mark classes | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | Offset16 | markArrayOffset | Offset to MarkArray table, from beginning | + // | | | of MarkLigPos subtable. | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | Offset16 | ligatureArrayOffset | Offset to LigatureArray table, | + // | | | from beginning of MarkLigPos subtable. | + // +--------------------+---------------------------------+------------------------------------------------------+ + ushort markCoverageOffset = reader.ReadOffset16(); + ushort ligatureCoverageOffset = reader.ReadOffset16(); + ushort markClassCount = reader.ReadUInt16(); + ushort markArrayOffset = reader.ReadOffset16(); + ushort ligatureArrayOffset = reader.ReadOffset16(); + + CoverageTable markCoverage = CoverageTable.Load(reader, offset + markCoverageOffset); + CoverageTable ligatureCoverage = CoverageTable.Load(reader, offset + ligatureCoverageOffset); + MarkArrayTable markArrayTable = new(reader, offset + markArrayOffset); + LigatureArrayTable ligatureArrayTable = new(reader, offset + ligatureArrayOffset, markClassCount); + + return new LookupType5Format1SubTable(markCoverage, ligatureCoverage, markArrayTable, ligatureArrayTable, lookupFlags, markFilteringSet); + } + + /// + public override bool TryUpdatePosition( + FontMetrics fontMetrics, + GPosTable table, + GlyphPositioningCollection collection, + Tag feature, + int index, + int count) + { + // Mark-to-Ligature Attachment Positioning. + // Implements: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-5-mark-to-ligature-attachment-positioning-subtable + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + int markIndex = this.markCoverage.CoverageIndexOf(glyphId); + if (markIndex < 0 || markIndex >= this.markArrayTable.MarkRecords.Length) + { + return false; + } + + // Search backward for a base glyph. + int baseGlyphIndex = index; + while (--baseGlyphIndex >= 0) + { + GlyphShapingData data = collection[baseGlyphIndex]; + if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphId, data)) + { + break; + } + } + + if (baseGlyphIndex < 0) + { + return false; + } + + ushort baseGlyphId = collection[baseGlyphIndex].GlyphId; + int ligatureIndex = this.ligatureCoverage.CoverageIndexOf(baseGlyphId); + if (ligatureIndex < 0 || ligatureIndex >= this.ligatureArrayTable.LigatureAttachTables.Length) + { + return false; + } + + // We must now check whether the ligature ID of the current mark glyph + // is identical to the ligature ID of the found ligature. + // If yes, we can directly use the component index. If not, we attach the mark + // glyph to the last component of the ligature. + LigatureAttachTable ligatureAttach = this.ligatureArrayTable.LigatureAttachTables[ligatureIndex]; + GlyphShapingData markGlyph = collection[index]; + GlyphShapingData ligGlyph = collection[baseGlyphIndex]; + int compIndex = ligGlyph.LigatureId > 0 && ligGlyph.LigatureId == markGlyph.LigatureId && markGlyph.LigatureComponent > 0 + ? Math.Min(markGlyph.LigatureComponent, ligGlyph.CodePointCount) - 1 + : ligGlyph.CodePointCount - 1; + + MarkRecord markRecord = this.markArrayTable.MarkRecords[markIndex]; + AnchorTable baseAnchor = ligatureAttach.ComponentRecords[compIndex].LigatureAnchorTables[markRecord.MarkClass]; + AdvancedTypographicUtils.ApplyAnchor(fontMetrics, collection, index, baseAnchor, markRecord, baseGlyphIndex, feature); + + return true; + } + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs new file mode 100644 index 0000000..1e5242c --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs @@ -0,0 +1,203 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// Lookup Type 6: Mark-to-Mark Attachment Positioning Subtable. + /// The MarkToMark attachment (MarkMarkPos) subtable is identical in form to the MarkToBase attachment subtable, although its function is different. + /// MarkToMark attachment defines the position of one mark relative to another mark as when, for example, + /// positioning tone marks with respect to vowel diacritical marks in Vietnamese. + /// + /// + internal static class LookupType6SubTable + { + /// + /// Loads the mark-to-mark attachment positioning subtable from the specified reader. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupSubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort subTableFormat = reader.ReadUInt16(); + + return subTableFormat switch + { + 1 => LookupType6Format1SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + _ => new NotImplementedSubTable(), + }; + } + + /// + /// MarkToMark Attachment Positioning Format 1: positions one mark relative to another mark. + /// + /// + internal sealed class LookupType6Format1SubTable : LookupSubTable + { + private readonly CoverageTable mark1Coverage; + private readonly CoverageTable mark2Coverage; + private readonly MarkArrayTable mark1ArrayTable; + private readonly Mark2ArrayTable mark2ArrayTable; + + /// + /// Initializes a new instance of the class. + /// + /// The combining mark (mark1) coverage table. + /// The base mark (mark2) coverage table. + /// The mark1 array table. + /// The mark2 array table. + /// The lookup qualifiers. + /// The mark filtering set index. + public LookupType6Format1SubTable( + CoverageTable mark1Coverage, + CoverageTable mark2Coverage, + MarkArrayTable mark1ArrayTable, + Mark2ArrayTable mark2ArrayTable, + LookupFlags lookupFlags, + ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.mark1Coverage = mark1Coverage; + this.mark2Coverage = mark2Coverage; + this.mark1ArrayTable = mark1ArrayTable; + this.mark2ArrayTable = mark2ArrayTable; + } + + /// + /// Loads the Format 1 mark-to-mark attachment positioning subtable. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupType6Format1SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + // MarkMarkPosFormat1 Subtable. + // +--------------------+---------------------------------+------------------------------------------------------+ + // | Type | Name | Description | + // +====================+=================================+======================================================+ + // | uint16 | posFormat | Format identifier: format = 1 | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | Offset16 | mark1CoverageOffset | Offset to Combining Mark Coverage table, | + // | | | from beginning of MarkMarkPos subtable. | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | Offset16 | mark2CoverageOffset | Offset to Base Mark Coverage table, | + // | | | from beginning of MarkMarkPos subtable. | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | uint16 | markClassCount | Number of Combining Mark classes defined | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | Offset16 | mark1ArrayOffset | Offset to MarkArray table for mark1, | + // | | | from beginning of MarkMarkPos subtable. | + // +--------------------+---------------------------------+------------------------------------------------------+ + // | Offset16 | mark2ArrayOffset | Offset to Mark2Array table for mark2, | + // | | | from beginning of MarkMarkPos subtable. | + // +--------------------+---------------------------------+------------------------------------------------------+ + ushort mark1CoverageOffset = reader.ReadOffset16(); + ushort mark2CoverageOffset = reader.ReadOffset16(); + ushort markClassCount = reader.ReadUInt16(); + ushort mark1ArrayOffset = reader.ReadOffset16(); + ushort mark2ArrayOffset = reader.ReadOffset16(); + + CoverageTable mark1Coverage = CoverageTable.Load(reader, offset + mark1CoverageOffset); + CoverageTable mark2Coverage = CoverageTable.Load(reader, offset + mark2CoverageOffset); + MarkArrayTable mark1ArrayTable = new(reader, offset + mark1ArrayOffset); + Mark2ArrayTable mark2ArrayTable = new(reader, markClassCount, offset + mark2ArrayOffset); + + return new LookupType6Format1SubTable(mark1Coverage, mark2Coverage, mark1ArrayTable, mark2ArrayTable, lookupFlags, markFilteringSet); + } + + /// + public override bool TryUpdatePosition( + FontMetrics fontMetrics, + GPosTable table, + GlyphPositioningCollection collection, + Tag feature, + int index, + int count) + { + // Mark to mark positioning. + // Implements: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-6-mark-to-mark-attachment-positioning-subtable + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + int mark1Index = this.mark1Coverage.CoverageIndexOf(glyphId); + if (mark1Index < 0 || mark1Index >= this.mark1ArrayTable.MarkRecords.Length) + { + return false; + } + + // Get the previous mark to attach to. + // HarfBuzz: search backwards for a suitable mark glyph until a non-mark glyph. + // It clears ignore flags when searching, but keeps mark attachment / filtering behavior. + LookupFlags searchFlags = this.LookupFlags & ~(LookupFlags.IgnoreMarks | LookupFlags.IgnoreBaseGlyphs | LookupFlags.IgnoreLigatures); + + SkippingGlyphIterator it = new(fontMetrics, collection, index, searchFlags, this.MarkFilteringSet); + + int j = it.Prev(); + if (j < 0) + { + return false; + } + + GlyphShapingData prevGlyph = collection[j]; + if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, prevGlyph.GlyphId, prevGlyph)) + { + return false; + } + + GlyphShapingData curGlyph = collection[index]; + + bool good; + int id1 = curGlyph.LigatureId; + int id2 = prevGlyph.LigatureId; + int comp1 = curGlyph.LigatureComponent; + int comp2 = prevGlyph.LigatureComponent; + + if (id1 == id2) + { + if (id1 == 0) + { + // Marks belonging to the same base. + good = true; + } + else + { + // Marks belonging to the same ligature component. + good = comp1 == comp2; + } + } + else + { + // If ligature ids don't match, one of the marks itself may be a ligature. + good = (id1 > 0 && comp1 <= 0) || (id2 > 0 && comp2 <= 0); + } + + if (!good) + { + return false; + } + + int mark2Index = this.mark2Coverage.CoverageIndexOf(prevGlyph.GlyphId); + if (mark2Index < 0 || mark2Index >= this.mark2ArrayTable.Mark2Records.Length) + { + return false; + } + + MarkRecord markRecord = this.mark1ArrayTable.MarkRecords[mark1Index]; + AnchorTable? baseAnchor = this.mark2ArrayTable.Mark2Records[mark2Index].MarkAnchorTable[markRecord.MarkClass]; + AdvancedTypographicUtils.ApplyAnchor(fontMetrics, collection, index, baseAnchor, markRecord, j, feature); + + return true; + } + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs new file mode 100644 index 0000000..d4ceffc --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs @@ -0,0 +1,319 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// Lookup Type 7: Contextual Positioning Subtables. + /// A Contextual Positioning subtable describes glyph positioning in context so a text-processing client can adjust the position + /// of one or more glyphs within a certain pattern of glyphs. + /// + /// + internal static class LookupType7SubTable + { + /// + /// Loads the contextual positioning subtable from the specified reader. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupSubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort subTableFormat = reader.ReadUInt16(); + + return subTableFormat switch + { + 1 => LookupType7Format1SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 2 => LookupType7Format2SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 3 => LookupType7Format3SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + _ => new NotImplementedSubTable(), + }; + } + + /// + /// Context Positioning Format 1: simple glyph contexts using individual glyph indices. + /// + internal sealed class LookupType7Format1SubTable : LookupSubTable + { + private readonly CoverageTable coverageTable; + private readonly SequenceRuleSetTable[] seqRuleSetTables; + + /// + /// Initializes a new instance of the class. + /// + /// The coverage table. + /// The array of sequence rule set tables. + /// The lookup qualifiers. + /// The mark filtering set index. + public LookupType7Format1SubTable( + CoverageTable coverageTable, + SequenceRuleSetTable[] seqRuleSetTables, + LookupFlags lookupFlags, + ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.seqRuleSetTables = seqRuleSetTables; + this.coverageTable = coverageTable; + } + + /// + /// Loads the Format 1 contextual positioning subtable. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupType7Format1SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + SequenceRuleSetTable[] seqRuleSets = TableLoadingUtils.LoadSequenceContextFormat1(reader, offset, out CoverageTable coverageTable); + + return new LookupType7Format1SubTable(coverageTable, seqRuleSets, lookupFlags, markFilteringSet); + } + + /// + public override bool TryUpdatePosition( + FontMetrics fontMetrics, + GPosTable table, + GlyphPositioningCollection collection, + Tag feature, + int index, + int count) + { + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + int offset = this.coverageTable.CoverageIndexOf(glyphId); + if (offset < 0 || offset >= this.seqRuleSetTables.Length) + { + return false; + } + + // TODO: Check this. + // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#example-7-contextual-substitution-format-1 + SequenceRuleSetTable ruleSetTable = this.seqRuleSetTables[offset]; + SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + foreach (SequenceRuleTable ruleTable in ruleSetTable.SequenceRuleTables) + { + int remaining = count - 1; + int seqLength = ruleTable.InputSequence.Length; + if (seqLength > remaining) + { + continue; + } + + if (!AdvancedTypographicUtils.MatchSequence(iterator, 1, ruleTable.InputSequence)) + { + continue; + } + + // It's a match. Perform position update and return true if anything changed. + return AdvancedTypographicUtils.ApplyLookupList( + fontMetrics, + table, + feature, + this.LookupFlags, + this.MarkFilteringSet, + ruleTable.SequenceLookupRecords, + collection, + index, + count); + } + + return false; + } + } + + /// + /// Context Positioning Format 2: class-based glyph contexts. + /// + internal sealed class LookupType7Format2SubTable : LookupSubTable + { + private readonly CoverageTable coverageTable; + private readonly ClassDefinitionTable classDefinitionTable; + private readonly ClassSequenceRuleSetTable[] sequenceRuleSetTables; + + /// + /// Initializes a new instance of the class. + /// + /// The coverage table. + /// The class definition table. + /// The array of class sequence rule set tables. + /// The lookup qualifiers. + /// The mark filtering set index. + public LookupType7Format2SubTable( + CoverageTable coverageTable, + ClassDefinitionTable classDefinitionTable, + ClassSequenceRuleSetTable[] sequenceRuleSetTables, + LookupFlags lookupFlags, + ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.coverageTable = coverageTable; + this.classDefinitionTable = classDefinitionTable; + this.sequenceRuleSetTables = sequenceRuleSetTables; + } + + /// + /// Loads the Format 2 contextual positioning subtable. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupType7Format2SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + CoverageTable coverageTable = + TableLoadingUtils.LoadSequenceContextFormat2( + reader, + offset, + out ClassDefinitionTable classDefTable, + out ClassSequenceRuleSetTable[] classSeqRuleSets); + + return new LookupType7Format2SubTable(coverageTable, classDefTable, classSeqRuleSets, lookupFlags, markFilteringSet); + } + + /// + public override bool TryUpdatePosition( + FontMetrics fontMetrics, + GPosTable table, + GlyphPositioningCollection collection, + Tag feature, + int index, + int count) + { + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + if (this.coverageTable.CoverageIndexOf(glyphId) < 0) + { + return false; + } + + int offset = this.classDefinitionTable.ClassIndexOf(glyphId); + if (offset < 0 || offset >= this.sequenceRuleSetTables.Length) + { + return false; + } + + ClassSequenceRuleSetTable ruleSetTable = this.sequenceRuleSetTables[offset]; + SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + foreach (ClassSequenceRuleTable ruleTable in ruleSetTable.SequenceRuleTables) + { + int remaining = count - 1; + int seqLength = ruleTable.InputSequence.Length; + if (seqLength > remaining) + { + continue; + } + + if (!AdvancedTypographicUtils.MatchClassSequence(iterator, 1, ruleTable.InputSequence, this.classDefinitionTable)) + { + continue; + } + + // It's a match. Perform position update and return true if anything changed. + return AdvancedTypographicUtils.ApplyLookupList( + fontMetrics, + table, + feature, + this.LookupFlags, + this.MarkFilteringSet, + ruleTable.SequenceLookupRecords, + collection, + index, + count); + } + + return false; + } + } + + /// + /// Context Positioning Format 3: coverage-based glyph contexts. + /// + internal sealed class LookupType7Format3SubTable : LookupSubTable + { + private readonly CoverageTable[] coverageTables; + private readonly SequenceLookupRecord[] sequenceLookupRecords; + + /// + /// Initializes a new instance of the class. + /// + /// The array of coverage tables, one per glyph in the input sequence. + /// The array of sequence lookup records. + /// The lookup qualifiers. + /// The mark filtering set index. + public LookupType7Format3SubTable( + CoverageTable[] coverageTables, + SequenceLookupRecord[] sequenceLookupRecords, + LookupFlags lookupFlags, + ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.coverageTables = coverageTables; + this.sequenceLookupRecords = sequenceLookupRecords; + } + + /// + /// Loads the Format 3 contextual positioning subtable. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupType7Format3SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + SequenceLookupRecord[] seqLookupRecords = + TableLoadingUtils.LoadSequenceContextFormat3(reader, offset, out CoverageTable[] coverageTables); + + return new LookupType7Format3SubTable(coverageTables, seqLookupRecords, lookupFlags, markFilteringSet); + } + + /// + public override bool TryUpdatePosition( + FontMetrics fontMetrics, + GPosTable table, + GlyphPositioningCollection collection, + Tag feature, + int index, + int count) + { + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + if (!AdvancedTypographicUtils.MatchCoverageSequence(iterator, this.coverageTables, index, index + count)) + { + return false; + } + + return AdvancedTypographicUtils.ApplyLookupList( + fontMetrics, + table, + feature, + this.LookupFlags, + this.MarkFilteringSet, + this.sequenceLookupRecords, + collection, + index, + count); + } + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs new file mode 100644 index 0000000..5b2dd75 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs @@ -0,0 +1,375 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// LookupType 8: Chained Contexts Positioning Subtable. + /// A Chained Contexts Positioning subtable describes glyph positioning in context with an ability to look back and/or look ahead in the sequence of glyphs. + /// The design of the Chained Contexts Positioning subtable is parallel to that of the Contextual Positioning subtable, including the availability of three formats. + /// Each format can describe one or more chained backtrack, input, and lookahead sequence combinations, and one or more positioning adjustments for glyphs in each input sequence. + /// + /// + internal static class LookupType8SubTable + { + /// + /// Loads the chaining context positioning subtable from the specified reader. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupSubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort substFormat = reader.ReadUInt16(); + + return substFormat switch + { + 1 => LookupType8Format1SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 2 => LookupType8Format2SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 3 => LookupType8Format3SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + _ => new NotImplementedSubTable(), + }; + } + + /// + /// Chained Context Positioning Format 1: simple glyph contexts. + /// + internal sealed class LookupType8Format1SubTable : LookupSubTable + { + private readonly CoverageTable coverageTable; + private readonly ChainedSequenceRuleSetTable[] seqRuleSetTables; + + /// + /// Initializes a new instance of the class. + /// + /// The coverage table. + /// The array of chained sequence rule set tables. + /// The lookup qualifiers. + /// The mark filtering set index. + private LookupType8Format1SubTable( + CoverageTable coverageTable, + ChainedSequenceRuleSetTable[] seqRuleSetTables, + LookupFlags lookupFlags, + ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.coverageTable = coverageTable; + this.seqRuleSetTables = seqRuleSetTables; + } + + /// + /// Loads the Format 1 chained context positioning subtable. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupType8Format1SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + ChainedSequenceRuleSetTable[] seqRuleSets = + TableLoadingUtils.LoadChainedSequenceContextFormat1(reader, offset, out CoverageTable coverageTable); + + return new LookupType8Format1SubTable(coverageTable, seqRuleSets, lookupFlags, markFilteringSet); + } + + /// + public override bool TryUpdatePosition( + FontMetrics fontMetrics, + GPosTable table, + GlyphPositioningCollection collection, + Tag feature, + int index, + int count) + { + // Implements Chained Contexts Substitution, Format 1: + // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#61-chained-contexts-substitution-format-1-simple-glyph-contexts + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + // Search for the current glyph in the Coverage table. + int offset = this.coverageTable.CoverageIndexOf(glyphId); + if (offset < 0 || offset >= this.seqRuleSetTables?.Length) + { + return false; + } + + if (this.seqRuleSetTables is null || this.seqRuleSetTables.Length is 0) + { + return false; + } + + ChainedSequenceRuleSetTable seqRuleSet = this.seqRuleSetTables[offset]; + if (seqRuleSet is null) + { + return false; + } + + // Apply ruleset for the given glyph id. + ChainedSequenceRuleTable[] rules = seqRuleSet.SequenceRuleTables; + SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + for (int lookupIndex = 0; lookupIndex < rules.Length; lookupIndex++) + { + ChainedSequenceRuleTable rule = rules[lookupIndex]; + if (!AdvancedTypographicUtils.ApplyChainedSequenceRule(iterator, rule)) + { + continue; + } + + bool hasChanged = false; + for (int j = 0; j < rule.SequenceLookupRecords.Length; j++) + { + SequenceLookupRecord sequenceLookupRecord = rule.SequenceLookupRecords[j]; + LookupTable lookup = table.LookupList.LookupTables[sequenceLookupRecord.LookupListIndex]; + ushort sequenceIndex = sequenceLookupRecord.SequenceIndex; + if (lookup.TryUpdatePosition(fontMetrics, table, collection, feature, index + sequenceIndex, 1)) + { + hasChanged = true; + } + } + + return hasChanged; + } + + return false; + } + } + + /// + /// Chained Context Positioning Format 2: class-based glyph contexts. + /// + internal sealed class LookupType8Format2SubTable : LookupSubTable + { + private readonly CoverageTable coverageTable; + private readonly ClassDefinitionTable inputClassDefinitionTable; + private readonly ClassDefinitionTable backtrackClassDefinitionTable; + private readonly ClassDefinitionTable lookaheadClassDefinitionTable; + private readonly ChainedClassSequenceRuleSetTable[] sequenceRuleSetTables; + + /// + /// Initializes a new instance of the class. + /// + /// The array of chained class sequence rule set tables. + /// The backtrack class definition table. + /// The input class definition table. + /// The lookahead class definition table. + /// The coverage table. + /// The lookup qualifiers. + /// The mark filtering set index. + private LookupType8Format2SubTable( + ChainedClassSequenceRuleSetTable[] sequenceRuleSetTables, + ClassDefinitionTable backtrackClassDefinitionTable, + ClassDefinitionTable inputClassDefinitionTable, + ClassDefinitionTable lookaheadClassDefinitionTable, + CoverageTable coverageTable, + LookupFlags lookupFlags, + ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.sequenceRuleSetTables = sequenceRuleSetTables; + this.backtrackClassDefinitionTable = backtrackClassDefinitionTable; + this.inputClassDefinitionTable = inputClassDefinitionTable; + this.lookaheadClassDefinitionTable = lookaheadClassDefinitionTable; + this.coverageTable = coverageTable; + } + + /// + /// Loads the Format 2 chained context positioning subtable. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupType8Format2SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + ChainedClassSequenceRuleSetTable[] seqRuleSets = TableLoadingUtils.LoadChainedSequenceContextFormat2( + reader, + offset, + out CoverageTable coverageTable, + out ClassDefinitionTable backtrackClassDefTable, + out ClassDefinitionTable inputClassDefTable, + out ClassDefinitionTable lookaheadClassDefTable); + + return new LookupType8Format2SubTable( + seqRuleSets, + backtrackClassDefTable, + inputClassDefTable, + lookaheadClassDefTable, + coverageTable, + lookupFlags, + markFilteringSet); + } + + /// + public override bool TryUpdatePosition( + FontMetrics fontMetrics, + GPosTable table, + GlyphPositioningCollection collection, + Tag feature, + int index, + int count) + { + // Implements Chained Contexts Substitution for Format 2: + // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#62-chained-contexts-substitution-format-2-class-based-glyph-contexts + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + // Search for the current glyph in the Coverage table. + int offset = this.coverageTable.CoverageIndexOf(glyphId); + if (offset < 0) + { + return false; + } + + // Search in the class definition table to find the class value assigned to the currently glyph. + int classId = this.inputClassDefinitionTable.ClassIndexOf(glyphId); + ChainedClassSequenceRuleTable[]? rules = classId >= 0 && classId < this.sequenceRuleSetTables.Length + ? this.sequenceRuleSetTables[classId].SubRules + : null; + + if (rules is null) + { + return false; + } + + // Apply ruleset for the given glyph class id. + SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + for (int lookupIndex = 0; lookupIndex < rules.Length; lookupIndex++) + { + ChainedClassSequenceRuleTable rule = rules[lookupIndex]; + + if (!AdvancedTypographicUtils.ApplyChainedClassSequenceRule(iterator, rule, this.inputClassDefinitionTable, this.backtrackClassDefinitionTable, this.lookaheadClassDefinitionTable)) + { + continue; + } + + // It's a match. Perform position update and return true if anything changed. + bool hasChanged = false; + for (int j = 0; j < rule.SequenceLookupRecords.Length; j++) + { + SequenceLookupRecord sequenceLookupRecord = rule.SequenceLookupRecords[j]; + LookupTable lookup = table.LookupList.LookupTables[sequenceLookupRecord.LookupListIndex]; + ushort sequenceIndex = sequenceLookupRecord.SequenceIndex; + if (lookup.TryUpdatePosition(fontMetrics, table, collection, feature, index + sequenceIndex, 1)) + { + hasChanged = true; + } + } + + return hasChanged; + } + + return false; + } + } + + /// + /// Chained Context Positioning Format 3: coverage-based glyph contexts. + /// + internal sealed class LookupType8Format3SubTable : LookupSubTable + { + private readonly SequenceLookupRecord[] seqLookupRecords; + private readonly CoverageTable[] backtrackCoverageTables; + private readonly CoverageTable[] inputCoverageTables; + private readonly CoverageTable[] lookaheadCoverageTables; + + /// + /// Initializes a new instance of the class. + /// + /// The array of sequence lookup records. + /// The array of backtrack coverage tables. + /// The array of input coverage tables. + /// The array of lookahead coverage tables. + /// The lookup qualifiers. + /// The mark filtering set index. + private LookupType8Format3SubTable( + SequenceLookupRecord[] seqLookupRecords, + CoverageTable[] backtrackCoverageTables, + CoverageTable[] inputCoverageTables, + CoverageTable[] lookaheadCoverageTables, + LookupFlags lookupFlags, + ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.seqLookupRecords = seqLookupRecords; + this.backtrackCoverageTables = backtrackCoverageTables; + this.inputCoverageTables = inputCoverageTables; + this.lookaheadCoverageTables = lookaheadCoverageTables; + } + + /// + /// Loads the Format 3 chained context positioning subtable. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The loaded . + public static LookupType8Format3SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + SequenceLookupRecord[] seqLookupRecords = TableLoadingUtils.LoadChainedSequenceContextFormat3( + reader, + offset, + out CoverageTable[] backtrackCoverageTables, + out CoverageTable[] inputCoverageTables, + out CoverageTable[] lookaheadCoverageTables); + + return new LookupType8Format3SubTable( + seqLookupRecords, + backtrackCoverageTables, + inputCoverageTables, + lookaheadCoverageTables, + lookupFlags, + markFilteringSet); + } + + /// + public override bool TryUpdatePosition( + FontMetrics fontMetrics, + GPosTable table, + GlyphPositioningCollection collection, + Tag feature, + int index, + int count) + { + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + if (!AdvancedTypographicUtils.CheckAllCoverages(fontMetrics, this.LookupFlags, this.MarkFilteringSet, collection, index, count, this.inputCoverageTables, this.backtrackCoverageTables, this.lookaheadCoverageTables)) + { + return false; + } + + // It's a match. Perform position update and return true if anything changed. + bool hasChanged = false; + foreach (SequenceLookupRecord lookupRecord in this.seqLookupRecords) + { + ushort sequenceIndex = lookupRecord.SequenceIndex; + ushort lookupIndex = lookupRecord.LookupListIndex; + + LookupTable lookup = table.LookupList.LookupTables[lookupIndex]; + if (lookup.TryUpdatePosition(fontMetrics, table, collection, feature, index + sequenceIndex, count - sequenceIndex)) + { + hasChanged = true; + } + } + + return hasChanged; + } + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType9SubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType9SubTable.cs new file mode 100644 index 0000000..be903c8 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType9SubTable.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// This lookup provides a mechanism whereby any other lookup type’s subtables are stored at a 32-bit offset location in the GPOS table. + /// This is needed if the total size of the subtables exceeds the 16-bit limits of the various other offsets in the GPOS table. + /// In this specification, the subtable stored at the 32-bit offset location is termed the "extension" subtable. + /// + /// + internal static class LookupType9SubTable + { + /// + /// Loads the extension positioning subtable from the specified reader. + /// + /// The big endian binary reader. + /// The offset to the beginning of the subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The delegate used to load the referenced extension subtable. + /// The loaded . + public static LookupSubTable Load( + BigEndianBinaryReader reader, + long offset, + LookupFlags lookupFlags, + ushort markFilteringSet, + Func subTableLoader) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort substFormat = reader.ReadUInt16(); + + return substFormat switch + { + 1 => LookupType9Format1SubTable.Load(reader, offset, lookupFlags, markFilteringSet, subTableLoader), + _ => new NotImplementedSubTable(), + }; + } + } + + /// + /// Extension Positioning Format 1: provides a 32-bit offset to a subtable of any other GPOS lookup type. + /// + /// + internal static class LookupType9Format1SubTable + { + /// + /// Loads the Format 1 extension positioning subtable, which resolves to another lookup type via a 32-bit offset. + /// + /// The big endian binary reader. + /// The offset to the beginning of the extension subtable. + /// The lookup qualifiers. + /// The mark filtering set index. + /// The delegate used to load the referenced extension subtable. + /// The loaded . + public static LookupSubTable Load( + BigEndianBinaryReader reader, + long offset, + LookupFlags lookupFlags, + ushort markFilteringSet, + Func subTableLoader) + { + // +----------+---------------------+------------------------------------------------------------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+=====================+====================================================================================================================================+ + // | uint16 | substFormat | Format identifier. Set to 1. | + // +----------+---------------------+------------------------------------------------------------------------------------------------------------------------------------+ + // | uint16 | extensionLookupType | Lookup type of subtable referenced by extensionOffset (that is, the extension subtable). | + // +----------+---------------------+------------------------------------------------------------------------------------------------------------------------------------+ + // | Offset32 | extensionOffset | Offset to the extension subtable, of lookup type extensionLookupType, relative to the start of the ExtensionSubstFormat1 subtable. | + // +----------+---------------------+------------------------------------------------------------------------------------------------------------------------------------+ + ushort extensionLookupType = reader.ReadUInt16(); + uint extensionOffset = reader.ReadOffset32(); + + // The extensionLookupType field must be set to any lookup type other than 9. + // All subtables in a LookupType 9 lookup must have the same extensionLookupType. + if (extensionLookupType == 9) + { + // Don't throw, we'll just ignore. + return new NotImplementedSubTable(); + } + + // Read the lookup table again with the updated offset. + return subTableLoader(extensionLookupType, lookupFlags, markFilteringSet, reader, offset + extensionOffset); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/Mark2ArrayTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/Mark2ArrayTable.cs new file mode 100644 index 0000000..c1b8c4c --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/Mark2ArrayTable.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// Represents the Mark2Array table used in MarkToMark attachment positioning (GPOS LookupType 6). + /// The Mark2Array table contains an array of Mark2Records, one for each mark2 glyph (the base mark), ordered by the mark2 Coverage index. + /// + /// + internal class Mark2ArrayTable + { + /// + /// Initializes a new instance of the class. + /// + /// The big endian binary reader. + /// The number of mark classes. + /// The offset to the start of the mark array table. + public Mark2ArrayTable(BigEndianBinaryReader reader, ushort markClassCount, long offset) + { + // +--------------+------------------------+--------------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==============+========================+======================================================================================+ + // | uint16 | mark2Count | Number of Mark2Records. | + // +--------------+------------------------+--------------------------------------------------------------------------------------+ + // | Mark2Record | mark2Records[markCount]| Array of Mark2Records, in Coverage order. | + // +--------------+------------------------+--------------------------------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + ushort markCount = reader.ReadUInt16(); + this.Mark2Records = new Mark2Record[markCount]; + for (int i = 0; i < markCount; i++) + { + this.Mark2Records[i] = new Mark2Record(reader, markClassCount, offset); + } + } + + /// + /// Gets the array of Mark2 records, in Coverage order. + /// + public Mark2Record[] Mark2Records { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/Mark2Record.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/Mark2Record.cs new file mode 100644 index 0000000..cb821de --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/Mark2Record.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// A Mark2Record declares one Anchor table for each mark class (including Class 0) identified in the MarkRecords of the MarkArray. + /// Each Anchor table specifies one mark2 attachment point used to attach all the mark1 glyphs in a particular class to the mark2 glyph. + /// + internal class Mark2Record + { + /// + /// Initializes a new instance of the class. + /// + /// The big endian binary reader. + /// The Number of Mark2 records. + /// Offset to the beginning of MarkArray table. + public Mark2Record(BigEndianBinaryReader reader, ushort markClassCount, long offset) + { + // +--------------+------------------------------------+--------------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==============+====================================+======================================================================================+ + // | Offset16 | mark2AnchorOffsets[markClassCount] | Array of offsets (one per class) to Anchor tables. Offsets are from beginning of | + // | | | Mark2Array table, in class order (offsets may be NULL). | + // +--------------+------------------------------------+--------------------------------------------------------------------------------------+ + ushort[] mark2AnchorOffsets = new ushort[markClassCount]; + this.MarkAnchorTable = new AnchorTable[markClassCount]; + for (int i = 0; i < markClassCount; i++) + { + mark2AnchorOffsets[i] = reader.ReadOffset16(); + } + + long position = reader.BaseStream.Position; + for (int i = 0; i < markClassCount; i++) + { + if (mark2AnchorOffsets[i] != 0) + { + this.MarkAnchorTable[i] = AnchorTable.Load(reader, offset + mark2AnchorOffsets[i]); + } + } + + reader.BaseStream.Position = position; + } + + /// + /// Gets the mark anchor table. + /// + public AnchorTable[] MarkAnchorTable { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/MarkArrayTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/MarkArrayTable.cs new file mode 100644 index 0000000..369af18 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/MarkArrayTable.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// The MarkArray table defines the class and the anchor point for a mark glyph. + /// Three GPOS subtable types — MarkToBase attachment, MarkToLigature attachment, + /// and MarkToMark attachment — use the MarkArray table to specify data for attaching marks. + /// The MarkArray table contains a count of the number of MarkRecords(markCount) and an array of those records(markRecords). + /// Each mark record defines the class of the mark and an offset to the Anchor table that contains data for the mark. + /// + /// + internal class MarkArrayTable + { + /// + /// Initializes a new instance of the class. + /// + /// The big endian binary reader. + /// The offset to the start of the mark array table. + public MarkArrayTable(BigEndianBinaryReader reader, long offset) + { + // +--------------+------------------------+--------------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==============+========================+======================================================================================+ + // | uint16 | markCount | Number of MarkRecords | + // +--------------+------------------------+--------------------------------------------------------------------------------------+ + // | MarkRecord | markRecords[markCount] | Array of MarkRecords, ordered by corresponding glyphs | + // | | | in the associated mark Coverage table. | + // +--------------+------------------------+--------------------------------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + ushort markCount = reader.ReadUInt16(); + this.MarkRecords = new MarkRecord[markCount]; + for (int i = 0; i < markCount; i++) + { + this.MarkRecords[i] = new MarkRecord(reader, offset); + } + } + + /// + /// Gets the mark records. + /// + public MarkRecord[] MarkRecords { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/MarkRecord.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/MarkRecord.cs new file mode 100644 index 0000000..2743c84 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/MarkRecord.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// Defines a mark record used in a mark array table: + /// + /// + [DebuggerDisplay("MarkClass: {MarkClass}, AnchorTable: {MarkAnchorTable}")] + internal readonly struct MarkRecord + { + /// + /// Initializes a new instance of the struct. + /// + /// The big endian binary reader. + /// Offset to the beginning of MarkArray table. + public MarkRecord(BigEndianBinaryReader reader, long offset) + { + // +--------------+------------------+--------------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==============+==================+======================================================================================+ + // | uint16 | markClass | Class defined for the associated mark. | + // +--------------+------------------+--------------------------------------------------------------------------------------+ + // | Offset16 | markAnchorOffset | Offset to Anchor table, from beginning of MarkArray table. | + // +--------------+------------------+--------------------------------------------------------------------------------------+ + this.MarkClass = reader.ReadUInt16(); + ushort markAnchorOffset = reader.ReadOffset16(); + + // Reset the reader position after reading the anchor table. + long readerPosition = reader.BaseStream.Position; + this.MarkAnchorTable = AnchorTable.Load(reader, offset + markAnchorOffset); + reader.BaseStream.Seek(readerPosition, SeekOrigin.Begin); + } + + /// + /// Gets the class defined for the associated mark. + /// + public uint MarkClass { get; } + + /// + /// Gets the mark anchor table. + /// + public AnchorTable MarkAnchorTable { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/NotImplementedSubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/NotImplementedSubTable.cs new file mode 100644 index 0000000..679ba06 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/NotImplementedSubTable.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// A placeholder subtable used for unimplemented or unrecognized GPOS lookup types. + /// Always returns from . + /// + internal class NotImplementedSubTable : LookupSubTable + { + /// + /// Initializes a new instance of the class. + /// + public NotImplementedSubTable() + : base(default, 0) + { + } + + /// + public override bool TryUpdatePosition( + FontMetrics fontMetrics, + GPosTable table, + GlyphPositioningCollection collection, + Tag feature, + int index, + int count) + => false; + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/PairValueRecord.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/PairValueRecord.cs new file mode 100644 index 0000000..067b85f --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/PairValueRecord.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// PairValueRecords are used in pair adjustment positioning subtables to adjust the placement or advances of two glyphs in relation to one another. + /// + /// + internal readonly struct PairValueRecord + { + /// + /// Initializes a new instance of the struct. + /// + /// The big endian binary reader. + /// The types of data in valueRecord1 — for the first glyph in the pair (may be zero). + /// The types of data in valueRecord2 — for the first glyph in the pair (may be zero). + /// The absolute stream position of the parent table for resolving device offsets. + public PairValueRecord(BigEndianBinaryReader reader, ValueFormat valueFormat1, ValueFormat valueFormat2, long parentBase = -1) + { + // +--------------+------------------+--------------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==============+==================+======================================================================================+ + // | uint16 | secondGlyph | Glyph ID of second glyph in the pair (first glyph is listed in the Coverage table). | + // +--------------+------------------+--------------------------------------------------------------------------------------+ + // | ValueRecord | valueRecord1 | Positioning data for the first glyph in the pair. | + // +--------------+------------------+--------------------------------------------------------------------------------------+ + // | ValueRecord | valueRecord2 | Positioning data for the second glyph in the pair. | + // +--------------+------------------+--------------------------------------------------------------------------------------+ + this.SecondGlyph = reader.ReadUInt16(); + this.ValueRecord1 = new ValueRecord(reader, valueFormat1, parentBase); + this.ValueRecord2 = new ValueRecord(reader, valueFormat2, parentBase); + } + + /// + /// Gets the second glyph ID. + /// + public ushort SecondGlyph { get; } + + /// + /// Gets the Positioning data for the first glyph in the pair. + /// + public ValueRecord ValueRecord1 { get; } + + /// + /// Gets the Positioning data for the second glyph in the pair. + /// + public ValueRecord ValueRecord2 { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/ValueFormat.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/ValueFormat.cs new file mode 100644 index 0000000..75d49d3 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/ValueFormat.cs @@ -0,0 +1,86 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// A ValueFormat flags field defines the types of positioning adjustment data that ValueRecords specify. + /// + /// + [Flags] + internal enum ValueFormat : ushort + { + // +--------+--------------------+---------------------------------------------+ + // | Mask | Name | Description | + // +========+====================+=============================================+ + // | 0x0001 | X_PLACEMENT | Includes horizontal adjustment for | + // | | | placement | + // +--------+--------------------+---------------------------------------------+ + // | 0x0002 | Y_PLACEMENT | Includes vertical adjustment for placement | + // +--------+--------------------+---------------------------------------------+ + // | 0x0004 | X_ADVANCE | Includes horizontal adjustment for | + // | | | advance | + // +--------+--------------------+---------------------------------------------+ + // | 0x0008 | Y_ADVANCE | Includes vertical adjustment for advance | + // +--------+--------------------+---------------------------------------------+ + // | 0x0010 | X_PLACEMENT_DEVICE | Includes Device table (non-variable font) / | + // | | | VariationIndex table (variable font) for | + // | | | horizontal placement | + // +--------+--------------------+---------------------------------------------+ + // | 0x0020 | Y_PLACEMENT_DEVICE | Includes Device table (non-variable font) / | + // | | | VariationIndex table (variable font) for | + // | | | vertical placement | + // +--------+--------------------+---------------------------------------------+ + // | 0x0040 | X_ADVANCE_DEVICE | Includes Device table (non-variable font) / | + // | | | VariationIndex table (variable font) for | + // | | | horizontal advance | + // +--------+--------------------+---------------------------------------------+ + // | 0x0080 | Y_ADVANCE_DEVICE | Includes Device table (non-variable font) / | + // | | | VariationIndex table (variable font) for | + // | | | vertical advance | + // +--------+--------------------+---------------------------------------------+ + // | 0xFF00 | Reserved | For future use (set to zero) | + // +--------+--------------------+---------------------------------------------+ + + /// + /// Includes horizontal adjustment for placement. + /// + XPlacement = 1, + + /// + /// Includes vertical adjustment for placement. + /// + YPlacement = 1 << 1, + + /// + /// Includes horizontal adjustment for advance. + /// + XAdvance = 1 << 2, + + /// + /// Includes vertical adjustment for advance. + /// + YAdvance = 1 << 3, + + /// + /// Includes Device table (non-variable font) or VariationIndex table (variable font) for horizontal placement. + /// + XPlacementDevice = 1 << 4, + + /// + /// Includes Device table (non-variable font) or VariationIndex table (variable font) for vertical placement. + /// + YPlacementDevice = 1 << 5, + + /// + /// Includes Device table (non-variable font) or VariationIndex table (variable font) for horizontal advance. + /// + XAdvanceDevice = 1 << 6, + + /// + /// Includes Device table (non-variable font) or VariationIndex table (variable font) for vertical advance. + /// + YAdvanceDevice = 1 << 7, + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/ValueRecord.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/ValueRecord.cs new file mode 100644 index 0000000..e282ae5 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/ValueRecord.cs @@ -0,0 +1,193 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos { + /// + /// GPOS subtables use ValueRecords to describe all the variables and values used to adjust the position + /// of a glyph or set of glyphs. A ValueRecord may define any combination of X and Y values (in design units) + /// to add to (positive values) or subtract from (negative values) the placement and advance values provided in the font. + /// + /// + internal readonly struct ValueRecord + { + /// + /// The deltaFormat value used by VariationIndex tables (as opposed to Device tables). + /// + private const ushort VariationIndexFormat = 0x8000; + + /// + /// Initializes a new instance of the struct. + /// + /// The big endian binary reader. + /// Defines the types of data in the ValueRecord. + public ValueRecord(BigEndianBinaryReader reader, ValueFormat valueFormat) + : this(reader, valueFormat, -1) + { + } + + /// + /// Initializes a new instance of the struct. + /// When is non-negative, device offsets are resolved to + /// VariationIndex (outerIndex, innerIndex) pairs for use with variable fonts. + /// + /// The big endian binary reader. + /// Defines the types of data in the ValueRecord. + /// + /// The absolute stream position of the immediate parent table (SinglePos subtable, + /// PairPosFormat2 subtable, or PairSet table). Device offsets are relative to this position. + /// Pass -1 to skip VariationIndex resolution. + /// + public ValueRecord(BigEndianBinaryReader reader, ValueFormat valueFormat, long parentBase) + { + // +----------+------------------+--------------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+==================+======================================================================================+ + // | int16 | xPlacement | Horizontal adjustment for placement, in | + // | | | design units. | + // +----------+------------------+--------------------------------------------------------------------------------------+ + // | int16 | yPlacement | Vertical adjustment for placement, in design | + // | | | units. | + // +----------+------------------+--------------------------------------------------------------------------------------+ + // | int16 | xAdvance | Horizontal adjustment for advance, in design | + // | | | units — only used for horizontal layout. | + // +----------+------------------+--------------------------------------------------------------------------------------+ + // | int16 | yAdvance | Vertical adjustment for advance, in design | + // | | | units — only used for vertical layout. | + // +----------+------------------+--------------------------------------------------------------------------------------+ + // | Offset16 | xPlaDeviceOffset | Offset to Device table (non-variable font) / | + // | | | VariationIndex table (variable font) for | + // | | | horizontal placement, from beginning of the | + // | | | immediate parent table (SinglePos or | + // | | | PairPosFormat2 lookup subtable, PairSet table | + // | | | within a PairPosFormat1 lookup subtable) — may be NULL. | + // +----------+------------------+--------------------------------------------------------------------------------------+ + // | Offset16 | yPlaDeviceOffset | Offset to Device table (non-variable font) / | + // | | | VariationIndex table (variable font) for vertical | + // | | | placement, from beginning of the immediate parent table (SinglePos or PairPosFormat2 | + // | | | lookup subtable, PairSet table within a | + // | | | PairPosFormat1 lookup subtable) — may be NULL. | + // +----------+------------------+--------------------------------------------------------------------------------------+ + // | Offset16 | xAdvDeviceOffset | Offset to Device table (non-variable font) / | + // | | | VariationIndex table (variable font) for | + // | | | horizontal advance, from beginning of the | + // | | | immediate parent table (SinglePos or | + // | | | PairPosFormat2 lookup subtable, PairSet table | + // | | | within a PairPosFormat1 lookup subtable) — may be NULL. | + // +----------+------------------+--------------------------------------------------------------------------------------+ + // | Offset16 | yAdvDeviceOffset | Offset to Device table (non-variable font) / | + // | | | VariationIndex table (variable font) for vertical | + // | | | advance, from beginning of the immediate | + // | | | parent table (SinglePos or PairPosFormat2 | + // | | | lookup subtable, PairSet table within a | + // | | | PairPosFormat1 lookup subtable) — may be NULL. | + // +----------+------------------+--------------------------------------------------------------------------------------+ + this.XPlacement = (valueFormat & ValueFormat.XPlacement) != 0 ? reader.ReadInt16() : (short)0; + this.YPlacement = (valueFormat & ValueFormat.YPlacement) != 0 ? reader.ReadInt16() : (short)0; + this.XAdvance = (valueFormat & ValueFormat.XAdvance) != 0 ? reader.ReadInt16() : (short)0; + this.YAdvance = (valueFormat & ValueFormat.YAdvance) != 0 ? reader.ReadInt16() : (short)0; + + short xPlaDevOff = (valueFormat & ValueFormat.XPlacementDevice) != 0 ? reader.ReadInt16() : (short)0; + short yPlaDevOff = (valueFormat & ValueFormat.YPlacementDevice) != 0 ? reader.ReadInt16() : (short)0; + short xAdvDevOff = (valueFormat & ValueFormat.XAdvanceDevice) != 0 ? reader.ReadInt16() : (short)0; + short yAdvDevOff = (valueFormat & ValueFormat.YAdvanceDevice) != 0 ? reader.ReadInt16() : (short)0; + + // Resolve device offsets to VariationIndex tables when the parent base is known. + if (parentBase >= 0 && ((ushort)xPlaDevOff | (ushort)yPlaDevOff | (ushort)xAdvDevOff | (ushort)yAdvDevOff) != 0) + { + long savedPosition = reader.BaseStream.Position; + this.XPlacementVariation = ResolveVariationIndex(reader, parentBase, xPlaDevOff); + this.YPlacementVariation = ResolveVariationIndex(reader, parentBase, yPlaDevOff); + this.XAdvanceVariation = ResolveVariationIndex(reader, parentBase, xAdvDevOff); + this.YAdvanceVariation = ResolveVariationIndex(reader, parentBase, yAdvDevOff); + reader.BaseStream.Position = savedPosition; + } + } + + /// + /// Gets the horizontal adjustment for placement, in design units. + /// + public short XPlacement { get; } + + /// + /// Gets the vertical adjustment for placement, in design units. + /// + public short YPlacement { get; } + + /// + /// Gets the horizontal adjustment for advance, in design units. + /// + public short XAdvance { get; } + + /// + /// Gets the vertical adjustment for advance, in design units. + /// + public short YAdvance { get; } + + /// + /// Gets the packed VariationIndex for horizontal placement: (outerIndex << 16) | innerIndex. + /// Zero means no variation data. + /// + public uint XPlacementVariation { get; } + + /// + /// Gets the packed VariationIndex for vertical placement: (outerIndex << 16) | innerIndex. + /// Zero means no variation data. + /// + public uint YPlacementVariation { get; } + + /// + /// Gets the packed VariationIndex for horizontal advance: (outerIndex << 16) | innerIndex. + /// Zero means no variation data. + /// + public uint XAdvanceVariation { get; } + + /// + /// Gets the packed VariationIndex for vertical advance: (outerIndex << 16) | innerIndex. + /// Zero means no variation data. + /// + public uint YAdvanceVariation { get; } + + /// + /// Gets a value indicating whether this record has any variation data. + /// + public bool HasVariation + => (this.XPlacementVariation | this.YPlacementVariation | this.XAdvanceVariation | this.YAdvanceVariation) != 0; + + /// + /// Reads a Device/VariationIndex table at the given offset and returns a packed VariationIndex + /// (outerIndex << 16 | innerIndex) if it is a VariationIndex table (deltaFormat == 0x8000), + /// or 0 if null, a Device table, or invalid. + /// + /// The big endian binary reader. + /// The absolute stream position of the parent table. + /// The offset to the Device/VariationIndex table from the parent table base. + /// The packed VariationIndex, or 0 if not applicable. + private static uint ResolveVariationIndex(BigEndianBinaryReader reader, long parentBase, short deviceOffset) + { + if (deviceOffset == 0) + { + return 0; + } + + // Device offsets are relative to the parent table base. + // Use absolute positioning to avoid BigEndianBinaryReader.Seek startOfStream rebasing. + reader.BaseStream.Position = parentBase + (ushort)deviceOffset; + + // VariationIndex table (reuses the Device table format): + // uint16 deltaSetOuterIndex + // uint16 deltaSetInnerIndex + // uint16 deltaFormat (0x8000 for VariationIndex, 1/2/3 for Device) + ushort first = reader.ReadUInt16(); + ushort second = reader.ReadUInt16(); + ushort format = reader.ReadUInt16(); + + if (format == VariationIndexFormat) + { + return ((uint)first << 16) | second; + } + + // TODO: Device table (per-ppem pixel adjustments for non-variable fonts) — not yet implemented. + return 0; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs new file mode 100644 index 0000000..1e617be --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -0,0 +1,559 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using SixLabors.Fonts.Tables.AdvancedTypographic.GPos; +using SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// The Glyph Positioning table (GPOS) provides precise control over glyph placement for + /// sophisticated text layout and rendering in each script and language system that a font supports. + /// + /// + internal class GPosTable : Table + { + /// + /// The tag for the horizontal kerning feature ('kern'). + /// + private static readonly Tag KernTag = Tag.Parse("kern"); + + /// + /// The tag for the vertical kerning feature ('vkrn'). + /// + private static readonly Tag VKernTag = Tag.Parse("vkrn"); + + /// + /// The OpenType table tag for the GPOS table. + /// + internal const string TableName = "GPOS"; + + /// + /// Initializes a new instance of the class. + /// + /// The script list table, or if not present. + /// The feature list table. + /// The lookup list table. + /// The feature variations table for variable fonts, or . + public GPosTable(ScriptList? scriptList, FeatureListTable featureList, LookupListTable lookupList, FeatureVariationsTable? featureVariations = null) + { + this.ScriptList = scriptList; + this.FeatureList = featureList; + this.LookupList = lookupList; + this.FeatureVariations = featureVariations; + } + + /// + /// Gets the script list table, or if not present. + /// + public ScriptList? ScriptList { get; } + + /// + /// Gets the feature list table. + /// + public FeatureListTable FeatureList { get; } + + /// + /// Gets the lookup list table containing all positioning lookups. + /// + public LookupListTable LookupList { get; } + + /// + /// Gets the feature variations table for variable fonts, or if not present. + /// + public FeatureVariationsTable? FeatureVariations { get; } + + /// + /// Loads the from the font reader. + /// + /// The font reader. + /// The , or if not present. + public static GPosTable? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + /// + /// Loads the from a big endian binary reader. + /// + /// The big endian binary reader. + /// The . + internal static GPosTable Load(BigEndianBinaryReader reader) + { + // GPOS Header, Version 1.0 + // +----------+-------------------+-----------------------------------------------------------+ + // | Type | Name | Description | + // +==========+===================+===========================================================+ + // | uint16 | majorVersion | Major version of the GPOS table, = 1 | + // +----------+-------------------+-----------------------------------------------------------+ + // | uint16 | minorVersion | Minor version of the GPOS table, = 0 | + // +----------+-------------------+-----------------------------------------------------------+ + // | Offset16 | scriptListOffset | Offset to ScriptList table, from beginning of GPOS table | + // +----------+-------------------+-----------------------------------------------------------+ + // | Offset16 | featureListOffset | Offset to FeatureList table, from beginning of GPOS table | + // +----------+-------------------+-----------------------------------------------------------+ + // | Offset16 | lookupListOffset | Offset to LookupList table, from beginning of GPOS table | + // +----------+-------------------+-----------------------------------------------------------+ + + // GPOS Header, Version 1.1 + // +----------+-------------------------+-------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+=========================+===============================================================================+ + // | uint16 | majorVersion | Major version of the GPOS table, = 1 | + // +----------+-------------------------+-------------------------------------------------------------------------------+ + // | uint16 | minorVersion | Minor version of the GPOS table, = 1 | + // +----------+-------------------------+-------------------------------------------------------------------------------+ + // | Offset16 | scriptListOffset | Offset to ScriptList table, from beginning of GPOS table | + // +----------+-------------------------+-------------------------------------------------------------------------------+ + // | Offset16 | featureListOffset | Offset to FeatureList table, from beginning of GPOS table | + // +----------+-------------------------+-------------------------------------------------------------------------------+ + // | Offset16 | lookupListOffset | Offset to LookupList table, from beginning of GPOS table | + // +----------+-------------------------+-------------------------------------------------------------------------------+ + // | Offset32 | featureVariationsOffset | Offset to FeatureVariations table, from beginning of GPOS table (may be NULL) | + // +----------+-------------------------+-------------------------------------------------------------------------------+ + ushort majorVersion = reader.ReadUInt16(); + ushort minorVersion = reader.ReadUInt16(); + + ushort scriptListOffset = reader.ReadOffset16(); + ushort featureListOffset = reader.ReadOffset16(); + ushort lookupListOffset = reader.ReadOffset16(); + uint featureVariationsOffset = (minorVersion == 1) ? reader.ReadOffset32() : 0; + + // TODO: Optimization. Allow only reading the scriptList. + ScriptList? scriptList = ScriptList.Load(reader, scriptListOffset); + + FeatureListTable featureList = FeatureListTable.Load(reader, featureListOffset); + + LookupListTable lookupList = LookupListTable.Load(reader, lookupListOffset); + + FeatureVariationsTable? featureVariations = featureVariationsOffset != 0 + ? FeatureVariationsTable.Load(reader, featureVariationsOffset, featureList) + : null; + + return new GPosTable(scriptList, featureList, lookupList, featureVariations); + } + + /// + /// Tries to update the positions of glyphs in the collection using GPOS lookup rules. + /// + /// The font metrics. + /// The glyph positioning collection. + /// When this method returns, indicates whether kerning was applied. + /// if any positioning was updated; otherwise, . + public bool TryUpdatePositions(FontMetrics fontMetrics, GlyphPositioningCollection collection, out bool kerned) + { + // Set max constraints to prevent OutOfMemoryException or infinite loops from attacks. + int maxCount = AdvancedTypographicUtils.GetMaxAllowableShapingCollectionCount(collection.Count); + int maxOperationsCount = AdvancedTypographicUtils.GetMaxAllowableShapingOperationsCount(collection.Count); + int currentOperations = 0; + bool maxOperationsReached = false; + + kerned = false; + bool updated = false; + for (int i = 0; i < collection.Count; i++) + { + if (!collection.ShouldProcess(fontMetrics, i)) + { + continue; + } + + ScriptClass current = this.GetScriptClass(CodePoint.GetScriptClass(collection[i].CodePoint)); + + int index = i; + int count = 1; + while (i < collection.Count - 1) + { + // We want to assign the same feature lookups to individual sections of the text rather + // than the text as a whole to ensure that different language shapers do not interfere + // with each other when the text contains multiple languages. + int ni = i + 1; + GlyphShapingData nextData = collection[ni]; + if (!collection.ShouldProcess(fontMetrics, ni)) + { + break; + } + + ScriptClass next = this.GetScriptClass(CodePoint.GetScriptClass(nextData.CodePoint)); + if (next != current && + current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClass.Inherited && + next is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClass.Inherited) + { + break; + } + + if (current is ScriptClass.Common or ScriptClass.Unknown or ScriptClass.Inherited) + { + current = next; + } + + i++; + count++; + + if (i >= maxCount) + { + break; + } + } + + Tag unicodeScriptTag = this.GetUnicodeScriptTag(current); + BaseShaper shaper = ShaperFactory.Create(current, unicodeScriptTag, fontMetrics, collection.TextOptions); + + if (shaper.MarkZeroingMode == MarkZeroingMode.PreGPos) + { + ZeroMarkAdvances(fontMetrics, collection, index, count); + } + + // Plan positioning features for each glyph. + shaper.Plan(collection, index, count); + IEnumerable shapingStages = shaper.GetShapingStages(); + SkippingGlyphIterator iterator = new(fontMetrics, collection, index, default, 0); + foreach (ShapingStage stage in shapingStages) + { + stage.PreProcessFeature(collection, index, count); + + Tag featureTag = stage.FeatureTag; + if (this.TryGetFeatureLookups(fontMetrics, in featureTag, current, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups)) + { + // Apply features in order. + foreach ((Tag Feature, ushort Index, LookupTable LookupTable) featureLookup in lookups) + { + Tag feature = featureLookup.Feature; + LookupTable featureLookupTable = featureLookup.LookupTable; + iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); + + while (iterator.Index < index + count) + { + if (currentOperations++ >= maxOperationsCount) + { + maxOperationsReached = true; + goto EndLookups; + } + + if (!collection[iterator.Index].EnabledFeatureTags.Contains(feature)) + { + iterator.Next(); + continue; + } + + bool success = featureLookup.LookupTable.TryUpdatePosition(fontMetrics, this, collection, featureLookup.Feature, iterator.Index, count - (iterator.Index - index)); + kerned |= success && (feature == KernTag || feature == VKernTag); + updated |= success; + iterator.Next(); + } + } + } + + stage.PostProcessFeature(collection, index, count); + } + + EndLookups: + if (shaper.MarkZeroingMode == MarkZeroingMode.PostGpos) + { + ZeroMarkAdvances(fontMetrics, collection, index, count); + } + + FixCursiveAttachment(collection, index, count); + FixMarkAttachment(collection, index, count); + UpdatePositions(fontMetrics, collection, index, count); + + if (i >= maxCount || maxOperationsReached) + { + return updated; + } + } + + return updated; + } + + /// + /// Tries to get the feature lookups for the given stage feature and script. + /// + /// The font metrics. + /// The feature tag for the current shaping stage. + /// The script class. + /// When this method returns, contains the list of feature lookups if found. + /// if lookups were found; otherwise, . + private bool TryGetFeatureLookups( + FontMetrics fontMetrics, + in Tag stageFeature, + ScriptClass script, + [NotNullWhen(true)] out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? value) + { + if (this.ScriptList is null) + { + value = null; + return false; + } + + // Resolve feature substitutions from FeatureVariations (variable fonts). + FeatureTableSubstitutionRecord[]? substitutions = this.FeatureVariations + ?.FindMatchingSubstitutions(fontMetrics.GetNormalizedCoordinates()); + + ScriptListTable scriptListTable = this.ScriptList.Default(); + Tag[] tags = UnicodeScriptTagMap.Instance[script]; + for (int i = 0; i < tags.Length; i++) + { + if (this.ScriptList.TryGetValue(tags[i].Value, out ScriptListTable? table)) + { + scriptListTable = table; + break; + } + } + + LangSysTable? defaultLangSysTable = scriptListTable.DefaultLangSysTable; + if (defaultLangSysTable != null) + { + value = this.GetFeatureLookups(stageFeature, substitutions, defaultLangSysTable); + return value.Count > 0; + } + + value = this.GetFeatureLookups(stageFeature, substitutions, scriptListTable.LangSysTables); + return value.Count > 0; + } + + /// + /// Gets the OpenType script tag for the given script class, checking against the font's ScriptList. + /// + /// The script class. + /// The matching script tag, or default if not found. + private Tag GetUnicodeScriptTag(ScriptClass script) + { + if (this.ScriptList is null) + { + return default; + } + + Tag[] tags = UnicodeScriptTagMap.Instance[script]; + for (int i = 0; i < tags.Length; i++) + { + if (this.ScriptList.TryGetValue(tags[i].Value, out ScriptListTable? _)) + { + return tags[i]; + } + } + + return default; + } + + /// + /// Gets the feature lookups for the given stage feature from the specified language system tables. + /// + /// The feature tag for the current shaping stage. + /// Optional feature table substitutions from FeatureVariations. + /// The language system tables to search. + /// A sorted list of feature lookups. + private List<(Tag Feature, ushort Index, LookupTable LookupTable)> GetFeatureLookups( + in Tag stageFeature, + FeatureTableSubstitutionRecord[]? substitutions, + params LangSysTable[] langSysTables) + { + List<(Tag Feature, ushort Index, LookupTable LookupTable)> lookups = []; + for (int i = 0; i < langSysTables.Length; i++) + { + ushort[] featureIndices = langSysTables[i].FeatureIndices; + for (int j = 0; j < featureIndices.Length; j++) + { + ushort featureIndex = featureIndices[j]; + FeatureTable featureTable = ResolveFeatureTable(this.FeatureList, featureIndex, substitutions); + Tag feature = featureTable.FeatureTag; + + if (stageFeature != feature) + { + continue; + } + + ushort[] lookupListIndices = featureTable.LookupListIndices; + for (int k = 0; k < lookupListIndices.Length; k++) + { + ushort lookupIndex = lookupListIndices[k]; + LookupTable lookupTable = this.LookupList.LookupTables[lookupIndex]; + lookups.Add(new(feature, lookupIndex, lookupTable)); + } + } + } + + lookups.Sort((x, y) => x.Index - y.Index); + return lookups; + } + + /// + /// Resolves the feature table for the given index, checking for substitutions from FeatureVariations first. + /// + /// The feature list table. + /// The feature index. + /// Optional feature table substitutions from FeatureVariations. + /// The resolved feature table. + private static FeatureTable ResolveFeatureTable( + FeatureListTable featureList, + ushort featureIndex, + FeatureTableSubstitutionRecord[]? substitutions) + { + if (substitutions is not null) + { + for (int i = 0; i < substitutions.Length; i++) + { + if (substitutions[i].FeatureIndex == featureIndex) + { + return substitutions[i].AlternateFeatureTable; + } + } + } + + return featureList.FeatureTables[featureIndex]; + } + + /// + /// Maps a script class to an effective script class, checking whether the font supports it. + /// Falls back to if the script is not present in the font. + /// + /// The script class to check. + /// The effective script class. + private ScriptClass GetScriptClass(ScriptClass current) + { + if (current is ScriptClass.Common or ScriptClass.Unknown or ScriptClass.Inherited) + { + return current; + } + + if (this.ScriptList is null) + { + return ScriptClass.Default; + } + + Tag[] tags = UnicodeScriptTagMap.Instance[current]; + + for (int i = 0; i < tags.Length; i++) + { + if (this.ScriptList.TryGetValue(tags[i].Value, out ScriptListTable? _)) + { + return current; + } + } + + // Script for `current` not present in the font: use default shaper. + return ScriptClass.Default; + } + + /// + /// Fixes cursive attachment positioning by propagating Y (or X for vertical) offsets. + /// + /// The glyph positioning collection. + /// The starting index. + /// The number of glyphs to process. + private static void FixCursiveAttachment(GlyphPositioningCollection collection, int index, int count) + { + LayoutMode layoutMode = collection.TextOptions.LayoutMode; + for (int i = 0; i < count; i++) + { + int currentIndex = i + index; + GlyphShapingData data = collection[currentIndex]; + if (data.CursiveAttachment != -1) + { + int j = data.CursiveAttachment + currentIndex; + if (j < index || j >= index + count) + { + return; + } + + GlyphShapingData cursiveData = collection[j]; + if (!AdvancedTypographicUtils.IsVerticalGlyph(data.CodePoint, layoutMode)) + { + data.Bounds.Y += cursiveData.Bounds.Y; + } + else + { + data.Bounds.X += cursiveData.Bounds.X; + } + } + } + } + + /// + /// Fixes mark attachment positioning by propagating offsets from base glyphs. + /// + /// The glyph positioning collection. + /// The starting index. + /// The number of glyphs to process. + private static void FixMarkAttachment(GlyphPositioningCollection collection, int index, int count) + { + for (int i = 0; i < count; i++) + { + int currentIndex = i + index; + GlyphShapingData data = collection[currentIndex]; + if (data.MarkAttachment != -1) + { + int j = data.MarkAttachment; + GlyphShapingData markData = collection[j]; + data.Bounds.X += markData.Bounds.X; + data.Bounds.Y += markData.Bounds.Y; + + if (data.Direction == TextDirection.LeftToRight) + { + for (int k = j; k < currentIndex; k++) + { + markData = collection[k]; + data.Bounds.X -= markData.Bounds.Width; + data.Bounds.Y -= markData.Bounds.Height; + } + } + else + { + for (int k = j + 1; k < currentIndex + 1; k++) + { + markData = collection[k]; + data.Bounds.X += markData.Bounds.Width; + data.Bounds.Y += markData.Bounds.Height; + } + } + } + } + } + + /// + /// Zeros the advance widths and heights for mark glyphs within the specified range. + /// + /// The font metrics. + /// The glyph positioning collection. + /// The starting index. + /// The number of glyphs to process. + private static void ZeroMarkAdvances(FontMetrics fontMetrics, GlyphPositioningCollection collection, int index, int count) + { + for (int i = 0; i < count; i++) + { + int currentIndex = i + index; + GlyphShapingData data = collection[currentIndex]; + if (AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphId, data)) + { + data.Bounds.Width = 0; + data.Bounds.Height = 0; + } + } + } + + /// + /// Updates glyph positions in the collection for the specified range. + /// + /// The font metrics. + /// The glyph positioning collection. + /// The starting index. + /// The number of glyphs to process. + private static void UpdatePositions(FontMetrics fontMetrics, GlyphPositioningCollection collection, int index, int count) + { + for (int i = 0; i < count; i++) + { + int currentIndex = i + index; + collection.UpdatePosition(fontMetrics, currentIndex); + } + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs new file mode 100644 index 0000000..06245e1 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs @@ -0,0 +1,275 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GSub { + /// + /// The headers of the GSUB and GPOS tables contain offsets to Lookup List tables (LookupList) for + /// glyph substitution (GSUB table) and glyph positioning (GPOS table). The LookupList table contains + /// an array of offsets to Lookup tables (lookupOffsets). + /// + /// + internal sealed class LookupListTable + { + /// + /// Initializes a new instance of the class. + /// + /// The number of lookups in this table. + /// The array of lookup tables. + private LookupListTable(ushort lookupCount, LookupTable[] lookupTables) + { + this.LookupCount = lookupCount; + this.LookupTables = lookupTables; + } + + /// + /// Gets the number of lookups in this table. + /// + public ushort LookupCount { get; } + + /// + /// Gets the array of lookup tables. + /// + public LookupTable[] LookupTables { get; } + + /// + /// Loads the from the binary reader at the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the lookup list table. + /// The loaded . + public static LookupListTable Load(BigEndianBinaryReader reader, long offset) + { + // +----------+----------------------------+---------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+============================+===============================================================+ + // | uint16 | lookupCount | Number of lookups in this table | + // +----------+----------------------------+---------------------------------------------------------------+ + // | Offset16 | lookupOffsets[lookupCount] | Array of offsets to Lookup tables, from beginning | + // | | | of LookupList — zero based (first lookup is Lookup index = 0) | + // +----------+----------------------------+---------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + + ushort lookupCount = reader.ReadUInt16(); + using Buffer lookupOffsetsBuffer = new(lookupCount); + Span lookupOffsets = lookupOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(lookupOffsets); + + LookupTable[] lookupTables = new LookupTable[lookupCount]; + + for (int i = 0; i < lookupTables.Length; i++) + { + lookupTables[i] = LookupTable.Load(reader, offset + lookupOffsets[i]); + } + + return new LookupListTable(lookupCount, lookupTables); + } + } + + /// + /// A Lookup table (Lookup) defines the specific conditions, type, and results of a substitution + /// or positioning action that is used to implement a feature. For example, a substitution + /// operation requires a list of target glyph indices to be replaced, a list of replacement glyph + /// indices, and a description of the type of substitution action. + /// + /// + internal sealed class LookupTable + { + /// + /// Initializes a new instance of the class. + /// + /// The lookup type identifying the kind of substitution. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The array of lookup subtables. + private LookupTable( + ushort lookupType, + LookupFlags lookupFlags, + ushort markFilteringSet, + LookupSubTable[] lookupSubTables) + { + this.LookupType = lookupType; + this.LookupFlags = lookupFlags; + this.MarkFilteringSet = markFilteringSet; + this.LookupSubTables = lookupSubTables; + } + + /// + /// Gets the lookup type, which determines the kind of substitution performed. + /// + public ushort LookupType { get; } + + /// + /// Gets the lookup qualifiers flags that control filtering of glyphs during lookup. + /// + public LookupFlags LookupFlags { get; } + + /// + /// Gets the index (base 0) into the GDEF mark glyph sets structure, used when the + /// flag is set. + /// + public ushort MarkFilteringSet { get; } + + /// + /// Gets the array of lookup subtables for this lookup. + /// + public LookupSubTable[] LookupSubTables { get; } + + /// + /// Loads the from the binary reader at the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the lookup table. + /// The loaded . + public static LookupTable Load(BigEndianBinaryReader reader, long offset) + { + // +----------+--------------------------------+-------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+================================+=============================================================+ + // | uint16 | lookupType | Different enumerations for GSUB and GPOS | + // +----------+--------------------------------+-------------------------------------------------------------+ + // | uint16 | lookupFlag | Lookup qualifiers | + // +----------+--------------------------------+-------------------------------------------------------------+ + // | uint16 | subTableCount | Number of subtables for this lookup | + // +----------+--------------------------------+-------------------------------------------------------------+ + // | Offset16 | subtableOffsets[subTableCount] | Array of offsets to lookup subtables, from beginning of | + // | | | Lookup table | + // +----------+--------------------------------+-------------------------------------------------------------+ + // | uint16 | markFilteringSet | Index (base 0) into GDEF mark glyph sets structure. | + // | | | This field is only present if the USE\_MARK\_FILTERING\_SET | + // | | | lookup flag is set. | + // +----------+--------------------------------+-------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + + ushort lookupType = reader.ReadUInt16(); + LookupFlags lookupFlags = reader.ReadUInt16(); + ushort subTableCount = reader.ReadUInt16(); + + using Buffer subTableOffsetsBuffer = new(subTableCount); + Span subTableOffsets = subTableOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(subTableOffsets); + + // The fifth bit indicates the presence of a MarkFilteringSet field in the Lookup table. + ushort markFilteringSet = ((lookupFlags & LookupFlags.UseMarkFilteringSet) != 0) + ? reader.ReadUInt16() + : (ushort)0; + + LookupSubTable[] lookupSubTables = new LookupSubTable[subTableCount]; + + for (int i = 0; i < lookupSubTables.Length; i++) + { + lookupSubTables[i] = LoadLookupSubTable(lookupType, lookupFlags, markFilteringSet, reader, offset + subTableOffsets[i]); + } + + return new LookupTable(lookupType, lookupFlags, markFilteringSet, lookupSubTables); + } + + /// + /// Attempts to perform a glyph substitution at the specified index in the collection. + /// + /// The font metrics. + /// The GSUB table. + /// The glyph substitution collection. + /// The feature tag to apply. + /// The index in the collection at which to attempt substitution. + /// The number of glyphs in the input sequence to consider. + /// if a substitution was performed; otherwise, . + public bool TrySubstitution( + FontMetrics fontMetrics, + GSubTable table, + GlyphSubstitutionCollection collection, + Tag feature, + int index, + int count) + { + foreach (LookupSubTable subTable in this.LookupSubTables) + { + if (subTable.TrySubstitution(fontMetrics, table, collection, feature, index, count)) + { + // A lookup is finished for a glyph after the client locates the target + // glyph or glyph context and performs a substitution, if specified. + return true; + } + } + + return false; + } + + /// + /// Loads a lookup subtable based on the lookup type. + /// + /// The lookup type identifying the kind of substitution. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The big-endian binary reader. + /// The offset to the beginning of the subtable. + /// The loaded . + private static LookupSubTable LoadLookupSubTable( + ushort lookupType, + LookupFlags lookupFlags, + ushort markFilteringSet, + BigEndianBinaryReader reader, + long offset) + => lookupType switch + { + 1 => LookupType1SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 2 => LookupType2SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 3 => LookupType3SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 4 => LookupType4SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 5 => LookupType5SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 6 => LookupType6SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 7 => LookupType7SubTable.Load(reader, offset, lookupFlags, markFilteringSet, LoadLookupSubTable), + 8 => LookupType8SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + _ => new NotImplementedSubTable(), + }; + } + + /// + /// Base class for all GSUB lookup subtables. Each subtable implements a specific + /// type of glyph substitution logic. + /// + /// + internal abstract class LookupSubTable + { + /// + /// Initializes a new instance of the class. + /// + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + protected LookupSubTable(LookupFlags lookupFlags, ushort markFilteringSet) + { + this.LookupFlags = lookupFlags; + this.MarkFilteringSet = markFilteringSet; + } + + /// + /// Gets the lookup qualifiers flags that control filtering of glyphs during lookup. + /// + public LookupFlags LookupFlags { get; } + + /// + /// Gets the index (base 0) into the GDEF mark glyph sets structure. + /// + public ushort MarkFilteringSet { get; } + + /// + /// Attempts to perform a glyph substitution at the specified index in the collection. + /// + /// The font metrics. + /// The GSUB table. + /// The glyph substitution collection. + /// The feature tag to apply. + /// The index in the collection at which to attempt substitution. + /// The number of glyphs in the input sequence to consider. + /// if a substitution was performed; otherwise, . + public abstract bool TrySubstitution( + FontMetrics fontMetrics, + GSubTable table, + GlyphSubstitutionCollection collection, + Tag feature, + int index, + int count); + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs new file mode 100644 index 0000000..cf7e6ee --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs @@ -0,0 +1,210 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GSub { + /// + /// Single substitution (SingleSubst) subtables tell a client to replace a single glyph with another glyph. + /// The subtables can be either of two formats. Both formats require two distinct sets of glyph indices: + /// one that defines input glyphs (specified in the Coverage table), and one that defines the output glyphs. + /// Format 1 requires less space than Format 2, but it is less flexible. + /// + /// + internal static class LookupType1SubTable + { + /// + /// Loads the single substitution lookup subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupSubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort substFormat = reader.ReadUInt16(); + + return substFormat switch + { + 1 => LookupType1Format1SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 2 => LookupType1Format2SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + _ => new NotImplementedSubTable(), + }; + } + } + + /// + /// Implements single substitution format 1. The substitute glyph ID is calculated by adding + /// a delta value to the original glyph ID. + /// + /// + internal sealed class LookupType1Format1SubTable : LookupSubTable + { + /// + /// The delta value to add to the original glyph ID to produce the substitute glyph ID. + /// + private readonly ushort deltaGlyphId; + + /// + /// The coverage table that defines the set of input glyph IDs. + /// + private readonly CoverageTable coverageTable; + + /// + /// Initializes a new instance of the class. + /// + /// The delta value to add to the original glyph ID. + /// The coverage table defining input glyphs. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + private LookupType1Format1SubTable(ushort deltaGlyphId, CoverageTable coverageTable, LookupFlags lookupFlags, ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.deltaGlyphId = deltaGlyphId; + this.coverageTable = coverageTable; + } + + /// + /// Loads the single substitution format 1 subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupType1Format1SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + // SingleSubstFormat1 + // +----------+----------------+----------------------------------------------------------+ + // | Type | Name | Description | + // +==========+================+==========================================================+ + // | uint16 | substFormat | Format identifier: format = 1 | + // +----------+----------------+----------------------------------------------------------+ + // | Offset16 | coverageOffset | Offset to Coverage table, from beginning of substitution | + // | | | subtable | + // +----------+----------------+----------------------------------------------------------+ + // | int16 | deltaGlyphID | Add to original glyph ID to get substitute glyph ID | + // +----------+----------------+----------------------------------------------------------+ + ushort coverageOffset = reader.ReadOffset16(); + ushort deltaGlyphId = reader.ReadUInt16(); + CoverageTable coverageTable = CoverageTable.Load(reader, offset + coverageOffset); + + return new LookupType1Format1SubTable(deltaGlyphId, coverageTable, lookupFlags, markFilteringSet); + } + + /// + public override bool TrySubstitution( + FontMetrics fontMetrics, + GSubTable table, + GlyphSubstitutionCollection collection, + Tag feature, + int index, + int count) + { + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + if (this.coverageTable.CoverageIndexOf(glyphId) > -1) + { + collection.Replace(index, (ushort)(glyphId + this.deltaGlyphId), feature); + return true; + } + + return false; + } + } + + /// + /// Implements single substitution format 2. Each input glyph is mapped to a specific + /// substitute glyph via an array ordered by coverage index. + /// + /// + internal sealed class LookupType1Format2SubTable : LookupSubTable + { + /// + /// The coverage table that defines the set of input glyph IDs. + /// + private readonly CoverageTable coverageTable; + + /// + /// The array of substitute glyph IDs, ordered by coverage index. + /// + private readonly ushort[] substituteGlyphs; + + /// + /// Initializes a new instance of the class. + /// + /// The array of substitute glyph IDs. + /// The coverage table defining input glyphs. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + private LookupType1Format2SubTable(ushort[] substituteGlyphs, CoverageTable coverageTable, LookupFlags lookupFlags, ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.substituteGlyphs = substituteGlyphs; + this.coverageTable = coverageTable; + } + + /// + /// Loads the single substitution format 2 subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupType1Format2SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + // SingleSubstFormat2 + // +----------+--------------------------------+-----------------------------------------------------------+ + // | Type | Name | Description | + // +==========+================================+===========================================================+ + // | uint16 | substFormat | Format identifier: format = 2 | + // +----------+--------------------------------+-----------------------------------------------------------+ + // | Offset16 | coverageOffset | Offset to Coverage table, from beginning of substitution | + // | | | subtable | + // +----------+--------------------------------+-----------------------------------------------------------+ + // | uint16 | glyphCount | Number of glyph IDs in the substituteGlyphIDs array | + // +----------+--------------------------------+-----------------------------------------------------------+ + // | uint16 | substituteGlyphIDs[glyphCount] | Array of substitute glyph IDs — ordered by Coverage index | + // +----------+--------------------------------+-----------------------------------------------------------+ + ushort coverageOffset = reader.ReadOffset16(); + ushort glyphCount = reader.ReadUInt16(); + ushort[] substituteGlyphIds = reader.ReadUInt16Array(glyphCount); + CoverageTable coverageTable = CoverageTable.Load(reader, offset + coverageOffset); + + return new LookupType1Format2SubTable(substituteGlyphIds, coverageTable, lookupFlags, markFilteringSet); + } + + /// + public override bool TrySubstitution( + FontMetrics fontMetrics, + GSubTable table, + GlyphSubstitutionCollection collection, + Tag feature, + int index, + int count) + { + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + int offset = this.coverageTable.CoverageIndexOf(glyphId); + + if (offset > -1 && offset < this.substituteGlyphs.Length) + { + collection.Replace(index, this.substituteGlyphs[offset], feature); + return true; + } + + return false; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs new file mode 100644 index 0000000..30b262b --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs @@ -0,0 +1,165 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GSub { + /// + /// A Multiple Substitution (MultipleSubst) subtable replaces a single glyph with more than one glyph, + /// as when multiple glyphs replace a single ligature. The subtable has a single format: MultipleSubstFormat1. + /// + /// + internal static class LookupType2SubTable + { + /// + /// Loads the multiple substitution lookup subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupSubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort substFormat = reader.ReadUInt16(); + + return substFormat switch + { + 1 => LookupType2Format1SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + _ => new NotImplementedSubTable(), + }; + } + } + + /// + /// Implements multiple substitution format 1. Each input glyph is replaced by a sequence + /// of glyphs defined by the corresponding sequence table. + /// + /// + internal sealed class LookupType2Format1SubTable : LookupSubTable + { + /// + /// The array of sequence tables, ordered by coverage index. + /// + private readonly SequenceTable[] sequenceTables; + + /// + /// The coverage table that defines the set of input glyph IDs. + /// + private readonly CoverageTable coverageTable; + + /// + /// Initializes a new instance of the class. + /// + /// The array of sequence tables. + /// The coverage table defining input glyphs. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + private LookupType2Format1SubTable(SequenceTable[] sequenceTables, CoverageTable coverageTable, LookupFlags lookupFlags, ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.sequenceTables = sequenceTables; + this.coverageTable = coverageTable; + } + + /// + /// Loads the multiple substitution format 1 subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupType2Format1SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + // Multiple Substitution Format 1 + // +----------+--------------------------------+-----------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+================================+=================================================================+ + // | uint16 | substFormat | Format identifier: format = 1 | + // +----------+--------------------------------+-----------------------------------------------------------------+ + // | Offset16 | coverageOffset | Offset to Coverage table, from beginning of substitution | + // | | | subtable | + // +----------+--------------------------------+-----------------------------------------------------------------+ + // | uint16 | sequenceCount | Number of Sequence table offsets in the sequenceOffsets array | + // +----------+--------------------------------+-----------------------------------------------------------------+ + // | Offset16 | sequenceOffsets[sequenceCount] | Array of offsets to Sequence tables. Offsets are from beginning | + // | | | of substitution subtable, ordered by Coverage index | + // +----------+--------------------------------+-----------------------------------------------------------------+ + ushort coverageOffset = reader.ReadOffset16(); + ushort sequenceCount = reader.ReadUInt16(); + + using Buffer sequenceOffsetsBuffer = new(sequenceCount); + Span sequenceOffsets = sequenceOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(sequenceOffsets); + + SequenceTable[] sequenceTables = new SequenceTable[sequenceCount]; + for (int i = 0; i < sequenceTables.Length; i++) + { + // Sequence Table + // +--------+--------------------------------+------------------------------------------------------+ + // | Type | Name | Description | + // +========+================================+======================================================+ + // | uint16 | glyphCount | Number of glyph IDs in the substituteGlyphIDs array. | + // | | | This must always be greater than 0. | + // +--------+--------------------------------+------------------------------------------------------+ + // | uint16 | substituteGlyphIDs[glyphCount] | String of glyph IDs to substitute | + // +--------+--------------------------------+------------------------------------------------------+ + reader.Seek(offset + sequenceOffsets[i], SeekOrigin.Begin); + ushort glyphCount = reader.ReadUInt16(); + sequenceTables[i] = new SequenceTable(reader.ReadUInt16Array(glyphCount)); + } + + CoverageTable coverageTable = CoverageTable.Load(reader, offset + coverageOffset); + + return new LookupType2Format1SubTable(sequenceTables, coverageTable, lookupFlags, markFilteringSet); + } + + /// + public override bool TrySubstitution( + FontMetrics fontMetrics, + GSubTable table, + GlyphSubstitutionCollection collection, + Tag feature, + int index, + int count) + { + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + int offset = this.coverageTable.CoverageIndexOf(glyphId); + + if (offset > -1 && offset < this.sequenceTables.Length) + { + collection.Replace(index, this.sequenceTables[offset].SubstituteGlyphs, feature); + return true; + } + + return false; + } + + /// + /// Represents a sequence table containing an ordered list of substitute glyph IDs + /// that replace a single input glyph. + /// + public readonly struct SequenceTable + { + /// + /// Initializes a new instance of the struct. + /// + /// The array of substitute glyph IDs. + public SequenceTable(ushort[] substituteGlyphs) + => this.SubstituteGlyphs = substituteGlyphs; + + /// + /// Gets the array of substitute glyph IDs. + /// + public ushort[] SubstituteGlyphs { get; } + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs new file mode 100644 index 0000000..93d3e93 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs @@ -0,0 +1,167 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GSub { + /// + /// An Alternate Substitution (AlternateSubst) subtable identifies any number of aesthetic alternatives + /// from which a user can choose a glyph variant to replace the input glyph. + /// + /// + internal static class LookupType3SubTable + { + /// + /// Loads the alternate substitution lookup subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupSubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort substFormat = reader.ReadUInt16(); + + return substFormat switch + { + 1 => LookupType3Format1SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + _ => new NotImplementedSubTable(), + }; + } + } + + /// + /// Implements alternate substitution format 1. Each input glyph can be replaced with any + /// one of a set of alternate glyphs. + /// + /// + internal sealed class LookupType3Format1SubTable : LookupSubTable + { + /// + /// The array of alternate set tables, ordered by coverage index. + /// + private readonly AlternateSetTable[] alternateSetTables; + + /// + /// The coverage table that defines the set of input glyph IDs. + /// + private readonly CoverageTable coverageTable; + + /// + /// Initializes a new instance of the class. + /// + /// The array of alternate set tables. + /// The coverage table defining input glyphs. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + private LookupType3Format1SubTable(AlternateSetTable[] alternateSetTables, CoverageTable coverageTable, LookupFlags lookupFlags, ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.alternateSetTables = alternateSetTables; + this.coverageTable = coverageTable; + } + + /// + /// Loads the alternate substitution format 1 subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupType3Format1SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + // Alternate Substitution Format 1 + // +----------+----------------------------------------+---------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+========================================+===============================================================+ + // | uint16 | substFormat | Format identifier: format = 1 | + // +----------+----------------------------------------+---------------------------------------------------------------+ + // | Offset16 | coverageOffset | Offset to Coverage table, from beginning of substitution | + // | | | subtable | + // +----------+----------------------------------------+---------------------------------------------------------------+ + // | uint16 | alternateSetCount | Number of AlternateSet tables | + // +----------+----------------------------------------+---------------------------------------------------------------+ + // | Offset16 | alternateSetOffsets[alternateSetCount] | Array of offsets to AlternateSet tables. Offsets are from | + // | | | beginning of substitution subtable, ordered by Coverage index | + // +----------+----------------------------------------+---------------------------------------------------------------+ + ushort coverageOffset = reader.ReadOffset16(); + ushort alternateSetCount = reader.ReadUInt16(); + + using Buffer alternateSetOffsetsBuffer = new(alternateSetCount); + Span alternateSetOffsets = alternateSetOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(alternateSetOffsets); + + AlternateSetTable[] alternateTables = new AlternateSetTable[alternateSetCount]; + for (int i = 0; i < alternateTables.Length; i++) + { + // AlternateSet Table + // +--------+-------------------------------+----------------------------------------------------+ + // | Type | Name | Description | + // +========+===============================+====================================================+ + // | uint16 | glyphCount | Number of glyph IDs in the alternateGlyphIDs array | + // +--------+-------------------------------+----------------------------------------------------+ + // | uint16 | alternateGlyphIDs[glyphCount] | Array of alternate glyph IDs, in arbitrary order | + // +--------+-------------------------------+----------------------------------------------------+ + reader.Seek(offset + alternateSetOffsets[i], SeekOrigin.Begin); + ushort glyphCount = reader.ReadUInt16(); + alternateTables[i] = new AlternateSetTable(reader.ReadUInt16Array(glyphCount)); + } + + CoverageTable coverageTable = CoverageTable.Load(reader, offset + coverageOffset); + + return new LookupType3Format1SubTable(alternateTables, coverageTable, lookupFlags, markFilteringSet); + } + + /// + public override bool TrySubstitution( + FontMetrics fontMetrics, + GSubTable table, + GlyphSubstitutionCollection collection, + Tag feature, + int index, + int count) + { + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + int offset = this.coverageTable.CoverageIndexOf(glyphId); + + if (offset > -1 && offset < this.alternateSetTables.Length) + { + // TODO: We're just choosing the first alternative here. + // It looks like the choice is arbitrary and should be determined by + // the client. + collection.Replace(index, this.alternateSetTables[offset].AlternateGlyphs[0], feature); + return true; + } + + return false; + } + + /// + /// Represents an alternate set table containing an array of alternate glyph IDs + /// for a single input glyph. + /// + public readonly struct AlternateSetTable + { + /// + /// Initializes a new instance of the struct. + /// + /// The array of alternate glyph IDs. + public AlternateSetTable(ushort[] alternateGlyphs) + => this.AlternateGlyphs = alternateGlyphs; + + /// + /// Gets the array of alternate glyph IDs, in arbitrary order. + /// + public readonly ushort[] AlternateGlyphs { get; } + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs new file mode 100644 index 0000000..650b553 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs @@ -0,0 +1,346 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GSub { + /// + /// A Ligature Substitution (LigatureSubst) subtable identifies ligature substitutions where a single glyph replaces multiple glyphs. + /// One LigatureSubst subtable can specify any number of ligature substitutions. + /// The subtable has one format: LigatureSubstFormat1. + /// + /// + internal static class LookupType4SubTable + { + /// + /// Loads the ligature substitution lookup subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupSubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort substFormat = reader.ReadUInt16(); + + return substFormat switch + { + 1 => LookupType4Format1SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + _ => new NotImplementedSubTable(), + }; + } + } + + /// + /// Implements ligature substitution format 1. A sequence of glyphs is replaced by a single + /// ligature glyph. The first glyph in the sequence is identified via the coverage table, and + /// the remaining component glyphs are specified in each ligature table. + /// + /// + internal sealed class LookupType4Format1SubTable : LookupSubTable + { + /// + /// The array of ligature set tables, ordered by coverage index. + /// + private readonly LigatureSetTable[] ligatureSetTables; + + /// + /// The coverage table that defines the set of first-component glyph IDs. + /// + private readonly CoverageTable coverageTable; + + /// + /// Initializes a new instance of the class. + /// + /// The array of ligature set tables. + /// The coverage table defining first-component glyphs. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + private LookupType4Format1SubTable(LigatureSetTable[] ligatureSetTables, CoverageTable coverageTable, LookupFlags lookupFlags, ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.ligatureSetTables = ligatureSetTables; + this.coverageTable = coverageTable; + } + + /// + /// Loads the ligature substitution format 1 subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupType4Format1SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + // Ligature Substitution Format 1 + // +----------+--------------------------------------+--------------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+======================================+====================================================================+ + // | uint16 | substFormat | Format identifier: format = 1 | + // +----------+--------------------------------------+--------------------------------------------------------------------+ + // | Offset16 | coverageOffset | Offset to Coverage table, from beginning of substitution | + // | | | subtable | + // +----------+--------------------------------------+--------------------------------------------------------------------+ + // | uint16 | ligatureSetCount | Number of LigatureSet tables | + // +----------+--------------------------------------+--------------------------------------------------------------------+ + // | Offset16 | ligatureSetOffsets[ligatureSetCount] | Array of offsets to LigatureSet tables. Offsets are from beginning | + // | | | of substitution subtable, ordered by Coverage index | + // +----------+--------------------------------------+--------------------------------------------------------------------+ + ushort coverageOffset = reader.ReadOffset16(); + ushort ligatureSetCount = reader.ReadUInt16(); + + using Buffer ligatureSetOffsetsBuffer = new(ligatureSetCount); + Span ligatureSetOffsets = ligatureSetOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(ligatureSetOffsets); + + LigatureSetTable[] ligatureSetTables = new LigatureSetTable[ligatureSetCount]; + for (int i = 0; i < ligatureSetTables.Length; i++) + { + // LigatureSet Table + // +----------+--------------------------------+--------------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+================================+====================================================================+ + // | uint16 | ligatureCount | Number of Ligature tables | + // +----------+--------------------------------+--------------------------------------------------------------------+ + // | Offset16 | ligatureOffsets[LigatureCount] | Array of offsets to Ligature tables. Offsets are from beginning of | + // | | | LigatureSet table, ordered by preference. | + // +----------+--------------------------------+--------------------------------------------------------------------+ + long ligatureSetOffset = offset + ligatureSetOffsets[i]; + reader.Seek(ligatureSetOffset, SeekOrigin.Begin); + ushort ligatureCount = reader.ReadUInt16(); + + using Buffer ligatureOffsetsBuffer = new(ligatureCount); + Span ligatureOffsets = ligatureOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(ligatureOffsets); + + LigatureTable[] ligatureTables = new LigatureTable[ligatureCount]; + + // Ligature Table + // +--------+---------------------------------------+------------------------------------------------------+ + // | Type | Name | Description | + // +========+=======================================+======================================================+ + // | uint16 | ligatureGlyph | glyph ID of ligature to substitute | + // +--------+---------------------------------------+------------------------------------------------------+ + // | uint16 | componentCount | Number of components in the ligature | + // +--------+---------------------------------------+------------------------------------------------------+ + // | uint16 | componentGlyphIDs[componentCount - 1] | Array of component glyph IDs — start with the second | + // | | | component, ordered in writing direction | + // +--------+---------------------------------------+------------------------------------------------------+ + for (int j = 0; j < ligatureTables.Length; j++) + { + reader.Seek(ligatureSetOffset + ligatureOffsets[j], SeekOrigin.Begin); + ushort ligatureGlyph = reader.ReadUInt16(); + ushort componentCount = reader.ReadUInt16(); + ushort[] componentGlyphIds = reader.ReadUInt16Array(componentCount - 1); + ligatureTables[j] = new LigatureTable(ligatureGlyph, componentGlyphIds); + } + + ligatureSetTables[i] = new LigatureSetTable(ligatureTables); + } + + CoverageTable coverageTable = CoverageTable.Load(reader, offset + coverageOffset); + + return new LookupType4Format1SubTable(ligatureSetTables, coverageTable, lookupFlags, markFilteringSet); + } + + /// + public override bool TrySubstitution( + FontMetrics fontMetrics, + GSubTable table, + GlyphSubstitutionCollection collection, + Tag feature, + int index, + int count) + { + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + int offset = this.coverageTable.CoverageIndexOf(glyphId); + if (offset < 0 || offset >= this.ligatureSetTables.Length) + { + return false; + } + + LigatureSetTable ligatureSetTable = this.ligatureSetTables[offset]; + SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + Span matchBuffer = stackalloc int[AdvancedTypographicUtils.MaxContextLength]; + for (int i = 0; i < ligatureSetTable.Ligatures.Length; i++) + { + LigatureTable ligatureTable = ligatureSetTable.Ligatures[i]; + int remaining = count - 1; + int compLength = ligatureTable.ComponentGlyphs.Length; + if (compLength > remaining) + { + continue; + } + + if (!AdvancedTypographicUtils.MatchInputSequence(iterator, feature, 1, ligatureTable.ComponentGlyphs, matchBuffer)) + { + continue; + } + + // From Harfbuzz: + // - If it *is* a mark ligature, we don't allocate a new ligature id, and leave + // the ligature to keep its old ligature id. This will allow it to attach to + // a base ligature in GPOS. Eg. if the sequence is: LAM,LAM,SHADDA,FATHA,HEH, + // and LAM,LAM,HEH for a ligature, they will leave SHADDA and FATHA with a + // ligature id and component value of 2. Then if SHADDA,FATHA form a ligature + // later, we don't want them to lose their ligature id/component, otherwise + // GPOS will fail to correctly position the mark ligature on top of the + // LAM,LAM,HEH ligature. See https://bugzilla.gnome.org/show_bug.cgi?id=676343 + // + // - If a ligature is formed of components that some of which are also ligatures + // themselves, and those ligature components had marks attached to *their* + // components, we have to attach the marks to the new ligature component + // positions! Now *that*'s tricky! And these marks may be following the + // last component of the whole sequence, so we should loop forward looking + // for them and update them. + // + // Eg. the sequence is LAM,LAM,SHADDA,FATHA,HEH, and the font first forms a + // 'calt' ligature of LAM,HEH, leaving the SHADDA and FATHA with a ligature + // id and component == 1. Now, during 'liga', the LAM and the LAM-HEH ligature + // form a LAM-LAM-HEH ligature. We need to reassign the SHADDA and FATHA to + // the new ligature with a component value of 2. + // + // This in fact happened to a font... See https://bugzilla.gnome.org/show_bug.cgi?id=437633 + GlyphShapingData data = collection[index]; + GlyphShapingClass shapingClass = AdvancedTypographicUtils.GetGlyphShapingClass(fontMetrics, glyphId, data); + bool isBaseLigature = shapingClass.IsBase; + bool isMarkLigature = shapingClass.IsMark; + + Span matches = matchBuffer[..Math.Min(ligatureTable.ComponentGlyphs.Length, matchBuffer.Length)]; + for (int j = 0; j < matches.Length && isMarkLigature; j++) + { + GlyphShapingData match = collection[matches[j]]; + if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, match.GlyphId, match)) + { + isBaseLigature = false; + isMarkLigature = false; + break; + } + } + + bool isLigature = !isBaseLigature && !isMarkLigature; + + int ligatureId = isLigature ? 0 : collection.LigatureId++; + int lastLigatureId = data.LigatureId; + int lastComponentCount = data.CodePointCount; + int currentComponentCount = lastComponentCount; + int idx = index + 1; + + // Set ligatureID and ligatureComponent on glyphs that were skipped in the matched sequence. + // This allows GPOS to attach marks to the correct ligature components. + foreach (int matchIndex in matches) + { + // Don't assign new ligature components for mark ligatures (see above). + if (isLigature) + { + idx = matchIndex; + } + else + { + while (idx < matchIndex) + { + GlyphShapingData current = collection[idx]; + int currentLC = current.LigatureComponent == -1 ? 1 : current.LigatureComponent; + int ligatureComponent = currentComponentCount - lastComponentCount + Math.Min(currentLC, lastComponentCount); + current.LigatureId = ligatureId; + current.LigatureComponent = ligatureComponent; + + idx++; + } + } + + GlyphShapingData last = collection[idx]; + lastLigatureId = last.LigatureId; + lastComponentCount = last.CodePointCount; + currentComponentCount += lastComponentCount; + idx++; // Skip base glyph + } + + // Adjust ligature components for any marks following + if (lastLigatureId > 0 && !isLigature) + { + // Only check glyphs managed by current shaper. + int followingCount = count - (idx - index); + for (int j = idx; j < followingCount; j++) + { + GlyphShapingData current = collection[j]; + if (current.LigatureId == lastLigatureId) + { + int currentLC = current.LigatureComponent == -1 ? 1 : current.LigatureComponent; + int ligatureComponent = currentComponentCount - lastComponentCount + Math.Min(currentLC, lastComponentCount); + current.LigatureId = ligatureId; + current.LigatureComponent = ligatureComponent; + } + else + { + break; + } + } + } + + // Delete the matched glyphs, and replace the current glyph with the ligature glyph + collection.Replace(index, matches, ligatureTable.GlyphId, ligatureId, feature); + return true; + } + + return false; + } + + /// + /// Represents a ligature set table containing an array of ligature tables + /// for a single first-component glyph, ordered by preference. + /// + public readonly struct LigatureSetTable + { + /// + /// Initializes a new instance of the struct. + /// + /// The array of ligature tables. + public LigatureSetTable(LigatureTable[] ligatures) + => this.Ligatures = ligatures; + + /// + /// Gets the array of ligature tables, ordered by preference. + /// + public LigatureTable[] Ligatures { get; } + } + + /// + /// Represents a ligature table that maps a sequence of component glyphs to a single + /// ligature glyph. + /// + public readonly struct LigatureTable + { + /// + /// Initializes a new instance of the struct. + /// + /// The glyph ID of the ligature to substitute. + /// The array of component glyph IDs (starting with the second component). + public LigatureTable(ushort glyphId, ushort[] componentGlyphs) + { + this.GlyphId = glyphId; + this.ComponentGlyphs = componentGlyphs; + } + + /// + /// Gets the glyph ID of the ligature to substitute. + /// + public ushort GlyphId { get; } + + /// + /// Gets the array of component glyph IDs, starting with the second component, + /// ordered in writing direction. + /// + public ushort[] ComponentGlyphs { get; } + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs new file mode 100644 index 0000000..a30053f --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs @@ -0,0 +1,348 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GSub { + /// + /// A Contextual Substitution subtable describes glyph substitutions in context that replace one + /// or more glyphs within a certain pattern of glyphs. + /// + /// + internal static class LookupType5SubTable + { + /// + /// Loads the contextual substitution lookup subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupSubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort subTableFormat = reader.ReadUInt16(); + + return subTableFormat switch + { + 1 => LookupType5Format1SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 2 => LookupType5Format2SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 3 => LookupType5Format3SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + _ => new NotImplementedSubTable(), + }; + } + } + + /// + /// Implements context substitution format 1 (simple glyph contexts). + /// Substitution rules are defined with specific glyph sequences. + /// + /// + internal sealed class LookupType5Format1SubTable : LookupSubTable + { + /// + /// The coverage table that defines the set of first input glyph IDs. + /// + private readonly CoverageTable coverageTable; + + /// + /// The array of sequence rule set tables, ordered by coverage index. + /// + private readonly SequenceRuleSetTable[] seqRuleSetTables; + + /// + /// Initializes a new instance of the class. + /// + /// The coverage table defining first input glyphs. + /// The array of sequence rule set tables. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + private LookupType5Format1SubTable(CoverageTable coverageTable, SequenceRuleSetTable[] seqRuleSetTables, LookupFlags lookupFlags, ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.coverageTable = coverageTable; + this.seqRuleSetTables = seqRuleSetTables; + } + + /// + /// Loads the context substitution format 1 subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupType5Format1SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + SequenceRuleSetTable[] seqRuleSets = TableLoadingUtils.LoadSequenceContextFormat1(reader, offset, out CoverageTable coverageTable); + + return new LookupType5Format1SubTable(coverageTable, seqRuleSets, lookupFlags, markFilteringSet); + } + + /// + public override bool TrySubstitution( + FontMetrics fontMetrics, + GSubTable table, + GlyphSubstitutionCollection collection, + Tag feature, + int index, + int count) + { + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + int offset = this.coverageTable.CoverageIndexOf(glyphId); + if (offset < 0 || offset >= this.seqRuleSetTables.Length) + { + return false; + } + + // TODO: Check this. + // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#example-7-contextual-substitution-format-1 + SequenceRuleSetTable ruleSetTable = this.seqRuleSetTables[offset]; + SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + foreach (SequenceRuleTable ruleTable in ruleSetTable.SequenceRuleTables) + { + int remaining = count - 1; + int seqLength = ruleTable.InputSequence.Length; + if (seqLength > remaining) + { + continue; + } + + if (!AdvancedTypographicUtils.MatchSequence(iterator, 1, ruleTable.InputSequence)) + { + continue; + } + + // It's a match. Perform substitutions and return true if anything changed. + return AdvancedTypographicUtils.ApplyLookupList( + fontMetrics, + table, + feature, + this.LookupFlags, + this.MarkFilteringSet, + ruleTable.SequenceLookupRecords, + collection, + index, + count); + } + + return false; + } + } + + /// + /// Implements context substitution format 2 (class-based glyph contexts). + /// Substitution rules are defined using glyph class definitions. + /// + /// + internal sealed class LookupType5Format2SubTable : LookupSubTable + { + /// + /// The coverage table that defines the set of first input glyph IDs. + /// + private readonly CoverageTable coverageTable; + + /// + /// The class definition table used to classify input glyphs. + /// + private readonly ClassDefinitionTable classDefinitionTable; + + /// + /// The array of class sequence rule set tables, indexed by class value. + /// + private readonly ClassSequenceRuleSetTable[] sequenceRuleSetTables; + + /// + /// Initializes a new instance of the class. + /// + /// The array of class sequence rule set tables. + /// The class definition table for input glyphs. + /// The coverage table defining first input glyphs. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + private LookupType5Format2SubTable( + ClassSequenceRuleSetTable[] sequenceRuleSetTables, + ClassDefinitionTable classDefinitionTable, + CoverageTable coverageTable, + LookupFlags lookupFlags, + ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.sequenceRuleSetTables = sequenceRuleSetTables; + this.classDefinitionTable = classDefinitionTable; + this.coverageTable = coverageTable; + } + + /// + /// Loads the context substitution format 2 subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupType5Format2SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + CoverageTable coverageTable = TableLoadingUtils.LoadSequenceContextFormat2(reader, offset, out ClassDefinitionTable classDefTable, out ClassSequenceRuleSetTable[] classSeqRuleSets); + + return new LookupType5Format2SubTable(classSeqRuleSets, classDefTable, coverageTable, lookupFlags, markFilteringSet); + } + + /// + public override bool TrySubstitution( + FontMetrics fontMetrics, + GSubTable table, + GlyphSubstitutionCollection collection, + Tag feature, + int index, + int count) + { + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + if (this.coverageTable.CoverageIndexOf(glyphId) <= -1) + { + return false; + } + + // TODO: Check this. + // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#52-context-substitution-format-2-class-based-glyph-contexts + int offset = this.classDefinitionTable.ClassIndexOf(glyphId); + if (offset < 0) + { + return false; + } + + ClassSequenceRuleSetTable? ruleSetTable = this.sequenceRuleSetTables[offset]; + if (ruleSetTable is null) + { + return false; + } + + SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + foreach (ClassSequenceRuleTable ruleTable in ruleSetTable.SequenceRuleTables) + { + int remaining = count - 1; + int seqLength = ruleTable.InputSequence.Length; + if (seqLength > remaining) + { + continue; + } + + if (!AdvancedTypographicUtils.MatchClassSequence(iterator, 1, ruleTable.InputSequence, this.classDefinitionTable)) + { + continue; + } + + // It's a match. Perform substitutions and return true if anything changed. + return AdvancedTypographicUtils.ApplyLookupList( + fontMetrics, + table, + feature, + this.LookupFlags, + this.MarkFilteringSet, + ruleTable.SequenceLookupRecords, + collection, + index, + count); + } + + return false; + } + } + + /// + /// Implements context substitution format 3 (coverage-based glyph contexts). + /// Substitution rules are defined using coverage tables for each position in the input sequence. + /// + /// + internal sealed class LookupType5Format3SubTable : LookupSubTable + { + /// + /// The array of coverage tables, one for each position in the input sequence. + /// + private readonly CoverageTable[] coverageTables; + + /// + /// The array of sequence lookup records that define the substitutions to apply. + /// + private readonly SequenceLookupRecord[] sequenceLookupRecords; + + /// + /// Initializes a new instance of the class. + /// + /// The array of coverage tables for each input position. + /// The array of sequence lookup records. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + private LookupType5Format3SubTable( + CoverageTable[] coverageTables, + SequenceLookupRecord[] sequenceLookupRecords, + LookupFlags lookupFlags, + ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.coverageTables = coverageTables; + this.sequenceLookupRecords = sequenceLookupRecords; + } + + /// + /// Loads the context substitution format 3 subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupType5Format3SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + SequenceLookupRecord[] seqLookupRecords = TableLoadingUtils.LoadSequenceContextFormat3(reader, offset, out CoverageTable[] coverageTables); + + return new LookupType5Format3SubTable(coverageTables, seqLookupRecords, lookupFlags, markFilteringSet); + } + + /// + public override bool TrySubstitution( + FontMetrics fontMetrics, + GSubTable table, + GlyphSubstitutionCollection collection, + Tag feature, + int index, + int count) + { + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#53-context-substitution-format-3-coverage-based-glyph-contexts + SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + if (!AdvancedTypographicUtils.MatchCoverageSequence(iterator, this.coverageTables, index, index + count)) + { + return false; + } + + // It's a match. Perform substitutions and return true if anything changed. + return AdvancedTypographicUtils.ApplyLookupList( + fontMetrics, + table, + feature, + this.LookupFlags, + this.MarkFilteringSet, + this.sequenceLookupRecords, + collection, + index, + count); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs new file mode 100644 index 0000000..e0c33b6 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs @@ -0,0 +1,408 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GSub { + /// + /// A Chained Contexts Substitution subtable describes glyph substitutions in context + /// with an ability to look back and/or look ahead in the sequence of glyphs. + /// + /// + internal static class LookupType6SubTable + { + /// + /// Loads the chaining context substitution lookup subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupSubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort substFormat = reader.ReadUInt16(); + + return substFormat switch + { + 1 => LookupType6Format1SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 2 => LookupType6Format2SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + 3 => LookupType6Format3SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + _ => new NotImplementedSubTable(), + }; + } + } + + /// + /// Implements chaining context substitution format 1 (simple glyph contexts). + /// Rules include backtrack, input, and lookahead glyph sequences for matching. + /// + /// + internal sealed class LookupType6Format1SubTable : LookupSubTable + { + /// + /// The coverage table that defines the set of first input glyph IDs. + /// + private readonly CoverageTable coverageTable; + + /// + /// The array of chained sequence rule set tables, ordered by coverage index. + /// + private readonly ChainedSequenceRuleSetTable[] seqRuleSetTables; + + /// + /// Initializes a new instance of the class. + /// + /// The coverage table defining first input glyphs. + /// The array of chained sequence rule set tables. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + private LookupType6Format1SubTable( + CoverageTable coverageTable, + ChainedSequenceRuleSetTable[] seqRuleSetTables, + LookupFlags lookupFlags, + ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.coverageTable = coverageTable; + this.seqRuleSetTables = seqRuleSetTables; + } + + /// + /// Loads the chaining context substitution format 1 subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupType6Format1SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + ChainedSequenceRuleSetTable[] seqRuleSets = TableLoadingUtils.LoadChainedSequenceContextFormat1(reader, offset, out CoverageTable coverageTable); + return new LookupType6Format1SubTable(coverageTable, seqRuleSets, lookupFlags, markFilteringSet); + } + + /// + public override bool TrySubstitution( + FontMetrics fontMetrics, + GSubTable table, + GlyphSubstitutionCollection collection, + Tag feature, + int index, + int count) + { + // Implements Chained Contexts Substitution, Format 1: + // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#61-chained-contexts-substitution-format-1-simple-glyph-contexts + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + // Search for the current glyph in the Coverage table. + int offset = this.coverageTable.CoverageIndexOf(glyphId); + if (offset <= -1) + { + return false; + } + + if (this.seqRuleSetTables is null || this.seqRuleSetTables.Length is 0) + { + return false; + } + + // Apply ruleset for the given glyph id. + SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + ChainedSequenceRuleSetTable seqRuleSet = this.seqRuleSetTables[offset]; + ChainedSequenceRuleTable[] rules = seqRuleSet.SequenceRuleTables; + for (int i = 0; i < rules.Length; i++) + { + ChainedSequenceRuleTable ruleTable = rules[i]; + if (!AdvancedTypographicUtils.ApplyChainedSequenceRule(iterator, ruleTable)) + { + continue; + } + + return AdvancedTypographicUtils.ApplyLookupList( + fontMetrics, + table, + feature, + this.LookupFlags, + this.MarkFilteringSet, + ruleTable.SequenceLookupRecords, + collection, + index, + count); + } + + return false; + } + } + + /// + /// Implements chaining context substitution format 2 (class-based glyph contexts). + /// Rules use class definitions for backtrack, input, and lookahead sequences. + /// + /// + internal sealed class LookupType6Format2SubTable : LookupSubTable + { + /// + /// The coverage table that defines the set of first input glyph IDs. + /// + private readonly CoverageTable coverageTable; + + /// + /// The class definition table used to classify input glyphs. + /// + private readonly ClassDefinitionTable inputClassDefinitionTable; + + /// + /// The class definition table used to classify backtrack glyphs. + /// + private readonly ClassDefinitionTable backtrackClassDefinitionTable; + + /// + /// The class definition table used to classify lookahead glyphs. + /// + private readonly ClassDefinitionTable lookaheadClassDefinitionTable; + + /// + /// The array of chained class sequence rule set tables, indexed by input class value. + /// + private readonly ChainedClassSequenceRuleSetTable[] sequenceRuleSetTables; + + /// + /// Initializes a new instance of the class. + /// + /// The array of chained class sequence rule set tables. + /// The class definition table for backtrack glyphs. + /// The class definition table for input glyphs. + /// The class definition table for lookahead glyphs. + /// The coverage table defining first input glyphs. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + private LookupType6Format2SubTable( + ChainedClassSequenceRuleSetTable[] sequenceRuleSetTables, + ClassDefinitionTable backtrackClassDefinitionTable, + ClassDefinitionTable inputClassDefinitionTable, + ClassDefinitionTable lookaheadClassDefinitionTable, + CoverageTable coverageTable, + LookupFlags lookupFlags, + ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.sequenceRuleSetTables = sequenceRuleSetTables; + this.backtrackClassDefinitionTable = backtrackClassDefinitionTable; + this.inputClassDefinitionTable = inputClassDefinitionTable; + this.lookaheadClassDefinitionTable = lookaheadClassDefinitionTable; + this.coverageTable = coverageTable; + } + + /// + /// Loads the chaining context substitution format 2 subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupType6Format2SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + ChainedClassSequenceRuleSetTable[] seqRuleSets = TableLoadingUtils.LoadChainedSequenceContextFormat2( + reader, + offset, + out CoverageTable coverageTable, + out ClassDefinitionTable backtrackClassDefTable, + out ClassDefinitionTable inputClassDefTable, + out ClassDefinitionTable lookaheadClassDefTable); + + return new LookupType6Format2SubTable( + seqRuleSets, + backtrackClassDefTable, + inputClassDefTable, + lookaheadClassDefTable, + coverageTable, + lookupFlags, + markFilteringSet); + } + + /// + public override bool TrySubstitution( + FontMetrics fontMetrics, + GSubTable table, + GlyphSubstitutionCollection collection, + Tag feature, + int index, + int count) + { + // Implements Chained Contexts Substitution for Format 2: + // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#62-chained-contexts-substitution-format-2-class-based-glyph-contexts + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + // Search for the current glyph in the Coverage table. + int offset = this.coverageTable.CoverageIndexOf(glyphId); + if (offset <= -1) + { + return false; + } + + // Search in the class definition table to find the class value assigned to the currently glyph. + int classId = this.inputClassDefinitionTable.ClassIndexOf(glyphId); + ChainedClassSequenceRuleTable[]? rules = classId >= 0 && classId < this.sequenceRuleSetTables.Length ? this.sequenceRuleSetTables[classId]?.SubRules : null; + if (rules is null) + { + return false; + } + + // Apply ruleset for the given glyph class id. + SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + for (int lookupIndex = 0; lookupIndex < rules.Length; lookupIndex++) + { + ChainedClassSequenceRuleTable ruleTable = rules[lookupIndex]; + + if (!AdvancedTypographicUtils.ApplyChainedClassSequenceRule(iterator, ruleTable, this.inputClassDefinitionTable, this.backtrackClassDefinitionTable, this.lookaheadClassDefinitionTable)) + { + continue; + } + + return AdvancedTypographicUtils.ApplyLookupList( + fontMetrics, + table, + feature, + this.LookupFlags, + this.MarkFilteringSet, + ruleTable.SequenceLookupRecords, + collection, + index, + count); + } + + return false; + } + } + + /// + /// Implements chaining context substitution format 3 (coverage-based glyph contexts). + /// Rules use separate coverage tables for backtrack, input, and lookahead sequences. + /// + /// + internal sealed class LookupType6Format3SubTable : LookupSubTable + { + /// + /// The array of sequence lookup records that define the substitutions to apply. + /// + private readonly SequenceLookupRecord[] sequenceLookupRecords; + + /// + /// The array of coverage tables for the backtrack sequence. + /// + private readonly CoverageTable[] backtrackCoverageTables; + + /// + /// The array of coverage tables for the input sequence. + /// + private readonly CoverageTable[] inputCoverageTables; + + /// + /// The array of coverage tables for the lookahead sequence. + /// + private readonly CoverageTable[] lookaheadCoverageTables; + + /// + /// Initializes a new instance of the class. + /// + /// The array of sequence lookup records. + /// The coverage tables for the backtrack sequence. + /// The coverage tables for the input sequence. + /// The coverage tables for the lookahead sequence. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + private LookupType6Format3SubTable( + SequenceLookupRecord[] seqLookupRecords, + CoverageTable[] backtrackCoverageTables, + CoverageTable[] inputCoverageTables, + CoverageTable[] lookaheadCoverageTables, + LookupFlags lookupFlags, + ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.sequenceLookupRecords = seqLookupRecords; + this.backtrackCoverageTables = backtrackCoverageTables; + this.inputCoverageTables = inputCoverageTables; + this.lookaheadCoverageTables = lookaheadCoverageTables; + } + + /// + /// Loads the chaining context substitution format 3 subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupType6Format3SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + SequenceLookupRecord[] seqLookupRecords = TableLoadingUtils.LoadChainedSequenceContextFormat3( + reader, + offset, + out CoverageTable[] backtrackCoverageTables, + out CoverageTable[] inputCoverageTables, + out CoverageTable[] lookaheadCoverageTables); + + return new LookupType6Format3SubTable( + seqLookupRecords, + backtrackCoverageTables, + inputCoverageTables, + lookaheadCoverageTables, + lookupFlags, + markFilteringSet); + } + + /// + public override bool TrySubstitution( + FontMetrics fontMetrics, + GSubTable table, + GlyphSubstitutionCollection collection, + Tag feature, + int index, + int count) + { + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + if (!AdvancedTypographicUtils.CheckAllCoverages( + fontMetrics, + this.LookupFlags, + this.MarkFilteringSet, + collection, + index, + count, + this.inputCoverageTables, + this.backtrackCoverageTables, + this.lookaheadCoverageTables)) + { + return false; + } + + // It's a match. Perform substitutions and return true if anything changed. + return AdvancedTypographicUtils.ApplyLookupList( + fontMetrics, + table, + feature, + this.LookupFlags, + this.MarkFilteringSet, + this.sequenceLookupRecords, + collection, + index, + count); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType7SubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType7SubTable.cs new file mode 100644 index 0000000..47b3819 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType7SubTable.cs @@ -0,0 +1,92 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GSub { + /// + /// This lookup provides a mechanism whereby any other lookup type’s subtables are stored at a 32-bit offset location + /// in the GSUB table. This is needed if the total size of the subtables exceeds the 16-bit limits of the various + /// other offsets in the GSUB table. In this specification, the subtable stored at the 32-bit offset location is + /// termed the "extension" subtable. + /// + /// + internal static class LookupType7SubTable + { + /// + /// Loads the extension substitution lookup subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The delegate used to load the referenced extension subtable. + /// The loaded . + public static LookupSubTable Load( + BigEndianBinaryReader reader, + long offset, + LookupFlags lookupFlags, + ushort markFilteringSet, + Func subTableLoader) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort substFormat = reader.ReadUInt16(); + + return substFormat switch + { + 1 => LookupType7Format1SubTable.Load(reader, offset, lookupFlags, markFilteringSet, subTableLoader), + _ => new NotImplementedSubTable(), + }; + } + } + + /// + /// Implements extension substitution format 1. This format provides a 32-bit offset to an + /// extension subtable of any other lookup type, enabling subtables that exceed the 16-bit + /// offset limit. + /// + /// + internal static class LookupType7Format1SubTable + { + /// + /// Loads the extension substitution format 1 subtable and resolves the referenced extension subtable. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the extension substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The delegate used to load the referenced extension subtable. + /// The loaded . + public static LookupSubTable Load( + BigEndianBinaryReader reader, + long offset, + LookupFlags lookupFlags, + ushort markFilteringSet, + Func subTableLoader) + { + // +----------+---------------------+------------------------------------------------------------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+=====================+====================================================================================================================================+ + // | uint16 | substFormat | Format identifier. Set to 1. | + // +----------+---------------------+------------------------------------------------------------------------------------------------------------------------------------+ + // | uint16 | extensionLookupType | Lookup type of subtable referenced by extensionOffset (that is, the extension subtable). | + // +----------+---------------------+------------------------------------------------------------------------------------------------------------------------------------+ + // | Offset32 | extensionOffset | Offset to the extension subtable, of lookup type extensionLookupType, relative to the start of the ExtensionSubstFormat1 subtable. | + // +----------+---------------------+------------------------------------------------------------------------------------------------------------------------------------+ + ushort extensionLookupType = reader.ReadUInt16(); + uint extensionOffset = reader.ReadOffset32(); + + // The extensionLookupType field must be set to any lookup type other than 7. + // All subtables in a LookupType 7 lookup must have the same extensionLookupType. + if (extensionLookupType == 7) + { + // Don't throw, we'll just ignore. + return new NotImplementedSubTable(); + } + + // Read the lookup table again with the updated offset. + return subTableLoader(extensionLookupType, lookupFlags, markFilteringSet, reader, offset + extensionOffset); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs new file mode 100644 index 0000000..6327c38 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs @@ -0,0 +1,204 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GSub { + /// + /// A Reverse Chaining Contextual Single Substitution subtable describes single glyph substitutions + /// in context with an ability to look back and/or look ahead in the sequence of glyphs. + /// The difference from other chaining lookups is that processing is applied in reverse order (from end of glyph sequence). + /// + /// + internal static class LookupType8SubTable + { + /// + /// Loads the reverse chaining contextual single substitution lookup subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupSubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + reader.Seek(offset, SeekOrigin.Begin); + ushort substFormat = reader.ReadUInt16(); + + return substFormat switch + { + 1 => LookupType8Format1SubTable.Load(reader, offset, lookupFlags, markFilteringSet), + _ => new NotImplementedSubTable(), + }; + } + } + + /// + /// Implements reverse chaining contextual single substitution format 1 (coverage-based glyph contexts). + /// Substitution is processed in reverse order from the end of the glyph sequence. + /// + /// + internal sealed class LookupType8Format1SubTable : LookupSubTable + { + /// + /// The array of substitute glyph IDs, ordered by coverage index. + /// + private readonly ushort[] substituteGlyphIds; + + /// + /// The coverage table that defines the set of input glyph IDs. + /// + private readonly CoverageTable coverageTable; + + /// + /// The array of coverage tables for the backtrack sequence. + /// + private readonly CoverageTable[] backtrackCoverageTables; + + /// + /// The array of coverage tables for the lookahead sequence. + /// + private readonly CoverageTable[] lookaheadCoverageTables; + + /// + /// Initializes a new instance of the class. + /// + /// The array of substitute glyph IDs. + /// The coverage table defining input glyphs. + /// The coverage tables for the backtrack sequence. + /// The coverage tables for the lookahead sequence. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + private LookupType8Format1SubTable( + ushort[] substituteGlyphIds, + CoverageTable coverageTable, + CoverageTable[] backtrackCoverageTables, + CoverageTable[] lookaheadCoverageTables, + LookupFlags lookupFlags, + ushort markFilteringSet) + : base(lookupFlags, markFilteringSet) + { + this.substituteGlyphIds = substituteGlyphIds; + this.coverageTable = coverageTable; + this.backtrackCoverageTables = backtrackCoverageTables; + this.lookaheadCoverageTables = lookaheadCoverageTables; + } + + /// + /// Loads the reverse chaining contextual single substitution format 1 subtable from the given offset. + /// + /// The big-endian binary reader. + /// The offset to the beginning of the substitution subtable. + /// The lookup qualifiers flags. + /// The index into the GDEF mark glyph sets structure. + /// The loaded . + public static LookupType8Format1SubTable Load(BigEndianBinaryReader reader, long offset, LookupFlags lookupFlags, ushort markFilteringSet) + { + // ReverseChainSingleSubstFormat1 + // +----------+-----------------------------------------------+----------------------------------------------+ + // | Type | Name | Description | + // +==========+===============================================+==============================================+ + // | uint16 | substFormat | Format identifier: format = 1 | + // +----------+-----------------------------------------------+----------------------------------------------+ + // | Offset16 | coverageOffset | Offset to Coverage table, from beginning | + // | | | of substitution subtable. | + // +----------+-----------------------------------------------+----------------------------------------------+ + // | uint16 | backtrackGlyphCount | Number of glyphs in the backtrack sequence. | + // +----------+-----------------------------------------------+----------------------------------------------+ + // | Offset16 | backtrackCoverageOffsets[backtrackGlyphCount] | Array of offsets to coverage tables in | + // | | | backtrack sequence, in glyph sequence | + // | | | order. | + // +----------+-----------------------------------------------+----------------------------------------------+ + // | uint16 | lookaheadGlyphCount | Number of glyphs in lookahead sequence. | + // +----------+-----------------------------------------------+----------------------------------------------+ + // | Offset16 | lookaheadCoverageOffsets[lookaheadGlyphCount] | Array of offsets to coverage tables in | + // | | | lookahead sequence, in glyph sequence order. | + // +----------+-----------------------------------------------+----------------------------------------------+ + // | uint16 | glyphCount | Number of glyph IDs in the | + // | | | substituteGlyphIDs array. | + // +----------+-----------------------------------------------+----------------------------------------------+ + // | uint16 | substituteGlyphIDs[glyphCount] | Array of substitute glyph IDs — ordered | + // | | | by Coverage index. | + // +----------+-----------------------------------------------+----------------------------------------------+ + ushort coverageOffset = reader.ReadOffset16(); + ushort backtrackGlyphCount = reader.ReadUInt16(); + + using Buffer backtrackCoverageOffsetsBuffer = new(backtrackGlyphCount); + Span backtrackCoverageOffsets = backtrackCoverageOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(backtrackCoverageOffsets); + + ushort lookaheadGlyphCount = reader.ReadUInt16(); + + using Buffer lookaheadCoverageOffsetsBuffer = new(lookaheadGlyphCount); + Span lookaheadCoverageOffsets = lookaheadCoverageOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(lookaheadCoverageOffsets); + + ushort glyphCount = reader.ReadUInt16(); + ushort[] substituteGlyphIds = reader.ReadUInt16Array(glyphCount); + + CoverageTable coverageTable = CoverageTable.Load(reader, offset + coverageOffset); + CoverageTable[] backtrackCoverageTables = CoverageTable.LoadArray(reader, offset, backtrackCoverageOffsets); + CoverageTable[] lookaheadCoverageTables = CoverageTable.LoadArray(reader, offset, lookaheadCoverageOffsets); + + return new LookupType8Format1SubTable( + substituteGlyphIds, + coverageTable, + backtrackCoverageTables, + lookaheadCoverageTables, + lookupFlags, + markFilteringSet); + } + + /// + public override bool TrySubstitution( + FontMetrics fontMetrics, + GSubTable table, + GlyphSubstitutionCollection collection, + Tag feature, + int index, + int count) + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#81-reverse-chaining-contextual-single-substitution-format-1-coverage-based-glyph-contexts + ushort glyphId = collection[index].GlyphId; + if (glyphId == 0) + { + return false; + } + + int offset = this.coverageTable.CoverageIndexOf(glyphId); + if (offset <= -1) + { + return false; + } + + for (int i = 0; i < this.backtrackCoverageTables.Length; ++i) + { + ushort id = collection[index - 1 - i].GlyphId; + if (id == 0 || this.backtrackCoverageTables[i].CoverageIndexOf(id) < 0) + { + return false; + } + } + + for (int i = 0; i < this.lookaheadCoverageTables.Length; ++i) + { + ushort id = collection[index + i].GlyphId; + if (id == 0 || this.lookaheadCoverageTables[i].CoverageIndexOf(id) < 0) + { + return false; + } + } + + // It's a match. Perform substitutions and return true if anything changed. + bool hasChanged = false; + for (int i = 0; i < this.substituteGlyphIds.Length; i++) + { + collection.Replace(index + i, this.substituteGlyphIds[i], feature); + hasChanged = true; + } + + return hasChanged; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs new file mode 100644 index 0000000..4892a5e --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.GSub { + /// + /// A placeholder lookup subtable used when the lookup type is not implemented. + /// This subtable always returns for substitution attempts. + /// + internal class NotImplementedSubTable : LookupSubTable + { + /// + /// Initializes a new instance of the class. + /// + public NotImplementedSubTable() + : base(default, 0) + { + } + + /// + public override bool TrySubstitution( + FontMetrics fontMetrics, + GSubTable table, + GlyphSubstitutionCollection collection, + Tag feature, + int index, + int count) + => false; + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs new file mode 100644 index 0000000..40d627f --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -0,0 +1,467 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using SixLabors.Fonts.Tables.AdvancedTypographic.GSub; +using SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// The Glyph Substitution (GSUB) table provides data for substitution of glyphs for appropriate rendering of scripts, + /// such as cursively-connecting forms in Arabic script, or for advanced typographic effects, such as ligatures. + /// + /// + internal class GSubTable : Table + { + /// + /// The OpenType table tag for the GSUB table. + /// + internal const string TableName = "GSUB"; + + /// + /// Initializes a new instance of the class. + /// + /// The script list table, or if not present. + /// The feature list table. + /// The lookup list table. + /// The feature variations table for variable fonts, or . + public GSubTable(ScriptList? scriptList, FeatureListTable featureList, LookupListTable lookupList, FeatureVariationsTable? featureVariations = null) + { + this.ScriptList = scriptList; + this.FeatureList = featureList; + this.LookupList = lookupList; + this.FeatureVariations = featureVariations; + } + + /// + /// Gets the script list table, or if not present. + /// + public ScriptList? ScriptList { get; } + + /// + /// Gets the feature list table. + /// + public FeatureListTable FeatureList { get; } + + /// + /// Gets the lookup list table containing all substitution lookups. + /// + public LookupListTable LookupList { get; } + + /// + /// Gets the feature variations table for variable fonts, or if not present. + /// + public FeatureVariationsTable? FeatureVariations { get; } + + /// + /// Loads the from the font reader. + /// + /// The font reader. + /// The , or if not present. + public static GSubTable? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + /// + /// Loads the from a big endian binary reader. + /// + /// The big endian binary reader. + /// The . + internal static GSubTable Load(BigEndianBinaryReader reader) + { + // GSUB Header, Version 1.0 + // +----------+-------------------+-----------------------------------------------------------+ + // | Type | Name | Description | + // +==========+===================+===========================================================+ + // | uint16 | majorVersion | Major version of the GSUB table, = 1 | + // +----------+-------------------+-----------------------------------------------------------+ + // | uint16 | minorVersion | Minor version of the GSUB table, = 0 | + // +----------+-------------------+-----------------------------------------------------------+ + // | Offset16 | scriptListOffset | Offset to ScriptList table, from beginning of GSUB table | + // +----------+-------------------+-----------------------------------------------------------+ + // | Offset16 | featureListOffset | Offset to FeatureList table, from beginning of GSUB table | + // +----------+-------------------+-----------------------------------------------------------+ + // | Offset16 | lookupListOffset | Offset to LookupList table, from beginning of GSUB table | + // +----------+-------------------+-----------------------------------------------------------+ + + // GSUB Header, Version 1.1 + // +----------+-------------------------+-------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+=========================+===============================================================================+ + // | uint16 | majorVersion | Major version of the GSUB table, = 1 | + // +----------+-------------------------+-------------------------------------------------------------------------------+ + // | uint16 | minorVersion | Minor version of the GSUB table, = 1 | + // +----------+-------------------------+-------------------------------------------------------------------------------+ + // | Offset16 | scriptListOffset | Offset to ScriptList table, from beginning of GSUB table | + // +----------+-------------------------+-------------------------------------------------------------------------------+ + // | Offset16 | featureListOffset | Offset to FeatureList table, from beginning of GSUB table | + // +----------+-------------------------+-------------------------------------------------------------------------------+ + // | Offset16 | lookupListOffset | Offset to LookupList table, from beginning of GSUB table | + // +----------+-------------------------+-------------------------------------------------------------------------------+ + // | Offset32 | featureVariationsOffset | Offset to FeatureVariations table, from beginning of GSUB table (may be NULL) | + // +----------+-------------------------+-------------------------------------------------------------------------------+ + ushort majorVersion = reader.ReadUInt16(); + ushort minorVersion = reader.ReadUInt16(); + + ushort scriptListOffset = reader.ReadOffset16(); + ushort featureListOffset = reader.ReadOffset16(); + ushort lookupListOffset = reader.ReadOffset16(); + uint featureVariationsOffset = (minorVersion == 1) ? reader.ReadOffset32() : 0; + + // TODO: Optimization. Allow only reading the scriptList. + ScriptList? scriptList = ScriptList.Load(reader, scriptListOffset); + + FeatureListTable featureList = FeatureListTable.Load(reader, featureListOffset); + + LookupListTable lookupList = LookupListTable.Load(reader, lookupListOffset); + + FeatureVariationsTable? featureVariations = featureVariationsOffset != 0 + ? FeatureVariationsTable.Load(reader, featureVariationsOffset, featureList) + : null; + + return new GSubTable(scriptList, featureList, lookupList, featureVariations); + } + + /// + /// Applies glyph substitution to the collection using GSUB lookup rules. + /// + /// The font metrics. + /// The glyph substitution collection. + public void ApplySubstitution(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) + { + // Set max constraints to prevent OutOfMemoryException or infinite loops from attacks. + int maxCount = AdvancedTypographicUtils.GetMaxAllowableShapingCollectionCount(collection.Count); + int maxOperationsCount = AdvancedTypographicUtils.GetMaxAllowableShapingOperationsCount(collection.Count); + int currentOperations = 0; + + for (int i = 0; i < collection.Count; i++) + { + // Choose a shaper based on the script. + // This determines which features to apply to which glyphs. + ScriptClass current = this.GetScriptClass(CodePoint.GetScriptClass(collection[i].CodePoint)); + + int index = i; + int count = 1; + while (i < collection.Count - 1) + { + // We want to assign the same feature lookups to individual sections of the text rather + // than the text as a whole to ensure that different language shapers do not interfere + // with each other when the text contains multiple languages. + ScriptClass next = this.GetScriptClass(CodePoint.GetScriptClass(collection[i + 1].CodePoint)); + if (next != current && + current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClass.Inherited && + next is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClass.Inherited) + { + break; + } + + if (current is ScriptClass.Common or ScriptClass.Unknown or ScriptClass.Inherited) + { + current = next; + } + + i++; + count++; + + if (i >= maxCount) + { + break; + } + } + + Tag unicodeScriptTag = this.GetUnicodeScriptTag(current); + BaseShaper shaper = ShaperFactory.Create(current, unicodeScriptTag, fontMetrics, collection.TextOptions); + + // Plan substitution features for each glyph. + // Shapers can adjust the count during initialization and feature processing so we must capture + // the current count to allow resetting indexes and processing counts. + int collectionCount = collection.Count; + shaper.Plan(collection, index, count); + int delta = collection.Count - collectionCount; + i += delta; + count += delta; + + IEnumerable stages = shaper.GetShapingStages(); + SkippingGlyphIterator iterator = new(fontMetrics, collection, index, default, 0); + foreach (ShapingStage stage in stages) + { + collectionCount = collection.Count; + stage.PreProcessFeature(collection, index, count); + + // Account for substitutions changing the length of the collection. + delta = collection.Count - collectionCount; + count += delta; + i += delta; + + Tag featureTag = stage.FeatureTag; + + this.ApplyFeature( + fontMetrics, + collection, + ref iterator, + in featureTag, + current, + index, + ref count, + ref i, + ref collectionCount, + maxCount, + maxOperationsCount, + ref currentOperations); + + collectionCount = collection.Count; + stage.PostProcessFeature(collection, index, count); + + // Account for substitutions changing the length of the collection. + delta = collection.Count - collectionCount; + count += delta; + i += delta; + } + } + } + + /// + /// Applies a specific feature's lookups to the glyph substitution collection. + /// + /// The font metrics. + /// The glyph substitution collection. + /// The skipping glyph iterator. + /// The feature tag to apply. + /// The current script class. + /// The starting index in the collection. + /// The number of glyphs to process (updated by substitutions). + /// The outer loop index (updated by substitutions). + /// The tracked collection count (updated by substitutions). + /// The maximum allowable collection count. + /// The maximum allowable operations count. + /// The current operations counter. + internal void ApplyFeature( + FontMetrics fontMetrics, + GlyphSubstitutionCollection collection, + ref SkippingGlyphIterator iterator, + in Tag featureTag, + ScriptClass current, + int index, + ref int count, + ref int i, + ref int collectionCount, + int maxCount, + int maxOperationsCount, + ref int currentOperations) + { + if (this.TryGetFeatureLookups(fontMetrics, in featureTag, current, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups)) + { + // Apply features in order. + foreach ((Tag Feature, ushort Index, LookupTable LookupTable) featureLookup in lookups) + { + Tag feature = featureLookup.Feature; + LookupTable featureLookupTable = featureLookup.LookupTable; + iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); + + while (iterator.Index < index + count) + { + if (collection.Count >= maxCount || currentOperations++ >= maxOperationsCount) + { + return; + } + + if (!collection[iterator.Index].EnabledFeatureTags.Contains(feature)) + { + iterator.Next(); + continue; + } + + collectionCount = collection.Count; + featureLookup.LookupTable.TrySubstitution(fontMetrics, this, collection, featureLookup.Feature, iterator.Index, count - (iterator.Index - index)); + iterator.Next(); + + // Account for substitutions changing the length of the collection. + int delta = collection.Count - collectionCount; + count += delta; + i += delta; + } + } + } + } + + /// + /// Tries to get the feature lookups for the given stage feature and script. + /// + /// The font metrics. + /// The feature tag for the current shaping stage. + /// The script class. + /// When this method returns, contains the list of feature lookups if found. + /// if lookups were found; otherwise, . + internal bool TryGetFeatureLookups( + FontMetrics fontMetrics, + in Tag stageFeature, + ScriptClass script, + [NotNullWhen(true)] out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? value) + { + if (this.ScriptList is null) + { + value = null; + return false; + } + + // Resolve feature substitutions from FeatureVariations (variable fonts). + FeatureTableSubstitutionRecord[]? substitutions = this.FeatureVariations + ?.FindMatchingSubstitutions(fontMetrics.GetNormalizedCoordinates()); + + ScriptListTable scriptListTable = this.ScriptList.Default(); + Tag[] tags = UnicodeScriptTagMap.Instance[script]; + for (int i = 0; i < tags.Length; i++) + { + if (this.ScriptList.TryGetValue(tags[i].Value, out ScriptListTable? table)) + { + scriptListTable = table; + break; + } + } + + LangSysTable? defaultLangSysTable = scriptListTable.DefaultLangSysTable; + if (defaultLangSysTable != null) + { + value = this.GetFeatureLookups(stageFeature, substitutions, defaultLangSysTable); + return value.Count > 0; + } + + value = this.GetFeatureLookups(stageFeature, substitutions, scriptListTable.LangSysTables); + return value.Count > 0; + } + + /// + /// Gets the OpenType script tag for the given script class, checking against the font's ScriptList. + /// + /// The script class. + /// The matching script tag, or default if not found. + private Tag GetUnicodeScriptTag(ScriptClass script) + { + if (this.ScriptList is null) + { + return default; + } + + Tag[] tags = UnicodeScriptTagMap.Instance[script]; + for (int i = 0; i < tags.Length; i++) + { + if (this.ScriptList.TryGetValue(tags[i].Value, out ScriptListTable? _)) + { + return tags[i]; + } + } + + return default; + } + + /// + /// Gets the feature lookups for the given stage feature from the specified language system tables. + /// + /// The feature tag for the current shaping stage. + /// Optional feature table substitutions from FeatureVariations. + /// The language system tables to search. + /// A sorted list of feature lookups. + private List<(Tag Feature, ushort Index, LookupTable LookupTable)> GetFeatureLookups( + in Tag stageFeature, + FeatureTableSubstitutionRecord[]? substitutions, + params LangSysTable[] langSysTables) + { + List<(Tag Feature, ushort Index, LookupTable LookupTable)> lookups = []; + for (int i = 0; i < langSysTables.Length; i++) + { + ushort[] featureIndices = langSysTables[i].FeatureIndices; + for (int j = 0; j < featureIndices.Length; j++) + { + ushort featureIndex = featureIndices[j]; + FeatureTable featureTable = ResolveFeatureTable(this.FeatureList, featureIndex, substitutions); + Tag feature = featureTable.FeatureTag; + + if (stageFeature != feature) + { + continue; + } + + ushort[] lookupListIndices = featureTable.LookupListIndices; + for (int k = 0; k < lookupListIndices.Length; k++) + { + ushort lookupIndex = lookupListIndices[k]; + LookupTable lookupTable = this.LookupList.LookupTables[lookupIndex]; + lookups.Add(new(feature, lookupIndex, lookupTable)); + } + } + } + + lookups.Sort((x, y) => x.Index - y.Index); + return lookups; + } + + /// + /// Resolves the feature table for the given index, checking for substitutions from FeatureVariations first. + /// + /// The feature list table. + /// The feature index. + /// Optional feature table substitutions from FeatureVariations. + /// The resolved feature table. + private static FeatureTable ResolveFeatureTable( + FeatureListTable featureList, + ushort featureIndex, + FeatureTableSubstitutionRecord[]? substitutions) + { + if (substitutions is not null) + { + for (int i = 0; i < substitutions.Length; i++) + { + if (substitutions[i].FeatureIndex == featureIndex) + { + return substitutions[i].AlternateFeatureTable; + } + } + } + + return featureList.FeatureTables[featureIndex]; + } + + /// + /// Maps a script class to an effective script class, checking whether the font supports it. + /// Falls back to if the script is not present in the font. + /// + /// The script class to check. + /// The effective script class. + private ScriptClass GetScriptClass(ScriptClass current) + { + if (current is ScriptClass.Common or ScriptClass.Unknown or ScriptClass.Inherited) + { + return current; + } + + if (this.ScriptList is null) + { + return ScriptClass.Default; + } + + Tag[] tags = UnicodeScriptTagMap.Instance[current]; + + for (int i = 0; i < tags.Length; i++) + { + if (this.ScriptList.TryGetValue(tags[i].Value, out ScriptListTable? _)) + { + return current; + } + } + + // Script for `current` not present in the font: use default shaper. + return ScriptClass.Default; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GlyphClassDef.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GlyphClassDef.cs new file mode 100644 index 0000000..abfe1a8 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GlyphClassDef.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// The GSUB and GPOS tables use the Glyph Class Definition table (GlyphClassDef) to identify which glyph classes to adjust with lookups. + /// + /// + public enum GlyphClassDef + { + /// + /// Base glyph (single character, spacing glyph). + /// + BaseGlyph = 1, + + /// + /// Ligature glyph (multiple character, spacing glyph). + /// + LigatureGlyph = 2, + + /// + /// Mark glyph (non-spacing combining glyph). + /// + MarkGlyph = 3, + + /// + /// Component glyph (part of single character, spacing glyph). + /// + ComponentGlyph = 4, + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/GlyphDefinitionTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/GlyphDefinitionTable.cs new file mode 100644 index 0000000..be972ab --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/GlyphDefinitionTable.cs @@ -0,0 +1,225 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// The GDEF table contains three kinds of information in subtables: + /// 1. glyph class definitions that classify different types of glyphs in a font; + /// 2. attachment point lists that identify glyph positioning attachments for each glyph; + /// and 3. ligature caret lists that provide information for caret positioning and text selection involving ligatures. + /// + /// + internal sealed class GlyphDefinitionTable : Table + { + /// + /// The OpenType table tag for the GDEF table. + /// + internal const string TableName = "GDEF"; + + /// + /// Gets the class definition table for glyph types (base, ligature, mark, component). + /// + public ClassDefinitionTable? GlyphClassDefinition { get; private set; } + + /// + /// Gets the attachment point list table for identifying glyph attachment points. + /// + public AttachmentListTable? AttachmentListTable { get; private set; } + + /// + /// Gets the ligature caret list table for positioning carets within ligatures. + /// + public LigatureCaretList? LigatureCaretList { get; private set; } + + /// + /// Gets the class definition table for mark attachment types. + /// + public ClassDefinitionTable? MarkAttachmentClassDef { get; private set; } + + /// + /// Gets the mark glyph sets table for filtering marks during lookup processing. + /// + public MarkGlyphSetsTable? MarkGlyphSetsTable { get; private set; } + + /// + /// Gets the item variation store table for variable font GDEF variations. + /// + public ItemVariationStore? ItemVariationStore { get; private set; } + + /// + /// Loads the from the font reader. + /// + /// The font reader. + /// The , or if not present. + public static GlyphDefinitionTable? Load(FontReader reader) + { + if (!reader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + /// + /// Tries to get the glyph class for the specified glyph. + /// + /// The glyph identifier. + /// When this method returns, contains the glyph class if found. + /// if the glyph class was found; otherwise, . + public bool TryGetGlyphClass(ushort glyphId, [NotNullWhen(true)] out GlyphClassDef? glyphClass) + { + glyphClass = null; + + if (this.GlyphClassDefinition is null) + { + return false; + } + + glyphClass = (GlyphClassDef)this.GlyphClassDefinition.ClassIndexOf(glyphId); + return true; + } + + /// + /// Tries to get the mark attachment class for the specified glyph. + /// + /// The glyph identifier. + /// When this method returns, contains the mark attachment class if found. + /// if the mark attachment class was found; otherwise, . + public bool TryGetMarkAttachmentClass(ushort glyphId, [NotNullWhen(true)] out GlyphClassDef? markAttachmentClass) + { + markAttachmentClass = null; + + if (this.MarkAttachmentClassDef is null) + { + return false; + } + + markAttachmentClass = (GlyphClassDef)this.MarkAttachmentClassDef.ClassIndexOf(glyphId); + return true; + } + + /// + /// Determines whether the specified glyph belongs to the given mark glyph set. + /// + /// The index of the mark glyph set. + /// The glyph identifier. + /// if the glyph is in the set; otherwise, . + public bool IsInMarkGlyphSet(ushort markGlyphSetIndex, ushort glyphId) + => this.MarkGlyphSetsTable?.Contains(markGlyphSetIndex, glyphId) == true; + + /// + /// Loads the from a big endian binary reader. + /// + /// The big endian binary reader. + /// The . + public static GlyphDefinitionTable Load(BigEndianBinaryReader reader) + { + // Header version 1.0 + // Type | Name | Description + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // uint16 | majorVersion | Major version of the GDEF table, = 1 + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // uint16 | minorVersion | Minor version of the GDEF table, = 0 + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | glyphClassDefOffset | Offset to class definition table for glyph type, from beginning of GDEF header(may be NULL). + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | attachListOffset | Offset to attachment point list table, from beginning of GDEF header(may be NULL). + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | ligCaretListOffset | Offset to ligature caret list table, from beginning of GDEF header(may be NULL). + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | markAttachClassDefOffset | Offset to class definition table for mark attachment type, from beginning of GDEF header(may be NULL). + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + + // Header version 1.2 + // Type | Name | Description + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // uint16 | majorVersion | Major version of the GDEF table, = 1 + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // uint16 | minorVersion | Minor version of the GDEF table, = 0 + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | glyphClassDefOffset | Offset to class definition table for glyph type, from beginning of GDEF header(may be NULL). + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | attachListOffset | Offset to attachment point list table, from beginning of GDEF header(may be NULL). + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | ligCaretListOffset | Offset to ligature caret list table, from beginning of GDEF header(may be NULL). + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | markAttachClassDefOffset | Offset to class definition table for mark attachment type, from beginning of GDEF header (may be NULL). + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | markGlyphSetsDefOffset | Offset to the table of mark glyph set definitions, from beginning of GDEF header (may be NULL). + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + + // Header version 1.3 + // Type | Name | Description + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // uint16 | majorVersion | Major version of the GDEF table, = 1 + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // uint16 | minorVersion | Minor version of the GDEF table, = 0 + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | glyphClassDefOffset | Offset to class definition table for glyph type, from beginning of GDEF header(may be NULL). + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | attachListOffset | Offset to attachment point list table, from beginning of GDEF header(may be NULL). + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | ligCaretListOffset | Offset to ligature caret list table, from beginning of GDEF header(may be NULL). + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | markAttachClassDefOffset | Offset to class definition table for mark attachment type, from beginning of GDEF header (may be NULL). + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | markGlyphSetsDefOffset | Offset to the table of mark glyph set definitions, from beginning of GDEF header (may be NULL). + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + // Offset32 | itemVarStoreOffset | Offset to the Item Variation Store table, from beginning of GDEF header (may be NULL). + // ----------|--------------------------|-------------------------------------------------------------------------------------------------------- + ushort majorVersion = reader.ReadUInt16(); + ushort minorVersion = reader.ReadUInt16(); + + ushort glyphClassDefOffset = reader.ReadUInt16(); + ushort attachListOffset = reader.ReadUInt16(); + ushort ligatureCaretListOffset = reader.ReadOffset16(); + ushort markAttachClassDefOffset = reader.ReadOffset16(); + ushort markGlyphSetsDefOffset = 0; + uint itemVarStoreOffset = 0; + + switch (minorVersion) + { + case 0: + break; + case 2: + markGlyphSetsDefOffset = reader.ReadUInt16(); + break; + case 3: + markGlyphSetsDefOffset = reader.ReadUInt16(); + itemVarStoreOffset = reader.ReadUInt32(); + break; + default: + throw new InvalidFontFileException($"Invalid value for 'minor version' {minorVersion} of GDEF table. Should be '0', '2' or '3'."); + } + + ClassDefinitionTable.TryLoad(reader, glyphClassDefOffset, out ClassDefinitionTable? classDefinitionTable); + AttachmentListTable? attachmentListTable = attachListOffset is 0 ? null : AttachmentListTable.Load(reader, attachListOffset); + LigatureCaretList? ligatureCaretList = ligatureCaretListOffset is 0 ? null : LigatureCaretList.Load(reader, ligatureCaretListOffset); + ClassDefinitionTable.TryLoad(reader, markAttachClassDefOffset, out ClassDefinitionTable? markAttachmentClassDef); + MarkGlyphSetsTable? markGlyphSetsTable = markGlyphSetsDefOffset is 0 ? null : MarkGlyphSetsTable.Load(reader, markGlyphSetsDefOffset); + + ItemVariationStore? itemVariationStore = null; + if (itemVarStoreOffset != 0) + { + itemVariationStore = ItemVariationStore.Load(reader, itemVarStoreOffset); + } + + return new GlyphDefinitionTable() + { + GlyphClassDefinition = classDefinitionTable, + AttachmentListTable = attachmentListTable, + LigatureCaretList = ligatureCaretList, + MarkAttachmentClassDef = markAttachmentClassDef, + MarkGlyphSetsTable = markGlyphSetsTable, + ItemVariationStore = itemVariationStore + }; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/KnownFeatureTags.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/KnownFeatureTags.cs new file mode 100644 index 0000000..2b130b2 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/KnownFeatureTags.cs @@ -0,0 +1,820 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// Provides enumeration for the different font features. + /// + /// + public enum KnownFeatureTags : uint + { + /// + /// Access All Alternates. Shortcode: aalt. + /// This feature makes all variations of a selected character accessible. This serves several purposes: An application may not support the feature by which the desired glyph would normally be accessed; + /// the user may need a glyph outside the context supported by the normal substitution, or the user may not know what feature produces the desired glyph. + /// Since many-to-one substitutions are not covered, ligatures would not appear in this table unless they were variant forms of another ligature. + /// + AccessAllAlternates = 0x61616C74U, + + /// + /// Above-base Forms. Shortcode: abvf. + /// Substitutes the above-base form of a vowel. + /// + AboveBaseForms = 0x61627666U, + + /// + /// Above-base Mark Positioning. Shortcode: abvm. + /// Positions marks above base glyphs. + /// + AboveBaseMarkPositioning = 0x6162766DU, + + /// + /// Above-base Substitutions. Shortcode: abvs. + /// Substitutes a ligature for a base glyph and mark that’s above it. + /// + AboveBaseSubstitutions = 0x61627673U, + + /// + /// Alternative Fractions. Shortccde: afrc. + /// Replaces figures separated by a slash with an alternative form. + /// + AlternativeFractions = 0x61667263U, + + /// + /// Akhand. Shortcode: akhn. + /// Preferentially substitutes a sequence of characters with a ligature. This substitution is done irrespective of any characters that may precede or follow the sequence. + /// + Akhand = 0x616B686EU, + + /// + /// Below-base Forms. Shortcode: blwf. + /// Substitutes the below-base form of a consonant in conjuncts. + /// + BelowBaseForms = 0x626C7766U, + + /// + /// Below-base Mark Positioning. Shortcode: blwm. + /// Positions marks below base glyphs. + /// + BelowBaseMarkPositioning = 0x626C776DU, + + /// + /// Below-base Substitutions. Shortcode: blws. + /// Produces ligatures that comprise of base glyph and below-base forms. + /// + BelowBaseSubstitutions = 0x626C7773U, + + /// + /// Contextual Alternates. Shortcode: calt. + /// In specified situations, replaces default glyphs with alternate forms which provide better joining behavior. + /// Used in script typefaces which are designed to have some or all of their glyphs join. + /// + ContextualAlternates = 0x63616C74U, + + /// + /// Case-Sensitive Forms. Shortcode: case. + /// Shifts various punctuation marks up to a position that works better with all-capital sequences or sets of lining figures; + /// also changes oldstyle figures to lining figures. By default, glyphs in a text face are designed to work with lowercase characters. + /// Some characters should be shifted vertically to fit the higher visual center of all-capital or lining text. + /// Also, lining figures are the same height (or close to it) as capitals, and fit much better with all-capital text. + /// + CaseSensitiveForms = 0x63617365U, + + /// + /// Glyph Composition/Decomposition. Shortcode: ccmp. + /// To minimize the number of glyph alternates, it is sometimes desirable to decompose the default glyph for a character into two or more glyphs. + /// Additionally, it may be preferable to compose default glyphs for two or more characters into a single glyph for better glyph processing. + /// This feature permits such composition/decomposition. The feature should be processed as the first feature processed, and should be processed only when it is called. + /// + GlyphCompositionDecomposition = 0x63636D70U, + + /// + /// Conjunct Form After Ro. Shortcode: cfar. + /// Substitutes alternate below-base or post-base forms in Khmer script when occurring after conjoined Ro ("Coeng Ra"). + /// + ConjunctFormAfterRo = 0x63666172U, + + /// + /// Conjunct Forms. Shortcode: cjct. + /// Produces conjunct forms of consonants in Indic scripts. This is similar to the Akhands feature, but is applied at a different sequential point in the process of shaping an Indic syllable. + /// + ConjunctForms = 0x636A6374U, + + /// + /// Contextual Ligatures. Shortcode: clig. + /// Replaces a sequence of glyphs with a single glyph which is preferred for typographic purposes. Unlike other ligature features, 'clig' specifies the context in which the ligature is recommended. + /// This capability is important in some script designs and for swash ligatures. + /// + ContextualLigatures = 0x636C6967U, + + /// + /// Centered CJK Punctuation. Shortcode: cpct. + /// Centers specific punctuation marks for those fonts that do not include centered and non-centered forms. + /// + CenteredCjkPunctuation = 0x63706374U, + + /// + /// Capital Spacing. Shortcode: cpsp. + /// Globally adjusts inter-glyph spacing for all-capital text. Most typefaces contain capitals and lowercase characters, and the capitals are positioned to work with the lowercase. + /// When capitals are used for words, they need more space between them for legibility and esthetics. + /// This feature would not apply to monospaced designs. Of course the user may want to override this behavior in order to do more pronounced letterspacing for esthetic reasons. + /// + CapitalSpacing = 0x63707370U, + + /// + /// Contextual Swash. Shortcode: cswh. + /// This feature replaces default character glyphs with corresponding swash glyphs in a specified context. Note that there may be more than one swash alternate for a given character. + /// + ContextualSwash = 0x63737768U, + + /// + /// Cursive Positioning. Shortcode: curs. + /// In cursive scripts like Arabic, this feature cursively positions adjacent glyphs. + /// + CursivePositioning = 0x63757273U, + + /// + /// Petite Capitals From Capitals. Shortcode: c2pc. + /// This feature turns capital characters into petite capitals. It is generally used for words which would otherwise be set in all caps, such as acronyms, + /// but which are desired in petite-cap form to avoid disrupting the flow of text. See the 'pcap' feature description for notes on the relationship of caps, + /// smallcaps and petite caps. + /// + PetiteCapitalsFromCapitals = 0x63327063U, + + /// + /// Small Capitals From Capitals. Shortcode: c2sc. + /// This feature turns capital characters into small capitals. It is generally used for words which would otherwise be set in all caps, + /// such as acronyms, but which are desired in small-cap form to avoid disrupting the flow of text. + /// + SmallCapitalsFromCapitals = 0x63327363U, + + /// + /// Distances. Shortcode: dist. + /// Provides a means to control distance between glyphs. + /// + Distances = 0x64697374U, + + /// + /// Discretionary Ligatures. Shortcode: dlig. + /// Replaces a sequence of glyphs with a single glyph which is preferred for typographic purposes. + /// This feature covers those ligatures which may be used for special effect, at the user’s preference. + /// + DiscretionaryLigatures = 0x646C6967U, + + /// + /// Denominators. Shortcode: dnom. + /// Replaces selected figures which follow a slash with denominator figures. + /// + Denominators = 0x646E6F6DU, + + /// + /// Dotless Forms. Shortcode: dtls. + /// This feature provides dotless forms for Math Alphanumeric characters, such as U+1D422 MATHEMATICAL BOLD SMALL I, U+1D423 MATHEMATICAL BOLD SMALL J, + /// U+1D456 U+MATHEMATICAL ITALIC SMALL I, U+1D457 MATHEMATICAL ITALIC SMALL J, and so on. The dotless forms are to be used as base forms for placing mathematical accents over them. + /// + DotlessForms = 0x64746C73U, + + /// + /// Expert Forms. Shortcode: expt. + /// Like the JIS78 Forms feature, this feature replaces standard forms in Japanese fonts with corresponding forms preferred by typographers. + /// Although most of the JIS78 substitutions are included, the expert substitution goes on to handle many more characters. + /// + ExpertForms = 0x65787074U, + + /// + /// Final Glyph on Line Alternates. Shortcode: falt. + /// Replaces line final glyphs with alternate forms specifically designed for this purpose (they would have less or more advance width as need may be), to help justification of text. + /// + FinalGlyphOnLineAlternates = 0x66616C74U, + + /// + /// Terminal Form #2. Shortcode: fin2. + /// Replaces the Alaph glyph at the end of Syriac words with its appropriate form, when the preceding base character cannot be joined to, + /// and that preceding base character is not a Dalath, Rish, or dotless Dalath-Rish. + /// + TerminalForm2 = 0x66696E32U, + + /// + /// Terminal Form #3. Shortcode: fin3. + /// Replaces Alaph glyphs at the end of Syriac words when the preceding base character is a Dalath, Rish, or dotless Dalath-Rish. + /// + TerminalForm3 = 0x66696E33U, + + /// + /// Terminal Forms. Shortcode: fina. + /// Replaces glyphs for characters that have applicable joining properties with an alternate form when occurring in a final context. + /// + TerminalForms = 0x66696E61U, + + /// + /// Flattened ascent forms. Shortcode: flac. + /// This feature provides flattened forms of accents to be used over high-rise bases such as capitals. + /// This feature should only change the shape of the accent and should not move it in the vertical or horizontal direction. + /// Moving of the accents is done by the math handling client. Accents are flattened by the Math engine if their base is higher than MATH.MathConstants.FlattenedAccentBaseHeight. + /// + FlattenedAscentForms = 0x666C6163U, + + /// + /// Fractions. Shortcode: frac. + /// Replaces figures separated by a slash with "common" (diagonal) fractions. + /// + Fractions = 0x66726163U, + + /// + /// Full Widths. Shortcode: fwid. + /// Replaces glyphs set on other widths with glyphs set on full (usually em) widths. In a CJKV font, this may include "lower ASCII" Latin characters and various symbols. + /// In a European font, this feature replaces proportionally-spaced glyphs with monospaced glyphs, which are generally set on widths of 0.6 em. + /// + FullWidths = 0x66776964U, + + /// + /// Half Forms. Shortcode: half. + /// Produces the half forms of consonants in Indic scripts. + /// + HalfForms = 0x68616C66U, + + /// + /// Halant Forms. Shortcode: haln. + /// Produces the halant forms of consonants in Indic scripts. + /// + HalantForms = 0x68616C6EU, + + /// + /// Alternate Half Widths. Shortcode: halt. + /// Respaces glyphs designed to be set on full-em widths, fitting them onto half-em widths. This differs from 'hwid' in that it does not substitute new glyphs. + /// + AlternateHalfWidths = 0x68616C74U, + + /// + /// Historical Forms. Shortcode: hist. + /// Some letterforms were in common use in the past, but appear anachronistic today. The best-known example is the long form of s; others would include the old Fraktur k. + /// Some fonts include the historical forms as alternates, so they can be used for a "period" effect. This feature replaces the default (current) forms with the historical alternates. + /// While some ligatures are also used for historical effect, this feature deals only with single characters. + /// + HistoricalForms = 0x68697374U, + + /// + /// Horizontal Kana Alternates. Shortcode: hkna. + /// Replaces standard kana with forms that have been specially designed for only horizontal writing. This is a typographic optimization for improved fit and more even color. Also see 'vkna'. + /// + HorizontalKanaAlternates = 0x686B6E61U, + + /// + /// Historical Ligatures. Shortcode: hlig. + /// Some ligatures were in common use in the past, but appear anachronistic today. Some fonts include the historical forms as alternates, so they can be used for a "period" effect. + /// This feature replaces the default (current) forms with the historical alternates. + /// + HistoricalLigatures = 0x686C6967U, + + /// + /// Hangul. Shortcode: hngl. + /// Replaces hanja (Chinese-style) Korean characters with the corresponding hangul (syllabic) characters. This effectively reverses the standard input method, + /// in which hangul are entered and replaced by hanja. Many of these substitutions are one-to-one (GSUB lookup type 1), + /// but hanja substitution often requires the user to choose from several possible hangul characters (GSUB lookup type 3). + /// + Hangul = 0x686E676CU, + + /// + /// Hojo Kanji Forms (JIS X 0212-1990 Kanji Forms). Shortcode: hojo. + /// The JIS X 0212-1990 (aka, "Hojo Kanji") and JIS X 0213:2004 character sets overlap significantly. + /// In some cases their prototypical glyphs differ. When building fonts that support both JIS X 0212-1990 and JIS X 0213:2004 (such as those supporting the Adobe-Japan 1-6 character collection), + /// it is recommended that JIS X 0213:2004 forms be preferred as the encoded form. The 'hojo' feature is used to access the JIS X 0212-1990 glyphs for the cases when the JIS X 0213:2004 form is encoded. + /// + HojoKanjiForms = 0x686F6A6FU, + + /// + /// Half Widths. Shortcode: hwid. + /// Replaces glyphs on proportional widths, or fixed widths other than half an em, with glyphs on half-em (en) widths. Many CJKV fonts have glyphs which are set on multiple widths; this feature selects the half-em version. + /// There are various contexts in which this is the preferred behavior, including compatibility with older desktop documents. + /// + HalfWidths = 0x68776964U, + + /// + /// Initial Forms. Shortcode: init. + /// Replaces glyphs for characters that have applicable joining properties with an alternate form when occurring in an initial context. + /// + InitialForms = 0x696E6974U, + + /// + /// Isolated Forms. Shortcode: isol. + /// Replaces glyphs for characters that have applicable joining properties with an alternate form when occurring in a isolate (non-joining) context. + /// + IsolatedForms = 0x69736F6CU, + + /// + /// Italics. Shortcode: ital. + /// Some fonts (such as Adobe’s Pro Japanese fonts) will have both Roman and Italic forms of some characters in a single font. + /// This feature replaces the Roman glyphs with the corresponding Italic glyphs. + /// + Italics = 0x6974616CU, + + /// + /// Justification Alternates. Shortcode: jalt. + /// Improves justification of text by replacing glyphs with alternate forms specifically designed for this purpose (they would have less or more advance width as need may be). + /// + JustificationAlternates = 0x6A616C74U, + + /// + /// JIS78 Forms. Shortcode: jp78. + /// This feature replaces default (JIS90) Japanese glyphs with the corresponding forms from the JIS C 6226-1978 (JIS78) specification. + /// + Jis78Forms = 0x6A703738U, + + /// + /// JIS83 Forms. Shortcode: jp83. + /// This feature replaces default (JIS90) Japanese glyphs with the corresponding forms from the JIS X 0208-1983 (JIS83) specification. + /// + Jis83Forms = 0x6A703833U, + + /// + /// JIS90 Forms. Shortcode: jp90. + /// This feature replaces Japanese glyphs from the JIS78 or JIS83 specifications with the corresponding forms from the JIS X 0208-1990 (JIS90) specification. + /// + Jis90Forms = 0x6A703930U, + + /// + /// JIS2004 Forms. Shortcode: jp04. + /// The National Language Council (NLC) of Japan has defined new glyph shapes for a number of JIS characters, which were incorporated into JIS X 0213:2004 as new prototypical forms. + /// The 'jp04' feature is a subset of the 'nlck' feature, and is used to access these prototypical glyphs in a manner that maintains the integrity of JIS X 0213:2004. + /// + Jis2004 = 0x6A703034U, + + /// + /// Kerning. Shortcode: kern. + /// Adjusts amount of space between glyphs, generally to provide optically consistent spacing between glyphs. + /// Although a well-designed typeface has consistent inter-glyph spacing overall, some glyph combinations require adjustment for improved legibility. + /// Besides standard adjustment in the horizontal direction, this feature can supply size-dependent kerning data via device tables, "cross-stream" kerning in the Y text direction, + /// and adjustment of glyph placement independent of the advance adjustment. Note that this feature may apply to runs of more than two glyphs, and would not be used in monospaced fonts. + /// Also note that this feature does not apply to text set vertically. + /// + Kerning = 0x6B65726EU, + + /// + /// Left Bounds. Shortcode: lfbd. + /// Aligns glyphs by their apparent left extents at the left ends of horizontal lines of text, replacing the default behavior of aligning glyphs by their origins. + /// This feature is called by the Optical Bounds ('opbd') feature. + /// + LeftBounds = 0x6C666264U, + + /// + /// Standard Ligatures. Shortcode: liga. + /// Replaces a sequence of glyphs with a single glyph which is preferred for typographic purposes. This feature covers the ligatures which the designer/manufacturer judges should be used in normal conditions. + /// + Ligatures = 0x6C696761U, + + /// + /// Leading Jamo Forms. Shortcode: ljmo. + /// Substitutes the leading jamo form of a cluster. + /// + LeadingJamoForms = 0x6C6A6D6FU, + + /// + /// Lining Figures. Shortcode: lnum. + /// This feature changes selected non-lining figures to lining figures. + /// + LiningFigures = 0x6C6E756DU, + + /// + /// Localized Forms. Shortcode: locl. + /// Many scripts used to write multiple languages over wide geographical areas have developed localized variant forms of specific letters, + /// which are used by individual literary communities. For example, a number of letters in the Bulgarian and Serbian alphabets have forms distinct from their Russian counterparts and from each other. + /// In some cases the localized form differs only subtly from the script "norm", in others the forms are radically distinct. This feature enables localized forms of glyphs to be substituted for default forms. + /// + LocalizedForms = 0x6C6F636CU, + + /// + /// Left-to-right glyph alternates. Shortcode: ltra. + /// This feature applies glyphic variants (other than mirrored forms) appropriate for left-to-right text (for mirrored forms, see 'ltrm'). + /// + LeftToRightGlyphAlternates = 0x6C747261U, + + /// + /// Left-to-right mirrored forms. Shortcode: ltrm. + /// This feature applies mirrored forms appropriate for left-to-right text. (For left-to-right glyph alternates, see 'ltra'). + /// + LeftToRightMirroredForms = 0x6C74726DU, + + /// + /// Mark Positioning. Shortcode: mark. + /// Positions mark glyphs with respect to base glyphs. + /// + MarkPositioning = 0x6D61726BU, + + /// + /// Medial Forms #2. Shortcode: med2. + /// Replaces Alaph glyphs in the middle of Syriac words when the preceding base character can be joined to. + /// + MedialForms2 = 0x6D656432U, + + /// + /// Medial Forms. Shortcode: medi + /// Replaces glyphs for characters that have applicable joining properties with an alternate form when occurring in a medial context. + /// This applies to characters that have the Unicode Joining_Type property value Dual_Joining. + /// + MedialForms = 0x6D656469U, + + /// + /// Mathematical Greek. Shortcode: mgrk. + /// Replaces standard typographic forms of Greek glyphs with corresponding forms commonly used in mathematical notation (which are a subset of the Greek alphabet). + /// + MathematicalGreek = 0x6D67726BU, + + /// + /// Mark to Mark Positioning. Shortcode: mkmk. + /// Positions marks with respect to other marks. Required in various non-Latin scripts like Arabic. + /// + MarkToMarkPositioning = 0x6D6B6D6BU, + + /// + /// Shortcode: mset. + /// Positions Arabic combining marks in fonts for Windows 95 using glyph substitution. + /// + Mset = 0x6D736574U, + + /// + /// Alternate Annotation Forms. Shortcode: nalt. + /// Replaces default glyphs with various notational forms (e.g. glyphs placed in open or solid circles, squares, parentheses, diamonds or rounded boxes). + /// In some cases an annotation form may already be present, but the user may want a different one. + /// + AlternateAnnotationForms = 0x6E616C74U, + + /// + /// NLC Kanji Forms. Shortcode: nlck. + /// The National Language Council (NLC) of Japan has defined new glyph shapes for a number of JIS characters in 2000. The 'nlck' feature is used to access those glyphs. + /// + NlcKanjiForms = 0x6E6C636BU, + + /// + /// Nukta Forms. Shortcode: nukt. + /// Produces Nukta forms in Indic scripts. + /// + NuktaForms = 0x6E756B74U, + + /// + /// Numerators. Shortcode: numr. + /// Replaces selected figures which precede a slash with numerator figures, and replaces the typographic slash with the fraction slash. + /// + Numerators = 0x6E756D72U, + + /// + /// Oldstyle Figures. Shortcode: onum. + /// This feature changes selected figures from the default or lining style to oldstyle form. + /// + OldstyleFigures = 0x6F6E756DU, + + /// + /// Optical Bounds. Shortcode: opbd. + /// Aligns glyphs by their apparent left or right extents in horizontal setting, or apparent top or bottom extents in vertical setting, + /// replacing the default behavior of aligning glyphs by their origins. Another name for this behavior would be visual justification. + /// The optical edge of a given glyph is only indirectly related to its advance width or bounding box; this feature provides a means for getting true visual alignment. + /// + OpticalBounds = 0x6F706264U, + + /// + /// Ordinals. Shortcode: ordn. + /// Replaces default alphabetic glyphs with the corresponding ordinal forms for use after figures. One exception to the follows-a-figure rule is the numero character (U+2116), + /// which is actually a ligature substitution, but is best accessed through this feature. + /// + Ordinals = 0x6F72646EU, + + /// + /// Ornaments. Shortcode: ornm. + /// This is a dual-function feature, which uses two input methods to give the user access to ornament glyphs (e.g. fleurons, dingbats and border elements) in the font. + /// One method replaces the bullet character with a selection from the full set of available ornaments; + /// the other replaces specific "lower ASCII" characters with ornaments assigned to them. The first approach supports the general or browsing user; + /// the second supports the power user. + /// + Ornaments = 0x6F726E6DU, + + /// + /// Proportional Alternate Widths. Shortcode: palt. + /// Respaces glyphs designed to be set on full-em widths, fitting them onto individual (more or less proportional) horizontal widths. + /// This differs from 'pwid' in that it does not substitute new glyphs (GPOS, not GSUB feature). The user may prefer the monospaced form, + /// or may simply want to ensure that the glyph is well-fit and not rotated in vertical setting (Latin forms designed for proportional spacing would be rotated). + /// + ProportionalAlternateWidths = 0x70616C74U, + + /// + /// Petite Capitals. Shortcode: pcap. + /// Some fonts contain an additional size of capital letters, shorter than the regular smallcaps and whimsically referred to as petite caps. + /// Such forms are most likely to be found in designs with a small lowercase x-height, where they better harmonise with lowercase text than + /// the taller smallcaps (for examples of petite caps, see the Emigre type families Mrs Eaves and Filosofia). This feature turns lowercase characters into petite capitals. + /// Forms related to petite capitals, such as specially designed figures, may be included. + /// + PetiteCapitals = 0x70636170U, + + /// + /// Proportional Kana. Shortcode: pkna. + /// Replaces glyphs, kana and kana-related, set on uniform widths (half or full-width) with proportional glyphs. + /// + ProportionalKana = 0x706B6E61U, + + /// + /// Proportional Figures. Shortcode: pnum. + /// Replaces figure glyphs set on uniform (tabular) widths with corresponding glyphs set on glyph-specific (proportional) widths. + /// Tabular widths will generally be the default, but this cannot be safely assumed. Of course this feature would not be present in monospaced designs. + /// + ProportionalFigures = 0x706E756DU, + + /// + /// Pre-base Forms. Shortcode: pref. + /// Substitutes the pre-base form of a consonant. + /// + PreBaseForms = 0x70726566U, + + /// + /// Pre-base Substitutions. Shortcode: pres. + /// Produces the pre-base forms of conjuncts in Indic scripts. It can also be used to substitute the appropriate glyph variant for pre-base vowel signs. + /// + PreBaseSubstitutions = 0x70726573U, + + /// + /// Post-base Forms. Shortcode: pstf. + /// Substitutes the post-base form of a consonant. + /// + PostBaseForms = 0x70737466U, + + /// + /// Post-base Substitutions. Shortcode: psts. + /// Substitutes a sequence of a base glyph and post-base glyph, with its ligaturised form. + /// + PostBaseSubstitutions = 0x70737473U, + + /// + /// Proportional Widths. Shortcode: pwid. + /// Replaces glyphs set on uniform widths (typically full or half-em) with proportionally spaced glyphs. + /// The proportional variants are often used for the Latin characters in CJKV fonts, but may also be used for Kana in Japanese fonts. + /// + ProportionalWidths = 0x70776964U, + + /// + /// Quarter Widths. Shortcode: qwid. + /// Replaces glyphs on other widths with glyphs set on widths of one quarter of an em (half an en). The characters involved are normally figures and some forms of punctuation. + /// + QuarterWidths = 0x71776964U, + + /// + /// Randomize. Shortcode: rand. + /// In order to emulate the irregularity and variety of handwritten text, this feature allows multiple alternate forms to be used. + /// + Randomize = 0x72616E64U, + + /// + /// Required Contextual Alternates. Shortcode: rclt. + /// In specified situations, replaces default glyphs with alternate forms which provide for better joining behavior or other glyph relationships. + /// Especially important in script typefaces which are designed to have some or all of their glyphs join, but applicable also to e.g. variants to improve spacing. + /// This feature is similar to 'calt', but with the difference that it should not be possible to turn off 'rclt' substitutions: they are considered essential to correct layout of the font. + /// + RequiredContextualAlternates = 0x72636C74U, + + /// + /// Required Ligatures. Shortcode: rlig. + /// Replaces a sequence of glyphs with a single glyph which is preferred for typographic purposes. This feature covers those ligatures, which the script determines as required to be used in normal conditions. + /// This feature is important for some scripts to insure correct glyph formation. + /// + RequiredLigatures = 0x726C6967U, + + /// + /// Rakar Forms. Shortcode: rkrf. + /// Produces conjoined forms for consonants with rakar in Devanagari and Gujarati scripts. + /// + RakarForms = 0x726B7266U, + + /// + /// Reph Form. Shortcode: rphf. + /// Substitutes the Reph form for a consonant and halant sequence. + /// + RephForm = 0x72706866U, + + /// + /// Right Bounds. Shortcode: rtbd. + /// Aligns glyphs by their apparent right extents at the right ends of horizontal lines of text, replacing the default behavior of aligning glyphs by their origins. + /// This feature is called by the Optical Bounds ('opbd') feature. + /// + RightBounds = 0x72746264U, + + /// + /// Right-to-left alternates. Shortcode: rtla. + /// This feature applies glyphic variants (other than mirrored forms) appropriate for right-to-left text. (For mirrored forms, see 'rtlm'.) + /// + RightToLeftAlternates = 0x72746C61U, + + /// + /// Right-to-left mirrored forms. Shortcode: rtlm. + /// This feature applies mirrored forms appropriate for right-to-left text other than for those characters that would be covered by the character-level mirroring step performed by an OpenType layout engine. + /// (For right-to-left glyph alternates, see 'rtla'.) + /// + RightToLeftMirroredForms = 0x72746C6DU, + + /// + /// Ruby Notation Forms. Shortcode: ruby. + /// Japanese typesetting often uses smaller kana glyphs, generally in superscripted form, to clarify the meaning of kanji which may be unfamiliar to the reader. + /// These are called "ruby", from the old typesetting term for four-point-sized type. This feature identifies glyphs in the font which have been designed for this use, + /// substituting them for the default designs. + /// + RubyNotationForms = 0x72756279U, + + /// + /// Required Variation Alternates. Shortcode: rvrn. + /// his feature is used in fonts that support OpenType Font Variations in order to select alternate glyphs for particular variation instances. + /// + RequiredVariationAlternates = 0x7276726EU, + + /// + /// Stylistic Alternates. Shortcode: salt. + /// Many fonts contain alternate glyph designs for a purely esthetic effect; these don’t always fit into a clear category like swash or historical. + /// As in the case of swash glyphs, there may be more than one alternate form. This feature replaces the default forms with the stylistic alternates. + /// + StylisticAlternates = 0x73616C74U, + + /// + /// Scientific Inferiors. Shortcode: sinf. + /// Replaces lining or oldstyle figures with inferior figures (smaller glyphs which sit lower than the standard baseline, primarily for chemical or mathematical notation). + /// May also replace lowercase characters with alphabetic inferiors. + /// + ScientificInferiors = 0x73696E66U, + + /// + /// Optical size. Shortcode: size. + /// This feature stores two kinds of information about the optical size of the font: design size + /// (the point size for which the font is optimized) and size range (the range of point sizes which the font can serve well), + /// as well as other information which helps applications use the size range. The design size is useful for determining proper tracking behavior. + /// The size range is useful in families which have fonts covering several ranges. Additional values serve to identify the set of fonts which share related size ranges, + /// and to identify their shared name. Note that sizes refer to nominal final output size, and are independent of viewing magnification or resolution. + /// + OpticalSize = 0x73697A65U, + + /// + /// Small Capitals. Shortcode: smcp. + /// This feature turns lowercase characters into small capitals. This corresponds to the common SC font layout. It is generally used for display lines set in Large and small caps, such as titles. + /// Forms related to small capitals, such as oldstyle figures, may be included. + /// + SmallCapitals = 0x736D6370U, + + /// + /// Simplified Forms. Shortcode: smpl. + /// Replaces "traditional" Chinese or Japanese forms with the corresponding "simplified" forms. + /// + SimplifiedForms = 0x736D706CU, + + /// + /// Math script style alternates. Shortcode: ssty. + /// This feature provides glyph variants adjusted to be more suitable for use in subscripts and superscripts. + /// + MathScriptStyleAlternates = 0x73737479U, + + /// + /// Stretching Glyph Decomposition. Shortcode: stch. + /// Unicode characters, such as the Syriac Abbreviation Mark (U+070F), that enclose other characters need to be able + /// to stretch in order to dynamically adapt to the width of the enclosed text. This feature defines a decomposition set + /// consisting of an odd number of glyphs which describe the stretching glyph. The odd numbered glyphs in the decomposition are + /// fixed reference points which are distributed evenly from the start to the end of the enclosed text. The even numbered glyphs may + /// be repeated as necessary to fill the space between the fixed glyphs. The first and last glyphs may either be simple glyphs with width at the baseline, + /// or mark glyphs. All other decomposition glyphs should have width, but must be defined as mark glyphs. + /// + StretchingGlyphDecomposition = 0x73746368U, + + /// + /// Subscript. Shortcode: subs. + /// The 'subs' feature may replace a default glyph with a subscript glyph, or it may combine a glyph substitution with positioning adjustments for proper placement. + /// + Subscript = 0x73756273U, + + /// + /// Superscript. Shortcode: sups. + /// Replaces lining or oldstyle figures with superior figures (primarily for footnote indication), and replaces lowercase letters with superior letters (primarily for abbreviated French titles). + /// + Superscript = 0x73757073U, + + /// + /// Swash. Shortcode: swsh. + /// This feature replaces default character glyphs with corresponding swash glyphs. Note that there may be more than one swash alternate for a given character. + /// + Swash = 0x73777368U, + + /// + /// Titling. Shortcode: titl. + /// This feature replaces the default glyphs with corresponding forms designed specifically for titling. + /// These may be all-capital and/or larger on the body, and adjusted for viewing at larger sizes. + /// + Titling = 0x7469746CU, + + /// + /// Trailing Jamo Forms. Shortcode: tjmo. + /// Substitutes the trailing jamo form of a cluster. + /// + TrailingJamoForms = 0x746A6D6FU, + + /// + /// Traditional Name Forms. Shortcode: tnam. + /// Replaces "simplified" Japanese kanji forms with the corresponding "traditional" forms. This is equivalent to the Traditional Forms feature, + /// but explicitly limited to the traditional forms considered proper for use in personal names (as many as 205 glyphs in some fonts). + /// + TraditionalNameForms = 0x746E616DU, + + /// + /// Tabular Figures. Shortcode: tnum. + /// Replaces figure glyphs set on proportional widths with corresponding glyphs set on uniform (tabular) widths. + /// Tabular widths will generally be the default, but this cannot be safely assumed. Of course this feature would not be present in monospaced designs. + /// + TabularFigures = 0x746E756DU, + + /// + /// Traditional Forms. Shortcode: trad. + /// Replaces 'simplified' Chinese hanzi or Japanese kanji forms with the corresponding 'traditional' forms. + /// + TraditionalForms = 0x74726164U, + + /// + /// Third Widths. Shortcode: twid. + /// Replaces glyphs on other widths with glyphs set on widths of one third of an em. The characters involved are normally figures and some forms of punctuation. + /// + ThirdWidths = 0x74776964U, + + /// + /// Unicase. Shortcode: unic. + /// This feature maps upper- and lowercase letters to a mixed set of lowercase and small capital forms, resulting in a single case alphabet + /// (for an example of unicase, see the Emigre type family Filosofia). The letters substituted may vary from font to font, as appropriate to the design. + /// If aligning to the x-height, smallcap glyphs may be substituted, or specially designed unicase forms might be used. Substitutions might also include specially designed figures. + /// + Unicase = 0x756E6963U, + + /// + /// Alternate Vertical Metrics. Shortcode: valt. + /// Repositions glyphs to visually center them within full-height metrics, for use in vertical setting. Typically applies to full-width Latin glyphs, + /// which are aligned on a common horizontal baseline and not rotated when set vertically in CJKV fonts. + /// + AlternateVerticalMetrics = 0x76616C74U, + + /// + /// Vattu Variants. Shortcode: vatu. + /// In an Indic consonant conjunct, substitutes a ligature glyph for a base consonant and a following vattu (below-base) form of a conjoining consonant, or for a half form of a consonant and a following vattu form. + /// + VattuVariants = 0x76617475U, + + /// + /// Vertical Alternates. Shortcode: vert. + /// Transforms default glyphs into glyphs that are appropriate for upright presentation in vertical writing mode.While the glyphs for most + /// characters in East Asian writing systems remain upright when set in vertical writing mode, some must be transformed — + /// usually by rotation, shifting, or different component ordering — for vertical writing mode. + /// + VerticalAlternates = 0x76657274U, + + /// + /// Alternate Vertical Half Metrics. Shortcode: vhal. + /// Respaces glyphs designed to be set on full-em heights, fitting them onto half-em heights. + /// + AlternateVerticalHalfMetrics = 0x7668616CU, + + /// + /// Vowel Jamo Forms. Shortcode: vjmo. + /// Substitutes the vowel jamo form of a cluster. + /// + VowelJamoForms = 0x766A6D6FU, + + /// + /// Vertical Kana Alternates. Shortcode: vkna. + /// Replaces standard kana with forms that have been specially designed for only vertical writing. This is a typographic optimization for improved fit and more even color. Also see 'hkna'. + /// + VerticalKanaAlternates = 0x766B6E61U, + + /// + /// Vertical Kerning. Shortcode: vkrn + /// Adjusts amount of space between glyphs, generally to provide optically consistent spacing between glyphs. + /// Although a well-designed typeface has consistent inter-glyph spacing overall, some glyph combinations require adjustment for improved legibility. + /// Besides standard adjustment in the vertical direction, this feature can supply size-dependent kerning data via device tables, + /// "cross-stream" kerning in the X text direction, and adjustment of glyph placement independent of the advance adjustment. + /// Note that this feature may apply to runs of more than two glyphs, and would not be used in monospaced fonts. Also note that this feature applies only to text set vertically. + /// + VerticalKerning = 0x766B726EU, + + /// + /// Proportional Alternate Vertical Metrics. Shortcode: vpal. + /// Respaces glyphs designed to be set on full-em heights, fitting them onto individual (more or less proportional) vertical heights. This differs from 'valt' in that it does not substitute new glyphs (GPOS, not GSUB feature). + /// The user may prefer the monospaced form, or may simply want to ensure that the glyph is well-fit. + /// + ProportionalAlternateVerticalMetrics = 0x7670616CU, + + /// + /// Vertical Alternates and Rotation. Shortcode: vrt2. + /// Replaces some fixed-width (half-, third- or quarter-width) or proportional-width glyphs (mostly Latin or katakana) with forms suitable for vertical writing (that is, rotated 90 degrees clockwise). + /// Note that these are a superset of the glyphs covered in the 'vert' table. + /// + VerticalAlternatesAndRotation = 0x76727432U, + + /// + /// Vertical Alternates for Rotation. Shortcode: vrtr. + /// Transforms default glyphs into glyphs that are appropriate for sideways presentation in vertical writing mode. + /// While the glyphs for most characters in East Asian writing systems remain upright when set in vertical writing mode, glyphs for other characters — + /// such as those of other scripts or for particular Western-style punctuation — are expected to be presented sideways in vertical writing. + /// + VerticalAlternatesForRotation = 0x76727472U, + + /// + /// Slashed Zero. Shortcode: zero. + /// Some fonts contain both a default form of zero, and an alternative form which uses a diagonal slash through the counter. Especially in condensed designs, it can be difficult to distinguish between 0 and O (zero and capital O) in any situation where capitals and lining figures may be arbitrarily mixed. + /// This feature allows the user to change from the default 0 to a slashed form. + /// + SlashedZero = 0x7A65726FU, + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/LigatureCaretList.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/LigatureCaretList.cs new file mode 100644 index 0000000..4cb7e0e --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/LigatureCaretList.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// The Ligature Caret List table (LigCaretList) provides caret positioning data for ligature glyphs, + /// enabling text processing clients to correctly position carets within ligatures for selection and cursor movement. + /// + /// + internal sealed class LigatureCaretList + { + /// + /// Gets or sets the array of ligature glyph tables, one per covered glyph, in Coverage Index order. + /// + public LigatureGlyph[]? LigatureGlyphs { get; internal set; } + + /// + /// Gets or sets the coverage table that defines which glyphs have ligature caret data. + /// + public CoverageTable? CoverageTable { get; internal set; } + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Offset from the beginning of the GDEF table to the LigCaretList table. + /// The . + public static LigatureCaretList Load(BigEndianBinaryReader reader, long offset) + { + // Ligature Caret list + // Type | Name | Description + // ----------|--------------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | coverageOffset | Offset to Coverage table - from beginning of LigCaretList table. + // ----------|--------------------------------|-------------------------------------------------------------------------------------------------------- + // uint16 | ligGlyphCount | Number of ligature glyphs. + // ----------|--------------------------------|-------------------------------------------------------------------------------------------------------- + // Offset16 | ligGlyphOffsets[ligGlyphCount] | Array of offsets to LigGlyph tables, from beginning of LigCaretList table —in Coverage Index order. + // ----------|--------------------------------|-------------------------------------------------------------------------------------------------------- + reader.Seek(offset, SeekOrigin.Begin); + + ushort coverageOffset = reader.ReadOffset16(); + ushort ligGlyphCount = reader.ReadUInt16(); + + using Buffer ligGlyphOffsetsBuffer = new(ligGlyphCount); + Span ligGlyphOffsets = ligGlyphOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(ligGlyphOffsets); + + LigatureCaretList ligatureCaretList = new() + { + CoverageTable = CoverageTable.Load(reader, offset + coverageOffset), + LigatureGlyphs = new LigatureGlyph[ligGlyphCount] + }; + + for (int i = 0; i < ligatureCaretList.LigatureGlyphs.Length; i++) + { + ligatureCaretList.LigatureGlyphs[i] = LigatureGlyph.Load(reader, offset + ligGlyphOffsets[i]); + } + + return ligatureCaretList; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/LigatureGlyph.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/LigatureGlyph.cs new file mode 100644 index 0000000..18b46b6 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/LigatureGlyph.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// A LigatureGlyph table contains the number of carets and the offsets to their caret value tables + /// for a single ligature glyph. + /// + /// + internal sealed class LigatureGlyph + { + /// + /// Gets or sets the array of offsets to caret value tables for this ligature glyph. + /// + public ushort[]? CaretValueOffsets { get; internal set; } + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Offset from the beginning of the LigGlyph table. + /// The . + public static LigatureGlyph Load(BigEndianBinaryReader reader, long offset) + { + reader.Seek(offset, SeekOrigin.Begin); + + ushort caretCount = reader.ReadUInt16(); + return new LigatureGlyph() + { + CaretValueOffsets = reader.ReadUInt16Array(caretCount) + }; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/LookupFlags.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/LookupFlags.cs new file mode 100644 index 0000000..f65f3fa --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/LookupFlags.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// LookupFlag bit enumeration, see: https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#lookup-table + /// + [Flags] + internal enum LookupFlags : ushort + { + /// + /// This bit relates only to the correct processing of the cursive attachment lookup type (GPOS lookup type 3). + /// When this bit is set, the last glyph in a given sequence to which the cursive attachment lookup is applied, will be positioned on the baseline. + /// + RightToLeft = 0x0001, + + /// + /// If set, skips over base glyphs. + /// + IgnoreBaseGlyphs = 0x0002, + + /// + /// If set, skips over ligatures. + /// + IgnoreLigatures = 0x0004, + + /// + /// If set, skips over all combining marks. + /// + IgnoreMarks = 0x0008, + + /// + /// If set, indicates that the lookup table structure is followed by a MarkFilteringSet field. + /// The layout engine skips over all mark glyphs not in the mark filtering set indicated. + /// + UseMarkFilteringSet = 0x0010, + + /// + /// For future use (Set to zero). + /// + Reserved = 0x00E0, + + /// + /// If not zero, skips over all marks of attachment type different from specified. + /// + MarkAttachmentTypeMask = 0xFF00 + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/MarkGlyphSetsTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/MarkGlyphSetsTable.cs new file mode 100644 index 0000000..c5b7179 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/MarkGlyphSetsTable.cs @@ -0,0 +1,84 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// The MarkGlyphSets table allows the definition of sets of mark glyphs that can be used + /// in lookup flag mark filtering. This provides more flexibility than the MarkAttachmentType. + /// + /// + internal sealed class MarkGlyphSetsTable + { + /// + /// Gets or sets the format identifier. + /// + public ushort Format { get; internal set; } + + /// + /// Gets or sets the array of offsets to Coverage tables, from the beginning of the MarkGlyphSets table. + /// + public uint[]? CoverageOffset { get; internal set; } + + /// + /// Gets the loaded Coverage tables for each mark glyph set. + /// + public CoverageTable[]? Coverages { get; private set; } + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Offset from the beginning of the GDEF table to the MarkGlyphSets table. + /// The . + public static MarkGlyphSetsTable Load(BigEndianBinaryReader reader, long offset) + { + reader.Seek(offset, SeekOrigin.Begin); + + MarkGlyphSetsTable markGlyphSetsTable = new() + { + Format = reader.ReadUInt16() + }; + + ushort markSetCount = reader.ReadUInt16(); + uint[] coverageOffsets = reader.ReadUInt32Array(markSetCount); + markGlyphSetsTable.CoverageOffset = coverageOffsets; + + // Load the referenced Coverage tables now so we can use them during shaping. + // Coverage offsets are relative to the start of the MarkGlyphSets table. + CoverageTable[] coverages = new CoverageTable[markSetCount]; + for (int i = 0; i < markSetCount; i++) + { + long covOffset = offset + coverageOffsets[i]; + coverages[i] = CoverageTable.Load(reader, covOffset); + } + + markGlyphSetsTable.Coverages = coverages; + return markGlyphSetsTable; + } + + /// + /// Determines whether the specified glyph is contained in the given mark glyph set. + /// + /// The index of the mark glyph set. + /// The glyph identifier to look up. + /// if the glyph is in the set; otherwise, . + public bool Contains(ushort markGlyphSetIndex, ushort glyphId) + { + CoverageTable[]? coverages = this.Coverages; + if (coverages is null) + { + return false; + } + + int i = markGlyphSetIndex; + if ((uint)i >= (uint)coverages.Length) + { + return false; + } + + return coverages[i].CoverageIndexOf(glyphId) >= 0; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/MarkZeroingMode.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/MarkZeroingMode.cs new file mode 100644 index 0000000..c5c05c9 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/MarkZeroingMode.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// Provides enumeration determining when to zero mark advances. + /// + internal enum MarkZeroingMode + { + /// + /// Zero mark advances before GPOS processing. + /// + PreGPos, + + /// + /// Zero mark advances after GPOS processing. + /// + PostGpos, + + /// + /// Do not zero mark advances. + /// + None + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/ScriptList.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/ScriptList.cs new file mode 100644 index 0000000..63f5228 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/ScriptList.cs @@ -0,0 +1,265 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// OpenType Layout fonts may contain one or more groups of glyphs used to render various scripts, + /// which are enumerated in a ScriptList table. Both the GSUB and GPOS tables define + /// Script List tables (ScriptList): + /// + /// + internal sealed class ScriptList : Dictionary + { + private readonly Tag scriptTag; + + /// + /// Initializes a new instance of the class. + /// + /// The tag of the first (default) script in the list. + private ScriptList(Tag scriptTag) => this.scriptTag = scriptTag; + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Offset from the beginning of the GPOS or GSUB table to the ScriptList table. + /// The , or if the script count is zero. + public static ScriptList? Load(BigEndianBinaryReader reader, long offset) + { + // ScriptListTable + // +--------------+----------------------------+-------------------------------------------------------------+ + // | Type | Name | Description | + // +==============+============================+=============================================================+ + // | uint16 | scriptCount | Number of ScriptRecords | + // +--------------+----------------------------+-------------------------------------------------------------+ + // | ScriptRecord | scriptRecords[scriptCount] | Array of ScriptRecords, listed alphabetically by script tag | + // +--------------+----------------------------+-------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + + ushort scriptCount = reader.ReadUInt16(); + + // Read records (tags and table offsets) + var scriptTags = new Tag[scriptCount]; + ushort[] scriptOffsets = new ushort[scriptCount]; + + for (int i = 0; i < scriptTags.Length; i++) + { + scriptTags[i] = reader.ReadUInt32(); + scriptOffsets[i] = reader.ReadUInt16(); + } + + // Read each table and add it to the dictionary + ScriptList? scriptList = null; + for (int i = 0; i < scriptCount; ++i) + { + Tag scriptTag = scriptTags[i]; + if (i == 0) + { + scriptList = new ScriptList(scriptTag); + } + + var scriptTable = ScriptListTable.Load(scriptTag, reader, offset + scriptOffsets[i]); + scriptList!.Add(scriptTag, scriptTable); + } + + return scriptList; + } + + /// + /// Gets the default script table (the first script in the list). + /// Dictionaries are unordered, so this uses the stored first script tag. + /// + /// The default . + public ScriptListTable Default() => this[this.scriptTag]; + } + + /// + /// A Script table identifies the language systems supported by a script and contains a default + /// language system table and an array of language system tables. + /// + /// + internal sealed class ScriptListTable + { + /// + /// Initializes a new instance of the class. + /// + /// The array of language system tables. + /// The default language system table, or if none. + /// The 4-byte script identification tag. + private ScriptListTable(LangSysTable[] langSysTables, LangSysTable? defaultLang, Tag scriptTag) + { + this.LangSysTables = langSysTables; + this.DefaultLangSysTable = defaultLang; + this.ScriptTag = scriptTag; + } + + /// + /// Gets the 4-byte script identification tag. + /// + public Tag ScriptTag { get; } + + /// + /// Gets the default language system table, or if none is defined. + /// + public LangSysTable? DefaultLangSysTable { get; } + + /// + /// Gets the array of language system tables for this script. + /// + public LangSysTable[] LangSysTables { get; } + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The 4-byte script identification tag. + /// The big endian binary reader. + /// Offset from the beginning of the Script table. + /// The . + public static ScriptListTable Load(Tag scriptTag, BigEndianBinaryReader reader, long offset) + { + // ScriptListTable + // +---------------+------------------------------+-------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +===============+==============================+===============================================================================+ + // | Offset16 | defaultLangSysOffset | Offset to default LangSys table, from beginning of Script table — may be NULL | + // +---------------+------------------------------+-------------------------------------------------------------------------------+ + // | uint16 | langSysCount | Number of LangSysRecords for this script — excluding the default LangSys | + // +---------------+------------------------------+-------------------------------------------------------------------------------+ + // | LangSysRecord | langSysRecords[langSysCount] | Array of LangSysRecords, listed alphabetically by LangSys tag | + // +---------------+------------------------------+-------------------------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + + ushort defaultLangSysOffset = reader.ReadOffset16(); + ushort langSysCount = reader.ReadUInt16(); + + var langSysRecords = new LangSysRecord[langSysCount]; + for (int i = 0; i < langSysRecords.Length; i++) + { + // LangSysRecord + // +----------+---------------+---------------------------------------------------------+ + // | Type | Name | Description | + // +==========+===============+=========================================================+ + // | Tag | langSysTag | 4-byte LangSysTag identifier | + // +----------+---------------+---------------------------------------------------------+ + // | Offset16 | langSysOffset | Offset to LangSys table, from beginning of Script table | + // +----------+---------------+---------------------------------------------------------+ + uint langSysTag = reader.ReadUInt32(); + ushort langSysOffset = reader.ReadOffset16(); + langSysRecords[i] = new LangSysRecord(langSysTag, langSysOffset); + } + + // Load the default table. + LangSysTable? defaultLangSysTable = null; + if (defaultLangSysOffset > 0) + { + defaultLangSysTable = LangSysTable.Load(0, reader, offset + defaultLangSysOffset); + } + + // Load the other table features. + // We do this last to avoid excessive seeking. + var langSysTables = new LangSysTable[langSysCount]; + for (int i = 0; i < langSysTables.Length; i++) + { + LangSysRecord langSysRecord = langSysRecords[i]; + langSysTables[i] = LangSysTable.Load(langSysRecord.LangSysTag, reader, offset + langSysRecord.LangSysOffset); + } + + return new ScriptListTable(langSysTables, defaultLangSysTable, scriptTag); + } + + /// + /// A LangSysRecord contains a language system tag and its offset to the LangSys table. + /// + private readonly struct LangSysRecord + { + /// + /// Initializes a new instance of the struct. + /// + /// The 4-byte language system tag identifier. + /// The offset to the LangSys table from the beginning of the Script table. + public LangSysRecord(uint langSysTag, ushort langSysOffset) + { + this.LangSysTag = langSysTag; + this.LangSysOffset = langSysOffset; + } + + /// + /// Gets the 4-byte language system tag identifier. + /// + public uint LangSysTag { get; } + + /// + /// Gets the offset to the LangSys table from the beginning of the Script table. + /// + public ushort LangSysOffset { get; } + } + } + + /// + /// The Language System table (LangSys) identifies language-system features for a script. + /// + /// + internal sealed class LangSysTable + { + /// + /// Initializes a new instance of the class. + /// + /// The 4-byte language system tag identifier. + /// The index of a required feature; 0xFFFF if none. + /// The array of indices into the FeatureList. + private LangSysTable(uint langSysTag, ushort requiredFeatureIndex, ushort[] featureIndices) + { + this.LangSysTag = langSysTag; + this.RequiredFeatureIndex = requiredFeatureIndex; + this.FeatureIndices = featureIndices; + } + + /// + /// Gets the 4-byte language system tag identifier. + /// + public uint LangSysTag { get; } + + /// + /// Gets the index of a feature required for this language system; 0xFFFF if no required features. + /// + public ushort RequiredFeatureIndex { get; } + + /// + /// Gets the array of indices into the FeatureList, in arbitrary order. + /// + public ushort[] FeatureIndices { get; } = Array.Empty(); + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The 4-byte language system tag identifier. + /// The big endian binary reader. + /// Offset from the beginning of the LangSys table. + /// The . + public static LangSysTable Load(uint langSysTag, BigEndianBinaryReader reader, long offset) + { + // +----------+-----------------------------------+-----------------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+===================================+=========================================================================================+ + // | Offset16 | lookupOrderOffset | = NULL(reserved for an offset to a reordering table) | + // +----------+-----------------------------------+-----------------------------------------------------------------------------------------+ + // | uint16 | requiredFeatureIndex | Index of a feature required for this language system; if no required features = 0xFFFF | + // +----------+-----------------------------------+-----------------------------------------------------------------------------------------+ + // | uint16 | featureIndexCount | Number of feature index values for this language system — excludes the required feature | + // +----------+-----------------------------------+-----------------------------------------------------------------------------------------+ + // | uint16 | featureIndices[featureIndexCount] | Array of indices into the FeatureList, in arbitrary order | + // +----------+-----------------------------------+-----------------------------------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + ushort lookupOrderOffset = reader.ReadOffset16(); + ushort requiredFeatureIndex = reader.ReadUInt16(); + ushort featureIndexCount = reader.ReadUInt16(); + + ushort[] featureIndices = reader.ReadUInt16Array(featureIndexCount); + return new LangSysTable(langSysTag, requiredFeatureIndex, featureIndices); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/SequenceLookupRecord.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/SequenceLookupRecord.cs new file mode 100644 index 0000000..7a4f044 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/SequenceLookupRecord.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// For all formats for both contextual and chained contextual lookups, a common record format + /// is used to specify an action—a nested lookup—to be applied to a glyph at a particular + /// sequence position within the input sequence. + /// + /// + [DebuggerDisplay("SequenceIndex: {SequenceIndex}, LookupListIndex: {LookupListIndex}")] + internal readonly struct SequenceLookupRecord + { + /// + /// Initializes a new instance of the struct. + /// + /// The index into the current glyph sequence (first glyph = 0). + /// The lookup to apply at that position (zero-based). + public SequenceLookupRecord(ushort sequenceIndex, ushort lookupListIndex) + { + this.SequenceIndex = sequenceIndex; + this.LookupListIndex = lookupListIndex; + } + + /// + /// Gets the index into the current glyph sequence (first glyph = 0). + /// + public ushort SequenceIndex { get; } + + /// + /// Gets the lookup to apply at the specified sequence position (zero-based index into the LookupList). + /// + public ushort LookupListIndex { get; } + + /// + /// Loads an array of values from the binary reader. + /// + /// The big endian binary reader. + /// The number of records to read. + /// The array of . + public static SequenceLookupRecord[] LoadArray(BigEndianBinaryReader reader, int count) + { + // +--------+-----------------+---------------------------------------------------+ + // | Type | Name | Description | + // +========+=================+===================================================+ + // | uint16 | SequenceIndex | Index into current glyph sequence-first glyph = 0 | + // +--------+-----------------+---------------------------------------------------+ + // | uint16 | LookupListIndex | Lookup to apply to that position-zero-based. | + // +--------+-----------------+---------------------------------------------------+ + var records = new SequenceLookupRecord[count]; + for (int i = 0; i < records.Length; i++) + { + records[i] = new SequenceLookupRecord(reader.ReadUInt16(), reader.ReadUInt16()); + } + + return records; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/SequenceRuleSetTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/SequenceRuleSetTable.cs new file mode 100644 index 0000000..3efa3de --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/SequenceRuleSetTable.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// A SequenceRuleSet table contains an array of SequenceRule tables that define context rules + /// for simple (glyph ID based) glyph contexts in Sequence Context Format 1. + /// + /// + internal sealed class SequenceRuleSetTable + { + /// + /// Initializes a new instance of the class. + /// + /// The array of sequence rule tables. + private SequenceRuleSetTable(SequenceRuleTable[] sequenceRuleTables) + => this.SequenceRuleTables = sequenceRuleTables; + + /// + /// Gets the array of sequence rule tables. + /// + public SequenceRuleTable[] SequenceRuleTables { get; } + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Offset from the beginning of the SequenceRuleSet table. + /// The . + public static SequenceRuleSetTable Load(BigEndianBinaryReader reader, long offset) + { + // SequenceRuleSet + // +----------+------------------------------+----------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+==============================+================================================================+ + // | uint16 | seqRuleCount | Number of SequenceRule tables | + // +----------+------------------------------+----------------------------------------------------------------+ + // | Offset16 | seqRuleOffsets[posRuleCount] | Array of offsets to SequenceRule tables, from beginning of the | + // | | | SequenceRuleSet table | + // +----------+------------------------------+----------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + ushort seqRuleCount = reader.ReadUInt16(); + + using Buffer seqRuleOffsetsBuffer = new(seqRuleCount); + Span seqRuleOffsets = seqRuleOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(seqRuleOffsets); + + var sequenceRuleTables = new SequenceRuleTable[seqRuleCount]; + for (int i = 0; i < sequenceRuleTables.Length; i++) + { + sequenceRuleTables[i] = SequenceRuleTable.Load(reader, offset + seqRuleOffsets[i]); + } + + return new SequenceRuleSetTable(sequenceRuleTables); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/SequenceRuleTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/SequenceRuleTable.cs new file mode 100644 index 0000000..7e1f2f1 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/SequenceRuleTable.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// A SequenceRule table describes a context rule using glyph IDs in Sequence Context Format 1. + /// + /// + internal sealed class SequenceRuleTable + { + /// + /// Initializes a new instance of the class. + /// + /// The array of input glyph IDs, starting with the second glyph. + /// The array of sequence lookup records. + private SequenceRuleTable(ushort[] inputSequence, SequenceLookupRecord[] seqLookupRecords) + { + this.InputSequence = inputSequence; + this.SequenceLookupRecords = seqLookupRecords; + } + + /// + /// Gets the array of input glyph IDs, starting with the second glyph. + /// + public ushort[] InputSequence { get; } + + /// + /// Gets the array of sequence lookup records specifying actions to be applied. + /// + public SequenceLookupRecord[] SequenceLookupRecords { get; } + + /// + /// Loads the from the binary reader at the specified offset. + /// + /// The big endian binary reader. + /// Offset from the beginning of the SequenceRule table. + /// The . + public static SequenceRuleTable Load(BigEndianBinaryReader reader, long offset) + { + // +----------------------+----------------------------------+---------------------------------------------------------+ + // | Type | Name | Description | + // +======================+==================================+=========================================================+ + // | uint16 | glyphCount | Number of glyphs in the input glyph sequence | + // +----------------------+----------------------------------+---------------------------------------------------------+ + // | uint16 | seqLookupCount | Number of SequenceLookupRecords | + // +----------------------+----------------------------------+---------------------------------------------------------+ + // | uint16 | inputSequence[glyphCount - 1] | Array of input glyph IDs—starting with the second glyph | + // +----------------------+----------------------------------+---------------------------------------------------------+ + // | SequenceLookupRecord | seqLookupRecords[seqLookupCount] | Array of Sequence lookup records | + // +----------------------+----------------------------------+---------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + ushort glyphCount = reader.ReadUInt16(); + ushort seqLookupCount = reader.ReadUInt16(); + ushort[] inputSequence = reader.ReadUInt16Array(glyphCount - 1); + SequenceLookupRecord[] seqLookupRecords = SequenceLookupRecord.LoadArray(reader, seqLookupCount); + + return new SequenceRuleTable(inputSequence, seqLookupRecords); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs new file mode 100644 index 0000000..286778e --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs @@ -0,0 +1,212 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers { + /// + /// This is a shaper for Arabic, and other cursive scripts. + /// The shaping state machine was ported from fontkit. + /// + /// + internal sealed class ArabicShaper : DefaultShaper + { + /// The 'mset' (mark positioning via substitution) feature tag. + private static readonly Tag MsetTag = Tag.Parse("mset"); + + /// The 'fina' (terminal forms) feature tag. + private static readonly Tag FinaTag = Tag.Parse("fina"); + + /// The 'fin2' (terminal forms #2) feature tag. + private static readonly Tag Fin2Tag = Tag.Parse("fin2"); + + /// The 'fin3' (terminal forms #3) feature tag. + private static readonly Tag Fin3Tag = Tag.Parse("fin3"); + + /// The 'isol' (isolated forms) feature tag. + private static readonly Tag IsolTag = Tag.Parse("isol"); + + /// The 'init' (initial forms) feature tag. + private static readonly Tag InitTag = Tag.Parse("init"); + + /// The 'medi' (medial forms) feature tag. + private static readonly Tag MediTag = Tag.Parse("medi"); + + /// The 'med2' (medial forms #2) feature tag. + private static readonly Tag Med2Tag = Tag.Parse("med2"); + + /// No joining action. + private const byte None = 0; + + /// Isolated form action. + private const byte Isol = 1; + + /// Final form action. + private const byte Fina = 2; + + /// Final form #2 action (for ALAPH). + private const byte Fin2 = 3; + + /// Final form #3 action (for ALAPH after DALATH RISH). + private const byte Fin3 = 4; + + /// Medial form action. + private const byte Medi = 5; + + /// Medial form #2 action (for ALAPH). + private const byte Med2 = 6; + + /// Initial form action. + private const byte Init = 7; + + /// + /// Arabic joining state machine table. Each entry is [prevAction, curAction, nextState]. + /// Rows are states (0-6), columns are joining type categories. + /// + private static readonly byte[,][] StateTable = + { + // # NonJoining, LeftJoining, RightJoining, DualJoining, ALAPH, DALATH RISH + // State 0: prev was U, not willing to join. + { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { None, Isol, 1 }, new byte[] { None, Isol, 2 }, new byte[] { None, Isol, 1 }, new byte[] { None, Isol, 6 } }, + + // State 1: prev was R or ISOL/ALAPH, not willing to join. + { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { None, Isol, 1 }, new byte[] { None, Isol, 2 }, new byte[] { None, Fin2, 5 }, new byte[] { None, Isol, 6 } }, + + // State 2: prev was D/L in ISOL form, willing to join. + { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { Init, Fina, 1 }, new byte[] { Init, Fina, 3 }, new byte[] { Init, Fina, 4 }, new byte[] { Init, Fina, 6 } }, + + // State 3: prev was D in FINA form, willing to join. + { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { Medi, Fina, 1 }, new byte[] { Medi, Fina, 3 }, new byte[] { Medi, Fina, 4 }, new byte[] { Medi, Fina, 6 } }, + + // State 4: prev was FINA ALAPH, not willing to join. + { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { Med2, Isol, 1 }, new byte[] { Med2, Isol, 2 }, new byte[] { Med2, Fin2, 5 }, new byte[] { Med2, Isol, 6 } }, + + // State 5: prev was FIN2/FIN3 ALAPH, not willing to join. + { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { Isol, Isol, 1 }, new byte[] { Isol, Isol, 2 }, new byte[] { Isol, Fin2, 5 }, new byte[] { Isol, Isol, 6 } }, + + // State 6: prev was DALATH/RISH, not willing to join. + { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { None, Isol, 1 }, new byte[] { None, Isol, 2 }, new byte[] { None, Fin3, 5 }, new byte[] { None, Isol, 6 } }, + }; + + /// + /// Initializes a new instance of the class. + /// + /// The script classification. + /// The text options. + public ArabicShaper(ScriptClass script, TextOptions textOptions) + : base(script, MarkZeroingMode.PostGpos, textOptions) + { + } + + /// + protected override void PlanFeatures(IGlyphShapingCollection collection, int index, int count) + { + this.AddFeature(collection, index, count, CcmpTag); + this.AddFeature(collection, index, count, LoclTag); + + this.AddFeature(collection, index, count, IsolTag, false); + this.AddFeature(collection, index, count, FinaTag, false); + this.AddFeature(collection, index, count, Fin2Tag, false); + this.AddFeature(collection, index, count, Fin3Tag, false); + this.AddFeature(collection, index, count, MediTag, false); + this.AddFeature(collection, index, count, Med2Tag, false); + this.AddFeature(collection, index, count, InitTag, false); + + // HarfBuzz plans these as Arabic-script features, independently of the + // generic horizontal feature list. Horizontal runs already get them from + // DefaultShaper; forced vertical Arabic needs them here as well. + if (collection.TextOptions.LayoutMode.IsVertical()) + { + this.AddFeature(collection, index, count, CaltTag); + this.AddFeature(collection, index, count, LigaTag); + this.AddFeature(collection, index, count, CligTag); + } + + this.AddFeature(collection, index, count, MsetTag); + } + + /// + protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) + { + base.AssignFeatures(collection, index, count); + + int prev = -1; + int state = 0; + byte[] actions = new byte[count]; + + // Apply the state machine to map glyphs to features. + for (int i = 0; i < count; i++) + { + GlyphShapingData data = collection[i + index]; + ArabicJoiningClass joiningClass = CodePoint.GetArabicJoiningClass(data.CodePoint); + ArabicJoiningType joiningType = joiningClass.JoiningType; + if (joiningType == ArabicJoiningType.Transparent) + { + actions[i] = None; + continue; + } + + int shapingClassIndex = GetShapingClassIndex(joiningType); + byte[] actionsWithState = StateTable[state, shapingClassIndex]; + byte prevAction = actionsWithState[0]; + byte curAction = actionsWithState[1]; + state = actionsWithState[2]; + + if (prevAction != None && prev != -1) + { + actions[prev] = prevAction; + } + + actions[i] = curAction; + prev = i; + } + + // Apply the chosen features to their respective glyphs. + for (int i = 0; i < actions.Length; i++) + { + switch (actions[i]) + { + case Fina: + collection.EnableShapingFeature(i + index, FinaTag); + break; + case Fin2: + collection.EnableShapingFeature(i + index, Fin2Tag); + break; + case Fin3: + collection.EnableShapingFeature(i + index, Fin3Tag); + break; + case Isol: + collection.EnableShapingFeature(i + index, IsolTag); + break; + case Init: + collection.EnableShapingFeature(i + index, InitTag); + break; + case Medi: + collection.EnableShapingFeature(i + index, MediTag); + break; + case Med2: + collection.EnableShapingFeature(i + index, Med2Tag); + break; + } + } + } + + /// + /// Maps an Arabic joining type to the corresponding column index in the state table. + /// + /// The Arabic joining type. + /// The state table column index. + private static int GetShapingClassIndex(ArabicJoiningType joiningType) => joiningType switch + { + ArabicJoiningType.NonJoining => 0, + ArabicJoiningType.LeftJoining => 1, + ArabicJoiningType.RightJoining => 2, + ArabicJoiningType.DualJoining or ArabicJoiningType.JoinCausing => 3, + + // TODO: ALAPH: 4 + // TODO: DALATH RISH': 5 + ArabicJoiningType.Transparent => 6, + _ => 0, + }; + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs new file mode 100644 index 0000000..3eb9c19 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using System.Collections.Generic; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers { + /// + /// Abstract base class for all script shapers. Defines the shaping pipeline + /// consisting of preprocessing, feature planning, postprocessing, and feature assignment stages. + /// + internal abstract class BaseShaper + { + /// + /// Gets or sets the script classification for this shaper. + /// + public ScriptClass ScriptClass { get; protected set; } + + /// + /// Gets or sets the mark zeroing mode that determines when mark advances are zeroed. + /// + public MarkZeroingMode MarkZeroingMode { get; protected set; } + + /// + /// Assigns the features to each glyph within the collection. + /// + /// The glyph shaping collection. + /// The zero-based index of the elements to assign. + /// The number of elements to assign. + public void Plan(IGlyphShapingCollection collection, int index, int count) + { + int collectionCount = collection.Count; + + this.PlanPreprocessingFeatures(collection, index, count); + + RecalculateCount(collection, ref collectionCount, ref count); + + this.PlanFeatures(collection, index, count); + + RecalculateCount(collection, ref collectionCount, ref count); + + this.PlanPostprocessingFeatures(collection, index, count); + + RecalculateCount(collection, ref collectionCount, ref count); + + this.AssignFeatures(collection, index, count); + } + + /// + /// Assigns the features to each glyph within the collection. + /// + /// The glyph shaping collection. + /// The zero-based index of the elements to assign. + /// The number of elements to assign. + protected abstract void PlanFeatures(IGlyphShapingCollection collection, int index, int count); + + /// + /// Assigns the preprocessing features to each glyph within the collection. + /// + /// The glyph shaping collection. + /// The zero-based index of the elements to assign. + /// The number of elements to assign. + protected abstract void PlanPreprocessingFeatures(IGlyphShapingCollection collection, int index, int count); + + /// + /// Assigns the postprocessing features to each glyph within the collection. + /// + /// The glyph shaping collection. + /// The zero-based index of the elements to assign. + /// The number of elements to assign. + protected abstract void PlanPostprocessingFeatures(IGlyphShapingCollection collection, int index, int count); + + /// + /// Assigns the shaper specific substitution features to each glyph within the collection. + /// + /// The glyph shaping collection. + /// The zero-based index of the elements to assign. + /// The number of elements to assign. + protected abstract void AssignFeatures(IGlyphShapingCollection collection, int index, int count); + + /// + /// Gets the ordered collection of shaping stages for this shaper. + /// + /// The shaping stages. + public abstract IEnumerable GetShapingStages(); + + /// + /// Recalculates the count when the collection size changes during shaping. + /// + /// The glyph shaping collection. + /// The previous collection count, updated to the current count. + /// The element count, adjusted by the size delta. + private static void RecalculateCount(IGlyphShapingCollection collection, ref int oldCount, ref int count) + { + // If the collection has changed size we need to recalculate the count. + int delta = collection.Count - oldCount; + count += delta; + oldCount += delta; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs new file mode 100644 index 0000000..69b724e --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs @@ -0,0 +1,338 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using System; +using System.Collections.Generic; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers { + /// + /// Default shaper, which will be applied to all glyphs. + /// Based on fontkit: + /// + internal class DefaultShaper : BaseShaper + { + /// The 'rvrn' (required variation alternates) feature tag. + protected static readonly Tag RvnrTag = Tag.Parse("rvrn"); + + /// The 'ltra' (left-to-right alternates) feature tag. + protected static readonly Tag LtraTag = Tag.Parse("ltra"); + + /// The 'ltrm' (left-to-right mirrored forms) feature tag. + protected static readonly Tag LtrmTag = Tag.Parse("ltrm"); + + /// The 'rtla' (right-to-left alternates) feature tag. + protected static readonly Tag RtlaTag = Tag.Parse("rtla"); + + /// The 'rtlm' (right-to-left mirrored forms) feature tag. + protected static readonly Tag RtlmTag = Tag.Parse("rtlm"); + + /// The 'frac' (fractions) feature tag. + protected static readonly Tag FracTag = Tag.Parse("frac"); + + /// The 'numr' (numerators) feature tag. + protected static readonly Tag NumrTag = Tag.Parse("numr"); + + /// The 'dnom' (denominators) feature tag. + protected static readonly Tag DnomTag = Tag.Parse("dnom"); + + /// The 'ccmp' (glyph composition/decomposition) feature tag. + protected static readonly Tag CcmpTag = Tag.Parse("ccmp"); + + /// The 'locl' (localized forms) feature tag. + protected static readonly Tag LoclTag = Tag.Parse("locl"); + + /// The 'rlig' (required ligatures) feature tag. + protected static readonly Tag RligTag = Tag.Parse("rlig"); + + /// The 'mark' (mark positioning) feature tag. + protected static readonly Tag MarkTag = Tag.Parse("mark"); + + /// The 'mkmk' (mark-to-mark positioning) feature tag. + protected static readonly Tag MkmkTag = Tag.Parse("mkmk"); + + /// The 'calt' (contextual alternates) feature tag. + protected static readonly Tag CaltTag = Tag.Parse("calt"); + + /// The 'clig' (contextual ligatures) feature tag. + protected static readonly Tag CligTag = Tag.Parse("clig"); + + /// The 'liga' (standard ligatures) feature tag. + protected static readonly Tag LigaTag = Tag.Parse("liga"); + + /// The 'rclt' (required contextual alternates) feature tag. + protected static readonly Tag RcltTag = Tag.Parse("rclt"); + + /// The 'curs' (cursive positioning) feature tag. + protected static readonly Tag CursTag = Tag.Parse("curs"); + + /// The 'kern' (kerning) feature tag. + protected static readonly Tag KernTag = Tag.Parse("kern"); + + /// The 'vert' (vertical alternates) feature tag. + protected static readonly Tag VertTag = Tag.Parse("vert"); + + /// The 'vkrn' (vertical kerning) feature tag. + protected static readonly Tag VKernTag = Tag.Parse("vkrn"); + + /// The fraction slash code point (U+2044). + private static readonly CodePoint FractionSlash = new(0x2044); + + /// The solidus (slash) code point (U+002F). + private static readonly CodePoint Slash = new(0x002F); + + /// The set of shaping stages accumulated during feature planning. + private readonly HashSet shapingStages = []; + + /// The kerning mode from the text options. + private readonly KerningMode kerningMode; + + /// The user-specified feature tags from the text options. + private readonly IReadOnlyList featureTags; + + /// + /// Initializes a new instance of the class with PostGpos mark zeroing. + /// + /// The script classification. + /// The text options. + internal DefaultShaper(ScriptClass script, TextOptions textOptions) + : this(script, MarkZeroingMode.PostGpos, textOptions) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The script classification. + /// The mark zeroing mode. + /// The text options. + protected DefaultShaper(ScriptClass script, MarkZeroingMode markZeroingMode, TextOptions textOptions) + { + this.ScriptClass = script; + this.MarkZeroingMode = markZeroingMode; + this.kerningMode = textOptions.KerningMode; + this.featureTags = textOptions.FeatureTags; + } + + /// + protected override void PlanFeatures(IGlyphShapingCollection collection, int index, int count) + { + } + + /// + protected override void PlanPreprocessingFeatures(IGlyphShapingCollection collection, int index, int count) + { + // Add variation Features. + this.AddFeature(collection, index, count, RvnrTag); + + // Add directional features. + for (int i = index; i < index + count; i++) + { + GlyphShapingData shapingData = collection[i]; + + if (shapingData.Direction == TextDirection.LeftToRight) + { + this.AddFeature(collection, i, 1, LtraTag); + this.AddFeature(collection, i, 1, LtrmTag); + } + else + { + this.AddFeature(collection, i, 1, RtlaTag); + this.AddFeature(collection, i, 1, RtlmTag); + } + } + + // TODO: Fractional feature should be assigned here but disabled. + // They should then be enabled in AssignFeatures. + } + + /// + protected override void PlanPostprocessingFeatures(IGlyphShapingCollection collection, int index, int count) + { + // Add common features. + this.AddFeature(collection, index, count, CcmpTag); + this.AddFeature(collection, index, count, LoclTag); + this.AddFeature(collection, index, count, RligTag); + this.AddFeature(collection, index, count, MarkTag); + this.AddFeature(collection, index, count, MkmkTag); + + LayoutMode layoutMode = collection.TextOptions.LayoutMode; + bool isVerticalLayout = false; + for (int i = index; i < index + count; i++) + { + GlyphShapingData shapingData = collection[i]; + isVerticalLayout |= AdvancedTypographicUtils.IsVerticalGlyph(shapingData.CodePoint, layoutMode); + } + + // Add horizontal or vertical features. + if (!isVerticalLayout) + { + // Add horizontal features. + this.AddFeature(collection, index, count, CaltTag); + this.AddFeature(collection, index, count, CligTag); + this.AddFeature(collection, index, count, LigaTag); + this.AddFeature(collection, index, count, RcltTag); + this.AddFeature(collection, index, count, CursTag); + this.AddFeature(collection, index, count, KernTag); + } + else + { + // We only apply `vert` feature.See: + // https://github.com/harfbuzz/harfbuzz/commit/d71c0df2d17f4590d5611239577a6cb532c26528 + // https://lists.freedesktop.org/archives/harfbuzz/2013-August/003490.html + + // We really want to find a 'vert' feature if there's any in the font, no + // matter which script/langsys it is listed (or not) under. + // See various bugs referenced from: + // https://github.com/harfbuzz/harfbuzz/issues/63 + this.AddFeature(collection, index, count, VertTag); + } + + // Add user defined features. + foreach (Tag feature in this.featureTags) + { + // We've already dealt with fractional features. + if (feature != FracTag && feature != NumrTag && feature != DnomTag) + { + this.AddFeature(collection, index, count, feature); + } + } + } + + /// + protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) + { + // TODO: We shouldn't be relying on the feature list + // User defined fractional features require special treatment. + // https://docs.microsoft.com/en-us/typography/opentype/spec/features_fj#tag-frac + if (this.HasFractions()) + { + this.AssignFractionalFeatures(collection, index, count); + } + } + + /// + /// Adds a shaping feature to the specified range of glyphs in the collection and registers the corresponding shaping stage. + /// + /// The glyph shaping collection. + /// The zero-based index of the first element. + /// The number of elements. + /// The feature tag to add. + /// Whether the feature is initially enabled. + /// An optional action to invoke before the feature is applied. + /// An optional action to invoke after the feature is applied. + protected void AddFeature( + IGlyphShapingCollection collection, + int index, + int count, + Tag feature, + bool enabled = true, + Action? preAction = null, + Action? postAction = null) + { + if (this.kerningMode == KerningMode.None) + { + if (feature == KernTag || feature == VKernTag) + { + return; + } + } + + int end = index + count; + for (int i = index; i < end; i++) + { + collection.AddShapingFeature(i, new TagEntry(feature, enabled)); + } + + this.shapingStages.Add(new ShapingStage(feature, preAction, postAction)); + } + + /// + public override IEnumerable GetShapingStages() => this.shapingStages; + + /// + /// Assigns fractional feature tags (numerator, denominator, fraction) to glyphs forming fraction sequences. + /// + /// The glyph shaping collection. + /// The zero-based index of the first element. + /// The number of elements. + private void AssignFractionalFeatures(IGlyphShapingCollection collection, int index, int count) + { + // Enable contextual fractions. + for (int i = index; i < index + count; i++) + { + GlyphShapingData shapingData = collection[i]; + if (shapingData.CodePoint == FractionSlash || shapingData.CodePoint == Slash) + { + int start = i; + int end = i + 1; + + // Apply numerator. + if (start > 0) + { + shapingData = collection[start - 1]; + while (start > 0 && CodePoint.IsDigit(shapingData.CodePoint)) + { + this.AddFeature(collection, start - 1, 1, NumrTag); + this.AddFeature(collection, start - 1, 1, FracTag); + start--; + } + } + + // Apply denominator. + if (end < collection.Count) + { + shapingData = collection[end]; + while (end < collection.Count && CodePoint.IsDigit(shapingData.CodePoint)) + { + this.AddFeature(collection, end, 1, DnomTag); + this.AddFeature(collection, end, 1, FracTag); + end++; + } + } + + // Apply fraction slash. + this.AddFeature(collection, i, 1, FracTag); + i = end - 1; + } + } + } + + /// + /// Determines whether the user-specified feature tags include fractional features. + /// + /// if fractional features are present; otherwise, . + private bool HasFractions() + { + bool hasNmr = false; + bool hasDnom = false; + + // My kingdom for a binary search on IReadOnlyList + for (int i = 0; i < this.featureTags.Count; i++) + { + Tag feature = this.featureTags[i]; + if (feature == FracTag) + { + return true; + } + + if (feature == DnomTag) + { + hasDnom = true; + } + + if (feature == NumrTag) + { + hasNmr = true; + } + + if (hasDnom && hasNmr) + { + return true; + } + } + + return false; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs new file mode 100644 index 0000000..65b658f --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -0,0 +1,527 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using System; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers { + /// + /// This is a shaper for the Hangul script, used by the Korean language. + /// The shaping state machine was ported from fontkit. + /// + /// + internal sealed class HangulShaper : DefaultShaper + { + /// The 'ljmo' (leading Jamo forms) feature tag. + private static readonly Tag LjmoTag = Tag.Parse("ljmo"); + + /// The 'vjmo' (vowel Jamo forms) feature tag. + private static readonly Tag VjmoTag = Tag.Parse("vjmo"); + + /// The 'tjmo' (trailing Jamo forms) feature tag. + private static readonly Tag TjmoTag = Tag.Parse("tjmo"); + + /// The base code point for precomposed Hangul syllables (U+AC00). + private const int HangulBase = 0xac00; + + /// The base code point for leading consonant Jamo (U+1100). + private const int LBase = 0x1100; // lead + + /// The base code point for vowel Jamo (U+1161). + private const int VBase = 0x1161; // vowel + + /// The base code point for trailing consonant Jamo (U+11A7). + private const int TBase = 0x11a7; // trail + + /// The number of leading consonant Jamo. + private const int LCount = 19; + + /// The number of vowel Jamo. + private const int VCount = 21; + + /// The number of trailing consonant Jamo (including no-trail). + private const int TCount = 28; + + /// The last leading consonant Jamo code point. + private const int LEnd = LBase + LCount - 1; + + /// The last vowel Jamo code point. + private const int VEnd = VBase + VCount - 1; + + /// The last trailing consonant Jamo code point. + private const int TEnd = TBase + TCount - 1; + + /// The dotted circle code point (U+25CC) used as a placeholder base. + private const int DottedCircle = 0x25cc; + + /// Other character category. + private const byte X = 0; + + /// Leading consonant category. + private const byte L = 1; + + /// Medial vowel category. + private const byte V = 2; + + /// Trailing consonant category. + private const byte T = 3; + + /// Composed lead-vowel syllable category. + private const byte LV = 4; + + /// Composed lead-vowel-trail syllable category. + private const byte LVT = 5; + + /// Tone mark category. + private const byte M = 6; + + /// No action. + private const byte None = 0; + + /// Decompose composed syllable action. + private const byte Decompose = 1; + + /// Compose Jamo sequence action. + private const byte Compose = 2; + + /// Reorder tone mark action. + private const byte ToneMark = 4; + + /// Invalid sequence (insert dotted circle) action. + private const byte Invalid = 5; + + /// + /// State machine table for Hangul syllable composition/decomposition. + /// Each entry is [action, nextState]. Rows are states, columns are character categories. + /// + private static readonly byte[,][] StateTable = + { + // # X L V T LV LVT M + // State 0: start state + { new byte[] { None, 0 }, new byte[] { None, 1 }, new byte[] { None, 0 }, new byte[] { None, 0 }, new byte[] { Decompose, 2 }, new byte[] { Decompose, 3 }, new byte[] { Invalid, 0 } }, + + // State 1: + { new byte[] { None, 0 }, new byte[] { None, 1 }, new byte[] { Compose, 2 }, new byte[] { None, 0 }, new byte[] { Decompose, 2 }, new byte[] { Decompose, 3 }, new byte[] { Invalid, 0 } }, + + // State 2: or + { new byte[] { None, 0 }, new byte[] { None, 1 }, new byte[] { None, 0 }, new byte[] { Compose, 3 }, new byte[] { Decompose, 2 }, new byte[] { Decompose, 3 }, new byte[] { ToneMark, 0 } }, + + // State 3: or + { new byte[] { None, 0 }, new byte[] { None, 1 }, new byte[] { None, 0 }, new byte[] { None, 0 }, new byte[] { Decompose, 2 }, new byte[] { Decompose, 3 }, new byte[] { ToneMark, 0 } }, + }; + + /// The font metrics used for glyph lookups during composition/decomposition. + private readonly FontMetrics fontMetrics; + + /// + /// Initializes a new instance of the class. + /// + /// The script classification. + /// The text options. + /// The font metrics for glyph lookups. + public HangulShaper(ScriptClass script, TextOptions textOptions, FontMetrics fontMetrics) + : base(script, MarkZeroingMode.None, textOptions) + => this.fontMetrics = fontMetrics; + + /// + protected override void PlanFeatures(IGlyphShapingCollection collection, int index, int count) + { + this.AddFeature(collection, index, count, LjmoTag, false); + this.AddFeature(collection, index, count, VjmoTag, false); + this.AddFeature(collection, index, count, TjmoTag, false); + } + + /// + protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) + { + for (int i = index; i < count; i++) + { + // Uniscribe does not apply 'calt' for Hangul, and certain fonts + // (Noto Sans CJK, Source Sans Han, etc) apply all of jamo lookups + // in calt, which is not desirable. + collection.DisableShapingFeature(i, CaltTag); + } + + // Apply the state machine to map glyphs to features. + if (collection is GlyphSubstitutionCollection substitutionCollection) + { + // Allocate a small buffer for composition operations. + Span compositionBuffer = stackalloc ushort[3]; + + // GSub + int state = 0; + for (int i = 0; i < count; i++) + { + if (i + index >= substitutionCollection.Count) + { + break; + } + + GlyphShapingData data = substitutionCollection[i + index]; + CodePoint codePoint = data.CodePoint; + int type = GetSyllableType(codePoint); + byte[] actionsWithState = StateTable[state, type]; + byte action = actionsWithState[0]; + state = actionsWithState[1]; + + // TODO: Do not stackalloc in the loop. + switch (action) + { + case Decompose: + + // Decompose the composed syllable if it is not supported by the font. + if (data.GlyphId == 0) + { + i = this.DecomposeGlyph(substitutionCollection, data, i, compositionBuffer); + } + + break; + + case Compose: + + // Found a decomposed syllable. Try to compose if supported by the font. + i = this.ComposeGlyph(substitutionCollection, i, type, compositionBuffer); + break; + + case ToneMark: + + // Got a valid syllable, followed by a tone mark. Move the tone mark to the beginning of the syllable. + this.ReOrderToneMark(substitutionCollection, data, i); + break; + + case Invalid: + + // Tone mark has no valid syllable to attach to, so insert a dotted circle. + i = this.InsertDottedCircle(substitutionCollection, data, i, compositionBuffer); + break; + } + } + } + else + { + // GPos + // Simply loop and enable based on type. + // Glyph substitution has handled [de]composition. + for (int i = 0; i < count; i++) + { + if (i + index >= collection.Count) + { + break; + } + + GlyphShapingData data = collection[i + index]; + CodePoint codePoint = data.CodePoint; + switch (GetSyllableType(codePoint)) + { + case L: + collection.EnableShapingFeature(i, LjmoTag); + break; + case V: + collection.EnableShapingFeature(i, VjmoTag); + break; + case T: + collection.EnableShapingFeature(i, TjmoTag); + break; + case LV: + collection.EnableShapingFeature(i, LjmoTag); + collection.EnableShapingFeature(i, VjmoTag); + break; + case LVT: + collection.EnableShapingFeature(i, LjmoTag); + collection.EnableShapingFeature(i, VjmoTag); + collection.EnableShapingFeature(i, TjmoTag); + break; + } + } + } + } + + /// + /// Gets the Hangul syllable type category for a code point. + /// + /// The code point to classify. + /// The syllable type constant (L, V, T, LV, LVT, M, or X). + private static int GetSyllableType(CodePoint codePoint) + { + GraphemeClusterClass type = CodePoint.GetGraphemeClusterClass(codePoint); + int value = codePoint.Value; + + return type switch + { + GraphemeClusterClass.HangulLead => L, + GraphemeClusterClass.HangulVowel => V, + GraphemeClusterClass.HangulTail => T, + GraphemeClusterClass.HangulLeadVowel => LV, + GraphemeClusterClass.HangulLeadVowelTail => LVT, + + // HANGUL SINGLE DOT TONE MARK + // HANGUL DOUBLE DOT TONE MARK + _ => value is >= 0x302E and <= 0x302F ? M : X, + }; + } + + /// + /// Gets the number of Jamo components in a syllable for tone mark reordering. + /// + /// The code point to measure. + /// The syllable length in Jamo components. + private static int GetSyllableLength(CodePoint codePoint) + => GetSyllableType(codePoint) switch + { + LV or LVT => 1, + V => 2, + T => 3, + _ => 0, + }; + + /// + /// Decomposes a precomposed Hangul syllable into its constituent Jamo glyphs. + /// + /// The glyph substitution collection. + /// The shaping data for the composed syllable. + /// The index of the glyph to decompose. + /// A buffer for temporary glyph ID storage. + /// The updated index after decomposition. + private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingData data, int index, Span compositinoBuffer) + { + // Decompose the syllable into a sequence of glyphs. + int s = data.CodePoint.Value - HangulBase; + int t = TBase + (s % TCount); + s = (s / TCount) | 0; + int l = (LBase + (s / VCount)) | 0; + int v = VBase + (s % VCount); + + FontMetrics metrics = this.fontMetrics; + + // Don't decompose if all of the components are not available + if (!metrics.TryGetGlyphId(new(l), out ushort ljmo) || + !metrics.TryGetGlyphId(new(v), out ushort vjmo) || + (!metrics.TryGetGlyphId(new(t), out ushort tjmo) && t != TBase)) + { + return index; + } + + // Replace the current glyph with decomposed L, V, and T glyphs, + // and apply the proper OpenType features to each component. + if (t <= TBase) + { + Span ii = compositinoBuffer[..2]; + ii[1] = vjmo; + ii[0] = ljmo; + + collection.Replace(index, ii, KnownFeatureTags.GlyphCompositionDecomposition); + collection.EnableShapingFeature(index, LjmoTag); + collection.EnableShapingFeature(index + 1, VjmoTag); + return index + 1; + } + + Span iii = compositinoBuffer[..3]; + iii[2] = tjmo; + iii[1] = vjmo; + iii[0] = ljmo; + + collection.Replace(index, iii, KnownFeatureTags.GlyphCompositionDecomposition); + collection.EnableShapingFeature(index, LjmoTag); + collection.EnableShapingFeature(index + 1, VjmoTag); + collection.EnableShapingFeature(index + 2, TjmoTag); + return index + 2; + } + + /// + /// Attempts to compose decomposed Jamo into a precomposed Hangul syllable. + /// + /// The glyph substitution collection. + /// The current index in the collection. + /// The syllable type of the current glyph. + /// A buffer for glyph IDs during composition. + /// The updated index after composition. + private int ComposeGlyph(GlyphSubstitutionCollection collection, int index, int type, Span compositionBuffer) + { + if (index == 0) + { + return index; + } + + GlyphShapingData prev = collection[index - 1]; + CodePoint prevCodePoint = prev.CodePoint; + int prevType = GetSyllableType(prevCodePoint); + + // Figure out what type of syllable we're dealing with + CodePoint lv = default; + int ljmo = -1, vjmo = -1, tjmo = -1; + + if (prevType == LV && type == T) + { + // + lv = prevCodePoint; + tjmo = index; + } + else + { + if (type == V) + { + // + ljmo = index - 1; + vjmo = index; + } + else + { + // + ljmo = index - 2; + vjmo = index - 1; + tjmo = index; + } + + CodePoint l = collection[ljmo].CodePoint; + CodePoint v = collection[vjmo].CodePoint; + + // Make sure L and V are combining characters + if (IsCombiningL(l) && IsCombiningV(v)) + { + lv = new CodePoint(HangulBase + ((((l.Value - LBase) * VCount) + (v.Value - VBase)) * TCount)); + } + } + + CodePoint t = tjmo >= 0 ? collection[tjmo].CodePoint : new CodePoint(TBase); + if ((lv != default) && (t.Value == TBase || IsCombiningT(t))) + { + CodePoint s = new(lv.Value + (t.Value - TBase)); + + // Replace with a composed glyph if supported by the font, + // otherwise apply the proper OpenType features to each component. + if (this.fontMetrics.TryGetGlyphId(s, out ushort id)) + { + int del = prevType == V ? 3 : 2; + int idx = index - del + 1; + collection.Replace(idx, del - 1, id, KnownFeatureTags.GlyphCompositionDecomposition); + collection[idx].CodePoint = s; + return idx; + } + } + + // Didn't compose (either a non-combining component or unsupported by font). + if (ljmo >= 0) + { + collection.EnableShapingFeature(ljmo, LjmoTag); + } + + if (vjmo >= 0) + { + collection.EnableShapingFeature(vjmo, VjmoTag); + } + + if (tjmo >= 0) + { + collection.EnableShapingFeature(tjmo, TjmoTag); + } + + if (prevType == LV) + { + // Sequence was originally , which got combined earlier. + // Either the T was non-combining, or the LVT glyph wasn't supported. + // Decompose the glyph again and apply OT features. + this.DecomposeGlyph(collection, collection[index - 1], index - 1, compositionBuffer); + return index + 1; + } + + return index; + } + + /// + /// Reorders a tone mark to the beginning of the preceding syllable. + /// + /// The glyph substitution collection. + /// The shaping data of the tone mark glyph. + /// The index of the tone mark in the collection. + private void ReOrderToneMark(GlyphSubstitutionCollection collection, GlyphShapingData data, int index) + { + if (index == 0) + { + return; + } + + // Move tone mark to the beginning of the previous syllable, unless it is zero width + // We don't have access to the glyphs metrics as an array when substituting so we have to loop. + FontMetrics fontMetrics = this.fontMetrics; + TextAttributes textAttributes = data.TextRun.TextAttributes; + TextDecorations textDecorations = data.TextRun.TextDecorations; + LayoutMode layoutMode = collection.TextOptions.LayoutMode; + ColorFontSupport colorFontSupport = collection.TextOptions.ColorFontSupport; + if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out FontGlyphMetrics? metrics) + && metrics.AdvanceWidth == 0) + { + return; + } + + GlyphShapingData prev = collection[index - 1]; + int len = GetSyllableLength(prev.CodePoint); + collection.MoveGlyph(index, index - len); + } + + /// + /// Inserts a dotted circle glyph as a placeholder for an invalid tone mark that has no syllable to attach to. + /// + /// The glyph substitution collection. + /// The shaping data of the invalid tone mark glyph. + /// The index of the tone mark in the collection. + /// A buffer for glyph IDs during insertion. + /// The updated index after insertion. + private int InsertDottedCircle(GlyphSubstitutionCollection collection, GlyphShapingData data, int index, Span compositionBuffer) + { + bool after = false; + FontMetrics fontMetrics = this.fontMetrics; + + if (fontMetrics.TryGetGlyphId(new(DottedCircle), out ushort id)) + { + TextAttributes textAttributes = data.TextRun.TextAttributes; + TextDecorations textDecorations = data.TextRun.TextDecorations; + LayoutMode layoutMode = collection.TextOptions.LayoutMode; + ColorFontSupport colorFontSupport = collection.TextOptions.ColorFontSupport; + if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out FontGlyphMetrics? metrics) + && metrics.AdvanceWidth != 0) + { + after = true; + } + + // If the tone mark is zero width, insert the dotted circle before, otherwise after + Span glyphs = compositionBuffer[..2]; + if (after) + { + glyphs[1] = id; + glyphs[0] = data.GlyphId; + } + else + { + glyphs[1] = data.GlyphId; + glyphs[0] = id; + } + + collection.Replace(index, glyphs, KnownFeatureTags.GlyphCompositionDecomposition); + return index + 1; + } + + return index; + } + + /// + /// Determines whether the code point is a combining leading consonant Jamo. + /// + /// The code point to test. + /// if the code point is in the leading Jamo range. + private static bool IsCombiningL(CodePoint code) => UnicodeUtility.IsInRangeInclusive((uint)code.Value, LBase, LEnd); + + /// + /// Determines whether the code point is a combining vowel Jamo. + /// + /// The code point to test. + /// if the code point is in the vowel Jamo range. + private static bool IsCombiningV(CodePoint code) => UnicodeUtility.IsInRangeInclusive((uint)code.Value, VBase, VEnd); + + /// + /// Determines whether the code point is a combining trailing consonant Jamo. + /// + /// The code point to test. + /// if the code point is in the trailing Jamo range. + private static bool IsCombiningT(CodePoint code) => UnicodeUtility.IsInRangeInclusive((uint)code.Value, TBase + 1, TEnd); + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HebrewShaper.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HebrewShaper.cs new file mode 100644 index 0000000..f45cb0f --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HebrewShaper.cs @@ -0,0 +1,307 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers { + /// + /// Hebrew shaper. Handles mark reordering (PATAH/QAMATS before SHEVA/HIRIQ before METEG) + /// and presentation form composition for legacy fonts without GPOS mark positioning. + /// Based on HarfBuzz: + /// + internal class HebrewShaper : DefaultShaper + { + /// + /// Hebrew presentation forms with dagesh for U+05D0..U+05EA. + /// A value of 0x0000 means no dagesh form exists for that letter. + /// + private static readonly ushort[] DageshForms = + [ + 0xFB30, // ALEF + 0xFB31, // BET + 0xFB32, // GIMEL + 0xFB33, // DALET + 0xFB34, // HE + 0xFB35, // VAV + 0xFB36, // ZAYIN + 0x0000, // HET + 0xFB38, // TET + 0xFB39, // YOD + 0xFB3A, // FINAL KAF + 0xFB3B, // KAF + 0xFB3C, // LAMED + 0x0000, // FINAL MEM + 0xFB3E, // MEM + 0x0000, // FINAL NUN + 0xFB40, // NUN + 0xFB41, // SAMEKH + 0x0000, // AYIN + 0xFB43, // FINAL PE + 0xFB44, // PE + 0x0000, // FINAL TSADI + 0xFB46, // TSADI + 0xFB47, // QOF + 0xFB48, // RESH + 0xFB49, // SHIN + 0xFB4A, // TAV + ]; + + /// The font metrics used for glyph lookups during composition. + private readonly FontMetrics fontMetrics; + + /// Whether the font has GSUB features for Hebrew. + private readonly bool hasGsub; + + /// + /// Initializes a new instance of the class. + /// + /// The script classification. + /// The text options. + /// The font metrics for glyph lookups. + /// Whether the font has GSUB features for Hebrew. + public HebrewShaper(ScriptClass script, TextOptions textOptions, FontMetrics fontMetrics, bool hasGsub) + : base(script, MarkZeroingMode.PostGpos, textOptions) + { + this.fontMetrics = fontMetrics; + this.hasGsub = hasGsub; + } + + /// + protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) + { + base.AssignFeatures(collection, index, count); + + if (collection is not GlyphSubstitutionCollection substitutionCollection) + { + return; + } + + // Step 1: Reorder Hebrew marks. + // Swap SHEVA/HIRIQ with following METEG when preceded by PATAH/QAMATS. + // https://bugzilla.mozilla.org/show_bug.cgi?id=728866 + ReorderMarks(substitutionCollection, index, count); + + // Step 2: Compose Hebrew presentation forms for legacy fonts. + // Only applied when the font lacks GSUB features (proxy for lacking GPOS mark). + if (!this.hasGsub) + { + ComposeHebrewForms(substitutionCollection, this.fontMetrics, index, count); + } + } + + /// + /// Reorders Hebrew combining marks to ensure correct rendering. + /// + /// Looks for the pattern [PATAH/QAMATS, SHEVA/HIRIQ, METEG/BELOW] and swaps + /// the last two marks. This ensures correct visual stacking of vowel points + /// and the meteg stress mark. + /// + /// + /// The glyph substitution collection. + /// The zero-based start index. + /// The number of elements to process. + private static void ReorderMarks(GlyphSubstitutionCollection collection, int index, int count) + { + int end = index + count; + for (int i = index + 2; i < end; i++) + { + int c0 = collection[i - 2].CodePoint.Value; + int c1 = collection[i - 1].CodePoint.Value; + int c2 = collection[i].CodePoint.Value; + + // c0: PATAH (U+05B7) or QAMATS (U+05B8) + // c1: SHEVA (U+05B0) or HIRIQ (U+05B4) + // c2: METEG (U+05BD) or a below-class mark + if (IsPatahOrQamats(c0) && IsShevaOrHiriq(c1) && IsMetegOrBelow(c2)) + { + // Swap positions i-1 and i. + GlyphShapingData data1 = collection[i - 1]; + GlyphShapingData data2 = collection[i]; + + // Swap codepoints and glyph IDs. + (collection[i - 1].CodePoint, collection[i].CodePoint) = (data2.CodePoint, data1.CodePoint); + (collection[i - 1].GlyphId, collection[i].GlyphId) = (data2.GlyphId, data1.GlyphId); + break; + } + } + } + + /// + /// Composes Hebrew base + mark sequences into precomposed presentation forms. + /// This is a fallback for legacy fonts that lack GPOS mark-to-base positioning. + /// + /// The glyph substitution collection. + /// The font metrics for glyph lookups. + /// The zero-based start index. + /// The number of elements to process. + private static void ComposeHebrewForms(GlyphSubstitutionCollection collection, FontMetrics fontMetrics, int index, int count) + { + int end = index + count; + for (int i = index + 1; i < end; i++) + { + int a = collection[i - 1].CodePoint.Value; + int b = collection[i].CodePoint.Value; + + int composed = TryCompose(a, b); + if (composed != 0 && fontMetrics.TryGetGlyphId(new CodePoint(composed), out ushort composedGlyphId)) + { + // Replace the two glyphs with the composed form. + collection.Replace(i - 1, 2, composedGlyphId, KnownFeatureTags.GlyphCompositionDecomposition); + end--; + i--; + } + } + } + + /// + /// Attempts to compose two Hebrew codepoints into a precomposed presentation form. + /// Returns the composed codepoint, or 0 if no composition exists. + /// + /// The first (base) codepoint value. + /// The second (combining mark) codepoint value. + /// The composed codepoint, or 0 if no composition exists. + private static int TryCompose(int a, int b) + { + switch (b) + { + case 0x05B4: // HIRIQ + + if (a == 0x05D9) + { + // YOD + return 0xFB1D; + } + + break; + + case 0x05B7: // PATAH + if (a == 0x05F2) + { + // YIDDISH YOD YOD + PATAH + return 0xFB1F; + } + + if (a == 0x05D0) + { + // ALEF + PATAH + return 0xFB2E; + } + + break; + + case 0x05B8: // QAMATS + if (a == 0x05D0) + { + // ALEF + QAMATS + return 0xFB2F; + } + + break; + + case 0x05B9: // HOLAM + if (a == 0x05D5) + { + // VAV + HOLAM + return 0xFB4B; + } + + break; + + case 0x05BC: // DAGESH + if (a is >= 0x05D0 and <= 0x05EA) + { + int form = DageshForms[a - 0x05D0]; + return form != 0 ? form : 0; + } + + if (a == 0xFB2A) + { + // SHIN WITH SHIN DOT + DAGESH + return 0xFB2C; + } + + if (a == 0xFB2B) + { + // SHIN WITH SIN DOT + DAGESH + return 0xFB2D; + } + + break; + + case 0x05BF: // RAFE + if (a == 0x05D1) + { + // BET + RAFE + return 0xFB4C; + } + + if (a == 0x05DB) + { + // KAF + RAFE + return 0xFB4D; + } + + if (a == 0x05E4) + { + // PE + RAFE + return 0xFB4E; + } + + break; + + case 0x05C1: // SHIN DOT + if (a == 0x05E9) + { + // SHIN + SHIN DOT + return 0xFB2A; + } + + if (a == 0xFB49) + { + // SHIN WITH DAGESH + SHIN DOT + return 0xFB2C; + } + + break; + + case 0x05C2: // SIN DOT + if (a == 0x05E9) + { + // SHIN + SIN DOT + return 0xFB2B; + } + + if (a == 0xFB49) + { + // SHIN WITH DAGESH + SIN DOT + return 0xFB2D; + } + + break; + } + + return 0; + } + + /// Returns if the codepoint is PATAH (U+05B7) or QAMATS (U+05B8). + /// The codepoint value to test. + /// if the codepoint is PATAH or QAMATS. + private static bool IsPatahOrQamats(int codepoint) + => codepoint is 0x05B7 or 0x05B8; + + /// Returns if the codepoint is SHEVA (U+05B0) or HIRIQ (U+05B4). + /// The codepoint value to test. + /// if the codepoint is SHEVA or HIRIQ. + private static bool IsShevaOrHiriq(int codepoint) + => codepoint is 0x05B0 or 0x05B4; + + /// + /// Returns if the codepoint is METEG (U+05BD) or a combining mark with CCC = Below (220). + /// Currently checks METEG only; generic CCC=220 detection would require combining class data. + /// + /// The codepoint value to test. + /// if the codepoint is METEG or a below-class mark. + private static bool IsMetegOrBelow(int codepoint) + => codepoint == 0x05BD; + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs new file mode 100644 index 0000000..114aad5 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -0,0 +1,1495 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Globalization; +using SixLabors.Fonts.Unicode; +using SixLabors.Fonts.Unicode.Resources; +using UnicodeTrieGenerator.StateAutomation; +using static SixLabors.Fonts.Unicode.Resources.IndicShapingData; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers { + /// + /// The IndicShaper supports Indic scripts e.g. Devanagari, Kannada, etc. + /// + internal sealed class IndicShaper : DefaultShaper + { + /// The state machine for Indic syllable identification. + private static readonly StateMachine StateMachine = + new(StateTable, AcceptingStates, Tags); + + /// Maps Indic shaping category codes to compact DFA symbol indices. + private static readonly int[] CategoryToSymbolId = BuildCategoryToSymbolId(); + + /// The 'rphf' (reph forms) feature tag. + private static readonly Tag RphfTag = Tag.Parse("rphf"); + + /// The 'nukt' (nukta forms) feature tag. + private static readonly Tag NuktTag = Tag.Parse("nukt"); + + /// The 'akhn' (akhands) feature tag. + private static readonly Tag AkhnTag = Tag.Parse("akhn"); + + /// The 'pref' (pre-base forms) feature tag. + private static readonly Tag PrefTag = Tag.Parse("pref"); + + /// The 'rkrf' (rakar forms) feature tag. + private static readonly Tag RkrfTag = Tag.Parse("rkrf"); + + /// The 'abvf' (above-base forms) feature tag. + private static readonly Tag AbvfTag = Tag.Parse("abvf"); + + /// The 'blwf' (below-base forms) feature tag. + private static readonly Tag BlwfTag = Tag.Parse("blwf"); + + /// The 'half' (half forms) feature tag. + private static readonly Tag HalfTag = Tag.Parse("half"); + + /// The 'pstf' (post-base forms) feature tag. + private static readonly Tag PstfTag = Tag.Parse("pstf"); + + /// The 'vatu' (vattu variants) feature tag. + private static readonly Tag VatuTag = Tag.Parse("vatu"); + + /// The 'cjct' (conjunct forms) feature tag. + private static readonly Tag CjctTag = Tag.Parse("cjct"); + + /// The 'cfar' (conjunct form after Ra) feature tag. + private static readonly Tag CfarTag = Tag.Parse("cfar"); + + /// The 'init' (initial forms) feature tag. + private static readonly Tag InitTag = Tag.Parse("init"); + + /// The 'abvs' (above-base substitutions) feature tag. + private static readonly Tag AbvsTag = Tag.Parse("abvs"); + + /// The 'blws' (below-base substitutions) feature tag. + private static readonly Tag BlwsTag = Tag.Parse("blws"); + + /// The 'pres' (pre-base substitutions) feature tag. + private static readonly Tag PresTag = Tag.Parse("pres"); + + /// The 'psts' (post-base substitutions) feature tag. + private static readonly Tag PstsTag = Tag.Parse("psts"); + + /// The 'haln' (halant forms) feature tag. + private static readonly Tag HalnTag = Tag.Parse("haln"); + + /// The 'dist' (distances) feature tag. + private static readonly Tag DistTag = Tag.Parse("dist"); + + /// The 'abvm' (above-base mark positioning) feature tag. + private static readonly Tag AbvmTag = Tag.Parse("abvm"); + + /// The 'blwm' (below-base mark positioning) feature tag. + private static readonly Tag BlwmTag = Tag.Parse("blwm"); + + /// Dotted circle code point (U+25CC) used as a placeholder base. + private const int DottedCircle = 0x25cc; + + /// The text options. + private readonly TextOptions textOptions; + + /// The font metrics used for glyph lookups. + private readonly FontMetrics fontMetrics; + + /// The script-specific shaping configuration for this Indic script. + private ShapingConfiguration indicConfiguration; + + /// Whether this font uses old-spec Indic script tags. + private readonly bool isOldSpec; + + /// Whether any broken clusters were detected during syllable setup. + private bool hasBrokenClusters; + + /// + /// Initializes a new instance of the class. + /// + /// The script classification. + /// The Unicode script tag found in the font. + /// The text options. + /// The font metrics for glyph lookups. + public IndicShaper(ScriptClass script, Tag unicodeScriptTag, TextOptions textOptions, FontMetrics fontMetrics) + : base(script, MarkZeroingMode.None, textOptions) + { + this.textOptions = textOptions; + this.fontMetrics = fontMetrics; + + if (IndicConfigurations.TryGetValue(script, out ShapingConfiguration value)) + { + this.indicConfiguration = value; + } + else + { + this.indicConfiguration = ShapingConfiguration.Default; + } + + this.isOldSpec = this.indicConfiguration.HasOldSpec && !unicodeScriptTag.ToString().EndsWith("2", StringComparison.OrdinalIgnoreCase); + } + + /// + protected override void PlanFeatures(IGlyphShapingCollection collection, int index, int count) + { + this.AddFeature(collection, index, count, LoclTag, preAction: this.SetupSyllables); + this.AddFeature(collection, index, count, CcmpTag); + + this.AddFeature(collection, index, count, NuktTag, preAction: this.InitialReorder); + this.AddFeature(collection, index, count, AkhnTag); + + this.AddFeature(collection, index, count, RphfTag, false); + this.AddFeature(collection, index, count, RkrfTag); + this.AddFeature(collection, index, count, PrefTag, false); + this.AddFeature(collection, index, count, BlwfTag, false); + this.AddFeature(collection, index, count, AbvfTag, false); + this.AddFeature(collection, index, count, HalfTag, false); + this.AddFeature(collection, index, count, PstfTag, false); + this.AddFeature(collection, index, count, VatuTag); + this.AddFeature(collection, index, count, CjctTag); + this.AddFeature(collection, index, count, CfarTag, false, postAction: this.FinalReorder); + + this.AddFeature(collection, index, count, InitTag, false); + this.AddFeature(collection, index, count, PresTag); + this.AddFeature(collection, index, count, AbvsTag); + this.AddFeature(collection, index, count, BlwsTag); + this.AddFeature(collection, index, count, PstsTag); + this.AddFeature(collection, index, count, HalnTag); + this.AddFeature(collection, index, count, DistTag); + this.AddFeature(collection, index, count, AbvmTag); + this.AddFeature(collection, index, count, BlwmTag); + } + + /// + protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) + { + if (collection is not GlyphSubstitutionCollection substitutionCollection) + { + return; + } + + FontMetrics fontMetrics = this.fontMetrics; + + // Decompose split matras + Span buffer = stackalloc ushort[16]; + int end = index + count; + for (int i = end - 1; i >= index; i--) + { + GlyphShapingData data = substitutionCollection[i]; + if ((Decompositions.TryGetValue(data.CodePoint.Value, out int[]? decompositions) || + UniversalShapingData.Decompositions.TryGetValue(data.CodePoint.Value, out decompositions)) && + decompositions != null) + { + Span ids = buffer[..decompositions.Length]; + bool shouldDecompose = true; + for (int j = 0; j < decompositions.Length; j++) + { + if (!fontMetrics.TryGetGlyphId(new CodePoint(decompositions[j]), out ushort id)) + { + shouldDecompose = false; + break; + } + + ids[j] = id; + } + + if (shouldDecompose) + { + substitutionCollection.Replace(i, ids, KnownFeatureTags.GlyphCompositionDecomposition); + for (int j = 0; j < decompositions.Length; j++) + { + substitutionCollection[i + j].CodePoint = new(decompositions[j]); + } + } + } + } + } + + /// + /// Identifies Indic syllables using the state machine and assigns shaping info to each glyph. + /// + /// The glyph shaping collection. + /// The zero-based start index. + /// The number of elements to process. + private void SetupSyllables(IGlyphShapingCollection collection, int index, int count) + { + if (collection is not GlyphSubstitutionCollection substitutionCollection) + { + return; + } + + this.hasBrokenClusters = false; + + Span values = count <= 64 ? stackalloc int[count] : new int[count]; + + for (int i = index; i < index + count; i++) + { + // Convert HarfBuzz-style Indic shaping categories into the compact + // DFA symbol indices used by the generated state machine. + // + // HarfBuzz category codes (C=1, V=2, MR=36, VBlw=21, etc.) are sparse + // and can be larger than the alphabet size of the DFA. Our state + // machine expects its input alphabet to be dense 0..N-1, matching the + // sequential IDs assigned in GenerateIndicShapingDataTrie. + // + // CategoryToSymbolId[IndicShapingCategory(codePoint)] performs this mapping, ensuring that + // every codepoint is presented to the DFA using the correct compact + // symbol index. + CodePoint codePoint = substitutionCollection[i].CodePoint; + values[i - index] = CategoryToSymbolId[IndicShapingCategory(codePoint)]; + } + + int syllable = 0; + int last = 0; + foreach (StateMatch match in StateMachine.Match(values)) + { + if (match.StartIndex > last) + { + ++syllable; + for (int i = last; i < match.StartIndex; i++) + { + GlyphShapingData data = substitutionCollection[i + index]; + data.IndicShapingEngineInfo = new(Categories.X, Positions.End, "non_indic_cluster", syllable); + } + } + + ++syllable; + + // Create shaper info. + for (int i = match.StartIndex; i <= match.EndIndex; i++) + { + GlyphShapingData data = substitutionCollection[i + index]; + CodePoint codePoint = data.CodePoint; + + string syllableType = match.Tags[0]; + + if (syllableType == "broken_cluster") + { + this.hasBrokenClusters = true; + } + + data.IndicShapingEngineInfo = new( + (Categories)IndicShapingCategory(codePoint), + (Positions)IndicShapingPosition(codePoint), + syllableType, + syllable); + } + + last = match.EndIndex + 1; + } + + if (last < count) + { + ++syllable; + for (int i = last; i < count; i++) + { + GlyphShapingData data = substitutionCollection[i + index]; + data.IndicShapingEngineInfo = new(Categories.X, Positions.End, "non_indic_cluster", syllable); + } + } + } + + /// + /// Gets the Indic shaping category for a code point (upper 8 bits of the shaping properties). + /// + /// The code point. + /// The shaping category value. + private static int IndicShapingCategory(CodePoint codePoint) + => UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) >> 8; + + /// + /// Gets the Indic shaping position for a code point (lower 8 bits as a bit flag). + /// + /// The code point. + /// The shaping position as a bit flag. + private static int IndicShapingPosition(CodePoint codePoint) + => 1 << (UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) & 0xFF); + + /// + /// Performs the initial reordering pass for Indic syllables, including base consonant + /// identification, reph handling, matra reordering, and feature assignment. + /// + /// The glyph shaping collection. + /// The zero-based start index. + /// The number of elements to process. + private void InitialReorder(IGlyphShapingCollection collection, int index, int count) + { + if (collection is not GlyphSubstitutionCollection substitutionCollection) + { + return; + } + + // Create a reusable temporary substitution collection and buffer to allow checking whether + // certain combinations will be substituted. + GlyphSubstitutionCollection tempCollection = new(this.textOptions); + Span tempBuffer = new GlyphShapingData[3]; + + ShapingConfiguration indicConfiguration = this.indicConfiguration; + FontMetrics fontMetrics = this.fontMetrics; + CodePoint viramaPoint = new(indicConfiguration.Virama); + + if (fontMetrics.TryGetGlyphId(viramaPoint, out ushort viramaId)) + { + for (int i = 0; i < count; i++) + { + GlyphShapingData data = substitutionCollection[i + index]; + IndicShapingEngineInfo? info = data.IndicShapingEngineInfo; + + if (info?.Position == Positions.Base_C) + { + GlyphShapingData virama = new(data, false) + { + GlyphId = viramaId, + CodePoint = viramaPoint + }; + + tempBuffer[2] = virama; + tempBuffer[1] = data; + tempBuffer[0] = virama; + + info.Position = this.ConsonantPosition(tempCollection, tempBuffer); + } + } + } + + int max = index + count; + int start = index; + int end = NextSyllable(substitutionCollection, index, max); + + if (this.hasBrokenClusters) + { + if (fontMetrics.TryGetGlyphId(new(DottedCircle), out ushort circleId)) + { + Span glyphs = stackalloc ushort[2]; + while (start < max) + { + GlyphShapingData data = substitutionCollection[start]; + IndicShapingEngineInfo? dataInfo = data.IndicShapingEngineInfo; + string? type = dataInfo?.SyllableType; + + if (type == "broken_cluster") + { + // Insert after possible Repha. + int i = start; + for (i = start; i < end; i++) + { + if (substitutionCollection[i].IndicShapingEngineInfo?.Category != Categories.Repha) + { + break; + } + } + + GlyphShapingData current = substitutionCollection[i]; + IndicShapingEngineInfo currentInfo = current.IndicShapingEngineInfo!; + glyphs[0] = circleId; + glyphs[1] = current.GlyphId; + + substitutionCollection.Replace(i, glyphs, KnownFeatureTags.GlyphCompositionDecomposition); + + // The dotted circle is now at position i (inherits original shaping info). + // Update it to be a dotted circle base. + GlyphShapingData dotted = substitutionCollection[i]; + dotted.IndicShapingEngineInfo!.Category = Categories.Dotted_Circle; + dotted.IndicShapingEngineInfo.Position = Positions.End; + + // The original mark glyph is now at position i + 1 (copy of original info). + // Its shaping info is already correct from the copy. + end++; + max++; + } + + start = end; + end = NextSyllable(substitutionCollection, start, max); + } + + start = index; + end = NextSyllable(substitutionCollection, index, max); + } + } + + _ = fontMetrics.TryGetGSubTable(out GSubTable? gSubTable); + while (start < max) + { + GlyphShapingData data = substitutionCollection[start]; + IndicShapingEngineInfo? dataInfo = data.IndicShapingEngineInfo; + string? type = dataInfo?.SyllableType; + + if (type is "symbol_cluster" or "non_indic_cluster") + { + goto Increment; + } + + // 1. Find base consonant: + // + // The shaping engine finds the base consonant of the syllable, using the + // following algorithm: starting from the end of the syllable, move backwards + // until a consonant is found that does not have a below-base or post-base + // form (post-base forms have to follow below-base forms), or that is not a + // pre-base reordering Ra, or arrive at the first consonant. The consonant + // stopped at will be the base. + int basePosition = end; + int limit = start; + bool hasReph = false; + + // If the syllable starts with Ra + Halant (in a script that has Reph) + // and has more than one consonant, Ra is excluded from candidates for + // base consonants. + if (start + 3 <= end && + indicConfiguration.RephPosition != Positions.Ra_To_Become_Reph && + gSubTable?.TryGetFeatureLookups(fontMetrics, in RphfTag, this.ScriptClass, out _) == true && + ((indicConfiguration.RephMode == RephMode.Implicit && !IsJoiner(substitutionCollection[start + 2])) || + (indicConfiguration.RephMode == RephMode.Explicit && substitutionCollection[start + 2].IndicShapingEngineInfo?.Category == Categories.ZWJ))) + { + // See if it matches the 'rphf' feature. + tempBuffer[2] = substitutionCollection[start + 2]; + tempBuffer[1] = substitutionCollection[start + 1]; + tempBuffer[0] = substitutionCollection[start]; + + if ((indicConfiguration.RephMode == RephMode.Explicit && this.WouldSubstitute(tempCollection, in RphfTag, tempBuffer)) || + this.WouldSubstitute(tempCollection, in RphfTag, tempBuffer[..2])) + { + limit += 2; + while (limit < end && IsJoiner(substitutionCollection[limit])) + { + limit++; + } + + basePosition = start; + hasReph = true; + } + } + else if (indicConfiguration.RephMode == RephMode.Log_Repha && + substitutionCollection[start].IndicShapingEngineInfo?.Category == Categories.Repha) + { + limit++; + while (limit < end && IsJoiner(substitutionCollection[limit])) + { + limit++; + } + + basePosition = start; + hasReph = true; + } + + switch (indicConfiguration.BasePosition) + { + case BasePosition.Last: + { + // Starting from the end of the syllable, move backwards + int i = end; + bool seenBelow = false; + + do + { + IndicShapingEngineInfo? prevInfo = substitutionCollection[--i].IndicShapingEngineInfo; + + // Until a consonant is found + if (IsConsonant(substitutionCollection[i])) + { + // that does not have a below-base or post-base form + // (post-base forms have to follow below-base forms), + if (prevInfo?.Position != Positions.Below_C && (prevInfo?.Position != Positions.Post_C || seenBelow)) + { + basePosition = i; + break; + } + + // or that is not a pre-base reordering Ra, + // + // IMPLEMENTATION NOTES: + // + // Our pre-base reordering Ra's are marked POS_POST_C, so will be skipped + // by the logic above already. + // + + // or arrive at the first consonant. The consonant stopped at will + // be the base. + if (prevInfo?.Position == Positions.Below_C) + { + seenBelow = true; + } + + basePosition = i; + } + else if (start < i && prevInfo?.Category == Categories.ZWJ && + substitutionCollection[i - 1].IndicShapingEngineInfo?.Category == Categories.H) + { + // A ZWJ after a Halant stops the base search, and requests an explicit + // half form. + // A ZWJ before a Halant, requests a subjoined form instead, and hence + // search continues. This is particularly important for Bengali + // sequence Ra,H,Ya that should form Ya-Phalaa by subjoining Ya. + break; + } + } + while (i > limit); + + break; + } + + case BasePosition.First: + { + // The first consonant is always the base. + basePosition = start; + + for (int i = basePosition + 1; i < end; i++) + { + GlyphShapingData c = substitutionCollection[i]; + if (IsConsonant(c) && c.IndicShapingEngineInfo != null) + { + c.IndicShapingEngineInfo.Position = Positions.Below_C; + } + } + + break; + } + } + + // If the syllable starts with Ra + Halant (in a script that has Reph) + // and has more than one consonant, Ra is excluded from candidates for + // base consonants. + // + // Only do this for unforced Reph. (ie. not for Ra,H,ZWJ) + if (hasReph && basePosition == start && limit - basePosition <= 2) + { + hasReph = false; + } + + // 2. Decompose and reorder Matras: + // + // Each matra and any syllable modifier sign in the cluster are moved to the + // appropriate position relative to the consonant(s) in the cluster. The + // shaping engine decomposes two- or three-part matras into their constituent + // parts before any repositioning. Matra characters are classified by which + // consonant in a conjunct they have affinity for and are reordered to the + // following positions: + // + // o Before first half form in the syllable + // o After subjoined consonants + // o After post-form consonant + // o After main consonant (for above marks) + // + // IMPLEMENTATION NOTES: + // + // The normalize() routine has already decomposed matras for us, so we don't + // need to worry about that. + + // 3. Reorder marks to canonical order: + // + // Adjacent nukta and halant or nukta and vedic sign are always repositioned + // if necessary, so that the nukta is first. + // + // IMPLEMENTATION NOTES: + // + // We don't need to do this: the normalize() routine already did this for us. + + // Reorder characters + for (int i = start; i < basePosition; i++) + { + IndicShapingEngineInfo? info = substitutionCollection[i].IndicShapingEngineInfo; + if (info != null) + { + info.Position = (Positions)Math.Min((int)Positions.Pre_C, (int)info.Position); + } + } + + if (basePosition < end) + { + IndicShapingEngineInfo? info = substitutionCollection[basePosition].IndicShapingEngineInfo; + if (info != null) + { + info.Position = Positions.Base_C; + } + } + + // Mark final consonants. A final consonant is one appearing after a matra, + // like in Khmer. + for (int i = basePosition + 1; i < end; i++) + { + if (substitutionCollection[i].IndicShapingEngineInfo?.Category == Categories.M) + { + for (int j = i + 1; j < end; j++) + { + GlyphShapingData c = substitutionCollection[j]; + if (IsConsonant(c) && c.IndicShapingEngineInfo != null) + { + c.IndicShapingEngineInfo.Position = Positions.Final_C; + break; + } + } + + break; + } + } + + // Handle beginning Ra + if (hasReph) + { + GlyphShapingData c = substitutionCollection[start]; + if (c.IndicShapingEngineInfo != null) + { + c.IndicShapingEngineInfo.Position = Positions.Ra_To_Become_Reph; + } + } + + // For old-style Indic script tags, move the first post-base Halant after + // last consonant. + // + // Reports suggest that in some scripts Uniscribe does this only if there + // is *not* a Halant after last consonant already (eg. Kannada), while it + // does it unconditionally in other scripts (eg. Malayalam). We don't + // currently know about other scripts, so we single out Malayalam for now. + // + // Kannada test case: + // U+0C9A,U+0CCD,U+0C9A,U+0CCD + // With some versions of Lohit Kannada. + // https://bugs.freedesktop.org/show_bug.cgi?id=59118 + // + // Malayalam test case: + // U+0D38,U+0D4D,U+0D31,U+0D4D,U+0D31,U+0D4D + // With lohit-ttf-20121122/Lohit-Malayalam.ttf + if (this.isOldSpec) + { + bool disallowDoubleHalants = this.ScriptClass != ScriptClass.Malayalam; + for (int i = basePosition + 1; i < end; i++) + { + if (substitutionCollection[i].IndicShapingEngineInfo?.Category == Categories.H) + { + int j; + for (j = end - 1; j > i; j--) + { + GlyphShapingData c = substitutionCollection[j]; + if (IsConsonant(c) || (disallowDoubleHalants && c.IndicShapingEngineInfo?.Category == Categories.H)) + { + break; + } + } + + if (j > i && substitutionCollection[j].IndicShapingEngineInfo?.Category != Categories.H) + { + // Move Halant to after last consonant. + substitutionCollection.MoveGlyph(i, j); + } + + break; + } + } + } + + // Attach misc marks to previous char to move with them. + Positions lastPosition = Positions.Start; + for (int i = start; i < end; i++) + { + IndicShapingEngineInfo? info = substitutionCollection[i].IndicShapingEngineInfo; + if (info != null) + { + if ((FlagUnsafe(info.Category) & (JoinerFlags | Flag(Categories.N) | Flag(Categories.RS) | Flag(Categories.CM) | (HalantOrCoengFlags & FlagUnsafe(info.Category)))) != 0) + { + info.Position = lastPosition; + if (info.Category == Categories.H && info.Position == Positions.Pre_M) + { + // Uniscribe doesn't move the Halant with Left Matra. + // TEST: U+092B,U+093F,U+094DE + // We follow. This is important for the Sinhala + // U+0DDA split matra since it decomposes to U+0DD9,U+0DCA + // where U+0DD9 is a left matra and U+0DCA is the virama. + // We don't want to move the virama with the left matra. + // TEST: U+0D9A,U+0DDA + for (int j = i; j > start; j--) + { + Positions? pos = substitutionCollection[j - 1].IndicShapingEngineInfo?.Position; + if (pos is not null and not Positions.Pre_M) + { + info.Position = pos.Value; + break; + } + } + } + } + else if (info.Position != Positions.SMVD) + { + // If an MPst follows an SM, update the SM's position to match + // so they move together during reordering. + if (info.Category == Categories.MPst + && i > start + && substitutionCollection[i - 1].IndicShapingEngineInfo?.Category == Categories.SM) + { + substitutionCollection[i - 1].IndicShapingEngineInfo!.Position = info.Position; + } + + lastPosition = info.Position; + } + } + } + + // For post-base consonants let them own anything before them + // since the last consonant or matra. + int last = basePosition; + for (int i = basePosition + 1; i < end; i++) + { + GlyphShapingData current = substitutionCollection[i]; + IndicShapingEngineInfo? info = current.IndicShapingEngineInfo; + if (info != null) + { + if (IsConsonant(current)) + { + for (int j = last + 1; j < i; j++) + { + IndicShapingEngineInfo? jInfo = substitutionCollection[j].IndicShapingEngineInfo; + if (jInfo?.Position < Positions.SMVD) + { + jInfo.Position = info.Position; + } + } + + last = i; + } + else if ((FlagUnsafe(info.Category) & (Flag(Categories.M) | Flag(Categories.MPst))) != 0) + { + last = i; + } + } + } + + substitutionCollection.Sort(start, end, (a, b) => + { + int pa = a.IndicShapingEngineInfo?.Position != null ? (int)a.IndicShapingEngineInfo.Position : 0; + int pb = b.IndicShapingEngineInfo?.Position != null ? (int)b.IndicShapingEngineInfo.Position : 0; + return pa - pb; + }); + + // Find base again + for (int i = start; i < end; i++) + { + if (substitutionCollection[i].IndicShapingEngineInfo?.Position == Positions.Base_C) + { + basePosition = i; + break; + } + } + + // Setup features now. + + // Reph. + for (int i = start; i < end; i++) + { + IndicShapingEngineInfo? info = substitutionCollection[i].IndicShapingEngineInfo; + if (info?.Position != Positions.Ra_To_Become_Reph) + { + break; + } + + substitutionCollection.EnableShapingFeature(i, RphfTag); + } + + // Pre-base + bool blwf = !this.isOldSpec && indicConfiguration.BlwfMode == BlwfMode.Pre_And_Post; + for (int i = start; i < basePosition; i++) + { + substitutionCollection.EnableShapingFeature(i, HalfTag); + if (blwf) + { + substitutionCollection.EnableShapingFeature(i, BlwfTag); + } + } + + // Post-base + for (int i = basePosition + 1; i < end; i++) + { + substitutionCollection.EnableShapingFeature(i, AbvfTag); + substitutionCollection.EnableShapingFeature(i, PstfTag); + substitutionCollection.EnableShapingFeature(i, BlwfTag); + } + + if (this.isOldSpec && this.ScriptClass == ScriptClass.Devanagari) + { + // Old-spec eye-lash Ra needs special handling. + // From the spec: + // + // "The feature 'below-base form' is applied to consonants + // having below-base forms and following the base consonant. + // The exception is vattu, which may appear below half forms + // as well as below the base glyph. The feature 'below-base + // form' will be applied to all such occurrences of Ra as well." + // + // Test case: U+0924,U+094D,U+0930,U+094d,U+0915 + // with Sanskrit 2003 font. + // + // However, note that Ra,Halant,ZWJ is the correct way to + // request eyelash form of Ra, so we wouldn't inhibit it + // in that sequence. + // + // Test case: U+0924,U+094D,U+0930,U+094d,U+200D,U+0915 + for (int i = start; i + 1 < basePosition; i++) + { + if (substitutionCollection[i].IndicShapingEngineInfo?.Category == Categories.Ra && + substitutionCollection[i + 1].IndicShapingEngineInfo?.Category == Categories.H && + (i + 1 == basePosition || substitutionCollection[i + 2].IndicShapingEngineInfo?.Category == Categories.ZWJ)) + { + substitutionCollection.EnableShapingFeature(i, BlwfTag); + substitutionCollection.EnableShapingFeature(i + 1, BlwfTag); + } + } + } + + const int prefLen = 2; + if (basePosition + prefLen < end && + gSubTable?.TryGetFeatureLookups(fontMetrics, in PrefTag, this.ScriptClass, out _) == true) + { + // Find a Halant,Ra sequence and mark it for pre-base reordering processing. + for (int i = basePosition + 1; i + prefLen - 1 < end; i++) + { + tempBuffer[1] = substitutionCollection[i + 1]; + tempBuffer[0] = substitutionCollection[i]; + if (this.WouldSubstitute(tempCollection, in PrefTag, tempBuffer[..2])) + { + for (int j = 0; j < prefLen; j++) + { + substitutionCollection.EnableShapingFeature(i++, PrefTag); + } + + // Mark the subsequent stuff with 'cfar'. Used in Khmer. + // Read the feature spec. + // This allows distinguishing the following cases with MS Khmer fonts: + // U+1784,U+17D2,U+179A,U+17D2,U+1782 + // U+1784,U+17D2,U+1782,U+17D2,U+179A + if (gSubTable.TryGetFeatureLookups(fontMetrics, in CfarTag, this.ScriptClass, out _)) + { + while (i < end) + { + substitutionCollection.EnableShapingFeature(i, CfarTag); + i++; + } + } + + break; + } + } + } + + // Apply ZWJ/ZWNJ effects + for (int i = start + 1; i < end; i++) + { + GlyphShapingData current = substitutionCollection[i]; + if (IsJoiner(current)) + { + bool nonJoiner = current.IndicShapingEngineInfo?.Category == Categories.ZWNJ; + int j = i; + + do + { + j--; + + // ZWJ/ZWNJ should disable CJCT. They do that by simply + // being there, since we don't skip them for the CJCT + // feature (ie. F_MANUAL_ZWJ) + + // A ZWNJ disables HALF. + if (nonJoiner) + { + substitutionCollection.DisableShapingFeature(j, HalfTag); + } + } + while (j > start && !IsConsonant(substitutionCollection[j])); + } + } + + Increment: + start = end; + end = NextSyllable(substitutionCollection, start, max); + } + } + + /// + /// Determines the positional class of a consonant by testing whether it would be + /// substituted by below-base, post-base, or pre-base features. + /// + /// A temporary substitution collection for testing. + /// The consonant and virama glyph data to test. + /// The consonant's positional class. + private Positions ConsonantPosition(GlyphSubstitutionCollection collection, ReadOnlySpan data) + { + if (this.WouldSubstitute(collection, in BlwfTag, data[..2]) || + this.WouldSubstitute(collection, in BlwfTag, data.Slice(1, 2))) + { + return Positions.Below_C; + } + + if (this.WouldSubstitute(collection, in PstfTag, data[..2]) || + this.WouldSubstitute(collection, in PstfTag, data.Slice(1, 2))) + { + return Positions.Post_C; + } + + if (this.WouldSubstitute(collection, in PrefTag, data[..2]) || + this.WouldSubstitute(collection, in PrefTag, data.Slice(1, 2))) + { + return Positions.Post_C; + } + + return Positions.Base_C; + } + + /// + /// Tests whether applying a specific feature to the given glyphs would produce a substitution. + /// + /// A temporary substitution collection for testing. + /// The feature tag to test. + /// The glyph data to test. + /// if a substitution would occur. + private bool WouldSubstitute(GlyphSubstitutionCollection collection, in Tag featureTag, ReadOnlySpan buffer) + { + collection.Clear(); + for (int i = 0; i < buffer.Length; i++) + { + collection.AddGlyph(buffer[i], i); + collection.EnableShapingFeature(i, featureTag); + } + + FontMetrics fontMetrics = this.fontMetrics; + if (fontMetrics.TryGetGSubTable(out GSubTable? gSubTable)) + { + const int index = 0; + SkippingGlyphIterator iterator = new(fontMetrics, collection, index, default, 0); + int initialCount = collection.Count; + int collectionCount = initialCount; + int count = initialCount - index; + int i = index; + + // Set max constraints to prevent OutOfMemoryException or infinite loops from attacks. + int maxCount = AdvancedTypographicUtils.GetMaxAllowableShapingCollectionCount(collection.Count); + int maxOperationsCount = AdvancedTypographicUtils.GetMaxAllowableShapingOperationsCount(collection.Count); + int currentOperations = 0; + + gSubTable.ApplyFeature( + fontMetrics, + collection, + ref iterator, + in featureTag, + this.ScriptClass, + index, + ref count, + ref i, + ref collectionCount, + maxCount, + maxOperationsCount, + ref currentOperations); + + return collection.Count != initialCount; + } + + return false; + } + + /// + /// Determines whether the glyph data represents an Indic consonant. + /// + /// The glyph shaping data. + /// if the glyph is a consonant. + private static bool IsConsonant(GlyphShapingData data) + => data.IndicShapingEngineInfo != null && (FlagUnsafe(data.IndicShapingEngineInfo.Category) & ConsonantFlags) != 0; + + /// + /// Determines whether the glyph data represents a joiner (ZWJ or ZWNJ). + /// + /// The glyph shaping data. + /// if the glyph is a joiner. + private static bool IsJoiner(GlyphShapingData data) + => data.IndicShapingEngineInfo != null && (FlagUnsafe(data.IndicShapingEngineInfo.Category) & JoinerFlags) != 0; + + /// + /// Determines whether the glyph data represents a halant or coeng character. + /// + /// The glyph shaping data. + /// if the glyph is a halant or coeng. + private static bool IsHalantOrCoeng(GlyphShapingData data) + => data.IndicShapingEngineInfo != null && (FlagUnsafe(data.IndicShapingEngineInfo.Category) & HalantOrCoengFlags) != 0; + + /// + /// Finds the start index of the next syllable in the collection. + /// + /// The glyph substitution collection. + /// The current index. + /// The maximum index bound. + /// The start index of the next syllable. + private static int NextSyllable(GlyphSubstitutionCollection collection, int index, int count) + { + if (index >= count) + { + return index; + } + + int? syllable = collection[index].IndicShapingEngineInfo?.Syllable; + while (++index < count) + { + if (collection[index].IndicShapingEngineInfo?.Syllable != syllable) + { + break; + } + } + + return index; + } + + /// + /// Performs the final reordering pass for Indic syllables, repositioning reph, + /// pre-base consonants, and pre-base matras after basic shaping. + /// + /// The glyph shaping collection. + /// The zero-based start index. + /// The number of elements to process. + private void FinalReorder(IGlyphShapingCollection collection, int index, int count) + { + if (collection is not GlyphSubstitutionCollection substitutionCollection) + { + return; + } + + int max = index + count; + int start = index; + int end = NextSyllable(substitutionCollection, index, max); + FontMetrics fontMetrics = this.fontMetrics; + _ = fontMetrics.TryGetGSubTable(out GSubTable? gSubTable); + while (start < max) + { + // 4. Final reordering: + // + // After the localized forms and basic shaping forms GSUB features have been + // applied (see below), the shaping engine performs some final glyph + // reordering before applying all the remaining font features to the entire + // cluster. + bool tryPref = gSubTable?.TryGetFeatureLookups(fontMetrics, in PrefTag, this.ScriptClass, out _) == true; + + // Find base consonant again. + int basePosition = start; + for (; basePosition < end; basePosition++) + { + if (substitutionCollection[basePosition].IndicShapingEngineInfo?.Position >= Positions.Base_C) + { + if (tryPref && basePosition + 1 < end) + { + for (int i = basePosition + 1; i < end; i++) + { + GlyphShapingData current = substitutionCollection[i]; + if (current.Features.FindIndex(x => x.Tag == PrefTag && x.Enabled) >= 0) + { + if (!current.IsSubstituted && current.IsLigated && !current.IsDecomposed) + { + // Ok, this was a 'pref' candidate but didn't form any. + // Base is around here... + basePosition = i; + while (basePosition < end && IsHalantOrCoeng(substitutionCollection[basePosition])) + { + basePosition++; + } + + IndicShapingEngineInfo? info = substitutionCollection[basePosition].IndicShapingEngineInfo; + if (info != null) + { + info.Position = Positions.Base_C; + tryPref = false; + } + } + + break; + } + } + } + + // For Malayalam, skip over unformed below- (but NOT post-) forms. + if (this.ScriptClass == ScriptClass.Malayalam) + { + for (int i = basePosition + 1; i < end; i++) + { + while (i < end && IsJoiner(substitutionCollection[i])) + { + i++; + } + + if (i == end || !IsHalantOrCoeng(substitutionCollection[i])) + { + break; + } + + i++; // Skip halant. + while (i < end && IsJoiner(substitutionCollection[i])) + { + i++; + } + + if (i < end) + { + GlyphShapingData current = substitutionCollection[i]; + if (IsConsonant(current) && current.IndicShapingEngineInfo?.Position == Positions.Below_C) + { + basePosition = i; + IndicShapingEngineInfo? info = substitutionCollection[basePosition].IndicShapingEngineInfo; + if (info != null) + { + info.Position = Positions.Base_C; + } + } + } + } + } + + if (start < basePosition && substitutionCollection[basePosition].IndicShapingEngineInfo?.Position > Positions.Base_C) + { + basePosition--; + } + + break; + } + } + + if (basePosition == end && start < basePosition && substitutionCollection[basePosition - 1].IndicShapingEngineInfo?.Category == Categories.ZWJ) + { + basePosition--; + } + + if (basePosition < end) + { + while (start < basePosition && (FlagUnsafe(substitutionCollection[basePosition].IndicShapingEngineInfo?.Category) & (Flag(Categories.N) | HalantOrCoengFlags)) != 0) + { + basePosition--; + } + } + + // o Reorder matras: + // + // If a pre-base matra character had been reordered before applying basic + // features, the glyph can be moved closer to the main consonant based on + // whether half-forms had been formed. Actual position for the matra is + // defined as "after last standalone halant glyph, after initial matra + // position and before the main consonant". If ZWJ or ZWNJ follow this + // halant, position is moved after it. + // + // Otherwise there can't be any pre-base matra characters. + if (start + 1 < end && start < basePosition) + { + // If we lost track of base, alas, position before last thingy. + int newPos = basePosition == end ? basePosition - 2 : basePosition - 1; + + // Malayalam / Tamil do not have "half" forms or explicit virama forms. + // The glyphs formed by 'half' are Chillus or ligated explicit viramas. + // We want to position matra after them. + if (this.ScriptClass is not ScriptClass.Malayalam and not ScriptClass.Tamil) + { + while (newPos > start && (FlagUnsafe(substitutionCollection[newPos].IndicShapingEngineInfo?.Category) & (Flag(Categories.M) | HalantOrCoengFlags)) == 0) + { + newPos--; + } + + // If we found no Halant we are done. + // Otherwise only proceed if the Halant does + // not belong to the Matra itself! + GlyphShapingData current = substitutionCollection[newPos]; + if (IsHalantOrCoeng(current) && current.IndicShapingEngineInfo?.Position != Positions.Pre_M) + { + // If ZWJ or ZWNJ follow this halant, position is moved after it. + if (newPos + 1 < end && IsJoiner(substitutionCollection[newPos + 1])) + { + newPos++; + } + } + else + { + newPos = start; // No move. + } + } + + if (start < newPos && substitutionCollection[newPos].IndicShapingEngineInfo?.Position != Positions.Pre_M) + { + // Now go see if there's actually any matras... + for (int i = newPos; i > start; i--) + { + if (substitutionCollection[i - 1].IndicShapingEngineInfo?.Position == Positions.Pre_M) + { + int oldPos = i - 1; + if (oldPos < basePosition && basePosition <= newPos) + { + // Shouldn't actually happen. + basePosition--; + } + + substitutionCollection.MoveGlyph(oldPos, newPos); + newPos--; + } + } + } + } + + // o Reorder reph: + // + // Reph’s original position is always at the beginning of the syllable, + // (i.e. it is not reordered at the character reordering stage). However, + // it will be reordered according to the basic-forms shaping results. + // Possible positions for reph, depending on the script, are; after main, + // before post-base consonant forms, and after post-base consonant forms. + + // Two cases: + // + // - If repha is encoded as a sequence of characters (Ra,H or Ra,H,ZWJ), then + // we should only move it if the sequence ligated to the repha form. + // + // - If repha is encoded separately and in the logical position, we should only + // move it if it did NOT ligate. If it ligated, it's probably the font trying + // to make it work without the reordering. + GlyphShapingData original = substitutionCollection[start]; + if (start + 1 < end && + original.IndicShapingEngineInfo?.Position == Positions.Ra_To_Become_Reph && + (original.IndicShapingEngineInfo?.Category == Categories.Repha != (original.IsLigated && !original.IsDecomposed))) + { + int newRephPos = start; + Positions rephPos = this.indicConfiguration.RephPosition; + bool found = false; + + // 1. If reph should be positioned after post-base consonant forms, + // proceed to step 5. + if (rephPos != Positions.After_Post) + { + // 2. If the reph repositioning class is not after post-base: target + // position is after the first explicit halant glyph between the + // first post-reph consonant and last main consonant. If ZWJ or ZWNJ + // are following this halant, position is moved after it. If such + // position is found, this is the target position. Otherwise, + // proceed to the next step. + // + // Note: in old-implementation fonts, where classifications were + // fixed in shaping engine, there was no case where reph position + // will be found on this step. + newRephPos = start + 1; + while (newRephPos < basePosition && !IsHalantOrCoeng(substitutionCollection[newRephPos])) + { + newRephPos++; + } + + if (newRephPos < basePosition && IsHalantOrCoeng(substitutionCollection[newRephPos])) + { + // ->If ZWJ or ZWNJ are following this halant, position is moved after it. + if (newRephPos + 1 < basePosition && IsJoiner(substitutionCollection[newRephPos + 1])) + { + newRephPos++; + } + + found = true; + } + + // 3. If reph should be repositioned after the main consonant: find the + // first consonant not ligated with main, or find the first + // consonant that is not a potential pre-base reordering Ra. + if (!found && rephPos == Positions.After_Main) + { + newRephPos = basePosition; + while (newRephPos + 1 < end && substitutionCollection[newRephPos + 1].IndicShapingEngineInfo?.Position <= Positions.After_Main) + { + newRephPos++; + } + + found = newRephPos < end; + } + + // 4. If reph should be positioned before post-base consonant, find + // first post-base classified consonant not ligated with main. If no + // consonant is found, the target position should be before the + // first matra, syllable modifier sign or vedic sign. + // + // This is our take on what step 4 is trying to say (and failing, BADLY). + if (!found && rephPos == Positions.After_Sub) + { + newRephPos = basePosition; + while (newRephPos + 1 < end && (substitutionCollection[newRephPos + 1].IndicShapingEngineInfo?.Position & (Positions.Post_C | Positions.After_Post | Positions.SMVD)) == 0) + { + newRephPos++; + } + + found = newRephPos < end; + } + } + + // 5. If no consonant is found in steps 3 or 4, move reph to a position + // immediately before the first post-base matra, syllable modifier + // sign or vedic sign that has a reordering class after the intended + // reph position. For example, if the reordering position for reph + // is post-main, it will skip above-base matras that also have a + // post-main position. + if (!found) + { + // Copied from step 2. + newRephPos = start + 1; + while (newRephPos < basePosition && !IsHalantOrCoeng(substitutionCollection[newRephPos])) + { + newRephPos++; + } + + if (newRephPos < basePosition && IsHalantOrCoeng(substitutionCollection[newRephPos])) + { + // ->If ZWJ or ZWNJ are following this halant, position is moved after it. + if (newRephPos + 1 < basePosition && IsJoiner(substitutionCollection[newRephPos + 1])) + { + newRephPos++; + } + + found = true; + } + } + + // 6. Otherwise, reorder reph to the end of the syllable. + if (!found) + { + newRephPos = end - 1; + while (newRephPos > start && substitutionCollection[newRephPos].IndicShapingEngineInfo?.Position == Positions.SMVD) + { + newRephPos--; + } + + // If the Reph is to be ending up after a Matra,Halant sequence, + // position it before that Halant so it can interact with the Matra. + // However, if it's a plain Consonant,Halant we shouldn't do that. + // Uniscribe doesn't do this. + // TEST: U+0930,U+094D,U+0915,U+094B,U+094D + if (IsHalantOrCoeng(substitutionCollection[newRephPos])) + { + for (int i = basePosition + 1; i < newRephPos; i++) + { + if ((FlagUnsafe(substitutionCollection[i].IndicShapingEngineInfo?.Category) & Flag(Categories.M)) != 0) + { + newRephPos--; + } + } + } + } + + if (newRephPos != start) + { + substitutionCollection.MoveGlyph(start, newRephPos); + } + + if (start < basePosition && basePosition <= newRephPos) + { + basePosition--; + } + } + + // o Reorder pre-base reordering consonants: + // + // If a pre-base reordering consonant is found, reorder it according to + // the following rules: + if (tryPref && basePosition + 1 < end) + { + for (int i = basePosition + 1; i < end; i++) + { + GlyphShapingData current = substitutionCollection[i]; + if (current.Features.FindIndex(x => x.Tag == PrefTag && x.Enabled) >= 0) + { + // 1. Only reorder a glyph produced by substitution during application + // of the feature. (Note that a font may shape a Ra consonant with + // the feature generally but block it in certain contexts.) + + // Note: We just check that something got substituted. We don't check that + // the feature actually did it... + // + // Reorder pref only if it ligated. + if (current.IsLigated && !current.IsDecomposed) + { + // 2. Try to find a target position the same way as for pre-base matra. + // If it is found, reorder pre-base consonant glyph. + // + // 3. If position is not found, reorder immediately before main + // consonant. + int newPos = basePosition; + + // Malayalam / Tamil do not have "half" forms or explicit virama forms. + // The glyphs formed by 'half' are Chillus or ligated explicit viramas. + // We want to position matra after them. + if (this.ScriptClass is not ScriptClass.Malayalam and not ScriptClass.Tamil) + { + while (newPos > start && (FlagUnsafe(substitutionCollection[newPos - 1].IndicShapingEngineInfo?.Category) & (Flag(Categories.M) | HalantOrCoengFlags)) == 0) + { + newPos--; + } + + // TODO: Remove once we have Kmher shaper. + // In Khmer coeng model, a H,Ra can go *after* matras. If it goes after a + // split matra, it should be reordered to *before* the left part of such matra. + if (newPos > start && substitutionCollection[newPos - 1].IndicShapingEngineInfo?.Category == Categories.M) + { + int oldPos = i; + for (int j = basePosition + 1; j < oldPos; j++) + { + if (substitutionCollection[j].IndicShapingEngineInfo?.Category == Categories.M) + { + newPos--; + break; + } + } + } + } + + if (newPos > start && IsHalantOrCoeng(substitutionCollection[newPos - 1])) + { + // -> If ZWJ or ZWNJ follow this halant, position is moved after it. + if (newPos < end && IsJoiner(substitutionCollection[newPos])) + { + newPos++; + } + } + + substitutionCollection.MoveGlyph(i, newPos); + + if (newPos <= basePosition && basePosition < i) + { + basePosition++; + } + } + + break; + } + } + } + + // Apply 'init' to the Left Matra if it's a word start. + if (substitutionCollection[start].IndicShapingEngineInfo?.Position == Positions.Pre_M && + (start == 0 || CodePoint.GetGeneralCategory(substitutionCollection[start - 1].CodePoint) is not UnicodeCategory.NonSpacingMark and not UnicodeCategory.Format)) + { + substitutionCollection.EnableShapingFeature(start, InitTag); + } + + start = end; + end = NextSyllable(substitutionCollection, start, max); + } + } + + /// + /// Builds a lookup table mapping Indic shaping category codes to compact DFA symbol indices. + /// + /// An array mapping category codes to symbol IDs. + private static int[] BuildCategoryToSymbolId() + { + // Get all enum values in declared order (important!) + Categories[] values = Enum.GetValues(); + + // Determine maximum underlying numeric category so we can index safetly + int maxCategoryValue = 0; + foreach (Categories v in values) + { + int val = (int)v; + if (val > maxCategoryValue) + { + maxCategoryValue = val; + } + } + + // Allocate mapping table indexed by Harfbuzz category code + int[] map = new int[maxCategoryValue + 1]; + + // Assign compact DFA symbol indices 0..N-1 in enum order + for (int symbolId = 0; symbolId < values.Length; symbolId++) + { + Categories cat = values[symbolId]; + int categoryCode = (int)cat; // Harfbuzz-style category code + map[categoryCode] = symbolId; // DFA symbol id + } + + return map; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs new file mode 100644 index 0000000..ea095d6 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs @@ -0,0 +1,507 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using System; +using UnicodeTrieGenerator.StateAutomation; +using static SixLabors.Fonts.Unicode.Resources.IndicShapingData; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers { + /// + /// Shaper for the Myanmar script. Handles syllable identification, reordering, + /// and application of Myanmar-specific OpenType features. + /// + internal sealed class MyanmarShaper : DefaultShaper + { + /// The state machine for Myanmar syllable identification. + private static readonly StateMachine StateMachine = + new( + Unicode.Resources.MyanmarShapingData.StateTable, + Unicode.Resources.MyanmarShapingData.AcceptingStates, + Unicode.Resources.MyanmarShapingData.Tags); + + /// Maps Myanmar shaping category codes to compact DFA symbol indices. + private static readonly int[] CategoryToSymbolId = BuildCategoryToSymbolId(); + + /// The 'rphf' (reph forms) feature tag. + private static readonly Tag RphfTag = Tag.Parse("rphf"); + + /// The 'pref' (pre-base forms) feature tag. + private static readonly Tag PrefTag = Tag.Parse("pref"); + + /// The 'blwf' (below-base forms) feature tag. + private static readonly Tag BlwfTag = Tag.Parse("blwf"); + + /// The 'pstf' (post-base forms) feature tag. + private static readonly Tag PstfTag = Tag.Parse("pstf"); + + /// The 'pres' (pre-base substitutions) feature tag. + private static readonly Tag PresTag = Tag.Parse("pres"); + + /// The 'abvs' (above-base substitutions) feature tag. + private static readonly Tag AbvsTag = Tag.Parse("abvs"); + + /// The 'blws' (below-base substitutions) feature tag. + private static readonly Tag BlwsTag = Tag.Parse("blws"); + + /// The 'psts' (post-base substitutions) feature tag. + private static readonly Tag PstsTag = Tag.Parse("psts"); + + /// Dotted circle code point (U+25CC) used as a placeholder base. + private const int DottedCircle = 0x25cc; + + /// The text options. + private readonly TextOptions textOptions; + + /// The font metrics used for glyph lookups. + private readonly FontMetrics fontMetrics; + + /// Whether any broken clusters were detected during syllable setup. + private bool hasBrokenClusters; + + /// + /// Initializes a new instance of the class. + /// + /// The script classification. + /// The text options. + /// The font metrics for glyph lookups. + public MyanmarShaper(ScriptClass script, TextOptions textOptions, FontMetrics fontMetrics) + : base(script, MarkZeroingMode.PreGPos, textOptions) + { + this.textOptions = textOptions; + this.fontMetrics = fontMetrics; + } + + /// + protected override void PlanFeatures(IGlyphShapingCollection collection, int index, int count) + { + this.AddFeature(collection, index, count, LoclTag, preAction: this.SetupSyllables); + this.AddFeature(collection, index, count, CcmpTag); + + this.AddFeature(collection, index, count, RphfTag, preAction: this.InitialReorder); + this.AddFeature(collection, index, count, PrefTag); + this.AddFeature(collection, index, count, BlwfTag); + this.AddFeature(collection, index, count, PstfTag); + + this.AddFeature(collection, index, count, PresTag); + this.AddFeature(collection, index, count, AbvsTag); + this.AddFeature(collection, index, count, BlwsTag); + this.AddFeature(collection, index, count, PstsTag); + } + + /// + /// Identifies Myanmar syllables using the state machine and assigns shaping info to each glyph. + /// + /// The glyph shaping collection. + /// The zero-based start index. + /// The number of elements to process. + private void SetupSyllables(IGlyphShapingCollection collection, int index, int count) + { + if (collection is not GlyphSubstitutionCollection substitutionCollection) + { + return; + } + + this.hasBrokenClusters = false; + + Span values = count <= 64 ? stackalloc int[count] : new int[count]; + + for (int i = index; i < index + count; i++) + { + // Convert HarfBuzz-style Myanmar shaping categories into the compact + // DFA symbol indices used by the generated state machine. + // + // HarfBuzz category codes (C=1, V=2, MR=36, VBlw=21, etc.) are sparse + // and can be larger than the alphabet size of the DFA. Our state + // machine expects its input alphabet to be dense 0..N-1, matching the + // sequential IDs assigned in GenerateMyanmarShapingData. + // + // CategoryToSymbolId[(int)my] performs this mapping, ensuring that + // every codepoint is presented to the DFA using the correct compact + // symbol index. + CodePoint codePoint = substitutionCollection[i].CodePoint; + MyanmarCategories my = (MyanmarCategories)IndicShapingCategory(codePoint); + values[i - index] = CategoryToSymbolId[(int)my]; + } + + int syllable = 0; + int last = 0; + foreach (StateMatch match in StateMachine.Match(values)) + { + if (match.StartIndex > last) + { + ++syllable; + for (int i = last; i < match.StartIndex; i++) + { + GlyphShapingData data = substitutionCollection[i + index]; + data.IndicShapingEngineInfo = new(Categories.X, Positions.End, "non_indic_cluster", syllable); + } + } + + ++syllable; + + // Create shaper info. + for (int i = match.StartIndex; i <= match.EndIndex; i++) + { + GlyphShapingData data = substitutionCollection[i + index]; + CodePoint codePoint = data.CodePoint; + + string syllableType = match.Tags[0]; + + if (syllableType == "broken_cluster") + { + this.hasBrokenClusters = true; + } + + data.IndicShapingEngineInfo = new( + (Categories)IndicShapingCategory(codePoint), + (Positions)IndicShapingPosition(codePoint), + syllableType, + syllable); + } + + last = match.EndIndex + 1; + } + + if (last < count) + { + ++syllable; + for (int i = last; i < count; i++) + { + GlyphShapingData data = substitutionCollection[i + index]; + data.IndicShapingEngineInfo = new(Categories.X, Positions.End, "non_indic_cluster", syllable); + } + } + } + + /// + /// Performs the initial reordering pass for Myanmar consonant syllables, including + /// dotted circle insertion for broken clusters. + /// + /// The glyph shaping collection. + /// The zero-based start index. + /// The number of elements to process. + private void InitialReorder(IGlyphShapingCollection collection, int index, int count) + { + if (collection is not GlyphSubstitutionCollection substitutionCollection) + { + return; + } + + FontMetrics fontMetrics = this.fontMetrics; + int max = index + count; + int start = index; + int end = NextSyllable(substitutionCollection, index, max); + + if (this.hasBrokenClusters) + { + if (fontMetrics.TryGetGlyphId(new(DottedCircle), out ushort circleId)) + { + Span glyphs = stackalloc ushort[2]; + while (start < max) + { + GlyphShapingData data = substitutionCollection[start]; + IndicShapingEngineInfo? dataInfo = data.IndicShapingEngineInfo; + string? type = dataInfo?.SyllableType; + + if (type == "broken_cluster") + { + // Insert after possible Repha. + int i = start; + for (i = start; i < end; i++) + { + if (substitutionCollection[i].IndicShapingEngineInfo?.Category != Categories.Repha) + { + break; + } + } + + GlyphShapingData current = substitutionCollection[i]; + glyphs[0] = current.GlyphId; + glyphs[1] = circleId; + + substitutionCollection.Replace(i, glyphs, KnownFeatureTags.GlyphCompositionDecomposition); + + // Update shaping info for newly inserted data. + GlyphShapingData dotted = substitutionCollection[i + 1]; + dotted.IndicShapingEngineInfo!.Category = Categories.Dotted_Circle; + + end++; + max++; + } + + start = end; + end = NextSyllable(substitutionCollection, start, max); + } + + start = index; + end = NextSyllable(substitutionCollection, index, max); + } + } + + while (start < max) + { + GlyphShapingData data = substitutionCollection[start]; + IndicShapingEngineInfo? dataInfo = data.IndicShapingEngineInfo; + string? type = dataInfo?.SyllableType; + + switch (type) + { + // We already inserted dotted-circles, so just call the consonant_syllable. + case "broken_cluster": + case "consonant_syllable": + ReorderConsonantSyllable(substitutionCollection, start, end); + break; + default: + break; + } + + start = end; + end = NextSyllable(substitutionCollection, start, max); + } + } + + /// + /// Reorders glyphs within a single Myanmar consonant syllable according to the Myanmar shaping spec. + /// + /// The glyph substitution collection. + /// The start index of the syllable. + /// The exclusive end index of the syllable. + private static void ReorderConsonantSyllable(GlyphSubstitutionCollection substitutionCollection, int start, int end) + { + int basePosition = end; + bool hasReph = false; + { + int limit = start; + if (start + 3 <= end && + substitutionCollection[start].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.Ra && + substitutionCollection[start + 1].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.As && + substitutionCollection[start + 2].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.H) + { + limit += 3; + basePosition = start; + hasReph = true; + } + + { + if (!hasReph) + { + basePosition = limit; + } + + for (int i = limit; i < end; i++) + { + if (IsConsonant(substitutionCollection[i])) + { + basePosition = i; + break; + } + } + } + } + + // Reorder + { + int i = start; + for (; i < start + (hasReph ? 3 : 0); i++) + { + substitutionCollection[i].IndicShapingEngineInfo!.Position = Positions.After_Main; + } + + for (; i < basePosition; i++) + { + substitutionCollection[i].IndicShapingEngineInfo!.Position = Positions.Pre_C; + } + + if (i < end) + { + substitutionCollection[i].IndicShapingEngineInfo!.Position = Positions.Base_C; + i++; + } + + Positions pos = Positions.After_Main; + + // The following loop may be ugly, but it implements all of Myanmar reordering! + for (; i < end; i++) + { + GlyphShapingData data = substitutionCollection[i]; + IndicShapingEngineInfo info = data.IndicShapingEngineInfo!; + + // Pre-base reordering + if (info.MyanmarCategory == MyanmarCategories.MR) + { + info.Position = Positions.Pre_C; + continue; + } + + // Left matra + if (info.MyanmarCategory == MyanmarCategories.VPre) + { + info.Position = Positions.Pre_M; + continue; + } + + if (info.MyanmarCategory == MyanmarCategories.VS) + { + info.Position = substitutionCollection[i - 1].IndicShapingEngineInfo!.Position; + continue; + } + + if (pos == Positions.After_Main && info.MyanmarCategory == MyanmarCategories.VBlw) + { + pos = Positions.Below_C; + info.Position = pos; + continue; + } + + if (pos == Positions.Below_C && info.MyanmarCategory == MyanmarCategories.A) + { + info.Position = Positions.Before_Sub; + continue; + } + + if (pos == Positions.Below_C && info.MyanmarCategory == MyanmarCategories.VBlw) + { + info.Position = pos; + continue; + } + + if (pos == Positions.Below_C && info.MyanmarCategory != MyanmarCategories.A) + { + pos = Positions.After_Sub; + info.Position = pos; + continue; + } + + info.Position = pos; + } + } + + substitutionCollection.Sort(start, end, (a, b) => + { + int pa = a.IndicShapingEngineInfo?.Position != null ? (int)a.IndicShapingEngineInfo.Position : 0; + int pb = b.IndicShapingEngineInfo?.Position != null ? (int)b.IndicShapingEngineInfo.Position : 0; + return pa - pb; + }); + + // Flip left-matra sequence. + int firstLeftMatra = end; + int lastLeftMatra = end; + + for (int i = start; i < end; i++) + { + if (substitutionCollection[i].IndicShapingEngineInfo?.Position == Positions.Pre_M) + { + if (firstLeftMatra == end) + { + firstLeftMatra = i; + } + + lastLeftMatra = i; + } + } + + // https://github.com/harfbuzz/harfbuzz/issues/3863 + if (firstLeftMatra < lastLeftMatra) + { + // No need to merge clusters, done already? + substitutionCollection.ReverseRange(firstLeftMatra, lastLeftMatra + 1); + + // Reverse back VS, etc. + int i = firstLeftMatra; + for (int j = i; j <= lastLeftMatra; j++) + { + if (substitutionCollection[j].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.VPre) + { + substitutionCollection.ReverseRange(i, j + 1); + i = j + 1; + } + } + } + } + + /// + /// Determines whether the glyph data represents a Myanmar consonant. + /// + /// The glyph shaping data. + /// if the glyph is a consonant. + private static bool IsConsonant(GlyphShapingData data) + => data.IndicShapingEngineInfo != null && (FlagUnsafe(data.IndicShapingEngineInfo.MyanmarCategory) & MyanmarConsonantFlags) != 0; + + /// + /// Finds the start index of the next syllable in the collection. + /// + /// The glyph substitution collection. + /// The current index. + /// The maximum index bound. + /// The start index of the next syllable. + private static int NextSyllable(GlyphSubstitutionCollection collection, int index, int count) + { + if (index >= count) + { + return index; + } + + int? syllable = collection[index].IndicShapingEngineInfo?.Syllable; + while (++index < count) + { + if (collection[index].IndicShapingEngineInfo?.Syllable != syllable) + { + break; + } + } + + return index; + } + + /// + /// Gets the Indic shaping category for a code point (upper 8 bits of the shaping properties). + /// + /// The code point. + /// The shaping category value. + private static int IndicShapingCategory(CodePoint codePoint) + => UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) >> 8; + + /// + /// Gets the Indic shaping position for a code point (lower 8 bits as a bit flag). + /// + /// The code point. + /// The shaping position as a bit flag. + private static int IndicShapingPosition(CodePoint codePoint) + => 1 << (UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) & 0xFF); + + /// + /// Builds a lookup table mapping Myanmar shaping category codes to compact DFA symbol indices. + /// + /// An array mapping category codes to symbol IDs. + private static int[] BuildCategoryToSymbolId() + { + // Get all enum values in declared order (important!) + MyanmarCategories[] values = Enum.GetValues(); + + // Determine maximum underlying numeric category so we can index safetly + int maxCategoryValue = 0; + foreach (MyanmarCategories v in values) + { + int val = (int)v; + if (val > maxCategoryValue) + { + maxCategoryValue = val; + } + } + + // Allocate mapping table indexed by Harfbuzz category code + int[] map = new int[maxCategoryValue + 1]; + + // Assign compact DFA symbol indices 0..N-1 in enum order + for (int symbolId = 0; symbolId < values.Length; symbolId++) + { + MyanmarCategories cat = values[symbolId]; + int categoryCode = (int)cat; // Harfbuzz-style category code + map[categoryCode] = symbolId; // DFA symbol id + } + + return map; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShaperFactory.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShaperFactory.cs new file mode 100644 index 0000000..caaa9e8 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShaperFactory.cs @@ -0,0 +1,133 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers { + /// + /// Factory for creating the appropriate script shaper based on a given script classification. + /// + internal static class ShaperFactory + { + /// The 'mym2' (Myanmar v2) script tag used to distinguish Myanmar shaper versions. + private static readonly Tag Mym2Tag = Tag.Parse("mym2"); + + /// + /// Creates a shaper based on the given script language. + /// + /// The script language. + /// The unicode script tag found in the font matching the script. + /// The current font metrics. + /// The global text options. + /// A shaper for the given script. + public static BaseShaper Create( + ScriptClass script, + Tag unicodeScriptTag, + FontMetrics fontMetrics, + TextOptions textOptions) + => script switch + { + // Arabic + ScriptClass.Arabic + or ScriptClass.Mongolian + or ScriptClass.Syriac + or ScriptClass.Nko + or ScriptClass.PhagsPa + or ScriptClass.Mandaic + or ScriptClass.Manichaean + or ScriptClass.PsalterPahlavi => new ArabicShaper(script, textOptions), + + // Hebrew + ScriptClass.Hebrew => new HebrewShaper(script, textOptions, fontMetrics, unicodeScriptTag != default), + + // Thai / Lao + ScriptClass.Thai + or ScriptClass.Lao => new ThaiShaper(script, textOptions, fontMetrics, unicodeScriptTag != default), + + // Hangul + ScriptClass.Hangul => new HangulShaper(script, textOptions, fontMetrics), + + // Indic + ScriptClass.Bengali + or ScriptClass.Devanagari + or ScriptClass.Gujarati + or ScriptClass.Gurmukhi + or ScriptClass.Kannada + or ScriptClass.Malayalam + or ScriptClass.Oriya + or ScriptClass.Tamil + or ScriptClass.Telugu + or ScriptClass.Khmer => new IndicShaper(script, unicodeScriptTag, textOptions, fontMetrics), + + // Myanmar + ScriptClass.Myanmar + + // If the designer designed the font for the 'DFLT' script, + // (or we ended up arbitrarily pick 'latn'), use the default shaper. + // Otherwise, use the specific shaper. + // + // If designer designed for 'mymr' tag, also send to default + // shaper. That's tag used from before Myanmar shaping spec + // was developed. The shaping spec uses 'mym2' tag. + => unicodeScriptTag == Mym2Tag + ? new MyanmarShaper(script, textOptions, fontMetrics) + : new DefaultShaper(script, textOptions), + + // Universal + ScriptClass.Balinese + or ScriptClass.Batak + or ScriptClass.Brahmi + or ScriptClass.Buginese + or ScriptClass.Buhid + or ScriptClass.Chakma + or ScriptClass.Cham + or ScriptClass.Duployan + or ScriptClass.EgyptianHieroglyphs + or ScriptClass.Grantha + or ScriptClass.Hanunoo + or ScriptClass.Javanese + or ScriptClass.Kaithi + or ScriptClass.KayahLi + or ScriptClass.Kharoshthi + or ScriptClass.Khojki + or ScriptClass.Khudawadi + or ScriptClass.Lepcha + or ScriptClass.Limbu + or ScriptClass.Mahajani + or ScriptClass.MeeteiMayek + or ScriptClass.Modi + or ScriptClass.PahawhHmong + or ScriptClass.Rejang + or ScriptClass.Saurashtra + or ScriptClass.Sharada + or ScriptClass.Siddham + or ScriptClass.Sinhala + or ScriptClass.Sundanese + or ScriptClass.SylotiNagri + or ScriptClass.Tagalog + or ScriptClass.Tagbanwa + or ScriptClass.TaiLe + or ScriptClass.TaiTham + or ScriptClass.TaiViet + or ScriptClass.Takri + or ScriptClass.Tibetan + or ScriptClass.Tifinagh + or ScriptClass.Tirhuta + or ScriptClass.Kawi + or ScriptClass.NagMundari + or ScriptClass.Garay + or ScriptClass.GurungKhema + or ScriptClass.KiratRai + or ScriptClass.OlOnal + or ScriptClass.Sunuwar + or ScriptClass.Todhri + or ScriptClass.TuluTigalari + or ScriptClass.BeriaErfe + or ScriptClass.Sidetic + or ScriptClass.TaiYo + or ScriptClass.TolongSiki + => new UniversalShaper(script, textOptions, fontMetrics), + _ => new DefaultShaper(script, textOptions), + }; + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs new file mode 100644 index 0000000..9f9d220 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers { + /// + /// An individual shaping stage. + /// Each stage must have a feature tag but can also contain pre and post feature processing operations. + /// + /// + /// For comparison purposes we only care about the feature tag as we want to avoid duplication. + /// + internal readonly struct ShapingStage : IEquatable + { + /// The optional action to invoke before the feature is applied. + private readonly Action? preAction; + + /// The optional action to invoke after the feature is applied. + private readonly Action? postAction; + + /// + /// Initializes a new instance of the struct. + /// + /// The OpenType feature tag for this stage. + /// An optional action to invoke before the feature is applied. + /// An optional action to invoke after the feature is applied. + public ShapingStage(Tag featureTag, Action? preAction, Action? postAction) + { + this.FeatureTag = featureTag; + this.preAction = preAction; + this.postAction = postAction; + } + + /// + /// Gets the OpenType feature tag for this stage. + /// + public Tag FeatureTag { get; } + + /// + /// Invokes the pre-processing action for this shaping stage, if one was provided. + /// + /// The glyph shaping collection. + /// The zero-based index of the first element. + /// The number of elements. + public void PreProcessFeature(IGlyphShapingCollection collection, int index, int count) + => this.preAction?.Invoke(collection, index, count); + + /// + /// Invokes the post-processing action for this shaping stage, if one was provided. + /// + /// The glyph shaping collection. + /// The zero-based index of the first element. + /// The number of elements. + public void PostProcessFeature(IGlyphShapingCollection collection, int index, int count) + => this.postAction?.Invoke(collection, index, count); + + /// + public override bool Equals(object? obj) + => obj is ShapingStage stage && this.Equals(stage); + + /// + public bool Equals(ShapingStage other) => this.FeatureTag.Equals(other.FeatureTag); + + /// + public override int GetHashCode() => HashCode.Combine(this.FeatureTag); + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs new file mode 100644 index 0000000..5d50291 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs @@ -0,0 +1,500 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using System; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers { + /// + /// Thai and Lao shaper. Handles SARA AM decomposition, NIKHAHIT/NIGGAHITA reordering, + /// and PUA-based fallback mark positioning for legacy fonts. + /// Based on HarfBuzz: + /// +#pragma warning disable SA1201 // Nested types are grouped with the static data they define. + internal class ThaiShaper : DefaultShaper + { + /// + /// Above-base state machine start states indexed by . + /// + private static readonly int[] AboveStartState = [0, 1, 0, 0, 3]; + + /// + /// Above-base state machine transitions. Rows are states (T0-T3), columns are (AV, BV, T). + /// + private static readonly StateTransition[,] AboveStateMachine = + { + { new(PuaAction.NOP, 3), new(PuaAction.NOP, 0), new(PuaAction.SD, 3) }, + { new(PuaAction.SL, 2), new(PuaAction.NOP, 1), new(PuaAction.SDL, 2) }, + { new(PuaAction.NOP, 3), new(PuaAction.NOP, 2), new(PuaAction.SL, 3) }, + { new(PuaAction.NOP, 3), new(PuaAction.NOP, 3), new(PuaAction.NOP, 3) }, + }; + + /// + /// Below-base state machine start states indexed by . + /// + private static readonly int[] BelowStartState = [0, 0, 1, 2, 2]; + + /// + /// Below-base state machine transitions. Rows are states (B0-B2), columns are (AV, BV, T). + /// + private static readonly StateTransition[,] BelowStateMachine = + { + { new(PuaAction.NOP, 0), new(PuaAction.NOP, 2), new(PuaAction.NOP, 0) }, + { new(PuaAction.NOP, 1), new(PuaAction.RD, 2), new(PuaAction.NOP, 1) }, + { new(PuaAction.NOP, 2), new(PuaAction.SD, 2), new(PuaAction.NOP, 2) }, + }; + + /// Shift-Down PUA mappings for tone marks and below-vowel marks. + private static readonly PuaMapping[] SdMappings = + [ + new(0x0E48, 0xF70A, 0xF88B), // MAI EK + new(0x0E49, 0xF70B, 0xF88E), // MAI THO + new(0x0E4A, 0xF70C, 0xF891), // MAI TRI + new(0x0E4B, 0xF70D, 0xF894), // MAI CHATTAWA + new(0x0E4C, 0xF70E, 0xF897), // THANTHAKHAT + new(0x0E38, 0xF718, 0xF89B), // SARA U + new(0x0E39, 0xF719, 0xF89C), // SARA UU + new(0x0E3A, 0xF71A, 0xF89D), // PHINTHU + ]; + + /// Shift-Down-Left PUA mappings for tone marks. + private static readonly PuaMapping[] SdlMappings = + [ + new(0x0E48, 0xF705, 0xF88C), // MAI EK + new(0x0E49, 0xF706, 0xF88F), // MAI THO + new(0x0E4A, 0xF707, 0xF892), // MAI TRI + new(0x0E4B, 0xF708, 0xF895), // MAI CHATTAWA + new(0x0E4C, 0xF709, 0xF898), // THANTHAKHAT + ]; + + /// Shift-Left PUA mappings for tone marks and above-vowel marks. + private static readonly PuaMapping[] SlMappings = + [ + new(0x0E48, 0xF713, 0xF88A), // MAI EK + new(0x0E49, 0xF714, 0xF88D), // MAI THO + new(0x0E4A, 0xF715, 0xF890), // MAI TRI + new(0x0E4B, 0xF716, 0xF893), // MAI CHATTAWA + new(0x0E4C, 0xF717, 0xF896), // THANTHAKHAT + new(0x0E31, 0xF710, 0xF884), // MAI HAN-AKAT + new(0x0E34, 0xF701, 0xF885), // SARA I + new(0x0E35, 0xF702, 0xF886), // SARA II + new(0x0E36, 0xF703, 0xF887), // SARA UE + new(0x0E37, 0xF704, 0xF888), // SARA UEE + new(0x0E47, 0xF712, 0xF889), // MAITAIKHU + new(0x0E4D, 0xF711, 0xF899), // NIKHAHIT + ]; + + /// Remove-Descender PUA mappings for consonants with removable descenders. + private static readonly PuaMapping[] RdMappings = + [ + new(0x0E0D, 0xF70F, 0xF89A), // YO YING + new(0x0E10, 0xF700, 0xF89E), // THO THAN + ]; + + /// The font metrics used for glyph lookups and PUA shaping. + private readonly FontMetrics fontMetrics; + + /// Whether the font has GSUB features for Thai/Lao. + private readonly bool hasGsub; + + /// + /// Thai consonant types for the PUA shaping state machines. + /// + private enum ConsonantType + { + /// Normal consonant. + NC, + + /// Ascending consonant (Thai: 0x0E1B, 0x0E1D, 0x0E1F). + AC, + + /// Consonant with removable descender (Thai: 0x0E0D, 0x0E10). + RC, + + /// Consonant with strict descender (Thai: 0x0E0E, 0x0E0F). + DC, + + /// Not a consonant. + NotConsonant + } + + /// + /// Thai mark types for the PUA shaping state machines. + /// + private enum MarkType + { + /// Above-vowel mark. + AV, + + /// Below-vowel mark. + BV, + + /// Tone mark. + T, + + /// Not a mark. + NotMark + } + + /// + /// Actions emitted by the PUA shaping state machines. + /// + private enum PuaAction + { + /// No operation. + NOP, + + /// Shift combining-mark down. + SD, + + /// Shift combining-mark left. + SL, + + /// Shift combining-mark down-left. + SDL, + + /// Remove descender from base consonant. + RD + } + + /// + /// Initializes a new instance of the class. + /// + /// The script classification. + /// The text options. + /// The font metrics for glyph lookups. + /// Whether the font has GSUB features for Thai/Lao. + public ThaiShaper(ScriptClass script, TextOptions textOptions, FontMetrics fontMetrics, bool hasGsub) + : base(script, MarkZeroingMode.PostGpos, textOptions) + { + this.fontMetrics = fontMetrics; + this.hasGsub = hasGsub; + } + + /// + protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) + { + base.AssignFeatures(collection, index, count); + + if (collection is not GlyphSubstitutionCollection substitutionCollection) + { + return; + } + + // Step 1: Always decompose SARA AM -> NIKHAHIT + SARA AA and reorder. + // This is needed even when the font has Thai/Lao GSUB tables. + count = PreprocessSaraAm(substitutionCollection, this.fontMetrics, index, count); + + // Step 2: PUA-based fallback mark positioning. + // Only applied for Thai (not Lao) when the font lacks Thai GSUB features. + if (this.ScriptClass == ScriptClass.Thai && !this.hasGsub) + { + DoThaiPuaShaping(substitutionCollection, this.fontMetrics, index, count); + } + } + + /// + /// Decomposes SARA AM (Thai U+0E33 / Lao U+0EB3) into NIKHAHIT + SARA AA, + /// then reorders NIKHAHIT backward over any above-base marks. + /// + /// This is needed even when the font has Thai/Lao GSUB tables. + /// + /// + /// + /// The glyph substitution collection. + /// The font metrics for glyph lookups. + /// The zero-based start index. + /// The number of elements to process. + /// The updated count after decomposition. + private static int PreprocessSaraAm(GlyphSubstitutionCollection collection, FontMetrics fontMetrics, int index, int count) + { + // Characters of significance: + // + // Thai Lao + // SARA AM: U+0E33 U+0EB3 + // SARA AA: U+0E32 U+0EB2 + // Nikhahit: U+0E4D U+0ECD + // + // When SARA AM is found, decompose into NIKHAHIT + SARA AA, + // then move NIKHAHIT backward past any above-base marks. + // + // Example: <0E14, 0E4B, 0E33> -> <0E14, 0E4D, 0E4B, 0E32> + int end = index + count; + for (int i = index; i < end; i++) + { + GlyphShapingData data = collection[i]; + int codepoint = data.CodePoint.Value; + + if (!IsSaraAm(codepoint)) + { + continue; + } + + int nikhahitCodepoint = NikhahitFromSaraAm(codepoint); + int saraAACodepoint = SaraAAFromSaraAm(codepoint); + + if (!fontMetrics.TryGetGlyphId(new CodePoint(nikhahitCodepoint), out ushort nikhahitId) || + !fontMetrics.TryGetGlyphId(new CodePoint(saraAACodepoint), out ushort saraAAId)) + { + continue; + } + + // Decompose SARA AM into [NIKHAHIT, SARA AA]. + // Replace puts NIKHAHIT at index i, SARA AA at index i+1. + collection.Replace(i, [nikhahitId, saraAAId], KnownFeatureTags.GlyphCompositionDecomposition); + collection[i].CodePoint = new CodePoint(nikhahitCodepoint); + collection[i + 1].CodePoint = new CodePoint(saraAACodepoint); + end++; + + // Move NIKHAHIT backward over any above-base marks. + int target = i; + while (target > index && IsAboveBaseMark(collection[target - 1].CodePoint.Value)) + { + target--; + } + + if (target < i) + { + collection.MoveGlyph(i, target); + } + + // Skip past SARA AA. + i++; + } + + return end - index; + } + + /// + /// Applies PUA-based fallback mark positioning using state machines. + /// Only used for Thai fonts that lack GSUB features. + /// + /// The glyph substitution collection. + /// The font metrics for glyph lookups. + /// The zero-based start index. + /// The number of elements to process. + private static void DoThaiPuaShaping(GlyphSubstitutionCollection collection, FontMetrics fontMetrics, int index, int count) + { + int aboveState = AboveStartState[(int)ConsonantType.NotConsonant]; + int belowState = BelowStartState[(int)ConsonantType.NotConsonant]; + int baseIndex = index; + + int end = index + count; + for (int i = index; i < end; i++) + { + int codepoint = collection[i].CodePoint.Value; + MarkType mt = GetMarkType(codepoint); + + if (mt == MarkType.NotMark) + { + ConsonantType ct = GetConsonantType(codepoint); + aboveState = AboveStartState[(int)ct]; + belowState = BelowStartState[(int)ct]; + baseIndex = i; + continue; + } + + StateTransition aboveEdge = AboveStateMachine[aboveState, (int)mt]; + StateTransition belowEdge = BelowStateMachine[belowState, (int)mt]; + aboveState = aboveEdge.NextState; + belowState = belowEdge.NextState; + + // At least one of the above/below actions is NOP. + PuaAction action = aboveEdge.Action != PuaAction.NOP ? aboveEdge.Action : belowEdge.Action; + + if (action == PuaAction.RD) + { + int baseCp = collection[baseIndex].CodePoint.Value; + int puaCp = ThaiPuaShape(baseCp, action, fontMetrics); + if (puaCp != baseCp && fontMetrics.TryGetGlyphId(new CodePoint(puaCp), out ushort puaId)) + { + collection[baseIndex].CodePoint = new CodePoint(puaCp); + collection[baseIndex].GlyphId = puaId; + } + } + else if (action != PuaAction.NOP) + { + int puaCp = ThaiPuaShape(codepoint, action, fontMetrics); + if (puaCp != codepoint && fontMetrics.TryGetGlyphId(new CodePoint(puaCp), out ushort puaId)) + { + collection[i].CodePoint = new CodePoint(puaCp); + collection[i].GlyphId = puaId; + } + } + } + } + + /// + /// Maps a Thai codepoint to its PUA variant based on the action. + /// Tries Windows PUA first, then Mac PUA. + /// + /// The original Thai codepoint value. + /// The PUA action to apply. + /// The font metrics for glyph lookups. + /// The PUA codepoint if found in the font; otherwise, the original codepoint. + private static int ThaiPuaShape(int codepoint, PuaAction action, FontMetrics fontMetrics) + { + ReadOnlySpan mappings = action switch + { + PuaAction.SD => SdMappings, + PuaAction.SDL => SdlMappings, + PuaAction.SL => SlMappings, + PuaAction.RD => RdMappings, + _ => default + }; + + for (int i = 0; i < mappings.Length; i++) + { + if (mappings[i].Original == codepoint) + { + // Try Windows PUA first. + if (fontMetrics.TryGetGlyphId(new CodePoint(mappings[i].WinPua), out _)) + { + return mappings[i].WinPua; + } + + // Try Mac PUA. + if (fontMetrics.TryGetGlyphId(new CodePoint(mappings[i].MacPua), out _)) + { + return mappings[i].MacPua; + } + + break; + } + } + + return codepoint; + } + + /// + /// Classifies a Thai consonant by its vertical extent. + /// Only works for Thai codepoints (U+0E01..U+0E2E). + /// + /// The codepoint value to classify. + /// The consonant type classification. + private static ConsonantType GetConsonantType(int codepoint) + { + // Ascending consonants (tall right stroke). + if (codepoint is 0x0E1B or 0x0E1D or 0x0E1F) + { + return ConsonantType.AC; + } + + // Consonants with removable descender. + if (codepoint is 0x0E0D or 0x0E10) + { + return ConsonantType.RC; + } + + // Consonants with strict descender. + if (codepoint is 0x0E0E or 0x0E0F) + { + return ConsonantType.DC; + } + + // Normal consonant range. + if (codepoint is >= 0x0E01 and <= 0x0E2E) + { + return ConsonantType.NC; + } + + return ConsonantType.NotConsonant; + } + + /// + /// Classifies a Thai mark by its position relative to the base consonant. + /// Only works for Thai codepoints. + /// + /// The codepoint value to classify. + /// The mark type classification. + private static MarkType GetMarkType(int codepoint) + { + // Above-vowel marks. + if (codepoint is 0x0E31 + or (>= 0x0E34 and <= 0x0E37) or 0x0E47 + or (>= 0x0E4D and <= 0x0E4E)) + { + return MarkType.AV; + } + + // Below-vowel marks. + if (codepoint is >= 0x0E38 and <= 0x0E3A) + { + return MarkType.BV; + } + + // Tone marks. + if (codepoint is >= 0x0E48 and <= 0x0E4C) + { + return MarkType.T; + } + + return MarkType.NotMark; + } + + /// + /// Returns if the codepoint is SARA AM (Thai U+0E33 or Lao U+0EB3). + /// + /// The codepoint value to test. + /// if the codepoint is SARA AM. + private static bool IsSaraAm(int codepoint) + => (codepoint & ~0x0080) == 0x0E33; + + /// + /// Derives NIKHAHIT/NIGGAHITA from SARA AM. Thai: U+0E4D, Lao: U+0ECD. + /// + /// The SARA AM codepoint value. + /// The corresponding NIKHAHIT codepoint value. + private static int NikhahitFromSaraAm(int codepoint) + => codepoint - 0x0E33 + 0x0E4D; + + /// + /// Derives SARA AA from SARA AM. Thai: U+0E32, Lao: U+0EB2. + /// + /// The SARA AM codepoint value. + /// The corresponding SARA AA codepoint value. + private static int SaraAAFromSaraAm(int codepoint) + => codepoint - 1; + + /// + /// Returns if the codepoint is an above-base mark that NIKHAHIT + /// should reorder past during SARA AM decomposition. + /// Uses the (codepoint & ~0x80) trick to handle both Thai and Lao uniformly. + /// + /// The codepoint value to test. + /// if the codepoint is an above-base mark. + private static bool IsAboveBaseMark(int codepoint) + { + int u = codepoint & ~0x0080; + return u is (>= 0x0E34 and <= 0x0E37) or (>= 0x0E47 and <= 0x0E4E) or 0x0E31 + or 0x0E3B; + } + + /// + /// State + action pair for state machine transitions. + /// + private readonly struct StateTransition(PuaAction action, int nextState) + { + /// Gets the PUA action to apply. + public PuaAction Action { get; } = action; + + /// Gets the next state for the state machine. + public int NextState { get; } = nextState; + } + + /// + /// PUA mapping entry: original codepoint, Windows PUA, Mac PUA. + /// + private readonly struct PuaMapping(ushort original, ushort winPua, ushort macPua) + { + /// Gets the original Thai codepoint. + public ushort Original { get; } = original; + + /// Gets the Windows PUA replacement codepoint. + public ushort WinPua { get; } = winPua; + + /// Gets the Mac PUA replacement codepoint. + public ushort MacPua { get; } = macPua; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs new file mode 100644 index 0000000..d7a2c6f --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs @@ -0,0 +1,489 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using SixLabors.Fonts.Unicode.Resources; +using System; +using System.Linq; +using UnicodeTrieGenerator.StateAutomation; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers { + /// + /// This shaper is an implementation of the Universal Shaping Engine, which + /// uses Unicode data to shape a number of scripts without a dedicated shaping engine. + /// . + /// + internal sealed class UniversalShaper : DefaultShaper + { + /// The state machine for Universal Shaping Engine syllable identification. + private static readonly StateMachine StateMachine = + new(UniversalShapingData.StateTable, UniversalShapingData.AcceptingStates, UniversalShapingData.Tags); + + /// The 'rphf' (reph forms) feature tag. + private static readonly Tag RphfTag = Tag.Parse("rphf"); + + /// The 'nukt' (nukta forms) feature tag. + private static readonly Tag NuktTag = Tag.Parse("nukt"); + + /// The 'akhn' (akhands) feature tag. + private static readonly Tag AkhnTag = Tag.Parse("akhn"); + + /// The 'pref' (pre-base forms) feature tag. + private static readonly Tag PrefTag = Tag.Parse("pref"); + + /// The 'rkrf' (rakar forms) feature tag. + private static readonly Tag RkrfTag = Tag.Parse("rkrf"); + + /// The 'abvf' (above-base forms) feature tag. + private static readonly Tag AbvfTag = Tag.Parse("abvf"); + + /// The 'blwf' (below-base forms) feature tag. + private static readonly Tag BlwfTag = Tag.Parse("blwf"); + + /// The 'half' (half forms) feature tag. + private static readonly Tag HalfTag = Tag.Parse("half"); + + /// The 'pstf' (post-base forms) feature tag. + private static readonly Tag PstfTag = Tag.Parse("pstf"); + + /// The 'vatu' (vattu variants) feature tag. + private static readonly Tag VatuTag = Tag.Parse("vatu"); + + /// The 'cjct' (conjunct forms) feature tag. + private static readonly Tag CjctTag = Tag.Parse("cjct"); + + /// The 'abvs' (above-base substitutions) feature tag. + private static readonly Tag AbvsTag = Tag.Parse("abvs"); + + /// The 'blws' (below-base substitutions) feature tag. + private static readonly Tag BlwsTag = Tag.Parse("blws"); + + /// The 'pres' (pre-base substitutions) feature tag. + private static readonly Tag PresTag = Tag.Parse("pres"); + + /// The 'psts' (post-base substitutions) feature tag. + private static readonly Tag PstsTag = Tag.Parse("psts"); + + /// The 'dist' (distances) feature tag. + private static readonly Tag DistTag = Tag.Parse("dist"); + + /// The 'abvm' (above-base mark positioning) feature tag. + private static readonly Tag AbvmTag = Tag.Parse("abvm"); + + /// The 'blwm' (below-base mark positioning) feature tag. + private static readonly Tag BlwmTag = Tag.Parse("blwm"); + + /// Dotted circle code point (U+25CC) used as a placeholder base. + private const int DottedCircle = 0x25cc; + + /// The font metrics used for glyph lookups. + private readonly FontMetrics fontMetrics; + + /// Whether any broken clusters were detected during syllable setup. + private bool hasBrokenClusters; + + /// + /// Initializes a new instance of the class. + /// + /// The script classification. + /// The text options. + /// The font metrics for glyph lookups. + public UniversalShaper(ScriptClass script, TextOptions textOptions, FontMetrics fontMetrics) + : base(script, MarkZeroingMode.PreGPos, textOptions) + => this.fontMetrics = fontMetrics; + + /// + protected override void PlanFeatures(IGlyphShapingCollection collection, int index, int count) + { + // Default glyph pre-processing group + this.AddFeature(collection, index, count, LoclTag, preAction: this.SetupSyllables); + this.AddFeature(collection, index, count, CcmpTag); + this.AddFeature(collection, index, count, NuktTag); + this.AddFeature(collection, index, count, AkhnTag); + + // Reordering group + this.AddFeature(collection, index, count, RphfTag, true, ClearSubstitutionFlags, RecordRhpf); + this.AddFeature(collection, index, count, PrefTag, true, ClearSubstitutionFlags, RecordPref); + + // Orthographic unit shaping group + this.AddFeature(collection, index, count, RkrfTag); + this.AddFeature(collection, index, count, AbvfTag); + this.AddFeature(collection, index, count, BlwfTag); + this.AddFeature(collection, index, count, HalfTag); + this.AddFeature(collection, index, count, PstfTag); + this.AddFeature(collection, index, count, VatuTag); + this.AddFeature(collection, index, count, CjctTag, postAction: this.Reorder); + + // Standard topographic presentation and positional feature application + this.AddFeature(collection, index, count, AbvsTag); + this.AddFeature(collection, index, count, BlwsTag); + this.AddFeature(collection, index, count, PresTag); + this.AddFeature(collection, index, count, PstsTag); + this.AddFeature(collection, index, count, DistTag); + this.AddFeature(collection, index, count, AbvmTag); + this.AddFeature(collection, index, count, BlwmTag); + } + + /// + protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) + => this.DecomposeSplitVowels(collection, index, count); + + /// + /// Decomposes split vowels into their constituent parts if supported by the font. + /// + /// The glyph shaping collection. + /// The zero-based start index. + /// The number of elements to process. + private void DecomposeSplitVowels(IGlyphShapingCollection collection, int index, int count) + { + if (collection is not GlyphSubstitutionCollection substitutionCollection) + { + return; + } + + FontMetrics fontMetrics = this.fontMetrics; + Span buffer = stackalloc ushort[16]; + int end = index + count; + for (int i = end - 1; i >= index; i--) + { + GlyphShapingData data = substitutionCollection[i]; + if (UniversalShapingData.Decompositions.TryGetValue(data.CodePoint.Value, out int[]? decompositions) && decompositions != null) + { + Span ids = buffer[..decompositions.Length]; + bool shouldDecompose = true; + for (int j = 0; j < decompositions.Length; j++) + { + if (!fontMetrics.TryGetGlyphId(new CodePoint(decompositions[j]), out ushort id)) + { + shouldDecompose = false; + break; + } + + ids[j] = id; + } + + if (shouldDecompose) + { + substitutionCollection.Replace(i, ids, KnownFeatureTags.GlyphCompositionDecomposition); + for (int j = 0; j < decompositions.Length; j++) + { + substitutionCollection[i + j].CodePoint = new(decompositions[j]); + } + } + } + } + } + + /// + /// Identifies syllables using the Universal Shaping Engine state machine and assigns shaping info to each glyph. + /// + /// The glyph shaping collection. + /// The zero-based start index. + /// The number of elements to process. + private void SetupSyllables(IGlyphShapingCollection collection, int index, int count) + { + if (collection is not GlyphSubstitutionCollection substitutionCollection) + { + return; + } + + this.hasBrokenClusters = false; + + Span values = count <= 64 ? stackalloc int[count] : new int[count]; + for (int i = index; i < index + count; i++) + { + CodePoint codePoint = substitutionCollection[i].CodePoint; + values[i - index] = UnicodeData.GetUniversalShapingSymbolCount((uint)codePoint.Value); + } + + int syllable = 0; + foreach (StateMatch match in StateMachine.Match(values)) + { + ++syllable; + + // Create shaper info + for (int i = match.StartIndex; i <= match.EndIndex; i++) + { + GlyphShapingData data = substitutionCollection[i + index]; + CodePoint codePoint = data.CodePoint; + string category = UniversalShapingData.Categories[UnicodeData.GetUniversalShapingSymbolCount((uint)codePoint.Value)]; + + string syllableType = match.Tags[0]; + + if (syllableType == "broken_cluster") + { + this.hasBrokenClusters = true; + } + + data.UniversalShapingEngineInfo = new(category, syllableType, syllable); + } + + // Assign rphf feature + int limit = substitutionCollection[match.StartIndex + index].UniversalShapingEngineInfo!.Category == "R" + ? 1 + : Math.Min(3, match.EndIndex - match.StartIndex); + + for (int i = match.StartIndex; i < match.StartIndex + limit; i++) + { + substitutionCollection.AddShapingFeature(i + index, new TagEntry(RcltTag, true)); + } + } + } + + /// + /// Clears substitution flags on all glyphs in the range, preparing for the next substitution pass. + /// + /// The glyph shaping collection. + /// The zero-based start index. + /// The number of elements to process. + private static void ClearSubstitutionFlags(IGlyphShapingCollection collection, int index, int count) + { + if (collection is not GlyphSubstitutionCollection substitutionCollection) + { + return; + } + + int end = index + count; + for (int i = index; i < end; i++) + { + GlyphShapingData data = substitutionCollection[i]; + data.IsSubstituted = false; + } + } + + /// + /// Records glyphs substituted by the 'rphf' feature by marking their category as repha ("R"). + /// + /// The glyph shaping collection. + /// The zero-based start index. + /// The number of elements to process. + private static void RecordRhpf(IGlyphShapingCollection collection, int index, int count) + { + if (collection is not GlyphSubstitutionCollection substitutionCollection) + { + return; + } + + int end = index + count; + for (int i = index; i < end; i++) + { + GlyphShapingData data = substitutionCollection[i]; + if (data.IsSubstituted && data.Features.Any(x => x.Tag == RphfTag)) + { + // Mark a substituted repha. + if (data.UniversalShapingEngineInfo != null) + { + data.UniversalShapingEngineInfo.Category = "R"; + } + } + } + } + + /// + /// Records glyphs substituted by the 'pref' feature by marking their category as pre-base vowel ("VPre"). + /// + /// The glyph shaping collection. + /// The zero-based start index. + /// The number of elements to process. + private static void RecordPref(IGlyphShapingCollection collection, int index, int count) + { + if (collection is not GlyphSubstitutionCollection substitutionCollection) + { + return; + } + + int end = index + count; + for (int i = index; i < end; i++) + { + GlyphShapingData data = substitutionCollection[i]; + if (data.IsSubstituted) + { + // Mark a substituted pref as VPre, as they behave the same way. + if (data.UniversalShapingEngineInfo != null) + { + data.UniversalShapingEngineInfo.Category = "VPre"; + } + } + } + } + + /// + /// Reorders glyphs within syllables, handling repha movement, pre-base vowel movement, + /// and dotted circle insertion for broken clusters. + /// + /// The glyph shaping collection. + /// The zero-based start index. + /// The number of elements to process. + private void Reorder(IGlyphShapingCollection collection, int index, int count) + { + if (collection is not GlyphSubstitutionCollection substitutionCollection) + { + return; + } + + FontMetrics fontMetrics = this.fontMetrics; + int max = index + count; + int start = index; + int end = NextSyllable(substitutionCollection, index, max); + + if (this.hasBrokenClusters) + { + if (fontMetrics.TryGetGlyphId(new(DottedCircle), out ushort circleId)) + { + Span glyphs = stackalloc ushort[2]; + while (start < max) + { + GlyphShapingData data = substitutionCollection[start]; + UniversalShapingEngineInfo? info = data.UniversalShapingEngineInfo; + string? type = info?.SyllableType; + + if (type == "broken_cluster") + { + // Insert after possible Repha. + int i = start; + for (i = start; i < end; i++) + { + if (substitutionCollection[i].UniversalShapingEngineInfo?.Category != "R") + { + break; + } + } + + GlyphShapingData current = substitutionCollection[i]; + UniversalShapingEngineInfo currentInfo = current.UniversalShapingEngineInfo!; + glyphs[0] = current.GlyphId; + glyphs[1] = circleId; + + substitutionCollection.Replace(i, glyphs, KnownFeatureTags.GlyphCompositionDecomposition); + + // Update shaping info for newly inserted data. + GlyphShapingData dotted = substitutionCollection[i + 1]; + dotted.UniversalShapingEngineInfo!.Category = "B"; + dotted.UniversalShapingEngineInfo.SyllableType = currentInfo.SyllableType; + dotted.UniversalShapingEngineInfo.Syllable = currentInfo.Syllable; + + end++; + max++; + } + + start = end; + end = NextSyllable(substitutionCollection, start, max); + } + + start = index; + end = NextSyllable(substitutionCollection, index, max); + } + } + + while (start < max) + { + GlyphShapingData data = substitutionCollection[start]; + UniversalShapingEngineInfo? info = data.UniversalShapingEngineInfo; + string? type = info?.SyllableType; + + // Only a few syllable types need reordering. + if (type is not "virama_terminated_cluster" and not "standard_cluster" and not "broken_cluster") + { + // TODO: Check this. Harfbuzz seems to test more categories and returns. + goto Increment; + } + + // Move things forward + if (info?.Category == "R" && end - start > 1) + { + // Got a repha. Reorder it to after first base, before first halant. + for (int i = start + 1; i < end; i++) + { + GlyphShapingData current = substitutionCollection[i]; + info = current.UniversalShapingEngineInfo; + if (IsBase(info) || IsHalant(current)) + { + // If we hit a halant, move before it; otherwise it's a base: move to it's + // place, and shift things in between backward. + if (IsHalant(current)) + { + i--; + } + + substitutionCollection.MoveGlyph(start, i); + break; + } + } + } + + // Move things back + for (int i = start, j = start; i < end; i++) + { + GlyphShapingData current = substitutionCollection[i]; + info = current.UniversalShapingEngineInfo; + + if (IsBase(info) || IsHalant(current)) + { + // If we hit a halant, move after it; otherwise move to the beginning, and + // shift things in between forward. + if (IsHalant(current)) + { + j = i + 1; + } + else + { + j = i; + } + } + else if ((info?.Category == "VPre" || info?.Category == "VMPre") + && current.LigatureComponent <= 0 // Only move the first component of a MultipleSubst + && j < i) + { + substitutionCollection.MoveGlyph(i, j); + } + } + + Increment: + start = end; + end = NextSyllable(substitutionCollection, start, max); + } + } + + /// + /// Finds the start index of the next syllable in the collection. + /// + /// The glyph substitution collection. + /// The current index. + /// The maximum index bound. + /// The start index of the next syllable. + private static int NextSyllable(GlyphSubstitutionCollection collection, int index, int count) + { + if (index >= count) + { + return index; + } + + int? syllable = collection[index].UniversalShapingEngineInfo?.Syllable; + while (++index < count) + { + if (collection[index].UniversalShapingEngineInfo?.Syllable != syllable) + { + break; + } + } + + return index; + } + + /// + /// Determines whether the glyph is a halant, halant-like, or invisible stacker character. + /// + /// The glyph shaping data. + /// if the glyph is a halant or equivalent. + private static bool IsHalant(GlyphShapingData data) + => (data.UniversalShapingEngineInfo?.Category is "H" or "HVM" or "IS") && !data.IsLigated; + + /// + /// Determines whether the shaping info represents a base consonant or generic base. + /// + /// The universal shaping engine info. + /// if the glyph is a base. + private static bool IsBase(UniversalShapingEngineInfo? info) + => info?.Category is "B" or "GB"; + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs new file mode 100644 index 0000000..95c63ab --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs @@ -0,0 +1,155 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// An iterator over a glyph shaping collection that respects OpenType lookup flags, + /// skipping glyphs that should be ignored (marks, base glyphs, ligatures) based on the flags. + /// + internal struct SkippingGlyphIterator + { + private readonly FontMetrics fontMetrics; + private bool ignoreMarks; + private bool ignoreBaseGlyphs; + private bool ignoreLigatures; + private ushort markAttachmentType; + private bool useMarkFilteringSet; + private ushort markFilteringSet; + + /// + /// Initializes a new instance of the struct. + /// + /// The font metrics for glyph class lookups. + /// The glyph shaping collection to iterate over. + /// The starting index in the collection. + /// The lookup flags that control which glyphs to skip. + /// The mark filtering set index, used when is set. + public SkippingGlyphIterator( + FontMetrics fontMetrics, + IGlyphShapingCollection collection, + int index, + LookupFlags lookupFlags, + ushort markFilteringSet) + { + this.fontMetrics = fontMetrics; + this.Collection = collection; + this.Index = index; + this.ignoreMarks = (lookupFlags & LookupFlags.IgnoreMarks) != 0; + this.ignoreBaseGlyphs = (lookupFlags & LookupFlags.IgnoreBaseGlyphs) != 0; + this.ignoreLigatures = (lookupFlags & LookupFlags.IgnoreLigatures) != 0; + this.markAttachmentType = (ushort)((int)(lookupFlags & LookupFlags.MarkAttachmentTypeMask) >> 8); + this.useMarkFilteringSet = (lookupFlags & LookupFlags.UseMarkFilteringSet) != 0; + this.markFilteringSet = markFilteringSet; + } + + /// + /// Gets the glyph shaping collection being iterated. + /// + public IGlyphShapingCollection Collection { get; } + + /// + /// Gets or sets the current index in the collection. + /// + public int Index { get; set; } + + /// + /// Advances to the next non-skipped glyph in the forward direction. + /// + /// The new index after advancing. + public int Next() + { + this.Move(1); + return this.Index; + } + + /// + /// Advances to the next non-skipped glyph in the backward direction. + /// + /// The new index after moving backward. + public int Prev() + { + this.Move(-1); + return this.Index; + } + + /// + /// Moves the iterator by the specified number of non-skipped glyphs. A negative count moves backward. + /// + /// The number of positions to move. Negative values move backward. + /// The new index after incrementing. + public int Increment(int count = 1) + { + int direction = count < 0 ? -1 : 1; + count = Math.Abs(count); + while (count-- > 0) + { + this.Move(direction); + } + + return this.Index; + } + + /// + /// Resets the iterator to a new index and lookup flags. + /// + /// The new starting index. + /// The new lookup flags. + /// The new mark filtering set index. + public void Reset(int index, LookupFlags lookupFlags, ushort markFilteringSet) + { + this.Index = index; + this.ignoreMarks = (lookupFlags & LookupFlags.IgnoreMarks) != 0; + this.ignoreBaseGlyphs = (lookupFlags & LookupFlags.IgnoreBaseGlyphs) != 0; + this.ignoreLigatures = (lookupFlags & LookupFlags.IgnoreLigatures) != 0; + this.markAttachmentType = (ushort)((int)(lookupFlags & LookupFlags.MarkAttachmentTypeMask) >> 8); + this.useMarkFilteringSet = (lookupFlags & LookupFlags.UseMarkFilteringSet) != 0; + this.markFilteringSet = markFilteringSet; + } + + /// + /// Moves the iterator one step in the given direction, skipping glyphs that should be ignored. + /// + /// The direction to move: 1 for forward, -1 for backward. + private void Move(int direction) + { + this.Index += direction; + while (this.Index >= 0 && this.Index < this.Collection.Count) + { + if (!this.ShouldIgnore(this.Index)) + { + break; + } + + this.Index += direction; + } + } + + /// + /// Determines whether the glyph at the given index should be ignored based on the current lookup flags. + /// + /// The index of the glyph to check. + /// if the glyph should be skipped; otherwise, . + private readonly bool ShouldIgnore(int index) + { + GlyphShapingData data = this.Collection[index]; + GlyphShapingClass shapingClass = AdvancedTypographicUtils.GetGlyphShapingClass(this.fontMetrics, data.GlyphId, data); + + if (this.useMarkFilteringSet && shapingClass.IsMark) + { + // Skip marks not in the lookup's MarkFilteringSet. + // This requires GDEF MarkGlyphSetsDef support. + if (!AdvancedTypographicUtils.IsInMarkFilteringSet(this.fontMetrics, this.markFilteringSet, data.GlyphId)) + { + return true; + } + } + + return (this.ignoreMarks && shapingClass.IsMark) || + (this.ignoreBaseGlyphs && shapingClass.IsBase) || + (this.ignoreLigatures && shapingClass.IsLigature) || + (this.markAttachmentType > 0 && shapingClass.IsMark && shapingClass.MarkAttachmentType != this.markAttachmentType); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/TableLoadingUtils.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/TableLoadingUtils.cs new file mode 100644 index 0000000..7a37fe2 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/TableLoadingUtils.cs @@ -0,0 +1,327 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// Provides shared utility methods for loading GPOS and GSUB lookup subtables. + /// + internal static class TableLoadingUtils + { + /// + /// Loads Sequence Context Format 1 (simple glyph contexts) data. + /// + /// The big endian binary reader. + /// Offset from the beginning of the subtable. + /// When this method returns, contains the loaded coverage table. + /// The array of sequence rule set tables. + internal static SequenceRuleSetTable[] LoadSequenceContextFormat1(BigEndianBinaryReader reader, long offset, out CoverageTable coverageTable) + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#seqctxt1 + // SequenceContextFormat1 + // +----------+------------------------------------+---------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+====================================+===============================================================+ + // | uint16 | format | Format identifier: format = 1 | + // +----------+------------------------------------+---------------------------------------------------------------+ + // | Offset16 | coverageOffset | Offset to Coverage table, from beginning of | + // | | | SequenceContextFormat1 table. | + // +----------+------------------------------------+---------------------------------------------------------------+ + // | uint16 | seqRuleSetCount | Number of SequenceRuleSet tables. | + // +----------+------------------------------------+---------------------------------------------------------------+ + // | Offset16 | seqRuleSetOffsets[seqRuleSetCount] | Array of offsets to SequenceRuleSet tables, from beginning of | + // | | | SequenceContextFormat1 table (offsets may be NULL). | + // +----------+------------------------------------+---------------------------------------------------------------+ + ushort coverageOffset = reader.ReadOffset16(); + ushort seqRuleSetCount = reader.ReadUInt16(); + + using Buffer seqRuleSetOffsetsBuffer = new(seqRuleSetCount); + Span seqRuleSetOffsets = seqRuleSetOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(seqRuleSetOffsets); + + var seqRuleSets = new SequenceRuleSetTable[seqRuleSetCount]; + + for (int i = 0; i < seqRuleSets.Length; i++) + { + seqRuleSets[i] = SequenceRuleSetTable.Load(reader, offset + seqRuleSetOffsets[i]); + } + + coverageTable = CoverageTable.Load(reader, offset + coverageOffset); + return seqRuleSets; + } + + /// + /// Loads Sequence Context Format 2 (class-based glyph contexts) data. + /// + /// The big endian binary reader. + /// Offset from the beginning of the subtable. + /// When this method returns, contains the loaded class definition table. + /// When this method returns, contains the array of class sequence rule set tables. + /// The coverage table. + internal static CoverageTable LoadSequenceContextFormat2(BigEndianBinaryReader reader, long offset, out ClassDefinitionTable classDefTable, out ClassSequenceRuleSetTable[] classSeqRuleSets) + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#sequence-context-format-2-class-based-glyph-contexts + // Context Positioning Subtable Format 2: Class-based Glyph Contexts. + // +----------+----------------------------------------------+--------------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+==============================================+====================================================================+ + // | uint16 | format | Format identifier: format = 2 | + // +----------+----------------------------------------------+--------------------------------------------------------------------+ + // | Offset16 | coverageOffset | Offset to Coverage table, from beginning of | + // | | | SequenceContextFormat2 table. | + // +----------+----------------------------------------------+--------------------------------------------------------------------+ + // | Offset16 | classDefOffset | Offset to ClassDef table, from beginning of | + // | | | SequenceContextFormat2 table. | + // +----------+----------------------------------------------+--------------------------------------------------------------------+ + // | uint16 | classSeqRuleSetCount | Number of ClassSequenceRuleSet tables. | + // +----------+----------------------------------------------+--------------------------------------------------------------------+ + // | Offset16 | classSeqRuleSetOffsets[classSeqRuleSetCount] | Array of offsets to ClassSequenceRuleSet tables, from beginning of | + // | | | SequenceContextFormat2 table (may be NULL). | + // +----------+----------------------------------------------+--------------------------------------------------------------------+ + ushort coverageOffset = reader.ReadOffset16(); + ushort classDefOffset = reader.ReadOffset16(); + ushort classSeqRuleSetCount = reader.ReadUInt16(); + + using Buffer classSeqRuleSetOffsetsBuffer = new(classSeqRuleSetCount); + Span classSeqRuleSetOffsets = classSeqRuleSetOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(classSeqRuleSetOffsets); + + var coverageTable = CoverageTable.Load(reader, offset + coverageOffset); + classDefTable = ClassDefinitionTable.Load(reader, offset + classDefOffset); + + classSeqRuleSets = new ClassSequenceRuleSetTable[classSeqRuleSetCount]; + for (int i = 0; i < classSeqRuleSets.Length; i++) + { + ushort ruleSetOffset = classSeqRuleSetOffsets[i]; + if (ruleSetOffset > 0) + { + classSeqRuleSets[i] = ClassSequenceRuleSetTable.Load(reader, offset + classSeqRuleSetOffsets[i]); + } + } + + return coverageTable; + } + + /// + /// Loads Sequence Context Format 3 (coverage-based glyph contexts) data. + /// + /// The big endian binary reader. + /// Offset from the beginning of the subtable. + /// When this method returns, contains the array of coverage tables for the input sequence. + /// The array of sequence lookup records. + internal static SequenceLookupRecord[] LoadSequenceContextFormat3(BigEndianBinaryReader reader, long offset, out CoverageTable[] coverageTables) + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#sequence-context-format-3-coverage-based-glyph-contexts + // SequenceContextFormat3 + // +----------------------+----------------------------------+-------------------------------------------+ + // | Type | Name | Description | + // +======================+==================================+===========================================+ + // | uint16 | format | Format identifier: format = 3 | + // +----------------------+----------------------------------+-------------------------------------------+ + // | uint16 | glyphCount | Number of glyphs in the input sequence | + // +----------------------+----------------------------------+-------------------------------------------+ + // | uint16 | seqLookupCount | Number of SequenceLookupRecords | + // +----------------------+----------------------------------+-------------------------------------------+ + // | Offset16 | coverageOffsets[glyphCount] | Array of offsets to Coverage tables, from | + // | | | beginning of SequenceContextFormat3 | + // | | | subtable | + // +----------------------+----------------------------------+-------------------------------------------+ + // | SequenceLookupRecord | seqLookupRecords[seqLookupCount] | Array of SequenceLookupRecords | + // +----------------------+----------------------------------+-------------------------------------------+ + ushort glyphCount = reader.ReadUInt16(); + ushort seqLookupCount = reader.ReadUInt16(); + ushort[] coverageOffsets = reader.ReadUInt16Array(glyphCount); + SequenceLookupRecord[] seqLookupRecords = SequenceLookupRecord.LoadArray(reader, seqLookupCount); + + coverageTables = new CoverageTable[glyphCount]; + for (int i = 0; i < coverageTables.Length; i++) + { + coverageTables[i] = CoverageTable.Load(reader, offset + coverageOffsets[i]); + } + + return seqLookupRecords; + } + + /// + /// Loads Chained Sequence Context Format 1 (simple glyph contexts) data. + /// + /// The big endian binary reader. + /// Offset from the beginning of the subtable. + /// When this method returns, contains the loaded coverage table. + /// The array of chained sequence rule set tables. + internal static ChainedSequenceRuleSetTable[] LoadChainedSequenceContextFormat1(BigEndianBinaryReader reader, long offset, out CoverageTable coverageTable) + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#chained-sequence-context-format-1-simple-glyph-contexts + // ChainedSequenceContextFormat1 + // +----------+--------------------------------------------------+------------------------------------------+ + // | Type | Name | Description | + // +==========+==================================================+==========================================+ + // | uint16 | format | Format identifier: format = 1 | + // +----------+--------------------------------------------------+------------------------------------------+ + // | Offset16 | coverageOffset | Offset to Coverage table, from beginning | + // | | | of ChainSequenceContextFormat1 table | + // +----------+--------------------------------------------------+------------------------------------------+ + // | uint16 | chainedSeqRuleSetCount | Number of ChainedSequenceRuleSet tables | + // +----------+--------------------------------------------------+------------------------------------------+ + // | Offset16 | chainedSeqRuleSetOffsets[chainedSeqRuleSetCount] | Array of offsets to ChainedSeqRuleSet | + // | | | tables, from beginning of | + // | | | ChainedSequenceContextFormat1 table | + // | | | (may be NULL) | + // +----------+--------------------------------------------------+------------------------------------------+ + ushort coverageOffset = reader.ReadOffset16(); + ushort chainedSeqRuleSetCount = reader.ReadUInt16(); + + using Buffer chainedSeqRuleSetOffsetsBuffer = new(chainedSeqRuleSetCount); + Span chainedSeqRuleSetOffsets = chainedSeqRuleSetOffsetsBuffer.GetSpan(); + reader.ReadUInt16Array(chainedSeqRuleSetOffsets); + + var seqRuleSets = new ChainedSequenceRuleSetTable[chainedSeqRuleSetCount]; + + for (int i = 0; i < seqRuleSets.Length; i++) + { + if (chainedSeqRuleSetOffsets[i] > 0) + { + seqRuleSets[i] = ChainedSequenceRuleSetTable.Load(reader, offset + chainedSeqRuleSetOffsets[i]); + } + } + + coverageTable = CoverageTable.Load(reader, offset + coverageOffset); + return seqRuleSets; + } + + /// + /// Loads Chained Sequence Context Format 2 (class-based glyph contexts) data. + /// + /// The big endian binary reader. + /// Offset from the beginning of the subtable. + /// When this method returns, contains the loaded coverage table. + /// When this method returns, contains the backtrack class definition table. + /// When this method returns, contains the input class definition table. + /// When this method returns, contains the lookahead class definition table. + /// The array of chained class sequence rule set tables. + internal static ChainedClassSequenceRuleSetTable[] LoadChainedSequenceContextFormat2( + BigEndianBinaryReader reader, + long offset, + out CoverageTable coverageTable, + out ClassDefinitionTable backtrackClassDefTable, + out ClassDefinitionTable inputClassDefTable, + out ClassDefinitionTable lookaheadClassDefTable) + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#chained-sequence-context-format-2-class-based-glyph-contexts + // ChainedSequenceContextFormat2 + // +----------+------------------------------------------------------------+---------------------------------------------------------------------+ + // | Type | Name | Description | + // +==========+============================================================+=====================================================================+ + // | uint16 | format | Format identifier: format = 2 | + // +----------+------------------------------------------------------------+---------------------------------------------------------------------+ + // | Offset16 | coverageOffset | Offset to Coverage table, from beginning | + // | | | of ChainedSequenceContextFormat2 table | + // +----------+------------------------------------------------------------+---------------------------------------------------------------------+ + // | Offset16 | backtrackClassDefOffset | Offset to ClassDef table containing | + // | | | backtrack sequence context, from | + // | | | beginning of ChainedSequenceContextFormat2 table | + // +----------+------------------------------------------------------------+---------------------------------------------------------------------+ + // | Offset16 | inputClassDefOffset | Offset to ClassDef table containing input | + // | | | sequence context, from beginning of | + // | | | ChainedSequenceContextFormat2 table | + // +----------+------------------------------------------------------------+---------------------------------------------------------------------+ + // | Offset16 | lookaheadClassDefOffset | Offset to ClassDef table containing | + // | | | lookahead sequence context, from | + // | | | beginning of ChainedSequenceContextFormat2 table | + // +----------+------------------------------------------------------------+---------------------------------------------------------------------+ + // | uint16 | chainedClassSeqRuleSetCount | Number of ChainedClassSequenceRuleSet tables | + // +----------+------------------------------------------------------------+---------------------------------------------------------------------+ + // | Offset16 | chainedClassSeqRuleSetOffsets[chainedClassSeqRuleSetCount] | Array of offsets to ChainedClassSequenceRuleSet tables, | + // | | | from beginning of ChainedSequenceContextFormat2 table (may be NULL) | + // +----------+------------------------------------------------------------+---------------------------------------------------------------------+ + ushort coverageOffset = reader.ReadOffset16(); + ushort backtrackClassDefOffset = reader.ReadOffset16(); + ushort inputClassDefOffset = reader.ReadOffset16(); + ushort lookaheadClassDefOffset = reader.ReadOffset16(); + ushort chainedClassSeqRuleSetCount = reader.ReadUInt16(); + ChainedClassSequenceRuleSetTable[] seqRuleSets = Array.Empty(); + if (chainedClassSeqRuleSetCount != 0) + { + ushort[] chainedClassSeqRuleSetOffsets = new ushort[chainedClassSeqRuleSetCount]; + for (int i = 0; i < chainedClassSeqRuleSetCount; i++) + { + chainedClassSeqRuleSetOffsets[i] = reader.ReadOffset16(); + } + + seqRuleSets = new ChainedClassSequenceRuleSetTable[chainedClassSeqRuleSetCount]; + for (int i = 0; i < seqRuleSets.Length; i++) + { + if (chainedClassSeqRuleSetOffsets[i] > 0) + { + seqRuleSets[i] = + ChainedClassSequenceRuleSetTable.Load(reader, offset + chainedClassSeqRuleSetOffsets[i]); + } + } + } + + coverageTable = CoverageTable.Load(reader, offset + coverageOffset); + backtrackClassDefTable = ClassDefinitionTable.Load(reader, offset + backtrackClassDefOffset); + inputClassDefTable = ClassDefinitionTable.Load(reader, offset + inputClassDefOffset); + lookaheadClassDefTable = ClassDefinitionTable.Load(reader, offset + lookaheadClassDefOffset); + return seqRuleSets; + } + + /// + /// Loads Chained Sequence Context Format 3 (coverage-based glyph contexts) data. + /// + /// The big endian binary reader. + /// Offset from the beginning of the subtable. + /// When this method returns, contains the array of backtrack coverage tables. + /// When this method returns, contains the array of input coverage tables. + /// When this method returns, contains the array of lookahead coverage tables. + /// The array of sequence lookup records. + internal static SequenceLookupRecord[] LoadChainedSequenceContextFormat3( + BigEndianBinaryReader reader, + long offset, + out CoverageTable[] backtrackCoverageTables, + out CoverageTable[] inputCoverageTables, + out CoverageTable[] lookaheadCoverageTables) + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#chseqctxt3 + // ChainedSequenceContextFormat3 1 + // +----------------------+-----------------------------------------------+----------------------------------------------------------------+ + // | Type | Name | Description | + // +======================+===============================================+================================================================+ + // | uint16 | format | Format identifier: format = 3 | + // +----------------------+-----------------------------------------------+----------------------------------------------------------------+ + // | uint16 | backtrackGlyphCount | Number of glyphs in the backtrack sequence | + // +----------------------+-----------------------------------------------+----------------------------------------------------------------+ + // | Offset16 | backtrackCoverageOffsets[backtrackGlyphCount] | Array of offsets to coverage tables for the backtrack sequence | + // +----------------------+-----------------------------------------------+----------------------------------------------------------------+ + // | uint16 | inputGlyphCount | Number of glyphs in the input sequence | + // +----------------------+-----------------------------------------------+----------------------------------------------------------------+ + // | Offset16 | inputCoverageOffsets[inputGlyphCount] | Array of offsets to coverage tables for the input sequence | + // +----------------------+-----------------------------------------------+----------------------------------------------------------------+ + // | uint16 | lookaheadGlyphCount | Number of glyphs in the lookahead sequence | + // +----------------------+-----------------------------------------------+----------------------------------------------------------------+ + // | Offset16 | lookaheadCoverageOffsets[lookaheadGlyphCount] | Array of offsets to coverage tables for the lookahead sequence | + // +----------------------+-----------------------------------------------+----------------------------------------------------------------+ + // | uint16 | seqLookupCount | Number of SequenceLookupRecords | + // +----------------------+-----------------------------------------------+----------------------------------------------------------------+ + // | SequenceLookupRecord | seqLookupRecords[seqLookupCount] | Array of SequenceLookupRecords | + // +----------------------+-----------------------------------------------+----------------------------------------------------------------+ + ushort backtrackGlyphCount = reader.ReadUInt16(); + ushort[] backtrackCoverageOffsets = reader.ReadUInt16Array(backtrackGlyphCount); + + ushort inputGlyphCount = reader.ReadUInt16(); + ushort[] inputCoverageOffsets = reader.ReadUInt16Array(inputGlyphCount); + + ushort lookaheadGlyphCount = reader.ReadUInt16(); + ushort[] lookaheadCoverageOffsets = reader.ReadUInt16Array(lookaheadGlyphCount); + + ushort seqLookupCount = reader.ReadUInt16(); + SequenceLookupRecord[] seqLookupRecords = SequenceLookupRecord.LoadArray(reader, seqLookupCount); + + backtrackCoverageTables = CoverageTable.LoadArray(reader, offset, backtrackCoverageOffsets); + inputCoverageTables = CoverageTable.LoadArray(reader, offset, inputCoverageOffsets); + lookaheadCoverageTables = CoverageTable.LoadArray(reader, offset, lookaheadCoverageOffsets); + return seqLookupRecords; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Tag.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Tag.cs new file mode 100644 index 0000000..7b501e9 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Tag.cs @@ -0,0 +1,112 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// Data type for tag identifiers. Tags are four byte integers, each byte representing a character. + /// Tags are used to identify tables, design-variation axes, scripts, languages, font features, and baselines with + /// human-readable names. + /// + public readonly struct Tag : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The tag value. + public Tag(uint value) => this.Value = value; + + /// + /// Gets the Tag value as 32 bit unsigned integer. + /// + public uint Value { get; } + + /// + /// Implicitly converts a to a . + /// + /// The unsigned integer value. + public static implicit operator Tag(uint value) => new(value); + + /// + /// Implicitly converts a to a . + /// + /// The feature tag enum value. + public static implicit operator Tag(KnownFeatureTags value) => new((uint)value); + + /// + /// Determines whether two instances are equal. + /// + /// The left tag. + /// The right tag. + /// if the tags are equal; otherwise, . + public static bool operator ==(Tag left, Tag right) => left.Equals(right); + + /// + /// Determines whether two instances are not equal. + /// + /// The left tag. + /// The right tag. + /// if the tags are not equal; otherwise, . + public static bool operator !=(Tag left, Tag right) => !(left == right); + + /// + /// Converts the string representation of a number to its Tag equivalent. + /// + /// A string containing a tag to convert. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Tag Parse(string value) + { + if (string.IsNullOrEmpty(value) || value.Length != 4) + { + return default; + } + + byte b3 = GetByte(value[3]); + byte b2 = GetByte(value[2]); + byte b1 = GetByte(value[1]); + byte b0 = GetByte(value[0]); + + return (uint)((b0 << 24) | (b1 << 16) | (b2 << 8) | b3); + } + + /// + /// Converts a character to a byte, returning 0 if the character is outside the byte range. + /// + /// The character to convert. + /// The byte value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte GetByte(char c) + { + if (c is >= (char)0 and <= (char)255) + { + return (byte)c; + } + + return 0; + } + + /// + public override bool Equals(object? obj) => obj is Tag tag && this.Equals(tag); + + /// + public bool Equals(Tag other) => this.Value == other.Value; + + /// + public override int GetHashCode() => HashCode.Combine(this.Value); + + /// + public override string ToString() + { + char[] chars = new char[4]; + chars[3] = (char)(this.Value & 0xFF); + chars[2] = (char)((this.Value >> 8) & 0xFF); + chars[1] = (char)((this.Value >> 16) & 0xFF); + chars[0] = (char)((this.Value >> 24) & 0xFF); + + return new string(chars); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/TagEntry.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/TagEntry.cs new file mode 100644 index 0000000..7273fbf --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/TagEntry.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// Represents an OpenType feature tag with an enabled/disabled state, used during shaping + /// to track which features are active for a given glyph. + /// + [DebuggerDisplay("Tag: {Tag}, Enabled: {Enabled}")] + internal struct TagEntry + { + /// + /// Initializes a new instance of the struct. + /// + /// The feature tag. + /// Whether the feature is enabled. + public TagEntry(Tag tag, bool enabled) + { + this.Tag = tag; + this.Enabled = enabled; + } + + /// + /// Gets or sets a value indicating whether the feature is enabled. + /// + public bool Enabled { get; set; } + + /// + /// Gets the feature tag. + /// + public Tag Tag { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/UnicodeScriptTagMap.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/UnicodeScriptTagMap.cs new file mode 100644 index 0000000..67c11b3 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/UnicodeScriptTagMap.cs @@ -0,0 +1,220 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using System; +using System.Collections.Generic; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic { + /// + /// Provides a map from Unicode to OTF . + /// + /// + internal sealed class UnicodeScriptTagMap : Dictionary + { + /// + /// The lazily-initialized singleton instance. + /// + private static readonly Lazy Lazy = new(CreateMap, isThreadSafe: true); + + /// + /// Prevents a default instance of the class from being created. + /// + private UnicodeScriptTagMap() + { + } + + /// + /// Gets the singleton instance of the . + /// + public static UnicodeScriptTagMap Instance => Lazy.Value; + + /// + /// Creates the Unicode script to OpenType tag map. + /// + /// The populated . + // TODO: This map will likely require updating to add aliases for old fonts. + // Use HarfBuzz as a reference. + private static UnicodeScriptTagMap CreateMap() + => new() + { + { ScriptClass.Unknown, new[] { Tag.Parse("zzzz") } }, + { ScriptClass.Common, new[] { Tag.Parse("zyyy") } }, + { ScriptClass.Inherited, new[] { Tag.Parse("zinh") } }, + { ScriptClass.Adlam, new[] { Tag.Parse("adlm") } }, + { ScriptClass.CaucasianAlbanian, new[] { Tag.Parse("aghb") } }, + { ScriptClass.Ahom, new[] { Tag.Parse("ahom") } }, + { ScriptClass.Arabic, new[] { Tag.Parse("arab") } }, + { ScriptClass.ImperialAramaic, new[] { Tag.Parse("armi") } }, + { ScriptClass.Armenian, new[] { Tag.Parse("armn") } }, + { ScriptClass.Avestan, new[] { Tag.Parse("avst") } }, + { ScriptClass.Balinese, new[] { Tag.Parse("bali") } }, + { ScriptClass.Bamum, new[] { Tag.Parse("bamu") } }, + { ScriptClass.BassaVah, new[] { Tag.Parse("bass") } }, + { ScriptClass.Batak, new[] { Tag.Parse("batk") } }, + { ScriptClass.Bengali, new[] { Tag.Parse("bng2"), Tag.Parse("beng") } }, + { ScriptClass.Bhaiksuki, new[] { Tag.Parse("bhks") } }, + { ScriptClass.Bopomofo, new[] { Tag.Parse("bopo") } }, + { ScriptClass.Brahmi, new[] { Tag.Parse("brah") } }, + { ScriptClass.Braille, new[] { Tag.Parse("brai") } }, + { ScriptClass.Buginese, new[] { Tag.Parse("bugi") } }, + { ScriptClass.Buhid, new[] { Tag.Parse("buhd") } }, + { ScriptClass.Chakma, new[] { Tag.Parse("cakm") } }, + { ScriptClass.CanadianAboriginal, new[] { Tag.Parse("cans") } }, + { ScriptClass.Carian, new[] { Tag.Parse("cari") } }, + { ScriptClass.Cham, new[] { Tag.Parse("cham") } }, + { ScriptClass.Cherokee, new[] { Tag.Parse("cher") } }, + { ScriptClass.Chorasmian, new[] { Tag.Parse("chrs") } }, + { ScriptClass.Coptic, new[] { Tag.Parse("copt") } }, + { ScriptClass.CyproMinoan, new[] { Tag.Parse("cpmn") } }, + { ScriptClass.Cypriot, new[] { Tag.Parse("cprt") } }, + { ScriptClass.Cyrillic, new[] { Tag.Parse("cyrl") } }, + { ScriptClass.Default, new[] { Tag.Parse("DFLT"), Tag.Parse("dflt"), Tag.Parse("latn") } }, + { ScriptClass.Devanagari, new[] { Tag.Parse("dev2"), Tag.Parse("deva") } }, + { ScriptClass.DivesAkuru, new[] { Tag.Parse("diak") } }, + { ScriptClass.Dogra, new[] { Tag.Parse("dogr") } }, + { ScriptClass.Deseret, new[] { Tag.Parse("dsrt") } }, + { ScriptClass.Duployan, new[] { Tag.Parse("dupl") } }, + { ScriptClass.EgyptianHieroglyphs, new[] { Tag.Parse("egyp") } }, + { ScriptClass.Elbasan, new[] { Tag.Parse("elba") } }, + { ScriptClass.Elymaic, new[] { Tag.Parse("elym") } }, + { ScriptClass.Ethiopic, new[] { Tag.Parse("ethi") } }, + { ScriptClass.Georgian, new[] { Tag.Parse("geor") } }, + { ScriptClass.Glagolitic, new[] { Tag.Parse("glag") } }, + { ScriptClass.GunjalaGondi, new[] { Tag.Parse("gong") } }, + { ScriptClass.MasaramGondi, new[] { Tag.Parse("gonm") } }, + { ScriptClass.Gothic, new[] { Tag.Parse("goth") } }, + { ScriptClass.Grantha, new[] { Tag.Parse("gran") } }, + { ScriptClass.Greek, new[] { Tag.Parse("grek") } }, + { ScriptClass.Gujarati, new[] { Tag.Parse("gjr2"), Tag.Parse("gujr") } }, + { ScriptClass.Gurmukhi, new[] { Tag.Parse("gur2"), Tag.Parse("guru") } }, + { ScriptClass.Hangul, new[] { Tag.Parse("hang") } }, + { ScriptClass.Han, new[] { Tag.Parse("hani") } }, + { ScriptClass.Hanunoo, new[] { Tag.Parse("hano") } }, + { ScriptClass.Hatran, new[] { Tag.Parse("hatr") } }, + { ScriptClass.Hebrew, new[] { Tag.Parse("hebr") } }, + { ScriptClass.Hiragana, new[] { Tag.Parse("hira") } }, + { ScriptClass.AnatolianHieroglyphs, new[] { Tag.Parse("hluw") } }, + { ScriptClass.PahawhHmong, new[] { Tag.Parse("hmng") } }, + { ScriptClass.NyiakengPuachueHmong, new[] { Tag.Parse("hmnp") } }, + { ScriptClass.KatakanaOrHiragana, new[] { Tag.Parse("hrkt") } }, + { ScriptClass.OldHungarian, new[] { Tag.Parse("hung") } }, + { ScriptClass.OldItalic, new[] { Tag.Parse("ital") } }, + { ScriptClass.Javanese, new[] { Tag.Parse("java") } }, + { ScriptClass.KayahLi, new[] { Tag.Parse("kali") } }, + { ScriptClass.Katakana, new[] { Tag.Parse("kana") } }, + { ScriptClass.Kharoshthi, new[] { Tag.Parse("khar") } }, + { ScriptClass.Khmer, new[] { Tag.Parse("khmr") } }, + { ScriptClass.Khojki, new[] { Tag.Parse("khoj") } }, + { ScriptClass.KhitanSmallScript, new[] { Tag.Parse("kits") } }, + { ScriptClass.Kannada, new[] { Tag.Parse("knd2"), Tag.Parse("knda") } }, + { ScriptClass.Kaithi, new[] { Tag.Parse("kthi") } }, + { ScriptClass.TaiTham, new[] { Tag.Parse("lana") } }, + { ScriptClass.Lao, new[] { Tag.Parse("lao ") } }, + { ScriptClass.Latin, new[] { Tag.Parse("latn") } }, + { ScriptClass.Lepcha, new[] { Tag.Parse("lepc") } }, + { ScriptClass.Limbu, new[] { Tag.Parse("limb") } }, + { ScriptClass.LinearA, new[] { Tag.Parse("lina") } }, + { ScriptClass.LinearB, new[] { Tag.Parse("linb") } }, + { ScriptClass.Lisu, new[] { Tag.Parse("lisu") } }, + { ScriptClass.Lycian, new[] { Tag.Parse("lyci") } }, + { ScriptClass.Lydian, new[] { Tag.Parse("lydi") } }, + { ScriptClass.Mahajani, new[] { Tag.Parse("mahj") } }, + { ScriptClass.Makasar, new[] { Tag.Parse("maka") } }, + { ScriptClass.Mandaic, new[] { Tag.Parse("mand") } }, + { ScriptClass.Manichaean, new[] { Tag.Parse("mani") } }, + { ScriptClass.Marchen, new[] { Tag.Parse("marc") } }, + { ScriptClass.Medefaidrin, new[] { Tag.Parse("medf") } }, + { ScriptClass.MendeKikakui, new[] { Tag.Parse("mend") } }, + { ScriptClass.MeroiticCursive, new[] { Tag.Parse("merc") } }, + { ScriptClass.MeroiticHieroglyphs, new[] { Tag.Parse("mero") } }, + { ScriptClass.Malayalam, new[] { Tag.Parse("mlm2"), Tag.Parse("mlym") } }, + { ScriptClass.Modi, new[] { Tag.Parse("modi") } }, + { ScriptClass.Mongolian, new[] { Tag.Parse("mong") } }, + { ScriptClass.Mro, new[] { Tag.Parse("mroo") } }, + { ScriptClass.MeeteiMayek, new[] { Tag.Parse("mtei") } }, + { ScriptClass.Multani, new[] { Tag.Parse("mult") } }, + { ScriptClass.Myanmar, new[] { Tag.Parse("mym2"), Tag.Parse("mymr") } }, + { ScriptClass.Nandinagari, new[] { Tag.Parse("nand") } }, + { ScriptClass.OldNorthArabian, new[] { Tag.Parse("narb") } }, + { ScriptClass.Nabataean, new[] { Tag.Parse("nbat") } }, + { ScriptClass.Newa, new[] { Tag.Parse("newa") } }, + { ScriptClass.Nko, new[] { Tag.Parse("nkoo") } }, + { ScriptClass.Nushu, new[] { Tag.Parse("nshu") } }, + { ScriptClass.Ogham, new[] { Tag.Parse("ogam") } }, + { ScriptClass.OlChiki, new[] { Tag.Parse("olck") } }, + { ScriptClass.OldTurkic, new[] { Tag.Parse("orkh") } }, + { ScriptClass.Oriya, new[] { Tag.Parse("ory2"), Tag.Parse("orya") } }, + { ScriptClass.Osage, new[] { Tag.Parse("osge") } }, + { ScriptClass.Osmanya, new[] { Tag.Parse("osma") } }, + { ScriptClass.OldUyghur, new[] { Tag.Parse("ougr") } }, + { ScriptClass.Palmyrene, new[] { Tag.Parse("palm") } }, + { ScriptClass.PauCinHau, new[] { Tag.Parse("pauc") } }, + { ScriptClass.OldPermic, new[] { Tag.Parse("perm") } }, + { ScriptClass.PhagsPa, new[] { Tag.Parse("phag") } }, + { ScriptClass.InscriptionalPahlavi, new[] { Tag.Parse("phli") } }, + { ScriptClass.PsalterPahlavi, new[] { Tag.Parse("phlp") } }, + { ScriptClass.Phoenician, new[] { Tag.Parse("phnx") } }, + { ScriptClass.Miao, new[] { Tag.Parse("plrd") } }, + { ScriptClass.InscriptionalParthian, new[] { Tag.Parse("prti") } }, + { ScriptClass.Rejang, new[] { Tag.Parse("rjng") } }, + { ScriptClass.HanifiRohingya, new[] { Tag.Parse("rohg") } }, + { ScriptClass.Runic, new[] { Tag.Parse("runr") } }, + { ScriptClass.Samaritan, new[] { Tag.Parse("samr") } }, + { ScriptClass.OldSouthArabian, new[] { Tag.Parse("sarb") } }, + { ScriptClass.Saurashtra, new[] { Tag.Parse("saur") } }, + { ScriptClass.SignWriting, new[] { Tag.Parse("sgnw") } }, + { ScriptClass.Shavian, new[] { Tag.Parse("shaw") } }, + { ScriptClass.Sharada, new[] { Tag.Parse("shrd") } }, + { ScriptClass.Siddham, new[] { Tag.Parse("sidd") } }, + { ScriptClass.Khudawadi, new[] { Tag.Parse("sind") } }, + { ScriptClass.Sinhala, new[] { Tag.Parse("sinh") } }, + { ScriptClass.Sogdian, new[] { Tag.Parse("sogd") } }, + { ScriptClass.OldSogdian, new[] { Tag.Parse("sogo") } }, + { ScriptClass.SoraSompeng, new[] { Tag.Parse("sora") } }, + { ScriptClass.Soyombo, new[] { Tag.Parse("soyo") } }, + { ScriptClass.Sundanese, new[] { Tag.Parse("sund") } }, + { ScriptClass.SylotiNagri, new[] { Tag.Parse("sylo") } }, + { ScriptClass.Syriac, new[] { Tag.Parse("syrc") } }, + { ScriptClass.Tagbanwa, new[] { Tag.Parse("tagb") } }, + { ScriptClass.Takri, new[] { Tag.Parse("takr") } }, + { ScriptClass.TaiLe, new[] { Tag.Parse("tale") } }, + { ScriptClass.NewTaiLue, new[] { Tag.Parse("talu") } }, + { ScriptClass.Tamil, new[] { Tag.Parse("tml2"), Tag.Parse("taml") } }, + { ScriptClass.Tangut, new[] { Tag.Parse("tang") } }, + { ScriptClass.TaiViet, new[] { Tag.Parse("tavt") } }, + { ScriptClass.Telugu, new[] { Tag.Parse("tel2"), Tag.Parse("telu") } }, + { ScriptClass.Tifinagh, new[] { Tag.Parse("tfng") } }, + { ScriptClass.Tagalog, new[] { Tag.Parse("tglg") } }, + { ScriptClass.Thaana, new[] { Tag.Parse("thaa") } }, + { ScriptClass.Thai, new[] { Tag.Parse("thai") } }, + { ScriptClass.Tibetan, new[] { Tag.Parse("tibt") } }, + { ScriptClass.Tirhuta, new[] { Tag.Parse("tirh") } }, + { ScriptClass.Tangsa, new[] { Tag.Parse("tnsa") } }, + { ScriptClass.Toto, new[] { Tag.Parse("toto") } }, + { ScriptClass.Ugaritic, new[] { Tag.Parse("ugar") } }, + { ScriptClass.Vai, new[] { Tag.Parse("vaii") } }, + { ScriptClass.Vithkuqi, new[] { Tag.Parse("vith") } }, + { ScriptClass.WarangCiti, new[] { Tag.Parse("wara") } }, + { ScriptClass.Wancho, new[] { Tag.Parse("wcho") } }, + { ScriptClass.OldPersian, new[] { Tag.Parse("xpeo") } }, + { ScriptClass.Cuneiform, new[] { Tag.Parse("xsux") } }, + { ScriptClass.Yezidi, new[] { Tag.Parse("yezi") } }, + { ScriptClass.Yi, new[] { Tag.Parse("yiii") } }, + { ScriptClass.ZanabazarSquare, new[] { Tag.Parse("zanb") } }, + { ScriptClass.BeriaErfe, new[] { Tag.Parse("berf") } }, + { ScriptClass.Garay, new[] { Tag.Parse("gara") } }, + { ScriptClass.GurungKhema, new[] { Tag.Parse("gukh") } }, + { ScriptClass.Kawi, new[] { Tag.Parse("kawi") } }, + { ScriptClass.KiratRai, new[] { Tag.Parse("krai") } }, + { ScriptClass.NagMundari, new[] { Tag.Parse("nagm") } }, + { ScriptClass.OlOnal, new[] { Tag.Parse("onao") } }, + { ScriptClass.Sidetic, new[] { Tag.Parse("sidt") } }, + { ScriptClass.Sunuwar, new[] { Tag.Parse("sunu") } }, + { ScriptClass.TaiYo, new[] { Tag.Parse("tayo") } }, + { ScriptClass.Todhri, new[] { Tag.Parse("todr") } }, + { ScriptClass.TolongSiki, new[] { Tag.Parse("tols") } }, + { ScriptClass.TuluTigalari, new[] { Tag.Parse("tutg") } }, + }; + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/AVarTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/AVarTable.cs new file mode 100644 index 0000000..8caaab0 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/AVarTable.cs @@ -0,0 +1,99 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Implements reading the Font Variations Table `avar`. + /// + /// + internal class AVarTable : Table + { + /// + /// The table name identifier for the avar table. + /// + internal const string TableName = "avar"; + + /// + /// Initializes a new instance of the class. + /// + /// The number of variation axes. + /// The segment maps array, one per axis. + public AVarTable(uint axisCount, SegmentMapRecord[] segmentMaps) + { + this.AxisCount = axisCount; + this.SegmentMaps = segmentMaps; + } + + /// + /// Gets the number of variation axes for the font. + /// + public uint AxisCount { get; } + + /// + /// Gets the segment maps array, one segment map for each axis, in the order of axes specified in the fvar table. + /// + public SegmentMapRecord[] SegmentMaps { get; } + + /// + /// Loads the avar table from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static AVarTable? Load(FontReader reader) + { + if (!reader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + /// + /// Loads the avar table from the specified binary reader. + /// + /// The big-endian binary reader positioned at the start of the avar table. + /// The . + public static AVarTable Load(BigEndianBinaryReader reader) + { + // VariationsTable `avar` + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Type | Name | Description | + // +=================+========================================+=========================================================================+ + // | uint16 | majorVersion | Major version number of the font variations table — set to 1. | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | uint16 | minorVersion | Minor version number of the font variations table — set to 0. | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | uint16 | (reserved) | This field is permanently reserved. Set to zero. | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | uint16 | axisCount | The number of variation axes in the font | + // | | | (the number of records in the axes array). | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | SegmentMaps | axisSegmentMaps[axisCount] | The segment maps array — one segment map for each axis, in the order of | + // | | | axes specified in the 'fvar' table. | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + ushort major = reader.ReadUInt16(); + ushort minor = reader.ReadUInt16(); + ushort reserved = reader.ReadUInt16(); + ushort axisCount = reader.ReadUInt16(); + + if (major != 1) + { + throw new NotSupportedException("Only version 1 of avar table is supported"); + } + + var segmentMaps = new SegmentMapRecord[axisCount]; + for (int i = 0; i < axisCount; i++) + { + segmentMaps[i] = SegmentMapRecord.Load(reader); + } + + return new AVarTable(axisCount, segmentMaps); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/AxisValueMapRecord.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/AxisValueMapRecord.cs new file mode 100644 index 0000000..55e2713 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/AxisValueMapRecord.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Represents a single axis value mapping record used in the avar table + /// to remap a normalized coordinate value to a modified value. + /// + /// + internal class AxisValueMapRecord + { + /// + /// Initializes a new instance of the class. + /// + /// The normalized coordinate value obtained using default normalization. + /// The modified, normalized coordinate value. + public AxisValueMapRecord(float fromCoordinate, float toCoordinate) + { + this.FromCoordinate = fromCoordinate; + this.ToCoordinate = toCoordinate; + } + + /// + /// Gets the normalized coordinate value obtained using default normalization. + /// + public float FromCoordinate { get; } + + /// + /// Gets the modified, normalized coordinate value. + /// + public float ToCoordinate { get; } + + /// + /// Loads an from the specified binary reader. + /// + /// The big-endian binary reader. + /// The . + public static AxisValueMapRecord Load(BigEndianBinaryReader reader) + { + // AxisValueMapRecord + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Type | Name | Description | + // +=================+========================================+=========================================================================+ + // | F2DOT14 | fromCoordinate | A normalized coordinate value obtained using default normalization. | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | F2DOT14 | toCoordinate | The modified, normalized coordinate value. | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + float fromCoordinate = reader.ReadF2Dot14(); + float toCoordinate = reader.ReadF2Dot14(); + + return new AxisValueMapRecord(fromCoordinate, toCoordinate); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/CVarTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/CVarTable.cs new file mode 100644 index 0000000..ca6b09d --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/CVarTable.cs @@ -0,0 +1,213 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Implements reading the CVT Variations table cvar. + /// The cvar table provides variation data for the Control Value Table (CVT) + /// used by TrueType hinting instructions. It uses the same Tuple Variation Store + /// format as gvar, but with a single dimension of deltas (CVT values rather than X/Y coordinates). + /// + /// + internal class CVarTable : Table + { + /// + /// The table name identifier for the cvar table. + /// + internal const string TableName = "cvar"; + + /// + /// Initializes a new instance of the class. + /// + /// The array of tuple variations containing CVT deltas. + public CVarTable(CVarTupleVariation[] tupleVariations) + => this.TupleVariations = tupleVariations; + + /// + /// Gets the tuple variations containing CVT deltas. + /// + public CVarTupleVariation[] TupleVariations { get; } + + /// + /// Loads the cvar table from the font reader. + /// The axis count must be known from the fvar table before loading cvar. + /// + /// The font reader. + /// The number of variation axes from fvar. + /// The loaded cvar table, or null if not present. + public static CVarTable? Load(FontReader reader, int axisCount) + { + if (!reader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader, axisCount); + } + } + + /// + /// Loads the cvar table from the specified binary reader. + /// + /// The big-endian binary reader positioned at the start of the cvar table. + /// The number of variation axes from fvar. + /// The . + public static CVarTable Load(BigEndianBinaryReader reader, int axisCount) + { + // cvar — CVT Variations Table + // The cvar table uses the Tuple Variation Store format. + // +--------------------------+-------------------------------------------+--------------------------------------------------------------+ + // | Type | Name | Description | + // +==========================+===========================================+==============================================================+ + // | uint16 | majorVersion | Major version — set to 1. | + // +--------------------------+-------------------------------------------+--------------------------------------------------------------+ + // | uint16 | minorVersion | Minor version — set to 0. | + // +--------------------------+-------------------------------------------+--------------------------------------------------------------+ + // | uint16 | tupleVariationCount | Packed field: high 4 bits are flags, | + // | | | low 12 bits are the number of tuple variation tables. | + // +--------------------------+-------------------------------------------+--------------------------------------------------------------+ + // | Offset16 | dataOffset | Offset from the start of the cvar table to the | + // | | | serialized data. | + // +--------------------------+-------------------------------------------+--------------------------------------------------------------+ + // | TupleVariation | tupleVariationHeaders[tupleVariationCount]| Array of tuple variation headers. | + // +--------------------------+-------------------------------------------+--------------------------------------------------------------+ + ushort majorVersion = reader.ReadUInt16(); + ushort minorVersion = reader.ReadUInt16(); + + if (majorVersion != 1) + { + throw new NotSupportedException("Only version 1 of cvar table is supported"); + } + + ushort tupleVariationCount = reader.ReadUInt16(); + bool hasSharedPointNumbers = (tupleVariationCount & GlyphVariationData.SharedPointNumbersMask) != 0; + int tupleCount = tupleVariationCount & GlyphVariationData.CountMask; + ushort dataOffset = reader.ReadOffset16(); + + // Read all tuple variation headers. + TupleVariation[] tupleVariations = new TupleVariation[tupleCount]; + for (int i = 0; i < tupleCount; i++) + { + tupleVariations[i] = TupleVariation.Load(reader, axisCount); + } + + // Seek to the serialized data. + reader.Seek(dataOffset, SeekOrigin.Begin); + + // If shared point numbers flag is set, decode them from the start of the serialized data. + ushort[]? sharedPointNumbers = null; + if (hasSharedPointNumbers) + { + sharedPointNumbers = GlyphVariationData.DecodePackedPoints(reader); + } + + // Decode each tuple's serialized data. + // Unlike gvar, cvar has only one set of deltas per tuple (CVT value adjustments). + CVarTupleVariation[] cvarTuples = new CVarTupleVariation[tupleCount]; + for (int i = 0; i < tupleCount; i++) + { + TupleVariation header = tupleVariations[i]; + long tupleDataStart = reader.BaseStream.Position; + + // Determine which CVT indices this tuple applies to. + ushort[]? pointNumbers; + if (header.HasPrivatePointNumbers) + { + pointNumbers = GlyphVariationData.DecodePackedPoints(reader); + } + else + { + pointNumbers = sharedPointNumbers; + } + + int nPoints = pointNumbers is { Length: > 0 } ? pointNumbers.Length : 0; + + short[]? deltas = null; + if (nPoints > 0) + { + // cvar has only one set of deltas (not X/Y pairs like gvar). + deltas = GlyphVariationData.DecodePackedDeltas(reader, nPoints); + } + else + { + // All CVT entries are referenced. Store raw bytes for deferred decoding. + long bytesConsumed = reader.BaseStream.Position - tupleDataStart; + int remaining = header.VariationDataSize - (int)bytesConsumed; + if (remaining > 0) + { + cvarTuples[i] = new CVarTupleVariation(header, pointNumbers, null, reader.ReadBytes(remaining)); + continue; + } + } + + // Skip any remaining bytes for this tuple. + long consumed = reader.BaseStream.Position - tupleDataStart; + int skip = header.VariationDataSize - (int)consumed; + if (skip > 0) + { + reader.BaseStream.Position += skip; + } + + cvarTuples[i] = new CVarTupleVariation(header, pointNumbers, deltas, null); + } + + return new CVarTable(cvarTuples); + } + } + + /// + /// Represents a single tuple variation for the cvar table with its CVT index references and deltas. + /// Unlike gvar's which has X/Y delta pairs, + /// cvar tuples have a single set of deltas for CVT values. + /// + internal class CVarTupleVariation + { + /// + /// Initializes a new instance of the class. + /// + /// The tuple variation header containing peak coordinates and flags. + /// The CVT indices this tuple applies to, or null/empty for all CVT entries. + /// The CVT deltas, or null if deferred. + /// The raw serialized delta data for deferred decoding, or null if already decoded. + public CVarTupleVariation( + TupleVariation tupleVariation, + ushort[]? pointNumbers, + short[]? deltas, + byte[]? rawDeltaData) + { + this.TupleVariation = tupleVariation; + this.PointNumbers = pointNumbers; + this.Deltas = deltas; + this.RawDeltaData = rawDeltaData; + } + + /// + /// Gets the tuple variation header containing peak coordinates and flags. + /// + public TupleVariation TupleVariation { get; } + + /// + /// Gets the CVT indices this tuple applies to. + /// An empty array means all CVT entries are referenced. + /// + public ushort[]? PointNumbers { get; } + + /// + /// Gets the CVT deltas for the referenced entries. + /// Null when deltas apply to all CVT entries and were deferred (see ). + /// + public short[]? Deltas { get; } + + /// + /// Gets the raw serialized delta data for deferred decoding. + /// Used when point numbers indicate "all CVT entries" and the actual count + /// is not known until the CVT table size is available. + /// + public byte[]? RawDeltaData { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/DeltaSet.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/DeltaSet.cs new file mode 100644 index 0000000..6ab0449 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/DeltaSet.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Represents a delta-set row in an subtable. + /// Each delta set contains per-region delta adjustment values. + /// + /// + internal class DeltaSet + { + /// + /// Initializes a new instance of the class by reading delta values from the binary reader. + /// + /// The big-endian binary reader positioned at the delta set data. + /// The number of deltas encoded using the larger (word) type. + /// Whether word deltas are 32-bit (int32) instead of 16-bit (int16). + /// The total number of region indices (and thus the total number of deltas). + public DeltaSet(BigEndianBinaryReader reader, int wordDeltas, bool longWords, ushort regionIndexCount) + { + this.ShortDeltas = new int[wordDeltas]; + for (int i = 0; i < wordDeltas; i++) + { + this.ShortDeltas[i] = longWords ? reader.ReadInt32() : reader.ReadInt16(); + } + + int remaining = regionIndexCount - wordDeltas; + this.RegionDeltas = new short[remaining]; + for (int i = 0; i < remaining; i++) + { + this.RegionDeltas[i] = longWords ? reader.ReadInt16() : reader.ReadSByte(); + } + + this.Deltas = new int[this.RegionDeltas.Length + this.ShortDeltas.Length]; + int offset = 0; + + for (int i = 0; i < this.ShortDeltas.Length; i++) + { + this.Deltas[offset++] = this.ShortDeltas[i]; + } + + for (int i = 0; i < this.RegionDeltas.Length; i++) + { + this.Deltas[offset++] = this.RegionDeltas[i]; + } + } + + /// + /// Gets the remaining deltas encoded using the smaller (short) type. + /// + public short[] RegionDeltas { get; } + + /// + /// Gets the initial deltas encoded using the larger (word) type. + /// + public int[] ShortDeltas { get; } + + /// + /// Gets the combined array of all deltas (word deltas followed by region deltas), one per region. + /// + public int[] Deltas { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/DeltaSetIndexMap.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/DeltaSetIndexMap.cs new file mode 100644 index 0000000..e1be926 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/DeltaSetIndexMap.cs @@ -0,0 +1,105 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Represents a single entry in a DeltaSetIndexMap, mapping a glyph ID to an outer/inner index pair + /// into an . + /// + /// + internal class DeltaSetIndexMap + { + /// + /// Mask for the low 4 bits of the entry format, giving the number of inner index bits minus one. + /// + private const int InnerIndexBitCountMask = 0x0F; + + /// + /// Mask for bits 4-5 of the entry format, giving the entry size minus one. + /// + private const int MapEntrySizeMask = 0x30; + + /// + /// Initializes a new instance of the class. + /// + /// The outer index into the ItemVariationStore. + /// The inner index into the ItemVariationStore. + public DeltaSetIndexMap(int outerIndex, int innerIndex) + { + this.OuterIndex = outerIndex; + this.InnerIndex = innerIndex; + } + + /// + /// Gets the outer index into the ItemVariationStore (selects the ItemVariationData subtable). + /// + public int OuterIndex { get; } + + /// + /// Gets the inner index into the ItemVariationStore (selects the delta set within the subtable). + /// + public int InnerIndex { get; } + + /// + /// Loads an array of entries from the specified binary reader. + /// + /// The big-endian binary reader. + /// The byte offset from the start of the stream to this map. If zero, no map is present. + /// The array of entries, or if the offset is zero. + public static DeltaSetIndexMap[]? Load(BigEndianBinaryReader reader, long offset) + { + // This can be null if the offset is zero. + if (offset == 0) + { + return null; + } + + // DeltaSetIndexMap. + // +-----------------+----------------------------------------+-----------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +=================+========================================+===================================================================================+ + // | uint8 | format | DeltaSetIndexMap format. Either 0 or 1 | + // +-----------------+----------------------------------------+-----------------------------------------------------------------------------------+ + // | uint8 | entryFormat | A packed field that describes the compressed representation of delta-set indices. | + // +-----------------+----------------------------------------+-----------------------------------------------------------------------------------+ + // | uint16 or uin32 | mapCount | The number of mapping entries. uint16 for format0, uint32 for format 1 | + // +-----------------+----------------------------------------+-----------------------------------------------------------------------------------+ + // | uint8 | mapData[variable] | The delta-set index mapping data. | + // +-----------------+----------------------------------------+-----------------------------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + byte format = reader.ReadUInt8(); + byte entryFormat = reader.ReadUInt8(); + + if (format is not (0 or 1)) + { + throw new NotSupportedException("Only format 0 or 1 of DeltaSetIndexMap is supported"); + } + + // Format 0 uses uint16 for mapCount, format 1 uses uint32. + int mapCount = format == 0 ? reader.ReadUInt16() : (int)reader.ReadUInt32(); + + int entrySize = ((entryFormat & MapEntrySizeMask) >> 4) + 1; + int innerBitCount = (entryFormat & InnerIndexBitCountMask) + 1; + int innerIndexMask = (1 << innerBitCount) - 1; + + DeltaSetIndexMap[] deltaSetIndexMaps = new DeltaSetIndexMap[mapCount]; + for (int i = 0; i < mapCount; i++) + { + int entry = entrySize switch + { + 1 => reader.ReadByte(), + 2 => (reader.ReadByte() << 8) | reader.ReadByte(), + 3 => (reader.ReadByte() << 16) | (reader.ReadByte() << 8) | reader.ReadByte(), + 4 => (reader.ReadByte() << 24) | (reader.ReadByte() << 16) | (reader.ReadByte() << 8) | reader.ReadByte(), + _ => throw new NotSupportedException("unsupported delta set index map"), + }; + deltaSetIndexMaps[i] = new DeltaSetIndexMap(entry >> innerBitCount, entry & innerIndexMask); + } + + return deltaSetIndexMaps; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/FVarTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/FVarTable.cs new file mode 100644 index 0000000..481d14b --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/FVarTable.cs @@ -0,0 +1,128 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Implements reading the Font Variations Table `fvar`. + /// + /// + internal class FVarTable : Table + { + /// + /// The table name identifier for the fvar table. + /// + internal const string TableName = "fvar"; + + /// + /// Initializes a new instance of the class. + /// + /// The number of variation axes. + /// The array of variation axis records. + /// The array of named instance records. + public FVarTable(ushort axisCount, VariationAxisRecord[] axes, InstanceRecord[] instances) + { + this.AxisCount = axisCount; + this.Axes = axes; + this.Instances = instances; + } + + /// + /// Gets the number of variation axes defined in this font. + /// + public ushort AxisCount { get; } + + /// + /// Gets the array of variation axis records defining each axis (e.g. weight, width). + /// + public VariationAxisRecord[] Axes { get; } + + /// + /// Gets the array of named instance records defined in this font. + /// + public InstanceRecord[] Instances { get; } + + /// + /// Loads the fvar table from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static FVarTable? Load(FontReader reader) + { + if (!reader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + /// + /// Loads the fvar table from the specified binary reader. + /// + /// The big-endian binary reader positioned at the start of the fvar table. + /// The . + public static FVarTable Load(BigEndianBinaryReader reader) + { + // VariationsTable `fvar` + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | Type | Name | Description | + // +=================+========================================+================================================================+ + // | uint16 | majorVersion | Major version number of the font variations table — set to 1. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | uint16 | minorVersion | Minor version number of the font variations table — set to 0. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | Offset16 | axesArrayOffset | Offset in bytes from the beginning of the table to the start | + // | | | of the VariationAxisRecord array. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | uint16 | (reserved) | This field is permanently reserved. Set to 2. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | uint16 | axisCount | The number of variation axes in the font | + // | | | (the number of records in the axes array). | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | uint16 | axisSize | The size in bytes of each VariationAxisRecord | + // | | | — set to 20 (0x0014) for this version. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | uint16 | instanceCount | The number of named instances defined in the font | + // | | | (the number of records in the instances array). | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | uint16 | instanceSize | The size in bytes of each InstanceRecord | + // | | | — set to either axisCount * sizeof(Fixed) + 4, | + // | | | or to axisCount * sizeof(Fixed) + 6. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + long startOffset = reader.BaseStream.Position; + ushort major = reader.ReadUInt16(); + ushort minor = reader.ReadUInt16(); + ushort axesArrayOffset = reader.ReadOffset16(); + ushort reserved = reader.ReadUInt16(); + ushort axisCount = reader.ReadUInt16(); + ushort axisSize = reader.ReadUInt16(); + ushort instanceCount = reader.ReadUInt16(); + ushort instanceSize = reader.ReadUInt16(); + + if (major != 1) + { + throw new NotSupportedException("Only version 1 of fvar table is supported"); + } + + VariationAxisRecord[] axesArray = new VariationAxisRecord[axisCount]; + for (int i = 0; i < axisCount; i++) + { + axesArray[i] = VariationAxisRecord.Load(reader, axesArrayOffset + (axisSize * i)); + } + + InstanceRecord[] instances = new InstanceRecord[instanceCount]; + long instancesOffset = reader.BaseStream.Position - startOffset; + for (int i = 0; i < instanceCount; i++) + { + instances[i] = InstanceRecord.Load(reader, instancesOffset + (i * instanceSize), axisCount); + } + + return new FVarTable(axisCount, axesArray, instances); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GVarTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GVarTable.cs new file mode 100644 index 0000000..c298adc --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GVarTable.cs @@ -0,0 +1,196 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Implements reading the Font Variations Table `gvar`. + /// + /// + internal class GVarTable : Table + { + /// + /// The table name identifier for the gvar table. + /// + internal const string TableName = "gvar"; + + /// + /// Initializes a new instance of the class. + /// + /// The number of variation axes. + /// The number of glyphs in the font. + /// The shared tuple records array, indexed by tuple index then axis. + /// The array of per-glyph variation data. + public GVarTable(ushort axisCount, ushort glyphCount, float[,] sharedTuples, GlyphVariationData[] glyphVariations) + { + this.AxisCount = axisCount; + this.GlyphCount = glyphCount; + this.SharedTuples = sharedTuples; + this.GlyphVariations = glyphVariations; + } + + /// + /// Gets the number of variation axes in the font. + /// + public ushort AxisCount { get; } + + /// + /// Gets the number of glyphs in this font. + /// + public ushort GlyphCount { get; } + + /// + /// Gets the shared tuple records array. Each row contains normalized coordinates for one shared tuple, one value per axis. + /// + public float[,] SharedTuples { get; } + + /// + /// Gets the array of per-glyph variation data tables. + /// + public GlyphVariationData[] GlyphVariations { get; } + + /// + /// Loads the gvar table from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static GVarTable? Load(FontReader reader) + { + if (!reader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader, out TableHeader? header)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader, header); + } + } + + /// + /// Loads the gvar table from the specified binary reader. + /// + /// The big-endian binary reader positioned at the start of the gvar table. + /// The table header providing the table length. + /// The . + public static GVarTable Load(BigEndianBinaryReader reader, TableHeader header) + { + // VariationsTable `gvar` + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Type | Name | Description | + // +=================+========================================+=========================================================================+ + // | uint16 | majorVersion | Major version number of the font variations table — set to 1. | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | uint16 | minorVersion | Minor version number of the font variations table — set to 0. | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | uint16 | axisCount | The number of variation axes in the font | + // | | | (the number of records in the axes array). | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | uint16 | sharedTupleCount | The number of shared tuple records. Shared tuple records can | + // | | | be referenced within glyph variation data tables for multiple glyphs, | + // | | | as opposed to other tuple records stored directly within a glyph | + // | | | variation data table. | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Offset32 | sharedTuplesOffset | Offset from the start of this table to the shared tuple records. | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | uint16 | glyphCount | The number of glyphs in this font. This must match the number of glyphs | + // | | | stored elsewhere in the font. | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | uint16 | flags | Bit-field that gives the format of the offset array that follows. | + // | | | If bit 0 is clear, the offsets are uint16; if bit 0 is set, | + // | | | the offsets are uint32. | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Offset32 | glyphVariationDataArrayOffset | Offset from the start of this table to the array of GlyphVariationData | + // | | | tables. | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Offset16 or | glyphVariationDataOffsets[glyphCount+1]| Offsets from the start of the GlyphVariationData array to each | + // | Offset32 | | GlyphVariationData table. | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + uint gvarTableLength = header.Length; + ushort major = reader.ReadUInt16(); + ushort minor = reader.ReadUInt16(); + ushort axisCount = reader.ReadUInt16(); + ushort sharedTupleCount = reader.ReadUInt16(); + uint sharedTuplesOffset = reader.ReadOffset32(); + ushort glyphCount = reader.ReadUInt16(); + ushort flags = reader.ReadUInt16(); + bool is32BitOffset = (flags & 1) == 1; + uint glyphVariationDataArrayOffset = reader.ReadOffset32(); + + if (major != 1) + { + throw new NotSupportedException("Only version 1 of gvar table is supported"); + } + + // Read glyphVariationDataOffsets[glyphCount + 1] immediately after the header, + // as required by the spec and as done by FreeType. + int offsetCount = glyphCount + 1; + uint[] glyphVariationOffsets = new uint[offsetCount]; + + for (int i = 0; i < offsetCount; i++) + { + // If offsets are 16-bit, values are stored in units of 2 bytes. + glyphVariationOffsets[i] = is32BitOffset + ? reader.ReadUInt32() + : (uint)(reader.ReadUInt16() * 2); + } + + // Shared tuple records + float[,] sharedTuples = new float[sharedTupleCount, axisCount]; + + if (sharedTupleCount > 0 && axisCount > 0) + { + long tuplesPos = sharedTuplesOffset; + long tuplesLimit = glyphVariationDataArrayOffset; + long bytesPerTuple = (long)axisCount * 2; + long bytesAvailable = tuplesLimit - tuplesPos; + + long maxTuples = bytesAvailable > 0 + ? bytesAvailable / bytesPerTuple + : 0; + + int tuplesToRead = (int)Math.Min(sharedTupleCount, maxTuples); + + reader.Seek(tuplesPos, SeekOrigin.Begin); + + for (int i = 0; i < tuplesToRead; i++) + { + for (int j = 0; j < axisCount; j++) + { + sharedTuples[i, j] = reader.ReadF2Dot14(); + } + } + + // Any remaining tuples default to 0.0F. + } + + // GlyphVariationData tables + long glyphDataBase = glyphVariationDataArrayOffset; + GlyphVariationData[] glyphVariations = new GlyphVariationData[glyphCount]; + + // Reader is positioned at table start + long gvarEnd = gvarTableLength; + + GlyphVariationData empty = new([]); + for (int i = 0; i < glyphCount; i++) + { + long start = glyphDataBase + glyphVariationOffsets[i]; + long end = glyphDataBase + glyphVariationOffsets[i + 1]; // spec gives glyphCount+1 offsets + + // Validate range (must be within table and non-decreasing). + // Equal offsets mean the glyph has no variation data. + if (start == end || start < glyphDataBase || end < start || end > gvarEnd || start + 2 > gvarEnd) + { + glyphVariations[i] = empty; + continue; + } + + glyphVariations[i] = GlyphVariationData.Load(reader, start, axisCount); + } + + return new GVarTable(axisCount, glyphCount, sharedTuples, glyphVariations); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationData.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationData.cs new file mode 100644 index 0000000..1da2632 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationData.cs @@ -0,0 +1,325 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Implements loading glyph variation data structure. + /// + /// + internal class GlyphVariationData + { + /// + /// Mask for the low bits to give the number of tuple variation tables. + /// + internal const int CountMask = 0x0FFF; + + /// + /// Flag indicating that some or all tuple variation tables reference a shared set of "point" numbers. + /// These shared numbers are represented as packed point number data at the start of the serialized data. + /// + internal const int SharedPointNumbersMask = 0x8000; + + /// + /// Flag indicating that packed deltas are zero and omitted. Lower 6 bits give run count - 1. + /// + private const int DeltasAreZero = 0x80; + + /// + /// Flag indicating that packed deltas are 16-bit (int16). Lower 6 bits give run count - 1. + /// If neither nor is set, deltas are 8-bit (int8). + /// + private const int DeltasAreWords = 0x40; + + /// + /// Mask for the lower 6 bits of a delta run header, giving run count - 1. + /// + private const int DeltaRunCountMask = 0x3F; + + /// + /// Flag in the first byte of packed point numbers indicating that point numbers are 16-bit. + /// + private const int PointsAreWords = 0x80; + + /// + /// Mask for the lower 7 bits of a point run header, giving run count - 1. + /// + private const int PointRunCountMask = 0x7F; + + /// + /// Initializes a new instance of the class. + /// + /// The decoded tuple variation headers with their point indices and deltas. + public GlyphVariationData(TupleVariationHeader[] tupleHeaders) + => this.TupleHeaders = tupleHeaders; + + /// + /// Gets the tuple variation headers with their decoded point indices and deltas. + /// + public TupleVariationHeader[] TupleHeaders { get; } + + /// + /// Gets a value indicating whether this glyph has any variation data. + /// + public bool HasData => this.TupleHeaders.Length > 0; + + /// + /// Loads glyph variation data from the specified binary reader. + /// + /// The big-endian binary reader. + /// The byte offset from the start of the gvar table to this glyph's variation data. + /// The number of variation axes. + /// The . + public static GlyphVariationData Load(BigEndianBinaryReader reader, long offset, int axisCount) + { + // GlyphVariationData + // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +======================+===========================================+==============================================================================+ + // | uint16 | tupleVariationCount | A packed field. The high 4 bits are flags, | + // | | | and the low 12 bits are the number of tuple variation tables for this glyph. | + // | | | The count can be any number between 1 and 4095. | + // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+ + // | Offset16 | dataOffset | Offset from the start of the GlyphVariationData table to the serialized data.| + // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+ + // | TupleVariation | tupleVariationHeaders[tupleVariationCount]| Array of tuple variation headers. | + // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+ + // NOTE: 'offset' is relative to the start of the gvar table. + reader.Seek(offset, SeekOrigin.Begin); + ushort tupleVariationCount = reader.ReadUInt16(); + bool hasSharedPointNumbers = (tupleVariationCount & SharedPointNumbersMask) != 0; + int tupleCount = tupleVariationCount & CountMask; + + // Spec: dataOffset is Offset16 (always 16-bit), independent of the gvar offset array format. + // This offset is relative to the start of this GlyphVariationData table. + ushort serializedDataOffset = reader.ReadOffset16(); + + // Read all tuple variation headers first (they come before the serialized data). + TupleVariation[] tupleVariations = new TupleVariation[tupleCount]; + for (int i = 0; i < tupleCount; i++) + { + tupleVariations[i] = TupleVariation.Load(reader, axisCount); + } + + // Now read the serialized data that follows the headers. + long serializedDataPos = offset + serializedDataOffset; + reader.Seek(serializedDataPos, SeekOrigin.Begin); + + // If shared point numbers flag is set, decode them from the start of the serialized data. + ushort[]? sharedPointNumbers = null; + if (hasSharedPointNumbers) + { + sharedPointNumbers = DecodePackedPoints(reader); + } + + // Decode each tuple's serialized data (point numbers and deltas). + TupleVariationHeader[] tupleHeaders = new TupleVariationHeader[tupleCount]; + for (int i = 0; i < tupleCount; i++) + { + TupleVariation header = tupleVariations[i]; + long tupleDataStart = reader.BaseStream.Position; + + // Determine which point numbers this tuple uses. + ushort[]? pointNumbers; + if (header.HasPrivatePointNumbers) + { + pointNumbers = DecodePackedPoints(reader); + } + else + { + pointNumbers = sharedPointNumbers; + } + + // The number of deltas to decode depends on whether specific points are referenced. + // If pointNumbers is empty (length 0), deltas apply to all points and the count + // is determined by the caller (TransformPoints). We use VariationDataSize to bound reading. + int nPoints = pointNumbers is { Length: > 0 } ? pointNumbers.Length : 0; + + short[]? deltasX = null; + short[]? deltasY = null; + if (nPoints > 0) + { + deltasX = DecodePackedDeltas(reader, nPoints); + deltasY = DecodePackedDeltas(reader, nPoints); + } + else + { + // When no explicit points are specified, we need to read all remaining data + // for this tuple. The deltas apply to all glyph points + 4 phantom points. + // We cannot know the point count here, so we store the raw bytes and decode later. + // However, the simpler approach used by fontkit is to decode based on the remaining + // bytes in this tuple's data block. We'll defer full decoding to TransformPoints + // by storing the raw data range. + long bytesConsumed = reader.BaseStream.Position - tupleDataStart; + int remaining = header.VariationDataSize - (int)bytesConsumed; + if (remaining > 0) + { + // Store raw bytes for deferred decoding when we know the point count. + tupleHeaders[i] = new TupleVariationHeader(header, pointNumbers, null, null, reader.ReadBytes(remaining)); + continue; + } + } + + // Skip any remaining bytes for this tuple that we haven't consumed. + long consumed = reader.BaseStream.Position - tupleDataStart; + int skip = header.VariationDataSize - (int)consumed; + if (skip > 0) + { + reader.BaseStream.Position += skip; + } + + tupleHeaders[i] = new TupleVariationHeader(header, pointNumbers, deltasX, deltasY, null); + } + + return new GlyphVariationData(tupleHeaders); + } + + /// + /// Decodes packed point numbers from the serialized data. + /// + /// The binary reader positioned at the packed point data. + /// + /// An array of absolute point indices, or an empty array if all points are referenced. + /// + /// + internal static ushort[] DecodePackedPoints(BigEndianBinaryReader reader) + { + // First byte determines the count of points. + byte firstByte = reader.ReadByte(); + int count; + if ((firstByte & PointsAreWords) != 0) + { + // High bit set: count is ((firstByte & 0x7F) << 8) | nextByte. + count = ((firstByte & PointRunCountMask) << 8) | reader.ReadByte(); + } + else + { + count = firstByte; + } + + // A count of 0 means "all points" — return empty array as sentinel. + if (count == 0) + { + return []; + } + + // Read run-length encoded point number deltas. + ushort[] points = new ushort[count]; + int i = 0; + while (i < count) + { + byte runHeader = reader.ReadByte(); + bool runPointsAreWords = (runHeader & PointsAreWords) != 0; + int runCount = (runHeader & PointRunCountMask) + 1; + + ushort accumulator = i > 0 ? points[i - 1] : (ushort)0; + for (int j = 0; j < runCount && i < count; j++, i++) + { + ushort delta = runPointsAreWords ? reader.ReadUInt16() : reader.ReadByte(); + accumulator += delta; + points[i] = accumulator; + } + } + + return points; + } + + /// + /// Decodes packed delta values from the serialized data. + /// + /// The binary reader positioned at the packed delta data. + /// The number of delta values to decode. + /// An array of decoded delta values. + /// + internal static short[] DecodePackedDeltas(BigEndianBinaryReader reader, int count) + { + short[] deltas = new short[count]; + int i = 0; + while (i < count) + { + byte runHeader = reader.ReadByte(); + bool areZero = (runHeader & DeltasAreZero) != 0; + bool areWords = (runHeader & DeltasAreWords) != 0; + int runCount = (runHeader & DeltaRunCountMask) + 1; + + for (int j = 0; j < runCount && i < count; j++, i++) + { + if (areZero) + { + deltas[i] = 0; + } + else if (areWords) + { + deltas[i] = reader.ReadInt16(); + } + else + { + deltas[i] = (short)(sbyte)reader.ReadByte(); + } + } + } + + return deltas; + } + } + + /// + /// Represents a fully decoded tuple variation header with its associated point indices and delta values. + /// + internal class TupleVariationHeader + { + /// + /// Initializes a new instance of the class. + /// + /// The tuple variation header containing peak coordinates and flags. + /// The point indices this tuple applies to, or null/empty for all points. + /// The X coordinate deltas, or null if deferred. + /// The Y coordinate deltas, or null if deferred. + /// The raw serialized delta data for deferred decoding, or null if already decoded. + public TupleVariationHeader( + TupleVariation tupleVariation, + ushort[]? pointNumbers, + short[]? deltasX, + short[]? deltasY, + byte[]? rawDeltaData) + { + this.TupleVariation = tupleVariation; + this.PointNumbers = pointNumbers; + this.DeltasX = deltasX; + this.DeltasY = deltasY; + this.RawDeltaData = rawDeltaData; + } + + /// + /// Gets the tuple variation header containing peak coordinates and flags. + /// + public TupleVariation TupleVariation { get; } + + /// + /// Gets the point indices this tuple applies to. + /// An empty array means all points are referenced. + /// Null means no point data was available. + /// + public ushort[]? PointNumbers { get; } + + /// + /// Gets the X coordinate deltas for the referenced points. + /// Null when deltas apply to all points and were deferred (see ). + /// + public short[]? DeltasX { get; } + + /// + /// Gets the Y coordinate deltas for the referenced points. + /// Null when deltas apply to all points and were deferred (see ). + /// + public short[]? DeltasY { get; } + + /// + /// Gets the raw serialized delta data for deferred decoding. + /// This is used when point numbers indicate "all points" and the actual point count + /// is not known until is called. + /// + public byte[]? RawDeltaData { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationProcessor.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationProcessor.cs new file mode 100644 index 0000000..f8d8103 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationProcessor.cs @@ -0,0 +1,1114 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using SixLabors.Fonts.Tables.TrueType.Glyphs; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// + /// This class transforms TrueType glyphs according to the data from + /// the OpenType variation tables (fvar, gvar, avar, HVAR, VVAR). + /// These tables allow infinite adjustments to glyph weight, width, slant, + /// and optical size without the designer needing to specify every exact style. + /// + /// Implementation is based on fontkit: + /// Docs for the item variations: + /// + internal class GlyphVariationProcessor + { + /// + /// The item variation store shared by CFF2 and other variation lookups. + /// + private readonly ItemVariationStore? itemStore; + + /// + /// The font variations table defining variation axes. + /// + private readonly FVarTable fvar; + + /// + /// The optional axis variations table for axis normalization remapping. + /// + private readonly AVarTable? avar; + + /// + /// The optional glyph variation table with per-glyph deltas. + /// + private readonly GVarTable? gVar; + + /// + /// The optional horizontal metrics variations table. + /// + private readonly HVarTable? hVar; + + /// + /// The optional vertical metrics variations table. + /// + private readonly VVarTable? vVar; + + /// + /// The optional metrics variations table for global font values. + /// + private readonly MVarTable? mVar; + + /// + /// The optional CVT variations table for hinting. + /// + private readonly CVarTable? cVar; + + /// + /// The normalized variation coordinates for the current font instance, one per axis. + /// + private readonly float[] normalizedCoords; + + /// + /// Cache of computed blend vectors, keyed by instance. + /// + private readonly ConcurrentDictionary blendVectors; + + /// + /// Cached CVT values with cvar deltas applied, keyed by the base CVT array. + /// Computed once per unique base CVT since the result depends only on + /// normalized coordinates and the base values, not on the glyph. + /// + private readonly ConcurrentDictionary cvtCache = new(ReferenceEqualityComparer.Instance); + + /// + /// Initializes a new instance of the class. + /// + /// The optional shared item variation store (used by CFF2). + /// The font variations table defining axes. Required. + /// The optional axis variations table for normalization remapping. + /// The optional glyph variation table. + /// The optional horizontal metrics variations table. + /// The optional vertical metrics variations table. + /// The optional global metrics variations table. + /// The optional CVT variations table. + /// The optional user-specified axis values in design space. + public GlyphVariationProcessor( + ItemVariationStore? itemStore, + FVarTable fVar, + AVarTable? aVar = null, + GVarTable? gVar = null, + HVarTable? hVar = null, + VVarTable? vVar = null, + MVarTable? mVar = null, + CVarTable? cVar = null, + float[]? userCoordinates = null) + { + DebugGuard.NotNull(fVar, nameof(fVar)); + + this.itemStore = itemStore; + this.fvar = fVar; + this.avar = aVar; + this.gVar = gVar; + this.hVar = hVar; + this.vVar = vVar; + this.mVar = mVar; + this.cVar = cVar; + this.normalizedCoords = this.NormalizeCoords(userCoordinates); + this.blendVectors = new(); + } + + /// + /// Gets the normalized variation coordinates for this processor instance. + /// Used by FeatureVariations condition evaluation. + /// + internal float[] NormalizedCoordinates => this.normalizedCoords; + + /// + /// Transforms glyph outline points by applying gvar variation deltas. + /// + /// The glyph identifier. + /// The glyph vector whose control points will be modified in-place. + public void TransformPoints(ushort glyphId, ref GlyphVector glyphPoints) + { + if (this.gVar is null) + { + return; + } + + if (glyphId >= this.gVar.GlyphCount) + { + return; + } + + GlyphVariationData variationData = this.gVar.GlyphVariations[glyphId]; + if (!variationData.HasData) + { + return; + } + + if (glyphPoints.IsComposite && glyphPoints.CompositeComponents is not null) + { + this.TransformCompositePoints(variationData, ref glyphPoints); + } + else + { + this.TransformSimplePoints(variationData, ref glyphPoints); + } + } + + /// + /// Transforms a simple (non-composite) glyph by applying gvar variation deltas to its outline points. + /// + /// The glyph's variation data from the gvar table. + /// The glyph vector whose control points will be modified in-place. + private void TransformSimplePoints(GlyphVariationData variationData, ref GlyphVector glyphPoints) + { + IList controlPoints = glyphPoints.ControlPoints; + int pointCount = controlPoints.Count; + + // gvar encodes deltas for outline points + 4 phantom points (LSB, advance width, + // TSB, advance height). We must decode all of them so X/Y delta streams stay aligned, + // even though we only apply deltas to the outline points. + const int PhantomPointCount = 4; + int totalPointCount = pointCount + PhantomPointCount; + + // Clone the original points for IUP reference (interpolation needs unmodified originals). + GlyphVector originPoints = GlyphVector.DeepClone(glyphPoints); + IList origPoints = originPoints.ControlPoints; + + foreach (TupleVariationHeader tupleHeader in variationData.TupleHeaders) + { + float factor = this.ResolveTupleFactor(tupleHeader); + if (factor == 0) + { + continue; + } + + // Resolve point numbers and deltas. + ushort[]? pointNumbers = tupleHeader.PointNumbers; + short[]? deltasX = tupleHeader.DeltasX; + short[]? deltasY = tupleHeader.DeltasY; + + // If deltas were deferred (all-points case), decode them now that we know the point count. + // Use totalPointCount (outline + phantom) so Y deltas start at the correct stream offset. + if (deltasX is null && tupleHeader.RawDeltaData is not null) + { + DecodeAllPointDeltas(tupleHeader.RawDeltaData, totalPointCount, out deltasX, out deltasY); + } + + if (deltasX is null || deltasY is null) + { + continue; + } + + bool allPoints = pointNumbers is null or { Length: 0 }; + + if (allPoints) + { + // Deltas apply to all points directly. Only apply to outline points (skip phantom). + int deltaCount = Math.Min(deltasX.Length, pointCount); + for (int i = 0; i < deltaCount; i++) + { + ControlPoint cp = controlPoints[i]; + cp.Point.X += MathF.Round(deltasX[i] * factor); + cp.Point.Y += MathF.Round(deltasY[i] * factor); + controlPoints[i] = cp; + } + } + else + { + // Deltas apply to specific points only; interpolate the rest. + using Buffer adjustXBuf = new(pointCount, clear: true); + using Buffer adjustYBuf = new(pointCount, clear: true); + using Buffer hasDeltaBuf = new(pointCount, clear: true); + Span adjustX = adjustXBuf.GetSpan(); + Span adjustY = adjustYBuf.GetSpan(); + Span hasDelta = hasDeltaBuf.GetSpan(); + + for (int i = 0; i < pointNumbers!.Length && i < deltasX.Length; i++) + { + int ptIdx = pointNumbers[i]; + if (ptIdx < pointCount) + { + hasDelta[ptIdx] = 1; + + // Round before IUP interpolation to match fontkit / FreeType behavior. + // IUP references rounded absolute positions, so rounding must happen first. + adjustX[ptIdx] = MathF.Round(deltasX[i] * factor); + adjustY[ptIdx] = MathF.Round(deltasY[i] * factor); + } + } + + // Interpolate unreferenced points. + InterpolateMissingDeltas( + controlPoints, + origPoints, + glyphPoints.EndPoints, + adjustX, + adjustY, + hasDelta); + + // Apply the accumulated deltas (already rounded for explicit points, + // IUP-interpolated for implicit points). + for (int i = 0; i < pointCount; i++) + { + ControlPoint cp = controlPoints[i]; + cp.Point.X += adjustX[i]; + cp.Point.Y += adjustY[i]; + controlPoints[i] = cp; + } + } + } + + // Recalculate bounds from the transformed points. + glyphPoints.Bounds = CalculateBounds(controlPoints); + } + + /// + /// Transforms a composite glyph by applying gvar deltas to component offsets. + /// For composite glyphs, gvar stores deltas for a synthetic point array: + /// one point per component (at the component's offset) plus 4 phantom points. + /// After applying deltas, the offset changes are propagated to all assembled + /// outline points belonging to each component. + /// + private void TransformCompositePoints(GlyphVariationData variationData, ref GlyphVector glyphPoints) + { + CompositeComponent[] components = glyphPoints.CompositeComponents!; + int componentCount = components.Length; + + // gvar "point count" for composites = number of components + 4 phantom points. + int syntheticPointCount = componentCount + 4; + + // Build synthetic points from component offsets. + using Buffer synXBuf = new(syntheticPointCount, clear: true); + using Buffer synYBuf = new(syntheticPointCount, clear: true); + Span synX = synXBuf.GetSpan(); + Span synY = synYBuf.GetSpan(); + + for (int i = 0; i < componentCount; i++) + { + synX[i] = components[i].Dx; + synY[i] = components[i].Dy; + } + + // Phantom points (LSB, advance width, TSB, advance height) are initialized to 0 + // and will receive deltas from gvar if present. + + // Apply each tuple's deltas to the synthetic points. + foreach (TupleVariationHeader tupleHeader in variationData.TupleHeaders) + { + float factor = this.ResolveTupleFactor(tupleHeader); + if (factor == 0) + { + continue; + } + + ushort[]? pointNumbers = tupleHeader.PointNumbers; + short[]? deltasX = tupleHeader.DeltasX; + short[]? deltasY = tupleHeader.DeltasY; + + if (deltasX is null && tupleHeader.RawDeltaData is not null) + { + DecodeAllPointDeltas(tupleHeader.RawDeltaData, syntheticPointCount, out deltasX, out deltasY); + } + + if (deltasX is null || deltasY is null) + { + continue; + } + + bool allPoints = pointNumbers is null or { Length: 0 }; + + if (allPoints) + { + int deltaCount = Math.Min(deltasX.Length, syntheticPointCount); + for (int i = 0; i < deltaCount; i++) + { + synX[i] += deltasX[i] * factor; + synY[i] += deltasY[i] * factor; + } + } + else + { + for (int i = 0; i < pointNumbers!.Length && i < deltasX.Length; i++) + { + int ptIdx = pointNumbers[i]; + if (ptIdx < syntheticPointCount) + { + synX[ptIdx] += deltasX[i] * factor; + synY[ptIdx] += deltasY[i] * factor; + } + } + } + } + + // Propagate offset changes to assembled outline points. + IList controlPoints = glyphPoints.ControlPoints; + int pointOffset = 0; + for (int c = 0; c < componentCount; c++) + { + float deltaX = MathF.Round(synX[c] - components[c].Dx); + float deltaY = MathF.Round(synY[c] - components[c].Dy); + + if (deltaX != 0 || deltaY != 0) + { + int end = pointOffset + components[c].PointCount; + for (int p = pointOffset; p < end && p < controlPoints.Count; p++) + { + ControlPoint cp = controlPoints[p]; + cp.Point.X += deltaX; + cp.Point.Y += deltaY; + controlPoints[p] = cp; + } + } + + pointOffset += components[c].PointCount; + } + + // Recalculate bounds from the transformed points. + glyphPoints.Bounds = CalculateBounds(controlPoints); + } + + /// + /// Gets the horizontal advance width adjustment for the given glyph from the HVAR table. + /// Returns 0 if no HVAR table is present. + /// + /// The glyph identifier. + /// The advance width delta value. + public float AdvanceAdjustment(int glyphId) + { + if (this.hVar is null) + { + return 0; + } + + return this.GetMetricDelta(glyphId, this.hVar.AdvanceWidthMapping, this.hVar.ItemVariationStore); + } + + /// + /// Gets the vertical advance height adjustment for the given glyph from the VVAR table. + /// Returns 0 if no VVAR table is present. + /// + /// The glyph identifier. + /// The advance height delta value. + public float VerticalAdvanceAdjustment(int glyphId) + { + if (this.vVar is null) + { + return 0; + } + + return this.GetMetricDelta(glyphId, this.vVar.AdvanceWidthMapping, this.vVar.ItemVariationStore); + } + + /// + /// Gets the delta adjustment for a global font metric from the MVAR table. + /// Returns 0 if no MVAR table is present or the tag is not found. + /// + /// The MVAR metric tag (e.g. 'hasc', 'hdsc'). + /// The metric delta value. + public float GetMVarDelta(Tag tag) + { + if (this.mVar is null) + { + return 0; + } + + if (!this.mVar.TryGetIndices(tag, out ushort outerIndex, out ushort innerIndex)) + { + return 0; + } + + return this.ComputeDelta(this.mVar.ItemVariationStore, outerIndex, innerIndex); + } + + /// + /// Applies cvar (CVT Variations) deltas to the base CVT values. + /// The result is computed once and cached, since cvar deltas depend only on + /// normalized axis coordinates, not on the glyph being processed. + /// Returns an adjusted copy of the CVT values with variation deltas applied, + /// or null if there is no cvar data. + /// + /// The base CVT values from the cvt table. + /// The varied CVT values, or null if no cvar table is present. + public short[]? ApplyCvtDeltas(short[] baseCvt) + { + if (this.cVar is null || this.cVar.TupleVariations.Length == 0) + { + return null; + } + + return this.cvtCache.GetOrAdd(baseCvt, this.ComputeCvtDeltas); + } + + /// + /// Computes the CVT values with cvar deltas applied for the given base CVT array. + /// + /// The base CVT values from the cvt table. + /// A new array of CVT values with variation deltas applied. + private short[] ComputeCvtDeltas(short[] baseCvt) + { + // Work on a copy so we don't modify the original CVT values. + short[] varied = new short[baseCvt.Length]; + Array.Copy(baseCvt, varied, baseCvt.Length); + + foreach (CVarTupleVariation cvarTuple in this.cVar!.TupleVariations) + { + TupleVariation tuple = cvarTuple.TupleVariation; + + // cvar always has embedded peak coordinates (per spec). + float[]? peakCoords = tuple.EmbeddedPeak; + if (peakCoords is null) + { + continue; + } + + float factor = this.TupleFactor( + tuple.IsIntermediateRegion, + peakCoords, + tuple.IntermediateStartRegion, + tuple.IntermediateEndRegion); + + if (factor == 0) + { + continue; + } + + short[]? deltas = cvarTuple.Deltas; + ushort[]? pointNumbers = cvarTuple.PointNumbers; + + // Handle deferred decoding for "all points" case. + if (deltas is null && cvarTuple.RawDeltaData is not null) + { + using MemoryStream ms = new(cvarTuple.RawDeltaData); + using BigEndianBinaryReader deltaReader = new(ms, false); + deltas = GlyphVariationData.DecodePackedDeltas(deltaReader, baseCvt.Length); + } + + if (deltas is null) + { + continue; + } + + bool allPoints = pointNumbers is null or { Length: 0 }; + if (allPoints) + { + // Deltas apply to all CVT entries. + int count = Math.Min(deltas.Length, varied.Length); + for (int i = 0; i < count; i++) + { + varied[i] += (short)MathF.Round(deltas[i] * factor); + } + } + else + { + // Deltas apply to specific CVT indices. + for (int i = 0; i < pointNumbers!.Length && i < deltas.Length; i++) + { + int idx = pointNumbers[i]; + if (idx < varied.Length) + { + varied[idx] += (short)MathF.Round(deltas[i] * factor); + } + } + } + } + + return varied; + } + + /// + /// Computes the blend vector for the given outer index in the item variation store. + /// Used by the CFF2 blend operator. + /// + /// The outer index into the item variation store. + /// An array of blend scalars, one per region. + public float[] BlendVector(int outerIndex) + { + if (this.itemStore is null) + { + return []; + } + + return this.GetOrComputeBlendVector(this.itemStore, outerIndex); + } + + /// + /// Computes the delta adjustment for a specific item in the item variation store. + /// + /// The outer index. + /// The inner index. + /// The delta value. + internal float Delta(int outerIndex, int innerIndex) + { + if (this.itemStore is null) + { + return 0; + } + + return this.ComputeDelta(this.itemStore, outerIndex, innerIndex); + } + + /// + /// Computes the delta adjustment for a specific item using an external ItemVariationStore. + /// Used for GDEF-based variation deltas in GPOS/GSUB device tables. + /// + /// The external ItemVariationStore (e.g. from GDEF). + /// The outer index. + /// The inner index. + /// The delta value. + internal float Delta(ItemVariationStore store, int outerIndex, int innerIndex) + => this.ComputeDelta(store, outerIndex, innerIndex); + + /// + /// Computes a delta from a given ItemVariationStore using cached blend vectors. + /// Shared by HVAR, VVAR, MVAR, and CFF2 delta lookups. + /// + private float ComputeDelta(ItemVariationStore store, int outerIndex, int innerIndex) + { + if (outerIndex >= store.ItemVariations.Length) + { + return 0; + } + + ItemVariationData variationData = store.ItemVariations[outerIndex]; + if (innerIndex >= variationData.DeltaSets.Length) + { + return 0; + } + + DeltaSet deltaSet = variationData.DeltaSets[innerIndex]; + float[] blendVector = this.GetOrComputeBlendVector(store, outerIndex); + float netAdjustment = 0; + for (int master = 0; master < variationData.RegionIndexes.Length; master++) + { + netAdjustment += deltaSet.Deltas[master] * blendVector[master]; + } + + return netAdjustment; + } + + /// + /// Gets or computes the blend vector for a given outer index in the specified ItemVariationStore. + /// Results are cached by ItemVariationData instance. + /// + private float[] GetOrComputeBlendVector(ItemVariationStore store, int outerIndex) + { + ItemVariationData variationData = store.ItemVariations[outerIndex]; + return this.blendVectors.GetOrAdd(variationData, _ => this.ComputeBlendVector(store, variationData)); + } + + /// + /// Computes the blend vector for the given item variation data by evaluating the region scalars + /// against the normalized coordinates. + /// + /// The item variation store containing the region list. + /// The item variation data subtable whose regions to evaluate. + /// An array of blend scalars, one per region index. + private float[] ComputeBlendVector(ItemVariationStore store, ItemVariationData variationData) + { + float[] blendVector = new float[variationData.RegionIndexes.Length]; + for (int i = 0; i < variationData.RegionIndexes.Length; i++) + { + float scalar = 1.0f; + ushort regionIndex = variationData.RegionIndexes[i]; + RegionAxisCoordinates[] axes = store.VariationRegionList.VariationRegions[regionIndex]; + + for (int j = 0; j < axes.Length; j++) + { + RegionAxisCoordinates axis = axes[j]; + + float axisScalar; + if (axis.StartCoord > axis.PeakCoord || axis.PeakCoord > axis.EndCoord) + { + axisScalar = 1; + } + else if (axis.StartCoord < 0 && axis.EndCoord > 0 && axis.PeakCoord != 0) + { + axisScalar = 1; + } + else if (axis.PeakCoord == 0) + { + axisScalar = 1; + } + else if (this.normalizedCoords[j] < axis.StartCoord || this.normalizedCoords[j] > axis.EndCoord) + { + axisScalar = 0; + } + else + { + if (this.normalizedCoords[j] == axis.PeakCoord) + { + axisScalar = 1; + } + else if (this.normalizedCoords[j] < axis.PeakCoord) + { + axisScalar = (this.normalizedCoords[j] - axis.StartCoord) / + (axis.PeakCoord - axis.StartCoord); + } + else + { + axisScalar = (axis.EndCoord - this.normalizedCoords[j]) / + (axis.EndCoord - axis.PeakCoord); + } + } + + scalar *= axisScalar; + } + + blendVector[i] = scalar; + } + + return blendVector; + } + + /// + /// Resolves peak coordinates and computes the tuple factor for a given tuple header. + /// Shared helper used by both simple and composite glyph variation paths. + /// + /// The tuple variation header. + /// The blending factor, or 0 if the tuple should be skipped. + private float ResolveTupleFactor(TupleVariationHeader tupleHeader) + { + TupleVariation tuple = tupleHeader.TupleVariation; + + // Resolve peak coordinates: either embedded or from shared tuples. + float[]? peakCoords = tuple.EmbeddedPeak; + if (peakCoords is null) + { + int sharedIdx = tuple.SharedTupleIndex; + if (sharedIdx >= this.gVar!.SharedTuples.GetLength(0)) + { + return 0; + } + + peakCoords = new float[this.gVar.AxisCount]; + for (int a = 0; a < this.gVar.AxisCount; a++) + { + peakCoords[a] = this.gVar.SharedTuples[sharedIdx, a]; + } + } + + return this.TupleFactor( + tuple.IsIntermediateRegion, + peakCoords, + tuple.IntermediateStartRegion, + tuple.IntermediateEndRegion); + } + + /// + /// Calculates the blending factor for a gvar tuple variation based on normalized coordinates. + /// + /// Whether this is an intermediate tuple with explicit start/end bounds. + /// The peak coordinates for this tuple. + /// The start coordinates (only for intermediate tuples). + /// The end coordinates (only for intermediate tuples). + /// A scalar factor in the range [0, 1] indicating how much this tuple contributes. + private float TupleFactor(bool isIntermediate, float[] peakCoords, float[]? startCoords, float[]? endCoords) + { + float factor = 1.0f; + + for (int i = 0; i < this.normalizedCoords.Length && i < peakCoords.Length; i++) + { + if (peakCoords[i] == 0) + { + // This axis doesn't affect this tuple. + continue; + } + + if (this.normalizedCoords[i] == 0) + { + // Normalized coordinate is at default; this tuple has no effect. + return 0; + } + + if (!isIntermediate) + { + // Non-intermediate tuple: simple linear interpolation. + // The valid range is between 0 and the peak coordinate. + float minVal = MathF.Min(0, peakCoords[i]); + float maxVal = MathF.Max(0, peakCoords[i]); + + if (this.normalizedCoords[i] < minVal || this.normalizedCoords[i] > maxVal) + { + return 0; + } + + factor *= this.normalizedCoords[i] / peakCoords[i]; + } + else + { + // Intermediate tuple: piecewise linear between start → peak → end. + if (this.normalizedCoords[i] < startCoords![i] || this.normalizedCoords[i] > endCoords![i]) + { + return 0; + } + + if (this.normalizedCoords[i] < peakCoords[i]) + { + factor *= (this.normalizedCoords[i] - startCoords[i]) / + (peakCoords[i] - startCoords[i]); + } + else if (this.normalizedCoords[i] > peakCoords[i]) + { + factor *= (endCoords![i] - this.normalizedCoords[i]) / + (endCoords[i] - peakCoords[i]); + } + + // If exactly at peak, factor contribution is 1 (no change). + } + } + + return factor; + } + + /// + /// Normalizes axis coordinates to the [-1, 1] range and applies avar remapping if present. + /// + /// + /// Optional user-specified axis values in design space (e.g. weight=700). + /// If null, default axis values are used. + /// + /// An array of normalized coordinates for each axis. + private float[] NormalizeCoords(float[]? userCoordinates) + { + int axisCount = this.fvar.AxisCount; + + // Use Buffer for temporary coords to avoid heap allocation. + using Buffer coordsBuf = new(axisCount); + Span coords = coordsBuf.GetSpan(); + + // Use user coordinates if provided, otherwise use defaults. + for (int i = 0; i < axisCount; i++) + { + VariationAxisRecord axis = this.fvar.Axes[i]; + if (userCoordinates is not null && i < userCoordinates.Length) + { + // Clamp to valid axis range. + coords[i] = Math.Clamp(userCoordinates[i], axis.MinValue, axis.MaxValue); + } + else + { + coords[i] = axis.DefaultValue; + } + } + + // The default mapping is linear along each axis, in two segments: + // from the minValue to defaultValue, and from defaultValue to maxValue. + float[] normalized = new float[axisCount]; + for (int i = 0; i < axisCount; i++) + { + VariationAxisRecord axis = this.fvar.Axes[i]; + if (coords[i] < axis.DefaultValue) + { + float denominator = axis.DefaultValue - axis.MinValue; + normalized[i] = denominator > 0 + ? (coords[i] - axis.DefaultValue) / denominator + : 0; + } + else + { + float denominator = axis.MaxValue - axis.DefaultValue; + normalized[i] = denominator > 0 + ? (coords[i] - axis.DefaultValue) / denominator + : 0; + } + } + + // If there is an avar table, the normalized value is remapped + // by interpolating between the two nearest mapped values. + if (this.avar is not null) + { + int segmentCount = Math.Min(this.avar.SegmentMaps.Length, axisCount); + for (int i = 0; i < segmentCount; i++) + { + SegmentMapRecord segment = this.avar.SegmentMaps[i]; + for (int j = 0; j < segment.AxisValueMap.Length; j++) + { + AxisValueMapRecord pair = segment.AxisValueMap[j]; + if (j >= 1 && normalized[i] < pair.FromCoordinate) + { + AxisValueMapRecord prev = segment.AxisValueMap[j - 1]; + float fromDelta = pair.FromCoordinate - prev.FromCoordinate; + if (fromDelta > 0) + { + normalized[i] = (((normalized[i] - prev.FromCoordinate) * (pair.ToCoordinate - prev.ToCoordinate)) / + fromDelta) + prev.ToCoordinate; + } + + break; + } + } + } + } + + return normalized; + } + + /// + /// Gets the metric delta for a given glyph by resolving the delta-set index mapping + /// and computing the delta from the item variation store. + /// + /// The glyph identifier. + /// The optional delta-set index mapping array. + /// The item variation store containing the delta data. + /// The computed delta value. + private float GetMetricDelta(int glyphId, DeltaSetIndexMap[]? mapping, ItemVariationStore store) + { + int outerIndex; + int innerIndex; + if (mapping is { Length: > 0 }) + { + int idx = Math.Min(glyphId, mapping.Length - 1); + outerIndex = mapping[idx].OuterIndex; + innerIndex = mapping[idx].InnerIndex; + } + else + { + outerIndex = 0; + innerIndex = glyphId; + } + + return this.ComputeDelta(store, outerIndex, innerIndex); + } + + /// + /// Decodes deferred delta data for the all-points case. + /// + private static void DecodeAllPointDeltas(byte[] rawData, int pointCount, out short[]? deltasX, out short[]? deltasY) + { + using MemoryStream ms = new(rawData); + using BigEndianBinaryReader reader = new(ms, false); + deltasX = GlyphVariationData.DecodePackedDeltas(reader, pointCount); + deltasY = GlyphVariationData.DecodePackedDeltas(reader, pointCount); + } + + /// + /// Interpolates deltas for points that don't have explicit delta values. + /// Processes each contour independently. + /// + private static void InterpolateMissingDeltas( + IList points, + IList origPoints, + IReadOnlyList endPoints, + Span adjustX, + Span adjustY, + Span hasDelta) + { + if (points.Count == 0 || endPoints.Count == 0) + { + return; + } + + int contourStart = 0; + for (int c = 0; c < endPoints.Count; c++) + { + int contourEnd = endPoints[c]; + + // Find first point with a delta in this contour. + int firstDelta = -1; + for (int p = contourStart; p <= contourEnd; p++) + { + if (hasDelta[p] != 0) + { + firstDelta = p; + break; + } + } + + if (firstDelta < 0) + { + // No deltas in this contour, skip. + contourStart = contourEnd + 1; + continue; + } + + int curDelta = firstDelta; + int p2 = firstDelta + 1; + while (p2 <= contourEnd) + { + if (hasDelta[p2] != 0) + { + // Interpolate the gap between curDelta and p2. + DeltaInterpolate(curDelta + 1, p2 - 1, curDelta, p2, origPoints, adjustX, adjustY); + curDelta = p2; + } + + p2++; + } + + if (curDelta == firstDelta) + { + // Only one delta point in this contour: shift all other points by the same amount. + DeltaShift(contourStart, contourEnd, curDelta, adjustX, adjustY); + } + else + { + // Interpolate remaining points that wrap around the contour boundary. + // Points after the last delta point to end of contour, and start of contour to first delta. + DeltaInterpolate(curDelta + 1, contourEnd, curDelta, firstDelta, origPoints, adjustX, adjustY); + if (firstDelta > contourStart) + { + DeltaInterpolate(contourStart, firstDelta - 1, curDelta, firstDelta, origPoints, adjustX, adjustY); + } + } + + contourStart = contourEnd + 1; + } + } + + /// + /// Interpolates delta values for points between two reference points. + /// Handles X and Y independently using linear interpolation with clamping. + /// + private static void DeltaInterpolate( + int p1, + int p2, + int ref1, + int ref2, + IList origPoints, + Span adjustX, + Span adjustY) + { + if (p1 > p2) + { + return; + } + + // Process X axis. + InterpolateAxis(p1, p2, ref1, ref2, origPoints, adjustX, isX: true); + + // Process Y axis. + InterpolateAxis(p1, p2, ref1, ref2, origPoints, adjustY, isX: false); + } + + /// + /// Interpolates delta values for points between two reference points along a single axis (X or Y). + /// Uses linear interpolation with clamping per the OpenType IUP algorithm. + /// + /// The first point index to interpolate (inclusive). + /// The last point index to interpolate (inclusive). + /// The first reference point index with a known delta. + /// The second reference point index with a known delta. + /// The original (unmodified) control points for coordinate reference. + /// The delta adjustment array to populate. + /// Whether to interpolate the X axis; if false, interpolates Y. + private static void InterpolateAxis( + int p1, + int p2, + int ref1, + int ref2, + IList origPoints, + Span adjust, + bool isX) + { + float in1 = isX ? origPoints[ref1].Point.X : origPoints[ref1].Point.Y; + float in2 = isX ? origPoints[ref2].Point.X : origPoints[ref2].Point.Y; + float out1 = in1 + adjust[ref1]; + float out2 = in2 + adjust[ref2]; + + // Ensure in1 <= in2 for interpolation. + if (in1 > in2) + { + (in1, in2) = (in2, in1); + (out1, out2) = (out2, out1); + } + + // Per the OpenType spec / FreeType: if the two reference points have the same + // input coordinate but different output coordinates, the inferred delta is zero. + if (in1 == in2 && out1 != out2) + { + return; + } + + float scale = in1 == in2 ? 0 : (out2 - out1) / (in2 - in1); + + for (int p = p1; p <= p2; p++) + { + float inVal = isX ? origPoints[p].Point.X : origPoints[p].Point.Y; + + float outVal; + if (inVal <= in1) + { + outVal = inVal + (out1 - in1); + } + else if (inVal >= in2) + { + outVal = inVal + (out2 - in2); + } + else + { + outVal = out1 + ((inVal - in1) * scale); + } + + adjust[p] = outVal - inVal; + } + } + + /// + /// Shifts all points in a contour range by the same delta as the reference point. + /// Used when only one point in a contour has an explicit delta. + /// + private static void DeltaShift(int p1, int p2, int refPoint, Span adjustX, Span adjustY) + { + float deltaX = adjustX[refPoint]; + float deltaY = adjustY[refPoint]; + + if (deltaX == 0 && deltaY == 0) + { + return; + } + + for (int p = p1; p <= p2; p++) + { + if (p != refPoint) + { + adjustX[p] = deltaX; + adjustY[p] = deltaY; + } + } + } + + /// + /// Calculates the bounding box of the given control points. + /// + /// The control points. + /// The bounding box encompassing all control points. + private static Bounds CalculateBounds(IList points) + { + if (points.Count == 0) + { + return default; + } + + float minX = float.MaxValue; + float minY = float.MaxValue; + float maxX = float.MinValue; + float maxY = float.MinValue; + + for (int i = 0; i < points.Count; i++) + { + Vector2 pt = points[i].Point; + if (pt.X < minX) + { + minX = pt.X; + } + + if (pt.Y < minY) + { + minY = pt.Y; + } + + if (pt.X > maxX) + { + maxX = pt.X; + } + + if (pt.Y > maxY) + { + maxY = pt.Y; + } + } + + return new Bounds(minX, minY, maxX, maxY); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/HVarTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/HVarTable.cs new file mode 100644 index 0000000..4836cd0 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/HVarTable.cs @@ -0,0 +1,125 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Implements reading the font variations table `HVAR`. + /// The HVAR table is used in variable fonts to provide variations for horizontal glyph metrics values. + /// This can be used to provide variation data for advance widths in the 'hmtx' table. + /// + /// + internal class HVarTable : Table + { + /// + /// The table name identifier for the HVAR table. + /// + internal const string TableName = "HVAR"; + + /// + /// Initializes a new instance of the class. + /// + /// The item variation store containing delta data. + /// The optional delta-set index mapping for advance widths. + /// The optional delta-set index mapping for left side bearings. + /// The optional delta-set index mapping for right side bearings. + public HVarTable( + ItemVariationStore itemVariationStore, + DeltaSetIndexMap[]? advanceWidthMapping, + DeltaSetIndexMap[]? lsbMapping, + DeltaSetIndexMap[]? rsbMapping) + { + this.ItemVariationStore = itemVariationStore; + this.AdvanceWidthMapping = advanceWidthMapping; + this.LsbMapping = lsbMapping; + this.RsbMapping = rsbMapping; + } + + /// + /// Gets the item variation store containing the variation delta data. + /// + public ItemVariationStore ItemVariationStore { get; } + + /// + /// Gets the optional delta-set index mapping for advance widths. + /// + public DeltaSetIndexMap[]? AdvanceWidthMapping { get; } + + /// + /// Gets the optional delta-set index mapping for left side bearings. + /// + public DeltaSetIndexMap[]? LsbMapping { get; } + + /// + /// Gets the optional delta-set index mapping for right side bearings. + /// + public DeltaSetIndexMap[]? RsbMapping { get; } + + /// + /// Loads the HVAR table from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static HVarTable? Load(FontReader reader) + { + if (!reader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + /// + /// Loads the HVAR table from the specified binary reader. + /// + /// The big-endian binary reader positioned at the start of the HVAR table. + /// The . + public static HVarTable Load(BigEndianBinaryReader reader) + { + // Horizontal metrics variations table + // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==========================+========================================+=========================================================================+ + // | uint16 | majorVersion | Major version number of the font variations table — set to 1. | + // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+ + // | uint16 | minorVersion | Minor version number of the font variations table — set to 0. | + // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Offset32 | itemVariationStoreOffset | Offset in bytes from the start of this table to the | + // | | | item variation store table. | + // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Offset32 | advanceWidthMappingOffset | Offset in bytes from the start of this table to the delta-set index | + // | | | mapping for advance widths (may be NULL). | + // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Offset32 | lsbMappingOffset | Offset in bytes from the start of this table to the delta-set index | + // | | | mapping for left side bearings (may be NULL). | + // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Offset32 | rsbMappingOffset | Offset in bytes from the start of this table to the delta-set index | + // | | | mapping for right side bearings (may be NULL). | + // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+ + ushort major = reader.ReadUInt16(); + ushort minor = reader.ReadUInt16(); + uint itemVariationStoreOffset = reader.ReadOffset32(); + uint advanceWidthMappingOffset = reader.ReadOffset32(); + uint lsbMappingOffset = reader.ReadOffset32(); + uint rsbMappingOffset = reader.ReadOffset32(); + + if (major != 1) + { + throw new NotSupportedException("Only version 1 of hvar table is supported"); + } + + ItemVariationStore itemVariationStore = ItemVariationStore.Load(reader, itemVariationStoreOffset); + + DeltaSetIndexMap[]? advanceWidthMapping = DeltaSetIndexMap.Load(reader, advanceWidthMappingOffset); + DeltaSetIndexMap[]? lsbMapping = DeltaSetIndexMap.Load(reader, lsbMappingOffset); + DeltaSetIndexMap[]? rsbMapping = DeltaSetIndexMap.Load(reader, rsbMappingOffset); + + return new HVarTable(itemVariationStore, advanceWidthMapping, lsbMapping, rsbMapping); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/InstanceRecord.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/InstanceRecord.cs new file mode 100644 index 0000000..728485a --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/InstanceRecord.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Defines a InstanceRecord. + /// + /// + internal class InstanceRecord + { + /// + /// Initializes a new instance of the class. + /// + /// The name ID for the subfamily name of this instance. + /// The name ID for the PostScript name of this instance. + /// The design-space coordinates for this instance, one per axis. + public InstanceRecord(ushort subfamilyNameId, ushort postScriptNameId, float[] coordinates) + { + this.SubfamilyNameId = subfamilyNameId; + this.PostScriptNameId = postScriptNameId; + this.Coordinates = coordinates; + } + + /// + /// Gets the name ID for entries in the 'name' table that provide subfamily names for this instance. + /// + public ushort SubfamilyNameId { get; } + + /// + /// Gets the name ID for entries in the 'name' table that provide PostScript names for this instance. + /// + public ushort PostScriptNameId { get; } + + /// + /// Gets the design-space coordinates array for this instance, one value per axis. + /// + public float[] Coordinates { get; } + + /// + /// Loads an from the specified binary reader. + /// + /// The big-endian binary reader. + /// The offset from the start of the fvar table to this instance record. + /// The number of variation axes. + /// The . + public static InstanceRecord Load(BigEndianBinaryReader reader, long offset, ushort axisCount) + { + // InstanceRecord + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | Type | Name | Description | + // +=================+========================================+================================================================+ + // | uint16 | subfamilyNameID | The name ID for entries in the 'name' table that provide | + // | | | subfamily names for this instance. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | uint16 | flags | Reserved for future use — set to 0. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | UserTuple | coordinates | The coordinates array for this instance. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | uint16 | postScriptNameID | Optional. The name ID for entries in the 'name' table that | + // | | | provide PostScript names for this instance. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + + ushort subfamilyNameId = reader.ReadUInt16(); + ushort flags = reader.ReadUInt16(); + + float[] coordinates = new float[axisCount]; + for (int i = 0; i < axisCount; i++) + { + coordinates[i] = reader.ReadFixed(); + } + + ushort postScriptNameId = reader.ReadUInt16(); + + return new InstanceRecord(subfamilyNameId, postScriptNameId, coordinates); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/ItemVariationData.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/ItemVariationData.cs new file mode 100644 index 0000000..760fe59 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/ItemVariationData.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Item variation data, docs: + /// + [DebuggerDisplay("ItemCount: {ItemCount}, WordDeltaCount: {WordDeltaCount}, RegionIndexCount: {RegionIndexes.Length}")] + internal sealed class ItemVariationData + { + /// + /// Count of "word" deltas. + /// + private const int WordDeltaCountMask = 0x7FFF; + + /// + /// Flag indicating that "word" deltas are long (int32). + /// + private const int LongWordsMask = 0x8000; + + /// + /// Initializes a new instance of the class. + /// + /// The number of delta sets for distinct items. + /// The packed word delta count field. + /// The array of region indices referenced by this subtable. + /// The array of delta set rows. + private ItemVariationData(ushort itemCount, ushort wordDeltaCount, ushort[] regionIndices, DeltaSet[] deltaSets) + { + this.ItemCount = itemCount; + this.WordDeltaCount = wordDeltaCount; + this.RegionIndexes = regionIndices; + this.DeltaSets = deltaSets; + } + + /// + /// Gets the number of delta sets for distinct items. + /// + public ushort ItemCount { get; } + + /// + /// Gets the packed word delta count field. The high bit is a flag indicating long words; + /// the low 15 bits give the count of word-sized deltas. + /// + public ushort WordDeltaCount { get; } + + /// + /// Gets the array of indices into the variation region list for the regions referenced by this subtable. + /// + public ushort[] RegionIndexes { get; } + + /// + /// Gets the array of delta set rows, one per item. + /// + public DeltaSet[] DeltaSets { get; } + + /// + /// Loads an from the specified binary reader. + /// + /// The big-endian binary reader. + /// The byte offset from the start of the stream to this subtable. + /// The . + public static ItemVariationData Load(BigEndianBinaryReader reader, long offset) + { + // ItemVariationData + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | Type | Name | Description | + // +=================+========================================+================================================================+ + // | uint16 | itemCount | The number of delta sets for distinct items. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | uint16 | wordDeltaCount | A packed field: the high bit is a flag. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // + uint16 | regionIndexCount | The number of variation regions referenced. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // + uint16 | regionIndexes[regionIndexCount] | Array of indices into the variation region list for | + // + | | the regions referenced by this item variation data table. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // + DeltaSet | deltaSets[itemCount] | Delta-set rows. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + ushort itemCount = reader.ReadUInt16(); + ushort wordDeltaCount = reader.ReadUInt16(); + ushort regionIndexCount = reader.ReadUInt16(); + ushort[] regionIndexes = new ushort[regionIndexCount]; + for (int i = 0; i < regionIndexCount; i++) + { + regionIndexes[i] = reader.ReadUInt16(); + } + + // The deltaSets array represents a logical two-dimensional table of delta values with itemCount rows and regionIndexCount columns. + // Logically, each DeltaSet record has regionIndexCount number of elements. The elements are represented using long and short types. + // These are either int16 and int8, or int32 and int16, according to whether the LONG_WORDS flag is set. + // The delta array has a sequence of deltas using the long type followed by a sequence of deltas using the short type. + bool longWords = (wordDeltaCount & LongWordsMask) != 0; + int wordDeltas = wordDeltaCount & WordDeltaCountMask; + var deltaSets = new DeltaSet[itemCount]; + for (int i = 0; i < itemCount; i++) + { + var deltaSet = new DeltaSet(reader, wordDeltas, longWords, regionIndexCount); + deltaSets[i] = deltaSet; + } + + return new ItemVariationData(itemCount, wordDeltaCount, regionIndexes, deltaSets); + } + + /// + public override int GetHashCode() => HashCode.Combine(this.ItemCount, this.WordDeltaCount, this.RegionIndexes); + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/ItemVariationStore.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/ItemVariationStore.cs new file mode 100644 index 0000000..c2dbb32 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/ItemVariationStore.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Implements reading the item variation store, which is used in most glyph variation data. + /// + /// + internal class ItemVariationStore + { + /// + /// Initializes a new instance of the class. + /// + /// The variation region list defining regions in the design space. + /// The array of item variation data subtables. + public ItemVariationStore(VariationRegionList variationRegionList, ItemVariationData[] itemVariations) + { + this.VariationRegionList = variationRegionList; + this.ItemVariations = itemVariations; + } + + /// + /// Gets the variation region list defining regions in the font's variation space. + /// + public VariationRegionList VariationRegionList { get; } + + /// + /// Gets the array of item variation data subtables. + /// + public ItemVariationData[] ItemVariations { get; } + + /// + /// Loads the item variation store from the specified binary reader. + /// + /// The big-endian binary reader. + /// The byte offset from the start of the stream to this store. + /// The optional total length of the parent table for bounds validation. + /// The . + public static ItemVariationStore Load(BigEndianBinaryReader reader, long offset, long? length = null) + { + // ItemVariationStore + // +--------------------------+--------------------------------------------------+-------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==========================+==================================================+=========================================================================+ + // | uint16 | format | Format — set to 1 | + // +--------------------------+--------------------------------------------------+-------------------------------------------------------------------------+ + // | Offset32 | variationRegionListOffset | Offset in bytes from the start of the item variation store | + // | | | to the variation region list. | + // +--------------------------+--------------------------------------------------+-------------------------------------------------------------------------+ + // | uint16 | itemVariationDataCount | The number of item variation data subtables. | + // +--------------------------+--------------------------------------------------+-------------------------------------------------------------------------+ + // | Offset32 | itemVariationDataOffsets[itemVariationDataCount] | Offsets in bytes from the start of the item variation store | + // | | | to each item variation data subtable. | + // +--------------------------+--------------------------------------------------+-------------------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + + ushort format = reader.ReadUInt16(); + if (format != 1) + { + throw new InvalidFontFileException($"Invalid value for variation Store Format {format}. Should be '1'."); + } + + uint variationRegionListOffset = reader.ReadOffset32(); + ushort itemVariationDataCount = reader.ReadUInt16(); + + if (length.HasValue && variationRegionListOffset > length) + { + throw new InvalidFontFileException("Invalid variation region list offset"); + } + + ItemVariationData[] itemVariations = new ItemVariationData[itemVariationDataCount]; + long itemVariationsOffset = reader.BaseStream.Position; + for (int i = 0; i < itemVariationDataCount; i++) + { + uint variationDataOffset = reader.ReadOffset32(); + itemVariationsOffset += 4; + if (length.HasValue && offset + variationDataOffset >= length) + { + throw new InvalidFontFileException("Bad offset to variation data subtable"); + } + + itemVariations[i] = ItemVariationData.Load(reader, offset + variationDataOffset); + + reader.BaseStream.Position = itemVariationsOffset; + } + + VariationRegionList variationRegionList = VariationRegionList.Load(reader, offset + variationRegionListOffset); + + return new ItemVariationStore(variationRegionList, itemVariations); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/MVarTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/MVarTable.cs new file mode 100644 index 0000000..5bdc247 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/MVarTable.cs @@ -0,0 +1,159 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Implements reading the font variations table `MVAR`. + /// The MVAR table is used in variable fonts to provide variations for global font metric values + /// such as ascender, descender, line gap, caret metrics, and other font-wide measurements. + /// + /// + internal class MVarTable : Table + { + /// + /// The table name identifier for the MVAR table. + /// + internal const string TableName = "MVAR"; + + /// + /// Initializes a new instance of the class. + /// + /// The item variation store containing delta data. + /// The array of metric value records. + public MVarTable(ItemVariationStore itemVariationStore, MetricValueRecord[] valueRecords) + { + this.ItemVariationStore = itemVariationStore; + this.ValueRecords = valueRecords; + } + + /// + /// Gets the item variation store containing the variation delta data. + /// + public ItemVariationStore ItemVariationStore { get; } + + /// + /// Gets the array of metric value records, sorted by tag for binary search. + /// + public MetricValueRecord[] ValueRecords { get; } + + /// + /// Loads the MVAR table from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static MVarTable? Load(FontReader reader) + { + if (!reader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + /// + /// Loads the MVAR table from the specified binary reader. + /// + /// The big-endian binary reader positioned at the start of the MVAR table. + /// The . + public static MVarTable Load(BigEndianBinaryReader reader) + { + // MVAR — Metrics Variations Table + // +--------------------------+------------------------------------------+----------------------------------------------------+ + // | Type | Name | Description | + // +==========================+==========================================+====================================================+ + // | uint16 | majorVersion | Major version — set to 1. | + // +--------------------------+------------------------------------------+----------------------------------------------------+ + // | uint16 | minorVersion | Minor version — set to 0. | + // +--------------------------+------------------------------------------+----------------------------------------------------+ + // | uint16 | reserved | Not used; set to 0. | + // +--------------------------+------------------------------------------+----------------------------------------------------+ + // | uint16 | valueRecordSize | Size in bytes of each value record. | + // +--------------------------+------------------------------------------+----------------------------------------------------+ + // | uint16 | valueRecordCount | Number of value records. | + // +--------------------------+------------------------------------------+----------------------------------------------------+ + // | Offset16 | itemVariationStoreOffset | Offset to ItemVariationStore. | + // +--------------------------+------------------------------------------+----------------------------------------------------+ + // | ValueRecord[] | valueRecords[valueRecordCount] | Array of value records. | + // +--------------------------+------------------------------------------+----------------------------------------------------+ + ushort majorVersion = reader.ReadUInt16(); + ushort minorVersion = reader.ReadUInt16(); + ushort reserved = reader.ReadUInt16(); + ushort valueRecordSize = reader.ReadUInt16(); + ushort valueRecordCount = reader.ReadUInt16(); + ushort itemVariationStoreOffset = reader.ReadOffset16(); + + if (majorVersion != 1) + { + throw new NotSupportedException("Only version 1 of MVAR table is supported"); + } + + // Read the value records. Each is typically 8 bytes (Tag + outerIndex + innerIndex). + MetricValueRecord[] valueRecords = new MetricValueRecord[valueRecordCount]; + for (int i = 0; i < valueRecordCount; i++) + { + long recordStart = reader.BaseStream.Position; + uint tag = reader.ReadUInt32(); + ushort outerIndex = reader.ReadUInt16(); + ushort innerIndex = reader.ReadUInt16(); + valueRecords[i] = new MetricValueRecord(tag, outerIndex, innerIndex); + + // Skip any extra bytes if valueRecordSize > 8 (future compatibility). + long consumed = reader.BaseStream.Position - recordStart; + if (consumed < valueRecordSize) + { + reader.BaseStream.Position += valueRecordSize - consumed; + } + } + + // Load the ItemVariationStore. + ItemVariationStore itemVariationStore = ItemVariationStore.Load(reader, itemVariationStoreOffset); + + return new MVarTable(itemVariationStore, valueRecords); + } + + /// + /// Finds the value record for the given tag using binary search. + /// Returns true if found, with the outer and inner indices set. + /// + /// The 4-byte metric tag to look up. + /// The outer index into the ItemVariationStore. + /// The inner index into the ItemVariationStore. + /// True if the tag was found; false otherwise. + public bool TryGetIndices(Tag tag, out ushort outerIndex, out ushort innerIndex) + { + // ValueRecords are sorted by tag per the spec, so binary search is valid. + int lo = 0; + int hi = this.ValueRecords.Length - 1; + while (lo <= hi) + { + int mid = lo + ((hi - lo) >> 1); + Tag midTag = this.ValueRecords[mid].Tag; + if (midTag == tag) + { + outerIndex = this.ValueRecords[mid].DeltaSetOuterIndex; + innerIndex = this.ValueRecords[mid].DeltaSetInnerIndex; + return true; + } + + if (midTag.Value < tag.Value) + { + lo = mid + 1; + } + else + { + hi = mid - 1; + } + } + + outerIndex = 0; + innerIndex = 0; + return false; + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/MVarTag.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/MVarTag.cs new file mode 100644 index 0000000..f1bdbcd --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/MVarTag.cs @@ -0,0 +1,115 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Defines the tags used by the MVAR table to identify font-wide metrics + /// that can be varied in a variable font. + /// Each tag maps to a specific field in the OS/2, hhea, vhea, or post tables. + /// + /// + internal static class MVarTag + { + // Horizontal metrics (OS/2 typo or hhea). + + /// OS/2.sTypoAscender / hhea.ascender ('hasc'). + public static readonly Tag HorizontalAscender = Tag.Parse("hasc"); + + /// OS/2.sTypoDescender / hhea.descender ('hdsc'). + public static readonly Tag HorizontalDescender = Tag.Parse("hdsc"); + + /// OS/2.sTypoLineGap / hhea.lineGap ('hlgp'). + public static readonly Tag HorizontalLineGap = Tag.Parse("hlgp"); + + /// OS/2.usWinAscent ('hcla'). + public static readonly Tag HorizontalClippingAscent = Tag.Parse("hcla"); + + /// OS/2.usWinDescent ('hcld'). + public static readonly Tag HorizontalClippingDescent = Tag.Parse("hcld"); + + // Vertical metrics (vhea). + + /// vhea.ascent ('vasc'). + public static readonly Tag VerticalAscender = Tag.Parse("vasc"); + + /// vhea.descent ('vdsc'). + public static readonly Tag VerticalDescender = Tag.Parse("vdsc"); + + /// vhea.lineGap ('vlgp'). + public static readonly Tag VerticalLineGap = Tag.Parse("vlgp"); + + // OS/2 subscript metrics. + + /// OS/2.ySubscriptXSize ('sbxs'). + public static readonly Tag SubscriptXSize = Tag.Parse("sbxs"); + + /// OS/2.ySubscriptYSize ('sbys'). + public static readonly Tag SubscriptYSize = Tag.Parse("sbys"); + + /// OS/2.ySubscriptXOffset ('sbxo'). + public static readonly Tag SubscriptXOffset = Tag.Parse("sbxo"); + + /// OS/2.ySubscriptYOffset ('sbyo'). + public static readonly Tag SubscriptYOffset = Tag.Parse("sbyo"); + + // OS/2 superscript metrics. + + /// OS/2.ySuperscriptXSize ('spxs'). + public static readonly Tag SuperscriptXSize = Tag.Parse("spxs"); + + /// OS/2.ySuperscriptYSize ('spys'). + public static readonly Tag SuperscriptYSize = Tag.Parse("spys"); + + /// OS/2.ySuperscriptXOffset ('spxo'). + public static readonly Tag SuperscriptXOffset = Tag.Parse("spxo"); + + /// OS/2.ySuperscriptYOffset ('spyo'). + public static readonly Tag SuperscriptYOffset = Tag.Parse("spyo"); + + // OS/2 strikeout metrics. + + /// OS/2.yStrikeoutSize ('strs'). + public static readonly Tag StrikeoutSize = Tag.Parse("strs"); + + /// OS/2.yStrikeoutPosition ('stro'). + public static readonly Tag StrikeoutPosition = Tag.Parse("stro"); + + // post underline metrics. + + /// post.underlineThickness ('unds'). + public static readonly Tag UnderlineThickness = Tag.Parse("unds"); + + /// post.underlinePosition ('undo'). + public static readonly Tag UnderlinePosition = Tag.Parse("undo"); + + // OS/2 miscellaneous metrics. + + /// OS/2.sxHeight ('xhgt'). + public static readonly Tag XHeight = Tag.Parse("xhgt"); + + /// OS/2.sCapHeight ('cpht'). + public static readonly Tag CapHeight = Tag.Parse("cpht"); + + // hhea caret metrics. + + /// hhea.caretSlopeRise ('hcrn'). + public static readonly Tag HorizontalCaretRise = Tag.Parse("hcrn"); + + /// hhea.caretSlopeRun ('hcrs'). + public static readonly Tag HorizontalCaretRun = Tag.Parse("hcrs"); + + /// hhea.caretOffset ('hcof'). + public static readonly Tag HorizontalCaretOffset = Tag.Parse("hcof"); + + // vhea caret metrics. + + /// vhea.caretSlopeRise ('vcrn'). + public static readonly Tag VerticalCaretRise = Tag.Parse("vcrn"); + + /// vhea.caretSlopeRun ('vcrs'). + public static readonly Tag VerticalCaretRun = Tag.Parse("vcrs"); + + /// vhea.caretOffset ('vcof'). + public static readonly Tag VerticalCaretOffset = Tag.Parse("vcof"); + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/MetricValueRecord.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/MetricValueRecord.cs new file mode 100644 index 0000000..3daceae --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/MetricValueRecord.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// A single MVAR value record mapping a metric tag to a delta-set index. + /// + internal readonly struct MetricValueRecord + { + /// + /// Initializes a new instance of the struct. + /// + /// The four-byte tag identifying the metric. + /// The outer index into the ItemVariationStore. + /// The inner index into the ItemVariationStore. + public MetricValueRecord(Tag tag, ushort deltaSetOuterIndex, ushort deltaSetInnerIndex) + { + this.Tag = tag; + this.DeltaSetOuterIndex = deltaSetOuterIndex; + this.DeltaSetInnerIndex = deltaSetInnerIndex; + } + + /// + /// Gets the four-byte tag identifying the metric (e.g. 'hasc', 'hdsc'). + /// + public Tag Tag { get; } + + /// + /// Gets the outer index into the ItemVariationStore. + /// + public ushort DeltaSetOuterIndex { get; } + + /// + /// Gets the inner index into the ItemVariationStore. + /// + public ushort DeltaSetInnerIndex { get; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/RegionAxisCoordinates.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/RegionAxisCoordinates.cs new file mode 100644 index 0000000..48dfb81 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/RegionAxisCoordinates.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Each RegionAxisCoordinates record provides coordinate values for a region along a single axis. + /// The three values must all be within the range -1.0 to +1.0. startCoord must be less than or equal to peakCoord, + /// and peakCoord must be less than or equal to endCoord. The three values must be either all non-positive or all non-negative with one possible exception: + /// if peakCoord is zero, then startCoord can be negative or 0 while endCoord can be positive or zero. + /// + /// + [DebuggerDisplay("StartCoord: {StartCoord}, PeakCoord: {PeakCoord}, EndCoord: {EndCoord}")] + public readonly struct RegionAxisCoordinates + { + /// + /// Gets the region start coordinate value for the current axis. + /// + public float StartCoord { get; init; } + + /// + /// Gets the region peak coordinate value for the current axis. + /// + public float PeakCoord { get; init; } + + /// + /// Gets the region end coordinate value for the current axis. + /// + public float EndCoord { get; init; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/SegmentMapRecord.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/SegmentMapRecord.cs new file mode 100644 index 0000000..70343d5 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/SegmentMapRecord.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Represents a segment map record from the avar table, containing an array of axis value + /// mapping pairs for a single variation axis. + /// + /// + internal class SegmentMapRecord + { + /// + /// Initializes a new instance of the class. + /// + /// The array of axis value map records for this axis. + public SegmentMapRecord(AxisValueMapRecord[] axisValueMap) => this.AxisValueMap = axisValueMap; + + /// + /// Gets the array of axis value map records defining the piecewise linear mapping for this axis. + /// + public AxisValueMapRecord[] AxisValueMap { get; } + + /// + /// Loads a from the specified binary reader. + /// + /// The big-endian binary reader. + /// The . + public static SegmentMapRecord Load(BigEndianBinaryReader reader) + { + // SegmentMapRecord + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Type | Name | Description | + // +=================+========================================+=========================================================================+ + // | uint16 | positionMapCount | The number of correspondence pairs for this axis. | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + // | AxisValueMap | axisValueMaps[positionMapCount] | The array of axis value map records for this axis. | + // +-----------------+----------------------------------------+-------------------------------------------------------------------------+ + ushort positionMapCount = reader.ReadUInt16(); + var axisValueMap = new AxisValueMapRecord[positionMapCount]; + for (int i = 0; i < positionMapCount; i++) + { + axisValueMap[i] = AxisValueMapRecord.Load(reader); + } + + return new SegmentMapRecord(axisValueMap); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/TupleVariation.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/TupleVariation.cs new file mode 100644 index 0000000..ec5e3e3 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/TupleVariation.cs @@ -0,0 +1,170 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Represents a tuple variation header from the gvar or cvar table, containing + /// the variation data size, tuple index flags, and optional peak/intermediate coordinates. + /// + /// + internal class TupleVariation + { + /// + /// Flag indicating that this tuple variation header includes an embedded peak tuple record, immediately after the tupleIndex field. + /// If set, the low 12 bits of the tupleIndex value are ignored. + /// Note that this must always be set within the 'cvar' table. + /// + internal const int EmbeddedPeakTupleMask = 0x8000; + + /// + /// Flag indicating that this tuple variation table applies to an intermediate region within the variation space. + /// If set, the header includes the two intermediate-region, start and end tuple records, immediately after the peak tuple record (if present). + /// + internal const int IntermediateRegionMask = 0x4000; + + /// + /// Flag indicating that the serialized data for this tuple variation table includes packed "point" number data. + /// If set, this tuple variation table uses that number data; if clear, this tuple variation table uses shared number + /// data found at the start of the serialized data for this glyph variation data or 'cvar' table. + /// + internal const int PrivatePointNumbersMask = 0x2000; + + /// + /// Mask for the low 12 bits to give the shared tuple records index. + /// + internal const int TupleIndexMask = 0x0FFF; + + /// + /// Initializes a new instance of the class. + /// + /// The number of variation axes. + /// The size in bytes of the serialized data for this tuple. + /// The packed tuple index containing flags and shared tuple index. + /// The optional embedded peak tuple coordinates, or null if using shared tuples. + /// The optional intermediate region start coordinates. + /// The optional intermediate region end coordinates. + public TupleVariation( + int axisCount, + ushort variationDataSize, + ushort tupleIndex, + float[]? embeddedPeak, + float[]? intermediateStartRegion, + float[]? intermediateEndRegion) + { + this.AxisCount = axisCount; + this.VariationDataSize = variationDataSize; + this.TupleIndex = tupleIndex; + this.EmbeddedPeak = embeddedPeak; + this.IntermediateStartRegion = intermediateStartRegion; + this.IntermediateEndRegion = intermediateEndRegion; + } + + /// + /// Gets the number of variation axes. + /// + public int AxisCount { get; } + + /// + /// Gets the size in bytes of the serialized data for this tuple variation table. + /// + public ushort VariationDataSize { get; } + + /// + /// Gets the packed tuple index field containing flags (high 4 bits) and shared tuple records index (low 12 bits). + /// + public ushort TupleIndex { get; } + + /// + /// Gets the shared tuple records index (low 12 bits of ). + /// Used to look up peak coordinates from when no embedded peak is present. + /// + public int SharedTupleIndex => this.TupleIndex & TupleIndexMask; + + /// + /// Gets a value indicating whether this tuple has private point numbers in its serialized data. + /// + public bool HasPrivatePointNumbers => (this.TupleIndex & PrivatePointNumbersMask) != 0; + + /// + /// Gets a value indicating whether this tuple has an intermediate region (start/end coordinates). + /// + public bool IsIntermediateRegion => (this.TupleIndex & IntermediateRegionMask) != 0; + + /// + /// Gets the embedded peak tuple coordinates, or null if the peak is referenced from shared tuples. + /// + public float[]? EmbeddedPeak { get; } + + /// + /// Gets the intermediate region start coordinates, or null if this is not an intermediate tuple. + /// + public float[]? IntermediateStartRegion { get; } + + /// + /// Gets the intermediate region end coordinates, or null if this is not an intermediate tuple. + /// + public float[]? IntermediateEndRegion { get; } + + /// + /// Loads a from the specified binary reader. + /// + /// The big-endian binary reader. + /// The number of variation axes. + /// The . + public static TupleVariation Load(BigEndianBinaryReader reader, int axisCount) + { + // TupleVariation + // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +======================+===========================================+==============================================================================+ + // | uint16 | variationDataSize | The size in bytes of the serialized data for this tuple variation table. | + // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+ + // | uint16 | tupleIndex | A packed field. The high 4 bits are flags. | + // | | | The low 12 bits are an index into a shared tuple records array. | + // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+ + // | Tuple | peakTuple | Peak tuple record for this tuple variation table — | + // | | | optional, determined by flags in the tupleIndex value. | + // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+ + // | Tuple | intermediateStartTuple | Intermediate start tuple record for this tuple variation table — | + // | | | optional, determined by flags in the tupleIndex value. | + // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+ + // | Tuple | intermediateEndTuple | Intermediate end tuple record for this tuple variation table — | + // | | | optional, determined by flags in the tupleIndex value. | + // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+ + ushort variationDataSize = reader.ReadUInt16(); + ushort tupleIndex = reader.ReadUInt16(); + + bool hasEmbeddedPeakTuple = (tupleIndex & EmbeddedPeakTupleMask) != 0; + bool hasIntermediateRegion = (tupleIndex & IntermediateRegionMask) != 0; + + float[]? embeddedPeak = null; + if (hasEmbeddedPeakTuple) + { + embeddedPeak = new float[axisCount]; + for (int i = 0; i < axisCount; i++) + { + embeddedPeak[i] = reader.ReadF2Dot14(); + } + } + + float[]? intermediateStartRegion = null; + float[]? intermediateEndRegion = null; + if (hasIntermediateRegion) + { + intermediateStartRegion = new float[axisCount]; + for (int i = 0; i < axisCount; i++) + { + intermediateStartRegion[i] = reader.ReadF2Dot14(); + } + + intermediateEndRegion = new float[axisCount]; + for (int i = 0; i < axisCount; i++) + { + intermediateEndRegion[i] = reader.ReadF2Dot14(); + } + } + + return new TupleVariation(axisCount, variationDataSize, tupleIndex, embeddedPeak, intermediateStartRegion, intermediateEndRegion); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VVarTable.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VVarTable.cs new file mode 100644 index 0000000..bc39cd3 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VVarTable.cs @@ -0,0 +1,138 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Implements reading the font variations table `VVAR`. + /// The VVAR table is used in variable fonts to provide variations for vertical glyph metrics values. + /// This can be used to provide variation data for advance heights in the 'vmtx' table. + /// + /// + internal class VVarTable : Table + { + /// + /// The table name identifier for the VVAR table. + /// + internal const string TableName = "VVAR"; + + /// + /// Initializes a new instance of the class. + /// + /// The item variation store containing delta data. + /// The optional delta-set index mapping for advance heights. + /// The optional delta-set index mapping for top side bearings. + /// The optional delta-set index mapping for bottom side bearings. + /// The optional delta-set index mapping for vertical origin Y coordinates. + public VVarTable( + ItemVariationStore itemVariationStore, + DeltaSetIndexMap[]? advanceWidthMapping, + DeltaSetIndexMap[]? tsbMapping, + DeltaSetIndexMap[]? bsbMapping, + DeltaSetIndexMap[]? vOrgMapping) + { + this.ItemVariationStore = itemVariationStore; + this.AdvanceWidthMapping = advanceWidthMapping; + this.TsbMapping = tsbMapping; + this.BsbMapping = bsbMapping; + this.VOrgMapping = vOrgMapping; + } + + /// + /// Gets the item variation store containing the variation delta data. + /// + public ItemVariationStore ItemVariationStore { get; } + + /// + /// Gets the optional delta-set index mapping for advance heights. + /// + public DeltaSetIndexMap[]? AdvanceWidthMapping { get; } + + /// + /// Gets the optional delta-set index mapping for top side bearings. + /// + public DeltaSetIndexMap[]? TsbMapping { get; } + + /// + /// Gets the optional delta-set index mapping for bottom side bearings. + /// + public DeltaSetIndexMap[]? BsbMapping { get; } + + /// + /// Gets the optional delta-set index mapping for Y coordinates of vertical origins. + /// + public DeltaSetIndexMap[]? VOrgMapping { get; } + + /// + /// Loads the VVAR table from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static VVarTable? Load(FontReader reader) + { + if (!reader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + /// + /// Loads the VVAR table from the specified binary reader. + /// + /// The big-endian binary reader positioned at the start of the VVAR table. + /// The . + public static VVarTable Load(BigEndianBinaryReader reader) + { + // Horizontal metrics variations table + // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Type | Name | Description | + // +==========================+========================================+=========================================================================+ + // | uint16 | majorVersion | Major version number of the font variations table — set to 1. | + // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+ + // | uint16 | minorVersion | Minor version number of the font variations table — set to 0. | + // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Offset32 | itemVariationStoreOffset | Offset in bytes from the start of this table to the | + // | | | item variation store table. | + // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Offset32 | advanceHeightMappingOffset | Offset in bytes from the start of this table to the delta-set index | + // | | | mapping for advance heights (may be NULL). | + // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Offset32 | tsbMappingOffset | Offset in bytes from the start of this table to the delta-set index | + // | | | mapping for top side bearings (may be NULL). | + // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Offset32 | bsbMappingOffset | Offset in bytes from the start of this table to the delta-set index | + // | | | mapping for bottom side bearings (may be NULL). | + // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+ + // | Offset32 | vOrgMappingOffset | Offset in bytes from the start of this table to the delta-set index | + // | | | mapping for Y coordinates of vertical origins (may be NULL). | + // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+ + ushort major = reader.ReadUInt16(); + ushort minor = reader.ReadUInt16(); + uint itemVariationStoreOffset = reader.ReadOffset32(); + uint advanceHeightMappingOffset = reader.ReadOffset32(); + uint tsbMappingOffset = reader.ReadOffset32(); + uint bsbMappingOffset = reader.ReadOffset32(); + uint vOrgMappingOffset = reader.ReadOffset32(); + + if (major != 1) + { + throw new NotSupportedException("Only version 1 of hvar table is supported"); + } + + ItemVariationStore itemVariationStore = ItemVariationStore.Load(reader, itemVariationStoreOffset); + + DeltaSetIndexMap[]? advanceHeightMapping = DeltaSetIndexMap.Load(reader, advanceHeightMappingOffset); + DeltaSetIndexMap[]? tsbMapping = DeltaSetIndexMap.Load(reader, tsbMappingOffset); + DeltaSetIndexMap[]? bsbMapping = DeltaSetIndexMap.Load(reader, bsbMappingOffset); + DeltaSetIndexMap[]? vOrgMapping = DeltaSetIndexMap.Load(reader, vOrgMappingOffset); + + return new VVarTable(itemVariationStore, advanceHeightMapping, tsbMapping, bsbMapping, vOrgMapping); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxis.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxis.cs new file mode 100644 index 0000000..2f795be --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxis.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// + /// + [DebuggerDisplay("Name: {Name}, Tag: {Tag}, Min: {Min}, Max: {Max}, Default: {Default}")] + public readonly struct VariationAxis + { + /// + /// Gets the name of the axes. + /// + public string Name { get; init; } + + /// + /// Gets tag identifying the design variation for the axis. + /// + public string Tag { get; init; } + + /// + /// Gets the minimum coordinate value for the axis. + /// + public float Min { get; init; } + + /// + /// Gets the maximum coordinate value for the axis. + /// + public float Max { get; init; } + + /// + /// Gets the default coordinate value for the axis. + /// + public float Default { get; init; } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxisRecord.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxisRecord.cs new file mode 100644 index 0000000..96b1773 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxisRecord.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Defines a VariationAxisRecord. + /// + /// + [DebuggerDisplay("Tag: {Tag}, MinValue: {MinValue}, MaxValue: {MaxValue}, DefaultValue: {DefaultValue}, AxisNameId: {AxisNameId}")] + internal class VariationAxisRecord + { + /// + /// Initializes a new instance of the class. + /// + /// The tag identifying the design variation for this axis. + /// The minimum coordinate value for this axis. + /// The default coordinate value for this axis. + /// The maximum coordinate value for this axis. + /// The axis qualifier flags. + /// The name ID for the display name of this axis. + internal VariationAxisRecord(string tag, float minValue, float defaultValue, float maxValue, ushort flags, ushort axisNameId) + { + this.Tag = tag; + this.MinValue = minValue; + this.MaxValue = maxValue; + this.DefaultValue = defaultValue; + this.Flags = flags; + this.AxisNameId = axisNameId; + } + + /// + /// Gets the tag identifying the design variation for this axis (e.g. "wght", "wdth"). + /// + public string Tag { get; } + + /// + /// Gets the minimum coordinate value for this axis. + /// + public float MinValue { get; } + + /// + /// Gets the default coordinate value for this axis. + /// + public float DefaultValue { get; } + + /// + /// Gets the maximum coordinate value for this axis. + /// + public float MaxValue { get; } + + /// + /// Gets the axis qualifier flags. + /// + public ushort Flags { get; } + + /// + /// Gets the name ID for entries in the 'name' table that provide a display name for this axis. + /// + public ushort AxisNameId { get; } + + /// + /// Loads a from the specified binary reader. + /// + /// The big-endian binary reader. + /// The byte offset from the start of the stream to this axis record. + /// The . + public static VariationAxisRecord Load(BigEndianBinaryReader reader, long offset) + { + // VariationAxisRecord + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | Type | Name | Description | + // +=================+========================================+================================================================+ + // | Tag | axisTag | Tag identifying the design variation for the axis. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | Fixed | minValue | The minimum coordinate value for the axis. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | Fixed | defaultValue | The default coordinate value for the axis. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | Fixed | maxValue | The maximum coordinate value for the axis. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | uint16 | flags | Axis qualifiers — see details below. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | uint16 | axisNameID | The name ID for entries in the 'name' table that provide | + // | | | a display name for this axis. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + + string tag = reader.ReadTag(); + float minValue = reader.ReadFixed(); + float defaultValue = reader.ReadFixed(); + float maxValue = reader.ReadFixed(); + ushort flags = reader.ReadUInt16(); + ushort axisNameID = reader.ReadUInt16(); + + return new VariationAxisRecord(tag, minValue, defaultValue, maxValue, flags, axisNameID); + } + } +} diff --git a/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationRegionList.cs b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationRegionList.cs new file mode 100644 index 0000000..37fc617 --- /dev/null +++ b/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationRegionList.cs @@ -0,0 +1,112 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; +using System.IO; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { + /// + /// Variation data is comprised of delta adjustment values that have effect over particular regions within the font’s variation space. + /// In a tuple variation store (described earlier in this chapter), the deltas are organized into groupings by region of applicability, with each grouping associated with a given region. + /// In contrast, the item variation store format organizes deltas into groupings by the target items to which they apply, with each grouping having deltas for several regions. + /// Accordingly, the item variation store uses different formats for describing the regions in which a set of deltas apply. + /// + /// + [DebuggerDisplay("AxisCount: {AxisCount}, RegionCount: {RegionCount}")] + internal class VariationRegionList + { + /// + /// An empty variation region list with no axes or regions. + /// + public static readonly VariationRegionList EmptyVariationRegionList = new(0, 0, new[] { Array.Empty() }); + + /// + /// Initializes a new instance of the class. + /// + /// The number of variation axes. + /// The number of variation regions. + /// The two-dimensional array of region axis coordinates, indexed by region then axis. + private VariationRegionList(ushort axisCount, ushort regionCount, RegionAxisCoordinates[][] variationRegions) + { + this.AxisCount = axisCount; + this.RegionCount = regionCount; + this.VariationRegions = variationRegions; + } + + /// + /// Gets the number of variation axes for this font. Must match the axisCount in the fvar table. + /// + public ushort AxisCount { get; } + + /// + /// Gets the number of variation regions in this list. + /// + public ushort RegionCount { get; } + + /// + /// Gets the array of variation regions. Each region is an array of , one per axis. + /// + public RegionAxisCoordinates[][] VariationRegions { get; } + + /// + /// Loads the variation region list from the specified binary reader. + /// + /// The big-endian binary reader. + /// The byte offset from the start of the stream to this region list. + /// The . + public static VariationRegionList Load(BigEndianBinaryReader reader, long offset) + { + // VariationRegionList + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | Type | Name | Description | + // +=================+========================================+================================================================+ + // | uint16 | axisCount | The number of variation axes for this font. | + // | | | This must be the same number as axisCount in the 'fvar' table. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // | uint16 | regionCount | The number of variation region tables in the variation region | + // | | | list. Must be less than 32,768. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + // + VariationRegion | variationRegions[regionCount] | Array of variation regions. | + // +-----------------+----------------------------------------+----------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + ushort axisCount = reader.ReadUInt16(); + ushort regionCount = reader.ReadUInt16(); + var variationRegions = new RegionAxisCoordinates[regionCount][]; + for (int i = 0; i < regionCount; i++) + { + variationRegions[i] = new RegionAxisCoordinates[axisCount]; + for (int j = 0; j < axisCount; j++) + { + float startCoord = reader.ReadF2Dot14(); + float peakCoord = reader.ReadF2Dot14(); + float endCoord = reader.ReadF2Dot14(); + + if (startCoord > peakCoord || peakCoord > endCoord) + { + throw new InvalidFontFileException("Region axis coordinates out of order"); + } + + if (startCoord < -0x4000 || endCoord > 0x4000) + { + throw new InvalidFontFileException("Region axis coordinate out of range"); + } + + if ((peakCoord < 0 && endCoord > 0) || (peakCoord > 0 && startCoord < 0)) + { + throw new InvalidFontFileException("Invalid region axis coordinates"); + } + + variationRegions[i][j] = new RegionAxisCoordinates() + { + StartCoord = startCoord, + PeakCoord = peakCoord, + EndCoord = endCoord + }; + } + } + + return new VariationRegionList(axisCount, regionCount, variationRegions); + } + } +} diff --git a/SixLabors.Fonts/Tables/Cff/5176.CFF.pdf b/SixLabors.Fonts/Tables/Cff/5176.CFF.pdf new file mode 100644 index 0000000..9219d7f Binary files /dev/null and b/SixLabors.Fonts/Tables/Cff/5176.CFF.pdf differ diff --git a/SixLabors.Fonts/Tables/Cff/5177.Type2.pdf b/SixLabors.Fonts/Tables/Cff/5177.Type2.pdf new file mode 100644 index 0000000..abe04f5 Binary files /dev/null and b/SixLabors.Fonts/Tables/Cff/5177.Type2.pdf differ diff --git a/SixLabors.Fonts/Tables/Cff/Cff1Parser.cs b/SixLabors.Fonts/Tables/Cff/Cff1Parser.cs new file mode 100644 index 0000000..cfd61df --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/Cff1Parser.cs @@ -0,0 +1,692 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Parses a Compact Font Format (CFF) font program as described in The Compact Font Format specification (Adobe Technical Note #5176). + /// A CFF font may contain multiple fonts and achieves compression by sharing details between fonts in the set. + /// + internal class Cff1Parser : CffParserBase + { + /// + /// Latin 1 Encoding: ISO 8859-1 is a single-byte encoding that can represent the first 256 Unicode characters. + /// + private static readonly Encoding Iso88591 = Encoding.GetEncoding("ISO-8859-1"); + + private long offset; + private int charStringsOffset; + private int charsetOffset; + private int encodingOffset = -1; + private int privateDICTOffset; + private int privateDICTLength; + + /// + /// Loads and parses a CFF1 font from the given reader at the specified offset. + /// + /// The binary reader positioned at the CFF1 data. + /// The absolute offset of the CFF1 table in the font stream. + /// The parsed . + public CffFont Load(BigEndianBinaryReader reader, long offset) + { + this.offset = offset; + + string fontName = ReadNameIndex(reader); + + List dataDicEntries = this.ReadTopDictIndex(reader); + string[] stringIndex = ReadStringIndex(reader); + + CffTopDictionary topDictionary = this.ResolveTopDictInfo(dataDicEntries, stringIndex); + + byte[][] globalSubrRawBuffers = ReadGlobalSubrIndex(reader); + + ReadFdSelect(reader, this.offset, topDictionary.CidFontInfo); + FontDict[] fontDicts = this.ReadFdArray(reader, this.offset, topDictionary.CidFontInfo.FDArray); + + CffPrivateDictionary? privateDictionary = this.ReadPrivateDict(reader); + CffGlyphData[] glyphs = this.ReadCharStringsIndex(reader, topDictionary, globalSubrRawBuffers, fontDicts, privateDictionary); + + this.ReadCharsets(reader, stringIndex, glyphs); + this.ReadEncodings(reader); + + return new(fontName, topDictionary, glyphs); + } + + /// + /// Reads the Name INDEX, which contains the PostScript name of the font. + /// + /// The binary reader. + /// The font name string. + private static string ReadNameIndex(BigEndianBinaryReader reader) + { + if (!TryReadIndexDataOffsets(reader, out CffIndexOffset[]? offsets)) + { + throw new InvalidFontFileException("No name index found."); + } + + // For Open Type the Name INDEX in the CFF data must contain only one entry; + // that is, there must be only one font in the CFF FontSet. + CffIndexOffset offset = offsets[0]; + return reader.ReadString(offset.Length, Iso88591); + } + + /// + /// Reads the Top DICT INDEX, which contains top-level dictionary entries for the font. + /// + /// The binary reader. + /// A list of parsed DICT entries. + private List ReadTopDictIndex(BigEndianBinaryReader reader) + { + // 8. Top DICT INDEX + // This contains the top - level DICTs of all the fonts in the FontSet + // stored in an INDEX structure.Objects contained within this + // INDEX correspond to those in the Name INDEX in both order + // and number. Each object is a DICT structure that corresponds to + // the top-level dictionary of a PostScript font. + // A font is identified by an entry in the Name INDEX and its data + // is accessed via the corresponding Top DICT + if (!TryReadIndexDataOffsets(reader, out CffIndexOffset[]? offsets)) + { + throw new InvalidFontFileException("No Top DICT index found."); + } + + // 9. Top DICT Data + // The names of the Top DICT operators shown in + // Table 9 are, where possible, the same as the corresponding Type 1 dict key. + // Operators that have no corresponding Type1 dict key are noted + // in the table below along with a default value, if any. (Several + // operators have been derived from FontInfo dict keys but have + // been grouped together with the Top DICT operators for + // simplicity.The keys from the FontInfo dict are indicated in the + // Default, notes column of Table 9) + return this.ReadDictData(reader, offsets[0].Length); + } + + /// + /// Reads the String INDEX containing font-specific strings referenced by SID. + /// + /// The binary reader. + /// An array of strings from the String INDEX. + private static string[] ReadStringIndex(BigEndianBinaryReader reader) + { + if (!TryReadIndexDataOffsets(reader, out CffIndexOffset[]? offsets)) + { + return []; + } + + string[] stringIndex = new string[offsets.Length]; + + // Allow reusing the same buffer for shorter reads. + using Buffer buffer = new(512); + Span bufferSpan = buffer.GetSpan(); + + for (int i = 0; i < offsets.Length; ++i) + { + int length = offsets[i].Length; + if (length < bufferSpan.Length) + { + Span slice = bufferSpan[..length]; + int actualRead = reader.BaseStream.Read(slice); + if (actualRead != length) + { + throw new InvalidFontFileException("Invalid string length."); + } + + stringIndex[i] = Iso88591.GetString(slice); + } + else + { + stringIndex[i] = reader.ReadString(length, Iso88591); + } + } + + return stringIndex; + } + + /// + /// Resolves a string identifier (SID) to its string value using the standard strings + /// table and the font-specific String INDEX. + /// + /// The SID to resolve. + /// The font-specific String INDEX. + /// The resolved string name. + private static string GetSid(int index, string[] stringIndex) + { + if (index >= 0 && index <= CffStandardStrings.Count - 1) + { + // Use standard name + return CffStandardStrings.GetName(index); + } + + if (index - CffStandardStrings.Count < stringIndex.Length) + { + return stringIndex[index - CffStandardStrings.Count]; + } + + // Technically this maps to .notdef, but PDFBox uses this + return "SID" + index; + } + + /// + /// Resolves the Top DICT entries into a by interpreting operator-operand pairs. + /// + /// The parsed DICT entries. + /// The font-specific String INDEX for SID resolution. + /// The populated . + private CffTopDictionary ResolveTopDictInfo(List entries, string[] stringIndex) + { + // TODO: Is CID mandatory? + CffTopDictionary metrics = new(); + foreach (CffDataDicEntry entry in entries) + { + switch (entry.Operator.Name) + { + default: +#if DEBUG + System.Diagnostics.Debug.WriteLine("topdic:" + entry.Operator.Name); +#endif + break; + case "XUID": + break; // nothing + case "version": + metrics.Version = GetSid((int)entry.Operands[0].RealNumValue, stringIndex); + break; + case "Notice": + metrics.Notice = GetSid((int)entry.Operands[0].RealNumValue, stringIndex); + break; + case "Copyright": + metrics.CopyRight = GetSid((int)entry.Operands[0].RealNumValue, stringIndex); + break; + case "FullName": + metrics.FullName = GetSid((int)entry.Operands[0].RealNumValue, stringIndex); + break; + case "FamilyName": + metrics.FamilyName = GetSid((int)entry.Operands[0].RealNumValue, stringIndex); + break; + case "Weight": + metrics.Weight = GetSid((int)entry.Operands[0].RealNumValue, stringIndex); + break; + case "UnderlinePosition": + metrics.UnderlinePosition = entry.Operands[0].RealNumValue; + break; + case "UnderlineThickness": + metrics.UnderlineThickness = entry.Operands[0].RealNumValue; + break; + case "FontBBox": + metrics.FontBBox = + [ + entry.Operands[0].RealNumValue, + entry.Operands[1].RealNumValue, + entry.Operands[2].RealNumValue, + entry.Operands[3].RealNumValue + ]; + break; + case "CharStrings": + this.charStringsOffset = (int)entry.Operands[0].RealNumValue; + break; + case "charset": + this.charsetOffset = (int)entry.Operands[0].RealNumValue; + break; + case "Encoding": + this.encodingOffset = (int)entry.Operands[0].RealNumValue; + break; + case "Private": + // private DICT size and offset + this.privateDICTLength = (int)entry.Operands[0].RealNumValue; + this.privateDICTOffset = (int)entry.Operands[1].RealNumValue; + break; + case "ROS": + // http://wwwimages.adobe.com/www.adobe.com/content/dam/acom/en/devnet/font/pdfs/5176.CFF.pdf + // A CFF CIDFont has the CIDFontName in the Name INDEX and a corresponding Top DICT. + // The Top DICT begins with ROS operator which specifies the Registry-Ordering - Supplement for the font. + // This will indicate to a CFF parser that special CID processing should be applied to this font. Specifically: + + // ROS operator combines the Registry, Ordering, and Supplement keys together. + // see Adobe Cmap resource , https://github.com/adobe-type-tools/cmap-resources + metrics.CidFontInfo.ROS_Register = GetSid((int)entry.Operands[0].RealNumValue, stringIndex); + metrics.CidFontInfo.ROS_Ordering = GetSid((int)entry.Operands[1].RealNumValue, stringIndex); + metrics.CidFontInfo.ROS_Supplement = GetSid((int)entry.Operands[2].RealNumValue, stringIndex); + + break; + case "CIDFontVersion": + metrics.CidFontInfo.CIDFontVersion = entry.Operands[0].RealNumValue; + break; + case "CIDCount": + metrics.CidFontInfo.CIDFountCount = (int)entry.Operands[0].RealNumValue; + break; + case "FDSelect": + metrics.CidFontInfo.FDSelect = (int)entry.Operands[0].RealNumValue; + break; + case "FDArray": + metrics.CidFontInfo.FDArray = (int)entry.Operands[0].RealNumValue; + break; + } + } + + return metrics; + } + + /// + /// Reads the Global Subrs INDEX, which contains shared subroutine charstring programs. + /// + /// The binary reader. + /// An array of byte arrays, each containing a global subroutine charstring. + private static byte[][] ReadGlobalSubrIndex(BigEndianBinaryReader reader) + + // 16. Local / Global Subrs INDEXes + // Both Type 1 and Type 2 charstrings support the notion of + // subroutines or subrs. + + // A subr is typically a sequence of charstring + // bytes representing a sub - program that occurs in more than one + // place in a font’s charstring data. + + // This subr may be stored once + // but referenced many times from within one or more charstrings + // by the use of the call subr operator whose operand is the + // number of the subr to be called. + + // The subrs are local to a particular font and + // cannot be shared between fonts. + + // Type 2 charstrings also permit global subrs which function in the same + // way but are called by the call gsubr operator and may be shared + // across fonts. + + // Local subrs are stored in an INDEX structure which is located via + // the offset operand of the Subrs operator in the Private DICT. + // A font without local subrs has no Subrs operator in the Private DICT. + + // Global subrs are stored in an INDEX structure which follows the + // String INDEX. A FontSet without any global subrs is represented + // by an empty Global Subrs INDEX. + => ReadSubrBuffer(reader); + + /// + /// Reads the Local Subrs INDEX, which contains font-private subroutine charstring programs. + /// + /// The binary reader. + /// An array of byte arrays, each containing a local subroutine charstring. + private static byte[][] ReadLocalSubrs(BigEndianBinaryReader reader) => ReadSubrBuffer(reader); + + /// + /// Reads the encoding data for the font if an encoding offset is specified. + /// + /// The binary reader. + // TODO: We don't actually need this right now. Will be important though if we ever introduce subsetting. + private void ReadEncodings(BigEndianBinaryReader reader) + { + // Encoding data is located via the offset operand to the + // Encoding operator in the Top DICT. + + // Only one Encoding operator can be + // specified per font except for CIDFonts which specify no + // encoding. + + // A glyph’s encoding is specified by a 1 - byte code that + // permits values in the range 0 - 255. + + // Each encoding is described by a format-type identifier byte + // followed by format-specific data.Two formats are currently + // defined as specified in Tables 11(Format 0) and 12(Format 1). + if (this.encodingOffset != -1) + { + byte encoding = reader.ReadByte(); + switch (encoding) + { + case 0: + ReadFormat0Encoding(reader); + break; + case 1: + ReadFormat1Encoding(reader); + break; + default: + + // TODO: Seek. + break; + } + } + } + + /// + /// Reads the charset data, which maps glyph indices to glyph names. + /// + /// The binary reader. + /// The font-specific String INDEX for SID resolution. + /// The glyph data array to populate with glyph names. + private void ReadCharsets(BigEndianBinaryReader reader, string[] stringIndex, CffGlyphData[] glyphs) + { + // Charset data is located via the offset operand to the + // charset operator in the Top DICT. + + // Each charset is described by a format- + // type identifier byte followed by format-specific data. + // Three formats are currently defined as shown in Tables + // 17, 18, and 20. + reader.BaseStream.Position = this.offset + this.charsetOffset; + switch (reader.ReadByte()) + { + default: + throw new NotSupportedException(); + case 0: + ReadCharsetsFormat0(reader, stringIndex, glyphs); + break; + case 1: + ReadCharsetsFormat1(reader, stringIndex, glyphs); + break; + case 2: + ReadCharsetsFormat2(reader, stringIndex, glyphs); + break; + } + } + + /// + /// Reads charset data in format 0, where each glyph has an individual SID entry. + /// + /// The binary reader. + /// The font-specific String INDEX for SID resolution. + /// The glyph data array to populate with glyph names. + private static void ReadCharsetsFormat0(BigEndianBinaryReader reader, string[] stringIndex, CffGlyphData[] glyphs) + { + // Table 17: Format 0 + // Type Name Description + // Card8 format =0 + // SID glyph[nGlyphs-1] Glyph name array + + // Each element of the glyph array represents the name of the + // corresponding glyph. This format should be used when the SIDs + // are in a fairly random order. The number of glyphs (nGlyphs) is + // the value of the count field in the + // CharStrings INDEX. (There is + // one less element in the glyph name array than nGlyphs because + // the .notdef glyph name is omitted.) + for (int i = 1; i < glyphs.Length; ++i) + { + ref CffGlyphData data = ref glyphs[i]; + data.GlyphName = GetSid(reader.ReadUInt16(), stringIndex); + } + } + + /// + /// Reads charset data in format 1, using Range1 structures with 1-byte nLeft counts. + /// + /// The binary reader. + /// The font-specific String INDEX for SID resolution. + /// The glyph data array to populate with glyph names. + private static void ReadCharsetsFormat1(BigEndianBinaryReader reader, string[] stringIndex, CffGlyphData[] glyphs) + { + // Table 18 Format 1 + // Type Name Description + // Card8 format =1 + // struct Range1[] Range1 array (see Table 19) + + // Table 19 Range1 Format (Charset) + // Type Name Description + // SID first First glyph in range + // Card8 nLeft Glyphs left in range(excluding first) + + // Each Range1 describes a group of sequential SIDs. The number + // of ranges is not explicitly specified in the font. Instead, software + // utilizing this data simply processes ranges until all glyphs in the + // font are covered. This format is particularly suited to charsets + // that are well ordered + for (int i = 1; i < glyphs.Length;) + { + int sid = reader.ReadUInt16(); // First glyph in range + int count = reader.ReadByte() + 1; // since it does not include first element. + do + { + ref CffGlyphData data = ref glyphs[i]; + data.GlyphName = GetSid(sid, stringIndex); + + count--; + i++; + sid++; + } + while (count > 0); + } + } + + /// + /// Reads charset data in format 2, using Range2 structures with 2-byte nLeft counts for large charsets. + /// + /// The binary reader. + /// The font-specific String INDEX for SID resolution. + /// The glyph data array to populate with glyph names. + private static void ReadCharsetsFormat2(BigEndianBinaryReader reader, string[] stringIndex, CffGlyphData[] glyphs) + { + // note:eg, Adobe's source-code-pro font + + // Table 20 Format 2 + // Type Name Description + // Card8 format 2 + // struct Range2[] Range2 array (see Table 21) + // + //----------------------------------------------- + // Table 21 Range2 Format + // Type Name Description + // SID first First glyph in range + // Card16 nLeft Glyphs left in range (excluding first) + //----------------------------------------------- + + // Format 2 differs from format 1 only in the size of the nLeft field in each range. + // This format is most suitable for fonts with a large well - ordered charset — for example, for Asian CIDFonts. + for (int i = 1; i < glyphs.Length;) + { + int sid = reader.ReadUInt16(); // First glyph in range + int count = reader.ReadUInt16() + 1; // since it does not include first element. + do + { + ref CffGlyphData data = ref glyphs[i]; + data.GlyphName = GetSid(sid, stringIndex); + + count--; + i++; + sid++; + } + while (count > 0); + } + } + + /// + /// Reads the CharStrings INDEX and creates glyph data objects for all glyphs in the font. + /// + /// The binary reader. + /// The top-level dictionary containing font metadata. + /// The global subroutine buffers. + /// The Font DICT array for CID fonts. + /// The private dictionary containing local subroutine references. + /// An array of for each glyph. + private CffGlyphData[] ReadCharStringsIndex( + BigEndianBinaryReader reader, + CffTopDictionary topDictionary, + byte[][] globalSubrBuffers, + FontDict[] fontDicts, + CffPrivateDictionary? privateDictionary) + { + // 14. CharStrings INDEX + + // This contains the charstrings of all the glyphs in a font stored in + // an INDEX structure. + + // Charstring objects contained within this + // INDEX are accessed by GID. + + // The first charstring(GID 0) must be + // the.notdef glyph. + + // The number of glyphs available in a font may + // be determined from the count field in the INDEX. + + // + + // The format of the charstring data, and therefore the method of + // interpretation, is specified by the + // CharstringType operator in the Top DICT. + + // The CharstringType operator has a default value + // of 2 indicating the Type 2 charstring format which was designed + // in conjunction with CFF. + + // Type 1 charstrings are documented in + // the "Adobe Type 1 Font Format" published by Addison - Wesley. + + // Type 2 charstrings are described in Adobe Technical Note #5177: + // "Type 2 Charstring Format." Other charstring types may also be + // supported by this method. + reader.BaseStream.Position = this.offset + this.charStringsOffset; + if (!TryReadIndexDataOffsets(reader, out CffIndexOffset[]? offsets)) + { + throw new InvalidFontFileException("No glyph data found."); + } + + int glyphCount = offsets.Length; + CffGlyphData[] glyphs = new CffGlyphData[glyphCount]; + byte[][]? localSubBuffer = privateDictionary?.LocalSubrRawBuffers; + + // Is the font a CID font? + FDRangeProvider fdRangeProvider = new(topDictionary.CidFontInfo); + bool isCidFont = topDictionary.CidFontInfo.FdRanges.Length > 0; + + for (int i = 0; i < glyphCount; ++i) + { + CffIndexOffset offset = offsets[i]; + byte[] charstringsBuffer = reader.ReadBytes(offset.Length); + + // Now we can parse the raw glyph instructions + if (isCidFont) + { + // Select proper local private dict + fdRangeProvider.SetCurrentGlyphIndex((ushort)i); + localSubBuffer = fontDicts[fdRangeProvider.SelectedFDArray].LocalSubr; + } + + glyphs[i] = new CffGlyphData( + (ushort)i, + globalSubrBuffers, + localSubBuffer ?? [], + privateDictionary?.NominalWidthX ?? 0, + charstringsBuffer, + 1); + } + + return glyphs; + } + + /// + /// Reads format 0 encoding data where each glyph has an individual code assignment. + /// + /// The binary reader. + private static void ReadFormat0Encoding(BigEndianBinaryReader reader) + { + // Table 11: Format 0 + // Type Name Description + // Card8 format = 0 + // Card8 nCodes Number of encoded glyphs + // Card8 code[nCodes] Code array + //------- + // Each element of the code array represents the encoding for the + // corresponding glyph. This format should be used when the + // codes are in a fairly random order + + // we have read format field( 1st field) .. + // so start with 2nd field + int nCodes = reader.ReadByte(); + byte[] codes = reader.ReadBytes(nCodes); + + // TODO: Implement based on PDFPig + } + + /// + /// Reads format 1 encoding data using Range1 structures for sequential code groups. + /// + /// The binary reader. + private static void ReadFormat1Encoding(BigEndianBinaryReader reader) + { + // Table 12 Format 1 + // Type Name Description + // Card8 format = 1 + // Card8 nRanges Number of code ranges + // struct Range1[nRanges] Range1 array(see Table 13) + //-------------- + int nRanges = reader.ReadByte(); + + // Table 13 Range1 Format(Encoding) + // Type Name Description + // Card8 first First code in range + // Card8 nLeft Codes left in range(excluding first) + //-------------- + // Each Range1 describes a group of sequential codes. For + // example, the codes 51 52 53 54 55 could be represented by the + // Range1: 51 4, and a perfectly ordered encoding of 256 codes can + // be described with the Range1: 0 255. + + // This format is particularly suited to encodings that are well ordered. + + // A few fonts have multiply - encoded glyphs which are not + // supported directly by any of the above formats. This situation is + // indicated by setting the high - order bit in the format byte and + // supplementing the encoding, regardless of format type, as + // shown in Table 14. + + // Table 14 Supplemental Encoding Data + // Type Name Description + // Card8 nSups Number of supplementary mappings + // struct Supplement[nSups] Supplementary encoding array(see Table 15 below) + + // Table 15 Supplement Format + // Type Name Description + // Card8 code Encoding + // SID glyph Name + } + + /// + /// Reads the Private DICT data containing font-level hinting values and local subroutine references. + /// + /// The binary reader. + /// The parsed , or if no Private DICT is present. + private CffPrivateDictionary? ReadPrivateDict(BigEndianBinaryReader reader) + { + // per-font + if (this.privateDICTLength == 0) + { + return null; + } + + reader.BaseStream.Position = this.offset + this.privateDICTOffset; + List dicData = this.ReadDictData(reader, this.privateDICTLength); + byte[][] localSubrRawBuffers = []; + int defaultWidthX = 0; + int nominalWidthX = 0; + + if (dicData.Count > 0) + { + // Interpret the values of private dict + foreach (CffDataDicEntry dicEntry in dicData) + { + switch (dicEntry.Operator.Name) + { + case "Subrs": + int localSubrsOffset = (int)dicEntry.Operands[0].RealNumValue; + reader.BaseStream.Position = this.offset + this.privateDICTOffset + localSubrsOffset; + localSubrRawBuffers = ReadLocalSubrs(reader); + break; + + case "defaultWidthX": + defaultWidthX = (int)dicEntry.Operands[0].RealNumValue; + break; + + case "nominalWidthX": + nominalWidthX = (int)dicEntry.Operands[0].RealNumValue; + break; + } + } + } + + return new CffPrivateDictionary(localSubrRawBuffers, defaultWidthX, nominalWidthX); + } + } +} diff --git a/SixLabors.Fonts/Tables/Cff/Cff1Table.cs b/SixLabors.Fonts/Tables/Cff/Cff1Table.cs new file mode 100644 index 0000000..fc229ce --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/Cff1Table.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; +using System; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Represents the Compact Font Format (CFF) version 1 table. + /// + /// + internal sealed class Cff1Table : Table, ICffTable + { + internal const string TableName = "CFF "; // 4 chars + + private readonly CffGlyphData[] glyphs; + + /// + /// Initializes a new instance of the class. + /// + /// The parsed CFF1 font. + public Cff1Table(CffFont cff1Font) => this.glyphs = cff1Font.Glyphs; + + /// + public int GlyphCount => this.glyphs.Length; + + /// + public ItemVariationStore? ItemVariationStore => null; + + /// + public CffGlyphData GetGlyph(int index) + => this.glyphs[index]; + + /// + /// Loads the CFF1 table from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static Cff1Table? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + /// + /// Loads the CFF1 table from the specified binary reader. + /// + /// The big-endian binary reader positioned at the CFF1 table header. + /// The . + public static Cff1Table Load(BigEndianBinaryReader reader) + { + // +------+---------------+----------------------------------------+ + // | Type | Name | Description | + // +======+===============+========================================+ + // | byte | majorVersion | Format major version. Set to 1. | + // +------+---------------+----------------------------------------+ + // | byte | minorVersion | Format minor version. Set to zero. | + // +------+---------------+----------------------------------------+ + // | byte | headerSize | Header size (bytes). | + // +------+---------------+----------------------------------------+ + // | byte | topDictLength | Length of Top DICT structure in bytes. | + // +------+---------------+----------------------------------------+ + long position = reader.BaseStream.Position; + byte[] header = reader.ReadBytes(4); + byte major = header[0]; + byte minor = header[1]; + byte hdrSize = header[2]; + byte offSize = header[3]; + + switch (major) + { + case 1: + Cff1Parser parser = new(); + return new(parser.Load(reader, position)); + + default: + throw new NotSupportedException(); + } + } + } +} diff --git a/SixLabors.Fonts/Tables/Cff/Cff2Font.cs b/SixLabors.Fonts/Tables/Cff/Cff2Font.cs new file mode 100644 index 0000000..ca0323c --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/Cff2Font.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Represents a parsed CFF2 font with an associated Item Variation Store for font variations. + /// + internal class Cff2Font : CffFont + { + /// + /// Initializes a new instance of the class. + /// + /// The PostScript font name. + /// The Top DICT data. + /// The parsed glyph data array. + /// The item variation store for blend interpolation. + public Cff2Font(string name, CffTopDictionary metrics, CffGlyphData[] glyphs, ItemVariationStore itemVariationStore) + : base(name, metrics, glyphs) => this.ItemVariationStore = itemVariationStore; + + /// + /// Gets or sets the Item Variation Store used for CFF2 blend interpolation. + /// + public ItemVariationStore ItemVariationStore { get; set; } + } +} diff --git a/SixLabors.Fonts/Tables/Cff/Cff2Parser.cs b/SixLabors.Fonts/Tables/Cff/Cff2Parser.cs new file mode 100644 index 0000000..6705ee5 --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/Cff2Parser.cs @@ -0,0 +1,251 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; +using System.IO; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Parses a Compact Font Format (CFF) version 2 font program. + /// + /// + internal class Cff2Parser : CffParserBase + { + private static readonly ItemVariationStore EmptyItemVariationStoreTable = new(VariationRegionList.EmptyVariationRegionList, []); + + private long offset; + + private double[]? fontMatrix; + private int charStringIndexOffset; + private int variationStoreOffset; + private int? fdArrayOffset; + private int? fdSelectOffset; + private ItemVariationStore? itemVariationStore; + + /// + /// Loads and parses a CFF2 font from the given reader. + /// + /// The binary reader positioned after the CFF2 header. + /// The header size in bytes. + /// The length of the Top DICT data in bytes. + /// The PostScript font name. + /// The absolute offset of the CFF2 table in the font stream. + /// The parsed . + public Cff2Font Load(BigEndianBinaryReader reader, byte hdrSize, ushort topDictLength, string fontName, long offset) + { + this.offset = offset; + reader.Seek(hdrSize, SeekOrigin.Begin); + + this.ReadTopDictData(reader, topDictLength); + reader.Seek(hdrSize + topDictLength, SeekOrigin.Begin); + + CidFontInfo cidFontInfo = new() + { + FDArray = this.fdArrayOffset.GetValueOrDefault(), + FDSelect = this.fdSelectOffset.GetValueOrDefault(), + }; + + byte[][] globalSubrRawBuffers = ReadSubrBuffer(reader, cff2: true); + + // The Item Variation Store is optional. When present, its offset is + // relative to the start of the CFF2 table. + if (this.variationStoreOffset > 0) + { + reader.Seek(this.variationStoreOffset, SeekOrigin.Begin); + ushort variationStoreLength = reader.ReadUInt16(); + this.itemVariationStore = variationStoreLength == 0 + ? EmptyItemVariationStoreTable + : ItemVariationStore.Load(reader, this.variationStoreOffset + 2); + } + else + { + this.itemVariationStore = EmptyItemVariationStoreTable; + } + + if (this.fdSelectOffset.HasValue) + { + ReadFdSelect(reader, this.offset, cidFontInfo); + } + + CffIndexOffset[] charStringOffsets = this.ReadCharStringIndex(reader); + byte[][] charStringBuffers = ReadCharStringBuffers(reader, charStringOffsets); + + int fdArrayOffset = this.fdArrayOffset.GetValueOrDefault(); + FontDict[] fontDicts = this.ReadFdArray(reader, this.offset, fdArrayOffset, cff2: true); + CffTopDictionary topDictionary = new() + { + CidFontInfo = cidFontInfo, + FontMatrix = this.fontMatrix ?? [0.001, 0, 0, 0.001, 0, 0] + }; + + CffPrivateDictionary privateDictionary = fontDicts.Length > 0 + ? new(fontDicts[0].LocalSubr, 0, 0) + : new([], 0, 0); + int glyphCount = charStringOffsets.Length; + CffGlyphData[] glyphs = this.ReadCharStringsIndex(topDictionary, globalSubrRawBuffers, fontDicts, privateDictionary, charStringBuffers, glyphCount); + + return new(fontName, topDictionary, glyphs, this.itemVariationStore); + } + + /// + /// Reads the CFF2 Top DICT data, extracting offsets for CharStrings, FDArray, FDSelect, and variation store. + /// + /// The binary reader. + /// The length in bytes of the Top DICT data. + private void ReadTopDictData(BigEndianBinaryReader reader, ushort topDictLength) + { + long startPosition = reader.BaseStream.Position; + long maxPosition = startPosition + topDictLength; + while (reader.BaseStream.Position < maxPosition) + { + CffDataDicEntry dataDicEntry = this.ReadEntry(reader); + switch (dataDicEntry.Operator.Name) + { + case "FontMatrix": + this.fontMatrix = new double[dataDicEntry.Operands.Length]; + for (int i = 0; i < dataDicEntry.Operands.Length; i++) + { + this.fontMatrix[i] = dataDicEntry.Operands[i].RealNumValue; + } + + break; + case "CharStrings": + this.charStringIndexOffset = (int)dataDicEntry.Operands[0].RealNumValue; + break; + case "FDArray": + this.fdArrayOffset = (int)dataDicEntry.Operands[0].RealNumValue; + break; + case "FDSelect": + this.fdSelectOffset = (int)dataDicEntry.Operands[0].RealNumValue; + break; + case "vstore": + this.variationStoreOffset = (int)dataDicEntry.Operands[0].RealNumValue; + break; + default: + throw new InvalidFontFileException("Error parsing TopDictData."); + } + } + } + + /// + /// Reads the CharString INDEX offsets for CFF2. + /// + /// The binary reader. + /// An array of representing each charstring's position and length. + private CffIndexOffset[] ReadCharStringIndex(BigEndianBinaryReader reader) + { + reader.BaseStream.Position = this.offset + this.charStringIndexOffset; + if (!TryReadIndexDataOffsets(reader, out CffIndexOffset[]? offsets, cff2: true)) + { + throw new InvalidFontFileException("No glyph data found."); + } + + return offsets; + } + + /// + /// Reads the raw charstring byte buffers for each glyph from the CharString INDEX. + /// + /// The binary reader. + /// The charstring INDEX offsets. + /// An array of byte arrays, each containing a glyph's charstring data. + private static byte[][] ReadCharStringBuffers(BigEndianBinaryReader reader, CffIndexOffset[] offsets) + { + int glyphCount = offsets.Length; + byte[][] charStringBuffers = new byte[offsets.Length][]; + for (int i = 0; i < glyphCount; ++i) + { + CffIndexOffset cffIndexOffset = offsets[i]; + charStringBuffers[i] = reader.ReadBytes(cffIndexOffset.Length); + } + + return charStringBuffers; + } + + /// + /// Creates glyph data objects for all glyphs from the pre-read charstring buffers. + /// + /// The top-level dictionary containing font metadata. + /// The global subroutine buffers. + /// The Font DICT array for CID fonts. + /// The private dictionary containing local subroutine references. + /// The raw charstring byte buffers for each glyph. + /// The total number of glyphs. + /// An array of for each glyph. + private CffGlyphData[] ReadCharStringsIndex( + CffTopDictionary topDictionary, + byte[][] globalSubrBuffers, + FontDict[] fontDicts, + CffPrivateDictionary? privateDictionary, + byte[][] charStringBuffers, + int glyphCount) + { + // 14. CharStrings INDEX + + // This contains the charstrings of all the glyphs in a font stored in + // an INDEX structure. + + // Charstring objects contained within this + // INDEX are accessed by GID. + + // The first charstring(GID 0) must be + // the.notdef glyph. + + // The number of glyphs available in a font may + // be determined from the count field in the INDEX. + + // + + // The format of the charstring data, and therefore the method of + // interpretation, is specified by the + // CharstringType operator in the Top DICT. + + // The CharstringType operator has a default value + // of 2 indicating the Type 2 charstring format which was designed + // in conjunction with CFF. + + // Type 1 charstrings are documented in + // the “Adobe Type 1 Font Format” published by Addison - Wesley. + + // Type 2 charstrings are described in Adobe Technical Note #5177: + // “Type 2 Charstring Format.” Other charstring types may also be + // supported by this method. + CffGlyphData[] glyphs = new CffGlyphData[glyphCount]; + byte[][]? localSubBuffer = privateDictionary?.LocalSubrRawBuffers; + + // Is the font a CID font? + FDRangeProvider fdRangeProvider = new(topDictionary.CidFontInfo); + bool isCidFont = topDictionary.CidFontInfo.FdRanges.Length > 0; + int vsIndex = fontDicts.Length > 0 ? fontDicts[0].VsIndex : 0; + for (int i = 0; i < glyphCount; ++i) + { + byte[] charstringsBuffer = charStringBuffers[i]; + + // Now we can parse the raw glyph instructions + // Select proper local private dict. + if (isCidFont) + { + fdRangeProvider.SetCurrentGlyphIndex((ushort)i); + int fdIndex = fdRangeProvider.SelectedFDArray; + localSubBuffer = fontDicts[fdIndex].LocalSubr; + vsIndex = fontDicts[fdIndex].VsIndex; + } + + glyphs[i] = new CffGlyphData( + (ushort)i, + globalSubrBuffers, + localSubBuffer ?? [], + privateDictionary?.NominalWidthX ?? 0, + charstringsBuffer, + 2, + this.itemVariationStore, + vsIndex) + { + FontMatrix = topDictionary.FontMatrix + }; + } + + return glyphs; + } + } +} diff --git a/SixLabors.Fonts/Tables/Cff/Cff2Table.cs b/SixLabors.Fonts/Tables/Cff/Cff2Table.cs new file mode 100644 index 0000000..e1846fb --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/Cff2Table.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Globalization; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; +using SixLabors.Fonts.Tables.General.Name; +using SixLabors.Fonts.WellKnownIds; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Represents the Compact Font Format (CFF) version 2 table. + /// + /// + internal sealed class Cff2Table : Table, ICffTable + { + internal const string TableName = "CFF2"; + + private readonly CffGlyphData[] glyphs; + + /// + /// Initializes a new instance of the class. + /// + /// The parsed CFF font. + /// The item variation store for font variations. + public Cff2Table(CffFont cffFont, ItemVariationStore itemVariationStore) + { + this.glyphs = cffFont.Glyphs; + this.ItemVariationStore = itemVariationStore; + } + + /// + public int GlyphCount => this.glyphs.Length; + + /// + public ItemVariationStore ItemVariationStore { get; } + + /// + public CffGlyphData GetGlyph(int index) + => this.glyphs[index]; + + /// + /// Loads the CFF2 table from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static Cff2Table? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + NameTable nameTable = fontReader.GetTable(); + string fontName = nameTable.GetNameById(CultureInfo.InvariantCulture, KnownNameIds.PostscriptName); + + using (binaryReader) + { + return Load(binaryReader, fontName); + } + } + + /// + /// Loads the CFF2 table from the specified binary reader. + /// + /// The big-endian binary reader positioned at the CFF2 table header. + /// The PostScript font name. + /// The . + public static Cff2Table Load(BigEndianBinaryReader reader, string fontName) + { + long position = reader.BaseStream.Position; + byte major = reader.ReadUInt8(); + byte minor = reader.ReadUInt8(); + byte hdrSize = reader.ReadUInt8(); + ushort topDictLength = reader.ReadUInt16(); + + switch (major) + { + case 2: + Cff2Parser parser = new(); + Cff2Font cffFont = parser.Load(reader, hdrSize, topDictLength, fontName, position); + return new(cffFont, cffFont.ItemVariationStore); + + default: + throw new NotSupportedException("CFF version 2 is expected"); + } + } + } +} diff --git a/SixLabors.Fonts/Tables/Cff/CffBoundsFinder.cs b/SixLabors.Fonts/Tables/Cff/CffBoundsFinder.cs new file mode 100644 index 0000000..03b35db --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/CffBoundsFinder.cs @@ -0,0 +1,237 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.Fonts.Rendering; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Calculates the bounding box of a CFF glyph by implementing + /// and tracking the minimum and maximum coordinates of all path operations. + /// + internal class CffBoundsFinder : IGlyphRenderer + { + private float minX; + private float maxX; + private float minY; + private float maxY; + private Vector2 currentXY; + private readonly int nsteps; + private bool open; + private bool firstEval; + + /// + /// Initializes a new instance of the class. + /// + public CffBoundsFinder() + { + this.minX = float.MaxValue; + this.maxX = float.MinValue; + this.minY = float.MaxValue; + this.maxY = float.MinValue; + this.nsteps = 3; + this.currentXY = Vector2.Zero; + this.open = false; + this.firstEval = true; + } + + /// + public void BeginFigure() + { + // Do nothing. + } + + /// + public bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) + => true; // Do nothing. + + /// + public void BeginText(in FontRectangle bounds) + { + // Do nothing. + } + + /// + public void EndFigure() + { + this.open = false; + this.currentXY = Vector2.Zero; + } + + /// + public void EndGlyph() + { + if (this.open) + { + this.EndFigure(); + } + } + + /// + public void EndText() + { + if (this.open) + { + this.EndFigure(); + } + } + + /// + public void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) + { + // Do nothing. + } + + /// + public void EndLayer() + { + // Do nothing. + } + + /// + public void LineTo(Vector2 point) + { + this.currentXY = point; + this.UpdateMinMax(point.X, point.Y); + this.open = true; + } + + /// + public void MoveTo(Vector2 point) + { + if (this.open) + { + this.EndFigure(); + } + + this.currentXY = point; + this.UpdateMinMax(point.X, point.Y); + } + + /// + public void ArcTo(float radiusX, float radiusY, float xAxisRotation, bool largeArc, bool sweep, Vector2 point) + { + // TODO: check this. I feel like we should have to implement it. + this.currentXY = point; + this.UpdateMinMax(point.X, point.Y); + this.open = true; + } + + /// + public void CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) + { + float eachstep = 1F / this.nsteps; + float t = eachstep; // Start + + for (int n = 1; n < this.nsteps; ++n) + { + float c = 1F - t; + Vector2 xy = (this.currentXY * c * c * c) + (secondControlPoint * 3 * t * c * c) + (thirdControlPoint * 3 * t * t * c) + (point * t * t * t); + this.UpdateMinMax(xy.X, xy.Y); + + t += eachstep; + } + + this.currentXY = point; + this.UpdateMinMax(point.X, point.Y); + this.open = true; + } + + /// + public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) + { + float eachstep = 1F / this.nsteps; + float t = eachstep; // Start + + for (int n = 1; n < this.nsteps; ++n) + { + float c = 1F - t; + Vector2 xy = (this.currentXY * c * c) + (secondControlPoint * 2 * t * c) + (point * t * t); + this.UpdateMinMax(xy.X, xy.Y); + + t += eachstep; + } + + this.currentXY = point; + this.UpdateMinMax(point.X, point.Y); + this.open = true; + } + + /// + public TextDecorations EnabledDecorations() + => TextDecorations.None; + + /// + public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) + { + // Do nothing. + } + + /// + /// Updates the tracked minimum and maximum coordinates with the given point. + /// + /// The x-coordinate to evaluate. + /// The y-coordinate to evaluate. + private void UpdateMinMax(float x0, float y0) + { + if (this.firstEval) + { + // 4 times + if (x0 < this.minX) + { + this.minX = x0; + } + + if (x0 > this.maxX) + { + this.maxX = x0; + } + + if (y0 < this.minY) + { + this.minY = y0; + } + + if (y0 > this.maxY) + { + this.maxY = y0; + } + + this.firstEval = false; + } + else + { + // 2 times + if (x0 < this.minX) + { + this.minX = x0; + } + else if (x0 > this.maxX) + { + this.maxX = x0; + } + + if (y0 < this.minY) + { + this.minY = y0; + } + else if (y0 > this.maxY) + { + this.maxY = y0; + } + } + } + + /// + /// Gets the computed bounding box from all tracked path coordinates. + /// + /// The representing the glyph bounding box. + public Bounds GetBounds() + => new( + (short)Math.Floor(this.minX), + (short)Math.Floor(this.minY), + (short)Math.Ceiling(this.maxX), + (short)Math.Ceiling(this.maxY)); + } +} diff --git a/SixLabors.Fonts/Tables/Cff/CffDataDicEntry.cs b/SixLabors.Fonts/Tables/Cff/CffDataDicEntry.cs new file mode 100644 index 0000000..89caf62 --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/CffDataDicEntry.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#if DEBUG +using System.Text; +#endif + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Represents a single key-value entry parsed from a CFF DICT structure. + /// The key is a and the value is an array of . + /// + internal class CffDataDicEntry + { + /// + /// Initializes a new instance of the class. + /// + /// The DICT operator. + /// The operand values for this operator. + public CffDataDicEntry(CFFOperator @operator, CffOperand[] operands) + { + this.Operator = @operator; + this.Operands = operands; + } + + /// + /// Gets the DICT operator that identifies this entry. + /// + public CFFOperator Operator { get; } + + /// + /// Gets the operand values associated with this operator. + /// + public CffOperand[] Operands { get; } + +#if DEBUG + /// + public override string ToString() + { + StringBuilder builder = new(); + int j = this.Operands.Length; + for (int i = 0; i < j; ++i) + { + if (i > 0) + { + builder.Append(' '); + } + + builder.Append(this.Operands[i].ToString()); + } + + builder.Append(' ') + .Append(this.Operator?.ToString() ?? string.Empty); + return builder.ToString(); + } +#endif + } +} diff --git a/SixLabors.Fonts/Tables/Cff/CffEvaluationEngine.cs b/SixLabors.Fonts/Tables/Cff/CffEvaluationEngine.cs new file mode 100644 index 0000000..5fbe9d7 --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/CffEvaluationEngine.cs @@ -0,0 +1,865 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Decodes the commands and numbers making up a Type 2 CharString. A Type 2 CharString extends on the Type 1 CharString format. + /// Compared to the Type 1 format, the Type 2 encoding offers smaller size and an opportunity for better rendering quality and + /// performance. The Type 2 charstring operators are (with one exception) a superset of the Type 1 operators. + /// + /// + /// A Type 2 charstring program is a sequence of unsigned 8-bit bytes that encode numbers and operators. + /// The byte value specifies a operator, a number, or subsequent bytes that are to be interpreted in a specific manner. + /// + internal ref struct CffEvaluationEngine + { + private static readonly Random Random = new(); + private float? width; + private int nStems; + private float x; + private float y; + private RefStack stack; + private readonly ReadOnlySpan charStrings; + private readonly ReadOnlySpan globalSubrBuffers; + private readonly ReadOnlySpan localSubrBuffers; + private TransformingGlyphRenderer transforming; + private readonly int nominalWidthX; + private readonly int globalBias; + private readonly int localBias; + private readonly Dictionary trans; + private bool isDisposed; + private readonly int version; + private readonly GlyphVariationProcessor? glyphVariationProcessor; + private int vsIndex; + + /// + /// Initializes a new instance of the struct. + /// + /// The raw charstring byte data for the glyph. + /// The global subroutine buffers. + /// The local subroutine buffers. + /// The nominal width used as a bias for charstring width values. + /// The CFF version (1 or 2). + /// The optional item variation store for CFF2 blend operations. + /// The optional font variations table. + /// The optional axis variations table. + /// The variation store index for blend operations. + public CffEvaluationEngine( + ReadOnlySpan charStrings, + ReadOnlySpan globalSubrBuffers, + ReadOnlySpan localSubrBuffers, + int nominalWidthX, + int version, + ItemVariationStore? itemVariationStore = null, + FVarTable? fVar = null, + AVarTable? aVar = null, + int vsIndex = 0) + { + this.transforming = default; + this.charStrings = charStrings; + this.globalSubrBuffers = globalSubrBuffers; + this.localSubrBuffers = localSubrBuffers; + this.nominalWidthX = nominalWidthX; + + this.globalBias = CalculateBias(this.globalSubrBuffers.Length); + this.localBias = CalculateBias(this.localSubrBuffers.Length); + this.trans = []; + + this.x = 0; + this.y = 0; + this.width = null; + this.nStems = 0; + this.stack = new(50); + this.isDisposed = false; + this.version = version; + this.glyphVariationProcessor = null; + + if (itemVariationStore != null) + { + if (fVar is null) + { + throw new InvalidFontFileException("missing fVar table required for glyph variations processing"); + } + + this.glyphVariationProcessor = new GlyphVariationProcessor(itemVariationStore, fVar, aVar); + } + + this.vsIndex = vsIndex; + } + + /// + /// Computes the bounding box of the glyph by evaluating the charstring program. + /// + /// The of the glyph. + public Bounds GetBounds() + { + this.Reset(); + + // TODO: It would be nice to avoid the allocation here. + CffBoundsFinder finder = new(); + + // Note: scale is passed with negative Y to flip the Y axis. + this.transforming = new(finder, Vector2.Zero, new Vector2(1, -1), Vector2.Zero, Matrix3x2.Identity); + + // Boolean IGlyphRenderer.BeginGlyph(..) is handled by the caller. + this.Parse(this.charStrings); + + // Some CFF end without closing the latest contour. + if (this.transforming.IsOpen) + { + this.transforming.EndFigure(); + } + + return finder.GetBounds(); + } + + /// + /// Evaluates the charstring program and renders the glyph outline to the specified renderer. + /// + /// The glyph renderer to output path operations to. + /// The origin point for rendering. + /// The scale factor to apply. + /// The offset to apply. + /// The transformation matrix to apply. + public void RenderTo(IGlyphRenderer renderer, Vector2 origin, Vector2 scale, Vector2 offset, Matrix3x2 transform) + { + this.Reset(); + + this.transforming = new(renderer, origin, scale, offset, transform); + + // Boolean IGlyphRenderer.BeginGlyph(..) is handled by the caller. + this.Parse(this.charStrings); + + // Some CFF end without closing the latest contour. + if (this.transforming.IsOpen) + { + this.transforming.EndFigure(); + } + } + + /// + /// Parses and interprets a Type 2 charstring byte buffer, executing operators and accumulating operands. + /// + /// The charstring byte data to parse. + private void Parse(ReadOnlySpan buffer) + { + SimpleBinaryReader reader = new(buffer); + bool endCharEncountered = false; + while (!endCharEncountered && reader.CanRead()) + { + byte b0 = reader.ReadByte(); + if (b0 < 32) + { + int index; + ReadOnlySpan subr; + bool phase; + float c1x; + float c1y; + float c2x; + float c2y; + + var oneByteOperator = (Type2Operator1)b0; + switch (oneByteOperator) + { + case Type2Operator1.Hstem: + case Type2Operator1.Vstem: + case Type2Operator1.Hstemhm: + case Type2Operator1.Vstemhm: + + this.ParseStems(); + break; + + case Type2Operator1.Vmoveto: + + if (this.stack.Length > 1) + { + this.CheckWidth(); + } + + this.y += this.stack.Shift(); + this.transforming.MoveTo(new Vector2(this.x, this.y)); + + this.stack.Clear(); + break; + + case Type2Operator1.Rlineto: + + while (this.stack.Length >= 2) + { + this.x += this.stack.Shift(); + this.y += this.stack.Shift(); + this.transforming.LineTo(new Vector2(this.x, this.y)); + } + + this.stack.Clear(); + break; + + case Type2Operator1.Hlineto: + case Type2Operator1.Vlineto: + phase = oneByteOperator == Type2Operator1.Hlineto; + + while (this.stack.Length >= 1) + { + if (phase) + { + this.x += this.stack.Shift(); + } + else + { + this.y += this.stack.Shift(); + } + + this.transforming.LineTo(new Vector2(this.x, this.y)); + phase = !phase; + } + + this.stack.Clear(); + break; + + case Type2Operator1.Rrcurveto: + + while (this.stack.Length > 0) + { + this.transforming.CubicBezierTo( + new Vector2(this.x += this.stack.Shift(), this.y += this.stack.Shift()), + new Vector2(this.x += this.stack.Shift(), this.y += this.stack.Shift()), + new Vector2(this.x += this.stack.Shift(), this.y += this.stack.Shift())); + } + + this.stack.Clear(); + break; + + case Type2Operator1.Callsubr: + index = (int)this.stack.Pop() + this.localBias; + subr = this.localSubrBuffers[index]; + + if (subr.Length > 0) + { + this.Parse(subr); + } + + break; + + case Type2Operator1.Return: + + if (this.version >= 2) + { + break; + } + + return; + + case Type2Operator1.Endchar: + + if (this.version >= 2) + { + break; + } + + if (this.stack.Length > 0) + { + this.CheckWidth(); + } + + if (this.transforming.IsOpen) + { + this.transforming.EndFigure(); + } + + endCharEncountered = true; + break; + + case Type2Operator1.VsIndex: + if (this.version < 2) + { + throw new NotSupportedException("blend operator is not supported in CFF v1"); + } + + this.vsIndex = (int)this.stack.Pop(); + break; + case Type2Operator1.Blend: + if (this.version < 2) + { + throw new NotSupportedException("blend operator is not supported in CFF v1"); + } + + if (this.glyphVariationProcessor is null) + { + throw new NotSupportedException("blend operator in non-variation font"); + } + + float[] blendVector = this.glyphVariationProcessor.BlendVector(this.vsIndex); + float numBlends = this.stack.Pop(); + float numOperands = numBlends * blendVector.Length; + int delta = this.stack.Length - (int)numOperands; + int basis = delta - (int)numBlends; + + for (int i = 0; i < numBlends; i++) + { + float sum = this.stack[basis + i]; + for (int j = 0; j < blendVector.Length; j++) + { + sum += blendVector[j] * this.stack[delta++]; + } + + this.stack[basis + i] = sum; + } + + while (numOperands-- > 0) + { + this.stack.Pop(); + } + + break; + + case Type2Operator1.Hintmask: + case Type2Operator1.Cntrmask: + + this.ParseStems(); + reader.Position += (this.nStems + 7) >> 3; + + break; + + case Type2Operator1.Rmoveto: + + if (this.stack.Length > 2) + { + this.CheckWidth(); + } + + this.x += this.stack.Shift(); + this.y += this.stack.Shift(); + this.transforming.MoveTo(new Vector2(this.x, this.y)); + + this.stack.Clear(); + break; + + case Type2Operator1.Hmoveto: + + if (this.stack.Length > 1) + { + this.CheckWidth(); + } + + this.x += this.stack.Shift(); + this.transforming.MoveTo(new Vector2(this.x, this.y)); + + this.stack.Clear(); + break; + + case Type2Operator1.Rcurveline: + + while (this.stack.Length >= 8) + { + this.transforming.CubicBezierTo( + new Vector2(this.x += this.stack.Shift(), this.y += this.stack.Shift()), + new Vector2(this.x += this.stack.Shift(), this.y += this.stack.Shift()), + new Vector2(this.x += this.stack.Shift(), this.y += this.stack.Shift())); + } + + this.transforming.LineTo(new Vector2(this.x += this.stack.Shift(), this.y += this.stack.Shift())); + + this.stack.Clear(); + break; + + case Type2Operator1.Rlinecurve: + + while (this.stack.Length >= 8) + { + this.x += this.stack.Shift(); + this.y += this.stack.Shift(); + this.transforming.LineTo(new Vector2(this.x, this.y)); + } + + c1x = this.x + this.stack.Shift(); + c1y = this.y + this.stack.Shift(); + c2x = c1x + this.stack.Shift(); + c2y = c1y + this.stack.Shift(); + this.x = c2x + this.stack.Shift(); + this.y = c2y + this.stack.Shift(); + + this.transforming.CubicBezierTo( + new Vector2(c1x, c1y), + new Vector2(c2x, c2y), + new Vector2(this.x, this.y)); + + this.stack.Clear(); + break; + + case Type2Operator1.Vvcurveto: + + if (this.stack.Length % 2 != 0) + { + this.x += this.stack.Shift(); + } + + while (this.stack.Length >= 4) + { + c1x = this.x; + c1y = this.y + this.stack.Shift(); + c2x = c1x + this.stack.Shift(); + c2y = c1y + this.stack.Shift(); + this.x = c2x; + this.y = c2y + this.stack.Shift(); + + this.transforming.CubicBezierTo( + new Vector2(c1x, c1y), + new Vector2(c2x, c2y), + new Vector2(this.x, this.y)); + } + + this.stack.Clear(); + break; + + case Type2Operator1.Hhcurveto: + + if (this.stack.Length % 2 != 0) + { + this.y += this.stack.Shift(); + } + + while (this.stack.Length >= 4) + { + c1x = this.x + this.stack.Shift(); + c1y = this.y; + c2x = c1x + this.stack.Shift(); + c2y = c1y + this.stack.Shift(); + this.x = c2x + this.stack.Shift(); + this.y = c2y; + + this.transforming.CubicBezierTo( + new Vector2(c1x, c1y), + new Vector2(c2x, c2y), + new Vector2(this.x, this.y)); + } + + this.stack.Clear(); + break; + + case Type2Operator1.Shortint: + + this.stack.Push(reader.ReadInt16BE()); + break; + + case Type2Operator1.Callgsubr: + + index = (int)this.stack.Pop() + this.globalBias; + subr = this.globalSubrBuffers[index]; + + if (subr.Length > 0) + { + this.Parse(subr); + } + + break; + + case Type2Operator1.Vhcurveto: + case Type2Operator1.Hvcurveto: + + phase = oneByteOperator == Type2Operator1.Hvcurveto; + while (this.stack.Length >= 4) + { + if (phase) + { + c1x = this.x + this.stack.Shift(); + c1y = this.y; + c2x = c1x + this.stack.Shift(); + c2y = c1y + this.stack.Shift(); + this.y = c2y + this.stack.Shift(); + this.x = c2x + (this.stack.Length == 1 ? this.stack.Shift() : 0); + } + else + { + c1x = this.x; + c1y = this.y + this.stack.Shift(); + c2x = c1x + this.stack.Shift(); + c2y = c1y + this.stack.Shift(); + this.x = c2x + this.stack.Shift(); + this.y = c2y + (this.stack.Length == 1 ? this.stack.Shift() : 0); + } + + this.transforming.CubicBezierTo(new Vector2(c1x, c1y), new Vector2(c2x, c2y), new Vector2(this.x, this.y)); + phase = !phase; + } + + this.stack.Clear(); + break; + + case Type2Operator1.Escape: + + bool a; + bool b; + byte twoByteOperator = reader.ReadByte(); + if (twoByteOperator >= 38) + { + ThrowInvalidOperator(twoByteOperator); + return; + } + + switch ((Type2Operator2)twoByteOperator) + { + case Type2Operator2.And: + + a = this.stack.Pop() != 0; + b = this.stack.Pop() != 0; + this.stack.Push((a && b) ? 1 : 0); + break; + + case Type2Operator2.Or: + + a = this.stack.Pop() != 0; + b = this.stack.Pop() != 0; + this.stack.Push((a || b) ? 1 : 0); + break; + + case Type2Operator2.Not: + + a = this.stack.Pop() != 0; + this.stack.Push(a ? 1 : 0); + break; + + case Type2Operator2.Abs: + + this.stack.Push(Math.Abs(this.stack.Pop())); + break; + + case Type2Operator2.Add: + + this.stack.Push(this.stack.Pop() + this.stack.Pop()); + break; + + case Type2Operator2.Sub: + + this.stack.Push(this.stack.Pop() - this.stack.Pop()); + break; + + case Type2Operator2.Div: + + this.stack.Push(this.stack.Pop() / this.stack.Pop()); + break; + + case Type2Operator2.Neg: + + this.stack.Push(-this.stack.Pop()); + break; + + case Type2Operator2.Eq: + + this.stack.Push(this.stack.Pop() == this.stack.Pop() ? 1 : 0); + break; + + case Type2Operator2.Drop: + + this.stack.Pop(); + break; + + case Type2Operator2.Put: + + float val = this.stack.Pop(); + int idx = (int)this.stack.Pop(); + + this.trans[idx] = val; + break; + + case Type2Operator2.Get: + + idx = (int)this.stack.Pop(); + this.trans.TryGetValue(idx, out float v); + this.stack.Push(v); + this.trans.Remove(idx); + break; + + case Type2Operator2.Ifelse: + + float s1 = this.stack.Pop(); + float s2 = this.stack.Pop(); + float v1 = this.stack.Pop(); + float v2 = this.stack.Pop(); + + this.stack.Push(v1 <= v2 ? s1 : s2); + break; + + case Type2Operator2.Random: + this.stack.Push((float)Random.NextDouble()); + break; + + case Type2Operator2.Mul: + + this.stack.Push(this.stack.Pop() * this.stack.Pop()); + break; + + case Type2Operator2.Sqrt: + + this.stack.Push(MathF.Sqrt(this.stack.Pop())); + break; + + case Type2Operator2.Dup: + + float m = this.stack.Pop(); + this.stack.Push(m); + this.stack.Push(m); + break; + + case Type2Operator2.Exch: + + float ex = this.stack.Pop(); + float ch = this.stack.Pop(); + this.stack.Push(ch); + this.stack.Push(ex); + break; + + case Type2Operator2.Index: + + idx = (int)this.stack.Pop(); + if (idx < 0) + { + idx = 0; + } + else if (idx > this.stack.Length - 1) + { + idx = this.stack.Length - 1; + } + + this.stack.Push(this.stack[idx]); + break; + + case Type2Operator2.Roll: + + int n = (int)this.stack.Pop(); + float j = this.stack.Pop(); + + if (j >= 0) + { + while (j > 0) + { + float t = this.stack[n - 1]; + for (int i = n - 2; i >= 0; i--) + { + this.stack[i + 1] = this.stack[i]; + } + + this.stack[0] = t; + j--; + } + } + else + { + while (j < 0) + { + float t = this.stack[0]; + for (int i = 0; i <= n; i++) + { + this.stack[i] = this.stack[i + 1]; + } + + this.stack[n - 1] = t; + j++; + } + } + + break; + + case Type2Operator2.Hflex: + + c1x = this.x + this.stack.Shift(); + c1y = this.y; + c2x = c1x + this.stack.Shift(); + c2y = c1y + this.stack.Shift(); + float c3x = c2x + this.stack.Shift(); + float c3y = c2y; + float c4x = c3x + this.stack.Shift(); + float c4y = c3y; + float c5x = c4x + this.stack.Shift(); + float c5y = c4y; + float c6x = c5x + this.stack.Shift(); + float c6y = c5y; + this.x = c6x; + this.y = c6y; + + this.transforming.CubicBezierTo(new Vector2(c1x, c1y), new Vector2(c2x, c2y), new Vector2(c3x, c3y)); + this.transforming.CubicBezierTo(new Vector2(c4x, c4y), new Vector2(c5x, c5y), new Vector2(c6x, c6y)); + + this.stack.Clear(); + break; + + case Type2Operator2.Flex: + + this.transforming.CubicBezierTo(new Vector2(this.stack.Shift(), this.stack.Shift()), new Vector2(this.stack.Shift(), this.stack.Shift()), new Vector2(this.stack.Shift(), this.stack.Shift())); + this.transforming.CubicBezierTo(new Vector2(this.stack.Shift(), this.stack.Shift()), new Vector2(this.stack.Shift(), this.stack.Shift()), new Vector2(this.stack.Shift(), this.stack.Shift())); + + this.stack.Shift(); + + this.stack.Clear(); + break; + + case Type2Operator2.Hflex1: + + c1x = this.x + this.stack.Shift(); + c1y = this.y + this.stack.Shift(); + c2x = c1x + this.stack.Shift(); + c2y = c1y + this.stack.Shift(); + c3x = c2x + this.stack.Shift(); + c3y = c2y; + c4x = c3x + this.stack.Shift(); + c4y = c3y; + c5x = c4x + this.stack.Shift(); + c5y = c4y + this.stack.Shift(); + c6x = c5x + this.stack.Shift(); + c6y = c5y; + this.x = c6x; + this.y = c6y; + + this.transforming.CubicBezierTo(new Vector2(c1x, c1y), new Vector2(c2x, c2y), new Vector2(c3x, c3y)); + this.transforming.CubicBezierTo(new Vector2(c4x, c4y), new Vector2(c5x, c5y), new Vector2(c6x, c6y)); + + this.stack.Clear(); + break; + + case Type2Operator2.Flex1: + + float startX = this.x; + float startY = this.y; + + c1x = this.x + this.stack.Shift(); + c1y = this.y + this.stack.Shift(); + + c2x = c1x + this.stack.Shift(); + c2y = c1y + this.stack.Shift(); + + c3x = c2x + this.stack.Shift(); + c3y = c2y + this.stack.Shift(); + + c4x = c3x + this.stack.Shift(); + c4y = c3y + this.stack.Shift(); + + c5x = c4x + this.stack.Shift(); + c5y = c4y + this.stack.Shift(); + + if (MathF.Abs(this.x - startX) > Math.Abs(this.y - startY)) + { + // horizontal + c6x = c5x + this.stack.Shift(); + c6y = startY; + } + else + { + c6x = startX; + c6y = c5y + this.stack.Shift(); + } + + this.x = c6x; + this.y = c6y; + + this.transforming.CubicBezierTo(new Vector2(c1x, c1y), new Vector2(c2x, c2y), new Vector2(c3x, c3y)); + this.transforming.CubicBezierTo(new Vector2(c4x, c4y), new Vector2(c5x, c5y), new Vector2(c6x, c6y)); + + this.stack.Clear(); + break; + } + + break; + } + } + else if (b0 < 247) + { + this.stack.Push(b0 - 139); + } + else if (b0 < 251) + { + byte b1 = reader.ReadByte(); + this.stack.Push(((b0 - 247) * 256) + b1 + 108); + } + else if (b0 < 255) + { + byte b1 = reader.ReadByte(); + this.stack.Push((-(b0 - 251) * 256) - b1 - 108); + } + else + { + this.stack.Push(reader.ReadFloatFixed1616()); + } + } + } + + /// + /// Releases the resources used by the evaluation engine stack. + /// + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.stack.Dispose(); + this.isDisposed = true; + } + + /// + /// Calculates the subroutine bias based on the number of subroutines, as specified in the Type 2 charstring format. + /// + /// The number of subroutines in the INDEX. + /// The bias value to add to subroutine indices. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CalculateBias(int count) + { + if (count == 0) + { + return 0; + } + + return (count < 1240) ? 107 : (count < 33900) ? 1131 : 32768; + } + + /// + /// Parses stem hint operators, consuming width if present and counting hint pairs. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ParseStems() + { + if (this.stack.Length % 2 != 0) + { + this.CheckWidth(); + } + + this.nStems += this.stack.Length >> 1; + this.stack.Clear(); + } + + /// + /// Checks whether a glyph width value is present at the bottom of the stack and consumes it. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CheckWidth() + => this.width ??= this.stack.Shift() + this.nominalWidthX; + + /// + /// Resets the evaluation engine state for a new rendering pass. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Reset() + { + this.x = 0; + this.y = 0; + this.width = null; + this.nStems = 0; + this.stack.Clear(); + this.trans.Clear(); + } + + /// + /// Throws an for an unrecognized charstring operator. + /// + /// The unrecognized operator byte value. + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowInvalidOperator(byte @operator) + => throw new InvalidFontFileException($"Unknown operator:{@operator}"); + } +} diff --git a/SixLabors.Fonts/Tables/Cff/CffFont.cs b/SixLabors.Fonts/Tables/Cff/CffFont.cs new file mode 100644 index 0000000..dd2e56a --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/CffFont.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Represents a parsed CFF font containing the top-level dictionary and glyph data. + /// + internal class CffFont + { + /// + /// Initializes a new instance of the class. + /// + /// The PostScript font name. + /// The Top DICT data containing font-wide properties. + /// The parsed glyph data array. + public CffFont(string name, CffTopDictionary metrics, CffGlyphData[] glyphs) + { + this.FontName = name; + this.Metrics = metrics; + this.Glyphs = glyphs; + } + + /// + /// Gets or sets the PostScript font name. + /// + public string FontName { get; set; } + + /// + /// Gets or sets the Top DICT data containing font-wide metrics and properties. + /// + public CffTopDictionary Metrics { get; set; } + + /// + /// Gets the array of glyph data parsed from the CharStrings INDEX. + /// + public CffGlyphData[] Glyphs { get; } + } +} diff --git a/SixLabors.Fonts/Tables/Cff/CffGlyphData.cs b/SixLabors.Fonts/Tables/Cff/CffGlyphData.cs new file mode 100644 index 0000000..efaacde --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/CffGlyphData.cs @@ -0,0 +1,135 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Represents the data for a single CFF glyph, including the raw charstring program + /// and subroutine references needed for evaluation and rendering. + /// + internal struct CffGlyphData + { + private readonly byte[][] globalSubrBuffers; + private readonly byte[][] localSubrBuffers; + private readonly byte[] charStrings; + private readonly int nominalWidthX; + private readonly int version; + private readonly ItemVariationStore? itemVariationStore; + private readonly int vsIndex; + + /// + /// Initializes a new instance of the struct. + /// + /// The glyph index (GID). + /// The global subroutine buffers. + /// The local subroutine buffers. + /// The nominal width bias for charstring width values. + /// The raw charstring byte data for this glyph. + /// The CFF version (1 or 2). + /// The optional item variation store for CFF2 blend operations. + /// The variation store index for blend operations. + public CffGlyphData( + ushort glyphIndex, + byte[][] globalSubrBuffers, + byte[][] localSubrBuffers, + int nominalWidthX, + byte[] charStrings, + int version, + ItemVariationStore? itemVariationStore = null, + int vsIndex = 0) + { + this.GlyphIndex = glyphIndex; + this.globalSubrBuffers = globalSubrBuffers; + this.localSubrBuffers = localSubrBuffers; + this.nominalWidthX = nominalWidthX; + this.charStrings = charStrings; + this.version = version; + this.itemVariationStore = itemVariationStore; + this.vsIndex = vsIndex; + + this.GlyphName = null; + + // Variations tables are only present for CFF2 format. + this.FVar = null; + this.AVar = null; + this.GVar = null; + } + + /// + /// Gets the glyph index (GID) within the font. + /// + public ushort GlyphIndex { get; } + + /// + /// Gets or sets the glyph name from the charset data. + /// + public string? GlyphName { get; set; } + + /// + /// Gets or sets the font variations table for CFF2 variable fonts. + /// + public FVarTable? FVar { get; set; } + + /// + /// Gets or sets the axis variations table for CFF2 variable fonts. + /// + public AVarTable? AVar { get; set; } + + /// + /// Gets or sets the glyph variations table for TrueType-style glyph variations. + /// + public GVarTable? GVar { get; set; } + + /// + /// Gets or sets the FontMatrix that transforms charstring coordinates to design units. + /// + public double[]? FontMatrix { get; set; } + + /// + /// Computes the bounding box of this glyph by evaluating the charstring program. + /// + /// The of the glyph. + public readonly Bounds GetBounds() + { + using CffEvaluationEngine engine = new( + this.charStrings, + this.globalSubrBuffers, + this.localSubrBuffers, + this.nominalWidthX, + this.version, + this.itemVariationStore, + this.FVar, + this.AVar, + this.vsIndex); + + return engine.GetBounds(); + } + + /// + /// Renders this glyph to the specified renderer by evaluating the charstring program. + /// + /// The glyph renderer to output path operations to. + /// The origin point for rendering. + /// The scale factor to apply. + /// The offset to apply. + /// The transformation matrix to apply. + public readonly void RenderTo(IGlyphRenderer renderer, Vector2 origin, Vector2 scale, Vector2 offset, Matrix3x2 transform) + { + using CffEvaluationEngine engine = new( + this.charStrings, + this.globalSubrBuffers, + this.localSubrBuffers, + this.nominalWidthX, + this.version, + this.itemVariationStore, + this.FVar, + this.AVar, + this.vsIndex); + + engine.RenderTo(renderer, origin, scale, offset, transform); + } + } +} diff --git a/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs b/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs new file mode 100644 index 0000000..ff1f83a --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs @@ -0,0 +1,177 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Represents a glyph metric from a particular Compact Font Face. + /// + internal class CffGlyphMetrics : FontGlyphMetrics + { + private CffGlyphData glyphData; + + /// + /// Initializes a new instance of the class with text attribute parameters. + /// + /// The font metrics. + /// The glyph identifier. + /// The Unicode code point. + /// The CFF glyph data containing the charstring program. + /// The glyph bounding box. + /// The advance width. + /// The advance height. + /// The left side bearing. + /// The top side bearing. + /// The units per em. + /// The text attributes. + /// The text decorations. + /// The glyph type. + internal CffGlyphMetrics( + StreamFontMetrics fontMetrics, + ushort glyphId, + CodePoint codePoint, + CffGlyphData glyphData, + Bounds bounds, + ushort advanceWidth, + ushort advanceHeight, + short leftSideBearing, + short topSideBearing, + ushort unitsPerEM, + TextAttributes textAttributes, + TextDecorations textDecorations, + GlyphType glyphType) + : base( + fontMetrics, + glyphId, + codePoint, + bounds, + advanceWidth, + advanceHeight, + leftSideBearing, + topSideBearing, + unitsPerEM, + textAttributes, + textDecorations, + glyphType) + => this.glyphData = glyphData; + + /// + /// Initializes a new instance of the class with offset, scale, and text run parameters. + /// + /// The font metrics. + /// The glyph identifier. + /// The Unicode code point. + /// The CFF glyph data containing the charstring program. + /// The glyph bounding box. + /// The advance width. + /// The advance height. + /// The left side bearing. + /// The top side bearing. + /// The units per em. + /// The glyph offset. + /// The scale factor. + /// The text run for rendering. + /// The glyph type. + internal CffGlyphMetrics( + StreamFontMetrics fontMetrics, + ushort glyphId, + CodePoint codePoint, + CffGlyphData glyphData, + Bounds bounds, + ushort advanceWidth, + ushort advanceHeight, + short leftSideBearing, + short topSideBearing, + ushort unitsPerEM, + Vector2 offset, + Vector2 scaleFactor, + TextRun textRun, + GlyphType glyphType) + : base( + fontMetrics, + glyphId, + codePoint, + bounds, + advanceWidth, + advanceHeight, + leftSideBearing, + topSideBearing, + unitsPerEM, + offset, + scaleFactor, + textRun, + glyphType) + => this.glyphData = glyphData; + + /// + internal override FontGlyphMetrics CloneForRendering(TextRun textRun) + => new CffGlyphMetrics( + this.FontMetrics, + this.GlyphId, + this.CodePoint, + this.glyphData, + this.Bounds, + this.AdvanceWidth, + this.AdvanceHeight, + this.LeftSideBearing, + this.TopSideBearing, + this.UnitsPerEm, + this.Offset, + this.ScaleFactor, + textRun, + this.GlyphType); + + /// + internal override void RenderTo( + IGlyphRenderer renderer, + int graphemeIndex, + Vector2 glyphOrigin, + Vector2 decorationOrigin, + GlyphLayoutMode mode, + TextOptions options) + { + // https://www.unicode.org/faq/unsup_char.html + if (ShouldSkipGlyphRendering(this.CodePoint)) + { + return; + } + + float pointSize = this.TextRun.Font?.Size ?? options.Font.Size; + float dpi = options.Dpi; + + glyphOrigin *= dpi; + decorationOrigin *= dpi; + float scaledPPEM = this.GetScaledSize(pointSize, dpi); + + Matrix3x2 rotation = GetRotationMatrix(mode); + FontRectangle box = this.GetBoundingBox(mode, glyphOrigin, scaledPPEM); + GlyphRendererParameters parameters = new(this, this.TextRun, pointSize, dpi, mode, graphemeIndex); + + if (renderer.BeginGlyph(in box, in parameters)) + { + if (!UnicodeUtility.ShouldRenderWhiteSpaceOnly(this.CodePoint)) + { + Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor; + + // Apply the CFF FontMatrix to convert charstring coordinates to design units. + // The normalized FontMatrix (fontMatrix * unitsPerEM) is identity for the default + // [0.001, 0, 0, 0.001, 0, 0] with upm=1000. + if (this.glyphData.FontMatrix is double[] fm) + { + float upm = this.UnitsPerEm; + scale *= new Vector2((float)(fm[0] * upm), (float)(fm[3] * upm)); + } + + Vector2 scaledOffset = this.Offset * scale; + this.glyphData.RenderTo(renderer, glyphOrigin, scale, scaledOffset, rotation); + } + + renderer.EndGlyph(); + this.RenderDecorationsTo(renderer, decorationOrigin, mode, rotation, scaledPPEM, options); + } + } + } +} diff --git a/SixLabors.Fonts/Tables/Cff/CffIndexOffset.cs b/SixLabors.Fonts/Tables/Cff/CffIndexOffset.cs new file mode 100644 index 0000000..6a951f3 --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/CffIndexOffset.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Represents the position and length of an element within a CFF INDEX structure. + /// + internal readonly struct CffIndexOffset + { + /// + /// The starting offset of the element within the INDEX data. + /// + public readonly int Start; + + /// + /// The length in bytes of the element. + /// + public readonly int Length; + + /// + /// Initializes a new instance of the struct. + /// + /// The starting offset of the element. + /// The length in bytes of the element. + public CffIndexOffset(int start, int len) + { + this.Start = start; + this.Length = len; + } + +#if DEBUG + /// + public override string ToString() => "Start:" + this.Start + ",Length:" + this.Length; +#endif + } +} diff --git a/SixLabors.Fonts/Tables/Cff/CffOperand.cs b/SixLabors.Fonts/Tables/Cff/CffOperand.cs new file mode 100644 index 0000000..867cfd4 --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/CffOperand.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#if DEBUG +using System.Globalization; +#endif + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Represents a numeric operand value from a CFF DICT entry. + /// Operands can be integers or real numbers as encoded in the DICT data. + /// + internal readonly struct CffOperand + { + /// + /// Initializes a new instance of the struct. + /// + /// The numeric value. + /// The operand kind (integer or real). + public CffOperand(double number, OperandKind kind) + { + this.Kind = kind; + this.RealNumValue = number; + } + + /// + /// Gets the kind of this operand (integer or real number). + /// + public readonly OperandKind Kind { get; } + + /// + /// Gets the numeric value of this operand. + /// + public readonly double RealNumValue { get; } + +#if DEBUG + /// + public override string ToString() + => this.Kind switch + { + OperandKind.IntNumber => ((int)this.RealNumValue).ToString(CultureInfo.InvariantCulture), + _ => this.RealNumValue.ToString(CultureInfo.InvariantCulture), + }; +#endif + + } +} diff --git a/SixLabors.Fonts/Tables/Cff/CffOperator.cs b/SixLabors.Fonts/Tables/Cff/CffOperator.cs new file mode 100644 index 0000000..bfa4aba --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/CffOperator.cs @@ -0,0 +1,155 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Represents a CFF DICT operator with its name and operand kind. + /// Operators are registered in a static dictionary keyed by their byte encoding + /// and looked up during DICT parsing. + /// + /// + internal sealed class CFFOperator + { + private static readonly Lazy> RegisteredOperators = new(CreateDictionary, true); + + /// + /// Initializes a new instance of the class. + /// + /// The operator name. + /// The expected operand format. + private CFFOperator(string name, OperatorOperandKind operandKind) + { + this.Name = name; + this.OperandKind = operandKind; + } + + /// + /// Gets the name of the operator (e.g. "CharStrings", "FontMatrix", "Private"). + /// + public string Name { get; } + + /// + /// Gets the expected operand format for this operator. + /// + public OperatorOperandKind OperandKind { get; } + + /// + /// Looks up a registered CFF operator by its one- or two-byte encoding. + /// + /// The first byte of the operator. + /// The second byte (0 for single-byte operators, or 12 prefix byte value). + /// The matching , or if not found. + public static CFFOperator GetOperatorByKey(byte b0, byte b1) + { + RegisteredOperators.Value.TryGetValue((b1 << 8) | b0, out CFFOperator? found); + return found!; + } + + /// + /// Creates the dictionary of all registered CFF DICT operators, keyed by their byte encoding. + /// + /// The dictionary of registered operators. + private static Dictionary CreateDictionary() + { + Dictionary dictionary = []; + + // Table 9: Top DICT Operator Entries + Register(dictionary, 0, "version", OperatorOperandKind.SID); + Register(dictionary, 1, "Notice", OperatorOperandKind.SID); + Register(dictionary, 12, 0, "Copyright", OperatorOperandKind.SID); + Register(dictionary, 2, "FullName", OperatorOperandKind.SID); + Register(dictionary, 3, "FamilyName", OperatorOperandKind.SID); + Register(dictionary, 4, "Weight", OperatorOperandKind.SID); + Register(dictionary, 12, 1, "isFixedPitch", OperatorOperandKind.Boolean); + Register(dictionary, 12, 2, "ItalicAngle", OperatorOperandKind.Number); + Register(dictionary, 12, 3, "UnderlinePosition", OperatorOperandKind.Number); + Register(dictionary, 12, 4, "UnderlineThickness", OperatorOperandKind.Number); + Register(dictionary, 12, 5, "PaintType", OperatorOperandKind.Number); + Register(dictionary, 12, 6, "CharstringType", OperatorOperandKind.Number); // default value 2 + Register(dictionary, 12, 7, "FontMatrix", OperatorOperandKind.Array); + Register(dictionary, 13, "UniqueID", OperatorOperandKind.Number); + Register(dictionary, 5, "FontBBox", OperatorOperandKind.Array); + Register(dictionary, 12, 8, "StrokeWidth", OperatorOperandKind.Number); + Register(dictionary, 14, "XUID", OperatorOperandKind.Array); + Register(dictionary, 15, "charset", OperatorOperandKind.Number); + Register(dictionary, 16, "Encoding", OperatorOperandKind.Number); + Register(dictionary, 17, "CharStrings", OperatorOperandKind.Number); + Register(dictionary, 18, "Private", OperatorOperandKind.NumberNumber); + Register(dictionary, 12, 20, "SyntheticBase", OperatorOperandKind.Number); + Register(dictionary, 12, 21, "PostScript", OperatorOperandKind.SID); + Register(dictionary, 12, 22, "BaseFontName", OperatorOperandKind.SID); + Register(dictionary, 12, 23, "BaseFontBlend", OperatorOperandKind.SID); + + // Table 10: CIDFont Operator Extensions + Register(dictionary, 12, 30, "ROS", OperatorOperandKind.SID_SID_Number); + Register(dictionary, 12, 31, "CIDFontVersion", OperatorOperandKind.Number); + Register(dictionary, 12, 32, "CIDFontRevision", OperatorOperandKind.Number); + Register(dictionary, 12, 33, "CIDFontType", OperatorOperandKind.Number); + Register(dictionary, 12, 34, "CIDCount", OperatorOperandKind.Number); + Register(dictionary, 12, 35, "UIDBase", OperatorOperandKind.Number); + Register(dictionary, 12, 36, "FDArray", OperatorOperandKind.Number); + Register(dictionary, 12, 37, "FDSelect", OperatorOperandKind.Number); + Register(dictionary, 12, 38, "FontName", OperatorOperandKind.SID); + + // Table 23: Private DICT Operators + Register(dictionary, 6, "BlueValues", OperatorOperandKind.Delta); + Register(dictionary, 7, "OtherBlues", OperatorOperandKind.Delta); + Register(dictionary, 8, "FamilyBlues", OperatorOperandKind.Delta); + Register(dictionary, 9, "FamilyOtherBlues", OperatorOperandKind.Delta); + Register(dictionary, 12, 9, "BlueScale", OperatorOperandKind.Number); + Register(dictionary, 12, 10, "BlueShift", OperatorOperandKind.Number); + Register(dictionary, 12, 11, "BlueFuzz", OperatorOperandKind.Number); + Register(dictionary, 10, "StdHW", OperatorOperandKind.Number); + Register(dictionary, 11, "StdVW", OperatorOperandKind.Number); + Register(dictionary, 12, 12, "StemSnapH", OperatorOperandKind.Delta); + Register(dictionary, 12, 13, "StemSnapV", OperatorOperandKind.Delta); + Register(dictionary, 12, 14, "ForceBold", OperatorOperandKind.Boolean); + + // reserved 12 15 + // reserved 12 16 + Register(dictionary, 12, 17, "LanguageGroup", OperatorOperandKind.Number); + Register(dictionary, 12, 18, "ExpansionFactor", OperatorOperandKind.Number); + Register(dictionary, 12, 19, "initialRandomSeed", OperatorOperandKind.Number); + + Register(dictionary, 19, "Subrs", OperatorOperandKind.Number); + Register(dictionary, 20, "defaultWidthX", OperatorOperandKind.Number); + Register(dictionary, 21, "nominalWidthX", OperatorOperandKind.Number); + + // CFF2 operators + Register(dictionary, 22, "vsindex", OperatorOperandKind.Number); + Register(dictionary, 23, "blend", OperatorOperandKind.Number); + Register(dictionary, 24, "vstore", OperatorOperandKind.Number); + + return dictionary; + } + + /// + /// Registers a two-byte CFF DICT operator. + /// + /// The operator dictionary. + /// The first byte of the operator (always 12 for two-byte operators). + /// The second byte of the operator. + /// The operator name. + /// The expected operand format. + private static void Register(Dictionary dictionary, byte b0, byte b1, string name, OperatorOperandKind operandKind) + => dictionary.Add((b1 << 8) | b0, new CFFOperator(name, operandKind)); + + /// + /// Registers a single-byte CFF DICT operator. + /// + /// The operator dictionary. + /// The operator byte value. + /// The operator name. + /// The expected operand format. + private static void Register(Dictionary dictionary, byte b0, string name, OperatorOperandKind operandKind) + => dictionary.Add(b0, new CFFOperator(name, operandKind)); + +#if DEBUG + /// + public override string ToString() => this.Name; +#endif + } +} diff --git a/SixLabors.Fonts/Tables/Cff/CffParserBase.cs b/SixLabors.Fonts/Tables/Cff/CffParserBase.cs new file mode 100644 index 0000000..ed26478 --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/CffParserBase.cs @@ -0,0 +1,507 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Base class for CFF1 and CFF2 parsers providing shared DICT parsing, + /// INDEX reading, FDSelect, and subroutine loading functionality. + /// + internal abstract class CffParserBase + { + private readonly StringBuilder pooledStringBuilder = new(); + + /// + /// Reads the FDSelect structure that maps glyph indices to Font DICT indices. + /// + /// The binary reader. + /// The absolute offset of the CFF table. + /// The CIDFont information to populate with FDSelect data. + protected static void ReadFdSelect(BigEndianBinaryReader reader, long offset, CidFontInfo cidFontInfo) + { + if (cidFontInfo.FDSelect is 0) + { + return; + } + + reader.BaseStream.Position = offset + cidFontInfo.FDSelect; + switch (reader.ReadByte()) + { + case 0: + { + cidFontInfo.FdSelectFormat = 0; + for (int i = 0; i < cidFontInfo.CIDFountCount; i++) + { + cidFontInfo.FdSelectMap[i] = reader.ReadByte(); + } + + break; + } + + case 3: + { + cidFontInfo.FdSelectFormat = 3; + ushort nRanges = reader.ReadUInt16(); + FDRange[] ranges = new FDRange[nRanges + 1]; + + cidFontInfo.FdSelectFormat = 3; + cidFontInfo.FdRanges = ranges; + for (int i = 0; i < nRanges; ++i) + { + ranges[i] = new FDRange(reader.ReadUInt16(), reader.ReadByte()); + } + + ranges[nRanges] = new FDRange(reader.ReadUInt16(), 0); // sentinel + break; + } + + case 4: + { + cidFontInfo.FdSelectFormat = 4; + uint nRanges = reader.ReadUInt32(); + FDRange[] ranges = new FDRange[nRanges + 1]; + + cidFontInfo.FdSelectFormat = 3; + cidFontInfo.FdRanges = ranges; + for (int i = 0; i < nRanges; ++i) + { + ranges[i] = new FDRange(reader.ReadUInt32(), reader.ReadUInt16()); + } + + ranges[nRanges] = new FDRange(reader.ReadUInt32(), 0); // sentinel + break; + } + + default: + throw new NotSupportedException("Only FD Select format 0, 3 and 4 are supported"); + } + } + + /// + /// Reads the Font DICT Array (FDArray), which contains per-font-dictionary entries for CIDFonts. + /// + /// The binary reader. + /// The absolute offset of the CFF table. + /// The offset to the FDArray INDEX relative to the CFF table. + /// Whether to use CFF2 INDEX format (32-bit count). + /// An array of entries. + protected FontDict[] ReadFdArray(BigEndianBinaryReader reader, long offset, long fdArrayOffset, bool cff2 = false) + { + if (fdArrayOffset is 0) + { + return []; + } + + reader.BaseStream.Position = offset + fdArrayOffset; + + if (!TryReadIndexDataOffsets(reader, out CffIndexOffset[]? offsets, cff2)) + { + return []; + } + + FontDict[] fontDicts = new FontDict[offsets.Length]; + for (int i = 0; i < fontDicts.Length; ++i) + { + // Read DICT data. + List dic = this.ReadDictData(reader, offsets[i].Length); + + // translate + int fontDictsOffset = 0; + int size = 0; + int name = 0; + + foreach (CffDataDicEntry entry in dic) + { + switch (entry.Operator.Name) + { + default: + throw new NotSupportedException(); + case "FontName": + name = (int)entry.Operands[0].RealNumValue; + break; + case "Private": // private dic + size = (int)entry.Operands[0].RealNumValue; + fontDictsOffset = (int)entry.Operands[1].RealNumValue; + break; + } + } + + fontDicts[i] = new FontDict(name, size, fontDictsOffset); + } + + foreach (FontDict fdict in fontDicts) + { + reader.BaseStream.Position = offset + fdict.PrivateDicOffset; + + List dicData = this.ReadDictData(reader, fdict.PrivateDicSize); + + if (dicData.Count > 0) + { + // Interpret the values of private dict. + foreach (CffDataDicEntry dicEntry in dicData) + { + switch (dicEntry.Operator.Name) + { + case "Subrs": + int localSubrsOffset = (int)dicEntry.Operands[0].RealNumValue; + reader.BaseStream.Position = offset + fdict.PrivateDicOffset + localSubrsOffset; + fdict.LocalSubr = ReadSubrBuffer(reader, cff2); + break; + + case "vsindex": + fdict.VsIndex = (int)dicEntry.Operands[0].RealNumValue; + break; + + case "defaultWidthX": + case "nominalWidthX": + break; + } + } + } + } + + return fontDicts; + } + + /// + /// Reads a single DICT entry consisting of operands followed by an operator. + /// + /// The binary reader. + /// The parsed . + protected CffDataDicEntry ReadEntry(BigEndianBinaryReader reader) + { + List operands = new(); + + //----------------------------- + // An operator is preceded by the operand(s) that + // specify its value. + //-------------------------------- + + //----------------------------- + // Operators and operands may be distinguished by inspection of + // their first byte: + // 0–21 specify operators and + // 28, 29, 30, and 32–254 specify operands(numbers). + // Byte values 22–27, 31, and 255 are reserved. + + // An operator may be preceded by up to a maximum of 48 operands + CFFOperator? @operator; + while (true) + { + byte b0 = reader.ReadUInt8(); + + if (b0 is >= 0 and <= 24) + { + // operators + @operator = ReadOperator(reader, b0); + break; // **break after found operator + } + else if (b0 is 28 or 29) + { + int num = ReadIntegerNumber(reader, b0); + operands.Add(new CffOperand(num, OperandKind.IntNumber)); + } + else if (b0 == 30) + { + double num = this.ReadRealNumber(reader); + operands.Add(new CffOperand(num, OperandKind.RealNumber)); + } + else if (b0 is >= 32 and <= 254) + { + int num = ReadIntegerNumber(reader, b0); + operands.Add(new CffOperand(num, OperandKind.IntNumber)); + } + else + { + throw new NotSupportedException("invalid DICT data b0 byte: " + b0); + } + } + + // I'm fairly confident that the operator can never be null. + return new CffDataDicEntry(@operator!, operands.ToArray()); + } + + /// + /// Attempts to read the offset array from a CFF INDEX structure. + /// + /// The binary reader. + /// When this method returns, contains the parsed index offsets, or if the INDEX is empty. + /// Whether to use CFF2 INDEX format (32-bit count). + /// if the INDEX contained at least one element; otherwise, . + protected static bool TryReadIndexDataOffsets(BigEndianBinaryReader reader, [NotNullWhen(true)] out CffIndexOffset[]? value, bool cff2 = false) + { + // INDEX Data + // An INDEX is an array of variable-sized objects.It comprises a + // header, an offset array, and object data. + // The offset array specifies offsets within the object data. + // An object is retrieved by + // indexing the offset array and fetching the object at the + // specified offset. + // The object’s length can be determined by subtracting its offset + // from the next offset in the offset array. + // An additional offset is added at the end of the offset array so the + // length of the last object may be determined. + // The INDEX format is shown in Table 7 + + // Table 7 INDEX Format + // Type Name Description + // Card16 count Number of objects stored in INDEX + // OffSize offSize Offset array element size + // Offset offset[count + 1] Offset array(from byte preceding object data) + // Card8 data[] Object data + + // Offsets in the offset array are relative to the byte that precedes + // the object data. Therefore the first element of the offset array + // is always 1. (This ensures that every object has a corresponding + // offset which is always nonzero and permits the efficient + // implementation of dynamic object loading.) + + // An empty INDEX is represented by a count field with a 0 value + // and no additional fields.Thus, the total size of an empty INDEX + // is 2 bytes. + + // Note 2 + // An INDEX may be skipped by jumping to the offset specified by the last + // element of the offset array + // CFF2 uses a 32-bit count; CFF1 uses 16-bit. + uint count = cff2 ? reader.ReadUInt32() : reader.ReadUInt16(); + if (count == 0) + { + value = null; + return false; + } + + int offSize = reader.ReadByte(); + int[] offsets = new int[count + 1]; + CffIndexOffset[] indexElems = new CffIndexOffset[count]; + for (int i = 0; i <= count; ++i) + { + offsets[i] = reader.ReadOffset(offSize); + } + + for (int i = 0; i < count; ++i) + { + indexElems[i] = new CffIndexOffset(offsets[i], offsets[i + 1] - offsets[i]); + } + + value = indexElems; + return true; + } + + /// + /// Reads a subroutine INDEX and returns the raw byte buffers for each subroutine. + /// + /// The binary reader. + /// Whether to use CFF2 INDEX format (32-bit count). + /// An array of byte arrays, each containing a subroutine charstring. + protected static byte[][] ReadSubrBuffer(BigEndianBinaryReader reader, bool cff2 = false) + { + if (!TryReadIndexDataOffsets(reader, out CffIndexOffset[]? offsets, cff2)) + { + return []; + } + + byte[][] rawBufferList = new byte[offsets.Length][]; + + for (int i = 0; i < rawBufferList.Length; ++i) + { + CffIndexOffset offset = offsets[i]; + rawBufferList[i] = reader.ReadBytes(offset.Length); + } + + return rawBufferList; + } + + /// + /// Reads DICT data of the specified length, parsing all operator-operand entries. + /// + /// The binary reader. + /// The length in bytes of the DICT data to read. + /// A list of parsed entries. + protected List ReadDictData(BigEndianBinaryReader reader, int length) + { + // 4. DICT Data + + // Font dictionary data comprising key-value pairs is represented + // in a compact tokenized format that is similar to that used to + // represent Type 1 charstrings. + + // Dictionary keys are encoded as 1- or 2-byte operators and dictionary values are encoded as + // variable-size numeric operands that represent either integer or + // real values. + + //----------------------------- + // A DICT is simply a sequence of + // operand(s)/operator bytes concatenated together. + int maxIndex = (int)(reader.BaseStream.Position + length); + List dicData = new(); + while (reader.BaseStream.Position < maxIndex) + { + CffDataDicEntry dicEntry = this.ReadEntry(reader); + dicData.Add(dicEntry); + } + + return dicData; + } + + /// + /// Reads a DICT operator (one or two bytes) from the reader. + /// + /// The binary reader. + /// The first byte of the operator. + /// The resolved . + private static CFFOperator ReadOperator(BigEndianBinaryReader reader, byte b0) + { + // Read operator key. + byte b1 = 0; + if (b0 == 12) + { + // 2 bytes + b1 = reader.ReadUInt8(); + } + + // Get registered operator by its key. + return CFFOperator.GetOperatorByKey(b0, b1); + } + + /// + /// Reads a real number operand encoded as a nibble-based BCD sequence. + /// + /// The binary reader. + /// The decoded real number value. + private double ReadRealNumber(BigEndianBinaryReader reader) + { + // from https://typekit.files.wordpress.com/2013/05/5176.cff.pdf + // A real number operand is provided in addition to integer + // operands.This operand begins with a byte value of 30 followed + // by a variable-length sequence of bytes.Each byte is composed + // of two 4 - bit nibbles asdefined in Table 5. + + // The first nibble of a + // pair is stored in the most significant 4 bits of a byte and the + // second nibble of a pair is stored in the least significant 4 bits of a byte + StringBuilder sb = this.pooledStringBuilder; + sb.Clear(); // reset + + bool done = false; + bool exponentMissing = false; + while (!done) + { + int b = reader.ReadByte(); + + int nb_0 = (b >> 4) & 0xf; + int nb_1 = b & 0xf; + + for (int i = 0; !done && i < 2; ++i) + { + int nibble = (i == 0) ? nb_0 : nb_1; + + switch (nibble) + { + case 0x0: + case 0x1: + case 0x2: + case 0x3: + case 0x4: + case 0x5: + case 0x6: + case 0x7: + case 0x8: + case 0x9: + sb.Append(nibble); + exponentMissing = false; + break; + case 0xa: + sb.Append('.'); + break; + case 0xb: + sb.Append('E'); + exponentMissing = true; + break; + case 0xc: + sb.Append("E-"); + exponentMissing = true; + break; + case 0xd: + break; + case 0xe: + sb.Append('-'); + break; + case 0xf: + done = true; + break; + default: + throw new FontException("Unable to read real number."); + } + } + } + + if (exponentMissing) + { + // the exponent is missing, just append "0" to avoid an exception + // not sure if 0 is the correct value, but it seems to fit + // see PDFBOX-1522 + sb.Append('0'); + } + + if (sb.Length == 0) + { + return 0d; + } + + if (!double.TryParse( + sb.ToString(), + NumberStyles.Number | NumberStyles.AllowExponent, + CultureInfo.InvariantCulture, + out double value)) + { + throw new NotSupportedException(); + } + + return value; + } + + /// + /// Reads an integer number operand from the DICT data based on the initial byte. + /// + /// The binary reader. + /// The initial byte that determines the encoding format. + /// The decoded integer value. + private static int ReadIntegerNumber(BigEndianBinaryReader reader, byte b0) + { + if (b0 == 28) + { + return reader.ReadInt16(); + } + + if (b0 == 29) + { + return reader.ReadInt32(); + } + + if (b0 is >= 32 and <= 246) + { + return b0 - 139; + } + + if (b0 is >= 247 and <= 250) + { + int b1 = reader.ReadByte(); + return ((b0 - 247) * 256) + b1 + 108; + } + + if (b0 is >= 251 and <= 254) + { + int b1 = reader.ReadByte(); + return (-(b0 - 251) * 256) - b1 - 108; + } + + throw new InvalidFontFileException("Invalid DICT data b0 byte: " + b0); + } + } +} diff --git a/SixLabors.Fonts/Tables/Cff/CffPrivateDictionary.cs b/SixLabors.Fonts/Tables/Cff/CffPrivateDictionary.cs new file mode 100644 index 0000000..85e308f --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/CffPrivateDictionary.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Represents data from a CFF Private DICT, which contains font-level hinting + /// values and local subroutine references. + /// + internal class CffPrivateDictionary + { + /// + /// Initializes a new instance of the class. + /// + /// The local subroutine byte buffers. + /// The default glyph width. + /// The nominal width bias. + public CffPrivateDictionary(byte[][]? localSubrRawBuffers, int defaultWidthX, int nominalWidthX) + { + this.LocalSubrRawBuffers = localSubrRawBuffers; + this.DefaultWidthX = defaultWidthX; + this.NominalWidthX = nominalWidthX; + } + + /// + /// Gets or sets the local subroutine raw byte buffers referenced by the Private DICT. + /// + public byte[][]? LocalSubrRawBuffers { get; set; } + + /// + /// Gets or sets the default width for glyphs that do not specify a width in the charstring. + /// + public int DefaultWidthX { get; set; } + + /// + /// Gets or sets the nominal width used as a bias for charstring width values. + /// + public int NominalWidthX { get; set; } + } +} diff --git a/SixLabors.Fonts/Tables/Cff/CffStandardStrings.cs b/SixLabors.Fonts/Tables/Cff/CffStandardStrings.cs new file mode 100644 index 0000000..3d704f8 --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/CffStandardStrings.cs @@ -0,0 +1,423 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Appendix A: Standard Strings + /// + internal static class CffStandardStrings + { + /// + /// The lookup table mapping standard SID values to their string names. + /// + private static readonly string[] StringIdentifierToString = + { + ".notdef", + "space", + "exclam", + "quotedbl", + "numbersign", + "dollar", + "percent", + "ampersand", + "quoteright", + "parenleft", + "parenright", + "asterisk", + "plus", + "comma", + "hyphen", + "period", + "slash", + "zero", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "colon", + "semicolon", + "less", + "equal", + "greater", + "question", + "at", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "bracketleft", + "backslash", + "bracketright", + "asciicircum", + "underscore", + "quoteleft", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "braceleft", + "bar", + "braceright", + "asciitilde", + "exclamdown", + "cent", + "sterling", + "fraction", + "yen", + "florin", + "section", + "currency", + "quotesingle", + "quotedblleft", + "guillemotleft", + "guilsinglleft", + "guilsinglright", + "fi", + "fl", + "endash", + "dagger", + "daggerdbl", + "periodcentered", + "paragraph", + "bullet", + "quotesinglbase", + "quotedblbase", + "quotedblright", + "guillemotright", + "ellipsis", + "perthousand", + "questiondown", + "grave", + "acute", + "circumflex", + "tilde", + "macron", + "breve", + "dotaccent", + "dieresis", + "ring", + "cedilla", + "hungarumlaut", + "ogonek", + "caron", + "emdash", + "AE", + "ordfeminine", + "Lslash", + "Oslash", + "OE", + "ordmasculine", + "ae", + "dotlessi", + "lslash", + "oslash", + "oe", + "germandbls", + "onesuperior", + "logicalnot", + "mu", + "trademark", + "Eth", + "onehalf", + "plusminus", + "Thorn", + "onequarter", + "divide", + "brokenbar", + "degree", + "thorn", + "threequarters", + "twosuperior", + "registered", + "minus", + "eth", + "multiply", + "threesuperior", + "copyright", + "Aacute", + "Acircumflex", + "Adieresis", + "Agrave", + "Aring", + "Atilde", + "Ccedilla", + "Eacute", + "Ecircumflex", + "Edieresis", + "Egrave", + "Iacute", + "Icircumflex", + "Idieresis", + "Igrave", + "Ntilde", + "Oacute", + "Ocircumflex", + "Odieresis", + "Ograve", + "Otilde", + "Scaron", + "Uacute", + "Ucircumflex", + "Udieresis", + "Ugrave", + "Yacute", + "Ydieresis", + "Zcaron", + "aacute", + "acircumflex", + "adieresis", + "agrave", + "aring", + "atilde", + "ccedilla", + "eacute", + "ecircumflex", + "edieresis", + "egrave", + "iacute", + "icircumflex", + "idieresis", + "igrave", + "ntilde", + "oacute", + "ocircumflex", + "odieresis", + "ograve", + "otilde", + "scaron", + "uacute", + "ucircumflex", + "udieresis", + "ugrave", + "yacute", + "ydieresis", + "zcaron", + "exclamsmall", + "Hungarumlautsmall", + "dollaroldstyle", + "dollarsuperior", + "ampersandsmall", + "Acutesmall", + "parenleftsuperior", + "parenrightsuperior", + "twodotenleader", + "onedotenleader", + "zerooldstyle", + "oneoldstyle", + "twooldstyle", + "threeoldstyle", + "fouroldstyle", + "fiveoldstyle", + "sixoldstyle", + "sevenoldstyle", + "eightoldstyle", + "nineoldstyle", + "commasuperior", + "threequartersemdash", + "periodsuperior", + "questionsmall", + "asuperior", + "bsuperior", + "centsuperior", + "dsuperior", + "esuperior", + "isuperior", + "lsuperior", + "msuperior", + "nsuperior", + "osuperior", + "rsuperior", + "ssuperior", + "tsuperior", + "ff", + "ffi", + "ffl", + "parenleftinferior", + "parenrightinferior", + "Circumflexsmall", + "hyphensuperior", + "Gravesmall", + "Asmall", + "Bsmall", + "Csmall", + "Dsmall", + "Esmall", + "Fsmall", + "Gsmall", + "Hsmall", + "Ismall", + "Jsmall", + "Ksmall", + "Lsmall", + "Msmall", + "Nsmall", + "Osmall", + "Psmall", + "Qsmall", + "Rsmall", + "Ssmall", + "Tsmall", + "Usmall", + "Vsmall", + "Wsmall", + "Xsmall", + "Ysmall", + "Zsmall", + "colonmonetary", + "onefitted", + "rupiah", + "Tildesmall", + "exclamdownsmall", + "centoldstyle", + "Lslashsmall", + "Scaronsmall", + "Zcaronsmall", + "Dieresissmall", + "Brevesmall", + "Caronsmall", + "Dotaccentsmall", + "Macronsmall", + "figuredash", + "hypheninferior", + "Ogoneksmall", + "Ringsmall", + "Cedillasmall", + "questiondownsmall", + "oneeighth", + "threeeighths", + "fiveeighths", + "seveneighths", + "onethird", + "twothirds", + "zerosuperior", + "foursuperior", + "fivesuperior", + "sixsuperior", + "sevensuperior", + "eightsuperior", + "ninesuperior", + "zeroinferior", + "oneinferior", + "twoinferior", + "threeinferior", + "fourinferior", + "fiveinferior", + "sixinferior", + "seveninferior", + "eightinferior", + "nineinferior", + "centinferior", + "dollarinferior", + "periodinferior", + "commainferior", + "Agravesmall", + "Aacutesmall", + "Acircumflexsmall", + "Atildesmall", + "Adieresissmall", + "Aringsmall", + "AEsmall", + "Ccedillasmall", + "Egravesmall", + "Eacutesmall", + "Ecircumflexsmall", + "Edieresissmall", + "Igravesmall", + "Iacutesmall", + "Icircumflexsmall", + "Idieresissmall", + "Ethsmall", + "Ntildesmall", + "Ogravesmall", + "Oacutesmall", + "Ocircumflexsmall", + "Otildesmall", + "Odieresissmall", + "OEsmall", + "Oslashsmall", + "Ugravesmall", + "Uacutesmall", + "Ucircumflexsmall", + "Udieresissmall", + "Yacutesmall", + "Thornsmall", + "Ydieresissmall", + "001.000", + "001.001", + "001.002", + "001.003", + "Black", + "Bold", + "Book", + "Light", + "Medium", + "Regular", + "Roman", + "Semibold" + }; + + /// + /// Gets the number of standard strings defined in the CFF specification. + /// + public static int Count { get; } = StringIdentifierToString.Length; + + /// + /// Gets the standard string name for the given SID (String Identifier). + /// + /// The standard string identifier index. + /// The standard string name. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetName(int sid) => StringIdentifierToString[sid]; + } +} diff --git a/SixLabors.Fonts/Tables/Cff/CffTopDictionary.cs b/SixLabors.Fonts/Tables/Cff/CffTopDictionary.cs new file mode 100644 index 0000000..bb64086 --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/CffTopDictionary.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Represents the Top DICT data from a CFF or CFF2 font, containing font-wide + /// metadata such as name strings, bounding box, underline metrics, and the FontMatrix. + /// + internal class CffTopDictionary + { + /// + /// Initializes a new instance of the class. + /// + public CffTopDictionary() => this.CidFontInfo = new(); + + /// + /// Gets or sets the font version string (SID). + /// + public string? Version { get; set; } + + /// + /// Gets or sets the font notice/trademark string (SID). + /// + public string? Notice { get; set; } + + /// + /// Gets or sets the font copyright string (SID). + /// + public string? CopyRight { get; set; } + + /// + /// Gets or sets the font full name string (SID). + /// + public string? FullName { get; set; } + + /// + /// Gets or sets the font family name string (SID). + /// + public string? FamilyName { get; set; } + + /// + /// Gets or sets the font weight string (SID), e.g. "Bold". + /// + public string? Weight { get; set; } + + /// + /// Gets or sets the underline position in design units. + /// + public double UnderlinePosition { get; set; } + + /// + /// Gets or sets the underline thickness in design units. + /// + public double UnderlineThickness { get; set; } + + /// + /// Gets or sets the font bounding box [xMin, yMin, xMax, yMax] in design units. + /// + public double[] FontBBox { get; set; } = []; + + /// + /// Gets or sets the font matrix that transforms charstring coordinates to user space. + /// Default is [0.001, 0, 0, 0.001, 0, 0] which maps 1000 charstring units to 1 user-space unit. + /// + public double[] FontMatrix { get; set; } = [0.001, 0, 0, 0.001, 0, 0]; + + /// + /// Gets or sets the CIDFont-specific information (ROS, FDSelect, FDArray). + /// + public CidFontInfo CidFontInfo { get; set; } + } +} diff --git a/SixLabors.Fonts/Tables/Cff/CidFontInfo.cs b/SixLabors.Fonts/Tables/Cff/CidFontInfo.cs new file mode 100644 index 0000000..2abae10 --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/CidFontInfo.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Contains CIDFont-specific information from the Top DICT of a CFF CIDFont. + /// + /// + internal class CidFontInfo + { + /// + /// Gets or sets the CIDFont Registry string from the ROS operator. + /// + public string? ROS_Register { get; set; } + + /// + /// Gets or sets the CIDFont Ordering string from the ROS operator. + /// + public string? ROS_Ordering { get; set; } + + /// + /// Gets or sets the CIDFont Supplement value from the ROS operator. + /// + public string? ROS_Supplement { get; set; } + + /// + /// Gets or sets the CIDFont version number. + /// + public double CIDFontVersion { get; set; } + + /// + /// Gets or sets the number of CIDs in the font (CIDCount operator). + /// + public int CIDFountCount { get; set; } + + /// + /// Gets or sets the offset to the FDSelect structure that maps glyphs to Font DICTs. + /// + public int FDSelect { get; set; } + + /// + /// Gets or sets the offset to the Font DICT (FDArray) INDEX. + /// + public int FDArray { get; set; } + + /// + /// Gets or sets the FDSelect format (0, 3, or 4). + /// + public int FdSelectFormat { get; set; } + + /// + /// Gets or sets the parsed FDSelect ranges for format 3/4. + /// + public FDRange[] FdRanges { get; set; } = []; + + /// + /// Gets or sets the FDSelect map for format 0, mapping glyph index to Font DICT index. + /// + public Dictionary FdSelectMap { get; set; } = []; + } +} diff --git a/SixLabors.Fonts/Tables/Cff/CompactFontTables.cs b/SixLabors.Fonts/Tables/Cff/CompactFontTables.cs new file mode 100644 index 0000000..c5e374e --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/CompactFontTables.cs @@ -0,0 +1,152 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; +using SixLabors.Fonts.Tables.General; +using SixLabors.Fonts.Tables.General.Colr; +using SixLabors.Fonts.Tables.General.Kern; +using SixLabors.Fonts.Tables.General.Name; +using SixLabors.Fonts.Tables.General.Post; +using SixLabors.Fonts.Tables.General.Svg; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Contains the collection of OpenType tables required for fonts with CFF or CFF2 outlines. + /// + internal sealed class CompactFontTables : IFontTables + { + /// + /// Initializes a new instance of the class with the required OpenType tables. + /// + /// The character-to-glyph mapping table. + /// The font header table. + /// The horizontal header table. + /// The horizontal metrics table. + /// The maximum profile table. + /// The naming table. + /// The OS/2 and Windows metrics table. + /// The PostScript name mapping table. + /// The CFF or CFF2 outline table. + public CompactFontTables( + CMapTable cmap, + HeadTable head, + HorizontalHeadTable hhea, + HorizontalMetricsTable htmx, + MaximumProfileTable maxp, + NameTable name, + OS2Table os2, + PostTable post, + ICffTable cff) + { + this.Cmap = cmap; + this.Head = head; + this.Hhea = hhea; + this.Htmx = htmx; + this.Maxp = maxp; + this.Name = name; + this.Os2 = os2; + this.Post = post; + this.Cff = cff; + } + + /// + public CMapTable Cmap { get; set; } + + /// + public HeadTable Head { get; set; } + + /// + public HorizontalHeadTable Hhea { get; set; } + + /// + public HorizontalMetricsTable Htmx { get; set; } + + /// + public MaximumProfileTable Maxp { get; set; } + + /// + public NameTable Name { get; set; } + + /// + public OS2Table Os2 { get; set; } + + /// + public PostTable Post { get; set; } + + /// + public GlyphDefinitionTable? Gdef { get; set; } + + /// + public GSubTable? GSub { get; set; } + + /// + public GPosTable? GPos { get; set; } + + /// + public ColrTable? Colr { get; set; } + + /// + public CpalTable? Cpal { get; set; } + + /// + public KerningTable? Kern { get; set; } + + /// + public VerticalHeadTable? Vhea { get; set; } + + /// + public VerticalMetricsTable? Vmtx { get; set; } + + /// + /// Gets or sets the optional 'fvar' (Font Variations) table defining variation axes. + /// + public FVarTable? FVar { get; set; } + + /// + /// Gets or sets the optional 'avar' (Axis Variations) table for non-linear axis mapping. + /// + public AVarTable? AVar { get; set; } + + /// + /// Gets or sets the optional 'gvar' (Glyph Variations) table. Typically unused for CFF fonts. + /// + public GVarTable? GVar { get; set; } + + /// + /// Gets or sets the optional 'HVAR' (Horizontal Metrics Variations) table. + /// + public HVarTable? HVar { get; set; } + + /// + /// Gets or sets the optional 'VVAR' (Vertical Metrics Variations) table. + /// + public VVarTable? VVar { get; set; } + + /// + /// Gets or sets the optional 'MVAR' (Metrics Variations) table for global metric deltas. + /// + public MVarTable? MVar { get; set; } + + /// + /// Gets or sets the optional SVG table containing scalable vector glyph data. + /// + public SvgTable? Svg { get; set; } + + // Tables Related to CFF Outlines + // +------+----------------------------------+ + // | Tag | Name | + // +======+==================================+ + // | CFF | Compact Font Format 1.0 | + // +------+----------------------------------+ + // | CFF2 | Compact Font Format 2.0 | + // +------+----------------------------------+ + // | VORG | Vertical Origin (optional table) | + // +------+----------------------------------+ + + /// + /// Gets or sets the CFF or CFF2 outline table. + /// + public ICffTable Cff { get; set; } + } +} diff --git a/SixLabors.Fonts/Tables/Cff/FDRange.cs b/SixLabors.Fonts/Tables/Cff/FDRange.cs new file mode 100644 index 0000000..b6a23b4 --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/FDRange.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Represents an element in an font dictionary array. + /// + internal readonly struct FDRange + { + /// + /// Initializes a new instance of the struct for FDSelect format 3. + /// + /// The first glyph index in the range. + /// The font dictionary index for glyphs in this range. + public FDRange(ushort first, byte fontDictionary) + { + this.First = first; + this.FontDictionary = fontDictionary; + } + + /// + /// Initializes a new instance of the struct for FDSelect format 4. + /// + /// The first glyph index in the range. + /// The font dictionary index for glyphs in this range. + public FDRange(uint first, ushort fontDictionary) + { + this.First = first; + this.FontDictionary = fontDictionary; + } + + /// + /// Gets the first glyph index in range. + /// + public uint First { get; } + + /// + /// Gets the font dictionary index for all glyphs in range. + /// + public ushort FontDictionary { get; } + + /// + public override string ToString() => $"First {this.First}, Dictionary {this.FontDictionary}."; + } +} diff --git a/SixLabors.Fonts/Tables/Cff/FDRangeProvider.cs b/SixLabors.Fonts/Tables/Cff/FDRangeProvider.cs new file mode 100644 index 0000000..0ac2566 --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/FDRangeProvider.cs @@ -0,0 +1,100 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Resolves the Font DICT index for a given glyph using the FDSelect data from a CIDFont. + /// Supports FDSelect format 0 (per-glyph map) and formats 3/4 (range-based). + /// + internal struct FDRangeProvider + { + private readonly int format; + private readonly FDRange[] ranges; + private readonly Dictionary fdSelectMap; + private uint currentGlyphIndex; + private uint endGlyphIndexMax; + private FDRange currentRange; + private int currentSelectedRangeIndex; + + /// + /// Initializes a new instance of the struct. + /// + /// The CIDFont information containing FDSelect data. + public FDRangeProvider(CidFontInfo cidFontInfo) + { + this.format = cidFontInfo.FdSelectFormat; + this.ranges = cidFontInfo.FdRanges; + this.fdSelectMap = cidFontInfo.FdSelectMap; + this.currentGlyphIndex = 0; + this.currentSelectedRangeIndex = 0; + + if (this.ranges.Length is not 0) + { + this.currentRange = this.ranges[0]; + this.endGlyphIndexMax = this.ranges[1].First; + } + else + { + // empty + this.currentRange = default; + this.endGlyphIndexMax = 0; + } + + this.SelectedFDArray = 0; + } + + /// + /// Gets the currently selected Font DICT array index. + /// + public ushort SelectedFDArray { get; private set; } + + /// + /// Sets the current glyph index and resolves the corresponding Font DICT index. + /// + /// The glyph index to look up. + public void SetCurrentGlyphIndex(ushort index) + { + switch (this.format) + { + case 0: + this.currentGlyphIndex = this.fdSelectMap[index]; + break; + + case 3: + case 4: + // Find proper range for selected index. + if (index >= this.currentRange.First && index < this.endGlyphIndexMax) + { + // Ok, in current range. + this.SelectedFDArray = this.currentRange.FontDictionary; + } + else + { + // Move to next range. + this.currentSelectedRangeIndex++; + this.currentRange = this.ranges[this.currentSelectedRangeIndex]; + + this.endGlyphIndexMax = this.ranges[this.currentSelectedRangeIndex + 1].First; + if (index >= this.currentRange.First && index < this.endGlyphIndexMax) + { + this.SelectedFDArray = this.currentRange.FontDictionary; + } + else + { + throw new NotSupportedException(); + } + } + + this.currentGlyphIndex = index; + + break; + + default: + throw new NotSupportedException(); + } + } + } +} diff --git a/SixLabors.Fonts/Tables/Cff/FontDict.cs b/SixLabors.Fonts/Tables/Cff/FontDict.cs new file mode 100644 index 0000000..e3e79ac --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/FontDict.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Represents a Font DICT entry from the FDArray in a CIDFont. + /// Each Font DICT contains a reference to its own Private DICT and local subroutines. + /// + internal class FontDict + { + /// + /// Initializes a new instance of the class. + /// + /// The Font DICT name SID. + /// The size in bytes of the associated Private DICT. + /// The offset to the associated Private DICT. + public FontDict(int name, int dictSize, int dictOffset) + { + this.FontName = name; + this.PrivateDicSize = dictSize; + this.PrivateDicOffset = dictOffset; + } + + /// + /// Gets or sets the Font DICT name SID. + /// + public int FontName { get; set; } + + /// + /// Gets the size in bytes of the associated Private DICT. + /// + public int PrivateDicSize { get; } + + /// + /// Gets the offset to the associated Private DICT. + /// + public int PrivateDicOffset { get; } + + /// + /// Gets or sets the local subroutine buffers from this Font DICT's Private DICT. + /// + public byte[][]? LocalSubr { get; set; } + + /// + /// Gets or sets the variation store index (CFF2 vsindex operator) for this Font DICT. + /// + public int VsIndex { get; set; } + } +} diff --git a/SixLabors.Fonts/Tables/Cff/ICffTable.cs b/SixLabors.Fonts/Tables/Cff/ICffTable.cs new file mode 100644 index 0000000..436bce9 --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/ICffTable.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Defines a common interface for CFF1 and CFF2 tables. + /// + internal interface ICffTable + { + /// + /// Gets the number of glyphs in the table. + /// + public int GlyphCount + { + get; + } + + /// + /// Gets the item variation store. + /// + /// The item variation store. If CFF1, there is no variations and null will be returned instead. + public ItemVariationStore? ItemVariationStore + { + get; + } + + /// + /// Gets the glyph data at the given index. + /// + /// The glyph index. + /// The . + public CffGlyphData GetGlyph(int index); + } +} diff --git a/SixLabors.Fonts/Tables/Cff/OperandKind.cs b/SixLabors.Fonts/Tables/Cff/OperandKind.cs new file mode 100644 index 0000000..9a21b79 --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/OperandKind.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Identifies whether a CFF DICT operand was encoded as an integer or a real number. + /// + internal enum OperandKind + { + /// + /// An integer operand (encoded as 1-5 bytes in the DICT data). + /// + IntNumber, + + /// + /// A real number operand (encoded as a nibble-based BCD sequence). + /// + RealNumber + } +} diff --git a/SixLabors.Fonts/Tables/Cff/OperatorOperandKind.cs b/SixLabors.Fonts/Tables/Cff/OperatorOperandKind.cs new file mode 100644 index 0000000..ab65455 --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/OperatorOperandKind.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Defines the operand interpretation for a CFF DICT operator. + /// Used to describe how operands on the DICT stack should be decoded. + /// + /// + internal enum OperatorOperandKind + { + /// + /// A string identifier referencing the String INDEX. + /// + SID, + + /// + /// A boolean value (0 or 1). + /// + Boolean, + + /// + /// A single numeric value (integer or real). + /// + Number, + + /// + /// An array of numeric values. + /// + Array, + + /// + /// A delta-encoded array of numeric values. + /// + Delta, + + /// + /// Two numeric values (e.g. Private DICT size and offset). + /// + NumberNumber, + + /// + /// Two SIDs followed by a number (e.g. ROS: Registry, Ordering, Supplement). + /// + SID_SID_Number, + } +} diff --git a/SixLabors.Fonts/Tables/Cff/RefStack{T}.cs b/SixLabors.Fonts/Tables/Cff/RefStack{T}.cs new file mode 100644 index 0000000..a8c3bbb --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/RefStack{T}.cs @@ -0,0 +1,172 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// A ref struct stack implementation that uses a pooled span to store the data. + /// + /// The type of elements in the stack. + internal ref struct RefStack + where T : unmanaged + { + private const int MaxLength = 0X7FFFFFC7; + private Buffer buffer; + private Span stack; + private bool isDisposed; + + /// + /// Initializes a new instance of the struct with the specified initial capacity. + /// + /// The initial capacity of the stack. Values less than 1 default to 4. + public RefStack(int capacity) + { + if (capacity < 1) + { + capacity = 4; + } + + this.buffer = new Buffer(capacity); + this.stack = this.buffer.GetSpan(); + this.isDisposed = false; + this.Length = 0; + } + + /// + /// Gets the number of elements currently in the stack. + /// + public int Length { get; private set; } + + /// + /// Gets or sets the element at the specified index in the stack. + /// + /// The zero-based index of the element. + /// The element at the specified index. + public T this[int index] + { + readonly get + { + if ((uint)index >= (uint)this.Length) + { + ThrowForOutOfRange(); + } + + return this.stack[index]; + } + + set + { + if ((uint)index >= (uint)this.Length) + { + this.Push(value); + return; + } + + this.stack[index] = value; + } + } + + /// + /// Adds an item to the stack. + /// + /// The item to add. + public void Push(T value) + { + if ((uint)this.Length < (uint)this.stack.Length) + { + this.stack[this.Length++] = value; + } + else + { + int capacity = this.stack.Length * 2; + if ((uint)capacity > MaxLength) + { + capacity = MaxLength; + } + + var newBuffer = new Buffer(capacity); + Span newStack = newBuffer.GetSpan(); + + this.stack.CopyTo(newStack); + this.buffer.Dispose(); + + this.buffer = newBuffer; + this.stack = newStack; + + this.stack[this.Length++] = value; + } + } + + /// + /// Removes the first element of the stack. + /// + /// The element. + public T Shift() + { + int newSize = this.Length - 1; + if (newSize < 0) + { + ThrowForEmptyStack(); + } + + T item = this.stack[0]; + this.stack = this.stack.Slice(1); + this.Length = newSize; + return item; + } + + /// + /// Removes the last element of the stack. + /// + /// The element. + public T Pop() + { + int newSize = this.Length - 1; + if (newSize < 0) + { + ThrowForEmptyStack(); + } + + this.Length = newSize; + return this.stack[newSize]; + } + + /// + /// Clears the current stack. + /// + public void Clear() + { + this.Length = 0; + this.stack = this.buffer.GetSpan(); + } + + /// + /// Releases the pooled buffer used by this stack. + /// + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.buffer.Dispose(); + this.isDisposed = true; + } + + /// + /// Throws an for an out-of-range index access. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowForOutOfRange() + => throw new InvalidOperationException("Index must be greater or equal to zero or less than the stack length."); + + /// + /// Throws an when attempting to pop or shift from an empty stack. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowForEmptyStack() => throw new InvalidOperationException("Empty stack!"); + } +} diff --git a/SixLabors.Fonts/Tables/Cff/SimpleBinaryReader.cs b/SixLabors.Fonts/Tables/Cff/SimpleBinaryReader.cs new file mode 100644 index 0000000..903d4e0 --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/SimpleBinaryReader.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// A lightweight big-endian binary reader over a buffer, + /// used for reading Type 2 charstring data without allocations. + /// + internal ref struct SimpleBinaryReader + { + private readonly ReadOnlySpan buffer; + + /// + /// Initializes a new instance of the struct. + /// + /// The byte buffer to read from. + public SimpleBinaryReader(ReadOnlySpan buffer) + { + this.buffer = buffer; + this.Position = 0; + } + + /// + /// Gets the total length of the underlying buffer. + /// + public readonly int Length => this.buffer.Length; + + /// + /// Gets or sets the current read position within the buffer. + /// + // TODO: Bounds checks. + public int Position { get; set; } + + /// + /// Gets a value indicating whether there are remaining bytes to read. + /// + /// if the position is within the buffer; otherwise, . + public readonly bool CanRead() => (uint)this.Position < this.buffer.Length; + + /// + /// Reads a single byte and advances the position. + /// + /// The byte value. + public byte ReadByte() => this.buffer[this.Position++]; + + /// + /// Reads a big-endian 16-bit signed integer and advances the position by 2 bytes. + /// + /// The 16-bit signed integer value. + public int ReadInt16BE() + { + byte b1 = this.buffer[this.Position + 1]; + byte b0 = this.buffer[this.Position]; + this.Position += 2; + + return (short)((b0 << 8) | b1); + } + + /// + /// Reads a big-endian 16.16 fixed-point number and advances the position by 4 bytes. + /// + /// The floating-point value. + public float ReadFloatFixed1616() + { + // Read a BE int, we parse it later. + byte b3 = this.buffer[this.Position + 3]; + byte b2 = this.buffer[this.Position + 2]; + byte b1 = this.buffer[this.Position + 1]; + byte b0 = this.buffer[this.Position]; + this.Position += 4; + + // This number is interpreted as a Fixed; that is, a signed number with 16 bits of fraction + float number = (short)((b0 << 8) | b1); + float fraction = (short)((b2 << 8) | b3) / 65536F; + return number + fraction; + } + } +} diff --git a/SixLabors.Fonts/Tables/Cff/TransformingGlyphRenderer.cs b/SixLabors.Fonts/Tables/Cff/TransformingGlyphRenderer.cs new file mode 100644 index 0000000..11d2157 --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/TransformingGlyphRenderer.cs @@ -0,0 +1,151 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Rendering; + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Used to apply a transform against any glyphs rendered by the engine. + /// + internal struct TransformingGlyphRenderer : IGlyphRenderer + { + private static readonly Vector2 YInverter = new(1, -1); + private readonly IGlyphRenderer renderer; + private Vector2 origin; + private Vector2 scale; + private Vector2 offset; + private Matrix3x2 transform; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying glyph renderer to delegate to. + /// The origin point for rendering. + /// The scale factor to apply. + /// The offset to apply before transformation. + /// The transformation matrix to apply. + public TransformingGlyphRenderer(IGlyphRenderer renderer, Vector2 origin, Vector2 scale, Vector2 offset, Matrix3x2 transform) + { + this.renderer = renderer; + this.origin = origin; + this.scale = scale; + this.offset = offset; + this.transform = transform; + this.IsOpen = false; + } + + /// + /// Gets or sets a value indicating whether a figure is currently open. + /// + public bool IsOpen { get; set; } + + /// + public void BeginFigure() + { + this.IsOpen = true; + this.renderer.BeginFigure(); + } + + /// + public bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) + { + this.IsOpen = false; + return this.renderer.BeginGlyph(in bounds, in parameters); + } + + /// + public void BeginText(in FontRectangle bounds) + { + this.IsOpen = false; + this.renderer.BeginText(in bounds); + } + + /// + public void EndFigure() + { + this.IsOpen = false; + this.renderer.EndFigure(); + } + + /// + public void EndGlyph() + { + this.IsOpen = false; + this.renderer.EndGlyph(); + } + + /// + public void EndText() + { + this.IsOpen = false; + this.renderer.EndText(); + } + + /// + public void LineTo(Vector2 point) + { + this.IsOpen = true; + this.renderer.LineTo(this.Transform(point)); + } + + /// + public void MoveTo(Vector2 point) + { + if (this.IsOpen) + { + this.EndFigure(); + } + + this.BeginFigure(); + this.renderer.MoveTo(this.Transform(point)); + this.IsOpen = true; + } + + /// + public void ArcTo(float radiusX, float radiusY, float rotationDegrees, bool largeArc, bool sweep, Vector2 point) + { + this.IsOpen = true; + this.renderer.ArcTo(radiusX * this.scale.X, radiusY * this.scale.Y, rotationDegrees, largeArc, sweep, this.Transform(point)); + } + + /// + public void CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) + { + this.IsOpen = true; + this.renderer.CubicBezierTo(this.Transform(secondControlPoint), this.Transform(thirdControlPoint), this.Transform(point)); + } + + /// + public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) + { + this.IsOpen = true; + this.renderer.QuadraticBezierTo(this.Transform(secondControlPoint), this.Transform(point)); + } + + /// + public readonly TextDecorations EnabledDecorations() + => this.renderer.EnabledDecorations(); + + /// + public readonly void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) + => this.renderer.SetDecoration(textDecorations, this.Transform(start), this.Transform(end), thickness); + + /// + /// Applies the scale, offset, transform matrix, and Y-axis inversion to the given point. + /// + /// The point to transform. + /// The transformed point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly Vector2 Transform(Vector2 point) + => (Vector2.Transform((point * this.scale) + this.offset, this.transform) * YInverter) + this.origin; + + /// + public readonly void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) + => this.renderer.BeginLayer(paint, fillRule, clipBounds); + + /// + public readonly void EndLayer() => this.renderer.EndLayer(); + } +} diff --git a/SixLabors.Fonts/Tables/Cff/Type2Operator1.cs b/SixLabors.Fonts/Tables/Cff/Type2Operator1.cs new file mode 100644 index 0000000..6db95ca --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/Type2Operator1.cs @@ -0,0 +1,171 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Single-byte Type 2 charstring operators (byte values 0-31). + /// + /// + internal enum Type2Operator1 : byte + { + /// + /// Reserved (0). + /// + Reserved0_ = 0, + + /// + /// Horizontal stem hint (1). + /// + Hstem, + + /// + /// Reserved (2). + /// + Reserved2_, + + /// + /// Vertical stem hint (3). + /// + Vstem, + + /// + /// Vertical moveto (4). + /// + Vmoveto, + + /// + /// Relative lineto (5). + /// + Rlineto, + + /// + /// Horizontal lineto (6). + /// + Hlineto, + + /// + /// Vertical lineto (7). + /// + Vlineto, + + /// + /// Relative rcurveto (8). Draws cubic Bezier curves. + /// + Rrcurveto, + + /// + /// Reserved (9). + /// + Reserved9_, + + /// + /// Call local subroutine (10). + /// + Callsubr, + + /// + /// Return from subroutine (11). + /// + Return, + + /// + /// Escape byte prefix for two-byte operators (12). + /// + Escape, + + /// + /// Reserved (13). + /// + Reserved13_, + + /// + /// End character (14). Finishes a charstring outline. + /// + Endchar, + + /// + /// CFF2 variation store index selector (15). + /// + VsIndex, + + /// + /// CFF2 blend operator for font variations (16). + /// + Blend, + + /// + /// Reserved (17). + /// + Reserved17_, + + /// + /// Horizontal stem hint with hintmask support (18). + /// + Hstemhm, + + /// + /// Hint mask (19). Specifies which stem hints are active. + /// + Hintmask, + + /// + /// Counter mask (20). Specifies counter control hints. + /// + Cntrmask, + + /// + /// Relative moveto (21). Starts a new subpath. + /// + Rmoveto, + + /// + /// Horizontal moveto (22). Starts a new subpath. + /// + Hmoveto, + + /// + /// Vertical stem hint with hintmask support (23). + /// + Vstemhm, + + /// + /// Relative curveto followed by lineto (24). + /// + Rcurveline, + + /// + /// Relative lineto followed by curveto (25). + /// + Rlinecurve, + + /// + /// Vertical-vertical curveto (26). Draws curves with vertical tangents. + /// + Vvcurveto, + + /// + /// Horizontal-horizontal curveto (27). Draws curves with horizontal tangents. + /// + Hhcurveto, + + /// + /// Short integer operand (28). Pushes a 16-bit integer onto the stack. + /// + Shortint, + + /// + /// Call global subroutine (29). + /// + Callgsubr, + + /// + /// Alternating vertical-horizontal curveto (30). + /// + Vhcurveto, + + /// + /// Alternating horizontal-vertical curveto (31). + /// + Hvcurveto, + } +} diff --git a/SixLabors.Fonts/Tables/Cff/Type2Operator2.cs b/SixLabors.Fonts/Tables/Cff/Type2Operator2.cs new file mode 100644 index 0000000..b5c03cb --- /dev/null +++ b/SixLabors.Fonts/Tables/Cff/Type2Operator2.cs @@ -0,0 +1,201 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.Cff { + /// + /// Two-byte Type 2 charstring operators (preceded by the escape byte 12). + /// + /// + internal enum Type2Operator2 : byte + { + /// + /// Reserved (0). + /// + Reserved0_ = 0, + + /// + /// Reserved (1). + /// + Reserved1_, + + /// + /// Reserved (2). + /// + Reserved2_, + + /// + /// Logical AND (3). Pops two booleans, pushes their conjunction. + /// + And, + + /// + /// Logical OR (4). Pops two booleans, pushes their disjunction. + /// + Or, + + /// + /// Logical NOT (5). Pops a boolean, pushes its negation. + /// + Not, + + /// + /// Reserved (6). + /// + Reserved6_, + + /// + /// Reserved (7). + /// + Reserved7_, + + /// + /// Reserved (8). + /// + Reserved8_, + + /// + /// Absolute value (9). + /// + Abs, + + /// + /// Addition (10). Pops two values, pushes their sum. + /// + Add, + + /// + /// Subtraction (11). Pops two values, pushes their difference. + /// + Sub, + + /// + /// Division (12). Pops two values, pushes their quotient. + /// + Div, + + /// + /// Reserved (13). + /// + Reserved13_, + + /// + /// Negation (14). Pops a value, pushes its negation. + /// + Neg, + + /// + /// Equality (15). Pops two values, pushes 1 if equal, 0 otherwise. + /// + Eq, + + /// + /// Reserved (16). + /// + Reserved16_, + + /// + /// Reserved (17). + /// + Reserved17_, + + /// + /// Drop (18). Removes the top element from the stack. + /// + Drop, + + /// + /// Reserved (19). + /// + Reserved19_, + + /// + /// Put (20). Stores a value in the transient array. + /// + Put, + + /// + /// Get (21). Retrieves a value from the transient array. + /// + Get, + + /// + /// If-else (22). Conditional operator. + /// + Ifelse, + + /// + /// Random (23). Pushes a pseudo-random number. + /// + Random, + + /// + /// Multiplication (24). Pops two values, pushes their product. + /// + Mul, + + /// + /// Reserved (25). + /// + Reserved25_, + + /// + /// Square root (26). Pops a value, pushes its square root. + /// + Sqrt, + + /// + /// Duplicate (27). Duplicates the top stack element. + /// + Dup, + + /// + /// Exchange (28). Swaps the top two elements on the argument stack. + /// + Exch, + + /// + /// Index (29). Copies an indexed element to the top of the stack. + /// + Index, + + /// + /// Roll (30). Rotates the top N stack elements. + /// + Roll, + + /// + /// Reserved (31). + /// + Reserved31_, + + /// + /// Reserved (32). + /// + Reserved32_, + + /// + /// Reserved (33). + /// + Reserved33_, + + /// + /// Horizontal flex (34). A flex mechanism for horizontal curves. + /// + Hflex, + + /// + /// Flex (35). A general flex mechanism for curves. + /// + Flex, + + /// + /// Horizontal flex variant 1 (36). + /// + Hflex1, + + /// + /// Flex variant 1 (37). A general flex with more control points. + /// + Flex1 + } +} diff --git a/SixLabors.Fonts/Tables/General/CMap/CMapSubTable.cs b/SixLabors.Fonts/Tables/General/CMap/CMapSubTable.cs new file mode 100644 index 0000000..ba01926 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/CMap/CMapSubTable.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using SixLabors.Fonts.WellKnownIds; +using System.Collections.Generic; + +namespace SixLabors.Fonts.Tables.General.CMap { + /// + /// Base class for all 'cmap' subtables that map character codes to glyph indices. + /// + /// + internal abstract class CMapSubTable + { + /// + /// Initializes a new instance of the class. + /// + public CMapSubTable() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The platform identifier. + /// The platform-specific encoding identifier. + /// The subtable format number. + public CMapSubTable(PlatformIDs platform, ushort encoding, ushort format) + { + this.Platform = platform; + this.Encoding = encoding; + this.Format = format; + } + + /// + /// Gets the subtable format number. + /// + public ushort Format { get; } + + /// + /// Gets the platform identifier. + /// + public PlatformIDs Platform { get; } + + /// + /// Gets the platform-specific encoding identifier. + /// + public ushort Encoding { get; } + + /// + /// Tries to get the glyph identifier for the given code point. + /// + /// The Unicode code point. + /// When this method returns, contains the glyph identifier if found; otherwise, 0. + /// if the glyph identifier was found; otherwise, . + public abstract bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId); + + /// + /// Tries to get the code point for the given glyph identifier. + /// + /// The glyph identifier. + /// When this method returns, contains the code point if found; otherwise, the default value. + /// if the code point was found; otherwise, . + public abstract bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint); + + /// + /// Gets the collection of all available code points in this subtable. + /// + /// An enumerable of available code point values. + public abstract IEnumerable GetAvailableCodePoints(); + } +} diff --git a/SixLabors.Fonts/Tables/General/CMap/EncodingRecord.cs b/SixLabors.Fonts/Tables/General/CMap/EncodingRecord.cs new file mode 100644 index 0000000..9220a46 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/CMap/EncodingRecord.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.WellKnownIds; + +namespace SixLabors.Fonts.Tables.General.CMap { + /// + /// Represents an encoding record in the 'cmap' table header. Each record specifies a platform ID, + /// encoding ID, and byte offset to the subtable for that encoding. + /// + /// + internal readonly struct EncodingRecord + { + /// + /// Initializes a new instance of the struct. + /// + /// The platform identifier. + /// The platform-specific encoding identifier. + /// The byte offset from the beginning of the 'cmap' table to the subtable. + public EncodingRecord(PlatformIDs platformID, ushort encodingID, uint offset) + { + this.PlatformID = platformID; + this.EncodingID = encodingID; + this.Offset = offset; + } + + /// + /// Gets the platform identifier. + /// + public PlatformIDs PlatformID { get; } + + /// + /// Gets the platform-specific encoding identifier. + /// + public ushort EncodingID { get; } + + /// + /// Gets the byte offset from the beginning of the 'cmap' table to the subtable. + /// + public uint Offset { get; } + + /// + /// Reads an from the specified reader. + /// + /// The binary reader positioned at the encoding record data. + /// The parsed . + public static EncodingRecord Read(BigEndianBinaryReader reader) + { + var platform = (PlatformIDs)reader.ReadUInt16(); + ushort encoding = reader.ReadUInt16(); + uint offset = reader.ReadOffset32(); + + return new EncodingRecord(platform, encoding, offset); + } + } +} diff --git a/SixLabors.Fonts/Tables/General/CMap/Format0SubTable.cs b/SixLabors.Fonts/Tables/General/CMap/Format0SubTable.cs new file mode 100644 index 0000000..2385741 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/CMap/Format0SubTable.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using SixLabors.Fonts.WellKnownIds; +using System.Collections.Generic; +using System.Linq; + +namespace SixLabors.Fonts.Tables.General.CMap { + /// + /// Format 0 is a simple byte encoding subtable that maps character codes 0–255 to glyph indices. + /// + /// + internal sealed class Format0SubTable : CMapSubTable + { + /// + /// Initializes a new instance of the class. + /// + /// The language code for Macintosh platform subtables. + /// The platform identifier. + /// The platform-specific encoding identifier. + /// The array of glyph indices indexed by character code. + public Format0SubTable(ushort language, PlatformIDs platform, ushort encoding, byte[] glyphIds) + : base(platform, encoding, 0) + { + this.Language = language; + this.GlyphIds = glyphIds; + } + + /// + /// Gets the language code for Macintosh platform subtables. + /// + public ushort Language { get; } + + /// + /// Gets the array of glyph indices indexed by character code. + /// + public byte[] GlyphIds { get; } + + /// + public override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) + { + int b = codePoint.Value; + if (b >= this.GlyphIds.Length) + { + glyphId = 0; + return false; + } + + glyphId = this.GlyphIds[b]; + return true; + } + + /// + public override bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) + { + for (int i = 0; i < this.GlyphIds.Length; i++) + { + if (this.GlyphIds[i] == glyphId) + { + codePoint = new CodePoint(i); + return true; + } + } + + codePoint = default; + return false; + } + + /// + public override IEnumerable GetAvailableCodePoints() + => Enumerable.Range(0, this.GlyphIds.Length); + + /// + /// Loads one or more instances from the specified encoding records and reader. + /// + /// The encoding records that share this subtable. + /// The binary reader positioned after the format field. + /// An enumerable of instances, one per encoding record. + public static IEnumerable Load(IEnumerable encodings, BigEndianBinaryReader reader) + { + // format has already been read by this point skip it + ushort length = reader.ReadUInt16(); + ushort language = reader.ReadUInt16(); + int glyphsCount = length - 6; + + // char 'A' == 65 thus glyph = glyphIds[65]; + byte[] glyphIds = reader.ReadBytes(glyphsCount); + + foreach (EncodingRecord encoding in encodings) + { + yield return new Format0SubTable(language, encoding.PlatformID, encoding.EncodingID, glyphIds); + } + } + } +} diff --git a/SixLabors.Fonts/Tables/General/CMap/Format12SubTable.cs b/SixLabors.Fonts/Tables/General/CMap/Format12SubTable.cs new file mode 100644 index 0000000..3dcfafe --- /dev/null +++ b/SixLabors.Fonts/Tables/General/CMap/Format12SubTable.cs @@ -0,0 +1,174 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using SixLabors.Fonts.WellKnownIds; +using System.Collections.Generic; +using System.Linq; + +namespace SixLabors.Fonts.Tables.General.CMap { + /// + /// Format 12 is a segmented coverage subtable used for character codes beyond the BMP (U+0000 to U+10FFFF). + /// It uses 32-bit character codes and groups of sequential mappings. + /// + /// + internal sealed class Format12SubTable : CMapSubTable + { + /// + /// Initializes a new instance of the class. + /// + /// The language code for this subtable. + /// The platform identifier. + /// The platform-specific encoding identifier. + /// The array of sequential map groups. + public Format12SubTable(uint language, PlatformIDs platform, ushort encoding, SequentialMapGroup[] groups) + : base(platform, encoding, 4) + { + this.Language = language; + this.SequentialMapGroups = groups; + } + + /// + /// Gets the array of sequential map groups defining character-to-glyph mappings. + /// + public SequentialMapGroup[] SequentialMapGroups { get; } + + /// + /// Gets the language code for this subtable. + /// + public uint Language { get; } + + /// + public override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) + { + int charAsInt = codePoint.Value; + + for (int i = 0; i < this.SequentialMapGroups.Length; i++) + { + ref SequentialMapGroup seg = ref this.SequentialMapGroups[i]; + + if (charAsInt >= seg.StartCodePoint && charAsInt <= seg.EndCodePoint) + { + glyphId = (ushort)(charAsInt - seg.StartCodePoint + seg.StartGlyphId); + return true; + } + } + + glyphId = 0; + return false; + } + + /// + public override bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) + { + for (int i = 0; i < this.SequentialMapGroups.Length; i++) + { + ref SequentialMapGroup seg = ref this.SequentialMapGroups[i]; + if (glyphId >= seg.StartGlyphId && glyphId <= seg.StartGlyphId + seg.EndCodePoint - seg.StartCodePoint) + { + // Reverse the calculation: + // Forward: glyphId = (codePoint - StartCodePoint) + StartGlyphId + // Reverse: codePoint = (glyphId - StartGlyphId) + StartCodePoint + codePoint = new CodePoint(glyphId - seg.StartGlyphId + seg.StartCodePoint); + return true; + } + } + + codePoint = default; + return false; + } + + /// + public override IEnumerable GetAvailableCodePoints() + => this.SequentialMapGroups.SelectMany(segment => + { + int start = (int)segment.StartCodePoint; + int end = (int)segment.EndCodePoint; + return Enumerable.Range(start, end - start + 1); + }); + + /// + /// Loads one or more instances from the specified encoding records and reader. + /// + /// The encoding records that share this subtable. + /// The binary reader positioned after the format field. + /// An enumerable of instances, one per encoding record. + public static IEnumerable Load(IEnumerable encodings, BigEndianBinaryReader reader) + { + // 'cmap' Subtable Format 4: + // Type | Name | Description + // -------------------|-------------------|------------------------------------------------------------------------ + // uint16 | format | Subtable format; set to 12. + // uint16 | reserved | Reserved; set to 0 + // uint32 | length | Byte length of this subtable(including the header) + // uint32 | language | For requirements on use of the language field, see "Use of the language field in 'cmap' subtables" in this document. + // uint32 | numGroups | Number of groupings which follow + // SequentialMapGroup | groups[numGroups] | Array of SequentialMapGroup records. + + // format has already been read by this point skip it + ushort reserved = reader.ReadUInt16(); + uint length = reader.ReadUInt32(); + uint language = reader.ReadUInt32(); + uint numGroups = reader.ReadUInt32(); + + var groups = new SequentialMapGroup[numGroups]; + for (var i = 0; i < numGroups; i++) + { + groups[i] = SequentialMapGroup.Load(reader); + } + + foreach (EncodingRecord encoding in encodings) + { + yield return new Format12SubTable(language, encoding.PlatformID, encoding.EncodingID, groups); + } + } + + /// + /// Represents a sequential map group record that maps a contiguous range of character codes + /// to a contiguous range of glyph indices. + /// + internal readonly struct SequentialMapGroup + { + /// + /// The first character code in this group. + /// + public readonly uint StartCodePoint; + + /// + /// The last character code in this group (inclusive). + /// + public readonly uint EndCodePoint; + + /// + /// The glyph index corresponding to the starting character code. + /// + public readonly uint StartGlyphId; + + /// + /// Initializes a new instance of the struct. + /// + /// The first character code in this group. + /// The last character code in this group. + /// The glyph index corresponding to the starting character code. + public SequentialMapGroup(uint startCodePoint, uint endCodePoint, uint startGlyph) + { + this.StartCodePoint = startCodePoint; + this.EndCodePoint = endCodePoint; + this.StartGlyphId = startGlyph; + } + + /// + /// Loads a from the specified reader. + /// + /// The binary reader positioned at the sequential map group data. + /// The parsed . + public static SequentialMapGroup Load(BigEndianBinaryReader reader) + { + var startCodePoint = reader.ReadUInt32(); + var endCodePoint = reader.ReadUInt32(); + var startGlyph = reader.ReadUInt32(); + return new SequentialMapGroup(startCodePoint, endCodePoint, startGlyph); + } + } + } +} diff --git a/SixLabors.Fonts/Tables/General/CMap/Format14SubTable.cs b/SixLabors.Fonts/Tables/General/CMap/Format14SubTable.cs new file mode 100644 index 0000000..9932325 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/CMap/Format14SubTable.cs @@ -0,0 +1,229 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using SixLabors.Fonts.WellKnownIds; +using System; +using System.Collections.Generic; +using System.IO; + +namespace SixLabors.Fonts.Tables.General.CMap { + /// + /// Subtable format 14 specifies the Unicode Variation Sequences (UVSes) supported by the font. + /// A Variation Sequence, according to the Unicode Standard, comprises a base character followed + /// by a variation selector. For example, <U+82A6, U+E0101>. + /// + /// + internal sealed class Format14SubTable : CMapSubTable + { + /// + /// The dictionary mapping variation selector code points to their associated variation selector records. + /// + private readonly Dictionary variationSelectors; + + /// + /// Initializes a new instance of the class. + /// + /// The dictionary of variation selector records keyed by selector code point. + /// The platform identifier. + /// The platform-specific encoding identifier. + private Format14SubTable(Dictionary variationSelectors, PlatformIDs platform, ushort encoding) + : base(platform, encoding, 5) + => this.variationSelectors = variationSelectors; + + /// + /// Loads one or more instances from the specified encoding records and reader. + /// + /// The encoding records that share this subtable. + /// The binary reader positioned after the format field. + /// The byte offset to the start of this format 14 subtable. + /// An enumerable of instances, one per encoding record. + public static IEnumerable Load( + IEnumerable encodings, + BigEndianBinaryReader reader, + long offset) + { + // +-------------------+------------------------------------+------------------------------------------------------+ + // | Type | Name | Description | + // +===================+====================================+======================================================+ + // | uint16 | format | Subtable format. Set to 14. | + // +-------------------+------------------------------------+------------------------------------------------------+ + // | uint32 | length | Byte length of this subtable (including this header) | + // +-------------------+------------------------------------+------------------------------------------------------+ + // | uint32 | numVarSelectorRecords | Number of variation Selector Records | + // +-------------------+------------------------------------+------------------------------------------------------+ + // | VariationSelector | varSelector[numVarSelectorRecords] | Array of VariationSelector records. | + // +-------------------+------------------------------------+------------------------------------------------------+ + uint length = reader.ReadUInt32(); + uint numVarSelectorRecords = reader.ReadUInt32(); + + var variationSelectors = new Dictionary(); + uint[] varSelectors = new uint[numVarSelectorRecords]; + uint[] defaultUVSOffsets = new uint[numVarSelectorRecords]; + uint[] nonDefaultUVSOffsets = new uint[numVarSelectorRecords]; + for (int i = 0; i < numVarSelectorRecords; ++i) + { + // +----------+---------------------+----------------------------------------------------+ + // | Type | Name | Description | + // +==========+=====================+====================================================+ + // | uint24 | varSelector | Variation selector | + // +----------+---------------------+----------------------------------------------------+ + // | Offset32 | defaultUVSOffset | Offset from the start of the format 14 subtable to | + // | | | Default UVS Table. May be 0. | + // +----------+---------------------+----------------------------------------------------+ + // | Offset32 | nonDefaultUVSOffset | Offset from the start of the format 14 subtable to | + // | | | Non-Default UVS Table. May be 0. | + // +----------+---------------------+----------------------------------------------------+ + varSelectors[i] = reader.ReadUInt24(); + defaultUVSOffsets[i] = reader.ReadUInt32(); + nonDefaultUVSOffsets[i] = reader.ReadUInt32(); + } + + for (int i = 0; i < numVarSelectorRecords; ++i) + { + var selector = new VariationSelector(); + if (defaultUVSOffsets[i] != 0) + { + // Default UVS table + // +--------------+-------------------------------+-------------------------------------+ + // | Type | Name | Description | + // +==============+===============================+=====================================+ + // | uint32 | numUnicodeValueRanges | Number of Unicode character ranges. | + // +--------------+-------------------------------+-------------------------------------+ + // | UnicodeRange | ranges[numUnicodeValueRanges] | Array of UnicodeRange records. | + // +--------------+-------------------------------+-------------------------------------+ + + // UnicodeRange Record + // +--------+-------------------+-------------------------------------------+ + // | Type | Name | Description | + // +========+===================+===========================================+ + // | uint24 | startUnicodeValue | First value in this range | + // +--------+-------------------+-------------------------------------------+ + // | uint8 | additionalCount | Number of additional values in this range | + // +--------+-------------------+-------------------------------------------+ + reader.Seek(offset + defaultUVSOffsets[i], SeekOrigin.Begin); + uint numUnicodeValueRanges = reader.ReadUInt32(); + for (int n = 0; n < numUnicodeValueRanges; n++) + { + uint startCode = reader.ReadUInt24(); + selector.DefaultStartCodes.Add(startCode); + selector.DefaultEndCodes.Add(startCode + reader.ReadByte()); + } + } + + if (nonDefaultUVSOffsets[i] != 0) + { + // Non-Default UVS table + // +------------+-----------------------------+------------------------------------+ + // | Type | Name | Description | + // +============+=============================+====================================+ + // | uint32 | numUVSMappings | Number of UVS Mappings that follow | + // +------------+-----------------------------+------------------------------------+ + // | UVSMapping | uvsMappings[numUVSMappings] | Array of UVSMapping records. | + // +------------+-----------------------------+------------------------------------+ + + // UVSMapping Record + // +--------+--------------+-------------------------------+ + // | Type | Name | Description | + // +========+==============+===============================+ + // | uint24 | unicodeValue | Base Unicode value of the UVS | + // +--------+--------------+-------------------------------+ + // | uint16 | glyphID | Glyph ID of the UVS | + // +--------+--------------+-------------------------------+ + reader.Seek(offset + nonDefaultUVSOffsets[i], SeekOrigin.Begin); + uint numUVSMappings = reader.ReadUInt32(); + for (int n = 0; n < numUVSMappings; n++) + { + uint unicodeValue = reader.ReadUInt24(); + ushort glyphID = reader.ReadUInt16(); + selector.UVSMappings.Add(unicodeValue, glyphID); + } + } + + variationSelectors.Add(varSelectors[i], selector); + } + + foreach (EncodingRecord encoding in encodings) + { + yield return new Format14SubTable(variationSelectors, encoding.PlatformID, encoding.EncodingID); + } + } + + /// + public override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) + { + glyphId = 0; + return false; + } + + /// + public override bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) + { + codePoint = default; + return false; + } + + /// + public override IEnumerable GetAvailableCodePoints() + => Array.Empty(); + + /// + /// Resolves a glyph identifier for a base character and variation selector pair using Unicode Variation Sequences. + /// + /// The base character code point. + /// The default glyph index for the base character. + /// The variation selector code point. + /// The resolved glyph identifier, or 0 if the next code point is not a variation selector. + public ushort CharacterPairToGlyphId(CodePoint codePoint, ushort defaultGlyphIndex, CodePoint nextCodePoint) + { + // Only check codepoint if nextCodepoint is a variation selector + if (this.variationSelectors.TryGetValue((uint)nextCodePoint.Value, out VariationSelector? sel)) + { + // If the sequence is a non-default UVS, return the mapped glyph + if (sel.UVSMappings.TryGetValue((uint)codePoint.Value, out ushort ret)) + { + return ret; + } + + // If the sequence is a default UVS, return the default glyph + for (int i = 0; i < sel.DefaultStartCodes.Count; ++i) + { + if (codePoint.Value >= sel.DefaultStartCodes[i] && codePoint.Value < sel.DefaultEndCodes[i]) + { + return defaultGlyphIndex; + } + } + + // At this point we are neither a non-default UVS nor a default UVS, + // but we know the nextCodepoint is a variation selector. Unicode says + // this glyph should be invisible: "no visible rendering for the VS" + // (http://unicode.org/faq/unsup_char.html#4) + return defaultGlyphIndex; + } + + // In all other cases, return 0 + return 0; + } + + /// + /// Represents a variation selector record containing default UVS ranges and non-default UVS mappings. + /// + private class VariationSelector + { + /// + /// Gets the list of start code points for default UVS ranges. + /// + public List DefaultStartCodes { get; } = []; + + /// + /// Gets the list of end code points (exclusive) for default UVS ranges. + /// + public List DefaultEndCodes { get; } = []; + + /// + /// Gets the dictionary mapping base character code points to glyph indices for non-default UVS mappings. + /// + public Dictionary UVSMappings { get; } = []; + } + } +} diff --git a/SixLabors.Fonts/Tables/General/CMap/Format4SubTable.cs b/SixLabors.Fonts/Tables/General/CMap/Format4SubTable.cs new file mode 100644 index 0000000..a32ff38 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/CMap/Format4SubTable.cs @@ -0,0 +1,280 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using SixLabors.Fonts.WellKnownIds; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SixLabors.Fonts.Tables.General.CMap { + /// + /// Format 4 is a segment mapping to delta values subtable used for character codes in the BMP (U+0000 to U+FFFF). + /// + /// + internal sealed class Format4SubTable : CMapSubTable + { + /// + /// Initializes a new instance of the class. + /// + /// The language code for Macintosh platform subtables. + /// The platform identifier. + /// The platform-specific encoding identifier. + /// The array of character code segments. + /// The glyph index array used for offset-based lookups. + public Format4SubTable(ushort language, PlatformIDs platform, ushort encoding, Segment[] segments, ushort[] glyphIds) + : base(platform, encoding, 4) + { + this.Language = language; + this.Segments = segments; + this.GlyphIds = glyphIds; + } + + /// + /// Gets the array of character code segments. + /// + public Segment[] Segments { get; } + + /// + /// Gets the glyph index array used for offset-based lookups. + /// + public ushort[] GlyphIds { get; } + + /// + /// Gets the language code for Macintosh platform subtables. + /// + public ushort Language { get; } + + /// + public override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) + { + int charAsInt = codePoint.Value; + + for (int i = 0; i < this.Segments.Length; i++) + { + ref Segment seg = ref this.Segments[i]; + + if (seg.End >= charAsInt && seg.Start <= charAsInt) + { + if (seg.Offset == 0) + { + glyphId = (ushort)((charAsInt + seg.Delta) & ushort.MaxValue); + return true; + } + + long offset = (seg.Offset / 2) + (charAsInt - seg.Start); + long idx = offset - this.Segments.Length + seg.Index; + + if (idx < 0 || idx >= this.GlyphIds.Length) + { + glyphId = 0; + return false; + } + + glyphId = this.GlyphIds[idx]; + return true; + } + } + + glyphId = 0; + return false; + } + + /// + public override bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) + { + for (int i = 0; i < this.Segments.Length; i++) + { + ref Segment seg = ref this.Segments[i]; + + if (seg.Offset == 0) + { + // Reverse the delta-based calculation + // Forward was: glyphId = (charAsInt + seg.Delta) & 0xFFFF + // Reverse should apply the inverse logic with the same wrap: + int candidate = (glyphId - seg.Delta) & ushort.MaxValue; + + if (candidate >= seg.Start && candidate <= seg.End) + { + codePoint = new CodePoint(candidate); + return true; + } + } + else + { + // Reverse the offset-based calculation: + // Forward logic: + // offset = (seg.Offset / 2) + (charAsInt - seg.Start) + // glyphId = GlyphIds[offset - Segments.Length + seg.Index] + + // To reverse, iterate over possible codepoints in the segment and find the matching glyphId. + for (long j = 0; j <= (seg.End - seg.Start); j++) + { + long offset = (seg.Offset / 2) + j; + long idx = offset - this.Segments.Length + seg.Index; + + if (idx < 0 || idx >= this.GlyphIds.Length) + { + codePoint = default; + return false; + } + + if (this.GlyphIds[idx] == glyphId) + { + codePoint = new CodePoint((int)(seg.Start + j)); + return true; + } + } + } + } + + codePoint = default; + return false; + } + + /// + public override IEnumerable GetAvailableCodePoints() + => this.Segments.SelectMany(segment => Enumerable.Range(segment.Start, segment.End - segment.Start + 1)); + + /// + /// Loads one or more instances from the specified encoding records and reader. + /// + /// The encoding records that share this subtable. + /// The binary reader positioned after the format field. + /// An enumerable of instances, one per encoding record. + public static IEnumerable Load(IEnumerable encodings, BigEndianBinaryReader reader) + { + // 'cmap' Subtable Format 4: + // Type | Name | Description + // -------|----------------------------|------------------------------------------------------------------------ + // uint16 | format | Format number is set to 4. + // uint16 | length | This is the length in bytes of the subtable. + // uint16 | language | Please see "Note on the language field in 'cmap' subtables" in this document. + // uint16 | segCountX2 | 2 x segCount. + // uint16 | searchRange | 2 x (2**floor(log2(segCount))) + // uint16 | entrySelector | log2(searchRange/2) + // uint16 | rangeShift | 2 x segCount - searchRange + // uint16 | endCount[segCount] | End characterCode for each segment, last=0xFFFF. + // uint16 | reservedPad | Set to 0. + // uint16 | startCount[segCount] | Start character code for each segment. + // int16 | idDelta[segCount] | Delta for all character codes in segment. + // uint16 | idRangeOffset[segCount] | Offsets into glyphIdArray or 0 + // uint16 | glyphIdArray[ ] | Glyph index array (arbitrary length) + // format has already been read by this point skip it + ushort length = reader.ReadUInt16(); + ushort language = reader.ReadUInt16(); + ushort segCountX2 = reader.ReadUInt16(); + ushort searchRange = reader.ReadUInt16(); + ushort entrySelector = reader.ReadUInt16(); + ushort rangeShift = reader.ReadUInt16(); + int segCount = segCountX2 / 2; + + using Buffer endCountBuffer = new(segCount); + Span endCounts = endCountBuffer.GetSpan(); + reader.ReadUInt16Array(endCounts); + + ushort reserved = reader.ReadUInt16(); + + using Buffer startCountsBuffer = new(segCount); + Span startCounts = startCountsBuffer.GetSpan(); + reader.ReadUInt16Array(startCounts); + + using Buffer idDeltaBuffer = new(segCount); + Span idDelta = idDeltaBuffer.GetSpan(); + reader.ReadInt16Array(idDelta); + + using Buffer idRangeOffsetBuffer = new(segCount); + Span idRangeOffset = idRangeOffsetBuffer.GetSpan(); + reader.ReadUInt16Array(idRangeOffset); + + // table length thus far + int headerLength = 16 + (segCount * 8); + int glyphIdCount = (length - headerLength) / 2; + + ushort[] glyphIds = reader.ReadUInt16Array(glyphIdCount); + + Segment[] segments = Segment.Create(endCounts, startCounts, idDelta, idRangeOffset); + + List table = []; + foreach (EncodingRecord encoding in encodings) + { + table.Add(new Format4SubTable(language, encoding.PlatformID, encoding.EncodingID, segments, glyphIds)); + } + + return table; + } + + /// + /// Represents a single segment in a Format 4 subtable, defining a contiguous range of character codes + /// and their mapping to glyph indices via delta or offset. + /// + internal readonly struct Segment + { + /// + /// Initializes a new instance of the struct. + /// + /// The zero-based index of this segment in the segment array. + /// The end character code for this segment. + /// The start character code for this segment. + /// The delta value to apply to character codes in this segment. + /// The offset into the glyph index array, or 0 if delta-based mapping is used. + public Segment(ushort index, ushort end, ushort start, short delta, ushort offset) + { + this.Index = index; + this.End = end; + this.Start = start; + this.Delta = delta; + this.Offset = offset; + } + + /// + /// Gets the zero-based index of this segment in the segment array. + /// + public ushort Index { get; } + + /// + /// Gets the delta value added to character codes to produce glyph indices. + /// + public short Delta { get; } + + /// + /// Gets the end character code for this segment (inclusive). + /// + public ushort End { get; } + + /// + /// Gets the offset into the glyph index array, or 0 if delta-based mapping is used. + /// + public ushort Offset { get; } + + /// + /// Gets the start character code for this segment. + /// + public ushort Start { get; } + + /// + /// Creates an array of instances from the parallel arrays read from the subtable. + /// + /// The end character codes for each segment. + /// The start character codes for each segment. + /// The delta values for each segment. + /// The range offset values for each segment. + /// An array of instances. + public static Segment[] Create(ReadOnlySpan endCounts, ReadOnlySpan startCode, ReadOnlySpan idDelta, ReadOnlySpan idRangeOffset) + { + int count = endCounts.Length; + Segment[] segments = new Segment[count]; + for (ushort i = 0; i < count; i++) + { + ushort start = startCode[i]; + ushort end = endCounts[i]; + short delta = idDelta[i]; + ushort offset = idRangeOffset[i]; + segments[i] = new Segment(i, end, start, delta, offset); + } + + return segments; + } + } + } +} diff --git a/SixLabors.Fonts/Tables/General/CMapTable.cs b/SixLabors.Fonts/Tables/General/CMapTable.cs new file mode 100644 index 0000000..2a28a66 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/CMapTable.cs @@ -0,0 +1,221 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.General.CMap; +using SixLabors.Fonts.Unicode; +using SixLabors.Fonts.WellKnownIds; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace SixLabors.Fonts.Tables.General { + /// + /// Represents the character to glyph index mapping table, which maps character codes to glyph indices. + /// + /// + internal sealed class CMapTable : Table + { + /// + /// The table name identifier. + /// + internal const string TableName = "cmap"; + + /// + /// The format 14 subtables for Unicode variation sequences. + /// + private readonly Format14SubTable[] format14SubTables = Array.Empty(); + + /// + /// Cached codepoints available in the font. + /// + private CodePoint[]? codepoints; + + /// + /// Initializes a new instance of the class. + /// + /// The collection of CMap subtables. + public CMapTable(IEnumerable tables) + { + this.Tables = tables.OrderBy(t => GetPreferredPlatformOrder(t.Platform)).ToArray(); + this.format14SubTables = this.Tables.OfType().ToArray(); + } + + /// + /// Gets the subtables ordered by preferred platform. + /// + internal CMapSubTable[] Tables { get; } + + /// + /// Gets the preferred platform ordering for subtable selection. + /// Windows is preferred, followed by Unicode, then Macintosh. + /// + /// The platform identifier. + /// The sort order value (lower is more preferred). + private static int GetPreferredPlatformOrder(PlatformIDs platform) + => platform switch + { + PlatformIDs.Windows => 0, + PlatformIDs.Unicode => 1, + PlatformIDs.Macintosh => 2, + _ => int.MaxValue + }; + + /// + /// Tries to get the glyph ID for the given code point, optionally considering the next code point + /// for Unicode Variation Sequence (UVS) matching. + /// + /// The code point to look up. + /// The optional next code point for UVS matching. + /// When this method returns, contains the glyph ID if found. + /// When this method returns, indicates whether the next code point was consumed as part of a UVS. + /// if a glyph was found; otherwise, . + public bool TryGetGlyphId(CodePoint codePoint, CodePoint? nextCodePoint, out ushort glyphId, out bool skipNextCodePoint) + { + skipNextCodePoint = false; + if (this.TryGetGlyphId(codePoint, out glyphId)) + { + // If there is a second codepoint, we are asked whether this is an UVS sequence + // - If true, return a glyph Id. + // - Otherwise, return 0. + if (nextCodePoint != null && this.format14SubTables.Length > 0) + { + foreach (Format14SubTable? cmap14 in this.format14SubTables) + { + ushort pairGlyphId = cmap14.CharacterPairToGlyphId(codePoint, glyphId, nextCodePoint.Value); + if (pairGlyphId > 0) + { + glyphId = pairGlyphId; + skipNextCodePoint = true; + return true; + } + } + } + + return true; + } + + return false; + } + + /// + /// Tries to get the glyph ID for the given code point by searching all subtables. + /// + /// The code point to look up. + /// When this method returns, contains the glyph ID if found. + /// if a non-zero glyph ID was found; otherwise, . + private bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) + { + foreach (CMapSubTable t in this.Tables) + { + // Keep looking until we have an index that's not the fallback. + // Regardless of the encoding scheme, character codes that do + // not correspond to any glyph in the font should be mapped to glyph index 0. + // The glyph at this location must be a special glyph representing a missing character, commonly known as .notdef. + if (t.TryGetGlyphId(codePoint, out glyphId) && glyphId > 0) + { + return true; + } + } + + glyphId = 0; + return false; + } + + /// + /// Tries to get the code point for the given glyph ID via reverse lookup. + /// + /// The glyph ID to look up. + /// When this method returns, contains the code point if found. + /// if a code point was found; otherwise, . + public bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) + { + foreach (CMapSubTable t in this.Tables) + { + if (t.TryGetCodePoint(glyphId, out codePoint)) + { + return true; + } + } + + codePoint = default; + return false; + } + + /// + /// Gets the unicode codepoints for which a glyph exists in the font. + /// + /// A read-only memory region containing the available codepoints. + public ReadOnlyMemory GetAvailableCodePoints() + { + if (this.codepoints is not null) + { + return this.codepoints; + } + + HashSet values = new(); + + foreach (int v in this.Tables.SelectMany(subtable => subtable.GetAvailableCodePoints())) + { + values.Add(v); + } + + return this.codepoints = values.OrderBy(v => v).Select(v => new CodePoint(v)).ToArray(); + } + + /// + /// Loads the from the specified font reader. + /// + /// The font reader. + /// The . + public static CMapTable Load(FontReader reader) + { + using BigEndianBinaryReader binaryReader = reader.GetReaderAtTablePosition(TableName); + return Load(binaryReader); + } + + /// + /// Loads the from the specified binary reader. + /// + /// The big-endian binary reader. + /// The . + public static CMapTable Load(BigEndianBinaryReader reader) + { + ushort version = reader.ReadUInt16(); + ushort numTables = reader.ReadUInt16(); + + var encodings = new EncodingRecord[numTables]; + for (int i = 0; i < numTables; i++) + { + encodings[i] = EncodingRecord.Read(reader); + } + + // foreach encoding we move forward looking for the subtables + var tables = new List(numTables); + foreach (IGrouping encoding in encodings.GroupBy(x => x.Offset)) + { + long offset = encoding.Key; + reader.Seek(offset, SeekOrigin.Begin); + + // Subtable format. + switch (reader.ReadUInt16()) + { + case 0: + tables.AddRange(Format0SubTable.Load(encoding, reader)); + break; + case 4: + tables.AddRange(Format4SubTable.Load(encoding, reader)); + break; + case 12: + tables.AddRange(Format12SubTable.Load(encoding, reader)); + break; + case 14: + tables.AddRange(Format14SubTable.Load(encoding, reader, offset)); + break; + } + } + + return new CMapTable(tables); + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/Affine2x3.cs b/SixLabors.Fonts/Tables/General/Colr/Affine2x3.cs new file mode 100644 index 0000000..9d92bbb --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/Affine2x3.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Represents a 2x3 affine transformation matrix used by COLR v1 PaintTransform operations. + /// Values are stored as Fixed 16.16 numbers. + /// + /// + internal readonly struct Affine2x3 + { + /// + /// The x-component of the x-basis vector. + /// + public readonly float Xx; + + /// + /// The y-component of the x-basis vector. + /// + public readonly float Yx; + + /// + /// The x-component of the y-basis vector. + /// + public readonly float Xy; + + /// + /// The y-component of the y-basis vector. + /// + public readonly float Yy; + + /// + /// The x-translation component. + /// + public readonly float Dx; + + /// + /// The y-translation component. + /// + public readonly float Dy; + + /// + /// Initializes a new instance of the struct. + /// + /// The x-component of the x-basis vector. + /// The y-component of the x-basis vector. + /// The x-component of the y-basis vector. + /// The y-component of the y-basis vector. + /// The x-translation component. + /// The y-translation component. + public Affine2x3(float xx, float yx, float xy, float yy, float dx, float dy) + { + this.Xx = xx; + this.Yx = yx; + this.Xy = xy; + this.Yy = yy; + this.Dx = dx; + this.Dy = dy; + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/BaseGlyphList.cs b/SixLabors.Fonts/Tables/General/Colr/BaseGlyphList.cs new file mode 100644 index 0000000..8760abd --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/BaseGlyphList.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Represents the BaseGlyphList table in COLR v1, which maps glyph IDs to their root paint table offsets. + /// + /// + internal sealed class BaseGlyphList + { + /// + /// Initializes a new instance of the class. + /// + /// The array of base glyph paint records. + public BaseGlyphList(BaseGlyphPaintRecord[] records) + => this.Records = records; + + /// + /// Gets the array of base glyph paint records, sorted by glyph ID. + /// + public BaseGlyphPaintRecord[] Records { get; } + + /// + /// Gets the number of base glyph paint records. + /// + public int Count => this.Records.Length; + + /// + /// Loads a from the given reader at the specified offset. + /// + /// The binary reader positioned within the COLR table. + /// The offset from the beginning of the COLR table to the BaseGlyphList. + /// The loaded , or if the offset is zero or the list is empty. + public static BaseGlyphList? Load(BigEndianBinaryReader reader, uint offset) + { + if (offset == 0) + { + return null; + } + + reader.Seek(offset, SeekOrigin.Begin); + uint count = reader.ReadUInt32(); + + if (count == 0) + { + return null; + } + + // Offsets are relative to the table start; convert to COLR-relative. + BaseGlyphPaintRecord[] records = new BaseGlyphPaintRecord[count]; + for (int i = 0; i < count; i++) + { + ushort glyphId = reader.ReadUInt16(); + records[i] = new BaseGlyphPaintRecord(glyphId, offset + reader.ReadOffset32()); + } + + // Spec says records are sorted by glyphId; assume font is correct + return new BaseGlyphList(records); + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/BaseGlyphPaintRecord.cs b/SixLabors.Fonts/Tables/General/Colr/BaseGlyphPaintRecord.cs new file mode 100644 index 0000000..1a5ca46 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/BaseGlyphPaintRecord.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Represents a record in the COLR v1 BaseGlyphList that associates a glyph ID with its root paint table offset. + /// + /// + internal readonly struct BaseGlyphPaintRecord + { + /// + /// Initializes a new instance of the struct. + /// + /// The glyph ID. + /// The offset to the root paint table for this glyph, relative to the beginning of the COLR table. + public BaseGlyphPaintRecord(ushort glyphId, uint paintOffset) + { + this.GlyphId = glyphId; + this.PaintOffset = paintOffset; + } + + /// + /// Gets the glyph ID. + /// + public ushort GlyphId { get; } + + /// + /// Gets the offset to the root paint table for this glyph, relative to the beginning of the COLR table. + /// + public uint PaintOffset { get; } + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/BaseGlyphRecord.cs b/SixLabors.Fonts/Tables/General/Colr/BaseGlyphRecord.cs new file mode 100644 index 0000000..8a8516b --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/BaseGlyphRecord.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Represents a COLR v0 BaseGlyph record that maps a glyph ID to a range of layer records. + /// + /// + internal readonly struct BaseGlyphRecord + { + /// + /// Initializes a new instance of the struct. + /// + /// The glyph ID of the base glyph. + /// The index of the first layer record for this glyph. + /// The number of layer records for this glyph. + public BaseGlyphRecord(ushort glyphId, ushort firstLayerIndex, ushort layerCount) + { + this.GlyphId = glyphId; + this.FirstLayerIndex = firstLayerIndex; + this.LayerCount = layerCount; + } + + /// + /// Gets the glyph ID of the base glyph. + /// + public ushort GlyphId { get; } + + /// + /// Gets the index of the first layer record for this glyph in the layer records array. + /// + public ushort FirstLayerIndex { get; } + + /// + /// Gets the number of contiguous layer records for this glyph. + /// + public ushort LayerCount { get; } + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/ClipBox.cs b/SixLabors.Fonts/Tables/General/Colr/ClipBox.cs new file mode 100644 index 0000000..ab450a4 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/ClipBox.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Abstract base class for COLR v1 ClipBox subtables, which define bounding boxes for clipping paint operations. + /// Format-dispatched into (static) and (variable). + /// + /// + internal abstract class ClipBox + { + /// + /// Gets the bounds of the clip box, optionally applying variation deltas. + /// + /// The COLR table used for resolving variation deltas. + /// The glyph variation processor, or for non-variable fonts. + /// The resolved bounding box. + public abstract Bounds GetBounds(ColrTable colr, GlyphVariationProcessor? processor); + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/ClipBoxFormat1.cs b/SixLabors.Fonts/Tables/General/Colr/ClipBoxFormat1.cs new file mode 100644 index 0000000..d048c47 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/ClipBoxFormat1.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Represents a COLR v1 ClipBox format 1 subtable with static (non-variable) int16 bounding box edges. + /// + /// + internal sealed class ClipBoxFormat1 : ClipBox + { + /// + /// The minimum x-coordinate of the clip box. + /// + private readonly short xMin; + + /// + /// The minimum y-coordinate of the clip box. + /// + private readonly short yMin; + + /// + /// The maximum x-coordinate of the clip box. + /// + private readonly short xMax; + + /// + /// The maximum y-coordinate of the clip box. + /// + private readonly short yMax; + + /// + /// Initializes a new instance of the class. + /// + /// The minimum x-coordinate. + /// The minimum y-coordinate. + /// The maximum x-coordinate. + /// The maximum y-coordinate. + public ClipBoxFormat1(short xMin, short yMin, short xMax, short yMax) + { + this.xMin = xMin; + this.yMin = yMin; + this.xMax = xMax; + this.yMax = yMax; + } + + /// + public override Bounds GetBounds(ColrTable colr, GlyphVariationProcessor? processor) + => new(this.xMin, this.yMin, this.xMax, this.yMax); + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/ClipBoxFormat2.cs b/SixLabors.Fonts/Tables/General/Colr/ClipBoxFormat2.cs new file mode 100644 index 0000000..d2bb062 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/ClipBoxFormat2.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Represents a COLR v1 ClipBox format 2 subtable with variation-aware int16 bounding box edges. + /// Each edge value is adjusted by a delta resolved from the ItemVariationStore. + /// + /// + internal sealed class ClipBoxFormat2 : ClipBox + { + /// + /// The minimum x-coordinate of the clip box. + /// + private readonly short xMin; + + /// + /// The minimum y-coordinate of the clip box. + /// + private readonly short yMin; + + /// + /// The maximum x-coordinate of the clip box. + /// + private readonly short xMax; + + /// + /// The maximum y-coordinate of the clip box. + /// + private readonly short yMax; + + /// + /// The base index into the ItemVariationStore delta sets for the four edge values. + /// + private readonly uint varIndexBase; + + /// + /// Initializes a new instance of the class. + /// + /// The minimum x-coordinate. + /// The minimum y-coordinate. + /// The maximum x-coordinate. + /// The maximum y-coordinate. + /// The base index into the ItemVariationStore delta sets. + public ClipBoxFormat2(short xMin, short yMin, short xMax, short yMax, uint varIndexBase) + { + this.xMin = xMin; + this.yMin = yMin; + this.xMax = xMax; + this.yMax = yMax; + this.varIndexBase = varIndexBase; + } + + /// + public override Bounds GetBounds(ColrTable colr, GlyphVariationProcessor? processor) + { + float dx0 = colr.ResolveDelta(processor, this.varIndexBase + 0u); + float dy0 = colr.ResolveDelta(processor, this.varIndexBase + 1u); + float dx1 = colr.ResolveDelta(processor, this.varIndexBase + 2u); + float dy1 = colr.ResolveDelta(processor, this.varIndexBase + 3u); + + float xMin = this.xMin + dx0; + float yMin = this.yMin + dy0; + float xMax = this.xMax + dx1; + float yMax = this.yMax + dy1; + + return new Bounds(xMin, yMin, xMax, yMax); + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/ClipList.cs b/SixLabors.Fonts/Tables/General/Colr/ClipList.cs new file mode 100644 index 0000000..4b1d5b1 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/ClipList.cs @@ -0,0 +1,155 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.IO; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Represents the COLR v1 ClipList table, which maps ranges of glyph IDs to clip boxes + /// that constrain paint operations. + /// + /// + internal sealed class ClipList + { + /// + /// Initializes a new instance of the class. + /// + /// The array of clip records mapping glyph ID ranges to clip box offsets. + /// The array of resolved clip boxes, one per record. Null entries indicate an unknown format. + public ClipList(ClipRecord[] records, ClipBox?[] boxes) + { + this.Records = records; + this.Boxes = boxes; + } + + /// + /// Gets the array of clip records, sorted by start glyph ID. + /// + public ClipRecord[] Records { get; } + + /// + /// Gets the array of resolved clip boxes, one per record. + /// A entry indicates a clip box with an unknown format. + /// + public ClipBox?[] Boxes { get; } + + /// + /// Gets the number of clip records. + /// + public int Count => this.Records.Length; + + /// + /// Loads a from the given reader at the specified offset. + /// + /// The binary reader positioned within the COLR table. + /// The offset from the beginning of the COLR table to the ClipList. + /// The loaded , or if the offset is zero. + public static ClipList? Load(BigEndianBinaryReader reader, long offset) + { + if (offset == 0) + { + return null; + } + + reader.Seek(offset, SeekOrigin.Begin); + + _ = reader.ReadByte(); // Version. Always 1. + uint count = reader.ReadUInt32(); + + ClipRecord[] records = new ClipRecord[count]; + for (int i = 0; i < count; i++) + { + ushort start = reader.ReadUInt16(); + ushort end = reader.ReadUInt16(); + uint boxOffset = reader.ReadOffset24(); + records[i] = new ClipRecord(start, end, boxOffset); + } + + // TODO: Should this be nullable? + ClipBox?[] boxes = new ClipBox?[count]; + for (int i = 0; i < count; i++) + { + uint boxOffset = records[i].ClipBoxOffset; + reader.Seek(offset + boxOffset, SeekOrigin.Begin); + + byte format = reader.ReadByte(); + short xMin = reader.ReadFWORD(); + short yMin = reader.ReadFWORD(); + short xMax = reader.ReadFWORD(); + short yMax = reader.ReadFWORD(); + + switch (format) + { + case 1: + boxes[i] = new ClipBoxFormat1(xMin, yMin, xMax, yMax); + break; + + case 2: + uint varIndexBase = reader.ReadUInt32(); + boxes[i] = new ClipBoxFormat2(xMin, yMin, xMax, yMax, varIndexBase); + break; + + default: + boxes[i] = null; // Unknown format + break; + } + } + + return new ClipList(records, boxes); + } + + /// + /// Attempts to retrieve the clip box bounds for the specified glyph ID using a binary search + /// over the sorted clip records. + /// + /// The glyph ID to look up. + /// The COLR table used for resolving variation deltas. + /// The glyph variation processor, or for non-variable fonts. + /// + /// When this method returns, contains the clip box bounds if found; otherwise, . + /// + /// if a clip box was found for the glyph; otherwise, . + public bool TryGetClipBox( + ushort glyphId, + ColrTable colr, + GlyphVariationProcessor? processor, + [NotNullWhen(true)] out Bounds? bounds) + { + int lo = 0; + int hi = this.Records.Length - 1; + + while (lo <= hi) + { + int mid = (lo + hi) >> 1; + ClipRecord rec = this.Records[mid]; + + if (glyphId < rec.StartGlyphId) + { + hi = mid - 1; + continue; + } + + if (glyphId > rec.EndGlyphId) + { + lo = mid + 1; + continue; + } + + ClipBox? box = this.Boxes[mid]; + if (box is null) + { + bounds = null; + return false; + } + + bounds = box.GetBounds(colr, processor); + return true; + } + + bounds = null; + return false; + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/ClipRecord.cs b/SixLabors.Fonts/Tables/General/Colr/ClipRecord.cs new file mode 100644 index 0000000..aafaf15 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/ClipRecord.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Represents a COLR v1 ClipRecord that defines a clip region for a range of glyph IDs. + /// + /// + internal readonly struct ClipRecord + { + /// + /// Initializes a new instance of the struct. + /// + /// The first glyph ID in the range covered by this clip record. + /// The last glyph ID in the range covered by this clip record. + /// The offset from the start of the COLR table to the ClipBox subtable. + public ClipRecord(ushort startGlyphId, ushort endGlyphId, uint clipBoxOffset) + { + this.StartGlyphId = startGlyphId; + this.EndGlyphId = endGlyphId; + this.ClipBoxOffset = clipBoxOffset; + } + + /// + /// Gets the first glyph ID in the range covered by this clip record. + /// + public ushort StartGlyphId { get; } + + /// + /// Gets the last glyph ID in the range covered by this clip record. + /// + public ushort EndGlyphId { get; } + + /// + /// Gets the offset from the start of the COLR table to a ClipBox subtable (Format 1 or 2) defining the clip region. + /// + public uint ClipBoxOffset { get; } + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/ColorLine.cs b/SixLabors.Fonts/Tables/General/Colr/ColorLine.cs new file mode 100644 index 0000000..d2ec61f --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/ColorLine.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Represents a COLR v1 ColorLine, which defines a sequence of color stops and an extend mode for gradient paints. + /// + /// + internal sealed class ColorLine + { + /// + /// Initializes a new instance of the class. + /// + /// The extend mode that determines how the gradient behaves outside the defined stop range. + /// The array of color stops defining the gradient. + public ColorLine(Extend extend, ColorStop[] stops) + { + this.Extend = extend; + this.Stops = stops; + } + + /// + /// Gets the extend mode that determines how the gradient behaves outside the defined stop range. + /// + public Extend Extend { get; } + + /// + /// Gets the array of color stops defining the gradient. + /// + public ColorStop[] Stops { get; } + + /// + /// Gets the number of color stops. + /// + public int Count => this.Stops.Length; + + /// + /// Loads a from the given reader at the current position. + /// + /// The binary reader. + /// The loaded . + public static ColorLine Load(BigEndianBinaryReader reader) + { + Extend extend = reader.ReadByte(); + ushort numStops = reader.ReadUInt16(); + + ColorStop[] stops = new ColorStop[numStops]; + for (int i = 0; i < numStops; i++) + { + stops[i] = ColorStop.Load(reader); + } + + return new ColorLine(extend, stops); + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/ColorStop.cs b/SixLabors.Fonts/Tables/General/Colr/ColorStop.cs new file mode 100644 index 0000000..eb08114 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/ColorStop.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Represents a COLR v1 color stop within a , defining a position, palette color, and alpha value. + /// + /// + internal readonly struct ColorStop + { + /// + /// Initializes a new instance of the struct. + /// + /// The position of this color stop along the gradient, as an F2DOT14 value. + /// The index into the CPAL palette for this stop's color. + /// The alpha value for this stop, as an F2DOT14 value. + public ColorStop(float stopOffset, ushort paletteIndex, float alpha) + { + this.StopOffset = stopOffset; + this.PaletteIndex = paletteIndex; + this.Alpha = alpha; + } + + /// + /// Gets the position of this color stop along the gradient, as an F2DOT14 value. + /// + public float StopOffset { get; } + + /// + /// Gets the index into the CPAL palette for this stop's color. + /// + public ushort PaletteIndex { get; } + + /// + /// Gets the alpha multiplier for this stop, as an F2DOT14 value. + /// + public float Alpha { get; } + + /// + /// Loads a from the given reader at the current position. + /// + /// The binary reader. + /// The loaded . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ColorStop Load(BigEndianBinaryReader reader) + => new(reader.ReadF2Dot14(), reader.ReadUInt16(), reader.ReadF2Dot14()); + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/ColrCompositeMode.cs b/SixLabors.Fonts/Tables/General/Colr/ColrCompositeMode.cs new file mode 100644 index 0000000..eb1f4f0 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/ColrCompositeMode.cs @@ -0,0 +1,152 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Defines the composite (blending) modes used by COLR v1 PaintComposite (format 32) and PaintVarComposite (format 33). + /// Includes Porter-Duff compositing operators and separable color blend modes. + /// + /// + internal enum ColrCompositeMode : byte + { + /// + /// Porter-Duff Clear: no output. + /// + Clear = 0, + + /// + /// Porter-Duff Src: source only. + /// + Src = 1, + + /// + /// Porter-Duff Dest: destination only. + /// + Dst = 2, + + /// + /// Porter-Duff Src Over: source over destination. + /// + SrcOver = 3, + + /// + /// Porter-Duff Dest Over: destination over source. + /// + DstOver = 4, + + /// + /// Porter-Duff Src In: source where destination exists. + /// + SrcIn = 5, + + /// + /// Porter-Duff Dest In: destination where source exists. + /// + DstIn = 6, + + /// + /// Porter-Duff Src Out: source where destination does not exist. + /// + SrcOut = 7, + + /// + /// Porter-Duff Dest Out: destination where source does not exist. + /// + DstOut = 8, + + /// + /// Porter-Duff Src Atop: source atop destination. + /// + SrcAtop = 9, + + /// + /// Porter-Duff Dest Atop: destination atop source. + /// + DstAtop = 10, + + /// + /// Porter-Duff Xor: exclusive OR of source and destination. + /// + Xor = 11, + + /// + /// Porter-Duff Plus: additive blending. + /// + Plus = 12, + + /// + /// Screen blend mode. + /// + Screen = 13, + + /// + /// Overlay blend mode. + /// + Overlay = 14, + + /// + /// Darken blend mode: selects the darker of source and destination. + /// + Darken = 15, + + /// + /// Lighten blend mode: selects the lighter of source and destination. + /// + Lighten = 16, + + /// + /// Color dodge blend mode. + /// + ColorDodge = 17, + + /// + /// Color burn blend mode. + /// + ColorBurn = 18, + + /// + /// Hard light blend mode. + /// + HardLight = 19, + + /// + /// Soft light blend mode. + /// + SoftLight = 20, + + /// + /// Difference blend mode. + /// + Difference = 21, + + /// + /// Exclusion blend mode. + /// + Exclusion = 22, + + /// + /// Multiply blend mode. + /// + Multiply = 23, + + /// + /// Hue blend mode: applies the hue of the source to the destination. + /// + Hue = 24, + + /// + /// Saturation blend mode: applies the saturation of the source to the destination. + /// + Saturation = 25, + + /// + /// Color blend mode: applies the hue and saturation of the source to the destination. + /// + Color = 26, + + /// + /// Luminosity blend mode: applies the luminosity of the source to the destination. + /// + Luminosity = 27 + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/ColrGlyphSourceBase.cs b/SixLabors.Fonts/Tables/General/Colr/ColrGlyphSourceBase.cs new file mode 100644 index 0000000..d5ea2b7 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/ColrGlyphSourceBase.cs @@ -0,0 +1,447 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; +using SixLabors.Fonts.Tables.TrueType.Glyphs; + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// A base class for COLR glyph sources. + /// + internal abstract class ColrGlyphSourceBase : IPaintedGlyphSource + { + /// + /// Initializes a new instance of the class. + /// + /// The COLR table. + /// The CPAL table, or null if not present. + /// Delegate that loads a glyph outline for the given glyph id. + public ColrGlyphSourceBase(ColrTable colr, CpalTable? cpal, Func glyphLoader) + { + this.Colr = colr; + this.Cpal = cpal; + this.GlyphLoader = glyphLoader; + } + + /// + /// Gets the COLR table. + /// + protected ColrTable Colr { get; } + + /// + /// Gets the CPAL table, or null if not present. + /// + protected CpalTable? Cpal { get; } + + /// + /// Gets the glyph loader delegate. + /// + protected Func GlyphLoader { get; } + + /// + public abstract bool TryGetPaintedGlyph(ushort glyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas); + + /// + /// Recursively flattens a COLR paint graph: + /// - Wrapper nodes pre-multiply their matrix into and recurse to the child. + /// - Composite emits backdrop subtree first (inherits ), + /// then source subtree with currentCompositeMode = node.CompositeMode. + /// - Leaf nodes emit concrete Rendering.Paint with Transform = accum and CompositeMode = currentBlend ?? default. + /// Colors and stop offsets are passed through; no Y-flip applied here. + /// + /// The COLR paint node. + /// The affine matrix in document space. + /// The active composite mode to apply to leaf paints, or null for default. + /// Optional CPAL palette for color resolution. + /// The COLR table for variation delta resolution. + /// The glyph variation processor, or null for non-variable fonts. + /// Collector for emitted leaf paints. + protected static void FlattenPaint( + Paint node, + Matrix3x2 transform, + CompositeMode mode, + CpalTable? cpal, + ColrTable colr, + GlyphVariationProcessor? processor, + List outLeaves) + { + // The input node will only be a paintable leaf here, as upstream resolution + // should have eliminated glyph/colr-glyph nodes and flattened composites. + switch (node) + { + case PaintSolid ps: + { + if (ps.PaletteIndex == 0xFFFF) + { + // "Use foreground" => represent as a SolidPaint with fully transparent color; + // renderer can substitute foreground if needed. + outLeaves.Add(new SolidPaint + { + Color = new GlyphColor(0, 0, 0, 0), + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + GlyphColor color = ResolveColor(cpal, ps.PaletteIndex, ps.Alpha); + outLeaves.Add(new SolidPaint + { + Color = color, + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + case PaintVarSolid pvs: + { + float alpha = pvs.Alpha + colr.ResolveDelta(processor, pvs.VarIndexBase + 0u); + + if (pvs.PaletteIndex == 0xFFFF) + { + outLeaves.Add(new SolidPaint + { + Color = new GlyphColor(0, 0, 0, 0), + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + GlyphColor color = ResolveColor(cpal, pvs.PaletteIndex, alpha); + outLeaves.Add(new SolidPaint + { + Color = color, + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + case PaintLinearGradient pl: + { + GradientStop[] stops = ResolveStops(pl.ColorLine, cpal); + outLeaves.Add(new LinearGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + P0 = new Vector2(pl.X0, pl.Y0), + P1 = new Vector2(pl.X1, pl.Y1), + P2 = new Vector2(pl.X2, pl.Y2), + Spread = MapSpread(pl.ColorLine.Extend), + Stops = stops, + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + case PaintVarLinearGradient vpl: + { + uint vib = vpl.VarIndexBase; + GradientStop[] stops = ResolveStops(vpl.ColorLine, cpal, colr, processor); + outLeaves.Add(new LinearGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + P0 = new Vector2(vpl.X0 + colr.ResolveDelta(processor, vib + 0u), vpl.Y0 + colr.ResolveDelta(processor, vib + 1u)), + P1 = new Vector2(vpl.X1 + colr.ResolveDelta(processor, vib + 2u), vpl.Y1 + colr.ResolveDelta(processor, vib + 3u)), + P2 = new Vector2(vpl.X2 + colr.ResolveDelta(processor, vib + 4u), vpl.Y2 + colr.ResolveDelta(processor, vib + 5u)), + Spread = MapSpread(vpl.ColorLine.Extend), + Stops = stops, + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + case PaintRadialGradient pr: + { + GradientStop[] stops = ResolveStops(pr.ColorLine, cpal); + outLeaves.Add(new RadialGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + Center0 = new Vector2(pr.X0, pr.Y0), + Radius0 = pr.Radius0, + Center1 = new Vector2(pr.X1, pr.Y1), + Radius1 = pr.Radius1, + Spread = MapSpread(pr.ColorLine.Extend), + Stops = stops, + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + case PaintVarRadialGradient vpr: + { + uint vib = vpr.VarIndexBase; + GradientStop[] stops = ResolveStops(vpr.ColorLine, cpal, colr, processor); + outLeaves.Add(new RadialGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + Center0 = new Vector2(vpr.X0 + colr.ResolveDelta(processor, vib + 0u), vpr.Y0 + colr.ResolveDelta(processor, vib + 1u)), + Radius0 = (ushort)(vpr.Radius0 + colr.ResolveDelta(processor, vib + 2u)), + Center1 = new Vector2(vpr.X1 + colr.ResolveDelta(processor, vib + 3u), vpr.Y1 + colr.ResolveDelta(processor, vib + 4u)), + Radius1 = (ushort)(vpr.Radius1 + colr.ResolveDelta(processor, vib + 5u)), + Spread = MapSpread(vpr.ColorLine.Extend), + Stops = stops, + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + case PaintSweepGradient sw: + { + GradientStop[] stops = ResolveStops(sw.ColorLine, cpal); + outLeaves.Add(new SweepGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + Center = new Vector2(sw.CenterX, sw.CenterY), + + // Spec says: add 1.0 and multiply by 180 to retrieve counter-clockwise degrees. + StartAngle = (sw.StartAngle + 1F) * 180F, + EndAngle = (sw.EndAngle + 1F) * 180F, + Spread = MapSpread(sw.ColorLine.Extend), + Stops = stops, + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + case PaintVarSweepGradient vsw: + { + uint vib = vsw.VarIndexBase; + GradientStop[] stops = ResolveStops(vsw.ColorLine, cpal, colr, processor); + float startAngle = vsw.StartAngle + colr.ResolveDelta(processor, vib + 2u); + float endAngle = vsw.EndAngle + colr.ResolveDelta(processor, vib + 3u); + outLeaves.Add(new SweepGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + Center = new Vector2(vsw.CenterX + colr.ResolveDelta(processor, vib + 0u), vsw.CenterY + colr.ResolveDelta(processor, vib + 1u)), + StartAngle = (startAngle + 1F) * 180F, + EndAngle = (endAngle + 1F) * 180F, + Spread = MapSpread(vsw.ColorLine.Extend), + Stops = stops, + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + default: + return; + } + } + + /// + /// Converts a glyph vector into a sequence of path commands. + /// + /// The glyph vector. + protected static List BuildPath(GlyphVector gv) + { + IList points = gv.ControlPoints; + IReadOnlyList ends = gv.EndPoints; + + List cmds = new(points.Count + ends.Count); + + int endOfContour = -1; + + for (int ci = 0; ci < ends.Count; ci++) + { + int startOfContour = endOfContour + 1; + endOfContour = ends[ci]; + + if (endOfContour < startOfContour) + { + continue; + } + + int length = endOfContour - startOfContour + 1; + if (length == 0) + { + continue; + } + + // Choose initial MoveTo: last on-curve, else first on-curve, else midpoint(last, first). + ControlPoint first = points[startOfContour]; + ControlPoint last = points[endOfContour]; + + Vector2 moveTo = last.OnCurve ? last.Point + : first.OnCurve ? first.Point + : Mid(last.Point, first.Point); + + cmds.Add(PathCommand.MoveTo(moveTo)); + + // Ring traversal over input points. + Vector2 curr = last.Point; + Vector2 next = first.Point; + + for (int p = 0; p < length; p++) + { + Vector2 prev = curr; + curr = next; + + int currentIndex = startOfContour + p; + int nextIndex = startOfContour + ((p + 1) % length); + int prevIndex = startOfContour + ((length + p - 1) % length); + + next = points[nextIndex].Point; + + bool currOn = points[currentIndex].OnCurve; + bool prevOn = points[prevIndex].OnCurve; + bool nextOn = points[nextIndex].OnCurve; + + if (currOn) + { + // Emit line to the current on-curve point unconditionally. + cmds.Add(PathCommand.LineTo(curr)); + continue; + } + + // Off-curve: insert implicit on-curve midpoints. + Vector2 prev2 = prevOn ? prev : Mid(curr, prev); + Vector2 next2 = nextOn ? next : Mid(curr, next); + + if (!prevOn) + { + // Conditional line when previous input point was off-curve. + cmds.Add(PathCommand.LineTo(prev2)); + } + + // Metrics emits a LineTo(prev2) immediately before the quadratic as well. + cmds.Add(PathCommand.LineTo(prev2)); + + // Quadratic segment with control at current off-curve and endpoint at next2. + cmds.Add(PathCommand.QuadraticTo(curr, next2)); + } + + cmds.Add(PathCommand.Close()); + } + + return cmds; + } + + /// + /// Maps COLR Extend to renderer SpreadMethod. + /// + /// The COLR extend mode. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static SpreadMethod MapSpread(Extend extend) + => extend switch + { + Extend.Pad => SpreadMethod.Pad, + Extend.Repeat => SpreadMethod.Repeat, + Extend.Reflect => SpreadMethod.Reflect, + _ => SpreadMethod.Pad + }; + + /// + /// Resolves a color line into concrete gradient stops. Offsets are clamped to [0,1]. + /// 0xFFFF palette indices are treated as transparent here (foreground color handled by text color elsewhere). + /// + /// The color line. + /// The CPAL table, or null if not present. + /// The resolved gradient stops. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static GradientStop[] ResolveStops(ColorLine line, CpalTable? cpal) + { + ColorStop[] src = line.Stops; + GradientStop[] stops = new GradientStop[src.Length]; + + for (int i = 0; i < src.Length; i++) + { + ref readonly ColorStop s = ref src[i]; + + GlyphColor c = s.PaletteIndex == 0xFFFF + ? new GlyphColor(0, 0, 0, 0) // transparent placeholder; renderer can blend with foreground + : ResolveColor(cpal, s.PaletteIndex, s.Alpha); + + float offset = Math.Clamp(s.StopOffset, 0F, 1F); + + stops[i] = new GradientStop(offset, c); + } + + return stops; + } + + /// + /// Resolves a variable color line into concrete gradient stops with variation deltas applied. + /// Offsets are clamped to [0,1]. + /// 0xFFFF palette indices are treated as transparent here (foreground color handled by text color elsewhere). + /// + /// The variable color line. + /// The CPAL table, or null if not present. + /// The COLR table for delta resolution. + /// The glyph variation processor, or null. + /// The resolved gradient stops. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static GradientStop[] ResolveStops(VarColorLine line, CpalTable? cpal, ColrTable colr, GlyphVariationProcessor? processor) + { + VarColorStop[] src = line.Stops; + GradientStop[] stops = new GradientStop[src.Length]; + + for (int i = 0; i < src.Length; i++) + { + ref readonly VarColorStop s = ref src[i]; + + // Per spec: VarColorStop has varIndexBase with offsets +0 = stopOffset, +1 = alpha. + float stopOffset = s.StopOffset + colr.ResolveDelta(processor, s.VarIndexBase + 0u); + float alpha = s.Alpha + colr.ResolveDelta(processor, s.VarIndexBase + 1u); + + GlyphColor c = s.PaletteIndex == 0xFFFF + ? new GlyphColor(0, 0, 0, 0) // transparent placeholder; renderer can blend with foreground + : ResolveColor(cpal, s.PaletteIndex, alpha); + + float offset = Math.Clamp(stopOffset, 0F, 1F); + + stops[i] = new GradientStop(offset, c); + } + + return stops; + } + + /// + /// Resolves a CPAL palette entry with an alpha multiplier. + /// + /// The CPAL table, or null if not present. + /// The palette entry index. + /// The alpha multiplier. + /// The resolved color. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static GlyphColor ResolveColor(CpalTable? cpal, int paletteEntryIndex, float alphaMul) + { + // Palette index 0 selection. If you later expose palette selection, thread it here. + GlyphColor baseColor = cpal is null ? new GlyphColor(0, 0, 0, 0) : cpal.GetGlyphColor(0, paletteEntryIndex); + + byte a = (byte)Math.Clamp((int)MathF.Round(baseColor.A * alphaMul), 0, 255); + return new GlyphColor(baseColor.R, baseColor.G, baseColor.B, a); + } + + /// + /// Calculates the midpoint between two vectors. + /// + /// The first vector to use in the midpoint calculation. + /// The second vector to use in the midpoint calculation. + /// A representing the point exactly halfway between the two input vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector2 Mid(Vector2 a, Vector2 b) + => (a + b) * .5F; + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/ColrTable.cs b/SixLabors.Fonts/Tables/General/Colr/ColrTable.cs new file mode 100644 index 0000000..e6a9f8f --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/ColrTable.cs @@ -0,0 +1,1611 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Represents the OpenType COLR table, which defines color glyph data for both v0 (layer-based) + /// and v1 (paint-based) color fonts. + /// + /// + internal class ColrTable : Table + { + /// + /// The table tag name "COLR". + /// + internal const string TableName = "COLR"; + + /// + /// The COLR v0 base glyph records mapping glyph IDs to layer ranges. + /// + private readonly BaseGlyphRecord[] glyphRecords; + + /// + /// The COLR v0 layer records defining color layers. + /// + private readonly LayerRecord[] layers; + + /// + /// The COLR v1 BaseGlyphList, or if not present. + /// + private readonly BaseGlyphList? baseGlyphList; + + /// + /// The COLR v1 LayerList, or if not present. + /// + private readonly LayerList? layerList; + + /// + /// The COLR v1 ClipList, or if not present. + /// + private readonly ClipList? clipList; + + /// + /// The ItemVariationStore for variable font data, or if not present. + /// + private readonly ItemVariationStore? itemVariationStore; + + /// + /// The DeltaSetIndexMap array for mapping variation indices, or if not present. + /// + private readonly DeltaSetIndexMap[]? deltaSetIndexMap; + + /// + /// Cache of resolved paint objects keyed by their COLR-relative offset. + /// + private readonly Dictionary? paintCache; + + /// + /// Initializes a new instance of the class for COLR v0 data only. + /// + /// The base glyph records. + /// The layer records. + public ColrTable( + BaseGlyphRecord[] glyphRecords, + LayerRecord[] layers) + : this(glyphRecords, layers, null, null, null, null, null, null, 0) + { + } + + /// + /// Initializes a new instance of the class with both v0 and optional v1 data. + /// + /// The COLR v0 base glyph records. + /// The COLR v0 layer records. + /// The COLR v1 base glyph list, or . + /// The COLR v1 layer list, or . + /// The COLR v1 clip list, or . + /// The ItemVariationStore for variable font data, or . + /// The DeltaSetIndexMap array, or . + /// The pre-loaded paint cache, or . + /// The COLR table version. + public ColrTable( + BaseGlyphRecord[] glyphRecords, + LayerRecord[] layers, + BaseGlyphList? baseGlyphList, + LayerList? layerList, + ClipList? clipList, + ItemVariationStore? itemVariationStore, + DeltaSetIndexMap[]? deltaSetIndexMap, + Dictionary? paintCache = null, + int version = 1) + { + this.glyphRecords = glyphRecords; + this.layers = layers; + this.baseGlyphList = baseGlyphList; + this.layerList = layerList; + this.clipList = clipList; + this.itemVariationStore = itemVariationStore; + this.deltaSetIndexMap = deltaSetIndexMap; + this.paintCache = paintCache; + this.Version = version; + } + + /// + /// Gets the COLR table version (0 or 1). + /// + public int Version { get; } + + /// + /// Resolves a variation delta for a given variable index using the COLR table's + /// own ItemVariationStore and optional DeltaSetIndexMap. + /// + /// The glyph variation processor (null for non-variable fonts). + /// The variable index (VarIndexBase + field offset). + /// The delta value, or 0 if no variation data is available. + internal float ResolveDelta(GlyphVariationProcessor? processor, uint varIdx) + { + if (processor is null || this.itemVariationStore is null) + { + return 0; + } + + int outer; + int inner; + if (this.deltaSetIndexMap is not null && varIdx < (uint)this.deltaSetIndexMap.Length) + { + DeltaSetIndexMap mapping = this.deltaSetIndexMap[varIdx]; + outer = mapping.OuterIndex; + inner = mapping.InnerIndex; + } + else + { + // Implicit mapping: upper 16 bits = outer, lower 16 bits = inner. + outer = (int)(varIdx >> 16); + inner = (int)(varIdx & 0xFFFF); + } + + return processor.Delta(this.itemVariationStore, outer, inner); + } + + /// + /// Loads the COLR table from the specified font reader. + /// + /// The font reader. + /// The loaded , or if the table is not present. + public static ColrTable? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + /// + /// Gets the COLR v0 layer records for the specified glyph. + /// + /// The glyph ID. + /// A span of layer records for the glyph, or an empty span if not found. + internal Span GetLayers(ushort glyph) + { + foreach (BaseGlyphRecord g in this.glyphRecords) + { + if (g.GlyphId == glyph) + { + return this.layers.AsSpan().Slice(g.FirstLayerIndex, g.LayerCount); + } + } + + return []; + } + + /// + /// Determines whether the specified glyph has an associated COLR v0 color glyph definition. + /// + /// The identifier of the glyph to check for a COLR v0 color glyph definition. + /// + /// if the specified glyph has a COLR v0 color glyph definition; otherwise, . + /// + public bool ContainsColorV0Glyph(ushort glyphId) + { + for (int i = 0; i < this.glyphRecords.Length; i++) + { + BaseGlyphRecord g = this.glyphRecords[i]; + if (g.GlyphId == glyphId) + { + return true; + } + } + + return false; + } + + /// + /// Determines whether the specified glyph has an associated COLR v1 color glyph definition. + /// + /// The identifier of the glyph to check for a COLR v1 color glyph definition. + /// + /// if the specified glyph has a COLR v1 color glyph definition; otherwise, . + /// + public bool ContainsColorV1Glyph(ushort glyphId) + { + if (this.baseGlyphList is null || this.layerList is null || this.paintCache is null) + { + return false; // No COLR v1 data + } + + return this.TryGetRootPaintOffset(glyphId, out uint _); + } + + /// + /// Attempts to retrieve the set of color layer records associated with the specified glyph. + /// + /// The glyph ID for which to retrieve color layer records. + /// + /// When this method returns, contains a span of structures + /// representing the color layers for the specified glyph, if found; otherwise, an empty span. + /// + /// + /// if color layer records are found for the specified glyph; otherwise, + /// . + /// + internal bool TryGetColrV0Layers(ushort glyph, out Span records) + { + for (int i = 0; i < this.glyphRecords.Length; i++) + { + BaseGlyphRecord g = this.glyphRecords[i]; + if (g.GlyphId == glyph) + { + records = this.layers.AsSpan().Slice(g.FirstLayerIndex, g.LayerCount); + return true; + } + } + + records = []; + return false; + } + + /// + /// Attempts to resolve and retrieve the list of color glyph layers for the specified glyph ID. + /// + /// The identifier of the glyph for which to resolve color layers. + /// The glyph variation processor, or null for non-variable fonts. + /// + /// When this method returns, contains a list of resolved glyph layers if the operation succeeds; otherwise, + /// . This parameter is passed uninitialized. + /// + /// if the color glyph layers were successfully resolved; otherwise, . + /// + internal bool TryGetColrV1Layers( + ushort glyphId, + GlyphVariationProcessor? processor, + [NotNullWhen(true)] out List? layers) + { + layers = null; + + if (this.baseGlyphList is null || this.layerList is null || this.paintCache is null) + { + return false; // No COLR v1 data + } + + // 1) Resolve root paint for the requested base glyph + if (!this.TryGetRootPaintOffset(glyphId, out uint rootOff) || rootOff == 0) + { + return false; + } + + if (!this.paintCache.TryGetValue(rootOff, out Paint? root) || root is null) + { + return false; + } + + // 2) Flatten paint graph to layers. Start with no current glyph id. + List acc = []; + this.FlattenPaintToLayers(root, null, Matrix3x2.Identity, Matrix3x2.Identity, false, CompositeMode.SrcOver, processor, acc); + + // 3) If nothing emitted, the graph did not bind any geometry (no PaintGlyph/ColrGlyph reached). + if (acc.Count == 0) + { + layers = null; + return false; + } + + layers = acc; + return true; + } + + /// + /// Recursively flattens a COLR v1 paint subtree into s. + /// A layer is emitted only when a leaf paint is reached under an active glyph-binding node: + /// + /// PaintGlyph sets the current glyph id to its GlyphId and recurses into its child paint. + /// PaintColrGlyph resolves that glyph's root paint, sets the current glyph id, and recurses. + /// Wrapper nodes (transform/translate/scale/rotate/skew, var forms) forward the current glyph id unchanged. + /// PaintComposite flattens both branches independently, forwarding the current glyph id to each. + /// Leaf paints (solid/linear/radial/sweep, var forms) emit a layer only if has a value. + /// + /// + /// The paint node to flatten. + /// + /// The glyph id whose outline will receive the paint. Set by PaintGlyph/PaintColrGlyph. + /// + /// The accumulated transform to apply to the glyph's geometry. + /// The accumulated transform to apply to the paint. + /// Whether wrapper transforms should be applied to the paint (true) or to the glyph geometry (false). + /// Accumulated composite mode. + /// The glyph variation processor, or null for non-variable fonts. + /// Accumulator for resolved layers. + private void FlattenPaintToLayers( + Paint node, + ushort? currentGlyphId, + Matrix3x2 glyphTransform, + Matrix3x2 paintTransform, + bool transformPaint, + CompositeMode compositeMode, + GlyphVariationProcessor? processor, + List outLayers) + { + switch (node) + { + // --------------------------- + // Containers and indirections + // --------------------------- + case PaintColrLayers pcl: + { + // Iterates layer indices and flattens each addressed paint subtree. + // No glyph id is implied here; child subtrees must bind via PaintGlyph/ColrGlyph. + int first = (int)pcl.FirstLayerIndex; + int count = pcl.NumLayers; + ReadOnlySpan offs = this.GetLayerPaintOffsets(first, count); + + for (int i = 0; i < offs.Length; i++) + { + uint off = offs[i]; + if (off == 0) + { + continue; + } + + if (this.paintCache!.TryGetValue(off, out Paint? child) && child is not null) + { + this.FlattenPaintToLayers(child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers); + } + } + + return; + } + + case PaintColrGlyph pcg: + { + // Resolve the referenced glyph's root paint and recurse through its own bindings. + if (this.TryGetRootPaintOffset(pcg.GlyphId, out uint off) && off != 0 + && this.paintCache!.TryGetValue(off, out Paint? colrRoot) && colrRoot is not null) + { + this.FlattenPaintToLayers(colrRoot, null, glyphTransform, Matrix3x2.Identity, false, compositeMode, processor, outLayers); + } + + return; + } + + case PaintGlyph pg: + { + // Bind geometry to the specified glyph id and recurse into its child paint. + this.FlattenPaintToLayers(pg.Child, pg.GlyphId, glyphTransform, Matrix3x2.Identity, true, compositeMode, processor, outLayers); + return; + } + + // --------------------------- + // Wrappers: forward glyph id + // --------------------------- + case PaintTransform pt: + { + Affine2x3 a = pt.Transform; + Matrix3x2 next = new(a.Xx, a.Yx, a.Xy, a.Yy, a.Dx, a.Dy); + if (transformPaint) + { + paintTransform *= next; + } + else + { + glyphTransform *= next; + } + + this.FlattenPaintToLayers(pt.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers); + return; + } + + case PaintVarTransform pvt: + { + VarAffine2x3 a = pvt.Transform; + uint vib = a.VarIndexBase; + float xx = a.Xx + this.ResolveDelta(processor, vib + 0u); + float yx = a.Yx + this.ResolveDelta(processor, vib + 1u); + float xy = a.Xy + this.ResolveDelta(processor, vib + 2u); + float yy = a.Yy + this.ResolveDelta(processor, vib + 3u); + float dx = a.Dx + this.ResolveDelta(processor, vib + 4u); + float dy = a.Dy + this.ResolveDelta(processor, vib + 5u); + Matrix3x2 next = new(xx, yx, xy, yy, dx, dy); + if (transformPaint) + { + paintTransform *= next; + } + else + { + glyphTransform *= next; + } + + this.FlattenPaintToLayers(pvt.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers); + return; + } + + case PaintTranslate t: + { + Matrix3x2 next = Matrix3x2.CreateTranslation(t.Dx, t.Dy); + if (transformPaint) + { + paintTransform *= next; + } + else + { + glyphTransform *= next; + } + + this.FlattenPaintToLayers(t.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers); + return; + } + + case PaintVarTranslate vt: + { + float dx = vt.Dx + this.ResolveDelta(processor, vt.VarIndexBase + 0u); + float dy = vt.Dy + this.ResolveDelta(processor, vt.VarIndexBase + 1u); + Matrix3x2 next = Matrix3x2.CreateTranslation(dx, dy); + if (transformPaint) + { + paintTransform *= next; + } + else + { + glyphTransform *= next; + } + + this.FlattenPaintToLayers(vt.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers); + return; + } + + case PaintScale s: + { + Matrix3x2 next = BuildScale(s.ScaleX, s.ScaleY, s.AroundCenter, s.CenterX, s.CenterY); + if (transformPaint) + { + paintTransform *= next; + } + else + { + glyphTransform *= next; + } + + this.FlattenPaintToLayers(s.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers); + return; + } + + case PaintVarScale vs: + { + uint vib = vs.VarIndexBase; + float sx = vs.ScaleX + this.ResolveDelta(processor, vib + 0u); + float sy = vs.Uniform ? sx : vs.ScaleY + this.ResolveDelta(processor, vib + 1u); + int centerOffset = vs.Uniform ? 1 : 2; + float cx = vs.AroundCenter ? vs.CenterX + this.ResolveDelta(processor, vib + (uint)centerOffset) : 0; + float cy = vs.AroundCenter ? vs.CenterY + this.ResolveDelta(processor, vib + (uint)centerOffset + 1u) : 0; + Matrix3x2 next = BuildScale(sx, sy, vs.AroundCenter, cx, cy); + if (transformPaint) + { + paintTransform *= next; + } + else + { + glyphTransform *= next; + } + + this.FlattenPaintToLayers(vs.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers); + return; + } + + case PaintRotate r: + { + Matrix3x2 next = BuildRotate(r.Angle, r.AroundCenter, r.CenterX, r.CenterY); + if (transformPaint) + { + paintTransform *= next; + } + else + { + glyphTransform *= next; + } + + this.FlattenPaintToLayers(r.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers); + return; + } + + case PaintVarRotate vr: + { + uint vib = vr.VarIndexBase; + float angle = vr.Angle + this.ResolveDelta(processor, vib + 0u); + float cx = vr.AroundCenter ? vr.CenterX + this.ResolveDelta(processor, vib + 1u) : 0; + float cy = vr.AroundCenter ? vr.CenterY + this.ResolveDelta(processor, vib + 2u) : 0; + Matrix3x2 next = BuildRotate(angle, vr.AroundCenter, cx, cy); + if (transformPaint) + { + paintTransform *= next; + } + else + { + glyphTransform *= next; + } + + this.FlattenPaintToLayers(vr.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers); + return; + } + + case PaintSkew k: + { + Matrix3x2 next = BuildSkew(k.XSkew, k.YSkew, k.AroundCenter, k.CenterX, k.CenterY); + if (transformPaint) + { + paintTransform *= next; + } + else + { + glyphTransform *= next; + } + + this.FlattenPaintToLayers(k.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers); + return; + } + + case PaintVarSkew vk: + { + uint vib = vk.VarIndexBase; + float xSkew = vk.XSkew + this.ResolveDelta(processor, vib + 0u); + float ySkew = vk.YSkew + this.ResolveDelta(processor, vib + 1u); + float cx = vk.AroundCenter ? vk.CenterX + this.ResolveDelta(processor, vib + 2u) : 0; + float cy = vk.AroundCenter ? vk.CenterY + this.ResolveDelta(processor, vib + 3u) : 0; + Matrix3x2 next = BuildSkew(xSkew, ySkew, vk.AroundCenter, cx, cy); + if (transformPaint) + { + paintTransform *= next; + } + else + { + glyphTransform *= next; + } + + this.FlattenPaintToLayers(vk.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers); + return; + } + + case PaintComposite comp: + { + compositeMode = MapCompositeMode(comp.CompositeMode); + + // Backdrop first, then Source. Both inherit the current glyph id. + this.FlattenPaintToLayers(comp.Backdrop, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers); + this.FlattenPaintToLayers(comp.Source, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers); + return; + } + + // --------------------------- + // Leaves: emit only if bound + // --------------------------- + case PaintSolid: + case PaintVarSolid: + case PaintLinearGradient: + case PaintVarLinearGradient: + case PaintRadialGradient: + case PaintVarRadialGradient: + case PaintSweepGradient: + case PaintVarSweepGradient: + { + // Only emit if we have an active glyph id (i.e., we are inside a PaintGlyph/ColrGlyph branch). + if (currentGlyphId.HasValue) + { + _ = this.TryGetClipBox(currentGlyphId.Value, processor, out Bounds? clip); + outLayers.Add(new ResolvedGlyphLayer(currentGlyphId.Value, node, glyphTransform, paintTransform, compositeMode, clip)); + } + + return; + } + + default: + { + // Unknown or unsupported node: do not emit and do not stop traversal. + return; + } + } + } + + /// + /// Builds a scale matrix, optionally around a center. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Matrix3x2 BuildScale(float sx, float sy, bool aroundCenter, float cx, float cy) + { + if (!aroundCenter) + { + return Matrix3x2.CreateScale(sx, sy); + } + + return Matrix3x2.CreateScale(sx, sy, new Vector2(cx, cy)); + } + + /// + /// Builds a rotation matrix, optionally around a center. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Matrix3x2 BuildRotate(float angleColrUnits, bool aroundCenter, float cx, float cy) + { + // COLR: 1.0 == 180° => radians = angle * π + float radians = angleColrUnits * MathF.PI; + + if (!aroundCenter) + { + return Matrix3x2.CreateRotation(radians); + } + + return Matrix3x2.CreateRotation(radians, new Vector2(cx, cy)); + } + + /// + /// Builds a skew matrix, optionally around a center. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Matrix3x2 BuildSkew(float xSkew, float ySkew, bool aroundCenter, float cx, float cy) + { + // COLR: 1.0 == 180° => radians = angle * π + float rx = xSkew * MathF.PI; + float ry = ySkew * MathF.PI; + + if (!aroundCenter) + { + return Matrix3x2.CreateSkew(rx, ry); + } + + return Matrix3x2.CreateSkew(rx, ry, new Vector2(cx, cy)); + } + + /// + /// Maps a COLR composite mode to the internal . + /// + /// Returns when is null + /// or when the value is not recognized. + /// + /// + /// The optional COLR composite mode. + /// The mapped . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static CompositeMode MapCompositeMode(ColrCompositeMode? mode) + => mode switch + { + // Porter–Duff + ColrCompositeMode.Clear => CompositeMode.Clear, + ColrCompositeMode.Src => CompositeMode.Src, + ColrCompositeMode.Dst => CompositeMode.Dest, + ColrCompositeMode.SrcOver => CompositeMode.SrcOver, + ColrCompositeMode.DstOver => CompositeMode.DestOver, + ColrCompositeMode.SrcIn => CompositeMode.SrcIn, + ColrCompositeMode.DstIn => CompositeMode.DestIn, + ColrCompositeMode.SrcOut => CompositeMode.SrcOut, + ColrCompositeMode.DstOut => CompositeMode.DestOut, + ColrCompositeMode.SrcAtop => CompositeMode.SrcAtop, + ColrCompositeMode.DstAtop => CompositeMode.DestAtop, + ColrCompositeMode.Xor => CompositeMode.Xor, + ColrCompositeMode.Plus => CompositeMode.Plus, + + // Blend modes + ColrCompositeMode.Screen => CompositeMode.Screen, + ColrCompositeMode.Overlay => CompositeMode.Overlay, + ColrCompositeMode.Darken => CompositeMode.Darken, + ColrCompositeMode.Lighten => CompositeMode.Lighten, + ColrCompositeMode.ColorDodge => CompositeMode.ColorDodge, + ColrCompositeMode.ColorBurn => CompositeMode.ColorBurn, + ColrCompositeMode.HardLight => CompositeMode.HardLight, + ColrCompositeMode.SoftLight => CompositeMode.SoftLight, + ColrCompositeMode.Difference => CompositeMode.Difference, + ColrCompositeMode.Exclusion => CompositeMode.Exclusion, + ColrCompositeMode.Multiply => CompositeMode.Multiply, + ColrCompositeMode.Hue => CompositeMode.Hue, + ColrCompositeMode.Saturation => CompositeMode.Saturation, + ColrCompositeMode.Color => CompositeMode.Color, + ColrCompositeMode.Luminosity => CompositeMode.Luminosity, + _ => CompositeMode.SrcOver, + }; + + /// + /// Attempts to retrieve the paint table offset associated with the specified glyph ID. + /// + /// The glyph ID for which to look up the paint table offset. + /// + /// When this method returns, contains the paint table offset for the specified glyph ID, if found; otherwise, zero. + /// This parameter is passed uninitialized. + /// + /// + /// if the paint table offset was found for the specified glyph ID; otherwise, . + /// + private bool TryGetRootPaintOffset(ushort glyphId, out uint paintOffset) + { + if (this.baseGlyphList is null) + { + paintOffset = 0; + return false; + } + + ReadOnlySpan recs = this.baseGlyphList.Records; + int lo = 0, hi = recs.Length - 1; + + while (lo <= hi) + { + int mid = (lo + hi) >> 1; + ushort gid = recs[mid].GlyphId; + + if (glyphId == gid) + { + paintOffset = recs[mid].PaintOffset; + return true; + } + + if (glyphId < gid) + { + hi = mid - 1; + } + else + { + lo = mid + 1; + } + } + + paintOffset = 0; + return false; + } + + /// + /// Gets a span of paint offsets from the layer list starting at the specified index. + /// + /// The index of the first paint offset. + /// The number of paint offsets to retrieve. + /// A read-only span of paint offsets, or an empty span if the layer list is null or the range is invalid. + private ReadOnlySpan GetLayerPaintOffsets(int first, int count) + { + if (this.layerList is null || count <= 0) + { + return []; + } + + Span offsets = this.layerList.PaintOffsets.AsSpan(); + if ((uint)first >= (uint)offsets.Length) + { + return []; + } + + int len = Math.Min(count, offsets.Length - first); + return offsets.Slice(first, len); + } + + /// + /// Attempts to retrieve the clip box bounds for the specified glyph ID. + /// + /// The glyph ID. + /// The glyph variation processor, or for non-variable fonts. + /// When this method returns, contains the clip bounds if found; otherwise, . + /// if a clip box was found; otherwise, . + private bool TryGetClipBox(ushort glyphId, GlyphVariationProcessor? processor, out Bounds? bounds) + { + if (this.clipList is null) + { + bounds = default; + return false; + } + + return this.clipList.TryGetClipBox(glyphId, this, processor, out bounds); + } + + /// + /// Loads the COLR table from the specified binary reader. + /// + /// The big-endian binary reader positioned at the start of the COLR table. + /// The loaded . + public static ColrTable Load(BigEndianBinaryReader reader) + { + // HEADER + + // Type | Name | Description + // ----------|------------------------|---------------------------------------------------------------------------------------------------- + // uint16 | version | Table version number(starts at 0). + // uint16 | numBaseGlyphRecords | Number of Base Glyph Records. + // Offset32 | baseGlyphRecordsOffset | Offset(from beginning of COLR table) to Base Glyph records. + // Offset32 | layerRecordsOffset | Offset(from beginning of COLR table) to Layer Records. + // uint16 | numLayerRecords | Number of Layer Records. + ushort version = reader.ReadUInt16(); + ushort numBaseGlyphRecords = reader.ReadUInt16(); + uint baseGlyphRecordsOffset = reader.ReadOffset32(); + uint layerRecordsOffset = reader.ReadOffset32(); + ushort numLayerRecords = reader.ReadUInt16(); + + uint baseGlyphListOffset = 0; + uint layerListOffset = 0; + uint clipListOffset = 0; + uint varIndexMapOffset = 0; + uint itemVariationStoreOffset = 0; + + if (version == 1) + { + // | Type | Name | Description | + // |----------|--------------------------|-------------------------------------------------------------------------------| + // | uint16 | version | Table version number—set to 1. | + // | uint16 | numBaseGlyphRecords | Number of BaseGlyph records; may be 0 in a version 1 table. | + // | Offset32 | baseGlyphRecordsOffset | Offset to baseGlyphRecords array, from beginning of COLR table (may be NULL). | + // | Offset32 | layerRecordsOffset | Offset to layerRecords array, from beginning of COLR table (may be NULL). | + // | uint16 | numLayerRecords | Number of Layer records; may be 0 in a version 1 table. | + // | Offset32 | baseGlyphListOffset | Offset to BaseGlyphList table, from beginning of COLR table. | + // | Offset32 | layerListOffset | Offset to LayerList table, from beginning of COLR table (may be NULL). | + // | Offset32 | clipListOffset | Offset to ClipList table, from beginning of COLR table (may be NULL). | + // | Offset32 | varIndexMapOffset | Offset to DeltaSetIndexMap table, from beginning of COLR table (may be NULL). | + // | Offset32 | itemVariationStoreOffset | Offset to ItemVariationStore, from beginning of COLR table (may be NULL). | + baseGlyphListOffset = reader.ReadOffset32(); + layerListOffset = reader.ReadOffset32(); + clipListOffset = reader.ReadOffset32(); + varIndexMapOffset = reader.ReadOffset32(); + itemVariationStoreOffset = reader.ReadOffset32(); + } + + // v0: BaseGlyph and Layer records (optional in v1; may be zero) + BaseGlyphRecord[] glyphs = []; + if (numBaseGlyphRecords != 0 && baseGlyphRecordsOffset != 0) + { + glyphs = new BaseGlyphRecord[numBaseGlyphRecords]; + reader.Seek(baseGlyphRecordsOffset, SeekOrigin.Begin); + + for (int i = 0; i < numBaseGlyphRecords; i++) + { + ushort gi = reader.ReadUInt16(); + ushort idx = reader.ReadUInt16(); + ushort num = reader.ReadUInt16(); + glyphs[i] = new BaseGlyphRecord(gi, idx, num); + } + } + + LayerRecord[] layerRecs = []; + if (numLayerRecords != 0 && layerRecordsOffset != 0) + { + layerRecs = new LayerRecord[numLayerRecords]; + reader.Seek(layerRecordsOffset, SeekOrigin.Begin); + + for (int i = 0; i < numLayerRecords; i++) + { + ushort gi = reader.ReadUInt16(); + ushort pi = reader.ReadUInt16(); + layerRecs[i] = new LayerRecord(gi, pi); + } + } + + // v1: BaseGlyphList, LayerList, ClipList (nullable if not present) + BaseGlyphList? baseGlyphList = null; + LayerList? layerList = null; + ClipList? clipList = null; + Dictionary? paintCache = null; + + if (version == 1) + { + baseGlyphList = BaseGlyphList.Load(reader, baseGlyphListOffset); + layerList = LayerList.Load(reader, layerListOffset); + clipList = ClipList.Load(reader, clipListOffset); + + paintCache = LoadPaintRoots(reader, baseGlyphList, layerList); + } + + ItemVariationStore? itemVariationStore = itemVariationStoreOffset != 0 + ? ItemVariationStore.Load(reader, itemVariationStoreOffset) + : null; + + DeltaSetIndexMap[]? deltaSetIndexMap = varIndexMapOffset != 0 + ? DeltaSetIndexMap.Load(reader, varIndexMapOffset) + : null; + + return new ColrTable(glyphs, layerRecs, baseGlyphList, layerList, clipList, itemVariationStore, deltaSetIndexMap, paintCache, 1); + } + + /// + /// Eagerly loads and caches all paint objects referenced by the BaseGlyphList and LayerList. + /// + /// The binary reader. + /// The base glyph list, or . + /// The layer list, or . + /// A dictionary mapping paint offsets to their resolved paint objects. + private static Dictionary LoadPaintRoots( + BigEndianBinaryReader reader, + BaseGlyphList? baseGlyphList, + LayerList? layerList) + { + PaintCaches caches = new(); + + // 1) Root paints from BaseGlyphList + if (baseGlyphList is not null) + { + foreach (BaseGlyphPaintRecord rec in baseGlyphList.Records) + { + if (rec.PaintOffset != 0) + { + _ = LoadPaintAt(reader, rec.PaintOffset, layerList, caches); + } + } + } + + // 2) All paints referenced by LayerList (PaintColrLayers points into these) + if (layerList is not null) + { + foreach (uint offset in layerList.PaintOffsets) + { + if (offset != 0) + { + _ = LoadPaintAt(reader, offset, layerList, caches); + } + } + } + + return caches.PaintCache; + } + + /// + /// Loads a paint object from the specified offset, using the cache to avoid redundant reads. + /// Recursively loads child paints as needed. + /// + /// The binary reader. + /// The COLR-relative offset of the paint table. + /// The layer list for resolving PaintColrLayers references, or . + /// The shared caches for deduplicating loaded objects. + /// The loaded paint object. + private static Paint LoadPaintAt( + BigEndianBinaryReader reader, + uint paintOffset, + LayerList? layerList, + PaintCaches caches) + { + if (caches.PaintCache.TryGetValue(paintOffset, out Paint? p)) + { + return p; + } + + long restore = reader.BaseStream.Position; + reader.Seek(paintOffset, SeekOrigin.Begin); + + byte format = reader.ReadByte(); + Paint result; + + switch (format) + { + // 1: PaintColrLayers + case 1: + { + byte numLayers = reader.ReadByte(); + uint firstLayerIndex = reader.ReadUInt32(); + + result = new PaintColrLayers + { + Format = format, + NumLayers = numLayers, + FirstLayerIndex = firstLayerIndex + }; + + // Walk children immediately: + if (layerList is not null) + { + for (uint i = 0; i < numLayers; i++) + { + int idx = (int)(firstLayerIndex + i); + uint layerPaintOff = layerList.PaintOffsets[idx]; + if (layerPaintOff != 0) + { + _ = LoadPaintAt(reader, layerPaintOff, layerList, caches); + } + } + } + + break; + } + + // 2/3: PaintSolid / PaintVarSolid + case 2: + { + ushort paletteIndex = reader.ReadUInt16(); + float alpha = reader.ReadF2Dot14(); + result = new PaintSolid { Format = format, PaletteIndex = paletteIndex, Alpha = alpha }; + break; + } + + case 3: + { + ushort paletteIndex = reader.ReadUInt16(); + float alpha = reader.ReadF2Dot14(); + uint varBase = reader.ReadUInt32(); + result = new PaintVarSolid { Format = format, PaletteIndex = paletteIndex, Alpha = alpha, VarIndexBase = varBase }; + break; + } + + // 4/5: PaintLinearGradient / PaintVarLinearGradient + case 4: + { + uint colorLineOff = reader.ReadOffset24(); + ColorLine line = LoadColorLineAt(reader, paintOffset + colorLineOff, caches); + short x0 = reader.ReadFWORD(); + short y0 = reader.ReadFWORD(); + short x1 = reader.ReadFWORD(); + short y1 = reader.ReadFWORD(); + short x2 = reader.ReadFWORD(); + short y2 = reader.ReadFWORD(); + result = new PaintLinearGradient { Format = format, ColorLine = line, X0 = x0, Y0 = y0, X1 = x1, Y1 = y1, X2 = x2, Y2 = y2 }; + break; + } + + case 5: + { + uint colorLineOff = reader.ReadOffset24(); + VarColorLine line = LoadVarColorLineAt(reader, paintOffset + colorLineOff, caches); + short x0 = reader.ReadFWORD(); + short y0 = reader.ReadFWORD(); + short x1 = reader.ReadFWORD(); + short y1 = reader.ReadFWORD(); + short x2 = reader.ReadFWORD(); + short y2 = reader.ReadFWORD(); + uint varBase = reader.ReadUInt32(); + result = new PaintVarLinearGradient + { + Format = format, + ColorLine = line, + X0 = x0, + Y0 = y0, + X1 = x1, + Y1 = y1, + X2 = x2, + Y2 = y2, + VarIndexBase = varBase + }; + break; + } + + // 6/7: PaintRadialGradient / PaintVarRadialGradient + case 6: + { + uint colorLineOff = reader.ReadOffset24(); + ColorLine line = LoadColorLineAt(reader, paintOffset + colorLineOff, caches); + short x0 = reader.ReadFWORD(); + short y0 = reader.ReadFWORD(); + ushort r0 = reader.ReadUFWORD(); + short x1 = reader.ReadFWORD(); + short y1 = reader.ReadFWORD(); + ushort r1 = reader.ReadUFWORD(); + result = new PaintRadialGradient + { + Format = format, + ColorLine = line, + X0 = x0, + Y0 = y0, + Radius0 = r0, + X1 = x1, + Y1 = y1, + Radius1 = r1 + }; + break; + } + + case 7: + { + uint colorLineOff = reader.ReadOffset24(); + VarColorLine line = LoadVarColorLineAt(reader, paintOffset + colorLineOff, caches); + short x0 = reader.ReadFWORD(); + short y0 = reader.ReadFWORD(); + ushort r0 = reader.ReadUFWORD(); + short x1 = reader.ReadFWORD(); + short y1 = reader.ReadFWORD(); + ushort r1 = reader.ReadUFWORD(); + uint varBase = reader.ReadUInt32(); + result = new PaintVarRadialGradient + { + Format = format, + ColorLine = line, + X0 = x0, + Y0 = y0, + Radius0 = r0, + X1 = x1, + Y1 = y1, + Radius1 = r1, + VarIndexBase = varBase + }; + break; + } + + // 8/9: PaintSweepGradient / PaintVarSweepGradient + case 8: + { + uint colorLineOff = reader.ReadOffset24(); + ColorLine line = LoadColorLineAt(reader, paintOffset + colorLineOff, caches); + short cx = reader.ReadFWORD(); + short cy = reader.ReadFWORD(); + float start = reader.ReadF2Dot14(); + float end = reader.ReadF2Dot14(); + result = new PaintSweepGradient + { + Format = format, + ColorLine = line, + CenterX = cx, + CenterY = cy, + StartAngle = start, + EndAngle = end + }; + break; + } + + case 9: + { + uint colorLineOff = reader.ReadOffset24(); + VarColorLine line = LoadVarColorLineAt(reader, paintOffset + colorLineOff, caches); + short cx = reader.ReadFWORD(); + short cy = reader.ReadFWORD(); + float start = reader.ReadF2Dot14(); + float end = reader.ReadF2Dot14(); + uint varBase = reader.ReadUInt32(); + result = new PaintVarSweepGradient + { + Format = format, + ColorLine = line, + CenterX = cx, + CenterY = cy, + StartAngle = start, + EndAngle = end, + VarIndexBase = varBase + }; + break; + } + + // 10: PaintGlyph + case 10: + { + uint childOff = reader.ReadOffset24(); + ushort gid = reader.ReadUInt16(); + Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches); + result = new PaintGlyph { Format = format, Child = child, GlyphId = gid }; + break; + } + + // 11: PaintColrGlyph + case 11: + { + ushort gid = reader.ReadUInt16(); + result = new PaintColrGlyph { Format = format, GlyphId = gid }; + + // Note: resolution of gid->root paint happens elsewhere when you interpret. + break; + } + + // 12/13: PaintTransform / PaintVarTransform + case 12: + { + uint childOff = reader.ReadOffset24(); + uint transformOff = reader.ReadOffset24(); + + Affine2x3 m = ReadAffine2x3At(reader, paintOffset + transformOff, caches); + Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches); + result = new PaintTransform { Format = format, Child = child, Transform = m }; + break; + } + + case 13: + { + uint childOff = reader.ReadOffset24(); + uint transformOff = reader.ReadOffset24(); + + VarAffine2x3 vm = ReadVarAffine2x3At(reader, paintOffset + transformOff, caches); + Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches); + result = new PaintVarTransform { Format = format, Child = child, Transform = vm }; + break; + } + + // 14/15: PaintTranslate / PaintVarTranslate + case 14: + { + uint childOff = reader.ReadOffset24(); + short dx = reader.ReadFWORD(); + short dy = reader.ReadFWORD(); + Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches); + result = new PaintTranslate { Format = format, Child = child, Dx = dx, Dy = dy }; + break; + } + + case 15: + { + uint childOff = reader.ReadOffset24(); + short dx = reader.ReadFWORD(); + short dy = reader.ReadFWORD(); + uint varBase = reader.ReadUInt32(); + Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches); + result = new PaintVarTranslate { Format = format, Child = child, Dx = dx, Dy = dy, VarIndexBase = varBase }; + break; + } + + // 16/17/18/19/20/21/22/23: Scale variants + case 16: // PaintScale + case 17: // PaintVarScale + case 18: // PaintScaleAroundCenter + case 19: // PaintVarScaleAroundCenter + case 20: // PaintScaleUniform + case 21: // PaintVarScaleUniform + case 22: // PaintScaleUniformAroundCenter + case 23: // PaintVarScaleUniformAroundCenter + { + bool aroundCenter = format is 18 or 19 or 22 or 23; + bool uniform = format is 20 or 21 or 22 or 23; + bool isVar = (format % 2) == 1; + + uint childOff = reader.ReadOffset24(); + float sx = reader.ReadF2Dot14(); + float sy = uniform ? sx : reader.ReadF2Dot14(); + + short cx = 0, cy = 0; + if (aroundCenter) + { + cx = reader.ReadFWORD(); + cy = reader.ReadFWORD(); + } + + uint varBase = isVar ? reader.ReadUInt32() : 0; + Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches); + + if (isVar) + { + result = new PaintVarScale + { + Format = format, + Child = child, + ScaleX = sx, + ScaleY = sy, + CenterX = cx, + CenterY = cy, + AroundCenter = aroundCenter, + Uniform = uniform, + VarIndexBase = varBase + }; + } + else + { + result = new PaintScale + { + Format = format, + Child = child, + ScaleX = sx, + ScaleY = sy, + CenterX = cx, + CenterY = cy, + AroundCenter = aroundCenter, + Uniform = uniform + }; + } + + break; + } + + // 24/25/26/27: Rotate variants + case 24: // PaintRotate + case 25: // PaintVarRotate + case 26: // PaintRotateAroundCenter + case 27: // PaintVarRotateAroundCenter + { + bool aroundCenter = format is 26 or 27; + bool isVar = (format % 2) == 1; + + uint childOff = reader.ReadOffset24(); + float angle = reader.ReadF2Dot14(); + + short cx = 0, cy = 0; + if (aroundCenter) + { + cx = reader.ReadFWORD(); + cy = reader.ReadFWORD(); + } + + uint varBase = isVar ? reader.ReadUInt32() : 0; + Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches); + + if (isVar) + { + result = new PaintVarRotate + { + Format = format, + Child = child, + Angle = angle, + CenterX = cx, + CenterY = cy, + AroundCenter = aroundCenter, + VarIndexBase = varBase + }; + } + else + { + result = new PaintRotate + { + Format = format, + Child = child, + Angle = angle, + CenterX = cx, + CenterY = cy, + AroundCenter = aroundCenter + }; + } + + break; + } + + // 28/29/30/31: Skew variants + case 28: // PaintSkew + case 29: // PaintVarSkew + case 30: // PaintSkewAroundCenter + case 31: // PaintVarSkewAroundCenter + { + bool aroundCenter = format is 30 or 31; + bool isVar = (format % 2) == 1; + + uint childOff = reader.ReadOffset24(); + float xskew = reader.ReadF2Dot14(); + float yskew = reader.ReadF2Dot14(); + + short cx = 0, cy = 0; + if (aroundCenter) + { + cx = reader.ReadFWORD(); + cy = reader.ReadFWORD(); + } + + uint varBase = isVar ? reader.ReadUInt32() : 0; + Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches); + + if (isVar) + { + result = new PaintVarSkew + { + Format = format, + Child = child, + XSkew = xskew, + YSkew = yskew, + CenterX = cx, + CenterY = cy, + AroundCenter = aroundCenter, + VarIndexBase = varBase + }; + } + else + { + result = new PaintSkew + { + Format = format, + Child = child, + XSkew = xskew, + YSkew = yskew, + CenterX = cx, + CenterY = cy, + AroundCenter = aroundCenter + }; + } + + break; + } + + // 32: Composite + case 32: + { + uint srcOff = reader.ReadOffset24(); + ColrCompositeMode mode = reader.ReadByte(); + uint backOff = reader.ReadOffset24(); + + Paint src = LoadPaintAt(reader, paintOffset + srcOff, layerList, caches); + Paint back = LoadPaintAt(reader, paintOffset + backOff, layerList, caches); + result = new PaintComposite { Format = format, CompositeMode = mode, Source = src, Backdrop = back }; + break; + } + + default: + // Unknown format -> treat as no-op solid (or throw). We'll store a stub. + result = new PaintSolid { Format = format, PaletteIndex = 0, Alpha = 0 }; + break; + } + + caches.PaintCache[paintOffset] = result; + reader.BaseStream.Position = restore; + return result; + } + + /// + /// Loads a from the specified offset, using the cache to avoid redundant reads. + /// + /// The binary reader. + /// The COLR-relative offset of the color line. + /// The shared caches. + /// The loaded color line. + private static ColorLine LoadColorLineAt(BigEndianBinaryReader reader, uint offset, PaintCaches caches) + { + if (caches.ColorLineCache.TryGetValue(offset, out ColorLine? line)) + { + return line; + } + + long restore = reader.BaseStream.Position; + reader.Seek(offset, SeekOrigin.Begin); + + line = ColorLine.Load(reader); + caches.ColorLineCache[offset] = line; + + reader.BaseStream.Position = restore; + + return line; + } + + /// + /// Loads a from the specified offset, using the cache to avoid redundant reads. + /// + /// The binary reader. + /// The COLR-relative offset of the variable color line. + /// The shared caches. + /// The loaded variable color line. + private static VarColorLine LoadVarColorLineAt(BigEndianBinaryReader reader, uint offset, PaintCaches caches) + { + if (caches.VarColorLineCache.TryGetValue(offset, out VarColorLine? line)) + { + return line; + } + + long restore = reader.BaseStream.Position; + reader.Seek(offset, SeekOrigin.Begin); + + line = VarColorLine.Load(reader); + caches.VarColorLineCache[offset] = line; + + reader.BaseStream.Position = restore; + return line; + } + + /// + /// Reads an matrix from the specified offset, using the cache to avoid redundant reads. + /// Matrix values are stored as Fixed 16.16 numbers. + /// + /// The binary reader. + /// The COLR-relative offset of the affine matrix. + /// The shared caches. + /// The loaded affine matrix. + private static Affine2x3 ReadAffine2x3At(BigEndianBinaryReader reader, uint offset, PaintCaches caches) + { + if (caches.AffineCache.TryGetValue(offset, out Affine2x3 m)) + { + return m; + } + + long restore = reader.BaseStream.Position; + reader.Seek(offset, SeekOrigin.Begin); + + float xx = reader.ReadFixed(); + float yx = reader.ReadFixed(); + float xy = reader.ReadFixed(); + float yy = reader.ReadFixed(); + float dx = reader.ReadFixed(); + float dy = reader.ReadFixed(); + + m = new Affine2x3(xx, yx, xy, yy, dx, dy); + caches.AffineCache[offset] = m; + + reader.BaseStream.Position = restore; + return m; + } + + /// + /// Reads a matrix from the specified offset, using the cache to avoid redundant reads. + /// Matrix values are stored as Fixed 16.16 numbers with an appended variation index base. + /// + /// The binary reader. + /// The COLR-relative offset of the variable affine matrix. + /// The shared caches. + /// The loaded variable affine matrix. + private static VarAffine2x3 ReadVarAffine2x3At(BigEndianBinaryReader reader, uint offset, PaintCaches caches) + { + if (caches.VarAffineCache.TryGetValue(offset, out VarAffine2x3 m)) + { + return m; + } + + long restore = reader.BaseStream.Position; + reader.Seek(offset, SeekOrigin.Begin); + + float xx = reader.ReadFixed(); + float yx = reader.ReadFixed(); + float xy = reader.ReadFixed(); + float yy = reader.ReadFixed(); + float dx = reader.ReadFixed(); + float dy = reader.ReadFixed(); + uint varBase = reader.ReadUInt32(); + + m = new VarAffine2x3(xx, yx, xy, yy, dx, dy, varBase); + caches.VarAffineCache[offset] = m; + + reader.BaseStream.Position = restore; + return m; + } + } + + /// + /// Holds per-load caches used during COLR table parsing to deduplicate paint objects, + /// color lines, and affine matrices that may be referenced from multiple offsets. + /// + internal sealed class PaintCaches + { + /// + /// Gets the cache of paint objects keyed by their COLR-relative offset. + /// + public Dictionary PaintCache { get; } = []; + + /// + /// Gets the cache of color lines keyed by their COLR-relative offset. + /// + public Dictionary ColorLineCache { get; } = []; + + /// + /// Gets the cache of variable color lines keyed by their COLR-relative offset. + /// + public Dictionary VarColorLineCache { get; } = []; + + /// + /// Gets the cache of affine matrices keyed by their COLR-relative offset. + /// + public Dictionary AffineCache { get; } = []; + + /// + /// Gets the cache of variable affine matrices keyed by their COLR-relative offset. + /// + public Dictionary VarAffineCache { get; } = []; + } + + /// + /// Represents a resolved COLR v1 glyph layer produced by flattening the paint DAG. + /// Associates a glyph ID with its paint node, geometry transform, paint transform, composite mode, and optional clip box. + /// +#pragma warning disable SA1201 // Elements should appear in the correct order + [DebuggerDisplay("Id: {GlyphId}")] + internal readonly struct ResolvedGlyphLayer +#pragma warning restore SA1201 // Elements should appear in the correct order + { + /// + /// Initializes a new instance of the struct. + /// + /// The glyph ID whose outline this layer paints. + /// The leaf paint node for this layer. + /// The accumulated affine transform applied to glyph geometry. + /// The accumulated affine transform applied to the leaf paint. + /// The composite mode to apply. + /// The optional clip box bounds, or . + public ResolvedGlyphLayer(ushort id, Paint paint, Matrix3x2 glyphTransform, Matrix3x2 paintTransform, CompositeMode mode, Bounds? clipBox) + { + this.GlyphId = id; + this.Paint = paint; + this.GlyphTransform = glyphTransform; + this.PaintTransform = paintTransform; + this.CompositeMode = mode; + this.ClipBox = clipBox; + } + + /// + /// Gets the glyph ID whose outline this layer paints. + /// + public ushort GlyphId { get; } + + /// + /// Gets the leaf paint node for this layer. + /// + public Paint Paint { get; } + + /// + /// Gets the accumulated affine transform applied to glyph geometry. + /// + public Matrix3x2 GlyphTransform { get; } + + /// + /// Gets the accumulated affine transform applied to the leaf paint. + /// + public Matrix3x2 PaintTransform { get; } + + /// + /// Gets the composite mode to apply when rendering this layer. + /// + public CompositeMode CompositeMode { get; } + + /// + /// Gets the optional clip box bounds for this layer, or if no clip applies. + /// + public Bounds? ClipBox { get; } + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/ColrV0GlyphSource.cs b/SixLabors.Fonts/Tables/General/Colr/ColrV0GlyphSource.cs new file mode 100644 index 0000000..fa1b44e --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/ColrV0GlyphSource.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Tables.TrueType.Glyphs; + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Supplies painted glyphs for COLR v0 fonts. + /// Flattens paint graphs into a linear stream and emits a . + /// + internal sealed class ColrV0GlyphSource : ColrGlyphSourceBase + { + /// + /// Cache of previously resolved painted glyphs keyed by glyph ID. + /// + private readonly ConcurrentDictionary cachedGlyphs = []; + + /// + /// Initializes a new instance of the class. + /// + /// The COLR table. + /// The CPAL table, or null if not present. + /// Delegate that loads a glyph outline for the given glyph id. + public ColrV0GlyphSource(ColrTable colr, CpalTable? cpal, Func glyphLoader) + : base(colr, cpal, glyphLoader) + { + } + + /// + public override bool TryGetPaintedGlyph(ushort glyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas) + { + (PaintedGlyph Glyph, PaintedCanvasMetadata Canvas) result = this.cachedGlyphs.GetOrAdd(glyphId, id => + { + if (this.Colr.TryGetColrV0Layers(id, out Span resolved)) + { + List layers = new(resolved.Length); + for (int i = 0; i < resolved.Length; i++) + { + LayerRecord rl = resolved[i]; + GlyphVector? gv = this.GlyphLoader(rl.GlyphId); + if (gv is null || !gv.Value.HasValue()) + { + continue; + } + + // Build geometry once for this layer. + List path = BuildPath(gv.Value); + + // Flatten paint graph: attach composite mode to leaves. + List leafPaints = []; + PaintSolid paint = new() { PaletteIndex = rl.PaletteIndex, Alpha = 1, Format = 2 }; + FlattenPaint(paint, Matrix3x2.Identity, CompositeMode.SrcOver, this.Cpal, this.Colr, null, leafPaints); + + // Emit one layer per leaf paint. + for (int p = 0; p < leafPaints.Count; p++) + { + // Unlike COLR v1, COLR v0 leaves have no transform so we can reuse the same path. + Rendering.Paint leaf = leafPaints[p]; + layers.Add(new PaintedLayer(leaf, FillRule.NonZero, leaf.Transform, null, path)); + } + } + + if (layers.Count > 0) + { + // Canvas viewBox in Y-up; renderer downstream decides orientation via flag. + PaintedGlyph glyph = new(layers); + PaintedCanvasMetadata canvas = new(FontRectangle.Empty, isYDown: false, rootTransform: Matrix3x2.Identity); + return (glyph, canvas); + } + } + + return (default, default); + }); + + glyph = result.Glyph; + canvas = result.Canvas; + return result.Glyph.Layers.Count > 0; + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/ColrV1GlyphSource.cs b/SixLabors.Fonts/Tables/General/Colr/ColrV1GlyphSource.cs new file mode 100644 index 0000000..22b63d1 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/ColrV1GlyphSource.cs @@ -0,0 +1,90 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; +using SixLabors.Fonts.Tables.TrueType.Glyphs; + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Supplies painted glyphs for COLR v1 fonts. + /// Flattens paint graphs into a linear stream and emits a . + /// + internal sealed class ColrV1GlyphSource : ColrGlyphSourceBase + { + /// + /// Cache of previously resolved painted glyphs keyed by glyph ID. + /// + private readonly ConcurrentDictionary cachedGlyphs = []; + + /// + /// The glyph variation processor for variable fonts, or for static fonts. + /// + private readonly GlyphVariationProcessor? processor; + + /// + /// Initializes a new instance of the class. + /// + /// The COLR table. + /// The CPAL table, or null if not present. + /// Delegate that loads a glyph outline for the given glyph id. + /// The glyph variation processor for variable fonts, or null. + public ColrV1GlyphSource(ColrTable colr, CpalTable? cpal, Func glyphLoader, GlyphVariationProcessor? processor = null) + : base(colr, cpal, glyphLoader) + => this.processor = processor; + + /// + public override bool TryGetPaintedGlyph(ushort glyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas) + { + (PaintedGlyph Glyph, PaintedCanvasMetadata Canvas) result = this.cachedGlyphs.GetOrAdd(glyphId, _ => + { + if (this.Colr.TryGetColrV1Layers(glyphId, this.processor, out List? resolved)) + { + List layers = new(resolved.Count); + for (int i = 0; i < resolved.Count; i++) + { + ResolvedGlyphLayer rl = resolved[i]; + GlyphVector? gv = this.GlyphLoader(rl.GlyphId); + if (gv is null || !gv.Value.HasValue()) + { + continue; + } + + // Build geometry once for this layer. + List path = BuildPath(gv.Value); + + // Flatten paint graph: accumulate wrapper transforms; attach composite mode to leaves. + List leafPaints = []; + FlattenPaint(rl.Paint, rl.PaintTransform, rl.CompositeMode, this.Cpal, this.Colr, this.processor, leafPaints); + + // Emit one layer per leaf paint. + Bounds? clip = rl.ClipBox; + for (int p = 0; p < leafPaints.Count; p++) + { + Rendering.Paint leaf = leafPaints[p]; + layers.Add(new PaintedLayer(leaf, FillRule.NonZero, rl.GlyphTransform, clip, path)); + } + } + + if (layers.Count > 0) + { + // Canvas viewBox in Y-up; renderer downstream decides orientation via flag. + PaintedGlyph glyph = new(layers); + PaintedCanvasMetadata canvas = new(FontRectangle.Empty, isYDown: false, rootTransform: Matrix3x2.Identity); + return (glyph, canvas); + } + } + + return (default, default); + }); + + glyph = result.Glyph; + canvas = result.Canvas; + return result.Glyph.Layers.Count > 0; + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/Extend.cs b/SixLabors.Fonts/Tables/General/Colr/Extend.cs new file mode 100644 index 0000000..8add8d7 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/Extend.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Defines the extend mode for COLR v1 gradient color lines, controlling how the gradient + /// is rendered outside the region defined by the color stops. + /// + /// + internal enum Extend : byte + { + /// + /// Pad: the color at the nearest stop is used for all positions outside the stop range. + /// + Pad = 0, + + /// + /// Repeat: the gradient pattern is repeated beyond the stop range. + /// + Repeat = 1, + + /// + /// Reflect: the gradient pattern is reflected (mirrored) alternately beyond the stop range. + /// + Reflect = 2 + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/LayerList.cs b/SixLabors.Fonts/Tables/General/Colr/LayerList.cs new file mode 100644 index 0000000..a83271a --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/LayerList.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Represents the COLR v1 LayerList, which stores an array of offsets to paint tables. + /// PaintColrLayers references ranges within this list to compose multi-layer color glyphs. + /// + /// + internal sealed class LayerList + { + /// + /// Initializes a new instance of the class. + /// + /// The array of paint table offsets relative to the beginning of the COLR table. + public LayerList(uint[] paintOffsets) + => this.PaintOffsets = paintOffsets; + + /// + /// Gets the array of paint table offsets relative to the beginning of the COLR table. + /// + public uint[] PaintOffsets { get; } + + /// + /// Gets the number of paint offsets in the list. + /// + public int Count => this.PaintOffsets.Length; + + /// + /// Loads a from the given reader at the specified offset. + /// + /// The binary reader positioned within the COLR table. + /// The offset from the beginning of the COLR table to the LayerList. + /// The loaded , or if the offset is zero or the list is empty. + public static LayerList? Load(BigEndianBinaryReader reader, uint offset) + { + if (offset == 0) + { + return null; + } + + reader.Seek(offset, SeekOrigin.Begin); + uint count = reader.ReadUInt32(); + + if (count == 0) + { + return null; + } + + // Offsets are relative to the table start; convert to COLR-relative. + uint[] offsets = new uint[count]; + for (int i = 0; i < count; i++) + { + offsets[i] = offset + reader.ReadOffset32(); + } + + return new LayerList(offsets); + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/LayerRecord.cs b/SixLabors.Fonts/Tables/General/Colr/LayerRecord.cs new file mode 100644 index 0000000..e57b41d --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/LayerRecord.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Represents a COLR v0 layer record that pairs a glyph ID with a CPAL palette entry index. + /// Each layer renders the glyph outline in the specified color. + /// + /// + internal readonly struct LayerRecord + { + /// + /// Initializes a new instance of the struct. + /// + /// The glyph ID for this layer. + /// The index into the CPAL palette for this layer's color. + public LayerRecord(ushort glyphId, ushort paletteIndex) + { + this.GlyphId = glyphId; + this.PaletteIndex = paletteIndex; + } + + /// + /// Gets the glyph ID for this layer. + /// + public ushort GlyphId { get; } + + /// + /// Gets the index into the CPAL palette for this layer's color. + /// A value of 0xFFFF indicates the foreground color. + /// + public ushort PaletteIndex { get; } + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/Paint.cs b/SixLabors.Fonts/Tables/General/Colr/Paint.cs new file mode 100644 index 0000000..96a24bc --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/Paint.cs @@ -0,0 +1,700 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Abstract base class for all COLR v1 paint table nodes in the paint DAG. + /// + /// + internal abstract class Paint + { + /// + /// Gets the paint format number identifying the type of this paint node. + /// + public byte Format { get; init; } + } + + /// + /// Represents a COLR v1 PaintColrLayers (format 1), which references a contiguous range of paint + /// layers in the LayerList to compose a multi-layer color glyph. + /// + /// + internal sealed class PaintColrLayers : Paint + { + /// + /// Gets the number of layers to compose. + /// + public byte NumLayers { get; init; } + + /// + /// Gets the index of the first paint in the LayerList. + /// + public uint FirstLayerIndex { get; init; } + } + + /// + /// Represents a COLR v1 PaintSolid (format 2), which fills with a solid color from the CPAL palette. + /// + /// + internal sealed class PaintSolid : Paint + { + /// + /// Gets the CPAL palette entry index. A value of 0xFFFF indicates the foreground color. + /// + public ushort PaletteIndex { get; init; } + + /// + /// Gets the alpha value as an F2DOT14 number. + /// + public float Alpha { get; init; } + } + + /// + /// Represents a COLR v1 PaintVarSolid (format 3), a variation-aware solid color fill. + /// + /// + internal sealed class PaintVarSolid : Paint + { + /// + /// Gets the CPAL palette entry index. A value of 0xFFFF indicates the foreground color. + /// + public ushort PaletteIndex { get; init; } + + /// + /// Gets the alpha value as an F2DOT14 number (varied via VarIndexBase + 0). + /// + public float Alpha { get; init; } + + /// + /// Gets the base index into the ItemVariationStore delta sets. + /// + public uint VarIndexBase { get; init; } + } + + /// + /// Represents a COLR v1 PaintLinearGradient (format 4), which fills with a linear gradient + /// defined by three points and a color line. + /// + /// + internal sealed class PaintLinearGradient : Paint + { + /// + /// Gets the color line defining the gradient stops and extend mode. + /// + public required ColorLine ColorLine { get; init; } + + /// + /// Gets the x-coordinate of the first gradient point (FWORD). + /// + public short X0 { get; init; } + + /// + /// Gets the y-coordinate of the first gradient point (FWORD). + /// + public short Y0 { get; init; } + + /// + /// Gets the x-coordinate of the second gradient point (FWORD). + /// + public short X1 { get; init; } + + /// + /// Gets the y-coordinate of the second gradient point (FWORD). + /// + public short Y1 { get; init; } + + /// + /// Gets the x-coordinate of the rotation point (FWORD). + /// + public short X2 { get; init; } + + /// + /// Gets the y-coordinate of the rotation point (FWORD). + /// + public short Y2 { get; init; } + } + + /// + /// Represents a COLR v1 PaintVarLinearGradient (format 5), a variation-aware linear gradient. + /// + /// + internal sealed class PaintVarLinearGradient : Paint + { + /// + /// Gets the variable color line defining the gradient stops and extend mode. + /// + public required VarColorLine ColorLine { get; init; } + + /// + /// Gets the x-coordinate of the first gradient point (FWORD, var +0). + /// + public short X0 { get; init; } + + /// + /// Gets the y-coordinate of the first gradient point (FWORD, var +1). + /// + public short Y0 { get; init; } + + /// + /// Gets the x-coordinate of the second gradient point (FWORD, var +2). + /// + public short X1 { get; init; } + + /// + /// Gets the y-coordinate of the second gradient point (FWORD, var +3). + /// + public short Y1 { get; init; } + + /// + /// Gets the x-coordinate of the rotation point (FWORD, var +4). + /// + public short X2 { get; init; } + + /// + /// Gets the y-coordinate of the rotation point (FWORD, var +5). + /// + public short Y2 { get; init; } + + /// + /// Gets the base index into the ItemVariationStore delta sets. + /// + public uint VarIndexBase { get; init; } + } + + /// + /// Represents a COLR v1 PaintRadialGradient (format 6), which fills with a radial gradient + /// defined by two circles and a color line. + /// + /// + internal sealed class PaintRadialGradient : Paint + { + /// + /// Gets the color line defining the gradient stops and extend mode. + /// + public required ColorLine ColorLine { get; init; } + + /// + /// Gets the x-coordinate of the first circle center (FWORD). + /// + public short X0 { get; init; } + + /// + /// Gets the y-coordinate of the first circle center (FWORD). + /// + public short Y0 { get; init; } + + /// + /// Gets the radius of the first circle (UFWORD). + /// + public ushort Radius0 { get; init; } + + /// + /// Gets the x-coordinate of the second circle center (FWORD). + /// + public short X1 { get; init; } + + /// + /// Gets the y-coordinate of the second circle center (FWORD). + /// + public short Y1 { get; init; } + + /// + /// Gets the radius of the second circle (UFWORD). + /// + public ushort Radius1 { get; init; } + } + + /// + /// Represents a COLR v1 PaintVarRadialGradient (format 7), a variation-aware radial gradient. + /// + /// + internal sealed class PaintVarRadialGradient : Paint + { + /// + /// Gets the variable color line defining the gradient stops and extend mode. + /// + public required VarColorLine ColorLine { get; init; } + + /// + /// Gets the x-coordinate of the first circle center (FWORD, var +0). + /// + public short X0 { get; init; } + + /// + /// Gets the y-coordinate of the first circle center (FWORD, var +1). + /// + public short Y0 { get; init; } + + /// + /// Gets the radius of the first circle (UFWORD, var +2). + /// + public ushort Radius0 { get; init; } + + /// + /// Gets the x-coordinate of the second circle center (FWORD, var +3). + /// + public short X1 { get; init; } + + /// + /// Gets the y-coordinate of the second circle center (FWORD, var +4). + /// + public short Y1 { get; init; } + + /// + /// Gets the radius of the second circle (UFWORD, var +5). + /// + public ushort Radius1 { get; init; } + + /// + /// Gets the base index into the ItemVariationStore delta sets. + /// + public uint VarIndexBase { get; init; } + } + + /// + /// Represents a COLR v1 PaintSweepGradient (format 8), which fills with a sweep (conical) gradient + /// around a center point between start and end angles. + /// + /// + internal sealed class PaintSweepGradient : Paint + { + /// + /// Gets the color line defining the gradient stops and extend mode. + /// + public required ColorLine ColorLine { get; init; } + + /// + /// Gets the x-coordinate of the sweep center (FWORD). + /// + public short CenterX { get; init; } + + /// + /// Gets the y-coordinate of the sweep center (FWORD). + /// + public short CenterY { get; init; } + + /// + /// Gets the start angle as an F2DOT14 value with bias per spec. + /// + public float StartAngle { get; init; } + + /// + /// Gets the end angle as an F2DOT14 value with bias per spec. + /// + public float EndAngle { get; init; } + } + + /// + /// Represents a COLR v1 PaintVarSweepGradient (format 9), a variation-aware sweep gradient. + /// + /// + internal sealed class PaintVarSweepGradient : Paint + { + /// + /// Gets the variable color line defining the gradient stops and extend mode. + /// + public required VarColorLine ColorLine { get; init; } + + /// + /// Gets the x-coordinate of the sweep center (FWORD, var +0). + /// + public short CenterX { get; init; } + + /// + /// Gets the y-coordinate of the sweep center (FWORD, var +1). + /// + public short CenterY { get; init; } + + /// + /// Gets the start angle as an F2DOT14 value (var +2). + /// + public float StartAngle { get; init; } + + /// + /// Gets the end angle as an F2DOT14 value (var +3). + /// + public float EndAngle { get; init; } + + /// + /// Gets the base index into the ItemVariationStore delta sets. + /// + public uint VarIndexBase { get; init; } + } + + /// + /// Represents a COLR v1 PaintGlyph (format 10), which binds a glyph outline to a child paint node. + /// The glyph outline serves as a clip path for the child paint. + /// + /// + internal sealed class PaintGlyph : Paint + { + /// + /// Gets the child paint node that fills the glyph outline. + /// + public required Paint Child { get; init; } + + /// + /// Gets the glyph ID whose outline is used as the clip path. + /// + public ushort GlyphId { get; init; } + } + + /// + /// Represents a COLR v1 PaintColrGlyph (format 11), which references another glyph's root paint + /// from the BaseGlyphList, allowing reuse of color glyph definitions. + /// + /// + internal sealed class PaintColrGlyph : Paint + { + /// + /// Gets the glyph ID whose root paint in the BaseGlyphList is referenced. + /// + public ushort GlyphId { get; init; } + } + + /// + /// Represents a COLR v1 PaintTransform (format 12), which applies an affine transformation to its child paint. + /// + /// + internal sealed class PaintTransform : Paint + { + /// + /// Gets the child paint node to transform. + /// + public required Paint Child { get; init; } + + /// + /// Gets the 2x3 affine transformation matrix. + /// + public Affine2x3 Transform { get; init; } + } + + /// + /// Represents a COLR v1 PaintVarTransform (format 13), a variation-aware affine transformation. + /// + /// + internal sealed class PaintVarTransform : Paint + { + /// + /// Gets the child paint node to transform. + /// + public required Paint Child { get; init; } + + /// + /// Gets the variation-aware 2x3 affine transformation matrix. + /// + public VarAffine2x3 Transform { get; init; } + } + + /// + /// Represents a COLR v1 PaintTranslate (format 14), which translates its child paint by a fixed offset. + /// + /// + internal sealed class PaintTranslate : Paint + { + /// + /// Gets the child paint node to translate. + /// + public required Paint Child { get; init; } + + /// + /// Gets the x-axis translation (FWORD). + /// + public short Dx { get; init; } + + /// + /// Gets the y-axis translation (FWORD). + /// + public short Dy { get; init; } + } + + /// + /// Represents a COLR v1 PaintVarTranslate (format 15), a variation-aware translation. + /// + /// + internal sealed class PaintVarTranslate : Paint + { + /// + /// Gets the child paint node to translate. + /// + public required Paint Child { get; init; } + + /// + /// Gets the x-axis translation (FWORD, var +0). + /// + public short Dx { get; init; } + + /// + /// Gets the y-axis translation (FWORD, var +1). + /// + public short Dy { get; init; } + + /// + /// Gets the base index into the ItemVariationStore delta sets. + /// + public uint VarIndexBase { get; init; } + } + + /// + /// Represents COLR v1 scale paint operations (formats 16, 18, 20, 22), which apply a scale + /// transformation to their child paint. Supports uniform/anisotropic and around-center variants. + /// + /// + internal sealed class PaintScale : Paint + { + /// + /// Gets the child paint node to scale. + /// + public required Paint Child { get; init; } + + /// + /// Gets the x-axis scale factor (F2DOT14). + /// + public float ScaleX { get; init; } + + /// + /// Gets the y-axis scale factor (F2DOT14). Equal to ScaleX for uniform scale formats. + /// + public float ScaleY { get; init; } + + /// + /// Gets the x-coordinate of the scale center (FWORD). Zero if not an "around center" format. + /// + public short CenterX { get; init; } + + /// + /// Gets the y-coordinate of the scale center (FWORD). Zero if not an "around center" format. + /// + public short CenterY { get; init; } + + /// + /// Gets a value indicating whether this is an "around center" scale format. + /// + public bool AroundCenter { get; init; } + + /// + /// Gets a value indicating whether this is a uniform (isotropic) scale. + /// + public bool Uniform { get; init; } + } + + /// + /// Represents COLR v1 variation-aware scale paint operations (formats 17, 19, 21, 23). + /// + /// + internal sealed class PaintVarScale : Paint + { + /// + /// Gets the child paint node to scale. + /// + public required Paint Child { get; init; } + + /// + /// Gets the x-axis scale factor (var +0). + /// + public float ScaleX { get; init; } + + /// + /// Gets the y-axis scale factor (var +1). Equal to ScaleX for uniform scale formats. + /// + public float ScaleY { get; init; } + + /// + /// Gets the x-coordinate of the scale center (var +2 if around center). + /// + public short CenterX { get; init; } + + /// + /// Gets the y-coordinate of the scale center (var +3 if around center). + /// + public short CenterY { get; init; } + + /// + /// Gets a value indicating whether this is an "around center" scale format. + /// + public bool AroundCenter { get; init; } + + /// + /// Gets a value indicating whether this is a uniform (isotropic) scale. + /// + public bool Uniform { get; init; } + + /// + /// Gets the base index into the ItemVariationStore delta sets. + /// + public uint VarIndexBase { get; init; } + } + + /// + /// Represents COLR v1 rotate paint operations (formats 24, 26), which apply a rotation + /// to their child paint, optionally around a specified center point. + /// + /// + internal sealed class PaintRotate : Paint + { + /// + /// Gets the child paint node to rotate. + /// + public required Paint Child { get; init; } + + /// + /// Gets the rotation angle as an F2DOT14 value (1.0 = 180 degrees). + /// + public float Angle { get; init; } + + /// + /// Gets the x-coordinate of the rotation center (FWORD). Zero if not "around center". + /// + public short CenterX { get; init; } + + /// + /// Gets the y-coordinate of the rotation center (FWORD). Zero if not "around center". + /// + public short CenterY { get; init; } + + /// + /// Gets a value indicating whether this is an "around center" rotation format. + /// + public bool AroundCenter { get; init; } + } + + /// + /// Represents COLR v1 variation-aware rotate paint operations (formats 25, 27). + /// + /// + internal sealed class PaintVarRotate : Paint + { + /// + /// Gets the child paint node to rotate. + /// + public required Paint Child { get; init; } + + /// + /// Gets the rotation angle (var +0). + /// + public float Angle { get; init; } + + /// + /// Gets the x-coordinate of the rotation center (var +1 if around center). + /// + public short CenterX { get; init; } + + /// + /// Gets the y-coordinate of the rotation center (var +2 if around center). + /// + public short CenterY { get; init; } + + /// + /// Gets a value indicating whether this is an "around center" rotation format. + /// + public bool AroundCenter { get; init; } + + /// + /// Gets the base index into the ItemVariationStore delta sets. + /// + public uint VarIndexBase { get; init; } + } + + /// + /// Represents COLR v1 skew paint operations (formats 28, 30), which apply a skew transformation + /// to their child paint, optionally around a specified center point. + /// + /// + internal sealed class PaintSkew : Paint + { + /// + /// Gets the child paint node to skew. + /// + public required Paint Child { get; init; } + + /// + /// Gets the x-axis skew angle as an F2DOT14 value. + /// + public float XSkew { get; init; } + + /// + /// Gets the y-axis skew angle as an F2DOT14 value. + /// + public float YSkew { get; init; } + + /// + /// Gets the x-coordinate of the skew center (FWORD). Zero if not "around center". + /// + public short CenterX { get; init; } + + /// + /// Gets the y-coordinate of the skew center (FWORD). Zero if not "around center". + /// + public short CenterY { get; init; } + + /// + /// Gets a value indicating whether this is an "around center" skew format. + /// + public bool AroundCenter { get; init; } + } + + /// + /// Represents COLR v1 variation-aware skew paint operations (formats 29, 31). + /// + /// + internal sealed class PaintVarSkew : Paint + { + /// + /// Gets the child paint node to skew. + /// + public required Paint Child { get; init; } + + /// + /// Gets the x-axis skew angle (var +0). + /// + public float XSkew { get; init; } + + /// + /// Gets the y-axis skew angle (var +1). + /// + public float YSkew { get; init; } + + /// + /// Gets the x-coordinate of the skew center (var +2 if around center). + /// + public short CenterX { get; init; } + + /// + /// Gets the y-coordinate of the skew center (var +3 if around center). + /// + public short CenterY { get; init; } + + /// + /// Gets a value indicating whether this is an "around center" skew format. + /// + public bool AroundCenter { get; init; } + + /// + /// Gets the base index into the ItemVariationStore delta sets. + /// + public uint VarIndexBase { get; init; } + } + + /// + /// Represents a COLR v1 PaintComposite (format 32), which composites a source paint over a + /// backdrop paint using a specified Porter-Duff or blend mode. + /// + /// + internal sealed class PaintComposite : Paint + { + /// + /// Gets the composite mode used to blend the source over the backdrop. + /// + public ColrCompositeMode CompositeMode { get; init; } + + /// + /// Gets the source paint node. + /// + public required Paint Source { get; init; } + + /// + /// Gets the backdrop paint node. + /// + public required Paint Backdrop { get; init; } + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/VarAffine2x3.cs b/SixLabors.Fonts/Tables/General/Colr/VarAffine2x3.cs new file mode 100644 index 0000000..de0ff72 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/VarAffine2x3.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Represents a variation-aware 2x3 affine transformation matrix used by COLR v1 PaintVarTransform operations. + /// Values are stored as Fixed 16.16 numbers with an associated variation index base for font variations. + /// + /// + internal readonly struct VarAffine2x3 + { + /// + /// The x-component of the x-basis vector. + /// + public readonly float Xx; + + /// + /// The y-component of the x-basis vector. + /// + public readonly float Yx; + + /// + /// The x-component of the y-basis vector. + /// + public readonly float Xy; + + /// + /// The y-component of the y-basis vector. + /// + public readonly float Yy; + + /// + /// The x-translation component. + /// + public readonly float Dx; + + /// + /// The y-translation component. + /// + public readonly float Dy; + + /// + /// The base index into the ItemVariationStore delta sets for this transform's variation data. + /// + public readonly uint VarIndexBase; + + /// + /// Initializes a new instance of the struct. + /// + /// The x-component of the x-basis vector. + /// The y-component of the x-basis vector. + /// The x-component of the y-basis vector. + /// The y-component of the y-basis vector. + /// The x-translation component. + /// The y-translation component. + /// The base index into the ItemVariationStore delta sets. + public VarAffine2x3(float xx, float yx, float xy, float yy, float dx, float dy, uint varIndexBase) + { + this.Xx = xx; + this.Yx = yx; + this.Xy = xy; + this.Yy = yy; + this.Dx = dx; + this.Dy = dy; + this.VarIndexBase = varIndexBase; + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/VarColorIndex.cs b/SixLabors.Fonts/Tables/General/Colr/VarColorIndex.cs new file mode 100644 index 0000000..f2dbd3a --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/VarColorIndex.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Represents a variation-aware color index in COLR v1, consisting of a palette index, + /// alpha value, and a variation index base for font variations. + /// + /// + internal readonly struct VarColorIndex + { + /// + /// Initializes a new instance of the struct. + /// + /// The index into the CPAL palette. + /// The alpha multiplier as an F2DOT14 value. + /// The base index into the ItemVariationStore delta sets. + public VarColorIndex(ushort paletteIndex, float alpha, uint varIndexBase) + { + this.PaletteIndex = paletteIndex; + this.Alpha = alpha; + this.VarIndexBase = varIndexBase; + } + + /// + /// Gets the index into the CPAL palette. + /// + public ushort PaletteIndex { get; } + + /// + /// Gets the alpha multiplier as an F2DOT14 value. + /// + public float Alpha { get; } + + /// + /// Gets the base index into the ItemVariationStore delta sets for this color's variation data. + /// + public uint VarIndexBase { get; } + + /// + /// Loads a from the given reader at the current position. + /// + /// The binary reader. + /// The loaded . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static VarColorIndex Load(BigEndianBinaryReader reader) + => new(reader.ReadUInt16(), reader.ReadF2Dot14(), reader.ReadUInt32()); + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/VarColorLine.cs b/SixLabors.Fonts/Tables/General/Colr/VarColorLine.cs new file mode 100644 index 0000000..c0e3908 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/VarColorLine.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Represents a variation-aware COLR v1 ColorLine, which defines a sequence of variable color stops + /// and an extend mode for gradient paints in variable fonts. + /// + /// + internal sealed class VarColorLine + { + /// + /// Initializes a new instance of the class. + /// + /// The extend mode that determines how the gradient behaves outside the defined stop range. + /// The array of variable color stops defining the gradient. + public VarColorLine(Extend extend, VarColorStop[] stops) + { + this.Extend = extend; + this.Stops = stops; + } + + /// + /// Gets the extend mode that determines how the gradient behaves outside the defined stop range. + /// + public Extend Extend { get; } + + /// + /// Gets the array of variable color stops defining the gradient. + /// + public VarColorStop[] Stops { get; } + + /// + /// Gets the number of color stops. + /// + public int Count => this.Stops.Length; + + /// + /// Loads a from the given reader at the current position. + /// + /// The binary reader. + /// The loaded . + public static VarColorLine Load(BigEndianBinaryReader reader) + { + Extend extend = reader.ReadByte(); + ushort numStops = reader.ReadUInt16(); + + VarColorStop[] stops = new VarColorStop[numStops]; + for (int i = 0; i < numStops; i++) + { + stops[i] = VarColorStop.Load(reader); + } + + return new VarColorLine(extend, stops); + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Colr/VarColorStop.cs b/SixLabors.Fonts/Tables/General/Colr/VarColorStop.cs new file mode 100644 index 0000000..b48b88e --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Colr/VarColorStop.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts.Tables.General.Colr { + /// + /// Represents a variation-aware COLR v1 color stop within a , + /// defining a position, palette color, alpha value, and a variation index base for font variations. + /// + /// + internal readonly struct VarColorStop + { + /// + /// Initializes a new instance of the struct. + /// + /// The position of this color stop along the gradient, as an F2DOT14 value. + /// The index into the CPAL palette for this stop's color. + /// The alpha value for this stop, as an F2DOT14 value. + /// The base index into the ItemVariationStore delta sets. + public VarColorStop(float stopOffset, ushort paletteIndex, float alpha, uint varIndexBase) + { + this.StopOffset = stopOffset; + this.PaletteIndex = paletteIndex; + this.Alpha = alpha; + this.VarIndexBase = varIndexBase; + } + + /// + /// Gets the position of this color stop along the gradient, as an F2DOT14 value. + /// + public float StopOffset { get; } + + /// + /// Gets the index into the CPAL palette for this stop's color. + /// + public ushort PaletteIndex { get; } + + /// + /// Gets the alpha multiplier for this stop, as an F2DOT14 value. + /// + public float Alpha { get; } + + /// + /// Gets the base index into the ItemVariationStore delta sets for this stop's variation data. + /// + public uint VarIndexBase { get; } + + /// + /// Loads a from the given reader at the current position. + /// + /// The binary reader. + /// The loaded . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static VarColorStop Load(BigEndianBinaryReader reader) + => new(reader.ReadF2Dot14(), reader.ReadUInt16(), reader.ReadF2Dot14(), reader.ReadUInt32()); + } +} diff --git a/SixLabors.Fonts/Tables/General/CpalTable.cs b/SixLabors.Fonts/Tables/General/CpalTable.cs new file mode 100644 index 0000000..6df0b39 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/CpalTable.cs @@ -0,0 +1,121 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.General { + /// + /// Represents the color palette table, which contains one or more palettes of colors + /// used by color fonts (e.g., COLR table). + /// + /// + internal class CpalTable : Table + { + /// + /// The table name identifier. + /// + internal const string TableName = "CPAL"; + + /// + /// The offsets into the palette entries array for each palette. + /// + private readonly ushort[] paletteOffsets; + + /// + /// The combined array of color records for all palettes. + /// + private readonly GlyphColor[] paletteEntries; + + /// + /// Initializes a new instance of the class. + /// + /// The index of each palette's first color record. + /// The combined color records for all palettes. + public CpalTable(ushort[] paletteOffsets, GlyphColor[] paletteEntries) + { + this.paletteEntries = paletteEntries; + this.paletteOffsets = paletteOffsets; + } + + /// + /// Gets the glyph color at the specified palette and entry indices. + /// + /// The zero-based palette index. + /// The zero-based entry index within the palette. + /// The . + public GlyphColor GetGlyphColor(int paletteIndex, int paletteEntryIndex) + => this.paletteEntries[this.paletteOffsets[paletteIndex] + paletteEntryIndex]; + + /// + /// Loads the from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static CpalTable? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + /// + /// Loads the from the specified binary reader. + /// + /// The big-endian binary reader. + /// The . + public static CpalTable Load(BigEndianBinaryReader reader) + { + // FORMAT 0 + + // Type | Name | Description + // ----------|---------------------------------|---------------------------------------------------------------------------------------------------- + // uint16 | version | Table version number (=0). + // uint16 | numPaletteEntries | Number of palette entries in each palette. + // uint16 | numPalettes | Number of palettes in the table. + // uint16 | numColorRecords | Total number of color records, combined for all palettes. + // Offset32 | offsetFirstColorRecord | Offset from the beginning of CPAL table to the first ColorRecord. + // uint16 | colorRecordIndices[numPalettes] | Index of each palette’s first color record in the combined color record array. + + // additional format 1 fields + // Offset32 | offsetPaletteTypeArray | Offset from the beginning of CPAL table to the Palette Type Array. Set to 0 if no array is provided. + // Offset32 | offsetPaletteLabelArray | Offset from the beginning of CPAL table to the Palette Labels Array. Set to 0 if no array is provided. + // Offset32 | offsetPaletteEntryLabelArray | Offset from the beginning of CPAL table to the Palette Entry Label Array.Set to 0 if no array is provided. + ushort version = reader.ReadUInt16(); + ushort numPaletteEntries = reader.ReadUInt16(); + ushort numPalettes = reader.ReadUInt16(); + ushort numColorRecords = reader.ReadUInt16(); + uint offsetFirstColorRecord = reader.ReadOffset32(); + + ushort[]? colorRecordIndices = reader.ReadUInt16Array(numPalettes); + + uint offsetPaletteTypeArray = 0; + uint offsetPaletteLabelArray = 0; + uint offsetPaletteEntryLabelArray = 0; + if (version == 1) + { + offsetPaletteTypeArray = reader.ReadOffset32(); + offsetPaletteLabelArray = reader.ReadOffset32(); + offsetPaletteEntryLabelArray = reader.ReadOffset32(); + } + + reader.Seek(offsetFirstColorRecord, SeekOrigin.Begin); + GlyphColor[] palettes = new GlyphColor[numColorRecords]; + for (int n = 0; n < numColorRecords; n++) + { + byte blue = reader.ReadByte(); + byte green = reader.ReadByte(); + byte red = reader.ReadByte(); + byte alpha = reader.ReadByte(); + palettes[n] = new GlyphColor(red, green, blue, alpha); + } + + return new CpalTable(colorRecordIndices, palettes); + } + } +} diff --git a/SixLabors.Fonts/Tables/General/HeadTable.cs b/SixLabors.Fonts/Tables/General/HeadTable.cs new file mode 100644 index 0000000..4b4a107 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/HeadTable.cs @@ -0,0 +1,350 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Tables.General { + /// + /// Represents the font header table, which contains global information about the font. + /// + /// + internal class HeadTable : Table + { + /// + /// The table name identifier. + /// + internal const string TableName = "head"; + + /// + /// Initializes a new instance of the class. + /// + /// The font header flags. + /// The Mac style flags. + /// The number of font design units per em. + /// The date the font was created. + /// The date the font was last modified. + /// The bounding box for all glyphs in the font. + /// The smallest readable size in pixels. + /// The format of the index-to-location table. + public HeadTable( + HeadFlags flags, + HeadMacStyle macStyle, + ushort unitsPerEm, + DateTime created, + DateTime modified, + Bounds bounds, + ushort lowestRecPPEM, + IndexLocationFormats indexToLocFormat) + { + this.Flags = flags; + this.MacStyle = macStyle; + this.UnitsPerEm = unitsPerEm; + this.Created = created; + this.Modified = modified; + this.Bounds = bounds; + this.LowestRecPPEM = lowestRecPPEM; + this.IndexLocationFormat = indexToLocFormat; + } + + /// + /// Specifies the format of the index-to-location ('loca') table offsets. + /// + internal enum IndexLocationFormats : short + { + /// + /// Short offsets (Offset16). + /// + Offset16 = 0, + + /// + /// Long offsets (Offset32). + /// + Offset32 = 1, + } + + /// + /// Font header flags indicating various font characteristics. + /// + [Flags] + internal enum HeadFlags : ushort + { + // Bit 0: Baseline for font at y = 0; + // Bit 1: Left sidebearing point at x = 0(relevant only for TrueType rasterizers) — see the note below regarding variable fonts; + // Bit 2: Instructions may depend on point size; + // Bit 3: Force ppem to integer values for all internal scaler math; may use fractional ppem sizes if this bit is clear; + // Bit 4: Instructions may alter advance width(the advance widths might not scale linearly); + // Bit 5: This bit is not used in OpenType, and should not be set in order to ensure compatible behavior on all platforms.If set, it may result in different behavior for vertical layout in some platforms. (See Apple's specification for details regarding behavior in Apple platforms.) + // Bits 6–10: These bits are not used in Opentype and should always be cleared. (See Apple's specification for details regarding legacy used in Apple platforms.) + // Bit 11: Font data is ‘lossless’ as a results of having been subjected to optimizing transformation and/or compression (such as e.g.compression mechanisms defined by ISO/IEC 14496-18, MicroType Express, WOFF 2.0 or similar) where the original font functionality and features are retained but the binary compatibility between input and output font files is not guaranteed.As a result of the applied transform, the ‘DSIG’ Table may also be invalidated. + // Bit 12: Font converted (produce compatible metrics) + // Bit 13: Font optimized for ClearType™. Note, fonts that rely on embedded bitmaps (EBDT) for rendering should not be considered optimized for ClearType, and therefore should keep this bit cleared. + // Bit 14: Last Resort font.If set, indicates that the glyphs encoded in the cmap subtables are simply generic symbolic representations of code point ranges and don’t truly represent support for those code points.If unset, indicates that the glyphs encoded in the cmap subtables represent proper support for those code points. + // Bit 15: Reserved, set to 0 + + /// + /// No flags set. + /// + None = 0, + + /// + /// Baseline for font at y = 0. + /// + BaselineY0 = 1 << 0, + + /// + /// Left sidebearing point at x = 0 (relevant only for TrueType rasterizers). + /// + LeftSidebearingPointAtX0 = 1 << 1, + + /// + /// Instructions may depend on point size. + /// + InstructionDependOnPointSize = 1 << 2, + + /// + /// Force ppem to integer values for all internal scaler math. + /// + ForcePPEMToInt = 1 << 3, + + /// + /// Instructions may alter advance width (the advance widths might not scale linearly). + /// + InstructionAlterAdvancedWidth = 1 << 4, + + // 1<<5 not used + // 1<<6 - 1<<10 not used + + /// + /// Font data is lossless as a result of having been compressed or optimized. + /// + FontDataLossLess = 1 << 11, + + /// + /// Font converted (produce compatible metrics). + /// + FontConverted = 1 << 12, + + /// + /// Font optimized for ClearType. + /// + OptimizedForClearType = 1 << 13, + + /// + /// Last Resort font. Glyphs are generic symbolic representations of code point ranges. + /// + LastResortFont = 1 << 14, + } + + /// + /// Macintosh style flags for the font. + /// + [Flags] + internal enum HeadMacStyle : ushort + { + /// + /// No style flags set. + /// + None = 0, + + /// + /// Bold style. + /// + Bold = 1 << 0, + + /// + /// Italic style. + /// + Italic = 1 << 1, + + /// + /// Underline style. + /// + Underline = 1 << 2, + + /// + /// Outline (hollow) style. + /// + Outline = 1 << 3, + + /// + /// Shadow style. + /// + Shadow = 1 << 4, + + /// + /// Condensed style. + /// + Condensed = 1 << 5, + + /// + /// Extended style. + /// + Extended = 1 << 6, + } + + /// + /// Gets the date the font was created. + /// + public DateTime Created { get; } + + /// + /// Gets the font header flags. + /// + public HeadFlags Flags { get; } + + /// + /// Gets the format of the index-to-location table. + /// + public IndexLocationFormats IndexLocationFormat { get; } + + /// + /// Gets the smallest readable size in pixels. + /// + public ushort LowestRecPPEM { get; } + + /// + /// Gets the Mac style flags. + /// + public HeadMacStyle MacStyle { get; } + + /// + /// Gets the date the font was last modified. + /// + public DateTime Modified { get; } + + /// + /// Gets the bounding box for all glyphs in the font. + /// + public Bounds Bounds { get; } + + /// + /// Gets the number of font design units per em. + /// + public ushort UnitsPerEm { get; } + + /// + /// Loads the from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static HeadTable? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + /// + /// Loads the from the specified binary reader. + /// + /// The big-endian binary reader. + /// The . + public static HeadTable Load(BigEndianBinaryReader reader) + { + // Type | Name | Description + // -------------|--------------------|---------------------------------------------------------------------------------------------------- + // uint16 | majorVersion | Major version number of the font header table — set to 1. + // uint16 | minorVersion | Minor version number of the font header table — set to 0. + // Fixed | fontRevision | Set by font manufacturer. + // uint32 | checkSumAdjustment | To compute: set it to 0, sum the entire font as uint32, then store 0xB1B0AFBA - sum.If the font is used as a component in a font collection file, the value of this field will be invalidated by changes to the file structure and font table directory, and must be ignored. + // uint32 | magicNumber | Set to 0x5F0F3CF5. + // uint16 | flags | Bit 0: Baseline for font at y = 0; + // Bit 1: Left sidebearing point at x = 0(relevant only for TrueType rasterizers) — see the note below regarding variable fonts; + // Bit 2: Instructions may depend on point size; + // Bit 3: Force ppem to integer values for all internal scaler math; may use fractional ppem sizes if this bit is clear; + // Bit 4: Instructions may alter advance width(the advance widths might not scale linearly); + // Bit 5: This bit is not used in OpenType, and should not be set in order to ensure compatible behavior on all platforms.If set, it may result in different behavior for vertical layout in some platforms. (See Apple's specification for details regarding behavior in Apple platforms.) + // Bits 6–10: These bits are not used in Opentype and should always be cleared. (See Apple's specification for details regarding legacy used in Apple platforms.) + // Bit 11: Font data is ‘lossless’ as a results of having been subjected to optimizing transformation and/or compression (such as e.g.compression mechanisms defined by ISO/IEC 14496-18, MicroType Express, WOFF 2.0 or similar) where the original font functionality and features are retained but the binary compatibility between input and output font files is not guaranteed.As a result of the applied transform, the ‘DSIG’ Table may also be invalidated. + // Bit 12: Font converted (produce compatible metrics) + // Bit 13: Font optimized for ClearType™. Note, fonts that rely on embedded bitmaps (EBDT) for rendering should not be considered optimized for ClearType, and therefore should keep this bit cleared. + // Bit 14: Last Resort font.If set, indicates that the glyphs encoded in the cmap subtables are simply generic symbolic representations of code point ranges and don’t truly represent support for those code points.If unset, indicates that the glyphs encoded in the cmap subtables represent proper support for those code points. + // Bit 15: Reserved, set to 0 + // uint16 | unitsPerEm | Valid range is from 16 to 16384. This value should be a power of 2 for fonts that have TrueType outlines. + // LONGDATETIME | created | Number of seconds since 12:00 midnight that started January 1st 1904 in GMT/UTC time zone. 64-bit integer + // LONGDATETIME | modified | Number of seconds since 12:00 midnight that started January 1st 1904 in GMT/UTC time zone. 64-bit integer + // int16 | xMin | For all glyph bounding boxes. + // int16 | yMin | For all glyph bounding boxes. + // int16 | xMax | For all glyph bounding boxes. + // int16 | yMax | For all glyph bounding boxes. + // uint16 | macStyle | Bit 0: Bold (if set to 1); + // Bit 1: Italic(if set to 1) + // Bit 2: Underline(if set to 1) + // Bit 3: Outline(if set to 1) + // Bit 4: Shadow(if set to 1) + // Bit 5: Condensed(if set to 1) + // Bit 6: Extended(if set to 1) + // Bits 7–15: Reserved(set to 0). + // uint16 | lowestRecPPEM | Smallest readable size in pixels. + // int16 | fontDirectionHint | Deprecated(Set to 2). + // 0: Fully mixed directional glyphs; + // 1: Only strongly left to right; + // 2: Like 1 but also contains neutrals; + // -1: Only strongly right to left; + // -2: Like -1 but also contains neutrals. 1 + // int16 | indexToLocFormat | 0 for short offsets (Offset16), 1 for long (Offset32). + // int16 | glyphDataFormat | 0 for current format. + ushort majorVersion = reader.ReadUInt16(); + ushort minorVersion = reader.ReadUInt16(); + uint fontRevision = reader.ReadUInt32(); + uint checkSumAdjustment = reader.ReadUInt32(); + uint magicNumber = reader.ReadUInt32(); + if (magicNumber != 0x5F0F3CF5) + { + throw new InvalidFontFileException("invalid magic number in 'head'"); + } + + HeadFlags flags = reader.ReadUInt16(); + ushort unitsPerEm = reader.ReadUInt16(); + if (unitsPerEm < 16 || unitsPerEm > 16384) + { + throw new InvalidFontFileException($"invalid units per em expected value between 16 and 16384 but found {unitsPerEm} in 'head'"); + } + + var startDate = new DateTime(1904, 01, 01, 0, 0, 0, DateTimeKind.Utc); + long seconds = reader.ReadInt64(); + DateTime created = startDate; + if (seconds > 0) + { + // Clear upper 32 bits, some fonts seem to have a non-zero upper 32 bits, like "C:\\Windows/Fonts\\cityb___.ttf" + // The max date for UInt32.MaxValue seconds is {06/02/2040 06:28:15}, which should be plenty for the time being. + seconds &= 0x00000000ffffffff; + created = startDate.AddSeconds(seconds); + } + + seconds = reader.ReadInt64(); + DateTime modified = startDate; + if (seconds > 0) + { + // Clear upper 32 bits, some fonts seem to have a non-zero upper 32 bits, like "C:\\Windows/Fonts\\cityb___.ttf" + // The max date for UInt32.MaxValue seconds is {06/02/2040 06:28:15}, which should be plenty for the time being. + seconds &= 0x00000000ffffffff; + modified = startDate.AddSeconds(seconds); + } + + var bounds = Bounds.Load(reader); // xMin, yMin, xMax, yMax + + HeadMacStyle macStyle = reader.ReadUInt16(); + ushort lowestRecPPEM = reader.ReadUInt16(); + short fontDirectionHint = reader.ReadInt16(); + IndexLocationFormats indexToLocFormat = reader.ReadInt16(); + short glyphDataFormat = reader.ReadInt16(); + + return new HeadTable( + flags, + macStyle, + unitsPerEm, + created, + modified, + bounds, + lowestRecPPEM, + indexToLocFormat); + } + } +} diff --git a/SixLabors.Fonts/Tables/General/HorizontalHeadTable.cs b/SixLabors.Fonts/Tables/General/HorizontalHeadTable.cs new file mode 100644 index 0000000..7ecfc72 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/HorizontalHeadTable.cs @@ -0,0 +1,212 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General { + /// + /// Represents the horizontal header table, which contains information needed to lay out fonts + /// whose characters are written horizontally. + /// + /// + internal class HorizontalHeadTable : Table + { + /// + /// The table name identifier. + /// + internal const string TableName = "hhea"; + + /// + /// Initializes a new instance of the class. + /// + /// The typographic ascender. + /// The typographic descender. + /// The typographic line gap. + /// The maximum advance width in font design units. + /// The minimum left side bearing. + /// The minimum right side bearing. + /// The maximum x extent: max(lsb + (xMax - xMin)). + /// The caret slope rise used to calculate the slope of the caret. + /// The caret slope run used to calculate the slope of the caret. + /// The caret offset for slanted fonts. + /// The number of horizontal metrics in the 'hmtx' table. + public HorizontalHeadTable( + short ascender, + short descender, + short lineGap, + ushort advanceWidthMax, + short minLeftSideBearing, + short minRightSideBearing, + short xMaxExtent, + short caretSlopeRise, + short caretSlopeRun, + short caretOffset, + ushort numberOfHMetrics) + { + this.Ascender = ascender; + this.Descender = descender; + this.LineGap = lineGap; + this.AdvanceWidthMax = advanceWidthMax; + this.MinLeftSideBearing = minLeftSideBearing; + this.MinRightSideBearing = minRightSideBearing; + this.XMaxExtent = xMaxExtent; + this.CaretSlopeRise = caretSlopeRise; + this.CaretSlopeRun = caretSlopeRun; + this.CaretOffset = caretOffset; + this.NumberOfHMetrics = numberOfHMetrics; + } + + /// + /// Gets the maximum advance width, in font design units. + /// + public ushort AdvanceWidthMax { get; } + + /// + /// Gets the typographic ascender distance from the baseline. + /// + public short Ascender { get; } + + /// + /// Gets the caret offset for slanted fonts. Set to 0 for non-slanted fonts. + /// + public short CaretOffset { get; } + + /// + /// Gets the caret slope rise. Set to 1 for a vertical caret. + /// + public short CaretSlopeRise { get; } + + /// + /// Gets the caret slope run. Set to 0 for a vertical caret. + /// + public short CaretSlopeRun { get; } + + /// + /// Gets the typographic descender distance from the baseline (typically negative). + /// + public short Descender { get; } + + /// + /// Gets the typographic line gap. + /// + public short LineGap { get; } + + /// + /// Gets the minimum left side bearing value. + /// + public short MinLeftSideBearing { get; } + + /// + /// Gets the minimum right side bearing value. + /// + public short MinRightSideBearing { get; } + + /// + /// Gets the number of horizontal metrics in the 'hmtx' table. + /// + public ushort NumberOfHMetrics { get; } + + /// + /// Gets the maximum x extent: max(lsb + (xMax - xMin)). + /// + public short XMaxExtent { get; } + + /// + /// Loads the from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static HorizontalHeadTable? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + /// + /// Loads the from the specified binary reader. + /// + /// The big-endian binary reader. + /// The . + public static HorizontalHeadTable Load(BigEndianBinaryReader reader) + { + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +========+=====================+=================================================================================+ + // | Fixed | version | 0x00010000 (1.0) | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | FWord | ascent | Distance from baseline of highest ascender | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | FWord | descent | Distance from baseline of lowest descender | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | FWord | lineGap | typographic line gap | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | uFWord | advanceWidthMax | must be consistent with horizontal metrics | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | FWord | minLeftSideBearing | must be consistent with horizontal metrics | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | FWord | minRightSideBearing | must be consistent with horizontal metrics | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | FWord | xMaxExtent | max(lsb + (xMax-xMin)) | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | int16 | caretSlopeRise | used to calculate the slope of the caret (rise/run) set to 1 for vertical caret | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | int16 | caretSlopeRun | 0 for vertical | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | FWord | caretOffset | set value to 0 for non-slanted fonts | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | int16 | reserved | set value to 0 | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | int16 | reserved | set value to 0 | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | int16 | reserved | set value to 0 | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | int16 | reserved | set value to 0 | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | int16 | metricDataFormat | 0 for current format | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | uint16 | numOfLongHorMetrics | number of advance widths in metrics table | + // +--------+---------------------+---------------------------------------------------------------------------------+ + ushort majorVersion = reader.ReadUInt16(); + ushort minorVersion = reader.ReadUInt16(); + short ascender = reader.ReadFWORD(); + short descender = reader.ReadFWORD(); + short lineGap = reader.ReadFWORD(); + ushort advanceWidthMax = reader.ReadUFWORD(); + short minLeftSideBearing = reader.ReadFWORD(); + short minRightSideBearing = reader.ReadFWORD(); + short xMaxExtent = reader.ReadFWORD(); + short caretSlopeRise = reader.ReadInt16(); + short caretSlopeRun = reader.ReadInt16(); + short caretOffset = reader.ReadInt16(); + reader.ReadInt16(); // reserved + reader.ReadInt16(); // reserved + reader.ReadInt16(); // reserved + reader.ReadInt16(); // reserved + short metricDataFormat = reader.ReadInt16(); // 0 + if (metricDataFormat != 0) + { + throw new InvalidFontTableException($"Expected metricDataFormat = 0 found {metricDataFormat}", TableName); + } + + ushort numberOfHMetrics = reader.ReadUInt16(); + + return new HorizontalHeadTable( + ascender, + descender, + lineGap, + advanceWidthMax, + minLeftSideBearing, + minRightSideBearing, + xMaxExtent, + caretSlopeRise, + caretSlopeRun, + caretOffset, + numberOfHMetrics); + } + } +} diff --git a/SixLabors.Fonts/Tables/General/HorizontalMetricsTable.cs b/SixLabors.Fonts/Tables/General/HorizontalMetricsTable.cs new file mode 100644 index 0000000..2ab128b --- /dev/null +++ b/SixLabors.Fonts/Tables/General/HorizontalMetricsTable.cs @@ -0,0 +1,122 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General { + /// + /// Represents the horizontal metrics table, which contains the horizontal layout metrics + /// (advance widths and left side bearings) for each glyph. + /// + /// + internal sealed class HorizontalMetricsTable : Table + { + /// + /// The table name identifier. + /// + internal const string TableName = "hmtx"; + + /// + /// The left side bearings array for all glyphs. + /// + private readonly short[] leftSideBearings; + + /// + /// The advance widths array for glyphs with full metric records. + /// + private readonly ushort[] advancedWidths; + + /// + /// Initializes a new instance of the class. + /// + /// The advance widths for each glyph. + /// The left side bearings for each glyph. + public HorizontalMetricsTable(ushort[] advancedWidths, short[] leftSideBearings) + { + this.advancedWidths = advancedWidths; + this.leftSideBearings = leftSideBearings; + } + + /// + /// Gets the advance width for the specified glyph. If the glyph index exceeds the + /// number of metric records, the last record's advance width is returned. + /// + /// The glyph index. + /// The advance width in font design units. + public ushort GetAdvancedWidth(int glyphIndex) + { + if (glyphIndex >= this.advancedWidths.Length) + { + // Records are indexed by glyph ID. As an optimization, the number of records can + // be less than the number of glyphs, in which case the advance width value of the + // last record applies to all remaining glyph IDs. + return this.advancedWidths[^1]; + } + + return this.advancedWidths[glyphIndex]; + } + + /// + /// Gets the left side bearing for the specified glyph. + /// + /// The glyph index. + /// The left side bearing in font design units. + internal short GetLeftSideBearing(int glyphIndex) + { + if (glyphIndex >= this.leftSideBearings.Length) + { + return this.leftSideBearings[^1]; + } + + return this.leftSideBearings[glyphIndex]; + } + + /// + /// Loads the from the specified font reader. + /// + /// The font reader. + /// The . + public static HorizontalMetricsTable Load(FontReader reader) + { + // you should load all dependent tables prior to manipulating the reader + HorizontalHeadTable headTable = reader.GetTable(); + MaximumProfileTable profileTable = reader.GetTable(); + + // Move to start of table + using BigEndianBinaryReader binaryReader = reader.GetReaderAtTablePosition(TableName); + return Load(binaryReader, headTable.NumberOfHMetrics, profileTable.GlyphCount); + } + + /// + /// Loads the from the specified binary reader. + /// + /// The big-endian binary reader. + /// The number of horizontal metric records (from 'hhea'). + /// The total number of glyphs in the font (from 'maxp'). + /// The . + public static HorizontalMetricsTable Load(BigEndianBinaryReader reader, int metricCount, int glyphCount) + { + // Type | Name | Description + // longHorMetric | hMetrics[numberOfHMetrics] | Paired advance width and left side bearing values for each glyph. Records are indexed by glyph ID. + // int16 | leftSideBearing[numGlyphs - numberOfHMetrics] | Left side bearings for glyph IDs greater than or equal to numberOfHMetrics. + int bearingCount = glyphCount - metricCount; + ushort[] advancedWidth = new ushort[metricCount]; + short[] leftSideBearings = new short[glyphCount]; + + for (int i = 0; i < metricCount; i++) + { + // longHorMetric Record: + // Type | Name | Description + // uint16 | advanceWidth | Glyph advance width, in font design units. + // int16 | lsb | Glyph left side bearing, in font design units. + advancedWidth[i] = reader.ReadUInt16(); + leftSideBearings[i] = reader.ReadInt16(); + } + + for (int i = 0; i < bearingCount; i++) + { + leftSideBearings[metricCount + i] = reader.ReadInt16(); + } + + return new HorizontalMetricsTable(advancedWidth, leftSideBearings); + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Kern/Format0SubTable.cs b/SixLabors.Fonts/Tables/General/Kern/Format0SubTable.cs new file mode 100644 index 0000000..4fd3103 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Kern/Format0SubTable.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Tables.General.Kern { + internal sealed class Format0SubTable : KerningSubTable + { + private readonly KerningPair[] pairs; + + public Format0SubTable(KerningPair[] pairs, KerningCoverage coverage) + : base(coverage) + => this.pairs = pairs; + + public static Format0SubTable Load(BigEndianBinaryReader reader, in KerningCoverage coverage) + { + // Type | Field | Description + // -------|---------------|-------------------------------------------------------- + // uint16 | nPairs | This gives the number of kerning pairs in the table. + // uint16 | searchRange | The largest power of two less than or equal to the value of nPairs, multiplied by the size in bytes of an entry in the table. + // uint16 | entrySelector | This is calculated as log2 of the largest power of two less than or equal to the value of nPairs. + // | | This value indicates how many iterations of the search loop will have to be made. (For example, in a list of eight items, there would have to be three iterations of the loop). + // uint16 | rangeShift | The value of nPairs minus the largest power of two less than or equal to nPairs, and then multiplied by the size in bytes of an entry in the table. + ushort pairCount = reader.ReadUInt16(); + ushort searchRange = reader.ReadUInt16(); + ushort entrySelector = reader.ReadUInt16(); + ushort rangeShift = reader.ReadUInt16(); + + KerningPair[] pairs = new KerningPair[pairCount]; + for (int i = 0; i < pairCount; i++) + { + pairs[i] = KerningPair.Read(reader); + } + + return new Format0SubTable(pairs, coverage); + } + + protected override bool TryGetOffset(ushort index1, ushort index2, out short offset) + { + int index = this.pairs.AsSpan().BinarySearch(new KerningPair(index1, index2, 0)); + + if (index >= 0) + { + ref KerningPair pair = ref this.pairs[index]; + offset = pair.Offset; + return true; + } + + offset = 0; + return false; + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Kern/KerningCoverage.cs b/SixLabors.Fonts/Tables/General/Kern/KerningCoverage.cs new file mode 100644 index 0000000..6c00343 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Kern/KerningCoverage.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Kern { + /// + /// Represents the coverage field of a kerning subtable, describing the format + /// and properties of the kerning data. + /// + /// + internal readonly struct KerningCoverage + { + /// + /// Initializes a new instance of the struct. + /// + /// Whether the table contains horizontal kerning data. + /// Whether the table contains minimum values instead of kerning values. + /// Whether kerning is perpendicular to the flow of text. + /// Whether the kerning value should replace the currently accumulated value. + /// The subtable format number. + private KerningCoverage(bool horizontal, bool hasMinimum, bool crossStream, bool overrideAccumulator, byte format) + { + this.Horizontal = horizontal; + this.HasMinimum = hasMinimum; + this.CrossStream = crossStream; + this.OverrideAccumulator = overrideAccumulator; + this.Format = format; + } + + /// + /// Gets a value indicating whether the table contains horizontal kerning data. + /// If , the table contains vertical kerning data. + /// + public bool Horizontal { get; } + + /// + /// Gets a value indicating whether the table contains minimum values. + /// If , the table contains kerning values. + /// + public bool HasMinimum { get; } + + /// + /// Gets a value indicating whether kerning is perpendicular to the flow of text. + /// + public bool CrossStream { get; } + + /// + /// Gets a value indicating whether the value in this table should replace + /// the value currently being accumulated. + /// + public bool OverrideAccumulator { get; } + + /// + /// Gets the format of the subtable. Only formats 0 and 2 have been defined. + /// + public byte Format { get; } + + /// + /// Reads a from the specified binary reader. + /// + /// The binary reader positioned at the coverage field. + /// The parsed . + public static KerningCoverage Read(BigEndianBinaryReader reader) + { + // The coverage field is divided into the following sub-fields, with sizes given in bits: + // Sub-field | Bits #'s | Size | Description + // -------------|----------|------|----------------------------------------------- + // horizontal | 0 | 1 | 1 if table has horizontal data, 0 if vertical. + // minimum | 1 | 1 | If this bit is set to 1, the table has minimum values.If set to 0, the table has kerning values. + // cross-stream | 2 | 1 | If set to 1, kerning is perpendicular to the flow of the text. + // If the text is normally written horizontally, kerning will be done in the up and down directions.If kerning values are positive, the text will be kerned upwards; if they are negative, the text will be kerned downwards. + // If the text is normally written vertically, kerning will be done in the left and right directions.If kerning values are positive, the text will be kerned to the right; if they are negative, the text will be kerned to the left. + // The value 0x8000 in the kerning data resets the cross-stream kerning back to 0. + // override | 3 | 1 | If this bit is set to 1 the value in this table should replace the value currently being accumulated. + // reserved1 | 4 -7 | 4 | Reserved.This should be set to zero. + // format | 8 -15 | 8 | Format of the subtable. Only formats 0 and 2 have been defined.Formats 1 and 3 through 255 are reserved for future use. + ushort coverage = reader.ReadUInt16(); + bool horizontal = (coverage & 0x1) == 1; + bool hasMinimum = ((coverage >> 1) & 0x1) == 1; + bool crossStream = ((coverage >> 2) & 0x1) == 1; + bool overrideAccumulator = ((coverage >> 3) & 0x1) == 1; + byte format = (byte)((coverage >> 7) & 0xff); + return new KerningCoverage(horizontal, hasMinimum, crossStream, overrideAccumulator, format); + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Kern/KerningPair.cs b/SixLabors.Fonts/Tables/General/Kern/KerningPair.cs new file mode 100644 index 0000000..642e8a9 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Kern/KerningPair.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Tables.General.Kern { + /// + /// Represents a kerning pair entry in the OpenType 'kern' table, mapping a pair of + /// glyph indices to a kerning offset value. + /// + /// + internal readonly struct KerningPair : IComparable + { + /// + /// Initializes a new instance of the struct. + /// + /// The glyph index for the left-hand glyph in the kerning pair. + /// The glyph index for the right-hand glyph in the kerning pair. + /// The kerning offset value in font design units. + internal KerningPair(ushort left, ushort right, short offset) + { + this.Left = left; + this.Right = right; + this.Offset = offset; + this.Key = CalculateKey(left, right); + } + + /// + /// Gets the composite key derived from the left and right glyph indices, used for fast lookup. + /// + public uint Key { get; } + + /// + /// Gets the glyph index for the left-hand glyph in the kerning pair. + /// + public ushort Left { get; } + + /// + /// Gets the glyph index for the right-hand glyph in the kerning pair. + /// + public ushort Right { get; } + + /// + /// Gets the kerning offset value in font design units. + /// Positive values move glyphs apart; negative values move them closer together. + /// + public short Offset { get; } + + /// + /// Calculates a composite lookup key from a pair of glyph indices. + /// + /// The left glyph index. + /// The right glyph index. + /// A 32-bit key combining both glyph indices. + public static uint CalculateKey(ushort left, ushort right) + { + uint value = (uint)(left << 16); + return value + right; + } + + /// + /// Reads a from the specified binary reader. + /// + /// The binary reader positioned at the start of the kerning pair data. + /// The parsed . + public static KerningPair Read(BigEndianBinaryReader reader) + + // Type | Field | Description + // -------|-------|------------------------------- + // uint16 | left | The glyph index for the left-hand glyph in the kerning pair. + // uint16 | right | The glyph index for the right-hand glyph in the kerning pair. + // FWORD | value | The kerning value for the above pair, in FUnits.If this value is greater than zero, the characters will be moved apart.If this value is less than zero, the character will be moved closer together. + => new KerningPair(reader.ReadUInt16(), reader.ReadUInt16(), reader.ReadFWORD()); + + /// + /// Compares this kerning pair to another based on the composite key. + /// + /// The other kerning pair to compare to. + /// A value indicating the relative order of the kerning pairs. + public int CompareTo(KerningPair other) + => this.Key.CompareTo(other.Key); + } +} diff --git a/SixLabors.Fonts/Tables/General/Kern/KerningSubTable.cs b/SixLabors.Fonts/Tables/General/Kern/KerningSubTable.cs new file mode 100644 index 0000000..d3d1ea3 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Kern/KerningSubTable.cs @@ -0,0 +1,111 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts.Tables.General.Kern { + /// + /// Represents a kerning subtable in the OpenType 'kern' table. + /// Each subtable contains kerning data in a specific format. + /// + /// + internal abstract class KerningSubTable + { + /// + /// The coverage flags describing the properties of this subtable. + /// + private readonly KerningCoverage coverage; + + /// + /// Initializes a new instance of the class. + /// + /// The coverage flags for this subtable. + public KerningSubTable(KerningCoverage coverage) + => this.coverage = coverage; + + /// + /// Loads a from the specified binary reader. + /// Returns if the subtable format is not supported. + /// + /// The binary reader positioned at the start of the subtable header. + /// The loaded , or for unsupported formats. + public static KerningSubTable? Load(BigEndianBinaryReader reader) + { + // Kerning subtables will share the same header format. + // This header is used to identify the format of the subtable and the kind of information it contains: + // +--------+----------+----------------------------------------------------------+ + // | Type | Field | Description | + // +========+==========+==========================================================+ + // | uint16 | version | Kern subtable version number | + // +--------+----------+----------------------------------------------------------+ + // | uint16 | length | Length of the subtable, in bytes(including this header). | + // +--------+----------+----------------------------------------------------------+ + // | uint16 | coverage | What type of information is contained in this table. | + // +--------+----------+----------------------------------------------------------+ + ushort subVersion = reader.ReadUInt16(); + ushort length = reader.ReadUInt16(); + KerningCoverage coverage = KerningCoverage.Read(reader); + if (coverage.Format == 0) + { + return Format0SubTable.Load(reader, coverage); + } + else + { + // we don't support versions other than 'Format 0' same as Windows + return null; + } + } + + /// + /// Attempts to get the kerning offset for the specified pair of glyph indices. + /// + /// The glyph index of the first (left) glyph. + /// The glyph index of the second (right) glyph. + /// When this method returns, contains the kerning offset if found. + /// if a kerning value was found; otherwise, . + protected abstract bool TryGetOffset(ushort index1, ushort index2, out short offset); + + /// + /// Attempts to apply the kerning offset for the specified glyph pair to the result vector. + /// The offset is applied to the X component for horizontal kerning or the Y component for vertical kerning. + /// + /// The glyph index of the first (left) glyph. + /// The glyph index of the second (right) glyph. + /// The vector to which the kerning offset is applied. + /// if a kerning offset was applied; otherwise, . + public bool TryApplyOffset(ushort index1, ushort index2, ref Vector2 result) + { + if (this.TryGetOffset(index1, index2, out short offset)) + { + if (this.coverage.Horizontal) + { + // apply to X + if (this.coverage.OverrideAccumulator) + { + result.X = offset; + } + else + { + result.X += offset; + } + } + else + { + // apply to Y + if (this.coverage.OverrideAccumulator) + { + result.Y = offset; + } + else + { + result.Y += offset; + } + } + + return true; + } + + return false; + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs b/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs new file mode 100644 index 0000000..a7f4a7a --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs @@ -0,0 +1,144 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Numerics; + +namespace SixLabors.Fonts.Tables.General.Kern { + /// + /// Represents the OpenType 'kern' table, which contains kerning pair adjustments + /// for positioning glyphs within a font. + /// + /// + internal sealed class KerningTable : Table + { + /// + /// The table tag name identifying the 'kern' table. + /// + internal const string TableName = "kern"; + + /// + /// The array of kerning subtables contained in this table. + /// + private readonly KerningSubTable[] kerningSubTable; + + /// + /// Initializes a new instance of the class. + /// + /// The array of kerning subtables. + public KerningTable(KerningSubTable[] kerningSubTable) + => this.kerningSubTable = kerningSubTable; + + /// + /// Gets the number of kerning subtables. + /// + public int Count => this.kerningSubTable.Length; + + /// + /// Loads the from the specified font reader. + /// Returns an empty table if the 'kern' table is not present. + /// + /// The font reader to read the table from. + /// The loaded . + public static KerningTable Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + // this table is optional. + return new KerningTable([]); + } + + using (binaryReader) + { + // Move to start of table. + return Load(binaryReader); + } + } + + /// + /// Loads the from the specified binary reader. + /// + /// The binary reader positioned at the start of the kern table data. + /// The loaded . + public static KerningTable Load(BigEndianBinaryReader reader) + { + // +--------+---------+-------------------------------------------+ + // | Type | Field | Description | + // +========+=========+===========================================+ + // | uint16 | version | Table version number(0) | + // +--------+---------+-------------------------------------------+ + // | uint16 | nTables | Number of subtables in the kerning table. | + // +--------+---------+-------------------------------------------+ + ushort version = reader.ReadUInt16(); + ushort subTableCount = reader.ReadUInt16(); + + List tables = new(subTableCount); + for (int i = 0; i < subTableCount; i++) + { + KerningSubTable? t = KerningSubTable.Load(reader); // returns null for unknown/supported table format + if (t != null) + { + tables.Add(t); + } + } + + return new KerningTable([.. tables]); + } + + /// + /// Updates glyph positions by applying kerning adjustments for the specified glyph pair. + /// + /// The font metrics used for position calculations. + /// The glyph positioning collection to update. + /// The index of the left glyph in the collection. + /// The index of the right glyph in the collection. + public void UpdatePositions(FontMetrics fontMetrics, GlyphPositioningCollection collection, int left, int right) + { + if (this.Count == 0 || collection.Count == 0) + { + return; + } + + GlyphShapingData current = collection[left]; + if (current.IsKerned) + { + // Already kerned via previous processing. + return; + } + + ushort currentId = current.GlyphId; + ushort nextId = collection[right].GlyphId; + + if (this.TryGetKerningOffset(currentId, nextId, out Vector2 result)) + { + collection.Advance(fontMetrics, left, currentId, (short)result.X, (short)result.Y); + current.IsKerned = true; + } + } + + /// + /// Attempts to get the accumulated kerning offset for the specified pair of glyph indices + /// by iterating through all kerning subtables. + /// + /// The glyph index of the current (left) glyph. + /// The glyph index of the next (right) glyph. + /// When this method returns, contains the accumulated kerning offset vector. + /// if any kerning was applied; otherwise, . + public bool TryGetKerningOffset(ushort current, ushort next, out Vector2 result) + { + result = Vector2.Zero; + if (this.Count == 0 || current == 0 || next == 0) + { + return false; + } + + bool kerned = false; + foreach (KerningSubTable sub in this.kerningSubTable) + { + kerned |= sub.TryApplyOffset(current, next, ref result); + } + + return kerned; + } + } +} diff --git a/SixLabors.Fonts/Tables/General/MaximumProfileTable.cs b/SixLabors.Fonts/Tables/General/MaximumProfileTable.cs new file mode 100644 index 0000000..ea3b1c6 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/MaximumProfileTable.cs @@ -0,0 +1,213 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General { + /// + /// Represents the maximum profile table, which establishes memory requirements for the font. + /// + /// + internal sealed class MaximumProfileTable : Table + { + /// + /// The table name identifier. + /// + internal const string TableName = "maxp"; + + /// + /// Initializes a new instance of the class + /// for version 0.5 (CFF fonts specifying only the glyph count). + /// + /// The number of glyphs in the font. + public MaximumProfileTable(ushort numGlyphs) + => this.GlyphCount = numGlyphs; + + /// + /// Initializes a new instance of the class + /// for version 1.0 (TrueType fonts with all fields). + /// + /// The number of glyphs in the font. + /// The maximum points in a non-composite glyph. + /// The maximum contours in a non-composite glyph. + /// The maximum points in a composite glyph. + /// The maximum contours in a composite glyph. + /// The maximum zones (1 if no twilight zone, 2 otherwise). + /// The maximum points used in the twilight zone (Z0). + /// The number of storage area locations. + /// The number of FDEFs. + /// The number of IDEFs. + /// The maximum stack depth. + /// The maximum byte count for glyph instructions. + /// The maximum number of components at top level for any composite glyph. + /// The maximum levels of recursion. + public MaximumProfileTable(ushort numGlyphs, ushort maxPoints, ushort maxContours, ushort maxCompositePoints, ushort maxCompositeContours, ushort maxZones, ushort maxTwilightPoints, ushort maxStorage, ushort maxFunctionDefs, ushort maxInstructionDefs, ushort maxStackElements, ushort maxSizeOfInstructions, ushort maxComponentElements, ushort maxComponentDepth) + : this(numGlyphs) + { + this.MaxPoints = maxPoints; + this.MaxContours = maxContours; + this.MaxCompositePoints = maxCompositePoints; + this.MaxCompositeContours = maxCompositeContours; + this.MaxZones = maxZones; + this.MaxTwilightPoints = maxTwilightPoints; + this.MaxStorage = maxStorage; + this.MaxFunctionDefs = maxFunctionDefs; + this.MaxInstructionDefs = maxInstructionDefs; + this.MaxStackElements = maxStackElements; + this.MaxSizeOfInstructions = maxSizeOfInstructions; + this.MaxComponentElements = maxComponentElements; + this.MaxComponentDepth = maxComponentDepth; + } + + /// + /// Gets the maximum points in a non-composite glyph. + /// + public ushort MaxPoints { get; } + + /// + /// Gets the maximum contours in a non-composite glyph. + /// + public ushort MaxContours { get; } + + /// + /// Gets the maximum points in a composite glyph. + /// + public ushort MaxCompositePoints { get; } + + /// + /// Gets the maximum contours in a composite glyph. + /// + public ushort MaxCompositeContours { get; } + + /// + /// Gets the maximum zones (1 if instructions do not use the twilight zone, otherwise 2). + /// + public ushort MaxZones { get; } + + /// + /// Gets the maximum points used in the twilight zone (Z0). + /// + public ushort MaxTwilightPoints { get; } + + /// + /// Gets the number of storage area locations. + /// + public ushort MaxStorage { get; } + + /// + /// Gets the number of FDEFs (equals to the highest function number + 1). + /// + public ushort MaxFunctionDefs { get; } + + /// + /// Gets the number of IDEFs. + /// + public ushort MaxInstructionDefs { get; } + + /// + /// Gets the maximum stack depth. + /// + public ushort MaxStackElements { get; } + + /// + /// Gets the maximum byte count for glyph instructions. + /// + public ushort MaxSizeOfInstructions { get; } + + /// + /// Gets the maximum number of components referenced at top level for any composite glyph. + /// + public ushort MaxComponentElements { get; } + + /// + /// Gets the maximum levels of recursion (1 for simple components). + /// + public ushort MaxComponentDepth { get; } + + /// + /// Gets the number of glyphs in the font. + /// + public ushort GlyphCount { get; } + + /// + /// Loads the from the specified font reader. + /// + /// The font reader. + /// The . + public static MaximumProfileTable Load(FontReader reader) + { + using (BigEndianBinaryReader r = reader.GetReaderAtTablePosition(TableName)) + { + return Load(r); + } + } + + /// + /// Loads the from the specified binary reader. + /// + /// The big-endian binary reader. + /// The . + public static MaximumProfileTable Load(BigEndianBinaryReader reader) + { + // This table establishes the memory requirements for this font.Fonts with CFF data must use Version 0.5 of this table, specifying only the numGlyphs field.Fonts with TrueType outlines must use Version 1.0 of this table, where all data is required. + // Version 0.5 + // Type | Name | Description + // -------|----------------------|--------------------------------------- + // Fixed | Table version number | 0x00005000 for version 0.5 (Note the difference in the representation of a non - zero fractional part, in Fixed numbers.) + // uint16 | numGlyphs | The number of glyphs in the font. + float version = reader.ReadFixed(); + ushort numGlyphs = reader.ReadUInt16(); + if (version == 0.5) + { + return new MaximumProfileTable(numGlyphs); + } + + // Version 1.0 + // Type | Name | Description + // -------|-----------------------|--------------------------------------- + // *Fixed | Table version number | 0x00010000 for version 1.0. + // *uint16| numGlyphs | The number of glyphs in the font. + // uint16 | maxPoints | Maximum points in a non - composite glyph. + // uint16 | maxContours | Maximum contours in a non - composite glyph. + // uint16 | maxCompositePoints | Maximum points in a composite glyph. + // uint16 | maxCompositeContours | Maximum contours in a composite glyph. + // uint16 | maxZones | 1 if instructions do not use the twilight zone (Z0), or 2 if instructions do use Z0; should be set to 2 in most cases. + // uint16 | maxTwilightPoints | Maximum points used in Z0. + // uint16 | maxStorage | Number of Storage Area locations. + // uint16 | maxFunctionDefs | Number of FDEFs, equals to the highest function number +1. + // uint16 | maxInstructionDefs | Number of IDEFs. + // uint16 | maxStackElements | Maximum stack depth2. + // uint16 | maxSizeOfInstructions | Maximum byte count for glyph instructions. + // uint16 | maxComponentElements | Maximum number of components referenced at "top level" for any composite glyph. + // uint16 | maxComponentDepth | Maximum levels of recursion; 1 for simple components. + ushort maxPoints = reader.ReadUInt16(); + ushort maxContours = reader.ReadUInt16(); + ushort maxCompositePoints = reader.ReadUInt16(); + ushort maxCompositeContours = reader.ReadUInt16(); + + ushort maxZones = reader.ReadUInt16(); + ushort maxTwilightPoints = reader.ReadUInt16(); + ushort maxStorage = reader.ReadUInt16(); + ushort maxFunctionDefs = reader.ReadUInt16(); + ushort maxInstructionDefs = reader.ReadUInt16(); + ushort maxStackElements = reader.ReadUInt16(); + ushort maxSizeOfInstructions = reader.ReadUInt16(); + ushort maxComponentElements = reader.ReadUInt16(); + ushort maxComponentDepth = reader.ReadUInt16(); + + return new MaximumProfileTable( + numGlyphs, + maxPoints, + maxContours, + maxCompositePoints, + maxCompositeContours, + maxZones, + maxTwilightPoints, + maxStorage, + maxFunctionDefs, + maxInstructionDefs, + maxStackElements, + maxSizeOfInstructions, + maxComponentElements, + maxComponentDepth); + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Name/NameRecord.cs b/SixLabors.Fonts/Tables/General/Name/NameRecord.cs new file mode 100644 index 0000000..c4f8065 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Name/NameRecord.cs @@ -0,0 +1,81 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Text; +using SixLabors.Fonts.Utilities; +using SixLabors.Fonts.WellKnownIds; + +namespace SixLabors.Fonts.Tables.General.Name { + /// + /// Represents a single name record entry in the OpenType 'name' table. + /// + /// + internal class NameRecord + { + /// + /// The resolved string value for this name record. + /// + private readonly string value; + + /// + /// Initializes a new instance of the class. + /// + /// The platform identifier. + /// The language identifier. + /// The name identifier. + /// The string value of the name record. + public NameRecord(PlatformIDs platform, ushort languageId, KnownNameIds nameId, string value) + { + this.Platform = platform; + this.LanguageID = languageId; + this.NameID = nameId; + this.value = value; + } + + /// + /// Gets the platform identifier for this name record. + /// + public PlatformIDs Platform { get; } + + /// + /// Gets the platform-specific language identifier for this name record. + /// + public ushort LanguageID { get; } + + /// + /// Gets the name identifier indicating what kind of name this record contains. + /// + public KnownNameIds NameID { get; } + + /// + /// Gets the string loader used to lazily read the string value from the font data. + /// + internal StringLoader? StringReader { get; private set; } + + /// + /// Gets the resolved string value for this name record. + /// + public string Value => this.StringReader?.Value ?? this.value; + + /// + /// Reads a from the specified binary reader. + /// + /// The binary reader positioned at the start of the name record. + /// The parsed . + public static NameRecord Read(BigEndianBinaryReader reader) + { + PlatformIDs platform = reader.ReadUInt16(); + EncodingIDs encodingId = reader.ReadUInt16(); + Encoding encoding = encodingId.AsEncoding(); + ushort languageID = reader.ReadUInt16(); + KnownNameIds nameID = reader.ReadUInt16(); + + StringLoader stringReader = StringLoader.Create(reader, encoding); + + return new NameRecord(platform, languageID, nameID, string.Empty) + { + StringReader = stringReader + }; + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Name/NameTable.cs b/SixLabors.Fonts/Tables/General/Name/NameTable.cs new file mode 100644 index 0000000..766e463 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Name/NameTable.cs @@ -0,0 +1,195 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using SixLabors.Fonts.Utilities; +using SixLabors.Fonts.WellKnownIds; + +namespace SixLabors.Fonts.Tables.General.Name { + /// + /// Represents the OpenType 'name' table, which contains human-readable string names + /// for features, font metadata, and other descriptive information. + /// + /// + internal class NameTable : Table + { + /// + /// The table tag name identifying the 'name' table. + /// + internal const string TableName = "name"; + + /// + /// The array of name records contained in this table. + /// + private readonly NameRecord[] names; + + /// + /// Initializes a new instance of the class. + /// + /// The name records. + /// The language tag strings (format 1 only). + internal NameTable(NameRecord[] names, string[] languages) + => this.names = names; + + /// + /// Gets the unique font identifier for the specified culture. + /// + /// The culture used to select the appropriate language-specific name. + /// The unique font identifier string. + public string Id(CultureInfo culture) + => this.GetNameById(culture, KnownNameIds.UniqueFontID); + + /// + /// Gets the full font name for the specified culture. + /// + /// The culture used to select the appropriate language-specific name. + /// The full font name string. + public string FontName(CultureInfo culture) + => this.GetNameById(culture, KnownNameIds.FullFontName); + + /// + /// Gets the font family name for the specified culture. + /// + /// The culture used to select the appropriate language-specific name. + /// The font family name string. + public string FontFamilyName(CultureInfo culture) + => this.GetNameById(culture, KnownNameIds.FontFamilyName); + + /// + /// Gets the font subfamily name (e.g., "Bold", "Italic") for the specified culture. + /// + /// The culture used to select the appropriate language-specific name. + /// The font subfamily name string. + public string FontSubFamilyName(CultureInfo culture) + => this.GetNameById(culture, KnownNameIds.FontSubfamilyName); + + /// + /// Gets the name string for the specified culture and name identifier. + /// Falls back to US English (0x0409), then the first Windows platform record, then any record. + /// + /// The culture used to select the appropriate language-specific name. + /// The name identifier to look up. + /// The name string, or if not found. + public string GetNameById(CultureInfo culture, KnownNameIds nameId) + { + int languageId = culture.LCID; + NameRecord? usaVersion = null; + NameRecord? firstWindows = null; + NameRecord? first = null; + foreach (NameRecord name in this.names) + { + if (name.NameID == nameId) + { + // Get just the first one, just in case. + first ??= name; + if (name.Platform == PlatformIDs.Windows) + { + // If us not found return the first windows one. + firstWindows ??= name; + if (name.LanguageID == 0x0409) + { + // Grab the us version as its on next best match. + usaVersion ??= name; + } + + if (name.LanguageID == languageId) + { + // Return the most exact first. + return name.Value; + } + } + } + } + + return usaVersion?.Value ?? + firstWindows?.Value ?? + first?.Value ?? + string.Empty; + } + + /// + /// Gets the name string for the specified culture and raw name identifier. + /// + /// The culture used to select the appropriate language-specific name. + /// The raw name identifier value to look up. + /// The name string, or if not found. + public string GetNameById(CultureInfo culture, ushort nameId) + => this.GetNameById(culture, (KnownNameIds)nameId); + + /// + /// Loads the from the specified font reader. + /// + /// The font reader to read the table from. + /// The loaded . + /// Thrown when the table is missing from the font. + public static NameTable? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + throw new InvalidFontTableException($"Table '{TableName}' is missing", TableName); + } + + using (binaryReader) + { + // Move to start of table. + return Load(binaryReader); + } + } + + /// + /// Loads the from the specified binary reader. + /// + /// The binary reader positioned at the start of the name table data. + /// The loaded . + public static NameTable Load(BigEndianBinaryReader reader) + { + var strings = new List(); + ushort format = reader.ReadUInt16(); + ushort nameCount = reader.ReadUInt16(); + ushort stringOffset = reader.ReadUInt16(); + + var names = new NameRecord[nameCount]; + + for (int i = 0; i < nameCount; i++) + { + names[i] = NameRecord.Read(reader); + StringLoader? sr = names[i].StringReader; + if (sr is not null) + { + strings.Add(sr); + } + } + + StringLoader[]? langs = Array.Empty(); + if (format == 1) + { + // Format 1 adds language data. + ushort langCount = reader.ReadUInt16(); + langs = new StringLoader[langCount]; + + for (int i = 0; i < langCount; i++) + { + langs[i] = StringLoader.Create(reader); + strings.Add(langs[i]); + } + } + + foreach (StringLoader readable in strings) + { + int readableStartOffset = stringOffset + readable.Offset; + + reader.Seek(readableStartOffset, SeekOrigin.Begin); + + readable.LoadValue(reader); + } + + string[] langNames = langs?.Select(x => x.Value).ToArray() ?? Array.Empty(); + + return new NameTable(names, langNames); + } + } +} diff --git a/SixLabors.Fonts/Tables/General/OS2Table.cs b/SixLabors.Fonts/Tables/General/OS2Table.cs new file mode 100644 index 0000000..e4a7ba5 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/OS2Table.cs @@ -0,0 +1,627 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Tables.General { + /// + /// Represents the OS/2 and Windows metrics table, which contains metrics required for Windows and OS/2. + /// + /// + internal sealed class OS2Table : Table + { + /// + /// The table name identifier. + /// + internal const string TableName = "OS/2"; + + /// + /// The font embedding licensing rights (fsType). + /// + private readonly ushort styleType; + + /// + /// The PANOSE classification number. + /// + private readonly byte[] panose; + + /// + /// The cap height in font design units. + /// + private readonly short capHeight; + + /// + /// The font family class and subclass (sFamilyClass). + /// + private readonly short familyClass; + + /// + /// The x-height in font design units. + /// + private readonly short heightX; + + /// + /// The four-character font vendor identification tag. + /// + private readonly string tag; + + /// + /// The code page range bits 0-31. + /// + private readonly ushort codePageRange1; + + /// + /// The code page range bits 32-63. + /// + private readonly ushort codePageRange2; + + /// + /// The Unicode range bits 0-31. + /// + private readonly uint unicodeRange1; + + /// + /// The Unicode range bits 32-63. + /// + private readonly uint unicodeRange2; + + /// + /// The Unicode range bits 64-95. + /// + private readonly uint unicodeRange3; + + /// + /// The Unicode range bits 96-127. + /// + private readonly uint unicodeRange4; + + /// + /// The break character (usBreakChar). + /// + private readonly ushort breakChar; + + /// + /// The default character displayed when a requested character is not in the font. + /// + private readonly ushort defaultChar; + + /// + /// The minimum Unicode index in this font. + /// + private readonly ushort firstCharIndex; + + /// + /// The maximum Unicode index in this font. + /// + private readonly ushort lastCharIndex; + + /// + /// The lower value of the size range for which this font is designed (version 5+). + /// + private readonly ushort lowerOpticalPointSize; + + /// + /// The maximum length of a target glyph context for any feature in this font. + /// + private readonly ushort maxContext; + + /// + /// The upper value of the size range for which this font is designed (version 5+). + /// + private readonly ushort upperOpticalPointSize; + + /// + /// The visual weight class of the font (usWeightClass). + /// + private readonly ushort weightClass; + + /// + /// The relative change from the normal aspect ratio (usWidthClass). + /// + private readonly ushort widthClass; + + /// + /// The average weighted width of the lower case letters and space. + /// + private readonly short averageCharWidth; + + /// + /// Initializes a new instance of the class with version 0 fields. + /// + /// The average character width. + /// The visual weight class. + /// The relative width class. + /// The embedding licensing rights. + /// The horizontal size for subscripts. + /// The vertical size for subscripts. + /// The horizontal offset for subscripts. + /// The vertical offset for subscripts. + /// The horizontal size for superscripts. + /// The vertical size for superscripts. + /// The horizontal offset for superscripts. + /// The vertical offset for superscripts. + /// The width of the strikeout stroke. + /// The position of the strikeout stroke relative to the baseline. + /// The font family class and subclass. + /// The PANOSE classification bytes. + /// Unicode range bits 0-31. + /// Unicode range bits 32-63. + /// Unicode range bits 64-95. + /// Unicode range bits 96-127. + /// The four-character vendor identification tag. + /// The font style selection flags. + /// The minimum Unicode index. + /// The maximum Unicode index. + /// The typographic ascender. + /// The typographic descender. + /// The typographic line gap. + /// The Windows ascent metric. + /// The Windows descent metric. + public OS2Table( + short averageCharWidth, + ushort weightClass, + ushort widthClass, + ushort styleType, + short subscriptXSize, + short subscriptYSize, + short subscriptXOffset, + short subscriptYOffset, + short superscriptXSize, + short superscriptYSize, + short superscriptXOffset, + short superscriptYOffset, + short strikeoutSize, + short strikeoutPosition, + short familyClass, + byte[] panose, + uint unicodeRange1, + uint unicodeRange2, + uint unicodeRange3, + uint unicodeRange4, + string tag, + FontStyleSelection fontStyle, + ushort firstCharIndex, + ushort lastCharIndex, + short typoAscender, + short typoDescender, + short typoLineGap, + ushort winAscent, + ushort winDescent) + { + this.averageCharWidth = averageCharWidth; + this.weightClass = weightClass; + this.widthClass = widthClass; + this.styleType = styleType; + this.SubscriptXSize = subscriptXSize; + this.SubscriptYSize = subscriptYSize; + this.SubscriptXOffset = subscriptXOffset; + this.SubscriptYOffset = subscriptYOffset; + this.SuperscriptXSize = superscriptXSize; + this.SuperscriptYSize = superscriptYSize; + this.SuperscriptXOffset = superscriptXOffset; + this.SuperscriptYOffset = superscriptYOffset; + this.StrikeoutSize = strikeoutSize; + this.StrikeoutPosition = strikeoutPosition; + this.familyClass = familyClass; + this.panose = panose; + this.unicodeRange1 = unicodeRange1; + this.unicodeRange2 = unicodeRange2; + this.unicodeRange3 = unicodeRange3; + this.unicodeRange4 = unicodeRange4; + this.tag = tag; + this.FontStyle = fontStyle; + this.firstCharIndex = firstCharIndex; + this.lastCharIndex = lastCharIndex; + this.TypoAscender = typoAscender; + this.TypoDescender = typoDescender; + this.TypoLineGap = typoLineGap; + this.WinAscent = winAscent; + this.WinDescent = winDescent; + } + + /// + /// Initializes a new instance of the class with version 1-4 fields. + /// + /// The base version 0 table to extend. + /// Code page range bits 0-31. + /// Code page range bits 32-63. + /// The x-height. + /// The cap height. + /// The default character index. + /// The break character index. + /// The maximum target glyph context length. + public OS2Table( + OS2Table version0Table, + ushort codePageRange1, + ushort codePageRange2, + short heightX, + short capHeight, + ushort defaultChar, + ushort breakChar, + ushort maxContext) + : this( + version0Table.averageCharWidth, + version0Table.weightClass, + version0Table.widthClass, + version0Table.styleType, + version0Table.SubscriptXSize, + version0Table.SubscriptYSize, + version0Table.SubscriptXOffset, + version0Table.SubscriptYOffset, + version0Table.SuperscriptXSize, + version0Table.SuperscriptYSize, + version0Table.SuperscriptXOffset, + version0Table.SuperscriptYOffset, + version0Table.StrikeoutSize, + version0Table.StrikeoutPosition, + version0Table.familyClass, + version0Table.panose, + version0Table.unicodeRange1, + version0Table.unicodeRange2, + version0Table.unicodeRange3, + version0Table.unicodeRange4, + version0Table.tag, + version0Table.FontStyle, + version0Table.firstCharIndex, + version0Table.lastCharIndex, + version0Table.TypoAscender, + version0Table.TypoDescender, + version0Table.TypoLineGap, + version0Table.WinAscent, + version0Table.WinDescent) + { + this.codePageRange1 = codePageRange1; + this.codePageRange2 = codePageRange2; + this.heightX = heightX; + this.capHeight = capHeight; + this.defaultChar = defaultChar; + this.breakChar = breakChar; + this.maxContext = maxContext; + } + + /// + /// Initializes a new instance of the class with version 5 fields. + /// + /// The base table (version < 5) to extend. + /// The lower optical point size. + /// The upper optical point size. + public OS2Table(OS2Table versionLessThan5Table, ushort lowerOpticalPointSize, ushort upperOpticalPointSize) + : this( + versionLessThan5Table, + versionLessThan5Table.codePageRange1, + versionLessThan5Table.codePageRange2, + versionLessThan5Table.heightX, + versionLessThan5Table.capHeight, + versionLessThan5Table.defaultChar, + versionLessThan5Table.breakChar, + versionLessThan5Table.maxContext) + { + this.lowerOpticalPointSize = lowerOpticalPointSize; + this.upperOpticalPointSize = upperOpticalPointSize; + } + + /// + /// Font style selection flags (fsSelection). + /// + [Flags] + internal enum FontStyleSelection : ushort + { + /// + /// No style flags set. + /// + NONE = 0, + + /// + /// Font contains italic or oblique characters. + /// + ITALIC = 1, + + /// + /// Characters are underscored. + /// + UNDERSCORE = 1 << 1, + + /// + /// Characters have their foreground and background reversed. + /// + NEGATIVE = 1 << 2, + + /// + /// Outline (hollow) characters, otherwise they are solid. + /// + OUTLINED = 1 << 3, + + /// + /// Characters are overstruck. + /// + STRIKEOUT = 1 << 4, + + /// + /// Characters are emboldened. + /// + BOLD = 1 << 5, + + /// + /// Characters are in the standard weight/style for the font. + /// + REGULAR = 1 << 6, + + /// + /// If set, it is strongly recommended to use OS/2.typoAscender - OS/2.typoDescender + OS/2.typoLineGap + /// as a value for default line spacing. + /// + USE_TYPO_METRICS = 1 << 7, + + /// + /// The font has ‘name’ table strings consistent with a weight/width/slope family + /// without requiring use of ‘name’ IDs 21 and 22. + /// + WWS = 1 << 8, + + /// + /// Font contains oblique characters. + /// + OBLIQUE = 1 << 9, + } + + /// + /// Gets the font style selection flags. + /// + public FontStyleSelection FontStyle { get; } + + /// + /// Gets the typographic ascender value. + /// + public short TypoAscender { get; } + + /// + /// Gets the typographic descender value. + /// + public short TypoDescender { get; } + + /// + /// Gets the typographic line gap value. + /// + public short TypoLineGap { get; } + + /// + /// Gets the Windows ascent metric used for clipping. + /// + public ushort WinAscent { get; } + + /// + /// Gets the Windows descent metric used for clipping. + /// + public ushort WinDescent { get; } + + /// + /// Gets the position of the strikeout stroke relative to the baseline. + /// + public short StrikeoutPosition { get; } + + /// + /// Gets the width of the strikeout stroke in font design units. + /// + public short StrikeoutSize { get; } + + /// + /// Gets the horizontal offset for subscript characters. + /// + public short SubscriptXOffset { get; } + + /// + /// Gets the horizontal size for subscript characters. + /// + public short SubscriptXSize { get; } + + /// + /// Gets the vertical offset for subscript characters. + /// + public short SubscriptYOffset { get; } + + /// + /// Gets the vertical size for subscript characters. + /// + public short SubscriptYSize { get; } + + /// + /// Gets the horizontal offset for superscript characters. + /// + public short SuperscriptXOffset { get; } + + /// + /// Gets the horizontal size for superscript characters. + /// + public short SuperscriptXSize { get; } + + /// + /// Gets the vertical offset for superscript characters. + /// + public short SuperscriptYOffset { get; } + + /// + /// Gets the vertical size for superscript characters. + /// + public short SuperscriptYSize { get; } + + /// + /// Loads the from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static OS2Table? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + /// + /// Loads the from the specified binary reader. + /// + /// The big-endian binary reader. + /// The . + public static OS2Table Load(BigEndianBinaryReader reader) + { + // Version 1.0 + // Type | Name | Comments + // -------|------------------------|----------------------- + // uint16 |version | 0x0005 + // int16 |xAvgCharWidth | + // uint16 |usWeightClass | + // uint16 |usWidthClass | + // uint16 |fsType | + // int16 |ySubscriptXSize | + // int16 |ySubscriptYSize | + // int16 |ySubscriptXOffset | + // int16 |ySubscriptYOffset | + // int16 |ySuperscriptXSize | + // int16 |ySuperscriptYSize | + // int16 |ySuperscriptXOffset | + // int16 |ySuperscriptYOffset | + // int16 |yStrikeoutSize | + // int16 |yStrikeoutPosition | + // int16 |sFamilyClass | + // uint8 |panose[10] | + // uint32 |ulUnicodeRange1 | Bits 0–31 + // uint32 |ulUnicodeRange2 | Bits 32–63 + // uint32 |ulUnicodeRange3 | Bits 64–95 + // uint32 |ulUnicodeRange4 | Bits 96–127 + // Tag |achVendID | + // uint16 |fsSelection | + // uint16 |usFirstCharIndex | + // uint16 |usLastCharIndex | + // int16 |sTypoAscender | + // int16 |sTypoDescender | + // int16 |sTypoLineGap | + // uint16 |usWinAscent | + // uint16 |usWinDescent | + // uint32 |ulCodePageRange1 | Bits 0–31 + // uint32 |ulCodePageRange2 | Bits 32–63 + // int16 |sxHeight | + // int16 |sCapHeight | + // uint16 |usDefaultChar | + // uint16 |usBreakChar | + // uint16 |usMaxContext | + // uint16 |usLowerOpticalPointSize | + // uint16 |usUpperOpticalPointSize | + ushort version = reader.ReadUInt16(); // assert 0x0005 + short averageCharWidth = reader.ReadInt16(); + ushort weightClass = reader.ReadUInt16(); + ushort widthClass = reader.ReadUInt16(); + ushort styleType = reader.ReadUInt16(); + short subscriptXSize = reader.ReadInt16(); + short subscriptYSize = reader.ReadInt16(); + short subscriptXOffset = reader.ReadInt16(); + short subscriptYOffset = reader.ReadInt16(); + + short superscriptXSize = reader.ReadInt16(); + short superscriptYSize = reader.ReadInt16(); + short superscriptXOffset = reader.ReadInt16(); + short superscriptYOffset = reader.ReadInt16(); + + short strikeoutSize = reader.ReadInt16(); + short strikeoutPosition = reader.ReadInt16(); + short familyClass = reader.ReadInt16(); + byte[] panose = reader.ReadUInt8Array(10); + uint unicodeRange1 = reader.ReadUInt32(); // Bits 0–31 + uint unicodeRange2 = reader.ReadUInt32(); // Bits 32–63 + uint unicodeRange3 = reader.ReadUInt32(); // Bits 64–95 + uint unicodeRange4 = reader.ReadUInt32(); // Bits 96–127 + string tag = reader.ReadTag(); + FontStyleSelection fontStyle = reader.ReadUInt16(); + ushort firstCharIndex = reader.ReadUInt16(); + ushort lastCharIndex = reader.ReadUInt16(); + short typoAscender = reader.ReadInt16(); + short typoDescender = reader.ReadInt16(); + short typoLineGap = reader.ReadInt16(); + ushort winAscent = reader.ReadUInt16(); + ushort winDescent = reader.ReadUInt16(); + + var version0Table = new OS2Table( + averageCharWidth, + weightClass, + widthClass, + styleType, + subscriptXSize, + subscriptYSize, + subscriptXOffset, + subscriptYOffset, + superscriptXSize, + superscriptYSize, + superscriptXOffset, + superscriptYOffset, + strikeoutSize, + strikeoutPosition, + familyClass, + panose, + unicodeRange1, + unicodeRange2, + unicodeRange3, + unicodeRange4, + tag, + fontStyle, + firstCharIndex, + lastCharIndex, + typoAscender, + typoDescender, + typoLineGap, + winAscent, + winDescent); + + if (version == 0) + { + return version0Table; + } + + short heightX = 0; + short capHeight = 0; + + ushort defaultChar = 0; + ushort breakChar = 0; + ushort maxContext = 0; + + ushort codePageRange1 = reader.ReadUInt16(); // Bits 0–31 + ushort codePageRange2 = reader.ReadUInt16(); // Bits 32–63 + + // fields exist only in > v1 https://docs.microsoft.com/en-us/typography/opentype/spec/os2 + if (version > 1) + { + heightX = reader.ReadInt16(); + capHeight = reader.ReadInt16(); + defaultChar = reader.ReadUInt16(); + breakChar = reader.ReadUInt16(); + maxContext = reader.ReadUInt16(); + } + + var versionLessThan5Table = new OS2Table( + version0Table, + codePageRange1, + codePageRange2, + heightX, + capHeight, + defaultChar, + breakChar, + maxContext); + + if (version < 5) + { + return versionLessThan5Table; + } + + ushort lowerOpticalPointSize = reader.ReadUInt16(); + ushort upperOpticalPointSize = reader.ReadUInt16(); + + return new OS2Table( + versionLessThan5Table, + lowerOpticalPointSize, + upperOpticalPointSize); + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Post/PostNameRecord.cs b/SixLabors.Fonts/Tables/General/Post/PostNameRecord.cs new file mode 100644 index 0000000..2239fa4 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Post/PostNameRecord.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Post { + /// + /// Represents a single glyph name record in the OpenType 'post' table (format 2.0). + /// + /// + internal class PostNameRecord + { + /// + /// Initializes a new instance of the class. + /// + /// The index into the glyph name data. + /// The resolved glyph name string. + internal PostNameRecord(ushort nameIndex, string name) + { + this.Name = name; + this.NameIndex = nameIndex; + } + + /// + /// Gets the index into the string data or the standard Apple glyph name map. + /// + public ushort NameIndex { get; } + + /// + /// Gets the resolved PostScript glyph name. + /// + public string Name { get; } + } +} diff --git a/SixLabors.Fonts/Tables/General/Post/PostTable.cs b/SixLabors.Fonts/Tables/General/Post/PostTable.cs new file mode 100644 index 0000000..4eb71a6 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Post/PostTable.cs @@ -0,0 +1,273 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Text; + +namespace SixLabors.Fonts.Tables.General.Post { + /// + /// Represents the OpenType 'post' table, which contains information needed + /// to use a font on a PostScript printer, including glyph names. + /// + /// + internal class PostTable : Table + { + /// + /// The table tag name identifying the 'post' table. + /// + internal const string TableName = "post"; + + /// + /// The standard Macintosh glyph name ordering for the first 258 glyph indices. + /// + private static readonly string[] AppleGlyphNameMap + = new[] + { + ".notdef", ".null", "nonmarkingreturn", "space", "exclam", "quotedbl", "numbersign", "dollar", "percent", "ampersand", "quotesingle", "parenleft", "parenright", + "asterisk", "plus", "comma", "hyphen", "period", "slash", "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "colon", "semicolon", + "less", "equal", "greater", "question", "at", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", + "X", "Y", "Z", "bracketleft", "backslash", "bracketright", "asciicircum", "underscore", "grave", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", + "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "braceleft", "bar", "braceright", "asciitilde", "Adieresis", "Aring", "Ccedilla", "Eacute", + "Ntilde", "Odieresis", "Udieresis", "aacute", "agrave", "acircumflex", "adieresis", "atilde", "aring", "ccedilla", "eacute", "egrave", "ecircumflex", "edieresis", + "iacute", "igrave", "icircumflex", "idieresis", "ntilde", "oacute", "ograve", "ocircumflex", "odieresis", "otilde", "uacute", "ugrave", "ucircumflex", "udieresis", + "dagger", "degree", "cent", "sterling", "section", "bullet", "paragraph", "germandbls", "registered", "copyright", "trademark", "acute", "dieresis", "notequal", + "AE", "Oslash", "infinity", "plusminus", "lessequal", "greaterequal", "yen", "mu", "partialdiff", "summation", "product", "pi", "integral", "ordfeminine", + "ordmasculine", "Omega", "ae", "oslash", "questiondown", "exclamdown", "logicalnot", "radical", "florin", "approxequal", "Delta", "guillemotleft", "guillemotright", + "ellipsis", "nonbreakingspace", "Agrave", "Atilde", "Otilde", "OE", "oe", "endash", "emdash", "quotedblleft", "quotedblright", "quoteleft", "quoteright", "divide", + "lozenge", "ydieresis", "Ydieresis", "fraction", "currency", "guilsinglleft", "guilsinglright", "fi", "fl", "daggerdbl", "periodcentered", "quotesinglbase", "quotedblbase", + "perthousand", "Acircumflex", "Ecircumflex", "Aacute", "Edieresis", "Egrave", "Iacute", "Icircumflex", "Idieresis", "Igrave", "Oacute", "Ocircumflex", "apple", "Ograve", + "Uacute", "Ucircumflex", "Ugrave", "dotlessi", "circumflex", "tilde", "macron", "breve", "dotaccent", "ring", "cedilla", "hungarumlaut", "ogonek", "caron", "Lslash", + "lslash", "Scaron", "scaron", "Zcaron", "zcaron", "brokenbar", "Eth", "eth", "Yacute", "yacute", "Thorn", "thorn", "minus", "multiply", "onesuperior", "twosuperior", + "threesuperior", "onehalf", "onequarter", "threequarters", "franc", "Gbreve", "gbreve", "Idotaccent", "Scedilla", "scedilla", "Cacute", "cacute", "Ccaron", "ccaron", "dcroat" + }; + + /// + /// Initializes a new instance of the class. + /// + /// The major version of the post table format. + /// The minor version of the post table format. + /// The suggested distance of the top of the underline from the baseline. + /// The suggested underline thickness. + /// The italic angle in counter-clockwise degrees from the vertical. + /// Non-zero if the font is monospaced; zero if proportionally spaced. + /// The minimum memory usage when downloaded as a Type 42 font. + /// The maximum memory usage when downloaded as a Type 42 font. + /// The minimum memory usage when downloaded as a Type 1 font. + /// The maximum memory usage when downloaded as a Type 1 font. + /// The array of post name records for glyph name mapping. + public PostTable( + ushort formatMajor, + ushort formatMinor, + short underlinePosition, + short underlineThickness, + float italicAngle, + uint isFixedPitch, + uint minMemType42, + uint maxMemType42, + uint minMemType1, + uint maxMemType1, + PostNameRecord[] postRecords) + { + this.FormatMajor = formatMajor; + this.FormatMinor = formatMinor; + this.UnderlinePosition = underlinePosition; + this.UnderlineThickness = underlineThickness; + this.ItalicAngle = italicAngle; + this.IsFixedPitch = isFixedPitch; + this.MinMemType42 = minMemType42; + this.MaxMemType42 = maxMemType42; + this.MinMemType1 = minMemType1; + this.MaxMemType1 = maxMemType1; + this.PostRecords = postRecords; + } + + /// + /// Gets the array of post name records mapping glyph indices to PostScript names. + /// + public PostNameRecord[] PostRecords { get; } + + /// + /// Gets the major version number of the post table format. + /// + public ushort FormatMajor { get; } + + /// + /// Gets the minor version number of the post table format. + /// + public ushort FormatMinor { get; } + + /// + /// Gets the suggested distance of the top of the underline from the baseline, in font design units. + /// Negative values indicate below baseline. + /// + public short UnderlinePosition { get; } + + /// + /// Gets the suggested underline thickness, in font design units. + /// + public short UnderlineThickness { get; } + + /// + /// Gets the italic angle in counter-clockwise degrees from the vertical. + /// Zero for upright text, negative for text that leans to the right. + /// + public float ItalicAngle { get; } + + /// + /// Gets a value indicating whether the font is monospaced. + /// Non-zero if the font is not proportionally spaced (i.e., monospaced). + /// + public uint IsFixedPitch { get; } + + /// + /// Gets the minimum memory usage when an OpenType font is downloaded as a Type 42 font. + /// + public uint MinMemType42 { get; } + + /// + /// Gets the maximum memory usage when an OpenType font is downloaded as a Type 42 font. + /// + public uint MaxMemType42 { get; } + + /// + /// Gets the minimum memory usage when an OpenType font is downloaded as a Type 1 font. + /// + public uint MinMemType1 { get; } + + /// + /// Gets the maximum memory usage when an OpenType font is downloaded as a Type 1 font. + /// + public uint MaxMemType1 { get; } + + /// + /// Loads the from the specified font reader. + /// + /// The font reader to read the table from. + /// The loaded , or if the table is not present. + public static PostTable? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + /// + /// Loads the from the specified binary reader. + /// + /// The binary reader positioned at the start of the post table data. + /// The loaded . + /// Thrown when the table format version is not supported. + public static PostTable Load(BigEndianBinaryReader reader) + { + // HEADER + // Type | Name | Description + // ----------------|---------------------|--------------------------------------------------------------- + // Version16Dot16 | version | 0x00010000 for version 1.0, 0x00020000 for version 2.0, 0x00025000 for version 2.5 (deprecated), 0x00030000 for version 3.0 + // Fixed | italicAngle | Italic angle in counter-clockwise degrees from the vertical. Zero for upright text, negative for text that leans to the right (forward). + // FWORD | underlinePosition | This is the suggested distance of the top of the underline from the baseline (negative values indicate below baseline). The PostScript definition of this FontInfo dictionary key (the y coordinate of the center of the stroke) is not used for historical reasons. The value of the PostScript key may be calculated by subtracting half the underlineThickness from the value of this field. + // FWORD | underlineThickness | Suggested values for the underline thickness. In general, the underline thickness should match the thickness of the underscore character (U+005F LOW LINE), and should also match the strikeout thickness, which is specified in the OS/2 table. + // uint32 | isFixedPitch | Set to 0 if the font is proportionally spaced, non-zero if the font is not proportionally spaced (i.e. monospaced). + // uint32 | minMemType42 | Minimum memory usage when an OpenType font is downloaded. + // uint32 | maxMemType42 | Maximum memory usage when an OpenType font is downloaded. + // uint32 | minMemType1 | Minimum memory usage when an OpenType font is downloaded as a Type 1 font. + // uint32 | maxMemType1 | Maximum memory usage when an OpenType font is downloaded as a Type 1 font. + + // FORMAT 2.0 + // Type | Name | Description + // --------|-----------------------------|-------------------------------------------------------------- + // uint16 | numGlyphs | Number of glyphs (this should be the same as numGlyphs in 'maxp' table). + // uint16 | glyphNameIndex[numGlyphs] | Array of indices into the string data. See below for details. + // uint8 | stringData[variable] | Storage for the string data. + ushort formatMajor = reader.ReadUInt16(); + ushort formatMinor = reader.ReadUInt16(); + float italicAngle = reader.ReadFixed(); + short underlinePosition = reader.ReadFWORD(); + short underlineThickness = reader.ReadFWORD(); + uint isFixedPitch = reader.ReadUInt32(); + uint minMemType42 = reader.ReadUInt32(); + uint maxMemType42 = reader.ReadUInt32(); + uint minMemType1 = reader.ReadUInt32(); + uint maxMemType1 = reader.ReadUInt32(); + + PostNameRecord[] records = Array.Empty(); + + if (formatMajor == 1) + { + // Supported, no extra subtables needed + } + else if (formatMajor == 2 && formatMinor == 0) + { + ushort numGlyphs = reader.ReadUInt16(); + records = new PostNameRecord[numGlyphs]; + + ushort[] glyphIndices = reader.ReadUInt16Array(numGlyphs); + + for (int i = 0; i < numGlyphs; i++) + { + ushort glyphNameIndex = glyphIndices[i]; + string name; + + // < 258 is a standard fixed apple mapping + if (glyphNameIndex <= 257) + { + name = AppleGlyphNameMap[glyphNameIndex]; + } + else + { + byte strLength = reader.ReadByte(); + name = reader.ReadString(strLength, Encoding.ASCII); + } + + records[i] = new PostNameRecord(glyphNameIndex, name); + } + } + else if (formatMajor > 3) + { + throw new NotSupportedException($"{TableName} table format {formatMajor}.{formatMinor} is not supported."); + } + + // TODO: Validate maximum numbers against maxp table. + return new PostTable( + formatMajor, + formatMinor, + underlinePosition, + underlineThickness, + italicAngle, + isFixedPitch, + minMemType42, + maxMemType42, + minMemType1, + maxMemType1, + records); + } + + /// + /// Gets the PostScript name for the glyph at the specified name index. + /// + /// The name index to look up. + /// The PostScript glyph name, or if not found. + public string? GetPostScriptName(int nameIndex) + { + if (this.PostRecords is not null) + { + for (int i = 0; i < this.PostRecords.Length; i++) + { + PostNameRecord p = this.PostRecords[i]; + + if (p.NameIndex == nameIndex) + { + return p.Name; + } + } + } + + return null; + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Svg/SvgDocumentIndexEntry.cs b/SixLabors.Fonts/Tables/General/Svg/SvgDocumentIndexEntry.cs new file mode 100644 index 0000000..6b4682b --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Svg/SvgDocumentIndexEntry.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Svg { + /// + /// Represents an entry in the SVG Document Index of the SVG table. + /// Each entry maps a contiguous range of glyph IDs to an SVG document. + /// + /// + internal readonly struct SvgDocumentIndexEntry + { + /// + /// Initializes a new instance of the struct. + /// + /// The first glyph ID in the range (inclusive). + /// The last glyph ID in the range (inclusive). + /// The offset from the beginning of the SVG Document Index to the SVG document. + /// The length of the SVG document data in bytes. + public SvgDocumentIndexEntry(ushort startGlyphId, ushort endGlyphId, uint svgDocOffset, uint svgDocLength) + { + this.StartGlyphId = startGlyphId; + this.EndGlyphId = endGlyphId; + this.SvgDocOffset = svgDocOffset; + this.SvgDocLength = svgDocLength; + } + + /// + /// Gets the first glyph ID in this range (inclusive). + /// + public ushort StartGlyphId { get; } + + /// + /// Gets the last glyph ID in this range (inclusive). + /// + public ushort EndGlyphId { get; } + + /// + /// Gets the offset from the beginning of the SVG Document Index to the SVG document. + /// + public uint SvgDocOffset { get; } + + /// + /// Gets the length of the SVG document data in bytes. + /// + public uint SvgDocLength { get; } + } +} diff --git a/SixLabors.Fonts/Tables/General/Svg/SvgGlyphSource.cs b/SixLabors.Fonts/Tables/General/Svg/SvgGlyphSource.cs new file mode 100644 index 0000000..8c76eb0 --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Svg/SvgGlyphSource.cs @@ -0,0 +1,1735 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Xml; +using System.Xml.Linq; +using SixLabors.Fonts.Rendering; + +namespace SixLabors.Fonts.Tables.General.Svg { + /// + /// Supplies painted glyphs (layers + commands + paints) and canvas metadata for OT-SVG glyphs. + /// Geometry coordinates are kept in SVG user space; all transforms are carried as matrices + /// on the canvas (root) and on each layer. No point-transforming is performed here. + /// + internal sealed class SvgGlyphSource : IPaintedGlyphSource + { + private static readonly SolidPaint DefaultBlackFillPaint = new() { Color = GlyphColor.Black }; + private readonly SvgTable svgTable; + private readonly ConcurrentDictionary<(int Start, int Length), ParsedDoc> docCache = []; + private readonly ConcurrentDictionary cachedGlyphs = []; + + /// + /// Initializes a new instance of the class. + /// + /// The SVG table. + public SvgGlyphSource(SvgTable svgTable) => this.svgTable = svgTable; + + /// + public bool TryGetPaintedGlyph(ushort glyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas) + { + (PaintedGlyph Glyph, PaintedCanvasMetadata Canvas) result = this.cachedGlyphs.GetOrAdd(glyphId, gid => + { + if (this.TryGetParsedDoc(gid, out ParsedDoc? parsed)) + { + XElement? root = parsed.Doc.Root; + if (root is not null) + { + FontRectangle viewBox = GetViewBox(root); + Matrix3x2 rootTransform = ParseTransform(root.Attribute("transform")?.Value); + + // Prefer a dedicated group with id="glyph{gid}", else fall back to the root. + string wantedId = "glyph" + gid.ToString(CultureInfo.InvariantCulture); + XElement glyphRoot = parsed.IdMap.TryGetValue(wantedId, out XElement? ge) ? ge : root; + + List layers = []; + Walk( + glyphRoot, + rootTransform, + inheritedPaint: DefaultBlackFillPaint, + inheritedOpacityMul: 1F, + outputLayers: layers, + parsedDoc: parsed); + + if (layers.Count > 0) + { + PaintedGlyph glyph = new(layers); + PaintedCanvasMetadata canvas = new(viewBox, true, rootTransform); + return (glyph, canvas); + } + } + } + + return (default, default); + }); + + glyph = result.Glyph; + canvas = result.Canvas; + return result.Glyph.Layers.Count > 0; + } + + private bool TryGetParsedDoc(ushort glyphId, [NotNullWhen(true)] out ParsedDoc? parsed) + { + parsed = default; + + if (!this.svgTable.TryGetDocumentSpan(glyphId, out int start, out int length)) + { + return false; + } + + (int Start, int Length) docKey = (start, length); + if (this.docCache.TryGetValue(docKey, out parsed)) + { + return true; + } + + if (!this.svgTable.TryOpenDecodedDocumentStream(glyphId, out Stream stream)) + { + return false; + } + + using (stream) + { + XDocument doc = LoadXml(stream); + if (doc.Root is null) + { + return false; + } + + // TODO: How large is this likely to get? If large, consider a more memory-efficient structure. + Dictionary idMap = new(1024, StringComparer.Ordinal); + + foreach (XElement e in doc.Root.DescendantsAndSelf()) + { + XAttribute? id = e.Attribute("id"); + if (id is not null) + { + idMap[id.Value] = e; // last-wins + } + } + + parsed = new ParsedDoc + { + Doc = doc, + IdMap = idMap + }; + + this.docCache[docKey] = parsed; + return true; + } + } + + private static XDocument LoadXml(Stream stream) + { + XmlReaderSettings settings = new() + { + DtdProcessing = DtdProcessing.Ignore, + XmlResolver = null, + IgnoreComments = true, + IgnoreProcessingInstructions = true, + IgnoreWhitespace = true + }; + + using XmlReader reader = XmlReader.Create(stream, settings); + return XDocument.Load(reader, LoadOptions.None); + } + + private static FontRectangle GetViewBox(XElement svg) + { + if (TryParseViewBox(svg.Attribute("viewBox")?.Value, out float x, out float y, out float w, out float h)) + { + return new FontRectangle(x, y, w, h); + } + + // No viewBox; return an empty rect. Metrics layer must decide fallback mapping. + return FontRectangle.Empty; + } + + private static void Walk( + XElement node, + Matrix3x2 parentLocalTransform, + Paint? inheritedPaint, + float inheritedOpacityMul, + List outputLayers, + ParsedDoc parsedDoc) + { + Dictionary idMap = parsedDoc.IdMap; + Matrix3x2 nodeTransform = ParseTransform(node.Attribute("transform")?.Value); + Matrix3x2 localTransform = parentLocalTransform * nodeTransform; + + FillRule fillRule = ResolveFillRule(node, FillRule.NonZero); + Paint? paint = ResolvePaint(node, inheritedPaint, parsedDoc, out bool fillNone, out float opacityMul); + float combinedOpacityMul = inheritedOpacityMul * opacityMul; + + string name = node.Name.LocalName; + switch (name) + { + case "svg": + case "g": + { + foreach (XElement child in node.Elements()) + { + Walk(child, localTransform, fillNone ? null : paint, combinedOpacityMul, outputLayers, parsedDoc); + } + + break; + } + + case "use": + { + string? href = GetHref(node); + if (href is null) + { + break; + } + + float ux = ParseFloat(node.Attribute("x")?.Value); + float uy = ParseFloat(node.Attribute("y")?.Value); + Matrix3x2 xf = parentLocalTransform + * Matrix3x2.CreateTranslation(ux, uy) + * nodeTransform; + + Paint? childInherited = fillNone ? null : paint; + + XElement? target = LookupById(idMap, href); + if (target is not null) + { + Walk(target, xf, childInherited, combinedOpacityMul, outputLayers, parsedDoc); + } + + break; + } + + case "path": + { + if (fillNone) + { + break; + } + + string? d = node.Attribute("d")?.Value; + if (string.IsNullOrWhiteSpace(d)) + { + break; + } + + List cmds = GetOrBuildPathCommands(node, d, parsedDoc); + if (cmds.Count > 0) + { + Paint? layerPaint = ApplyOpacityToPaint(paint, combinedOpacityMul); + outputLayers.Add(new(layerPaint, fillRule, localTransform, null, cmds)); + } + + break; + } + + case "polygon": + case "polyline": + { + if (fillNone) + { + break; + } + + string pts = node.Attribute("points")?.Value ?? string.Empty; + float[] coords = ParseFloatList(pts); + if (coords.Length >= 4) + { + bool close = string.Equals(node.Name.LocalName, "polygon", StringComparison.Ordinal); + List cmds = GetOrBuildPolyCommands(node, coords, close, parsedDoc); + if (cmds.Count > 0) + { + Paint? layerPaint = ApplyOpacityToPaint(paint, combinedOpacityMul); + outputLayers.Add(new(layerPaint, fillRule, localTransform, null, cmds)); + } + } + + break; + } + + case "rect": + { + if (fillNone) + { + break; + } + + float x = ParseFloat(node.Attribute("x")?.Value); + float y = ParseFloat(node.Attribute("y")?.Value); + float w = ParseFloat(node.Attribute("width")?.Value); + float h = ParseFloat(node.Attribute("height")?.Value); + + // TODO: Rounded corners (rx/ry) not handled here (could be approximated later if needed). + if (w > 0f && h > 0f) + { + List cmds = GetOrBuildRectCommands(node, x, y, w, h, parsedDoc); + if (cmds.Count > 0) + { + Paint? layerPaint = ApplyOpacityToPaint(paint, combinedOpacityMul); + outputLayers.Add(new(layerPaint, fillRule, localTransform, null, cmds)); + } + } + + break; + } + + case "circle": + { + if (fillNone) + { + break; + } + + float cx = ParseFloat(node.Attribute("cx")?.Value); + float cy = ParseFloat(node.Attribute("cy")?.Value); + float r = ParseFloat(node.Attribute("r")?.Value); + if (r > 0f) + { + List cmds = GetOrBuildEllipseCommands(node, cx, cy, r, r, parsedDoc); + if (cmds.Count > 0) + { + Paint? layerPaint = ApplyOpacityToPaint(paint, combinedOpacityMul); + outputLayers.Add(new(layerPaint, fillRule, localTransform, null, cmds)); + } + } + + break; + } + + case "ellipse": + { + if (fillNone) + { + break; + } + + float cx = ParseFloat(node.Attribute("cx")?.Value); + float cy = ParseFloat(node.Attribute("cy")?.Value); + float rx = ParseFloat(node.Attribute("rx")?.Value); + float ry = ParseFloat(node.Attribute("ry")?.Value); + if (rx > 0f && ry > 0f) + { + List cmds = GetOrBuildEllipseCommands(node, cx, cy, rx, ry, parsedDoc); + if (cmds.Count > 0) + { + Paint? layerPaint = ApplyOpacityToPaint(paint, combinedOpacityMul); + outputLayers.Add(new(layerPaint, fillRule, localTransform, null, cmds)); + } + } + + break; + } + + default: + { + // Unhandled (image, text, mask, clipPath, etc.) in v1. + break; + } + } + } + + private static Paint? ApplyOpacityToPaint(Paint? basePaint, float opacityMul) + { + if (basePaint is null) + { + return null; + } + + float effective = Math.Clamp(basePaint.Opacity * opacityMul, 0f, 1f); + if (effective <= 0f) + { + return null; + } + + return basePaint switch + { + SolidPaint s => new SolidPaint { Color = s.Color, Opacity = effective }, + LinearGradientPaint lg => new LinearGradientPaint + { + Units = lg.Units, + P0 = lg.P0, + P1 = lg.P1, + Spread = lg.Spread, + Stops = lg.Stops, + Transform = lg.Transform, + Opacity = effective + }, + RadialGradientPaint rg => new RadialGradientPaint + { + Units = rg.Units, + Center0 = rg.Center0, + Radius0 = rg.Radius0, + Center1 = rg.Center1, + Radius1 = rg.Radius1, + Spread = rg.Spread, + Stops = rg.Stops, + Transform = rg.Transform, + Opacity = effective + }, + _ => null, + }; + } + + private static FillRule ResolveFillRule(XElement e, FillRule inheritedDefault) + { + string? styleRule = TryCss(e.Attribute("style")?.Value, "fill-rule"); + string? attrRule = e.Attribute("fill-rule")?.Value; + string? value = styleRule ?? attrRule; + + if (string.Equals(value, "evenodd", StringComparison.OrdinalIgnoreCase)) + { + return FillRule.EvenOdd; + } + + if (string.Equals(value, "nonzero", StringComparison.OrdinalIgnoreCase)) + { + return FillRule.NonZero; + } + + return inheritedDefault; + } + + private static Paint? ResolvePaint( + XElement e, + Paint? inherited, + ParsedDoc parsedDoc, + out bool fillNone, + out float opacityMul) + { + fillNone = false; + opacityMul = 1f; + + string? style = e.Attribute("style")?.Value; + string? fillAttr = e.Attribute("fill")?.Value; + string? opacityAttr = e.Attribute("opacity")?.Value; + string? fillOpacityAttr = e.Attribute("fill-opacity")?.Value; + + string? styleFill = TryCss(style, "fill"); + string? styleOpacity = TryCss(style, "opacity"); + string? styleFillOpacity = TryCss(style, "fill-opacity"); + + string? fill = styleFill ?? fillAttr; + string? op = styleOpacity ?? opacityAttr; + string? fop = styleFillOpacity ?? fillOpacityAttr; + + if (!string.IsNullOrEmpty(op) && float.TryParse(op, NumberStyles.Float, CultureInfo.InvariantCulture, out float o)) + { + opacityMul *= Math.Clamp(o, 0f, 1f); + } + + if (!string.IsNullOrEmpty(fop) && float.TryParse(fop, NumberStyles.Float, CultureInfo.InvariantCulture, out float fo)) + { + opacityMul *= Math.Clamp(fo, 0f, 1f); + } + + if (string.IsNullOrEmpty(fill)) + { + return inherited; + } + + if (string.Equals(fill, "none", StringComparison.OrdinalIgnoreCase)) + { + fillNone = true; + return null; + } + + if (TryParseColor(fill, out GlyphColor color)) + { + return new SolidPaint { Color = color }; + } + + if (TryExtractUrlId(fill, out string? paintId) && paintId is not null) + { + return ResolvePaintServer(paintId, parsedDoc) ?? inherited; + } + + return inherited; + } + + /// + /// Resolves a referenced paint server and caches the parsed paint so repeated + /// uses of the same gradient id do not rebuild the gradient definition. + /// + /// The referenced paint server identifier. + /// The parsed SVG document and its caches. + /// The resolved paint, or if the reference is unknown. + private static Paint? ResolvePaintServer(string id, ParsedDoc parsedDoc) + { + if (parsedDoc.PaintServerCache.TryGetValue(id, out Paint? cached)) + { + return cached; + } + + if (!parsedDoc.IdMap.TryGetValue(id, out XElement? server)) + { + return null; + } + + string tag = server.Name.LocalName; + Paint? paint = tag switch + { + // SVG only has linearGradient and radialGradient. + "linearGradient" => BuildLinearGradient(server, parsedDoc.IdMap), + "radialGradient" => BuildRadialGradient(server, parsedDoc.IdMap), + _ => null + }; + + if (paint is not null) + { + parsedDoc.PaintServerCache.TryAdd(id, paint); + } + + return paint; + } + + private static LinearGradientPaint? BuildLinearGradient(XElement grad, Dictionary idMap) + { + GradientUnits units = GradientUnits.ObjectBoundingBox; + SpreadMethod spread = SpreadMethod.Pad; + Matrix3x2 gxf = Matrix3x2.Identity; + + float? x1 = null, y1 = null, x2 = null, y2 = null; + List<(float Offset, GlyphColor Color)> stops = []; + + HashSet visited = new(StringComparer.Ordinal); + XElement? cur = grad; + + while (cur is not null) + { + string? u = cur.Attribute("gradientUnits")?.Value; + if (u is not null) + { + units = ParseGradientUnits(u); + } + + string? sm = cur.Attribute("spreadMethod")?.Value; + if (sm is not null) + { + spread = ParseSpreadMethod(sm); + } + + gxf = ParseTransform(cur.Attribute("gradientTransform")?.Value) * gxf; + + x1 ??= ParseCoordNullable(cur.Attribute("x1")?.Value); + y1 ??= ParseCoordNullable(cur.Attribute("y1")?.Value); + x2 ??= ParseCoordNullable(cur.Attribute("x2")?.Value); + y2 ??= ParseCoordNullable(cur.Attribute("y2")?.Value); + + bool hadStops = false; + foreach (XElement s in cur.Elements()) + { + if (s.Name.LocalName != "stop") + { + continue; + } + + if (TryParseStop(s, out float off, out GlyphColor c)) + { + stops.Add((off, c)); + hadStops = true; + } + } + + if (hadStops) + { + break; + } + + string? href = GetHref(cur); + if (href is null || href.Length <= 1 || href[0] != '#') + { + break; + } + + string refId = href[1..]; + if (!visited.Add(refId) || !idMap.TryGetValue(refId, out cur)) + { + break; + } + } + + if (!x1.HasValue) + { + x1 = 0f; + } + + if (!y1.HasValue) + { + y1 = 0f; + } + + if (!x2.HasValue) + { + x2 = units == GradientUnits.ObjectBoundingBox ? 1f : 0f; + } + + if (!y2.HasValue) + { + y2 = 0f; + } + + GradientStop[] gs = BuildStopsArray(stops); + + return new LinearGradientPaint + { + Units = units, + P0 = new Vector2(x1.Value, y1.Value), + P1 = new Vector2(x2.Value, y2.Value), + Spread = spread, + Stops = gs, + Transform = gxf + }; + } + + private static RadialGradientPaint? BuildRadialGradient(XElement grad, Dictionary idMap) + { + GradientUnits units = GradientUnits.ObjectBoundingBox; + SpreadMethod spread = SpreadMethod.Pad; + Matrix3x2 gxf = Matrix3x2.Identity; + + float? cx = null, cy = null, r = null, fx = null, fy = null, fr = null; + List<(float Offset, GlyphColor Color)> stops = []; + + HashSet visited = new(StringComparer.Ordinal); + XElement? cur = grad; + + while (cur is not null) + { + string? u = cur.Attribute("gradientUnits")?.Value; + if (u is not null) + { + units = ParseGradientUnits(u); + } + + string? sm = cur.Attribute("spreadMethod")?.Value; + if (sm is not null) + { + spread = ParseSpreadMethod(sm); + } + + gxf = ParseTransform(cur.Attribute("gradientTransform")?.Value) * gxf; + + cx ??= ParseCoordNullable(cur.Attribute("cx")?.Value); + cy ??= ParseCoordNullable(cur.Attribute("cy")?.Value); + r ??= ParseRadiusNullable(cur.Attribute("r")?.Value); + fx ??= ParseCoordNullable(cur.Attribute("fx")?.Value); + fy ??= ParseCoordNullable(cur.Attribute("fy")?.Value); + fr ??= ParseRadiusNullable(cur.Attribute("fr")?.Value); + + bool hadStops = false; + foreach (XElement s in cur.Elements()) + { + if (s.Name.LocalName != "stop") + { + continue; + } + + if (TryParseStop(s, out float off, out GlyphColor c)) + { + stops.Add((off, c)); + hadStops = true; + } + } + + if (hadStops) + { + break; + } + + string? href = GetHref(cur); + if (href is null || href.Length <= 1 || href[0] != '#') + { + break; + } + + string refId = href[1..]; + if (!visited.Add(refId) || !idMap.TryGetValue(refId, out cur)) + { + break; + } + } + + if (!cx.HasValue) + { + cx = units == GradientUnits.ObjectBoundingBox ? 0.5f : 0f; + } + + if (!cy.HasValue) + { + cy = units == GradientUnits.ObjectBoundingBox ? 0.5f : 0f; + } + + if (!r.HasValue) + { + r = units == GradientUnits.ObjectBoundingBox ? 0.5f : 0f; + } + + if (!fx.HasValue) + { + fx = cx.Value; + } + + if (!fy.HasValue) + { + fy = cy.Value; + } + + if (!fr.HasValue) + { + fr = 0f; + } + + GradientStop[] gs = BuildStopsArray(stops); + + // Center0=(fx,fy), Radius0=fr; Center1=(cx,cy), Radius1=r + return new RadialGradientPaint + { + Units = units, + Center0 = new Vector2(fx.Value, fy.Value), + Radius0 = fr.Value, + Center1 = new Vector2(cx.Value, cy.Value), + Radius1 = r.Value, + Spread = spread, + Stops = gs, + Transform = gxf + }; + } + + private static SpreadMethod ParseSpreadMethod(string value) + { + if (string.Equals(value, "reflect", StringComparison.OrdinalIgnoreCase)) + { + return SpreadMethod.Reflect; + } + + if (string.Equals(value, "repeat", StringComparison.OrdinalIgnoreCase)) + { + return SpreadMethod.Repeat; + } + + return SpreadMethod.Pad; + } + + private static GradientUnits ParseGradientUnits(string value) + => string.Equals(value, "userSpaceOnUse", StringComparison.OrdinalIgnoreCase) + ? GradientUnits.UserSpaceOnUse + : GradientUnits.ObjectBoundingBox; + + private static float? ParseCoordNullable(string? s) + { + if (string.IsNullOrEmpty(s)) + { + return null; + } + + if (s.EndsWith('%')) + { + if (float.TryParse(s.AsSpan(0, s.Length - 1), NumberStyles.Float, CultureInfo.InvariantCulture, out float p)) + { + return p / 100f; + } + + return null; + } + + if (float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out float v)) + { + return v; // In OBB this is already a fraction; in userSpace it is absolute user units. + } + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float? ParseRadiusNullable(string? s) + => ParseCoordNullable(s); + + private static bool TryParseStop(XElement stop, out float offset, out GlyphColor color) + { + offset = 0f; + color = default; + + string? style = stop.Attribute("style")?.Value; + string? offAttr = stop.Attribute("offset")?.Value; + + string? sc = stop.Attribute("stop-color")?.Value ?? TryCss(style, "stop-color"); + string? so = stop.Attribute("stop-opacity")?.Value ?? TryCss(style, "stop-opacity"); + + if (!string.IsNullOrEmpty(offAttr)) + { + if (offAttr.EndsWith('%')) + { + if (float.TryParse(offAttr.AsSpan(0, offAttr.Length - 1), NumberStyles.Float, CultureInfo.InvariantCulture, out float p)) + { + offset = Math.Clamp(p / 100f, 0f, 1f); + } + } + else if (float.TryParse(offAttr, NumberStyles.Float, CultureInfo.InvariantCulture, out float v)) + { + offset = Math.Clamp(v, 0f, 1f); + } + } + + GlyphColor baseColor = new(0, 0, 0, 255); + if (!string.IsNullOrEmpty(sc) && TryParseColor(sc, out GlyphColor parsed)) + { + baseColor = parsed; + } + + float aMul = 1f; + if (!string.IsNullOrEmpty(so) && float.TryParse(so, NumberStyles.Float, CultureInfo.InvariantCulture, out float soVal)) + { + aMul = Math.Clamp(soVal, 0f, 1f); + } + + byte a = (byte)Math.Clamp((int)Math.Round(baseColor.A * aMul), 0, 255); + color = new GlyphColor(baseColor.R, baseColor.G, baseColor.B, a); + return true; + } + + private static GradientStop[] BuildStopsArray(List<(float Offset, GlyphColor Color)> list) + { + if (list.Count == 0) + { + return + [ + new GradientStop(0f, new GlyphColor(0, 0, 0, 255)), + new GradientStop(1f, new GlyphColor(0, 0, 0, 255)) + ]; + } + + list.Sort((a, b) => a.Offset.CompareTo(b.Offset)); + GradientStop[] stops = new GradientStop[list.Count]; + for (int i = 0; i < list.Count; i++) + { + (float o, GlyphColor c) = list[i]; + stops[i] = new GradientStop(Math.Clamp(o, 0f, 1f), c); + } + + return stops; + } + + private static string? TryCss(string? style, string prop) + { + if (string.IsNullOrEmpty(style)) + { + return null; + } + + ReadOnlySpan span = style.AsSpan(); + while (span.Length > 0) + { + int semi = span.IndexOf(';'); + ReadOnlySpan part = semi >= 0 ? span[..semi] : span; + span = semi >= 0 ? span[(semi + 1)..] : []; + + int colon = part.IndexOf(':'); + if (colon <= 0) + { + continue; + } + + ReadOnlySpan name = part[..colon].Trim(); + if (name.Equals(prop.AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + return part[(colon + 1)..].Trim().ToString(); + } + } + + return null; + } + + private static bool TryParseColor(string s, out GlyphColor color) + { + if (GlyphColor.TryParseNamed(s, out color)) + { + return true; + } + + if (GlyphColor.TryParseHex(s, out GlyphColor hex)) + { + color = hex; + return true; + } + + if (s.StartsWith("rgb", StringComparison.OrdinalIgnoreCase)) + { + int l = s.IndexOf('('); + int r = s.IndexOf(')'); + if (l >= 0 && r > l) + { + ReadOnlySpan inner = s.AsSpan(l + 1, r - l - 1); + Span ranges = stackalloc Range[5]; + int count = inner.Split(ranges, ','); + if (count >= 3) + { + byte rr = ParseByte(inner[ranges[0]]); + byte gg = ParseByte(inner[ranges[1]]); + byte bb = ParseByte(inner[ranges[2]]); + byte aa = 255; + if (count >= 4 && float.TryParse(inner[ranges[3]].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float af)) + { + aa = (byte)Math.Clamp((int)Math.Round(255f * af), 0, 255); + } + + color = new GlyphColor(rr, gg, bb, aa); + return true; + } + } + } + + return false; + + static byte ParseByte(ReadOnlySpan x) + { + if (x.IsEmpty) + { + return 0; + } + + ReadOnlySpan t = x.Trim(); + if (t[^1] == '%') + { + if (float.TryParse(t[..^1], NumberStyles.Float, CultureInfo.InvariantCulture, out float p)) + { + return (byte)Math.Clamp((int)Math.Round(255f * (p / 100f)), 0, 255); + } + + return 0; + } + + if (int.TryParse(t, NumberStyles.Integer, CultureInfo.InvariantCulture, out int v)) + { + return (byte)Math.Clamp(v, 0, 255); + } + + return 0; + } + } + + private static bool TryExtractUrlId(string s, [NotNullWhen(true)] out string? id) + { + id = null; + + int lp = s.IndexOf("url(", StringComparison.OrdinalIgnoreCase); + if (lp < 0) + { + return false; + } + + int rp = s.IndexOf(')', lp + 4); + if (rp < 0) + { + return false; + } + + string inner = s[(lp + 4)..rp].Trim(); + if (inner.Length > 1 && inner[0] == '#') + { + id = inner[1..]; + return true; + } + + return false; + } + + private static XElement? LookupById(Dictionary idMap, string href) + { + if (string.IsNullOrEmpty(href) || href[0] != '#') + { + return null; + } + + return idMap.TryGetValue(href.AsSpan(1).ToString(), out XElement? e) ? e : null; + } + + private static string? GetHref(XElement e) + { + XNamespace xlink = "http://www.w3.org/1999/xlink"; + return e.Attribute(xlink + "href")?.Value ?? e.Attribute("href")?.Value; + } + + /// + /// Returns cached path commands for an SVG path element, or parses and caches them + /// when the element is a reusable definition with an id. + /// + /// The SVG node that owns the geometry. + /// The raw SVG path data. + /// The parsed SVG document and its caches. + /// The parsed path commands. + private static List GetOrBuildPathCommands(XElement node, string d, ParsedDoc parsedDoc) + { + if (TryGetCachedGeometry(node, parsedDoc, out List? cached, out string? geometryId)) + { + return cached; + } + + return CacheGeometry(geometryId, parsedDoc, BuildCommandsFromPathData(d)); + } + + /// + /// Returns cached path commands for a polygon or polyline definition, or builds and caches them. + /// + /// The SVG node that owns the geometry. + /// The parsed coordinate list. + /// Whether the geometry should be explicitly closed. + /// The parsed SVG document and its caches. + /// The parsed path commands. + private static List GetOrBuildPolyCommands(XElement node, float[] coords, bool close, ParsedDoc parsedDoc) + { + if (TryGetCachedGeometry(node, parsedDoc, out List? cached, out string? geometryId)) + { + return cached; + } + + return CacheGeometry(geometryId, parsedDoc, BuildCommandsFromPoly(coords, close)); + } + + /// + /// Returns cached path commands for a rectangle definition, or builds and caches them. + /// + /// The SVG node that owns the geometry. + /// The rectangle origin X. + /// The rectangle origin Y. + /// The rectangle width. + /// The rectangle height. + /// The parsed SVG document and its caches. + /// The parsed path commands. + private static List GetOrBuildRectCommands( + XElement node, + float x, + float y, + float w, + float h, + ParsedDoc parsedDoc) + { + if (TryGetCachedGeometry(node, parsedDoc, out List? cached, out string? geometryId)) + { + return cached; + } + + float[] coords = + [ + x, y, + x + w, y, + x + w, y + h, + x, y + h + ]; + + return CacheGeometry(geometryId, parsedDoc, BuildCommandsFromPoly(coords, close: true)); + } + + /// + /// Returns cached path commands for an ellipse or circle definition, or builds and caches them. + /// + /// The SVG node that owns the geometry. + /// The ellipse center X. + /// The ellipse center Y. + /// The ellipse radius on the X axis. + /// The ellipse radius on the Y axis. + /// The parsed SVG document and its caches. + /// The parsed path commands. + private static List GetOrBuildEllipseCommands( + XElement node, + float cx, + float cy, + float rx, + float ry, + ParsedDoc parsedDoc) + { + if (TryGetCachedGeometry(node, parsedDoc, out List? cached, out string? geometryId)) + { + return cached; + } + + return CacheGeometry(geometryId, parsedDoc, BuildCommandsForEllipse(cx, cy, rx, ry)); + } + + /// + /// Looks up cached geometry for a reusable SVG element by its id. + /// + /// The SVG node that may have cached geometry. + /// The parsed SVG document and its caches. + /// When this method returns, contains the cached commands if found. + /// When this method returns, contains the element id used as the cache key. + /// if cached geometry was found; otherwise, . + private static bool TryGetCachedGeometry( + XElement node, + ParsedDoc parsedDoc, + [NotNullWhen(true)] out List? cached, + [NotNullWhen(true)] out string? geometryId) + { + geometryId = node.Attribute("id")?.Value; + if (geometryId is not null && parsedDoc.GeometryCache.TryGetValue(geometryId, out List? commands)) + { + cached = commands; + return true; + } + + cached = null; + return false; + } + + /// + /// Stores geometry in the per-document cache when the + /// source element has a reusable id. + /// + /// The cache key, or when the element is anonymous. + /// The parsed SVG document and its caches. + /// The newly built commands. + /// The cached or materialized command list. + private static List CacheGeometry(string? geometryId, ParsedDoc parsedDoc, List commands) + { + if (commands.Count == 0) + { + return []; + } + + if (geometryId is not null) + { + parsedDoc.GeometryCache.TryAdd(geometryId, commands); + } + + return commands; + } + + private static List BuildCommandsFromPoly(float[] coords, bool close) + { + List cmds = []; + + Vector2 start = new(coords[0], coords[1]); + cmds.Add(PathCommand.MoveTo(start)); + + Vector2 prev = start; + for (int i = 2; i + 1 < coords.Length; i += 2) + { + Vector2 p = new(coords[i], coords[i + 1]); + if (!NearlyEqual(prev, p)) + { + cmds.Add(PathCommand.LineTo(p)); + prev = p; + } + } + + if (close && !NearlyEqual(prev, start)) + { + cmds.Add(PathCommand.LineTo(start)); + cmds.Add(PathCommand.Close()); + } + + return cmds; + } + + private static List BuildCommandsForEllipse(float cx, float cy, float rx, float ry) + { + List cmds = []; + + // Start at (cx + rx, cy) + Vector2 s = new(cx + rx, cy); + cmds.Add(PathCommand.MoveTo(s)); + + // First half to (cx - rx, cy) + Vector2 p1 = new(cx - rx, cy); + cmds.Add(PathCommand.ArcTo(rx, ry, 0f, true, true, p1)); + + // Second half back to start + Vector2 p2 = new(cx + rx, cy); + cmds.Add(PathCommand.ArcTo(rx, ry, 0f, true, true, p2)); + + cmds.Add(PathCommand.Close()); + return cmds; + } + + private static List BuildCommandsFromPathData(string d) + { + List cmds = []; + + ReadOnlySpan s = d.AsSpan(); + + Vector2 first = default; + Vector2 curr = default; + Vector2 lastc = default; + + Vector2 p1, p2, p3; + + char op = '\0'; + char prevOp = '\0'; + bool rel = false; + bool figureOpen = false; + + while (true) + { + s = s.TrimStart(); + if (s.Length == 0) + { + break; + } + + char ch = s[0]; + if (char.IsDigit(ch) || ch == '-' || ch == '+' || ch == '.') + { + if (s.Length == 0 || op == 'Z') + { + return []; + } + } + else if (IsSeparator(ch)) + { + s = TrimSeparator(s); + } + else + { + op = ch; + rel = false; + if (char.IsLower(op)) + { + op = char.ToUpper(op, CultureInfo.InvariantCulture); + rel = true; + } + + s = TrimSeparator(s[1..]); + } + + switch (op) + { + case 'M': + { + s = FindPoint(s, rel, curr, out p1); + + if (figureOpen) + { + cmds.Add(PathCommand.Close()); + } + + cmds.Add(PathCommand.MoveTo(p1)); + first = curr = p1; + prevOp = '\0'; + op = 'L'; + figureOpen = true; + break; + } + + case 'L': + { + s = FindPoint(s, rel, curr, out p1); + if (!NearlyEqual(p1, curr)) + { + cmds.Add(PathCommand.LineTo(p1)); + } + + curr = p1; + break; + } + + case 'H': + { + s = FindScaler(s, out float x); + if (rel) + { + x += curr.X; + } + + p1 = new Vector2(x, curr.Y); + if (!NearlyEqual(p1, curr)) + { + cmds.Add(PathCommand.LineTo(p1)); + } + + curr = p1; + break; + } + + case 'V': + { + s = FindScaler(s, out float y); + if (rel) + { + y += curr.Y; + } + + p1 = new Vector2(curr.X, y); + if (!NearlyEqual(p1, curr)) + { + cmds.Add(PathCommand.LineTo(p1)); + } + + curr = p1; + break; + } + + case 'C': + { + s = FindPoint(s, rel, curr, out p1); + s = FindPoint(s, rel, curr, out p2); + s = FindPoint(s, rel, curr, out p3); + + cmds.Add(PathCommand.CubicTo(p1, p2, p3)); + + lastc = p2; + curr = p3; + break; + } + + case 'S': + { + s = FindPoint(s, rel, curr, out p2); + s = FindPoint(s, rel, curr, out p3); + + p1 = curr; + if (prevOp is 'C' or 'S') + { + p1.X -= lastc.X - curr.X; + p1.Y -= lastc.Y - curr.Y; + } + + cmds.Add(PathCommand.CubicTo(p1, p2, p3)); + + lastc = p2; + curr = p3; + break; + } + + case 'Q': + { + s = FindPoint(s, rel, curr, out p1); + s = FindPoint(s, rel, curr, out p2); + + cmds.Add(PathCommand.QuadraticTo(p1, p2)); + + lastc = p1; + curr = p2; + break; + } + + case 'T': + { + s = FindPoint(s, rel, curr, out p2); + + p1 = curr; + if (prevOp is 'Q' or 'T') + { + p1.X -= lastc.X - curr.X; + p1.Y -= lastc.Y - curr.Y; + } + + cmds.Add(PathCommand.QuadraticTo(p1, p2)); + + lastc = p1; + curr = p2; + break; + } + + case 'A': + { + if (TryFindScaler(ref s, out float rx) + && TryTrimSeparator(ref s) + && TryFindScaler(ref s, out float ry) + && TryTrimSeparator(ref s) + && TryFindScaler(ref s, out float angle) + && TryTrimSeparator(ref s) + && TryFindScaler(ref s, out float largeArc) + && TryTrimSeparator(ref s) + && TryFindScaler(ref s, out float sweep) + && TryFindPoint(ref s, rel, curr, out p1)) + { + cmds.Add(PathCommand.ArcTo(rx, ry, angle, largeArc == 1, sweep == 1, p1)); + curr = p1; + } + + break; + } + + case 'Z': + { + if (figureOpen) + { + if (!NearlyEqual(curr, first)) + { + cmds.Add(PathCommand.LineTo(first)); + } + + cmds.Add(PathCommand.Close()); + curr = first; + figureOpen = false; + } + + break; + } + + default: + { + return []; + } + } + + if (prevOp == 0) + { + first = curr; + } + + prevOp = op; + if (op == 'M') + { + figureOpen = true; + } + } + + return cmds; + } + + private static bool TryParseViewBox(string? s, out float x, out float y, out float w, out float h) + { + x = 0f; + y = 0f; + w = 0f; + h = 0f; + + if (string.IsNullOrEmpty(s)) + { + return false; + } + + float[] v = ParseFloatList(s); + if (v.Length == 4) + { + x = v[0]; + y = v[1]; + w = v[2]; + h = v[3]; + return true; + } + + return false; + } + + private static Matrix3x2 ParseTransform(string? s) + { + if (string.IsNullOrEmpty(s)) + { + return Matrix3x2.Identity; + } + + Matrix3x2 m = Matrix3x2.Identity; + int i = 0; + int n = s.Length; + + while (i < n) + { + SkipSep(s, ref i); + if (i >= n) + { + break; + } + + int start = i; + while (i < n && char.IsLetter(s[i])) + { + i++; + } + + ReadOnlySpan op = s.AsSpan(start, i - start); + + SkipSep(s, ref i); + if (i >= n || s[i] != '(') + { + break; + } + + i++; // '(' + + int argsStart = i; + int depth = 1; + while (i < n && depth > 0) + { + if (s[i] == '(') + { + depth++; + } + else if (s[i] == ')') + { + depth--; + } + + i++; + } + + ReadOnlySpan args = s.AsSpan(argsStart, (i - argsStart) - 1); + float[] a = ParseFloatList(args); + + Matrix3x2 t = Matrix3x2.Identity; + if (op.SequenceEqual("matrix")) + { + if (a.Length >= 6) + { + t = new Matrix3x2(a[0], a[1], a[2], a[3], a[4], a[5]); + } + } + else if (op.SequenceEqual("translate")) + { + if (a.Length == 1) + { + t = Matrix3x2.CreateTranslation(a[0], 0f); + } + else if (a.Length >= 2) + { + t = Matrix3x2.CreateTranslation(a[0], a[1]); + } + } + else if (op.SequenceEqual("scale")) + { + if (a.Length == 1) + { + t = Matrix3x2.CreateScale(a[0], a[0]); + } + else if (a.Length >= 2) + { + t = Matrix3x2.CreateScale(a[0], a[1]); + } + } + else if (op.SequenceEqual("rotate")) + { + if (a.Length >= 1) + { + t = Matrix3x2.CreateRotation(a[0] * (float)(Math.PI / 180.0)); + } + } + else if (op.SequenceEqual("skewX")) + { + if (a.Length >= 1) + { + t = new Matrix3x2(1f, 0f, MathF.Tan(a[0] * (float)(Math.PI / 180.0)), 1f, 0f, 0f); + } + } + else if (op.SequenceEqual("skewY")) + { + if (a.Length >= 1) + { + t = new Matrix3x2(1f, MathF.Tan(a[0] * (float)(Math.PI / 180.0)), 0f, 1f, 0f, 0f); + } + } + + m *= t; + SkipSep(s, ref i); + } + + return m; + + static void SkipSep(string s, ref int i) + { + int n = s.Length; + while (i < n) + { + char c = s[i]; + if (char.IsWhiteSpace(c) || c == ',') + { + i++; + } + else + { + break; + } + } + } + } + + private static ReadOnlySpan FindPoint(ReadOnlySpan str, bool rel, Vector2 current, out Vector2 value) + { + str = FindScaler(str, out float x); + str = FindScaler(str, out float y); + + if (rel) + { + x += current.X; + y += current.Y; + } + + value = new Vector2(x, y); + return str; + } + + private static ReadOnlySpan FindScaler(ReadOnlySpan str, out float scaler) + { + str = TrimSeparator(str); + scaler = 0f; + + for (int i = 0; i < str.Length; i++) + { + if (IsSeparator(str[i])) + { + scaler = ParseFloat(str[..i]); + return str[i..]; + } + } + + if (str.Length > 0) + { + scaler = ParseFloat(str); + } + + return []; + } + + private static bool TryTrimSeparator(ref ReadOnlySpan str) + { + ReadOnlySpan result = TrimSeparator(str); + if (str[^result.Length..].StartsWith(result)) + { + str = result; + return true; + } + + return false; + } + + private static bool TryFindScaler(ref ReadOnlySpan str, out float value) + { + ReadOnlySpan result = FindScaler(str, out float v); + if (str[^result.Length..].StartsWith(result)) + { + value = v; + str = result; + return true; + } + + value = default; + return false; + } + + private static bool TryFindPoint(ref ReadOnlySpan str, bool relative, Vector2 current, out Vector2 value) + { + ReadOnlySpan result = FindPoint(str, relative, current, out Vector2 v); + if (str[^result.Length..].StartsWith(result)) + { + value = v; + str = result; + return true; + } + + value = default; + return false; + } + + private static bool IsSeparator(char ch) + => char.IsWhiteSpace(ch) || ch == ','; + + private static ReadOnlySpan TrimSeparator(ReadOnlySpan s) + { + int idx = 0; + for (; idx < s.Length; idx++) + { + if (!IsSeparator(s[idx])) + { + break; + } + } + + return s[idx..]; + } + + private static float ParseFloat(ReadOnlySpan str) + => str.IsEmpty ? 0 : float.Parse(str, CultureInfo.InvariantCulture); + + private static float[] ParseFloatList(string s) + => string.IsNullOrEmpty(s) ? [] : ParseFloatList(s.AsSpan()); + + private static float[] ParseFloatList(ReadOnlySpan s) + { + if (s.IsEmpty) + { + return []; + } + + List vals = []; + int i = 0; + int n = s.Length; + + while (i < n) + { + while (i < n && (char.IsWhiteSpace(s[i]) || s[i] == ',')) + { + i++; + } + + if (i >= n) + { + break; + } + + int start = i; + + if (s[i] is '+' or '-') + { + i++; + } + + bool dot = false; + while (i < n) + { + char c = s[i]; + if (char.IsDigit(c)) + { + i++; + continue; + } + + if (c == '.' && !dot) + { + dot = true; + i++; + continue; + } + + break; + } + + if (i < n && (s[i] == 'e' || s[i] == 'E')) + { + i++; + if (i < n && (s[i] == '+' || s[i] == '-')) + { + i++; + } + + while (i < n && char.IsDigit(s[i])) + { + i++; + } + } + + if (float.TryParse(s[start..i], NumberStyles.Float, CultureInfo.InvariantCulture, out float v)) + { + vals.Add(v); + } + } + + return [.. vals]; + } + + private static bool NearlyEqual(in Vector2 a, in Vector2 b, float eps = 1e-3f) + => MathF.Abs(a.X - b.X) <= eps && MathF.Abs(a.Y - b.Y) <= eps; + + private sealed class ParsedDoc + { + public required XDocument Doc { get; init; } + + public required Dictionary IdMap { get; init; } + + /// + /// Gets the per-document cache of parsed geometry for reusable SVG defs. + /// + public ConcurrentDictionary> GeometryCache { get; } = new(StringComparer.Ordinal); + + /// + /// Gets the per-document cache of resolved paint servers. + /// + public ConcurrentDictionary PaintServerCache { get; } = new(StringComparer.Ordinal); + } + } +} diff --git a/SixLabors.Fonts/Tables/General/Svg/SvgTable.cs b/SixLabors.Fonts/Tables/General/Svg/SvgTable.cs new file mode 100644 index 0000000..7c4d4ae --- /dev/null +++ b/SixLabors.Fonts/Tables/General/Svg/SvgTable.cs @@ -0,0 +1,287 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; +using System.IO.Compression; +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts.Tables.General.Svg { + /// + /// Represents the SVG table which contains SVG documents for glyph rendering. + /// + /// + internal class SvgTable : Table + { + /// + /// The table name identifier for the SVG table. + /// + internal const string TableName = "SVG "; + + /// + /// The raw byte data containing the SVG document payloads. + /// + private readonly byte[] tableData; + + /// + /// The offset from the beginning of the SVG table to the SVG Document Index. + /// + private readonly uint svgDocIndexOffset; + + /// + /// The absolute offset of the start of the table data buffer within the font stream. + /// + private readonly uint tableBaseOffset; + + /// + /// The array of SVG Document Index entries, sorted by start glyph ID. + /// + private readonly SvgDocumentIndexEntry[] entries; + + /// + /// Initializes a new instance of the class. + /// + /// The raw byte data containing the SVG document payloads. + /// The offset from the beginning of the SVG table to the SVG Document Index. + /// The absolute offset of the start of the table data buffer within the font stream. + /// The array of SVG Document Index entries. + private SvgTable(byte[] tableData, uint svgDocIndexOffset, uint tableBaseOffset, SvgDocumentIndexEntry[] entries) + { + this.tableData = tableData; + this.svgDocIndexOffset = svgDocIndexOffset; + this.tableBaseOffset = tableBaseOffset; + this.entries = entries; + } + + /// + /// Loads the SVG table from the specified font reader. + /// + /// The font reader to read the table from. + /// The , or if the table is not present in the font. + public static SvgTable? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + /// + /// Loads the SVG table from the specified binary reader. + /// + /// The binary reader positioned at the start of the SVG table. + /// The , or if the table is not valid in the font. + public static SvgTable? Load(BigEndianBinaryReader reader) + { + // HEADER + // | Type | Name | Description | + // | ---------| ------------------| ----------------------------------------------------------| + // | uint16 | version | Table version number(starts at 0). | + // | Offset32 | svgDocIndexOffset | Offset(from beginning of SVG table) to SVG Document Index.| + // | uint32 | reserved | Reserved; set to 0 + ushort version = reader.ReadUInt16(); + if (version != 0) + { + throw new NotSupportedException($"Only SVG table version 0 is supported. Found version {version}."); + } + + uint svgDocIndexOffset = reader.ReadUInt32(); + _ = reader.ReadUInt32(); // reserved + + // SVG Document Index + // | Type | Name | Description | + // | ------------------| -----------| ------------------------------------------------------------| + // | uint16 | numEntries | Number of entries in the SVG Document Index. | + // | Entry[numEntries] | entries | Array of SVG Document Index Entries(sorted by startGlyphID).| + reader.Seek(svgDocIndexOffset, SeekOrigin.Begin); + ushort numEntries = reader.ReadUInt16(); + if (numEntries == 0) + { + // The spec says the number of entries must be non-zero. + return null; + } + + SvgDocumentIndexEntry[] entries = new SvgDocumentIndexEntry[numEntries]; + + // SVG Document Index Entry + // | Type | Name | Description | + // | ---------| -------------| -----------------------------------------------------------------------------------------| + // | uint16 | startGlyphID | First glyph ID in this range(inclusive). | + // | uint16 | endGlyphID | Last glyph ID in this range(inclusive). | + // | Offset32 | svgDocOffset | Offset from the beginning of the SVG Document Index to an SVG document. Must be non-zero.| + + // Track min relative offset from the Document Index and absolute max end. + uint minRelOffset = uint.MaxValue; + uint maxEnd = 0; + for (int i = 0; i < numEntries; i++) + { + ushort startGlyphId = reader.ReadUInt16(); + ushort endGlyphId = reader.ReadUInt16(); + uint svgDocOffset = reader.ReadUInt32(); + uint svgDocLength = reader.ReadUInt32(); + + if (svgDocOffset == 0 || svgDocLength == 0) + { + throw new InvalidFontFileException("SVG table contains an entry with zero offset or length."); + } + + if (svgDocOffset < minRelOffset) + { + minRelOffset = svgDocOffset; + } + + // Track the farthest byte we need to cover in the table buffer. + uint absEnd = svgDocIndexOffset + svgDocOffset + svgDocLength; + if (absEnd > maxEnd) + { + maxEnd = absEnd; + } + + entries[i] = new SvgDocumentIndexEntry(startGlyphId, endGlyphId, svgDocOffset, svgDocLength); + } + + // Read exactly the covered range. + uint tableStart = svgDocIndexOffset + minRelOffset; + int byteCount = (int)(maxEnd - tableStart); + + reader.Seek(tableStart, SeekOrigin.Begin); + byte[] tableData = reader.ReadBytes(byteCount); + + return new SvgTable(tableData, svgDocIndexOffset, tableStart, entries); + } + + /// + /// Returns true if the SVG Document Index contains a document for . + /// + /// The glyph identifier to look up. + /// if a document exists for the glyph; otherwise, . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ContainsGlyph(ushort glyphId) + => this.TryFindEntry(glyphId, out _); + + /// + /// Gets the encoded document slice for a glyph without opening a stream. + /// + /// The glyph identifier to look up. + /// When this method returns, contains the start offset within the table data buffer. + /// When this method returns, contains the length of the SVG document in bytes. + /// if a document was found for the glyph; otherwise, . + public bool TryGetDocumentSpan(ushort glyphId, out int start, out int length) + { + if (this.TryFindEntry(glyphId, out SvgDocumentIndexEntry e)) + { + start = (int)((this.svgDocIndexOffset + e.SvgDocOffset) - this.tableBaseOffset); + length = (int)e.SvgDocLength; + return true; + } + + start = 0; + length = 0; + return false; + } + + /// + /// Opens a decoding stream for the SVG document associated with the specified glyph. + /// If the payload is gzip-compressed (RFC 1952), wraps it in a ; + /// otherwise returns the raw memory stream. The caller owns the returned stream. + /// + /// The glyph identifier to look up. + /// When this method returns, contains the decoded SVG document stream, or if not found. + /// if a document was found and a stream was opened; otherwise, . + public bool TryOpenDecodedDocumentStream(ushort glyphId, out Stream stream) + { + if (!this.TryOpenEncodedDocumentStream(glyphId, out Stream encoded)) + { + stream = Stream.Null; + return false; + } + + if (encoded is MemoryStream ms && ms.Length >= 2) + { + long pos = ms.Position; + int b0 = ms.ReadByte(); + int b1 = ms.ReadByte(); + ms.Position = pos; + + // Start of GZIP (RFC1952) + if (b0 == 0x1F && b1 == 0x8B) + { + stream = new GZipStream(ms, CompressionMode.Decompress, leaveOpen: false); + return true; + } + } + + stream = encoded; // plain UTF-8 XML + return true; + } + + /// + /// Attempts to open a raw (potentially gzip-compressed) memory stream for the SVG document + /// associated with the specified glyph. + /// + /// The glyph identifier to look up. + /// When this method returns, contains the encoded SVG document stream, or if not found. + /// if a document was found and a stream was opened; otherwise, . + private bool TryOpenEncodedDocumentStream(ushort glyphId, out Stream stream) + { + if (this.TryFindEntry(glyphId, out SvgDocumentIndexEntry e)) + { + int start = (int)((this.svgDocIndexOffset + e.SvgDocOffset) - this.tableBaseOffset); + int length = (int)e.SvgDocLength; + stream = new MemoryStream(this.tableData, start, length, writable: false); + return true; + } + + stream = Stream.Null; + return false; + } + + /// + /// Performs a binary search on the SVG Document Index entries to find the entry + /// whose glyph ID range contains the specified glyph. + /// + /// The glyph identifier to search for. + /// When this method returns, contains the matching index entry, or the default value if not found. + /// if a matching entry was found; otherwise, . + private bool TryFindEntry(ushort glyphId, out SvgDocumentIndexEntry entry) + { + int lo = 0; + int hi = this.entries.Length - 1; + int candidate = -1; + + while (lo <= hi) + { + int mid = (int)((uint)(lo + hi) >> 1); + ushort start = this.entries[mid].StartGlyphId; + + if (start <= glyphId) + { + candidate = mid; + lo = mid + 1; + } + else + { + hi = mid - 1; + } + } + + if (candidate >= 0) + { + SvgDocumentIndexEntry e = this.entries[candidate]; + if (glyphId <= e.EndGlyphId) + { + entry = e; + return true; + } + } + + entry = default; + return false; + } + } +} diff --git a/SixLabors.Fonts/Tables/General/VerticalHeadTable.cs b/SixLabors.Fonts/Tables/General/VerticalHeadTable.cs new file mode 100644 index 0000000..91ababf --- /dev/null +++ b/SixLabors.Fonts/Tables/General/VerticalHeadTable.cs @@ -0,0 +1,238 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General { + /// + /// Represents the vertical header table, which contains information needed to lay out fonts + /// whose characters are written vertically. + /// + /// + internal sealed class VerticalHeadTable : Table + { + /// + /// The table name identifier. + /// + internal const string TableName = "vhea"; + + /// + /// Initializes a new instance of the class. + /// + /// The vertical typographic ascender. + /// The vertical typographic descender. + /// The vertical typographic line gap. + /// The maximum advance height. + /// The minimum top side bearing. + /// The minimum bottom side bearing. + /// The maximum y extent. + /// The caret slope rise. + /// The caret slope run. + /// The caret offset for slanted fonts. + /// The number of vertical metrics in the 'vmtx' table. + public VerticalHeadTable( + short ascender, + short descender, + short lineGap, + short advanceHeightMax, + short minTopSideBearing, + short minBottomSideBearing, + short yMaxExtent, + short caretSlopeRise, + short caretSlopeRun, + short caretOffset, + ushort numberOfVMetrics) + { + this.Ascender = ascender; + this.Descender = descender; + this.LineGap = lineGap; + this.AdvanceHeightMax = advanceHeightMax; + this.MinTopSideBearing = minTopSideBearing; + this.MinBottomSideBearing = minBottomSideBearing; + this.YMaxExtent = yMaxExtent; + this.CaretSlopeRise = caretSlopeRise; + this.CaretSlopeRun = caretSlopeRun; + this.CaretOffset = caretOffset; + this.NumberOfVMetrics = numberOfVMetrics; + } + + /// + /// Gets the vertical typographic ascender. + /// + public short Ascender { get; } + + /// + /// Gets the vertical typographic descender. + /// + public short Descender { get; } + + /// + /// Gets the vertical typographic line gap. + /// + public short LineGap { get; } + + /// + /// Gets the maximum advance height in font design units. + /// + public short AdvanceHeightMax { get; } + + /// + /// Gets the minimum top side bearing in font design units. + /// + public short MinTopSideBearing { get; } + + /// + /// Gets the minimum bottom side bearing in font design units. + /// + public short MinBottomSideBearing { get; } + + /// + /// Gets the maximum y extent: minTopSideBearing + (yMin - yMax). + /// + public short YMaxExtent { get; } + + /// + /// Gets the caret slope rise. A value of 0 for rise and 1 for run specifies a horizontal caret. + /// + public short CaretSlopeRise { get; } + + /// + /// Gets the caret slope run. A value of 0 for non-slanted fonts. + /// + public short CaretSlopeRun { get; } + + /// + /// Gets the caret offset for slanted fonts. Set to 0 for non-slanted fonts. + /// + public short CaretOffset { get; } + + /// + /// Gets the number of vertical metrics in the 'vmtx' table. + /// + public ushort NumberOfVMetrics { get; } + + /// + /// Loads the from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static VerticalHeadTable? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + /// + /// Loads the from the specified binary reader. + /// + /// The big-endian binary reader. + /// The . + public static VerticalHeadTable Load(BigEndianBinaryReader reader) + { + // +---------+----------------------+----------------------------------------------------------------------+ + // | Type | Name | Description | + // +=========+======================+======================================================================+ + // | fixed32 | version | Version number of the Vertical Header Table (0x00011000 for | + // | | | the current version). | + // +---------+----------------------+----------------------------------------------------------------------+ + // | int16 | vertTypoAscender | The vertical typographic ascender for this font. It is the distance | + // | | | in FUnits from the vertical center baseline to the right of the | + // | | | design space. This will usually be set to half the horizontal | + // | | | advance of full-width glyphs. For example, if the full width is | + // | | | 1000 FUnits, this field will be set to 500. | + // +---------+----------------------+----------------------------------------------------------------------+ + // | int16 | vertTypoDescender | The vertical typographic descender for this font. It is the | + // | | | distance in FUnits from the vertical center baseline to the left of | + // | | | the design space. This will usually be set to half the horizontal | + // | | | advance of full-width glyphs. For example, if the full width is | + // | | | 1000 FUnits, this field will be set to -500. | + // +---------+----------------------+----------------------------------------------------------------------+ + // | int16 | vertTypoLineGap | The vertical typographic line gap for this font. | + // +---------+----------------------+----------------------------------------------------------------------+ + // | int16 | advanceHeightMax | The maximum advance height measurement in FUnits found in | + // | | | the font. This value must be consistent with the entries in the | + // | | | vertical metrics table. | + // +---------+----------------------+----------------------------------------------------------------------+ + // | int16 | minTopSideBearing | The minimum top side bearing measurement in FUnits found in | + // | | | the font, in FUnits. This value must be consistent with the | + // | | | entries in the vertical metrics table. | + // +---------+----------------------+----------------------------------------------------------------------+ + // | int16 | minBottomSideBearing | The minimum bottom side bearing measurement in FUnits | + // | | | found in the font, in FUnits. This value must be consistent with | + // | | | the entries in the vertical metrics table. | + // +---------+----------------------+----------------------------------------------------------------------+ + // | int16 | yMaxExtent | This is defined as the value of the minTopSideBearing field | + // | | | added to the result of the value of the yMin field subtracted | + // | | | from the value of the yMax field. | + // +---------+----------------------+----------------------------------------------------------------------+ + // | int16 | caretSlopeRise | The value of the caretSlopeRise field divided by the value of the | + // | | | caretSlopeRun field determines the slope of the caret. A value | + // | | | of 0 for the rise and a value of 1 for the run specifies a | + // | | | horizontal caret. A value of 1 for the rise and a value of 0 for the | + // | | | run specifies a vertical caret. A value between 0 for the rise and | + // | | | 1 for the run is desirable for fonts whose glyphs are oblique or | + // | | | italic. For a vertical font, a horizontal caret is best. | + // +---------+----------------------+----------------------------------------------------------------------+ + // | int16 | caretSlopeRun | See the caretSlopeRise field. Value = 0 for non-slanted fonts. | + // +---------+----------------------+----------------------------------------------------------------------+ + // | int16 | caretOffset | The amount by which the highlight on a slanted glyph needs to | + // | | | be shifted away from the glyph in order to produce the best | + // | | | appearance. Set value equal to 0 for non-slanted fonts. | + // +---------+----------------------+----------------------------------------------------------------------+ + // | int16 | reserved | Set to 0. | + // +---------+----------------------+----------------------------------------------------------------------+ + // | int16 | reserved | Set to 0. | + // +---------+----------------------+----------------------------------------------------------------------+ + // | int16 | reserved | Set to 0. | + // +---------+----------------------+----------------------------------------------------------------------+ + // | int16 | reserved | Set to 0. | + // +---------+----------------------+----------------------------------------------------------------------+ + // | int16 | metricDataFormat | Set to 0. | + // +---------+----------------------+----------------------------------------------------------------------+ + // | uint16 | numOfLongVerMetrics | Number of advance heights in the Vertical Metrics table. | + // +---------+----------------------+----------------------------------------------------------------------+ + ushort majorVersion = reader.ReadUInt16(); + ushort minorVersion = reader.ReadUInt16(); + short vertTypoAscender = reader.ReadInt16(); + short vertTypoDescender = reader.ReadInt16(); + short vertTypoLineGap = reader.ReadInt16(); + short advanceHeightMax = reader.ReadInt16(); + short minTopSideBearing = reader.ReadInt16(); + short minBottomSideBearing = reader.ReadInt16(); + short yMaxExtent = reader.ReadInt16(); + short caretSlopeRise = reader.ReadInt16(); + short caretSlopeRun = reader.ReadInt16(); + short caretOffset = reader.ReadInt16(); + reader.ReadInt16(); // reserved + reader.ReadInt16(); // reserved + reader.ReadInt16(); // reserved + reader.ReadInt16(); // reserved + short metricDataFormat = reader.ReadInt16(); // 0 + + if (metricDataFormat != 0) + { + throw new InvalidFontTableException($"Expected metricDataFormat = 0 found {metricDataFormat}", TableName); + } + + ushort numOfLongVerMetrics = reader.ReadUInt16(); + + return new VerticalHeadTable( + vertTypoAscender, + vertTypoDescender, + vertTypoLineGap, + advanceHeightMax, + minTopSideBearing, + minBottomSideBearing, + yMaxExtent, + caretSlopeRise, + caretSlopeRun, + caretOffset, + numOfLongVerMetrics); + } + } +} diff --git a/SixLabors.Fonts/Tables/General/VerticalMetricsTable.cs b/SixLabors.Fonts/Tables/General/VerticalMetricsTable.cs new file mode 100644 index 0000000..44b78ca --- /dev/null +++ b/SixLabors.Fonts/Tables/General/VerticalMetricsTable.cs @@ -0,0 +1,127 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General { + /// + /// Represents the vertical metrics table, which contains the vertical layout metrics + /// (advance heights and top side bearings) for each glyph. + /// + /// + internal sealed class VerticalMetricsTable : Table + { + /// + /// The table name identifier. + /// + internal const string TableName = "vmtx"; + + /// + /// The top side bearings array for all glyphs. + /// + private readonly short[] topSideBearings; + + /// + /// The advance heights array for glyphs with full metric records. + /// + private readonly ushort[] advancedHeights; + + /// + /// Initializes a new instance of the class. + /// + /// The advance heights for each glyph. + /// The top side bearings for each glyph. + public VerticalMetricsTable(ushort[] advancedHeights, short[] topSideBearings) + { + this.advancedHeights = advancedHeights; + this.topSideBearings = topSideBearings; + } + + /// + /// Gets the advance height for the specified glyph. If the glyph index exceeds the + /// number of metric records, the first record's advance height is returned. + /// + /// The glyph index. + /// The advance height in font design units. + public ushort GetAdvancedHeight(int glyphIndex) + { + if (glyphIndex >= this.advancedHeights.Length) + { + return this.advancedHeights[0]; + } + + return this.advancedHeights[glyphIndex]; + } + + /// + /// Gets the top side bearing for the specified glyph. + /// + /// The glyph index. + /// The top side bearing in font design units. + internal short GetTopSideBearing(int glyphIndex) + { + if (glyphIndex >= this.topSideBearings.Length) + { + return this.topSideBearings[0]; + } + + return this.topSideBearings[glyphIndex]; + } + + /// + /// Loads the from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static VerticalMetricsTable? Load(FontReader reader) + { + // You should load all dependent tables prior to manipulating the reader + VerticalHeadTable headTable = reader.GetTable(); + MaximumProfileTable profileTable = reader.GetTable(); + + // Move to start of table + if (!reader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader, headTable.NumberOfVMetrics, profileTable.GlyphCount); + } + } + + /// + /// Loads the from the specified binary reader. + /// + /// The big-endian binary reader. + /// The number of vertical metric records (from 'vhea'). + /// The total number of glyphs in the font (from 'maxp'). + /// The . + public static VerticalMetricsTable Load(BigEndianBinaryReader reader, int metricCount, int glyphCount) + { + // Type | Name | Description + // longVerMetric | vMetrics[numberOfVMetrics] | Paired advance height and top side bearing values for each glyph. Records are indexed by glyph ID. + // int16 | leftSideBearing[numGlyphs - numberOfVMetrics] | Top side bearings for glyph IDs greater than or equal to numberOfVMetrics. + int bearingCount = glyphCount - metricCount; + ushort[] advancedHeights = new ushort[metricCount]; + short[] topSideBearings = new short[glyphCount]; + + for (int i = 0; i < metricCount; i++) + { + // longVerMetric Record: + // Type | Name | Description + // -------| ------------- | ----------------------------------------------------------- + // uint16 | advanceHeight | The advance height of the glyph.Signed integer in FUnits. + // int16 | topSideBearing| The top side bearing of the glyph. Signed integer in FUnits + advancedHeights[i] = reader.ReadUInt16(); + topSideBearings[i] = reader.ReadInt16(); + } + + for (int i = 0; i < bearingCount; i++) + { + topSideBearings[metricCount + i] = reader.ReadInt16(); + } + + return new VerticalMetricsTable(advancedHeights, topSideBearings); + } + } +} diff --git a/SixLabors.Fonts/Tables/IFontTables.cs b/SixLabors.Fonts/Tables/IFontTables.cs new file mode 100644 index 0000000..4068f5d --- /dev/null +++ b/SixLabors.Fonts/Tables/IFontTables.cs @@ -0,0 +1,130 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Tables.General; +using SixLabors.Fonts.Tables.General.Colr; +using SixLabors.Fonts.Tables.General.Kern; +using SixLabors.Fonts.Tables.General.Name; +using SixLabors.Fonts.Tables.General.Post; + +namespace SixLabors.Fonts.Tables { +#pragma warning disable SA1600 // Elements should be documented + /// + /// Defines the contract for shared font tables + /// + /// + internal interface IFontTables + { + // Required Tables both TTF and CFF + // +-------+-----------------------------------+ + // | Tag | Name | + // +=======+===================================+ + // | cmap | Character to glyph mapping | + // +-------+-----------------------------------+ + // | head | Font header | + // +-------+-----------------------------------+ + // | hhea | Horizontal header | + // +-------+-----------------------------------+ + // | hmtx | Horizontal metrics | + // +-------+-----------------------------------+ + // | maxp | Maximum profile | + // +-------+-----------------------------------+ + // | name | Naming table | + // +-------+-----------------------------------+ + // | OS/2 | OS/2 and Windows specific metrics | + // +-------+-----------------------------------+ + // | post | PostScript information | + // +-------+-----------------------------------+ + CMapTable Cmap { get; set; } + + HeadTable Head { get; set; } + + HorizontalHeadTable Hhea { get; set; } + + HorizontalMetricsTable Htmx { get; set; } + + MaximumProfileTable Maxp { get; set; } + + NameTable Name { get; set; } + + OS2Table Os2 { get; set; } + + PostTable Post { get; set; } + + // Advanced Typographic Tables + // +------+-------------------------+ + // | Tag | Name | + // +======+=========================+ + // | BASE | Baseline data | + // +------+-------------------------+ + // | GDEF | Glyph definition data | + // +------+-------------------------+ + // | GPOS | Glyph positioning data | + // +------+-------------------------+ + // | GSUB | Glyph substitution data | + // +------+-------------------------+ + // | JSTF | Justification data | + // +------+-------------------------+ + // | MATH | Math layout data | + // +------+-------------------------+ + public GlyphDefinitionTable? Gdef { get; set; } + + public GSubTable? GSub { get; set; } + + public GPosTable? GPos { get; set; } + + // Tables Related to Color Fonts + // +------+------------------------------------------+ + // | Tag | Name | + // +======+==========================================+ + // | COLR | Color table | + // +------+------------------------------------------+ + // | CPAL | Color palette table | + // +------+------------------------------------------+ + // | CBDT | Color bitmap data | + // +------+------------------------------------------+ + // | CBLC | Color bitmap location data | + // +------+------------------------------------------+ + // | sbix | Standard bitmap graphics | + // +------+------------------------------------------+ + // | SVG | The SVG (Scalable Vector Graphics) table | + // +------+------------------------------------------+ + ColrTable? Colr { get; set; } + + CpalTable? Cpal { get; set; } + + // +------+---------------------------+ + // | Tag | Name | + // +======+===========================+ + // | DSIG | Digital signature | + // +------+---------------------------+ + // | hdmx | Horizontal device metrics | + // +------+---------------------------+ + // | kern | Kerning | + // +------+---------------------------+ + // | LTSH | Linear threshold data | + // +------+---------------------------+ + // | MERG | Merge | + // +------+---------------------------+ + // | meta | Metadata | + // +------+---------------------------+ + // | STAT | Style attributes | + // +------+---------------------------+ + // | PCLT | PCL 5 data | + // +------+---------------------------+ + // | VDMX | Vertical device metrics | + // +------+---------------------------+ + // | vhea | Vertical Metrics header | + // +------+---------------------------+ + // | vmtx | Vertical Metrics | + // +------+---------------------------+ + KerningTable? Kern { get; set; } + + VerticalHeadTable? Vhea { get; set; } + + VerticalMetricsTable? Vmtx { get; set; } + } +#pragma warning restore SA1600 // Elements should be documented + +} diff --git a/SixLabors.Fonts/Tables/Table.cs b/SixLabors.Fonts/Tables/Table.cs new file mode 100644 index 0000000..6a64c37 --- /dev/null +++ b/SixLabors.Fonts/Tables/Table.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables { + /// + /// The base class for all OpenType font tables. + /// + internal abstract class Table + { + } +} diff --git a/SixLabors.Fonts/Tables/TableFormat.cs b/SixLabors.Fonts/Tables/TableFormat.cs new file mode 100644 index 0000000..ae65d02 --- /dev/null +++ b/SixLabors.Fonts/Tables/TableFormat.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables { + /// + /// Specifies the font container format used to store font table data. + /// + internal enum TableFormat + { + /// + /// woff font table format. + /// + Woff, + + /// + /// woff2 font table format. + /// + Woff2, + + /// + /// otf font table format. + /// + Otf + } +} diff --git a/SixLabors.Fonts/Tables/TableHeader.cs b/SixLabors.Fonts/Tables/TableHeader.cs new file mode 100644 index 0000000..900b958 --- /dev/null +++ b/SixLabors.Fonts/Tables/TableHeader.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables { + /// + /// Represents a table record entry in the font directory. + /// Each record contains the tag, checksum, offset, and length of a font table. + /// + /// + internal class TableHeader + { + /// + /// Initializes a new instance of the class. + /// + /// The four-byte table tag identifier. + /// The checksum for the table. + /// The byte offset of the table from the beginning of the font file. + /// The length of the table in bytes. + public TableHeader(string tag, uint checkSum, uint offset, uint len) + { + this.Tag = tag; + this.CheckSum = checkSum; + this.Offset = offset; + this.Length = len; + } + + /// + /// Gets the four-byte table tag identifier (e.g. "head", "glyf", "cmap"). + /// + public string Tag { get; } + + /// + /// Gets the byte offset of the table from the beginning of the font file. + /// + public uint Offset { get; } + + /// + /// Gets the checksum for the table, used to verify table integrity. + /// + public uint CheckSum { get; } + + /// + /// Gets the length of the table data in bytes. + /// + public uint Length { get; } + + /// + /// Reads a from the given reader. + /// + /// The binary reader positioned at the table record. + /// The parsed . + public static TableHeader Read(BigEndianBinaryReader reader) => new TableHeader( + reader.ReadTag(), + reader.ReadUInt32(), + reader.ReadOffset32(), + reader.ReadUInt32()); + + /// + /// Creates a positioned at the start of this table's data. + /// + /// The font file stream. + /// A reader positioned at the table data. + public virtual BigEndianBinaryReader CreateReader(Stream stream) + { + stream.Seek(this.Offset, SeekOrigin.Begin); + + return new BigEndianBinaryReader(stream, true); + } + } +} diff --git a/SixLabors.Fonts/Tables/TableLoader.cs b/SixLabors.Fonts/Tables/TableLoader.cs new file mode 100644 index 0000000..4eca5da --- /dev/null +++ b/SixLabors.Fonts/Tables/TableLoader.cs @@ -0,0 +1,155 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; +using SixLabors.Fonts.Tables.Cff; +using SixLabors.Fonts.Tables.General; +using SixLabors.Fonts.Tables.General.Colr; +using SixLabors.Fonts.Tables.General.Kern; +using SixLabors.Fonts.Tables.General.Name; +using SixLabors.Fonts.Tables.General.Post; +using SixLabors.Fonts.Tables.General.Svg; +using SixLabors.Fonts.Tables.TrueType; +using SixLabors.Fonts.Tables.TrueType.Glyphs; +using SixLabors.Fonts.Tables.TrueType.Hinting; +using System; +using System.Collections.Generic; + +namespace SixLabors.Fonts.Tables { + /// + /// Provides registration and loading of font tables by tag or CLR type. + /// Maps four-byte table tags (e.g. "cmap", "head") to their static Load factory methods. + /// + internal class TableLoader + { + private readonly Dictionary> loaders = []; + private readonly Dictionary types = []; + private readonly Dictionary> typesLoaders = []; + + /// + /// Initializes a new instance of the class + /// with all known table parsers registered. + /// + public TableLoader() + { + // We will hard code mapping registration in here for all the tables + this.Register(NameTable.TableName, NameTable.Load); + this.Register(CMapTable.TableName, CMapTable.Load); + this.Register(HeadTable.TableName, HeadTable.Load); + this.Register(HorizontalHeadTable.TableName, HorizontalHeadTable.Load); + this.Register(HorizontalMetricsTable.TableName, HorizontalMetricsTable.Load); + this.Register(VerticalHeadTable.TableName, VerticalHeadTable.Load); + this.Register(VerticalMetricsTable.TableName, VerticalMetricsTable.Load); + this.Register(MaximumProfileTable.TableName, MaximumProfileTable.Load); + this.Register(OS2Table.TableName, OS2Table.Load); + this.Register(IndexLocationTable.TableName, IndexLocationTable.Load); + this.Register(GlyphTable.TableName, GlyphTable.Load); + this.Register(KerningTable.TableName, KerningTable.Load); + this.Register(ColrTable.TableName, ColrTable.Load); + this.Register(CpalTable.TableName, CpalTable.Load); + this.Register(GPosTable.TableName, GPosTable.Load); + this.Register(GSubTable.TableName, GSubTable.Load); + this.Register(CvtTable.TableName, CvtTable.Load); + this.Register(FpgmTable.TableName, FpgmTable.Load); + this.Register(PrepTable.TableName, PrepTable.Load); + this.Register(GlyphDefinitionTable.TableName, GlyphDefinitionTable.Load); + this.Register(PostTable.TableName, PostTable.Load); + this.Register(Cff1Table.TableName, Cff1Table.Load); + this.Register(Cff2Table.TableName, Cff2Table.Load); + this.Register(AVarTable.TableName, AVarTable.Load); + this.Register(GVarTable.TableName, GVarTable.Load); + this.Register(FVarTable.TableName, FVarTable.Load); + this.Register(HVarTable.TableName, HVarTable.Load); + this.Register(VVarTable.TableName, VVarTable.Load); + this.Register(MVarTable.TableName, MVarTable.Load); + this.Register(CVarTable.TableName, _ => null); + this.Register(SvgTable.TableName, SvgTable.Load); + } + + /// + /// Gets the default shared instance with all standard tables registered. + /// + public static TableLoader Default { get; } = new(); + + /// + /// Gets the four-byte tag string associated with the given table type. + /// + /// The CLR type of the table. + /// The tag string, or if the type is not registered. + public string? GetTag(Type type) + { + this.types.TryGetValue(type, out string? value); + + return value; + } + + /// + /// Gets the four-byte tag string associated with the given table type. + /// + /// The CLR type of the table. + /// The tag string. + public string GetTag() + { + this.types.TryGetValue(typeof(TType), out string? value); + return value!; + } + + /// + /// Gets all registered table CLR types. + /// + internal IEnumerable RegisteredTypes() => this.types.Keys; + + /// + /// Gets all registered four-byte table tags. + /// + internal IEnumerable RegisteredTags() => this.types.Values; + + private void Register(string tag, Func createFunc) + where T : Table + { + lock (this.loaders) + { + if (!this.loaders.ContainsKey(tag)) + { + this.loaders.Add(tag, createFunc); + this.types.Add(typeof(T), tag); + this.typesLoaders.Add(typeof(T), createFunc); + } + } + } + + /// + /// Loads a table by its four-byte tag string. + /// Returns an if no parser is registered for the tag. + /// + /// The four-byte table tag. + /// The font reader. + /// The loaded table, or an for unrecognized tags. + internal Table? Load(string tag, FontReader reader) + + // loader missing? register an unknown type loader and carry on + => this.loaders.TryGetValue(tag, out Func? func) + ? func.Invoke(reader) + : new UnknownTable(tag); + + /// + /// Loads a table by its CLR type. + /// + /// The table type to load. + /// The font reader. + /// The loaded table instance, or . + /// Thrown when the table type has not been registered. + internal TTable? Load(FontReader reader) + where TTable : Table + { + // loader missing register an unknown type loader and carry on + if (this.typesLoaders.TryGetValue(typeof(TTable), out Func? func)) + { + return (TTable?)func.Invoke(reader); + } + + throw new MissingFontTableException("Font table not registered.", nameof(TTable)); + } + } +} diff --git a/SixLabors.Fonts/Tables/TripleEncodingRecord.cs b/SixLabors.Fonts/Tables/TripleEncodingRecord.cs new file mode 100644 index 0000000..c6cbad3 --- /dev/null +++ b/SixLabors.Fonts/Tables/TripleEncodingRecord.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables { + /// + /// Represents a single entry in the WOFF2 triplet encoding table, which defines how + /// glyph coordinate triplets (flag, x, y) are packed into a compact binary representation. + /// + /// + internal readonly struct TripleEncodingRecord + { + /// The total number of bytes for this triplet (flag + x + y). + public readonly byte ByteCount; + + /// The number of bits used to encode the X coordinate value. + public readonly byte XBits; + + /// The number of bits used to encode the Y coordinate value. + public readonly byte YBits; + + /// The delta offset added to the raw X coordinate value before applying the sign. + public readonly ushort DeltaX; + + /// The delta offset added to the raw Y coordinate value before applying the sign. + public readonly ushort DeltaY; + + /// The sign multiplier for the X coordinate (-1, 0, or 1). + public readonly sbyte Xsign; + + /// The sign multiplier for the Y coordinate (-1, 0, or 1). + public readonly sbyte Ysign; + + /// + /// Initializes a new instance of the struct. + /// + /// The total byte count for this triplet. + /// The number of bits for the X coordinate. + /// The number of bits for the Y coordinate. + /// The delta offset for X. + /// The delta offset for Y. + /// The sign multiplier for X. + /// The sign multiplier for Y. + public TripleEncodingRecord( + byte byteCount, + byte xbits, + byte ybits, + ushort deltaX, + ushort deltaY, + sbyte xsign, + sbyte ysign) + { + this.ByteCount = byteCount; + this.XBits = xbits; + this.YBits = ybits; + this.DeltaX = deltaX; + this.DeltaY = deltaY; + this.Xsign = xsign; + this.Ysign = ysign; + } + + /// + /// Transforms a raw X coordinate value using the delta and sign from this record. + /// + /// The raw X coordinate value read from the stream. + /// The signed, delta-adjusted X coordinate. + public int Tx(int orgX) => (orgX + this.DeltaX) * this.Xsign; + + /// + /// Transforms a raw Y coordinate value using the delta and sign from this record. + /// + /// The raw Y coordinate value read from the stream. + /// The signed, delta-adjusted Y coordinate. + public int Ty(int orgY) => (orgY + this.DeltaY) * this.Ysign; + } +} diff --git a/SixLabors.Fonts/Tables/TripleEncodingTable.cs b/SixLabors.Fonts/Tables/TripleEncodingTable.cs new file mode 100644 index 0000000..b200390 --- /dev/null +++ b/SixLabors.Fonts/Tables/TripleEncodingTable.cs @@ -0,0 +1,269 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; + +namespace SixLabors.Fonts.Tables { + /// + /// Provides the WOFF2 triplet encoding lookup table. Each of the 128 index values + /// maps to a that defines how a glyph coordinate + /// triplet (flag, x, y) is packed into bytes. + /// + /// + /// + /// Source code is based on https://github.com/LayoutFarm/Typography. + /// + internal class TripleEncodingTable + { + /// + /// Gets the singleton instance of the triplet encoding table. + /// + public static readonly TripleEncodingTable EncTable = new TripleEncodingTable(); + private readonly List records = new List(); + + private TripleEncodingTable() => this.BuildTable(); + + /// + /// Gets the at the specified index (0–127). + /// + /// The triplet encoding index derived from the glyph flag byte. + /// The encoding record for the given index. + public TripleEncodingRecord this[int i] => this.records[i]; + + private void BuildTable() + { + // Each of the 128 index values define the following properties and specified in details in the table below: + + // Byte count(total number of bytes used for this set of coordinate values including one byte for 'flag' value). + // Number of bits used to represent X coordinate value(X bits). + // Number of bits used to represent Y coordinate value(Y bits). + // An additional incremental amount to be added to X bits value(delta X). + // An additional incremental amount to be added to Y bits value(delta Y). + // The sign of X coordinate value(X sign). + // The sign of Y coordinate value(Y sign). + + // Please note that "Byte Count" field reflects total size of the triplet(flag, xCoordinate, yCoordinate), + // including ‘flag’ value that is encoded in a separate stream. + + // Triplet Encoding + // Index ByteCount Xbits Ybits DeltaX DeltaY Xsign Ysign + // (set 1.1) + // 0 2 0 8 N/A 0 N/A - + // 1 0 + + // 2 256 - + // 3 256 + + // 4 512 - + // 5 512 + + // 6 768 - + // 7 768 + + // 8 1024 - + // 9 1024 + + this.BuildRecords(2, 0, 8, Array.Empty(), new ushort[] { 0, 256, 512, 768, 1024 }); + + // Index ByteCount Xbits Ybits DeltaX DeltaY Xsign Ysign + // (set 1.2) + // 10 2 8 0 0 N/A - N/A + // 11 0 + + // 12 256 - + // 13 256 + + // 14 512 - + // 15 512 + + // 16 768 - + // 17 768 + + // 18 1024 - + // 19 1024 + + this.BuildRecords(2, 8, 0, new ushort[] { 0, 256, 512, 768, 1024 }, Array.Empty()); + + // Index ByteCount Xbits Ybits DeltaX DeltaY Xsign Ysign + // (set 2.1) + // 20 2 4 4 1 1 - - + // 21 1 + - + // 22 1 - + + // 23 1 + + + // 24 17 - - + // 25 17 + - + // 26 17 - + + // 27 17 + + + // 28 33 - - + // 29 33 + - + // 30 33 - + + // 31 33 + + + // 32 49 - - + // 33 49 + - + // 34 49 - + + // 35 49 + + + this.BuildRecords(2, 4, 4, new ushort[] { 1 }, new ushort[] { 1, 17, 33, 49 }); + + // Index ByteCount Xbits Ybits DeltaX DeltaY Xsign Ysign + // (set 2.2) + // 36 2 4 4 17 1 - - + // 37 1 + - + // 38 1 - + + // 39 1 + + + // 40 17 - - + // 41 17 + - + // 42 17 - + + // 43 17 + + + // 44 33 - - + // 45 33 + - + // 46 33 - + + // 47 33 + + + // 48 49 - - + // 49 49 + - + // 50 49 - + + // 51 49 + + + this.BuildRecords(2, 4, 4, new ushort[] { 17 }, new ushort[] { 1, 17, 33, 49 }); + + // Index ByteCount Xbits Ybits DeltaX DeltaY Xsign Ysign + // (set 2.3) + // 52 2 4 4 33 1 - - + // 53 1 + - + // 54 1 - + + // 55 1 + + + // 56 17 - - + // 57 17 + - + // 58 17 - + + // 59 17 + + + // 60 33 - - + // 61 33 + - + // 62 33 - + + // 63 33 + + + // 64 49 - - + // 65 49 + - + // 66 49 - + + // 67 49 + + + this.BuildRecords(2, 4, 4, new ushort[] { 33 }, new ushort[] { 1, 17, 33, 49 }); + + // Index ByteCount Xbits Ybits DeltaX DeltaY Xsign Ysign + // (set 2.4) + // 68 2 4 4 49 1 - - + // 69 1 + - + // 70 1 - + + // 71 1 + + + // 72 17 - - + // 73 17 + - + // 74 17 - + + // 75 17 + + + // 76 33 - - + // 77 33 + - + // 78 33 - + + // 79 33 + + + // 80 49 - - + // 81 49 + - + // 82 49 - + + // 83 49 + + + this.BuildRecords(2, 4, 4, new ushort[] { 49 }, new ushort[] { 1, 17, 33, 49 }); + + // Index ByteCount Xbits Ybits DeltaX DeltaY Xsign Ysign + // (set 3.1) + // 84 3 8 8 1 1 - - + // 85 1 + - + // 86 1 - + + // 87 1 + + + // 88 257 - - + // 89 257 + - + // 90 257 - + + // 91 257 + + + // 92 513 - - + // 93 513 + - + // 94 513 - + + // 95 513 + + + this.BuildRecords(3, 8, 8, new ushort[] { 1 }, new ushort[] { 1, 257, 513 }); + + // Index ByteCount Xbits Ybits DeltaX DeltaY Xsign Ysign + // (set 3.2) + // 96 3 8 8 257 1 - - + // 97 1 + - + // 98 1 - + + // 99 1 + + + // 100 257 - - + // 101 257 + - + // 102 257 - + + // 103 257 + + + // 104 513 - - + // 105 513 + - + // 106 513 - + + // 107 513 + + + this.BuildRecords(3, 8, 8, new ushort[] { 257 }, new ushort[] { 1, 257, 513 }); + + // Index ByteCount Xbits Ybits DeltaX DeltaY Xsign Ysign + // (set 3.3) + // 108 3 8 8 513 1 - - + // 109 1 + - + // 110 1 - + + // 111 1 + + + // 112 257 - - + // 113 257 + - + // 114 257 - + + // 115 257 + + + // 116 513 - - + // 117 513 + - + // 118 513 - + + // 119 513 + + + this.BuildRecords(3, 8, 8, new ushort[] { 513 }, new ushort[] { 1, 257, 513 }); + + // Index ByteCount Xbits Ybits DeltaX DeltaY Xsign Ysign + // (set 4) + // 120 4 12 12 0 0 - - + // 121 + - + // 122 - + + // 123 + + + this.BuildRecords(4, 12, 12, new ushort[] { 0 }, new ushort[] { 0 }); + + // Index ByteCount Xbits Ybits DeltaX DeltaY Xsign Ysign + // (set 5) + // 124 5 16 16 0 0 - - + // 125 + - + // 126 - + + // 127 + + + this.BuildRecords(5, 16, 16, new ushort[] { 0 }, new ushort[] { 0 }); + } + + private void BuildRecords(byte byteCount, byte xbits, byte ybits, ushort[] deltaXs, ushort[] deltaYs) + { + if (deltaXs.Equals(Array.Empty())) + { + // (set 1.1) + for (int y = 0; y < deltaYs.Length; ++y) + { + this.AddRecord(byteCount, xbits, ybits, 0, deltaYs[y], 0, -1); + this.AddRecord(byteCount, xbits, ybits, 0, deltaYs[y], 0, 1); + } + } + else if (deltaYs.Equals(Array.Empty())) + { + // (set 1.2) + for (int x = 0; x < deltaXs.Length; ++x) + { + this.AddRecord(byteCount, xbits, ybits, deltaXs[x], 0, -1, 0); + this.AddRecord(byteCount, xbits, ybits, deltaXs[x], 0, 1, 0); + } + } + else + { + // set 2.1, - set5 + for (int x = 0; x < deltaXs.Length; ++x) + { + ushort deltaX = deltaXs[x]; + + for (int y = 0; y < deltaYs.Length; ++y) + { + ushort deltaY = deltaYs[y]; + + this.AddRecord(byteCount, xbits, ybits, deltaX, deltaY, -1, -1); + this.AddRecord(byteCount, xbits, ybits, deltaX, deltaY, 1, -1); + this.AddRecord(byteCount, xbits, ybits, deltaX, deltaY, -1, 1); + this.AddRecord(byteCount, xbits, ybits, deltaX, deltaY, 1, 1); + } + } + } + } + + private void AddRecord(byte byteCount, byte xbits, byte ybits, ushort deltaX, ushort deltaY, sbyte xsign, sbyte ysign) + { + var rec = new TripleEncodingRecord(byteCount, xbits, ybits, deltaX, deltaY, xsign, ysign); + this.records.Add(rec); + } + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/Glyphs/CompositeComponent.cs b/SixLabors.Fonts/Tables/TrueType/Glyphs/CompositeComponent.cs new file mode 100644 index 0000000..e2e083f --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/Glyphs/CompositeComponent.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.TrueType.Glyphs { + /// + /// Stores the original component offset and point count for a single component + /// within a composite glyph. Used during gvar variation processing to apply + /// per-component offset deltas to the assembled outline. + /// + internal readonly struct CompositeComponent + { + /// + /// Initializes a new instance of the struct. + /// + /// The original X offset of this component. + /// The original Y offset of this component. + /// The number of control points contributed by this component. + public CompositeComponent(float dx, float dy, int pointCount) + { + this.Dx = dx; + this.Dy = dy; + this.PointCount = pointCount; + } + + /// + /// Gets the original X offset of this component (before variation). + /// + public float Dx { get; } + + /// + /// Gets the original Y offset of this component (before variation). + /// + public float Dy { get; } + + /// + /// Gets the number of control points contributed by this component + /// to the assembled composite glyph. + /// + public int PointCount { get; } + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/Glyphs/CompositeGlyphFlags.cs b/SixLabors.Fonts/Tables/TrueType/Glyphs/CompositeGlyphFlags.cs new file mode 100644 index 0000000..761b6f2 --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/Glyphs/CompositeGlyphFlags.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Tables.TrueType.Glyphs { + /* + ## Composite Glyph Flags + + | Mask | Name | Description + |--------|--------------------------|-------------------- + | 0x0001 | ARG_1_AND_2_ARE_WORDS | Bit 0: If this is set, the arguments are 16-bit (uint16 or int16); otherwise, they are bytes (uint8 or int8). + | 0x0002 | ARGS_ARE_XY_VALUES | Bit 1: If this is set, the arguments are signed xy values; otherwise, they are unsigned point numbers. + | 0x0004 | ROUND_XY_TO_GRID | Bit 2: For the xy values if the preceding is true. + | 0x0008 | WE_HAVE_A_SCALE | Bit 3: This indicates that there is a simple scale for the component. Otherwise, scale = 1.0. + | 0x0020 | MORE_COMPONENTS | Bit 5: Indicates at least one more glyph after this one. + | 0x0040 | WE_HAVE_AN_X_AND_Y_SCALE | Bit 6: The x direction will use a different scale from the y direction. + | 0x0080 | WE_HAVE_A_TWO_BY_TWO | Bit 7: There is a 2 by 2 transformation that will be used to scale the component. + | 0x0100 | WE_HAVE_INSTRUCTIONS | Bit 8: Following the last component are instructions for the composite character. + | 0x0200 | USE_MY_METRICS | Bit 9: If set, this forces the aw and lsb (and rsb) for the composite to be equal to those from this original glyph. This works for hinted and unhinted characters. + | 0x0400 | OVERLAP_COMPOUND | Bit 10: If set, the components of the compound glyph overlap. Use of this flag is not required in OpenType — that is, it is valid to have components overlap without having this flag set. It may affect behaviors in some platforms, however. (See Apple’s specification for details regarding behavior in Apple platforms.) When used, it must be set on the flag word for the first component. See additional remarks, above, for the similar OVERLAP_SIMPLE flag used in simple-glyph descriptions. + | 0x0800 | SCALED_COMPONENT_OFFSET | Bit 11: The composite is designed to have the component offset scaled. + | 0x1000 | UNSCALED_COMPONENT_OFFSET| Bit 12: The composite is designed not to have the component offset scaled. + | 0xE010 | Reserved | Bits 4, 13, 14 and 15 are reserved: set to 0. + */ + + /// + /// Flags used in composite glyph descriptions within the ‘glyf’ table. + /// + /// + [Flags] + internal enum CompositeGlyphFlags : ushort + { + /// + /// If set, the arguments are 16-bit (uint16 or int16); otherwise, they are bytes (uint8 or int8). + /// + Args1And2AreWords = 1, + + /// + /// If set, the arguments are signed xy values; otherwise, they are unsigned point numbers. + /// + ArgsAreXYValues = 2, + + /// + /// If set, round the xy values to the nearest grid line. + /// + RoundXYToGrid = 4, + + /// + /// Indicates that there is a simple scale for the component. Otherwise, scale = 1.0. + /// + WeHaveAScale = 8, + + /// + /// This bit is reserved. Set it to 0. + /// + Reserved = 16, + + /// + /// Indicates at least one more glyph after this one. + /// + MoreComponents = 32, + + /// + /// The x direction will use a different scale from the y direction. + /// + WeHaveXAndYScale = 64, + + /// + /// There is a 2 by 2 transformation that will be used to scale the component. + /// + WeHaveATwoByTwo = 128, + + /// + /// Following the last component are instructions for the composite character. + /// + WeHaveInstructions = 256, + + /// + /// If set, forces the advance width and side bearings for the composite to be equal + /// to those from this component glyph. Works for hinted and unhinted characters. + /// + UseMyMetrics = 512, + + /// + /// If set, the components of the compound glyph overlap. Use of this flag is not + /// required in OpenType — it is valid to have components overlap without this flag set. + /// + OverlapCompound = 1024, + + /// + /// The composite is designed to have the component offset scaled. + /// + ScaledComponentOffset = 2048, + + /// + /// The composite is designed not to have the component offset scaled. + /// + UnscaledComponentOffset = 4096 + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/Glyphs/CompositeGlyphLoader.cs b/SixLabors.Fonts/Tables/TrueType/Glyphs/CompositeGlyphLoader.cs new file mode 100644 index 0000000..dbce9c5 --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/Glyphs/CompositeGlyphLoader.cs @@ -0,0 +1,201 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace SixLabors.Fonts.Tables.TrueType.Glyphs { + /// + /// Implements loading of composite (compound) glyph descriptions from the 'glyf' table. + /// A composite glyph references one or more component glyphs, each with its own transformation. + /// + /// + internal sealed class CompositeGlyphLoader : GlyphLoader + { + private readonly Bounds bounds; + private readonly Composite[] composites; + private readonly ReadOnlyMemory instructions; + + /// + /// Initializes a new instance of the class. + /// + /// The component glyph references. + /// The composite glyph bounding box. + /// The hinting instructions for this composite glyph. + public CompositeGlyphLoader(IEnumerable composites, Bounds bounds, ReadOnlyMemory instructions) + { + this.composites = [.. composites]; + this.bounds = bounds; + this.instructions = instructions; + } + + /// + public override GlyphVector CreateGlyph(GlyphTable table) + { + List controlPoints = []; + List endPoints = []; + CompositeComponent[] components = new CompositeComponent[this.composites.Length]; + for (int i = 0; i < this.composites.Length; i++) + { + Composite composite = this.composites[i]; + GlyphVector clone = GlyphVector.DeepClone(table.GetGlyph(composite.GlyphIndex)); + GlyphVector.TransformInPlace(ref clone, composite.Transformation); + ushort endPointOffset = (ushort)controlPoints.Count; + + // Store original component offset and point count for gvar processing. + components[i] = new CompositeComponent( + composite.Transformation.Translation.X, + composite.Transformation.Translation.Y, + clone.ControlPoints.Count); + + controlPoints.AddRange(clone.ControlPoints); + foreach (ushort p in clone.EndPoints) + { + endPoints.Add((ushort)(p + endPointOffset)); + } + } + + return new(controlPoints, endPoints, this.bounds, this.instructions, true) + { + CompositeComponents = components + }; + } + + /// + /// Reads a composite glyph description from the binary reader. + /// + /// The big-endian binary reader positioned after the glyph header. + /// The glyph bounding box. + /// A containing the composite glyph data. + public static CompositeGlyphLoader LoadCompositeGlyph(BigEndianBinaryReader reader, in Bounds bounds) + { + List composites = []; + CompositeGlyphFlags flags; + do + { + flags = (CompositeGlyphFlags)reader.ReadUInt16(); + ushort glyphIndex = reader.ReadUInt16(); + + LoadArguments(reader, flags, out int dx, out int dy); + + Matrix3x2 transform = Matrix3x2.Identity; + transform.Translation = new Vector2(dx, dy); + + if ((flags & CompositeGlyphFlags.WeHaveAScale) != 0) + { + float scale = reader.ReadF2Dot14(); // Format 2.14 + transform.M11 = scale; + transform.M22 = scale; + } + else if ((flags & CompositeGlyphFlags.WeHaveXAndYScale) != 0) + { + transform.M11 = reader.ReadF2Dot14(); + transform.M22 = reader.ReadF2Dot14(); + } + else if ((flags & CompositeGlyphFlags.WeHaveATwoByTwo) != 0) + { + transform.M11 = reader.ReadF2Dot14(); + transform.M12 = reader.ReadF2Dot14(); + transform.M21 = reader.ReadF2Dot14(); + transform.M22 = reader.ReadF2Dot14(); + } + + composites.Add(new Composite(glyphIndex, flags, transform)); + } + while ((flags & CompositeGlyphFlags.MoreComponents) != 0); + + byte[] instructions = []; + if ((flags & CompositeGlyphFlags.WeHaveInstructions) != 0) + { + // Read the instructions if they exist. + ushort instructionSize = reader.ReadUInt16(); + instructions = reader.ReadUInt8Array(instructionSize); + } + + return new CompositeGlyphLoader(composites, bounds, instructions); + } + + /// + /// Reads the component arguments (offsets or point numbers) from the binary reader + /// based on the specified composite glyph flags. + /// + /// The big-endian binary reader. + /// The composite glyph flags for this component. + /// When this method returns, contains the x offset or point number. + /// When this method returns, contains the y offset or point number. + public static void LoadArguments(BigEndianBinaryReader reader, CompositeGlyphFlags flags, out int dx, out int dy) + { + // are we 16 or 8 bits values? + if ((flags & CompositeGlyphFlags.Args1And2AreWords) != 0) + { + // 16 bit + // are we int or unit? + if ((flags & CompositeGlyphFlags.ArgsAreXYValues) != 0) + { + // signed + dx = reader.ReadInt16(); + dy = reader.ReadInt16(); + } + else + { + // unsigned + dx = reader.ReadUInt16(); + dy = reader.ReadUInt16(); + } + } + else + { + // 8 bit + // are we sbyte or byte? + if ((flags & CompositeGlyphFlags.ArgsAreXYValues) != 0) + { + // signed + dx = reader.ReadSByte(); + dy = reader.ReadSByte(); + } + else + { + // unsigned + dx = reader.ReadByte(); + dy = reader.ReadByte(); + } + } + } + + /// + /// Represents a single component reference within a composite glyph, + /// storing the referenced glyph index, flags, and transformation matrix. + /// + public readonly struct Composite + { + /// + /// Initializes a new instance of the struct. + /// + /// The glyph index of the component. + /// The composite glyph flags. + /// The transformation matrix to apply to the component. + public Composite(ushort glyphIndex, CompositeGlyphFlags flags, Matrix3x2 transformation) + { + this.GlyphIndex = glyphIndex; + this.Flags = flags; + this.Transformation = transformation; + } + + /// + /// Gets the glyph index of the component. + /// + public ushort GlyphIndex { get; } + + /// + /// Gets the composite glyph flags for this component. + /// + public CompositeGlyphFlags Flags { get; } + + /// + /// Gets the transformation matrix to apply to the component's outline. + /// + public Matrix3x2 Transformation { get; } + } + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/Glyphs/ControlPoint.cs b/SixLabors.Fonts/Tables/TrueType/Glyphs/ControlPoint.cs new file mode 100644 index 0000000..e8b46b2 --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/Glyphs/ControlPoint.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.Fonts.Tables.TrueType.Glyphs { + /// + /// Represents a true type glyph control point. + /// + internal struct ControlPoint : IEquatable + { + /// + /// Gets or sets the position of the point. + /// + public Vector2 Point; + + /// + /// Gets or sets a value indicating whether the point is on a curve. + /// + public bool OnCurve; + + /// + /// Initializes a new instance of the struct. + /// + /// The position. + /// Whether the point is on a curve. + public ControlPoint(Vector2 point, bool onCurve) + { + this.Point = point; + this.OnCurve = onCurve; + } + + /// + /// Compares two instances for equality. + /// + /// The left operand. + /// The right operand. + /// if the two instances are equal; otherwise, . + public static bool operator ==(ControlPoint left, ControlPoint right) + => left.Equals(right); + + /// + /// Compares two instances for inequality. + /// + /// The left operand. + /// The right operand. + /// if the two instances are not equal; otherwise, . + public static bool operator !=(ControlPoint left, ControlPoint right) + => !(left == right); + + /// + public override bool Equals(object? obj) + => obj is ControlPoint point && this.Equals(point); + + /// + public readonly bool Equals(ControlPoint other) + => this.Point.Equals(other.Point) + && this.OnCurve == other.OnCurve; + + /// + public override readonly int GetHashCode() + => HashCode.Combine(this.Point, this.OnCurve); + + /// + public override readonly string ToString() + => FormattableString.Invariant($"Point: {this.Point}, OnCurve: {this.OnCurve}"); + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/Glyphs/EmptyGlyphLoader.cs b/SixLabors.Fonts/Tables/TrueType/Glyphs/EmptyGlyphLoader.cs new file mode 100644 index 0000000..2e18cf5 --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/Glyphs/EmptyGlyphLoader.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.TrueType.Glyphs { + /// + /// A that produces an empty glyph outline. + /// Used for glyphs that have no outline data (e.g. space characters). + /// + internal class EmptyGlyphLoader : GlyphLoader + { + private bool loop; + private readonly Bounds fallbackEmptyBounds; + private GlyphVector? glyph; + + /// + /// Initializes a new instance of the class. + /// + /// The fallback bounds to use if glyph 0 cannot be resolved. + public EmptyGlyphLoader(Bounds fallbackEmptyBounds) + => this.fallbackEmptyBounds = fallbackEmptyBounds; + + /// + public override GlyphVector CreateGlyph(GlyphTable table) + { + if (this.loop) + { + this.glyph ??= GlyphVector.Empty(this.fallbackEmptyBounds); + return this.glyph.Value; + } + + this.loop = true; + this.glyph ??= GlyphVector.Empty(table.GetGlyph(0).Bounds); + return this.glyph.Value; + } + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphLoader.cs b/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphLoader.cs new file mode 100644 index 0000000..b0f37a5 --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphLoader.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.TrueType.Glyphs { + /// + /// Base class for loading glyph outlines from the 'glyf' table. + /// Subclasses handle simple, composite, and empty glyph descriptions. + /// + internal abstract class GlyphLoader + { + /// + /// Creates a representing this glyph's outline. + /// + /// The glyph table used to resolve component glyphs in composite descriptions. + /// The . + public abstract GlyphVector CreateGlyph(GlyphTable table); + + /// + /// Reads a glyph description from the binary reader and returns the appropriate loader. + /// + /// The big-endian binary reader positioned at the start of the glyph description. + /// A for the glyph (simple or composite). + public static GlyphLoader Load(BigEndianBinaryReader reader) + { + short contoursCount = reader.ReadInt16(); + var bounds = Bounds.Load(reader); + + if (contoursCount >= 0) + { + return SimpleGlyphLoader.LoadSimpleGlyph(reader, contoursCount, bounds); + } + + return CompositeGlyphLoader.LoadCompositeGlyph(reader, bounds); + } + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphTable.cs b/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphTable.cs new file mode 100644 index 0000000..00312f3 --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphTable.cs @@ -0,0 +1,115 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Concurrent; +using System.IO; +using SixLabors.Fonts.Tables.Woff; + +namespace SixLabors.Fonts.Tables.TrueType.Glyphs { + /// + /// Represents the 'glyf' table containing TrueType glyph outline data. + /// Each glyph is lazily loaded and cached on first access. + /// + /// + internal class GlyphTable : Table + { + /// + /// The table tag name. + /// + internal const string TableName = "glyf"; + private readonly GlyphLoader[] loaders; + private readonly ConcurrentDictionary glyphCache; + + /// + /// Initializes a new instance of the class. + /// + /// The array of glyph loaders, one per glyph. + public GlyphTable(GlyphLoader[] glyphLoaders) + { + this.loaders = glyphLoaders; + this.glyphCache = new(Environment.ProcessorCount, glyphLoaders.Length); + } + + /// + /// Gets the number of glyphs in this table. + /// + public int GlyphCount => this.loaders.Length; + + /// + /// Gets the for the glyph at the specified index. + /// + /// The zero-based glyph index. + /// The , or an empty vector if the index is out of range. + // TODO: Make this non-virtual + internal virtual GlyphVector GetGlyph(int index) + { + if (index < 0 || index >= this.loaders.Length) + { + return GlyphVector.Empty(); + } + + return this.glyphCache.GetOrAdd(index, i => this.loaders[i].CreateGlyph(this)); + } + + /// + /// Loads the 'glyf' table from the specified font reader. + /// + /// The font reader. + /// The . + public static GlyphTable Load(FontReader reader) + { + uint[] locations = reader.GetTable().GlyphOffsets; + + // Use an empty bounds instance as the fallback. + // We will substitute this with the advance width/height to determine bounds instead when rendering/measuring. + Bounds fallbackEmptyBounds = Bounds.Empty; + + using BigEndianBinaryReader binaryReader = reader.GetReaderAtTablePosition(TableName); + return Load(binaryReader, reader.TableFormat, locations, in fallbackEmptyBounds); + } + + /// + /// Loads the 'glyf' table from the specified binary reader. + /// + /// The big-endian binary reader positioned at the start of the table. + /// The table format (e.g. WOFF2 vs standard). + /// The glyph offset array from the 'loca' table. + /// The fallback bounds for empty glyphs. + /// The . + public static GlyphTable Load( + BigEndianBinaryReader reader, + TableFormat format, + uint[] locations, + in Bounds fallbackEmptyBounds) + { + EmptyGlyphLoader empty = new(fallbackEmptyBounds); + int entryCount = locations.Length; + int glyphCount = entryCount - 1; // last entry is a placeholder to the end of the table + GlyphLoader[] glyphs = new GlyphLoader[glyphCount]; + + // Special case for WOFF2 format where all glyphs need to be read in one go. + if (format is TableFormat.Woff2) + { + return new GlyphTable(Woff2Utils.LoadAllGlyphs(reader, empty)); + } + + for (int i = 0; i < glyphCount; i++) + { + if (locations[i] == locations[i + 1]) + { + // This is an empty glyph; + glyphs[i] = empty; + } + else + { + // Move to start of glyph. + reader.Seek(locations[i], SeekOrigin.Begin); + glyphs[i] = GlyphLoader.Load(reader); + } + } + + return new GlyphTable(glyphs); + } + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphVector.cs b/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphVector.cs new file mode 100644 index 0000000..8757097 --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphVector.cs @@ -0,0 +1,166 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.Fonts.Tables.TrueType.Hinting; + +namespace SixLabors.Fonts.Tables.TrueType.Glyphs { + /// + /// Represents the raw glyph outlines for a given glyph comprised of a collection of glyph table entries. + /// The type is mutable by design to reduce copying during transformation. + /// + internal struct GlyphVector + { + /// + /// Initializes a new instance of the struct. + /// + /// The control points defining the glyph outline. + /// The indices of the last point of each contour. + /// The glyph bounding box. + /// The TrueType hinting instructions. + /// Whether this glyph is a composite glyph. + internal GlyphVector( + IList controlPoints, + IReadOnlyList endPoints, + Bounds bounds, + ReadOnlyMemory instructions, + bool isComposite) + { + this.ControlPoints = controlPoints; + this.EndPoints = endPoints; + this.Bounds = bounds; + this.Instructions = instructions; + this.IsComposite = isComposite; + } + + /// + /// Gets or sets the control points defining the glyph outline. + /// + public IList ControlPoints { get; set; } + + /// + /// Gets or sets the indices of the last point of each contour. + /// + public IReadOnlyList EndPoints { get; set; } + + /// + /// Gets or sets the TrueType hinting instructions for this glyph. + /// + public ReadOnlyMemory Instructions { get; set; } + + /// + /// Gets or sets a value indicating whether this is a composite glyph. + /// + public bool IsComposite { get; set; } + + /// + /// Gets or sets the glyph bounding box. + /// + public Bounds Bounds { get; set; } + + /// + /// Gets or sets the composite component information used for gvar variation processing. + /// Each entry stores the original component offset and the number of control points + /// contributed by that component, so that TransformPoints can apply per-component + /// offset deltas to the assembled outline. + /// Null for simple (non-composite) glyphs. + /// + public CompositeComponent[]? CompositeComponents { get; set; } + + /// + /// Creates an empty glyph vector with no control points or contours. + /// + /// The optional bounds to assign to the empty glyph. + /// An empty . + public static GlyphVector Empty(Bounds bounds = default) + => new(Array.Empty(), Array.Empty(), bounds, Array.Empty(), false); + + /// + /// Transforms a glyph vector by a specified 3x2 matrix. + /// + /// The glyph vector to transform. + /// The transformation matrix. + public static void TransformInPlace(ref GlyphVector src, Matrix3x2 matrix) + { + IList controlPoints = src.ControlPoints; + for (int i = 0; i < controlPoints.Count; i++) + { + ControlPoint point = controlPoints[i]; + point.Point = Vector2.Transform(point.Point, matrix); + controlPoints[i] = point; + } + + src.Bounds = Bounds.Transform(src.Bounds, matrix); + } + + /// + /// Applies True Type hinting to the specified glyph vector. + /// + /// The hinting mode. + /// The glyph vector to hint. + /// The True Type interpreter. + /// The first phantom point. + /// The second phantom point. + /// The third phantom point. + /// The fourth phantom point. + public static void Hint( + HintingMode hintingMode, + ref GlyphVector glyph, + TrueTypeInterpreter interpreter, + Vector2 pp1, + Vector2 pp2, + Vector2 pp3, + Vector2 pp4) + { + if (hintingMode == HintingMode.None) + { + return; + } + + ControlPoint[] controlPoints = new ControlPoint[glyph.ControlPoints.Count + 4]; + controlPoints[^4].Point = pp1; + controlPoints[^3].Point = pp2; + controlPoints[^2].Point = pp3; + controlPoints[^1].Point = pp4; + + for (int i = 0; i < glyph.ControlPoints.Count; i++) + { + controlPoints[i] = glyph.ControlPoints[i]; + } + + if (interpreter.TryHintGlyph(controlPoints, glyph.EndPoints, glyph.Instructions, glyph.IsComposite)) + { + for (int i = 0; i < glyph.ControlPoints.Count; i++) + { + glyph.ControlPoints[i] = controlPoints[i]; + } + } + } + + /// + /// Creates a new glyph vector that is a deep copy of the specified instance. + /// + /// The source glyph vector to copy. + /// The cloned . + public static GlyphVector DeepClone(GlyphVector src) + { + List controlPoints = [.. src.ControlPoints]; + List endPoints = [.. src.EndPoints]; + + return new(controlPoints, endPoints, src.Bounds, src.Instructions, src.IsComposite) + { + CompositeComponents = src.CompositeComponents is not null + ? [.. src.CompositeComponents] + : null + }; + } + + /// + /// Returns a value indicating whether the current instance is empty. + /// + /// The indicating the result. + public readonly bool HasValue() => this.ControlPoints?.Count > 0; + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/Glyphs/SimpleGlyphLoader.cs b/SixLabors.Fonts/Tables/TrueType/Glyphs/SimpleGlyphLoader.cs new file mode 100644 index 0000000..882b26b --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/Glyphs/SimpleGlyphLoader.cs @@ -0,0 +1,226 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.Fonts.Tables.TrueType.Glyphs { + /// + /// Implements loading Simple Glyph Description which is part of the `glyph`table. + /// + /// + internal class SimpleGlyphLoader : GlyphLoader + { + private readonly ControlPoint[] controlPoints; + private readonly ushort[] endPoints; + private readonly Bounds bounds; + private readonly byte[] instructions; + + /// + /// Initializes a new instance of the class. + /// + /// The glyph's control points. + /// The indices of the last point of each contour. + /// The glyph bounding box. + /// The hinting instructions for this glyph. + public SimpleGlyphLoader(ControlPoint[] controlPoints, ushort[] endPoints, Bounds bounds, byte[] instructions) + { + this.controlPoints = controlPoints; + this.endPoints = endPoints; + this.bounds = bounds; + this.instructions = instructions; + } + + /// + /// Initializes a new instance of the class + /// for a glyph with zero contours (bounds only). + /// + /// The glyph bounding box. + public SimpleGlyphLoader(Bounds bounds) + { + this.controlPoints = Array.Empty(); + this.endPoints = Array.Empty(); + this.instructions = Array.Empty(); + this.bounds = bounds; + } + + [Flags] + private enum Flags : byte + { + /// + /// The point is is off the curve. + /// + ControlPoint = 0, + + /// + /// The point is on the curve. + /// + OnCurve = 1, + + /// + /// If set, the corresponding x-coordinate is 1 byte long. If not set, 2 bytes. + /// + XByte = 2, + + /// + /// If set, the corresponding y-coordinate is 1 byte long. If not set, 2 bytes. + /// + YByte = 4, + + /// + /// f set, the next byte specifies the number of additional times this set of flags is to be repeated. + /// In this way, the number of flags listed can be smaller than the number of points in a character. + /// + Repeat = 8, + + /// + /// This flag has two meanings, depending on how the x-Short Vector flag is set. + /// If x-Short Vector is set, this bit describes the sign of the value, with 1 equalling positive and 0 negative. + /// If the x-Short Vector bit is not set and this bit is set, then the current x-coordinate is the same as the previous x-coordinate. + /// If the x-Short Vector bit is not set and this bit is also not set, the current x-coordinate is a signed 16-bit delta vector. + /// + XSignOrSame = 16, + + /// + /// This flag has two meanings, depending on how the y-Short Vector flag is set. + /// If y-Short Vector is set, this bit describes the sign of the value, with 1 equalling positive and 0 negative. + /// If the y-Short Vector bit is not set and this bit is set, then the current y-coordinate is the same as the previous y-coordinate. + /// If the y-Short Vector bit is not set and this bit is also not set, the current y-coordinate is a signed 16-bit delta vector. + /// + YSignOrSame = 32 + } + + /// + public override GlyphVector CreateGlyph(GlyphTable table) + => new(this.controlPoints, this.endPoints, this.bounds, this.instructions, false); + + /// + /// Reads a simple glyph description from the binary reader. + /// + /// The big-endian binary reader positioned after the glyph header. + /// The number of contours in the glyph. + /// The glyph bounding box. + /// A containing the simple glyph data. + public static GlyphLoader LoadSimpleGlyph(BigEndianBinaryReader reader, short count, in Bounds bounds) + { + if (count == 0) + { + return new SimpleGlyphLoader(bounds); + } + + // +-----------------+----------------------------------------+--------------------------------------------------------------------+ + // | Type | Name | Description | + // +=================+========================================+====================================================================+ + // | uint16 | endPtsOfContours[n] | Array of last points of each contour; n is the number of contours. | + // +-----------------+----------------------------------------+--------------------------------------------------------------------+ + // | uint16 | instructionLength | Total number of bytes for instructions. | + // +-----------------+----------------------------------------+--------------------------------------------------------------------+ + // | uint8 | instructions[n] | Array of instructions for each glyph; | + // | | | n is the number of instructions. | + // +-----------------+----------------------------------------+--------------------------------------------------------------------+ + // | uint8 | flags[n] | Array of flags for each coordinate in outline; | + // | | | n is the number of flags. | + // +-----------------+----------------------------------------+--------------------------------------------------------------------+ + // | uint8 or int16 | xCoordinates[] | First coordinates relative to(0, 0); | + // | | | others are relative to previous point. | + // +-----------------+----------------------------------------+--------------------------------------------------------------------+ + // | uint8 or int16 | yCoordinates[] | First coordinates relative to (0, 0); | + // | | | others are relative to previous point. | + // +-----------------+----------------------------------------+--------------------------------------------------------------------+ + ushort[] endPoints = reader.ReadUInt16Array(count); + + ushort instructionSize = reader.ReadUInt16(); + byte[] instructions = reader.ReadUInt8Array(instructionSize); + + // TODO: should this take the max points rather? + int pointCount = 0; + if (count > 0) + { + pointCount = endPoints[count - 1] + 1; + } + + Flags[] flags = ReadFlags(reader, pointCount); + short[] xs = ReadCoordinates(reader, pointCount, flags, Flags.XByte, Flags.XSignOrSame); + short[] ys = ReadCoordinates(reader, pointCount, flags, Flags.YByte, Flags.YSignOrSame); + + var controlPoints = new ControlPoint[xs.Length]; + for (int i = 0; i < flags.Length; i++) + { + controlPoints[i] = new(new Vector2(xs[i], ys[i]), (flags[i] & Flags.OnCurve) == Flags.OnCurve); + } + + return new SimpleGlyphLoader(controlPoints, endPoints, bounds, instructions); + } + + /// + /// Reads the packed flag array for all points in a simple glyph. + /// + /// The big-endian binary reader. + /// The number of flags (points) to read. + /// An array of flags, one per control point. + private static Flags[] ReadFlags(BigEndianBinaryReader reader, int flagCount) + { + var result = new Flags[flagCount]; + int c = 0; + int repeatCount = 0; + Flags flag = default; + while (c < flagCount) + { + if (repeatCount > 0) + { + repeatCount--; + } + else + { + flag = (Flags)reader.ReadUInt8(); + if ((flag & Flags.Repeat) == Flags.Repeat) + { + repeatCount = reader.ReadByte(); + } + } + + result[c++] = flag; + } + + return result; + } + + /// + /// Reads a coordinate array (x or y) for all points in a simple glyph, applying delta decoding. + /// + /// The big-endian binary reader. + /// The number of points to read. + /// The per-point flags array. + /// The flag indicating the coordinate is stored as a single byte. + /// The flag indicating sign (if byte) or same-as-previous (if word). + /// An array of absolute coordinate values. + private static short[] ReadCoordinates(BigEndianBinaryReader reader, int pointCount, Flags[] flags, Flags isByte, Flags signOrSame) + { + short[] xs = new short[pointCount]; + short x = 0; + for (int i = 0; i < pointCount; i++) + { + short dx; + Flags currentFlag = flags[i]; + if ((currentFlag & isByte) == isByte) + { + byte b = reader.ReadByte(); + dx = (short)((currentFlag & signOrSame) == signOrSame ? b : -b); + } + else if ((currentFlag & signOrSame) == signOrSame) + { + dx = 0; + } + else + { + dx = reader.ReadInt16(); + } + + x += dx; + xs[i] = x; + } + + return xs; + } + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/Hinting/CvtTable.cs b/SixLabors.Fonts/Tables/TrueType/Hinting/CvtTable.cs new file mode 100644 index 0000000..a2bf20a --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/Hinting/CvtTable.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.TrueType.Hinting { + /// + /// Represents the 'cvt ' (Control Value Table) which contains a list of values + /// that can be referenced by TrueType hinting instructions. + /// + /// + internal class CvtTable : Table + { + /// + /// The table tag name. Note the trailing space is required. + /// + internal const string TableName = "cvt "; // space on the end of cvt is important/required + + /// + /// Initializes a new instance of the class. + /// + /// The array of control values. + public CvtTable(short[] controlValues) => this.ControlValues = controlValues; + + /// + /// Gets the array of control values referenceable by TrueType hinting instructions. + /// + public short[] ControlValues { get; } + + /// + /// Loads the 'cvt ' table from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static CvtTable? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader, out TableHeader? header)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader, header.Length); + } + } + + /// + /// Loads the 'cvt ' table from the specified binary reader. + /// + /// The big-endian binary reader positioned at the start of the table. + /// The length of the table in bytes. + /// The . + public static CvtTable Load(BigEndianBinaryReader reader, uint tableLength) + { + // HEADER + + // Type | Description + // ---------| ------------ + // FWORD[n] | List of n values referenceable by instructions.n is the number of FWORD items that fit in the size of the table. + const int shortSize = sizeof(short); + + int itemCount = (int)(tableLength / shortSize); + + short[] controlValues = reader.ReadFWORDArray(itemCount); + + return new CvtTable(controlValues); + } + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/Hinting/FpgmTable.cs b/SixLabors.Fonts/Tables/TrueType/Hinting/FpgmTable.cs new file mode 100644 index 0000000..32c2667 --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/Hinting/FpgmTable.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.TrueType.Hinting { + /// + /// Represents the 'fpgm' (Font Program) table which contains TrueType instructions + /// that are executed once when the font is first loaded. These instructions typically + /// define functions and instruction definitions used by glyph programs. + /// + /// + internal class FpgmTable : Table + { + /// + /// The table tag name. + /// + internal const string TableName = "fpgm"; + + /// + /// Initializes a new instance of the class. + /// + /// The font program bytecode instructions. + public FpgmTable(byte[] instructions) => this.Instructions = instructions; + + /// + /// Gets the font program bytecode instructions. + /// + public byte[] Instructions { get; } + + /// + /// Loads the 'fpgm' table from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static FpgmTable? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader, out TableHeader? header)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader, header.Length); + } + } + + /// + /// Loads the 'fpgm' table from the specified binary reader. + /// + /// The big-endian binary reader positioned at the start of the table. + /// The length of the table in bytes. + /// The . + public static FpgmTable Load(BigEndianBinaryReader reader, uint tableLength) + { + // HEADER + + // Type | Description + // ---------| ------------ + // uint8[n] | Instructions. n is the number of uint8 items that fit in the size of the table. + byte[] instructions = reader.ReadUInt8Array((int)tableLength); + + return new FpgmTable(instructions); + } + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/Hinting/PrepTable.cs b/SixLabors.Fonts/Tables/TrueType/Hinting/PrepTable.cs new file mode 100644 index 0000000..d57731f --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/Hinting/PrepTable.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.TrueType.Hinting { + /// + /// Represents the 'prep' (Control Value Program) table which contains TrueType instructions + /// that are executed whenever the point size or font transformation changes. The prep program + /// typically adjusts control values in the CVT for the current rendering size. + /// + /// + internal class PrepTable : Table + { + /// + /// The table tag name. + /// + internal const string TableName = "prep"; + + /// + /// Initializes a new instance of the class. + /// + /// The control value program bytecode instructions. + public PrepTable(byte[] instructions) => this.Instructions = instructions; + + /// + /// Gets the control value program bytecode instructions. + /// + public byte[] Instructions { get; } + + /// + /// Loads the 'prep' table from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static PrepTable? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader, out TableHeader? header)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader, header.Length); + } + } + + /// + /// Loads the 'prep' table from the specified binary reader. + /// + /// The big-endian binary reader positioned at the start of the table. + /// The length of the table in bytes. + /// The . + public static PrepTable Load(BigEndianBinaryReader reader, uint tableLength) + { + // HEADER + + // Type | Description + // ---------| ------------ + // uint8[n] | Set of instructions executed whenever point size or font or transformation change. n is the number of uint8 items that fit in the size of the table. + byte[]? instructions = reader.ReadUInt8Array((int)tableLength); + + return new PrepTable(instructions); + } + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.OpCodes.cs b/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.OpCodes.cs new file mode 100644 index 0000000..a84051e --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.OpCodes.cs @@ -0,0 +1,481 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Tables.TrueType.Hinting { + internal partial class TrueTypeInterpreter + { + /// + /// Gets stack pre-validation table matching FreeType's Pop_Push_Count. + /// Each byte encodes (popCount << 4) | pushCount for the corresponding opcode. + /// Used to validate stack depth before executing each instruction. + /// Opcodes that consume a variable number of arguments (e.g. NPUSHB, SHP, SHPIX, IP, + /// ALIGNRP, FLIPPT, GETVARIATION) are encoded as (0, 0) and handled specially. + /// + private static ReadOnlySpan PopPushCount => + [ + + // 0x00 + /* SVTCA[0] */ 0x00, + /* SVTCA[1] */ 0x00, + /* SPVTCA[0] */ 0x00, + /* SPVTCA[1] */ 0x00, + /* SFVTCA[0] */ 0x00, + /* SFVTCA[1] */ 0x00, + /* SPVTL[0] */ 0x20, + /* SPVTL[1] */ 0x20, + /* SFVTL[0] */ 0x20, + /* SFVTL[1] */ 0x20, + /* SPVFS */ 0x20, + /* SFVFS */ 0x20, + /* GPV */ 0x02, + /* GFV */ 0x02, + /* SFVTPV */ 0x00, + /* ISECT */ 0x50, + + // 0x10 + /* SRP0 */ 0x10, + /* SRP1 */ 0x10, + /* SRP2 */ 0x10, + /* SZP0 */ 0x10, + /* SZP1 */ 0x10, + /* SZP2 */ 0x10, + /* SZPS */ 0x10, + /* SLOOP */ 0x10, + /* RTG */ 0x00, + /* RTHG */ 0x00, + /* SMD */ 0x10, + /* ELSE */ 0x00, + /* JMPR */ 0x10, + /* SCVTCI */ 0x10, + /* SSWCI */ 0x10, + /* SSW */ 0x10, + + // 0x20 + /* DUP */ 0x12, + /* POP */ 0x10, + /* CLEAR */ 0x00, + /* SWAP */ 0x22, + /* DEPTH */ 0x01, + /* CINDEX */ 0x11, + /* MINDEX */ 0x10, + /* ALIGNPTS */ 0x20, + /* INS_$28 */ 0x00, + /* UTP */ 0x10, + /* LOOPCALL */ 0x20, + /* CALL */ 0x10, + /* FDEF */ 0x10, + /* ENDF */ 0x00, + /* MDAP[0] */ 0x10, + /* MDAP[1] */ 0x10, + + // 0x30 + /* IUP[0] */ 0x00, + /* IUP[1] */ 0x00, + /* SHP[0] */ 0x00, // loops + /* SHP[1] */ 0x00, // loops + /* SHC[0] */ 0x10, + /* SHC[1] */ 0x10, + /* SHZ[0] */ 0x10, + /* SHZ[1] */ 0x10, + /* SHPIX */ 0x10, // loops + /* IP */ 0x00, // loops + /* MSIRP[0] */ 0x20, + /* MSIRP[1] */ 0x20, + /* ALIGNRP */ 0x00, // loops + /* RTDG */ 0x00, + /* MIAP[0] */ 0x20, + /* MIAP[1] */ 0x20, + + // 0x40 + /* NPUSHB */ 0x00, + /* NPUSHW */ 0x00, + /* WS */ 0x20, + /* RS */ 0x11, + /* WCVTP */ 0x20, + /* RCVT */ 0x11, + /* GC[0] */ 0x11, + /* GC[1] */ 0x11, + /* SCFS */ 0x20, + /* MD[0] */ 0x21, + /* MD[1] */ 0x21, + /* MPPEM */ 0x01, + /* MPS */ 0x01, + /* FLIPON */ 0x00, + /* FLIPOFF */ 0x00, + /* DEBUG */ 0x10, + + // 0x50 + /* LT */ 0x21, + /* LTEQ */ 0x21, + /* GT */ 0x21, + /* GTEQ */ 0x21, + /* EQ */ 0x21, + /* NEQ */ 0x21, + /* ODD */ 0x11, + /* EVEN */ 0x11, + /* IF */ 0x10, + /* EIF */ 0x00, + /* AND */ 0x21, + /* OR */ 0x21, + /* NOT */ 0x11, + /* DELTAP1 */ 0x10, + /* SDB */ 0x10, + /* SDS */ 0x10, + + // 0x60 + /* ADD */ 0x21, + /* SUB */ 0x21, + /* DIV */ 0x21, + /* MUL */ 0x21, + /* ABS */ 0x11, + /* NEG */ 0x11, + /* FLOOR */ 0x11, + /* CEILING */ 0x11, + /* ROUND[0] */ 0x11, + /* ROUND[1] */ 0x11, + /* ROUND[2] */ 0x11, + /* ROUND[3] */ 0x11, + /* NROUND[0] */ 0x11, + /* NROUND[1] */ 0x11, + /* NROUND[2] */ 0x11, + /* NROUND[3] */ 0x11, + + // 0x70 + /* WCVTF */ 0x20, + /* DELTAP2 */ 0x10, + /* DELTAP3 */ 0x10, + /* DELTAC1 */ 0x10, + /* DELTAC2 */ 0x10, + /* DELTAC3 */ 0x10, + /* SROUND */ 0x10, + /* S45ROUND */ 0x10, + /* JROT */ 0x20, + /* JROF */ 0x20, + /* ROFF */ 0x00, + /* INS_$7B */ 0x00, + /* RUTG */ 0x00, + /* RDTG */ 0x00, + /* SANGW */ 0x10, + /* AA */ 0x10, + + // 0x80 + /* FLIPPT */ 0x00, // loops + /* FLIPRGON */ 0x20, + /* FLIPRGOFF */ 0x20, + /* INS_$83 */ 0x00, + /* INS_$84 */ 0x00, + /* SCANCTRL */ 0x10, + /* SDPVTL[0] */ 0x20, + /* SDPVTL[1] */ 0x20, + /* GETINFO */ 0x11, + /* IDEF */ 0x10, + /* ROLL */ 0x33, + /* MAX */ 0x21, + /* MIN */ 0x21, + /* SCANTYPE */ 0x10, + /* INSTCTRL */ 0x20, + /* INS_$8F */ 0x00, + + // 0x90 + /* INS_$90 */ 0x00, + /* GETVAR */ 0x00, // variable push, handled specially + /* GETDATA */ 0x01, + /* INS_$93 */ 0x00, + /* INS_$94 */ 0x00, + /* INS_$95 */ 0x00, + /* INS_$96 */ 0x00, + /* INS_$97 */ 0x00, + /* INS_$98 */ 0x00, + /* INS_$99 */ 0x00, + /* INS_$9A */ 0x00, + /* INS_$9B */ 0x00, + /* INS_$9C */ 0x00, + /* INS_$9D */ 0x00, + /* INS_$9E */ 0x00, + /* INS_$9F */ 0x00, + + // 0xA0 + /* INS_$A0 */ 0x00, + /* INS_$A1 */ 0x00, + /* INS_$A2 */ 0x00, + /* INS_$A3 */ 0x00, + /* INS_$A4 */ 0x00, + /* INS_$A5 */ 0x00, + /* INS_$A6 */ 0x00, + /* INS_$A7 */ 0x00, + /* INS_$A8 */ 0x00, + /* INS_$A9 */ 0x00, + /* INS_$AA */ 0x00, + /* INS_$AB */ 0x00, + /* INS_$AC */ 0x00, + /* INS_$AD */ 0x00, + /* INS_$AE */ 0x00, + /* INS_$AF */ 0x00, + + // 0xB0 + /* PUSHB[0] */ 0x01, + /* PUSHB[1] */ 0x02, + /* PUSHB[2] */ 0x03, + /* PUSHB[3] */ 0x04, + /* PUSHB[4] */ 0x05, + /* PUSHB[5] */ 0x06, + /* PUSHB[6] */ 0x07, + /* PUSHB[7] */ 0x08, + /* PUSHW[0] */ 0x01, + /* PUSHW[1] */ 0x02, + /* PUSHW[2] */ 0x03, + /* PUSHW[3] */ 0x04, + /* PUSHW[4] */ 0x05, + /* PUSHW[5] */ 0x06, + /* PUSHW[6] */ 0x07, + /* PUSHW[7] */ 0x08, + + // 0xC0 + /* MDRP[00] */ 0x10, + /* MDRP[01] */ 0x10, + /* MDRP[02] */ 0x10, + /* MDRP[03] */ 0x10, + /* MDRP[04] */ 0x10, + /* MDRP[05] */ 0x10, + /* MDRP[06] */ 0x10, + /* MDRP[07] */ 0x10, + /* MDRP[08] */ 0x10, + /* MDRP[09] */ 0x10, + /* MDRP[10] */ 0x10, + /* MDRP[11] */ 0x10, + /* MDRP[12] */ 0x10, + /* MDRP[13] */ 0x10, + /* MDRP[14] */ 0x10, + /* MDRP[15] */ 0x10, + + // 0xD0 + /* MDRP[16] */ 0x10, + /* MDRP[17] */ 0x10, + /* MDRP[18] */ 0x10, + /* MDRP[19] */ 0x10, + /* MDRP[20] */ 0x10, + /* MDRP[21] */ 0x10, + /* MDRP[22] */ 0x10, + /* MDRP[23] */ 0x10, + /* MDRP[24] */ 0x10, + /* MDRP[25] */ 0x10, + /* MDRP[26] */ 0x10, + /* MDRP[27] */ 0x10, + /* MDRP[28] */ 0x10, + /* MDRP[29] */ 0x10, + /* MDRP[30] */ 0x10, + /* MDRP[31] */ 0x10, + + // 0xE0 + /* MIRP[00] */ 0x20, + /* MIRP[01] */ 0x20, + /* MIRP[02] */ 0x20, + /* MIRP[03] */ 0x20, + /* MIRP[04] */ 0x20, + /* MIRP[05] */ 0x20, + /* MIRP[06] */ 0x20, + /* MIRP[07] */ 0x20, + /* MIRP[08] */ 0x20, + /* MIRP[09] */ 0x20, + /* MIRP[10] */ 0x20, + /* MIRP[11] */ 0x20, + /* MIRP[12] */ 0x20, + /* MIRP[13] */ 0x20, + /* MIRP[14] */ 0x20, + /* MIRP[15] */ 0x20, + + // 0xF0 + /* MIRP[16] */ 0x20, + /* MIRP[17] */ 0x20, + /* MIRP[18] */ 0x20, + /* MIRP[19] */ 0x20, + /* MIRP[20] */ 0x20, + /* MIRP[21] */ 0x20, + /* MIRP[22] */ 0x20, + /* MIRP[23] */ 0x20, + /* MIRP[24] */ 0x20, + /* MIRP[25] */ 0x20, + /* MIRP[26] */ 0x20, + /* MIRP[27] */ 0x20, + /* MIRP[28] */ 0x20, + /* MIRP[29] */ 0x20, + /* MIRP[30] */ 0x20, + /* MIRP[31] */ 0x20, + ]; + +#pragma warning disable SA1201 // Elements should appear in the correct order + /// + /// TrueType instruction opcodes used by the bytecode interpreter. + /// + private enum OpCode : byte +#pragma warning restore SA1201 // Elements should appear in the correct order + { + SVTCA0, + SVTCA1, + SPVTCA0, + SPVTCA1, + SFVTCA0, + SFVTCA1, + SPVTL0, + SPVTL1, + SFVTL0, + SFVTL1, + SPVFS, + SFVFS, + GPV, + GFV, + SFVTPV, + ISECT, + SRP0, + SRP1, + SRP2, + SZP0, + SZP1, + SZP2, + SZPS, + SLOOP, + RTG, + RTHG, + SMD, + ELSE, + JMPR, + SCVTCI, + SSWCI, + SSW, + DUP, + POP, + CLEAR, + SWAP, + DEPTH, + CINDEX, + MINDEX, + ALIGNPTS, + /* unused: 0x28 */ + UTP = 0x29, + LOOPCALL, + CALL, + FDEF, + ENDF, + MDAP0, + MDAP1, + IUP0, + IUP1, + SHP0, + SHP1, + SHC0, + SHC1, + SHZ0, + SHZ1, + SHPIX, + IP, + MSIRP0, + MSIRP1, + ALIGNRP, + RTDG, + MIAP0, + MIAP1, + NPUSHB, + NPUSHW, + WS, + RS, + WCVTP, + RCVT, + GC0, + GC1, + SCFS, + MD0, + MD1, + MPPEM, + MPS, + FLIPON, + FLIPOFF, + DEBUG, + LT, + LTEQ, + GT, + GTEQ, + EQ, + NEQ, + ODD, + EVEN, + IF, + EIF, + AND, + OR, + NOT, + DELTAP1, + SDB, + SDS, + ADD, + SUB, + DIV, + MUL, + ABS, + NEG, + FLOOR, + CEILING, + ROUND0, + ROUND1, + ROUND2, + ROUND3, + NROUND0, + NROUND1, + NROUND2, + NROUND3, + WCVTF, + DELTAP2, + DELTAP3, + DELTAC1, + DELTAC2, + DELTAC3, + SROUND, + S45ROUND, + JROT, + JROF, + ROFF, + /* unused: 0x7B */ + RUTG = 0x7C, + RDTG, + SANGW, + AA, + FLIPPT, + FLIPRGON, + FLIPRGOFF, + /* unused: 0x83 - 0x84 */ + SCANCTRL = 0x85, + SDPVTL0, + SDPVTL1, + GETINFO, + IDEF, + ROLL, + MAX, + MIN, + SCANTYPE, + INSTCTRL, + /* unused: 0x8F - 0x90 */ + GETVARIATION = 0x91, + GETDATA, + /* unused: 0x93 - 0xAF */ + PUSHB1 = 0xB0, + PUSHB2, + PUSHB3, + PUSHB4, + PUSHB5, + PUSHB6, + PUSHB7, + PUSHB8, + PUSHW1, + PUSHW2, + PUSHW3, + PUSHW4, + PUSHW5, + PUSHW6, + PUSHW7, + PUSHW8, + MDRP, // range of 32 values, 0xC0 - 0xDF, + MIRP = 0xE0 // range of 32 values, 0xE0 - 0xFF + } + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs b/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs new file mode 100644 index 0000000..ebb4be5 --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs @@ -0,0 +1,3532 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.Fonts.Tables.TrueType.Glyphs; + +namespace SixLabors.Fonts.Tables.TrueType.Hinting { + /// + /// Code adapted from + /// . + /// + /// Reference material: + /// – + /// the original TrueType instruction set and execution model. + /// – + /// details on how Microsoft's ClearType rasterizer interprets TrueType hints. + /// – + /// documentation of FreeType's subpixel hinting engines, including the v40 "minimal" interpreter. + /// + /// + /// This implementation matches the behavior of FreeType's v40 subpixel hinting interpreter, + /// with horizontal hinting disabled and full vertical TrueType instruction processing preserved. + /// It follows the v40 model in which outlines are adjusted primarily along the Y-axis and + /// instructions operate without backward compatibility constraints. This corresponds to + /// FreeType's configuration where TT_CONFIG_OPTION_SUBPIXEL_HINTING selects the + /// minimal (v40) engine and backward_compatibility is forced to zero. + /// + /// + /// + /// Modern ClearType-hinted fonts are designed for this style of processing and will render + /// consistently under this interpreter. Legacy CRT-era fonts such as Arial or Times New Roman + /// also render cleanly under v40 semantics, though without legacy bi-level horizontal snapping, + /// which v40 intentionally omits. + /// + /// + internal partial class TrueTypeInterpreter + { + // Current and saved graphics state. cvtState is captured after the prep (CVT) program + // runs so that each glyph program begins with a consistent baseline. + private GraphicsState state; + private GraphicsState cvtState; + + private readonly ExecutionStack stack; + private readonly InstructionStream[] functions; + private readonly InstructionStream[] instructionDefs; + + // Control Value Table: baseControlValueTable holds the scaled values after prep execution; + // controlValueTable is a working copy restored at the start of each glyph program. + private float[] baseControlValueTable; + private float[] controlValueTable; + + // Storage area shared between prep and glyph programs. prepStorage holds the reference + // to the storage array as it was after prep execution. Glyph programs use copy-on-write + // (see WS instruction) so that prep state is preserved across glyphs. + private int[] storage; + private int[]? prepStorage; + private bool inGlyphProgram; + + private IReadOnlyList contours; + private float scale; + private int ppem; + private int callStackSize; + + // Dot product of freedom and projection vectors, used to decompose + // scalar distances into movement along the freedom vector. + private float fdotp; + + // Super-rounding parameters set by SROUND/S45ROUND. + private float roundThreshold; + private float roundPhase; + private float roundPeriod; + + // IUP tracking — once both axes have been interpolated, further IUP calls are skipped + // and v40 backward compatibility blocks Y movement (post-IUP restriction). + private bool iupXCalled; + private bool iupYCalled; + private bool isComposite; + + // Normalized variation axis coordinates for variable fonts, used by GETVARIATION/GETINFO. + private float[]? normalizedAxisCoordinates; + + // FreeType TT_RunIns safety counters to prevent pathological fonts + // from hanging the interpreter. Limits are computed per-glyph based on + // point count and CVT size. + private long insCounter; + private long loopcallCounter; + private long negJumpCounter; + private long loopcallCounterMax; + private long negJumpCounterMax; + + // Zone pointers: zp0/zp1/zp2 are the three zone pointer registers (ZP0-ZP2). + // They can reference either the glyph zone (points) or the twilight zone. + private Zone zp0; + private Zone zp1; + private Zone zp2; + private Zone points; + private Zone twilight; + + private static readonly float Sqrt2Over2 = (float)(Math.Sqrt(2) / 2); + private const int MaxCallStack = 128; + private const long MaxRunnableOpcodes = 1_000_000; + private const float Epsilon = 0.000001F; + +#if DEBUG + private readonly List debugList = []; +#endif + +#if HINTING_TRACE + private readonly System.Text.StringBuilder traceLog = new(); + private int traceGlyphIndex; +#endif + + /// + /// Initializes a new instance of the class + /// with resource limits sourced from the font's maxp table. + /// + /// Maximum stack depth. + /// Number of storage area locations. + /// Number of function definition slots (FDEF). + /// Number of instruction definition slots (IDEF). When non-zero, a full 256-entry lookup table is allocated. + /// Number of points in the twilight zone. + public TrueTypeInterpreter(int maxStack, int maxStorage, int maxFunctions, int maxInstructionDefs, int maxTwilightPoints) + { + this.stack = new ExecutionStack(maxStack); + this.storage = new int[maxStorage]; + this.functions = new InstructionStream[maxFunctions]; + this.instructionDefs = new InstructionStream[maxInstructionDefs > 0 ? 256 : 0]; + this.state = default; + this.cvtState = default; + this.twilight = new Zone(maxTwilightPoints, isTwilight: true); + this.controlValueTable = []; + this.baseControlValueTable = []; + this.contours = []; + } + + /// + /// Sets the normalized axis coordinates for variable font hinting. + /// These are used by the GETVARIATION and GETINFO instructions. + /// + /// Normalized axis coordinates in the range [-1, 1], or for non-variable fonts. + public void SetNormalizedAxisCoordinates(float[]? coordinates) + => this.normalizedAxisCoordinates = coordinates; + + /// + /// Executes the font program (fpgm) to populate function definitions (FDEF/IDEF). + /// This must be called once per font before any CVT or glyph programs are executed. + /// + /// The raw font program bytecode. + public void InitializeFunctionDefs(byte[] instructions) + => this.Execute(new StackInstructionStream(instructions, 0), false, true); + + /// + /// Scales the Control Value Table and executes the prep (CVT) program. + /// The prep program typically sets up the graphics state and may modify CVT entries + /// for the current pixel size. The resulting state is saved and restored for each + /// subsequent glyph program execution. + /// + /// The raw CVT entries from the font, or if absent. + /// The scale factor to apply to CVT entries (units-per-em to pixels). + /// The pixels-per-em value at the current size. + /// The raw prep program bytecode, or if absent. + public void SetControlValueTable(short[]? cvt, float scale, float ppem, byte[]? cvProgram) + { + if (this.scale == scale || cvt == null) + { + return; + } + else + { + if (this.controlValueTable.Length == 0 && cvt.Length > 0) + { + this.controlValueTable = new float[cvt.Length]; + } + + for (int i = 0; i < cvt.Length; i++) + { + this.controlValueTable[i] = cvt[i] * scale; + } + } + + this.scale = scale; + this.ppem = (int)Math.Round(ppem); + this.zp0 = this.zp1 = this.zp2 = this.points; + this.state.Reset(); + this.stack.Clear(); + + if (cvProgram != null) + { + // Initialize safety counters for the prep program (no glyph points yet). + this.insCounter = 0; + this.loopcallCounter = 0; + this.negJumpCounter = 0; + int cvtSize = this.controlValueTable.Length; + this.loopcallCounterMax = 300 + (22 * (long)cvtSize); + this.negJumpCounterMax = this.loopcallCounterMax; + + this.Execute(new StackInstructionStream(cvProgram, 0), false, false); + + // Save prep program storage state so glyph programs can read it (copy-on-write in WS). + this.prepStorage = this.storage; + + // save off the CVT graphics state so that we can restore it for each glyph we hint + if ((this.state.InstructionControl & InstructionControlFlags.UseDefaultGraphicsState) != 0) + { + this.cvtState.Reset(); + } + else + { + // always reset a few fields; copy the reset + this.cvtState = this.state; + this.cvtState.Freedom = Vector2.UnitX; + this.cvtState.Projection = Vector2.UnitX; + this.cvtState.DualProjection = Vector2.UnitX; + this.cvtState.RoundState = RoundMode.ToGrid; + this.cvtState.Loop = 1; + } + } + + if (this.controlValueTable.Length > 0) + { + if (this.baseControlValueTable.Length != this.controlValueTable.Length) + { + this.baseControlValueTable = new float[this.controlValueTable.Length]; + } + + Array.Copy(this.controlValueTable, this.baseControlValueTable, this.controlValueTable.Length); + } + else + { + this.baseControlValueTable = []; + } + } + + /// + /// Attempts to apply TrueType hinting instructions to the specified glyph outline. + /// + /// + /// Hinting will not be applied if the instructions buffer is empty or if grid fitting is + /// inhibited by the current interpreter state. If the instructions are malformed or an error occurs during + /// execution, the method returns and the glyph outline remains unhinted. + /// + /// An array of control points representing the glyph's outline to be hinted. + /// A read-only list of indices indicating the end points of each contour in the glyph. + /// A read-only memory buffer containing the TrueType hinting instructions to execute. + /// Indicates whether the glyph is a composite glyph. Set to for composite glyphs; otherwise, . + /// if hinting was successfully applied; otherwise, . + public bool TryHintGlyph( + ControlPoint[] controlPoints, + IReadOnlyList endPoints, + ReadOnlyMemory instructions, + bool isComposite) + { + if (instructions.Length == 0) + { + return false; + } + + // Check if the CVT program disabled hinting + if ((this.state.InstructionControl & InstructionControlFlags.InhibitGridFitting) != 0) + { + return false; + } + + try + { + // Save contours and points + this.contours = endPoints; + this.zp0 = this.zp1 = this.zp2 = this.points = new Zone(controlPoints, isTwilight: false); + + // reset all of our shared state + this.state = this.cvtState; + this.callStackSize = 0; + + // FreeType preserves prep program storage via copy-on-write in WS. + // Restore the prep storage pointer; if glyph writes, WS will copy first. + if (this.prepStorage != null) + { + this.storage = this.prepStorage; + } + else + { + Array.Clear(this.storage, 0, this.storage.Length); + } + + this.inGlyphProgram = true; + + if (this.baseControlValueTable.Length > 0) + { + if (this.controlValueTable.Length != this.baseControlValueTable.Length) + { + this.controlValueTable = new float[this.baseControlValueTable.Length]; + } + + Array.Copy(this.baseControlValueTable, this.controlValueTable, this.baseControlValueTable.Length); + } + else + { + this.controlValueTable = []; + } + + this.ResetTwilightZone(); + +#if DEBUG + this.debugList.Clear(); +#endif + +#if HINTING_TRACE + this.traceLog.Clear(); + this.traceLog.AppendLine(System.FormattableString.Invariant($"=== GLYPH {this.traceGlyphIndex++} pts={controlPoints.Length - 4} composite={isComposite} ===")); +#endif + + this.stack.Clear(); + this.OnVectorsUpdated(); + this.iupXCalled = false; + this.iupYCalled = false; + this.isComposite = isComposite; + + // FreeType TT_RunIns — initialize safety counters. + this.insCounter = 0; + this.loopcallCounter = 0; + this.negJumpCounter = 0; + int nPoints = controlPoints.Length; + int cvtSize = this.controlValueTable.Length; + if (nPoints > 0) + { + this.loopcallCounterMax = Math.Max(50, 10 * (long)nPoints) + Math.Max(50, cvtSize / 10); + } + else + { + this.loopcallCounterMax = 300 + (22 * (long)cvtSize); + } + + this.negJumpCounterMax = this.loopcallCounterMax; + + // normalize the round state settings + switch (this.state.RoundState) + { + case RoundMode.Super: + this.SetSuperRound(1.0f); + break; + case RoundMode.Super45: + this.SetSuperRound(Sqrt2Over2); + break; + } + + this.Execute(new StackInstructionStream(instructions, 0), false, false); + +#if HINTING_TRACE + System.Console.Error.Write(this.traceLog); +#endif + + return true; + } + catch (Exception) + { +#if HINTING_TRACE + System.Console.Error.Write(this.traceLog); + + // Rethrow to diagnose hinting failures. + throw; +#endif + return false; + } + } + + /// + /// Resets all twilight zone points to the origin and clears their touch state, + /// preventing stale data from leaking between glyph programs. + /// + private void ResetTwilightZone() + { + // In FreeType, twilight points are defined to have original coordinates at (0,0). + // Reset both original and current coordinates, and clear touch state, to avoid state leaking between glyphs. + ControlPoint[] twCurrent = this.twilight.Current; + ControlPoint[] twOriginal = this.twilight.Original; + + int len = twCurrent.Length; + for (int i = 0; i < len; i++) + { + twCurrent[i].Point = default; + twOriginal[i].Point = default; + } + + Array.Clear(this.twilight.TouchState, 0, this.twilight.TouchState.Length); + } + + /// + /// Core instruction dispatch loop. Reads and executes opcodes from the given + /// instruction stream until the stream is exhausted or an error terminates execution. + /// + /// The instruction stream to execute. + /// + /// when executing inside a CALL/LOOPCALL function body. + /// Controls whether ENDF returns to the caller or exits execution. + /// + /// + /// when executing the font program (fpgm), which permits + /// FDEF and IDEF instructions. Glyph and prep programs set this to . + /// + private void Execute(StackInstructionStream stream, bool inFunction, bool allowFunctionDefs) + { + while (!stream.Done) + { + int rawOpcode = stream.NextByte(); + OpCode opcode = (OpCode)rawOpcode; + +#if DEBUG + this.debugList.Add(opcode); +#endif + + // FreeType TT_RunIns — global instruction counter to prevent infinite loops. + if (++this.insCounter > MaxRunnableOpcodes) + { + return; + } + + // FreeType TT_RunIns — pre-validate stack depth before dispatch. + byte popPush = PopPushCount[rawOpcode]; + int pops = popPush >> 4; + int pushes = popPush & 0xF; + +#if HINTING_TRACE + int preStackCount = this.stack.Count; + this.TracePreInstruction(opcode, pops); +#endif + + // Underflow: push zeroes to fill missing args (FreeType non-pedantic mode). + if (this.stack.Count < pops) + { + int missing = pops - this.stack.Count; + this.stack.Clear(); + for (int z = 0; z < pops; z++) + { + this.stack.Push(0); + } + } + + // Overflow: exit the run loop (FreeType non-pedantic: set error and return). + if (this.stack.Count - pops + pushes > this.stack.Capacity) + { + return; + } + + switch (opcode) + { + // ==== PUSH INSTRUCTIONS ==== + case OpCode.NPUSHB: + case OpCode.PUSHB1: + case OpCode.PUSHB2: + case OpCode.PUSHB3: + case OpCode.PUSHB4: + case OpCode.PUSHB5: + case OpCode.PUSHB6: + case OpCode.PUSHB7: + case OpCode.PUSHB8: + { + int count = opcode == OpCode.NPUSHB ? stream.NextByte() : opcode - OpCode.PUSHB1 + 1; + for (int i = 0; i < count; i++) + { + this.stack.Push(stream.NextByte()); + } + } + + break; + case OpCode.NPUSHW: + case OpCode.PUSHW1: + case OpCode.PUSHW2: + case OpCode.PUSHW3: + case OpCode.PUSHW4: + case OpCode.PUSHW5: + case OpCode.PUSHW6: + case OpCode.PUSHW7: + case OpCode.PUSHW8: + { + int count = opcode == OpCode.NPUSHW ? stream.NextByte() : opcode - OpCode.PUSHW1 + 1; + for (int i = 0; i < count; i++) + { + this.stack.Push(stream.NextWord()); + } + } + + break; + + // ==== STORAGE MANAGEMENT ==== + case OpCode.RS: + { + int loc = this.stack.Pop(); + if ((uint)loc >= (uint)this.storage.Length) + { + this.stack.Push(0); + } + else + { + this.stack.Push(this.storage[loc]); + } + } + + break; + case OpCode.WS: + { + int value = this.stack.Pop(); + int loc = this.stack.Pop(); + if ((uint)loc < (uint)this.storage.Length) + { + // FreeType copy-on-write: when glyph program first writes to storage, + // make a private copy so prep program state is preserved for other glyphs. + if (this.inGlyphProgram && this.storage == this.prepStorage) + { + int[] glyphStorage = new int[this.storage.Length]; + Array.Copy(this.storage, glyphStorage, this.storage.Length); + this.storage = glyphStorage; + } + + this.storage[loc] = value; + } + } + + break; + + // ==== CONTROL VALUE TABLE ==== + case OpCode.WCVTP: + { + float value = this.stack.PopFloat(); + int loc = this.stack.Pop(); + if ((uint)loc < (uint)this.controlValueTable.Length) + { + this.controlValueTable[loc] = value; + } + } + + break; + case OpCode.WCVTF: + { + int value = this.stack.Pop(); + int loc = this.stack.Pop(); + if ((uint)loc < (uint)this.controlValueTable.Length) + { + this.controlValueTable[loc] = value * this.scale; + } + } + + break; + case OpCode.RCVT: + { + int loc = this.stack.Pop(); + if ((uint)loc >= (uint)this.controlValueTable.Length) + { + this.stack.Push(0); + } + else + { + this.stack.Push(this.controlValueTable[loc]); + } + + break; + } + + // ==== STATE VECTORS ==== + case OpCode.SVTCA0: + case OpCode.SVTCA1: + { + byte axis = opcode - OpCode.SVTCA0; + this.SetFreedomVectorToAxis(axis); + this.SetProjectionVectorToAxis(axis); + } + + break; + case OpCode.SFVTPV: + { + this.state.Freedom = this.state.Projection; + this.OnVectorsUpdated(); + break; + } + + case OpCode.SPVTCA0: + case OpCode.SPVTCA1: + { + this.SetProjectionVectorToAxis(opcode - OpCode.SPVTCA0); + break; + } + + case OpCode.SFVTCA0: + case OpCode.SFVTCA1: + { + this.SetFreedomVectorToAxis(opcode - OpCode.SFVTCA0); + break; + } + + case OpCode.SPVTL0: + case OpCode.SPVTL1: + case OpCode.SFVTL0: + case OpCode.SFVTL1: + { + this.SetVectorToLine(opcode - OpCode.SPVTL0, false); + break; + } + + case OpCode.SDPVTL0: + case OpCode.SDPVTL1: + { + this.SetVectorToLine(opcode - OpCode.SDPVTL0, true); + break; + } + + case OpCode.SPVFS: + case OpCode.SFVFS: + { + int y = this.stack.Pop(); + int x = this.stack.Pop(); + Vector2 vec = Vector2.Normalize(new Vector2(F2Dot14ToFloat(x), F2Dot14ToFloat(y))); + if (opcode == OpCode.SFVFS) + { + this.state.Freedom = vec; + } + else + { + this.state.Projection = vec; + this.state.DualProjection = vec; + } + + this.OnVectorsUpdated(); + } + + break; + case OpCode.GPV: + case OpCode.GFV: + { + Vector2 vec = opcode == OpCode.GPV ? this.state.Projection : this.state.Freedom; + this.stack.Push(FloatToF2Dot14(vec.X)); + this.stack.Push(FloatToF2Dot14(vec.Y)); + } + + break; + + // ==== GRAPHICS STATE ==== + case OpCode.SRP0: + { + this.state.Rp0 = this.stack.Pop(); + break; + } + + case OpCode.SRP1: + { + this.state.Rp1 = this.stack.Pop(); + break; + } + + case OpCode.SRP2: + { + this.state.Rp2 = this.stack.Pop(); + break; + } + + case OpCode.SZP0: + { + if (this.TryGetZoneFromStack(out Zone szp0Zone)) + { + this.zp0 = szp0Zone; + } + + break; + } + + case OpCode.SZP1: + { + if (this.TryGetZoneFromStack(out Zone szp1Zone)) + { + this.zp1 = szp1Zone; + } + + break; + } + + case OpCode.SZP2: + { + if (this.TryGetZoneFromStack(out Zone szp2Zone)) + { + this.zp2 = szp2Zone; + } + + break; + } + + case OpCode.SZPS: + { + if (this.TryGetZoneFromStack(out Zone szpsZone)) + { + this.zp0 = this.zp1 = this.zp2 = szpsZone; + } + + break; + } + + case OpCode.RTHG: + { + this.state.RoundState = RoundMode.ToHalfGrid; + break; + } + + case OpCode.RTG: + { + this.state.RoundState = RoundMode.ToGrid; + break; + } + + case OpCode.RTDG: + { + this.state.RoundState = RoundMode.ToDoubleGrid; + break; + } + + case OpCode.RDTG: + { + this.state.RoundState = RoundMode.DownToGrid; + break; + } + + case OpCode.RUTG: + { + this.state.RoundState = RoundMode.UpToGrid; + break; + } + + case OpCode.ROFF: + { + this.state.RoundState = RoundMode.Off; + break; + } + + case OpCode.SROUND: + { + this.state.RoundState = RoundMode.Super; + this.SetSuperRound(1.0f); + break; + } + + case OpCode.S45ROUND: + { + this.state.RoundState = RoundMode.Super45; + this.SetSuperRound(Sqrt2Over2); + break; + } + + case OpCode.INSTCTRL: + { + // FreeType Ins_INSTCTRL. + // Always pop both arguments to keep the stack balanced. + int selector = this.stack.Pop(); + int value = this.stack.Pop(); + + // FreeType restricts selectors 1-2 to the prep (CVT) program only. + // Selector 3 (NativeClearType) can also be set during prep. + // Glyph programs cannot modify instruction control flags. + if (selector is >= 1 and <= 3 && !this.inGlyphProgram) + { + int bit = 1 << (selector - 1); + + // FreeType validates: if value != 0, it must equal the expected bit. + if (value == 0) + { + this.state.InstructionControl = (InstructionControlFlags)((int)this.state.InstructionControl & ~bit); + } + else if (value == bit) + { + this.state.InstructionControl = (InstructionControlFlags)((int)this.state.InstructionControl | bit); + } + } + } + + break; + case OpCode.SCANCTRL: /* instruction unspported */ + case OpCode.SCANTYPE: /* instruction unspported */ + case OpCode.SANGW: /* instruction unspported */ + { + this.stack.Pop(); + break; + } + + case OpCode.SLOOP: + { + int loop = this.stack.Pop(); + if (loop < 0) + { + // FreeType sets Bad_Argument error and returns without modifying state. + break; + } + + // FreeType heuristically caps loop count at 16 bits. + this.state.Loop = loop > 0xFFFF ? 0xFFFF : loop; + break; + } + + case OpCode.SMD: + { + this.state.MinDistance = this.stack.PopFloat(); + break; + } + + case OpCode.SCVTCI: + { + this.state.ControlValueCutIn = this.stack.PopFloat(); + break; + } + + case OpCode.SSWCI: + { + this.state.SingleWidthCutIn = this.stack.PopFloat(); + break; + } + + case OpCode.SSW: + { + this.state.SingleWidthValue = this.stack.Pop() * this.scale; + break; + } + + case OpCode.FLIPON: + { + this.state.AutoFlip = true; + break; + } + + case OpCode.FLIPOFF: + { + this.state.AutoFlip = false; + break; + } + + case OpCode.SDB: + { + this.state.DeltaBase = this.stack.Pop(); + break; + } + + case OpCode.SDS: + { + this.state.DeltaShift = this.stack.Pop(); + break; + } + + // ==== POINT MEASUREMENT ==== + case OpCode.GC0: + { + int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp2.Current.Length) + { + this.stack.Push(0); + break; + } + + this.stack.Push(this.Project(this.zp2.GetCurrent(pointIndex))); + break; + } + + case OpCode.GC1: + { + int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp2.Current.Length) + { + this.stack.Push(0); + break; + } + + this.stack.Push(this.DualProject(this.zp2.GetOriginal(pointIndex))); + break; + } + + case OpCode.SCFS: + { + float value = this.stack.PopFloat(); + int index = this.stack.Pop(); + if ((uint)index >= (uint)this.zp2.Current.Length) + { + break; + } + + Vector2 point = this.zp2.GetCurrent(index); + this.MovePoint(this.zp2, index, value - this.Project(point)); + + // Moving twilight points moves their "original" value also + if (this.zp2.IsTwilight) + { + this.zp2.Original[index].Point = this.zp2.Current[index].Point; + } + } + + break; + case OpCode.MD0: + { + int i0 = this.stack.Pop(); + int i1 = this.stack.Pop(); + if ((uint)i0 >= (uint)this.zp1.Current.Length || + (uint)i1 >= (uint)this.zp0.Current.Length) + { + this.stack.Push(0); + break; + } + + this.stack.Push(this.DualProject(this.zp0.GetOriginal(i1) - this.zp1.GetOriginal(i0))); + } + + break; + case OpCode.MD1: + { + int i0 = this.stack.Pop(); + int i1 = this.stack.Pop(); + if ((uint)i0 >= (uint)this.zp1.Current.Length || + (uint)i1 >= (uint)this.zp0.Current.Length) + { + this.stack.Push(0); + break; + } + + this.stack.Push(this.Project(this.zp0.GetCurrent(i1) - this.zp1.GetCurrent(i0))); + } + + break; + case OpCode.MPS: // MPS should return point size, but we assume DPI so it's the same as pixel size + case OpCode.MPPEM: + { + this.stack.Push(this.ppem); + break; + } + + case OpCode.AA: /* deprecated instruction */ + { + this.stack.Pop(); + break; + } + + // ==== POINT MODIFICATION ==== + case OpCode.FLIPPT: + { + // FreeType: FLIP instructions skip when backward_compatibility == 0x7. + bool nativeClearType = (this.state.InstructionControl & InstructionControlFlags.NativeClearType) != 0; + bool blocked = !nativeClearType && this.iupXCalled && this.iupYCalled; + for (int i = 0; i < this.state.Loop; i++) + { + int index = this.stack.Pop(); + if (blocked || (uint)index >= (uint)this.points.Current.Length) + { + continue; + } + + this.points.Current[index].OnCurve ^= true; + } + + this.state.Loop = 1; + } + + break; + case OpCode.FLIPRGON: + { + bool nativeClearType = (this.state.InstructionControl & InstructionControlFlags.NativeClearType) != 0; + bool blocked = !nativeClearType && this.iupXCalled && this.iupYCalled; + int end = this.stack.Pop(); + int start = this.stack.Pop(); + if (blocked || + (uint)end >= (uint)this.points.Current.Length || + (uint)start >= (uint)this.points.Current.Length) + { + break; + } + + for (int i = start; i <= end; i++) + { + this.points.Current[i].OnCurve = true; + } + } + + break; + case OpCode.FLIPRGOFF: + { + bool nativeClearType = (this.state.InstructionControl & InstructionControlFlags.NativeClearType) != 0; + bool blocked = !nativeClearType && this.iupXCalled && this.iupYCalled; + int end = this.stack.Pop(); + int start = this.stack.Pop(); + if (blocked || + (uint)end >= (uint)this.points.Current.Length || + (uint)start >= (uint)this.points.Current.Length) + { + break; + } + + for (int i = start; i <= end; i++) + { + this.points.Current[i].OnCurve = false; + } + } + + break; + case OpCode.SHP0: + case OpCode.SHP1: + { + // FreeType Ins_SHP: uses Move_Zp2_Point for each point. + if (!this.TryComputeDisplacement((int)opcode, out _, out _, out Vector2 displacement)) + { + // FreeType: Compute_Point_Displacement failure returns (no Fail label, loop NOT reset). + for (int i = 0; i < this.state.Loop; i++) + { + this.stack.Pop(); + } + + this.state.Loop = 1; + break; + } + + for (int i = 0; i < this.state.Loop; i++) + { + int pointIndex = this.stack.Pop(); + if ((uint)pointIndex < (uint)this.zp2.Current.Length) + { + this.MoveZp2Point(this.zp2, pointIndex, displacement.X, displacement.Y, true); + } + } + + this.state.Loop = 1; + } + + break; + case OpCode.SHPIX: + { + // FreeType Ins_SHPIX: v40 backward compatibility gating. + float magnitude = this.stack.PopFloat(); + float dx = magnitude * this.state.Freedom.X; + float dy = magnitude * this.state.Freedom.Y; + bool nativeClearType = (this.state.InstructionControl & InstructionControlFlags.NativeClearType) != 0; + bool postIUP = this.iupXCalled && this.iupYCalled; + bool inTwilight = this.zp0.IsTwilight || this.zp1.IsTwilight || this.zp2.IsTwilight; + + for (int i = 0; i < this.state.Loop; i++) + { + int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp2.Current.Length) + { + continue; + } + + if (!nativeClearType) + { + // Backward compat mode: gated Y-only movement. + // Twilight zone always allowed; otherwise need composite+freeY or Y-touched. + // Post-IUP (0x7): nothing moves (MoveZp2Point blocks Y at post-IUP). + if (inTwilight || + (!postIUP && + ((this.isComposite && this.state.Freedom.Y != 0) || + ((this.zp2.TouchState[pointIndex] & TouchState.Y) == TouchState.Y)))) + { + this.MoveZp2Point(this.zp2, pointIndex, 0, dy, true); + } + } + else + { + // Native ClearType: move freely on both axes. + this.MoveZp2Point(this.zp2, pointIndex, dx, dy, true); + } + } + + this.state.Loop = 1; + break; + } + + case OpCode.SHC0: + case OpCode.SHC1: + { + if (!this.TryComputeDisplacement((int)opcode, out Zone zone, out int point, out Vector2 displacement)) + { + this.stack.Pop(); + break; + } + + int contour = this.stack.Pop(); + int bounds = this.zp2.IsTwilight ? 1 : this.contours.Count; + if ((uint)contour >= (uint)bounds) + { + break; + } + + int start = contour == 0 ? 0 : this.contours[contour - 1] + 1; + int count = this.zp2.IsTwilight ? this.zp2.Current.Length : this.contours[contour] + 1; + ControlPoint[] current = this.zp2.Current; + TouchState[] states = this.zp2.TouchState; + + for (int i = start; i < count; i++) + { + // Don't move the reference point + if (zone.Current != current || point != i) + { + this.MoveZp2Point(this.zp2, i, displacement.X, displacement.Y, true); + } + } + } + + break; + case OpCode.SHZ0: + case OpCode.SHZ1: + { + // FreeType Ins_SHZ: pop zone index first, then compute displacement. + int shzZone = this.stack.Pop(); + if ((uint)shzZone >= 2) + { + break; + } + + if (!this.TryComputeDisplacement((int)opcode, out Zone zone, out int point, out Vector2 displacement)) + { + break; + } + + int count = 0; + if (this.zp2.IsTwilight) + { + count = this.zp2.Current.Length; + } + else if (this.contours.Count > 0) + { + count = this.contours[this.contours.Count - 1] + 1; + } + + ControlPoint[] current = this.zp2.Current; + for (int i = 0; i < count; i++) + { + // Don't move the reference point + if (zone.Current != current || point != i) + { + this.MoveZp2Point(this.zp2, i, displacement.X, displacement.Y, false); + } + } + } + + break; + case OpCode.MIAP0: + case OpCode.MIAP1: + { + float distance = this.ReadCvt(); + int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp0.Current.Length) + { + // FreeType Fail label: still sets rp0/rp1. + this.state.Rp0 = pointIndex; + this.state.Rp1 = pointIndex; + break; + } + + // this instruction is used in the CVT to set up twilight points with original values + if (this.zp0.IsTwilight) + { + Vector2 original = this.state.Freedom * distance; + this.zp0.Original[pointIndex].Point = original; + this.zp0.Current[pointIndex].Point = original; + } + + // current position of the point along the projection vector + Vector2 point = this.zp0.GetCurrent(pointIndex); + float currentPos = this.Project(point); + if (opcode == OpCode.MIAP1) + { + // only use the CVT if we are above the cut-in point + if (Math.Abs(distance - currentPos) > this.state.ControlValueCutIn) + { + distance = currentPos; + } + + distance = this.Round(distance); + } + + this.MovePoint(this.zp0, pointIndex, distance - currentPos); + this.state.Rp0 = pointIndex; + this.state.Rp1 = pointIndex; + } + + break; + case OpCode.MDAP0: + case OpCode.MDAP1: + { + // FreeType Ins_MDAP: bounds check before access. + int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp0.Current.Length) + { + break; + } + + Vector2 point = this.zp0.GetCurrent(pointIndex); + float distance = 0.0f; + if (opcode == OpCode.MDAP1) + { + distance = this.Project(point); + distance = this.Round(distance) - distance; + } + + this.MovePoint(this.zp0, pointIndex, distance); + this.state.Rp0 = pointIndex; + this.state.Rp1 = pointIndex; + } + + break; + case OpCode.MSIRP0: + case OpCode.MSIRP1: + { + float targetDistance = this.stack.PopFloat(); + int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp1.Current.Length || + (uint)this.state.Rp0 >= (uint)this.zp0.Current.Length) + { + break; + } + + // if we're operating on the twilight zone, initialize the points + if (this.zp1.IsTwilight) + { + ControlPoint[] zp0Original = this.zp0.Original; + ControlPoint[] zp1Current = this.zp1.Current; + ControlPoint[] zp1Original = this.zp1.Original; + zp1Original[pointIndex].Point = zp0Original[this.state.Rp0].Point + (targetDistance * this.state.Freedom / this.fdotp); + zp1Current[pointIndex].Point = zp1Original[pointIndex].Point; + } + + float currentDistance = this.Project(this.zp1.GetCurrent(pointIndex) - this.zp0.GetCurrent(this.state.Rp0)); + this.MovePoint(this.zp1, pointIndex, targetDistance - currentDistance); + + this.state.Rp1 = this.state.Rp0; + this.state.Rp2 = pointIndex; + if (opcode == OpCode.MSIRP1) + { + this.state.Rp0 = pointIndex; + } + } + + break; + case OpCode.IP: + { + // FreeType Ins_IP: bounds check rp1 first. + if ((uint)this.state.Rp1 >= (uint)this.zp0.Current.Length) + { + // Fail label: drain stack and reset loop. + for (int i = 0; i < this.state.Loop; i++) + { + this.stack.Pop(); + } + + this.state.Loop = 1; + break; + } + + Vector2 originalBase = this.zp0.GetOriginal(this.state.Rp1); + Vector2 currentBase = this.zp0.GetCurrent(this.state.Rp1); + + // FreeType: if rp2 fails, set ranges to 0 but continue. + float originalRange = 0; + float currentRange = 0; + if ((uint)this.state.Rp2 < (uint)this.zp1.Current.Length) + { + originalRange = this.DualProject(this.zp1.GetOriginal(this.state.Rp2) - originalBase); + currentRange = this.Project(this.zp1.GetCurrent(this.state.Rp2) - currentBase); + } + + for (int i = 0; i < this.state.Loop; i++) + { + int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp2.Current.Length) + { + continue; + } + + Vector2 point = this.zp2.GetCurrent(pointIndex); + float currentDistance = this.Project(point - currentBase); + float originalDistance = this.DualProject(this.zp2.GetOriginal(pointIndex) - originalBase); + + float newDistance = 0.0f; + if (originalDistance != 0.0f) + { + // a range of 0.0f is invalid according to the spec (would result in a div by zero) + if (originalRange == 0.0f) + { + newDistance = originalDistance; + } + else + { + newDistance = originalDistance * currentRange / originalRange; + } + } + + this.MovePoint(this.zp2, pointIndex, newDistance - currentDistance); + } + + this.state.Loop = 1; + } + + break; + case OpCode.ALIGNRP: + { + // FreeType Ins_ALIGNRP: bounds check rp0 first. + if ((uint)this.state.Rp0 >= (uint)this.zp0.Current.Length) + { + for (int i = 0; i < this.state.Loop; i++) + { + this.stack.Pop(); + } + + this.state.Loop = 1; + break; + } + + for (int i = 0; i < this.state.Loop; i++) + { + int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp1.Current.Length) + { + continue; + } + + Vector2 p1 = this.zp1.GetCurrent(pointIndex); + Vector2 p2 = this.zp0.GetCurrent(this.state.Rp0); + this.MovePoint(this.zp1, pointIndex, -this.Project(p1 - p2)); + } + + this.state.Loop = 1; + } + + break; + case OpCode.ALIGNPTS: + { + // FreeType Ins_ALIGNPTS: args[1] (top) = p2 in zp0, args[0] (deeper) = p1 in zp1. + int p2 = this.stack.Pop(); + int p1 = this.stack.Pop(); + if ((uint)p1 >= (uint)this.zp1.Current.Length || + (uint)p2 >= (uint)this.zp0.Current.Length) + { + break; + } + + float distance = this.Project(this.zp0.GetCurrent(p2) - this.zp1.GetCurrent(p1)) / 2; + this.MovePoint(this.zp1, p1, distance); + this.MovePoint(this.zp0, p2, -distance); + } + + break; + case OpCode.UTP: + { + int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp0.Current.Length) + { + break; + } + + this.zp0.TouchState[pointIndex] &= ~this.GetTouchState(); + break; + } + + case OpCode.IUP0: + case OpCode.IUP1: + { + // FreeType: IUP returns immediately once both axes have been processed. + if (this.iupXCalled && this.iupYCalled) + { + break; + } + + unsafe + { + // bail if no contours (empty outline) + if (this.contours.Count == 0) + { + break; + } + + fixed (ControlPoint* currentPtr = this.points.Current) + { + fixed (ControlPoint* originalPtr = this.points.Original) + { + // opcode controls whether we care about X or Y direction + // do some pointer trickery so we can operate on the + // points in a direction-agnostic manner + TouchState touchMask; + byte* current; + byte* original; + if (opcode == OpCode.IUP0) + { + this.iupYCalled = true; + touchMask = TouchState.Y; + current = (byte*)¤tPtr->Point.Y; + original = (byte*)&originalPtr->Point.Y; + } + else + { + this.iupXCalled = true; + touchMask = TouchState.X; + current = (byte*)¤tPtr->Point.X; + original = (byte*)&originalPtr->Point.X; + } + + int point = 0; + for (int i = 0; i < this.contours.Count; i++) + { + ushort endPoint = this.contours[i]; + int firstPoint = point; + int firstTouched = -1; + int lastTouched = -1; + + for (; point <= endPoint; point++) + { + // check whether this point has been touched + if ((this.points.TouchState[point] & touchMask) != 0) + { + // if this is the first touched point in the contour, note it and continue + if (firstTouched < 0) + { + firstTouched = point; + lastTouched = point; + continue; + } + + // otherwise, interpolate all untouched points + // between this point and our last touched point + InterpolatePoints(current, original, lastTouched + 1, point - 1, lastTouched, point); + lastTouched = point; + } + } + + // check if we had any touched points at all in this contour + if (firstTouched >= 0) + { + // there are two cases left to handle: + // 1. there was only one touched point in the whole contour, in + // which case we want to shift everything relative to that one + // 2. several touched points, in which case handle the gap from the + // beginning to the first touched point and the gap from the last + // touched point to the end of the contour + if (lastTouched == firstTouched) + { + float delta = *GetPoint(current, lastTouched) - *GetPoint(original, lastTouched); + if (delta != 0.0f) + { + for (int j = firstPoint; j < lastTouched; j++) + { + *GetPoint(current, j) += delta; + } + + for (int j = lastTouched + 1; j <= endPoint; j++) + { + *GetPoint(current, j) += delta; + } + } + } + else + { + InterpolatePoints(current, original, lastTouched + 1, endPoint, lastTouched, firstTouched); + if (firstTouched > 0) + { + InterpolatePoints(current, original, firstPoint, firstTouched - 1, lastTouched, firstTouched); + } + } + } + } + } + } + } + + break; + } + + case OpCode.ISECT: + { + // move point P to the intersection of lines A and B + int ib1 = this.stack.Pop(); + int ib0 = this.stack.Pop(); + int ia1 = this.stack.Pop(); + int ia0 = this.stack.Pop(); + int index = this.stack.Pop(); + if ((uint)ib0 >= (uint)this.zp0.Current.Length || + (uint)ib1 >= (uint)this.zp0.Current.Length || + (uint)ia0 >= (uint)this.zp1.Current.Length || + (uint)ia1 >= (uint)this.zp1.Current.Length || + (uint)index >= (uint)this.zp2.Current.Length) + { + break; + } + + Vector2 b1 = this.zp0.GetCurrent(ib1); + Vector2 b0 = this.zp0.GetCurrent(ib0); + Vector2 a1 = this.zp1.GetCurrent(ia1); + Vector2 a0 = this.zp1.GetCurrent(ia0); + + // calculate intersection using determinants: https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line + Vector2 da = a0 - a1; + Vector2 db = b0 - b1; + float den = (da.X * db.Y) - (da.Y * db.X); + if (Math.Abs(den) <= Epsilon) + { + // parallel lines; spec says to put the point "into the middle of the two lines" + this.zp2.Current[index].Point = (a0 + a1 + b0 + b1) / 4; + } + else + { + float t = (a0.X * a1.Y) - (a0.Y * a1.X); + float u = (b0.X * b1.Y) - (b0.Y * b1.X); + Vector2 p = new((t * db.X) - (da.X * u), (t * db.Y) - (da.Y * u)); + this.zp2.Current[index].Point = p / den; + } + + this.zp2.TouchState[index] = TouchState.Both; + } + + break; + + // ==== STACK MANAGEMENT ==== + case OpCode.DUP: + { + this.stack.Duplicate(); + break; + } + + case OpCode.POP: + { + this.stack.Pop(); + break; + } + + case OpCode.CLEAR: + { + this.stack.Clear(); + break; + } + + case OpCode.SWAP: + { + this.stack.Swap(); + break; + } + + case OpCode.DEPTH: + { + this.stack.Depth(); + break; + } + + case OpCode.CINDEX: + { + this.stack.Copy(); + break; + } + + case OpCode.MINDEX: + { + this.stack.Move(); + break; + } + + case OpCode.ROLL: + { + this.stack.Roll(); + break; + } + + // ==== FLOW CONTROL ==== + case OpCode.IF: + { + // value is false; jump to the next else block or endif marker + // otherwise, we don't have to do anything; we'll keep executing this block + if (!this.stack.PopBool()) + { + int indent = 1; + while (indent > 0) + { + opcode = SkipNext(ref stream); + switch (opcode) + { + case OpCode.IF: + indent++; + break; + case OpCode.EIF: + indent--; + break; + case OpCode.ELSE: + if (indent == 1) + { + indent = 0; + } + + break; + } + } + } + } + + break; + case OpCode.ELSE: + { + // assume we hit the true statement of some previous if block + // if we had hit false, we would have jumped over this + int indent = 1; + while (indent > 0) + { + opcode = SkipNext(ref stream); + switch (opcode) + { + case OpCode.IF: + indent++; + break; + case OpCode.EIF: + indent--; + break; + } + } + } + + break; + case OpCode.EIF: /* nothing to do */ + { + break; + } + + case OpCode.JROT: + case OpCode.JROF: + { + if (this.stack.PopBool() == (opcode == OpCode.JROT)) + { + int offset = this.stack.Pop(); + if (offset < 0 && ++this.negJumpCounter > this.negJumpCounterMax) + { + return; + } + + stream.Jump(offset - 1); + } + else + { + this.stack.Pop(); // ignore the offset + } + } + + break; + case OpCode.JMPR: + { + int offset = this.stack.Pop(); + if (offset < 0 && ++this.negJumpCounter > this.negJumpCounterMax) + { + // FreeType sets Execution_Too_Long error and returns. + return; + } + + stream.Jump(offset - 1); + break; + } + + // ==== LOGICAL OPS ==== + case OpCode.LT: + { + int b = this.stack.Pop(); + int a = this.stack.Pop(); + this.stack.Push(a < b); + } + + break; + case OpCode.LTEQ: + { + int b = this.stack.Pop(); + int a = this.stack.Pop(); + this.stack.Push(a <= b); + } + + break; + case OpCode.GT: + { + int b = this.stack.Pop(); + int a = this.stack.Pop(); + this.stack.Push(a > b); + } + + break; + case OpCode.GTEQ: + { + int b = this.stack.Pop(); + int a = this.stack.Pop(); + this.stack.Push(a >= b); + } + + break; + case OpCode.EQ: + { + int b = this.stack.Pop(); + int a = this.stack.Pop(); + this.stack.Push(a == b); + } + + break; + case OpCode.NEQ: + { + int b = this.stack.Pop(); + int a = this.stack.Pop(); + this.stack.Push(a != b); + } + + break; + case OpCode.AND: + { + bool b = this.stack.PopBool(); + bool a = this.stack.PopBool(); + this.stack.Push(a && b); + } + + break; + case OpCode.OR: + { + bool b = this.stack.PopBool(); + bool a = this.stack.PopBool(); + this.stack.Push(a || b); + } + + break; + case OpCode.NOT: + { + this.stack.Push(!this.stack.PopBool()); + break; + } + + case OpCode.ODD: + { + int value = (int)this.Round(this.stack.PopFloat()); + this.stack.Push(value % 2 != 0); + } + + break; + case OpCode.EVEN: + { + int value = (int)this.Round(this.stack.PopFloat()); + this.stack.Push(value % 2 == 0); + } + + break; + + // ==== ARITHMETIC ==== + case OpCode.ADD: + { + int b = this.stack.Pop(); + int a = this.stack.Pop(); + this.stack.Push(a + b); + } + + break; + case OpCode.SUB: + { + int b = this.stack.Pop(); + int a = this.stack.Pop(); + this.stack.Push(a - b); + } + + break; + case OpCode.DIV: + { + int b = this.stack.Pop(); + int a = this.stack.Pop(); + if (b == 0) + { + // FreeType sets Divide_By_Zero error and returns. + return; + } + + long result = ((long)a << 6) / b; + this.stack.Push((int)result); + } + + break; + case OpCode.MUL: + { + int b = this.stack.Pop(); + int a = this.stack.Pop(); + long result = ((long)a * b) >> 6; + this.stack.Push((int)result); + } + + break; + case OpCode.ABS: + { + this.stack.Push(Math.Abs(this.stack.Pop())); + break; + } + + case OpCode.NEG: + { + this.stack.Push(-this.stack.Pop()); + break; + } + + case OpCode.FLOOR: + { + this.stack.Push(this.stack.Pop() & ~63); + break; + } + + case OpCode.CEILING: + { + this.stack.Push((this.stack.Pop() + 63) & ~63); + break; + } + + case OpCode.MAX: + { + this.stack.Push(Math.Max(this.stack.Pop(), this.stack.Pop())); + break; + } + + case OpCode.MIN: + { + this.stack.Push(Math.Min(this.stack.Pop(), this.stack.Pop())); + break; + } + + // ==== FUNCTIONS ==== + case OpCode.FDEF: + { + if (!allowFunctionDefs || inFunction) + { + return; + } + + this.functions[this.stack.Pop()] = stream.ToMemory(); + while (SkipNext(ref stream) != OpCode.ENDF) + { + } + } + + break; + case OpCode.IDEF: + { + if (!allowFunctionDefs || inFunction) + { + return; + } + + this.instructionDefs[this.stack.Pop()] = stream.ToMemory(); + while (SkipNext(ref stream) != OpCode.ENDF) + { + } + } + + break; + case OpCode.ENDF: + { + if (!inFunction) + { + return; + } + + return; + } + + case OpCode.CALL: + case OpCode.LOOPCALL: + { + this.callStackSize++; + if (this.callStackSize > MaxCallStack) + { + // FreeType sets Stack_Overflow error and returns. + return; + } + + int funcIndex = this.stack.Pop(); + if ((uint)funcIndex >= (uint)this.functions.Length) + { + // FreeType sets Invalid_Reference error and returns. + return; + } + + InstructionStream function = this.functions[funcIndex]; + int count = opcode == OpCode.LOOPCALL ? this.stack.Pop() : 1; + + // FreeType: only LOOPCALL increments the loopcall counter, not CALL. + if (opcode == OpCode.LOOPCALL) + { + this.loopcallCounter += count; + if (this.loopcallCounter > this.loopcallCounterMax) + { + // FreeType sets Execution_Too_Long error and returns. + return; + } + } + + if (count > 0) + { + for (int i = 0; i < count; i++) + { + this.Execute(function.ToStack(), true, false); + } + } + + this.callStackSize--; + } + + break; + + // ==== ROUNDING ==== + // we don't have "engine compensation" so the variants are unnecessary + case OpCode.ROUND0: + case OpCode.ROUND1: + case OpCode.ROUND2: + case OpCode.ROUND3: + { + this.stack.Push(this.Round(this.stack.PopFloat())); + break; + } + + case OpCode.NROUND0: + case OpCode.NROUND1: + case OpCode.NROUND2: + case OpCode.NROUND3: + { + break; + } + + // ==== DELTA EXCEPTIONS ==== + case OpCode.DELTAC1: + case OpCode.DELTAC2: + case OpCode.DELTAC3: + { + int last = this.stack.Pop(); + for (int i = 1; i <= last; i++) + { + int cvtIndex = this.stack.Pop(); + int arg = this.stack.Pop(); + + // upper 4 bits of the 8-bit arg is the relative ppem + // the opcode specifies the base to add to the ppem + int triggerPpem = (arg >> 4) & 0xF; + triggerPpem += (opcode - OpCode.DELTAC1) * 16; + triggerPpem += this.state.DeltaBase; + + // if the current ppem matches the trigger, apply the exception + if (this.ppem == triggerPpem) + { + // the lower 4 bits of the arg is the amount to shift + // it's encoded such that 0 isn't an allowable value (who wants to shift by 0 anyway?) + int amount = (arg & 0xF) - 8; + if (amount >= 0) + { + amount++; + } + + amount *= 1 << (6 - this.state.DeltaShift); + + // update the CVT (FreeType non-pedantic: silently ignore out-of-bounds) + if ((uint)cvtIndex < (uint)this.controlValueTable.Length) + { + this.controlValueTable[cvtIndex] += F26Dot6ToFloat(amount); + } + } + } + } + + break; + case OpCode.DELTAP1: + case OpCode.DELTAP2: + case OpCode.DELTAP3: + { + // SHPIX and DELTAP don't execute unless moving a composite on the + // y axis or moving a previously y touched point. + // https://github.com/freetype/freetype/blob/3ab1875cd22536b3d715b3b104b7fb744b9c25c5/src/truetype/ttinterp.h#L298 + bool postIUP = this.iupXCalled && this.iupYCalled; + bool composite = this.isComposite; + int last = this.stack.Pop(); + for (int i = 1; i <= last; i++) + { + int pointIndex = this.stack.Pop(); + int arg = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp0.Current.Length) + { + continue; + } + + // upper 4 bits of the 8-bit arg is the relative ppem + // the opcode specifies the base to add to the ppem + int triggerPpem = (arg >> 4) & 0xF; + triggerPpem += this.state.DeltaBase; + if (opcode != OpCode.DELTAP1) + { + triggerPpem += (opcode - OpCode.DELTAP2 + 1) * 16; + } + + // if the current ppem matches the trigger, apply the exception + if (this.ppem == triggerPpem) + { + // the lower 4 bits of the arg is the amount to shift + // it's encoded such that 0 isn't an allowable value (who wants to shift by 0 anyway?) + int amount = (arg & 0xF) - 8; + if (amount >= 0) + { + amount++; + } + + amount *= 1 << (6 - this.state.DeltaShift); + + // FreeType Ins_DELTAP: v40 backward compatibility gating. + bool nativeClearType = (this.state.InstructionControl & InstructionControlFlags.NativeClearType) != 0; + if (nativeClearType) + { + this.MovePoint(this.zp0, pointIndex, F26Dot6ToFloat(amount)); + } + else + { + // Compat mode: gate on !postIUP AND (composite+freeY or Y-touched). + TouchState state = this.zp0.TouchState[pointIndex]; + if (!postIUP && + ((composite && this.state.Freedom.Y != 0) || + ((state & TouchState.Y) == TouchState.Y))) + { + this.MovePoint(this.zp0, pointIndex, F26Dot6ToFloat(amount)); + } + } + } + } + } + + break; + + // ==== MISCELLANEOUS ==== + case OpCode.DEBUG: + { + this.stack.Pop(); + break; + } + + case OpCode.GETINFO: + { + // FreeType Ins_GETINFO. + // Report v40 interpreter identity and ClearType capability flags. + int selector = this.stack.Pop(); + int result = 0; + + // Selector bit 0: interpreter version. + if ((selector & 0x1) != 0) + { + result = 40; + } + + // Selector bits 1-2: rotation/stretching — always false in v40. + + // Selector bit 3: variation glyph (FreeType Ins_GETINFO). + // Set result bit 10 when the font is a variable font instance. + if ((selector & 0x8) != 0 && this.normalizedAxisCoordinates is not null) + { + result |= 1 << 10; + } + + // Selector bit 5: grayscale rendering. + // FreeType v40 sets grayscale = FALSE, so this bit is NOT set. + + // Selector bit 6: subpixel hinting is available (v40 default). + if ((selector & 0x40) != 0) + { + result |= 1 << 13; + } + + // Selector bit 10: subpixel positioned. + if ((selector & 0x400) != 0) + { + result |= 1 << 17; + } + + // Selector bit 11: symmetrical smoothing. + if ((selector & 0x800) != 0) + { + result |= 1 << 18; + } + + this.stack.Push(result); + } + + break; + + case OpCode.GETVARIATION: + { + // FreeType Ins_GETVARIATION. + // Push normalized axis coordinates as F2Dot14 integers. + // FreeType stores coords as F16Dot16 and does >> 2 to get F2Dot14. + // We store floats in [-1,1], so multiply by 16384 to get F2Dot14. + if (this.normalizedAxisCoordinates is not null) + { + for (int i = 0; i < this.normalizedAxisCoordinates.Length; i++) + { + this.stack.Push((int)Math.Round(this.normalizedAxisCoordinates[i] * 16384)); + } + } + + break; + } + + case OpCode.GETDATA: + { + // FreeType Ins_GETDATA. + // Always returns 17. + this.stack.Push(17); + break; + } + + default: + { + if (opcode >= OpCode.MIRP) + { + this.MoveIndirectRelative(opcode - OpCode.MIRP); + } + else if (opcode >= OpCode.MDRP) + { + this.MoveDirectRelative(opcode - OpCode.MDRP); + } + else + { + // check if this is a runtime-defined opcode + int index = (int)opcode; + if (index > this.instructionDefs.Length || !this.instructionDefs[index].IsValid) + { + // FreeType sets Invalid_Opcode error and terminates execution. + return; + } + + this.callStackSize++; + if (this.callStackSize > MaxCallStack) + { + return; + } + + this.Execute(this.instructionDefs[index].ToStack(), true, false); + this.callStackSize--; + } + + break; + } + } + +#if HINTING_TRACE + this.TracePostInstruction(opcode, pops, pushes, preStackCount); +#endif + } + } + + /// + /// Pops a CVT index from the stack and returns the corresponding value. + /// Returns 0 for out-of-bounds indices (FreeType non-pedantic behavior). + /// + private float ReadCvt() + { + int loc = this.stack.Pop(); + if ((uint)loc >= (uint)this.controlValueTable.Length) + { + return 0; + } + + return this.controlValueTable[loc]; + } + + /// + /// Recomputes the cached dot product of the freedom and projection vectors. + /// Must be called whenever either vector changes. + /// + private void OnVectorsUpdated() + { + this.fdotp = Vector2.Dot(this.state.Freedom, this.state.Projection); + if (Math.Abs(this.fdotp) < Epsilon) + { + this.fdotp = 1.0f; + } + } + + /// + /// Sets the freedom vector to one of the coordinate axes (SFVTCA). + /// + /// 0 for the Y-axis, 1 for the X-axis. + private void SetFreedomVectorToAxis(int axis) + { + this.state.Freedom = axis == 0 ? Vector2.UnitY : Vector2.UnitX; + this.OnVectorsUpdated(); + } + + /// + /// Sets the projection and dual-projection vectors to one of the coordinate axes (SPVTCA). + /// + /// 0 for the Y-axis, 1 for the X-axis. + private void SetProjectionVectorToAxis(int axis) + { + this.state.Projection = axis == 0 ? Vector2.UnitY : Vector2.UnitX; + this.state.DualProjection = this.state.Projection; + + this.OnVectorsUpdated(); + } + + /// + /// Sets a projection or freedom vector to the direction of a line between two points + /// (SPVTL/SFVTL/SDPVTL). The mode's low bit selects the perpendicular direction. + /// + /// 0=SPVTL0, 1=SPVTL1, 2=SFVTL0, 3=SFVTL1. + /// When , also sets the dual-projection vector from original coordinates. + private void SetVectorToLine(int mode, bool dual) + { + int index1 = this.stack.Pop(); + int index2 = this.stack.Pop(); + Vector2 p1 = this.zp2.GetCurrent(index1); + Vector2 p2 = this.zp1.GetCurrent(index2); + + Vector2 line = p2 - p1; + if (line.LengthSquared() == 0) + { + // invalid; just set to whatever + if (mode >= 2) + { + this.state.Freedom = Vector2.UnitX; + } + else + { + this.state.Projection = Vector2.UnitX; + this.state.DualProjection = Vector2.UnitX; + } + } + else + { + // if mode is 1 or 3, we want a perpendicular vector + if ((mode & 0x1) != 0) + { + line = new Vector2(-line.Y, line.X); + } + + line = Vector2.Normalize(line); + + if (mode >= 2) + { + this.state.Freedom = line; + } + else + { + this.state.Projection = line; + this.state.DualProjection = line; + } + } + + // set the dual projection vector using original points + if (dual) + { + p1 = this.zp2.GetOriginal(index1); + p2 = this.zp1.GetOriginal(index2); + line = p2 - p1; + + if (line.LengthSquared() == 0) + { + this.state.DualProjection = Vector2.UnitX; + } + else + { + if ((mode & 0x1) != 0) + { + line = new Vector2(-line.Y, line.X); + } + + this.state.DualProjection = Vector2.Normalize(line); + } + } + + this.OnVectorsUpdated(); + } + + /// + /// Pops a zone index from the stack and returns the corresponding zone. + /// Returns for invalid indices (FreeType non-pedantic: silently ignores). + /// + private bool TryGetZoneFromStack(out Zone zone) + { + int zoneIndex = this.stack.Pop(); + switch (zoneIndex) + { + case 0: + zone = this.twilight; + return true; + case 1: + zone = this.points; + return true; + default: + // FreeType non-pedantic: silently ignore invalid zone pointers. + zone = default; + return false; + } + } + + /// + /// Configures super-rounding parameters from a packed mode byte (SROUND/S45ROUND). + /// Bits 7-6 select the period multiplier, bits 5-4 the phase, and bits 3-0 the threshold. + /// + /// Base period: 1.0 for SROUND, sqrt(2)/2 for S45ROUND. + private void SetSuperRound(float period) + { + int mode = this.stack.Pop(); + this.roundPeriod = (mode & 0xC0) switch + { + 0 => period / 2, + 0x40 => period, + 0x80 => period * 2, + _ => period * 2, // Reserved; FreeType treats as period * 2. + }; + + // bits 5-4 are the phase + switch (mode & 0x30) + { + case 0: + this.roundPhase = 0; + break; + case 0x10: + this.roundPhase = this.roundPeriod / 4; + break; + case 0x20: + this.roundPhase = this.roundPeriod / 2; + break; + case 0x30: + this.roundPhase = this.roundPeriod * 3 / 4; + break; + } + + // bits 3-0 are the threshold + if ((mode & 0xF) == 0) + { + this.roundThreshold = this.roundPeriod - 1; + } + else + { + this.roundThreshold = ((mode & 0xF) - 4) * this.roundPeriod / 8; + } + } + + /// + /// Move Indirect Relative Point (MIRP). Moves a point so that its distance from RP0 + /// matches a CVT value, subject to rounding, cut-in, and minimum distance constraints + /// controlled by the instruction's flag bits. + /// + /// MIRP flag bits: bit 4=set RP0, bit 3=minimum distance, bit 2=round, bits 1-0=engine compensation. + private void MoveIndirectRelative(int flags) + { + float cvt = this.ReadCvt(); + int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp1.Current.Length || + (uint)this.state.Rp0 >= (uint)this.zp0.Current.Length) + { + // FreeType Fail label: still sets reference points. + this.state.Rp1 = this.state.Rp0; + this.state.Rp2 = pointIndex; + if ((flags & 0x10) != 0) + { + this.state.Rp0 = pointIndex; + } + + return; + } + + if (Math.Abs(cvt - this.state.SingleWidthValue) < this.state.SingleWidthCutIn) + { + if (cvt >= 0) + { + cvt = this.state.SingleWidthValue; + } + else + { + cvt = -this.state.SingleWidthValue; + } + } + + // if we're looking at the twilight zone we need to prepare the points there + Vector2 originalReference = this.zp0.GetOriginal(this.state.Rp0); + if (this.zp1.IsTwilight) + { + Vector2 initialValue = originalReference + (this.state.Freedom * cvt); + this.zp1.Original[pointIndex].Point = initialValue; + this.zp1.Current[pointIndex].Point = initialValue; + } + + Vector2 point = this.zp1.GetCurrent(pointIndex); + float originalDistance = this.DualProject(this.zp1.GetOriginal(pointIndex) - originalReference); + float currentDistance = this.Project(point - this.zp0.GetCurrent(this.state.Rp0)); + + if (this.state.AutoFlip && Math.Sign(originalDistance) != Math.Sign(cvt)) + { + cvt = -cvt; + } + + // if bit 2 is set, round the distance and look at the cut-in value + float distance = cvt; + if ((flags & 0x4) != 0) + { + // only perform cut-in tests when both points are in the same zone + if (this.zp0.IsTwilight == this.zp1.IsTwilight && Math.Abs(cvt - originalDistance) > this.state.ControlValueCutIn) + { + cvt = originalDistance; + } + + distance = this.Round(cvt); + } + + // if bit 3 is set, constrain to the minimum distance + if ((flags & 0x8) != 0) + { + if (originalDistance >= 0) + { + distance = Math.Max(distance, this.state.MinDistance); + } + else + { + distance = Math.Min(distance, -this.state.MinDistance); + } + } + + // move the point + this.MovePoint(this.zp1, pointIndex, distance - currentDistance); + this.state.Rp1 = this.state.Rp0; + this.state.Rp2 = pointIndex; + if ((flags & 0x10) != 0) + { + this.state.Rp0 = pointIndex; + } + } + + /// + /// Move Direct Relative Point (MDRP). Moves a point so that its distance from RP0 + /// matches the original outline distance, subject to rounding and minimum distance + /// constraints controlled by the instruction's flag bits. + /// + /// MDRP flag bits: bit 4=set RP0, bit 3=minimum distance, bit 2=round, bits 1-0=engine compensation. + private void MoveDirectRelative(int flags) + { + int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp1.Current.Length || + (uint)this.state.Rp0 >= (uint)this.zp0.Current.Length) + { + // FreeType Fail label: still sets reference points. + this.state.Rp1 = this.state.Rp0; + this.state.Rp2 = pointIndex; + if ((flags & 0x10) != 0) + { + this.state.Rp0 = pointIndex; + } + + return; + } + + Vector2 p1 = this.zp0.GetOriginal(this.state.Rp0); + Vector2 p2 = this.zp1.GetOriginal(pointIndex); + float originalDistance = this.DualProject(p2 - p1); + + // single width cut-in test + if (Math.Abs(originalDistance - this.state.SingleWidthValue) < this.state.SingleWidthCutIn) + { + if (originalDistance >= 0) + { + originalDistance = this.state.SingleWidthValue; + } + else + { + originalDistance = -this.state.SingleWidthValue; + } + } + + // if bit 2 is set, perform rounding + float distance = originalDistance; + if ((flags & 0x4) != 0) + { + distance = this.Round(distance); + } + + // if bit 3 is set, constrain to the minimum distance + if ((flags & 0x8) != 0) + { + if (originalDistance >= 0) + { + distance = Math.Max(distance, this.state.MinDistance); + } + else + { + distance = Math.Min(distance, -this.state.MinDistance); + } + } + + // move the point + originalDistance = this.Project(this.zp1.GetCurrent(pointIndex) - this.zp0.GetCurrent(this.state.Rp0)); + this.MovePoint(this.zp1, pointIndex, distance - originalDistance); + this.state.Rp1 = this.state.Rp0; + this.state.Rp2 = pointIndex; + if ((flags & 0x10) != 0) + { + this.state.Rp0 = pointIndex; + } + } + + /// + /// Computes the displacement vector for SHP/SHC/SHZ instructions by projecting the + /// movement of the reference point (RP1 or RP2 depending on mode) from its original + /// to its current position onto the freedom vector. + /// + /// Opcode value; bit 0 selects RP1 in ZP0 (1) or RP2 in ZP1 (0). + /// Receives the reference zone. + /// Receives the reference point index. + /// Receives the computed displacement vector. + /// if the reference point is valid; otherwise . + private bool TryComputeDisplacement(int mode, out Zone zone, out int point, out Vector2 displacement) + { + if ((mode & 1) == 0) + { + zone = this.zp1; + point = this.state.Rp2; + } + else + { + zone = this.zp0; + point = this.state.Rp1; + } + + if ((uint)point >= (uint)zone.Current.Length) + { + displacement = default; + return false; + } + + float distance = this.Project(zone.GetCurrent(point) - zone.GetOriginal(point)); + displacement = distance * this.state.Freedom / this.fdotp; + return true; + } + + /// + /// Returns the touch state flags corresponding to the current freedom vector axes. + /// Used by UTP to selectively clear touch bits. + /// + private TouchState GetTouchState() + { + TouchState touch = TouchState.None; + if (this.state.Freedom.X != 0) + { + touch = TouchState.X; + } + + if (this.state.Freedom.Y != 0) + { + touch |= TouchState.Y; + } + + return touch; + } + + /// + /// Moves a point along the freedom vector by the given distance, applying v40 + /// backward compatibility restrictions: X movement is always blocked in compat mode, + /// Y movement is blocked only after both IUP passes have completed (post-IUP). + /// Corresponds to FreeType's Direct_Move / func_move. + /// + private void MovePoint(Zone zone, int index, float distance) + { + // X is always blocked in backward compat mode. + // Y is blocked only when backward_compatibility == 0x7 (post-IUP). + bool nativeClearType = (this.state.InstructionControl & InstructionControlFlags.NativeClearType) != 0; + bool postIUP = this.iupXCalled && this.iupYCalled; + + if (this.state.Freedom.X != 0) + { + if (nativeClearType) + { + float dx = distance * this.state.Freedom.X / this.fdotp; + zone.Current[index].Point.X += dx; + } + + zone.TouchState[index] |= TouchState.X; + } + + if (this.state.Freedom.Y != 0) + { + if (nativeClearType || !postIUP) + { + float dy = distance * this.state.Freedom.Y / this.fdotp; + zone.Current[index].Point.Y += dy; + } + + zone.TouchState[index] |= TouchState.Y; + } + +#if HINTING_TRACE + this.traceLog.AppendLine(System.FormattableString.Invariant($" -> pt[{index}] = ({zone.Current[index].Point.X:F2}, {zone.Current[index].Point.Y:F2}) dist={distance:F2}")); +#endif + } + + /// + /// Moves a ZP2 point by explicit (dx, dy) deltas with the same v40 backward + /// compatibility restrictions as . Used by SHP, SHC, SHZ, + /// and SHPIX where the displacement is pre-computed rather than derived from a scalar distance. + /// Corresponds to FreeType's Move_Zp2_Point. + /// + private void MoveZp2Point(Zone zone, int index, float dx, float dy, bool touch) + { + // X is always blocked in compat mode. + // Y is blocked only at backward_compatibility == 0x7 (post-IUP). + bool nativeClearType = (this.state.InstructionControl & InstructionControlFlags.NativeClearType) != 0; + bool postIUP = this.iupXCalled && this.iupYCalled; + + if (this.state.Freedom.X != 0) + { + if (nativeClearType) + { + zone.Current[index].Point.X += dx; + } + + if (touch) + { + zone.TouchState[index] |= TouchState.X; + } + } + + if (this.state.Freedom.Y != 0) + { + if (nativeClearType || !postIUP) + { + zone.Current[index].Point.Y += dy; + } + + if (touch) + { + zone.TouchState[index] |= TouchState.Y; + } + } + +#if HINTING_TRACE + this.traceLog.AppendLine(System.FormattableString.Invariant($" -> zp2[{index}] = ({zone.Current[index].Point.X:F2}, {zone.Current[index].Point.Y:F2}) dx={dx:F2} dy={dy:F2}")); +#endif + } + + /// + /// Rounds a distance value according to the current round state. + /// FreeType v40 uses zero engine compensation for all modes. + /// + private float Round(float value) + { + switch (this.state.RoundState) + { + case RoundMode.Off: + // FreeType's Round_None with compensation = 0. + return value; + + case RoundMode.ToGrid: + { + // Round_To_Grid with compensation = 0. + if (value >= 0F) + { + float val = (float)Math.Floor(value + 0.5F); + if (val < 0F) + { + val = 0F; + } + + return val; + } + else + { + float val = -(float)Math.Floor(-value + 0.5F); + if (val > 0F) + { + val = 0F; + } + + return val; + } + } + + case RoundMode.ToHalfGrid: + { + // Round_To_Half_Grid with compensation = 0. + if (value >= 0F) + { + float val = (float)Math.Floor(value) + 0.5F; + if (val < 0F) + { + val = 0.5F; + } + + return val; + } + else + { + float val = -((float)Math.Floor(-value) + 0.5F); + if (val > 0F) + { + val = -0.5F; + } + + return val; + } + } + + case RoundMode.DownToGrid: + { + // Round_Down_To_Grid with compensation = 0. + if (value >= 0F) + { + float val = (float)Math.Floor(value); + if (val < 0F) + { + val = 0F; + } + + return val; + } + else + { + float val = -(float)Math.Floor(-value); + if (val > 0F) + { + val = 0F; + } + + return val; + } + } + + case RoundMode.UpToGrid: + { + // Round_Up_To_Grid with compensation = 0. + if (value >= 0F) + { + float val = (float)Math.Ceiling(value); + if (val < 0F) + { + val = 0F; + } + + return val; + } + else + { + float val = -(float)Math.Ceiling(-value); + if (val > 0F) + { + val = 0F; + } + + return val; + } + } + + case RoundMode.ToDoubleGrid: + { + // Round_To_Double_Grid: grid step is 0.5 pixels. + const float step = 0.5F; + + if (value >= 0F) + { + float val = step * (float)Math.Floor((value / step) + 0.5F); + if (val < 0F) + { + val = 0F; + } + + return val; + } + else + { + float val = -step * (float)Math.Floor((-value / step) + 0.5F); + if (val > 0F) + { + val = 0F; + } + + return val; + } + } + + case RoundMode.Super: + case RoundMode.Super45: + { + // Round_Super / Round_Super_45 with compensation = 0. + float period = this.roundPeriod; + float phase = this.roundPhase; + float threshold = this.roundThreshold; + + if (value >= 0F) + { + float val = value - phase + threshold; + val = (float)Math.Floor(val / period) * period; + val += phase; + + if (val < 0F) + { + val = phase; + } + + return val; + } + else + { + float val = -value - phase + threshold; + val = (float)Math.Floor(val / period) * period; + val = -val - phase; + + if (val > 0F) + { + val = -phase; + } + + return val; + } + } + + default: + return value; + } + } + + /// Projects a point difference onto the projection vector. + private float Project(Vector2 point) => Vector2.Dot(point, this.state.Projection); + + /// Projects a point difference onto the dual-projection vector (used for original coordinates). + private float DualProject(Vector2 point) => Vector2.Dot(point, this.state.DualProjection); + + /// + /// Reads and skips the next instruction in the stream, advancing past any inline + /// data bytes for push instructions. Used by FDEF/IDEF to scan for ENDF and by + /// IF/ELSE to skip over conditional blocks. + /// + private static OpCode SkipNext(ref StackInstructionStream stream) + { + OpCode opcode = stream.NextOpCode(); + switch (opcode) + { + case OpCode.NPUSHB: + case OpCode.PUSHB1: + case OpCode.PUSHB2: + case OpCode.PUSHB3: + case OpCode.PUSHB4: + case OpCode.PUSHB5: + case OpCode.PUSHB6: + case OpCode.PUSHB7: + case OpCode.PUSHB8: + { + int count = opcode == OpCode.NPUSHB ? stream.NextByte() : opcode - OpCode.PUSHB1 + 1; + stream.Skip(count); + } + + break; + case OpCode.NPUSHW: + case OpCode.PUSHW1: + case OpCode.PUSHW2: + case OpCode.PUSHW3: + case OpCode.PUSHW4: + case OpCode.PUSHW5: + case OpCode.PUSHW6: + case OpCode.PUSHW7: + case OpCode.PUSHW8: + { + int count = opcode == OpCode.NPUSHW ? stream.NextByte() : opcode - OpCode.PUSHW1 + 1; + stream.SkipWord(count); + } + + break; + } + + return opcode; + } + + /// + /// Interpolates untouched points between two reference points, preserving + /// their relative positions in the original outline. Used by IUP. + /// Operates on raw byte pointers to support direction-agnostic X/Y processing. + /// + private static unsafe void InterpolatePoints(byte* current, byte* original, int start, int end, int ref1, int ref2) + { + if (start > end) + { + return; + } + + // figure out how much the two reference points + // have been shifted from their original positions + float delta1, delta2; + float lower = *GetPoint(original, ref1); + float upper = *GetPoint(original, ref2); + if (lower > upper) + { + (upper, lower) = (lower, upper); + + delta1 = *GetPoint(current, ref2) - lower; + delta2 = *GetPoint(current, ref1) - upper; + } + else + { + delta1 = *GetPoint(current, ref1) - lower; + delta2 = *GetPoint(current, ref2) - upper; + } + + float lowerCurrent = delta1 + lower; + float upperCurrent = delta2 + upper; + float scale = (upperCurrent - lowerCurrent) / (upper - lower); + + for (int i = start; i <= end; i++) + { + // three cases: if it's to the left of the lower reference point or to + // the right of the upper reference point, do a shift based on that ref point. + // otherwise, interpolate between the two of them + float pos = *GetPoint(original, i); + if (pos <= lower) + { + pos += delta1; + } + else if (pos >= upper) + { + pos += delta2; + } + else + { + pos = lowerCurrent + ((pos - lower) * scale); + } + + *GetPoint(current, i) = pos; + } + } + + // Fixed-point conversion helpers. + // F2Dot14: 2-bit integer + 14-bit fraction, range [-2, ~2). Used for unit vectors. + // F26Dot6: 26-bit integer + 6-bit fraction. The native format for point coordinates + // in the TrueType interpreter. Our implementation uses float throughout but converts + // at the stack boundary to maintain compatibility with instruction semantics. + private static float F2Dot14ToFloat(int value) => (short)value / 16384.0f; + + private static int FloatToF2Dot14(float value) => (int)(uint)(short)Math.Round(value * 16384.0f); + + private static float F26Dot6ToFloat(int value) => value / 64.0f; + + private static int FloatToF26Dot6(float value) => (int)Math.Round(value * 64.0f); + + private static unsafe float* GetPoint(byte* data, int index) => (float*)(data + (sizeof(ControlPoint) * index)); + +#if HINTING_TRACE + private void TracePreInstruction(OpCode opcode, int pops) + { + System.Text.StringBuilder sb = this.traceLog; + sb.Append(System.FormattableString.Invariant($"[{this.insCounter}] {opcode} (stk={this.stack.Count})")); + + // Show the top stack values that this instruction will consume. + int available = Math.Min(pops, this.stack.Count); + if (available > 0) + { + sb.Append(" args=["); + for (int i = available - 1; i >= 0; i--) + { + if (i < available - 1) + { + sb.Append(", "); + } + + sb.Append(this.stack.Peek(i)); + } + + sb.Append(']'); + } + + sb.AppendLine(); + } + + private void TracePostInstruction(OpCode opcode, int pops, int pushes, int preStackCount) + { + int postStackCount = this.stack.Count; + int expectedDelta = pushes - pops; + int actualDelta = postStackCount - preStackCount; + + // Skip variable-pop/push instructions where PopPushCount is not authoritative. + bool variablePop = opcode is + OpCode.NPUSHB or OpCode.NPUSHW or + OpCode.PUSHB1 or OpCode.PUSHB2 or OpCode.PUSHB3 or OpCode.PUSHB4 or + OpCode.PUSHB5 or OpCode.PUSHB6 or OpCode.PUSHB7 or OpCode.PUSHB8 or + OpCode.PUSHW1 or OpCode.PUSHW2 or OpCode.PUSHW3 or OpCode.PUSHW4 or + OpCode.PUSHW5 or OpCode.PUSHW6 or OpCode.PUSHW7 or OpCode.PUSHW8 or + OpCode.SHP0 or OpCode.SHP1 or + OpCode.FLIPRGON or OpCode.FLIPRGOFF or + OpCode.DELTAP1 or OpCode.DELTAP2 or OpCode.DELTAP3 or + OpCode.DELTAC1 or OpCode.DELTAC2 or OpCode.DELTAC3 or + OpCode.LOOPCALL or OpCode.CALL or + OpCode.FDEF or OpCode.IDEF or + OpCode.GETVARIATION or + OpCode.ENDF or OpCode.AA; + + if (!variablePop && actualDelta != expectedDelta) + { + this.traceLog.AppendLine( + System.FormattableString.Invariant( + $" *** STACK IMBALANCE: expected delta={expectedDelta} (pop={pops} push={pushes}), actual delta={actualDelta} (pre={preStackCount} post={postStackCount})")); + } + } + + /// + /// Gets the accumulated trace log for the most recent glyph hinting operation. + /// Only available when compiled with the HINTING_TRACE constant. + /// + internal string GetTraceLog() => this.traceLog.ToString(); +#endif + +#pragma warning disable SA1201 // Elements should appear in the correct order + /// + /// Specifies the rounding mode used by the TrueType interpreter. + /// + private enum RoundMode +#pragma warning restore SA1201 // Elements should appear in the correct order + { + /// + /// Round to the nearest half-grid line. + /// + ToHalfGrid, + + /// + /// Round to the nearest grid line. + /// + ToGrid, + + /// + /// Round to the nearest double-grid line. + /// + ToDoubleGrid, + + /// + /// Round down to the nearest grid line. + /// + DownToGrid, + + /// + /// Round up to the nearest grid line. + /// + UpToGrid, + + /// + /// No rounding. + /// + Off, + + /// + /// Super-rounding with a period of 1.0. + /// + Super, + + /// + /// Super-rounding with a period of sqrt(2)/2. + /// + Super45 + } + + /// + /// Flags controlling instruction execution behavior, set by the INSTCTRL instruction. + /// + [Flags] + private enum InstructionControlFlags + { + /// + /// No special instruction control. + /// + None, + + /// + /// Inhibit grid fitting (disables hinting). + /// + InhibitGridFitting = 0x1, + + /// + /// Use the default graphics state instead of the state saved by the prep program. + /// + UseDefaultGraphicsState = 0x2, + + /// + /// Native ClearType mode is active. + /// + NativeClearType = 0x4 + } + + /// + /// Tracks which axes a point has been touched (moved) along by hinting instructions. + /// Used by IUP (Interpolate Untouched Points) to determine which points need interpolation. + /// + [Flags] + private enum TouchState + { + /// + /// The point has not been touched. + /// + None = 0, + + /// + /// The point has been touched along the X axis. + /// + X = 0x1, + + /// + /// The point has been touched along the Y axis. + /// + Y = 0x2, + + /// + /// The point has been touched along both axes. + /// + Both = X | Y + } + + /// + /// An immutable snapshot of an instruction stream position, used to store function + /// and instruction definitions (FDEF/IDEF) for later execution via CALL/LOOPCALL. + /// + private readonly struct InstructionStream + { + private readonly ReadOnlyMemory instructions; + private readonly int ip; + + /// + /// Initializes a new instance of the struct. + /// + /// The instruction bytecode buffer. + /// The byte offset into the buffer. + public InstructionStream(ReadOnlyMemory instructions, int offset) + { + this.instructions = instructions; + this.ip = offset; + } + + /// + /// Gets a value indicating whether this stream references a valid instruction buffer. + /// + public bool IsValid => !this.instructions.IsEmpty; + + /// + /// Creates a mutable positioned at this stream's offset. + /// + /// A new . + public StackInstructionStream ToStack() => new(this.instructions, this.ip); + } + + /// + /// A mutable, stack-allocated instruction stream that reads TrueType bytecode + /// sequentially and supports forward/backward jumps. + /// + private ref struct StackInstructionStream + { + private readonly ReadOnlyMemory origin; + private readonly ReadOnlySpan instructions; + private int ip; + + /// + /// Initializes a new instance of the struct. + /// + /// The instruction bytecode buffer. + /// The byte offset to start reading from. + public StackInstructionStream(ReadOnlyMemory instructions, int offset) + { + this.origin = instructions; + this.instructions = instructions.Span; + this.ip = offset; + } + + /// + /// Gets a value indicating whether this stream references a valid instruction buffer. + /// + public readonly bool IsValid => !this.instructions.IsEmpty; + + /// + /// Gets a value indicating whether the instruction pointer has reached the end of the buffer. + /// + public readonly bool Done => this.ip >= this.instructions.Length; + + /// + /// Reads the next byte from the stream and advances the instruction pointer. + /// + /// The byte value. + public int NextByte() + { + ReadOnlySpan span = this.instructions; + int offset = this.ip; + if ((uint)offset >= (uint)span.Length) + { + ThrowEndOfInstructions(); + } + + byte b = span[offset]; + this.ip++; + return b; + } + + /// + /// Skips the specified number of bytes in the stream. + /// + /// The number of bytes to skip. + public void Skip(int count) + { + this.ip += count; + if ((uint)this.ip >= (uint)this.instructions.Length) + { + ThrowEndOfInstructions(); + } + } + + /// + /// Reads the next byte as an . + /// + /// The opcode. + public OpCode NextOpCode() => (OpCode)this.NextByte(); + + /// + /// Reads the next two bytes as a signed 16-bit word (big-endian). + /// + /// The signed word value. + public int NextWord() => (short)(ushort)((this.NextByte() << 8) | this.NextByte()); + + /// + /// Skips the specified number of 16-bit words in the stream. + /// + /// The number of words to skip. + public void SkipWord(int count) => this.Skip(count * 2); + + /// + /// Moves the instruction pointer by the specified byte offset (can be negative for backward jumps). + /// + /// The byte offset to jump. + public void Jump(int offset) => this.ip += offset; + + /// + /// Creates an immutable snapshot at the current position. + /// + /// A new . + public readonly InstructionStream ToMemory() => new(this.origin, this.ip); + + private static void ThrowEndOfInstructions() => throw new FontException("no more instructions"); + } + + /// + /// Holds the TrueType graphics state registers used during instruction execution. + /// This includes vector directions, rounding settings, reference points, and control flags. + /// + private struct GraphicsState + { + /// The freedom vector direction. + public Vector2 Freedom; + + /// The dual projection vector, used for original outline measurements. + public Vector2 DualProjection; + + /// The projection vector direction. + public Vector2 Projection; + + /// The instruction control flags set by the INSTCTRL instruction. + public InstructionControlFlags InstructionControl; + + /// The current rounding mode. + public RoundMode RoundState; + + /// The minimum distance value (in pixels, F26Dot6). + public float MinDistance; + + /// The control value cut-in threshold. + public float ControlValueCutIn; + + /// The single width cut-in threshold. + public float SingleWidthCutIn; + + /// The single width value. + public float SingleWidthValue; + + /// The delta base value for DELTAP/DELTAC instructions. + public int DeltaBase; + + /// The delta shift value for DELTAP/DELTAC instructions. + public int DeltaShift; + + /// The loop variable controlling repeated instruction execution. + public int Loop; + + /// Reference point 0. + public int Rp0; + + /// Reference point 1. + public int Rp1; + + /// Reference point 2. + public int Rp2; + + /// Whether auto-flip is enabled for MIAP and MIRP instructions. + public bool AutoFlip; + + /// + /// Resets all graphics state fields to their default values. + /// + public void Reset() + { + this.Freedom = Vector2.UnitX; + this.Projection = Vector2.UnitX; + this.DualProjection = Vector2.UnitX; + this.InstructionControl = InstructionControlFlags.None; + this.RoundState = RoundMode.ToGrid; + this.MinDistance = 1.0f; + this.ControlValueCutIn = 17.0f / 16.0f; + this.SingleWidthCutIn = 0.0f; + this.SingleWidthValue = 0.0f; + this.DeltaBase = 9; + this.DeltaShift = 3; + this.Loop = 1; + this.Rp0 = this.Rp1 = this.Rp2 = 0; + this.AutoFlip = true; + } + } + + /// + /// Represents a point zone in the TrueType interpreter. There are two zones: + /// the glyph zone (containing the glyph's outline points) and the twilight zone + /// (containing points created by instructions for reference purposes). + /// + private struct Zone + { + /// The current (hinted) control points. + public ControlPoint[] Current; + + /// The original (unhinted) control points. + public ControlPoint[] Original; + + /// Per-point touch state tracking for IUP interpolation. + public TouchState[] TouchState; + + /// Whether this is the twilight zone. + public bool IsTwilight; + + /// + /// Initializes a new instance of the struct for the twilight zone. + /// + /// The maximum number of twilight points. + /// Whether this is the twilight zone. + public Zone(int maxTwilightPoints, bool isTwilight) + { + this.IsTwilight = isTwilight; + this.Current = new ControlPoint[maxTwilightPoints]; + this.Original = new ControlPoint[maxTwilightPoints]; + this.TouchState = new TouchState[maxTwilightPoints]; + } + + /// + /// Initializes a new instance of the struct for the glyph zone, + /// copying the control points to create an original (unhinted) backup. + /// + /// The glyph's control points (used as current points; copied for originals). + /// Whether this is the twilight zone. + public Zone(ControlPoint[] controlPoints, bool isTwilight) + { + this.IsTwilight = isTwilight; + this.Current = controlPoints; + + ControlPoint[] original = new ControlPoint[controlPoints.Length]; + controlPoints.AsSpan().CopyTo(original); + this.Original = original; + this.TouchState = new TouchState[controlPoints.Length]; + } + + /// + /// Gets the current (hinted) position of the point at the specified index. + /// + /// The point index. + /// The current position. + public readonly Vector2 GetCurrent(int index) => this.Current[index].Point; + + /// + /// Gets the original (unhinted) position of the point at the specified index. + /// + /// The point index. + /// The original position. + public readonly Vector2 GetOriginal(int index) => this.Original[index].Point; + } + + /// + /// A fixed-capacity integer stack used by the TrueType bytecode interpreter. + /// Values are stored as 32-bit integers; F26Dot6 and F2Dot14 conversions are handled at push/pop time. + /// + private class ExecutionStack + { + private readonly int[] s; + + /// + /// Initializes a new instance of the class. + /// + /// The maximum stack depth. + public ExecutionStack(int maxStack) => this.s = new int[maxStack]; + + /// + /// Gets the current number of elements on the stack. + /// + public int Count { get; private set; } + + /// + /// Gets the maximum capacity of the stack. + /// + public int Capacity => this.s.Length; + + /// + /// Peeks at the top element without removing it. + /// + /// The top element value. + public int Peek() => this.Peek(0); + + /// + /// Pops the top element and returns it as a boolean (non-zero is ). + /// + /// The boolean value. + public bool PopBool() => this.Pop() != 0; + + /// + /// Pops the top element and converts it from F26Dot6 to a float. + /// + /// The float value. + public float PopFloat() => F26Dot6ToFloat(this.Pop()); + + /// + /// Pushes a boolean value onto the stack (1 for , 0 for ). + /// + /// The boolean value to push. + public void Push(bool value) => this.Push(value ? 1 : 0); + + /// + /// Pushes a float value onto the stack, converting it to F26Dot6 format. + /// + /// The float value to push. + public void Push(float value) => this.Push(FloatToF26Dot6(value)); + + /// + /// Clears all elements from the stack. + /// + public void Clear() => this.Count = 0; + + /// + /// Pushes the current stack depth onto the stack. + /// + public void Depth() => this.Push(this.Count); + + /// + /// Duplicates the top element on the stack. + /// + public void Duplicate() => this.Push(this.Peek()); + + /// + /// Copies the element at the index specified by the top stack value. + /// + public void Copy() => this.Copy(this.Pop() - 1); + + /// + /// Copies the element at the specified index (from top) and pushes it. + /// + /// The zero-based index from the top of the stack. + public void Copy(int index) => this.Push(this.Peek(index)); + + /// + /// Moves the element at the index specified by the top stack value to the top. + /// + public void Move() => this.Move(this.Pop() - 1); + + /// + /// Rolls the top three elements (equivalent to Move(2)). + /// + public void Roll() => this.Move(2); + + /// + /// Moves the element at the specified index to the top of the stack, + /// shifting elements above it down by one position. + /// + /// The zero-based index from the top of the stack. + public void Move(int index) + { + int c = this.Count; + int[] a = this.s; + int val = this.Peek(index); + for (int i = c - index - 1; i < c - 1; i++) + { + a[i] = a[i + 1]; + } + + a[c - 1] = val; + } + + /// + /// Swaps the top two elements on the stack. + /// + public void Swap() + { + int c = this.Count; + if (c < 2) + { + ThrowStackOverflow(); + } + + int[] a = this.s; + (a[c - 2], a[c - 1]) = (a[c - 1], a[c - 2]); + } + + /// + /// Pushes an integer value onto the stack. + /// + /// The integer value to push. + public void Push(int value) + { + if (this.Count == this.s.Length) + { + ThrowStackOverflow(); + } + + this.s[this.Count++] = value; + } + + /// + /// Pops and returns the top element from the stack. + /// + /// The popped integer value. + public int Pop() + { + if (this.Count == 0) + { + ThrowStackOverflow(); + } + + return this.s[--this.Count]; + } + + /// + /// Peeks at the element at the specified index from the top of the stack without removing it. + /// + /// The zero-based index from the top of the stack. + /// The integer value at the specified position. + public int Peek(int index) + { + if (index < 0 || index >= this.Count) + { + ThrowStackOverflow(); + } + + return this.s[this.Count - index - 1]; + } + + private static void ThrowStackOverflow() => throw new FontException("stack overflow"); + } + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/IndexLocationTable.cs b/SixLabors.Fonts/Tables/TrueType/IndexLocationTable.cs new file mode 100644 index 0000000..14b94be --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/IndexLocationTable.cs @@ -0,0 +1,98 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.General; +using System; + +namespace SixLabors.Fonts.Tables.TrueType { + /// + /// Represents the 'loca' (Index to Location) table which maps glyph IDs to byte offsets + /// within the 'glyf' table, enabling random access to individual glyph outlines. + /// + /// + internal sealed class IndexLocationTable : Table + { + /// + /// The table tag name. + /// + internal const string TableName = "loca"; + + /// + /// Initializes a new instance of the class. + /// + /// The array of glyph byte offsets into the 'glyf' table. + public IndexLocationTable(uint[] convertedData) + => this.GlyphOffsets = convertedData; + + /// + /// Gets the array of byte offsets into the 'glyf' table, one per glyph plus a trailing entry. + /// + public uint[] GlyphOffsets { get; } + + /// + /// Loads the 'loca' table from the specified font reader. + /// + /// The font reader. + /// The , or if the table is not present. + public static IndexLocationTable? Load(FontReader fontReader) + { + HeadTable head = fontReader.GetTable(); + + MaximumProfileTable maxp = fontReader.GetTable(); + + // Must not get a binary reader until all depended data is retrieved in case they need to use the stream. + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader, maxp.GlyphCount, head.IndexLocationFormat); + } + } + + /// + /// Loads the 'loca' table from the specified binary reader. + /// + /// The big-endian binary reader positioned at the start of the table. + /// The number of glyphs in the font. + /// The index location format (short or long offsets) from the 'head' table. + /// The . + public static IndexLocationTable Load(BigEndianBinaryReader reader, int glyphCount, HeadTable.IndexLocationFormats format) + { + int entryCount = glyphCount + 1; + + if (format == HeadTable.IndexLocationFormats.Offset16) + { + // Type | Name | Description + // ---------|-------------|--------------------------------------- + // Offset16 | offsets[n] | The actual local offset divided by 2 is stored. The value of n is numGlyphs + 1. The value for numGlyphs is found in the 'maxp' table. + using Buffer dataBuffer = new(entryCount); + Span data = dataBuffer.GetSpan(); + reader.ReadUInt16Array(data); + + uint[] convertedData = new uint[entryCount]; + for (int i = 0; i < entryCount; i++) + { + convertedData[i] = (uint)(data[i] * 2); + } + + return new IndexLocationTable(convertedData); + } + else if (format == HeadTable.IndexLocationFormats.Offset32) + { + // Type | Name | Description + // ---------|-------------|--------------------------------------- + // Offset32 | offsets[n] | The actual local offset is stored. The value of n is numGlyphs + 1. The value for numGlyphs is found in the 'maxp' table. + uint[] data = reader.ReadUInt32Array(entryCount); + + return new IndexLocationTable(data); + } + else + { + throw new InvalidFontTableException("indexToLocFormat an invalid value", "head"); + } + } + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/TrueTypeFontTables.cs b/SixLabors.Fonts/Tables/TrueType/TrueTypeFontTables.cs new file mode 100644 index 0000000..06f0883 --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/TrueTypeFontTables.cs @@ -0,0 +1,190 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; +using SixLabors.Fonts.Tables.General; +using SixLabors.Fonts.Tables.General.Colr; +using SixLabors.Fonts.Tables.General.Kern; +using SixLabors.Fonts.Tables.General.Name; +using SixLabors.Fonts.Tables.General.Post; +using SixLabors.Fonts.Tables.General.Svg; +using SixLabors.Fonts.Tables.TrueType.Glyphs; +using SixLabors.Fonts.Tables.TrueType.Hinting; + +namespace SixLabors.Fonts.Tables.TrueType { + /// + /// Implements for TrueType fonts, providing access to + /// required and optional tables including TrueType-specific tables such as + /// 'glyf', 'loca', 'cvt', 'fpgm', and 'prep'. + /// + internal sealed class TrueTypeFontTables : IFontTables + { + /// + /// Initializes a new instance of the class. + /// + /// The character-to-glyph mapping table. + /// The font header table. + /// The horizontal header table. + /// The horizontal metrics table. + /// The maximum profile table. + /// The naming table. + /// The OS/2 and Windows metrics table. + /// The PostScript name table. + /// The glyph data table. + /// The index-to-location table. + public TrueTypeFontTables( + CMapTable cmap, + HeadTable head, + HorizontalHeadTable hhea, + HorizontalMetricsTable htmx, + MaximumProfileTable maxp, + NameTable name, + OS2Table os2, + PostTable post, + GlyphTable glyph, + IndexLocationTable loca) + { + this.Cmap = cmap; + this.Head = head; + this.Hhea = hhea; + this.Htmx = htmx; + this.Maxp = maxp; + this.Name = name; + this.Os2 = os2; + this.Post = post; + this.Glyf = glyph; + this.Loca = loca; + } + + /// + public CMapTable Cmap { get; set; } + + /// + public HeadTable Head { get; set; } + + /// + public HorizontalHeadTable Hhea { get; set; } + + /// + public HorizontalMetricsTable Htmx { get; set; } + + /// + public MaximumProfileTable Maxp { get; set; } + + /// + public NameTable Name { get; set; } + + /// + public OS2Table Os2 { get; set; } + + /// + public PostTable Post { get; set; } + + /// + public GlyphDefinitionTable? Gdef { get; set; } + + /// + public GSubTable? GSub { get; set; } + + /// + public GPosTable? GPos { get; set; } + + /// + public ColrTable? Colr { get; set; } + + /// + public CpalTable? Cpal { get; set; } + + /// + public KerningTable? Kern { get; set; } + + /// + public VerticalHeadTable? Vhea { get; set; } + + /// + public VerticalMetricsTable? Vmtx { get; set; } + + /// + /// Gets or sets the optional SVG table containing scalable vector glyph data. + /// + public SvgTable? Svg { get; set; } + + // Tables Related to TrueType Outlines + // +------+-----------------------------------------------+ + // | Tag | Name | + // +======+===============================================+ + // | cvt | Control Value Table (optional table) | + // +------+-----------------------------------------------+ + // | fpgm | Font program (optional table) | + // +------+-----------------------------------------------+ + // | glyf | Glyph data | + // +------+-----------------------------------------------+ + // | loca | Index to location | + // +------+-----------------------------------------------+ + // | prep | CVT Program (optional table) | + // +------+-----------------------------------------------+ + // | gasp | Grid-fitting/Scan-conversion (optional table) | + // +------+-----------------------------------------------+ + + /// + /// Gets or sets the optional 'cvt ' (Control Value Table) for TrueType hinting. + /// + public CvtTable? Cvt { get; set; } + + /// + /// Gets or sets the optional 'fpgm' (Font Program) table for TrueType hinting. + /// + public FpgmTable? Fpgm { get; set; } + + /// + /// Gets or sets the 'glyf' (Glyph Data) table containing TrueType glyph outlines. + /// + public GlyphTable Glyf { get; set; } + + /// + /// Gets or sets the 'loca' (Index to Location) table mapping glyph IDs to offsets in 'glyf'. + /// + public IndexLocationTable Loca { get; set; } + + /// + /// Gets or sets the optional 'prep' (Control Value Program) table for TrueType hinting. + /// + public PrepTable? Prep { get; set; } + + /// + /// Gets or sets the optional 'fvar' (Font Variations) table defining variation axes. + /// + public FVarTable? Fvar { get; set; } + + /// + /// Gets or sets the optional 'avar' (Axis Variations) table for non-linear axis mapping. + /// + public AVarTable? Avar { get; set; } + + /// + /// Gets or sets the optional 'gvar' (Glyph Variations) table for TrueType outline deltas. + /// + public GVarTable? Gvar { get; set; } + + /// + /// Gets or sets the optional 'HVAR' (Horizontal Metrics Variations) table. + /// + public HVarTable? Hvar { get; set; } + + /// + /// Gets or sets the optional 'VVAR' (Vertical Metrics Variations) table. + /// + public VVarTable? Vvar { get; set; } + + /// + /// Gets or sets the optional 'MVAR' (Metrics Variations) table for global metric deltas. + /// + public MVarTable? Mvar { get; set; } + + /// + /// Gets or sets the optional 'cvar' (CVT Variations) table for control value deltas. + /// + public CVarTable? Cvar { get; set; } + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.CompatibilityLists.cs b/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.CompatibilityLists.cs new file mode 100644 index 0000000..677b999 --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.CompatibilityLists.cs @@ -0,0 +1,119 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; + +namespace SixLabors.Fonts.Tables.TrueType { + /// + /// Contains compatibility lists for tricky fonts. + /// + public partial class TrueTypeGlyphMetrics + { + /// + /// Represents a set of font family names that require font hinting to render correctly. + /// Based on the FreeType list in ttobjs.c + /// + private static readonly HashSet MustHintFonts = + new(StringComparer.Ordinal) + { + "cpop", + "DFGirl-W6-WIN-BF", + "DFGothic-EB", + "DFGyoSho-Lt", + "DFHei", + "DFHSGothic-W5", + "DFHSMincho-W3", + "DFHSMincho-W7", + "DFKaiSho-SB", + "DFKaiShu", + "DFKai-SB", + "DFMing", + "DLC", + "HuaTianKaiTi?", + "HuaTianSongTi?", + "Ming(for ISO10646)", + "MingLiU", + "MingMedium", + "PMingLiU", + "MingLi43", + }; + + /// + /// Contains the set of font family names that should never be suggested as font hints. + /// Based on community reports of rendering issues. + /// + private static readonly HashSet NeverHint = + new(StringComparer.Ordinal) + { + // Currently empty, but we may add entries here in the future if we identify + // any fonts that render better without hinting. + }; + + /// + /// Determines the effective hinting mode for the current font based on its name and the specified mode. + /// + /// + /// If the font name matches an entry in the internal 'NeverHint' list, hinting is disabled + /// regardless of the requested mode. If the font name matches an entry in the 'MustHintFonts' list, standard + /// hinting is enforced. Otherwise, the provided mode is used. This method ensures consistent rendering for certain + /// fonts that require special handling. + /// + /// The requested hinting mode to use if no font-specific override applies. + /// + /// A value indicating the hinting mode to apply. Returns a font-specific override if the font name matches a + /// configured pattern; otherwise, returns the specified mode. + /// + private HintingMode GetHintingMode(HintingMode mode) + { + ReadOnlySpan faceName = SkipPdfFontRandomTag(this.FontMetrics.Description.FontNameInvariantCulture); + + // We use partial matching here since some platforms/face collections may include additional style or + // foundry-specific information in the face name. + foreach (string needle in NeverHint) + { + if (faceName.Contains(needle, StringComparison.Ordinal)) + { + return HintingMode.None; + } + } + + foreach (string needle in MustHintFonts) + { + if (faceName.Contains(needle, StringComparison.Ordinal)) + { + return HintingMode.Standard; + } + } + + return mode; + } + + /// + /// Strips a PDF font subset randomization prefix (6 uppercase letters followed by '+') from the font name, if present. + /// + /// The font name to process. + /// The font name with the prefix removed, or the original name if no prefix was found. + private static ReadOnlySpan SkipPdfFontRandomTag(string name) + { + // Fonts embedded in PDFs are sometimes made unique by prepending a randomization prefix to their names. + // As defined in the PDF Reference ("Font Subsets"), it consists of 6 uppercase letters followed by '+'. + // For safety, we only skip prefixes that conform to this rule. + if (name.Length > 7 + && IsAsciiUpper(name[0]) + && IsAsciiUpper(name[1]) + && IsAsciiUpper(name[2]) + && IsAsciiUpper(name[3]) + && IsAsciiUpper(name[4]) + && IsAsciiUpper(name[5]) + && name[6] == '+') + { + return name.AsSpan(7); + } + + return name; + + static bool IsAsciiUpper(char c) => (uint)(c - 'A') <= ('Z' - 'A'); + } + } +} diff --git a/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs b/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs new file mode 100644 index 0000000..f3d1e73 --- /dev/null +++ b/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs @@ -0,0 +1,260 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Tables.TrueType.Glyphs; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tables.TrueType { + /// + /// Represents a glyph metric from a particular TrueType font face. + /// + public partial class TrueTypeGlyphMetrics : FontGlyphMetrics + { + private static readonly Vector2 YInverter = new(1, -1); + private readonly GlyphVector vector; + private readonly ConcurrentDictionary scaledVectorCache = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The font metrics this glyph belongs to. + /// The glyph identifier. + /// The Unicode code point for this glyph. + /// The glyph outline vector. + /// The advance width in font units. + /// The advance height in font units. + /// The left side bearing in font units. + /// The top side bearing in font units. + /// The units per em for the font. + /// The text attributes. + /// The text decorations. + /// The glyph type. + internal TrueTypeGlyphMetrics( + StreamFontMetrics font, + ushort glyphId, + CodePoint codePoint, + GlyphVector vector, + ushort advanceWidth, + ushort advanceHeight, + short leftSideBearing, + short topSideBearing, + ushort unitsPerEM, + TextAttributes textAttributes, + TextDecorations textDecorations, + GlyphType glyphType) + : base( + font, + glyphId, + codePoint, + vector.Bounds, + advanceWidth, + advanceHeight, + leftSideBearing, + topSideBearing, + unitsPerEM, + textAttributes, + textDecorations, + glyphType) + => this.vector = vector; + + /// + /// Initializes a new instance of the class + /// with explicit offset, scale, and text run for rendering clones. + /// + /// The font metrics this glyph belongs to. + /// The glyph identifier. + /// The Unicode code point for this glyph. + /// The glyph outline vector. + /// The advance width in font units. + /// The advance height in font units. + /// The left side bearing in font units. + /// The top side bearing in font units. + /// The units per em for the font. + /// The rendering offset. + /// The scale factor. + /// The text run this glyph is associated with. + /// The glyph type. + internal TrueTypeGlyphMetrics( + StreamFontMetrics font, + ushort glyphId, + CodePoint codePoint, + GlyphVector vector, + ushort advanceWidth, + ushort advanceHeight, + short leftSideBearing, + short topSideBearing, + ushort unitsPerEM, + Vector2 offset, + Vector2 scaleFactor, + TextRun textRun, + GlyphType glyphType) + : base( + font, + glyphId, + codePoint, + vector.Bounds, + advanceWidth, + advanceHeight, + leftSideBearing, + topSideBearing, + unitsPerEM, + offset, + scaleFactor, + textRun, + glyphType) + => this.vector = vector; + + /// + internal override FontGlyphMetrics CloneForRendering(TextRun textRun) + => new TrueTypeGlyphMetrics( + this.FontMetrics, + this.GlyphId, + this.CodePoint, + GlyphVector.DeepClone(this.vector), + this.AdvanceWidth, + this.AdvanceHeight, + this.LeftSideBearing, + this.TopSideBearing, + this.UnitsPerEm, + this.Offset, + this.ScaleFactor, + textRun, + this.GlyphType); + + /// + /// Gets the outline for the current glyph. + /// + /// The . + internal GlyphVector GetOutline() => this.vector; + + /// + internal override void RenderTo( + IGlyphRenderer renderer, + int graphemeIndex, + Vector2 glyphOrigin, + Vector2 decorationOrigin, + GlyphLayoutMode mode, + TextOptions options) + { + // https://www.unicode.org/faq/unsup_char.html + if (ShouldSkipGlyphRendering(this.CodePoint)) + { + return; + } + + float pointSize = this.TextRun.Font?.Size ?? options.Font.Size; + float dpi = options.Dpi; + + glyphOrigin *= dpi; + decorationOrigin *= dpi; + float scaledPPEM = this.GetScaledSize(pointSize, dpi); + + Matrix3x2 rotation = GetRotationMatrix(mode); + FontRectangle box = this.GetBoundingBox(mode, glyphOrigin, scaledPPEM); + GlyphRendererParameters parameters = new(this, this.TextRun, pointSize, dpi, mode, graphemeIndex); + + if (renderer.BeginGlyph(in box, in parameters)) + { + if (!UnicodeUtility.ShouldRenderWhiteSpaceOnly(this.CodePoint)) + { + GlyphVector scaledVector = this.scaledVectorCache.GetOrAdd(scaledPPEM, _ => + { + // Create a scaled deep copy of the vector so that we do not alter + // the globally cached instance. + GlyphVector clone = GlyphVector.DeepClone(this.vector); + Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor; + + Matrix3x2 matrix = Matrix3x2.CreateScale(scale); + matrix.Translation = this.Offset * scale; + GlyphVector.TransformInPlace(ref clone, matrix); + + float pixelSize = scaledPPEM / 72F; + this.FontMetrics.ApplyTrueTypeHinting(this.GetHintingMode(options.HintingMode), this, ref clone, scale, pixelSize); + + // Rotation must happen after hinting. + GlyphVector.TransformInPlace(ref clone, rotation); + return clone; + }); + + IList controlPoints = scaledVector.ControlPoints; + IReadOnlyList endPoints = scaledVector.EndPoints; + + int endOfContour = -1; + for (int i = 0; i < scaledVector.EndPoints.Count; i++) + { + renderer.BeginFigure(); + int startOfContour = endOfContour + 1; + endOfContour = endPoints[i]; + + Vector2 prev; + Vector2 curr = (YInverter * controlPoints[endOfContour].Point) + glyphOrigin; + Vector2 next = (YInverter * controlPoints[startOfContour].Point) + glyphOrigin; + + if (controlPoints[endOfContour].OnCurve) + { + renderer.MoveTo(curr); + } + else + { + if (controlPoints[startOfContour].OnCurve) + { + renderer.MoveTo(next); + } + else + { + // If both first and last points are off-curve, start at their middle. + Vector2 startPoint = (curr + next) * .5F; + renderer.MoveTo(startPoint); + } + } + + int length = endOfContour - startOfContour + 1; + for (int p = 0; p < length; p++) + { + prev = curr; + curr = next; + int currentIndex = startOfContour + p; + int nextIndex = startOfContour + ((p + 1) % length); + int prevIndex = startOfContour + ((length + p - 1) % length); + next = (YInverter * controlPoints[nextIndex].Point) + glyphOrigin; + + if (controlPoints[currentIndex].OnCurve) + { + // This is a straight line. + renderer.LineTo(curr); + } + else + { + Vector2 prev2 = prev; + Vector2 next2 = next; + + if (!controlPoints[prevIndex].OnCurve) + { + prev2 = (curr + prev) * .5F; + renderer.LineTo(prev2); + } + + if (!controlPoints[nextIndex].OnCurve) + { + next2 = (curr + next) * .5F; + } + + renderer.LineTo(prev2); + renderer.QuadraticBezierTo(curr, next2); + } + } + + renderer.EndFigure(); + } + } + + renderer.EndGlyph(); + this.RenderDecorationsTo(renderer, decorationOrigin, mode, rotation, scaledPPEM, options); + } + } + } +} diff --git a/SixLabors.Fonts/Tables/TtcHeader.cs b/SixLabors.Fonts/Tables/TtcHeader.cs new file mode 100644 index 0000000..9157aa2 --- /dev/null +++ b/SixLabors.Fonts/Tables/TtcHeader.cs @@ -0,0 +1,116 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables { + /// + /// Represents a font collection header (for .ttc font collections). + /// A font collection contains one or more fonts where typically the glyf table is shared by multiple fonts to save space, + /// but other tables are not. + /// Each font in the collection has its own set of tables. + /// + internal class TtcHeader + { + internal const string TableName = "ttcf"; + + /// + /// Initializes a new instance of the class. + /// + /// The TTC tag, expected to be "ttcf". + /// The major version of the TTC header (1 or 2). + /// The minor version of the TTC header. + /// The number of fonts in the collection. + /// Array of byte offsets to each font's offset table. + /// The DSIG table tag (version 2+ only, otherwise 0). + /// The length of the DSIG table in bytes (version 2+ only, otherwise 0). + /// The byte offset of the DSIG table (version 2+ only, otherwise 0). + public TtcHeader(string ttcTag, ushort majorVersion, ushort minorVersion, uint numFonts, uint[] offsetTable, uint dsigTag, uint dsigLength, uint dsigOffset) + { + this.TtcTag = ttcTag; + this.MajorVersion = majorVersion; + this.MinorVersion = minorVersion; + this.NumFonts = numFonts; + this.OffsetTable = offsetTable; + this.DsigTag = dsigTag; + this.DsigLength = dsigLength; + this.DsigOffset = dsigOffset; + } + + /// + /// Gets the tag, should be "ttcf". + /// + public string TtcTag { get; } + + /// + /// Gets the major version of the TTC header. Version 1 has no DSIG; version 2 includes DSIG fields. + /// + public ushort MajorVersion { get; } + + /// + /// Gets the minor version of the TTC header. + /// + public ushort MinorVersion { get; } + + /// + /// Gets the number of fonts contained in the collection. + /// + public uint NumFonts { get; } + + /// + /// Gets the array of offsets to the OffsetTable of each font. Use for each font. + /// + public uint[] OffsetTable { get; } + + /// + /// Gets the tag of the DSIG (digital signature) table. Only present in version 2+ headers. + /// + public uint DsigTag { get; } + + /// + /// Gets the length of the DSIG table in bytes. Only present in version 2+ headers. + /// + public uint DsigLength { get; } + + /// + /// Gets the byte offset of the DSIG table from the beginning of the file. Only present in version 2+ headers. + /// + public uint DsigOffset { get; } + + /// + /// Reads a from the given reader. + /// + /// The binary reader positioned at the start of the TTC header. + /// The parsed . + /// Thrown when the tag is not "ttcf". + public static TtcHeader Read(BigEndianBinaryReader reader) + { + string tag = reader.ReadTag(); + + if (tag != TableName) + { + throw new InvalidFontTableException($"Expected tag = {TableName} found {tag}", TableName); + } + + ushort majorVersion = reader.ReadUInt16(); + ushort minorVersion = reader.ReadUInt16(); + uint numFonts = reader.ReadUInt32(); + uint[] offsetTable = new uint[numFonts]; + for (int i = 0; i < numFonts; ++i) + { + offsetTable[i] = reader.ReadOffset32(); + } + + // Version 2 fields + uint dsigTag = 0; + uint dsigLength = 0; + uint dsigOffset = 0; + if (majorVersion >= 2) + { + dsigTag = reader.ReadUInt32(); + dsigLength = reader.ReadUInt32(); + dsigOffset = reader.ReadUInt32(); + } + + return new TtcHeader(tag, majorVersion, minorVersion, numFonts, offsetTable, dsigTag, dsigLength, dsigOffset); + } + } +} diff --git a/SixLabors.Fonts/Tables/UnknownTable.cs b/SixLabors.Fonts/Tables/UnknownTable.cs new file mode 100644 index 0000000..99609d8 --- /dev/null +++ b/SixLabors.Fonts/Tables/UnknownTable.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables { + /// + /// Represents an unrecognized or unsupported font table. + /// Used as a placeholder when the table loader encounters a tag it has no registered parser for. + /// + internal sealed class UnknownTable : Table + { + /// + /// Initializes a new instance of the class. + /// + /// The four-byte table tag. + internal UnknownTable(string name) + => this.Name = name; + + /// + /// Gets the four-byte table tag that was not recognized. + /// + public string Name { get; } + } +} diff --git a/SixLabors.Fonts/Tables/Woff/Woff2GlyphLoader.cs b/SixLabors.Fonts/Tables/Woff/Woff2GlyphLoader.cs new file mode 100644 index 0000000..7eb4c95 --- /dev/null +++ b/SixLabors.Fonts/Tables/Woff/Woff2GlyphLoader.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.TrueType.Glyphs; + +namespace SixLabors.Fonts.Tables.Woff { + /// + /// A glyph loader for WOFF2 fonts that wraps a pre-parsed + /// from the transformed glyph data stream. + /// See: . + /// + internal sealed class Woff2GlyphLoader : GlyphLoader + { + /// + /// The glyph vector containing the decoded outline data. + /// + private GlyphVector glyphVector; + + /// + /// Initializes a new instance of the class. + /// + /// The pre-parsed glyph vector from the WOFF2 transformed glyph stream. + public Woff2GlyphLoader(GlyphVector glyphVector) => this.glyphVector = glyphVector; + + /// + /// Creates a glyph vector, computing bounding box on demand if not already set. + /// + /// The glyph table. + /// The . + public override GlyphVector CreateGlyph(GlyphTable table) + { + if (this.glyphVector.Bounds == default) + { + this.glyphVector.Bounds = Bounds.Load(this.glyphVector.ControlPoints); + } + + return this.glyphVector; + } + } +} diff --git a/SixLabors.Fonts/Tables/Woff/Woff2TableHeader.cs b/SixLabors.Fonts/Tables/Woff/Woff2TableHeader.cs new file mode 100644 index 0000000..cd24393 --- /dev/null +++ b/SixLabors.Fonts/Tables/Woff/Woff2TableHeader.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.Woff { + /// + /// Represents a table directory entry in a WOFF2 font file. + /// Each entry describes a single font table within the WOFF2 container. + /// See: . + /// + internal sealed class Woff2TableHeader : TableHeader + { + /// + /// Initializes a new instance of the class. + /// + /// The 4-byte table identifier tag. + /// The checksum of the uncompressed table data. + /// The offset to the table data within the decompressed WOFF2 data stream. + /// The length of the table data (transform length if transformed, otherwise original length). + public Woff2TableHeader(string tag, uint checkSum, uint offset, uint len) + : base(tag, checkSum, offset, len) + { + } + } +} diff --git a/SixLabors.Fonts/Tables/Woff/Woff2Utils.cs b/SixLabors.Fonts/Tables/Woff/Woff2Utils.cs new file mode 100644 index 0000000..17eb51d --- /dev/null +++ b/SixLabors.Fonts/Tables/Woff/Woff2Utils.cs @@ -0,0 +1,721 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Numerics; +using SixLabors.Fonts.Tables.TrueType.Glyphs; + +namespace SixLabors.Fonts.Tables.Woff { + // Source code is based on https://github.com/LayoutFarm/Typography + // see https://github.com/LayoutFarm/Typography/blob/master/Typography.OpenFont/WebFont/Woff2Reader.cs + // TODO: There's still some cleanup required here to bring the code up to a maintainable standard. + + /// + /// Provides utility methods for reading and decoding WOFF2 font data, + /// including variable-length integer decoding and transformed glyph table reconstruction. + /// See: . + /// + internal static class Woff2Utils + { + /// + /// The set of known table tags used for WOFF2 table directory encoding. + /// Table indices 0-62 map to these well-known tags; index 63 indicates an arbitrary 4-byte tag follows. + /// + // We don't reuse the const tag headers from our table types for clarity. + private static readonly string[] KnownTableTags = + { + "cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post", "cvt ", + "fpgm", "glyf", "loca", "prep", "CFF ", "VORG", "EBDT", "EBLC", "gasp", + "hdmx", "kern", "LTSH", "PCLT", "VDMX", "vhea", "vmtx", "BASE", "GDEF", + "GPOS", "GSUB", "EBSC", "JSTF", "MATH", "CBDT", "CBLC", "COLR", "CPAL", + "SVG ", "sbix", "acnt", "avar", "bdat", "bloc", "bsln", "cvar", "fdsc", + "feat", "fmtx", "fvar", "gvar", "hsty", "just", "lcar", "mort", "morx", + "opbd", "prop", "trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill" + }; + + /// + /// The 255UInt16 encoding code indicating the value is stored as a following big-endian 16-bit word. + /// + private const byte OneMoreByteCode1 = 255; + + /// + /// The 255UInt16 encoding code indicating the value is a following byte plus LowestUCode * 2. + /// + private const byte OneMoreByteCode2 = 254; + + /// + /// The 255UInt16 encoding code indicating the value is stored as a following big-endian 16-bit word (explicit two bytes). + /// + private const byte WordCode = 253; + + /// + /// The lowest single-byte code value that triggers multi-byte 255UInt16 decoding. + /// Values below this are returned directly as the decoded result. + /// + private const byte LowestUCode = 253; + + /// + /// Reads the WOFF2 table directory headers from the given reader. + /// + /// The big-endian binary reader positioned at the start of the table directory. + /// The number of table directory entries to read. + /// A read-only dictionary mapping table tags to their entries. + public static ReadOnlyDictionary ReadWoff2Headers(BigEndianBinaryReader reader, int tableCount) + { + uint expectedTableStartAt = 0; + var headers = new Dictionary(tableCount); + for (int i = 0; i < tableCount; i++) + { + Woff2TableHeader woffTableHeader = Read(reader, expectedTableStartAt, out uint nextExpectedTableStartAt); + expectedTableStartAt = nextExpectedTableStartAt; + headers.Add(woffTableHeader.Tag, woffTableHeader); + } + + return new ReadOnlyDictionary(headers); + } + + /// + /// Reads a single WOFF2 table directory entry, decoding the flags byte, table tag, + /// original length, and optional transform length. + /// See: . + /// + /// The big-endian binary reader. + /// The expected offset for this table within the decompressed data stream. + /// When this method returns, contains the expected offset for the next table. + /// The parsed . + public static Woff2TableHeader Read(BigEndianBinaryReader reader, uint expectedTableStartAt, out uint nextExpectedTableStartAt) + { + // Leave the first byte open to store flagByte + const uint woff2FlagsTransform = 1 << 8; + byte flagsByte = reader.ReadByte(); + int knownTable = flagsByte & 0x3F; + string tableName = knownTable == 0x3F ? reader.ReadTag() : KnownTableTags[knownTable]; + + uint flags = 0; + byte xformVersion = (byte)((flagsByte >> 6) & 0x03); + + // 0 means xform for glyph/loca, non-0 for others + if (tableName is "glyf" or "loca") + { + if (xformVersion == 0) + { + flags |= woff2FlagsTransform; + } + } + else if (xformVersion != 0) + { + flags |= woff2FlagsTransform; + } + + flags |= xformVersion; + + if (!ReadUIntBase128(reader, out uint tableOrigLength)) + { + throw new FontException("Error parsing woff2 table header"); + } + + uint tableTransformLength = tableOrigLength; + if ((flags & woff2FlagsTransform) != 0) + { + if (!ReadUIntBase128(reader, out tableTransformLength)) + { + throw new FontException("Error parsing woff2 table header"); + } + + if (tableName == "loca" && tableTransformLength > 0) + { + throw new FontException("Error parsing woff2 table header"); + } + } + + nextExpectedTableStartAt = expectedTableStartAt + tableTransformLength; + if (nextExpectedTableStartAt < expectedTableStartAt) + { + throw new FontException("Error parsing woff2 table header"); + } + + return new Woff2TableHeader(tableName, 0, expectedTableStartAt, tableTransformLength); + } + + /// + /// Loads all glyph outlines from a WOFF2 transformed glyph table stream. + /// This reconstructs both simple and composite glyphs from the WOFF2 sub-streams + /// (nContour, nPoints, flags, glyph, composite, bbox, and instruction streams). + /// See: . + /// + /// The big-endian binary reader positioned at the start of the transformed glyf table. + /// The empty glyph loader to use for glyphs with no outline data. + /// An array of instances, one per glyph. + public static GlyphLoader[] LoadAllGlyphs(BigEndianBinaryReader reader, EmptyGlyphLoader emptyGlyphLoader) + { + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | Data Type | Semantic | Description and value type (if applicable) | + // +===========+=======================+=======================================================================================================+ + // | Fixed | version | = 0x00000000 | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | UInt16 | numGlyphs | Number of glyphs | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | UInt16 | indexFormat | Offset format for loca table, should be consistent with indexToLocFormat | + // | | | of the original head table (see specification) | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | UInt32 | nContourStreamSize | Size of nContour stream in bytes | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | UInt32 | nPointsStreamSize | Size of nPoints stream in bytes | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | UInt32 | flagStreamSize | Size of flag stream in bytes | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | UInt32 | glyphStreamSize | Size of glyph stream in bytes (a stream of variable-length encoded values, see description below) | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | UInt32 | compositeStreamSize | Size of composite stream in bytes (a stream of variable-length encoded values, see description below) | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | UInt32 | bboxStreamSize | Size of bbox data in bytes representing combined length of bboxBitmap (a packed bit array) | + // | | | and bboxStream (a stream of Int16 values) | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | UInt32 | instructionStreamSize | Size of instruction stream (a stream of UInt8 values) | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | Int16 | nContourStream[] | Stream of Int16 values representing number of contours for each glyph record | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | 255UInt16 | nPointsStream[] | Stream of values representing number of outline points for each contour in glyph records | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | UInt8 | flagStream[] | Stream of UInt8 values representing flag values for each outline point. | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | Vary | glyphStream[] | Stream of bytes representing point coordinate values using variable length | + // | | | encoding format (defined in subclause 5.2) | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | Vary | compositeStream[] | Stream of bytes representing component flag values and associated composite glyph data | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | UInt8 | bboxBitmap[] | Bitmap (a numGlyphs-long bit array) indicating explicit bounding boxes | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | Int16 | bboxStream[] | Stream of Int16 values representing glyph bounding box data | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + // | UInt8 | instructionStream[] | Stream of UInt8 values representing a set of instructions for each corresponding glyph | + // +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+ + uint version = reader.ReadUInt32(); + ushort numGlyphs = reader.ReadUInt16(); + ushort indexFormatOffset = reader.ReadUInt16(); + + uint nContourStreamSize = reader.ReadUInt32(); + uint nPointsStreamSize = reader.ReadUInt32(); + uint flagStreamSize = reader.ReadUInt32(); + uint glyphStreamSize = reader.ReadUInt32(); + uint compositeStreamSize = reader.ReadUInt32(); + uint bboxStreamSize = reader.ReadUInt32(); + uint instructionStreamSize = reader.ReadUInt32(); + + long nCountStreamOffset = reader.BaseStream.Position; + long nPointStreamOffset = nCountStreamOffset + nContourStreamSize; + long flagStreamOffset = nPointStreamOffset + nPointsStreamSize; + long glyphStreamOffset = flagStreamOffset + flagStreamSize; + long compositeStreamOffset = glyphStreamOffset + glyphStreamSize; + + long bboxStreamOffset = compositeStreamOffset + compositeStreamSize; + long instructionStreamOffset = bboxStreamOffset + bboxStreamSize; + + var glyphs = new GlyphVector[numGlyphs]; + var allGlyphs = new GlyphData[numGlyphs]; + var glyphLoaders = new GlyphLoader[numGlyphs]; + var compositeGlyphs = new List(); + int contourCount = 0; + for (ushort i = 0; i < numGlyphs; i++) + { + short numContour = reader.ReadInt16(); + allGlyphs[i] = new GlyphData(i, numContour); + if (numContour > 0) + { + contourCount += numContour; + + // >0 => simple glyph + // -1 = composite + // 0 = empty glyph + } + else if (numContour < 0) + { + // Composite glyph, resolve later. + compositeGlyphs.Add(i); + } + } + + ushort[] pntPerContours = new ushort[contourCount]; + for (int i = 0; i < contourCount; i++) + { + // Each of these is the number of points of that contour. + pntPerContours[i] = Read255UInt16(reader); + } + + // FlagStream, flags value for each point. + // Each byte in flags stream represents one point. + byte[] flagStream = reader.ReadBytes((int)flagStreamSize); + + // Some composite glyphs have instructions so we must check all composite glyphs before read the glyph stream. + using (MemoryStream compositeMemoryStream = new()) + { + reader.BaseStream.Position = compositeStreamOffset; + compositeMemoryStream.Write(reader.ReadBytes((int)compositeStreamSize), 0, (int)compositeStreamSize); + compositeMemoryStream.Position = 0; + + using (BigEndianBinaryReader compositeReader = new(compositeMemoryStream, false)) + { + for (ushort i = 0; i < compositeGlyphs.Count; i++) + { + ushort compositeGlyphIndex = compositeGlyphs[i]; + allGlyphs[compositeGlyphIndex].CompositeHasInstructions = CompositeHasInstructions(compositeReader); + } + } + + reader.BaseStream.Position = glyphStreamOffset; + } + + int curFlagsIndex = 0; + int pntContourIndex = 0; + for (int i = 0; i < allGlyphs.Length; i++) + { + glyphs[i] = ReadSimpleGlyphData( + reader, + ref allGlyphs[i], + pntPerContours, + ref pntContourIndex, + flagStream, + ref curFlagsIndex); + } + + // Now we read the composite stream again and create composite glyphs. + for (ushort i = 0; i < compositeGlyphs.Count; i++) + { + int compositeGlyphIndex = compositeGlyphs[i]; + glyphs[compositeGlyphIndex] = ReadCompositeGlyphData(glyphs, reader); + } + + // Read the bounding box stream. + reader.BaseStream.Position = bboxStreamOffset; + int bitmapCount = ((numGlyphs + 31) >> 5) << 2; + byte[] boundsBitmap = ExpandBitmap(reader.ReadBytes(bitmapCount)); + for (ushort i = 0; i < numGlyphs; i++) + { + GlyphData data = allGlyphs[i]; + if (boundsBitmap[i] == 1) + { + // Read explicit bounds from the stream. + // If the bounds are not explicit, the glyph loader will calculate them on demand. + glyphs[i].Bounds = Bounds.Load(reader); + } + else if (data.NumContour < 0) + { + throw new NotSupportedException("Composite glyph must have a bounding box."); + } + } + + // Read the instructions stream. + reader.BaseStream.Position = instructionStreamOffset; + for (int i = 0; i < allGlyphs.Length; i++) + { + ref GlyphVector vector = ref glyphs[i]; + GlyphData data = allGlyphs[i]; + if (data.InstructionsLength > 0) + { + vector.Instructions = reader.ReadBytes(data.InstructionsLength); + } + + glyphLoaders[i] = new Woff2GlyphLoader(vector); + } + + // Finally compile the complete glyphs. + for (ushort i = 0; i < numGlyphs; i++) + { + if (!glyphs[i].HasValue()) + { + glyphLoaders[i] = emptyGlyphLoader; + continue; + } + + glyphLoaders[i] = new Woff2GlyphLoader(glyphs[i]); + } + + return glyphLoaders; + } + + /// + /// Reads simple glyph outline data from the WOFF2 glyph stream, decoding point coordinates + /// using the triple encoding format defined in the WOFF2 specification. + /// + /// The big-endian binary reader positioned in the glyph stream. + /// A reference to the glyph data containing contour count and instruction metadata. + /// The array of point counts per contour across all glyphs. + /// A reference to the current index within . + /// The flag stream bytes, one per outline point. + /// A reference to the current index within . + /// The decoded , or for empty or composite glyphs. + private static GlyphVector ReadSimpleGlyphData( + BigEndianBinaryReader reader, + ref GlyphData glyphData, + ushort[] pntPerContours, + ref int pntContourIndex, + byte[] flagStream, + ref int flagStreamIndex) + { + if (glyphData.NumContour == 0) + { + return default; + } + + if (glyphData.NumContour < 0) + { + // Composite glyph. Check if this has instruction or not + // and read the length. We don't actually use the data but it ensures + // we maintain the correct location within the stream. + if (glyphData.CompositeHasInstructions) + { + Read255UInt16(reader); + } + + return default; // Skip composite glyph (resolve later). + } + + int curX = 0; + int curY = 0; + int numContour = glyphData.NumContour; + ushort[] endPoints = new ushort[numContour]; + ushort pointCount = 0; + + for (ushort i = 0; i < numContour; i++) + { + ushort numPoint = pntPerContours[pntContourIndex++]; + pointCount += numPoint; + endPoints[i] = (ushort)(pointCount - 1); + } + + var controlPoints = new ControlPoint[pointCount]; + int n = 0; + for (int i = 0; i < numContour; i++) + { + int endContour = endPoints[i]; + for (; n <= endContour; ++n) + { + byte f = flagStream[flagStreamIndex++]; + + // int f1 = (f >> 7); // Most significant 1 bit -> on/off curve. + int xyFormat = f & 0x7F; // Remaining 7 bits x, y format. + + TripleEncodingRecord enc = TripleEncodingTable.EncTable[xyFormat]; // 0-128 + + byte[] packedXY = reader.ReadBytes(enc.ByteCount - 1); // byte count include 1 byte flags, so actual read=> byteCount-1 + + int x; + int y; + switch (enc.XBits) + { + default: + throw new NotSupportedException(); + case 0: // 0,8, + x = 0; + y = enc.Ty(packedXY[0]); + break; + case 4: // 4,4 + x = enc.Tx(packedXY[0] >> 4); + y = enc.Ty(packedXY[0] & 0xF); + break; + case 8: // 8,0 or 8,8 + x = enc.Tx(packedXY[0]); + y = enc.YBits == 8 ? + enc.Ty(packedXY[1]) : + 0; + break; + case 12: // 12,12 + x = enc.Tx((packedXY[0] << 4) | (packedXY[1] >> 4)); + y = enc.Ty(((packedXY[1] & 0xF) << 8) | packedXY[2]); + break; + case 16: // 16,16 + x = enc.Tx((packedXY[0] << 8) | packedXY[1]); + y = enc.Ty((packedXY[2] << 8) | packedXY[3]); + break; + } + + // Most significant 1 bit -> on/off curve. + controlPoints[n] = new(new Vector2(curX += x, curY += y), f >> 7 == 0); + } + } + + // Read the instructions length for later parsing. + glyphData.InstructionsLength = Read255UInt16(reader); + + // Bounds and instructions are read later. + return new GlyphVector(controlPoints, endPoints, default, Array.Empty(), false); + } + + /// + /// Determines whether a composite glyph record in the composite stream contains instructions + /// by scanning through all component entries and checking the flag. + /// + /// The big-endian binary reader positioned at the start of the composite glyph record. + /// if the composite glyph contains instructions; otherwise, . + private static bool CompositeHasInstructions(BigEndianBinaryReader reader) + { + bool weHaveInstructions = false; + CompositeGlyphFlags flags = CompositeGlyphFlags.MoreComponents; + while ((flags & CompositeGlyphFlags.MoreComponents) != 0) + { + flags = reader.ReadUInt16(); + weHaveInstructions |= (flags & CompositeGlyphFlags.WeHaveInstructions) != 0; + int argSize = 2; // glyph index + if ((flags & CompositeGlyphFlags.Args1And2AreWords) != 0) + { + argSize += 4; + } + else + { + argSize += 2; + } + + if ((flags & CompositeGlyphFlags.WeHaveAScale) != 0) + { + argSize += 2; + } + else if ((flags & CompositeGlyphFlags.WeHaveXAndYScale) != 0) + { + argSize += 4; + } + else if ((flags & CompositeGlyphFlags.WeHaveATwoByTwo) != 0) + { + argSize += 8; + } + + reader.BaseStream.Seek(argSize, SeekOrigin.Current); + } + + return weHaveInstructions; + } + + /// + /// Reads composite glyph data from the WOFF2 composite stream, recursively resolving + /// component glyphs and applying their transforms. + /// + /// The array of all glyph vectors, used for resolving component references. + /// The big-endian binary reader positioned in the composite stream. + /// The assembled composite . + private static GlyphVector ReadCompositeGlyphData(GlyphVector[] createdGlyphs, BigEndianBinaryReader reader) + { + List controlPoints = new(); + List endPoints = new(); + CompositeGlyphFlags flags; + do + { + flags = reader.ReadUInt16(); + ushort glyphIndex = reader.ReadUInt16(); + if (!createdGlyphs[glyphIndex].HasValue()) + { + // This glyph has not been read yet, resolve it first. + long position = reader.BaseStream.Position; + createdGlyphs[glyphIndex] = ReadCompositeGlyphData(createdGlyphs, reader); + reader.BaseStream.Position = position; + } + + CompositeGlyphLoader.LoadArguments(reader, flags, out int dx, out int dy); + + Matrix3x2 transform = Matrix3x2.Identity; + transform.Translation = new Vector2(dx, dy); + + if ((flags & CompositeGlyphFlags.WeHaveAScale) != 0) + { + float scale = reader.ReadF2Dot14(); + transform.M11 = scale; + transform.M22 = scale; + } + else if ((flags & CompositeGlyphFlags.WeHaveXAndYScale) != 0) + { + transform.M11 = reader.ReadF2Dot14(); + transform.M22 = reader.ReadF2Dot14(); + } + else if ((flags & CompositeGlyphFlags.WeHaveATwoByTwo) != 0) + { + transform.M11 = reader.ReadF2Dot14(); + transform.M12 = reader.ReadF2Dot14(); + transform.M21 = reader.ReadF2Dot14(); + transform.M22 = reader.ReadF2Dot14(); + } + + var clone = GlyphVector.DeepClone(createdGlyphs[glyphIndex]); + GlyphVector.TransformInPlace(ref clone, transform); + ushort endPointOffset = (ushort)controlPoints.Count; + + controlPoints.AddRange(clone.ControlPoints); + foreach (ushort p in clone.EndPoints) + { + endPoints.Add((ushort)(p + endPointOffset)); + } + } + while ((flags & CompositeGlyphFlags.MoreComponents) != 0); + + // Bounds and instructions are read later. + return new GlyphVector(controlPoints, endPoints, default, Array.Empty(), true); + } + + /// + /// Expands a packed bitmap byte array into an array of individual bit values, + /// where each byte in the result is either 0 or 1. + /// Used to decode the bounding box bitmap in the WOFF2 glyf table. + /// + /// The packed bitmap bytes. + /// An expanded byte array where each element represents a single bit from the input. + private static byte[] ExpandBitmap(byte[] orgBBoxBitmap) + { + byte[] expandArr = new byte[orgBBoxBitmap.Length * 8]; + + int index = 0; + for (int i = 0; i < orgBBoxBitmap.Length; i++) + { + byte b = orgBBoxBitmap[i]; + expandArr[index++] = (byte)((b >> 7) & 0x1); + expandArr[index++] = (byte)((b >> 6) & 0x1); + expandArr[index++] = (byte)((b >> 5) & 0x1); + expandArr[index++] = (byte)((b >> 4) & 0x1); + expandArr[index++] = (byte)((b >> 3) & 0x1); + expandArr[index++] = (byte)((b >> 2) & 0x1); + expandArr[index++] = (byte)((b >> 1) & 0x1); + expandArr[index++] = (byte)((b >> 0) & 0x1); + } + + return expandArr; + } + + /// + /// Reads the UIntBase128 Data Type. + /// + /// The binary reader using big endian encoding. + /// The result as uint. + /// true, if succeeded. + private static bool ReadUIntBase128(BigEndianBinaryReader reader, out uint result) + { + // UIntBase128 is a different variable length encoding of unsigned integers, + // suitable for values up to 2^(32) - 1. + // A UIntBase128 encoded number is a sequence of bytes for which the most significant bit + // is set for all but the last byte, + // and clear for the last byte. + // + // The number itself is base 128 encoded in the lower 7 bits of each byte. + // Thus, a decoding procedure for a UIntBase128 is: + // start with value = 0. + // Consume a byte, setting value = old value times 128 + (byte bitwise - and 127). + // Repeat last step until the most significant bit of byte is false. + // + // UIntBase128 encoding format allows a possibility of sub-optimal encoding, + // where e.g.the same numerical value can be represented with variable number of bytes(utilizing leading 'zeros'). + // For example, the value 63 could be encoded as either one byte 0x3F or two(or more) bytes: [0x80, 0x3f]. + // An encoder must not allow this to happen and must produce shortest possible encoding. + // A decoder MUST reject the font file if it encounters a UintBase128 - encoded value with leading zeros(a value that starts with the byte 0x80), + // if UintBase128 - encoded sequence is longer than 5 bytes, + // or if a UintBase128 - encoded value exceeds 232 - 1. + uint accum = 0; + result = 0; + for (int i = 0; i < 5; i++) + { + byte data_byte = reader.ReadByte(); + + // No leading 0's + if (i == 0 && data_byte == 0x80) + { + return false; + } + + // If any of top 7 bits are set then << 7 would overflow. + if ((accum & 0xFE000000) != 0) + { + return false; + } + + accum = (accum << 7) | (uint)(data_byte & 0x7F); + + // Spin until most significant bit of data byte is false. + if ((data_byte & 0x80) == 0) + { + result = accum; + return true; + } + } + + // UIntBase128 sequence exceeds 5 bytes. + return false; + } + + /// + /// Reads the UIntBase255 Data Type. + /// + /// The binary reader using big endian encoding. + /// The UIntBase255 result. + private static ushort Read255UInt16(BigEndianBinaryReader reader) + { + // 255UInt16 Variable-length encoding of a 16-bit unsigned integer for optimized intermediate font data storage. + // 255UInt16 is a variable-length encoding of an unsigned integer + // in the range 0 to 65535 inclusive. + // This data type is intended to be used as intermediate representation of various font values, + // which are typically expressed as UInt16 but represent relatively small values. + // Depending on the encoded value, the length of the data field may be one to three bytes, + // where the value of the first byte either represents the small value itself or is treated as a code that defines the format of the additional byte(s). + byte code = reader.ReadByte(); + if (code == WordCode) + { + int value = reader.ReadByte(); + value <<= 8; + value &= 0xff00; + int value2 = reader.ReadByte(); + value |= value2 & 0x00ff; + + return (ushort)value; + } + else if (code == OneMoreByteCode1) + { + return (ushort)(reader.ReadByte() + LowestUCode); + } + else if (code == OneMoreByteCode2) + { + return (ushort)(reader.ReadByte() + (LowestUCode * 2)); + } + else + { + return code; + } + } + + /// + /// Stores intermediate metadata for a glyph being decoded from a WOFF2 transformed glyph table, + /// including its contour count, instruction length, and whether a composite glyph has instructions. + /// + private struct GlyphData + { + /// + /// The index of the glyph within the font. + /// + public readonly ushort GlyphIndex; + + /// + /// The number of contours for this glyph. + /// A positive value indicates a simple glyph, negative indicates composite, and zero indicates empty. + /// + public readonly short NumContour; + + /// + /// The length in bytes of the TrueType instructions for this glyph. + /// + public int InstructionsLength; + + /// + /// Gets or sets a value indicating whether this composite glyph contains TrueType instructions. + /// + public bool CompositeHasInstructions; + + /// + /// Initializes a new instance of the struct. + /// + /// The index of the glyph within the font. + /// The number of contours for the glyph. + public GlyphData(ushort glyphIndex, short contourCount) + { + this.GlyphIndex = glyphIndex; + this.NumContour = contourCount; + this.InstructionsLength = 0; + this.CompositeHasInstructions = false; + } + } + } +} diff --git a/SixLabors.Fonts/Tables/Woff/WoffTableHeader.cs b/SixLabors.Fonts/Tables/Woff/WoffTableHeader.cs new file mode 100644 index 0000000..8476e91 --- /dev/null +++ b/SixLabors.Fonts/Tables/Woff/WoffTableHeader.cs @@ -0,0 +1,87 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO; + +namespace SixLabors.Fonts.Tables.Woff { + /// + /// Represents a table directory entry in a WOFF1 font file. + /// Each entry describes a single font table within the WOFF container, + /// including its compressed and original lengths. + /// See: . + /// + internal sealed class WoffTableHeader : TableHeader + { + /// + /// Initializes a new instance of the class. + /// + /// The 4-byte sfnt table identifier. + /// The offset to the data from the beginning of the WOFF file. + /// The length of the compressed table data, excluding padding. + /// The length of the uncompressed table data, excluding padding. + /// The checksum of the uncompressed table data. + public WoffTableHeader(string tag, uint offset, uint compressedLength, uint origLength, uint checkSum) + : base(tag, checkSum, offset, origLength) + => this.CompressedLength = compressedLength; + + /// + /// Gets the length of the compressed table data, excluding padding. + /// + public uint CompressedLength { get; } + + /// + /// Creates a for the table data, decompressing with zlib if necessary. + /// + /// The stream containing the WOFF font data. + /// A positioned at the start of the uncompressed table data. + public override BigEndianBinaryReader CreateReader(Stream stream) + { + // Stream is not compressed. + if (this.Length == this.CompressedLength) + { + return base.CreateReader(stream); + } + + // Read all data from the compressed stream. + stream.Seek(this.Offset, SeekOrigin.Begin); + using var compressedStream = new IO.ZlibInflateStream(stream); + byte[] uncompressedBytes = new byte[this.Length]; + int totalBytesRead = 0; + int bytesLeftToRead = uncompressedBytes.Length; + while (totalBytesRead < this.Length) + { + int bytesRead = compressedStream.Read(uncompressedBytes, totalBytesRead, bytesLeftToRead); + if (bytesRead <= 0) + { + throw new InvalidFontFileException($"Could not read compressed data! Expected bytes: {this.Length}, bytes read: {totalBytesRead}"); + } + + totalBytesRead += bytesRead; + bytesLeftToRead -= bytesRead; + } + + var memoryStream = new MemoryStream(uncompressedBytes); + return new BigEndianBinaryReader(memoryStream, false); + } + + // WOFF TableDirectoryEntry + // UInt32 | tag | 4-byte sfnt table identifier. + // UInt32 | offset | Offset to the data, from beginning of WOFF file. + // UInt32 | compLength | Length of the compressed data, excluding padding. + // UInt32 | origLength | Length of the uncompressed table, excluding padding. + // UInt32 | origChecksum | Checksum of the uncompressed table. + + /// + /// Reads a from the given reader. + /// + /// The big-endian binary reader. + /// The parsed . + public static new WoffTableHeader Read(BigEndianBinaryReader reader) => + new WoffTableHeader( + reader.ReadTag(), + reader.ReadUInt32(), + reader.ReadUInt32(), + reader.ReadUInt32(), + reader.ReadUInt32()); + } +} diff --git a/SixLabors.Fonts/TextAlignment.cs b/SixLabors.Fonts/TextAlignment.cs new file mode 100644 index 0000000..132d5f5 --- /dev/null +++ b/SixLabors.Fonts/TextAlignment.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Text alignment modes. + /// + public enum TextAlignment + { + /// + /// Aligns text from the left or top when the text direction is + /// and from the right or bottom when the text direction is . + /// + Start = 0, + + /// + /// Aligns text from the right or bottom when the text direction is + /// and from the left or top when the text direction is . + /// + End = 1, + + /// + /// Aligns text from the center. + /// + Center = 2 + } +} diff --git a/SixLabors.Fonts/TextAttributes.cs b/SixLabors.Fonts/TextAttributes.cs new file mode 100644 index 0000000..ba9c5d9 --- /dev/null +++ b/SixLabors.Fonts/TextAttributes.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts { + /// + /// Provides enumeration of various text attributes. + /// + [Flags] + public enum TextAttributes + { + /// + /// No attributes are applied + /// + None = 0, + + /// + /// The text set slightly below the normal line of type. + /// + Subscript = 1 << 0, + + /// + /// The text set slightly above the normal line of type. + /// + Superscript = 1 << 1, + } +} diff --git a/SixLabors.Fonts/TextBidiMode.cs b/SixLabors.Fonts/TextBidiMode.cs new file mode 100644 index 0000000..52b6e47 --- /dev/null +++ b/SixLabors.Fonts/TextBidiMode.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Specifies how bidirectional text is resolved. + /// + public enum TextBidiMode + { + /// + /// Uses the Unicode Bidirectional Algorithm with each character's bidirectional class. + /// + Normal = 0, + + /// + /// Lays out text in the resolved text direction, ignoring each character's normal bidirectional class. + /// + Override = 1, + } +} diff --git a/SixLabors.Fonts/TextBlock.Visitors.cs b/SixLabors.Fonts/TextBlock.Visitors.cs new file mode 100644 index 0000000..e26de5f --- /dev/null +++ b/SixLabors.Fonts/TextBlock.Visitors.cs @@ -0,0 +1,815 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Unicode; +using System; +using System.Collections.Generic; + +namespace SixLabors.Fonts { + /// + /// Visitor types for streaming laid-out glyphs into operations. + /// + public sealed partial class TextBlock + { + /// + /// Adds a flushed grapheme to its source-order word-boundary segment. + /// + /// The source-order word-boundary segments. + /// The word metrics array being accumulated. + /// The grapheme metrics just emitted. + private static void AccumulateWordMetrics( + List wordSegments, + WordMetrics[] wordMetrics, + in GraphemeMetrics grapheme) + => AccumulateWordMetrics( + wordSegments, + wordMetrics, + grapheme.GraphemeIndex, + grapheme.Advance, + grapheme.Bounds, + grapheme.RenderableBounds); + + /// + /// Adds one coalesced grapheme rectangle set to its source-order word-boundary segment. + /// + /// The source-order word-boundary segments. + /// The word metrics array being accumulated. + /// The source grapheme index owning the rectangles. + /// The positioned logical advance rectangle for the grapheme. + /// The rendered glyph bounds for the grapheme. + /// The union of logical advance and rendered glyph bounds for the grapheme. + private static void AccumulateWordMetrics( + List wordSegments, + WordMetrics[] wordMetrics, + int graphemeIndex, + FontRectangle advance, + FontRectangle bounds, + FontRectangle renderableBounds) + { + int wordIndex = FindWordMetricIndex(wordSegments, graphemeIndex); + WordSegmentRun segment = wordSegments[wordIndex]; + WordMetrics metrics = wordMetrics[wordIndex]; + + // WordMetrics is the final value type, but this array slot also acts as the running + // accumulator for its source segment. Once a slot has source ranges, subsequent + // graphemes in the same word segment union into the rectangles already stored there. + bool hasMetrics = HasWordMetrics(metrics); + + wordMetrics[wordIndex] = new WordMetrics( + hasMetrics ? FontRectangle.Union(metrics.Advance, advance) : advance, + hasMetrics ? FontRectangle.Union(metrics.Bounds, bounds) : bounds, + hasMetrics ? FontRectangle.Union(metrics.RenderableBounds, renderableBounds) : renderableBounds, + segment.GraphemeStart, + segment.GraphemeEnd, + segment.StringStart, + segment.StringEnd); + } + + /// + /// Coalesces consecutive laid-out glyph entries that belong to the same grapheme. + /// + private struct GraphemeMetricsAccumulator + { + private readonly GraphemeMetrics[] graphemes; + private readonly float dpi; + private int count; + private int graphemeIndex; + private int stringIndex; + private int bidiLevel; + private bool isLineBreak; + private Font? font; + private FontRectangle advanceBounds; + private FontRectangle bounds; + private bool hasCurrent; + + /// + /// Initializes a new instance of the struct. + /// + /// The target grapheme array to fill. + /// The target DPI. + public GraphemeMetricsAccumulator(GraphemeMetrics[] graphemes, float dpi) + { + this.graphemes = graphemes; + this.dpi = dpi; + this.count = 0; + this.graphemeIndex = 0; + this.stringIndex = 0; + this.bidiLevel = 0; + this.isLineBreak = false; + this.font = null; + this.advanceBounds = FontRectangle.Empty; + this.bounds = FontRectangle.Empty; + this.hasCurrent = false; + } + + /// + /// Gets the number of graphemes emitted so far. + /// + public readonly int Count => this.count; + + /// + /// Adds one laid-out glyph entry to the current grapheme, flushing the previous grapheme when needed. + /// + /// The laid-out glyph entry. + public void Visit(in GlyphLayout glyph) + => this.Visit(glyph, out _); + + /// + /// Adds one laid-out glyph entry to the current grapheme, returning emitted metrics when the previous grapheme is flushed. + /// + /// The laid-out glyph entry. + /// The emitted grapheme metrics when this method returns . + /// when a grapheme was emitted. + public bool Visit( + in GlyphLayout glyph, + out GraphemeMetrics metrics) + { + FontRectangle advanceBounds = glyph.MeasureAdvance(this.dpi); + FontRectangle bounds = glyph.MeasureBounds(this.dpi); + + if (!this.hasCurrent) + { + this.Start(glyph, advanceBounds, bounds); + metrics = default; + return false; + } + + if (glyph.GraphemeIndex != this.graphemeIndex) + { + bool emitted = this.Flush(out metrics); + this.Start(glyph, advanceBounds, bounds); + return emitted; + } + + this.advanceBounds = FontRectangle.Union(this.advanceBounds, advanceBounds); + this.bounds = FontRectangle.Union(this.bounds, bounds); + this.isLineBreak |= CodePoint.IsNewLine(glyph.CodePoint); + metrics = default; + return false; + } + + /// + /// Flushes the current line's pending grapheme. + /// + public void EndLine() => this.Flush(out _); + + /// + /// Flushes the current line's pending grapheme. + /// + /// The emitted grapheme metrics when this method returns . + /// when a grapheme was emitted. + public bool EndLine(out GraphemeMetrics metrics) + => this.Flush(out metrics); + + /// + /// Starts a new grapheme from the first emitted glyph in a consecutive grapheme run. + /// + /// The first glyph in the grapheme. + /// The positioned logical advance bounds for . + /// The rendered bounds for . + private void Start( + in GlyphLayout glyph, + in FontRectangle advanceBounds, + in FontRectangle bounds) + { + this.graphemeIndex = glyph.GraphemeIndex; + this.stringIndex = glyph.StringIndex; + this.bidiLevel = glyph.BidiLevel; + this.isLineBreak = CodePoint.IsNewLine(glyph.CodePoint); + this.font = glyph.Font; + this.advanceBounds = advanceBounds; + this.bounds = bounds; + this.hasCurrent = true; + } + + /// + /// Emits the current grapheme while preserving the visual order produced by text layout. + /// + /// The emitted grapheme metrics when this method returns . + /// when a grapheme was emitted. + private bool Flush(out GraphemeMetrics metrics) + { + if (!this.hasCurrent) + { + metrics = default; + return false; + } + + FontRectangle renderableBounds = FontRectangle.Union(this.advanceBounds, this.bounds); + metrics = new GraphemeMetrics( + this.advanceBounds, + this.bounds, + renderableBounds, + this.font!, + this.graphemeIndex, + this.stringIndex, + this.bidiLevel, + this.isLineBreak); + + this.graphemes[this.count] = metrics; + this.count++; + this.hasCurrent = false; + return true; + } + } + + /// + /// Coalesces laid-out glyph entries into grapheme metrics and word metrics in the same stream. + /// + private struct GraphemeAndWordMetricsAccumulator + { + private readonly List wordSegments; + private readonly WordMetrics[] wordMetrics; + private GraphemeMetricsAccumulator graphemes; + + /// + /// Initializes a new instance of the struct. + /// + /// The target grapheme array to fill. + /// The target DPI. + /// The source-order word-boundary segments. + /// The target word metrics array to fill. + public GraphemeAndWordMetricsAccumulator( + GraphemeMetrics[] graphemes, + float dpi, + List wordSegments, + WordMetrics[] wordMetrics) + { + this.wordSegments = wordSegments; + this.wordMetrics = wordMetrics; + this.graphemes = new(graphemes, dpi); + } + + /// + /// Gets the number of graphemes emitted so far. + /// + public readonly int Count => this.graphemes.Count; + + /// + /// Adds one laid-out glyph entry to the current grapheme and updates word metrics when a grapheme is emitted. + /// + /// The laid-out glyph entry. + public void Visit(in GlyphLayout glyph) + { + if (this.graphemes.Visit(glyph, out GraphemeMetrics metrics)) + { + AccumulateWordMetrics(this.wordSegments, this.wordMetrics, metrics); + } + } + + /// + /// Flushes the current line's pending grapheme and updates word metrics when a grapheme is emitted. + /// + public void EndLine() + { + if (this.graphemes.EndLine(out GraphemeMetrics metrics)) + { + AccumulateWordMetrics(this.wordSegments, this.wordMetrics, metrics); + } + } + } + + /// + /// Coalesces laid-out glyph entries into word metrics without storing grapheme metrics. + /// + private struct WordMetricsVisitor : TextLayout.IGlyphLayoutVisitor + { + private readonly List wordSegments; + private readonly WordMetrics[] wordMetrics; + private readonly float dpi; + private int graphemeIndex; + private FontRectangle advanceBounds; + private FontRectangle bounds; + private bool hasCurrent; + + /// + /// Initializes a new instance of the struct. + /// + /// The source-order word-boundary segments. + /// The target word metrics array to fill. + /// The target DPI. + public WordMetricsVisitor( + List wordSegments, + WordMetrics[] wordMetrics, + float dpi) + { + this.wordSegments = wordSegments; + this.wordMetrics = wordMetrics; + this.dpi = dpi; + this.graphemeIndex = 0; + this.advanceBounds = FontRectangle.Empty; + this.bounds = FontRectangle.Empty; + this.hasCurrent = false; + } + + /// + public readonly void BeginLine(int lineIndex) + { + } + + /// + public void Visit(in GlyphLayout glyph) + { + FontRectangle advanceBounds = glyph.MeasureAdvance(this.dpi); + FontRectangle bounds = glyph.MeasureBounds(this.dpi); + + if (!this.hasCurrent) + { + this.Start(glyph, advanceBounds, bounds); + return; + } + + if (glyph.GraphemeIndex != this.graphemeIndex) + { + this.Flush(); + this.Start(glyph, advanceBounds, bounds); + return; + } + + this.advanceBounds = FontRectangle.Union(this.advanceBounds, advanceBounds); + this.bounds = FontRectangle.Union(this.bounds, bounds); + } + + /// + public void EndLine() => this.Flush(); + + /// + /// Starts a new word-metrics grapheme from the first emitted glyph in a consecutive grapheme run. + /// + /// The first glyph in the grapheme. + /// The positioned logical advance bounds for . + /// The rendered bounds for . + private void Start( + in GlyphLayout glyph, + in FontRectangle advanceBounds, + in FontRectangle bounds) + { + this.graphemeIndex = glyph.GraphemeIndex; + this.advanceBounds = advanceBounds; + this.bounds = bounds; + this.hasCurrent = true; + } + + /// + /// Emits the current grapheme directly into its source-order word-boundary segment. + /// + private void Flush() + { + if (!this.hasCurrent) + { + return; + } + + FontRectangle renderableBounds = FontRectangle.Union(this.advanceBounds, this.bounds); + AccumulateWordMetrics( + this.wordSegments, + this.wordMetrics, + this.graphemeIndex, + this.advanceBounds, + this.bounds, + renderableBounds); + + this.hasCurrent = false; + } + } + + /// + /// Accumulates the rendered rectangle as glyphs stream from layout. + /// + private struct RenderedRectangleAccumulator : TextLayout.IGlyphLayoutVisitor + { + private readonly float dpi; + private float left; + private float top; + private float right; + private float bottom; + private bool any; + + /// + /// Initializes a new instance of the struct. + /// + /// The target DPI. + public RenderedRectangleAccumulator(float dpi) + { + this.dpi = dpi; + this.left = float.MaxValue; + this.top = float.MaxValue; + this.right = float.MinValue; + this.bottom = float.MinValue; + this.any = false; + } + + /// + public readonly void BeginLine(int lineIndex) + { + } + + /// + public void Visit(in GlyphLayout glyph) + { + FontRectangle box = glyph.MeasureBounds(this.dpi); + if (box.Width <= 0 && box.Height <= 0) + { + return; + } + + if (box.Left < this.left) + { + this.left = box.Left; + } + + if (box.Top < this.top) + { + this.top = box.Top; + } + + if (box.Right > this.right) + { + this.right = box.Right; + } + + if (box.Bottom > this.bottom) + { + this.bottom = box.Bottom; + } + + this.any = true; + } + + /// + /// Returns the accumulated rendered bounds. + /// + /// The rendered bounds of all visited glyphs. + public readonly FontRectangle Result() + => this.any ? FontRectangle.FromLTRB(this.left, this.top, this.right, this.bottom) : FontRectangle.Empty; + + /// + public readonly void EndLine() + { + } + } + + /// + /// Builds the bounds and grapheme metrics array while glyphs stream from layout. + /// + private struct GraphemeMetricsVisitor : TextLayout.IGlyphLayoutVisitor + { + private readonly float dpi; + private GraphemeMetricsAccumulator graphemes; + private float left; + private float top; + private float right; + private float bottom; + private bool hasBounds; + + /// + /// Initializes a new instance of the struct. + /// + /// The target DPI. + /// The grapheme metrics array to fill. + public GraphemeMetricsVisitor( + float dpi, + GraphemeMetrics[] graphemes) + { + this.dpi = dpi; + this.graphemes = new(graphemes, dpi); + this.left = float.MaxValue; + this.top = float.MaxValue; + this.right = float.MinValue; + this.bottom = float.MinValue; + this.hasBounds = false; + } + + /// + public readonly void BeginLine(int lineIndex) + { + } + + /// + public void Visit(in GlyphLayout glyph) + { + FontRectangle glyphBox = glyph.MeasureBounds(this.dpi); + bool hasGlyphBox = glyphBox.Width > 0 || glyphBox.Height > 0; + + if (hasGlyphBox && glyphBox.Left < this.left) + { + this.left = glyphBox.Left; + } + + if (hasGlyphBox && glyphBox.Top < this.top) + { + this.top = glyphBox.Top; + } + + if (hasGlyphBox && glyphBox.Right > this.right) + { + this.right = glyphBox.Right; + } + + if (hasGlyphBox && glyphBox.Bottom > this.bottom) + { + this.bottom = glyphBox.Bottom; + } + + this.hasBounds |= hasGlyphBox; + this.graphemes.Visit(glyph); + } + + /// + /// Returns the accumulated rendered bounds. + /// + /// The rendered bounds of all visited glyphs. + public readonly FontRectangle Bounds() + => this.hasBounds ? FontRectangle.FromLTRB(this.left, this.top, this.right, this.bottom) : FontRectangle.Empty; + + /// + public void EndLine() => this.graphemes.EndLine(); + } + + /// + /// Builds the bounds, grapheme metrics, and word metrics arrays while glyphs stream from layout. + /// + private struct GraphemeAndWordMetricsVisitor : TextLayout.IGlyphLayoutVisitor + { + private readonly float dpi; + private GraphemeAndWordMetricsAccumulator graphemes; + private float left; + private float top; + private float right; + private float bottom; + private bool hasBounds; + + /// + /// Initializes a new instance of the struct. + /// + /// The target DPI. + /// The grapheme metrics array to fill. + /// The source-order word-boundary segments. + /// The word metrics array to fill. + public GraphemeAndWordMetricsVisitor( + float dpi, + GraphemeMetrics[] graphemes, + List wordSegments, + WordMetrics[] wordMetrics) + { + this.dpi = dpi; + this.graphemes = new(graphemes, dpi, wordSegments, wordMetrics); + this.left = float.MaxValue; + this.top = float.MaxValue; + this.right = float.MinValue; + this.bottom = float.MinValue; + this.hasBounds = false; + } + + /// + public readonly void BeginLine(int lineIndex) + { + } + + /// + public void Visit(in GlyphLayout glyph) + { + FontRectangle glyphBox = glyph.MeasureBounds(this.dpi); + bool hasGlyphBox = glyphBox.Width > 0 || glyphBox.Height > 0; + + if (hasGlyphBox && glyphBox.Left < this.left) + { + this.left = glyphBox.Left; + } + + if (hasGlyphBox && glyphBox.Top < this.top) + { + this.top = glyphBox.Top; + } + + if (hasGlyphBox && glyphBox.Right > this.right) + { + this.right = glyphBox.Right; + } + + if (hasGlyphBox && glyphBox.Bottom > this.bottom) + { + this.bottom = glyphBox.Bottom; + } + + this.hasBounds |= hasGlyphBox; + this.graphemes.Visit(glyph); + } + + /// + /// Returns the accumulated rendered bounds. + /// + /// The rendered bounds of all visited glyphs. + public readonly FontRectangle Bounds() + => this.hasBounds ? FontRectangle.FromLTRB(this.left, this.top, this.right, this.bottom) : FontRectangle.Empty; + + /// + public void EndLine() => this.graphemes.EndLine(); + } + + /// + /// Builds the per-line grapheme metrics results while glyphs stream from layout. + /// + private struct LineLayoutVisitor : TextLayout.IGlyphLayoutVisitor + { + private readonly TextBox textBox; + private readonly TextOptions options; + private readonly float wrappingLength; + private readonly LineMetrics[] metrics; + private readonly LineLayout[] lines; + private readonly GraphemeMetrics[] graphemes; + private readonly WordMetrics[] wordMetrics; + private GraphemeAndWordMetricsAccumulator graphemeAccumulator; + private int lineIndex; + private int lineGraphemeStart; + private int metricIndex; + + /// + /// Initializes a new instance of the struct. + /// + /// The shaped and line-broken text box. + /// The text options used for layout. + /// The wrapping length in pixels. + /// The grapheme metrics array to fill. + /// The line metrics aligned with the line-broken text box. + /// The line layout array to fill. + /// The source-order word-boundary segments. + /// The word metrics for the source text. + /// The target DPI. + public LineLayoutVisitor( + TextBox textBox, + TextOptions options, + float wrappingLength, + GraphemeMetrics[] graphemes, + LineMetrics[] metrics, + LineLayout[] lines, + List wordSegments, + WordMetrics[] wordMetrics, + float dpi) + { + this.textBox = textBox; + this.options = options; + this.wrappingLength = wrappingLength; + this.metrics = metrics; + this.lines = lines; + this.graphemes = graphemes; + this.wordMetrics = wordMetrics; + this.graphemeAccumulator = new(graphemes, dpi, wordSegments, wordMetrics); + this.lineIndex = 0; + this.lineGraphemeStart = 0; + this.metricIndex = 0; + } + + /// + public void BeginLine(int lineIndex) + { + this.lineGraphemeStart = this.graphemeAccumulator.Count; + this.metricIndex = lineIndex; + } + + /// + public void Visit(in GlyphLayout glyph) + => this.graphemeAccumulator.Visit(glyph); + + /// + public void EndLine() + { + this.graphemeAccumulator.EndLine(); + + // TextLayout owns the visual line loop, so the slice is recorded here instead of + // reconstructing line membership from metrics after glyph emission. + ReadOnlyMemory lineGraphemes = new(this.graphemes, this.lineGraphemeStart, this.graphemeAccumulator.Count - this.lineGraphemeStart); + this.lines[this.lineIndex] = new LineLayout( + this.textBox, + this.options, + this.wrappingLength, + this.metricIndex, + in this.metrics[this.metricIndex], + lineGraphemes, + this.wordMetrics); + + this.lineIndex++; + } + } + + /// + /// Builds one per-glyph metrics array while glyphs stream from layout. + /// + private struct GlyphMetricsVisitor : TextLayout.IGlyphLayoutVisitor + { + private readonly GlyphMetrics[] glyphMetrics; + private readonly float dpi; + private readonly int lineIndex; + private int count; + private int currentLineIndex; + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to fill. + /// The target DPI. + public GlyphMetricsVisitor( + GlyphMetrics[] glyphMetrics, + float dpi) + : this(glyphMetrics, dpi, -1) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to fill. + /// The target DPI. + /// The line index to collect. + public GlyphMetricsVisitor( + GlyphMetrics[] glyphMetrics, + float dpi, + int lineIndex) + { + this.glyphMetrics = glyphMetrics; + this.dpi = dpi; + this.lineIndex = lineIndex; + this.count = 0; + this.currentLineIndex = -1; + } + + /// + public void BeginLine(int lineIndex) + => this.currentLineIndex = lineIndex; + + /// + public void Visit(in GlyphLayout glyph) + { + if (this.lineIndex >= 0 && this.currentLineIndex != this.lineIndex) + { + return; + } + + FontRectangle advance = glyph.MeasureAdvance(this.dpi); + FontRectangle bounds = glyph.MeasureBounds(this.dpi); + FontRectangle renderableBounds = FontRectangle.Union(advance, bounds); + + this.glyphMetrics[this.count] = new GlyphMetrics( + glyph.Glyph.GlyphMetrics.CodePoint, + advance, + bounds, + renderableBounds, + glyph.Font, + glyph.GraphemeIndex, + glyph.StringIndex); + + this.count++; + } + + /// + public readonly void EndLine() + { + } + } + + /// + /// Renders glyphs as they stream from layout. + /// + private struct GlyphRendererVisitor : TextLayout.IGlyphLayoutVisitor + { + private readonly IGlyphRenderer renderer; + private readonly TextOptions options; + private readonly int lineIndex; + private int currentLineIndex; + + /// + /// Initializes a new instance of the struct. + /// + /// The target renderer. + /// The text options used for rendering. + /// The line index to render, or -1 to render every line. + public GlyphRendererVisitor(IGlyphRenderer renderer, TextOptions options, int lineIndex) + { + this.renderer = renderer; + this.options = options; + this.lineIndex = lineIndex; + this.currentLineIndex = -1; + } + + /// + public void BeginLine(int lineIndex) => this.currentLineIndex = lineIndex; + + /// + public readonly void Visit(in GlyphLayout glyph) + { + if (this.lineIndex > -1 && this.currentLineIndex != this.lineIndex) + { + return; + } + + glyph.Glyph.RenderTo(this.renderer, glyph.GraphemeIndex, glyph.GlyphOrigin, glyph.DecorationOrigin, glyph.LayoutMode, this.options); + } + + /// + public readonly void EndLine() + { + } + } + } +} diff --git a/SixLabors.Fonts/TextBlock.cs b/SixLabors.Fonts/TextBlock.cs new file mode 100644 index 0000000..528623e --- /dev/null +++ b/SixLabors.Fonts/TextBlock.cs @@ -0,0 +1,584 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.Fonts.Rendering; + +namespace SixLabors.Fonts { + /// + /// Represents text prepared for repeated line layout, measurement, and rendering. + /// + public sealed partial class TextBlock + { + /// + /// Initializes a new instance of the class. + /// + /// The text to prepare. + /// The text options used to prepare, measure, and render the block. + /// + /// is ignored while preparing the block; pass the wrapping length + /// to the measurement or rendering method. Use -1 there to disable wrapping. + /// + public TextBlock(string text, TextOptions options) + : this(text.AsSpan(), options) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The text to prepare. + /// The text options used to prepare, measure, and render the block. + /// + /// is ignored while preparing the block; pass the wrapping length + /// to the measurement or rendering method. Use -1 there to disable wrapping. + /// + public TextBlock(ReadOnlySpan text, TextOptions options) + { + this.Options = options; + + if (text.IsEmpty) + { + this.LogicalLine = new(new TextLine(), [], [], []); + return; + } + + ShapedText shaped = TextLayout.ShapeText(text, options); + this.LogicalLine = TextLayout.ComposeLogicalLine(shaped, text, options); + } + + /// + /// Gets the text options used by this block. + /// + internal TextOptions Options { get; } + + /// + /// Gets the prepared logical line and line break opportunities. + /// + internal LogicalTextLine LogicalLine { get; } + + /// + /// Breaks this block into lines for the supplied wrapping length. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The line-broken text box. + internal TextBox BreakLines(float wrappingLength) + => TextLayout.BreakLines(this.LogicalLine, this.Options, wrappingLength); + + /// + /// Measures the full set of layout metrics for this block at the supplied wrapping length. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// A instance containing every measurement for the laid-out text. + public TextMetrics Measure(float wrappingLength) + { + TextBox textBox = this.BreakLines(wrappingLength); + float dpi = this.Options.Dpi; + bool isHorizontal = this.Options.LayoutMode.IsHorizontal(); + + FontRectangle advance = GetAdvance(textBox, dpi, isHorizontal); + + GraphemeMetrics[] graphemes = new GraphemeMetrics[CountGraphemeMetrics(textBox)]; + WordMetrics[] wordMetrics = new WordMetrics[this.LogicalLine.WordSegments.Count]; + + GraphemeAndWordMetricsVisitor visitor = new(dpi, graphemes, this.LogicalLine.WordSegments, wordMetrics); + TextLayout.LayoutText(textBox, this.Options, wrappingLength, ref visitor); + + FontRectangle bounds = visitor.Bounds(); + FontRectangle absoluteAdvance = new(this.Options.Origin.X, this.Options.Origin.Y, advance.Width, advance.Height); + FontRectangle renderableBounds = FontRectangle.Union(absoluteAdvance, bounds); + + LineMetrics[] lineMetrics = GetLineMetrics(textBox, this.Options, wrappingLength); + + return new TextMetrics( + this, + textBox, + wrappingLength, + advance, + bounds, + renderableBounds, + textBox.TextLines.Count, + graphemes, + lineMetrics, + wordMetrics); + } + + /// + /// Measures the logical advance of this block at the supplied wrapping length. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The logical advance rectangle. + public FontRectangle MeasureAdvance(float wrappingLength) + { + TextBox textBox = this.BreakLines(wrappingLength); + return GetAdvance(textBox, this.Options.Dpi, this.Options.LayoutMode.IsHorizontal()); + } + + /// + /// Measures the rendered glyph bounds of this block at the supplied wrapping length. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The rendered glyph bounds. + public FontRectangle MeasureBounds(float wrappingLength) + => GetBounds(this.BreakLines(wrappingLength), this.Options, wrappingLength); + + /// + /// Measures the union of logical advance and rendered glyph bounds at the supplied wrapping length. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The full renderable bounds. + public FontRectangle MeasureRenderableBounds(float wrappingLength) + { + TextBox textBox = this.BreakLines(wrappingLength); + FontRectangle advance = GetAdvance(textBox, this.Options.Dpi, this.Options.LayoutMode.IsHorizontal()); + FontRectangle absoluteAdvance = new(this.Options.Origin.X, this.Options.Origin.Y, advance.Width, advance.Height); + FontRectangle bounds = GetBounds(textBox, this.Options, wrappingLength); + return FontRectangle.Union(absoluteAdvance, bounds); + } + + /// + /// Gets the positioned metrics of each laid-out glyph entry. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// A read-only memory region containing per-glyph metrics entries. + public ReadOnlyMemory GetGlyphMetrics(float wrappingLength) + => this.GetGlyphMetricsArray(wrappingLength); + + /// + /// Gets the positioned metrics of each laid-out grapheme. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// A read-only memory region containing per-grapheme metrics entries. + public ReadOnlyMemory GetGraphemeMetrics(float wrappingLength) + { + TextBox textBox = this.BreakLines(wrappingLength); + return GetGraphemeMetricsArray(textBox, this.Options, wrappingLength); + } + + /// + /// Gets the positioned metrics of each Unicode word-boundary segment. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// A read-only memory region containing per-word-boundary segment metrics entries. + public ReadOnlyMemory GetWordMetrics(float wrappingLength) + { + TextBox textBox = this.BreakLines(wrappingLength); + WordMetrics[] wordMetrics = new WordMetrics[this.LogicalLine.WordSegments.Count]; + WordMetricsVisitor visitor = new(this.LogicalLine.WordSegments, wordMetrics, this.Options.Dpi); + TextLayout.LayoutText(textBox, this.Options, wrappingLength, ref visitor); + return wordMetrics; + } + + /// + /// Gets the number of laid-out lines at the supplied wrapping length. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The laid-out line count. + public int CountLines(float wrappingLength) + => this.BreakLines(wrappingLength).TextLines.Count; + + /// + /// Gets per-line layout metrics at the supplied wrapping length. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// A read-only memory region containing in pixel units. + public ReadOnlyMemory GetLineMetrics(float wrappingLength) + => GetLineMetrics(this.BreakLines(wrappingLength), this.Options, wrappingLength); + + /// + /// Gets visual line layouts for this block at the supplied wrapping length. + /// + /// + /// The returned memory contains every laid-out line, including lines produced by hard line breaks. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// A read-only memory region containing entries in final layout order. + public ReadOnlyMemory GetLineLayouts(float wrappingLength) + { + TextBox textBox = this.BreakLines(wrappingLength); + if (textBox.TextLines.Count == 0) + { + return ReadOnlyMemory.Empty; + } + + return this.GetLineLayouts(textBox, wrappingLength); + } + + /// + /// Creates an enumerator that lays out this block one line at a time. + /// + /// A line layout enumerator for this block. + public LineLayoutEnumerator EnumerateLineLayouts() + => new(this); + + /// + /// Gets a single line layout for an already line-broken text line. + /// + /// The line to lay out. + /// The wrapping length in pixels. + /// The block-level text direction used for alignment. + /// The line layout for the supplied line. + internal LineLayout GetLineLayout( + TextLine textLine, + float wrappingLength, + TextDirection textDirection) + { + TextBox textBox = new([textLine], textDirection); + + return this.GetLineLayouts(textBox, wrappingLength)[0]; + } + + /// + /// Gets visual line layouts for an already line-broken text box. + /// + /// The shaped and line-broken text box. + /// The wrapping length in pixels. + /// The line layouts for the supplied text box. + private LineLayout[] GetLineLayouts(TextBox textBox, float wrappingLength) + { + GraphemeMetrics[] graphemes = new GraphemeMetrics[CountGraphemeMetrics(textBox)]; + LineMetrics[] metrics = GetLineMetrics(textBox, this.Options, wrappingLength); + LineLayout[] lines = new LineLayout[textBox.TextLines.Count]; + + WordMetrics[] wordMetrics = new WordMetrics[this.LogicalLine.WordSegments.Count]; + LineLayoutVisitor visitor = new(textBox, this.Options, wrappingLength, graphemes, metrics, lines, this.LogicalLine.WordSegments, wordMetrics, this.Options.Dpi); + TextLayout.LayoutText(textBox, this.Options, wrappingLength, ref visitor); + + return lines; + } + + /// + /// Renders this block to the supplied glyph renderer at the supplied wrapping length. + /// + /// The target renderer. + /// The wrapping length in pixels. Use -1 to disable wrapping. + public void RenderTo(IGlyphRenderer renderer, float wrappingLength) + { + TextBox textBox = this.BreakLines(wrappingLength); + FontRectangle rect = GetBounds(textBox, this.Options, wrappingLength); + + RenderTo(renderer, textBox, this.Options, wrappingLength, rect); + } + + /// + /// Renders an already line-broken text box to the supplied glyph renderer. + /// + /// The target renderer. + /// The shaped and line-broken text box. + /// The text options used for rendering. + /// The wrapping length in pixels. + /// The bounds passed to the renderer. + /// The line index to render, or -1 to render every line. + internal static void RenderTo( + IGlyphRenderer renderer, + TextBox textBox, + TextOptions options, + float wrappingLength, + in FontRectangle bounds, + int lineIndex = -1) + { + renderer.BeginText(in bounds); + + GlyphRendererVisitor visitor = new(renderer, options, lineIndex); + TextLayout.LayoutText(textBox, options, wrappingLength, ref visitor); + + renderer.EndText(); + } + + /// + /// Measures the rendered glyph bounds of an already line-broken text box. + /// + /// The shaped and line-broken text box. + /// The text options used for layout. + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The union of the rendered glyph bounds. + private static FontRectangle GetBounds(TextBox textBox, TextOptions options, float wrappingLength) + { + if (textBox.TextLines.Count == 0) + { + return FontRectangle.Empty; + } + + RenderedRectangleAccumulator visitor = new(options.Dpi); + TextLayout.LayoutText(textBox, options, wrappingLength, ref visitor); + return visitor.Result(); + } + + /// + /// Gets per-line layout metrics for an already line-broken text box. + /// + /// The shaped and line-broken text box. + /// The text options used to calculate line metrics. + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// An array of in pixel units. + private static LineMetrics[] GetLineMetrics(TextBox textBox, TextOptions options, float wrappingLength) + { + if (textBox.TextLines.Count == 0) + { + return []; + } + + LineMetrics[] metrics = new LineMetrics[textBox.TextLines.Count]; + + // Determine the line-box extent used for alignment within the flow direction. + float maxScaledAdvance = textBox.ScaledMaxAdvance(); + if (options.TextAlignment != TextAlignment.Start && wrappingLength > 0) + { + maxScaledAdvance = MathF.Max(wrappingLength / options.Dpi, maxScaledAdvance); + } + + TextDirection direction = textBox.TextDirection(); + LayoutMode layoutMode = options.LayoutMode; + + bool isHorizontalLayout = layoutMode.IsHorizontal(); + float lineOffset = isHorizontalLayout ? options.Origin.Y : options.Origin.X; + + bool reverseLineOrder = layoutMode is + LayoutMode.HorizontalBottomTop + or LayoutMode.VerticalRightLeft + or LayoutMode.VerticalMixedRightLeft; + + int i = reverseLineOrder ? textBox.TextLines.Count - 1 : 0; + int step = reverseLineOrder ? -1 : 1; + int graphemeOffset = 0; + + while (i >= 0 && i < textBox.TextLines.Count) + { + TextLine line = textBox.TextLines[i]; + + // Calculate the line start position in the current flow direction. + float offset = isHorizontalLayout + ? TextLayout.CalculateLineOffsetX( + line.ScaledLineAdvance, + maxScaledAdvance, + options.HorizontalAlignment, + options.TextAlignment, + direction) + : TextLayout.CalculateLineOffsetY( + line.ScaledLineAdvance, + maxScaledAdvance, + options.VerticalAlignment, + options.TextAlignment, + direction); + + // Delta captured during layout when ascender/descender were symmetrically + // adjusted to match browser-like line-box behavior. + float delta = line.ScaledMaxDelta; + + // Core typographic region within the line box. + // We add back 2*delta to recover the pre-adjustment ascender+descender span + // used for deriving guide positions. + float coreHeight = line.ScaledMaxAscender + line.ScaledMaxDescender + (2 * delta); + + // Additional leading in the line box (for example from line spacing). + float extra = line.ScaledMaxLineHeight - coreHeight; + + // Baseline position within the line box. + float baseline = (extra * 0.5f) + line.ScaledMaxAscender + delta; + + // Ascender line position relative to the same origin. + float ascender = baseline - line.ScaledMaxAscender + delta; + + // Descender line position relative to the same origin. + float descender = baseline + line.ScaledMaxDescender + delta; + Vector2 start = isHorizontalLayout + ? new(options.Origin.X + (offset * options.Dpi), lineOffset) + : new(lineOffset, options.Origin.Y + (offset * options.Dpi)); + + Vector2 extent = isHorizontalLayout + ? new(line.ScaledLineAdvance * options.Dpi, line.ScaledMaxLineHeight * options.Dpi) + : new(line.ScaledMaxLineHeight * options.Dpi, line.ScaledLineAdvance * options.Dpi); + + // Bidi reordering mutates entries into visual order, so the source + // start is the minimum original source index rather than line[0]. + int stringIndex = line[0].StringIndex; + int graphemeIndex = line[0].GraphemeIndex; + for (int j = 1; j < line.Count; j++) + { + stringIndex = Math.Min(stringIndex, line[j].StringIndex); + graphemeIndex = Math.Min(graphemeIndex, line[j].GraphemeIndex); + } + + metrics[i] = new LineMetrics( + ascender * options.Dpi, + baseline * options.Dpi, + descender * options.Dpi, + line.ScaledMaxLineHeight * options.Dpi, + start, + extent, + stringIndex, + graphemeIndex, + line.GraphemeCount, + graphemeOffset); + + graphemeOffset += line.GraphemeCount; + lineOffset += line.ScaledMaxLineHeight * options.Dpi; + i += step; + } + + return metrics; + } + + /// + /// Counts grapheme metrics entries across all lines in an already line-broken text box. + /// + /// The shaped and line-broken text box. + /// The number of grapheme metrics entries. + private static int CountGraphemeMetrics(TextBox textBox) + { + int count = 0; + for (int i = 0; i < textBox.TextLines.Count; i++) + { + count += textBox.TextLines[i].GraphemeCount; + } + + return count; + } + + /// + /// Gets grapheme metrics entries by streaming laid-out glyphs. + /// + /// The shaped and line-broken text box. + /// The text options used for layout. + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The grapheme metrics entries. + internal static GraphemeMetrics[] GetGraphemeMetricsArray( + TextBox textBox, + TextOptions options, + float wrappingLength) + { + int count = CountGraphemeMetrics(textBox); + if (count == 0) + { + return []; + } + + GraphemeMetrics[] graphemes = new GraphemeMetrics[count]; + GraphemeMetricsVisitor visitor = new(options.Dpi, graphemes); + TextLayout.LayoutText(textBox, options, wrappingLength, ref visitor); + return graphemes; + } + + /// + /// Finds the source-order word-boundary range containing the supplied grapheme index. + /// + /// The source-order word-boundary segments. + /// The grapheme index to locate. + /// The matching word metrics index, or -1 when no range contains the grapheme. + private static int FindWordMetricIndex(List wordSegments, int graphemeIndex) + { + int min = 0; + int max = wordSegments.Count - 1; + while (min <= max) + { + int mid = (min + max) >> 1; + WordSegmentRun segment = wordSegments[mid]; + if (graphemeIndex < segment.GraphemeStart) + { + max = mid - 1; + continue; + } + + if (graphemeIndex >= segment.GraphemeEnd) + { + min = mid + 1; + continue; + } + + return mid; + } + + return -1; + } + + /// + /// Gets a value indicating whether positioned metrics have been added to a word segment. + /// + /// The word metrics to inspect. + /// when a grapheme has been accumulated for the segment. + private static bool HasWordMetrics(in WordMetrics metrics) + + // Default WordMetrics has no source range. Any real word segment has an exclusive end + // index, so the range is the sentinel that avoids treating FontRectangle.Empty as geometry. + => metrics.GraphemeEnd != 0 || metrics.StringEnd != 0; + + /// + /// Gets one per-glyph metrics collection by streaming laid-out glyphs. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The positioned glyph metrics. + internal GlyphMetrics[] GetGlyphMetricsArray(float wrappingLength) + { + TextBox textBox = this.BreakLines(wrappingLength); + return GetGlyphMetricsArray(textBox, this.Options, wrappingLength); + } + + /// + /// Gets one per-glyph metrics collection by streaming laid-out glyphs. + /// + /// The shaped and line-broken text box. + /// The text options used for layout. + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The line index to collect, or -1 to collect every line. + /// The positioned glyph metrics. + internal static GlyphMetrics[] GetGlyphMetricsArray( + TextBox textBox, + TextOptions options, + float wrappingLength, + int lineIndex = -1) + { + int count = lineIndex < 0 ? textBox.CountGlyphLayouts() : textBox.TextLines[lineIndex].CountGlyphLayouts(); + if (count == 0) + { + return []; + } + + GlyphMetrics[] result = new GlyphMetrics[count]; + GlyphMetricsVisitor visitor = new(result, options.Dpi, lineIndex); + TextLayout.LayoutText(textBox, options, wrappingLength, ref visitor); + return result; + } + + /// + /// Measures the logical advance of an already line-broken text box. + /// + /// The shaped and line-broken text box. + /// The target DPI. + /// Whether the layout direction is horizontal. + /// The logical advance rectangle. + private static FontRectangle GetAdvance(TextBox textBox, float dpi, bool isHorizontalLayout) + { + if (textBox.TextLines.Count == 0) + { + return FontRectangle.Empty; + } + + if (isHorizontalLayout) + { + float width = 0; + float height = 0; + for (int i = 0; i < textBox.TextLines.Count; i++) + { + TextLine line = textBox.TextLines[i]; + width = MathF.Max(width, line.ScaledLineAdvance); + height += line.ScaledMaxLineHeight; + } + + return new FontRectangle(0, 0, width * dpi, height * dpi); + } + + float verticalWidth = 0; + float verticalHeight = 0; + for (int i = 0; i < textBox.TextLines.Count; i++) + { + TextLine line = textBox.TextLines[i]; + verticalWidth += line.ScaledMaxLineHeight; + verticalHeight = MathF.Max(verticalHeight, line.ScaledLineAdvance); + } + + return new FontRectangle(0, 0, verticalWidth * dpi, verticalHeight * dpi); + } + } +} diff --git a/SixLabors.Fonts/TextBox.cs b/SixLabors.Fonts/TextBox.cs new file mode 100644 index 0000000..75a7064 --- /dev/null +++ b/SixLabors.Fonts/TextBox.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Linq; + +namespace SixLabors.Fonts { + /// + /// Represents a shaped and line-broken block of text. + /// + internal sealed class TextBox + { + private readonly TextDirection textDirection; + + private float? scaledMaxAdvance; + + private float? minY; + + private int glyphLayoutCount; + + private bool hasGlyphLayoutCounts; + + /// + /// Initializes a new instance of the class. + /// + /// The shaped, line-broken lines that make up this text box. + /// The block-level text direction. + public TextBox(IReadOnlyList textLines, TextDirection textDirection) + { + this.TextLines = textLines; + this.textDirection = textDirection; + } + + /// + /// Gets the shaped and line-broken lines that make up the text. + /// + public IReadOnlyList TextLines { get; } + + /// + /// Returns the widest scaled line advance across all lines. The result is memoized. + /// + /// The widest scaled line advance. + public float ScaledMaxAdvance() + => this.scaledMaxAdvance ??= this.TextLines.Max(x => x.ScaledLineAdvance); + + /// + /// Returns the smallest (most negative) scaled Y position encountered across all lines. + /// Used to detect ink that extends above the typographic ascender (stacked marks in Tibetan etc.). + /// The result is memoized. + /// + /// The smallest scaled Y position in the text box. + public float ScaledMinY() + => this.minY ??= this.TextLines.Min(x => x.ScaledMinY); + + /// + /// Counts all glyph entries emitted from this text box. The result is memoized. + /// + /// The number of glyph entries that layout will emit. + public int CountGlyphLayouts() + => this.hasGlyphLayoutCounts ? this.glyphLayoutCount : this.CountGlyphLayoutsCore(); + + /// + /// Computes the glyph-layout count in one pass. + /// + /// The number of glyph entries that layout will emit. + private int CountGlyphLayoutsCore() + { + int count = 0; + for (int i = 0; i < this.TextLines.Count; i++) + { + count += this.TextLines[i].CountGlyphLayouts(); + } + + this.glyphLayoutCount = count; + this.hasGlyphLayoutCounts = true; + return count; + } + + /// + /// Returns the block-level text direction used for alignment calculations. + /// + /// The block-level text direction. + public TextDirection TextDirection() => this.textDirection; + } +} diff --git a/SixLabors.Fonts/TextDecorations.cs b/SixLabors.Fonts/TextDecorations.cs new file mode 100644 index 0000000..a574d56 --- /dev/null +++ b/SixLabors.Fonts/TextDecorations.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts { + /// + /// Provides enumeration of various text decorations. + /// + [Flags] + public enum TextDecorations + { + /// + /// No attributes are applied + /// + None = 0, + + /// + /// The text is underlined + /// + Underline = 1 << 0, + + /// + /// The text contains a horizontal line through the center. + /// + Strikeout = 1 << 1, + + /// + /// The text contains a horizontal line above it + /// + Overline = 1 << 2 + } +} diff --git a/SixLabors.Fonts/TextDirection.cs b/SixLabors.Fonts/TextDirection.cs new file mode 100644 index 0000000..4417446 --- /dev/null +++ b/SixLabors.Fonts/TextDirection.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Specifies the writing direction for text. + /// + public enum TextDirection + { + /// + /// Left to right. + /// + LeftToRight = 0, + + /// + /// Right to left. + /// + RightToLeft = 1, + + /// + /// Automatically determined. + /// + Auto = 2, + } +} diff --git a/SixLabors.Fonts/TextEllipsis.cs b/SixLabors.Fonts/TextEllipsis.cs new file mode 100644 index 0000000..6b13541 --- /dev/null +++ b/SixLabors.Fonts/TextEllipsis.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Specifies ellipsis behavior when laid-out text is limited to a maximum number of lines. + /// + public enum TextEllipsis + { + /// + /// Do not insert an ellipsis marker. + /// + None = 0, + + /// + /// Insert the standard ellipsis marker. + /// + Standard, + + /// + /// Insert the marker specified by . + /// + Custom + } +} diff --git a/SixLabors.Fonts/TextHit.cs b/SixLabors.Fonts/TextHit.cs new file mode 100644 index 0000000..e109861 --- /dev/null +++ b/SixLabors.Fonts/TextHit.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Represents a hit-tested grapheme position in laid-out text. + /// + public readonly struct TextHit + { + /// + /// Initializes a new instance of the struct. + /// + /// The zero-based line index. + /// The grapheme index in the original text. + /// The UTF-16 index in the original text. + /// Whether the hit is on the trailing side of the grapheme. + internal TextHit(int lineIndex, int graphemeIndex, int stringIndex, bool isTrailing) + { + this.LineIndex = lineIndex; + this.GraphemeIndex = graphemeIndex; + this.StringIndex = stringIndex; + this.IsTrailing = isTrailing; + } + + /// + /// Gets the zero-based line index. + /// + public int LineIndex { get; } + + /// + /// Gets the zero-based grapheme index in the original text. + /// + public int GraphemeIndex { get; } + + /// + /// Gets the zero-based UTF-16 code unit index in the original text. + /// + public int StringIndex { get; } + + /// + /// Gets the grapheme insertion index represented by this hit. + /// + public int GraphemeInsertionIndex => this.GraphemeIndex + (this.IsTrailing ? 1 : 0); + + /// + /// Gets a value indicating whether the hit is on the trailing side of the grapheme. + /// + public bool IsTrailing { get; } + } +} diff --git a/SixLabors.Fonts/TextHyphenation.cs b/SixLabors.Fonts/TextHyphenation.cs new file mode 100644 index 0000000..223af03 --- /dev/null +++ b/SixLabors.Fonts/TextHyphenation.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Specifies hyphenation marker behavior when text breaks at hyphenation opportunities. + /// + public enum TextHyphenation + { + /// + /// Do not insert a hyphenation marker. + /// + None = 0, + + /// + /// Insert the standard hyphenation marker. + /// + Standard, + + /// + /// Insert the marker specified by . + /// + Custom + } +} diff --git a/SixLabors.Fonts/TextInteraction.cs b/SixLabors.Fonts/TextInteraction.cs new file mode 100644 index 0000000..3198a19 --- /dev/null +++ b/SixLabors.Fonts/TextInteraction.cs @@ -0,0 +1,1446 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.Fonts { + /// + /// Provides shared helpers for text interaction metrics. + /// + /// + /// Text interaction uses grapheme advance rectangles as the logical hit target. Ink bounds can be + /// empty, overhang the advance, or exclude whitespace, which makes them unsuitable for caret + /// positioning and selection highlighting. + /// + internal static class TextInteraction + { + /// + /// Hit tests a point against a complete laid-out text box. + /// + /// All laid-out lines ordered by their visual position. + /// The full grapheme metrics buffer flattened in visual order. + /// The text-space coordinate to resolve to a grapheme hit. + /// The orientation used to interpret the line and grapheme advances. + /// The nearest grapheme hit. + public static TextHit HitTest( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + Vector2 point, + LayoutMode layoutMode) + { + if (lines.IsEmpty || graphemes.IsEmpty) + { + return new(-1, -1, -1, false); + } + + bool isHorizontal = layoutMode.IsHorizontal(); + int lineIndex = FindLine(lines, point, isHorizontal); + + // LineMetrics preserve their source line index, while grapheme metrics are emitted in + // visual line order. Locate the line slice by source range so reverse line-order modes + // pair the hit-tested line with its own graphemes. + int graphemeOffset = GetGraphemeOffset(lines[lineIndex]); + ReadOnlySpan lineGraphemes = graphemes.Slice(graphemeOffset, lines[lineIndex].GraphemeCount); + + return HitTestLine(lineIndex, lineGraphemes, point, isHorizontal); + } + + /// + /// Hit tests a point against one laid-out line. + /// + /// The zero-based visual index of the line being hit tested. + /// Only the grapheme metrics belonging to the target line. + /// The coordinate to compare against the line's primary advance axis. + /// The line orientation that determines which axis is primary. + /// The nearest grapheme hit. + public static TextHit HitTestLine( + int lineIndex, + ReadOnlySpan graphemes, + Vector2 point, + LayoutMode layoutMode) + => HitTestLine(lineIndex, graphemes, point, layoutMode.IsHorizontal()); + + /// + /// Gets a caret position from a complete laid-out text box. + /// + /// All laid-out lines available for caret placement. + /// The flattened grapheme metrics that back the full text box. + /// The logical insertion position to convert into a visual caret. + /// The layout orientation used when the caret geometry was calculated. + /// The caret position in pixel units. + public static CaretPosition GetCaretPosition( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + int graphemeIndex, + LayoutMode layoutMode) + { + if (lines.IsEmpty || graphemes.IsEmpty) + { + return new(-1, -1, -1, default, default, false, default, default, 0); + } + + int lineIndex = FindLineByGraphemeIndex(lines, graphemeIndex); + LineMetrics line = lines[lineIndex]; + + // See HitTest: line source indices and flattened storage offsets are deliberately + // separate because bidi reordering can make source order differ from visual order. + int graphemeOffset = GetGraphemeOffset(line); + ReadOnlySpan lineGraphemes = graphemes.Slice(graphemeOffset, line.GraphemeCount); + + return GetCaretPositionLine(lineIndex, line, lineGraphemes, graphemeIndex, layoutMode); + } + + /// + /// Gets a caret position from one laid-out line. + /// + /// The zero-based visual index of the supplied line. + /// The metrics for the single line that will host the caret. + /// The visual-order grapheme metrics for that one line. + /// The logical insertion position to place within the supplied line. + /// The orientation that determines the caret edge direction. + /// The caret position in pixel units. + public static CaretPosition GetCaretPositionLine( + int lineIndex, + in LineMetrics line, + ReadOnlySpan graphemes, + int graphemeIndex, + LayoutMode layoutMode) + { + if (graphemes.IsEmpty) + { + return new(lineIndex, line.GraphemeIndex, line.StringIndex, default, default, false, default, default, 0); + } + + return CreateCaret(lineIndex, line, graphemes, graphemeIndex, layoutMode.IsHorizontal()); + } + + /// + /// Gets an absolute caret position from a complete laid-out text box. + /// + /// All laid-out lines available for caret placement. + /// The flattened grapheme metrics that back the full text box. + /// The absolute placement within the text box. + /// The layout orientation used when the caret geometry was calculated. + /// The resolved text direction used to choose the visual start or end of the scope. + /// The caret position in pixel units. + public static CaretPosition GetCaret( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + CaretPlacement placement, + LayoutMode layoutMode, + TextDirection direction) + { + if (lines.IsEmpty || graphemes.IsEmpty) + { + return new(-1, -1, -1, default, default, false, default, default, 0); + } + + int targetGraphemeIndex = placement == CaretPlacement.Start + ? GetSourceTextStart(graphemes) + : GetSourceTextEnd(graphemes); + + int lineIndex = FindLineByGraphemeIndex(lines, targetGraphemeIndex); + + LineMetrics line = lines[lineIndex]; + int graphemeOffset = GetGraphemeOffset(line); + ReadOnlySpan lineGraphemes = graphemes.Slice(graphemeOffset, line.GraphemeCount); + + return GetCaretLine(lineIndex, line, lineGraphemes, placement, layoutMode, direction); + } + + /// + /// Gets an absolute caret position from one laid-out line. + /// + /// The zero-based visual index of the supplied line. + /// The metrics for the single line that will host the caret. + /// The visual-order grapheme metrics for that one line. + /// The absolute placement within the line. + /// The orientation that determines the caret edge direction. + /// The resolved text direction used to choose the visual start or end of the scope. + /// The caret position in pixel units. + public static CaretPosition GetCaretLine( + int lineIndex, + in LineMetrics line, + ReadOnlySpan graphemes, + CaretPlacement placement, + LayoutMode layoutMode, + TextDirection direction) + { + if (graphemes.IsEmpty) + { + return new(lineIndex, line.GraphemeIndex, line.StringIndex, default, default, false, default, default, 0); + } + + return CreateCaretAtVisualLineEdge(lineIndex, line, graphemes, placement, layoutMode.IsHorizontal(), direction); + } + + /// + /// Moves a caret within a complete laid-out text box. + /// + /// The visual lines across which the caret may move. + /// The flattened grapheme metrics used to resolve movement targets. + /// The source-order word-boundary segment metrics used for word movement. + /// The starting caret location before applying the movement. + /// The requested caret navigation command. + /// The orientation rules that control horizontal versus vertical motion. + /// The resolved text direction used to choose line and text start/end. + /// The moved caret position in pixel units. + public static CaretPosition MoveCaret( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + ReadOnlySpan wordMetrics, + CaretPosition caret, + CaretMovement movement, + LayoutMode layoutMode, + TextDirection direction) + { + if (lines.IsEmpty || graphemes.IsEmpty) + { + return caret; + } + + bool isHorizontal = layoutMode.IsHorizontal(); + int lineIndex = GetCaretLineIndex(lines, graphemes, caret); + LineMetrics line = lines[lineIndex]; + int graphemeOffset = GetGraphemeOffset(line); + ReadOnlySpan lineGraphemes = graphemes.Slice(graphemeOffset, line.GraphemeCount); + int target = caret.GraphemeIndex; + switch (movement) + { + case CaretMovement.Previous: + target = GetPreviousInsertionIndex(graphemes, caret.GraphemeIndex, GetSourceTextStart(graphemes)); + break; + + case CaretMovement.Next: + target = GetNextInsertionIndex(graphemes, caret.GraphemeIndex, GetSourceTextEnd(graphemes)); + break; + + case CaretMovement.PreviousWord: + target = GetPreviousWordBoundary(wordMetrics, caret.GraphemeIndex, GetSourceTextStart(graphemes)); + break; + + case CaretMovement.NextWord: + target = GetNextWordBoundary(wordMetrics, caret.GraphemeIndex, GetSourceTextEnd(graphemes)); + break; + + case CaretMovement.LineStart: + return GetCaretLine(lineIndex, line, lineGraphemes, CaretPlacement.Start, layoutMode, direction); + + case CaretMovement.LineEnd: + return GetCaretLine(lineIndex, line, lineGraphemes, CaretPlacement.End, layoutMode, direction); + + case CaretMovement.TextStart: + return GetCaret(lines, graphemes, CaretPlacement.Start, layoutMode, direction); + + case CaretMovement.TextEnd: + return GetCaret(lines, graphemes, CaretPlacement.End, layoutMode, direction); + + case CaretMovement.LineUp: + return MoveCaretToAdjacentLine( + lines, + graphemes, + caret, + lineIndex, + lineDown: false, + isHorizontal: isHorizontal, + layoutMode: layoutMode); + + case CaretMovement.LineDown: + return MoveCaretToAdjacentLine( + lines, + graphemes, + caret, + lineIndex, + lineDown: true, + isHorizontal: isHorizontal, + layoutMode: layoutMode); + } + + return GetCaretPosition(lines, graphemes, target, layoutMode); + } + + /// + /// Moves a caret within one laid-out line. + /// + /// The zero-based visual index of the current line. + /// The line metrics that constrain the movement. + /// The grapheme metrics available within that line. + /// The source-order word-boundary segment metrics used for word movement. + /// The caret location to move inside the line. + /// The in-line caret navigation command to execute. + /// The orientation used to choose the caret axis within the line. + /// The resolved text direction used to choose line start/end. + /// The moved caret position in pixel units. + public static CaretPosition MoveCaretLine( + int lineIndex, + in LineMetrics line, + ReadOnlySpan graphemes, + ReadOnlySpan wordMetrics, + CaretPosition caret, + CaretMovement movement, + LayoutMode layoutMode, + TextDirection direction) + { + if (graphemes.IsEmpty) + { + return caret; + } + + int lineStart = GetSourceLineStart(graphemes); + int lineEnd = GetSourceLineEnd(graphemes); + int target = caret.GraphemeIndex; + switch (movement) + { + case CaretMovement.Previous: + target = GetPreviousInsertionIndex(graphemes, caret.GraphemeIndex, lineStart); + break; + + case CaretMovement.Next: + target = GetNextInsertionIndex(graphemes, caret.GraphemeIndex, lineEnd); + break; + + case CaretMovement.PreviousWord: + target = Math.Max( + lineStart, + GetPreviousWordBoundary(wordMetrics, caret.GraphemeIndex, lineStart)); + break; + + case CaretMovement.NextWord: + target = Math.Min( + lineEnd, + GetNextWordBoundary(wordMetrics, caret.GraphemeIndex, lineEnd)); + break; + + case CaretMovement.LineStart: + case CaretMovement.TextStart: + return GetCaretLine(lineIndex, line, graphemes, CaretPlacement.Start, layoutMode, direction); + + case CaretMovement.LineEnd: + case CaretMovement.TextEnd: + return GetCaretLine(lineIndex, line, graphemes, CaretPlacement.End, layoutMode, direction); + + case CaretMovement.LineUp: + case CaretMovement.LineDown: + return caret; + } + + return GetCaretPositionLine(lineIndex, line, graphemes, target, layoutMode); + } + + /// + /// Gets the word-boundary segment metrics containing the supplied grapheme insertion index. + /// + /// The source-order word metrics to search. + /// The grapheme insertion index to locate. + /// The matching word metrics. + public static WordMetrics GetWordMetrics(ReadOnlySpan wordMetrics, int graphemeIndex) + { + if (wordMetrics.IsEmpty) + { + return default; + } + + for (int i = 0; i < wordMetrics.Length; i++) + { + WordMetrics metrics = wordMetrics[i]; + if (graphemeIndex >= metrics.GraphemeStart && graphemeIndex < metrics.GraphemeEnd) + { + return metrics; + } + + if (graphemeIndex < metrics.GraphemeStart) + { + return metrics; + } + } + + return wordMetrics[^1]; + } + + /// + /// Gets selection rectangles from a complete laid-out text box. + /// + /// The visual lines that may contribute selection rectangles. + /// The flattened grapheme metrics scanned for the selected range. + /// The first source grapheme insertion boundary in the selection. + /// The final source grapheme insertion boundary in the selection. + /// The orientation used when converting ranges into rectangles. + /// A read-only memory region containing the selection rectangles in visual order. + public static ReadOnlyMemory GetSelectionBounds( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + int graphemeStart, + int graphemeEnd, + LayoutMode layoutMode) + { + if (lines.IsEmpty || graphemes.IsEmpty || graphemeStart == graphemeEnd) + { + return ReadOnlyMemory.Empty; + } + + int selectionStart = Math.Min(graphemeStart, graphemeEnd); + int selectionEnd = Math.Max(graphemeStart, graphemeEnd); + int rectangleCount = CountSelectionBounds(lines, graphemes, selectionStart, selectionEnd); + if (rectangleCount == 0) + { + return ReadOnlyMemory.Empty; + } + + FontRectangle[] result = new FontRectangle[rectangleCount]; + int count = 0; + bool isHorizontal = layoutMode.IsHorizontal(); + + for (int i = 0; i < lines.Length; i++) + { + LineMetrics line = lines[i]; + int graphemeOffset = GetGraphemeOffset(line); + ReadOnlySpan lineGraphemes = graphemes.Slice(graphemeOffset, line.GraphemeCount); + if (CountSelectionBoundsLine(lineGraphemes, selectionStart, selectionEnd) == 0) + { + continue; + } + + count += FillSelectionBoundsLine(line, lineGraphemes, selectionStart, selectionEnd, isHorizontal, result.AsSpan(count)); + } + + return result; + } + + /// + /// Gets selection rectangles for one laid-out line. + /// + /// The single line for which selection rectangles are produced. + /// The line-local grapheme metrics scanned in visual order. + /// The first source grapheme insertion boundary applied to this line. + /// The final source grapheme insertion boundary applied to this line. + /// The orientation used to map the selected run onto the line box. + /// A read-only memory region containing the line selection rectangles in visual order. + public static ReadOnlyMemory GetSelectionBoundsLine( + in LineMetrics line, + ReadOnlySpan graphemes, + int graphemeStart, + int graphemeEnd, + LayoutMode layoutMode) + { + if (graphemes.IsEmpty || graphemeStart == graphemeEnd) + { + return ReadOnlyMemory.Empty; + } + + int selectionStart = Math.Min(graphemeStart, graphemeEnd); + int selectionEnd = Math.Max(graphemeStart, graphemeEnd); + int count = CountSelectionBoundsLine(graphemes, selectionStart, selectionEnd); + if (count == 0) + { + return ReadOnlyMemory.Empty; + } + + FontRectangle[] result = new FontRectangle[count]; + _ = FillSelectionBoundsLine(line, graphemes, selectionStart, selectionEnd, layoutMode.IsHorizontal(), result); + return result; + } + + /// + /// Gets selection bounds for one measured grapheme. + /// + /// The visual lines used to find the grapheme's line box. + /// The flattened grapheme metrics that back the full text box. + /// The measured grapheme to select. + /// The orientation used to map the grapheme advance onto the line box. + /// A read-only memory region containing the grapheme selection bounds. + public static ReadOnlyMemory GetSelectionBounds( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + in GraphemeMetrics grapheme, + LayoutMode layoutMode) + { + if (lines.IsEmpty || graphemes.IsEmpty) + { + return ReadOnlyMemory.Empty; + } + + int lineIndex = FindLineByGraphemeIndex(lines, grapheme.GraphemeIndex); + FontRectangle[] result = [CreateSelectionBounds(lines[lineIndex], grapheme, layoutMode.IsHorizontal())]; + return result; + } + + /// + /// Gets selection bounds for one measured grapheme within one laid-out line. + /// + /// The line that provides the cross-axis selection extent. + /// The measured grapheme to select. + /// The orientation used to map the grapheme advance onto the line box. + /// A read-only memory region containing the grapheme selection bounds. + public static ReadOnlyMemory GetSelectionBoundsLine( + in LineMetrics line, + in GraphemeMetrics grapheme, + LayoutMode layoutMode) + { + FontRectangle[] result = [CreateSelectionBounds(line, grapheme, layoutMode.IsHorizontal())]; + return result; + } + + /// + /// Finds the visual line nearest to a point. + /// + /// The candidate visual lines to compare with the point. + /// The coordinate whose cross-axis position selects the nearest line. + /// Indicates whether line advances are measured along the x-axis. + /// The nearest line index. + private static int FindLine( + ReadOnlySpan lines, + Vector2 point, + bool isHorizontal) + { + float cross = isHorizontal ? point.Y : point.X; + for (int i = 0; i < lines.Length; i++) + { + float lineStart = isHorizontal ? lines[i].Start.Y : lines[i].Start.X; + float lineEnd = isHorizontal ? lines[i].Start.Y + lines[i].Extent.Y : lines[i].Start.X + lines[i].Extent.X; + if (cross >= lineStart && cross < lineEnd) + { + return i; + } + } + + float lineFirstStart = isHorizontal ? lines[0].Start.Y : lines[0].Start.X; + return cross < lineFirstStart ? 0 : lines.Length - 1; + } + + /// + /// Finds the line that owns the supplied grapheme index. + /// + /// The visual lines whose source ranges are searched. + /// The source grapheme index to locate. + /// The nearest owning line index. + private static int FindLineByGraphemeIndex( + ReadOnlySpan lines, + int graphemeIndex) + { + for (int i = 0; i < lines.Length; i++) + { + LineMetrics line = lines[i]; + int lineStart = line.GraphemeIndex; + int lineEnd = lineStart + line.GraphemeCount; + if (graphemeIndex >= lineStart && graphemeIndex <= lineEnd) + { + return i; + } + } + + return 0; + } + + /// + /// Hit tests a point against one laid-out line after the layout mode has been normalized. + /// + /// The zero-based visual index of the normalized line. + /// The grapheme metrics already isolated for that line. + /// The coordinate to compare with each grapheme advance rectangle. + /// Indicates whether the primary hit-test axis is horizontal. + /// The nearest grapheme hit. + private static TextHit HitTestLine( + int lineIndex, + ReadOnlySpan graphemes, + Vector2 point, + bool isHorizontal) + { + int index = FindNearestGrapheme(graphemes, isHorizontal ? point.X : point.Y, isHorizontal); + GraphemeMetrics grapheme = graphemes[index]; + FontRectangle advance = grapheme.Advance; + float midpoint = isHorizontal + ? advance.Left + (advance.Width * 0.5F) + : advance.Top + (advance.Height * 0.5F); + float primary = isHorizontal ? point.X : point.Y; + bool trailing = IsRightToLeft(grapheme) + ? primary < midpoint + : primary >= midpoint; + + return new(lineIndex, grapheme.GraphemeIndex, grapheme.StringIndex, trailing); + } + + /// + /// Creates a caret line for a grapheme insertion index. + /// + /// The zero-based visual index of the caret's line. + /// The line metrics used to size the caret segment. + /// The line-local grapheme metrics searched for neighboring edges. + /// The logical insertion position to materialize as a caret. + /// Indicates whether the caret spans vertically or horizontally. + /// The caret position in pixel units. + private static CaretPosition CreateCaret( + int lineIndex, + in LineMetrics line, + ReadOnlySpan graphemes, + int graphemeIndex, + bool isHorizontal) + { + int previousIndex = FindGraphemeBySourceIndex(graphemes, graphemeIndex - 1); + int nextIndex = FindGraphemeBySourceIndex(graphemes, graphemeIndex); + + if (nextIndex < 0 && previousIndex < 0) + { + int nearestIndex = FindNearestGraphemeIndex(graphemes, graphemeIndex); + GraphemeMetrics nearest = graphemes[nearestIndex]; + bool trailing = graphemeIndex > nearest.GraphemeIndex; + CreateCaretEdge(line, nearest, trailing, isHorizontal, out Vector2 start, out Vector2 end); + + return new( + lineIndex, + graphemeIndex, + nearest.StringIndex, + start, + end, + false, + default, + default, + GetLineNavigationPosition(start, isHorizontal)); + } + + if (nextIndex >= 0) + { + GraphemeMetrics next = graphemes[nextIndex]; + CreateCaretEdge(line, next, trailing: false, isHorizontal, out Vector2 start, out Vector2 end); + + if (previousIndex >= 0) + { + GraphemeMetrics previous = graphemes[previousIndex]; + CreateCaretEdge(line, previous, trailing: true, isHorizontal, out Vector2 secondaryStart, out Vector2 secondaryEnd); + + // At a bidi boundary the same logical insertion point has one visual edge on + // each neighboring run. Return both instead of asking callers to choose affinity. + if (start != secondaryStart || end != secondaryEnd) + { + return new( + lineIndex, + graphemeIndex, + next.StringIndex, + start, + end, + true, + secondaryStart, + secondaryEnd, + GetLineNavigationPosition(start, isHorizontal)); + } + } + + return new( + lineIndex, + graphemeIndex, + next.StringIndex, + start, + end, + false, + default, + default, + GetLineNavigationPosition(start, isHorizontal)); + } + + GraphemeMetrics previousOnly = graphemes[previousIndex]; + + // Editor-mode hard breaks can create a blank visual line whose only source + // ownership is the preceding newline grapheme. A caret requested immediately + // after that grapheme should sit at the start of the blank line, not after + // the newline marker's trimmed layout box. + if (previousOnly.IsLineBreak && graphemeIndex == previousOnly.GraphemeIndex + 1) + { + Vector2 start; + Vector2 end; + if (isHorizontal) + { + float x = IsRightToLeft(previousOnly) ? line.Start.X + line.Extent.X : line.Start.X; + start = new Vector2(x, line.Start.Y); + end = new Vector2(x, line.Start.Y + line.Extent.Y); + } + else + { + float y = IsRightToLeft(previousOnly) ? line.Start.Y + line.Extent.Y : line.Start.Y; + start = new Vector2(line.Start.X, y); + end = new Vector2(line.Start.X + line.Extent.X, y); + } + + // The newline grapheme gives the blank line source ownership, but the + // editable insertion point after Enter belongs at the new line start. + return new( + lineIndex, + graphemeIndex, + previousOnly.StringIndex, + start, + end, + false, + default, + default, + GetLineNavigationPosition(start, isHorizontal)); + } + + CreateCaretEdge(line, previousOnly, trailing: true, isHorizontal, out Vector2 primaryStart, out Vector2 primaryEnd); + + return new( + lineIndex, + graphemeIndex, + previousOnly.StringIndex, + primaryStart, + primaryEnd, + false, + default, + default, + GetLineNavigationPosition(primaryStart, isHorizontal)); + } + + /// + /// Creates one visual caret edge for a grapheme. + /// + /// The containing line that defines the caret span. + /// The grapheme whose leading or trailing edge is used. + /// Specifies whether the logical trailing side should be chosen. + /// Indicates whether caret edges vary along the x-axis. + /// Receives the first endpoint of the caret segment. + /// Receives the second endpoint of the caret segment. + private static void CreateCaretEdge( + in LineMetrics line, + in GraphemeMetrics grapheme, + bool trailing, + bool isHorizontal, + out Vector2 start, + out Vector2 end) + { + FontRectangle advance = grapheme.Advance; + bool useEnd = IsRightToLeft(grapheme) ? !trailing : trailing; + + if (isHorizontal) + { + // Bidi layout can produce negative advance widths. Left/Right are + // rectangle construction edges in that case, so choose the physical + // min/max x edge after logical leading/trailing has been resolved. + float physicalStart = MathF.Min(advance.Left, advance.Right); + float physicalEnd = MathF.Max(advance.Left, advance.Right); + float x = useEnd ? physicalEnd : physicalStart; + + start = new Vector2(x, line.Start.Y); + end = new Vector2(x, line.Start.Y + line.Extent.Y); + return; + } + + float physicalTop = MathF.Min(advance.Top, advance.Bottom); + float physicalBottom = MathF.Max(advance.Top, advance.Bottom); + float y = useEnd ? physicalBottom : physicalTop; + + start = new Vector2(line.Start.X, y); + end = new Vector2(line.Start.X + line.Extent.X, y); + } + + /// + /// Creates a caret at the source start or end boundary of a laid-out line. + /// + /// The zero-based visual index of the line. + /// The line metrics used to size the caret segment. + /// The line-local grapheme metrics in visual order. + /// The source boundary to place within the line. + /// Indicates whether the caret spans vertically or horizontally. + /// The resolved text direction used to choose the visual start or end of the scope. + /// The caret position at the requested line boundary. + private static CaretPosition CreateCaretAtVisualLineEdge( + int lineIndex, + in LineMetrics line, + ReadOnlySpan graphemes, + CaretPlacement placement, + bool isHorizontal, + TextDirection direction) + { + bool isStart = placement == CaretPlacement.Start; + int insertionIndex = isStart ? GetSourceLineStart(graphemes) : GetSourceLineEnd(graphemes); + int visualIndex = FindGraphemeBySourceIndex(graphemes, isStart ? insertionIndex : insertionIndex - 1); + GraphemeMetrics grapheme = graphemes[visualIndex]; + bool isRightToLeft = direction == TextDirection.RightToLeft; + bool useEnd = isStart == isRightToLeft; + + // Start/end placement is anchored to the source boundary grapheme for + // the returned insertion index, but the visible caret sits on the line + // box edge. The resolved paragraph direction chooses which physical + // line edge represents start or end. + Vector2 start; + Vector2 end; + if (isHorizontal) + { + float x = useEnd ? line.Start.X + line.Extent.X : line.Start.X; + start = new Vector2(x, line.Start.Y); + end = new Vector2(x, line.Start.Y + line.Extent.Y); + } + else + { + float y = useEnd ? line.Start.Y + line.Extent.Y : line.Start.Y; + start = new Vector2(line.Start.X, y); + end = new Vector2(line.Start.X + line.Extent.X, y); + } + + return new( + lineIndex, + insertionIndex, + grapheme.StringIndex, + start, + end, + false, + default, + default, + GetLineNavigationPosition(start, isHorizontal)); + } + + /// + /// Moves the caret to the nearest matching position on an adjacent visual line. + /// + /// The set of visual lines available for adjacent-line navigation. + /// The flattened grapheme metrics used to resolve the new caret target. + /// The caret location before moving to the neighbor line. + /// The visual index of the line that currently contains the caret. + /// Specifies whether movement is toward the next visual line. + /// Indicates whether preserved column data uses the x-axis. + /// The orientation used when reconstructing the destination caret. + /// The moved caret position in pixel units. + private static CaretPosition MoveCaretToAdjacentLine( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + CaretPosition caret, + int lineIndex, + bool lineDown, + bool isHorizontal, + LayoutMode layoutMode) + { + int targetLineIndex = FindAdjacentLine(lines, lineIndex, lineDown, isHorizontal); + if (targetLineIndex == lineIndex) + { + return caret; + } + + LineMetrics targetLine = lines[targetLineIndex]; + int graphemeOffset = GetGraphemeOffset(targetLine); + ReadOnlySpan targetGraphemes = graphemes.Slice(graphemeOffset, targetLine.GraphemeCount); + + Vector2 hitPoint = isHorizontal + ? new(caret.LineNavigationPosition, targetLine.Start.Y + (targetLine.Extent.Y * 0.5F)) + : new(targetLine.Start.X + (targetLine.Extent.X * 0.5F), caret.LineNavigationPosition); + + TextHit hit = HitTestLineForCaretNavigation(targetLineIndex, targetGraphemes, hitPoint, isHorizontal); + CaretPosition moved = GetCaretPositionLine( + targetLineIndex, + targetLine, + targetGraphemes, + hit.GraphemeInsertionIndex, + layoutMode); + + // Preserve the original requested line position so repeated LineUp/LineDown movement + // returns to the same visual column after passing through shorter lines. + return WithLineNavigationPosition(moved, caret.LineNavigationPosition); + } + + /// + /// Hit tests a line for keyboard caret navigation. + /// + /// The zero-based visual index of the line being navigated. + /// The line-local grapheme metrics considered as navigation targets. + /// The projected point used to preserve visual column alignment. + /// Indicates whether navigation compares x coordinates first. + /// The nearest grapheme hit. + private static TextHit HitTestLineForCaretNavigation( + int lineIndex, + ReadOnlySpan graphemes, + Vector2 point, + bool isHorizontal) + { + int index = FindNearestCaretNavigationGrapheme(graphemes, isHorizontal ? point.X : point.Y, isHorizontal); + GraphemeMetrics grapheme = graphemes[index]; + FontRectangle advance = grapheme.Advance; + float midpoint = isHorizontal + ? advance.Left + (advance.Width * 0.5F) + : advance.Top + (advance.Height * 0.5F); + float primary = isHorizontal ? point.X : point.Y; + bool trailing = IsRightToLeft(grapheme) + ? primary < midpoint + : primary >= midpoint; + + return new(lineIndex, grapheme.GraphemeIndex, grapheme.StringIndex, trailing); + } + + /// + /// Finds the nearest grapheme that should participate in keyboard caret navigation. + /// + /// The visual-order graphemes filtered for caret navigation. + /// The coordinate on the primary advance axis to compare. + /// Indicates whether the primary axis maps to horizontal movement. + /// The nearest grapheme metrics index within . + private static int FindNearestCaretNavigationGrapheme( + ReadOnlySpan graphemes, + float primary, + bool isHorizontal) + { + int first = -1; + int last = -1; + for (int i = 0; i < graphemes.Length; i++) + { + first = first < 0 ? i : first; + last = i; + + FontRectangle advance = graphemes[i].Advance; + float start = isHorizontal ? advance.Left : advance.Top; + float end = isHorizontal ? advance.Right : advance.Bottom; + if (primary >= start && primary < end) + { + return i; + } + } + + FontRectangle firstAdvance = graphemes[first].Advance; + float firstStart = isHorizontal ? firstAdvance.Left : firstAdvance.Top; + return primary < firstStart ? first : last; + } + + /// + /// Finds the adjacent visual line in the requested direction. + /// + /// The visual lines among which an adjacent line is searched. + /// The current visual line index. + /// Specifies whether the search moves forward in visual order. + /// Indicates whether cross-axis distances are measured vertically. + /// The adjacent line index, or when no line exists in that direction. + private static int FindAdjacentLine( + ReadOnlySpan lines, + int lineIndex, + bool lineDown, + bool isHorizontal) + { + float currentStart = GetLineCrossStart(lines[lineIndex], isHorizontal); + float currentEnd = GetLineCrossEnd(lines[lineIndex], isHorizontal); + int targetLineIndex = lineIndex; + float bestDistance = float.MaxValue; + for (int i = 0; i < lines.Length; i++) + { + if (i == lineIndex) + { + continue; + } + + float distance = lineDown + ? GetLineCrossStart(lines[i], isHorizontal) - currentEnd + : currentStart - GetLineCrossEnd(lines[i], isHorizontal); + + if (distance >= 0 && distance < bestDistance) + { + targetLineIndex = i; + bestDistance = distance; + } + } + + return targetLineIndex; + } + + /// + /// Gets a valid line index for the supplied caret. + /// + /// The laid-out lines used to validate the caret's stored line index. + /// The flattened grapheme metrics used to resolve the caret when its line index is stale. + /// The caret whose associated visual line must be resolved. + /// The line index. + private static int GetCaretLineIndex( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + in CaretPosition caret) + { + if ((uint)caret.LineIndex < (uint)lines.Length) + { + return caret.LineIndex; + } + + return FindLineByGraphemeIndex(lines, caret.GraphemeIndex); + } + + /// + /// Gets the nearest Unicode word boundary before the supplied grapheme insertion index. + /// + /// The source-order word metrics to search. + /// The grapheme insertion index to move from. + /// The minimum grapheme insertion index that can be returned. + /// The previous word boundary. + private static int GetPreviousWordBoundary( + ReadOnlySpan wordMetrics, + int graphemeIndex, + int limit) + { + int target = limit; + for (int i = 0; i < wordMetrics.Length; i++) + { + WordMetrics metrics = wordMetrics[i]; + if (metrics.GraphemeStart >= graphemeIndex) + { + break; + } + + target = Math.Max(target, metrics.GraphemeStart); + if (metrics.GraphemeEnd < graphemeIndex) + { + target = Math.Max(target, metrics.GraphemeEnd); + } + } + + return target; + } + + /// + /// Gets the nearest Unicode word boundary after the supplied grapheme insertion index. + /// + /// The source-order word metrics to search. + /// The grapheme insertion index to move from. + /// The maximum grapheme insertion index that can be returned. + /// The next word boundary. + private static int GetNextWordBoundary( + ReadOnlySpan wordMetrics, + int graphemeIndex, + int limit) + { + for (int i = 0; i < wordMetrics.Length; i++) + { + WordMetrics metrics = wordMetrics[i]; + if (metrics.GraphemeStart > graphemeIndex) + { + return Math.Min(limit, metrics.GraphemeStart); + } + + if (metrics.GraphemeEnd > graphemeIndex) + { + return Math.Min(limit, metrics.GraphemeEnd); + } + } + + return limit; + } + + /// + /// Gets the previous measured grapheme insertion index. + /// + /// The grapheme metrics that define valid caret stops. + /// The caret insertion index to move from. + /// The minimum grapheme insertion index that can be returned. + /// The previous measured grapheme insertion index. + private static int GetPreviousInsertionIndex( + ReadOnlySpan graphemes, + int graphemeIndex, + int limit) + { + int target = limit; + for (int i = 0; i < graphemes.Length; i++) + { + int start = graphemes[i].GraphemeIndex; + if (start < graphemeIndex) + { + target = Math.Max(target, start); + } + + // The trailing boundary is derived only from an actual measured grapheme. + // This avoids walking through sparse source indices left by trimmed text. + int end = start + 1; + if (end < graphemeIndex) + { + target = Math.Max(target, end); + } + } + + return target; + } + + /// + /// Gets the next measured grapheme insertion index. + /// + /// The grapheme metrics that define valid caret stops. + /// The caret insertion index to move from. + /// The maximum grapheme insertion index that can be returned. + /// The next measured grapheme insertion index. + private static int GetNextInsertionIndex( + ReadOnlySpan graphemes, + int graphemeIndex, + int limit) + { + int target = limit; + for (int i = 0; i < graphemes.Length; i++) + { + int start = graphemes[i].GraphemeIndex; + if (start > graphemeIndex) + { + target = Math.Min(target, start); + } + + // The trailing boundary is derived only from an actual measured grapheme. + // This avoids walking through sparse source indices left by trimmed text. + int end = start + 1; + if (end > graphemeIndex) + { + target = Math.Min(target, end); + } + } + + return target; + } + + /// + /// Gets the first source grapheme insertion index in the laid-out text. + /// + /// The laid-out grapheme metrics searched for the earliest source insertion point. + /// The source text start insertion index. + private static int GetSourceTextStart(ReadOnlySpan graphemes) + { + int start = graphemes[0].GraphemeIndex; + for (int i = 1; i < graphemes.Length; i++) + { + start = Math.Min(start, graphemes[i].GraphemeIndex); + } + + return start; + } + + /// + /// Gets the final source grapheme insertion index in the laid-out text. + /// + /// The laid-out grapheme metrics searched for the final source insertion point. + /// The source text end insertion index. + private static int GetSourceTextEnd(ReadOnlySpan graphemes) + { + int end = graphemes[0].GraphemeIndex + 1; + for (int i = 1; i < graphemes.Length; i++) + { + end = Math.Max(end, graphemes[i].GraphemeIndex + 1); + } + + return end; + } + + /// + /// Gets the first source grapheme insertion index for a line. + /// + /// The line-local grapheme metrics. + /// The source line start insertion index. + private static int GetSourceLineStart(ReadOnlySpan graphemes) + { + int start = graphemes[0].GraphemeIndex; + for (int i = 1; i < graphemes.Length; i++) + { + start = Math.Min(start, graphemes[i].GraphemeIndex); + } + + return start; + } + + /// + /// Gets the final source grapheme insertion index for a line. + /// + /// The line-local grapheme metrics. + /// The source line end insertion index. + private static int GetSourceLineEnd(ReadOnlySpan graphemes) + { + int end = graphemes[0].GraphemeIndex + 1; + for (int i = 1; i < graphemes.Length; i++) + { + end = Math.Max(end, graphemes[i].GraphemeIndex + 1); + } + + return end; + } + + /// + /// Gets the cross-axis start of a line. + /// + /// The line whose cross-axis origin is requested. + /// Indicates whether the cross axis corresponds to y coordinates. + /// The cross-axis start. + private static float GetLineCrossStart(in LineMetrics line, bool isHorizontal) + => isHorizontal ? line.Start.Y : line.Start.X; + + /// + /// Gets the cross-axis end of a line. + /// + /// The line whose cross-axis limit is requested. + /// Indicates whether the cross axis corresponds to y coordinates. + /// The cross-axis end. + private static float GetLineCrossEnd(in LineMetrics line, bool isHorizontal) + => isHorizontal ? line.Start.Y + line.Extent.Y : line.Start.X + line.Extent.X; + + /// + /// Gets the coordinate to preserve for repeated visual line movement. + /// + /// The primary caret endpoint used to preserve visual column movement. + /// Indicates whether the preserved coordinate is taken from x. + /// The line navigation position. + private static float GetLineNavigationPosition(Vector2 start, bool isHorizontal) + => isHorizontal ? start.X : start.Y; + + /// + /// Creates a copy of the caret with a specific preserved line navigation position. + /// + /// The caret value to clone with updated navigation metadata. + /// The preserved visual column or row coordinate. + /// The caret position. + private static CaretPosition WithLineNavigationPosition( + in CaretPosition caret, + float lineNavigationPosition) + => new( + caret.LineIndex, + caret.GraphemeIndex, + caret.StringIndex, + caret.Start, + caret.End, + caret.HasSecondary, + caret.SecondaryStart, + caret.SecondaryEnd, + lineNavigationPosition); + + /// + /// Fills one line's selection rectangles from visually contiguous selected grapheme advances. + /// + /// The line that will receive one or more selection rectangles. + /// The line-local grapheme metrics grouped into visual runs. + /// The first source grapheme insertion boundary in the selected range. + /// The final source grapheme insertion boundary in the selected range. + /// Indicates whether rectangles expand primarily along x. + /// The destination span that receives the generated rectangles. + /// The number of selection rectangles written. + private static int FillSelectionBoundsLine( + in LineMetrics line, + ReadOnlySpan graphemes, + int selectionStart, + int selectionEnd, + bool isHorizontal, + Span result) + { + int count = 0; + bool hasSelection = false; + float start = 0; + float end = 0; + for (int i = 0; i < graphemes.Length; i++) + { + GraphemeMetrics grapheme = graphemes[i]; + + // Selections are caret boundary ranges: [start, end). A grapheme is selected + // when its source start sits inside that boundary span. + int graphemeStart = grapheme.GraphemeIndex; + bool isSelected = graphemeStart >= selectionStart && graphemeStart < selectionEnd; + if (!isSelected) + { + // A logical range can be visually discontinuous after bidi reordering. Flush at + // the first unselected visual grapheme so selection never covers that gap. + if (hasSelection) + { + result[count++] = CreateSelectionBounds(line, start, end, isHorizontal); + hasSelection = false; + } + + continue; + } + + FontRectangle advance = grapheme.Advance; + float currentStart = isHorizontal ? advance.Left : advance.Top; + float currentEnd = isHorizontal ? advance.Right : advance.Bottom; + if (!hasSelection) + { + start = currentStart; + end = currentEnd; + hasSelection = true; + continue; + } + + start = Math.Min(start, currentStart); + end = Math.Max(end, currentEnd); + } + + if (hasSelection) + { + result[count++] = CreateSelectionBounds(line, start, end, isHorizontal); + } + + return count; + } + + /// + /// Creates a selection rectangle for a contiguous visual run. + /// + /// The containing line used to fill the rectangle on the secondary axis. + /// The first selected coordinate along the primary layout axis. + /// The last selected coordinate along the primary layout axis. + /// Indicates whether the primary axis runs left to right. + /// The selection rectangle in pixel units. + private static FontRectangle CreateSelectionBounds( + in LineMetrics line, + float start, + float end, + bool isHorizontal) + => + isHorizontal + ? FontRectangle.FromLTRB(start, line.Start.Y, end, line.Start.Y + line.Extent.Y) + : FontRectangle.FromLTRB(line.Start.X, start, line.Start.X + line.Extent.X, end); + + /// + /// Creates a selection rectangle for one measured grapheme. + /// + /// The containing line used to fill the rectangle on the secondary axis. + /// The grapheme whose advance defines the primary-axis selection extent. + /// Indicates whether the primary axis runs left to right. + /// The selection rectangle in pixel units. + private static FontRectangle CreateSelectionBounds( + in LineMetrics line, + in GraphemeMetrics grapheme, + bool isHorizontal) + { + FontRectangle advance = grapheme.Advance; + float start = isHorizontal ? advance.Left : advance.Top; + float end = isHorizontal ? advance.Right : advance.Bottom; + return CreateSelectionBounds(line, start, end, isHorizontal); + } + + /// + /// Counts how many selection rectangles are required for a grapheme range. + /// + /// The visual lines searched for selected graphemes. + /// The flattened grapheme metrics used to count visual runs. + /// The first source grapheme insertion boundary used for counting. + /// The final source grapheme insertion boundary used for counting. + /// The number of selection rectangles. + private static int CountSelectionBounds( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + int selectionStart, + int selectionEnd) + { + int count = 0; + for (int i = 0; i < lines.Length; i++) + { + LineMetrics line = lines[i]; + int graphemeOffset = GetGraphemeOffset(line); + ReadOnlySpan lineGraphemes = graphemes.Slice(graphemeOffset, line.GraphemeCount); + + // Source grapheme indices can have gaps because trailing whitespace is trimmed. + // Count actual measured graphemes instead of deriving a dense range from the line. + count += CountSelectionBoundsLine(lineGraphemes, selectionStart, selectionEnd); + } + + return count; + } + + /// + /// Counts visually contiguous selected grapheme runs in one line. + /// + /// The visual-order grapheme metrics for the current line. + /// The first source grapheme insertion boundary applied to that line. + /// The final source grapheme insertion boundary applied to that line. + /// The number of selected visual runs. + private static int CountSelectionBoundsLine( + ReadOnlySpan graphemes, + int selectionStart, + int selectionEnd) + { + int count = 0; + bool hasSelection = false; + for (int i = 0; i < graphemes.Length; i++) + { + GraphemeMetrics grapheme = graphemes[i]; + + // Selections are caret boundary ranges: [start, end). A grapheme is selected + // when its source start sits inside that boundary span. + int graphemeStart = grapheme.GraphemeIndex; + bool isSelected = graphemeStart >= selectionStart && graphemeStart < selectionEnd; + + if (!isSelected) + { + hasSelection = false; + continue; + } + + if (!hasSelection) + { + count++; + hasSelection = true; + } + } + + return count; + } + + /// + /// Finds the grapheme whose advance contains the primary coordinate, or the nearest edge grapheme. + /// + /// The visual-order grapheme metrics searched for a hit target. + /// The coordinate along the primary layout axis. + /// Indicates whether the primary axis is horizontal. + /// The nearest grapheme metrics index within . + private static int FindNearestGrapheme(ReadOnlySpan graphemes, float primary, bool isHorizontal) + { + for (int i = 0; i < graphemes.Length; i++) + { + FontRectangle advance = graphemes[i].Advance; + float start = isHorizontal ? advance.Left : advance.Top; + float end = isHorizontal ? advance.Right : advance.Bottom; + if (primary >= start && primary < end) + { + return i; + } + } + + FontRectangle first = graphemes[0].Advance; + float firstStart = isHorizontal ? first.Left : first.Top; + return primary < firstStart ? 0 : graphemes.Length - 1; + } + + /// + /// Finds the metrics entry for a source grapheme index within one visual line. + /// + /// The visual-order grapheme metrics belonging to one line. + /// The logical grapheme index to look up directly. + /// The grapheme metrics index, or -1 when the grapheme is not in the line. + private static int FindGraphemeBySourceIndex(ReadOnlySpan graphemes, int graphemeIndex) + { + for (int i = 0; i < graphemes.Length; i++) + { + if (graphemes[i].GraphemeIndex == graphemeIndex) + { + return i; + } + } + + return -1; + } + + /// + /// Finds the nearest metrics entry for a source grapheme index within one visual line. + /// + /// The visual-order grapheme metrics used for nearest-index matching. + /// The logical grapheme index whose closest visual entry is needed. + /// The nearest grapheme metrics index within . + private static int FindNearestGraphemeIndex(ReadOnlySpan graphemes, int graphemeIndex) + { + int nearest = 0; + int distance = Math.Abs(graphemes[0].GraphemeIndex - graphemeIndex); + for (int i = 1; i < graphemes.Length; i++) + { + int currentDistance = Math.Abs(graphemes[i].GraphemeIndex - graphemeIndex); + if (currentDistance < distance) + { + nearest = i; + distance = currentDistance; + } + } + + return nearest; + } + + /// + /// Gets a value indicating whether the grapheme advances right-to-left in source order. + /// + /// The grapheme whose resolved bidi level is inspected. + /// when the resolved bidi level is odd. + private static bool IsRightToLeft(in GraphemeMetrics grapheme) + => (grapheme.BidiLevel & 1) != 0; + + /// + /// Gets the offset of a line's graphemes within the flattened metrics array. + /// + /// The line whose stored grapheme offset identifies the desired slice. + /// The flattened grapheme metrics offset. + private static int GetGraphemeOffset(in LineMetrics line) + => line.GraphemeOffset; + } +} diff --git a/SixLabors.Fonts/TextInteractionMode.cs b/SixLabors.Fonts/TextInteractionMode.cs new file mode 100644 index 0000000..a24a8eb --- /dev/null +++ b/SixLabors.Fonts/TextInteractionMode.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Specifies how text interaction positions are modeled for laid-out text. + /// + public enum TextInteractionMode + { + /// + /// Uses paragraph-style interaction where trailing breaking whitespace at line ends does not create additional caret stops. + /// + Paragraph, + + /// + /// Uses editor-style interaction where ordinary trailing breaking whitespace at line ends remains addressable by caret movement and selection. + /// + Editor + } +} diff --git a/SixLabors.Fonts/TextJustification.cs b/SixLabors.Fonts/TextJustification.cs new file mode 100644 index 0000000..1a7053c --- /dev/null +++ b/SixLabors.Fonts/TextJustification.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Text justification modes. + /// + public enum TextJustification + { + /// + /// No justification + /// + None = 0, + + /// + /// The text is justified by adding space between words (effectively varying word-spacing), + /// which is most appropriate for languages that separate words using spaces, like English or Korean. + /// + InterWord, + + /// + /// The text is justified by adding space between characters (effectively varying letter-spacing), + /// which is most appropriate for languages like Japanese. + /// + InterCharacter + } +} diff --git a/SixLabors.Fonts/TextLayout.LineBreaking.cs b/SixLabors.Fonts/TextLayout.LineBreaking.cs new file mode 100644 index 0000000..d165410 --- /dev/null +++ b/SixLabors.Fonts/TextLayout.LineBreaking.cs @@ -0,0 +1,673 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// Line break candidate collection and layout-level tailoring. + /// + internal static partial class TextLayout + { + private const int SoftHyphen = 0x00AD; + private const int StandardHyphen = 0x2010; + private const int StandardEllipsis = 0x2026; + + /// + /// Composes the logical from shaped glyph data before width-dependent line breaking. + /// + /// The width-independent shaping state. + /// The original source text. + /// The text shaping and layout options. + /// The logical text line and line break opportunities before line breaking. + public static LogicalTextLine ComposeLogicalLine( + in ShapedText shapedText, + ReadOnlySpan text, + TextOptions options) + { + bool isHorizontalLayout = shapedText.LayoutMode.IsHorizontal(); + bool isVerticalLayout = shapedText.LayoutMode.IsVertical(); + bool isVerticalMixedLayout = shapedText.LayoutMode.IsVerticalMixed(); + + int graphemeIndex = 0; + int codePointIndex = 0; + int glyphSearchIndex = 0; + TextLine textLine = new(); + int stringIndex = 0; + List wordSegments = []; + List hyphenationMarkers = []; + CodePoint? hyphenationMarkerCodePoint = GetHyphenationMarkerCodePoint(options); + + // No glyph should contain more than 64 metrics. + // We do a sanity check below just in case. + Span decomposedAdvancesBuffer = stackalloc float[64]; + + // Word-boundary segments are prepared with the logical line, while grapheme + // and codepoint enumeration still own shaping data creation. + SpanWordEnumerator wordEnumerator = new(text); + while (wordEnumerator.MoveNext()) + { + WordSegment wordSegment = wordEnumerator.Current; + int wordSegmentGraphemeStart = graphemeIndex; + + SpanGraphemeEnumerator graphemeEnumerator = new(wordSegment.Span); + while (graphemeEnumerator.MoveNext()) + { + // Now enumerate through each codepoint in the grapheme. + ReadOnlySpan grapheme = graphemeEnumerator.Current.Span; + int graphemeCodePointIndex = 0; + SpanCodePointEnumerator codePointEnumerator = new(grapheme); + while (codePointEnumerator.MoveNext()) + { + if (!shapedText.Positionings.TryGetGlyphMetricsAtOffset( + codePointIndex, + ref glyphSearchIndex, + out float pointSize, + out bool isSubstituted, + out bool isVerticalSubstitution, + out bool isDecomposed, + out IReadOnlyList? glyphData)) + { + // Codepoint was skipped during original enumeration. + codePointIndex++; + graphemeCodePointIndex++; + continue; + } + + List metrics = []; + for (int i = 0; i < glyphData.Count; i++) + { + GlyphPositioningCollection.GlyphPositioningData data = glyphData[i]; + if (data.Data.IsPlaceholder) + { + textLine.AddPlaceholder( + data, + graphemeIndex, + stringIndex, + isHorizontalLayout, + isVerticalMixedLayout, + options.LineSpacing); + + continue; + } + + metrics.Add(data.Metrics); + } + + if (metrics.Count == 0) + { + // This source codepoint was skipped during shaping; any placeholder + // sharing the same source offset has already been added above. + codePointIndex++; + graphemeCodePointIndex++; + continue; + } + + FontGlyphMetrics glyph = metrics[0]; + + // Retrieve the current codepoint from the enumerator. + // If the glyph represents a substituted codepoint and the substitution is a single codepoint substitution, + // or composite glyph, then the codepoint should be updated to the substitution value so we can read its properties. + // Substitutions that are decomposed glyphs will have multiple metrics and any layout should be based on the + // original codepoint. + // + // Note: Not all glyphs in a font will have a codepoint associated with them. e.g. most compositions, ligatures, etc. + CodePoint codePoint = codePointEnumerator.Current; + if (isSubstituted && metrics.Count == 1) + { + codePoint = glyph.CodePoint; + } + + // Determine whether the glyph advance should be calculated using vertical or horizontal metrics + // For vertical mixed layout we will rotate glyphs with the vertical orientation type R or TR + // which do not already have a vertical substitution. + bool shouldRotate = isVerticalMixedLayout && + !isVerticalSubstitution && + CodePoint.GetVerticalOrientationType(codePoint) is + VerticalOrientationType.Rotate or + VerticalOrientationType.TransformRotate; + + // Determine whether the glyph advance should be offset for vertical layout. + bool shouldOffset = isVerticalLayout && + !isVerticalSubstitution && + CodePoint.GetVerticalOrientationType(codePoint) is + VerticalOrientationType.Rotate or + VerticalOrientationType.TransformRotate; + + if (CodePoint.IsVariationSelector(codePoint)) + { + codePointIndex++; + graphemeCodePointIndex++; + continue; + } + + // Calculate the advance for the current codepoint. + + // This should never happen, but we need to ensure that the buffer is large enough + // if, for some crazy reason, a glyph does contain more than 64 metrics. + Span decomposedAdvances = metrics.Count > decomposedAdvancesBuffer.Length + ? new float[metrics.Count] + : decomposedAdvancesBuffer[..(isDecomposed ? metrics.Count : 1)]; + + float glyphAdvance; + if (isHorizontalLayout || shouldRotate) + { + glyphAdvance = glyph.AdvanceWidth; + } + else + { + glyphAdvance = glyph.AdvanceHeight; + } + + decomposedAdvances[0] = glyphAdvance; + + bool isSoftHyphen = codePoint.Value == SoftHyphen; + if (isSoftHyphen) + { + glyphAdvance = 0; + decomposedAdvances[0] = 0; + } + else if (CodePoint.IsTabulation(codePoint)) + { + if (options.TabWidth > -1F) + { + // Do not use the default font tab width. Instead find the advance for the space glyph + // and multiply that by the options value. + CodePoint space = new(0x0020); + if (glyph.FontMetrics.TryGetGlyphId(space, out ushort spaceGlyphId)) + { + FontGlyphMetrics spaceMetrics = glyph.FontMetrics.GetGlyphMetrics( + space, + spaceGlyphId, + glyph.TextAttributes, + glyph.TextDecorations, + shapedText.LayoutMode, + options.ColorFontSupport); + + if (isHorizontalLayout || shouldRotate) + { + glyphAdvance = spaceMetrics.AdvanceWidth * options.TabWidth; + glyph.SetAdvanceWidth((ushort)glyphAdvance); + } + else + { + glyphAdvance = spaceMetrics.AdvanceHeight * options.TabWidth; + glyph.SetAdvanceHeight((ushort)glyphAdvance); + } + } + } + } + else if (metrics.Count == 1 && (CodePoint.IsZeroWidthJoiner(codePoint) || CodePoint.IsZeroWidthNonJoiner(codePoint))) + { + // The zero-width joiner characters should be ignored when determining word or + // line break boundaries so are safe to skip here. Any existing instances are the result of font error + // unless multiple metrics are associated with code point. In this case they are most likely the result + // of a substitution and shouldn't be ignored. + glyphAdvance = 0; + decomposedAdvances[0] = 0; + } + else if (!CodePoint.IsNewLine(codePoint)) + { + // Standard text. + // If decomposed we need to add the advance; otherwise, use the largest advance for the metrics. + if (isHorizontalLayout || shouldRotate) + { + for (int i = 1; i < metrics.Count; i++) + { + float a = metrics[i].AdvanceWidth; + if (isDecomposed) + { + glyphAdvance += a; + decomposedAdvances[i] = a; + } + else if (a > glyphAdvance) + { + glyphAdvance = a; + } + } + } + else + { + for (int i = 1; i < metrics.Count; i++) + { + float a = metrics[i].AdvanceHeight; + if (isDecomposed) + { + glyphAdvance += a; + decomposedAdvances[i] = a; + } + else if (a > glyphAdvance) + { + glyphAdvance = a; + } + } + } + } + + // Now scale the advance. We use inches for comparison. + if (isHorizontalLayout || shouldRotate) + { + float scaleAX = pointSize / glyph.ScaleFactor.X; + glyphAdvance *= scaleAX; + for (int i = 0; i < decomposedAdvances.Length; i++) + { + decomposedAdvances[i] *= scaleAX; + } + } + else + { + float scaleAY = pointSize / glyph.ScaleFactor.Y; + glyphAdvance *= scaleAY; + for (int i = 0; i < decomposedAdvances.Length; i++) + { + decomposedAdvances[i] *= scaleAY; + } + } + + int graphemeCodePointMax = CodePoint.GetCodePointCount(grapheme) - 1; + + // For non-decomposed glyphs the length is always 1. + int glyphDataIndex = 0; + + for (int i = 0; i < decomposedAdvances.Length; i++) + { + // Determine if this is the last codepoint in the grapheme. + bool isLastInGrapheme = graphemeCodePointIndex == graphemeCodePointMax && i == decomposedAdvances.Length - 1; + + float decomposedAdvance = decomposedAdvances[i]; + + // Work out the scaled metrics for the glyph. + while (glyphData[glyphDataIndex].Data.IsPlaceholder) + { + glyphDataIndex++; + } + + GlyphPositioningCollection.GlyphPositioningData positionedGlyph = glyphData[glyphDataIndex]; + FontGlyphMetrics metric = positionedGlyph.Metrics; + + // Adjust the advance for the last decomposed glyph to add tracking if applicable. + // Tracking should only be added once per grapheme, so only on the last codepoint of the grapheme. + if (isLastInGrapheme && options.Tracking != 0 && i == decomposedAdvances.Length - 1) + { + // Tracking should not be applied to tab characters or non-rendered codepoints. + if (!CodePoint.IsTabulation(codePoint) && !UnicodeUtility.ShouldNotBeRendered(codePoint)) + { + if (isHorizontalLayout || shouldRotate) + { + float scaleAX = pointSize / glyph.ScaleFactor.X; + decomposedAdvance += options.Tracking * metric.FontMetrics.UnitsPerEm * scaleAX; + } + else + { + float scaleAY = pointSize / glyph.ScaleFactor.Y; + decomposedAdvance += options.Tracking * metric.FontMetrics.UnitsPerEm * scaleAY; + } + } + } + + // Convert design-space units to pixels based on the target point size. + // ScaleFactor.Y represents the vertical UPEM scaling factor for this glyph. + float scaleY = pointSize / metric.ScaleFactor.Y; + + // Choose which metrics table to use based on layout orientation. + // Horizontal is the default; vertical fonts use VMTX if available. + IMetricsHeader metricsHeader = isHorizontalLayout || shouldRotate + ? metric.FontMetrics.HorizontalMetrics + : metric.FontMetrics.VerticalMetrics; + + // Ascender and descender are stored in font design units, so scale them to pixels. + float ascender = metricsHeader.Ascender * scaleY; + + // Match browser line-height calculation logic. + // Reference: https://www.w3.org/TR/CSS2/visudet.html#propdef-line-height + // The line height in CSS is based on a multiple of the font-size (pointSize), + // but fonts may define a custom LineHeight in their metrics that differs from UPEM. + float descender = Math.Abs(metricsHeader.Descender * scaleY); + float lineHeight = metric.UnitsPerEm * scaleY; + + // The delta centers the font's line box within the CSS line box when + // LineHeight differs from the nominal font size. + float delta = ((metricsHeader.LineHeight * scaleY) - lineHeight) * 0.5F; + + // Adjust ascender and descender symmetrically by delta to preserve visual balance. + ascender -= delta; + descender -= delta; + + GlyphLayoutMode mode = GlyphLayoutMode.Horizontal; + if (isVerticalLayout) + { + mode = GlyphLayoutMode.Vertical; + } + else if (isVerticalMixedLayout) + { + mode = shouldRotate ? GlyphLayoutMode.VerticalRotated : GlyphLayoutMode.Vertical; + } + + int hyphenationMarkerIndex = -1; + if (isSoftHyphen && hyphenationMarkerCodePoint.HasValue) + { + // U+00AD is shaped as an invisible source entry, but if this exact + // discretionary break is later selected we need a visible marker with + // the same run, font attributes, bidi mapping, and source mapping. Build + // that marker here while those values are already in hand; BreakLines can + // then account for its advance without rescanning or reshaping the line. + hyphenationMarkerIndex = hyphenationMarkers.Count; + hyphenationMarkers.Add(CreateGeneratedMarker( + glyph, + pointSize, + shapedText.BidiRuns[shapedText.BidiMap[codePointIndex]], + graphemeIndex, + isLastInGrapheme, + codePointIndex, + graphemeCodePointIndex, + stringIndex, + hyphenationMarkerCodePoint.Value, + shapedText.LayoutMode, + positionedGlyph.Font, + options)); + } + + // Add our metrics to the line. + textLine.Add( + isDecomposed ? new FontGlyphMetrics[] { metric } : metrics, + positionedGlyph.Font, + pointSize, + decomposedAdvance, + lineHeight, + ascender, + descender, + delta, + shapedText.BidiRuns[shapedText.BidiMap[codePointIndex]], + graphemeIndex, + isLastInGrapheme, + codePointIndex, + graphemeCodePointIndex, + shouldRotate || shouldOffset, + isDecomposed, + stringIndex, + mode, + options.LineSpacing, + hyphenationMarkerIndex); + + glyphDataIndex++; + } + + codePointIndex++; + graphemeCodePointIndex++; + } + + stringIndex += grapheme.Length; + graphemeIndex++; + } + + wordSegments.Add(new WordSegmentRun( + wordSegmentGraphemeStart, + graphemeIndex, + wordSegment.Utf16Offset, + wordSegment.Utf16Offset + wordSegment.Utf16Length)); + } + + // Placeholders do not consume source text. A placeholder inserted at + // the final source position has no following codepoint to visit in + // the main loop, so we add those trailing placeholder entries here. + if (shapedText.Positionings.TryGetGlyphMetricsAtOffset( + codePointIndex, + ref glyphSearchIndex, + out _, + out _, + out _, + out _, + out IReadOnlyList? endGlyphData)) + { + for (int i = 0; i < endGlyphData.Count; i++) + { + GlyphPositioningCollection.GlyphPositioningData data = endGlyphData[i]; + if (data.Data.IsPlaceholder) + { + textLine.AddPlaceholder( + data, + graphemeIndex, + stringIndex, + isHorizontalLayout, + isVerticalMixedLayout, + options.LineSpacing); + } + } + } + + // Line break candidates are width-independent and belong with the composed logical line. + List lineBreaks = CollectLineBreaks(text, hyphenationMarkerCodePoint.HasValue); + + return new LogicalTextLine(textLine, lineBreaks, wordSegments, hyphenationMarkers); + } + + /// + /// Applies line-break opportunities to a shaped using the configured + /// behavior and supplied wrapping length. + /// Finalizes each line (trimming trailing whitespace and applying bidi reordering) and applies + /// justification where requested. + /// + /// The logical text line and line break opportunities to break. + /// The text shaping and layout options. + /// The wrapping length in pixels. + /// The shaped, line-broken, finalized text box ready for glyph placement. + public static TextBox BreakLines( + in LogicalTextLine logicalLine, + TextOptions options, + float wrappingLength) + { + int maxLines = options.MaxLines; + + if (maxLines == 0) + { + TextDirection emptyTextDirection = options.TextDirection == TextDirection.RightToLeft + ? TextDirection.RightToLeft + : TextDirection.LeftToRight; + + return new TextBox([], emptyTextDirection); + } + + TextDirection textDirection = GetTextDirection(logicalLine, options); + + List textLines = []; + TextLineBreakEnumerator lineEnumerator = new(logicalLine, options); + + while (lineEnumerator.MoveNext(wrappingLength)) + { + textLines.Add(lineEnumerator.Current); + } + + return new TextBox(textLines, textDirection); + } + + /// + /// Gets the block-level text direction for a prepared logical line. + /// + /// The prepared logical line. + /// The text options used for layout. + /// The block-level text direction. + public static TextDirection GetTextDirection(in LogicalTextLine logicalLine, TextOptions options) + => options.TextDirection == TextDirection.Auto && logicalLine.TextLine.Count > 0 + ? logicalLine.TextLine[0].TextDirection + : options.TextDirection; + + /// + /// Collects the line break opportunities used by the wrapping loop. + /// + /// + /// + /// is the Unicode-conforming default line breaker. Its default + /// constructor remains independent from layout policy so the Unicode conformance tests continue to + /// describe only the default UAX #14 behavior. This method is the boundary where layout-specific + /// tailoring is requested. + /// + /// + /// The line breaker itself is streaming and does not allocate. Layout materializes the resulting + /// break opportunities because the line fitting loop scans the same candidates repeatedly while it + /// removes finalized lines from the front of the shaped text line. + /// + /// + /// Solidus handling is intentionally conservative. UAX #14 classifies U+002F SOLIDUS as SY, which + /// gives ordinary text a break opportunity after a slash. That is valid for the default algorithm, + /// but it produced undesirable layout in issue 448 for ordinary slash-separated text. At the same + /// time, UAX #14 section 8 explicitly calls out URL tailoring that can allow breaks after slash + /// separated URL segments even when the next segment starts with a digit. The result here is: + /// keep default slash behavior for standard enumeration, suppress ordinary slash breaks for layout, + /// and reintroduce the narrow URL numeric-segment break only for URL-like runs. + /// + /// + /// The original source text being laid out. + /// Whether soft-hyphen break opportunities should be included. + /// The ordered line break opportunities after layout-level tailoring. + private static List CollectLineBreaks(ReadOnlySpan text, bool includeHyphenationBreaks) + { + LineBreakEnumerator lineBreakEnumerator = new(text, tailorUrls: true); + List lineBreaks = []; + while (lineBreakEnumerator.MoveNext()) + { + LineBreak lineBreak = lineBreakEnumerator.Current; + if (lineBreak.IsHyphenationBreak && !includeHyphenationBreaks) + { + continue; + } + + lineBreaks.Add(lineBreak); + } + + return lineBreaks; + } + + private static CodePoint? GetHyphenationMarkerCodePoint(TextOptions options) + => options.TextHyphenation switch + { + TextHyphenation.Standard => new CodePoint(StandardHyphen), + TextHyphenation.Custom => options.CustomHyphen, + _ => null + }; + + /// + /// Creates a visible generated marker that matches the layout style of the anchor entry. + /// + /// The glyph metric that supplies font, run, attributes, and decorations. + /// The point size at which the marker is rendered. + /// The bidi run that the marker belongs to. + /// The source grapheme index to map the marker to. + /// Whether the marker maps to the last entry in its grapheme. + /// The source codepoint index to map the marker to. + /// The source codepoint-in-grapheme index to map the marker to. + /// The UTF-16 source index to map the marker to. + /// The marker codepoint to create. + /// The layout mode used to calculate marker orientation. + /// The font used to shape and render the marker. + /// The text options used for layout. + /// The generated marker entry. + internal static GlyphLayoutData CreateGeneratedMarker( + FontGlyphMetrics anchorMetric, + float pointSize, + BidiRun bidiRun, + int graphemeIndex, + bool isLastInGrapheme, + int codePointIndex, + int graphemeCodePointIndex, + int stringIndex, + CodePoint markerCodePoint, + LayoutMode layoutMode, + Font font, + TextOptions options) + { + anchorMetric.FontMetrics.TryGetGlyphId(markerCodePoint, out ushort markerGlyphId); + + FontGlyphMetrics markerMetric = anchorMetric.FontMetrics.GetGlyphMetrics( + markerCodePoint, + markerGlyphId, + anchorMetric.TextAttributes, + anchorMetric.TextDecorations, + layoutMode, + options.ColorFontSupport); + + markerMetric = markerMetric.CloneForRendering(anchorMetric.TextRun); + + bool isHorizontalLayout = layoutMode.IsHorizontal(); + bool isVerticalLayout = layoutMode.IsVertical(); + bool isVerticalMixedLayout = layoutMode.IsVerticalMixed(); + bool shouldRotate = isVerticalMixedLayout && + CodePoint.GetVerticalOrientationType(markerCodePoint) is + VerticalOrientationType.Rotate or + VerticalOrientationType.TransformRotate; + + bool shouldOffset = isVerticalLayout && + CodePoint.GetVerticalOrientationType(markerCodePoint) is + VerticalOrientationType.Rotate or + VerticalOrientationType.TransformRotate; + + GlyphLayoutMode markerMode = GlyphLayoutMode.Horizontal; + if (isVerticalLayout) + { + markerMode = GlyphLayoutMode.Vertical; + } + else if (isVerticalMixedLayout) + { + markerMode = shouldRotate ? GlyphLayoutMode.VerticalRotated : GlyphLayoutMode.Vertical; + } + + float markerAdvance = isHorizontalLayout || shouldRotate + ? markerMetric.AdvanceWidth * (pointSize / markerMetric.ScaleFactor.X) + : markerMetric.AdvanceHeight * (pointSize / markerMetric.ScaleFactor.Y); + + // Generated markers must reserve the same CSS line box as ordinary glyphs + // from the same run so truncation and discretionary hyphens do not collapse + // or expand line spacing. + float markerScaleY = pointSize / markerMetric.ScaleFactor.Y; + IMetricsHeader markerMetricsHeader = isHorizontalLayout || shouldRotate + ? markerMetric.FontMetrics.HorizontalMetrics + : markerMetric.FontMetrics.VerticalMetrics; + + float markerAscender = markerMetricsHeader.Ascender * markerScaleY; + float markerDescender = Math.Abs(markerMetricsHeader.Descender * markerScaleY); + float markerLineHeight = markerMetric.UnitsPerEm * markerScaleY; + float markerDelta = ((markerMetricsHeader.LineHeight * markerScaleY) - markerLineHeight) * 0.5F; + + markerAscender -= markerDelta; + markerDescender -= markerDelta; + + FontRectangle markerBox = FontGlyphMetrics.ShouldSkipGlyphRendering(markerMetric.CodePoint) + ? FontRectangle.Empty + : markerMetric.GetBoundingBox(markerMode, Vector2.Zero, pointSize); + + return new GlyphLayoutData( + new FontGlyphMetrics[] { markerMetric }, + font, + pointSize, + markerAdvance, + markerLineHeight * options.LineSpacing, + markerAscender, + markerDescender, + markerDelta, + MathF.Min(0, markerBox.Y), + bidiRun, + graphemeIndex, + isLastInGrapheme, + codePointIndex, + graphemeCodePointIndex, + shouldRotate || shouldOffset, + false, + stringIndex); + } + + /// + /// Gets the configured ellipsis marker codepoint. + /// + /// The text options used for layout. + /// The configured ellipsis marker codepoint, or when ellipsis is disabled. + public static CodePoint? GetEllipsisMarkerCodePoint(TextOptions options) + => options.TextEllipsis switch + { + TextEllipsis.Standard => new CodePoint(StandardEllipsis), + TextEllipsis.Custom => options.CustomEllipsis, + _ => null + }; + } +} diff --git a/SixLabors.Fonts/TextLayout.Visitors.cs b/SixLabors.Fonts/TextLayout.Visitors.cs new file mode 100644 index 0000000..69dbb9f --- /dev/null +++ b/SixLabors.Fonts/TextLayout.Visitors.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Visitor types for streaming laid-out glyphs through the layout pipeline. + /// + internal static partial class TextLayout + { + /// + /// Receives laid-out glyphs streamed from the layout pipeline. + /// Implementations are value types so the generic dispatch is specialized by the JIT and no boxing or + /// delegate allocation is required. + /// + internal interface IGlyphLayoutVisitor + { + /// + /// Invoked before glyphs are streamed for a laid-out line. + /// + /// The zero-based index of the line in the line-broken text box. + public void BeginLine(int lineIndex); + + /// + /// Invoked once for each laid-out glyph in layout order. + /// + /// The laid-out glyph. + public void Visit(in GlyphLayout glyph); + + /// + /// Invoked after glyphs have been streamed for a laid-out line. + /// + public void EndLine(); + } + } +} diff --git a/SixLabors.Fonts/TextLayout.cs b/SixLabors.Fonts/TextLayout.cs new file mode 100644 index 0000000..04357b0 --- /dev/null +++ b/SixLabors.Fonts/TextLayout.cs @@ -0,0 +1,1530 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// Encapsulates logic for laying out text. + /// + internal static partial class TextLayout + { + /// + /// Resolves the ordered sequence of instances that cover . + /// + /// + /// If is or empty, a single run covering the entire + /// grapheme range of using is returned. Otherwise the + /// supplied runs are ordered, gaps are filled with default-font runs, and overlapping ranges are trimmed. + /// + /// The text to partition into runs. + /// The text options supplying the default font and optional user-defined runs. + /// The resolved runs that together cover the entire grapheme range of . + public static IReadOnlyList BuildTextRuns(ReadOnlySpan text, TextOptions options) + { + int start = 0; + int end = text.GetGraphemeCount(); + if (end == 0) + { + return []; + } + + if (options.TextRuns is null || options.TextRuns.Count == 0) + { + return new TextRun[] + { + new() + { + Start = 0, + End = text.GetGraphemeCount(), + Font = options.Font + } + }; + } + + List textRuns = []; + foreach (TextRun textRun in options.TextRuns.OrderBy(x => x.Start)) + { + // Fill gaps within runs. + if (textRun.Start > start) + { + textRuns.Add(new() + { + Start = start, + End = textRun.Start, + Font = options.Font + }); + } + + // Add the current run, ensuring the font is not null. + textRun.Font ??= options.Font; + + if (textRun.Placeholder.HasValue && textRun.End != textRun.Start) + { + throw new ArgumentException("Placeholder text runs must be zero-length insertion runs.", nameof(options)); + } + + // Ensure that the previous run does not overlap the current. + if (textRuns.Count > 0) + { + int prevIndex = textRuns.Count - 1; + TextRun previous = textRuns[prevIndex]; + previous.End = Math.Min(previous.End, textRun.Start); + } + + textRuns.Add(textRun); + start = textRun.End; + } + + // Add a final run if required. + if (start < end) + { + textRuns.Add(new() + { + Start = start, + End = end, + Font = options.Font + }); + } + + return textRuns; + } + + /// + /// Shapes into shaping state that is independent of the wrapping length. + /// + /// + /// Performs the font-run build, bidi analysis, GSUB/GPOS shaping (including fallback font + /// resolution for unmapped codepoints). The result contains the positioned glyph collection + /// and bidi state used by logical line composition. + /// + /// The text to process. + /// The text options used while shaping. + /// The wrapping-independent shaping state. + public static ShapedText ShapeText(ReadOnlySpan text, TextOptions options) + { + // Gather the font and fallbacks. + Font[] fallbackFonts = (options.FallbackFontFamilies?.Count > 0) + ? [.. options.FallbackFontFamilies.Select(x => new Font(x, options.Font.Size, options.Font.RequestedStyle))] + : []; + + LayoutMode layoutMode = options.LayoutMode; + GlyphSubstitutionCollection substitutions = new(options); + GlyphPositioningCollection positionings = new(options); + + // Analyse the text for bidi directional runs. + BidiAlgorithm bidi = BidiAlgorithm.Instance.Value!; + BidiData bidiData = new(); + bidiData.Init(text, (sbyte)options.TextDirection); + + if (options.TextBidiMode == TextBidiMode.Override) + { + BidiCharacterType overrideType = options.TextDirection == TextDirection.Auto + ? (bidi.ResolveEmbeddingLevel(bidiData.Types) == 1 ? BidiCharacterType.RightToLeft : BidiCharacterType.LeftToRight) + : (options.TextDirection == TextDirection.RightToLeft ? BidiCharacterType.RightToLeft : BidiCharacterType.LeftToRight); + + for (int i = 0; i < bidiData.Types.Length; i++) + { + // Bidi override is a higher-level protocol override: real text behaves as the requested + // strong direction, while separators and explicit bidi controls keep their structural role. + bidiData.Types[i] = bidiData.Types[i] switch + { + BidiCharacterType.ParagraphSeparator + or BidiCharacterType.SegmentSeparator + or BidiCharacterType.BoundaryNeutral + or BidiCharacterType.LeftToRightEmbedding + or BidiCharacterType.RightToLeftEmbedding + or BidiCharacterType.LeftToRightOverride + or BidiCharacterType.RightToLeftOverride + or BidiCharacterType.PopDirectionalFormat + or BidiCharacterType.LeftToRightIsolate + or BidiCharacterType.RightToLeftIsolate + or BidiCharacterType.FirstStrongIsolate + or BidiCharacterType.PopDirectionalIsolate => bidiData.Types[i], + _ => overrideType, + }; + } + } + + bidi.Process(bidiData); + + // Get the list of directional runs + BidiRun[] bidiRuns = [.. BidiRun.CoalesceLevels(bidi.ResolvedLevels)]; + Dictionary bidiMap = []; + + // Incrementally build out collection of glyphs. + IReadOnlyList textRuns = BuildTextRuns(text, options); + + // First do multiple font runs using the individual text runs. + bool complete = true; + int textRunIndex = 0; + int codePointIndex = 0; + int bidiRunIndex = 0; + foreach (TextRun textRun in textRuns) + { + if (textRun.Placeholder.HasValue) + { + substitutions.Clear(); + + while (bidiRunIndex < bidiRuns.Length && codePointIndex == bidiRuns[bidiRunIndex].End) + { + bidiRunIndex++; + } + + // Placeholder direction comes from the bidi region at the insertion + // point. If the insertion point is after all source text, use the + // default even/LTR embedding level. + BidiRun placeholderBidiRun = bidiRunIndex < bidiRuns.Length + ? bidiRuns[bidiRunIndex] + : new(BidiCharacterType.LeftToRight, 2, codePointIndex, 0); + + // Placeholder runs are inserted into the layout stream and do not consume + // source graphemes, source codepoints, or bidi runs. + substitutions.AddPlaceholder( + CodePoint.ObjectReplacementChar, + placeholderBidiRun, + textRun, + codePointIndex); + + complete &= positionings.TryAdd(textRun.Font!, substitutions); + textRunIndex++; + continue; + } + + if (!DoFontRun( + textRun.Slice(text), + textRun.Start, + textRuns, + ref textRunIndex, + ref codePointIndex, + ref bidiRunIndex, + false, + textRun.Font!, + bidiRuns, + bidiMap, + substitutions, + positionings)) + { + complete = false; + } + } + + if (!complete) + { + // Finally try our fallback fonts. + // We do a complete run here across the whole collection. + foreach (Font font in fallbackFonts) + { + textRunIndex = 0; + codePointIndex = 0; + bidiRunIndex = 0; + if (DoFontRun( + text, + 0, + textRuns, + ref textRunIndex, + ref codePointIndex, + ref bidiRunIndex, + true, + font, + bidiRuns, + bidiMap, + substitutions, + positionings)) + { + break; + } + } + } + + // Update the positions of the glyphs in the completed collection. + // Each set of metrics is associated with single font and will only be updated + // by that font so it's safe to use a single collection. + Font? lastFont = null; + for (int i = 0; i < textRuns.Count; i++) + { + TextRun textRun = textRuns[i]; + + if (textRun.Font == lastFont) + { + continue; + } + + textRun.Font!.FontMetrics.UpdatePositions(positionings); + lastFont = textRun.Font; + } + + foreach (Font font in fallbackFonts) + { + font.FontMetrics.UpdatePositions(positionings); + } + + return new ShapedText(positionings, bidiRuns, bidiMap, layoutMode); + } + + /// + /// Lays out the supplied , streaming each laid-out glyph through the + /// supplied in layout order using the supplied wrapping length for alignment. + /// + /// + /// The visitor type is constrained to a struct implementing + /// so the JIT specializes dispatch per visitor — no boxing or delegate allocation. + /// + /// The concrete visitor struct type. + /// The shaped and line-broken text. + /// The text options used to lay out . + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The visitor that receives each positioned glyph. + public static void LayoutText( + TextBox textBox, + TextOptions options, + float wrappingLength, + ref TVisitor visitor) + where TVisitor : struct, IGlyphLayoutVisitor + { + if (textBox.TextLines.Count == 0) + { + return; + } + + LayoutMode layoutMode = options.LayoutMode; + + Vector2 boxLocation = options.Origin / options.Dpi; + Vector2 penLocation = boxLocation; + + // When wrapping is enabled, the wrapping length defines the minimum line-box + // extent used by alignment. + float maxScaledAdvance = textBox.ScaledMaxAdvance(); + if (options.TextAlignment != TextAlignment.Start && wrappingLength > 0) + { + maxScaledAdvance = Math.Max(wrappingLength / options.Dpi, maxScaledAdvance); + } + + TextDirection direction = textBox.TextDirection(); + + if (layoutMode == LayoutMode.HorizontalTopBottom) + { + for (int i = 0; i < textBox.TextLines.Count; i++) + { + visitor.BeginLine(i); + LayoutLineHorizontal( + textBox, + textBox.TextLines[i], + direction, + maxScaledAdvance, + options, + i, + ref boxLocation, + ref penLocation, + ref visitor); + + visitor.EndLine(); + } + } + else if (layoutMode == LayoutMode.HorizontalBottomTop) + { + int index = 0; + for (int i = textBox.TextLines.Count - 1; i >= 0; i--) + { + visitor.BeginLine(i); + LayoutLineHorizontal( + textBox, + textBox.TextLines[i], + direction, + maxScaledAdvance, + options, + index++, + ref boxLocation, + ref penLocation, + ref visitor); + + visitor.EndLine(); + } + } + else if (layoutMode is LayoutMode.VerticalLeftRight) + { + for (int i = 0; i < textBox.TextLines.Count; i++) + { + visitor.BeginLine(i); + LayoutLineVertical( + textBox, + textBox.TextLines[i], + direction, + maxScaledAdvance, + options, + i, + ref boxLocation, + ref penLocation, + ref visitor); + + visitor.EndLine(); + } + } + else if (layoutMode is LayoutMode.VerticalRightLeft) + { + int index = 0; + for (int i = textBox.TextLines.Count - 1; i >= 0; i--) + { + visitor.BeginLine(i); + LayoutLineVertical( + textBox, + textBox.TextLines[i], + direction, + maxScaledAdvance, + options, + index++, + ref boxLocation, + ref penLocation, + ref visitor); + + visitor.EndLine(); + } + } + else if (layoutMode is LayoutMode.VerticalMixedLeftRight) + { + for (int i = 0; i < textBox.TextLines.Count; i++) + { + visitor.BeginLine(i); + LayoutLineVerticalMixed( + textBox, + textBox.TextLines[i], + direction, + maxScaledAdvance, + options, + i, + ref boxLocation, + ref penLocation, + ref visitor); + + visitor.EndLine(); + } + } + else + { + int index = 0; + for (int i = textBox.TextLines.Count - 1; i >= 0; i--) + { + visitor.BeginLine(i); + LayoutLineVerticalMixed( + textBox, + textBox.TextLines[i], + direction, + maxScaledAdvance, + options, + index++, + ref boxLocation, + ref penLocation, + ref visitor); + + visitor.EndLine(); + } + } + } + + /// + /// Positions one line of horizontal text. Applies vertical-block alignment (on the first line), + /// horizontal-block alignment, per-line text alignment, and any first-line ink-overshoot + /// compensation, then streams each positioned glyph through . + /// + /// The concrete visitor struct type. + /// The containing text box (used to look up sibling lines for block alignment). + /// The line being laid out. + /// The resolved text direction for this line. + /// The widest scaled line advance in the block (or wrapping length). + /// The text options used to position the line. + /// The zero-based visual index of this line within the block. + /// The running top-left position of the glyph boxes; advanced by this method. + /// The running pen position used for glyph placement; advanced by this method. + /// The visitor that receives each positioned glyph. + private static void LayoutLineHorizontal( + TextBox textBox, + TextLine textLine, + TextDirection direction, + float maxScaledAdvance, + TextOptions options, + int index, + ref Vector2 boxLocation, + ref Vector2 penLocation, + ref TVisitor visitor) + where TVisitor : struct, IGlyphLayoutVisitor + { + // Offset the location to center the line vertically. + bool isFirstLine = index == 0; + float scaledLineHeight = textLine.ScaledMaxLineHeight; + + // Recover the unscaled line height to calculate proper centering + float unscaledLineHeight = scaledLineHeight / options.LineSpacing; + float advanceY = scaledLineHeight; + + // Center the glyphs within the extra space created by LineSpacing + float offsetY = (advanceY - unscaledLineHeight) * .5F; + float yLineAdvance = advanceY - offsetY; + + float originX = penLocation.X; + float offsetX = 0; + + // Set the Y origin for the first horizontal line and account for tall stacks. + if (isFirstLine) + { + // ScaledMinY is the minimum ink Y for this line in Y down (baseline at 0). + // -ScaledMinY is the actual ascent required to contain the ink. + // ScaledMaxAscender is the typographic ascent we already used to build the line box. + float requiredAscent = -textLine.ScaledMinY; + float extraAscent = requiredAscent - textLine.ScaledMaxAscender; + + if (extraAscent > 0) + { + // Shift the baseline down only by the extra ascent needed so that + // stacked glyphs (Tibetan, etc) fit inside the bitmap. For Latin, + // requiredAscent ~= ScaledMaxAscender and extraAscent is zero. + offsetY += extraAscent; + advanceY += extraAscent; + } + + switch (options.VerticalAlignment) + { + case VerticalAlignment.Center: + for (int i = 0; i < textBox.TextLines.Count; i++) + { + offsetY -= textBox.TextLines[i].ScaledMaxLineHeight * .5F; + } + + break; + case VerticalAlignment.Bottom: + for (int i = 0; i < textBox.TextLines.Count; i++) + { + offsetY -= textBox.TextLines[i].ScaledMaxLineHeight; + } + + break; + } + } + + penLocation.Y += offsetY; + + // Set the X-Origin for horizontal alignment. + switch (options.HorizontalAlignment) + { + case HorizontalAlignment.Right: + offsetX = -maxScaledAdvance; + break; + case HorizontalAlignment.Center: + offsetX = -(maxScaledAdvance * .5F); + break; + } + + // Set the alignment of lines within the text. + if (direction == TextDirection.LeftToRight) + { + switch (options.TextAlignment) + { + case TextAlignment.End: + offsetX += maxScaledAdvance - textLine.ScaledLineAdvance; + break; + case TextAlignment.Center: + offsetX += (maxScaledAdvance * .5F) - (textLine.ScaledLineAdvance * .5F); + break; + } + } + else + { + switch (options.TextAlignment) + { + case TextAlignment.Start: + offsetX += maxScaledAdvance - textLine.ScaledLineAdvance; + break; + case TextAlignment.Center: + offsetX += (maxScaledAdvance * .5F) - (textLine.ScaledLineAdvance * .5F); + break; + } + } + + penLocation.X += offsetX; + Vector2 boundsLocation = boxLocation; + + bool emitted = false; + for (int i = 0; i < textLine.Count; i++) + { + GlyphLayoutData data = textLine[i]; + float layoutAdvance = data.ScaledAdvance; + + if (data.IsNewLine) + { + FontGlyphMetrics metric = data.Metrics[0]; + + // Hard breaks bypass the normal glyph loop, but still need the + // current pen position plus the same baseline origin used by glyphs. + Vector2 hardBreakGlyphOrigin = penLocation + new Vector2(0, textLine.ScaledMaxAscender); + + visitor.Visit( + new GlyphLayout( + new Glyph(metric, data.PointSize), + data.Font, + boundsLocation, + hardBreakGlyphOrigin, + penLocation, + data.ScaledAdvance, + yLineAdvance, + GlyphLayoutMode.Horizontal, + data.BidiRun.Level, + true, + data.GraphemeIndex, + data.StringIndex)); + + penLocation.X = originX; + penLocation.Y += yLineAdvance; + boxLocation.X = originX; + boxLocation.Y += advanceY; + boundsLocation.X = originX; + boundsLocation.Y += advanceY; + return; + } + + int j = 0; + foreach (FontGlyphMetrics metric in data.Metrics) + { + Vector2 glyphOrigin = penLocation + new Vector2(0, textLine.ScaledMaxAscender); + + visitor.Visit( + new GlyphLayout( + new Glyph(metric, data.PointSize), + data.Font, + boundsLocation, + glyphOrigin, + glyphOrigin, + data.ScaledAdvance, + advanceY, + GlyphLayoutMode.Horizontal, + data.BidiRun.Level, + i == 0 && j == 0, + data.GraphemeIndex, + data.StringIndex)); + + emitted = true; + j++; + } + + boxLocation.X += layoutAdvance; + penLocation.X += layoutAdvance; + boundsLocation.X += data.ScaledAdvance; + } + + boxLocation.X = originX; + penLocation.X = originX; + if (emitted) + { + penLocation.Y += yLineAdvance; + boxLocation.Y += advanceY; + } + } + + /// + /// Positions one line of vertical text ( and + /// ). All glyphs are treated as naturally vertical — + /// transformed (rotated) graphemes receive grapheme-level horizontal centering based on the + /// collective ink width of every entry sharing a grapheme index. + /// + /// The concrete visitor struct type. + /// The containing text box (used to look up sibling lines for block alignment). + /// The line being laid out. + /// The resolved text direction for this line. + /// The longest scaled line advance in the block (or wrapping length). + /// The text options used to position the line. + /// The zero-based visual index of this line within the block. + /// The running top-left position of the glyph boxes; advanced by this method. + /// The running pen position used for glyph placement; advanced by this method. + /// The visitor that receives each positioned glyph. + private static void LayoutLineVertical( + TextBox textBox, + TextLine textLine, + TextDirection direction, + float maxScaledAdvance, + TextOptions options, + int index, + ref Vector2 boxLocation, + ref Vector2 penLocation, + ref TVisitor visitor) + where TVisitor : struct, IGlyphLayoutVisitor + { + float originY = penLocation.Y; + float offsetY = 0; + + // Offset the location to center the line horizontally. + float scaledMaxLineHeight = textLine.ScaledMaxLineHeight; + + // Recover the unscaled line height to calculate proper centering + float unscaledLineHeight = scaledMaxLineHeight / options.LineSpacing; + float advanceX = scaledMaxLineHeight; + + // Center the glyphs within the extra space created by LineSpacing + float offsetX = (advanceX - unscaledLineHeight) * .5F; + float xLineAdvance = advanceX - offsetX; + + // Set the Y-Origin for the line. + switch (options.VerticalAlignment) + { + case VerticalAlignment.Top: + offsetY = 0; + break; + case VerticalAlignment.Center: + offsetY -= maxScaledAdvance * .5F; + break; + case VerticalAlignment.Bottom: + offsetY -= maxScaledAdvance; + break; + } + + // Set the alignment of lines within the text. + if (direction == TextDirection.LeftToRight) + { + switch (options.TextAlignment) + { + case TextAlignment.End: + offsetY += maxScaledAdvance - textLine.ScaledLineAdvance; + break; + case TextAlignment.Center: + offsetY += (maxScaledAdvance * .5F) - (textLine.ScaledLineAdvance * .5F); + break; + } + } + else + { + switch (options.TextAlignment) + { + case TextAlignment.Start: + offsetY += maxScaledAdvance - textLine.ScaledLineAdvance; + break; + case TextAlignment.Center: + offsetY += (maxScaledAdvance * .5F) - (textLine.ScaledLineAdvance * .5F); + break; + } + } + + bool isFirstLine = index == 0; + if (isFirstLine) + { + // In vertical layout, first-line Y ascent compensation introduces unwanted + // leading space before the first glyph. Keep first-line handling limited + // to X-origin block alignment only. + + // Set the X-Origin for horizontal alignment. + switch (options.HorizontalAlignment) + { + case HorizontalAlignment.Right: + for (int i = 0; i < textBox.TextLines.Count; i++) + { + offsetX -= textBox.TextLines[i].ScaledMaxLineHeight; + } + + break; + case HorizontalAlignment.Center: + for (int i = 0; i < textBox.TextLines.Count; i++) + { + offsetX -= textBox.TextLines[i].ScaledMaxLineHeight * .5F; + } + + break; + } + } + + penLocation.Y += offsetY; + penLocation.X += offsetX; + + float lineOriginX = penLocation.X; + Vector2 boundsLocation = boxLocation; + float boundsLineOriginX = boundsLocation.X; + + bool emitted = false; + + // Grapheme-scoped state for transformed glyph alignment. + // + // IMPORTANT: GlyphLayoutData is per-codepoint, not per-grapheme. + // Complex scripts can therefore produce multiple entries for a single grapheme. + // For example Devanagari "र्कि" can end up as two entries ("र्" and "कि") even though it + // visually shapes as a single cluster. + // + // - Compute a single alignX for the whole grapheme (across all entries with the same GraphemeIndex). + // - Apply that alignX as a positional offset only, never as part of pen/box advance. + // - Transformed entries still advance along X within the grapheme (horizontal glyphs inside a vertical flow), + // then X is reset at the end of the grapheme. + float currentGraphemeAlignX = 0; + bool currentGraphemeIsTransformed = false; + + for (int i = 0; i < textLine.Count; i++) + { + GlyphLayoutData data = textLine[i]; + float layoutAdvance = data.ScaledAdvance; + float scaledLineHeight = data.ScaledLineHeight / options.LineSpacing; + + if (data.IsNewLine) + { + FontGlyphMetrics metric = data.Metrics[0]; + Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; + + // Hard breaks bypass the normal glyph loop, but still need the + // current pen position plus the same vertical glyph origin adjustment. + Vector2 hardBreakDecorationOrigin = penLocation + new Vector2((unscaledLineHeight - scaledLineHeight) * .5F, 0); + Vector2 hardBreakGlyphOrigin = hardBreakDecorationOrigin + new Vector2(0, (metric.Bounds.Max.Y + metric.TopSideBearing) * scale.Y); + + visitor.Visit( + new GlyphLayout( + new Glyph(metric, data.PointSize), + data.Font, + boundsLocation, + hardBreakGlyphOrigin, + hardBreakDecorationOrigin, + xLineAdvance, + data.ScaledAdvance, + GlyphLayoutMode.Vertical, + data.BidiRun.Level, + true, + data.GraphemeIndex, + data.StringIndex)); + + boxLocation.X += advanceX; + boxLocation.Y = originY; + penLocation.X += xLineAdvance; + penLocation.Y = originY; + boundsLocation.X += advanceX; + boundsLocation.Y = originY; + return; + } + + int j = 0; + + bool isFirstInGrapheme = data.GraphemeCodePointIndex == 0; + float alignX = 0; + float entryScaledAdvanceWidth = 0; + + if (isFirstInGrapheme) + { + // Reset grapheme-scoped state at the start of each grapheme. + currentGraphemeAlignX = 0; + currentGraphemeIsTransformed = false; + + // Determine whether this grapheme contains any transformed entries. + // This is intentionally done at grapheme scope because individual entries can differ. + int graphemeIndex = data.GraphemeIndex; + + for (int k = i; k < textLine.Count; k++) + { + GlyphLayoutData g = textLine[k]; + + if (g.GraphemeIndex != graphemeIndex) + { + break; + } + + if (g.IsTransformed) + { + currentGraphemeIsTransformed = true; + break; + } + } + + if (currentGraphemeIsTransformed) + { + // In vertical layout, glyphs with a vertical orientation of TransformRotate/TransformUpright are + // rendered as "horizontal" glyphs inside a vertical flow. + // + // Their horizontal metrics (including LSB) are still expressed in the font's horizontal writing mode, + // so without an adjustment these glyphs appear shifted within the column. + // + // To make transformed glyphs align visually with naturally-vertical glyphs, we center the ink bounds + // of the ENTIRE grapheme (across all entries with the same GraphemeIndex) within the column width + // (`scaledMaxLineHeight`). + float minX = float.PositiveInfinity; + float maxX = float.NegativeInfinity; + + for (int k = i; k < textLine.Count; k++) + { + GlyphLayoutData g = textLine[k]; + + if (g.GraphemeIndex != graphemeIndex) + { + break; + } + + foreach (FontGlyphMetrics m in g.Metrics) + { + Vector2 s = new Vector2(g.PointSize) / m.ScaleFactor; + + float glyphMinX = m.Bounds.Min.X * s.X; + float glyphMaxX = m.Bounds.Max.X * s.X; + + if (glyphMinX < minX) + { + minX = glyphMinX; + } + + if (glyphMaxX > maxX) + { + maxX = glyphMaxX; + } + } + } + + float inkWidth = maxX - minX; + + // Normalize ink minX to 0 and center within the entry's own line box. + // The decoration origin has already centered that entry line box within + // the widest line box, so using the widest line box here would apply the + // mixed-size offset twice. + // This is grapheme-correct and avoids centering based only on the "first" entry, + // which is not representative for marks like reph in Devanagari. + currentGraphemeAlignX = -minX + ((scaledLineHeight - inkWidth) * .5F); + } + } + + if (currentGraphemeIsTransformed) + { + // Apply the grapheme-level horizontal centering offset to every entry in the grapheme. + // This is positional only and must never be folded into any advance. + alignX = currentGraphemeAlignX; + + // Transformed glyphs are still positioned using horizontal metrics (`AdvanceWidth`) even though + // they participate in a vertical flow. `AdvanceWidth` gives us the horizontal pen advance we must + // apply between entries inside the transformed grapheme. + foreach (FontGlyphMetrics m in data.Metrics) + { + Vector2 s = new Vector2(data.PointSize) / m.ScaleFactor; + entryScaledAdvanceWidth += m.AdvanceWidth * s.X; + } + } + + foreach (FontGlyphMetrics metric in data.Metrics) + { + // Align the glyph horizontally and vertically centering vertically around the baseline. + Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; + float glyphAlignX = alignX; + + if (!currentGraphemeIsTransformed) + { + // Vertical origin fallback places the vertical origin at half the + // horizontal advance. The decoration origin has already centered this + // entry's line box in the column, so center the glyph advance inside it. + glyphAlignX = (scaledLineHeight - (metric.AdvanceWidth * scale.X)) * .5F; + } + + // Move the glyph origin without changing the advance or decoration origin. + Vector2 glyphOffset = new(glyphAlignX, (metric.Bounds.Max.Y + metric.TopSideBearing) * scale.Y); + Vector2 decorationOrigin = penLocation + new Vector2((unscaledLineHeight - scaledLineHeight) * .5F, 0); + Vector2 glyphOrigin = decorationOrigin + glyphOffset; + + float advanceW = advanceX; + + if (currentGraphemeIsTransformed && !isFirstInGrapheme) + { + // For transformed glyphs after the first in the grapheme we advance + // horizontally using the horizontal advance not the line height. + // This gives us the correct total advance across the grapheme. + advanceW = scale.X * metric.AdvanceWidth; + } + + visitor.Visit( + new GlyphLayout( + new Glyph(metric, data.PointSize), + data.Font, + boundsLocation, + glyphOrigin, + decorationOrigin, + advanceW, + data.ScaledAdvance, + GlyphLayoutMode.Vertical, + data.BidiRun.Level, + i == 0 && j == 0, + data.GraphemeIndex, + data.StringIndex)); + + emitted = true; + j++; + } + + if (currentGraphemeIsTransformed) + { + // Advance horizontally between entries inside the transformed grapheme. + boxLocation.X += entryScaledAdvanceWidth; + penLocation.X += entryScaledAdvanceWidth; + } + + if (currentGraphemeIsTransformed) + { + boundsLocation.X += entryScaledAdvanceWidth; + } + + if (data.IsLastInGrapheme) + { + penLocation.Y += layoutAdvance; + boxLocation.X = lineOriginX; + penLocation.X = lineOriginX; + boundsLocation.Y += data.ScaledAdvance; + boundsLocation.X = boundsLineOriginX; + } + } + + boxLocation.Y = originY; + penLocation.Y = originY; + if (emitted) + { + boxLocation.X += advanceX; + penLocation.X += xLineAdvance; + } + } + + /// + /// Positions one line of vertical-mixed text ( + /// and ). Transformed entries are rotated 90° + /// and laid out sideways using the font's horizontal metrics while the pen still advances + /// along Y; naturally-vertical entries are positioned using their vertical metrics. + /// + /// The concrete visitor struct type. + /// The containing text box (used to look up sibling lines for block alignment). + /// The line being laid out. + /// The resolved text direction for this line. + /// The longest scaled line advance in the block (or wrapping length). + /// The text options used to position the line. + /// The zero-based visual index of this line within the block. + /// The running top-left position of the glyph boxes; advanced by this method. + /// The running pen position used for glyph placement; advanced by this method. + /// The visitor that receives each positioned glyph. + private static void LayoutLineVerticalMixed( + TextBox textBox, + TextLine textLine, + TextDirection direction, + float maxScaledAdvance, + TextOptions options, + int index, + ref Vector2 boxLocation, + ref Vector2 penLocation, + ref TVisitor visitor) + where TVisitor : struct, IGlyphLayoutVisitor + { + float originY = penLocation.Y; + float offsetY = 0; + + // Offset the location to center the line horizontally. + float scaledMaxLineHeight = textLine.ScaledMaxLineHeight; + + // Recover the unscaled line height to calculate proper centering + float unscaledLineHeight = scaledMaxLineHeight / options.LineSpacing; + float advanceX = scaledMaxLineHeight; + + // Center the glyphs within the extra space created by LineSpacing + float offsetX = (advanceX - unscaledLineHeight) * .5F; + float xLineAdvance = advanceX - offsetX; + + // Set the Y-Origin for the line. + switch (options.VerticalAlignment) + { + case VerticalAlignment.Top: + offsetY = 0; + break; + case VerticalAlignment.Center: + offsetY -= maxScaledAdvance * .5F; + break; + case VerticalAlignment.Bottom: + offsetY -= maxScaledAdvance; + break; + } + + // Set the alignment of lines within the text. + if (direction == TextDirection.LeftToRight) + { + switch (options.TextAlignment) + { + case TextAlignment.End: + offsetY += maxScaledAdvance - textLine.ScaledLineAdvance; + break; + case TextAlignment.Center: + offsetY += (maxScaledAdvance * .5F) - (textLine.ScaledLineAdvance * .5F); + break; + } + } + else + { + switch (options.TextAlignment) + { + case TextAlignment.Start: + offsetY += maxScaledAdvance - textLine.ScaledLineAdvance; + break; + case TextAlignment.Center: + offsetY += (maxScaledAdvance * .5F) - (textLine.ScaledLineAdvance * .5F); + break; + } + } + + bool isFirstLine = index == 0; + if (isFirstLine) + { + // In vertical-mixed layout, first-line Y ascent compensation introduces + // unwanted leading space before the first glyph. Keep first-line handling + // limited to X-origin block alignment only. + + // Set the X-Origin for horizontal alignment. + switch (options.HorizontalAlignment) + { + case HorizontalAlignment.Right: + for (int i = 0; i < textBox.TextLines.Count; i++) + { + offsetX -= textBox.TextLines[i].ScaledMaxLineHeight; + } + + break; + case HorizontalAlignment.Center: + for (int i = 0; i < textBox.TextLines.Count; i++) + { + offsetX -= textBox.TextLines[i].ScaledMaxLineHeight * .5F; + } + + break; + } + } + + penLocation.Y += offsetY; + penLocation.X += offsetX; + Vector2 boundsLocation = boxLocation; + + bool emitted = false; + for (int i = 0; i < textLine.Count; i++) + { + GlyphLayoutData data = textLine[i]; + float layoutAdvance = data.ScaledAdvance; + float scaledLineHeight = data.ScaledLineHeight / options.LineSpacing; + + if (data.IsNewLine) + { + FontGlyphMetrics metric = data.Metrics[0]; + Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; + + // Hard breaks bypass the normal glyph loop, but still need the + // current pen position plus the same vertical glyph origin adjustment. + Vector2 hardBreakDecorationOrigin = penLocation + new Vector2((unscaledLineHeight - scaledLineHeight) * .5F, 0); + Vector2 hardBreakGlyphOrigin = hardBreakDecorationOrigin + new Vector2(0, (metric.Bounds.Max.Y + metric.TopSideBearing) * scale.Y); + + visitor.Visit( + new GlyphLayout( + new Glyph(metric, data.PointSize), + data.Font, + boundsLocation, + hardBreakGlyphOrigin, + hardBreakDecorationOrigin, + xLineAdvance, + data.ScaledAdvance, + GlyphLayoutMode.Vertical, + data.BidiRun.Level, + true, + data.GraphemeIndex, + data.StringIndex)); + + boxLocation.X += advanceX; + boxLocation.Y = originY; + penLocation.X += xLineAdvance; + penLocation.Y = originY; + boundsLocation.X += advanceX; + boundsLocation.Y = originY; + return; + } + + if (data.IsTransformed) + { + int j = 0; + foreach (FontGlyphMetrics metric in data.Metrics) + { + // The glyph will be rotated 90 degrees for vertical mixed layout. + // We still advance along Y, but the glyphs are laid out sideways in X. + + // Calculate the initial horizontal offset to center the glyph baseline: + // - Take half the difference between the max line height (scaledMaxLineHeight) + // and the current glyph's line height (data.ScaledLineHeight). + // - The line height includes both ascender and descender metrics. + float baselineDelta = (unscaledLineHeight - scaledLineHeight) * .5F; + + // Adjust the horizontal offset further by considering the descender differences: + // - Subtract the current glyph's descender (data.ScaledDescender) to align it properly. + float descenderAbs = Math.Abs(data.ScaledDescender); + float descenderDelta = (Math.Abs(textLine.ScaledMaxDescender) - descenderAbs) * .5F; + + float centerOffsetX = baselineDelta + descenderAbs + descenderDelta; + Vector2 glyphOrigin = penLocation + new Vector2(centerOffsetX, 0); + + visitor.Visit( + new GlyphLayout( + new Glyph(metric, data.PointSize), + data.Font, + boundsLocation, + glyphOrigin, + glyphOrigin, + advanceX, + data.ScaledAdvance, + GlyphLayoutMode.VerticalRotated, + data.BidiRun.Level, + i == 0 && j == 0, + data.GraphemeIndex, + data.StringIndex)); + + emitted = true; + j++; + } + } + else + { + int j = 0; + foreach (FontGlyphMetrics metric in data.Metrics) + { + // Align the glyph horizontally and vertically centering vertically around the baseline. + Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; + + // Vertical origin fallback places the vertical origin at half the + // horizontal advance. The decoration origin has already centered this + // entry's line box in the column, so center the glyph advance inside it. + float glyphAlignX = (scaledLineHeight - (metric.AdvanceWidth * scale.X)) * .5F; + Vector2 glyphOffset = new(glyphAlignX, (metric.Bounds.Max.Y + metric.TopSideBearing) * scale.Y); + Vector2 decorationOrigin = penLocation + new Vector2((unscaledLineHeight - scaledLineHeight) * .5F, 0); + Vector2 glyphOrigin = decorationOrigin + glyphOffset; + + visitor.Visit( + new GlyphLayout( + new Glyph(metric, data.PointSize), + data.Font, + boundsLocation, + glyphOrigin, + decorationOrigin, + advanceX, + data.ScaledAdvance, + GlyphLayoutMode.Vertical, + data.BidiRun.Level, + i == 0 && j == 0, + data.GraphemeIndex, + data.StringIndex)); + + emitted = true; + j++; + } + } + + penLocation.Y += layoutAdvance; + boundsLocation.Y += data.ScaledAdvance; + } + + boxLocation.Y = originY; + penLocation.Y = originY; + if (emitted) + { + boxLocation.X += advanceX; + penLocation.X += xLineAdvance; + } + } + + /// + /// Shapes a single font run — maps codepoints in to glyph ids using + /// , then runs GSUB substitution and GPOS positioning. Codepoints that + /// the font cannot map are recorded for a later fallback pass. + /// + /// The run-relative text slice to shape. + /// The starting grapheme index (absolute within the original input). + /// The ordered list of resolved text runs. + /// The index of the current text run; advanced as the enumerator crosses run boundaries. + /// The running codepoint index (absolute within the original input). + /// The running bidi run index. + /// + /// if this call is the fallback-font pass (in which case unmapped codepoints + /// may still emit .notdef glyphs). + /// + /// The font to shape with. + /// The resolved bidi runs covering the whole input. + /// A codepoint → bidi-run mapping accumulated across shaping passes. + /// The GSUB substitution collection to write into. + /// The GPOS positioning collection to write into. + /// + /// if every codepoint mapped successfully; if any + /// codepoint remains unmapped (so a fallback-font pass is needed). + /// + private static bool DoFontRun( + ReadOnlySpan text, + int start, + IReadOnlyList textRuns, + ref int textRunIndex, + ref int codePointIndex, + ref int bidiRunIndex, + bool isFallbackRun, + Font font, + BidiRun[] bidiRuns, + Dictionary bidiMap, + GlyphSubstitutionCollection substitutions, + GlyphPositioningCollection positionings) + { + // For each run we start with a fresh substitution collection to avoid + // overwriting the glyph ids. + substitutions.Clear(); + + // Enumerate through each grapheme in the text. + int graphemeIndex = start; + SpanGraphemeEnumerator graphemeEnumerator = new(text); + while (graphemeEnumerator.MoveNext()) + { + ReadOnlySpan grapheme = graphemeEnumerator.Current.Span; + int graphemeMax = grapheme.Length - 1; + int graphemeCodePointIndex = 0; + int charIndex = 0; + + while (textRunIndex < textRuns.Count - 1 && graphemeIndex == textRuns[textRunIndex].End) + { + textRunIndex++; + } + + // Now enumerate through each codepoint in the grapheme. + bool skipNextCodePoint = false; + SpanCodePointEnumerator codePointEnumerator = new(grapheme); + while (codePointEnumerator.MoveNext()) + { + if (codePointIndex == bidiRuns[bidiRunIndex].End) + { + bidiRunIndex++; + } + + if (skipNextCodePoint) + { + codePointIndex++; + graphemeCodePointIndex++; + continue; + } + + bidiMap[codePointIndex] = bidiRunIndex; + + int charsConsumed = 0; + CodePoint current = codePointEnumerator.Current; + charIndex += current.Utf16SequenceLength; + CodePoint? next = graphemeCodePointIndex < graphemeMax + ? CodePoint.DecodeFromUtf16At(grapheme, charIndex, out charsConsumed) + : null; + + charIndex += charsConsumed; + + // Get the glyph id for the codepoint and add to the collection. + bool hasGlyph = font.FontMetrics.TryGetGlyphId(current, next, out ushort glyphId, out skipNextCodePoint); + + // Unsupported default-ignorable code points such as FE0F should not block + // GSUB sequences like emoji ZWJ ligatures. Preserve joiners explicitly. + if (!hasGlyph && + UnicodeUtility.IsDefaultIgnorableCodePoint((uint)current.Value) && + !UnicodeUtility.ShouldRenderWhiteSpaceOnly(current) && + !CodePoint.IsZeroWidthJoiner(current) && + !CodePoint.IsZeroWidthNonJoiner(current)) + { + codePointIndex++; + graphemeCodePointIndex++; + continue; + } + + substitutions.AddGlyph(glyphId, current, (TextDirection)bidiRuns[bidiRunIndex].Direction, textRuns[textRunIndex], codePointIndex); + + codePointIndex++; + graphemeCodePointIndex++; + } + + graphemeIndex++; + } + + // Apply the simple and complex substitutions. + // TODO: Investigate HarfBuzz normalizer. + SubstituteBidiMirrors(font.FontMetrics, substitutions); + font.FontMetrics.ApplySubstitution(substitutions); + + return !isFallbackRun + ? positionings.TryAdd(font, substitutions) + : positionings.TryUpdate(font, substitutions); + } + + /// + /// Substitutes mirrored bracket glyphs (for example ()) inside right-to-left + /// bidi runs, per Unicode Bidirectional Algorithm rule L4. Relies on the font's rtlm + /// feature when available and falls back to the Unicode mirror table otherwise. + /// + /// The font metrics used to look up mirrored glyph ids. + /// The substitution collection whose glyphs will be rewritten in place. + private static void SubstituteBidiMirrors(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) + { + for (int i = 0; i < collection.Count; i++) + { + GlyphShapingData data = collection[i]; + + if (data.Direction != TextDirection.RightToLeft) + { + continue; + } + + if (!CodePoint.TryGetBidiMirror(data.CodePoint, out CodePoint mirror)) + { + continue; + } + + if (fontMetrics.TryGetGlyphId(mirror, out ushort glyphId)) + { + collection.Replace(i, glyphId, KnownFeatureTags.RightToLeftMirroredForms); + } + } + + // TODO: This only replaces certain glyphs. We should investigate the specification further. + // https://www.unicode.org/reports/tr50/#vertical_alternates + if (collection.TextOptions.LayoutMode.IsHorizontal()) + { + return; + } + + for (int i = 0; i < collection.Count; i++) + { + GlyphShapingData data = collection[i]; + if (CodePoint.GetVerticalOrientationType(data.CodePoint) is VerticalOrientationType.Upright or VerticalOrientationType.TransformUpright) + { + continue; + } + + if (!CodePoint.TryGetVerticalMirror(data.CodePoint, out CodePoint mirror)) + { + continue; + } + + if (fontMetrics.TryGetGlyphId(mirror, out ushort glyphId)) + { + collection.Replace(i, glyphId, KnownFeatureTags.VerticalAlternates); + } + } + } + + /// + /// Calculates the X offset to apply to a single line of horizontal text so that it is positioned + /// within the wrapping block according to the requested horizontal and text alignment. + /// + /// + /// The returned offset is in unscaled (pre-Dpi) units and is combined with the pen location at + /// layout time. The result depends on the text direction because + /// and flip under right-to-left text. + /// + /// The scaled advance of the current line. + /// The scaled advance of the widest line (or wrapping length, whichever is greater). + /// Block-level horizontal alignment of the whole text. + /// Per-line alignment within the block. + /// The resolved text direction for this line. + /// The X offset to add to the line's pen location. + internal static float CalculateLineOffsetX( + float lineAdvance, + float maxScaledAdvance, + HorizontalAlignment horizontalAlignment, + TextAlignment textAlignment, + TextDirection direction) + { + float offsetX = 0; + + // Set the X-Origin for horizontal alignment. + switch (horizontalAlignment) + { + case HorizontalAlignment.Right: + offsetX = -maxScaledAdvance; + break; + case HorizontalAlignment.Center: + offsetX = -(maxScaledAdvance * .5F); + break; + } + + // Set the alignment of lines within the text. + if (direction == TextDirection.LeftToRight) + { + switch (textAlignment) + { + case TextAlignment.End: + offsetX += maxScaledAdvance - lineAdvance; + break; + case TextAlignment.Center: + offsetX += (maxScaledAdvance * .5F) - (lineAdvance * .5F); + break; + } + } + else + { + switch (textAlignment) + { + case TextAlignment.Start: + offsetX += maxScaledAdvance - lineAdvance; + break; + case TextAlignment.Center: + offsetX += (maxScaledAdvance * .5F) - (lineAdvance * .5F); + break; + } + } + + return offsetX; + } + + /// + /// Calculates the Y offset to apply to a single line of vertical text so that it is positioned + /// within the wrapping block according to the requested vertical and text alignment. + /// + /// + /// The returned offset is in unscaled (pre-Dpi) units and is combined with the pen location at + /// layout time. The result depends on the text direction because + /// and flip under right-to-left text. + /// + /// The scaled advance of the current line. + /// The scaled advance of the longest line (or wrapping length, whichever is greater). + /// Block-level vertical alignment of the whole text. + /// Per-line alignment within the block. + /// The resolved text direction for this line. + /// The Y offset to add to the line's pen location. + internal static float CalculateLineOffsetY( + float lineAdvance, + float maxScaledAdvance, + VerticalAlignment verticalAlignment, + TextAlignment textAlignment, + TextDirection direction) + { + float offsetY = 0; + + // Set the Y-Origin for the line. + switch (verticalAlignment) + { + case VerticalAlignment.Top: + offsetY = 0; + break; + case VerticalAlignment.Center: + offsetY -= maxScaledAdvance * .5F; + break; + case VerticalAlignment.Bottom: + offsetY -= maxScaledAdvance; + break; + } + + // Set the alignment of lines within the text. + if (direction == TextDirection.LeftToRight) + { + switch (textAlignment) + { + case TextAlignment.End: + offsetY += maxScaledAdvance - lineAdvance; + break; + case TextAlignment.Center: + offsetY += (maxScaledAdvance * .5F) - (lineAdvance * .5F); + break; + } + } + else + { + switch (textAlignment) + { + case TextAlignment.Start: + offsetY += maxScaledAdvance - lineAdvance; + break; + case TextAlignment.Center: + offsetY += (maxScaledAdvance * .5F) - (lineAdvance * .5F); + break; + } + } + + return offsetY; + } + } +} diff --git a/SixLabors.Fonts/TextLine.cs b/SixLabors.Fonts/TextLine.cs new file mode 100644 index 0000000..fb9afc9 --- /dev/null +++ b/SixLabors.Fonts/TextLine.cs @@ -0,0 +1,1097 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// A shaped line of text — an ordered sequence of entries plus + /// per-line aggregate metrics (advance, ascender, descender, etc.) used to position the line + /// during layout. + /// + internal sealed class TextLine + { + private readonly List data; + private readonly Dictionary advances = []; + + /// + /// Initializes a new instance of the class with a small default capacity. + /// + public TextLine() => this.data = new(16); + + /// + /// Initializes a new instance of the class with the specified initial + /// entry capacity. + /// + /// Initial capacity for the internal entry list. + public TextLine(int capacity) => this.data = new(capacity); + + /// + /// Initializes a new instance of the class by copying another line. + /// + /// The line to copy. + public TextLine(TextLine source) + { + this.data = [.. source.data]; + this.SkipJustification = source.SkipJustification; + this.ScaledLineAdvance = source.ScaledLineAdvance; + this.ScaledMaxLineHeight = source.ScaledMaxLineHeight; + this.ScaledMaxAscender = source.ScaledMaxAscender; + this.ScaledMaxDescender = source.ScaledMaxDescender; + this.ScaledMaxDelta = source.ScaledMaxDelta; + this.ScaledMinY = source.ScaledMinY; + } + + /// + /// Gets the number of entries in this line. + /// + public int Count => this.data.Count; + + /// + /// Gets the number of graphemes in this line. + /// + public int GraphemeCount + { + get + { + int count = 0; + int lastGraphemeIndex = -1; + for (int i = 0; i < this.data.Count; i++) + { + int graphemeIndex = this.data[i].GraphemeIndex; + if (graphemeIndex == lastGraphemeIndex) + { + continue; + } + + count++; + lastGraphemeIndex = graphemeIndex; + } + + return count; + } + } + + /// + /// Gets a value indicating whether this line should be skipped during text justification. + /// Set by for lines that end a paragraph. + /// + public bool SkipJustification { get; private set; } + + /// + /// Gets the scaled advance contributed to line layout and measurement. + /// + public float ScaledLineAdvance { get; private set; } + + /// + /// Gets the greatest scaled line height across all entries, multiplied by the configured + /// line-spacing factor. + /// + public float ScaledMaxLineHeight { get; private set; } = -1; + + /// + /// Gets the greatest scaled ascender across all entries in this line. + /// + public float ScaledMaxAscender { get; private set; } = -1; + + /// + /// Gets the greatest scaled descender across all entries in this line. + /// + public float ScaledMaxDescender { get; private set; } = -1; + + /// + /// Gets the greatest scaled symmetric-metrics delta across all entries in this line. + /// Browsers adjust ascender/descender symmetrically for baseline alignment; this captures + /// that adjustment. + /// + public float ScaledMaxDelta { get; private set; } = float.MinValue; + + /// + /// Gets the smallest (most negative) scaled Y position across all entries in this line. + /// Used to detect ink that extends above the typographic ascender (for example stacked + /// marks in Tibetan) so the layout engine can reserve extra ascent. + /// + public float ScaledMinY { get; private set; } + + /// + /// Gets the entry at the given index. + /// + /// The zero-based index into this line. + /// The entry at the given index. + public GlyphLayoutData this[int index] => this.data[index]; + + /// + /// Counts the glyph entries emitted from this line. + /// + /// The number of glyph entries that layout will emit for this line. + public int CountGlyphLayouts() + { + int count = 0; + for (int i = 0; i < this.data.Count; i++) + { + count += this.data[i].Metrics.Count; + } + + return count; + } + + /// + /// Appends a shaped entry to this line, updating the aggregated line-level metrics. + /// + /// The glyph metrics produced by shaping this entry's codepoint. + /// The font used to shape and render this entry. + /// The point size at which the entry is rendered. + /// The scaled advance contributed by this entry. + /// The scaled line height contributed by this entry (before line-spacing). + /// The scaled typographic ascender. + /// The scaled typographic descender. + /// The symmetric metrics delta applied during line-box construction. + /// The bidi run this entry belongs to. + /// The grapheme index in the source text. + /// Whether this entry is the last codepoint in its grapheme cluster. + /// The codepoint index in the source text. + /// The index of the codepoint within its grapheme cluster. + /// Whether the entry participates in a transformed (rotated) vertical layout. + /// Whether the entry was produced by Unicode decomposition. + /// The character index in the source string. + /// The glyph-level layout mode to use for ink bounds computation. + /// The line-spacing factor to apply to . + /// The marker index to use if this entry becomes a selected soft-hyphen break. + public void Add( + IReadOnlyList metrics, + Font font, + float pointSize, + float scaledAdvance, + float scaledLineHeight, + float scaledAscender, + float scaledDescender, + float scaledDelta, + BidiRun bidiRun, + int graphemeIndex, + bool isLastInGrapheme, + int codePointIndex, + int graphemeCodePointIndex, + bool isTransformed, + bool isDecomposed, + int stringIndex, + GlyphLayoutMode layoutMode, + float lineSpacing, + int hyphenationMarkerIndex = GlyphLayoutData.NoHyphenationMarker) + { + // Apply LineSpacing to scaledLineHeight before storing + scaledLineHeight *= lineSpacing; + + // Reset metrics. + // We track the maximum metrics for each line to ensure glyphs can be aligned. + if (graphemeCodePointIndex == 0) + { + // TODO: Check this logic is correct. + this.ScaledLineAdvance += scaledAdvance; + } + + this.ScaledMaxLineHeight = MathF.Max(this.ScaledMaxLineHeight, scaledLineHeight); + this.ScaledMaxAscender = MathF.Max(this.ScaledMaxAscender, scaledAscender); + this.ScaledMaxDescender = MathF.Max(this.ScaledMaxDescender, scaledDescender); + this.ScaledMaxDelta = MathF.Max(this.ScaledMaxDelta, scaledDelta); + + // Track the true top of the ink in device space (Y down, baseline at 0). + // For scripts with stacked marks (Tibetan, etc) this can be significantly + // above the typographic ascender, so we cannot trust ascender alone. + float scaledMinY = 0; + for (int i = 0; i < metrics.Count; i++) + { + FontGlyphMetrics metric = metrics[i]; + if (FontGlyphMetrics.ShouldSkipGlyphRendering(metric.CodePoint)) + { + continue; + } + + FontRectangle bbox = metric.GetBoundingBox(layoutMode, Vector2.Zero, pointSize); + scaledMinY = MathF.Min(scaledMinY, bbox.Y); + } + + // ScaledMinY is the minimum ink Y over all glyphs in this line, in Y down. + // It is usually <= 0; more negative means more ink above the baseline. + if (this.data.Count == 0) + { + this.ScaledMinY = scaledMinY; + } + else + { + this.ScaledMinY = MathF.Min(this.ScaledMinY, scaledMinY); + } + + this.data.Add(new( + metrics, + font, + pointSize, + scaledAdvance, + scaledLineHeight, + scaledAscender, + scaledDescender, + scaledDelta, + scaledMinY, + bidiRun, + graphemeIndex, + isLastInGrapheme, + codePointIndex, + graphemeCodePointIndex, + isTransformed, + isDecomposed, + stringIndex, + hyphenationMarkerIndex)); + } + + /// + /// Adds an inline placeholder entry at an existing source codepoint position without consuming source text. + /// + /// The positioned placeholder glyph data. + /// The source grapheme index at the placeholder insertion point. + /// The source UTF-16 index at the placeholder insertion point. + /// when the current layout advances horizontally. + /// when the current layout is vertical mixed. + /// The line-spacing factor to apply to placeholder line height. + public void AddPlaceholder( + GlyphPositioningCollection.GlyphPositioningData placeholder, + int graphemeIndex, + int stringIndex, + bool isHorizontalLayout, + bool isVerticalMixedLayout, + float lineSpacing) + { + FontGlyphMetrics placeholderGlyph = placeholder.Metrics; + bool isPlaceholderHorizontal = isHorizontalLayout || isVerticalMixedLayout; + float placeholderAdvance = isPlaceholderHorizontal + ? placeholderGlyph.AdvanceWidth + : placeholderGlyph.AdvanceHeight; + + Vector2 placeholderScale = new( + placeholder.PointSize / placeholderGlyph.ScaleFactor.X, + placeholder.PointSize / placeholderGlyph.ScaleFactor.Y); + + placeholderAdvance *= isPlaceholderHorizontal ? placeholderScale.X : placeholderScale.Y; + + GlyphLayoutMode placeholderMode = isHorizontalLayout + ? GlyphLayoutMode.Horizontal + : GlyphLayoutMode.Vertical; + + FontRectangle placeholderBox = placeholderGlyph.GetBoundingBox(placeholderMode, Vector2.Zero, placeholder.PointSize); + + IMetricsHeader metricsHeader = isPlaceholderHorizontal + ? placeholderGlyph.FontMetrics.HorizontalMetrics + : placeholderGlyph.FontMetrics.VerticalMetrics; + + // Placeholder bounds can extend beyond the surrounding run font's + // normal ascender/descender band. Keep the run font line-box model as + // the baseline contribution, then expand only the side the placeholder + // actually overhangs so following lines reserve enough space. + float placeholderScaleY = placeholder.PointSize / placeholderGlyph.ScaleFactor.Y; + float placeholderLineHeight = placeholderGlyph.UnitsPerEm * placeholderScaleY; + float placeholderDelta = ((metricsHeader.LineHeight * placeholderScaleY) - placeholderLineHeight) * .5F; + float placeholderAscender = (metricsHeader.Ascender * placeholderScaleY) - placeholderDelta; + float placeholderDescender = Math.Abs(metricsHeader.Descender * placeholderScaleY) - placeholderDelta; + placeholderAscender = MathF.Max(placeholderAscender, -placeholderBox.Top); + placeholderDescender = MathF.Max(placeholderDescender, placeholderBox.Bottom); + placeholderLineHeight = MathF.Max( + placeholderLineHeight, + placeholderAscender + placeholderDescender + (2 * placeholderDelta)); + + // Placeholders share the source codepoint offset at their insertion point, + // but they do not consume source grapheme, codepoint, or UTF-16 indexes. + this.Add( + new FontGlyphMetrics[] { placeholderGlyph }, + placeholder.Font, + placeholder.PointSize, + placeholderAdvance, + placeholderLineHeight, + placeholderAscender, + placeholderDescender, + placeholderDelta, + placeholder.Data.BidiRun, + graphemeIndex, + true, + placeholder.Offset, + 0, + false, + false, + stringIndex, + placeholderMode, + lineSpacing); + } + + /// + /// Inserts all entries from into this line at the given index + /// and recomputes aggregated metrics. + /// + /// The zero-based index at which to insert. + /// The line whose entries should be inserted. + public void InsertAt(int index, TextLine textLine) + { + this.data.InsertRange(index, textLine.data); + RecalculateLineMetrics(this); + } + + /// + /// Returns the cumulative scaled advance up to and including the glyph at the given index. + /// Whitespace entries at or after are skipped so the returned value + /// represents the advance at the last non-whitespace glyph before a potential line break. + /// + /// Results are memoized by index. + /// The zero-based index to measure up to. + /// The cumulative scaled advance. + public float MeasureAt(int index) + { + if (this.advances.TryGetValue(index, out float advance)) + { + return advance; + } + + if (index >= this.data.Count) + { + index = this.data.Count - 1; + } + + while (index >= 0 && CodePoint.IsWhiteSpace(this.data[index].CodePoint)) + { + // If the index is whitespace, we need to measure at the previous + // non-whitespace glyph to ensure we don't break too early. + index--; + } + + advance = 0; + for (int i = 0; i <= index; i++) + { + advance += this.data[i].ScaledAdvance; + } + + this.advances[index] = advance; + return advance; + } + + /// + /// Gets the marker advance for a selected soft-hyphen entry. + /// + /// The soft-hyphen entry index in this line. + /// The markers prepared with the logical line. + /// The scaled advance of the visible hyphenation marker. + public float GetHyphenationMarkerAdvance( + int index, + List hyphenationMarkers) + => hyphenationMarkers[this.data[index].HyphenationMarkerIndex].ScaledAdvance; + + /// + /// Replaces a selected soft-hyphen entry with its prepared visible marker. + /// + /// The soft-hyphen entry index in this line. + /// The markers prepared with the logical line. + public void ApplyHyphenationMarker( + int index, + List hyphenationMarkers) + { + this.data[index] = hyphenationMarkers[this.data[index].HyphenationMarkerIndex]; + RecalculateLineMetrics(this); + } + + /// + /// Applies an ellipsis marker to the end of this line. + /// + /// The marker codepoint to append. + /// The wrapping length in inches. + /// The text options used for layout. + public void ApplyEllipsisMarker( + CodePoint markerCodePoint, + float scaledWrappingLength, + TextOptions options) + { + // The marker replaces the hidden tail, so breakable whitespace at the + // truncation edge is removed before we choose the marker style or decide + // how many graphemes fit. + this.RemoveTrailingBreakingWhitespace(); + + GlyphLayoutData anchor = this.data[^1]; + GlyphLayoutData marker = TextLayout.CreateGeneratedMarker( + anchor.Metrics[0], + anchor.PointSize, + anchor.BidiRun, + anchor.GraphemeIndex, + anchor.IsLastInGrapheme, + anchor.CodePointIndex, + anchor.GraphemeCodePointIndex, + anchor.StringIndex, + markerCodePoint, + options.LayoutMode, + anchor.Font, + options); + + while (this.data.Count > 0 && + this.ScaledLineAdvance + marker.ScaledAdvance > scaledWrappingLength) + { + // Remove a whole grapheme at a time. Truncating through a decomposed + // cluster would corrupt the same source unit that selection and caret + // metrics expose as indivisible. + this.RemoveLastGrapheme(); + } + + // CSS block ellipsis allows the marker to displace the whole final line. + // That means an overflowing line can become marker-only, but only because + // hidden text exists after the clamp point. + this.data.Add(marker); + RecalculateLineMetrics(this); + } + + /// + /// Removes trailing breakable whitespace from the line. + /// + /// + /// When , keeps ordinary trailing breaking whitespace for editor interaction. + /// + private void RemoveTrailingBreakingWhitespace(bool preserveTrailingBreakingWhitespace = false) + { + int index = this.data.Count; + while (index > 1) + { + CodePoint point = this.data[index - 1].CodePoint; + if (!CodePoint.IsWhiteSpace(point) || CodePoint.IsNonBreakingSpace(point)) + { + break; + } + + if (preserveTrailingBreakingWhitespace && !CodePoint.IsNewLine(point)) + { + break; + } + + index--; + } + + if (index < this.data.Count) + { + this.data.RemoveRange(index, this.data.Count - index); + RecalculateLineMetrics(this); + } + } + + /// + /// Removes the last complete grapheme from the line. + /// + private void RemoveLastGrapheme() + { + int end = this.data.Count - 1; + int graphemeIndex = this.data[end].GraphemeIndex; + int start = end; + while (start > 0 && this.data[start - 1].GraphemeIndex == graphemeIndex) + { + start--; + } + + this.data.RemoveRange(start, end - start + 1); + RecalculateLineMetrics(this); + } + + /// + /// Splits this line at the first non-whitespace glyph whose cumulative advance meets or + /// exceeds . On success, the split-off tail is returned as a new + /// line and removed from this one; both lines have their aggregated metrics recomputed. + /// + /// The scaled advance threshold at which to split. + /// The trailing portion of the split, or if no split was performed. + /// if a split occurred; otherwise . + public bool TrySplitAt(float length, [NotNullWhen(true)] out TextLine? result) + { + float advance = this.data[0].ScaledAdvance; + + // Ensure at least one glyph is in the line. + // trailing whitespace should be ignored as it is trimmed + // on finalization. + for (int i = 1; i < this.data.Count; i++) + { + GlyphLayoutData glyph = this.data[i]; + advance += glyph.ScaledAdvance; + if (CodePoint.IsWhiteSpace(glyph.CodePoint)) + { + continue; + } + + if (advance >= length) + { + int count = this.data.Count - i; + result = new(count); + result.data.AddRange(this.data.GetRange(i, count)); + RecalculateLineMetrics(result); + + this.data.RemoveRange(i, count); + RecalculateLineMetrics(this); + return true; + } + } + + result = null; + return false; + } + + /// + /// Splits this line at the glyph immediately preceding the supplied + /// wrap position. When is set, the split is delayed until + /// the nearest boundary outside a CSS keep-all word unit sequence. + /// + /// The resolved line-break opportunity. + /// When , avoid breaking within keep-all word unit sequences. + /// The trailing portion of the split, or if no split was performed. + /// if a split occurred; otherwise . + public bool TrySplitAt(LineBreak lineBreak, bool keepAll, [NotNullWhen(true)] out TextLine? result) + { + int index = this.data.Count; + while (index > 0) + { + if (this.data[--index].CodePointIndex == lineBreak.PositionWrap) + { + break; + } + } + + // CSS word-break: keep-all suppresses implicit breaks between typographic letter units. + if (index > 0 + && !lineBreak.Required + && keepAll + && this.IsKeepAllSuppressedBreak(index)) + { + while (index > 0 && this.IsKeepAllSuppressedBreak(index)) + { + index--; + } + } + + if (index == 0) + { + result = null; + return false; + } + + // Create a new line ensuring we capture the initial metrics. + int count = this.data.Count - index; + result = new(count); + result.data.AddRange(this.data.GetRange(index, count)); + RecalculateLineMetrics(result); + + // Remove those items from this line. + this.data.RemoveRange(index, count); + RecalculateLineMetrics(this); + + return true; + } + + /// + /// Splits a terminal hard-break grapheme into its own line. + /// + /// The terminal hard-break line, or if no split was performed. + /// if a terminal hard break was split; otherwise . + public bool TrySplitTerminalHardBreak([NotNullWhen(true)] out TextLine? result) + { + int end = this.data.Count - 1; + if (end <= 0 || !this.data[end].IsNewLine) + { + result = null; + return false; + } + + int graphemeIndex = this.data[end].GraphemeIndex; + int start = end; + while (start > 0 && this.data[start - 1].GraphemeIndex == graphemeIndex) + { + start--; + } + + int count = this.data.Count - start; + result = new(count); + result.data.AddRange(this.data.GetRange(start, count)); + RecalculateLineMetrics(result); + + this.data.RemoveRange(start, count); + RecalculateLineMetrics(this); + return true; + } + + /// + /// Returns whether CSS word-break: keep-all suppresses the candidate break before + /// the entry at . + /// + /// + /// See CSS Text Module Level 4, word-break. + /// + /// The entry index immediately after the candidate break. + /// if the candidate break is within a keep-all word unit sequence. + private bool IsKeepAllSuppressedBreak(int index) + { + if (index <= 0 || index >= this.data.Count) + { + return false; + } + + return IsKeepAllWordUnit(this.data[index - 1].CodePoint) + && IsKeepAllWordUnit(this.data[index].CodePoint); + } + + /// + /// Returns whether participates in a CSS keep-all word unit + /// sequence. + /// + /// + /// CSS keep-all uses typographic letter units and the Unicode line-breaking + /// classes NU, AL, AI, and ID. + /// See Unicode Standard Annex #14, Line Breaking Classes. + /// + /// The code point to classify. + /// if the code point participates in a keep-all word unit sequence. + private static bool IsKeepAllWordUnit(CodePoint codePoint) + => CodePoint.IsLetter(codePoint) + || CodePoint.IsNumber(codePoint) + || CodePoint.GetLineBreakClass(codePoint) is + LineBreakClass.Numeric + or LineBreakClass.Alphabetic + or LineBreakClass.Ambiguous + or LineBreakClass.Ideographic; + + /// + /// Finalizes this line after line-breaking: trims trailing breaking whitespace when requested, + /// applies bidi reordering so entries are in visual order, and recomputes aggregated metrics. + /// + /// + /// When , marks the line so becomes a no-op + /// (used for paragraph-final lines). + /// + /// + /// When , moves decomposed grapheme advances to the final visual entry. + /// + /// + /// When , keeps ordinary trailing breaking whitespace in the finalized line. + /// + /// This line, for fluent chaining. + public TextLine Finalize( + bool skipJustification = false, + bool normalizeDecomposedAdvances = false, + bool preserveTrailingBreakingWhitespace = false) + { + this.SkipJustification = skipJustification; + this.RemoveTrailingBreakingWhitespace(preserveTrailingBreakingWhitespace); + this.BidiReOrder(); + + if (normalizeDecomposedAdvances) + { + this.NormalizeDecomposedAdvances(); + } + + RecalculateLineMetrics(this); + return this; + } + + /// + /// Moves decomposed grapheme advances when bidi reordering moved the grapheme boundary marker. + /// + private void NormalizeDecomposedAdvances() + { + int start = 0; + while (start < this.data.Count) + { + int graphemeIndex = this.data[start].GraphemeIndex; + int end = start + 1; + bool hasDecomposedEntry = this.data[start].IsDecomposed; + + while (end < this.data.Count && this.data[end].GraphemeIndex == graphemeIndex) + { + hasDecomposedEntry |= this.data[end].IsDecomposed; + end++; + } + + if (hasDecomposedEntry && end - start > 1 && !this.data[end - 1].IsLastInGrapheme) + { + float advance = 0; + for (int i = start; i < end; i++) + { + GlyphLayoutData glyph = this.data[i]; + advance += glyph.ScaledAdvance; + glyph.ScaledAdvance = 0; + glyph.IsLastInGrapheme = false; + this.data[i] = glyph; + } + + GlyphLayoutData last = this.data[end - 1]; + last.ScaledAdvance = advance; + last.IsLastInGrapheme = true; + this.data[end - 1] = last; + } + + start = end; + } + } + + /// + /// Distributes the remaining space between the line advance and the wrapping length across + /// either inter-character or inter-word gaps, as configured by + /// . + /// + /// + /// No-op when the line was finalized with skipJustification, when wrapping is + /// disabled, when no justification style is selected, or when the line is already at or + /// beyond the wrapping length. + /// + /// The text options supplying the wrapping length and justification style. + public void Justify(TextOptions options) + { + if (options.WrappingLength == -1F || options.TextJustification == TextJustification.None) + { + return; + } + + if (this.ScaledLineAdvance == 0) + { + return; + } + + float delta = (options.WrappingLength / options.Dpi) - this.ScaledLineAdvance; + if (delta <= 0) + { + return; + } + + // Increase the advance for all non zero-width glyphs but the last. + if (options.TextJustification == TextJustification.InterCharacter) + { + int nonZeroCount = 0; + for (int i = 0; i < this.data.Count; i++) + { + GlyphLayoutData glyph = this.data[i]; + if (!CodePoint.IsZeroWidthJoiner(glyph.CodePoint) + && !CodePoint.IsZeroWidthNonJoiner(glyph.CodePoint)) + { + nonZeroCount++; + } + } + + int opportunityCount = nonZeroCount - 1; + if (opportunityCount == 0) + { + return; + } + + float padding = delta / opportunityCount; + int remainingOpportunities = opportunityCount; + for (int i = 0; i < this.data.Count && remainingOpportunities > 0; i++) + { + GlyphLayoutData glyph = this.data[i]; + if (!CodePoint.IsZeroWidthJoiner(glyph.CodePoint) + && !CodePoint.IsZeroWidthNonJoiner(glyph.CodePoint)) + { + glyph.ScaledAdvance += padding; + this.data[i] = glyph; + remainingOpportunities--; + } + } + + RecalculateLineMetrics(this); + return; + } + + // Increase the advance for all spaces but the last. + if (options.TextJustification == TextJustification.InterWord) + { + // Count all the whitespace characters. + int whiteSpaceCount = 0; + for (int i = 0; i < this.data.Count; i++) + { + GlyphLayoutData glyph = this.data[i]; + if (CodePoint.IsWhiteSpace(glyph.CodePoint)) + { + whiteSpaceCount++; + } + } + + if (whiteSpaceCount == 0) + { + return; + } + + float padding = delta / whiteSpaceCount; + for (int i = 0; i < this.data.Count; i++) + { + GlyphLayoutData glyph = this.data[i]; + if (CodePoint.IsWhiteSpace(glyph.CodePoint)) + { + glyph.ScaledAdvance += padding; + this.data[i] = glyph; + } + } + } + + RecalculateLineMetrics(this); + } + + /// + /// Re-orders the entries in this line from logical to visual order according to the + /// Unicode Bidirectional Algorithm (, rules L1 and L2). + /// + public void BidiReOrder() + { + // Build up the collection of ordered runs. + BidiRun run = this.data[0].BidiRun; + OrderedBidiRun orderedRun = new(run.Level); + OrderedBidiRun? current = orderedRun; + for (int i = 0; i < this.data.Count; i++) + { + GlyphLayoutData g = this.data[i]; + if (run != g.BidiRun) + { + run = g.BidiRun; + current.Next = new(run.Level); + current = current.Next; + } + + current.Add(g); + } + + // Reorder them into visual order. + orderedRun = LinearReOrder(orderedRun); + + // Now perform a recursive reversal of each run. + // From the highest level found in the text to the lowest odd level on each line, including intermediate levels + // not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher. + // https://unicode.org/reports/tr9/#L2 + int max = 0; + int min = int.MaxValue; + for (int i = 0; i < this.data.Count; i++) + { + int level = this.data[i].BidiRun.Level; + if (level > max) + { + max = level; + } + + if ((level & 1) != 0 && level < min) + { + min = level; + } + } + + if (min > max) + { + min = max; + } + + if (max == 0 || (min == max && (max & 1) == 0)) + { + // Nothing to reverse. + return; + } + + // Now apply the reversal and replace the original contents. + int minLevelToReverse = max; + while (minLevelToReverse >= min) + { + current = orderedRun; + while (current != null) + { + if (current.Level >= minLevelToReverse) + { + current.Reverse(); + } + + current = current.Next; + } + + minLevelToReverse--; + } + + this.data.Clear(); + current = orderedRun; + while (current != null) + { + this.data.AddRange(current.AsSlice()); + current = current.Next; + } + } + + /// + /// Recomputes the aggregated per-line metrics (advance, max line height, ascender, + /// descender, delta, min-Y) from the current entries. Called after any mutation that + /// can affect these — split, insert, trim, justify. + /// + /// The line to recompute metrics for. + private static void RecalculateLineMetrics(TextLine textLine) + { + // Lastly recalculate this line metrics. + float advance = 0; + float ascender = 0; + float descender = 0; + float delta = 0; + float lineHeight = 0; + float minY = 0; + for (int i = 0; i < textLine.Count; i++) + { + GlyphLayoutData glyph = textLine[i]; + advance += glyph.ScaledAdvance; + ascender = MathF.Max(ascender, glyph.ScaledAscender); + descender = MathF.Max(descender, glyph.ScaledDescender); + delta = MathF.Max(delta, glyph.ScaledDelta); + lineHeight = MathF.Max(lineHeight, glyph.ScaledLineHeight); + minY = MathF.Min(minY, glyph.ScaledMinY); + } + + textLine.ScaledLineAdvance = advance; + textLine.ScaledMaxAscender = ascender; + textLine.ScaledMaxDescender = descender; + textLine.ScaledMaxDelta = delta; + textLine.ScaledMaxLineHeight = lineHeight; + textLine.ScaledMinY = minY; + + textLine.advances.Clear(); + } + + /// + /// Reorders a series of runs from logical to visual order, returning the left most run. + /// + /// + /// The ordered bidi run. + /// The . + private static OrderedBidiRun LinearReOrder(OrderedBidiRun? line) + { + BidiRange? range = null; + OrderedBidiRun? run = line; + + while (run != null) + { + OrderedBidiRun? next = run.Next; + + while (range != null && range.Level > run.Level + && range.Previous != null && range.Previous.Level >= run.Level) + { + range = BidiRange.MergeWithPrevious(range); + } + + if (range != null && range.Level >= run.Level) + { + // Attach run to the range. + if ((run.Level & 1) != 0) + { + // Odd, range goes to the right of run. + run.Next = range.Left; + range.Left = run; + } + else + { + // Even, range goes to the left of run. + range.Right!.Next = run; + range.Right = run; + } + + range.Level = run.Level; + } + else + { + BidiRange r = new(); + r.Left = r.Right = run; + r.Level = run.Level; + r.Previous = range; + range = r; + } + + run = next; + } + + while (range?.Previous != null) + { + range = BidiRange.MergeWithPrevious(range); + } + + // Terminate. + range!.Right!.Next = null; + return range!.Left!; + } + + /// + /// A node in the linked list of contiguous same-level bidi runs used by . + /// Each node owns the glyph entries at its bidi embedding level and can be reversed in place. + /// + private sealed class OrderedBidiRun + { + private ArrayBuilder info; + + /// + /// Initializes a new instance of the class. + /// + /// The bidi embedding level for this run. + public OrderedBidiRun(int level) => this.Level = level; + + /// Gets the bidi embedding level of this run. + public int Level { get; } + + /// Gets or sets the next run in visual order. + public OrderedBidiRun? Next { get; set; } + + /// Appends an entry to this run. + /// The entry to append. + public void Add(GlyphLayoutData info) => this.info.Add(info); + + /// Returns a slice view over this run's entries. + /// A slice over the entries. + public ArraySlice AsSlice() => this.info.AsSlice(); + + /// Reverses the entries in this run in place (for rule L2). + public void Reverse() => this.AsSlice().Span.Reverse(); + } + + /// + /// An intermediate grouping of links used by the linear-reorder + /// algorithm to stitch pairs of same-level ranges together. + /// + private sealed class BidiRange + { + /// Gets or sets the shared bidi embedding level for this range. + public int Level { get; set; } + + /// Gets or sets the leftmost run in the range. + public OrderedBidiRun? Left { get; set; } + + /// Gets or sets the rightmost run in the range. + public OrderedBidiRun? Right { get; set; } + + /// Gets or sets the previous range in the processing stack. + public BidiRange? Previous { get; set; } + + /// + /// Stitches the current range with its predecessor, producing a single merged range + /// whose internal orientation depends on the predecessor's embedding level parity. + /// + /// The current range whose will be merged. + /// The merged range (always the predecessor instance, reused in place). + public static BidiRange MergeWithPrevious(BidiRange? range) + { + BidiRange previous = range!.Previous!; + BidiRange left; + BidiRange right; + + if ((previous.Level & 1) != 0) + { + // Odd, previous goes to the right of range. + left = range; + right = previous; + } + else + { + // Even, previous goes to the left of range. + left = previous; + right = range; + } + + // Stitch them + left.Right!.Next = right.Left; + previous.Left = left.Left; + previous.Right = right.Right; + + return previous; + } + } + } +} diff --git a/SixLabors.Fonts/TextLineBreakEnumerator.cs b/SixLabors.Fonts/TextLineBreakEnumerator.cs new file mode 100644 index 0000000..fe8acfa --- /dev/null +++ b/SixLabors.Fonts/TextLineBreakEnumerator.cs @@ -0,0 +1,341 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using System; +using System.Collections.Generic; + +namespace SixLabors.Fonts { + /// + /// Breaks a prepared logical line one visual line at a time. + /// + internal sealed class TextLineBreakEnumerator + { + private readonly LogicalTextLine logicalLine; + private readonly TextOptions options; + private readonly bool breakAll; + private readonly bool keepAll; + private readonly bool breakWord; + private readonly bool normalizeDecomposedAdvances; + private readonly int maxLines; + private readonly CodePoint? ellipsisMarkerCodePoint; + private readonly IReadOnlyList lineBreaks; + private TextLine textLine; + private int processed; + private int lineCount; + private TextLine? current; + + /// + /// Initializes a new instance of the class. + /// + /// The logical line to break. + /// The text options used for layout. + public TextLineBreakEnumerator(in LogicalTextLine logicalLine, TextOptions options) + { + this.logicalLine = logicalLine; + this.options = options; + this.breakAll = options.WordBreaking == WordBreaking.BreakAll; + this.keepAll = options.WordBreaking == WordBreaking.KeepAll; + this.breakWord = options.WordBreaking == WordBreaking.BreakWord; + this.normalizeDecomposedAdvances = options.LayoutMode.IsVertical(); + this.maxLines = options.MaxLines; + this.ellipsisMarkerCodePoint = TextLayout.GetEllipsisMarkerCodePoint(options); + this.lineBreaks = logicalLine.LineBreaks; + + // The breaker mutates the remaining line as it advances, so each cursor owns + // a clone of the immutable prepared line held by TextBlock. + this.textLine = new(logicalLine.TextLine); + } + + /// + /// Gets the current finalized visual line. + /// + public TextLine Current => this.current!; + + /// + /// Advances to the next visual line using the supplied wrapping length. + /// + /// The wrapping length in pixels. + /// when a line was produced. + public bool MoveNext(float wrappingLength) + { + if (this.textLine.Count == 0) + { + return false; + } + + bool shouldWrap = wrappingLength > 0; + + // Wrapping length is always provided in pixels. Convert to inches for comparison. + float scaledWrappingLength = shouldWrap ? wrappingLength / this.options.Dpi : float.MaxValue; + + while (this.textLine.Count > 0) + { + LineBreak? bestBreak = null; + foreach (LineBreak lineBreak in this.lineBreaks) + { + // Skip breaks that are already behind the processed portion. + if (lineBreak.PositionWrap <= this.processed) + { + continue; + } + + // Measure the text up to the adjusted break point. + int measureIndex = lineBreak.PositionMeasure - this.processed; + float advance = this.textLine.MeasureAt(measureIndex); + if (lineBreak.IsHyphenationBreak) + { + advance += this.textLine.GetHyphenationMarkerAdvance( + measureIndex - 1, + this.logicalLine.HyphenationMarkers); + } + + if (advance >= scaledWrappingLength) + { + bestBreak ??= lineBreak; + break; + } + + // If it's a mandatory break, stop immediately. + if (lineBreak.Required) + { + bestBreak = lineBreak; + break; + } + + // Update the best break. + bestBreak = lineBreak; + } + + if (bestBreak != null) + { + if (this.BreakAt(bestBreak.Value, scaledWrappingLength)) + { + return true; + } + + continue; + } + + return this.BreakLastLine(scaledWrappingLength); + } + + return false; + } + + /// + /// Breaks the current remaining line at the supplied break opportunity. + /// + /// The selected line break opportunity. + /// The wrapping length in inches. + /// when a visual line was produced. + private bool BreakAt(LineBreak breakAt, float scaledWrappingLength) + { + if (this.breakAll) + { + return this.BreakAtAnyGlyph(breakAt, scaledWrappingLength); + } + + int hyphenationMarkerIndex = breakAt.PositionMeasure - this.processed - 1; + + // Split the current line at the adjusted break index. + if (this.textLine.TrySplitAt(breakAt, this.keepAll, out TextLine? remaining)) + { + if (breakAt.IsHyphenationBreak) + { + this.textLine.ApplyHyphenationMarker( + hyphenationMarkerIndex, + this.logicalLine.HyphenationMarkers); + } + + if (breakAt.Required + && this.options.TextInteractionMode == TextInteractionMode.Editor + && remaining.Count > 0 + && remaining[0].IsNewLine + && this.textLine.TrySplitTerminalHardBreak(out TextLine? blankLine)) + { + // Consecutive hard breaks need an editable blank line for the break that + // ended this segment, plus the next break still waiting in the remainder. + remaining.InsertAt(0, blankLine); + } + + // If 'keepAll' is true then the break could be later than expected. + this.processed = this.keepAll + ? this.processed + Math.Max(this.textLine.Count, breakAt.PositionWrap - this.processed) + : breakAt.PositionWrap; + + if (this.breakWord) + { + // A break was found, but we need to check if the line is too long + // and break if required. + if (this.textLine.ScaledLineAdvance > scaledWrappingLength && + this.textLine.TrySplitAt(scaledWrappingLength, out TextLine? overflow)) + { + // Reinsert the overflow at the beginning of the remaining line. + this.processed -= overflow.Count; + remaining.InsertAt(0, overflow); + } + } + + bool stopLayout = this.SetCurrent( + this.textLine, + breakAt.Required, + remaining.Count > 0, + scaledWrappingLength); + + this.textLine = stopLayout ? new TextLine() : remaining; + return true; + } + + this.processed += this.textLine.Count; + return false; + } + + /// + /// Breaks the current remaining line using CSS behavior. + /// + /// The selected line break opportunity. + /// The wrapping length in inches. + /// when a visual line was produced. + private bool BreakAtAnyGlyph(LineBreak breakAt, float scaledWrappingLength) + { + TextLine? remaining; + if (breakAt.Required) + { + if (this.textLine.TrySplitAt(breakAt, this.keepAll, out remaining)) + { + this.processed = breakAt.PositionWrap; + + bool stopLayout = this.SetCurrent( + this.textLine, + true, + remaining.Count > 0, + scaledWrappingLength); + + this.textLine = stopLayout ? new TextLine() : remaining; + return true; + } + } + else if (this.textLine.TrySplitAt(scaledWrappingLength, out remaining)) + { + this.processed += this.textLine.Count; + + bool stopLayout = this.SetCurrent( + this.textLine, + false, + remaining.Count > 0, + scaledWrappingLength); + + this.textLine = stopLayout ? new TextLine() : remaining; + return true; + } + else + { + this.processed += this.textLine.Count; + } + + return false; + } + + /// + /// Breaks and finalizes the last remaining line. + /// + /// The wrapping length in inches. + /// when a visual line was produced. + private bool BreakLastLine(float scaledWrappingLength) + { + if (this.breakWord || this.breakAll) + { + while (this.textLine.ScaledLineAdvance > scaledWrappingLength) + { + if (!this.textLine.TrySplitAt(scaledWrappingLength, out TextLine? overflow)) + { + break; + } + + bool stopLayout = this.SetCurrent( + this.textLine, + false, + overflow.Count > 0, + scaledWrappingLength); + + // Width-based overflow splits do not come from a stored LineBreak, so the + // cursor advances by the consumed entries before the next MoveNext scan. + this.processed += this.textLine.Count; + this.textLine = stopLayout ? new TextLine() : overflow; + return true; + } + } + + if (this.options.TextInteractionMode == TextInteractionMode.Editor + && this.textLine.TrySplitTerminalHardBreak(out TextLine? hardBreakLine)) + { + // A terminal Enter has no following glyph for the normal required-break split. + // Editor interaction still needs the next blank line as a caret target. + this.SetCurrent( + this.textLine, + true, + true, + scaledWrappingLength); + + this.textLine = hardBreakLine; + return true; + } + + this.SetCurrent( + this.textLine, + true, + false, + scaledWrappingLength); + + this.textLine = new TextLine(); + return true; + } + + /// + /// Finalizes the current line and stores it as the enumerator result. + /// + /// The line to finalize. + /// Whether the line should skip justification. + /// Whether source text remains after this line. + /// The wrapping length in inches. + /// when no further lines should be produced. + private bool SetCurrent( + TextLine line, + bool skipJustification, + bool hasOverflow, + float scaledWrappingLength) + { + bool isLimitedFinalLine = this.maxLines > -1 && this.lineCount + 1 >= this.maxLines; + if (isLimitedFinalLine && hasOverflow) + { + // A max-lines ellipsis is a final-line transformation: wrapping has already + // chosen the visible line, so the marker replaces the tail of that line and + // the line must behave like a paragraph-final line for justification. + if (this.ellipsisMarkerCodePoint.HasValue) + { + line.ApplyEllipsisMarker(this.ellipsisMarkerCodePoint.Value, scaledWrappingLength, this.options); + } + + skipJustification = true; + } + + bool preserveTrailingBreakingWhitespace = this.options.TextInteractionMode == TextInteractionMode.Editor; + + // Paragraph layout trims trailing breaking whitespace. Editor interaction keeps + // ordinary trailing whitespace addressable so typed spaces can advance the caret. + this.current = line.Finalize( + skipJustification, + this.normalizeDecomposedAdvances, + preserveTrailingBreakingWhitespace); + + if (!this.current.SkipJustification) + { + this.current.Justify(this.options); + } + + this.lineCount++; + return isLimitedFinalLine; + } + } +} diff --git a/SixLabors.Fonts/TextMeasurer.cs b/SixLabors.Fonts/TextMeasurer.cs new file mode 100644 index 0000000..e5ddfb6 --- /dev/null +++ b/SixLabors.Fonts/TextMeasurer.cs @@ -0,0 +1,200 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts { + /// + /// Encapsulates logic for laying out and then measuring text properties. + /// + public static class TextMeasurer + { + /// + public static TextMetrics Measure(string text, TextOptions options) + => Measure(text.AsSpan(), options); + + /// + /// Measures the full set of layout metrics for the supplied text in a single pass. + /// + /// The text. + /// The text options. controls wrapping; use -1 to disable wrapping. + /// A instance containing every measurement for the laid-out text. + public static TextMetrics Measure(ReadOnlySpan text, TextOptions options) + { + TextBlock block = new(text, options); + return block.Measure(options.WrappingLength); + } + + /// + public static FontRectangle MeasureAdvance(string text, TextOptions options) + => MeasureAdvance(text.AsSpan(), options); + + /// + /// Measures the logical advance of the text in pixel units. + /// + /// The text. + /// The text options. controls wrapping; use -1 to disable wrapping. + /// The logical advance rectangle of the text if it was to be rendered. + public static FontRectangle MeasureAdvance(ReadOnlySpan text, TextOptions options) + { + if (text.IsEmpty) + { + return FontRectangle.Empty; + } + + TextBlock block = new(text, options); + return block.MeasureAdvance(options.WrappingLength); + } + + /// + public static FontRectangle MeasureBounds(string text, TextOptions options) + => MeasureBounds(text.AsSpan(), options); + + /// + public static FontRectangle MeasureRenderableBounds(string text, TextOptions options) + => MeasureRenderableBounds(text.AsSpan(), options); + + /// + /// Measures the rendered glyph bounds of the text in pixel units. + /// + /// The text. + /// The text options. controls wrapping; use -1 to disable wrapping. + /// The rendered glyph bounds of the text if it was to be rendered. + public static FontRectangle MeasureBounds(ReadOnlySpan text, TextOptions options) + { + if (text.IsEmpty) + { + return FontRectangle.Empty; + } + + TextBlock block = new(text, options); + return block.MeasureBounds(options.WrappingLength); + } + + /// + /// Measures the full renderable bounds of the text in pixel units. + /// + /// The text. + /// The text options. controls wrapping; use -1 to disable wrapping. + /// + /// The union of the logical advance rectangle and the rendered glyph bounds if the text was to be rendered. + /// + public static FontRectangle MeasureRenderableBounds(ReadOnlySpan text, TextOptions options) + { + if (text.IsEmpty) + { + return FontRectangle.Empty; + } + + TextBlock block = new(text, options); + return block.MeasureRenderableBounds(options.WrappingLength); + } + + /// + public static ReadOnlyMemory GetGlyphMetrics(string text, TextOptions options) + => GetGlyphMetrics(text.AsSpan(), options); + + /// + /// Gets the positioned metrics of each laid-out glyph entry in pixel units. + /// + /// The text. + /// The text options. controls wrapping; use -1 to disable wrapping. + /// A read-only memory region containing the per-glyph metrics entries of the text if it was to be rendered. + public static ReadOnlyMemory GetGlyphMetrics(ReadOnlySpan text, TextOptions options) + { + if (text.IsEmpty) + { + return ReadOnlyMemory.Empty; + } + + TextBlock block = new(text, options); + return block.GetGlyphMetrics(options.WrappingLength); + } + + /// + public static ReadOnlyMemory GetGraphemeMetrics(string text, TextOptions options) + => GetGraphemeMetrics(text.AsSpan(), options); + + /// + /// Gets the positioned metrics of each laid-out grapheme in pixel units. + /// + /// The text. + /// The text options. controls wrapping; use -1 to disable wrapping. + /// A read-only memory region containing the per-grapheme metrics entries of the text if it was to be rendered. + public static ReadOnlyMemory GetGraphemeMetrics(ReadOnlySpan text, TextOptions options) + { + if (text.IsEmpty) + { + return ReadOnlyMemory.Empty; + } + + TextBlock block = new(text, options); + return block.GetGraphemeMetrics(options.WrappingLength); + } + + /// + public static ReadOnlyMemory GetWordMetrics(string text, TextOptions options) + => GetWordMetrics(text.AsSpan(), options); + + /// + /// Gets the positioned metrics of each Unicode word-boundary segment in pixel units. + /// + /// The text. + /// The text options. controls wrapping; use -1 to disable wrapping. + /// A read-only memory region containing the per-word-boundary segment metrics entries of the text if it was to be rendered. + public static ReadOnlyMemory GetWordMetrics(ReadOnlySpan text, TextOptions options) + { + if (text.IsEmpty) + { + return ReadOnlyMemory.Empty; + } + + TextBlock block = new(text, options); + return block.GetWordMetrics(options.WrappingLength); + } + + /// + public static int CountLines(string text, TextOptions options) + => CountLines(text.AsSpan(), options); + + /// + /// Gets the number of laid-out lines contained within the text. + /// + /// The text. + /// The text options. controls wrapping; use -1 to disable wrapping. + /// The laid-out line count. + public static int CountLines(ReadOnlySpan text, TextOptions options) + { + if (text.IsEmpty) + { + return 0; + } + + TextBlock block = new(text, options); + return block.CountLines(options.WrappingLength); + } + + /// + public static ReadOnlyMemory GetLineMetrics(string text, TextOptions options) + => GetLineMetrics(text.AsSpan(), options); + + /// + /// Gets per-line layout metrics for the supplied text. + /// + /// The text to measure. + /// The text options. controls wrapping; use -1 to disable wrapping. + /// + /// A read-only memory region containing in pixel units, one entry per laid-out line. + /// + public static ReadOnlyMemory GetLineMetrics(ReadOnlySpan text, TextOptions options) + { + if (text.IsEmpty) + { + return ReadOnlyMemory.Empty; + } + + TextBlock block = new(text, options); + return block.GetLineMetrics(options.WrappingLength); + } + } +} diff --git a/SixLabors.Fonts/TextMetrics.cs b/SixLabors.Fonts/TextMetrics.cs new file mode 100644 index 0000000..42ca0da --- /dev/null +++ b/SixLabors.Fonts/TextMetrics.cs @@ -0,0 +1,229 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; + +namespace SixLabors.Fonts { + /// + /// Encapsulates the full set of measurement results for laid-out text. + /// + public sealed class TextMetrics + { + private readonly TextBlock textBlock; + private readonly TextBox textBox; + private readonly float wrappingLength; + private readonly LayoutMode layoutMode; + private readonly TextDirection textDirection; + private readonly GraphemeMetrics[] graphemeMetrics; + private readonly LineMetrics[] lineMetrics; + private readonly WordMetrics[] wordMetrics; + private GlyphMetrics[]? glyphMetrics; + + internal TextMetrics( + TextBlock textBlock, + TextBox textBox, + float wrappingLength, + FontRectangle advance, + FontRectangle bounds, + FontRectangle renderableBounds, + int lineCount, + GraphemeMetrics[] graphemes, + LineMetrics[] lines, + WordMetrics[] words) + { + this.textBlock = textBlock; + this.textBox = textBox; + this.wrappingLength = wrappingLength; + this.layoutMode = textBlock.Options.LayoutMode; + this.textDirection = textBox.TextLines.Count == 0 + ? (textBlock.Options.TextDirection == TextDirection.RightToLeft ? TextDirection.RightToLeft : TextDirection.LeftToRight) + : textBox.TextDirection(); + + this.Advance = advance; + this.Bounds = bounds; + this.RenderableBounds = renderableBounds; + this.LineCount = lineCount; + this.graphemeMetrics = graphemes; + this.lineMetrics = lines; + this.wordMetrics = words; + } + + /// + /// Gets the logical advance rectangle of the text in pixel units. + /// + /// + /// Reflects line-box height and horizontal or vertical text advance from the layout model. + /// Does not guarantee that all rendered glyph pixels fit within the returned rectangle. + /// + public FontRectangle Advance { get; } + + /// + /// Gets the rendered glyph bounds of the text in pixel units. + /// + /// + /// This is the tight ink bounds enclosing all rendered glyphs and may be smaller or larger + /// than the logical advance. May have a non-zero origin. + /// + public FontRectangle Bounds { get; } + + /// + /// Gets the union of the logical advance rectangle (positioned at the text options origin) + /// and the rendered glyph bounds in pixel units. + /// + /// + /// Use this rectangle when both typographic advance and rendered glyph overshoot + /// must fit within the same bounding box. + /// + public FontRectangle RenderableBounds { get; } + + /// + /// Gets the number of laid-out lines in the text. + /// + public int LineCount { get; } + + /// + /// Gets the grapheme metrics entries in final layout order. + /// + public ReadOnlySpan GraphemeMetrics => this.graphemeMetrics; + + /// + /// Gets the per-line layout metrics for the text. + /// + public ReadOnlySpan LineMetrics => this.lineMetrics; + + /// + /// Gets the word-boundary segment metrics in source order. + /// + public ReadOnlySpan WordMetrics => this.wordMetrics; + + /// + /// Hit tests the supplied point against the laid-out grapheme advance bounds. + /// + /// The point in pixel units. + /// The hit-tested grapheme position. + public TextHit HitTest(Vector2 point) + => TextInteraction.HitTest( + this.LineMetrics, + this.GraphemeMetrics, + point, + this.layoutMode); + + /// + /// Gets the caret position for the supplied hit. + /// + /// The hit-tested grapheme position. + /// The caret position in pixel units. + public CaretPosition GetCaretPosition(TextHit hit) + => TextInteraction.GetCaretPosition( + this.LineMetrics, + this.GraphemeMetrics, + hit.GraphemeInsertionIndex, + this.layoutMode); + + /// + /// Gets an absolute caret position in the laid-out text. + /// + /// The absolute caret placement. + /// The caret position in pixel units. + public CaretPosition GetCaret(CaretPlacement placement) + => TextInteraction.GetCaret( + this.LineMetrics, + this.GraphemeMetrics, + placement, + this.layoutMode, + this.textDirection); + + /// + /// Moves the supplied caret by the requested operation. + /// + /// The current caret position. + /// The movement operation. + /// The moved caret position in pixel units. + public CaretPosition MoveCaret(CaretPosition caret, CaretMovement movement) + => TextInteraction.MoveCaret( + this.LineMetrics, + this.GraphemeMetrics, + this.WordMetrics, + caret, + movement, + this.layoutMode, + this.textDirection); + + /// + /// Gets the word metrics for the word-boundary segment containing the supplied hit-tested grapheme position. + /// + /// The hit-tested grapheme position. + /// The word metrics containing the hit grapheme. + public WordMetrics GetWordMetrics(TextHit hit) + => TextInteraction.GetWordMetrics(this.WordMetrics, hit.GraphemeIndex); + + /// + /// Gets the word metrics for the word-boundary segment containing the supplied caret position. + /// + /// The caret position. + /// The word metrics containing the caret's grapheme insertion index. + public WordMetrics GetWordMetrics(CaretPosition caret) + => TextInteraction.GetWordMetrics(this.WordMetrics, caret.GraphemeIndex); + + /// + /// Gets selection bounds between two hit-tested grapheme positions. + /// + /// The fixed selection endpoint. + /// The active selection endpoint. + /// A read-only memory region containing the selection bounds in visual order and pixel units. + public ReadOnlyMemory GetSelectionBounds(TextHit anchor, TextHit focus) + => TextInteraction.GetSelectionBounds( + this.LineMetrics, + this.GraphemeMetrics, + anchor.GraphemeInsertionIndex, + focus.GraphemeInsertionIndex, + this.layoutMode); + + /// + /// Gets selection bounds between two caret positions. + /// + /// The fixed selection endpoint. + /// The active selection endpoint. + /// A read-only memory region containing the selection bounds in visual order and pixel units. + public ReadOnlyMemory GetSelectionBounds(CaretPosition anchor, CaretPosition focus) + => TextInteraction.GetSelectionBounds( + this.LineMetrics, + this.GraphemeMetrics, + anchor.GraphemeIndex, + focus.GraphemeIndex, + this.layoutMode); + + /// + /// Gets selection bounds for the supplied grapheme metrics. + /// + /// The grapheme metrics to select. + /// A read-only memory region containing the selection bounds in visual order and pixel units. + public ReadOnlyMemory GetSelectionBounds(GraphemeMetrics metrics) + => TextInteraction.GetSelectionBounds( + this.LineMetrics, + this.GraphemeMetrics, + metrics, + this.layoutMode); + + /// + /// Gets selection bounds for the supplied word metrics. + /// + /// The word metrics to select. + /// A read-only memory region containing the selection bounds in visual order and pixel units. + public ReadOnlyMemory GetSelectionBounds(WordMetrics metrics) + => TextInteraction.GetSelectionBounds( + this.LineMetrics, + this.GraphemeMetrics, + metrics.GraphemeStart, + metrics.GraphemeEnd, + this.layoutMode); + + /// + public ReadOnlyMemory GetGlyphMetrics() + => this.glyphMetrics ??= TextBlock.GetGlyphMetricsArray( + this.textBox, + this.textBlock.Options, + this.wrappingLength); + } +} diff --git a/SixLabors.Fonts/TextOptions.cs b/SixLabors.Fonts/TextOptions.cs new file mode 100644 index 0000000..24f2c83 --- /dev/null +++ b/SixLabors.Fonts/TextOptions.cs @@ -0,0 +1,249 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// Provides configuration options for rendering and shaping text. + /// + public class TextOptions + { + private float dpi = 72F; + private float lineSpacing = 1F; + private Font? font; + + /// + /// Initializes a new instance of the class. + /// + /// The font. + public TextOptions(Font font) => this.Font = font; + + /// + /// Initializes a new instance of the class from properties + /// copied from the given instance. + /// + /// The options whose properties are copied into this instance. + public TextOptions(TextOptions options) + { + this.Font = options.Font; + this.FallbackFontFamilies = new List(options.FallbackFontFamilies); + this.TabWidth = options.TabWidth; + this.HintingMode = options.HintingMode; + this.Dpi = options.Dpi; + this.LineSpacing = options.LineSpacing; + this.Origin = options.Origin; + this.WrappingLength = options.WrappingLength; + this.MaxLines = options.MaxLines; + this.WordBreaking = options.WordBreaking; + this.TextEllipsis = options.TextEllipsis; + this.CustomEllipsis = options.CustomEllipsis; + this.TextHyphenation = options.TextHyphenation; + this.CustomHyphen = options.CustomHyphen; + this.TextDirection = options.TextDirection; + this.TextBidiMode = options.TextBidiMode; + this.TextInteractionMode = options.TextInteractionMode; + this.TextAlignment = options.TextAlignment; + this.TextJustification = options.TextJustification; + this.HorizontalAlignment = options.HorizontalAlignment; + this.VerticalAlignment = options.VerticalAlignment; + this.LayoutMode = options.LayoutMode; + this.KerningMode = options.KerningMode; + this.Tracking = options.Tracking; + this.ColorFontSupport = options.ColorFontSupport; + this.FeatureTags = new List(options.FeatureTags); + this.TextRuns = new List(options.TextRuns); + this.DecorationPositioningMode = options.DecorationPositioningMode; + } + + /// + /// Gets or sets the font. + /// + public Font Font + { + get => this.font!; + set + { + Guard.NotNull(value, nameof(this.Font)); + this.font = value; + } + } + + /// + /// Gets or sets the collection of fallback font families to use when + /// a specific glyph is missing from . + /// + public IReadOnlyList FallbackFontFamilies { get; set; } = Array.Empty(); + + /// + /// Gets or sets the DPI (Dots Per Inch) to render/measure the text at. + /// + /// Defaults to 72F. + /// + public float Dpi + { + get => this.dpi; + + set + { + Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.Dpi)); + this.dpi = value; + } + } + + /// + /// Gets or sets the width of the tab. Measured as the distance in spaces (U+0020). + /// + /// + /// If value is -1 then the font default tab width is used. + /// + public float TabWidth { get; set; } = -1F; + + /// + /// Gets or sets a value indicating whether to apply hinting - The use of mathematical instructions + /// to adjust the display of an outline font so that it lines up with a rasterized grid. + /// + public HintingMode HintingMode { get; set; } + + /// + /// Gets or sets the line spacing. Applied as a multiple of the line height. + /// + /// Defaults to 1F. + /// + public float LineSpacing + { + get => this.lineSpacing; + + set + { + Guard.IsTrue(value != 0, nameof(this.LineSpacing), "Value must not be equal to 0."); + this.lineSpacing = value; + } + } + + /// + /// Gets or sets the rendering origin. + /// + public Vector2 Origin { get; set; } = Vector2.Zero; + + /// + /// Gets or sets the length in pixel units (px) at which text will automatically wrap onto a new line. + /// This property also affects the width or height (depending on the ) of the text box + /// for alignment of text. + /// + /// + /// If value is -1 then wrapping is disabled. + /// + public float WrappingLength { get; set; } = -1F; + + /// + /// Gets or sets the maximum number of lines to lay out. + /// + /// + /// If value is -1 then the number of lines is unlimited. + /// + public int MaxLines { get; set; } = -1; + + /// + /// Gets or sets the word breaking mode to use when wrapping text. + /// + public WordBreaking WordBreaking { get; set; } + + /// + /// Gets or sets the ellipsis behavior to use when laid-out text is limited to a maximum number of lines. + /// + public TextEllipsis TextEllipsis { get; set; } + + /// + /// Gets or sets the ellipsis marker to use when is Custom. + /// + public CodePoint? CustomEllipsis { get; set; } + + /// + /// Gets or sets the hyphenation marker behavior to use when text breaks at hyphenation opportunities. + /// + public TextHyphenation TextHyphenation { get; set; } + + /// + /// Gets or sets the hyphenation marker to use when is Custom. + /// + public CodePoint? CustomHyphen { get; set; } + + /// + /// Gets or sets the text direction. + /// + public TextDirection TextDirection { get; set; } = TextDirection.Auto; + + /// + /// Gets or sets how bidirectional text is resolved. + /// + public TextBidiMode TextBidiMode { get; set; } + + /// + /// Gets or sets how caret movement and selection model trailing breaking whitespace. + /// + public TextInteractionMode TextInteractionMode { get; set; } + + /// + /// Gets or sets the text alignment of the text within the box. + /// + public TextAlignment TextAlignment { get; set; } + + /// + /// Gets or sets the justification of the text within the box. + /// + public TextJustification TextJustification { get; set; } + + /// + /// Gets or sets the horizontal alignment of the text box. + /// + public HorizontalAlignment HorizontalAlignment { get; set; } + + /// + /// Gets or sets the vertical alignment of the text box. + /// + public VerticalAlignment VerticalAlignment { get; set; } + + /// + /// Gets or sets the layout mode for the text lines. + /// + public LayoutMode LayoutMode { get; set; } + + /// + /// Gets or sets the kerning mode indicating whether to apply kerning (character spacing adjustments) + /// to the glyph positions from information found within the font. + /// + public KerningMode KerningMode { get; set; } + + /// + /// Gets or sets the tracking (letter-spacing) value. + /// Tracking adjusts the spacing between all characters uniformly and is measured in em. + /// Positive values increase spacing, negative values decrease spacing, and zero applies no adjustment. + /// + public float Tracking { get; set; } + + /// + /// Gets or sets the positioning mode used for rendering decorations. + /// + public DecorationPositioningMode DecorationPositioningMode { get; set; } + + /// + /// Gets or sets the color font support options. + /// + public ColorFontSupport ColorFontSupport { get; set; } = ColorFontSupport.ColrV1 | ColorFontSupport.ColrV0 | ColorFontSupport.Svg; + + /// + /// Gets or sets the collection of additional feature tags to apply during glyph shaping. + /// + public IReadOnlyList FeatureTags { get; set; } = Array.Empty(); + + /// + /// Gets or sets an optional collection of text runs to apply to the body of text. + /// + public IReadOnlyList TextRuns { get; set; } = Array.Empty(); + } +} diff --git a/SixLabors.Fonts/TextPlaceholder.cs b/SixLabors.Fonts/TextPlaceholder.cs new file mode 100644 index 0000000..e26973f --- /dev/null +++ b/SixLabors.Fonts/TextPlaceholder.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Represents an atomic inline placeholder with caller-supplied dimensions. + /// + public readonly struct TextPlaceholder + { + /// + /// Initializes a new instance of the struct. + /// + /// The placeholder width in pixel units. + /// The placeholder height in pixel units. + /// The placeholder alignment against surrounding text. + /// The distance from the placeholder top edge to its baseline in pixel units. + public TextPlaceholder( + float width, + float height, + TextPlaceholderAlignment alignment, + float baselineOffset) + { + this.Width = width; + this.Height = height; + this.Alignment = alignment; + this.BaselineOffset = baselineOffset; + } + + /// + /// Gets the placeholder width in pixel units. + /// + public float Width { get; } + + /// + /// Gets the placeholder height in pixel units. + /// + public float Height { get; } + + /// + /// Gets the placeholder alignment against surrounding text. + /// + public TextPlaceholderAlignment Alignment { get; } + + /// + /// Gets the distance from the placeholder top edge to its baseline in pixel units. + /// + public float BaselineOffset { get; } + } +} diff --git a/SixLabors.Fonts/TextPlaceholderAlignment.cs b/SixLabors.Fonts/TextPlaceholderAlignment.cs new file mode 100644 index 0000000..0823f93 --- /dev/null +++ b/SixLabors.Fonts/TextPlaceholderAlignment.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Specifies how an inline placeholder is aligned against surrounding text. + /// + public enum TextPlaceholderAlignment + { + /// + /// Align the placeholder baseline with the surrounding text baseline. + /// + Baseline, + + /// + /// Align the placeholder above the surrounding text baseline. + /// + AboveBaseline, + + /// + /// Align the placeholder below the surrounding text baseline. + /// + BelowBaseline, + + /// + /// Align the placeholder with the top of the surrounding line box. + /// + Top, + + /// + /// Align the placeholder with the bottom of the surrounding line box. + /// + Bottom, + + /// + /// Align the placeholder with the middle of the surrounding line box. + /// + Middle + } +} diff --git a/SixLabors.Fonts/TextRun.cs b/SixLabors.Fonts/TextRun.cs new file mode 100644 index 0000000..52111d5 --- /dev/null +++ b/SixLabors.Fonts/TextRun.cs @@ -0,0 +1,104 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts { + /// + /// Represents a run of text spanning a series of graphemes within a string. + /// + public class TextRun + { + /// + /// Gets or sets the inclusive start index of the first grapheme in this . + /// + public int Start { get; set; } + + /// + /// Gets or sets the exclusive end index of the last grapheme in this . + /// + public int End { get; set; } + + /// + /// Gets or sets the font for this run. + /// + public Font? Font { get; set; } + + /// + /// Gets or sets the text attributes applied to this run. + /// + public TextAttributes TextAttributes { get; set; } + + /// + /// Gets or sets the text decorations applied to this run. + /// + public TextDecorations TextDecorations { get; set; } + + /// + /// Gets or sets the inline placeholder represented by this run. + /// + /// + /// Placeholder runs are inserted at and must have equal to . + /// + public TextPlaceholder? Placeholder { get; set; } + + /// + /// Returns the slice of the given text representing this . + /// + /// The text to slice. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan Slice(ReadOnlySpan text) + { + ValidateRange(this.Start, this.End); + + // Convert grapheme indices into char indices so we can slice + int chars = 0; + int count = 0; + int start = 0; + int length = 0; + SpanGraphemeEnumerator graphemeEnumerator = new(text); + while (graphemeEnumerator.MoveNext()) + { + if (count == this.Start) + { + start = chars; + } + + SpanCodePointEnumerator codePointEnumerator = new(graphemeEnumerator.Current.Span); + while (codePointEnumerator.MoveNext()) + { + chars += codePointEnumerator.Current.Utf16SequenceLength; + length = chars - start; + } + + if (++count == this.End) + { + break; + } + } + + return text.Slice(start, length); + } + + /// + public override string ToString() + => $"[TextRun: Start={this.Start}, End={this.End}, TextAttributes={this.TextAttributes}]"; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ValidateRange(int start, int end) + { + if (start < 0 || end < 0) + { + throw new ArgumentOutOfRangeException($"Start '{start}' and End '{end}' must be greater or equal to zero."); + } + + if (end <= start) + { + throw new ArgumentOutOfRangeException($"End '{end}' must be greater than Start '{start}'."); + } + } + } +} diff --git a/SixLabors.Fonts/Unicode/ArabicJoiningClass.cs b/SixLabors.Fonts/Unicode/ArabicJoiningClass.cs new file mode 100644 index 0000000..76e2f7f --- /dev/null +++ b/SixLabors.Fonts/Unicode/ArabicJoiningClass.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts.Unicode { + /// + /// Represents the Unicode joining properties of a given . + /// + /// + /// + /// + /// This combines the Unicode Joining_Type and Joining_Group + /// properties used by cursive shaping. Unlisted nonspacing marks, enclosing marks, + /// and format controls follow the Unicode default joining behavior. + /// + public readonly struct ArabicJoiningClass + { + /// + /// Initializes a new instance of the struct. + /// + /// The codepoint. + public ArabicJoiningClass(CodePoint codePoint) + { + UnicodeCategory category = CodePoint.GetGeneralCategory(codePoint); + uint value = UnicodeData.GetJoiningClass((uint)codePoint.Value); + this.JoiningType = GetJoiningType(codePoint, value, category); + this.JoiningGroup = (ArabicJoiningGroup)((value >> 16) & 0xFF); + } + + /// + /// Gets the Unicode joining type. + /// + public ArabicJoiningType JoiningType { get; } + + /// + /// Gets the Unicode joining group. + /// + public ArabicJoiningGroup JoiningGroup { get; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ArabicJoiningType GetJoiningType(CodePoint codePoint, uint value, UnicodeCategory category) + { + var type = (ArabicJoiningType)(value & 0xFF); + + // All others not explicitly listed have joining type U + if (type == ArabicJoiningType.NonJoining) + { + // 200C; ZERO WIDTH NON-JOINER; U; No_Joining_Group + // 200D; ZERO WIDTH JOINER; C; No_Joining_Group + // 202F; NARROW NO-BREAK SPACE; U; No_Joining_Group + // 2066; LEFT-TO-RIGHT ISOLATE; U; No_Joining_Group + // 2067; RIGHT-TO-LEFT ISOLATE; U; No_Joining_Group + // 2068; FIRST STRONG ISOLATE; U; No_Joining_Group + // 2069; POP DIRECTIONAL ISOLATE; U; No_Joining_Group + if (codePoint.Value is 0x200C + or 0x200D + or 0x202F + or 0x2066 + or 0x2067 + or 0x2068 + or 0x2069) + { + return type; + } + + // Those that are not explicitly listed and that are of General Category Mn, Me, or Cf have joining type T. + if (category is UnicodeCategory.NonSpacingMark or UnicodeCategory.EnclosingMark or UnicodeCategory.Format) + { + type = ArabicJoiningType.Transparent; + } + } + + return type; + } + } +} diff --git a/SixLabors.Fonts/Unicode/ArabicJoiningGroup.cs b/SixLabors.Fonts/Unicode/ArabicJoiningGroup.cs new file mode 100644 index 0000000..adc1d0b --- /dev/null +++ b/SixLabors.Fonts/Unicode/ArabicJoiningGroup.cs @@ -0,0 +1,545 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode { + /// + /// Unicode Joining_Group property values used for Arabic-script cursive shaping. + /// + /// + /// + /// Joining groups identify characters that share the same basic joining shape for + /// cursive joining. + /// + public enum ArabicJoiningGroup + { + /// + /// African_Feh + /// + AfricanFeh, + + /// + /// African_Noon + /// + AfricanNoon, + + /// + /// African_Qaf + /// + AfricanQaf, + + /// + /// Ain + /// + Ain, + + /// + /// Alaph + /// + Alaph, + + /// + /// Alef + /// + Alef, + + /// + /// Beh + /// + Beh, + + /// + /// Beth + /// + Beth, + + /// + /// Burushaski_Yeh_Barree + /// + BurushaskiYehBarree, + + /// + /// Dal + /// + Dal, + + /// + /// Dalath_Rish + /// + DalathRish, + + /// + /// E + /// + E, + + /// + /// Farsi_Yeh + /// + FarsiYeh, + + /// + /// Fe + /// + Fe, + + /// + /// Feh + /// + Feh, + + /// + /// Final_Semkath + /// + FinalSemkath, + + /// + /// Gaf + /// + Gaf, + + /// + /// Gamal + /// + Gamal, + + /// + /// Hah + /// + Hah, + + /// + /// Hanifi_Rohingya_Kinna_Ya + /// + HanifiRohingyaKinnaYa, + + /// + /// Hanifi_Rohingya_Pa + /// + HanifiRohingyaPa, + + /// + /// He + /// + He, + + /// + /// Heh + /// + Heh, + + /// + /// Heh_Goal + /// + HehGoal, + + /// + /// Heth + /// + Heth, + + /// + /// Kaf + /// + Kaf, + + /// + /// Kaph + /// + Kaph, + + /// + /// Kashmiri_Yeh + /// + KashmiriYeh, + + /// + /// Khaph + /// + Khaph, + + /// + /// Knotted_Heh + /// + KnottedHeh, + + /// + /// Lam + /// + Lam, + + /// + /// Lamadh + /// + Lamadh, + + /// + /// Malayalam_Bha + /// + MalayalamBha, + + /// + /// Malayalam_Ja + /// + MalayalamJa, + + /// + /// Malayalam_Lla + /// + MalayalamLla, + + /// + /// Malayalam_Llla + /// + MalayalamLlla, + + /// + /// Malayalam_Nga + /// + MalayalamNga, + + /// + /// Malayalam_Nna + /// + MalayalamNna, + + /// + /// Malayalam_Nnna + /// + MalayalamNnna, + + /// + /// Malayalam_Nya + /// + MalayalamNya, + + /// + /// Malayalam_Ra + /// + MalayalamRa, + + /// + /// Malayalam_Ssa + /// + MalayalamSsa, + + /// + /// Malayalam_Tta + /// + MalayalamTta, + + /// + /// Manichaean_Aleph + /// + ManichaeanAleph, + + /// + /// Manichaean_Ayin + /// + ManichaeanAyin, + + /// + /// Manichaean_Beth + /// + ManichaeanBeth, + + /// + /// Manichaean_Daleth + /// + ManichaeanDaleth, + + /// + /// Manichaean_Dhamedh + /// + ManichaeanDhamedh, + + /// + /// Manichaean_Five + /// + ManichaeanFive, + + /// + /// Manichaean_Gimel + /// + ManichaeanGimel, + + /// + /// Manichaean_Heth + /// + ManichaeanHeth, + + /// + /// Manichaean_Hundred + /// + ManichaeanHundred, + + /// + /// Manichaean_Kaph + /// + ManichaeanKaph, + + /// + /// Manichaean_Lamedh + /// + ManichaeanLamedh, + + /// + /// Manichaean_Mem + /// + ManichaeanMem, + + /// + /// Manichaean_Nun + /// + ManichaeanNun, + + /// + /// Manichaean_One + /// + ManichaeanOne, + + /// + /// Manichaean_Pe + /// + ManichaeanPe, + + /// + /// Manichaean_Qoph + /// + ManichaeanQoph, + + /// + /// Manichaean_Resh + /// + ManichaeanResh, + + /// + /// Manichaean_Sadhe + /// + ManichaeanSadhe, + + /// + /// Manichaean_Samekh + /// + ManichaeanSamekh, + + /// + /// Manichaean_Taw + /// + ManichaeanTaw, + + /// + /// Manichaean_Ten + /// + ManichaeanTen, + + /// + /// Manichaean_Teth + /// + ManichaeanTeth, + + /// + /// Manichaean_Thamedh + /// + ManichaeanThamedh, + + /// + /// Manichaean_Twenty + /// + ManichaeanTwenty, + + /// + /// Manichaean_Waw + /// + ManichaeanWaw, + + /// + /// Manichaean_Yodh + /// + ManichaeanYodh, + + /// + /// Manichaean_Zayin + /// + ManichaeanZayin, + + /// + /// Meem + /// + Meem, + + /// + /// Mim + /// + Mim, + + /// + /// No_Joining_Group + /// + NoJoiningGroup, + + /// + /// Noon + /// + Noon, + + /// + /// Nun + /// + Nun, + + /// + /// Nya + /// + Nya, + + /// + /// Pe + /// + Pe, + + /// + /// Qaf + /// + Qaf, + + /// + /// Qaph + /// + Qaph, + + /// + /// Reh + /// + Reh, + + /// + /// Reversed_Pe + /// + ReversedPe, + + /// + /// Rohingya_Yeh + /// + RohingyaYeh, + + /// + /// Sad + /// + Sad, + + /// + /// Sadhe + /// + Sadhe, + + /// + /// Seen + /// + Seen, + + /// + /// Semkath + /// + Semkath, + + /// + /// Shin + /// + Shin, + + /// + /// Straight_Waw + /// + StraightWaw, + + /// + /// Swash_Kaf + /// + SwashKaf, + + /// + /// Syriac_Waw + /// + SyriacWaw, + + /// + /// Tah + /// + Tah, + + /// + /// Taw + /// + Taw, + + /// + /// Teh_Marbuta + /// + TehMarbuta, + + /// + /// Teh_Marbuta_Goal, formerly known by the stable alias Hamza_On_Heh_Goal. + /// + TehMarbutaGoal, + + /// + /// Teth + /// + Teth, + + /// + /// Thin_Noon + /// + ThinNoon, + + /// + /// Thin_Yeh + /// + ThinYeh, + + /// + /// Vertical_Tail + /// + VerticalTail, + + /// + /// Waw + /// + Waw, + + /// + /// Yeh + /// + Yeh, + + /// + /// Yeh_Barree + /// + YehBarree, + + /// + /// Yeh_With_Tail + /// + YehWithTail, + + /// + /// Yudh + /// + Yudh, + + /// + /// Yudh_He + /// + YudhHe, + + /// + /// Zain + /// + Zain, + + /// + /// Zhain + /// + Zhain + } +} diff --git a/SixLabors.Fonts/Unicode/ArabicJoiningType.cs b/SixLabors.Fonts/Unicode/ArabicJoiningType.cs new file mode 100644 index 0000000..0ee3677 --- /dev/null +++ b/SixLabors.Fonts/Unicode/ArabicJoiningType.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode { + /// + /// Unicode Joining_Type property values used for Arabic-script cursive shaping. + /// + /// + /// + /// These values describe how a character participates in cursive joining with + /// neighboring characters. + /// + public enum ArabicJoiningType + { + /// + /// Right_Joining (R): joins on the right side only. + /// + RightJoining, + + /// + /// Left_Joining (L): joins on the left side only. + /// + LeftJoining, + + /// + /// Dual_Joining (D): joins on both sides. + /// + DualJoining, + + /// + /// Join_Causing (C): causes adjacent join-capable characters to join. + /// + JoinCausing, + + /// + /// Non_Joining (U): does not participate in cursive joining. + /// + NonJoining, + + /// + /// Transparent (T): ignored when determining the joining relationship of surrounding characters. + /// + Transparent + } +} diff --git a/SixLabors.Fonts/Unicode/BidiAlgorithm.cs b/SixLabors.Fonts/Unicode/BidiAlgorithm.cs new file mode 100644 index 0000000..a4b363d --- /dev/null +++ b/SixLabors.Fonts/Unicode/BidiAlgorithm.cs @@ -0,0 +1,1599 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace SixLabors.Fonts.Unicode { + /// + /// Implementation of Unicode Bidirection Algorithm (UAX #9) + /// https://unicode.org/reports/tr9/ + /// + /// + /// + /// The Bidi algorithm uses a number of memory arrays for resolved + /// types, level information, bracket types, x9 removal maps and + /// more... + /// + /// + /// This implementation of the Bidi algorithm has been designed + /// to reduce memory pressure on the GC by re-using the same + /// work buffers, so instances of this class should be re-used + /// as much as possible. + /// + /// + internal sealed class BidiAlgorithm + { + /// + /// The original BidiCharacterType types as provided by the caller + /// + private ReadOnlyArraySlice originalTypes; + + /// + /// Paired bracket types as provided by caller + /// + private ReadOnlyArraySlice pairedBracketTypes; + + /// + /// Paired bracket values as provided by caller + /// + private ReadOnlyArraySlice pairedBracketValues; + + /// + /// Try if the incoming data is known to contain brackets + /// + private bool hasBrackets; + + /// + /// True if the incoming data is known to contain embedding runs + /// + private bool hasEmbeddings; + + /// + /// True if the incoming data is known to contain isolating runs + /// + private bool hasIsolates; + + /// + /// Two directional mapping of isolate start/end pairs + /// + /// + /// The forward mapping maps the start index to the end index. + /// The reverse mapping maps the end index to the start index. + /// + private readonly BidiDictionary isolatePairs = new(); + + /// + /// The working BidiCharacterType types + /// + private ArraySlice workingTypes; + + /// + /// The buffer underlying _workingTypes + /// + private ArrayBuilder workingTypesBuffer; + + /// + /// The buffer underlying resolvedLevels + /// + private ArrayBuilder resolvedLevelsBuffer; + + /// + /// The resolve paragraph embedding level + /// + private sbyte paragraphEmbeddingLevel; + + /// + /// The status stack used during resolution of explicit + /// embedding and isolating runs + /// + private readonly Stack statusStack = new(); + + /// + /// Mapping used to virtually remove characters for rule X9 + /// + private ArrayBuilder x9Map; + + /// + /// Re-usable list of level runs + /// + private readonly List levelRuns = new(); + + /// + /// Mapping for the current isolating sequence, built + /// by joining level runs from the x9 map. + /// + private ArrayBuilder isolatedRunMapping; + + /// + /// A stack of pending isolate openings used by FindIsolatePairs() + /// + private readonly Stack pendingIsolateOpenings = new(); + + /// + /// The level of the isolating run currently being processed + /// + private int runLevel; + + /// + /// The direction of the isolating run currently being processed + /// + private BidiCharacterType runDirection; + + /// + /// The length of the isolating run currently being processed + /// + private int runLength; + + /// + /// A mapped slice of the resolved types for the isolating run currently + /// being processed + /// + private MappedArraySlice runResolvedTypes; + + /// + /// A mapped slice of the original types for the isolating run currently + /// being processed + /// + private ReadonlyMappedArraySlice runOriginalTypes; + + /// + /// A mapped slice of the run levels for the isolating run currently + /// being processed + /// + private MappedArraySlice runLevels; + + /// + /// A mapped slice of the paired bracket types of the isolating + /// run currently being processed + /// + private ReadonlyMappedArraySlice runBidiPairedBracketTypes; + + /// + /// A mapped slice of the paired bracket values of the isolating + /// run currently being processed + /// + private ReadonlyMappedArraySlice runPairedBracketValues; + + /// + /// Maximum pairing depth for paired brackets + /// + private const int MaxPairedBracketDepth = 63; + + /// + /// Reusable list of pending opening brackets used by the + /// LocatePairedBrackets method + /// + private readonly List pendingOpeningBrackets = new(); + + /// + /// Resolved list of paired brackets + /// + private readonly List pairedBrackets = new(); + + /// + /// Initializes a new instance of the class. + /// + public BidiAlgorithm() + { + } + + /// + /// Gets a per-thread instance that can be re-used as often + /// as necessary. + /// + public static ThreadLocal Instance { get; } = new ThreadLocal(() => new BidiAlgorithm()); + + /// + /// Gets the resolved levels. + /// + public ArraySlice ResolvedLevels { get; private set; } + + /// + /// Gets the resolved paragraph embedding level + /// + public int ResolvedParagraphEmbeddingLevel => this.paragraphEmbeddingLevel; + + /// + /// Process data from a BidiData instance + /// + /// The Bidi Unicode data. + public void Process(BidiData data) + => this.Process( + data.Types, + data.PairedBracketTypes, + data.PairedBracketValues, + data.ParagraphEmbeddingLevel, + data.HasBrackets, + data.HasEmbeddings, + data.HasIsolates, + null); + + /// + /// Processes Bidi Data + /// + public void Process( + ReadOnlyArraySlice types, + ReadOnlyArraySlice pairedBracketTypes, + ReadOnlyArraySlice pairedBracketValues, + sbyte paragraphEmbeddingLevel, + bool? hasBrackets, + bool? hasEmbeddings, + bool? hasIsolates, + ArraySlice? outLevels) + { + // Reset state + this.isolatePairs.Clear(); + this.workingTypesBuffer.Clear(); + this.levelRuns.Clear(); + this.resolvedLevelsBuffer.Clear(); + + // Setup original types and working types + this.originalTypes = types; + this.workingTypes = this.workingTypesBuffer.Add(types); + + // Capture paired bracket values and types + this.pairedBracketTypes = pairedBracketTypes; + this.pairedBracketValues = pairedBracketValues; + + // Store things we know + this.hasBrackets = hasBrackets ?? this.pairedBracketTypes.Length == this.originalTypes.Length; + this.hasEmbeddings = hasEmbeddings ?? true; + this.hasIsolates = hasIsolates ?? true; + + // Find all isolate pairs + this.FindIsolatePairs(); + + // Resolve the paragraph embedding level + if (paragraphEmbeddingLevel == 2) + { + this.paragraphEmbeddingLevel = this.ResolveEmbeddingLevel(this.originalTypes); + } + else + { + this.paragraphEmbeddingLevel = paragraphEmbeddingLevel; + } + + // Create resolved levels buffer + if (outLevels.HasValue) + { + if (outLevels.Value.Length != this.originalTypes.Length) + { + throw new ArgumentException("Out levels must be the same length as the input data"); + } + + this.ResolvedLevels = outLevels.Value; + } + else + { + this.ResolvedLevels = this.resolvedLevelsBuffer.Add(this.originalTypes.Length); + this.ResolvedLevels.Fill(this.paragraphEmbeddingLevel); + } + + // Resolve explicit embedding levels (Rules X1-X8) + this.ResolveExplicitEmbeddingLevels(); + + // Build the rule X9 map + this.BuildX9RemovalMap(); + + // Process all isolated run sequences + this.ProcessIsolatedRunSequences(); + + // Reset whitespace levels + this.ResetWhitespaceLevels(); + + // Clean up + this.AssignLevelsToCodePointsRemovedByX9(); + } + + /// + /// Resolve the paragraph embedding level if not explicitly passed + /// by the caller. Also used by rule X5c for FSI isolating sequences. + /// + /// The data to be evaluated + /// The resolved embedding level + public sbyte ResolveEmbeddingLevel(ReadOnlyArraySlice data) + { + // P2 + for (int i = 0; i < data.Length; ++i) + { + switch (data[i]) + { + case BidiCharacterType.LeftToRight: + // P3 + return 0; + + case BidiCharacterType.ArabicLetter: + case BidiCharacterType.RightToLeft: + // P3 + return 1; + + case BidiCharacterType.FirstStrongIsolate: + case BidiCharacterType.LeftToRightIsolate: + case BidiCharacterType.RightToLeftIsolate: + // Skip isolate pairs + // (Because we're working with a slice, we need to adjust the indices + // we're using for the isolatePairs map) + if (this.isolatePairs.TryGetValue(data.Start + i, out i)) + { + i -= data.Start; + } + else + { + i = data.Length; + } + + break; + } + } + + // P3 + return 0; + } + + /// + /// Build a list of matching isolates for a directionality slice + /// Implements BD9 + /// + private void FindIsolatePairs() + { + // Redundant? + if (!this.hasIsolates) + { + return; + } + + // Lets double check this as we go and clear the flag + // if there actually aren't any isolate pairs as this might + // mean we can skip some later steps + this.hasIsolates = false; + + // BD9... + this.pendingIsolateOpenings.Clear(); + for (int i = 0; i < this.originalTypes.Length; i++) + { + BidiCharacterType t = this.originalTypes[i]; + if (t is BidiCharacterType.LeftToRightIsolate + or BidiCharacterType.RightToLeftIsolate + or BidiCharacterType.FirstStrongIsolate) + { + this.pendingIsolateOpenings.Push(i); + this.hasIsolates = true; + } + else if (t == BidiCharacterType.PopDirectionalIsolate) + { + if (this.pendingIsolateOpenings.Count > 0) + { + this.isolatePairs.Add(this.pendingIsolateOpenings.Pop(), i); + } + + this.hasIsolates = true; + } + } + } + + /// + /// Resolve the explicit embedding levels from the original + /// data. Implements rules X1 to X8. + /// + private void ResolveExplicitEmbeddingLevels() + { + // Redundant? + if (!this.hasIsolates && !this.hasEmbeddings) + { + return; + } + + // Work variables + this.statusStack.Clear(); + int overflowIsolateCount = 0; + int overflowEmbeddingCount = 0; + int validIsolateCount = 0; + + // Constants + const int maxStackDepth = 125; + + // Rule X1 - setup initial state + this.statusStack.Clear(); + + // Neutral + this.statusStack.Push(new Status(this.paragraphEmbeddingLevel, BidiCharacterType.OtherNeutral, false)); + + // Process all characters + for (int i = 0; i < this.originalTypes.Length; i++) + { + switch (this.originalTypes[i]) + { + case BidiCharacterType.RightToLeftEmbedding: + { + // Rule X2 + sbyte newLevel = (sbyte)((this.statusStack.Peek().EmbeddingLevel + 1) | 1); + if (newLevel <= maxStackDepth && overflowIsolateCount == 0 && overflowEmbeddingCount == 0) + { + this.statusStack.Push(new Status(newLevel, BidiCharacterType.OtherNeutral, false)); + this.ResolvedLevels[i] = newLevel; + } + else if (overflowIsolateCount == 0) + { + overflowEmbeddingCount++; + } + + break; + } + + case BidiCharacterType.LeftToRightEmbedding: + { + // Rule X3 + sbyte newLevel = (sbyte)((this.statusStack.Peek().EmbeddingLevel + 2) & ~1); + if (newLevel < maxStackDepth && overflowIsolateCount == 0 && overflowEmbeddingCount == 0) + { + this.statusStack.Push(new Status(newLevel, BidiCharacterType.OtherNeutral, false)); + this.ResolvedLevels[i] = newLevel; + } + else if (overflowIsolateCount == 0) + { + overflowEmbeddingCount++; + } + + break; + } + + case BidiCharacterType.RightToLeftOverride: + { + // Rule X4 + sbyte newLevel = (sbyte)((this.statusStack.Peek().EmbeddingLevel + 1) | 1); + if (newLevel <= maxStackDepth && overflowIsolateCount == 0 && overflowEmbeddingCount == 0) + { + this.statusStack.Push(new Status(newLevel, BidiCharacterType.RightToLeft, false)); + this.ResolvedLevels[i] = newLevel; + } + else if (overflowIsolateCount == 0) + { + overflowEmbeddingCount++; + } + + break; + } + + case BidiCharacterType.LeftToRightOverride: + { + // Rule X5 + sbyte newLevel = (sbyte)((this.statusStack.Peek().EmbeddingLevel + 2) & ~1); + if (newLevel <= maxStackDepth && overflowIsolateCount == 0 && overflowEmbeddingCount == 0) + { + this.statusStack.Push(new Status(newLevel, BidiCharacterType.LeftToRight, false)); + this.ResolvedLevels[i] = newLevel; + } + else if (overflowIsolateCount == 0) + { + overflowEmbeddingCount++; + } + + break; + } + + case BidiCharacterType.RightToLeftIsolate: + case BidiCharacterType.LeftToRightIsolate: + case BidiCharacterType.FirstStrongIsolate: + { + // Rule X5a, X5b and X5c + BidiCharacterType resolvedIsolate = this.originalTypes[i]; + + if (resolvedIsolate == BidiCharacterType.FirstStrongIsolate) + { + if (!this.isolatePairs.TryGetValue(i, out int endOfIsolate)) + { + endOfIsolate = this.originalTypes.Length; + } + + // Rule X5c + if (this.ResolveEmbeddingLevel(this.originalTypes.Slice(i + 1, endOfIsolate - (i + 1))) == 1) + { + resolvedIsolate = BidiCharacterType.RightToLeftIsolate; + } + else + { + resolvedIsolate = BidiCharacterType.LeftToRightIsolate; + } + } + + // Replace RLI's level with current embedding level + Status tos = this.statusStack.Peek(); + this.ResolvedLevels[i] = tos.EmbeddingLevel; + + // Apply override + if (tos.OverrideStatus != BidiCharacterType.OtherNeutral) + { + this.workingTypes[i] = tos.OverrideStatus; + } + + // Work out new level + sbyte newLevel; + if (resolvedIsolate == BidiCharacterType.RightToLeftIsolate) + { + newLevel = (sbyte)((tos.EmbeddingLevel + 1) | 1); + } + else + { + newLevel = (sbyte)((tos.EmbeddingLevel + 2) & ~1); + } + + // Valid? + if (newLevel <= maxStackDepth && overflowIsolateCount == 0 && overflowEmbeddingCount == 0) + { + validIsolateCount++; + this.statusStack.Push(new Status(newLevel, BidiCharacterType.OtherNeutral, true)); + } + else + { + overflowIsolateCount++; + } + + break; + } + + case BidiCharacterType.BoundaryNeutral: + { + // Mentioned in rule X6 - "for all types besides ..., BN, ..." + // no-op + break; + } + + default: + { + // Rule X6 + Status tos = this.statusStack.Peek(); + this.ResolvedLevels[i] = tos.EmbeddingLevel; + if (tos.OverrideStatus != BidiCharacterType.OtherNeutral) + { + this.workingTypes[i] = tos.OverrideStatus; + } + + break; + } + + case BidiCharacterType.PopDirectionalIsolate: + { + // Rule X6a + if (overflowIsolateCount > 0) + { + overflowIsolateCount--; + } + else if (validIsolateCount != 0) + { + overflowEmbeddingCount = 0; + while (!this.statusStack.Peek().IsolateStatus) + { + this.statusStack.Pop(); + } + + this.statusStack.Pop(); + validIsolateCount--; + } + + Status tos = this.statusStack.Peek(); + this.ResolvedLevels[i] = tos.EmbeddingLevel; + if (tos.OverrideStatus != BidiCharacterType.OtherNeutral) + { + this.workingTypes[i] = tos.OverrideStatus; + } + + break; + } + + case BidiCharacterType.PopDirectionalFormat: + { + // Rule X7 + if (overflowIsolateCount == 0) + { + if (overflowEmbeddingCount > 0) + { + overflowEmbeddingCount--; + } + else if (!this.statusStack.Peek().IsolateStatus && this.statusStack.Count >= 2) + { + this.statusStack.Pop(); + } + } + + break; + } + + case BidiCharacterType.ParagraphSeparator: + { + // Rule X8 + this.ResolvedLevels[i] = this.paragraphEmbeddingLevel; + break; + } + } + } + } + + /// + /// Build a map to the original data positions that excludes all + /// the types defined by rule X9 + /// + private void BuildX9RemovalMap() + { + // Reserve room for the x9 map + this.x9Map.Length = this.originalTypes.Length; + + if (this.hasEmbeddings || this.hasIsolates) + { + // Build a map the removes all x9 characters + int j = 0; + for (int i = 0; i < this.originalTypes.Length; i++) + { + if (!IsRemovedByX9(this.originalTypes[i])) + { + this.x9Map[j++] = i; + } + } + + // Set the final length + this.x9Map.Length = j; + } + else + { + for (int i = 0, count = this.originalTypes.Length; i < count; i++) + { + this.x9Map[i] = i; + } + } + } + + /// + /// Find the original character index for an entry in the X9 map + /// + /// Index in the x9 removal map + /// Index to the original data + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int MapX9(int index) => this.x9Map[index]; + + /// + /// Add a new level run + /// + /// + /// This method resolves the sos and eos values for the run + /// and adds the run to the list + /// /// + /// The index of the start of the run (in x9 removed units) + /// The length of the run (in x9 removed units) + /// The level of the run + private void AddLevelRun(int start, int length, int level) + { + // Get original indices to first and last character in this run + int firstCharIndex = this.MapX9(start); + int lastCharIndex = this.MapX9(start + length - 1); + + // Work out sos + int i = firstCharIndex - 1; + while (i >= 0 && IsRemovedByX9(this.originalTypes[i])) + { + i--; + } + + sbyte prevLevel = i < 0 ? this.paragraphEmbeddingLevel : this.ResolvedLevels[i]; + BidiCharacterType sos = DirectionFromLevel(Math.Max(prevLevel, level)); + + // Work out eos + BidiCharacterType lastType = this.workingTypes[lastCharIndex]; + int nextLevel; + if (lastType is BidiCharacterType.LeftToRightIsolate + or BidiCharacterType.RightToLeftIsolate + or BidiCharacterType.FirstStrongIsolate) + { + nextLevel = this.paragraphEmbeddingLevel; + } + else + { + i = lastCharIndex + 1; + while (i < this.originalTypes.Length && IsRemovedByX9(this.originalTypes[i])) + { + i++; + } + + nextLevel = i >= this.originalTypes.Length ? this.paragraphEmbeddingLevel : this.ResolvedLevels[i]; + } + + BidiCharacterType eos = DirectionFromLevel(Math.Max(nextLevel, level)); + + // Add the run + this.levelRuns.Add(new LevelRun(start, length, level, sos, eos)); + } + + /// + /// Find all runs of the same level, populating the _levelRuns + /// collection + /// + private void FindLevelRuns() + { + int currentLevel = -1; + int runStart = 0; + for (int i = 0; i < this.x9Map.Length; ++i) + { + int level = this.ResolvedLevels[this.MapX9(i)]; + if (level != currentLevel) + { + if (currentLevel != -1) + { + this.AddLevelRun(runStart, i - runStart, currentLevel); + } + + currentLevel = level; + runStart = i; + } + } + + // Don't forget the final level run + if (currentLevel != -1) + { + this.AddLevelRun(runStart, this.x9Map.Length - runStart, currentLevel); + } + } + + /// + /// Given a character index, find the level run that starts at that position + /// + /// The index into the original (unmapped) data + /// The index of the run that starts at that index + private int FindRunForIndex(int index) + { + for (int i = 0; i < this.levelRuns.Count; i++) + { + // Passed index is for the original non-x9 filtered data, however + // the level run ranges are for the x9 filtered data. Convert before + // comparing + if (this.MapX9(this.levelRuns[i].Start) == index) + { + return i; + } + } + + throw new InvalidOperationException("Internal error"); + } + + /// + /// Determine and the process all isolated run sequences + /// + private void ProcessIsolatedRunSequences() + { + // Find all runs with the same level + this.FindLevelRuns(); + + // Process them one at a time by first building + // a mapping using slices from the x9 map for each + // run section that needs to be joined together to + // form an complete run. That full run mapping + // will be placed in _isolatedRunMapping and then + // processed by ProcessIsolatedRunSequence(). + while (this.levelRuns.Count > 0) + { + // Clear the mapping + this.isolatedRunMapping.Clear(); + + // Combine mappings from this run and all runs that continue on from it + int runIndex = 0; + BidiCharacterType eos; + BidiCharacterType sos = this.levelRuns[0].Sos; + int level = this.levelRuns[0].Level; + while (true) + { + // Get the run + LevelRun r = this.levelRuns[runIndex]; + + // The eos of the isolating run is the eos of the + // last level run that comprises it. + eos = r.Eos; + + // Remove this run as we've now processed it + this.levelRuns.RemoveAt(runIndex); + + // Add the x9 map indices for the run range to the mapping + // for this isolated run + this.isolatedRunMapping.Add(this.x9Map.AsSlice(r.Start, r.Length)); + + // Get the last character and see if it's an isolating run with a matching + // PDI and concatenate that run to this one + int lastCharacterIndex = this.isolatedRunMapping[this.isolatedRunMapping.Length - 1]; + BidiCharacterType lastType = this.originalTypes[lastCharacterIndex]; + if ((lastType == BidiCharacterType.LeftToRightIsolate || lastType == BidiCharacterType.RightToLeftIsolate || lastType == BidiCharacterType.FirstStrongIsolate) && + this.isolatePairs.TryGetValue(lastCharacterIndex, out int nextRunIndex)) + { + // Find the continuing run index + runIndex = this.FindRunForIndex(nextRunIndex); + } + else + { + break; + } + } + + // Process this isolated run + this.ProcessIsolatedRunSequence(sos, eos, level); + } + } + + /// + /// Process a single isolated run sequence, where the character sequence + /// mapping is currently held in _isolatedRunMapping. + /// + private void ProcessIsolatedRunSequence(BidiCharacterType sos, BidiCharacterType eos, int runLevel) + { + // Create mappings onto the underlying data + this.runResolvedTypes = new MappedArraySlice(this.workingTypes, this.isolatedRunMapping.AsSlice()); + this.runOriginalTypes = new ReadonlyMappedArraySlice(this.originalTypes, this.isolatedRunMapping.AsSlice()); + this.runLevels = new MappedArraySlice(this.ResolvedLevels, this.isolatedRunMapping.AsSlice()); + if (this.hasBrackets) + { + this.runBidiPairedBracketTypes = new ReadonlyMappedArraySlice(this.pairedBracketTypes, this.isolatedRunMapping.AsSlice()); + this.runPairedBracketValues = new ReadonlyMappedArraySlice(this.pairedBracketValues, this.isolatedRunMapping.AsSlice()); + } + + this.runLevel = runLevel; + this.runDirection = DirectionFromLevel(runLevel); + this.runLength = this.runResolvedTypes.Length; + + // By tracking the types of characters known to be in the current run, we can + // skip some of the rules that we know won't apply. The flags will be + // initialized while we're processing rule W1 below. + bool hasEN = false; + bool hasAL = false; + bool hasES = false; + bool hasCS = false; + bool hasAN = false; + bool hasET = false; + + // Rule W1 + // Also, set hasXX flags + int i; + BidiCharacterType prevType = sos; + for (i = 0; i < this.runLength; i++) + { + BidiCharacterType t = this.runResolvedTypes[i]; + switch (t) + { + case BidiCharacterType.NonspacingMark: + this.runResolvedTypes[i] = prevType; + break; + + case BidiCharacterType.LeftToRightIsolate: + case BidiCharacterType.RightToLeftIsolate: + case BidiCharacterType.FirstStrongIsolate: + case BidiCharacterType.PopDirectionalIsolate: + prevType = BidiCharacterType.OtherNeutral; + break; + + case BidiCharacterType.EuropeanNumber: + hasEN = true; + prevType = t; + break; + + case BidiCharacterType.ArabicLetter: + hasAL = true; + prevType = t; + break; + + case BidiCharacterType.EuropeanSeparator: + hasES = true; + prevType = t; + break; + + case BidiCharacterType.CommonSeparator: + hasCS = true; + prevType = t; + break; + + case BidiCharacterType.ArabicNumber: + hasAN = true; + prevType = t; + break; + + case BidiCharacterType.EuropeanTerminator: + hasET = true; + prevType = t; + break; + + default: + prevType = t; + break; + } + } + + // Rule W2 + if (hasEN) + { + for (i = 0; i < this.runLength; i++) + { + if (this.runResolvedTypes[i] == BidiCharacterType.EuropeanNumber) + { + for (int j = i - 1; j >= 0; j--) + { + BidiCharacterType t = this.runResolvedTypes[j]; + if (t is BidiCharacterType.LeftToRight + or BidiCharacterType.RightToLeft + or BidiCharacterType.ArabicLetter) + { + if (t == BidiCharacterType.ArabicLetter) + { + this.runResolvedTypes[i] = BidiCharacterType.ArabicNumber; + hasAN = true; + } + + break; + } + } + } + } + } + + // Rule W3 + if (hasAL) + { + for (i = 0; i < this.runLength; i++) + { + if (this.runResolvedTypes[i] == BidiCharacterType.ArabicLetter) + { + this.runResolvedTypes[i] = BidiCharacterType.RightToLeft; + } + } + } + + // Rule W4 + if ((hasES || hasCS) && (hasEN || hasAN)) + { + for (i = 1; i < this.runLength - 1; ++i) + { + ref BidiCharacterType rt = ref this.runResolvedTypes[i]; + if (rt == BidiCharacterType.EuropeanSeparator) + { + BidiCharacterType prevSepType = this.runResolvedTypes[i - 1]; + BidiCharacterType succSepType = this.runResolvedTypes[i + 1]; + + if (prevSepType == BidiCharacterType.EuropeanNumber && succSepType == BidiCharacterType.EuropeanNumber) + { + // ES between EN and EN + rt = BidiCharacterType.EuropeanNumber; + } + } + else if (rt == BidiCharacterType.CommonSeparator) + { + BidiCharacterType prevSepType = this.runResolvedTypes[i - 1]; + BidiCharacterType succSepType = this.runResolvedTypes[i + 1]; + + if ((prevSepType == BidiCharacterType.ArabicNumber && succSepType == BidiCharacterType.ArabicNumber) || + (prevSepType == BidiCharacterType.EuropeanNumber && succSepType == BidiCharacterType.EuropeanNumber)) + { + // CS between (AN and AN) or (EN and EN) + rt = prevSepType; + } + } + } + } + + // Rule W5 + if (hasET && hasEN) + { + for (i = 0; i < this.runLength; ++i) + { + if (this.runResolvedTypes[i] == BidiCharacterType.EuropeanTerminator) + { + // Locate end of sequence + int seqStart = i; + int seqEnd = i; + while (seqEnd < this.runLength && this.runResolvedTypes[seqEnd] == BidiCharacterType.EuropeanTerminator) + { + seqEnd++; + } + + // Preceded by, or followed by EN? + if ((seqStart == 0 ? sos : this.runResolvedTypes[seqStart - 1]) == BidiCharacterType.EuropeanNumber + || (seqEnd == this.runLength ? eos : this.runResolvedTypes[seqEnd]) == BidiCharacterType.EuropeanNumber) + { + // Change the entire range + for (int j = seqStart; i < seqEnd; ++i) + { + this.runResolvedTypes[i] = BidiCharacterType.EuropeanNumber; + } + } + + // continue at end of sequence + i = seqEnd; + } + } + } + + // Rule W6 + if (hasES || hasET || hasCS) + { + for (i = 0; i < this.runLength; ++i) + { + ref BidiCharacterType t = ref this.runResolvedTypes[i]; + if (t is BidiCharacterType.EuropeanSeparator + or BidiCharacterType.EuropeanTerminator + or BidiCharacterType.CommonSeparator) + { + t = BidiCharacterType.OtherNeutral; + } + } + } + + // Rule W7. + if (hasEN) + { + BidiCharacterType prevStrongType = sos; + for (i = 0; i < this.runLength; ++i) + { + ref BidiCharacterType rt = ref this.runResolvedTypes[i]; + if (rt == BidiCharacterType.EuropeanNumber) + { + // If prev strong type was an L change this to L too + if (prevStrongType == BidiCharacterType.LeftToRight) + { + this.runResolvedTypes[i] = BidiCharacterType.LeftToRight; + } + } + + // Remember previous strong type (NB: AL should already be changed to R) + if (rt is BidiCharacterType.LeftToRight or BidiCharacterType.RightToLeft) + { + prevStrongType = rt; + } + } + } + + // Rule N0 - process bracket pairs + if (this.hasBrackets) + { + int count; + List? pairedBrackets = this.LocatePairedBrackets(); + for (i = 0, count = pairedBrackets.Count; i < count; i++) + { + BracketPair pb = pairedBrackets[i]; + BidiCharacterType dir = this.InspectPairedBracket(pb); + + // Case "d" - no strong types in the brackets, ignore + if (dir == BidiCharacterType.OtherNeutral) + { + continue; + } + + // Case "b" - strong type found that matches the embedding direction + if ((dir == BidiCharacterType.LeftToRight || dir == BidiCharacterType.RightToLeft) && dir == this.runDirection) + { + this.SetPairedBracketDirection(pb, dir); + continue; + } + + // Case "c" - found opposite strong type found, look before to establish context + dir = this.InspectBeforePairedBracket(pb, sos); + if (dir == this.runDirection || dir == BidiCharacterType.OtherNeutral) + { + dir = this.runDirection; + } + + this.SetPairedBracketDirection(pb, dir); + } + } + + // Rules N1 and N2 - resolve neutral types + for (i = 0; i < this.runLength; ++i) + { + BidiCharacterType t = this.runResolvedTypes[i]; + if (IsNeutralType(t)) + { + // Locate end of sequence + int seqStart = i; + int seqEnd = i; + while (seqEnd < this.runLength && IsNeutralType(this.runResolvedTypes[seqEnd])) + { + seqEnd++; + } + + // Work out the preceding type + BidiCharacterType typeBefore; + if (seqStart == 0) + { + typeBefore = sos; + } + else + { + typeBefore = this.runResolvedTypes[seqStart - 1]; + if (typeBefore is BidiCharacterType.ArabicNumber or BidiCharacterType.EuropeanNumber) + { + typeBefore = BidiCharacterType.RightToLeft; + } + } + + // Work out the following type + BidiCharacterType typeAfter; + if (seqEnd == this.runLength) + { + typeAfter = eos; + } + else + { + typeAfter = this.runResolvedTypes[seqEnd]; + if (typeAfter is BidiCharacterType.ArabicNumber or BidiCharacterType.EuropeanNumber) + { + typeAfter = BidiCharacterType.RightToLeft; + } + } + + // Work out the final resolved type + BidiCharacterType resolvedType; + if (typeBefore == typeAfter) + { + // Rule N1 + resolvedType = typeBefore; + } + else + { + // Rule N2 + resolvedType = this.runDirection; + } + + // Apply changes + for (int j = seqStart; j < seqEnd; j++) + { + this.runResolvedTypes[j] = resolvedType; + } + + // continue after this run + i = seqEnd; + } + } + + // Rules I1 and I2 - resolve implicit types + if ((this.runLevel & 0x01) == 0) + { + // Rule I1 - even + for (i = 0; i < this.runLength; i++) + { + BidiCharacterType t = this.runResolvedTypes[i]; + ref sbyte l = ref this.runLevels[i]; + if (t == BidiCharacterType.RightToLeft) + { + l++; + } + else if (t is BidiCharacterType.ArabicNumber or BidiCharacterType.EuropeanNumber) + { + l += 2; + } + } + } + else + { + // Rule I2 - odd + for (i = 0; i < this.runLength; i++) + { + BidiCharacterType t = this.runResolvedTypes[i]; + ref sbyte l = ref this.runLevels[i]; + if (t != BidiCharacterType.RightToLeft) + { + l++; + } + } + } + } + + /// + /// Locate all pair brackets in the current isolating run + /// + /// A sorted list of BracketPairs + private List LocatePairedBrackets() + { + // Clear work collections + this.pendingOpeningBrackets.Clear(); + this.pairedBrackets.Clear(); + + // Since List.Sort is expensive on memory if called often (it internally + // allocates an ArraySorted object) and since we will rarely have many + // items in this list (most paragraphs will only have a handful of bracket + // pairs - if that), we use a simple linear lookup and insert most of the + // time. If there are more that `sortLimit` paired brackets we abort th + // linear searching/inserting and using List.Sort at the end. + const int sortLimit = 8; + + // Process all characters in the run, looking for paired brackets + for (int ich = 0, length = this.runLength; ich < length; ich++) + { + // Ignore non-neutral characters + if (this.runResolvedTypes[ich] != BidiCharacterType.OtherNeutral) + { + continue; + } + + switch (this.runBidiPairedBracketTypes[ich]) + { + case BidiPairedBracketType.Open: + if (this.pendingOpeningBrackets.Count == MaxPairedBracketDepth) + { + goto exit; + } + + this.pendingOpeningBrackets.Insert(0, ich); + break; + + case BidiPairedBracketType.Close: + // see if there is a match + for (int i = 0; i < this.pendingOpeningBrackets.Count; i++) + { + if (this.runPairedBracketValues[ich] == this.runPairedBracketValues[this.pendingOpeningBrackets[i]]) + { + // Add this paired bracket set + int opener = this.pendingOpeningBrackets[i]; + if (this.pairedBrackets.Count < sortLimit) + { + int ppi = 0; + while (ppi < this.pairedBrackets.Count && this.pairedBrackets[ppi].OpeningIndex < opener) + { + ppi++; + } + + this.pairedBrackets.Insert(ppi, new BracketPair(opener, ich)); + } + else + { + this.pairedBrackets.Add(new BracketPair(opener, ich)); + } + + // remove up to and including matched opener + this.pendingOpeningBrackets.RemoveRange(0, i + 1); + break; + } + } + + break; + } + } + + exit: + + // Is a sort pending? + if (this.pairedBrackets.Count > sortLimit) + { + this.pairedBrackets.Sort(); + } + + return this.pairedBrackets; + } + + /// + /// Inspect a paired bracket set and determine its strong direction + /// + /// The paired bracket to be inspected + /// The direction of the bracket set content + private BidiCharacterType InspectPairedBracket(in BracketPair pb) + { + BidiCharacterType dirEmbed = DirectionFromLevel(this.runLevel); + BidiCharacterType dirOpposite = BidiCharacterType.OtherNeutral; + for (int ich = pb.OpeningIndex + 1; ich < pb.ClosingIndex; ich++) + { + BidiCharacterType dir = GetStrongTypeN0(this.runResolvedTypes[ich]); + if (dir == BidiCharacterType.OtherNeutral) + { + continue; + } + + if (dir == dirEmbed) + { + return dir; + } + + dirOpposite = dir; + } + + return dirOpposite; + } + + /// + /// Look for a strong type before a paired bracket + /// + /// The paired bracket set to be inspected + /// The sos in case nothing found before the bracket + /// The strong direction before the brackets + private BidiCharacterType InspectBeforePairedBracket(in BracketPair pb, BidiCharacterType sos) + { + for (int ich = pb.OpeningIndex - 1; ich >= 0; --ich) + { + BidiCharacterType dir = GetStrongTypeN0(this.runResolvedTypes[ich]); + if (dir != BidiCharacterType.OtherNeutral) + { + return dir; + } + } + + return sos; + } + + /// + /// Sets the direction of a bracket pair, including setting the direction of + /// NSM's inside the brackets and following. + /// + /// The paired brackets + /// The resolved direction for the bracket pair + private void SetPairedBracketDirection(in BracketPair pb, BidiCharacterType dir) + { + // Set the direction of the brackets + this.runResolvedTypes[pb.OpeningIndex] = dir; + this.runResolvedTypes[pb.ClosingIndex] = dir; + + // Set the directionality of NSM's inside the brackets + // BN characters (such as ZWJ or ZWSP) that appear between the base bracket character + // and the nonspacing mark should be ignored. + for (int i = pb.OpeningIndex + 1; i < pb.ClosingIndex; i++) + { + if (this.runOriginalTypes[i] == BidiCharacterType.NonspacingMark) + { + this.runResolvedTypes[i] = dir; + } + else if (this.runOriginalTypes[i] != BidiCharacterType.BoundaryNeutral) + { + break; + } + } + + // Set the directionality of NSM's following the brackets + for (int i = pb.ClosingIndex + 1; i < this.runLength; i++) + { + if (this.runOriginalTypes[i] == BidiCharacterType.NonspacingMark) + { + this.runResolvedTypes[i] = dir; + } + else if (this.runOriginalTypes[i] != BidiCharacterType.BoundaryNeutral) + { + break; + } + } + } + + /// + /// Resets whitespace levels. Implements rule L1 + /// + private void ResetWhitespaceLevels() + { + for (int i = 0; i < this.ResolvedLevels.Length; i++) + { + BidiCharacterType t = this.originalTypes[i]; + if (t is BidiCharacterType.ParagraphSeparator or BidiCharacterType.SegmentSeparator) + { + // Rule L1, clauses one and two. + this.ResolvedLevels[i] = this.paragraphEmbeddingLevel; + + // Rule L1, clause three. + for (int j = i - 1; j >= 0; --j) + { + if (IsWhitespace(this.originalTypes[j])) + { + // including format codes + this.ResolvedLevels[j] = this.paragraphEmbeddingLevel; + } + else + { + break; + } + } + } + } + + // Rule L1, clause four. + for (int j = this.ResolvedLevels.Length - 1; j >= 0; j--) + { + if (IsWhitespace(this.originalTypes[j])) + { // including format codes + this.ResolvedLevels[j] = this.paragraphEmbeddingLevel; + } + else + { + break; + } + } + } + + /// + /// Assign levels to any characters that would be have been + /// removed by rule X9. The idea is to keep level runs together + /// that would otherwise be broken by an interfering isolate/embedding + /// control character. + /// + private void AssignLevelsToCodePointsRemovedByX9() + { + // Redundant? + if (!this.hasIsolates && !this.hasEmbeddings) + { + return; + } + + // No-op? + if (this.workingTypes.Length == 0) + { + return; + } + + // Fix up first character + if (this.ResolvedLevels[0] < 0) + { + this.ResolvedLevels[0] = this.paragraphEmbeddingLevel; + } + + if (IsRemovedByX9(this.originalTypes[0])) + { + this.workingTypes[0] = this.originalTypes[0]; + } + + for (int i = 1, length = this.workingTypes.Length; i < length; i++) + { + BidiCharacterType t = this.originalTypes[i]; + if (IsRemovedByX9(t)) + { + this.workingTypes[i] = t; + this.ResolvedLevels[i] = this.ResolvedLevels[i - 1]; + } + } + } + + /// + /// Check if a directionality type represents whitespace + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsWhitespace(BidiCharacterType biditype) + => biditype switch + { + BidiCharacterType.LeftToRightEmbedding + or BidiCharacterType.RightToLeftEmbedding + or BidiCharacterType.LeftToRightOverride + or BidiCharacterType.RightToLeftOverride + or BidiCharacterType.PopDirectionalFormat + or BidiCharacterType.LeftToRightIsolate + or BidiCharacterType.RightToLeftIsolate + or BidiCharacterType.FirstStrongIsolate + or BidiCharacterType.PopDirectionalIsolate + or BidiCharacterType.BoundaryNeutral + or BidiCharacterType.Whitespace => true, + _ => false, + }; + + /// + /// Convert a level to a direction where odd is RTL and + /// even is LTR + /// + /// The level to convert + /// A directionality + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static BidiCharacterType DirectionFromLevel(int level) + => ((level & 0x1) == 0) ? BidiCharacterType.LeftToRight : BidiCharacterType.RightToLeft; + + /// + /// Helper to check if a directionality is removed by rule X9 + /// + /// The bidi type to check + /// True if rule X9 would remove this character; otherwise false + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsRemovedByX9(BidiCharacterType biditype) + => biditype switch + { + BidiCharacterType.LeftToRightEmbedding + or BidiCharacterType.RightToLeftEmbedding + or BidiCharacterType.LeftToRightOverride + or BidiCharacterType.RightToLeftOverride + or BidiCharacterType.PopDirectionalFormat + or BidiCharacterType.BoundaryNeutral => true, + _ => false, + }; + + /// + /// Check if a a directionality is neutral for rules N1 and N2 + /// + /// The direction. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsNeutralType(BidiCharacterType dir) + => dir switch + { + BidiCharacterType.ParagraphSeparator + or BidiCharacterType.SegmentSeparator + or BidiCharacterType.Whitespace + or BidiCharacterType.OtherNeutral + or BidiCharacterType.RightToLeftIsolate + or BidiCharacterType.LeftToRightIsolate + or BidiCharacterType.FirstStrongIsolate + or BidiCharacterType.PopDirectionalIsolate => true, + _ => false, + }; + + /// + /// Maps a direction to a strong type for rule N0 + /// + /// The direction to map + /// A strong direction - R, L or ON + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static BidiCharacterType GetStrongTypeN0(BidiCharacterType dir) + => dir switch + { + BidiCharacterType.EuropeanNumber + or BidiCharacterType.ArabicNumber + or BidiCharacterType.ArabicLetter + or BidiCharacterType.RightToLeft => BidiCharacterType.RightToLeft, + BidiCharacterType.LeftToRight => BidiCharacterType.LeftToRight, + _ => BidiCharacterType.OtherNeutral, + }; + + /// + /// Hold the start and end index of a pair of brackets + /// + private readonly struct BracketPair : IComparable + { + /// + /// Initializes a new instance of the struct. + /// + /// Index of the opening bracket + /// Index of the closing bracket + public BracketPair(int openingIndex, int closingIndex) + { + this.OpeningIndex = openingIndex; + this.ClosingIndex = closingIndex; + } + + /// + /// Gets the index of the opening bracket + /// + public int OpeningIndex { get; } + + /// + /// Gets the index of the closing bracket + /// + public int ClosingIndex { get; } + + public int CompareTo(BracketPair other) + => this.OpeningIndex.CompareTo(other.OpeningIndex); + } + + /// + /// Status stack entry used while resolving explicit + /// embedding levels + /// + private readonly struct Status + { + public Status(sbyte embeddingLevel, BidiCharacterType overrideStatus, bool isolateStatus) + { + this.EmbeddingLevel = embeddingLevel; + this.OverrideStatus = overrideStatus; + this.IsolateStatus = isolateStatus; + } + + public sbyte EmbeddingLevel { get; } + + public BidiCharacterType OverrideStatus { get; } + + public bool IsolateStatus { get; } + } + + /// + /// Provides information about a level run - a continuous + /// sequence of equal levels. + /// + private readonly struct LevelRun + { + public LevelRun(int start, int length, int level, BidiCharacterType sos, BidiCharacterType eos) + { + this.Start = start; + this.Length = length; + this.Level = level; + this.Sos = sos; + this.Eos = eos; + } + + public int Start { get; } + + public int Length { get; } + + public int Level { get; } + + public BidiCharacterType Sos { get; } + + public BidiCharacterType Eos { get; } + } + } +} diff --git a/SixLabors.Fonts/Unicode/BidiCharacterType.cs b/SixLabors.Fonts/Unicode/BidiCharacterType.cs new file mode 100644 index 0000000..edb9611 --- /dev/null +++ b/SixLabors.Fonts/Unicode/BidiCharacterType.cs @@ -0,0 +1,141 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode { + /// + /// Unicode Bidi_Class property values. + /// + /// + /// + /// These are the bidirectional character types used by the Unicode Bidirectional + /// Algorithm. The algorithm resolves these input classes into embedding levels and + /// final visual ordering; the enum represents the original character class. + /// + public enum BidiCharacterType + { + // Strong Types + + /// + /// Left-to-Right (L). + /// + LeftToRight = 0, + + /// + /// Right-to-Left (R). + /// + RightToLeft = 1, + + /// + /// Right-to-Left Arabic (AL). + /// + ArabicLetter = 2, + + // Weak Types + + /// + /// European Number (EN). + /// + EuropeanNumber = 3, + + /// + /// European Number Separator (ES). + /// + EuropeanSeparator = 4, + + /// + /// European Number Terminator (ET). + /// + EuropeanTerminator = 5, + + /// + /// Arabic Number (AN). + /// + ArabicNumber = 6, + + /// + /// Common Number Separator (CS). + /// + CommonSeparator = 7, + + /// + /// Nonspacing Mark (NSM). + /// + NonspacingMark = 8, + + /// + /// Boundary Neutral (BN). + /// + BoundaryNeutral = 9, + + // Neutral Types + + /// + /// Paragraph Separator (B). + /// + ParagraphSeparator = 10, + + /// + /// Segment Separator (S). + /// + SegmentSeparator = 11, + + /// + /// Whitespace (WS). + /// + Whitespace = 12, + + /// + /// Other Neutral (ON). + /// + OtherNeutral = 13, + + // Explicit Formatting Types - Embed + + /// + /// Left-to-Right Embedding (LRE). + /// + LeftToRightEmbedding = 14, + + /// + /// Left-to-Right Override (LRO). + /// + LeftToRightOverride = 15, + + /// + /// Right-to-Left Embedding (RLE). + /// + RightToLeftEmbedding = 16, + + /// + /// Right-to-Left Override (RLO). + /// + RightToLeftOverride = 17, + + /// + /// Pop Directional Format (PDF). + /// + PopDirectionalFormat = 18, + + // Explicit Formatting Types - Isolate + + /// + /// Left-to-Right Isolate (LRI). + /// + LeftToRightIsolate = 19, + + /// + /// Right-to-Left Isolate (RLI). + /// + RightToLeftIsolate = 20, + + /// + /// First Strong Isolate (FSI). + /// + FirstStrongIsolate = 21, + + /// + /// Pop Directional Isolate (PDI). + /// + PopDirectionalIsolate = 22, + } +} diff --git a/SixLabors.Fonts/Unicode/BidiClass.cs b/SixLabors.Fonts/Unicode/BidiClass.cs new file mode 100644 index 0000000..d79f465 --- /dev/null +++ b/SixLabors.Fonts/Unicode/BidiClass.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts.Unicode { + /// + /// Represents the Unicode Bidi value of a given . + /// + /// + public readonly struct BidiClass + { + private readonly uint bidiValue; + + /// + /// Initializes a new instance of the struct. + /// + /// The codepoint. + public BidiClass(CodePoint codePoint) + => this.bidiValue = UnicodeData.GetBidiData((uint)codePoint.Value); + + /// + /// Gets the Unicode Bidirectional character type. + /// + public BidiCharacterType CharacterType + => (BidiCharacterType)(this.bidiValue >> 24); + + /// + /// Gets the Unicode Bidirectional paired bracket type. + /// + public BidiPairedBracketType PairedBracketType + => (BidiPairedBracketType)((this.bidiValue >> 16) & 0xFF); + + /// + /// Gets the codepoint representing the bracket pairing for this instance. + /// + /// + /// When this method returns, contains the codepoint representing the bracket pairing for this instance; + /// otherwise, the default value for the type of the parameter. + /// This parameter is passed uninitialized. + /// . + /// if this instance has a bracket pairing; otherwise, + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetPairedBracket(out CodePoint codePoint) + { + if (this.PairedBracketType == BidiPairedBracketType.None) + { + codePoint = default; + return false; + } + + codePoint = new CodePoint(this.bidiValue & 0xFFFF); + return true; + } + } +} diff --git a/SixLabors.Fonts/Unicode/BidiData.cs b/SixLabors.Fonts/Unicode/BidiData.cs new file mode 100644 index 0000000..a995230 --- /dev/null +++ b/SixLabors.Fonts/Unicode/BidiData.cs @@ -0,0 +1,178 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; + +namespace SixLabors.Fonts.Unicode { + /// + /// Represents a unicode string and all associated attributes + /// for each character required for the Bidi algorithm + /// + internal class BidiData + { + private ArrayBuilder types; + private ArrayBuilder pairedBracketTypes; + private ArrayBuilder pairedBracketValues; + private ArrayBuilder savedTypes; + private ArrayBuilder savedPairedBracketTypes; + private ArrayBuilder tempLevelBuffer; + private readonly List paragraphPositions = new(); + + public sbyte ParagraphEmbeddingLevel { get; private set; } + + public bool HasBrackets { get; private set; } + + public bool HasEmbeddings { get; private set; } + + public bool HasIsolates { get; private set; } + + /// + /// Gets the length of the data held by the BidiData + /// + public int Length => this.types.Length; + + /// + /// Gets the bidi character type of each code point + /// + public ArraySlice Types { get; private set; } + + /// + /// Gets the paired bracket type for each code point + /// + public ArraySlice PairedBracketTypes { get; private set; } + + /// + /// Gets the paired bracket value for code point + /// + /// + /// The paired bracket values are the code points + /// of each character where the opening code point + /// is replaced with the closing code point for easier + /// matching. Also, bracket code points are mapped + /// to their canonical equivalents + /// + public ArraySlice PairedBracketValues { get; private set; } + + /// + /// Initialize with a text value. + /// + /// The text to process. + /// The paragraph embedding level + public void Init(ReadOnlySpan text, sbyte paragraphEmbeddingLevel) + { + // Set working buffer sizes + // TODO: This allocates more than it should for some arrays. + int length = CodePoint.GetCodePointCount(text); + this.types.Length = length; + this.pairedBracketTypes.Length = length; + this.pairedBracketValues.Length = length; + + this.paragraphPositions.Clear(); + this.ParagraphEmbeddingLevel = paragraphEmbeddingLevel; + + // Resolve the BidiCharacterType, paired bracket type and paired + // bracket values for all code points + this.HasBrackets = false; + this.HasEmbeddings = false; + this.HasIsolates = false; + + int i = 0; + var codePointEnumerator = new SpanCodePointEnumerator(text); + while (codePointEnumerator.MoveNext()) + { + CodePoint codePoint = codePointEnumerator.Current; + BidiClass bidi = CodePoint.GetBidiClass(codePoint); + + // Look up BidiCharacterType + BidiCharacterType dir = bidi.CharacterType; + this.types[i] = dir; + + switch (dir) + { + case BidiCharacterType.LeftToRightEmbedding: + case BidiCharacterType.LeftToRightOverride: + case BidiCharacterType.RightToLeftEmbedding: + case BidiCharacterType.RightToLeftOverride: + case BidiCharacterType.PopDirectionalFormat: + this.HasEmbeddings = true; + break; + + case BidiCharacterType.LeftToRightIsolate: + case BidiCharacterType.RightToLeftIsolate: + case BidiCharacterType.FirstStrongIsolate: + case BidiCharacterType.PopDirectionalIsolate: + this.HasIsolates = true; + break; + } + + // Lookup paired bracket types + BidiPairedBracketType pbt = bidi.PairedBracketType; + this.pairedBracketTypes[i] = pbt; + + if (pbt == BidiPairedBracketType.Open) + { + // Opening bracket types can never have a null pairing. + bidi.TryGetPairedBracket(out CodePoint paired); + this.pairedBracketValues[i] = CodePoint.GetCanonicalType(paired).Value; + + this.HasBrackets = true; + } + else if (pbt == BidiPairedBracketType.Close) + { + this.pairedBracketValues[i] = CodePoint.GetCanonicalType(codePoint).Value; + this.HasBrackets = true; + } + + i++; + } + + // Create slices on work buffers + this.Types = this.types.AsSlice(); + this.PairedBracketTypes = this.pairedBracketTypes.AsSlice(); + this.PairedBracketValues = this.pairedBracketValues.AsSlice(); + } + + /// + /// Save the Types and PairedBracketTypes of this bididata + /// + /// + /// This is used when processing embedded style runs with + /// BidiCharacterType overrides. TextLayout saves the data, + /// overrides the style runs to neutral, processes the bidi + /// data for the entire paragraph and then restores this data + /// before processing the embedded runs. + /// + public void SaveTypes() + { + // Capture the types data + this.savedTypes.Clear(); + this.savedTypes.Add(this.types.AsSlice()); + this.savedPairedBracketTypes.Clear(); + this.savedPairedBracketTypes.Add(this.pairedBracketTypes.AsSlice()); + } + + /// + /// Restore the data saved by SaveTypes + /// + public void RestoreTypes() + { + this.types.Clear(); + this.types.Add(this.savedTypes.AsSlice()); + this.pairedBracketTypes.Clear(); + this.pairedBracketTypes.Add(this.savedPairedBracketTypes.AsSlice()); + } + + /// + /// Gets a temporary level buffer. Used by TextLayout when + /// resolving style runs with different BidiCharacterType. + /// + /// Length of the required ExpandableBuffer + /// An uninitialized level ExpandableBuffer + public ArraySlice GetTempLevelBuffer(int length) + { + this.tempLevelBuffer.Clear(); + return this.tempLevelBuffer.Add(length, false); + } + } +} diff --git a/SixLabors.Fonts/Unicode/BidiDictionary{T1,T2}.cs b/SixLabors.Fonts/Unicode/BidiDictionary{T1,T2}.cs new file mode 100644 index 0000000..6e49059 --- /dev/null +++ b/SixLabors.Fonts/Unicode/BidiDictionary{T1,T2}.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +namespace SixLabors.Fonts.Unicode { + /// + /// A simple bi-directional dictionary. + /// + /// Key type + /// Value type + internal sealed class BidiDictionary + where T1 : struct + where T2 : struct + { + public Dictionary Forward { get; } = new Dictionary(); + + public Dictionary Reverse { get; } = new Dictionary(); + + public void Clear() + { + this.Forward.Clear(); + this.Reverse.Clear(); + } + + public void Add(T1 key, T2 value) + { + this.Forward.Add(key, value); + this.Reverse.Add(value, key); + } + + public bool TryGetValue(T1 key, out T2 value) => this.Forward.TryGetValue(key, out value); + + public bool TryGetKey(T2 value, out T1 key) => this.Reverse.TryGetValue(value, out key); + + public bool ContainsKey(T1 key) => this.Forward.ContainsKey(key); + + public bool ContainsValue(T2 value) => this.Reverse.ContainsKey(value); + } +} diff --git a/SixLabors.Fonts/Unicode/BidiPairedBracketType.cs b/SixLabors.Fonts/Unicode/BidiPairedBracketType.cs new file mode 100644 index 0000000..13f10c6 --- /dev/null +++ b/SixLabors.Fonts/Unicode/BidiPairedBracketType.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode { + /// + /// Unicode Bidi_Paired_Bracket_Type property values. + /// + /// + /// + /// UAX #9 uses this property with Bidi_Paired_Bracket to find bracket pairs + /// while resolving neutral characters. + /// + public enum BidiPairedBracketType + { + /// + /// No paired bracket behavior. + /// + None = 0, + + /// + /// Opening paired bracket. + /// + Open = 1, + + /// + /// Closing paired bracket. + /// + Close = 2 + } +} diff --git a/SixLabors.Fonts/Unicode/BidiRun.cs b/SixLabors.Fonts/Unicode/BidiRun.cs new file mode 100644 index 0000000..59d34aa --- /dev/null +++ b/SixLabors.Fonts/Unicode/BidiRun.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; + +namespace SixLabors.Fonts.Unicode { + internal readonly struct BidiRun : IEquatable + { + public BidiRun(BidiCharacterType direction, int level, int start, int length) + { + this.Direction = direction; + this.Level = level; + this.Start = start; + this.Length = length; + } + + public BidiCharacterType Direction { get; } + + public int Level { get; } + + public int Start { get; } + + public int Length { get; } + + public int End => this.Start + this.Length; + + public static bool operator ==(BidiRun left, BidiRun right) => left.Equals(right); + + public static bool operator !=(BidiRun left, BidiRun right) => !(left == right); + + public override string ToString() => $"{this.Start} - {this.End} - {this.Direction}"; + + public static IEnumerable CoalesceLevels(ReadOnlyArraySlice levels) + { + if (levels.Length == 0) + { + yield break; + } + + int startRun = 0; + sbyte runLevel = levels[0]; + BidiCharacterType direction; + for (int i = 1; i < levels.Length; i++) + { + if (levels[i] == runLevel) + { + continue; + } + + // End of this run + direction = (runLevel & 0x01) == 0 ? BidiCharacterType.LeftToRight : BidiCharacterType.RightToLeft; + yield return new BidiRun(direction, runLevel, startRun, i - startRun); + + // Move to next run + startRun = i; + runLevel = levels[i]; + } + + direction = (runLevel & 0x01) == 0 ? BidiCharacterType.LeftToRight : BidiCharacterType.RightToLeft; + yield return new BidiRun(direction, runLevel, startRun, levels.Length - startRun); + } + + public override bool Equals(object? obj) + => obj is BidiRun run && this.Equals(run); + + public bool Equals(BidiRun other) + => this.Direction == other.Direction + && this.Level == other.Level + && this.Start == other.Start + && this.Length == other.Length + && this.End == other.End; + + public override int GetHashCode() + => HashCode.Combine(this.Direction, this.Level, this.Start, this.Length, this.End); + } +} diff --git a/SixLabors.Fonts/Unicode/CodePoint.cs b/SixLabors.Fonts/Unicode/CodePoint.cs new file mode 100644 index 0000000..6a839b9 --- /dev/null +++ b/SixLabors.Fonts/Unicode/CodePoint.cs @@ -0,0 +1,822 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts.Unicode { + /// + /// Represents a Unicode value ([ U+0000..U+10FFFF ], inclusive). + /// + /// + /// This type's constructors and conversion operators validate the input, so consumers can call the APIs + /// assuming that the underlying instance is well-formed. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public readonly struct CodePoint : IComparable, IComparable, IEquatable + { + // Supplementary plane code points are encoded as 2 UTF-16 code units + private const int MaxUtf16CharsPerCodePoint = 2; + + // Supplementary plane code points are encoded as 4 UTF-8 code units + internal const int MaxUtf8BytesPerCodePoint = 4; + private const byte IsWhiteSpaceFlag = 0x80; + private const byte IsLetterOrDigitFlag = 0x40; + private const byte UnicodeCategoryMask = 0x1F; + + private readonly uint value; + + /// + /// Initializes a new instance of the struct. + /// + /// The char representing the UTF-16 code unit + /// + /// If represents a UTF-16 surrogate code point + /// U+D800..U+DFFF, inclusive. + /// + public CodePoint(char value) + { + uint expanded = value; + + if (UnicodeUtility.IsSurrogateCodePoint(expanded)) + { + ThrowArgumentOutOfRange(expanded, nameof(value), "Must not be in [ U+D800..U+DFFF ], inclusive."); + } + + this.value = expanded; + } + + /// + /// Initializes a new instance of the struct. + /// + /// A char representing a UTF-16 high surrogate code unit. + /// A char representing a UTF-16 low surrogate code unit. + /// + /// If does not represent a UTF-16 high surrogate code unit + /// or does not represent a UTF-16 low surrogate code unit. + /// + public CodePoint(char highSurrogate, char lowSurrogate) + : this((uint)char.ConvertToUtf32(highSurrogate, lowSurrogate), false) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The value to create the codepoint. + /// + /// If does not represent a value Unicode scalar value. + /// + public CodePoint(int value) + : this((uint)value) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The value to create the codepoint. + /// + /// If does not represent a value Unicode scalar value. + /// + public CodePoint(uint value) + { + if (!IsValid(value)) + { + ThrowArgumentOutOfRange(value, nameof(value), "Must be in [ U+0000..U+10FFFF ], inclusive."); + } + + this.value = value; + } + + // Non-validating ctor +#pragma warning disable IDE0060 // Remove unused parameter + private CodePoint(uint scalarValue, bool unused) + { + UnicodeUtility.DebugAssertIsValidCodePoint(scalarValue); + this.value = scalarValue; + } +#pragma warning restore IDE0060 // Remove unused parameter + + // Contains information about the ASCII character range [ U+0000..U+007F ], with: + // - 0x80 bit if set means 'is whitespace' + // - 0x40 bit if set means 'is letter or digit' + // - 0x20 bit is reserved for future use + // - bottom 5 bits are the UnicodeCategory of the character + private static ReadOnlySpan AsciiCharInfo => + [ + 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x8E, 0x8E, 0x8E, 0x8E, 0x8E, 0x0E, 0x0E, // U+0000..U+000F + 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, // U+0010..U+001F + 0x8B, 0x18, 0x18, 0x18, 0x1A, 0x18, 0x18, 0x18, 0x14, 0x15, 0x18, 0x19, 0x18, 0x13, 0x18, 0x18, // U+0020..U+002F + 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x18, 0x18, 0x19, 0x19, 0x19, 0x18, // U+0030..U+003F + 0x18, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, // U+0040..U+004F + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x14, 0x18, 0x15, 0x1B, 0x12, // U+0050..U+005F + 0x1B, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, // U+0060..U+006F + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x14, 0x19, 0x15, 0x19, 0x0E, // U+0070..U+007F + ]; + + /// + /// Gets a value indicating whether this value is ASCII ([ U+0000..U+007F ]) + /// and therefore representable by a single UTF-8 code unit. + /// + public bool IsAscii => UnicodeUtility.IsAsciiCodePoint(this.value); + + /// + /// Gets a value indicating whether this value is within the BMP ([ U+0000..U+FFFF ]) + /// and therefore representable by a single UTF-16 code unit. + /// + public bool IsBmp => UnicodeUtility.IsBmpCodePoint(this.value); + + /// + /// Gets the Unicode plane (0 to 16, inclusive) which contains this scalar. + /// + public int Plane => UnicodeUtility.GetPlane(this.value); + + // Displayed as "'' (U+XXXX)"; e.g., "'e' (U+0065)" + private string DebuggerDisplay => FormattableString.Invariant($"U+{this.value:X4} '{(IsValid(this.value) ? this.ToString() : "\uFFFD")}'"); + + /// + /// Gets the Unicode value as an integer. + /// + public int Value => (int)this.value; + + /// + /// Gets the length in code units () of the + /// UTF-16 sequence required to represent this scalar value. + /// + /// + /// The return value will be 1 or 2. + /// + public int Utf16SequenceLength + { + get + { + int codeUnitCount = UnicodeUtility.GetUtf16SequenceLength(this.value); + Debug.Assert(codeUnitCount is > 0 and <= MaxUtf16CharsPerCodePoint, $"Invalid Utf16SequenceLength {codeUnitCount}."); + return codeUnitCount; + } + } + + /// + /// Gets the length in code units of the + /// UTF-8 sequence required to represent this scalar value. + /// + /// + /// The return value will be 1 through 4, inclusive. + /// + public int Utf8SequenceLength + { + get + { + int codeUnitCount = UnicodeUtility.GetUtf8SequenceLength(this.value); + Debug.Assert(codeUnitCount is > 0 and <= MaxUtf8BytesPerCodePoint, $"Invalid Utf8SequenceLength {codeUnitCount}."); + return codeUnitCount; + } + } + + /// + /// Gets a instance that represents the Unicode replacement character U+FFFD. + /// + public static CodePoint ReplacementChar { get; } = new CodePoint(0xFFFD); + + /// + /// Gets a instance that represents the Unicode object replacement character U+FFFC. + /// + public static CodePoint ObjectReplacementChar { get; } = new CodePoint(0xFFFC); + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + + // Operators below are explicit because they may throw. + public static explicit operator CodePoint(char ch) => new(ch); + + public static explicit operator CodePoint(uint value) => new(value); + + public static explicit operator CodePoint(int value) => new(value); + + public static bool operator ==(CodePoint left, CodePoint right) => left.value == right.value; + + public static bool operator !=(CodePoint left, CodePoint right) => left.value != right.value; + + public static bool operator <(CodePoint left, CodePoint right) => left.value < right.value; + + public static bool operator <=(CodePoint left, CodePoint right) => left.value <= right.value; + + public static bool operator >(CodePoint left, CodePoint right) => left.value > right.value; + + public static bool operator >=(CodePoint left, CodePoint right) => left.value >= right.value; +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + + /// + /// Returns if is a valid Unicode code + /// point, i.e., is in [ U+0000..U+10FFFF ], inclusive. + /// + /// The value to evaluate. + /// if represents a valid codepoint; otherwise, + public static bool IsValid(int value) => IsValid((uint)value); + + /// + /// Returns if is a valid Unicode code + /// point, i.e., is in [ U+0000..U+10FFFF ], inclusive. + /// + /// The value to evaluate. + /// if represents a valid codepoint; otherwise, + public static bool IsValid(uint value) => UnicodeUtility.IsValidCodePoint(value); + + /// + /// Gets a value indicating whether the given codepoint is white space. + /// + /// The codepoint to evaluate. + /// if is a whitespace character; otherwise, + public static bool IsWhiteSpace(in CodePoint codePoint) + { + if (codePoint.IsAscii) + { + return (AsciiCharInfo[codePoint.Value] & IsWhiteSpaceFlag) != 0; + } + + // Only BMP code points can be white space, so only call into char + // if the incoming value is within the BMP. + return codePoint.IsBmp && char.IsWhiteSpace((char)codePoint.Value); + } + + /// + /// Gets a value indicating whether the given codepoint is a non-breaking space. + /// + /// The codepoint to evaluate. + /// if is a non-breaking space character; otherwise, + public static bool IsNonBreakingSpace(in CodePoint codePoint) + => codePoint.Value == 0x00A0; + + /// + /// Gets a value indicating whether the given codepoint is a zero-width-non-joiner. + /// + /// The codepoint to evaluate. + /// if is a zero-width-non-joiner character; otherwise, + public static bool IsZeroWidthNonJoiner(in CodePoint codePoint) + => codePoint.Value == 0x200C; + + /// + /// Gets a value indicating whether the given codepoint is a zero-width-joiner. + /// + /// The codepoint to evaluate. + /// if is a zero-width-joiner character; otherwise, + public static bool IsZeroWidthJoiner(in CodePoint codePoint) + => codePoint.Value == 0x200D; + + /// + /// Gets a value indicating whether the given codepoint is a variation selector. + /// + /// + /// The codepoint to evaluate. + /// if is a variation selector character; otherwise, + public static bool IsVariationSelector(in CodePoint codePoint) + => (codePoint.Value & 0xFFF0) == 0xFE00; + + /// + /// Gets a value indicating whether the given codepoint is a control character. + /// + /// The codepoint to evaluate. + /// if is a control character; otherwise, + public static bool IsControl(in CodePoint codePoint) => + + // Per the Unicode stability policy, the set of control characters + // is forever fixed at [ U+0000..U+001F ], [ U+007F..U+009F ]. No + // characters will ever be added to or removed from the "control characters" + // group. See https://www.unicode.org/policies/stability_policy.html. + // + // Logic below depends on CodePoint.Value never being -1 (since CodePoint is a validating type) + // 00..1F (+1) => 01..20 (&~80) => 01..20 + // 7F..9F (+1) => 80..A0 (&~80) => 00..20 + ((codePoint.value + 1) & ~0x80u) <= 0x20u; + + /// + /// Returns a value that indicates whether the specified codepoint is categorized as a decimal digit. + /// + /// The codepoint to evaluate. + /// if is a decimal digit; otherwise, + public static bool IsDigit(in CodePoint codePoint) + { + if (codePoint.IsAscii) + { + return UnicodeUtility.IsInRangeInclusive(codePoint.value, '0', '9'); + } + else + { + return GetGeneralCategory(codePoint) == UnicodeCategory.DecimalDigitNumber; + } + } + + /// + /// Returns a value that indicates whether the specified codepoint is categorized as a letter. + /// + /// The codepoint to evaluate. + /// if is a letter; otherwise, + public static bool IsLetter(in CodePoint codePoint) + { + if (codePoint.IsAscii) + { + return ((codePoint.value - 'A') & ~0x20u) <= 'Z' - 'A'; // [A-Za-z] + } + else + { + return IsCategoryLetter(GetGeneralCategory(codePoint)); + } + } + + /// + /// Returns a value that indicates whether the specified codepoint is categorized as a letter or decimal digit. + /// + /// The codepoint to evaluate. + /// if is a letter or decimal digit; otherwise, + public static bool IsLetterOrDigit(in CodePoint codePoint) + { + if (codePoint.IsAscii) + { + return (AsciiCharInfo[codePoint.Value] & IsLetterOrDigitFlag) != 0; + } + else + { + return IsCategoryLetterOrDecimalDigit(GetGeneralCategory(codePoint)); + } + } + + /// + /// Returns a value that indicates whether the specified codepoint is categorized as a lowercase letter. + /// + /// The codepoint to evaluate. + /// if is a lowercase letter; otherwise, + public static bool IsLower(in CodePoint codePoint) + { + if (codePoint.IsAscii) + { + return UnicodeUtility.IsInRangeInclusive(codePoint.value, 'a', 'z'); + } + else + { + return GetGeneralCategory(codePoint) == UnicodeCategory.LowercaseLetter; + } + } + + /// + /// Returns a value that indicates whether the specified codepoint is categorized as a number. + /// + /// The codepoint to evaluate. + /// if is a number; otherwise, + public static bool IsNumber(in CodePoint codePoint) + { + if (codePoint.IsAscii) + { + return UnicodeUtility.IsInRangeInclusive(codePoint.value, '0', '9'); + } + else + { + return IsCategoryNumber(GetGeneralCategory(codePoint)); + } + } + + /// + /// Returns a value that indicates whether the specified codepoint is categorized as punctuation. + /// + /// The codepoint to evaluate. + /// if is punctuation; otherwise, + public static bool IsPunctuation(in CodePoint codePoint) + => IsCategoryPunctuation(GetGeneralCategory(codePoint)); + + /// + /// Returns a value that indicates whether the specified codepoint is categorized as a separator. + /// + /// The codepoint to evaluate. + /// if is a separator; otherwise, + public static bool IsSeparator(in CodePoint codePoint) + => IsCategorySeparator(GetGeneralCategory(codePoint)); + + /// + /// Returns a value that indicates whether the specified codepoint is categorized as a symbol. + /// + /// The codepoint to evaluate. + /// if is a symbol; otherwise, + public static bool IsSymbol(in CodePoint codePoint) + => IsCategorySymbol(GetGeneralCategory(codePoint)); + + /// + /// Returns a value that indicates whether the specified codepoint is categorized as a mark. + /// + /// The codepoint to evaluate. + /// if is a symbol; otherwise, + public static bool IsMark(in CodePoint codePoint) + => IsCategoryMark(GetGeneralCategory(codePoint)); + + /// + /// Returns a value that indicates whether the specified codepoint is categorized as an uppercase letter. + /// + /// The codepoint to evaluate. + /// if is a uppercase letter; otherwise, + public static bool IsUpper(in CodePoint codePoint) + { + if (codePoint.IsAscii) + { + return UnicodeUtility.IsInRangeInclusive(codePoint.value, 'A', 'Z'); + } + else + { + return GetGeneralCategory(codePoint) == UnicodeCategory.UppercaseLetter; + } + } + + /// + /// Gets a value indicating whether the given codepoint is a tabulation indicator. + /// + /// The codepoint to evaluate. + /// if is a tabulation indicator; otherwise, + public static bool IsTabulation(in CodePoint codePoint) + => codePoint.value == 0x0009; + + /// + /// Gets a value indicating whether the given codepoint is a new line indicator. + /// + /// The codepoint to evaluate. + /// if is a new line indicator; otherwise, + public static bool IsNewLine(in CodePoint codePoint) + => codePoint.Value switch + { + // See https://www.unicode.org/standard/reports/tr13/tr13-5.html + 0x000A // LINE FEED (LF) + or 0x000B // LINE TABULATION (VT) + or 0x000C // FORM FEED (FF) + or 0x000D // CARRIAGE RETURN (CR) + or 0x0085 // NEXT LINE (NEL) + or 0x2028 // LINE SEPARATOR (LS) + or 0x2029 => true, // PARAGRAPH SEPARATOR (PS) + _ => false, + }; + + /// + /// Returns the number of codepoints in a given string buffer. + /// + /// The source buffer to parse. + /// The count. + public static int GetCodePointCount(ReadOnlySpan source) + { + if (source.IsEmpty) + { + return 0; + } + + int count = 0; + SpanCodePointEnumerator enumerator = new(source); + while (enumerator.MoveNext()) + { + count++; + } + + return count; + } + + /// + /// Gets the canonical representation of a given codepoint. + /// + /// + /// The code point to be mapped. + /// The mapped canonical code point, or the passed . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static CodePoint GetCanonicalType(in CodePoint codePoint) + { + if (codePoint.Value == 0x3008) + { + return new CodePoint(0x2329); + } + + if (codePoint.Value == 0x3009) + { + return new CodePoint(0x232A); + } + + return codePoint; + } + + /// + /// Gets the for the given codepoint. + /// + /// The codepoint to evaluate. + /// The . + public static BidiClass GetBidiClass(in CodePoint codePoint) + => new(codePoint); + + /// + /// Gets the codepoint representing the bidi mirror for this instance. + /// + /// + /// The code point to be mapped. + /// + /// When this method returns, contains the codepoint representing the bidi mirror for this instance; + /// otherwise, the default value for the type of the parameter. + /// This parameter is passed uninitialized. + /// . + /// if this instance has a mirror; otherwise, + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryGetBidiMirror(in CodePoint codePoint, out CodePoint mirror) + { + uint value = UnicodeData.GetBidiMirror(codePoint.value); + + if (value == 0u) + { + mirror = default; + return false; + } + + mirror = new CodePoint(value); + return true; + } + + /// + /// Gets the codepoint representing the vertical mirror for this instance. + /// + /// + /// The code point to be mapped. + /// + /// When this method returns, contains the codepoint representing the vertical mirror for this instance; + /// otherwise, the default value for the type of the parameter. + /// This parameter is passed uninitialized. + /// . + /// if this instance has a mirror; otherwise, + public static bool TryGetVerticalMirror(in CodePoint codePoint, out CodePoint mirror) + { + uint value = UnicodeUtility.GetVerticalMirror((uint)codePoint.Value); + + if (value == 0u) + { + mirror = default; + return false; + } + + mirror = new CodePoint(value); + return true; + } + + /// + /// Gets the for the given codepoint. + /// + /// The codepoint to evaluate. + /// The . + public static LineBreakClass GetLineBreakClass(in CodePoint codePoint) + => UnicodeData.GetLineBreakClass(codePoint.value); + + /// + /// Gets the for the given codepoint. + /// + /// The codepoint. + /// The . + public static WordBreakClass GetWordBreakClass(in CodePoint codePoint) + => UnicodeData.GetWordBreakClass(codePoint.value); + + /// + /// Gets the for the given codepoint. + /// + /// The codepoint to evaluate. + /// The . + public static GraphemeClusterClass GetGraphemeClusterClass(in CodePoint codePoint) + => UnicodeData.GetGraphemeClusterClass(codePoint.value); + + /// + /// Gets the for the given codepoint. + /// + /// The codepoint to evaluate. + /// The . + public static VerticalOrientationType GetVerticalOrientationType(in CodePoint codePoint) + => UnicodeData.GetVerticalOrientation(codePoint.value); + + /// + /// Gets the for the given codepoint. + /// + /// The codepoint to evaluate. + /// The . + /// + /// This returns the Unicode East_Asian_Width property value from UAX #11. It does + /// not resolve context-sensitive display-cell width; for example, + /// may resolve as narrow or wide + /// depending on language, script, source encoding, font, or explicit markup. + /// + public static EastAsianWidthClass GetEastAsianWidthClass(in CodePoint codePoint) + => UnicodeData.GetEastAsianWidthClass(codePoint.value); + + /// + /// Gets the for the given codepoint. + /// + /// The codepoint to evaluate. + /// The . + public static EmojiProperties GetEmojiProperties(in CodePoint codePoint) + => UnicodeData.GetEmojiProperties(codePoint.value); + + /// + /// Gets the for the given codepoint. + /// + /// The codepoint to evaluate. + /// The . + public static ArabicJoiningClass GetArabicJoiningClass(in CodePoint codePoint) + => new(codePoint); + + /// + /// Gets the for the given codepoint. + /// + /// The codepoint to evaluate. + /// The . + public static ScriptClass GetScriptClass(in CodePoint codePoint) + => UnicodeData.GetScriptClass(codePoint.value); + + /// + /// Gets the for the given codepoint. + /// + /// The codepoint to evaluate. + /// The . + public static IndicConjunctBreakClass GetIndicConjunctBreakClass(in CodePoint codePoint) + => UnicodeData.GetIndicConjunctBreakClass(codePoint.value); + + /// + /// Gets the for the given codepoint. + /// + /// The codepoint to evaluate. + /// The . + public static IndicSyllabicCategory GetIndicSyllabicCategory(in CodePoint codePoint) + => UnicodeData.GetIndicSyllabicCategory(codePoint.value); + + /// + /// Gets the for the given codepoint. + /// + /// The codepoint to evaluate. + /// The . + public static IndicPositionalCategory GetIndicPositionalCategory(in CodePoint codePoint) + => UnicodeData.GetIndicPositionalCategory(codePoint.value); + + /// + /// Gets the for the given codepoint. + /// + /// The codepoint to evaluate. + /// The . + public static UnicodeCategory GetGeneralCategory(in CodePoint codePoint) + { + if (codePoint.IsAscii) + { + return (UnicodeCategory)(AsciiCharInfo[codePoint.Value] & UnicodeCategoryMask); + } + + return UnicodeData.GetUnicodeCategory(codePoint.value); + } + + /// + /// Reads the at specified position. + /// + /// The text to read from. + /// The index to read at. + /// The count of chars consumed reading the buffer. + /// The . + internal static CodePoint ReadAt(string text, int index, out int charsConsumed) + => DecodeFromUtf16At(text.AsMemory().Span, index, out charsConsumed); + + /// + /// Decodes the from the provided UTF-16 source buffer at the specified position. + /// + /// The buffer to read from. + /// The index to read at. + /// The . + internal static CodePoint DecodeFromUtf16At(ReadOnlySpan source, int index) + => DecodeFromUtf16At(source, index, out int _); + + /// + /// Decodes the from the provided UTF-16 source buffer at the specified position. + /// + /// The buffer to read from. + /// The index to read at. + /// The count of chars consumed reading the buffer. + /// The . + internal static CodePoint DecodeFromUtf16At(ReadOnlySpan source, int index, out int charsConsumed) + { + if (index >= source.Length) + { + charsConsumed = 0; + return default; + } + + // Optimistically assume input is within BMP. + charsConsumed = 1; + uint code = source[index]; + + // High surrogate + if (UnicodeUtility.IsHighSurrogateCodePoint(code)) + { + uint hi, low; + + hi = code; + index++; + + if (index == source.Length) + { + return ReplacementChar; + } + + low = source[index]; + + if (UnicodeUtility.IsLowSurrogateCodePoint(low)) + { + charsConsumed = 2; + return new CodePoint(UnicodeUtility.GetScalarFromUtf16SurrogatePair(hi, low)); + } + + return ReplacementChar; + } + + if (UnicodeUtility.IsLowSurrogateCodePoint(code)) + { + return ReplacementChar; + } + + return new CodePoint(code); + } + + /// + int IComparable.CompareTo(object? obj) + { + if (obj is null) + { + return 1; // non-null ("this") always sorts after null + } + + if (obj is CodePoint other) + { + return this.CompareTo(other); + } + + throw new ArgumentException("Object must be of type CodePoint."); + } + + /// + public int CompareTo(CodePoint other) + + // Values don't span entire 32-bit domain so won't integer overflow. + => this.Value - other.Value; + + /// + public override bool Equals(object? obj) => obj is CodePoint point && this.Equals(point); + + /// + public bool Equals(CodePoint other) => this.value == other.value; + + /// + public override int GetHashCode() => HashCode.Combine(this.value); + + /// + public override string ToString() + { + if (this.IsBmp) + { + return ((char)this.value).ToString(); + } + else + { + Span buffer = stackalloc char[MaxUtf16CharsPerCodePoint]; + UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneCodePoint(this.value, out buffer[0], out buffer[1]); + return buffer.ToString(); + } + } + + /// + /// Returns this instance displayed as "'<char>' (U+XXXX)"; e.g., "'e' (U+0065)" + /// + /// The . + internal string ToDebuggerDisplay() => this.DebuggerDisplay; + + // Returns true if this Unicode category represents a letter + private static bool IsCategoryLetter(UnicodeCategory category) + => UnicodeUtility.IsInRangeInclusive((uint)category, (uint)UnicodeCategory.UppercaseLetter, (uint)UnicodeCategory.OtherLetter); + + // Returns true if this Unicode category represents a letter or a decimal digit + private static bool IsCategoryLetterOrDecimalDigit(UnicodeCategory category) + => UnicodeUtility.IsInRangeInclusive((uint)category, (uint)UnicodeCategory.UppercaseLetter, (uint)UnicodeCategory.OtherLetter) + || (category == UnicodeCategory.DecimalDigitNumber); + + // Returns true if this Unicode category represents a number + private static bool IsCategoryNumber(UnicodeCategory category) + => UnicodeUtility.IsInRangeInclusive((uint)category, (uint)UnicodeCategory.DecimalDigitNumber, (uint)UnicodeCategory.OtherNumber); + + // Returns true if this Unicode category represents a punctuation mark + private static bool IsCategoryPunctuation(UnicodeCategory category) + => UnicodeUtility.IsInRangeInclusive((uint)category, (uint)UnicodeCategory.ConnectorPunctuation, (uint)UnicodeCategory.OtherPunctuation); + + // Returns true if this Unicode category represents a separator + private static bool IsCategorySeparator(UnicodeCategory category) + => UnicodeUtility.IsInRangeInclusive((uint)category, (uint)UnicodeCategory.SpaceSeparator, (uint)UnicodeCategory.ParagraphSeparator); + + // Returns true if this Unicode category represents a symbol + private static bool IsCategorySymbol(UnicodeCategory category) + => UnicodeUtility.IsInRangeInclusive((uint)category, (uint)UnicodeCategory.MathSymbol, (uint)UnicodeCategory.OtherSymbol); + + // Returns true if this Unicode category represents a mark + private static bool IsCategoryMark(UnicodeCategory category) + => UnicodeUtility.IsInRangeInclusive((uint)category, (uint)UnicodeCategory.NonSpacingMark, (uint)UnicodeCategory.EnclosingMark); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentOutOfRange(uint value, string paramName, string message) + => throw new ArgumentOutOfRangeException(paramName, $"The value {UnicodeUtility.ToHexString(value)} is not a valid Unicode code point value. {message}"); + } +} diff --git a/SixLabors.Fonts/Unicode/EastAsianWidthClass.cs b/SixLabors.Fonts/Unicode/EastAsianWidthClass.cs new file mode 100644 index 0000000..c2eb56c --- /dev/null +++ b/SixLabors.Fonts/Unicode/EastAsianWidthClass.cs @@ -0,0 +1,81 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode { + /// + /// Unicode East_Asian_Width property values. + /// + /// + /// + /// East_Asian_Width is a Unicode character property used when interoperating with + /// East Asian legacy encodings and typography. The property has six default values, + /// but any operation that needs a display-cell width must still resolve those values + /// to a context-specific narrow or wide result. + /// + public enum EastAsianWidthClass + { + /// + /// Neutral (N): characters that are not East Asian for this property. + /// + /// + /// Neutral characters are not normally found in legacy East Asian character sets or + /// traditional East Asian typography. UAX #11 recommends treating them like narrow + /// characters for practical resolved-width decisions, but the property value itself + /// is distinct from . + /// + Neutral = 0, + + /// + /// Ambiguous (A): characters that can be either wide or narrow depending on context. + /// + /// + /// Ambiguous characters need extra information, such as language, script, font, + /// source encoding, or explicit markup, before they can be resolved to a display + /// width. In East Asian legacy contexts they may be treated as wide; + /// otherwise UAX #11 recommends treating them as narrow by default. + /// + Ambiguous = 1, + + /// + /// Fullwidth (F): explicitly encoded fullwidth compatibility characters. + /// + /// + /// Fullwidth characters have a compatibility decomposition of type <wide> + /// to another Unicode character that is implicitly narrow. They exist to preserve + /// round-tripping with mixed-width East Asian legacy encodings. + /// + Fullwidth = 2, + + /// + /// Halfwidth (H): explicitly encoded halfwidth compatibility characters. + /// + /// + /// Halfwidth characters have a compatibility decomposition of type <narrow> + /// to another Unicode character that is implicitly wide, with the special case of + /// U+20A9 WON SIGN. They are distinct from ordinary narrow characters because they + /// can still behave like East Asian compatibility forms for font selection and some + /// punctuation behavior. + /// + Halfwidth = 3, + + /// + /// Narrow (Na): characters that are always narrow and have explicit wide or fullwidth counterparts. + /// + /// + /// Narrow characters are implicitly narrow in East Asian typography and legacy + /// character sets. ASCII is the common example: the ordinary ASCII code points are + /// Narrow, while their compatibility forms are Fullwidth. + /// + Narrow = 4, + + /// + /// Wide (W): characters that are always wide in East Asian typography. + /// + /// + /// Wide characters behave like ideographs for East Asian layout. This includes + /// many Han, Kana, Hangul, and emoji-presentation characters that are not encoded + /// as explicit Fullwidth compatibility forms. + /// + Wide = 5 + } +} diff --git a/SixLabors.Fonts/Unicode/EmojiProperties.cs b/SixLabors.Fonts/Unicode/EmojiProperties.cs new file mode 100644 index 0000000..64c5cac --- /dev/null +++ b/SixLabors.Fonts/Unicode/EmojiProperties.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Unicode { + /// + /// Binary Unicode emoji properties from UTS #51. + /// + [Flags] + public enum EmojiProperties : byte + { + /// + /// No emoji properties. + /// + None = 0, + + /// + /// The scalar has the Emoji property. + /// + Emoji = 1 << 0, + + /// + /// The scalar defaults to emoji presentation. + /// + EmojiPresentation = 1 << 1, + + /// + /// The scalar is an emoji modifier. + /// + EmojiModifier = 1 << 2, + + /// + /// The scalar can be followed by an emoji modifier. + /// + EmojiModifierBase = 1 << 3, + + /// + /// The scalar is used as a component in emoji sequences. + /// + EmojiComponent = 1 << 4, + + /// + /// The scalar can be followed by U+FE0E to request text presentation. + /// + TextPresentationSequenceBase = 1 << 5, + + /// + /// The scalar can be followed by U+FE0F to request emoji presentation. + /// + EmojiPresentationSequenceBase = 1 << 6, + + /// + /// The scalar can start an emoji keycap sequence. + /// + EmojiKeycapSequenceBase = 1 << 7 + } +} diff --git a/SixLabors.Fonts/Unicode/GraphemeCluster.cs b/SixLabors.Fonts/Unicode/GraphemeCluster.cs new file mode 100644 index 0000000..c338008 --- /dev/null +++ b/SixLabors.Fonts/Unicode/GraphemeCluster.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Unicode { + /// + /// Represents a Unicode grapheme cluster and metadata derived while enumerating it. + /// + public readonly ref struct GraphemeCluster + { + /// + /// Initializes a new instance of the struct. + /// + /// The UTF-16 span containing the grapheme cluster. + /// The UTF-16 offset of the cluster in the original source. + /// The number of Unicode scalar values in the cluster. + /// The policy-resolved terminal cell width of the cluster. + /// The cluster flags derived while scanning the cluster. + /// The first code point in the cluster. + public GraphemeCluster( + ReadOnlySpan span, + int utf16Offset, + int codePointCount, + int terminalCellWidth, + GraphemeClusterFlags flags, + CodePoint firstCodePoint) + { + this.Span = span; + this.Utf16Offset = utf16Offset; + this.CodePointCount = codePointCount; + this.Utf16Length = span.Length; + this.TerminalCellWidth = terminalCellWidth; + this.Flags = flags; + this.FirstCodePoint = firstCodePoint; + } + + /// + /// Gets the UTF-16 span containing the grapheme cluster. + /// + public ReadOnlySpan Span { get; } + + /// + /// Gets the UTF-16 offset of the cluster in the original source. + /// + public int Utf16Offset { get; } + + /// + /// Gets the UTF-16 length of the cluster. + /// + public int Utf16Length { get; } + + /// + /// Gets the number of Unicode scalar values in the cluster. + /// + public int CodePointCount { get; } + + /// + /// Gets the policy-resolved terminal cell width of the cluster. + /// + public int TerminalCellWidth { get; } + + /// + /// Gets the cluster flags derived while scanning the cluster. + /// + public GraphemeClusterFlags Flags { get; } + + /// + /// Gets the first code point in the cluster. + /// + public CodePoint FirstCodePoint { get; } + } +} diff --git a/SixLabors.Fonts/Unicode/GraphemeClusterClass.cs b/SixLabors.Fonts/Unicode/GraphemeClusterClass.cs new file mode 100644 index 0000000..42fd17c --- /dev/null +++ b/SixLabors.Fonts/Unicode/GraphemeClusterClass.cs @@ -0,0 +1,128 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode { + /// + /// Unicode Grapheme_Cluster_Break property values and local rule sentinels. + /// + /// + /// + /// UAX #29 uses these classes in ordered boundary rules to determine extended + /// grapheme clusters. Some members are rule sentinels rather than Unicode property + /// values exposed by the standard. + /// + public enum GraphemeClusterClass + { + /// + /// Rule sentinel that matches any code point. + /// + /// + /// This is not a Unicode property value; it represents the "any" operand in + /// UAX #29 boundary rules. + /// + Any = 0, + + /// + /// U+000D CARRIAGE RETURN (CR). + /// + CarriageReturn = 1, + + /// + /// U+000A LINE FEED (LF). + /// + LineFeed = 2, + + /// + /// Controls, separators, formats, and default-ignorable unassigned code points + /// that form hard grapheme cluster boundaries. + /// + /// + /// This class excludes CR, LF, U+200C ZERO WIDTH NON-JOINER, U+200D ZERO + /// WIDTH JOINER, and prepended concatenation marks because those participate + /// in more specific UAX #29 rules. + /// + Control = 3, + + /// + /// Extending code points that remain in the same extended grapheme cluster as + /// the preceding base. + /// + /// + /// This includes Grapheme_Extend code points, emoji modifiers, U+200C ZERO + /// WIDTH NON-JOINER, and a small number of spacing marks needed for canonical + /// equivalence. + /// + Extend = 4, + + /// + /// Regional indicator symbols used to build flag emoji pairs. + /// + RegionalIndicator = 5, + + /// + /// Code points that prepend to the following grapheme cluster. + /// + /// + /// This includes Indic_Syllabic_Category values Consonant_Preceding_Repha and + /// Consonant_Prefixed, plus Prepended_Concatenation_Mark code points. + /// + Prepend = 6, + + /// + /// Spacing marks that extend the previous grapheme cluster. + /// + /// + /// This includes spacing marks whose Grapheme_Cluster_Break value is not + /// Extend, plus U+0E33 THAI CHARACTER SARA AM and U+0EB3 LAO VOWEL SIGN AM. + /// + SpacingMark = 7, + + /// + /// Hangul leading consonant Jamo (Hangul_Syllable_Type = L). + /// + HangulLead = 8, + + /// + /// Hangul vowel Jamo (Hangul_Syllable_Type = V). + /// + HangulVowel = 9, + + /// + /// Hangul trailing consonant Jamo (Hangul_Syllable_Type = T). + /// + HangulTail = 10, + + /// + /// Hangul LV syllables. + /// + HangulLeadVowel = 11, + + /// + /// Hangul LVT syllables. + /// + HangulLeadVowelTail = 12, + + /// + /// Extended pictographic code points used by GB11 emoji ZWJ sequence handling. + /// + /// + /// This is not itself a Grapheme_Cluster_Break property value; UAX #29 uses + /// it when matching emoji ZWJ sequences. + /// + ExtendedPictographic = 13, + + /// + /// U+200D ZERO WIDTH JOINER. + /// + ZeroWidthJoiner = 14, + + /// + /// Other. + /// + /// + /// This is the Unicode Other / XX fallback for code points + /// without an explicit grapheme cluster break class. + /// + Other = 0xFF + } +} diff --git a/SixLabors.Fonts/Unicode/GraphemeClusterFlags.cs b/SixLabors.Fonts/Unicode/GraphemeClusterFlags.cs new file mode 100644 index 0000000..2a6a720 --- /dev/null +++ b/SixLabors.Fonts/Unicode/GraphemeClusterFlags.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Unicode { + /// + /// Flags describing properties of a Unicode grapheme cluster. + /// + [Flags] + public enum GraphemeClusterFlags : byte + { + /// + /// No flags. + /// + None = 0, + + /// + /// At least one scalar in the cluster resolved to two terminal cells before + /// any whole-cluster emoji override was applied. + /// + ContainsWide = 1 << 0, + + /// + /// At least one scalar in the cluster has East Asian Width Ambiguous. + /// + ContainsAmbiguous = 1 << 1, + + /// + /// All scalars in the cluster are zero-width for terminal measurement. + /// + AllZeroWidth = 1 << 2, + + /// + /// The cluster contains a C0 or C1 control scalar. + /// + ContainsControl = 1 << 3, + + /// + /// The cluster contains an emoji-like scalar or sequence. + /// + ContainsEmoji = 1 << 4, + + /// + /// The cluster contains a zero-width joiner sequence. + /// + ContainsZwjSequence = 1 << 5, + + /// + /// The cluster contains a variation selector. + /// + ContainsVariationSelector = 1 << 6, + + /// + /// The cluster contains exactly one code point. + /// + IsSingleCodePoint = 1 << 7 + } +} diff --git a/SixLabors.Fonts/Unicode/IndicConjunctBreakClass.cs b/SixLabors.Fonts/Unicode/IndicConjunctBreakClass.cs new file mode 100644 index 0000000..3029edc --- /dev/null +++ b/SixLabors.Fonts/Unicode/IndicConjunctBreakClass.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode { + /// + /// Unicode Indic_Conjunct_Break property values used by UAX #29 GB9c. + /// + /// + /// UAX #29 GB9c uses this property to keep matching consonant-linker-consonant + /// sequences inside one extended grapheme cluster. + /// + /// + public enum IndicConjunctBreakClass + { + /// + /// No Indic conjunct break behavior. This is the default for code points that do not + /// participate in GB9c. + /// + None = 0, + + /// + /// Consonant: a code point that can start or continue an Indic conjunct sequence. + /// + /// + /// In GB9c this is the stable anchor on either side of the linker sequence. + /// + Consonant = 1, + + /// + /// Extend: a combining or extending code point that is transparent inside an Indic conjunct sequence. + /// + /// + /// Extend code points can appear between the consonant and linker without ending + /// the conjunct sequence. + /// + Extend = 2, + + /// + /// Linker: a code point, such as a virama, that connects a following consonant. + /// + /// + /// A linker keeps the following consonant in the same extended grapheme cluster + /// when the surrounding GB9c pattern matches. + /// + Linker = 3 + } +} diff --git a/SixLabors.Fonts/Unicode/IndicPositionalCategory.cs b/SixLabors.Fonts/Unicode/IndicPositionalCategory.cs new file mode 100644 index 0000000..e4b1930 --- /dev/null +++ b/SixLabors.Fonts/Unicode/IndicPositionalCategory.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode { + /// + /// Unicode Indic_Positional_Category property values. + /// + /// + /// + /// These values describe the notional slot of dependent vowels, visible viramas, and related + /// signs around an Indic syllable core. The property supplements + /// for shaping and segmentation, but it is not a + /// prescriptive font-design or final glyph-placement property. is + /// the default for code points whose syllabic role does not have a positional slot. + /// + public enum IndicPositionalCategory + { + /// + /// Bottom. + /// + Bottom = 0, + + /// + /// Bottom_And_Left. + /// + BottomAndLeft = 1, + + /// + /// Bottom_And_Right. + /// + BottomAndRight = 2, + + /// + /// Left. + /// + Left = 3, + + /// + /// Left_And_Right. + /// + LeftAndRight = 4, + + /// + /// Overstruck. + /// + Overstruck = 6, + + /// + /// Right. + /// + Right = 7, + + /// + /// Top. + /// + Top = 8, + + /// + /// Top_And_Bottom. + /// + TopAndBottom = 9, + + /// + /// Top_And_Bottom_And_Left. + /// + TopAndBottomAndLeft = 10, + + /// + /// Top_And_Bottom_And_Right. + /// + TopAndBottomAndRight = 11, + + /// + /// Top_And_Left. + /// + TopAndLeft = 12, + + /// + /// Top_And_Left_And_Right. + /// + TopAndLeftAndRight = 13, + + /// + /// Top_And_Right. + /// + TopAndRight = 14, + + /// + /// Visual_Order_Left. + /// + VisualOrderLeft = 15, + + /// + /// Not applicable. + /// + /// + /// This is the Unicode fallback for code points without an explicit + /// Indic_Positional_Category. + /// + NA = 0xFF, + } +} diff --git a/SixLabors.Fonts/Unicode/IndicSyllabicCategory.cs b/SixLabors.Fonts/Unicode/IndicSyllabicCategory.cs new file mode 100644 index 0000000..2a4a943 --- /dev/null +++ b/SixLabors.Fonts/Unicode/IndicSyllabicCategory.cs @@ -0,0 +1,204 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode { + /// + /// Unicode Indic_Syllabic_Category property values. + /// + /// + /// + /// These values describe subtypes relevant to Indic syllable, or aksara, + /// construction and segmentation. + /// + public enum IndicSyllabicCategory + { + /// + /// Avagraha. + /// + Avagraha = 0, + + /// + /// Bindu. + /// + Bindu = 1, + + /// + /// Brahmi_Joining_Number. + /// + BrahmiJoiningNumber = 2, + + /// + /// Cantillation_Mark. + /// + CantillationMark = 3, + + /// + /// Consonant + /// + Consonant = 4, + + /// + /// Consonant_Dead + /// + ConsonantDead = 5, + + /// + /// Consonant_Final + /// + ConsonantFinal = 6, + + /// + /// Consonant_Head_Letter + /// + ConsonantHeadLetter = 7, + + /// + /// Consonant_Initial_Postfixed + /// + ConsonantInitialPostfixed = 8, + + /// + /// Consonant_Killer + /// + ConsonantKiller = 9, + + /// + /// Consonant_Medial + /// + ConsonantMedial = 10, + + /// + /// Consonant_Placeholder + /// + ConsonantPlaceholder = 11, + + /// + /// Consonant_Preceding_Repha + /// + ConsonantPrecedingRepha = 12, + + /// + /// Consonant_Prefixed + /// + ConsonantPrefixed = 13, + + /// + /// Consonant_Subjoined + /// + ConsonantSubjoined = 14, + + /// + /// Consonant_Succeeding_Repha + /// + ConsonantSucceedingRepha = 15, + + /// + /// Consonant_With_Stacker + /// + ConsonantWithStacker = 16, + + /// + /// Gemination_Mark + /// + GeminationMark = 17, + + /// + /// Invisible_Stacker + /// + InvisibleStacker = 18, + + /// + /// Joiner + /// + Joiner = 19, + + /// + /// Modifying_Letter + /// + ModifyingLetter = 20, + + /// + /// Non_Joiner + /// + NonJoiner = 21, + + /// + /// Nukta + /// + Nukta = 22, + + /// + /// Number + /// + Number = 23, + + /// + /// Number_Joiner + /// + NumberJoiner = 24, + + /// + /// Pure_Killer + /// + PureKiller = 26, + + /// + /// Register_Shifter + /// + RegisterShifter = 27, + + /// + /// Reordering_Killer + /// + ReorderingKiller = 28, + + /// + /// Syllable_Modifier + /// + SyllableModifier = 29, + + /// + /// Tone_Letter + /// + ToneLetter = 30, + + /// + /// Tone_Mark + /// + ToneMark = 31, + + /// + /// Virama + /// + Virama = 32, + + /// + /// Visarga + /// + Visarga = 33, + + /// + /// Vowel + /// + Vowel = 34, + + /// + /// Vowel_Dependent + /// + VowelDependent = 35, + + /// + /// Vowel_Independent + /// + VowelIndependent = 36, + + /// + /// Other. + /// + /// + /// This is the Unicode fallback for code points without an explicit + /// Indic_Syllabic_Category. + /// + Other = 0xFF + } +} diff --git a/SixLabors.Fonts/Unicode/LineBreak.cs b/SixLabors.Fonts/Unicode/LineBreak.cs new file mode 100644 index 0000000..8503f7c --- /dev/null +++ b/SixLabors.Fonts/Unicode/LineBreak.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.Fonts.Unicode { + /// + /// Information about a potential line break position. + /// + [DebuggerDisplay("{PositionMeasure}/{PositionWrap} @ {Required}")] + internal readonly struct LineBreak + { + /// + /// Initializes a new instance of the struct. + /// + /// The code point index to measure to + /// The code point index to actually break the line at + /// True if this is a required line break; otherwise false + /// True if this is a manual hyphenation break; otherwise false + public LineBreak( + int positionMeasure, + int positionWrap, + bool required = false, + bool isHyphenationBreak = false) + { + this.PositionMeasure = positionMeasure; + this.PositionWrap = positionWrap; + this.Required = required; + this.IsHyphenationBreak = isHyphenationBreak; + } + + /// + /// Gets the break position, before any trailing whitespace. + /// This doesn't include trailing whitespace. + /// + public int PositionMeasure { get; } + + /// + /// Gets the break position, after any trailing whitespace. + /// This includes trailing whitespace. + /// + public int PositionWrap { get; } + + /// + /// Gets a value indicating whether there should be a forced line break here. + /// + public bool Required { get; } + + /// + /// Gets a value indicating whether this is a manual hyphenation break. + /// + public bool IsHyphenationBreak { get; } + } +} diff --git a/SixLabors.Fonts/Unicode/LineBreakClass.cs b/SixLabors.Fonts/Unicode/LineBreakClass.cs new file mode 100644 index 0000000..6fea60a --- /dev/null +++ b/SixLabors.Fonts/Unicode/LineBreakClass.cs @@ -0,0 +1,256 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode { + /// + /// Unicode Line_Break property values. + /// + /// + public enum LineBreakClass : uint + { + /// + /// Open punctuation (OP): characters that prohibit a line break after them. + /// + OpenPunctuation = 0, + + /// + /// Close punctuation (CL): characters that prohibit a line break before them. + /// + ClosePunctuation = 1, + + /// + /// Close parenthesis (CP): closing bracket and parenthesis characters that prohibit a line break before them. + /// + CloseParenthesis = 2, + + /// + /// Quotation (QU): quotation marks that can behave as opening punctuation, closing punctuation, or both. + /// + Quotation = 3, + + /// + /// Non-breaking glue (GL): characters that prohibit line breaks before and after themselves. + /// + Glue = 4, + + /// + /// Nonstarter (NS): characters that allow only indirect line breaks before themselves. + /// + Nonstarter = 5, + + /// + /// Exclamation or interrogation (EX): sentence punctuation that prohibits a line break before itself. + /// + Exclamation = 6, + + /// + /// Symbols allowing break after (SY): characters that prevent a line break before and allow one after. + /// + BreakSymbols = 7, + + /// + /// Infix numeric separator (IS): characters that suppress breaks inside numeric expressions. + /// + InfixNumeric = 8, + + /// + /// Prefix numeric (PR): characters that stay with a following numeric expression. + /// + PrefixNumeric = 9, + + /// + /// Postfix numeric (PO): characters that stay with a preceding numeric expression. + /// + PostfixNumeric = 10, + + /// + /// Numeric (NU): digits and related characters that form numeric expressions. + /// + Numeric = 11, + + /// + /// Alphabetic (AL): letters and ordinary symbols that use alphabetic line breaking behavior. + /// + Alphabetic = 12, + + /// + /// Hebrew letter (HL): Hebrew characters with special hyphen and solidus behavior. + /// + HebrewLetter = 13, + + /// + /// Ideographic (ID): ideographic characters that generally allow breaks before or after. + /// + Ideographic = 14, + + /// + /// Inseparable (IN): characters, such as leaders, that allow only indirect line breaks between pairs. + /// + Inseparable = 15, + + /// + /// Hyphen (HY): hyphen-minus and similar characters that allow breaks after except in numeric context. + /// + Hyphen = 16, + + /// + /// Break after (BA): characters that generally provide a line break opportunity after themselves. + /// + BreakAfter = 17, + + /// + /// Break before (BB): characters that generally provide a line break opportunity before themselves. + /// + BreakBefore = 18, + + /// + /// Break before and after (B2): characters that allow breaks on either side, but not between two B2 characters. + /// + BreakBeforeAndAfter = 19, + + /// + /// Zero width space (ZW): an explicit opportunity for a line break. + /// + ZeroWidthSpace = 20, + + /// + /// Combining mark (CM): combining marks and related controls that stay with the preceding character. + /// + CombiningMark = 21, + + /// + /// Word joiner (WJ): characters that prohibit line breaks before and after themselves. + /// + WordJoiner = 22, + + /// + /// Hangul LV syllable (H2): part of the Hangul sequence classes used to form Korean syllable blocks. + /// + HangulLeadVowelSyllable = 23, + + /// + /// Hangul LVT syllable (H3): part of the Hangul sequence classes used to form Korean syllable blocks. + /// + HangulLeadVowelTailSyllable = 24, + + /// + /// Hangul L Jamo (JL): leading consonant Jamo used to form Korean syllable blocks. + /// + HangulLeadJamo = 25, + + /// + /// Hangul V Jamo (JV): vowel Jamo used to form Korean syllable blocks. + /// + HangulVowelJamo = 26, + + /// + /// Hangul T Jamo (JT): trailing consonant Jamo used to form Korean syllable blocks. + /// + HangulTailJamo = 27, + + /// + /// Regional indicator (RI): symbols paired for flag sequences. + /// + RegionalIndicator = 28, + + /// + /// Emoji base (EB): emoji characters that must not break from a following emoji modifier. + /// + EmojiBase = 29, + + /// + /// Emoji modifier (EM): emoji modifiers that must not break from a preceding emoji base. + /// + EmojiModifier = 30, + + /// + /// Zero width joiner (ZWJ): joiner control that prohibits breaks inside joiner sequences. + /// + ZeroWidthJoiner = 31, + + /// + /// Contingent break (CB): break opportunity determined by additional layout information. + /// + ContingentBreak = 32, + + /// + /// Ambiguous alphabetic or ideographic (AI): resolved by LB1 before rule evaluation. + /// + Ambiguous = 33, + + /// + /// Mandatory break (BK): characters that cause a line break after themselves. + /// + MandatoryBreak = 34, + + /// + /// Conditional Japanese starter (CJ): small Kana and related characters resolved by tailoring. + /// + ConditionalJapaneseStarter = 35, + + /// + /// Carriage return (CR): causes a line break after itself except in a CR LF sequence. + /// + CarriageReturn = 36, + + /// + /// Line feed (LF): causes a line break after itself. + /// + LineFeed = 37, + + /// + /// Next line (NL): causes a line break after itself. + /// + NextLine = 38, + + /// + /// Complex context dependent (SA): requires language-specific analysis for line break opportunities. + /// + ComplexContext = 39, + + /// + /// Surrogate (SG): surrogate code points, which do not occur in well-formed Unicode scalar text. + /// + Surrogate = 40, + + /// + /// Space (SP): enables indirect line breaks. + /// + Space = 41, + + /// + /// Aksara (AK): consonants that form orthographic syllables in Brahmic scripts. + /// + Aksara = 43, + + /// + /// Aksara pre-base (AP): pre-base signs, such as repha, that form Brahmic orthographic syllables. + /// + AksaraPrebase = 44, + + /// + /// Aksara start (AS): independent vowels and related starters for Brahmic orthographic syllables. + /// + AksaraStart = 45, + + /// + /// Unambiguous hyphen (HH): hyphen characters with unambiguous break-after behavior except word-initially. + /// + UnambiguousHyphen = 46, + + /// + /// Virama final (VF): final consonant viramas that form Brahmic orthographic syllables. + /// + ViramaFinal = 47, + + /// + /// Virama (VI): conjoining viramas that form Brahmic orthographic syllables. + /// + Virama = 48, + + /// + /// Unknown (XX): code points without an explicit line break class. + /// + Unknown = 0xFF, + } +} diff --git a/SixLabors.Fonts/Unicode/LineBreakEnumerator.cs b/SixLabors.Fonts/Unicode/LineBreakEnumerator.cs new file mode 100644 index 0000000..a5ddf46 --- /dev/null +++ b/SixLabors.Fonts/Unicode/LineBreakEnumerator.cs @@ -0,0 +1,2089 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts.Unicode { + /// + /// Enumerates potential line break opportunities for a span of text. + /// This is the engine behind the Unicode Line Breaking Algorithm as defined by + /// Unicode Standard Annex #14 (UAX #14): + /// . + /// The implementation keeps a three-code-point window over the input and applies + /// the LB rules in specification order. Each rule method is named after the + /// corresponding UAX #14 rule so the code can be reviewed against the standard. + /// + internal ref struct LineBreakEnumerator + { + /// + /// Sentinel value representing start of text for LB2 and context checks. + /// + private const int StartOfText = -1; + + /// + /// Sentinel value representing end of text for LB3. + /// + private const int EndOfText = -2; + + /// + /// U+25CC DOTTED CIRCLE. LB28a treats it as an aksara base for Indic conjunct handling. + /// + private const int DottedCircle = 0x25CC; + + /// + /// U+002F SOLIDUS. Used by layout-level URL tailoring around slash-separated path segments. + /// + private const int Solidus = 0x002F; + + /// + /// U+003A COLON. Used to recognize URI scheme markers. + /// + private const int Colon = 0x003A; + + /// + /// U+002E FULL STOP. Used to recognize www. host prefixes and URI scheme characters. + /// + private const int FullStop = 0x002E; + + /// + /// U+002D HYPHEN-MINUS. Valid inside URI schemes and host labels. + /// + private const int HyphenMinus = 0x002D; + + /// + /// U+00AD SOFT HYPHEN. It creates a manual hyphenation opportunity but is not rendered unless that break is chosen. + /// + private const int SoftHyphen = 0x00AD; + + /// + /// U+002B PLUS SIGN. Valid inside URI schemes. + /// + private const int PlusSign = 0x002B; + + /// + /// U+0057 LATIN CAPITAL LETTER W. Used by the ASCII www. recognizer. + /// + private const int UppercaseW = 0x0057; + + /// + /// U+0077 LATIN SMALL LETTER W. Used by the ASCII www. recognizer. + /// + private const int LowercaseW = 0x0077; + + /// + /// U+0022 QUOTATION MARK. Treated as a hard boundary while recognizing URL-like runs. + /// + private const int QuotationMark = 0x0022; + + /// + /// U+0027 APOSTROPHE. Treated as a hard boundary while recognizing URL-like runs. + /// + private const int Apostrophe = 0x0027; + + /// + /// U+003C LESS-THAN SIGN. Treated as a hard boundary while recognizing URL-like runs. + /// + private const int LessThanSign = 0x003C; + + /// + /// U+003E GREATER-THAN SIGN. Treated as a hard boundary while recognizing URL-like runs. + /// + private const int GreaterThanSign = 0x003E; + + /// + /// The UTF-16 source being inspected. The ref struct keeps this span without allocating. + /// + private readonly ReadOnlySpan source; + + /// + /// Enables layout-only URL tailoring. The public constructor leaves this disabled so Unicode + /// conformance tests see the un-tailored UAX #14 result. + /// + private readonly bool tailorUrls; + + /// + /// UTF-16 offset of the next code point to decode from . + /// + private int charPosition; + + /// + /// Code point index immediately after the last decoded code point. + /// + private int pointPosition; + + /// + /// Tracks whether the artificial end-of-text sentinel has been pushed into the rule window. + /// + private bool endOfTextPushed; + + /// + /// The last emitted wrap position. This prevents LB3 from emitting a duplicate final break. + /// + private int previousBreakPosition; + + /// + /// The code point immediately before in the rule window. + /// + private LineBreakCodePoint previous; + + /// + /// The left side of the boundary currently being evaluated. + /// + private LineBreakCodePoint current; + + /// + /// The right side of the boundary currently being evaluated. + /// + private LineBreakCodePoint next; + + /// + /// State for LB8. A zero width space followed by spaces permits a break after the space run. + /// + private bool lb8; + + /// + /// State shared by rules that suppress breaks across a following run of spaces. + /// + private bool spaces; + + /// + /// Count of consecutive regional-indicator pairs used by LB30a. + /// + private int regionalIndicatorCount; + + /// + /// Streaming recognizer state used only when is enabled. + /// + private UrlTailoringState urlTailoringState; + + /// + /// Initializes a new instance of the struct. + /// + /// The source text to inspect for UAX #14 break opportunities. + public LineBreakEnumerator(ReadOnlySpan source) + : this(source, false) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The source text to inspect for line break opportunities. + /// Whether to apply layout-level URL solidus tailoring. + internal LineBreakEnumerator(ReadOnlySpan source, bool tailorUrls) + : this() + { + this.source = source; + this.tailorUrls = tailorUrls; + this.previous = LineBreakCodePoint.CreateSentinel(StartOfText, 0, 0); + this.current = LineBreakCodePoint.CreateSentinel(StartOfText, 0, 0); + this.next = LineBreakCodePoint.CreateSentinel(StartOfText, 0, 0); + } + + private enum BreakAction + { + /// + /// The rule did not apply; continue evaluating later rules. + /// + Pass, + + /// + /// The rule forbids a break at the current boundary. + /// + NoBreak, + + /// + /// The rule permits an optional break at the current boundary. + /// + MayBreak, + + /// + /// The rule requires a break at the current boundary. + /// + MustBreak + } + + /// + /// Gets the most recently discovered line break opportunity. + /// + public LineBreak Current { get; private set; } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An enumerator that iterates through the collection. + public readonly LineBreakEnumerator GetEnumerator() => this; + + /// + /// Advances the enumerator to the next element of the collection. + /// + /// + /// if the enumerator was successfully advanced to the next element; + /// if the enumerator has passed the end of the collection. + /// + public bool MoveNext() + { + while (true) + { + if (this.charPosition < this.source.Length) + { + this.Push(this.ReadNext()); + } + else if (!this.endOfTextPushed) + { + this.Push(LineBreakCodePoint.CreateSentinel(EndOfText, this.next.Length, this.next.CharEnd)); + this.endOfTextPushed = true; + } + else + { + this.Current = default; + return false; + } + + BreakAction action = this.GetBreakAction(); + if (this.tailorUrls) + { + action = this.ApplyUrlTailoring(action); + } + + switch (action) + { + case BreakAction.NoBreak: + case BreakAction.Pass: + break; + + case BreakAction.MayBreak: + case BreakAction.MustBreak: + this.Current = new LineBreak( + this.FindPriorNonWhitespace(this.current), + this.current.Length, + action == BreakAction.MustBreak, + this.current.HasValue(SoftHyphen)); + this.previousBreakPosition = this.current.Length; + return true; + + default: + throw new InvalidOperationException($"Invalid line break action {action}."); + } + } + } + + /// + /// Decodes the next UTF-16 code point, maps its line break class according to LB1, + /// and packages the additional context needed by later rules. + /// + private LineBreakCodePoint ReadNext() + { + int charStart = this.charPosition; + CodePoint codePoint = CodePoint.DecodeFromUtf16At(this.source, charStart, out int charsConsumed); + UnicodeCategory category = CodePoint.GetGeneralCategory(codePoint); + LineBreakClass cls = MapClass(CodePoint.GetLineBreakClass(codePoint), category); + bool isUrlLikeRun = this.tailorUrls && this.urlTailoringState.Update(codePoint); + + this.charPosition += charsConsumed; + this.pointPosition++; + + return new LineBreakCodePoint( + codePoint, + cls, + category, + this.pointPosition, + charStart, + this.charPosition, + isUrlLikeRun); + } + + /// + /// Applies the LB1 class remapping required before any rule decisions are made. + /// + /// + /// LB1 resolves ambiguous, surrogate, unknown, complex-context, and conditional Japanese + /// starter classes before the rest of the rule chain observes them: + /// AI/SG/XX to AL, SA to CM or AL based on general category, and CJ to NS. + /// + private static LineBreakClass MapClass(LineBreakClass c, UnicodeCategory category) + => c switch + { + LineBreakClass.Ambiguous or LineBreakClass.Surrogate or LineBreakClass.Unknown => LineBreakClass.Alphabetic, + LineBreakClass.ComplexContext => category is UnicodeCategory.NonSpacingMark or UnicodeCategory.SpacingCombiningMark + ? LineBreakClass.CombiningMark + : LineBreakClass.Alphabetic, + LineBreakClass.ConditionalJapaneseStarter => LineBreakClass.Nonstarter, + _ => c + }; + + /// + /// Applies the layout URL tailoring from UAX #14 section 8 while preserving the default + /// enumerator behavior for callers that need strict Unicode conformance. + /// + /// + /// The tailoring suppresses ordinary layout breaks at a solidus unless the current token has + /// already been recognized as URL-like. It also adds the URL numeric path case that default + /// LB25 intentionally blocks, for example the boundary after 2024/ in + /// https://example/2024/05. + /// + private readonly BreakAction ApplyUrlTailoring(BreakAction action) + { + if (action == BreakAction.MustBreak || this.current.IsSentinel) + { + return action; + } + + if (this.next.HasValue(Solidus)) + { + return BreakAction.NoBreak; + } + + if (!this.current.HasValue(Solidus)) + { + return action; + } + + if (!this.current.IsUrlLikeRun) + { + return BreakAction.NoBreak; + } + + if (action == BreakAction.NoBreak + && !this.previous.IsSentinel + && !this.next.IsSentinel + && CodePoint.IsDigit(this.previous.CodePoint) + && CodePoint.IsDigit(this.next.CodePoint)) + { + return BreakAction.MayBreak; + } + + return action; + } + + /// + /// Advances the three-code-point rule window. Ignored combining marks and zero width joiners + /// from LB9 are folded into the current position instead of becoming a new boundary. + /// + private void Push(LineBreakCodePoint codePoint) + { + if (this.next.Ignored) + { + this.current.Length = this.next.Length; + this.current.CharEnd = this.next.CharEnd; + } + else + { + this.previous = this.current; + this.current = this.next; + } + + this.next = codePoint; + } + + /// + /// Evaluates the UAX #14 rules in order for the boundary between + /// and . + /// + /// + /// The first rule to return anything other than decides the + /// boundary. If no rule prevents a break, LB31 is represented by the final + /// return. + /// + private BreakAction GetBreakAction() + { + BreakAction action; + + action = this.LB02(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB03(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB04(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB05(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB06(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LBSpacesStop(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB07(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB08(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB08a(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB09(); + if (action != BreakAction.Pass) + { + return action; + } + + this.LB10(); + + action = this.LB11(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB12(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB12a(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB13(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB14(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB15a(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB15b(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB15c(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB15d(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB16(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB17(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB18(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB19(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB19a(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB20(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB20a(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB21a(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB21(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB21b(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB22(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB23(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB23a(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB24(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB25(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB26(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB27(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB28(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB28a(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB29(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB30(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB30a(); + if (action != BreakAction.Pass) + { + return action; + } + + action = this.LB30b(); + if (action != BreakAction.Pass) + { + return action; + } + + // LB31: Break everywhere else. + return BreakAction.MayBreak; + } + + /// + /// LB2: Never break at the start of text. + /// + private readonly BreakAction LB02() + => this.current.IsStartOfText && !this.next.IsEndOfText + ? BreakAction.NoBreak + : BreakAction.Pass; + + /// + /// LB3: Always break at the end of text. + /// + private readonly BreakAction LB03() + => this.next.IsEndOfText && (this.current.Length == 0 || this.current.Length != this.previousBreakPosition) + ? BreakAction.MayBreak + : BreakAction.Pass; + + /// + /// LB4: Always break after a mandatory break character. + /// + private readonly BreakAction LB04() + => this.current.Is(LineBreakClass.MandatoryBreak) ? BreakAction.MustBreak : BreakAction.Pass; + + /// + /// LB5: Treat CR followed by LF as an indivisible newline; otherwise break after CR, LF, and NL. + /// + private readonly BreakAction LB05() + { + if (this.current.Is(LineBreakClass.CarriageReturn)) + { + return this.next.Is(LineBreakClass.LineFeed) ? BreakAction.NoBreak : BreakAction.MustBreak; + } + + return this.current.Is(LineBreakClass.LineFeed) || this.current.Is(LineBreakClass.NextLine) + ? BreakAction.MustBreak + : BreakAction.Pass; + } + + /// + /// LB6: Do not break before mandatory break characters. + /// + private readonly BreakAction LB06() + => this.next.Is(LineBreakClass.MandatoryBreak) + || this.next.Is(LineBreakClass.CarriageReturn) + || this.next.Is(LineBreakClass.LineFeed) + || this.next.Is(LineBreakClass.NextLine) + ? BreakAction.NoBreak + : BreakAction.Pass; + + /// + /// Internal space-run handling for rules that suppress a break until after spaces have been consumed. + /// + /// + /// This is not a standalone UAX rule. It carries the "do not break inside the intervening spaces" + /// part of LB8, LB14, LB15a, LB16, and LB17 after the rule that started the space run has fired. + /// It also resets the LB30a regional-indicator count whenever the current code point is not RI. + /// + private BreakAction LBSpacesStop() + { + if (!this.current.Is(LineBreakClass.RegionalIndicator)) + { + this.regionalIndicatorCount = 0; + } + + if (this.spaces) + { + if (!this.next.Is(LineBreakClass.Space)) + { + this.spaces = false; + } + + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB7: Do not break before spaces or zero width space. + /// + /// + /// The exceptions for ZW, OP, QU, CL, CP, and B2 are handled by their later dedicated rules. + /// + private readonly BreakAction LB07() + { + if (this.next.Is(LineBreakClass.ZeroWidthSpace)) + { + return BreakAction.NoBreak; + } + + if (this.next.Is(LineBreakClass.Space) + && !this.current.Is(LineBreakClass.ZeroWidthSpace) + && !this.current.Is(LineBreakClass.OpenPunctuation) + && !this.current.Is(LineBreakClass.Quotation) + && !this.current.Is(LineBreakClass.ClosePunctuation) + && !this.current.Is(LineBreakClass.CloseParenthesis) + && !this.current.Is(LineBreakClass.BreakBeforeAndAfter)) + { + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB8: Break before any character following a zero width space, even if spaces intervene. + /// + private BreakAction LB08() + { + if (this.lb8) + { + this.lb8 = false; + return BreakAction.MayBreak; + } + + if (this.current.Is(LineBreakClass.ZeroWidthSpace)) + { + if (this.next.Is(LineBreakClass.Space)) + { + this.lb8 = true; + return BreakAction.NoBreak; + } + + return BreakAction.MayBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB8a: Do not break after a zero width joiner. + /// + private readonly BreakAction LB08a() + => this.current.Is(LineBreakClass.ZeroWidthJoiner) ? BreakAction.NoBreak : BreakAction.Pass; + + /// + /// LB9: Do not break a combining mark or zero width joiner away from its base character. + /// + /// + /// When LB9 applies, the right-side code point is marked as ignored so + /// folds it into the current logical position and later boundaries see the combined item. + /// + private BreakAction LB09() + { + if (!IsBkCrLfNlSpZw(this.current) + && (this.next.Is(LineBreakClass.CombiningMark) || this.next.Is(LineBreakClass.ZeroWidthJoiner))) + { + this.next.Ignored = true; + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB10: Treat any remaining combining marks or zero width joiners as alphabetic. + /// + /// + /// LB10 is a class rewrite rather than a boundary decision, so it does not return a + /// . + /// + private void LB10() + { + if (this.current.Is(LineBreakClass.CombiningMark) || this.current.Is(LineBreakClass.ZeroWidthJoiner)) + { + this.current.Class = LineBreakClass.Alphabetic; + } + + if (this.next.Is(LineBreakClass.CombiningMark) || this.next.Is(LineBreakClass.ZeroWidthJoiner)) + { + this.next.Class = LineBreakClass.Alphabetic; + } + } + + /// + /// LB11: Do not break before or after word joiner. + /// + private readonly BreakAction LB11() + => this.next.Is(LineBreakClass.WordJoiner) || this.current.Is(LineBreakClass.WordJoiner) + ? BreakAction.NoBreak + : BreakAction.Pass; + + /// + /// LB12: Do not break after a glue character. + /// + private readonly BreakAction LB12() + => this.current.Is(LineBreakClass.Glue) ? BreakAction.NoBreak : BreakAction.Pass; + + /// + /// LB12a: Do not break before a glue character except after spaces, break-after, hyphen, or Hebrew hyphen. + /// + private readonly BreakAction LB12a() + { + if (this.next.Is(LineBreakClass.Glue) + && !this.current.Is(LineBreakClass.Space) + && !this.current.Is(LineBreakClass.BreakAfter) + && !this.current.Is(LineBreakClass.Hyphen) + && !this.current.Is(LineBreakClass.UnambiguousHyphen)) + { + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB13: Do not break before closing punctuation, closing parenthesis, exclamation/interrogation, + /// or inseparable symbols. + /// + private readonly BreakAction LB13() + => this.next.Is(LineBreakClass.ClosePunctuation) + || this.next.Is(LineBreakClass.CloseParenthesis) + || this.next.Is(LineBreakClass.Exclamation) + || this.next.Is(LineBreakClass.BreakSymbols) + ? BreakAction.NoBreak + : BreakAction.Pass; + + /// + /// LB14: Do not break after an opening punctuation, even after intervening spaces. + /// + private BreakAction LB14() + { + if (this.current.Is(LineBreakClass.OpenPunctuation)) + { + if (this.next.Is(LineBreakClass.Space)) + { + this.spaces = true; + } + + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB15a: Do not break after an initial quotation mark following a start-like context, + /// even after intervening spaces. + /// + private BreakAction LB15a() + { + if (IsSotBkCrLfNlOpQuGlSpZw(this.previous) + && this.current.Is(LineBreakClass.Quotation) + && this.current.Category == UnicodeCategory.InitialQuotePunctuation) + { + this.spaces = true; + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB15b: Do not break before a final quotation mark when it closes a quotation-like run. + /// + private readonly BreakAction LB15b() + { + if (this.next.Is(LineBreakClass.Quotation) + && this.next.Category == UnicodeCategory.FinalQuotePunctuation) + { + if (!this.TryGetAfterNext(out LineBreakCodePoint after) || IsSpGlWjClQuCpExIsSyBkCrLfNlZw(after)) + { + return BreakAction.NoBreak; + } + } + + return BreakAction.Pass; + } + + /// + /// LB15c: Permit a break between a space and an inseparable separator before a number. + /// + private readonly BreakAction LB15c() + { + if (this.current.Is(LineBreakClass.Space) + && this.next.Is(LineBreakClass.InfixNumeric) + && this.TryGetAfterNext(out LineBreakCodePoint after) + && after.Is(LineBreakClass.Numeric)) + { + return BreakAction.MayBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB15d: Do not break before inseparable separators in other contexts. + /// + private readonly BreakAction LB15d() + => this.next.Is(LineBreakClass.InfixNumeric) ? BreakAction.NoBreak : BreakAction.Pass; + + /// + /// LB16: Do not break between closing punctuation or closing parenthesis and a nonstarter, + /// even with intervening spaces. + /// + private BreakAction LB16() + { + if (this.current.Is(LineBreakClass.ClosePunctuation) || this.current.Is(LineBreakClass.CloseParenthesis)) + { + if (this.ClassAfterSpacesIs(this.current.CharEnd, LineBreakClass.Nonstarter)) + { + if (this.next.Is(LineBreakClass.Space)) + { + this.spaces = true; + } + + return BreakAction.NoBreak; + } + + if (this.next.Is(LineBreakClass.Space)) + { + return BreakAction.NoBreak; + } + } + + return BreakAction.Pass; + } + + /// + /// LB17: Do not break within balanced punctuation pairs, even with intervening spaces. + /// + private BreakAction LB17() + { + if (this.current.Is(LineBreakClass.BreakBeforeAndAfter)) + { + if (this.ClassAfterSpacesIs(this.current.CharEnd, LineBreakClass.BreakBeforeAndAfter)) + { + if (!this.next.Is(LineBreakClass.Space)) + { + return BreakAction.NoBreak; + } + + this.spaces = true; + return BreakAction.NoBreak; + } + + if (this.next.Is(LineBreakClass.Space)) + { + return BreakAction.NoBreak; + } + } + + return BreakAction.Pass; + } + + /// + /// LB18: Break after spaces. + /// + private readonly BreakAction LB18() + => this.current.Is(LineBreakClass.Space) ? BreakAction.MayBreak : BreakAction.Pass; + + /// + /// LB19: Do not break before or after quotation marks. + /// + /// + /// Initial and final quotation categories are handled by LB15a and LB15b where the standard + /// gives them more specific behavior. + /// + private readonly BreakAction LB19() + { + if (this.next.Is(LineBreakClass.Quotation) && this.next.Category != UnicodeCategory.InitialQuotePunctuation) + { + return BreakAction.NoBreak; + } + + if (this.current.Is(LineBreakClass.Quotation) && this.current.Category != UnicodeCategory.FinalQuotePunctuation) + { + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB19a: Applies the East Asian quotation mark tailoring used by the Unicode line break tests. + /// + private readonly BreakAction LB19a() + { + if (!IsEastAsian(this.current) && this.next.Is(LineBreakClass.Quotation)) + { + return BreakAction.NoBreak; + } + + if (this.next.Is(LineBreakClass.Quotation) + && (!this.TryGetAfterNext(out LineBreakCodePoint after) || !IsEastAsian(after))) + { + return BreakAction.NoBreak; + } + + if (this.current.Is(LineBreakClass.Quotation) && !IsEastAsian(this.next)) + { + return BreakAction.NoBreak; + } + + if ((this.previous.IsStartOfText || !IsEastAsian(this.previous)) + && this.current.Is(LineBreakClass.Quotation)) + { + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB20: Break before and after contingent break characters. + /// + private readonly BreakAction LB20() + => this.current.Is(LineBreakClass.ContingentBreak) || this.next.Is(LineBreakClass.ContingentBreak) + ? BreakAction.MayBreak + : BreakAction.Pass; + + /// + /// LB20a: Do not break after a leading hyphen or Hebrew hyphen before alphabetic text. + /// + private readonly BreakAction LB20a() + { + if (IsSotBkCrLfNlSpZwCbGl(this.previous) + && (this.current.Is(LineBreakClass.Hyphen) || this.current.Is(LineBreakClass.UnambiguousHyphen)) + && (this.next.Is(LineBreakClass.Alphabetic) || this.next.Is(LineBreakClass.HebrewLetter))) + { + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB21: Do not break before break-after, hyphen, Hebrew hyphen, or nonstarter; + /// do not break after break-before. + /// + private readonly BreakAction LB21() + { + if (this.current.Is(LineBreakClass.BreakBefore) + || this.next.Is(LineBreakClass.BreakAfter) + || this.next.Is(LineBreakClass.UnambiguousHyphen) + || this.next.Is(LineBreakClass.Hyphen) + || this.next.Is(LineBreakClass.Nonstarter)) + { + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB21a: Do not break after Hebrew letters followed by hyphen or Hebrew hyphen. + /// + private readonly BreakAction LB21a() + { + if (this.previous.Is(LineBreakClass.HebrewLetter) + && (this.current.Is(LineBreakClass.Hyphen) || this.current.Is(LineBreakClass.UnambiguousHyphen)) + && !this.next.Is(LineBreakClass.HebrewLetter)) + { + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB21b: Do not break between solidus-like symbols and Hebrew letters. + /// + private readonly BreakAction LB21b() + => this.current.Is(LineBreakClass.BreakSymbols) && this.next.Is(LineBreakClass.HebrewLetter) + ? BreakAction.NoBreak + : BreakAction.Pass; + + /// + /// LB22: Do not break before ellipses and other inseparable characters. + /// + private readonly BreakAction LB22() + => this.next.Is(LineBreakClass.Inseparable) ? BreakAction.NoBreak : BreakAction.Pass; + + /// + /// LB23: Do not break between letters and numbers. + /// + private readonly BreakAction LB23() + { + if ((this.current.Is(LineBreakClass.Alphabetic) || this.current.Is(LineBreakClass.HebrewLetter)) + && this.next.Is(LineBreakClass.Numeric)) + { + return BreakAction.NoBreak; + } + + if (this.current.Is(LineBreakClass.Numeric) + && (this.next.Is(LineBreakClass.Alphabetic) || this.next.Is(LineBreakClass.HebrewLetter))) + { + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB23a: Do not break between numeric prefixes or postfixes and ideographs, emoji bases, + /// or emoji modifiers. + /// + private readonly BreakAction LB23a() + { + if (this.current.Is(LineBreakClass.PrefixNumeric) && IsIdEbEm(this.next)) + { + return BreakAction.NoBreak; + } + + if (this.next.Is(LineBreakClass.PostfixNumeric) && IsIdEbEm(this.current)) + { + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB24: Do not break between numeric prefixes or postfixes and alphabetic letters. + /// + private readonly BreakAction LB24() + { + if ((this.current.Is(LineBreakClass.PrefixNumeric) || this.current.Is(LineBreakClass.PostfixNumeric)) + && (this.next.Is(LineBreakClass.Alphabetic) || this.next.Is(LineBreakClass.HebrewLetter))) + { + return BreakAction.NoBreak; + } + + if ((this.current.Is(LineBreakClass.Alphabetic) || this.current.Is(LineBreakClass.HebrewLetter)) + && (this.next.Is(LineBreakClass.PrefixNumeric) || this.next.Is(LineBreakClass.PostfixNumeric))) + { + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB25: Do not break within numeric expressions. + /// + /// + /// This implementation covers the multi-code-point contexts from the rule by looking backward + /// across SY/IS separators and forward across optional opening punctuation. The layout URL + /// tailoring can later reintroduce a narrow solidus break inside recognized URL path segments. + /// + private readonly BreakAction LB25() + { + bool hasNumericScanEnd = false; + int numericScanCharEnd = 0; + if (this.next.Is(LineBreakClass.PostfixNumeric) || this.next.Is(LineBreakClass.PrefixNumeric)) + { + numericScanCharEnd = this.current.Is(LineBreakClass.ClosePunctuation) || this.current.Is(LineBreakClass.CloseParenthesis) + ? this.previous.CharEnd + : this.current.CharEnd; + hasNumericScanEnd = true; + } + else if (this.next.Is(LineBreakClass.Numeric)) + { + numericScanCharEnd = this.current.CharEnd; + hasNumericScanEnd = true; + } + + if (hasNumericScanEnd) + { + int scanCharEnd = numericScanCharEnd; + while (this.TryReadBackward(scanCharEnd, out LineBreakCodePoint codePoint)) + { + if (codePoint.Is(LineBreakClass.BreakSymbols) || codePoint.Is(LineBreakClass.InfixNumeric)) + { + scanCharEnd = codePoint.CharStart; + continue; + } + + if (codePoint.Is(LineBreakClass.Numeric)) + { + return BreakAction.NoBreak; + } + + break; + } + } + + if (this.current.Is(LineBreakClass.PostfixNumeric) || this.current.Is(LineBreakClass.PrefixNumeric)) + { + if (this.next.Is(LineBreakClass.OpenPunctuation)) + { + if (this.TryGetAfterNext(out LineBreakCodePoint after)) + { + if (after.Is(LineBreakClass.Numeric)) + { + return BreakAction.NoBreak; + } + + if (after.Is(LineBreakClass.InfixNumeric) + && this.TryGetAfterNext(out LineBreakCodePoint afterAfter, 2) + && afterAfter.Is(LineBreakClass.Numeric)) + { + return BreakAction.NoBreak; + } + } + } + else if (this.next.Is(LineBreakClass.Numeric)) + { + return BreakAction.NoBreak; + } + } + + if (this.current.Is(LineBreakClass.Hyphen) && this.next.Is(LineBreakClass.Numeric)) + { + return BreakAction.NoBreak; + } + + if (this.current.Is(LineBreakClass.InfixNumeric) && this.next.Is(LineBreakClass.Numeric)) + { + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB26: Do not break a Korean syllable block. + /// + private readonly BreakAction LB26() + { + if (this.current.Is(LineBreakClass.HangulLeadJamo) && IsJlJvH2H3(this.next)) + { + return BreakAction.NoBreak; + } + + if ((this.current.Is(LineBreakClass.HangulVowelJamo) || this.current.Is(LineBreakClass.HangulLeadVowelSyllable)) + && (this.next.Is(LineBreakClass.HangulVowelJamo) || this.next.Is(LineBreakClass.HangulTailJamo))) + { + return BreakAction.NoBreak; + } + + if ((this.current.Is(LineBreakClass.HangulTailJamo) || this.current.Is(LineBreakClass.HangulLeadVowelTailSyllable)) + && this.next.Is(LineBreakClass.HangulTailJamo)) + { + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB27: Treat Korean syllable blocks like ideographs for numeric prefix and postfix handling. + /// + private readonly BreakAction LB27() + { + if (IsJlJvJtH2H3(this.current) && this.next.Is(LineBreakClass.PostfixNumeric)) + { + return BreakAction.NoBreak; + } + + if (this.current.Is(LineBreakClass.PrefixNumeric) && IsJlJvJtH2H3(this.next)) + { + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB28: Do not break between alphabetic letters. + /// + private readonly BreakAction LB28() + => (this.current.Is(LineBreakClass.Alphabetic) || this.current.Is(LineBreakClass.HebrewLetter)) + && (this.next.Is(LineBreakClass.Alphabetic) || this.next.Is(LineBreakClass.HebrewLetter)) + ? BreakAction.NoBreak + : BreakAction.Pass; + + /// + /// LB28a: Do not break inside orthographic syllables for Brahmic scripts. + /// + /// + /// This is the Unicode 15+ aksara rule family. It keeps aksara bases, viramas, invisible + /// stackers, and following bases together, with U+25CC DOTTED CIRCLE treated as a base. + /// + private readonly BreakAction LB28a() + { + if (this.current.Is(LineBreakClass.AksaraPrebase) && IsAksaraBase(this.next)) + { + return BreakAction.NoBreak; + } + + if (IsAksaraBase(this.current) + && (this.next.Is(LineBreakClass.ViramaFinal) || this.next.Is(LineBreakClass.Virama))) + { + return BreakAction.NoBreak; + } + + if (IsAksaraBase(this.previous) + && this.current.Is(LineBreakClass.Virama) + && (this.next.Is(LineBreakClass.Aksara) || this.next.CodePoint.Value == DottedCircle)) + { + return BreakAction.NoBreak; + } + + if (IsAksaraBase(this.current) + && IsAksaraBase(this.next) + && this.TryGetAfterNext(out LineBreakCodePoint after) + && after.Is(LineBreakClass.ViramaFinal)) + { + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB29: Do not break between numeric punctuation and alphabetic letters. + /// + private readonly BreakAction LB29() + => this.current.Is(LineBreakClass.InfixNumeric) + && (this.next.Is(LineBreakClass.Alphabetic) || this.next.Is(LineBreakClass.HebrewLetter)) + ? BreakAction.NoBreak + : BreakAction.Pass; + + /// + /// LB30: Do not break between letters or numbers and non-East-Asian opening or closing punctuation. + /// + private readonly BreakAction LB30() + { + if ((this.current.Is(LineBreakClass.Alphabetic) + || this.current.Is(LineBreakClass.HebrewLetter) + || this.current.Is(LineBreakClass.Numeric)) + && this.next.Is(LineBreakClass.OpenPunctuation) + && !IsEastAsian(this.next)) + { + return BreakAction.NoBreak; + } + + if (this.current.Is(LineBreakClass.CloseParenthesis) + && !IsEastAsian(this.current) + && (this.next.Is(LineBreakClass.Alphabetic) + || this.next.Is(LineBreakClass.HebrewLetter) + || this.next.Is(LineBreakClass.Numeric))) + { + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// LB30a: Break between regional indicator symbols only at even boundaries. + /// + /// + /// This keeps flag emoji pairs together by forbidding the first RI/RI boundary in each run and + /// allowing the next one. + /// + private BreakAction LB30a() + { + if (this.current.Is(LineBreakClass.RegionalIndicator) && this.next.Is(LineBreakClass.RegionalIndicator)) + { + this.regionalIndicatorCount++; + if (this.regionalIndicatorCount % 2 != 0) + { + return BreakAction.NoBreak; + } + } + + return BreakAction.Pass; + } + + /// + /// LB30b: Do not break between emoji base characters and emoji modifiers. + /// + private readonly BreakAction LB30b() + { + if (this.current.Is(LineBreakClass.EmojiBase) && this.next.Is(LineBreakClass.EmojiModifier)) + { + return BreakAction.NoBreak; + } + + if (this.next.Is(LineBreakClass.EmojiModifier) + && this.current.Category == UnicodeCategory.OtherNotAssigned + && CodePoint.GetGraphemeClusterClass(this.current.CodePoint) == GraphemeClusterClass.ExtendedPictographic) + { + return BreakAction.NoBreak; + } + + return BreakAction.Pass; + } + + /// + /// Scans forward from over spaces and checks the first non-space class. + /// + /// + /// LB16 and LB17 both have "with intervening spaces" forms. This helper performs that lookahead + /// without advancing the streaming enumerator state. + /// + private readonly bool ClassAfterSpacesIs(int charIndex, LineBreakClass cls) + { + int scanChar = charIndex; + int scanLength = this.current.Length; + + while (this.TryReadForward(scanChar, scanLength, out LineBreakCodePoint codePoint)) + { + if (!codePoint.Is(LineBreakClass.Space)) + { + return codePoint.Is(cls); + } + + scanChar = codePoint.CharEnd; + scanLength = codePoint.Length; + } + + return false; + } + + /// + /// Reads a code point after without advancing the enumerator. + /// + /// The decoded lookahead code point. + /// The number of code points after to inspect. + /// when the requested lookahead exists. + private readonly bool TryGetAfterNext(out LineBreakCodePoint codePoint, int offset = 1) + { + codePoint = default; + int scanChar = this.next.CharEnd; + int scanLength = this.next.Length; + + for (int i = 0; i < offset; i++) + { + if (!this.TryReadForward(scanChar, scanLength, out codePoint)) + { + return false; + } + + scanChar = codePoint.CharEnd; + scanLength = codePoint.Length; + } + + return true; + } + + /// + /// Decodes a code point at without advancing the enumerator. + /// + /// The UTF-16 index to decode from. + /// The code point length to assign to the decoded lookahead item. + /// The decoded lookahead code point. + /// when a code point was available. + private readonly bool TryReadForward(int charIndex, int length, out LineBreakCodePoint codePoint) + { + if (!this.next.IsSentinel && charIndex == this.next.CharStart) + { + codePoint = this.next; + return true; + } + + if (charIndex >= this.source.Length) + { + codePoint = default; + return false; + } + + CodePoint cp = CodePoint.DecodeFromUtf16At(this.source, charIndex, out int charsConsumed); + UnicodeCategory category = CodePoint.GetGeneralCategory(cp); + LineBreakClass cls = MapClass(CodePoint.GetLineBreakClass(cp), category); + + codePoint = new LineBreakCodePoint( + cp, + cls, + category, + length + 1, + charIndex, + charIndex + charsConsumed); + return true; + } + + /// + /// Decodes the code point ending at without moving the stream. + /// + /// + /// Most callers hit the already-buffered , , or + /// entries. Decoding from source is the fallback for longer LB25 or trimming + /// scans and still does not allocate. + /// + /// The UTF-16 index immediately after the code point to read. + /// The decoded lookbehind code point. + /// when a code point was available. + private readonly bool TryReadBackward(int charEnd, out LineBreakCodePoint codePoint) + { + if (!this.current.IsSentinel && charEnd == this.current.CharEnd) + { + codePoint = this.current; + return true; + } + + if (!this.previous.IsSentinel && charEnd == this.previous.CharEnd) + { + codePoint = this.previous; + return true; + } + + if (!this.next.IsSentinel && charEnd == this.next.CharEnd) + { + codePoint = this.next; + return true; + } + + if (charEnd <= 0) + { + codePoint = default; + return false; + } + + int charStart = charEnd - 1; + if (charStart > 0 + && char.IsLowSurrogate(this.source[charStart]) + && char.IsHighSurrogate(this.source[charStart - 1])) + { + charStart--; + } + + CodePoint cp = CodePoint.DecodeFromUtf16At(this.source, charStart, out int _); + UnicodeCategory category = CodePoint.GetGeneralCategory(cp); + LineBreakClass cls = MapClass(CodePoint.GetLineBreakClass(cp), category); + + codePoint = new LineBreakCodePoint(cp, cls, category, 0, charStart, charEnd); + return true; + } + + /// + /// Walks backward from a wrap position to the nearest non-breaking trailing content so that + /// measurement excludes trailing spaces and hard line terminators while wrapping still occurs + /// at the original boundary. + /// + private readonly int FindPriorNonWhitespace(LineBreakCodePoint from) + { + int measure = from.Length; + int charEnd = from.CharEnd; + + if (this.TryReadBackward(charEnd, out LineBreakCodePoint codePoint) + && (codePoint.Is(LineBreakClass.MandatoryBreak) || codePoint.Is(LineBreakClass.LineFeed) || codePoint.Is(LineBreakClass.CarriageReturn))) + { + measure--; + charEnd = codePoint.CharStart; + } + + while (this.TryReadBackward(charEnd, out codePoint)) + { + if (codePoint.Is(LineBreakClass.Space)) + { + measure--; + charEnd = codePoint.CharStart; + } + else + { + break; + } + } + + return measure; + } + + /// + /// Checks the class exclusions used by LB9 before combining marks are folded into their base. + /// + private static bool IsBkCrLfNlSpZw(LineBreakCodePoint codePoint) + => codePoint.Is(LineBreakClass.MandatoryBreak) + || codePoint.Is(LineBreakClass.CarriageReturn) + || codePoint.Is(LineBreakClass.LineFeed) + || codePoint.Is(LineBreakClass.NextLine) + || codePoint.Is(LineBreakClass.Space) + || codePoint.Is(LineBreakClass.ZeroWidthSpace); + + /// + /// Checks the start-like contexts that allow LB15a initial quotation handling. + /// + private static bool IsSotBkCrLfNlOpQuGlSpZw(LineBreakCodePoint codePoint) + => codePoint.IsStartOfText + || codePoint.Is(LineBreakClass.MandatoryBreak) + || codePoint.Is(LineBreakClass.CarriageReturn) + || codePoint.Is(LineBreakClass.LineFeed) + || codePoint.Is(LineBreakClass.NextLine) + || codePoint.Is(LineBreakClass.OpenPunctuation) + || codePoint.Is(LineBreakClass.Quotation) + || codePoint.Is(LineBreakClass.Glue) + || codePoint.Is(LineBreakClass.Space) + || codePoint.Is(LineBreakClass.ZeroWidthSpace); + + /// + /// Checks the classes that may follow a final quotation mark for LB15b. + /// + private static bool IsSpGlWjClQuCpExIsSyBkCrLfNlZw(LineBreakCodePoint codePoint) + => codePoint.Is(LineBreakClass.Space) + || codePoint.Is(LineBreakClass.Glue) + || codePoint.Is(LineBreakClass.WordJoiner) + || codePoint.Is(LineBreakClass.ClosePunctuation) + || codePoint.Is(LineBreakClass.Quotation) + || codePoint.Is(LineBreakClass.CloseParenthesis) + || codePoint.Is(LineBreakClass.Exclamation) + || codePoint.Is(LineBreakClass.InfixNumeric) + || codePoint.Is(LineBreakClass.BreakSymbols) + || codePoint.Is(LineBreakClass.MandatoryBreak) + || codePoint.Is(LineBreakClass.CarriageReturn) + || codePoint.Is(LineBreakClass.LineFeed) + || codePoint.Is(LineBreakClass.NextLine) + || codePoint.Is(LineBreakClass.ZeroWidthSpace); + + /// + /// Checks the leading contexts used by LB20a for hyphenated words. + /// + private static bool IsSotBkCrLfNlSpZwCbGl(LineBreakCodePoint codePoint) + => codePoint.IsStartOfText + || codePoint.Is(LineBreakClass.MandatoryBreak) + || codePoint.Is(LineBreakClass.CarriageReturn) + || codePoint.Is(LineBreakClass.LineFeed) + || codePoint.Is(LineBreakClass.NextLine) + || codePoint.Is(LineBreakClass.Space) + || codePoint.Is(LineBreakClass.ZeroWidthSpace) + || codePoint.Is(LineBreakClass.ContingentBreak) + || codePoint.Is(LineBreakClass.Glue); + + /// + /// Checks the ideographic and emoji classes that participate in LB23a and LB27. + /// + private static bool IsIdEbEm(LineBreakCodePoint codePoint) + => codePoint.Is(LineBreakClass.Ideographic) + || codePoint.Is(LineBreakClass.EmojiBase) + || codePoint.Is(LineBreakClass.EmojiModifier); + + /// + /// Checks the Hangul classes allowed after a leading jamo for LB26. + /// + private static bool IsJlJvH2H3(LineBreakCodePoint codePoint) + => codePoint.Is(LineBreakClass.HangulLeadJamo) + || codePoint.Is(LineBreakClass.HangulVowelJamo) + || codePoint.Is(LineBreakClass.HangulLeadVowelSyllable) + || codePoint.Is(LineBreakClass.HangulLeadVowelTailSyllable); + + /// + /// Checks the Hangul syllable-block classes used by LB27. + /// + private static bool IsJlJvJtH2H3(LineBreakCodePoint codePoint) + => codePoint.Is(LineBreakClass.HangulLeadJamo) + || codePoint.Is(LineBreakClass.HangulVowelJamo) + || codePoint.Is(LineBreakClass.HangulTailJamo) + || codePoint.Is(LineBreakClass.HangulLeadVowelSyllable) + || codePoint.Is(LineBreakClass.HangulLeadVowelTailSyllable); + + /// + /// Checks whether a code point is an aksara base for LB28a. + /// + private static bool IsAksaraBase(LineBreakCodePoint codePoint) + => codePoint.Is(LineBreakClass.Aksara) + || codePoint.Is(LineBreakClass.AksaraStart) + || codePoint.CodePoint.Value == DottedCircle; + + /// + /// Checks whether a code point has East Asian width for LB19a and LB30 punctuation behavior. + /// + private static bool IsEastAsian(LineBreakCodePoint codePoint) + { + if (codePoint.IsSentinel) + { + return false; + } + + EastAsianWidthClass width = CodePoint.GetEastAsianWidthClass(codePoint.CodePoint); + return width is EastAsianWidthClass.Fullwidth or EastAsianWidthClass.Halfwidth or EastAsianWidthClass.Wide; + } + + /// + /// Determines where a plain-text run should stop while looking for URL markers. + /// + private static bool IsUrlRunBoundary(CodePoint codePoint) + { + if (CodePoint.IsWhiteSpace(codePoint)) + { + return true; + } + + return codePoint.Value is QuotationMark or Apostrophe or LessThanSign or GreaterThanSign; + } + + /// + /// Determines whether is valid after the first URI scheme character. + /// + private static bool IsUrlSchemeCharacter(CodePoint codePoint) + => (codePoint.IsAscii && CodePoint.IsLetterOrDigit(codePoint)) + || codePoint.Value is PlusSign or HyphenMinus or FullStop; + + /// + /// Determines whether is valid as the first URI scheme character. + /// + private static bool IsUrlSchemeStartCharacter(CodePoint codePoint) + => codePoint.IsAscii && CodePoint.IsLetter(codePoint); + + /// + /// Determines whether may be part of the host prefix check. + /// + private static bool IsUrlHostCharacter(CodePoint codePoint) + => (codePoint.IsAscii && CodePoint.IsLetterOrDigit(codePoint)) + || codePoint.Value is HyphenMinus or FullStop; + + /// + /// Determines whether is ASCII W or w. + /// + private static bool IsAsciiW(CodePoint codePoint) + => codePoint.Value is UppercaseW or LowercaseW; + + /// + /// Streaming recognizer for the URL-shaped tokens needed by UAX #14 section 8 tailoring. + /// + /// + /// This is deliberately not a URI parser. It recognizes two common plain-text signals while the + /// main line-break stream is already decoding the source: a valid ASCII URI scheme followed by + /// ://, or a www. prefix at a host-label boundary. Once a run is URL-like, later + /// solidus boundaries in that run can use the tailored behavior without rescanning the text. + /// + private struct UrlTailoringState + { + /// + /// Length of the current ASCII URI-scheme candidate, or zero when no scheme is active. + /// + private int schemeLength; + + /// + /// Number of consecutive ASCII w or W characters in a possible www. prefix. + /// + private int wwwPrefixLength; + + /// + /// Indicates that the current non-boundary run has already matched a URL signal. + /// + private bool isUrlLikeRun; + + /// + /// Indicates that the previous code point was : ending a valid scheme candidate. + /// + private bool previousWasColonAfterValidScheme; + + /// + /// Indicates that the previous code point was the first slash in a :// marker. + /// + private bool previousWasFirstSchemeSlash; + + /// + /// Indicates that the previous code point could be part of an ASCII host label. + /// + private bool previousWasHostCharacter; + + /// + /// Blocks scheme recognition until a non-scheme character resets the candidate. + /// + /// + /// URI schemes must start with an ASCII letter. A run such as 1http: should not + /// become valid just because later characters are allowed inside a scheme. + /// + private bool schemeBlocked; + + /// + /// Updates the recognizer with the next decoded code point. + /// + /// The code point from the main line-break stream. + /// when this code point belongs to a URL-like run. + public bool Update(CodePoint codePoint) + { + if (IsUrlRunBoundary(codePoint)) + { + this = default; + return false; + } + + bool currentIsUrlLike = this.isUrlLikeRun; + bool currentWasColonAfterValidScheme = false; + bool currentWasFirstSchemeSlash = false; + + if (codePoint.Value == Solidus) + { + if (this.previousWasFirstSchemeSlash) + { + this.isUrlLikeRun = true; + currentIsUrlLike = true; + } + + currentWasFirstSchemeSlash = this.previousWasColonAfterValidScheme; + this.schemeLength = 0; + this.schemeBlocked = false; + this.wwwPrefixLength = 0; + } + else + { + this.UpdateSchemeState(codePoint, out currentWasColonAfterValidScheme); + this.UpdateWwwPrefixState(codePoint, ref currentIsUrlLike); + } + + this.previousWasColonAfterValidScheme = currentWasColonAfterValidScheme; + this.previousWasFirstSchemeSlash = currentWasFirstSchemeSlash; + this.previousWasHostCharacter = IsUrlHostCharacter(codePoint); + + return currentIsUrlLike; + } + + /// + /// Updates the ASCII URI-scheme candidate state. + /// + /// The code point from the main line-break stream. + /// + /// Set to when is the colon after + /// a valid URI scheme candidate. + /// + private void UpdateSchemeState(CodePoint codePoint, out bool currentWasColonAfterValidScheme) + { + currentWasColonAfterValidScheme = false; + + if (codePoint.Value == Colon) + { + currentWasColonAfterValidScheme = this.schemeLength > 0; + this.schemeLength = 0; + this.schemeBlocked = false; + return; + } + + if (IsUrlSchemeCharacter(codePoint)) + { + if (this.schemeLength > 0) + { + this.schemeLength++; + } + else if (!this.schemeBlocked && IsUrlSchemeStartCharacter(codePoint)) + { + this.schemeLength = 1; + } + else + { + this.schemeBlocked = true; + } + + return; + } + + this.schemeLength = 0; + this.schemeBlocked = false; + } + + /// + /// Updates the www. prefix recognizer. + /// + /// The code point from the main line-break stream. + /// + /// The URL-like status to return for the current code point, updated when the prefix completes. + /// + private void UpdateWwwPrefixState(CodePoint codePoint, ref bool currentIsUrlLike) + { + if (this.isUrlLikeRun) + { + currentIsUrlLike = true; + return; + } + + if (this.wwwPrefixLength == 3 && codePoint.Value == FullStop) + { + this.isUrlLikeRun = true; + currentIsUrlLike = true; + this.wwwPrefixLength = 0; + return; + } + + if (!IsAsciiW(codePoint)) + { + this.wwwPrefixLength = 0; + return; + } + + if (!this.previousWasHostCharacter) + { + this.wwwPrefixLength = 1; + } + else if (this.wwwPrefixLength is 1 or 2) + { + this.wwwPrefixLength++; + } + else + { + this.wwwPrefixLength = 0; + } + } + } + + /// + /// The decoded code point plus the UAX #14 state needed to evaluate a boundary. + /// + /// + /// The struct stores both code point and UTF-16 positions so the enumerator can stream over the + /// original span, trim trailing whitespace for measurement, and perform bounded lookahead/lookbehind + /// without allocating intermediate collections. + /// + private struct LineBreakCodePoint + { + /// + /// Initializes a new instance of the struct for a real code point. + /// + /// The decoded Unicode scalar or replacement character. + /// The LB1-resolved line break class. + /// The general category for quote and emoji-specific rules. + /// The one-based code point index immediately after this item. + /// The UTF-16 index where this code point starts. + /// The UTF-16 index immediately after this code point. + /// Whether this item belongs to a URL-like run for layout tailoring. + public LineBreakCodePoint( + CodePoint codePoint, + LineBreakClass cls, + UnicodeCategory category, + int length, + int charStart, + int charEnd, + bool isUrlLikeRun = false) + { + this.CodePoint = codePoint; + this.Class = cls; + this.Category = category; + this.Length = length; + this.CharStart = charStart; + this.CharEnd = charEnd; + this.SentinelValue = 0; + this.Ignored = false; + this.IsUrlLikeRun = isUrlLikeRun; + } + + /// + /// Initializes a new instance of the struct as a sentinel + /// representing start or end of text. + /// + /// The sentinel value. + /// The code point length associated with the sentinel boundary. + /// The UTF-16 boundary associated with the sentinel. + private LineBreakCodePoint(int sentinel, int length, int charEnd) + { + this.CodePoint = default; + this.Class = default; + this.Category = default; + this.Length = length; + this.CharStart = charEnd; + this.CharEnd = charEnd; + this.SentinelValue = sentinel; + this.Ignored = false; + this.IsUrlLikeRun = false; + } + + /// + /// Gets the decoded code point. + /// + public CodePoint CodePoint { get; } + + /// + /// Gets or sets the LB1-resolved line break class. + /// + /// + /// LB10 can rewrite a remaining CM or ZWJ to AL after LB9 has handled attached marks. + /// + public LineBreakClass Class { get; set; } + + /// + /// Gets the Unicode general category for quote and emoji-context checks. + /// + public UnicodeCategory Category { get; } + + /// + /// Gets or sets the one-based code point index immediately after this item. + /// + /// + /// LB9 ignored marks extend the current item, so updates this value when + /// a mark is folded into its base. + /// + public int Length { get; set; } + + /// + /// Gets the UTF-16 index where this item starts. + /// + public int CharStart { get; } + + /// + /// Gets or sets the UTF-16 index immediately after this item. + /// + /// + /// LB9 ignored marks extend the current item, so updates this value when + /// a mark is folded into its base. + /// + public int CharEnd { get; set; } + + /// + /// Gets the sentinel value, or zero for a real code point. + /// + public int SentinelValue { get; } + + /// + /// Gets or sets a value indicating whether LB9 folded this item into the previous base. + /// + public bool Ignored { get; set; } + + /// + /// Gets a value indicating whether this item belongs to a URL-like run for layout tailoring. + /// + public bool IsUrlLikeRun { get; } + + /// + /// Gets a value indicating whether this item is a start or end sentinel. + /// + public readonly bool IsSentinel => this.SentinelValue != 0; + + /// + /// Gets a value indicating whether this item is the start-of-text sentinel. + /// + public readonly bool IsStartOfText => this.SentinelValue == StartOfText; + + /// + /// Gets a value indicating whether this item is the end-of-text sentinel. + /// + public readonly bool IsEndOfText => this.SentinelValue == EndOfText; + + /// + /// Creates a start-of-text or end-of-text sentinel. + /// + /// The sentinel value to assign. + /// The code point boundary associated with the sentinel. + /// The UTF-16 boundary associated with the sentinel. + /// The sentinel item. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static LineBreakCodePoint CreateSentinel(int sentinel, int length, int charEnd) => new(sentinel, length, charEnd); + + /// + /// Checks whether this item has the given line break class. + /// + /// The class to compare. + /// when this is a real item with the requested class. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Is(LineBreakClass cls) => !this.IsSentinel && this.Class == cls; + + /// + /// Checks whether this item has the given scalar value. + /// + /// The scalar value to compare. + /// when this is a real item with the requested value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool HasValue(int value) => !this.IsSentinel && this.CodePoint.Value == value; + } + } +} diff --git a/SixLabors.Fonts/Unicode/MemoryExtensions.cs b/SixLabors.Fonts/Unicode/MemoryExtensions.cs new file mode 100644 index 0000000..674ac8a --- /dev/null +++ b/SixLabors.Fonts/Unicode/MemoryExtensions.cs @@ -0,0 +1,254 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Unicode { + /// + /// Contains extension methods for memory types. + /// + public static class MemoryExtensions + { + /// + /// Returns an enumeration of from the provided span. + /// + /// The read-only span of char elements representing the text to enumerate. + /// + /// Invalid UTF-16 sequences will be represented in the enumeration by . + /// + /// The . + public static SpanCodePointEnumerator EnumerateCodePoints(this ReadOnlySpan span) + => new(span); + + /// + /// Returns an enumeration of from the provided span. + /// + /// The span of char elements representing the text to enumerate. + /// + /// Invalid UTF-16 sequences will be represented in the enumeration by . + /// + /// The . + public static SpanCodePointEnumerator EnumerateCodePoints(this Span span) + => new(span); + + /// + /// Returns the number of code points in the provided text. + /// + /// The text to enumerate. + /// The number of code points. + public static int GetCodePointCount(this string text) => text.AsSpan().GetCodePointCount(); + + /// + /// Returns the number of code points in the provided span. + /// + /// The read-only span of char elements representing the text to enumerate. + /// The number of code points. + public static int GetCodePointCount(this ReadOnlySpan span) => CodePoint.GetCodePointCount(span); + + /// + /// Returns the number of code points in the provided span. + /// + /// The span of char elements representing the text to enumerate. + /// The number of code points. + public static int GetCodePointCount(this Span span) => CodePoint.GetCodePointCount(span); + + /// + /// Returns an enumeration of grapheme clusters from the provided span. + /// + /// The read-only span of char elements representing the text to enumerate. + /// + /// Invalid UTF-16 sequences are treated as while determining grapheme boundaries. + /// + /// The . + public static SpanGraphemeEnumerator EnumerateGraphemes(this ReadOnlySpan span) + => new(span); + + /// + /// Returns an enumeration of grapheme clusters from the provided span with terminal-width metadata. + /// + /// The read-only span of char elements representing the text to enumerate. + /// + /// The terminal width options used to resolve . + /// + /// + /// Invalid UTF-16 sequences are treated as while determining grapheme boundaries. + /// Terminal width options affect only the width metadata on each returned cluster; they do not affect grapheme segmentation. + /// + /// The . + public static SpanGraphemeEnumerator EnumerateGraphemes(this ReadOnlySpan span, TerminalWidthOptions terminalWidthOptions) + => new(span, terminalWidthOptions); + + /// + /// Returns an enumeration of grapheme clusters from the provided span. + /// + /// The span of char elements representing the text to enumerate. + /// + /// Invalid UTF-16 sequences are treated as while determining grapheme boundaries. + /// + /// The . + public static SpanGraphemeEnumerator EnumerateGraphemes(this Span span) + => new(span); + + /// + /// Returns an enumeration of grapheme clusters from the provided span with terminal-width metadata. + /// + /// The span of char elements representing the text to enumerate. + /// + /// The terminal width options used to resolve . + /// + /// + /// Invalid UTF-16 sequences are treated as while determining grapheme boundaries. + /// Terminal width options affect only the width metadata on each returned cluster; they do not affect grapheme segmentation. + /// + /// The . + public static SpanGraphemeEnumerator EnumerateGraphemes(this Span span, TerminalWidthOptions terminalWidthOptions) + => new(span, terminalWidthOptions); + + /// + /// Returns an enumeration of Unicode word-boundary segments from the provided span. + /// + /// The read-only span of char elements representing the text to enumerate. + /// + /// Invalid UTF-16 sequences are treated as while determining word boundaries. + /// + /// The . + public static SpanWordEnumerator EnumerateWordSegments(this ReadOnlySpan span) + => new(span); + + /// + /// Returns an enumeration of Unicode word-boundary segments from the provided span. + /// + /// The span of char elements representing the text to enumerate. + /// + /// Invalid UTF-16 sequences are treated as while determining word boundaries. + /// + /// The . + public static SpanWordEnumerator EnumerateWordSegments(this Span span) + => new(span); + + /// + /// Returns the terminal cell width of the provided text. + /// + /// The text to measure. + /// + /// The terminal cell width, or -1 when the configured control-character policy treats + /// any grapheme cluster in the text as non-printable. + /// + public static int GetTerminalCellWidth(this string text) => text.AsSpan().GetTerminalCellWidth(); + + /// + /// Returns the terminal cell width of the provided text. + /// + /// The text to measure. + /// The terminal width options to apply while measuring. + /// + /// The terminal cell width, or -1 when the configured control-character policy treats + /// any grapheme cluster in the text as non-printable. + /// + public static int GetTerminalCellWidth(this string text, TerminalWidthOptions terminalWidthOptions) + => text.AsSpan().GetTerminalCellWidth(terminalWidthOptions); + + /// + /// Returns the terminal cell width of the provided span. + /// + /// The read-only span of char elements representing the text to measure. + /// + /// The terminal cell width, or -1 when the configured control-character policy treats + /// any grapheme cluster in the text as non-printable. + /// + public static int GetTerminalCellWidth(this ReadOnlySpan span) + => span.GetTerminalCellWidth(default); + + /// + /// Returns the terminal cell width of the provided span. + /// + /// The read-only span of char elements representing the text to measure. + /// The terminal width options to apply while measuring. + /// + /// The terminal cell width, or -1 when the configured control-character policy treats + /// any grapheme cluster in the text as non-printable. + /// + public static int GetTerminalCellWidth(this ReadOnlySpan span, TerminalWidthOptions terminalWidthOptions) + { + int width = 0; + SpanGraphemeEnumerator enumerator = new(span, terminalWidthOptions); + + while (enumerator.MoveNext()) + { + int terminalCellWidth = enumerator.Current.TerminalCellWidth; + if (terminalCellWidth < 0) + { + return -1; + } + + width += terminalCellWidth; + } + + return width; + } + + /// + /// Returns the terminal cell width of the provided span. + /// + /// The span of char elements representing the text to measure. + /// + /// The terminal cell width, or -1 when the configured control-character policy treats + /// any grapheme cluster in the text as non-printable. + /// + public static int GetTerminalCellWidth(this Span span) + => ((ReadOnlySpan)span).GetTerminalCellWidth(); + + /// + /// Returns the terminal cell width of the provided span. + /// + /// The span of char elements representing the text to measure. + /// The terminal width options to apply while measuring. + /// + /// The terminal cell width, or -1 when the configured control-character policy treats + /// any grapheme cluster in the text as non-printable. + /// + public static int GetTerminalCellWidth(this Span span, TerminalWidthOptions terminalWidthOptions) + => ((ReadOnlySpan)span).GetTerminalCellWidth(terminalWidthOptions); + + /// + /// Returns the number of grapheme clusters in the provided text. + /// + /// The text to enumerate. + /// The number of grapheme clusters. + public static int GetGraphemeCount(this string text) => text.AsSpan().GetGraphemeCount(); + + /// + /// Returns the number of grapheme clusters in the provided span. + /// + /// The read-only span of char elements representing the text to enumerate. + /// The number of grapheme clusters. + public static int GetGraphemeCount(this ReadOnlySpan span) + { + int count = 0; + SpanGraphemeEnumerator enumerator = new(span); + while (enumerator.MoveNext()) + { + count++; + } + + return count; + } + + /// + /// Returns the number of grapheme clusters in the provided span. + /// + /// The span of char elements representing the text to enumerate. + /// The number of grapheme clusters. + public static int GetGraphemeCount(this Span span) + { + int count = 0; + SpanGraphemeEnumerator enumerator = new(span); + while (enumerator.MoveNext()) + { + count++; + } + + return count; + } + } +} diff --git a/SixLabors.Fonts/Unicode/README.md b/SixLabors.Fonts/Unicode/README.md new file mode 100644 index 0000000..92d6ebf --- /dev/null +++ b/SixLabors.Fonts/Unicode/README.md @@ -0,0 +1,9 @@ +UnicodeTrieBuilder and accompanying classes ported from the following sources: + +https://github.com/dotnet/runtime MIT +https://github.com/unicode-org/icu ICU License (Permissive) +https://github.com/toptensoftware/RichTextKit Apache 2.0 +https://github.com/foliojs/unicode-trie MIT + +Further information and implementation examples can be found at +http://www.unicode.org/reports/tr41/tr41-26.html diff --git a/SixLabors.Fonts/Unicode/Resources/ArabicShapingTrie.Generated.cs b/SixLabors.Fonts/Unicode/Resources/ArabicShapingTrie.Generated.cs new file mode 100644 index 0000000..978f8c5 --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/ArabicShapingTrie.Generated.cs @@ -0,0 +1,160 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class ArabicShapingTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 240, 1, 0, 0, 0, 0, 0, 80, 56, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 72, 2, 0, 0, 80, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, + 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, + 64, 2, 0, 0, 104, 2, 0, 0, 112, 2, 0, 0, 120, 2, 0, 0, 128, 2, 0, 0, 136, 2, 0, 0, 144, 2, 0, 0, 152, 2, 0, 0, 160, 2, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 181, 2, 0, 0, 189, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 197, 2, 0, 0, 205, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 213, 2, 0, 0, 221, 2, 0, 0, 229, 2, 0, 0, 237, 2, 0, 0, 245, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 252, 2, 0, 0, 4, 3, 0, 0, 4, 3, 0, 0, 6, 3, 0, 0, 13, 3, 0, 0, + 19, 3, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 24, 3, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 96, 9, 0, 0, 96, 9, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, + 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 160, 9, 0, 0, 224, 9, 0, 0, 32, 10, 0, 0, 96, 10, 0, 0, 160, 10, 0, 0, 212, 10, 0, 0, 224, 8, 0, 0, 20, 11, 0, 0, 71, 0, 0, 0, 94, 8, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, + 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 157, 8, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 32, 3, 0, 0, 40, 3, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 48, 3, 0, 0, 54, 3, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 62, 3, 0, 0, 70, 3, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 78, 3, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 82, 3, 0, 0, 90, 3, 0, 0, 96, 3, 0, 0, 104, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 4, 3, 0, 0, 4, 3, 0, 0, 124, 3, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, + 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 55, 2, 1, 0, 55, 2, 1, 0, 55, 2, 1, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 27, 0, + 4, 0, 72, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 98, 0, 0, 0, 5, 0, 2, 0, 99, 0, 0, 0, 5, 0, 2, 0, 6, 0, 0, 0, 92, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 18, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 79, 0, 0, 0, 79, 0, 2, 0, 84, 0, 2, 0, 84, 0, 2, 0, 82, 0, 2, 0, 82, 0, 2, 0, 90, 0, 2, 0, 90, 0, 2, 0, 3, 0, + 2, 0, 3, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 12, 0, 2, 0, 12, 0, 2, 0, 12, 0, 3, 0, 72, 0, 2, 0, 14, 0, 2, 0, 77, 0, 2, 0, 25, 0, 2, 0, 30, 0, 2, 0, 70, 0, 2, 0, 73, 0, 2, 0, 22, 0, 0, 0, 98, 0, 2, 0, 99, 0, 2, 0, 99, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 6, 0, 2, 0, 77, 0, 4, 0, 72, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 4, 0, 72, 0, 0, 0, 5, 0, 0, 0, 98, 0, 0, 0, 98, 0, 2, 0, 99, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 18, 0, + 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 18, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 2, 0, 84, 0, 2, 0, 84, 0, 2, 0, 84, 0, 2, 0, 82, 0, + 2, 0, 82, 0, 2, 0, 90, 0, 2, 0, 3, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 77, 0, 2, 0, 77, 0, 2, 0, 16, 0, 2, 0, 88, 0, 2, 0, 16, 0, 2, 0, 25, 0, 2, 0, 25, 0, 2, 0, 25, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 30, 0, 2, 0, 30, 0, + 2, 0, 30, 0, 2, 0, 30, 0, 2, 0, 73, 0, 2, 0, 73, 0, 2, 0, 73, 0, 2, 0, 73, 0, 2, 0, 75, 0, 2, 0, 29, 0, 2, 0, 18, 0, 0, 0, 92, 0, 2, 0, 23, 0, 2, 0, 23, 0, 0, 0, 93, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 2, 0, 12, 0, 0, 0, 101, 0, 2, 0, 12, 0, 0, 0, 98, 0, + 2, 0, 99, 0, 2, 0, 99, 0, 0, 0, 100, 0, 0, 0, 100, 0, 4, 0, 72, 0, 0, 0, 92, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 0, 0, 9, 0, 0, 0, 79, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 84, 0, 2, 0, 82, 0, 2, 0, 3, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 29, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 5, 0, 72, 0, 0, 0, 4, 0, 4, 0, 72, 0, 2, 0, 7, 0, 2, 0, 17, 0, 2, 0, 17, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 21, 0, 0, 0, 89, 0, 0, 0, 104, 0, 2, 0, 24, 0, + 2, 0, 94, 0, 2, 0, 94, 0, 2, 0, 102, 0, 0, 0, 103, 0, 2, 0, 26, 0, 2, 0, 31, 0, 2, 0, 71, 0, 2, 0, 74, 0, 2, 0, 85, 0, 2, 0, 15, 0, 2, 0, 11, 0, 2, 0, 76, 0, 2, 0, 80, 0, 0, 0, 83, 0, 2, 0, 78, 0, 0, 0, 10, 0, 2, 0, 86, 0, 0, 0, 91, 0, 2, 0, 7, 0, 2, 0, 17, 0, 0, 0, 10, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 0, 0, 105, 0, 2, 0, 28, 0, 2, 0, 13, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 18, 0, 2, 0, 18, 0, + 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 79, 0, 2, 0, 84, 0, 2, 0, 3, 0, 2, 0, 3, 0, 2, 0, 3, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 70, 0, 2, 0, 70, 0, 2, 0, 73, 0, 2, 0, 73, 0, 2, 0, 73, 0, 2, 0, 30, 0, 0, 0, 79, 0, 0, 0, 79, 0, 2, 0, 84, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 84, 0, 0, 0, 79, 0, + 2, 0, 18, 0, 0, 0, 5, 0, 0, 0, 5, 0, 2, 0, 12, 0, 2, 0, 12, 0, 2, 0, 99, 0, 0, 0, 98, 0, 0, 0, 98, 0, 2, 0, 8, 0, 2, 0, 8, 0, 2, 0, 18, 0, 2, 0, 84, 0, 2, 0, 84, 0, 2, 0, 25, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, + 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, + 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 3, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, + 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 36, 0, 4, 0, 33, 0, 2, 0, 39, 0, 2, 0, 42, 0, 2, 0, 37, 0, 2, 0, 38, 0, 4, 0, 32, 0, 0, 0, 40, 0, 2, 0, 34, 0, 0, 0, 35, 0, 0, 0, 41, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 3, 0, 72, 0, 3, 0, 72, 0, 3, 0, 72, 0, 2, 0, 96, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 2, 0, 73, 0, 2, 0, 18, 0, 2, 0, 90, 0, 2, 0, 90, 0, 2, 0, 16, 0, 0, 0, 97, 0, 2, 0, 73, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 6, 0, + 2, 0, 6, 0, 2, 0, 18, 0, 2, 0, 90, 0, 2, 0, 14, 0, 2, 0, 77, 0, 2, 0, 30, 0, 2, 0, 70, 0, 2, 0, 99, 0, 2, 0, 99, 0, 0, 0, 79, 0, 0, 0, 98, 0, 0, 0, 81, 0, 4, 0, 72, 0, 0, 0, 9, 0, 2, 0, 82, 0, 2, 0, 16, 0, 0, 0, 87, 0, 0, 0, 79, 0, 2, 0, 3, 0, 2, 0, 25, 0, 2, 0, 77, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 0, 0, 79, 0, + 2, 0, 99, 0, 2, 0, 0, 0, 2, 0, 2, 0, 2, 0, 1, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 18, 0, 2, 0, 16, 0, 2, 0, 3, 0, 2, 0, 2, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 30, 0, 2, 0, 16, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 3, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, + 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 5, 0, 72, 0, 5, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, + 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 3, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 43, 0, 2, 0, 45, 0, 2, 0, 45, 0, 2, 0, 49, 0, 2, 0, 49, 0, 0, 0, 46, 0, 4, 0, 72, 0, 0, 0, 67, 0, 4, 0, 72, 0, 0, 0, 69, 0, 0, 0, 69, 0, 4, 0, 72, 0, 4, 0, 72, 0, 1, 0, 50, 0, 0, 0, 64, 0, 0, 0, 68, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 2, 0, 53, 0, 2, 0, 47, 0, 2, 0, 65, 0, + 2, 0, 54, 0, 1, 0, 55, 0, 2, 0, 61, 0, 2, 0, 44, 0, 2, 0, 44, 0, 2, 0, 57, 0, 2, 0, 57, 0, 0, 0, 60, 0, 2, 0, 58, 0, 2, 0, 58, 0, 2, 0, 58, 0, 0, 0, 59, 0, 4, 0, 72, 0, 4, 0, 72, 0, 0, 0, 62, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 56, 0, 2, 0, 48, 0, 2, 0, 63, 0, 2, 0, 66, 0, + 0, 0, 51, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, + 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 1, 0, 72, 0, 2, 0, 72, 0, + 2, 0, 20, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 20, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 19, 0, 2, 0, 72, 0, + 2, 0, 72, 0, 2, 0, 20, 0, 2, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 19, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 0, 0, 9, 0, 2, 0, 90, 0, 2, 0, 25, 0, 4, 0, 72, 0, 2, 0, 95, 0, 2, 0, 99, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, + 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, + 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, + 1, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 5, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/BidiMirrorTrie.Generated.cs b/SixLabors.Fonts/Unicode/Resources/BidiMirrorTrie.Generated.cs new file mode 100644 index 0000000..addb9d5 --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/BidiMirrorTrie.Generated.cs @@ -0,0 +1,156 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class BidiMirrorTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 0, 1, 0, 0, 0, 0, 0, 208, 54, 0, 0, 16, 2, 0, 0, 24, 2, 0, 0, 32, 2, 0, 0, 40, 2, 0, 0, 54, 2, 0, 0, 62, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, + 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, + 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 56, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 69, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 77, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 85, 2, 0, 0, 92, 2, 0, 0, 94, 2, 0, 0, 102, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 108, 2, 0, 0, 116, 2, 0, 0, 124, 2, 0, 0, 131, 2, 0, 0, 139, 2, 0, 0, 147, 2, 0, 0, 154, 2, 0, 0, 162, 2, 0, 0, 170, 2, 0, 0, 176, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 182, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 190, 2, 0, 0, 198, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 206, 2, 0, 0, 214, 2, 0, 0, 222, 2, 0, 0, 229, 2, 0, 0, 48, 2, 0, 0, 237, 2, 0, 0, 48, 2, 0, 0, 245, 2, 0, 0, 253, 2, 0, 0, 5, 3, 0, 0, 13, 3, 0, 0, 21, 3, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 28, 3, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 36, 3, 0, 0, 44, 3, 0, 0, 47, 3, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 55, 3, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 62, 3, 0, 0, 70, 3, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 76, 3, 0, 0, 84, 3, 0, 0, 92, 3, 0, 0, 100, 3, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, + 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 48, 2, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 216, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, + 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 192, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 62, 0, 0, 0, 0, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93, 0, 0, 0, 0, 0, 0, 0, 91, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 125, 0, 0, 0, 0, 0, 0, 0, 123, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 187, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 171, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 15, 0, 0, 58, 15, 0, 0, 61, 15, 0, 0, 60, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 156, 22, 0, 0, 155, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 58, 32, 0, 0, 57, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 70, 32, 0, 0, + 69, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 126, 32, 0, 0, 125, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 142, 32, 0, 0, 141, 32, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 34, 0, 0, 12, 34, 0, 0, 13, 34, 0, 0, 8, 34, 0, 0, 9, 34, 0, 0, 10, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 245, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254, 43, 0, 0, 163, 41, 0, 0, 155, 41, 0, 0, 160, 41, 0, 0, 0, 0, 0, 0, 238, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 61, 34, 0, 0, 60, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 205, 34, 0, 0, 0, 0, 0, 0, 76, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 69, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 34, 0, 0, 82, 34, 0, 0, 85, 34, 0, 0, 84, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 34, 0, 0, 100, 34, 0, 0, 103, 34, 0, 0, 102, 34, 0, 0, 105, 34, 0, 0, 104, 34, 0, 0, 107, 34, 0, 0, 106, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 111, 34, 0, 0, 110, 34, 0, 0, 113, 34, 0, 0, 112, 34, 0, 0, 115, 34, 0, 0, 114, 34, 0, 0, 117, 34, 0, 0, 116, 34, 0, 0, 119, 34, 0, 0, 118, 34, 0, 0, 121, 34, 0, 0, + 120, 34, 0, 0, 123, 34, 0, 0, 122, 34, 0, 0, 125, 34, 0, 0, 124, 34, 0, 0, 127, 34, 0, 0, 126, 34, 0, 0, 129, 34, 0, 0, 128, 34, 0, 0, 131, 34, 0, 0, 130, 34, 0, 0, 133, 34, 0, 0, 132, 34, 0, 0, 135, 34, 0, 0, 134, 34, 0, 0, 137, 34, 0, 0, 136, 34, 0, 0, 139, 34, 0, 0, 138, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 144, 34, 0, 0, 143, 34, 0, 0, 146, 34, 0, 0, + 145, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 184, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 163, 34, 0, 0, 162, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 222, 42, 0, 0, 0, 0, 0, 0, 228, 42, 0, 0, 227, 42, 0, 0, 0, 0, 0, 0, + 229, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 177, 34, 0, 0, 176, 34, 0, 0, 179, 34, 0, 0, 178, 34, 0, 0, 181, 34, 0, 0, 180, 34, 0, 0, 183, 34, 0, 0, 182, 34, 0, 0, 220, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 202, 34, 0, 0, 201, 34, 0, 0, 204, 34, 0, 0, 203, 34, 0, 0, 67, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 209, 34, 0, 0, 208, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 215, 34, 0, 0, 214, 34, 0, 0, 217, 34, 0, 0, 216, 34, 0, 0, 219, 34, 0, 0, 218, 34, 0, 0, 221, 34, 0, 0, 220, 34, 0, 0, 223, 34, 0, 0, 222, 34, 0, 0, 225, 34, 0, 0, + 224, 34, 0, 0, 227, 34, 0, 0, 226, 34, 0, 0, 229, 34, 0, 0, 228, 34, 0, 0, 231, 34, 0, 0, 230, 34, 0, 0, 233, 34, 0, 0, 232, 34, 0, 0, 235, 34, 0, 0, 234, 34, 0, 0, 237, 34, 0, 0, 236, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 241, 34, 0, 0, 240, 34, 0, 0, 250, 34, 0, 0, 251, 34, 0, 0, 252, 34, 0, 0, 0, 0, 0, 0, 253, 34, 0, 0, 254, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 242, 34, 0, 0, 243, 34, 0, 0, 244, 34, 0, 0, 246, 34, 0, 0, 247, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 35, 0, 0, 8, 35, 0, 0, 11, 35, 0, 0, 10, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 35, 0, 0, 41, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 105, 39, 0, 0, 104, 39, 0, 0, 107, 39, 0, 0, 106, 39, 0, 0, 109, 39, 0, 0, 108, 39, 0, 0, 111, 39, 0, 0, 110, 39, 0, 0, 113, 39, 0, 0, 112, 39, 0, 0, 115, 39, 0, 0, 114, 39, 0, 0, 117, 39, 0, 0, + 116, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 196, 39, 0, 0, 195, 39, 0, 0, 198, 39, 0, 0, 197, 39, 0, 0, 0, 0, 0, 0, 201, 39, 0, 0, 200, 39, 0, 0, 0, 0, 0, 0, 205, 39, 0, 0, 0, 0, 0, 0, 203, 39, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 214, 39, 0, 0, 213, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 184, 34, 0, 0, 222, 39, 0, 0, 221, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 227, 39, 0, 0, 226, 39, 0, 0, 229, 39, 0, 0, 228, 39, 0, 0, 231, 39, 0, 0, + 230, 39, 0, 0, 233, 39, 0, 0, 232, 39, 0, 0, 235, 39, 0, 0, 234, 39, 0, 0, 237, 39, 0, 0, 236, 39, 0, 0, 239, 39, 0, 0, 238, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 132, 41, 0, 0, 131, 41, 0, 0, 134, 41, 0, 0, 133, 41, 0, 0, 136, 41, 0, 0, 135, 41, 0, 0, 138, 41, 0, 0, 137, 41, 0, 0, 140, 41, 0, 0, 139, 41, 0, 0, 144, 41, 0, 0, 143, 41, 0, 0, 142, 41, 0, 0, 141, 41, 0, 0, 146, 41, 0, 0, 145, 41, 0, 0, 148, 41, 0, 0, 147, 41, 0, 0, 150, 41, 0, 0, 149, 41, 0, 0, 152, 41, 0, 0, 151, 41, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 33, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 34, 0, 0, 165, 41, 0, 0, 164, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 169, 41, 0, 0, 168, 41, 0, 0, 171, 41, 0, 0, 170, 41, 0, 0, 173, 41, 0, 0, 172, 41, 0, 0, 175, 41, 0, 0, 174, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 152, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 193, 41, 0, 0, 192, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 197, 41, 0, 0, 196, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 208, 41, 0, 0, 207, 41, 0, 0, 210, 41, 0, 0, 209, 41, 0, 0, 0, 0, 0, 0, 213, 41, 0, 0, 212, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 217, 41, 0, 0, 216, 41, 0, 0, 219, 41, 0, 0, 218, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 233, 41, 0, 0, 232, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 249, 41, 0, 0, 248, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 253, 41, 0, 0, 252, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 42, 0, 0, 43, 42, 0, 0, 46, 42, 0, 0, 45, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 53, 42, 0, 0, 52, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 61, 42, 0, 0, 60, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 42, 0, 0, 100, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 122, 42, 0, 0, 121, 42, 0, 0, 124, 42, 0, 0, 123, 42, 0, 0, 126, 42, 0, 0, 125, 42, 0, 0, 128, 42, 0, 0, 127, 42, 0, 0, 130, 42, 0, 0, 129, 42, 0, 0, 132, 42, 0, 0, 131, 42, 0, 0, 134, 42, 0, 0, 133, 42, 0, 0, 136, 42, 0, 0, 135, 42, 0, 0, 138, 42, 0, 0, 137, 42, 0, 0, 140, 42, 0, 0, + 139, 42, 0, 0, 142, 42, 0, 0, 141, 42, 0, 0, 144, 42, 0, 0, 143, 42, 0, 0, 146, 42, 0, 0, 145, 42, 0, 0, 148, 42, 0, 0, 147, 42, 0, 0, 150, 42, 0, 0, 149, 42, 0, 0, 152, 42, 0, 0, 151, 42, 0, 0, 154, 42, 0, 0, 153, 42, 0, 0, 156, 42, 0, 0, 155, 42, 0, 0, 158, 42, 0, 0, 157, 42, 0, 0, 160, 42, 0, 0, 159, 42, 0, 0, 162, 42, 0, 0, 161, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 167, 42, 0, 0, 166, 42, 0, 0, 169, 42, 0, 0, 168, 42, 0, 0, 171, 42, 0, 0, 170, 42, 0, 0, 173, 42, 0, 0, 172, 42, 0, 0, 0, 0, 0, 0, 176, 42, 0, 0, 175, 42, 0, 0, 178, 42, 0, 0, 177, 42, 0, 0, 180, 42, 0, 0, 179, 42, 0, 0, 182, 42, 0, 0, 181, 42, 0, 0, 184, 42, 0, 0, 183, 42, 0, 0, 186, 42, 0, 0, 185, 42, 0, 0, 188, 42, 0, 0, 187, 42, 0, 0, 190, 42, 0, 0, + 189, 42, 0, 0, 192, 42, 0, 0, 191, 42, 0, 0, 194, 42, 0, 0, 193, 42, 0, 0, 196, 42, 0, 0, 195, 42, 0, 0, 198, 42, 0, 0, 197, 42, 0, 0, 200, 42, 0, 0, 199, 42, 0, 0, 202, 42, 0, 0, 201, 42, 0, 0, 204, 42, 0, 0, 203, 42, 0, 0, 206, 42, 0, 0, 205, 42, 0, 0, 208, 42, 0, 0, 207, 42, 0, 0, 210, 42, 0, 0, 209, 42, 0, 0, 212, 42, 0, 0, 211, 42, 0, 0, 214, 42, 0, 0, 213, 42, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 166, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 169, 34, 0, 0, 168, 34, 0, 0, 171, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 42, 0, 0, 236, 42, 0, 0, 36, 34, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 248, 42, 0, 0, 247, 42, 0, 0, 250, 42, 0, 0, 249, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 31, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 46, 0, 0, 2, 46, 0, 0, 5, 46, 0, 0, 4, 46, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 46, 0, 0, 9, 46, 0, 0, 0, 0, 0, 0, 13, 46, 0, 0, 12, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 46, 0, 0, 28, 46, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 33, 46, 0, 0, 32, 46, 0, 0, 35, 46, 0, 0, 34, 46, 0, 0, 37, 46, 0, 0, 36, 46, 0, 0, 39, 46, 0, 0, 38, 46, 0, 0, 41, 46, 0, 0, 40, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 86, 46, 0, 0, 85, 46, 0, 0, 88, 46, 0, 0, 87, 46, 0, 0, 90, 46, 0, 0, 89, 46, 0, 0, 92, 46, 0, 0, 91, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 48, 0, 0, 8, 48, 0, 0, 11, 48, 0, 0, 10, 48, 0, 0, 13, 48, 0, 0, 12, 48, 0, 0, 15, 48, 0, 0, 14, 48, 0, 0, 17, 48, 0, 0, 16, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 48, 0, 0, 20, 48, 0, 0, 23, 48, 0, 0, 22, 48, 0, 0, 25, 48, 0, 0, 24, 48, 0, 0, 27, 48, 0, 0, 26, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 254, 0, 0, 89, 254, 0, 0, + 92, 254, 0, 0, 91, 254, 0, 0, 94, 254, 0, 0, 93, 254, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 254, 0, 0, 100, 254, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 255, 0, 0, 8, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 255, 0, 0, 0, 0, 0, 0, 28, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 61, 255, 0, 0, 0, 0, 0, 0, 59, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93, 255, 0, 0, 0, 0, 0, 0, 91, 255, 0, 0, 0, 0, 0, 0, 96, 255, 0, 0, + 95, 255, 0, 0, 0, 0, 0, 0, 99, 255, 0, 0, 98, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/BidiTrie.Generated.cs b/SixLabors.Fonts/Unicode/Resources/BidiTrie.Generated.cs new file mode 100644 index 0000000..006f9cc --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/BidiTrie.Generated.cs @@ -0,0 +1,538 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class BidiTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 8, 14, 0, 0, 0, 0, 0, 16, 204, 0, 0, 105, 3, 0, 0, 113, 3, 0, 0, 121, 3, 0, 0, 129, 3, 0, 0, 153, 3, 0, 0, 161, 3, 0, 0, 169, 3, 0, 0, 177, 3, 0, 0, 137, 3, 0, 0, 145, 3, 0, 0, 137, 3, 0, 0, 145, 3, 0, 0, 137, 3, 0, 0, 145, 3, 0, 0, 137, 3, 0, 0, 145, 3, 0, 0, 137, 3, 0, 0, 145, 3, 0, 0, 137, 3, 0, 0, 145, 3, 0, 0, 183, 3, 0, 0, 191, 3, 0, 0, + 199, 3, 0, 0, 207, 3, 0, 0, 215, 3, 0, 0, 223, 3, 0, 0, 219, 3, 0, 0, 227, 3, 0, 0, 235, 3, 0, 0, 243, 3, 0, 0, 238, 3, 0, 0, 246, 3, 0, 0, 137, 3, 0, 0, 145, 3, 0, 0, 137, 3, 0, 0, 145, 3, 0, 0, 254, 3, 0, 0, 6, 4, 0, 0, 137, 3, 0, 0, 145, 3, 0, 0, 137, 3, 0, 0, 145, 3, 0, 0, 137, 3, 0, 0, 145, 3, 0, 0, 12, 4, 0, 0, 20, 4, 0, 0, 28, 4, 0, 0, + 36, 4, 0, 0, 44, 4, 0, 0, 52, 4, 0, 0, 58, 4, 0, 0, 66, 4, 0, 0, 71, 4, 0, 0, 79, 4, 0, 0, 82, 4, 0, 0, 90, 4, 0, 0, 97, 4, 0, 0, 105, 4, 0, 0, 111, 4, 0, 0, 119, 4, 0, 0, 118, 4, 0, 0, 126, 4, 0, 0, 134, 4, 0, 0, 142, 4, 0, 0, 150, 4, 0, 0, 157, 4, 0, 0, 165, 4, 0, 0, 173, 4, 0, 0, 177, 4, 0, 0, 51, 4, 0, 0, 185, 4, 0, 0, 193, 4, 0, 0, + 201, 4, 0, 0, 203, 4, 0, 0, 211, 4, 0, 0, 219, 4, 0, 0, 227, 4, 0, 0, 228, 4, 0, 0, 236, 4, 0, 0, 244, 4, 0, 0, 252, 4, 0, 0, 228, 4, 0, 0, 4, 5, 0, 0, 9, 5, 0, 0, 252, 4, 0, 0, 228, 4, 0, 0, 17, 5, 0, 0, 25, 5, 0, 0, 227, 4, 0, 0, 33, 5, 0, 0, 41, 5, 0, 0, 219, 4, 0, 0, 49, 5, 0, 0, 137, 3, 0, 0, 57, 5, 0, 0, 61, 5, 0, 0, 69, 5, 0, 0, + 71, 5, 0, 0, 79, 5, 0, 0, 87, 5, 0, 0, 227, 4, 0, 0, 228, 4, 0, 0, 95, 5, 0, 0, 219, 4, 0, 0, 0, 4, 0, 0, 99, 5, 0, 0, 236, 4, 0, 0, 219, 4, 0, 0, 227, 4, 0, 0, 137, 3, 0, 0, 107, 5, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 113, 5, 0, 0, 121, 5, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 125, 5, 0, 0, 133, 5, 0, 0, 137, 3, 0, 0, 137, 5, 0, 0, 144, 5, 0, 0, + 137, 3, 0, 0, 152, 5, 0, 0, 160, 5, 0, 0, 167, 5, 0, 0, 48, 5, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 175, 5, 0, 0, 183, 5, 0, 0, 191, 5, 0, 0, 199, 5, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 207, 5, 0, 0, 137, 3, 0, 0, 215, 5, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 223, 5, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 231, 5, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 239, 5, 0, 0, 240, 4, 0, 0, 240, 4, 0, 0, 240, 4, 0, 0, 137, 3, 0, 0, 245, 5, 0, 0, 253, 5, 0, 0, 215, 5, 0, 0, 5, 6, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 12, 6, 0, 0, + 225, 4, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 20, 6, 0, 0, 28, 6, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 30, 6, 0, 0, 38, 6, 0, 0, 46, 6, 0, 0, 137, 3, 0, 0, 53, 6, 0, 0, 61, 6, 0, 0, 137, 3, 0, 0, 69, 6, 0, 0, 73, 6, 0, 0, 81, 6, 0, 0, 32, 5, 0, 0, 84, 6, 0, 0, 49, 5, 0, 0, 92, 6, 0, 0, 0, 4, 0, 0, 100, 6, 0, 0, + 137, 3, 0, 0, 107, 6, 0, 0, 137, 3, 0, 0, 112, 6, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 118, 6, 0, 0, 126, 6, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 215, 3, 0, 0, 215, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 133, 6, 0, 0, 141, 6, 0, 0, 145, 6, 0, 0, 153, 6, 0, 0, 159, 6, 0, 0, 166, 6, 0, 0, 174, 6, 0, 0, 182, 6, 0, 0, 190, 6, 0, 0, 198, 6, 0, 0, 170, 5, 0, 0, 206, 6, 0, 0, 214, 6, 0, 0, 222, 6, 0, 0, 137, 3, 0, 0, 230, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, + 234, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 240, 6, 0, 0, 246, 6, 0, 0, 137, 3, 0, 0, 252, 6, 0, 0, 3, 7, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 249, 6, 0, 0, 9, 7, 0, 0, 38, 6, 0, 0, 17, 7, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 35, 6, 0, 0, 38, 6, 0, 0, + 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 25, 7, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 31, 7, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 7, 0, 0, 45, 7, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 53, 7, 0, 0, 38, 6, 0, 0, 60, 7, 0, 0, 67, 7, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, + 75, 7, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 83, 7, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 91, 7, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 215, 3, 0, 0, 38, 6, 0, 0, 99, 7, 0, 0, 102, 7, 0, 0, 137, 3, 0, 0, + 110, 7, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 41, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 117, 7, 0, 0, 123, 7, 0, 0, 131, 7, 0, 0, 139, 7, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 147, 7, 0, 0, 223, 5, 0, 0, 137, 3, 0, 0, 176, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 38, 6, 0, 0, 155, 7, 0, 0, 190, 3, 0, 0, 137, 3, 0, 0, 123, 7, 0, 0, 159, 7, 0, 0, 137, 3, 0, 0, 167, 7, 0, 0, 175, 7, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 179, 7, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 30, 6, 0, 0, 175, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 38, 6, 0, 0, 38, 6, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 123, 7, 0, 0, 38, 6, 0, 0, 187, 7, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 192, 7, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 197, 7, 0, 0, 205, 7, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 139, 5, 0, 0, 38, 6, 0, 0, 29, 6, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 213, 7, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 221, 7, 0, 0, 228, 7, 0, 0, 137, 3, 0, 0, + 235, 7, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 97, 5, 0, 0, 243, 7, 0, 0, 137, 3, 0, 0, 251, 7, 0, 0, 2, 8, 0, 0, 137, 3, 0, 0, 201, 4, 0, 0, 7, 8, 0, 0, 137, 3, 0, 0, 226, 4, 0, 0, 137, 3, 0, 0, 15, 8, 0, 0, 23, 8, 0, 0, 228, 4, 0, 0, 137, 3, 0, 0, 27, 8, 0, 0, 227, 4, 0, 0, 35, 8, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 41, 8, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 48, 8, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 52, 8, 0, 0, 60, 8, 0, 0, 68, 8, 0, 0, 51, 4, 0, 0, 51, 4, 0, 0, 51, 4, 0, 0, 76, 8, 0, 0, 51, 4, 0, 0, 51, 4, 0, 0, 51, 4, 0, 0, 51, 4, 0, 0, 51, 4, 0, 0, 51, 4, 0, 0, 51, 4, 0, 0, + 51, 4, 0, 0, 51, 4, 0, 0, 51, 4, 0, 0, 81, 8, 0, 0, 89, 8, 0, 0, 51, 4, 0, 0, 93, 8, 0, 0, 51, 4, 0, 0, 99, 8, 0, 0, 103, 8, 0, 0, 111, 8, 0, 0, 119, 8, 0, 0, 123, 8, 0, 0, 131, 8, 0, 0, 51, 4, 0, 0, 51, 4, 0, 0, 51, 4, 0, 0, 137, 8, 0, 0, 145, 8, 0, 0, 153, 8, 0, 0, 161, 8, 0, 0, 169, 8, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 177, 8, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 36, 14, 0, 0, 36, 14, 0, 0, 100, 14, 0, 0, 164, 14, 0, 0, 36, 14, 0, 0, 36, 14, 0, 0, 36, 14, 0, 0, 36, 14, 0, 0, 36, 14, 0, 0, 36, 14, 0, 0, 220, 14, 0, 0, 28, 15, 0, 0, 92, 15, 0, 0, 108, 15, 0, 0, 172, 15, 0, 0, 184, 15, 0, 0, 36, 14, 0, 0, + 36, 14, 0, 0, 248, 15, 0, 0, 36, 14, 0, 0, 36, 14, 0, 0, 36, 14, 0, 0, 48, 16, 0, 0, 112, 16, 0, 0, 176, 16, 0, 0, 232, 16, 0, 0, 28, 17, 0, 0, 72, 17, 0, 0, 132, 17, 0, 0, 188, 17, 0, 0, 216, 17, 0, 0, 24, 18, 0, 0, 225, 9, 0, 0, 33, 10, 0, 0, 97, 10, 0, 0, 160, 10, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 224, 10, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 24, 11, 0, 0, 65, 11, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 129, 11, 0, 0, 160, 1, 0, 0, 167, 11, 0, 0, 226, 11, 0, 0, 34, 12, 0, 0, 98, 12, 0, 0, 162, 12, 0, 0, 226, 12, 0, 0, 34, 13, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 98, 13, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 185, 8, 0, 0, 137, 3, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 193, 8, 0, 0, 223, 5, 0, 0, 137, 3, 0, 0, 220, 4, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 201, 8, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 208, 8, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 216, 8, 0, 0, 219, 8, 0, 0, 227, 8, 0, 0, 134, 4, 0, 0, + 233, 8, 0, 0, 241, 8, 0, 0, 137, 3, 0, 0, 249, 8, 0, 0, 0, 9, 0, 0, 8, 9, 0, 0, 16, 9, 0, 0, 137, 3, 0, 0, 134, 4, 0, 0, 24, 9, 0, 0, 31, 9, 0, 0, 134, 4, 0, 0, 39, 9, 0, 0, 46, 9, 0, 0, 54, 9, 0, 0, 134, 4, 0, 0, 134, 4, 0, 0, 137, 3, 0, 0, 134, 4, 0, 0, 62, 9, 0, 0, 134, 4, 0, 0, 70, 9, 0, 0, 78, 9, 0, 0, 84, 9, 0, 0, 90, 9, 0, 0, + 98, 9, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 134, 4, 0, 0, 134, 4, 0, 0, 106, 9, 0, 0, 137, 3, 0, 0, 134, 4, 0, 0, 114, 9, 0, 0, 134, 4, 0, 0, 122, 9, 0, 0, 51, 4, 0, 0, 130, 9, 0, 0, 138, 9, 0, 0, 145, 9, 0, 0, 152, 9, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 160, 9, 0, 0, 134, 4, 0, 0, 168, 9, 0, 0, + 176, 9, 0, 0, 183, 9, 0, 0, 134, 4, 0, 0, 191, 9, 0, 0, 198, 9, 0, 0, 245, 8, 0, 0, 206, 9, 0, 0, 245, 8, 0, 0, 214, 9, 0, 0, 222, 9, 0, 0, 227, 4, 0, 0, 228, 9, 0, 0, 235, 9, 0, 0, 242, 9, 0, 0, 0, 4, 0, 0, 250, 9, 0, 0, 49, 5, 0, 0, 137, 3, 0, 0, 201, 4, 0, 0, 1, 10, 0, 0, 137, 3, 0, 0, 7, 10, 0, 0, 0, 4, 0, 0, 12, 10, 0, 0, 20, 10, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 25, 10, 0, 0, 227, 4, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 91, 7, 0, 0, 33, 10, 0, 0, 0, 4, 0, 0, 99, 5, 0, 0, 70, 5, 0, 0, 40, 10, 0, 0, 137, 3, 0, 0, 46, 10, 0, 0, 54, 10, 0, 0, 252, 4, 0, 0, 137, 3, 0, 0, 228, 9, 0, 0, 62, 10, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 70, 10, 0, 0, 78, 10, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 82, 10, 0, 0, 90, 10, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 98, 10, 0, 0, 70, 5, 0, 0, 106, 10, 0, 0, 137, 3, 0, 0, 112, 10, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 118, 10, 0, 0, 126, 10, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 131, 10, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 138, 10, 0, 0, 146, 10, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 149, 10, 0, 0, 70, 5, 0, 0, 157, 10, 0, 0, 161, 10, 0, 0, 169, 10, 0, 0, 137, 3, 0, 0, 176, 10, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 184, 10, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 188, 10, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 196, 10, 0, 0, 202, 10, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 208, 10, 0, 0, 116, 10, 0, 0, 137, 3, 0, 0, 216, 10, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 101, 5, 0, 0, 0, 4, 0, 0, 208, 8, 0, 0, 63, 10, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 222, 10, 0, 0, 230, 10, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 238, 10, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 205, 7, 0, 0, 246, 10, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 250, 10, 0, 0, 137, 3, 0, 0, 0, 11, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 8, 10, 0, 0, 137, 3, 0, 0, 6, 11, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 14, 11, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 253, 4, 0, 0, 22, 11, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 117, 7, 0, 0, + 26, 11, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 34, 11, 0, 0, 41, 11, 0, 0, 41, 11, 0, 0, 215, 3, 0, 0, 49, 11, 0, 0, 200, 4, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 57, 11, 0, 0, 65, 11, 0, 0, 71, 11, 0, 0, 137, 3, 0, 0, 77, 11, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 85, 11, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 93, 11, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 101, 11, 0, 0, + 176, 3, 0, 0, 108, 11, 0, 0, 108, 11, 0, 0, 171, 3, 0, 0, 171, 3, 0, 0, 99, 11, 0, 0, 99, 11, 0, 0, 116, 11, 0, 0, 120, 11, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 215, 3, 0, 0, 128, 11, 0, 0, 215, 3, 0, 0, 135, 11, 0, 0, 142, 11, 0, 0, 150, 11, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 158, 11, 0, 0, 166, 11, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 8, 10, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 0, 11, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 66, 10, 0, 0, 137, 3, 0, 0, 171, 11, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 179, 11, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 184, 11, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 192, 11, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 134, 4, 0, 0, 134, 4, 0, 0, 134, 4, 0, 0, 134, 4, 0, 0, 134, 4, 0, 0, 134, 4, 0, 0, 200, 11, 0, 0, 137, 3, 0, 0, 134, 4, 0, 0, 134, 4, 0, 0, 208, 11, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 216, 11, 0, 0, 51, 4, 0, 0, 221, 11, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 114, 4, 0, 0, 229, 11, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 237, 11, 0, 0, 245, 11, 0, 0, 253, 11, 0, 0, 5, 12, 0, 0, 13, 12, 0, 0, 21, 12, 0, 0, 137, 3, 0, 0, 28, 12, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 38, 6, 0, 0, 36, 12, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 41, 6, 0, 0, 41, 12, 0, 0, 45, 12, 0, 0, 117, 7, 0, 0, 53, 12, 0, 0, 171, 3, 0, 0, 137, 3, 0, 0, 59, 12, 0, 0, 137, 3, 0, 0, 64, 12, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 250, 6, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, + 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 72, 12, 0, 0, 193, 8, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 79, 12, 0, 0, 87, 12, 0, 0, 36, 12, 0, 0, 38, 6, 0, 0, 95, 12, 0, 0, 38, 6, 0, 0, 103, 12, 0, 0, 108, 12, 0, 0, 116, 12, 0, 0, 137, 3, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, + 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 40, 6, 0, 0, 124, 12, 0, 0, 132, 12, 0, 0, 38, 6, 0, 0, 139, 12, 0, 0, 147, 12, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 155, 12, 0, 0, 38, 6, 0, 0, 38, 6, 0, 0, 160, 12, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 168, 12, 0, 0, 176, 12, 0, 0, 176, 12, 0, 0, 176, 12, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 215, 3, 0, 0, 215, 3, 0, 0, 215, 3, 0, 0, 215, 3, 0, 0, 215, 3, 0, 0, 215, 3, 0, 0, 215, 3, 0, 0, 184, 12, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, + 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 137, 3, 0, 0, 104, 3, 1, 0, 104, 3, 1, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, + 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 11, 0, 0, 0, 10, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 10, + 0, 0, 0, 10, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 13, 0, 0, 0, 13, 41, 0, 1, 13, 40, 0, 2, 13, 0, 0, 0, 13, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, + 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93, 0, 1, 13, 0, 0, 0, 13, 91, 0, 2, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 125, 0, 1, 13, 0, 0, 0, 13, 123, 0, 2, 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, + 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 7, 0, 0, 0, 13, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 2, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 2, 0, 0, 0, 7, 0, 0, 0, 2, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, + 0, 0, 0, 6, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 8, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 6, 0, 0, 0, 13, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 13, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 8, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 6, 0, 0, 0, 6, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 6, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 5, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, + 59, 15, 1, 13, 58, 15, 2, 13, 61, 15, 1, 13, 60, 15, 2, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 156, 22, 1, 13, + 155, 22, 2, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 9, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 14, 0, 0, 0, 16, 0, 0, 0, 18, 0, 0, 0, 15, 0, 0, 0, 17, 0, 0, 0, 7, 0, 0, 0, 5, + 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 7, 70, 32, 1, 13, 69, 32, 2, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 12, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 19, + 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 22, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 13, 126, 32, 1, 13, 125, 32, 2, 13, 0, 0, 0, 0, + 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 13, 142, 32, 1, 13, 141, 32, 2, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, + 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 9, 35, 1, 13, 8, 35, 2, 13, 11, 35, 1, 13, 10, 35, 2, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 42, 35, 1, 13, 41, 35, 2, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, + 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 105, 39, 1, 13, 104, 39, 2, 13, 107, 39, 1, 13, 106, 39, 2, 13, 109, 39, 1, 13, 108, 39, 2, 13, 111, 39, 1, 13, 110, 39, 2, 13, 113, 39, 1, 13, 112, 39, 2, 13, 115, 39, 1, 13, 114, 39, 2, 13, 117, 39, 1, 13, 116, 39, 2, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 198, 39, 1, 13, 197, 39, 2, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 231, 39, 1, 13, 230, 39, 2, 13, 233, 39, 1, 13, 232, 39, 2, 13, 235, 39, 1, 13, 234, 39, 2, 13, 237, 39, 1, 13, 236, 39, 2, 13, 239, 39, 1, 13, 238, 39, 2, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 132, 41, 1, 13, 131, 41, 2, 13, 134, 41, 1, 13, 133, 41, 2, 13, 136, 41, 1, 13, 135, 41, 2, 13, 138, 41, 1, 13, 137, 41, 2, 13, 140, 41, 1, 13, 139, 41, 2, 13, 144, 41, 1, 13, 143, 41, 2, 13, 142, 41, 1, 13, 141, 41, 2, 13, + 146, 41, 1, 13, 145, 41, 2, 13, 148, 41, 1, 13, 147, 41, 2, 13, 150, 41, 1, 13, 149, 41, 2, 13, 152, 41, 1, 13, 151, 41, 2, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 217, 41, 1, 13, 216, 41, 2, 13, 219, 41, 1, 13, 218, 41, 2, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 253, 41, 1, 13, 252, 41, 2, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 13, 0, 0, 0, 13, 35, 46, 1, 13, 34, 46, 2, 13, 37, 46, 1, 13, 36, 46, 2, 13, 39, 46, 1, 13, 38, 46, 2, 13, + 41, 46, 1, 13, 40, 46, 2, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 86, 46, 1, 13, 85, 46, 2, 13, 88, 46, 1, 13, 87, 46, 2, 13, 90, 46, 1, 13, 89, 46, 2, 13, 92, 46, 1, 13, 91, 46, 2, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 12, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 48, 1, 13, 8, 48, 2, 13, 11, 48, 1, 13, 10, 48, 2, 13, 13, 48, 1, 13, 12, 48, 2, 13, 15, 48, 1, 13, 14, 48, 2, 13, 17, 48, 1, 13, 16, 48, 2, 13, 0, 0, 0, 13, 0, 0, 0, 13, 21, 48, 1, 13, 20, 48, 2, 13, 23, 48, 1, 13, 22, 48, 2, 13, 25, 48, 1, 13, 24, 48, 2, 13, 27, 48, 1, 13, 26, 48, 2, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 13, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 7, 0, 0, 0, 13, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 7, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 90, 254, 1, 13, 89, 254, 2, 13, 92, 254, 1, 13, 91, 254, 2, 13, 94, 254, 1, 13, 93, 254, 2, 13, 0, 0, 0, 5, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, + 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 13, 0, 0, 0, 13, 9, 255, 1, 13, 8, 255, 2, 13, 0, 0, 0, 13, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, + 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 61, 255, 1, 13, 0, 0, 0, 13, 59, 255, 2, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93, 255, 1, 13, 0, 0, 0, 13, 91, 255, 2, 13, 0, 0, 0, 13, 96, 255, 1, 13, 95, 255, 2, 13, 0, 0, 0, 13, 99, 255, 1, 13, 98, 255, 2, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, + 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, + 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 13, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, + 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 13, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, + 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, + 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, + 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, + 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, + 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, + 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, + 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, + 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, + 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, + 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, + 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/EastAsianWidthTrie.Generated.cs b/SixLabors.Fonts/Unicode/Resources/EastAsianWidthTrie.Generated.cs new file mode 100644 index 0000000..82d7f04 --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/EastAsianWidthTrie.Generated.cs @@ -0,0 +1,329 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class EastAsianWidthTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 0, 17, 0, 0, 0, 0, 0, 80, 122, 0, 0, 229, 3, 0, 0, 237, 3, 0, 0, 245, 3, 0, 0, 253, 3, 0, 0, 13, 4, 0, 0, 21, 4, 0, 0, 29, 4, 0, 0, 37, 4, 0, 0, 45, 4, 0, 0, 53, 4, 0, 0, 61, 4, 0, 0, 69, 4, 0, 0, 5, 4, 0, 0, 13, 4, 0, 0, 74, 4, 0, 0, 82, 4, 0, 0, 5, 4, 0, 0, 13, 4, 0, 0, 86, 4, 0, 0, 94, 4, 0, 0, 5, 4, 0, 0, 13, 4, 0, 0, + 101, 4, 0, 0, 109, 4, 0, 0, 117, 4, 0, 0, 125, 4, 0, 0, 121, 4, 0, 0, 129, 4, 0, 0, 133, 4, 0, 0, 141, 4, 0, 0, 149, 4, 0, 0, 157, 4, 0, 0, 165, 4, 0, 0, 173, 4, 0, 0, 177, 4, 0, 0, 185, 4, 0, 0, 5, 4, 0, 0, 13, 4, 0, 0, 5, 4, 0, 0, 13, 4, 0, 0, 5, 4, 0, 0, 13, 4, 0, 0, 5, 4, 0, 0, 13, 4, 0, 0, 5, 4, 0, 0, 13, 4, 0, 0, 5, 4, 0, 0, + 13, 4, 0, 0, 5, 4, 0, 0, 13, 4, 0, 0, 5, 4, 0, 0, 13, 4, 0, 0, 5, 4, 0, 0, 13, 4, 0, 0, 5, 4, 0, 0, 13, 4, 0, 0, 5, 4, 0, 0, 13, 4, 0, 0, 5, 4, 0, 0, 13, 4, 0, 0, 5, 4, 0, 0, 13, 4, 0, 0, 5, 4, 0, 0, 13, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, + 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, + 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, + 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, + 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, + 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, + 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 201, 4, 0, 0, 209, 4, 0, 0, 229, 3, 0, 0, 217, 4, 0, 0, 225, 4, 0, 0, 231, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 239, 4, 0, 0, 247, 4, 0, 0, 251, 4, 0, 0, 3, 5, 0, 0, 10, 5, 0, 0, 17, 5, 0, 0, 24, 5, 0, 0, 31, 5, 0, 0, + 39, 5, 0, 0, 47, 5, 0, 0, 55, 5, 0, 0, 63, 5, 0, 0, 71, 5, 0, 0, 78, 5, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 86, 5, 0, 0, 93, 5, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 99, 5, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 107, 5, 0, 0, 117, 4, 0, 0, + 117, 4, 0, 0, 112, 5, 0, 0, 128, 4, 0, 0, 116, 5, 0, 0, 124, 5, 0, 0, 131, 5, 0, 0, 139, 5, 0, 0, 147, 5, 0, 0, 155, 5, 0, 0, 163, 5, 0, 0, 171, 5, 0, 0, 179, 5, 0, 0, 187, 5, 0, 0, 195, 5, 0, 0, 203, 5, 0, 0, 211, 5, 0, 0, 217, 5, 0, 0, 225, 5, 0, 0, 231, 5, 0, 0, 239, 5, 0, 0, 245, 5, 0, 0, 229, 3, 0, 0, 253, 5, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 4, 6, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 6, 6, 0, 0, 229, 3, 0, 0, 14, 6, 0, 0, + 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, + 22, 6, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 196, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 29, 6, 0, 0, 189, 4, 0, 0, 37, 6, 0, 0, 38, 6, 0, 0, 46, 6, 0, 0, 193, 4, 0, 0, 40, 6, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 54, 6, 0, 0, 58, 6, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 63, 6, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 70, 6, 0, 0, 38, 6, 0, 0, 193, 4, 0, 0, 76, 6, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 81, 6, 0, 0, 193, 4, 0, 0, 88, 6, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, + 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 96, 6, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 104, 6, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, + 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, + 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, + 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, + 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, + 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, + 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, + 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, + 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 112, 6, 0, 0, 189, 4, 0, 0, 120, 6, 0, 0, 127, 6, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 135, 6, 0, 0, 136, 6, 0, 0, 136, 6, 0, 0, 144, 6, 0, 0, 145, 6, 0, 0, 146, 6, 0, 0, 154, 6, 0, 0, + 162, 6, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 20, 16, 0, 0, 20, 16, 0, 0, 52, 16, 0, 0, 116, 16, 0, 0, 180, 16, 0, 0, 244, 16, 0, 0, 20, 16, 0, 0, 40, 17, 0, 0, 20, 16, 0, 0, 88, 17, 0, 0, 20, 16, 0, 0, 148, 17, 0, 0, 212, 17, 0, 0, 228, 17, 0, 0, 20, 18, 0, 0, 84, 18, 0, 0, 148, 18, 0, 0, + 196, 18, 0, 0, 20, 16, 0, 0, 20, 16, 0, 0, 20, 16, 0, 0, 20, 16, 0, 0, 20, 16, 0, 0, 20, 16, 0, 0, 20, 16, 0, 0, 20, 16, 0, 0, 20, 16, 0, 0, 20, 16, 0, 0, 20, 16, 0, 0, 20, 16, 0, 0, 20, 16, 0, 0, 20, 16, 0, 0, 64, 10, 0, 0, 128, 10, 0, 0, 181, 10, 0, 0, 244, 10, 0, 0, 52, 11, 0, 0, 95, 11, 0, 0, 159, 11, 0, 0, 192, 6, 0, 0, 194, 11, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 250, 11, 0, 0, 41, 12, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 105, 12, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 153, 12, 0, 0, 217, 12, 0, 0, 246, 12, 0, 0, 192, 6, 0, 0, 28, 13, 0, 0, 91, 13, 0, 0, 154, 13, 0, 0, 218, 13, 0, 0, 19, 14, 0, 0, 83, 14, 0, 0, 147, 14, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, + 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, + 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 211, 14, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, + 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 211, 14, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 19, 15, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 83, 15, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 83, 15, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, + 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, + 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, + 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, + 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, + 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, + 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, + 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, + 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 170, 6, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 29, 6, 0, 0, 176, 6, 0, 0, 38, 6, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 85, 6, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 184, 6, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 192, 6, 0, 0, 197, 6, 0, 0, 204, 6, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 194, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 208, 6, 0, 0, 208, 6, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, + 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 215, 6, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, + 229, 3, 0, 0, 180, 6, 0, 0, 229, 3, 0, 0, 223, 6, 0, 0, 228, 6, 0, 0, 117, 4, 0, 0, 234, 6, 0, 0, 239, 6, 0, 0, 246, 6, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 254, 6, 0, 0, 194, 4, 0, 0, 4, 7, 0, 0, 12, 7, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 193, 4, 0, 0, 20, 7, 0, 0, 193, 4, 0, 0, 26, 7, 0, 0, 196, 4, 0, 0, 193, 4, 0, 0, + 34, 7, 0, 0, 42, 7, 0, 0, 193, 4, 0, 0, 38, 6, 0, 0, 50, 7, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 51, 7, 0, 0, 193, 4, 0, 0, 59, 7, 0, 0, 67, 7, 0, 0, 73, 7, 0, 0, 80, 7, 0, 0, 215, 6, 0, 0, 5, 4, 0, 0, 86, 7, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 197, 4, 0, 0, 5, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 93, 7, 0, 0, + 101, 7, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 109, 7, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 190, 4, 0, 0, 117, 7, 0, 0, 24, 7, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 125, 7, 0, 0, 133, 7, 0, 0, 193, 4, 0, 0, 140, 7, 0, 0, 148, 7, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, + 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 193, 4, 0, 0, 59, 7, 0, 0, 229, 3, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, + 117, 4, 0, 0, 117, 4, 0, 0, 129, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, 5, 4, 0, 0, + 5, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, + 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, + 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 117, 4, 0, 0, 156, 7, 0, 0, 228, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/EmojiTrie.Generated.cs b/SixLabors.Fonts/Unicode/Resources/EmojiTrie.Generated.cs new file mode 100644 index 0000000..3666178 --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/EmojiTrie.Generated.cs @@ -0,0 +1,226 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class EmojiTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 8, 14, 0, 0, 0, 0, 0, 32, 82, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 184, 2, 0, 0, 192, 2, 0, 0, 206, 2, 0, 0, 214, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, + 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, + 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 191, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 9, 5, 0, 0, 218, 2, 0, 0, 226, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 17, 5, 0, 0, 183, 2, 0, 0, 234, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 241, 2, 0, 0, 248, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 251, 2, 0, 0, 2, 3, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 7, 3, 0, 0, 13, 3, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 217, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 20, 3, 0, 0, 28, 3, 0, 0, 30, 3, 0, 0, 38, 3, 0, 0, 46, 3, 0, 0, 54, 3, 0, 0, 62, 3, 0, 0, 70, 3, 0, 0, 78, 3, 0, 0, 86, 3, 0, 0, 92, 3, 0, 0, 100, 3, 0, 0, 107, 3, 0, 0, 114, 3, 0, 0, 122, 3, 0, 0, 125, 3, 0, 0, 133, 3, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 141, 3, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 148, 3, 0, 0, 183, 2, 0, 0, 156, 3, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 221, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 162, 3, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 14, 5, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 32, 11, 0, 0, 32, 11, 0, 0, 56, 11, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, + 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 220, 10, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 225, 9, 0, 0, 33, 10, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 96, 10, 0, 0, 169, 3, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 174, 3, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 178, 3, 0, 0, 186, 3, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 193, 3, 0, 0, 201, 3, 0, 0, 208, 3, 0, 0, + 215, 3, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 223, 3, 0, 0, 231, 3, 0, 0, 237, 3, 0, 0, 239, 3, 0, 0, 247, 3, 0, 0, 255, 3, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 23, 4, 0, 0, 31, 4, 0, 0, 39, 4, 0, 0, 46, 4, 0, 0, 54, 4, 0, 0, 62, 4, 0, 0, 70, 4, 0, 0, 78, 4, 0, 0, 86, 4, 0, 0, 91, 4, 0, 0, 99, 4, 0, 0, + 105, 4, 0, 0, 112, 4, 0, 0, 119, 4, 0, 0, 127, 4, 0, 0, 135, 4, 0, 0, 142, 4, 0, 0, 237, 3, 0, 0, 149, 4, 0, 0, 183, 2, 0, 0, 157, 4, 0, 0, 165, 4, 0, 0, 173, 4, 0, 0, 181, 4, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 189, 4, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 194, 4, 0, 0, 202, 4, 0, 0, 210, 4, 0, 0, 213, 4, 0, 0, 237, 3, 0, 0, 219, 4, 0, 0, 226, 4, 0, 0, 237, 3, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 234, 4, 0, 0, 242, 4, 0, 0, 237, 3, 0, 0, 250, 4, 0, 0, 2, 5, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 25, 5, 0, 0, 25, 5, 0, 0, 25, 5, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, + 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 183, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 241, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 241, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 241, 0, 0, 0, 241, 0, 0, 0, 241, 0, 0, 0, 241, 0, 0, 0, + 241, 0, 0, 0, 241, 0, 0, 0, 241, 0, 0, 0, 241, 0, 0, 0, 241, 0, 0, 0, 241, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 99, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 99, 0, 0, 0, + 99, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, + 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 99, 0, 0, 0, + 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 97, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 105, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, + 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 97, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 97, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 97, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 107, 0, 0, 0, 11, 0, 0, 0, 107, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 107, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, + 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 3, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 97, 0, 0, 0, 3, 0, 0, 0, 97, 0, 0, 0, 107, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 107, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, + 99, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, + 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 99, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 11, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, + 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, + 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, + 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/GraphemeTrie.Generated.cs b/SixLabors.Fonts/Unicode/Resources/GraphemeTrie.Generated.cs new file mode 100644 index 0000000..016cd97 --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/GraphemeTrie.Generated.cs @@ -0,0 +1,459 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class GraphemeTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 16, 14, 0, 0, 0, 0, 0, 48, 173, 0, 0, 100, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 124, 3, 0, 0, 148, 3, 0, 0, 156, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, + 108, 3, 0, 0, 116, 3, 0, 0, 164, 3, 0, 0, 172, 3, 0, 0, 168, 3, 0, 0, 176, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 184, 3, 0, 0, 192, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 196, 3, 0, 0, 204, 3, 0, 0, 212, 3, 0, 0, + 220, 3, 0, 0, 228, 3, 0, 0, 236, 3, 0, 0, 242, 3, 0, 0, 250, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 255, 3, 0, 0, 7, 4, 0, 0, 12, 4, 0, 0, 20, 4, 0, 0, 26, 4, 0, 0, 34, 4, 0, 0, 33, 4, 0, 0, 41, 4, 0, 0, 46, 4, 0, 0, 54, 4, 0, 0, 200, 4, 0, 0, 207, 4, 0, 0, 211, 4, 0, 0, 108, 3, 0, 0, 62, 4, 0, 0, 108, 3, 0, 0, 218, 4, 0, 0, 70, 4, 0, 0, + 226, 4, 0, 0, 228, 4, 0, 0, 236, 4, 0, 0, 244, 4, 0, 0, 252, 4, 0, 0, 253, 4, 0, 0, 5, 5, 0, 0, 13, 5, 0, 0, 21, 5, 0, 0, 22, 5, 0, 0, 30, 5, 0, 0, 35, 5, 0, 0, 21, 5, 0, 0, 22, 5, 0, 0, 43, 5, 0, 0, 51, 5, 0, 0, 252, 4, 0, 0, 59, 5, 0, 0, 67, 5, 0, 0, 244, 4, 0, 0, 75, 5, 0, 0, 227, 4, 0, 0, 83, 5, 0, 0, 108, 3, 0, 0, 91, 5, 0, 0, + 59, 5, 0, 0, 99, 5, 0, 0, 244, 4, 0, 0, 252, 4, 0, 0, 105, 5, 0, 0, 113, 5, 0, 0, 121, 5, 0, 0, 129, 5, 0, 0, 131, 5, 0, 0, 78, 4, 0, 0, 244, 4, 0, 0, 252, 4, 0, 0, 108, 3, 0, 0, 139, 5, 0, 0, 196, 8, 0, 0, 108, 3, 0, 0, 147, 5, 0, 0, 154, 5, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 158, 5, 0, 0, 166, 5, 0, 0, 108, 3, 0, 0, 170, 5, 0, 0, 177, 5, 0, 0, + 108, 3, 0, 0, 185, 5, 0, 0, 193, 5, 0, 0, 200, 5, 0, 0, 74, 5, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 208, 5, 0, 0, 216, 5, 0, 0, 224, 5, 0, 0, 232, 5, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 231, 8, 0, 0, 231, 8, 0, 0, 231, 8, 0, 0, 240, 8, 0, 0, 240, 8, 0, 0, 246, 8, 0, 0, 21, 9, 0, 0, 21, 9, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 193, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 240, 5, 0, 0, 246, 5, 0, 0, 47, 5, 0, 0, 47, 5, 0, 0, 108, 3, 0, 0, 252, 5, 0, 0, 4, 6, 0, 0, 108, 3, 0, 0, 150, 4, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 103, 5, 0, 0, + 9, 6, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 17, 6, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 24, 6, 0, 0, 108, 3, 0, 0, 31, 6, 0, 0, 39, 6, 0, 0, 108, 3, 0, 0, 160, 3, 0, 0, 47, 6, 0, 0, 55, 6, 0, 0, 63, 6, 0, 0, 66, 6, 0, 0, 74, 6, 0, 0, 80, 6, 0, 0, 88, 6, 0, 0, 96, 6, 0, 0, + 108, 3, 0, 0, 103, 6, 0, 0, 108, 3, 0, 0, 110, 6, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 116, 6, 0, 0, 124, 6, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 164, 3, 0, 0, 164, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 156, 4, 0, 0, 162, 4, 0, 0, 142, 6, 0, 0, 104, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 160, 3, 0, 0, 203, 5, 0, 0, 108, 3, 0, 0, 44, 9, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 51, 9, 0, 0, 58, 9, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 61, 9, 0, 0, 68, 9, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 73, 9, 0, 0, 79, 9, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 87, 9, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 93, 9, 0, 0, 101, 9, 0, 0, 103, 9, 0, 0, 111, 9, 0, 0, 119, 9, 0, 0, 127, 9, 0, 0, 135, 9, 0, 0, 143, 9, 0, 0, 151, 9, 0, 0, 159, 9, 0, 0, 165, 9, 0, 0, 173, 9, 0, 0, 180, 9, 0, 0, 187, 9, 0, 0, 195, 9, 0, 0, 198, 9, 0, 0, 206, 9, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 214, 9, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 221, 9, 0, 0, 108, 3, 0, 0, 229, 9, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 131, 6, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 237, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 164, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 137, 6, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 145, 6, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 235, 9, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 152, 6, 0, 0, 35, 4, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 172, 5, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 160, 6, 0, 0, 168, 6, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 204, 8, 0, 0, 207, 8, 0, 0, 176, 6, 0, 0, 184, 6, 0, 0, 108, 3, 0, 0, 192, 6, 0, 0, 199, 6, 0, 0, 232, 8, 0, 0, 226, 4, 0, 0, 204, 6, 0, 0, 45, 4, 0, 0, 212, 6, 0, 0, 108, 3, 0, 0, 218, 6, 0, 0, 226, 6, 0, 0, 230, 6, 0, 0, 108, 3, 0, 0, 238, 6, 0, 0, 16, 4, 0, 0, 246, 6, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 254, 6, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, + 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, + 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, + 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, + 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, + 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, + 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, + 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, + 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, + 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, + 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, + 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, + 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, + 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 36, 9, 0, 0, 30, 9, 0, 0, 31, 9, 0, 0, 32, 9, 0, 0, + 33, 9, 0, 0, 34, 9, 0, 0, 35, 9, 0, 0, 254, 8, 0, 0, 5, 9, 0, 0, 22, 9, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 2, 7, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 176, 3, 0, 0, 176, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 124, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 35, 4, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 170, 4, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 16, 14, 0, 0, 16, 14, 0, 0, 80, 14, 0, 0, 176, 13, 0, 0, 176, 13, 0, 0, 176, 13, 0, 0, 176, 13, 0, 0, 176, 13, 0, 0, 176, 13, 0, 0, 176, 13, 0, 0, 176, 13, 0, 0, 176, 13, 0, 0, 144, 14, 0, 0, 160, 14, 0, 0, 176, 13, 0, 0, 176, 13, 0, 0, 176, 13, 0, 0, + 176, 13, 0, 0, 224, 14, 0, 0, 176, 13, 0, 0, 176, 13, 0, 0, 176, 13, 0, 0, 16, 15, 0, 0, 80, 15, 0, 0, 144, 15, 0, 0, 200, 15, 0, 0, 176, 13, 0, 0, 252, 15, 0, 0, 48, 16, 0, 0, 104, 16, 0, 0, 132, 16, 0, 0, 184, 16, 0, 0, 68, 11, 0, 0, 116, 11, 0, 0, 226, 9, 0, 0, 33, 10, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 92, 10, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, + 149, 1, 0, 0, 149, 1, 0, 0, 177, 11, 0, 0, 218, 11, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 127, 10, 0, 0, 149, 1, 0, 0, 26, 12, 0, 0, 180, 10, 0, 0, 85, 12, 0, 0, 149, 12, 0, 0, 207, 12, 0, 0, 15, 13, 0, 0, 79, 13, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, + 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, + 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, + 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, + 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, + 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, + 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, + 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, + 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, + 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, + 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, + 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, + 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, + 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, + 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, + 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, + 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 149, 1, 0, 0, 244, 10, 0, 0, 4, 11, 0, 0, 60, 7, 0, 0, 18, 4, 0, 0, 27, 4, 0, 0, 64, 7, 0, 0, 88, 6, 0, 0, 84, 4, 0, 0, 92, 4, 0, 0, 108, 3, 0, 0, 28, 4, 0, 0, 70, 7, 0, 0, 215, 8, 0, 0, 76, 7, 0, 0, 88, 6, 0, 0, 81, 7, 0, 0, 100, 4, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 89, 7, 0, 0, 16, 4, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 237, 3, 0, 0, 97, 7, 0, 0, 129, 5, 0, 0, 131, 5, 0, 0, 105, 7, 0, 0, 113, 7, 0, 0, 108, 3, 0, 0, 119, 7, 0, 0, 108, 4, 0, 0, 104, 5, 0, 0, 108, 3, 0, 0, 127, 7, 0, 0, 135, 7, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 143, 7, 0, 0, 151, 7, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 156, 7, 0, 0, 164, 7, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 172, 7, 0, 0, 45, 4, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 180, 7, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 186, 7, 0, 0, 194, 7, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 199, 7, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 113, 4, 0, 0, 121, 4, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 206, 7, 0, 0, 214, 7, 0, 0, 222, 7, 0, 0, 226, 7, 0, 0, 234, 7, 0, 0, 108, 3, 0, 0, 128, 4, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 242, 7, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 247, 7, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 255, 7, 0, 0, 5, 8, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 11, 8, 0, 0, 136, 4, 0, 0, 108, 3, 0, 0, 19, 8, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 25, 8, 0, 0, 144, 4, 0, 0, 31, 8, 0, 0, 39, 8, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 177, 4, 0, 0, 47, 8, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 89, 8, 0, 0, 107, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 185, 4, 0, 0, 105, 8, 0, 0, 242, 5, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 113, 8, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 148, 3, 0, 0, 164, 3, 0, 0, 164, 3, 0, 0, 164, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 164, 3, 0, 0, 164, 3, 0, 0, 164, 3, 0, 0, 164, 3, 0, 0, 164, 3, 0, 0, 164, 3, 0, 0, 164, 3, 0, 0, 192, 4, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, + 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, + 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, + 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 148, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 10, 7, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 45, 4, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 18, 7, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 26, 7, 0, 0, 30, 7, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 103, 5, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 28, 7, 0, 0, 108, 3, 0, 0, 38, 7, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 44, 7, 0, 0, 108, 3, 0, 0, 36, 4, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 41, 4, 0, 0, 108, 3, 0, 0, 52, 7, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 35, 4, 0, 0, 55, 8, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 59, 8, 0, 0, 108, 3, 0, 0, 65, 8, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 13, 9, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 71, 8, 0, 0, 223, 8, 0, 0, 77, 8, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 84, 8, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 164, 3, 0, 0, 97, 8, 0, 0, 27, 4, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 164, 3, 0, 0, 121, 8, 0, 0, 164, 3, 0, 0, 128, 8, 0, 0, 135, 8, 0, 0, 143, 8, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 151, 8, 0, 0, + 159, 8, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 77, 7, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 65, 8, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 6, 7, 0, 0, 108, 3, 0, 0, 164, 8, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 164, 8, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 48, 5, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 172, 8, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 65, 8, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 167, 5, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 100, 9, 0, 0, 242, 9, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, + 246, 9, 0, 0, 224, 9, 0, 0, 254, 9, 0, 0, 3, 10, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 11, 10, 0, 0, 19, 10, 0, 0, 26, 10, 0, 0, 30, 10, 0, 0, 188, 8, 0, 0, 38, 10, 0, 0, 45, 10, 0, 0, 53, 10, 0, 0, 28, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 61, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 64, 10, 0, 0, + 30, 10, 0, 0, 30, 10, 0, 0, 180, 8, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 72, 10, 0, 0, 30, 10, 0, 0, 80, 10, 0, 0, 88, 10, 0, 0, 94, 10, 0, 0, 101, 10, 0, 0, 108, 10, 0, 0, 116, 10, 0, 0, 124, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 131, 10, 0, 0, 108, 3, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, + 139, 10, 0, 0, 146, 10, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 154, 10, 0, 0, 30, 10, 0, 0, 242, 9, 0, 0, 108, 3, 0, 0, 162, 10, 0, 0, 108, 3, 0, 0, 170, 10, 0, 0, 175, 10, 0, 0, 183, 10, 0, 0, 30, 10, 0, 0, 191, 10, 0, 0, 194, 10, 0, 0, 78, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, + 30, 10, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 202, 10, 0, 0, 26, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 108, 3, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, + 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 30, 10, 0, 0, 80, 10, 0, 0, 99, 3, 1, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, + 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, + 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, + 255, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, + 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 14, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, + 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 7, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/IndicConjunctBreakTrie.Generated.cs b/SixLabors.Fonts/Unicode/Resources/IndicConjunctBreakTrie.Generated.cs new file mode 100644 index 0000000..2cb06c4 --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/IndicConjunctBreakTrie.Generated.cs @@ -0,0 +1,365 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class IndicConjunctBreakTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 8, 14, 0, 0, 0, 0, 0, 96, 136, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, 81, 3, 0, 0, 89, 3, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, + 65, 3, 0, 0, 73, 3, 0, 0, 113, 3, 0, 0, 121, 3, 0, 0, 117, 3, 0, 0, 125, 3, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, 133, 3, 0, 0, 141, 3, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, 145, 3, 0, 0, 153, 3, 0, 0, 161, 3, 0, 0, + 169, 3, 0, 0, 173, 3, 0, 0, 181, 3, 0, 0, 187, 3, 0, 0, 195, 3, 0, 0, 65, 3, 0, 0, 73, 3, 0, 0, 200, 3, 0, 0, 208, 3, 0, 0, 212, 3, 0, 0, 220, 3, 0, 0, 226, 3, 0, 0, 234, 3, 0, 0, 233, 3, 0, 0, 241, 3, 0, 0, 246, 3, 0, 0, 254, 3, 0, 0, 192, 5, 0, 0, 199, 5, 0, 0, 203, 5, 0, 0, 65, 3, 0, 0, 184, 3, 0, 0, 65, 3, 0, 0, 210, 5, 0, 0, 218, 5, 0, 0, + 165, 4, 0, 0, 171, 4, 0, 0, 6, 4, 0, 0, 179, 4, 0, 0, 187, 4, 0, 0, 193, 4, 0, 0, 14, 4, 0, 0, 201, 4, 0, 0, 226, 5, 0, 0, 227, 5, 0, 0, 235, 5, 0, 0, 240, 5, 0, 0, 209, 4, 0, 0, 215, 4, 0, 0, 22, 4, 0, 0, 223, 4, 0, 0, 187, 4, 0, 0, 231, 4, 0, 0, 30, 4, 0, 0, 239, 4, 0, 0, 248, 5, 0, 0, 249, 5, 0, 0, 1, 6, 0, 0, 65, 3, 0, 0, 247, 4, 0, 0, + 253, 4, 0, 0, 38, 4, 0, 0, 9, 6, 0, 0, 216, 3, 0, 0, 10, 6, 0, 0, 18, 6, 0, 0, 9, 6, 0, 0, 5, 5, 0, 0, 11, 5, 0, 0, 46, 4, 0, 0, 9, 6, 0, 0, 216, 3, 0, 0, 65, 3, 0, 0, 24, 6, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 32, 6, 0, 0, 39, 6, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 43, 6, 0, 0, 51, 6, 0, 0, 65, 3, 0, 0, 55, 6, 0, 0, 62, 6, 0, 0, + 65, 3, 0, 0, 69, 6, 0, 0, 77, 6, 0, 0, 84, 6, 0, 0, 247, 5, 0, 0, 65, 3, 0, 0, 19, 5, 0, 0, 54, 4, 0, 0, 27, 5, 0, 0, 35, 5, 0, 0, 43, 5, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 142, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 92, 6, 0, 0, 98, 6, 0, 0, 104, 6, 0, 0, 104, 6, 0, 0, 19, 5, 0, 0, 51, 5, 0, 0, 62, 4, 0, 0, 65, 3, 0, 0, 110, 6, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 117, 6, 0, 0, + 214, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 125, 6, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 132, 6, 0, 0, 19, 5, 0, 0, 59, 5, 0, 0, 70, 4, 0, 0, 65, 3, 0, 0, 109, 3, 0, 0, 140, 6, 0, 0, 148, 6, 0, 0, 67, 5, 0, 0, 72, 5, 0, 0, 78, 4, 0, 0, 154, 6, 0, 0, 80, 5, 0, 0, 86, 4, 0, 0, + 65, 3, 0, 0, 161, 6, 0, 0, 65, 3, 0, 0, 166, 6, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 172, 6, 0, 0, 180, 6, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 113, 3, 0, 0, 113, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 213, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 109, 3, 0, 0, 87, 6, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 187, 6, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 182, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 113, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 193, 6, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 197, 6, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 204, 6, 0, 0, 235, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 57, 6, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 212, 6, 0, 0, 219, 6, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 226, 6, 0, 0, 234, 6, 0, 0, 65, 3, 0, 0, 242, 6, 0, 0, 249, 6, 0, 0, 65, 3, 0, 0, 88, 5, 0, 0, 92, 5, 0, 0, 94, 4, 0, 0, 100, 5, 0, 0, 65, 3, 0, 0, 255, 6, 0, 0, 7, 7, 0, 0, 108, 5, 0, 0, 65, 3, 0, 0, 11, 7, 0, 0, 216, 3, 0, 0, 102, 4, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 116, 5, 0, 0, 19, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 249, 5, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 125, 3, 0, 0, 125, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 235, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 132, 13, 0, 0, 132, 13, 0, 0, 4, 13, 0, 0, 4, 13, 0, 0, 4, 13, 0, 0, 4, 13, 0, 0, 4, 13, 0, 0, 4, 13, 0, 0, 4, 13, 0, 0, 4, 13, 0, 0, 4, 13, 0, 0, 4, 13, 0, 0, 196, 13, 0, 0, 212, 13, 0, 0, 4, 13, 0, 0, 4, 13, 0, 0, 4, 13, 0, 0, + 4, 13, 0, 0, 20, 14, 0, 0, 4, 13, 0, 0, 4, 13, 0, 0, 4, 13, 0, 0, 68, 14, 0, 0, 132, 14, 0, 0, 180, 14, 0, 0, 236, 14, 0, 0, 4, 13, 0, 0, 32, 15, 0, 0, 80, 15, 0, 0, 136, 15, 0, 0, 164, 15, 0, 0, 216, 15, 0, 0, 155, 10, 0, 0, 225, 9, 0, 0, 33, 10, 0, 0, 96, 10, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 185, 10, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 241, 10, 0, 0, 26, 11, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 90, 11, 0, 0, 8, 1, 0, 0, 127, 11, 0, 0, 186, 11, 0, 0, 234, 11, 0, 0, 42, 12, 0, 0, 100, 12, 0, 0, 133, 12, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 196, 12, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 124, 5, 0, 0, 110, 4, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 117, 6, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 38, 7, 0, 0, 65, 3, 0, 0, + 44, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 50, 7, 0, 0, 65, 3, 0, 0, 236, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 241, 3, 0, 0, 65, 3, 0, 0, 58, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 216, 3, 0, 0, 107, 3, 0, 0, 178, 3, 0, 0, 62, 7, 0, 0, + 135, 3, 0, 0, 70, 7, 0, 0, 248, 5, 0, 0, 65, 3, 0, 0, 132, 5, 0, 0, 118, 4, 0, 0, 140, 5, 0, 0, 77, 7, 0, 0, 135, 3, 0, 0, 82, 7, 0, 0, 90, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 95, 7, 0, 0, 216, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 182, 3, 0, 0, 103, 7, 0, 0, 135, 3, 0, 0, 106, 7, 0, 0, 1, 6, 0, 0, 114, 7, 0, 0, 148, 5, 0, 0, + 152, 5, 0, 0, 126, 4, 0, 0, 226, 5, 0, 0, 65, 3, 0, 0, 107, 3, 0, 0, 122, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 130, 7, 0, 0, 138, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 143, 7, 0, 0, 151, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 159, 7, 0, 0, 245, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 167, 7, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 173, 7, 0, 0, 181, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 186, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 160, 5, 0, 0, 134, 4, 0, 0, 194, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 197, 7, 0, 0, 245, 3, 0, 0, + 168, 5, 0, 0, 172, 5, 0, 0, 142, 4, 0, 0, 19, 5, 0, 0, 149, 4, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 205, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 209, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 217, 7, 0, 0, 223, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 229, 7, 0, 0, 237, 7, 0, 0, 65, 3, 0, 0, 241, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 48, 7, 0, 0, 180, 5, 0, 0, 185, 5, 0, 0, 157, 4, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 23, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 245, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 31, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 249, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 235, 3, 0, 0, 1, 8, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 5, 8, 0, 0, 65, 3, 0, 0, 11, 8, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 78, 7, 0, 0, 65, 3, 0, 0, 17, 8, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 24, 8, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 29, 8, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 113, 3, 0, 0, 37, 8, 0, 0, 178, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 45, 8, 0, 0, 53, 8, 0, 0, 94, 6, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 61, 8, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 113, 3, 0, 0, 69, 8, 0, 0, 113, 3, 0, 0, 76, 8, 0, 0, 83, 8, 0, 0, 91, 8, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 99, 8, 0, 0, 107, 8, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 78, 7, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 11, 8, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 126, 7, 0, 0, 65, 3, 0, 0, 112, 8, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 112, 8, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 117, 8, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 125, 8, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 11, 8, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 52, 6, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 183, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 113, 3, 0, 0, 113, 3, 0, 0, 113, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 113, 3, 0, 0, 113, 3, 0, 0, 113, 3, 0, 0, 113, 3, 0, 0, 113, 3, 0, 0, 113, 3, 0, 0, 113, 3, 0, 0, 125, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, + 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 65, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/IndicPositionalCategoryTrie.Generated.cs b/SixLabors.Fonts/Unicode/Resources/IndicPositionalCategoryTrie.Generated.cs new file mode 100644 index 0000000..f69cb73 --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/IndicPositionalCategoryTrie.Generated.cs @@ -0,0 +1,271 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class IndicPositionalCategoryTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 112, 1, 0, 0, 0, 0, 0, 208, 99, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 100, 2, 0, 0, 108, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, + 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, + 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 92, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 132, 2, 0, 0, 134, 2, 0, 0, 142, 2, 0, 0, 253, 5, 0, 0, 150, 2, 0, 0, 151, 2, 0, 0, 159, 2, 0, 0, 70, 5, 0, 0, 167, 2, 0, 0, 151, 2, 0, 0, 175, 2, 0, 0, 78, 5, 0, 0, 167, 2, 0, 0, 151, 2, 0, 0, 183, 2, 0, 0, 86, 5, 0, 0, 150, 2, 0, 0, 187, 2, 0, 0, 195, 2, 0, 0, 253, 5, 0, 0, 94, 5, 0, 0, 201, 2, 0, 0, 209, 2, 0, 0, 84, 2, 0, 0, 217, 2, 0, 0, + 95, 5, 0, 0, 225, 2, 0, 0, 253, 5, 0, 0, 150, 2, 0, 0, 187, 2, 0, 0, 233, 2, 0, 0, 241, 2, 0, 0, 249, 2, 0, 0, 251, 2, 0, 0, 3, 3, 0, 0, 253, 5, 0, 0, 150, 2, 0, 0, 84, 2, 0, 0, 9, 3, 0, 0, 17, 3, 0, 0, 84, 2, 0, 0, 22, 3, 0, 0, 30, 3, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 34, 3, 0, 0, 62, 5, 0, 0, 84, 2, 0, 0, 255, 5, 0, 0, 42, 3, 0, 0, + 84, 2, 0, 0, 50, 3, 0, 0, 103, 5, 0, 0, 7, 6, 0, 0, 15, 6, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 58, 3, 0, 0, 66, 3, 0, 0, 74, 3, 0, 0, 82, 3, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 90, 3, 0, 0, 96, 3, 0, 0, 111, 5, 0, 0, 111, 5, 0, 0, 84, 2, 0, 0, 102, 3, 0, 0, 110, 3, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 118, 3, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 125, 3, 0, 0, 133, 3, 0, 0, 84, 2, 0, 0, 136, 3, 0, 0, 84, 2, 0, 0, 143, 3, 0, 0, 151, 3, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 159, 3, 0, 0, 162, 3, 0, 0, 170, 3, 0, 0, 117, 5, 0, 0, 178, 3, 0, 0, 186, 3, 0, 0, + 84, 2, 0, 0, 193, 3, 0, 0, 84, 2, 0, 0, 200, 3, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 122, 5, 0, 0, 208, 3, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 130, 5, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 137, 5, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 145, 5, 0, 0, 216, 3, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 224, 3, 0, 0, 227, 3, 0, 0, 234, 3, 0, 0, 153, 5, 0, 0, 84, 2, 0, 0, 161, 5, 0, 0, 241, 3, 0, 0, 84, 2, 0, 0, 132, 2, 0, 0, 246, 3, 0, 0, 52, 6, 0, 0, 168, 5, 0, 0, 84, 2, 0, 0, 254, 3, 0, 0, 6, 4, 0, 0, 10, 4, 0, 0, 84, 2, 0, 0, 18, 4, 0, 0, 169, 5, 0, 0, 26, 4, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 34, 4, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 208, 9, 0, 0, 208, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, + 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 80, 9, 0, 0, 8, 1, 0, 0, 14, 9, 0, 0, 78, 8, 0, 0, 141, 8, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 200, 8, 0, 0, 222, 8, 0, 0, 42, 4, 0, 0, 188, 5, 0, 0, 196, 5, 0, 0, 200, 5, 0, 0, 178, 3, 0, 0, 46, 4, 0, 0, 16, 6, 0, 0, 84, 2, 0, 0, 208, 5, 0, 0, 46, 5, 0, 0, 53, 4, 0, 0, 20, 6, 0, 0, 178, 3, 0, 0, 57, 4, 0, 0, 65, 4, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 70, 4, 0, 0, 28, 6, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 250, 2, 0, 0, 78, 4, 0, 0, 249, 2, 0, 0, 81, 4, 0, 0, 89, 4, 0, 0, 97, 4, 0, 0, 84, 2, 0, 0, 103, 4, 0, 0, 111, 4, 0, 0, 216, 5, 0, 0, 84, 2, 0, 0, 116, 4, 0, 0, 124, 4, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 132, 4, 0, 0, 140, 4, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 145, 4, 0, 0, + 36, 6, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 153, 4, 0, 0, 224, 5, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 161, 4, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 225, 5, 0, 0, 169, 4, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 174, 4, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 181, 4, 0, 0, 189, 4, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 193, 4, 0, 0, 54, 5, 0, 0, 233, 5, 0, 0, 201, 4, 0, 0, 209, 4, 0, 0, 84, 2, 0, 0, 216, 4, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 224, 4, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 229, 4, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 44, 6, 0, 0, 237, 4, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 237, 5, 0, 0, 245, 4, 0, 0, 84, 2, 0, 0, 251, 4, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 1, 5, 0, 0, 132, 2, 0, 0, 7, 5, 0, 0, + 15, 5, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 245, 5, 0, 0, 23, 5, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 31, 5, 0, 0, 39, 5, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 177, 5, 0, 0, 181, 5, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, + 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 84, 2, 0, 0, 83, 2, 1, 0, 83, 2, 1, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 12, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 13, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 7, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 13, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 15, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 9, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 8, 0, 0, 0, 14, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, + 3, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 12, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 15, 0, 0, 0, 7, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, + 3, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 14, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 8, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 8, 0, 0, 0, + 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 7, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 8, 0, 0, 0, 12, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 12, 0, 0, 0, 4, 0, 0, 0, 13, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, + 7, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, + 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 8, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 9, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/IndicShapingData.Generated.cs b/SixLabors.Fonts/Unicode/Resources/IndicShapingData.Generated.cs new file mode 100644 index 0000000..19eca2b --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/IndicShapingData.Generated.cs @@ -0,0 +1,726 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; +using System.Collections.Generic; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static partial class IndicShapingData + { + public static int[][] StateTable => new int[234][] + { + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,2,3,4,5,6,7,8,9,10,11,11,12,8,13,14,15,16,17,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,19,20,21,22,23,24,25,0,0,26,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,29,30,31,32,33,34,35,0,0,36,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,39,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,40,0,0,0,41,42,0,43,10,0,0,0,0,0,40,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,0,0,0,45,46,46,8,9,0,0,0,12,8,0,0,0,0,0,0,0,0,0,0,0,0,45,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,45,46,46,8,9,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,45,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,47,48,49,50,8,9,10,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,0,10,0,0,51,10,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,51 }, + new int[] { 0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,52,53,54,55,56,57,58,0,0,59,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,4,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,2,3,4,5,6,7,8,9,10,11,11,12,8,0,2,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,19,62,21,22,23,24,25,0,0,26,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,0,63,64,64,8,9,10,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,65,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,66,0,67,67,0,68,69,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,68 }, + new int[] { 0,2,0,0,0,0,0,0,0,0,11,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,70,71,0,0,41,42,0,43,10,0,0,0,0,0,70,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,0,0,72,20,73,73,23,24,25,0,0,0,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,74,0,0,0,75,76,0,77,25,0,0,0,0,0,74,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, + new int[] { 0,0,0,0,79,80,80,23,24,0,0,0,26,23,0,0,0,0,0,0,0,0,0,0,0,0,79,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,19,20,21,73,23,24,25,0,0,26,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,81,82,83,84,23,24,25,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,0,0,25,0,0,85,25,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85 }, + new int[] { 0,0,0,0,0,0,0,0,0,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,19,20,73,73,23,24,25,0,0,0,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,0,86,87,87,23,24,25,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,88,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,89,90,0,0,75,76,0,77,25,0,0,0,0,0,89,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, + new int[] { 0,0,0,91,30,92,32,33,34,35,0,0,0,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,93,0,0,0,94,95,0,96,35,0,0,0,0,0,93,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, + new int[] { 0,0,0,0,98,99,99,33,34,0,0,0,36,33,0,0,0,0,0,0,0,0,0,0,0,0,98,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,0,98,99,99,33,34,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,98,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,100,101,102,103,33,34,35,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,0,0,35,0,0,104,35,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,104 }, + new int[] { 0,0,0,0,0,0,0,0,0,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,29,30,92,32,33,34,35,0,0,0,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,0,105,106,106,33,34,35,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,107,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,108,109,0,0,94,95,0,96,35,0,0,0,0,0,108,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, + new int[] { 0,0,0,0,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,110,5,111,112,8,9,10,0,0,113,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,0,114,114,0,43,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,40,0,115,0,114,114,0,43,10,0,0,0,0,0,40,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,0,0,0,0,10,0,0,51,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,51 }, + new int[] { 0,116,71,0,0,0,0,0,0,0,0,0,0,0,0,116,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,40,0,0,0,114,42,0,43,10,0,0,0,0,0,40,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,0,0,0,0,46,46,8,117,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,117 }, + new int[] { 0,0,0,0,48,49,50,8,9,10,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,0,49,49,8,9,10,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,0,46,46,8,9,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,118,46,46,8,9,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,0,10,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,119,53,55,55,56,57,58,0,0,0,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,120,0,0,0,121,122,0,123,58,0,0,0,0,0,120,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, + new int[] { 0,0,0,0,125,126,126,56,57,0,0,0,59,56,0,0,0,0,0,0,0,0,0,0,0,0,125,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,0,125,126,126,56,57,0,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,125,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,127,128,129,130,56,57,58,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,0,0,58,0,0,131,58,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,131 }, + new int[] { 0,0,0,0,0,0,0,0,0,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,52,53,55,55,56,57,58,0,0,0,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,0,132,133,133,56,57,58,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,134,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,135,136,0,0,121,122,0,123,58,0,0,0,0,0,135,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, + new int[] { 0,74,3,4,5,137,138,8,139,140,0,11,12,8,0,74,15,0,0,0,0,0,0,0,0,0,141,0,0,0,0,0,0,0,0,139 }, + new int[] { 0,0,0,0,0,41,142,0,43,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,0,0,0,143,46,46,8,9,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,143,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,116,71,0,0,41,142,0,43,10,0,0,0,0,0,116,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,0,0,0,0,67,67,0,68,69,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,68 }, + new int[] { 0,0,0,0,0,0,0,0,68,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,68 }, + new int[] { 0,0,0,0,0,69,0,0,144,69,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,144 }, + new int[] { 0,0,0,0,0,0,0,0,0,69,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,145,5,146,147,8,9,10,0,0,148,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,0,114,114,0,43,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,0,0,0,20,73,73,23,24,25,0,0,0,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,0,79,80,80,23,24,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,79,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,149,20,150,151,23,24,25,0,0,152,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,0,0,153,153,0,77,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, + new int[] { 0,74,0,154,0,153,153,0,77,25,0,0,0,0,0,74,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, + new int[] { 0,0,0,0,0,25,0,0,85,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85 }, + new int[] { 0,155,90,0,0,0,0,0,0,0,0,0,0,0,0,155,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,74,0,0,0,153,76,0,77,25,0,0,0,0,0,74,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, + new int[] { 0,0,0,0,0,80,80,23,156,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,156 }, + new int[] { 0,0,0,0,82,83,84,23,24,25,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,0,0,83,83,23,24,25,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,0,0,80,80,23,24,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,0,157,80,80,23,24,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,0,0,25,0,0,0,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,75,158,0,77,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, + new int[] { 0,0,0,0,159,80,80,23,24,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,159,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,155,90,0,0,75,158,0,77,25,0,0,0,0,0,155,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, + new int[] { 0,0,0,160,20,161,162,23,24,25,0,0,163,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,0,0,153,153,0,77,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77 }, + new int[] { 0,0,0,0,30,92,32,33,34,35,0,0,0,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,0,98,99,99,33,34,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,98,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,164,30,165,166,33,34,35,0,0,167,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,0,0,168,168,0,96,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, + new int[] { 0,93,0,169,0,168,168,0,96,35,0,0,0,0,0,93,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, + new int[] { 0,0,0,0,0,35,0,0,104,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,104 }, + new int[] { 0,170,109,0,0,0,0,0,0,0,0,0,0,0,0,170,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,93,0,0,0,168,95,0,96,35,0,0,0,0,0,93,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, + new int[] { 0,0,0,0,0,99,99,33,171,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,171 }, + new int[] { 0,0,0,0,101,102,103,33,34,35,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,0,0,102,102,33,34,35,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,0,0,99,99,33,34,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,0,172,99,99,33,34,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,0,0,35,0,0,0,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,94,173,0,96,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, + new int[] { 0,0,0,0,174,99,99,33,34,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,174,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,170,109,0,0,94,173,0,96,35,0,0,0,0,0,170,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, + new int[] { 0,0,0,175,30,176,177,33,34,35,0,0,178,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,0,0,168,168,0,96,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96 }, + new int[] { 0,0,0,39,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,45,46,46,8,9,0,0,0,113,8,0,0,0,0,0,0,0,0,0,0,0,0,45,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,110,5,111,7,8,9,10,0,0,113,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,110,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,0,0,0,0,43,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,40,0,0,0,114,114,0,43,10,0,0,0,0,0,40,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,0,0,179,0,180,181,0,43,10,0,0,182,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,183,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,53,55,55,56,57,58,0,0,0,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,184,53,185,186,56,57,58,0,0,187,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,0,0,188,188,0,123,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, + new int[] { 0,120,0,189,0,188,188,0,123,58,0,0,0,0,0,120,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, + new int[] { 0,0,0,0,0,58,0,0,131,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,131 }, + new int[] { 0,190,136,0,0,0,0,0,0,0,0,0,0,0,0,190,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,120,0,0,0,188,122,0,123,58,0,0,0,0,0,120,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, + new int[] { 0,0,0,0,0,126,126,56,191,0,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,191 }, + new int[] { 0,0,0,0,128,129,130,56,57,58,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,0,0,129,129,56,57,58,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,0,0,126,126,56,57,0,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,0,192,126,126,56,57,0,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,0,0,58,0,0,0,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,121,193,0,123,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, + new int[] { 0,0,0,0,194,126,126,56,57,0,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,194,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,190,136,0,0,121,193,0,123,58,0,0,0,0,0,190,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, + new int[] { 0,0,0,195,53,196,197,56,57,58,0,0,198,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,0,0,188,188,0,123,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,123 }, + new int[] { 0,0,0,0,45,199,199,8,139,25,0,0,12,8,0,0,0,0,0,0,0,0,0,0,0,0,200,0,0,0,0,0,0,0,0,139 }, + new int[] { 0,74,0,154,45,199,199,8,139,25,0,0,0,8,0,74,0,0,0,0,0,0,0,0,0,0,200,0,0,0,0,0,0,0,0,139 }, + new int[] { 0,0,0,0,0,140,0,0,201,140,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,201 }, + new int[] { 0,0,0,0,0,0,0,0,0,140,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,202,203,0,0,41,42,0,43,10,0,0,0,0,0,202,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,0,0,41,0,114,114,0,43,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,0,0,0,0,114,142,0,43,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,0,0,0,0,69,0,0,0,69,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,39,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,45,46,46,8,9,0,0,0,148,8,0,0,0,0,0,0,0,0,0,0,0,0,45,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,145,5,146,7,8,9,10,0,0,148,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,145,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,72,20,73,73,23,24,25,0,0,0,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,0,79,80,80,23,24,0,0,0,152,23,0,0,0,0,0,0,0,0,0,0,0,0,79,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,149,20,150,73,23,24,25,0,0,152,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,149,20,73,73,23,24,25,0,0,0,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,0,0,0,0,0,77,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77 }, + new int[] { 0,74,0,0,0,153,153,0,77,25,0,0,0,0,0,74,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, + new int[] { 0,0,0,204,0,205,206,0,77,25,0,0,207,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,208,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,75,0,153,153,0,77,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, + new int[] { 0,0,0,0,0,153,158,0,77,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, + new int[] { 0,0,0,72,20,73,73,23,24,25,0,0,0,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,0,79,80,80,23,24,0,0,0,163,23,0,0,0,0,0,0,0,0,0,0,0,0,79,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,160,20,161,73,23,24,25,0,0,163,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,160,20,73,73,23,24,25,0,0,0,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, + new int[] { 0,0,0,209,30,92,92,33,34,35,0,0,0,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,0,98,99,99,33,34,0,0,0,167,33,0,0,0,0,0,0,0,0,0,0,0,0,98,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,164,30,165,92,33,34,35,0,0,167,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,164,30,92,92,33,34,35,0,0,0,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,0,0,0,0,0,96,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96 }, + new int[] { 0,93,0,0,0,168,168,0,96,35,0,0,0,0,0,93,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, + new int[] { 0,0,0,210,0,211,212,0,96,35,0,0,213,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,214,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,94,0,168,168,0,96,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, + new int[] { 0,0,0,0,0,168,173,0,96,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, + new int[] { 0,0,0,209,30,92,92,33,34,35,0,0,0,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,0,98,99,99,33,34,0,0,0,178,33,0,0,0,0,0,0,0,0,0,0,0,0,98,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,175,30,176,92,33,34,35,0,0,178,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,175,30,92,92,33,34,35,0,0,0,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,71,0,114,114,0,43,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,0,0,0,0,0,0,0,43,0,0,0,182,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,0,0,179,0,180,114,0,43,10,0,0,182,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,0,0,179,0,114,114,0,43,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,48,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,119,53,55,55,56,57,58,0,0,0,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,0,125,126,126,56,57,0,0,0,187,56,0,0,0,0,0,0,0,0,0,0,0,0,125,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,184,53,185,55,56,57,58,0,0,187,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,184,53,55,55,56,57,58,0,0,0,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,0,0,0,0,0,123,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,123 }, + new int[] { 0,120,0,0,0,188,188,0,123,58,0,0,0,0,0,120,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, + new int[] { 0,0,0,215,0,216,217,0,123,58,0,0,218,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,123 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,219,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,121,0,188,188,0,123,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, + new int[] { 0,0,0,0,0,188,193,0,123,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, + new int[] { 0,0,0,119,53,55,55,56,57,58,0,0,0,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,0,125,126,126,56,57,0,0,0,198,56,0,0,0,0,0,0,0,0,0,0,0,0,125,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,195,53,196,55,56,57,58,0,0,198,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,195,53,55,55,56,57,58,0,0,0,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, + new int[] { 0,0,0,0,0,46,46,8,220,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,220 }, + new int[] { 0,221,90,0,0,114,42,0,43,10,0,0,0,0,0,221,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, + new int[] { 0,0,0,0,0,140,0,0,0,140,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,222,5,223,224,8,139,140,0,0,225,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, + new int[] { 0,0,0,0,0,226,226,0,227,140,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,227 }, + new int[] { 0,0,0,90,0,153,153,0,77,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77 }, + new int[] { 0,0,0,0,0,0,0,0,77,0,0,0,207,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77 }, + new int[] { 0,0,0,204,0,205,153,0,77,25,0,0,207,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77 }, + new int[] { 0,0,0,204,0,153,153,0,77,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,82,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,30,92,92,33,34,35,0,0,0,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, + new int[] { 0,0,0,109,0,168,168,0,96,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96 }, + new int[] { 0,0,0,0,0,0,0,0,96,0,0,0,213,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96 }, + new int[] { 0,0,0,210,0,211,168,0,96,35,0,0,213,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96 }, + new int[] { 0,0,0,210,0,168,168,0,96,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,136,0,188,188,0,123,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,123 }, + new int[] { 0,0,0,0,0,0,0,0,123,0,0,0,218,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,123 }, + new int[] { 0,0,0,215,0,216,188,0,123,58,0,0,218,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,123 }, + new int[] { 0,0,0,215,0,188,188,0,123,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,123 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,25,0,0,85,25,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85 }, + new int[] { 0,0,0,228,5,229,230,8,139,140,0,0,231,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, + new int[] { 0,0,0,232,5,233,233,8,139,140,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, + new int[] { 0,0,0,0,45,46,46,8,139,0,0,0,225,8,0,0,0,0,0,0,0,0,0,0,0,0,45,0,0,0,0,0,0,0,0,139 }, + new int[] { 0,0,0,222,5,223,233,8,139,140,0,0,225,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, + new int[] { 0,0,0,222,5,233,233,8,139,140,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, + new int[] { 0,0,0,0,0,0,0,0,227,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,227 }, + new int[] { 0,0,0,0,0,140,0,0,201,140,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,201 }, + new int[] { 0,0,0,232,5,233,233,8,139,140,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, + new int[] { 0,0,0,0,45,46,46,8,139,0,0,0,231,8,0,0,0,0,0,0,0,0,0,0,0,0,45,0,0,0,0,0,0,0,0,139 }, + new int[] { 0,0,0,228,5,229,233,8,139,140,0,0,231,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, + new int[] { 0,0,0,228,5,233,233,8,139,140,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, + new int[] { 0,0,0,0,5,233,233,8,139,140,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, + new int[] { 0,0,0,0,45,46,46,8,139,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,45,0,0,0,0,0,0,0,0,139 } + }; + + public static bool[] AcceptingStates => new bool[] + { + false, + true, + true, + true, + true, + true, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + true, + true, + true, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + true, + false, + true, + true, + false, + false, + true, + true, + true, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + false, + true, + true, + false, + true, + true, + true, + true, + true, + false, + true, + true, + true, + true, + false, + true, + false, + true, + true, + false, + false, + true, + true, + false, + true, + true, + true, + true, + false, + true, + true, + true, + true, + false, + true, + false, + true, + true, + false, + false, + true, + true, + false, + true, + true, + true, + true, + false, + true, + true, + false, + true, + true, + false, + false, + true, + true, + true, + true, + true, + false, + true, + false, + true, + true, + false, + false, + true, + true, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + true, + true, + true, + false, + true, + true, + false, + true, + true, + false, + false, + true, + true, + true, + false, + true, + true, + true, + false, + true, + true, + false, + true, + true, + false, + false, + true, + true, + true, + false, + true, + true, + true, + false, + true, + true, + false, + true, + false, + true, + true, + false, + true, + true, + false, + false, + true, + true, + true, + false, + true, + true, + false, + true, + true, + true, + true, + true, + false, + true, + true, + false, + true, + true, + false, + true, + true, + false, + true, + false, + true, + true, + false, + true, + true, + true, + false, + true, + true, + false, + true, + true, + false, + true, + true, + true, + false + }; + + public static string[][] Tags => new string[234][] + { + Array.Empty(), + new string[] { "broken_cluster" }, + new string[] { "consonant_syllable" }, + new string[] { "vowel_syllable" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + Array.Empty(), + Array.Empty(), + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "standalone_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "consonant_syllable" }, + new string[] { "broken_cluster" }, + new string[] { "symbol_cluster" }, + Array.Empty(), + new string[] { "broken_cluster" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + Array.Empty(), + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + Array.Empty(), + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + Array.Empty(), + new string[] { "broken_cluster" }, + Array.Empty(), + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + Array.Empty(), + Array.Empty(), + new string[] { "broken_cluster" }, + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + Array.Empty(), + Array.Empty(), + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + new string[] { "consonant_syllable","broken_cluster" }, + new string[] { "broken_cluster" }, + Array.Empty(), + new string[] { "broken_cluster" }, + new string[] { "symbol_cluster" }, + Array.Empty(), + new string[] { "symbol_cluster" }, + new string[] { "symbol_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "consonant_syllable" }, + Array.Empty(), + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + Array.Empty(), + new string[] { "consonant_syllable" }, + Array.Empty(), + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + Array.Empty(), + Array.Empty(), + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + Array.Empty(), + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "vowel_syllable" }, + Array.Empty(), + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + Array.Empty(), + new string[] { "vowel_syllable" }, + Array.Empty(), + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + Array.Empty(), + Array.Empty(), + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + Array.Empty(), + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + new string[] { "broken_cluster" }, + Array.Empty(), + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + Array.Empty(), + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + Array.Empty(), + Array.Empty(), + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + Array.Empty(), + new string[] { "standalone_cluster" }, + Array.Empty(), + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + Array.Empty(), + Array.Empty(), + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + Array.Empty(), + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable","broken_cluster" }, + new string[] { "consonant_syllable","broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "symbol_cluster" }, + new string[] { "broken_cluster" }, + Array.Empty(), + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "consonant_syllable" }, + Array.Empty(), + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + Array.Empty(), + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + Array.Empty(), + Array.Empty(), + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + Array.Empty(), + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "vowel_syllable" }, + Array.Empty(), + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + Array.Empty(), + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + Array.Empty(), + Array.Empty(), + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + Array.Empty(), + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + new string[] { "broken_cluster" }, + Array.Empty(), + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + Array.Empty(), + new string[] { "standalone_cluster" }, + Array.Empty(), + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + Array.Empty(), + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + Array.Empty(), + Array.Empty(), + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + Array.Empty(), + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + Array.Empty(), + new string[] { "broken_cluster" }, + new string[] { "consonant_syllable","broken_cluster" }, + new string[] { "consonant_syllable","broken_cluster" }, + new string[] { "consonant_syllable","broken_cluster" }, + new string[] { "consonant_syllable" }, + Array.Empty(), + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + Array.Empty(), + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + Array.Empty(), + new string[] { "vowel_syllable" }, + new string[] { "vowel_syllable" }, + Array.Empty(), + new string[] { "standalone_cluster" }, + Array.Empty(), + new string[] { "standalone_cluster" }, + new string[] { "standalone_cluster" }, + Array.Empty(), + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable","broken_cluster" }, + new string[] { "consonant_syllable","broken_cluster" }, + Array.Empty(), + new string[] { "consonant_syllable","broken_cluster" }, + new string[] { "consonant_syllable","broken_cluster" }, + Array.Empty(), + new string[] { "consonant_syllable","broken_cluster" }, + new string[] { "consonant_syllable","broken_cluster" }, + Array.Empty(), + new string[] { "consonant_syllable","broken_cluster" }, + new string[] { "consonant_syllable","broken_cluster" }, + new string[] { "consonant_syllable","broken_cluster" }, + Array.Empty() + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/IndicShapingData.cs b/SixLabors.Fonts/Unicode/Resources/IndicShapingData.cs new file mode 100644 index 0000000..0d5fca9 --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/IndicShapingData.cs @@ -0,0 +1,462 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts.Unicode.Resources { + internal static partial class IndicShapingData + { + /// + /// Script shaping category values used for Indic, Khmer, and Myanmar text + /// classification. These values correspond to the category codes used by + /// HarfBuzz in its Indic-style shaping engines, including the extended + /// categories required for Myanmar. + /// + /// The values serve as the input alphabet for the script syllable machines + /// and determine script-specific parsing, reordering, and dotted circle insertion. + /// + /// Categories are sourced from the OpenType Script Development Specifications + /// and HarfBuzz's generated Indic tables: + /// + /// Indic specification: + /// https://learn.microsoft.com/en-us/typography/script-development/devanagari + /// + /// General Indic shaper category model and data: + /// https://github.com/harfbuzz/harfbuzz/blob/main/src/hb-ot-shaper-indic.cc + /// https://github.com/harfbuzz/harfbuzz/blob/main/src/hb-ot-shaper-indic-table.hh + /// + /// Khmer specification: + /// https://learn.microsoft.com/en-us/typography/script-development/khmer + /// + /// Myanmar specification: + /// https://learn.microsoft.com/en-us/typography/script-development/myanmar + /// + /// Myanmar machine exports: + /// https://github.com/harfbuzz/harfbuzz/blob/main/src/hb-ot-shaper-myanmar-machine.rl + /// + /// Notes: + /// * X is the default category and always has value 0. + /// * Coeng intentionally shares the same category value as H, matching + /// HarfBuzz behavior for Khmer. + /// * Some values are shared across scripts (for example VAbv, VBlw, VPre, + /// VPst) because the OpenType model for dependent vowels is the same. + /// * Myanmar-specific medial and tone categories begin at 32 and above, + /// matching HarfBuzz's numeric category assignments. + /// + public enum Categories : int + { + // Core Indic-style categories (shared across scripts where applicable) + X = 0, // Uncategorized / default + + C = 1, // Consonant + V = 2, // Dependent vowel + N = 3, // Nukta + H = 4, // Halant (virama) + + // Coeng = H, // Khmer Coeng, mapped to H in HarfBuzz + ZWNJ = 5, // Zero width non-joiner + ZWJ = 6, // Zero width joiner + M = 7, // Generic matra / dependent vowel + SM = 8, // Syllable modifier / visarga / tone marks + A = 9, // Vowel sign A (and related) + + // VD = 9, // Vowel-dependent sign (shares code with A) + Placeholder = 10, // Placeholder (NBSP, etc.) + Dotted_Circle = 11, // Explicit dotted circle + + RS = 12, // Register shifter (Khmer) + MPst = 13, // Post-base matra + Repha = 14, // Repha form + Ra = 15, // Consonant Ra + CM = 16, // Consonant medial + Symbol = 17, // Symbol / Avagraha-like mark + CS = 18, // Consonant-with-stacker / special consonant + + SMPst = 57, // Post-base spacing mark (shared Indic / Myanmar) + + // Shared positional vowel / matra categories (Indic / Khmer / Myanmar) + VAbv = 20, // Above-base vowel or matra + VBlw = 21, // Below-base vowel or matra + VPre = 22, // Pre-base vowel or matra + VPst = 23, // Post-base vowel or matra + + // Khmer-specific categories + Robatic = 25, // Khmer Robatic sign + Xgroup = 26, // Khmer X-group matra sequence + Ygroup = 27, // Khmer Y-group matra sequence + Coeng = 28, // Remove once we no longer need it for Khmer + + // Myanmar-specific categories + // IV = V, // Independent vowel (shares code 2 with V in HarfBuzz) + // DB = N, // Dot-below (shares code 3 with N) + // GB = Placeholder, // Generic base / placeholder (shares code 10) + As = 32, // Asat + MH = 35, // Medial Ha + MR = 36, // Medial Ra + MW = 37, // Medial Wa / Shan Wa + MY = 38, // Medial Ya / Mon Na / Mon Ma + PT = 39, // Pwo and related tone marks + VS = 40, // Variation selector + ML = 41 // Medial Mon La + } + + // Categories used in the Myanmar shaping engine. + // Note: + // The OpenType Myanmar spec defines categories D, D0, and P. + // HarfBuzz collapses: + // D => GB + // D0 => D => GB + // P => GB + // We follow the same normalization, so D, D0 and P do not appear + // as distinct category flags. + // Only the symbols that appear in the Myanmar grammar. + // Values must match the Categories enum and the Ragel `export` codes. + public enum MyanmarCategories : int + { + C = Categories.C, + IV = Categories.V, + DB = Categories.N, + H = Categories.H, + ZWNJ = Categories.ZWNJ, + ZWJ = Categories.ZWJ, + SM = Categories.SM, + A = Categories.A, + GB = Categories.Placeholder, + Dotted_Circle = Categories.Dotted_Circle, + Ra = Categories.Ra, + CS = Categories.CS, + SMPst = Categories.SMPst, + + VAbv = Categories.VAbv, + VBlw = Categories.VBlw, + VPre = Categories.VPre, + VPst = Categories.VPst, + + As = Categories.As, + MH = Categories.MH, + MR = Categories.MR, + MW = Categories.MW, + MY = Categories.MY, + PT = Categories.PT, + VS = Categories.VS, + ML = Categories.ML, + } + + [Flags] + public enum MyanmarSyllableType + { + Consonant_Syllable = 1 << 0, + Broken_Cluster = 1 << 1, + NonMyanmar_Cluster = 1 << 2 + } + + // Visual positions in a syllable from left to right. + [Flags] + public enum Positions + { + Start = 1 << 0, + Ra_To_Become_Reph = 1 << 1, + Pre_M = 1 << 2, + Pre_C = 1 << 3, + Base_C = 1 << 4, + After_Main = 1 << 5, + Above_C = 1 << 6, + Before_Sub = 1 << 7, + Below_C = 1 << 8, + After_Sub = 1 << 9, + Before_Post = 1 << 10, + Post_C = 1 << 11, + After_Post = 1 << 12, + Final_C = 1 << 13, + SMVD = 1 << 14, + End = 1 << 15 + } + + public enum BasePosition + { + First, + + Last + } + + public enum RephMode + { + /// + /// Reph formed out of initial Ra,H sequence. + /// + Implicit, + + /// + /// Reph formed out of initial Ra,H,ZWJ sequence. + /// + Explicit, + + /// + /// Encoded Repha character, no reordering needed. + /// + Vis_Repha, + + /// + /// Encoded Repha character, needs reordering. + /// + Log_Repha + } + + public enum BlwfMode + { + /// + /// Below-forms feature applied to pre-base and post-base. + /// + Pre_And_Post, + + /// + /// Below-forms feature applied to post-base only. + /// + Post_Only + } + + public static Dictionary IndicConfigurations { get; } = new() + { + { + ScriptClass.Devanagari, + new() + { + HasOldSpec = true, + Virama = 0x094D, + BasePosition = BasePosition.Last, + RephPosition = Positions.Before_Post, + RephMode = RephMode.Implicit, + BlwfMode = BlwfMode.Pre_And_Post + } + }, + { + ScriptClass.Bengali, + new() + { + HasOldSpec = true, + Virama = 0x09CD, + BasePosition = BasePosition.Last, + RephPosition = Positions.After_Sub, + RephMode = RephMode.Implicit, + BlwfMode = BlwfMode.Pre_And_Post + } + }, + { + ScriptClass.Gurmukhi, + new() + { + HasOldSpec = true, + Virama = 0x0A4D, + BasePosition = BasePosition.Last, + RephPosition = Positions.Before_Sub, + RephMode = RephMode.Implicit, + BlwfMode = BlwfMode.Pre_And_Post + } + }, + { + ScriptClass.Gujarati, + new() + { + HasOldSpec = true, + Virama = 0x0ACD, + BasePosition = BasePosition.Last, + RephPosition = Positions.Before_Post, + RephMode = RephMode.Implicit, + BlwfMode = BlwfMode.Pre_And_Post + } + }, + { + ScriptClass.Oriya, + new() + { + HasOldSpec = true, + Virama = 0x0B4D, + BasePosition = BasePosition.Last, + RephPosition = Positions.After_Main, + RephMode = RephMode.Implicit, + BlwfMode = BlwfMode.Pre_And_Post + } + }, + { + ScriptClass.Tamil, + new() + { + HasOldSpec = true, + Virama = 0x0BCD, + BasePosition = BasePosition.Last, + RephPosition = Positions.After_Post, + RephMode = RephMode.Implicit, + BlwfMode = BlwfMode.Pre_And_Post + } + }, + { + ScriptClass.Telugu, + new() + { + HasOldSpec = true, + Virama = 0x0C4D, + BasePosition = BasePosition.Last, + RephPosition = Positions.After_Post, + RephMode = RephMode.Explicit, + BlwfMode = BlwfMode.Post_Only + } + }, + { + ScriptClass.Kannada, + new() + { + HasOldSpec = true, + Virama = 0x0CCD, + BasePosition = BasePosition.Last, + RephPosition = Positions.After_Post, + RephMode = RephMode.Implicit, + BlwfMode = BlwfMode.Post_Only + } + }, + { + ScriptClass.Malayalam, + new() + { + HasOldSpec = true, + Virama = 0x0D4D, + BasePosition = BasePosition.Last, + RephPosition = Positions.After_Main, + RephMode = RephMode.Log_Repha, + BlwfMode = BlwfMode.Pre_And_Post + } + }, + { + ScriptClass.Khmer, + new() + { + HasOldSpec = true, + Virama = 0x17D2, + BasePosition = BasePosition.First, + RephPosition = Positions.Ra_To_Become_Reph, + RephMode = RephMode.Vis_Repha, + BlwfMode = BlwfMode.Pre_And_Post + } + } + }; + + public static Dictionary Decompositions { get; } = new() + { + // Khmer + { 0x17BE, new int[] { 0x17C1, 0x17BE } }, + { 0x17BF, new int[] { 0x17C1, 0x17BF } }, + { 0x17C0, new int[] { 0x17C1, 0x17C0 } }, + { 0x17C4, new int[] { 0x17C1, 0x17C4 } }, + { 0x17C5, new int[] { 0x17C1, 0x17C5 } } + }; + + public static uint ConsonantFlags { get; } = + Flag(Categories.C) | + Flag(Categories.Ra) | + Flag(Categories.CM) | + Flag(Categories.V) | + Flag(Categories.Placeholder) | + Flag(Categories.Dotted_Circle); + + // Note: + // We treat Vowels and placeholders as if they were consonants.This is safe because Vowels + // cannot happen in a consonant syllable.The plus side however is, we can call the + // consonant syllable logic from the vowel syllable function and get it all right! + // Keep in sync with the categories used in the Myanmar state machine generator. + public static uint MyanmarConsonantFlags { get; } = + Flag(MyanmarCategories.C) | + Flag(MyanmarCategories.CS) | + Flag(MyanmarCategories.Ra) | + Flag(MyanmarCategories.IV) | + Flag(MyanmarCategories.GB) | + Flag(MyanmarCategories.Dotted_Circle); + + public static uint JoinerFlags { get; } = + Flag(Categories.ZWJ) | + Flag(Categories.ZWNJ); + + public static uint HalantOrCoengFlags { get; } = + Flag(Categories.H) | + Flag(Categories.Coeng); + + /// + /// Provides a flag value for the given category. Only valid for categories < 32. + /// + /// The category for which to generate a bit flag. If null, the default category is used. + /// A 32-bit unsigned integer with a single bit set corresponding to the specified category value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Flag(Categories? categories) + => FlagCoreChecked((int)(categories ?? default)); + + /// + /// Provides a flag value for the given category. Only valid for categories < 32. + /// + /// The category for which to generate a bit flag. If null, the default category is used. + /// A 32-bit unsigned integer with a single bit set corresponding to the specified category value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Flag(MyanmarCategories? categories) + => FlagCoreChecked((int)(categories ?? default)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint FlagCoreChecked(int value) + { +#if DEBUG + if ((uint)value >= 32u) + { + throw new ArgumentOutOfRangeException( + nameof(value), + "Flag() is only defined for enum values < 32."); + } +#endif + return 1u << value; + } + + /// + /// Returns a bit flag corresponding to the specified category, or zero if the category value is out of range. + /// + /// The category for which to generate a bit flag. If null, the default category is used. + /// + /// A 32-bit unsigned integer with a single bit set corresponding to the specified category value; returns 0 if the + /// category value is not between 0 and 31, inclusive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint FlagUnsafe(Categories? categories) + => FlagUnsafeCore((int)(categories ?? default)); + + /// + /// Returns a bit flag corresponding to the specified category, or zero if the category value is out of range. + /// + /// The category for which to generate a bit flag. If null, the default category is used. + /// + /// A 32-bit unsigned integer with a single bit set corresponding to the specified category value; returns 0 if the + /// category value is not between 0 and 31, inclusive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint FlagUnsafe(MyanmarCategories? categories) + => FlagUnsafeCore((int)(categories ?? default)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint FlagUnsafeCore(int value) => value < 32 ? 1u << value : 0u; + + internal struct ShapingConfiguration + { + public static ShapingConfiguration Default = new() + { + HasOldSpec = false, + Virama = 0, + BasePosition = BasePosition.Last, + RephPosition = Positions.Before_Post, + RephMode = RephMode.Implicit, + BlwfMode = BlwfMode.Pre_And_Post + }; + + public bool HasOldSpec; + public int Virama; + public BasePosition BasePosition; + public Positions RephPosition; + public RephMode RephMode; + public BlwfMode BlwfMode; + } + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/IndicShapingTrie.Generated.cs b/SixLabors.Fonts/Unicode/Resources/IndicShapingTrie.Generated.cs new file mode 100644 index 0000000..f5a9cf4 --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/IndicShapingTrie.Generated.cs @@ -0,0 +1,617 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class IndicShapingTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 0, 17, 0, 0, 0, 0, 0, 16, 235, 0, 0, 60, 4, 0, 0, 68, 4, 0, 0, 76, 4, 0, 0, 84, 4, 0, 0, 108, 4, 0, 0, 116, 4, 0, 0, 121, 4, 0, 0, 129, 4, 0, 0, 75, 4, 0, 0, 83, 4, 0, 0, 75, 4, 0, 0, 83, 4, 0, 0, 75, 4, 0, 0, 83, 4, 0, 0, 75, 4, 0, 0, 83, 4, 0, 0, 75, 4, 0, 0, 83, 4, 0, 0, 75, 4, 0, 0, 83, 4, 0, 0, 75, 4, 0, 0, 83, 4, 0, 0, + 75, 4, 0, 0, 83, 4, 0, 0, 75, 4, 0, 0, 83, 4, 0, 0, 127, 4, 0, 0, 135, 4, 0, 0, 143, 4, 0, 0, 151, 4, 0, 0, 75, 4, 0, 0, 83, 4, 0, 0, 75, 4, 0, 0, 83, 4, 0, 0, 75, 4, 0, 0, 83, 4, 0, 0, 75, 4, 0, 0, 83, 4, 0, 0, 75, 4, 0, 0, 83, 4, 0, 0, 152, 4, 0, 0, 160, 4, 0, 0, 165, 4, 0, 0, 173, 4, 0, 0, 179, 4, 0, 0, 187, 4, 0, 0, 193, 4, 0, 0, + 201, 4, 0, 0, 75, 4, 0, 0, 83, 4, 0, 0, 75, 4, 0, 0, 83, 4, 0, 0, 75, 4, 0, 0, 83, 4, 0, 0, 75, 4, 0, 0, 83, 4, 0, 0, 148, 4, 0, 0, 156, 4, 0, 0, 209, 4, 0, 0, 217, 4, 0, 0, 213, 4, 0, 0, 221, 4, 0, 0, 229, 4, 0, 0, 237, 4, 0, 0, 60, 4, 0, 0, 245, 4, 0, 0, 253, 4, 0, 0, 5, 5, 0, 0, 9, 5, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 17, 5, 0, 0, 23, 5, 0, 0, 31, 5, 0, 0, 39, 5, 0, 0, 47, 5, 0, 0, 53, 5, 0, 0, 61, 5, 0, 0, 69, 5, 0, 0, 77, 5, 0, 0, 83, 5, 0, 0, 91, 5, 0, 0, 99, 5, 0, 0, 107, 5, 0, 0, 113, 5, 0, 0, 121, 5, 0, 0, 129, 5, 0, 0, 137, 5, 0, 0, 143, 5, 0, 0, 151, 5, 0, 0, 159, 5, 0, 0, 167, 5, 0, 0, 175, 5, 0, 0, 183, 5, 0, 0, 190, 5, 0, 0, 198, 5, 0, 0, + 204, 5, 0, 0, 212, 5, 0, 0, 220, 5, 0, 0, 228, 5, 0, 0, 234, 5, 0, 0, 242, 5, 0, 0, 250, 5, 0, 0, 2, 6, 0, 0, 8, 6, 0, 0, 16, 6, 0, 0, 24, 6, 0, 0, 32, 6, 0, 0, 39, 6, 0, 0, 47, 6, 0, 0, 55, 6, 0, 0, 63, 6, 0, 0, 68, 6, 0, 0, 76, 6, 0, 0, 92, 4, 0, 0, 84, 6, 0, 0, 91, 6, 0, 0, 99, 6, 0, 0, 92, 4, 0, 0, 107, 6, 0, 0, 115, 6, 0, 0, + 123, 6, 0, 0, 128, 6, 0, 0, 135, 6, 0, 0, 142, 6, 0, 0, 150, 6, 0, 0, 92, 4, 0, 0, 158, 6, 0, 0, 166, 6, 0, 0, 174, 6, 0, 0, 182, 6, 0, 0, 190, 6, 0, 0, 60, 4, 0, 0, 198, 6, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 204, 6, 0, 0, + 60, 4, 0, 0, 212, 6, 0, 0, 202, 6, 0, 0, 220, 6, 0, 0, 60, 4, 0, 0, 216, 6, 0, 0, 60, 4, 0, 0, 175, 4, 0, 0, 226, 6, 0, 0, 219, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 234, 6, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 226, 6, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 242, 6, 0, 0, 250, 6, 0, 0, 2, 7, 0, 0, 10, 7, 0, 0, 18, 7, 0, 0, 26, 7, 0, 0, 34, 7, 0, 0, 42, 7, 0, 0, 50, 7, 0, 0, 219, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 242, 6, 0, 0, 60, 4, 0, 0, + 5, 5, 0, 0, 60, 4, 0, 0, 220, 4, 0, 0, 58, 7, 0, 0, 66, 7, 0, 0, 74, 7, 0, 0, 82, 7, 0, 0, 90, 7, 0, 0, 95, 7, 0, 0, 103, 7, 0, 0, 60, 4, 0, 0, 111, 7, 0, 0, 90, 7, 0, 0, 119, 7, 0, 0, 127, 7, 0, 0, 135, 7, 0, 0, 143, 7, 0, 0, 218, 4, 0, 0, 89, 4, 0, 0, 151, 7, 0, 0, 156, 7, 0, 0, 164, 7, 0, 0, 171, 7, 0, 0, 179, 7, 0, 0, 187, 7, 0, 0, + 90, 7, 0, 0, 195, 7, 0, 0, 90, 7, 0, 0, 203, 7, 0, 0, 211, 7, 0, 0, 60, 4, 0, 0, 5, 5, 0, 0, 175, 4, 0, 0, 217, 7, 0, 0, 223, 7, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 231, 7, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 234, 6, 0, 0, 60, 4, 0, 0, 238, 7, 0, 0, 218, 4, 0, 0, 60, 4, 0, 0, 246, 7, 0, 0, 250, 7, 0, 0, 2, 8, 0, 0, 10, 8, 0, 0, 18, 8, 0, 0, 60, 4, 0, 0, 25, 8, 0, 0, 33, 8, 0, 0, 60, 4, 0, 0, 225, 4, 0, 0, 41, 8, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 49, 8, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 55, 8, 0, 0, 63, 8, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 71, 8, 0, 0, 75, 8, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 83, 8, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 89, 8, 0, 0, 60, 4, 0, 0, 198, 6, 0, 0, 60, 4, 0, 0, 96, 8, 0, 0, 104, 8, 0, 0, 112, 8, 0, 0, 112, 8, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 218, 4, 0, 0, 92, 4, 0, 0, + 120, 8, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 87, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 220, 4, 0, 0, 104, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 171, 4, 0, 0, 60, 4, 0, 0, 165, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 128, 8, 0, 0, 160, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 249, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 135, 8, 0, 0, 174, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 143, 8, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 144, 8, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 143, 8, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 144, 8, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 230, 6, 0, 0, 60, 4, 0, 0, 152, 8, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 89, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 86, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 226, 6, 0, 0, 160, 8, 0, 0, 168, 8, 0, 0, 176, 8, 0, 0, 184, 8, 0, 0, + 192, 8, 0, 0, 200, 8, 0, 0, 205, 8, 0, 0, 212, 8, 0, 0, 220, 8, 0, 0, 228, 8, 0, 0, 236, 8, 0, 0, 243, 8, 0, 0, 226, 6, 0, 0, 251, 8, 0, 0, 255, 8, 0, 0, 7, 9, 0, 0, 15, 9, 0, 0, 23, 9, 0, 0, 29, 9, 0, 0, 37, 9, 0, 0, 45, 9, 0, 0, 90, 7, 0, 0, 53, 9, 0, 0, 61, 9, 0, 0, 69, 9, 0, 0, 77, 9, 0, 0, 116, 8, 0, 0, 60, 4, 0, 0, 49, 8, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 85, 9, 0, 0, 93, 9, 0, 0, 143, 8, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 101, 9, 0, 0, 108, 9, 0, 0, 85, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 143, 8, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 144, 8, 0, 0, 143, 8, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 144, 8, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 143, 7, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 219, 4, 0, 0, 92, 4, 0, 0, 115, 9, 0, 0, 123, 9, 0, 0, 131, 9, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 88, 4, 0, 0, 104, 4, 0, 0, 139, 9, 0, 0, 60, 4, 0, 0, 147, 9, 0, 0, 154, 9, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 160, 9, 0, 0, 171, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 174, 4, 0, 0, 168, 9, 0, 0, + 176, 9, 0, 0, 143, 8, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 144, 8, 0, 0, 143, 8, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 144, 8, 0, 0, 112, 17, 0, 0, 112, 17, 0, 0, 176, 17, 0, 0, 228, 17, 0, 0, 44, 17, 0, 0, 44, 17, 0, 0, 44, 17, 0, 0, 44, 17, 0, 0, 44, 17, 0, 0, 44, 17, 0, 0, 44, 17, 0, 0, 44, 17, 0, 0, 44, 17, 0, 0, 252, 17, 0, 0, 60, 18, 0, 0, 44, 17, 0, 0, 44, 17, 0, 0, + 44, 17, 0, 0, 44, 17, 0, 0, 44, 17, 0, 0, 96, 18, 0, 0, 148, 18, 0, 0, 204, 18, 0, 0, 4, 19, 0, 0, 44, 17, 0, 0, 44, 17, 0, 0, 44, 17, 0, 0, 44, 17, 0, 0, 80, 18, 0, 0, 68, 19, 0, 0, 84, 19, 0, 0, 148, 19, 0, 0, 64, 10, 0, 0, 128, 10, 0, 0, 192, 10, 0, 0, 0, 11, 0, 0, 64, 11, 0, 0, 107, 11, 0, 0, 171, 11, 0, 0, 206, 11, 0, 0, 239, 11, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 39, 12, 0, 0, 103, 12, 0, 0, 160, 1, 0, 0, 161, 1, 0, 0, 46, 2, 0, 0, 167, 12, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 215, 12, 0, 0, 23, 13, 0, 0, 55, 13, 0, 0, 161, 1, 0, 0, 93, 13, 0, 0, 157, 13, 0, 0, 220, 13, 0, 0, 28, 14, 0, 0, 92, 14, 0, 0, 156, 14, 0, 0, 220, 14, 0, 0, 160, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 252, 14, 0, 0, 161, 1, 0, 0, 53, 15, 0, 0, 117, 15, 0, 0, 161, 1, 0, 0, 128, 15, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 182, 15, 0, 0, 161, 1, 0, 0, 246, 15, 0, 0, 160, 1, 0, 0, 161, 1, 0, 0, 28, 16, 0, 0, 161, 1, 0, 0, 63, 16, 0, 0, 161, 1, 0, 0, 93, 16, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 157, 16, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 160, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 173, 16, 0, 0, 160, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 173, 16, 0, 0, 161, 4, 0, 0, 184, 9, 0, 0, 192, 9, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 200, 9, 0, 0, 208, 9, 0, 0, 211, 9, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 217, 9, 0, 0, 143, 8, 0, 0, 104, 4, 0, 0, 218, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 226, 6, 0, 0, 60, 4, 0, 0, 139, 8, 0, 0, + 85, 4, 0, 0, 60, 4, 0, 0, 225, 9, 0, 0, 5, 5, 0, 0, 200, 9, 0, 0, 229, 9, 0, 0, 60, 4, 0, 0, 237, 9, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 218, 4, 0, 0, 143, 9, 0, 0, 245, 9, 0, 0, 85, 4, 0, 0, 60, 4, 0, 0, 193, 4, 0, 0, 60, 4, 0, 0, 252, 9, 0, 0, 0, 10, 0, 0, 5, 10, 0, 0, 60, 4, 0, 0, 87, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 104, 8, 0, 0, 220, 4, 0, 0, 90, 4, 0, 0, 150, 4, 0, 0, 13, 10, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 21, 10, 0, 0, 24, 10, 0, 0, 32, 10, 0, 0, 60, 4, 0, 0, 174, 4, 0, 0, 40, 10, 0, 0, 92, 4, 0, 0, 48, 10, 0, 0, 55, 10, 0, 0, + 63, 10, 0, 0, 219, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 71, 10, 0, 0, 84, 8, 0, 0, 60, 4, 0, 0, 79, 10, 0, 0, 86, 10, 0, 0, 94, 10, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 102, 10, 0, 0, 60, 4, 0, 0, 110, 10, 0, 0, 117, 10, 0, 0, 123, 10, 0, 0, 129, 10, 0, 0, 137, 10, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 141, 8, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 145, 10, 0, 0, 60, 4, 0, 0, 153, 10, 0, 0, 60, 4, 0, 0, 160, 10, 0, 0, 60, 4, 0, 0, 114, 10, 0, 0, 165, 10, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 174, 4, 0, 0, 60, 4, 0, 0, 173, 10, 0, 0, 181, 10, 0, 0, 188, 10, 0, 0, 60, 4, 0, 0, 193, 4, 0, 0, 219, 4, 0, 0, + 104, 4, 0, 0, 55, 8, 0, 0, 104, 4, 0, 0, 89, 4, 0, 0, 104, 8, 0, 0, 196, 10, 0, 0, 201, 10, 0, 0, 208, 10, 0, 0, 213, 10, 0, 0, 221, 10, 0, 0, 225, 10, 0, 0, 233, 10, 0, 0, 239, 10, 0, 0, 247, 10, 0, 0, 254, 10, 0, 0, 6, 11, 0, 0, 12, 11, 0, 0, 20, 11, 0, 0, 25, 11, 0, 0, 33, 11, 0, 0, 41, 11, 0, 0, 49, 11, 0, 0, 54, 11, 0, 0, 62, 11, 0, 0, 92, 4, 0, 0, + 70, 11, 0, 0, 78, 11, 0, 0, 85, 11, 0, 0, 93, 11, 0, 0, 101, 11, 0, 0, 107, 11, 0, 0, 115, 11, 0, 0, 123, 11, 0, 0, 131, 11, 0, 0, 136, 11, 0, 0, 144, 11, 0, 0, 152, 11, 0, 0, 201, 8, 0, 0, 160, 11, 0, 0, 168, 11, 0, 0, 176, 11, 0, 0, 184, 11, 0, 0, 188, 11, 0, 0, 196, 11, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 201, 8, 0, 0, + 204, 11, 0, 0, 212, 11, 0, 0, 92, 4, 0, 0, 201, 8, 0, 0, 220, 11, 0, 0, 228, 11, 0, 0, 140, 8, 0, 0, 202, 8, 0, 0, 236, 11, 0, 0, 244, 11, 0, 0, 251, 11, 0, 0, 3, 12, 0, 0, 11, 12, 0, 0, 19, 12, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 202, 8, 0, 0, 27, 12, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 112, 9, 0, 0, 35, 12, 0, 0, 41, 12, 0, 0, 49, 12, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 57, 12, 0, 0, 61, 12, 0, 0, 69, 12, 0, 0, 77, 12, 0, 0, 81, 12, 0, 0, 89, 12, 0, 0, 90, 7, 0, 0, 96, 12, 0, 0, 149, 10, 0, 0, 60, 4, 0, 0, 242, 6, 0, 0, 55, 8, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 104, 12, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, + 112, 12, 0, 0, 120, 12, 0, 0, 125, 12, 0, 0, 133, 12, 0, 0, 138, 12, 0, 0, 143, 12, 0, 0, 149, 12, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 157, 12, 0, 0, 161, 12, 0, 0, 169, 12, 0, 0, 177, 12, 0, 0, 183, 12, 0, 0, 191, 12, 0, 0, 85, 4, 0, 0, 55, 8, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 199, 12, 0, 0, + 207, 12, 0, 0, 212, 12, 0, 0, 220, 12, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 227, 12, 0, 0, 60, 4, 0, 0, 235, 12, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 219, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 243, 12, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 91, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 104, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 145, 10, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 220, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 200, 9, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 251, 12, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 3, 13, 0, 0, 11, 13, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 242, 6, 0, 0, 174, 4, 0, 0, 19, 13, 0, 0, 60, 4, 0, 0, 174, 4, 0, 0, + 143, 9, 0, 0, 24, 13, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 28, 13, 0, 0, 34, 13, 0, 0, 88, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 42, 13, 0, 0, 50, 13, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 200, 9, 0, 0, 58, 13, 0, 0, 87, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 107, 9, 0, 0, 60, 4, 0, 0, 65, 13, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 247, 12, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 220, 4, 0, 0, 144, 8, 0, 0, 73, 13, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 145, 10, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 81, 13, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 89, 13, 0, 0, 94, 13, 0, 0, 101, 13, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 85, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 107, 13, 0, 0, 112, 13, 0, 0, 91, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 226, 6, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 119, 13, 0, 0, 139, 8, 0, 0, 139, 8, 0, 0, 60, 4, 0, 0, 143, 7, 0, 0, 152, 8, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 91, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 220, 4, 0, 0, 60, 4, 0, 0, 169, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 63, 8, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 126, 13, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 87, 4, 0, 0, 87, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 104, 8, 0, 0, 242, 6, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 246, 7, 0, 0, 60, 4, 0, 0, 19, 8, 0, 0, + 134, 13, 0, 0, 142, 13, 0, 0, 60, 4, 0, 0, 149, 13, 0, 0, 144, 13, 0, 0, 157, 13, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 240, 6, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 85, 8, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 162, 13, 0, 0, 170, 13, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 174, 4, 0, 0, 177, 13, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 185, 13, 0, 0, 193, 13, 0, 0, 60, 4, 0, 0, 54, 8, 0, 0, 201, 13, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 209, 13, 0, 0, + 214, 13, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 104, 4, 0, 0, 222, 13, 0, 0, 60, 4, 0, 0, 63, 10, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 104, 4, 0, 0, 219, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 104, 4, 0, 0, 197, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 174, 4, 0, 0, 230, 13, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 238, 13, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 246, 13, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 254, 13, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 160, 8, 0, 0, 60, 4, 0, 0, 6, 14, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 171, 4, 0, 0, 218, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 142, 13, 0, 0, 14, 14, 0, 0, 22, 14, 0, 0, 30, 14, 0, 0, 38, 14, 0, 0, 46, 14, 0, 0, 92, 4, 0, 0, 108, 12, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 49, 8, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 87, 4, 0, 0, 167, 4, 0, 0, 54, 14, 0, 0, + 220, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 54, 8, 0, 0, 92, 4, 0, 0, 62, 14, 0, 0, 149, 10, 0, 0, 85, 4, 0, 0, 68, 14, 0, 0, 126, 13, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 227, 6, 0, 0, 76, 14, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 219, 4, 0, 0, 84, 14, 0, 0, 49, 8, 0, 0, 60, 4, 0, 0, 160, 10, 0, 0, 60, 4, 0, 0, 193, 4, 0, 0, 92, 14, 0, 0, 100, 14, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 86, 4, 0, 0, 108, 14, 0, 0, 116, 14, 0, 0, 60, 4, 0, 0, 123, 14, 0, 0, 131, 14, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 147, 9, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 200, 9, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 144, 8, 0, 0, 92, 4, 0, 0, 143, 8, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 144, 8, 0, 0, 143, 8, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 138, 14, 0, 0, 143, 8, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 142, 14, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 146, 14, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 138, 14, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, + 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 218, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 152, 14, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 157, 14, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 162, 14, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 168, 14, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 60, 4, 0, 0, 88, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, + 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 92, 4, 0, 0, 138, 14, 0, 0, 59, 4, 1, 0, 59, 4, 1, 0, 59, 4, 1, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 57, 0, 0, 14, 57, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 15, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 8, 3, 0, 0, 14, 17, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, + 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 8, 4, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 15, 0, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 10, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 15, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 3, 0, 0, 14, 17, 0, 0, 12, 7, 0, 0, 2, 7, 0, 0, 12, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, + 2, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 8, 4, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, + 4, 2, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 15, 0, 0, 4, 1, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 15, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 3, 0, 0, 0, 0, 0, 0, 12, 7, 0, 0, 2, 7, 0, 0, 12, 13, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, + 8, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 15, 0, 0, 0, 4, 16, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 15, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 3, 0, 0, 14, 17, 0, 0, 12, 7, 0, 0, 2, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 12, 7, 0, 0, 0, 0, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 8, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 14, 9, 0, 0, 6, 3, 0, 0, 14, 9, 0, 0, 6, 3, 0, 0, 6, 3, 0, 0, 6, 3, 0, 0, 0, 0, 0, 0, 7, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, + 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 15, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 3, 0, 0, 14, 17, 0, 0, + 12, 7, 0, 0, 5, 7, 0, 0, 12, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 5, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 8, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 3, 0, 0, 5, 7, 0, 0, + 12, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 15, 0, 0, 0, 4, 1, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 15, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 9, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 0, 0, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 15, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 3, 0, 0, 14, 17, 0, 0, 7, 7, 0, 0, 7, 7, 0, 0, 7, 7, 0, 0, 7, 7, 0, 0, 7, 7, 0, 0, 9, 7, 0, 0, + 9, 7, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 7, 7, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 7, 7, 0, 0, 7, 7, 0, 0, 6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, + 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 7, 7, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 15, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, + 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 15, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 3, 0, 0, 14, 17, 0, 0, 7, 7, 0, 0, 7, 7, 0, 0, 7, 7, 0, 0, 7, 7, 0, 0, 7, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, + 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 7, 7, 0, 0, 6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 7, 7, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 15, 18, 0, 0, 15, 18, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 4, 10, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 15, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 17, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 0, 0, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 6, 4, 0, 0, + 6, 14, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 12, 7, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 15, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 15, 0, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 15, 0, 0, 0, 9, 7, 0, 0, 6, 3, 0, 0, 6, 3, 0, 0, 6, 3, 0, 0, 6, 3, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 9, 7, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 15, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 6, 3, 0, 0, 6, 3, 0, 0, 6, 3, 0, 0, 6, 3, 0, 0, 6, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 14, 8, 0, 0, 15, 0, 0, 0, 14, 8, 0, 0, 15, 0, 0, 0, 6, 3, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 11, 0, 0, 0, 3, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 9, 7, 0, 0, 14, 17, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 0, 0, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, + 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 8, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 15, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 15, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 11, 23, 0, 0, 11, 23, 0, 0, 6, 20, 0, 0, + 6, 20, 0, 0, 8, 21, 0, 0, 8, 21, 0, 0, 3, 22, 0, 0, 14, 9, 0, 0, 6, 20, 0, 0, 6, 20, 0, 0, 6, 20, 0, 0, 14, 9, 0, 0, 8, 3, 0, 0, 14, 8, 0, 0, 15, 28, 0, 0, 6, 32, 0, 0, 11, 38, 0, 0, 15, 36, 0, 0, 8, 37, 0, 0, 8, 35, 0, 0, 4, 1, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 11, 23, 0, 0, 11, 23, 0, 0, 8, 21, 0, 0, 8, 21, 0, 0, 4, 15, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 8, 38, 0, 0, 8, 38, 0, 0, + 8, 41, 0, 0, 4, 1, 0, 0, 11, 23, 0, 0, 11, 39, 0, 0, 11, 39, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 11, 23, 0, 0, 11, 23, 0, 0, 11, 39, 0, 0, 11, 39, 0, 0, 11, 39, 0, 0, 11, 39, 0, 0, 11, 39, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 6, 20, 0, 0, 6, 20, 0, 0, 6, 20, 0, 0, 6, 20, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 8, 37, 0, 0, 11, 23, 0, 0, 3, 22, 0, 0, 6, 20, 0, 0, 6, 20, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 4, 1, 0, 0, 14, 8, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 6, 20, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 15, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 4, 2, 0, 0, 4, 2, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 6, 3, 0, 0, 14, 8, 0, 0, 12, 7, 0, 0, 6, 12, 0, 0, 6, 12, 0, 0, + 14, 8, 0, 0, 4, 16, 0, 0, 12, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 12, 7, 0, 0, 15, 28, 0, 0, 14, 8, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 17, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 14, 8, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, + 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 11, 3, 0, 0, 11, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 1, 0, 0, 4, 16, 0, 0, + 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 0, 0, 0, 0, 15, 28, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, + 6, 3, 0, 0, 6, 3, 0, 0, 6, 3, 0, 0, 6, 3, 0, 0, 6, 3, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 4, 16, 0, 0, 14, 8, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 6, 3, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, + 2, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 11, 4, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 6, 0, 0, 0, 8, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 8, 0, 0, 4, 16, 0, 0, 14, 8, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, + 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 28, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 14, 17, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 16, 0, 0, + 4, 16, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 6, 3, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, + 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 8, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 15, 0, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 17, 0, 0, 14, 17, 0, 0, 14, 17, 0, 0, + 14, 17, 0, 0, 14, 9, 0, 0, 14, 17, 0, 0, 14, 17, 0, 0, 14, 17, 0, 0, 14, 17, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 14, 9, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 8, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, + 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 5, 0, 0, 15, 6, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 57, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 57, 0, 0, 14, 57, 0, 0, 14, 57, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 14, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 11, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 9, 7, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 6, 4, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 14, 8, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 16, 0, 0, 4, 1, 0, 0, 14, 57, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 16, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 8, 4, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 17, 0, 0, 14, 17, 0, 0, 14, 17, 0, 0, 14, 17, 0, 0, 14, 17, 0, 0, 14, 17, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 2, 0, 0, 9, 7, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 8, 3, 0, 0, 8, 3, 0, 0, 8, 3, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, + 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 14, 8, 0, 0, + 14, 8, 0, 0, 4, 16, 0, 0, 14, 8, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 6, 3, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 11, 4, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 6, 20, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, + 2, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, + 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 4, 1, 0, 0, 11, 39, 0, 0, 6, 3, 0, 0, 11, 3, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, + 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 6, 3, 0, 0, 15, 0, 0, 0, 6, 3, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 8, 0, 0, 15, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 4, 1, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, + 9, 7, 0, 0, 9, 7, 0, 0, 15, 0, 0, 0, 11, 3, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 40, 0, 0, 15, 40, 0, 0, 15, 40, 0, 0, 15, 40, 0, 0, 15, 40, 0, 0, 15, 40, 0, 0, 15, 40, 0, 0, 15, 40, 0, 0, 15, 40, 0, 0, 15, 40, 0, 0, 15, 40, 0, 0, 15, 40, 0, 0, 15, 40, 0, 0, 15, 40, 0, 0, 15, 40, 0, 0, 15, 40, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 5, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 5, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 3, 0, 0, 8, 3, 0, 0, 8, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 28, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 15, 18, 0, 0, 15, 18, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, + 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 6, 4, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 9, 7, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 4, 10, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 8, 4, 0, 0, 8, 3, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, + 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 28, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 8, 3, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, + 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 11, 4, 0, 0, + 14, 17, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 8, 0, 0, 8, 3, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 0, 0, 0, 2, 7, 0, 0, 14, 8, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, + 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 11, 4, 0, 0, 6, 3, 0, 0, 14, 8, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 9, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 14, 8, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 8, 3, 0, 0, 9, 7, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, + 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 8, 3, 0, 0, 8, 3, 0, 0, 14, 17, 0, 0, + 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 11, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 14, 57, 0, 0, 14, 57, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 14, 17, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 0, 0, 0, 0, + 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 28, 0, 0, 6, 14, 0, 0, 14, 8, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 8, 4, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 8, 3, 0, 0, 14, 17, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 14, 8, 0, 0, 14, 57, 0, 0, 15, 18, 0, 0, 15, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, + 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 8, 4, 0, 0, 8, 3, 0, 0, 14, 17, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, + 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 8, 4, 0, 0, 8, 3, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, + 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 8, 4, 0, 0, 9, 7, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 9, 7, 0, 0, + 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 11, 4, 0, 0, 8, 3, 0, 0, 4, 1, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 8, 4, 0, 0, + 8, 3, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, + 9, 7, 0, 0, 2, 7, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 9, 7, 0, 0, 15, 28, 0, 0, 6, 0, 0, 0, 4, 16, 0, 0, 6, 14, 0, 0, 4, 16, 0, 0, 8, 3, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 8, 4, 0, 0, 14, 17, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 2, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, + 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 14, 8, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 15, 18, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 14, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, + 15, 28, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 17, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 8, 4, 0, 0, 14, 17, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 0, 0, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, + 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 8, 3, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 28, 0, 0, 11, 14, 0, 0, 4, 16, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, + 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 15, 28, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 10, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 6, 14, 0, 0, 14, 8, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, + 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 28, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 6, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 14, 8, 0, 0, 4, 16, 0, 0, 9, 7, 0, 0, + 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, + 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/IndicSyllabicCategoryTrie.Generated.cs b/SixLabors.Fonts/Unicode/Resources/IndicSyllabicCategoryTrie.Generated.cs new file mode 100644 index 0000000..34c6f84 --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/IndicSyllabicCategoryTrie.Generated.cs @@ -0,0 +1,363 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class IndicSyllabicCategoryTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 112, 1, 0, 0, 0, 0, 0, 192, 135, 0, 0, 88, 2, 0, 0, 96, 2, 0, 0, 104, 2, 0, 0, 112, 2, 0, 0, 136, 2, 0, 0, 144, 2, 0, 0, 149, 2, 0, 0, 157, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, + 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, + 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 111, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 165, 2, 0, 0, 123, 4, 0, 0, 59, 5, 0, 0, 1, 6, 0, 0, 173, 2, 0, 0, 131, 4, 0, 0, 67, 5, 0, 0, 181, 2, 0, 0, 189, 2, 0, 0, 211, 4, 0, 0, 75, 5, 0, 0, 197, 2, 0, 0, 205, 2, 0, 0, 139, 4, 0, 0, 83, 5, 0, 0, 219, 4, 0, 0, 213, 2, 0, 0, 139, 4, 0, 0, 91, 5, 0, 0, 9, 6, 0, 0, 221, 2, 0, 0, 206, 6, 0, 0, 99, 5, 0, 0, 76, 8, 0, 0, 229, 2, 0, 0, + 147, 4, 0, 0, 107, 5, 0, 0, 17, 6, 0, 0, 237, 2, 0, 0, 155, 4, 0, 0, 115, 5, 0, 0, 245, 2, 0, 0, 229, 2, 0, 0, 163, 4, 0, 0, 123, 5, 0, 0, 25, 6, 0, 0, 253, 2, 0, 0, 175, 7, 0, 0, 131, 5, 0, 0, 214, 6, 0, 0, 183, 7, 0, 0, 162, 5, 0, 0, 5, 3, 0, 0, 103, 2, 0, 0, 191, 7, 0, 0, 170, 5, 0, 0, 13, 3, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 227, 4, 0, 0, + 197, 7, 0, 0, 20, 3, 0, 0, 28, 3, 0, 0, 9, 8, 0, 0, 52, 8, 0, 0, 103, 2, 0, 0, 200, 7, 0, 0, 36, 3, 0, 0, 33, 6, 0, 0, 222, 6, 0, 0, 230, 6, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 178, 5, 0, 0, 186, 5, 0, 0, 41, 6, 0, 0, 49, 6, 0, 0, 200, 7, 0, 0, 57, 6, 0, 0, 44, 3, 0, 0, 100, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 201, 7, 0, 0, 52, 3, 0, 0, 209, 7, 0, 0, 123, 7, 0, 0, 200, 7, 0, 0, 238, 6, 0, 0, 246, 6, 0, 0, 103, 2, 0, 0, 254, 6, 0, 0, 200, 7, 0, 0, 65, 6, 0, 0, 60, 3, 0, 0, 84, 8, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 68, 3, 0, 0, 235, 4, 0, 0, 138, 5, 0, 0, 103, 2, 0, 0, 76, 3, 0, 0, 171, 4, 0, 0, + 200, 7, 0, 0, 243, 4, 0, 0, 200, 7, 0, 0, 83, 3, 0, 0, 217, 7, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 29, 8, 0, 0, 146, 7, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 54, 8, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 153, 7, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 61, 8, 0, 0, 69, 8, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 37, 8, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 159, 7, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 91, 3, 0, 0, 194, 5, 0, 0, 131, 7, 0, 0, + 99, 3, 0, 0, 107, 3, 0, 0, 6, 7, 0, 0, 115, 3, 0, 0, 123, 3, 0, 0, 225, 7, 0, 0, 138, 7, 0, 0, 202, 5, 0, 0, 103, 2, 0, 0, 131, 3, 0, 0, 251, 4, 0, 0, 146, 5, 0, 0, 14, 7, 0, 0, 73, 6, 0, 0, 22, 7, 0, 0, 17, 8, 0, 0, 167, 7, 0, 0, 200, 7, 0, 0, 30, 7, 0, 0, 25, 8, 0, 0, 115, 4, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 78, 6, 0, 0, 210, 5, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 224, 9, 0, 0, 224, 9, 0, 0, 32, 10, 0, 0, 84, 10, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, + 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 156, 9, 0, 0, 47, 1, 0, 0, 78, 8, 0, 0, 142, 8, 0, 0, 206, 8, 0, 0, 47, 1, 0, 0, 47, 1, 0, 0, 47, 1, 0, 0, 47, 1, 0, 0, 47, 1, 0, 0, 47, 1, 0, 0, + 47, 1, 0, 0, 47, 1, 0, 0, 9, 9, 0, 0, 31, 9, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 139, 3, 0, 0, 3, 5, 0, 0, 92, 8, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 147, 3, 0, 0, 38, 7, 0, 0, 154, 5, 0, 0, 218, 5, 0, 0, 155, 3, 0, 0, 11, 5, 0, 0, 46, 7, 0, 0, + 103, 2, 0, 0, 163, 3, 0, 0, 226, 5, 0, 0, 53, 7, 0, 0, 19, 5, 0, 0, 171, 3, 0, 0, 59, 7, 0, 0, 179, 3, 0, 0, 100, 8, 0, 0, 86, 6, 0, 0, 187, 3, 0, 0, 94, 6, 0, 0, 103, 2, 0, 0, 102, 6, 0, 0, 110, 6, 0, 0, 195, 3, 0, 0, 27, 5, 0, 0, 203, 3, 0, 0, 179, 4, 0, 0, 211, 3, 0, 0, 118, 6, 0, 0, 126, 6, 0, 0, 187, 4, 0, 0, 219, 3, 0, 0, 45, 8, 0, 0, + 134, 6, 0, 0, 67, 7, 0, 0, 227, 3, 0, 0, 1, 8, 0, 0, 142, 6, 0, 0, 235, 3, 0, 0, 243, 3, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 134, 6, 0, 0, 251, 3, 0, 0, 35, 5, 0, 0, 103, 2, 0, 0, 134, 6, 0, 0, 3, 4, 0, 0, 75, 7, 0, 0, 103, 2, 0, 0, 150, 6, 0, 0, 11, 4, 0, 0, 108, 8, 0, 0, 115, 8, 0, 0, 228, 7, 0, 0, + 234, 5, 0, 0, 236, 7, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 150, 6, 0, 0, 19, 4, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 158, 6, 0, 0, 27, 4, 0, 0, 43, 5, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 166, 6, 0, 0, 35, 4, 0, 0, 195, 4, 0, 0, 83, 7, 0, 0, 43, 4, 0, 0, + 249, 5, 0, 0, 200, 7, 0, 0, 51, 4, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 91, 7, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 174, 6, 0, 0, 59, 4, 0, 0, 203, 4, 0, 0, 244, 7, 0, 0, 249, 7, 0, 0, 67, 4, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 182, 6, 0, 0, 99, 7, 0, 0, 75, 4, 0, 0, + 190, 6, 0, 0, 83, 4, 0, 0, 100, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 107, 7, 0, 0, 91, 4, 0, 0, 115, 7, 0, 0, 51, 5, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 198, 6, 0, 0, + 99, 4, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 107, 4, 0, 0, 242, 5, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, 103, 2, 0, 0, + 103, 2, 0, 0, 103, 2, 0, 0, 87, 2, 1, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 11, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 1, 0, 0, 0, 255, 0, 0, 0, 29, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 1, 0, 0, 0, 17, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 10, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, + 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 1, 0, 0, 0, 20, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 1, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, + 1, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 9, 0, 0, 0, 1, 0, 0, 0, 26, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 255, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 26, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, 31, 0, 0, 0, 33, 0, 0, 0, 18, 0, 0, 0, 26, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 35, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 29, 0, 0, 0, 15, 0, 0, 0, 9, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 26, 0, 0, 0, 18, 0, 0, 0, 29, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 1, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 35, 0, 0, 0, 29, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 18, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 26, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 6, 0, 0, 0, 33, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 6, 0, 0, 0, 33, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 22, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 35, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 32, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 34, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 14, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 32, 0, 0, 0, 1, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 6, 0, 0, 0, 33, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 29, 0, 0, 0, 22, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, 32, 0, 0, 0, 22, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 32, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, 255, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 18, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 32, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 32, 0, 0, 0, + 22, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 32, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 32, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 32, 0, 0, 0, 22, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, + 32, 0, 0, 0, 22, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 26, 0, 0, 0, 18, 0, 0, 0, 13, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 26, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 16, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 11, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 12, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 1, 0, 0, 0, 33, 0, 0, 0, 17, 0, 0, 0, 18, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 32, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 1, 0, 0, 0, + 33, 0, 0, 0, 22, 0, 0, 0, 35, 0, 0, 0, 26, 0, 0, 0, 18, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 18, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 1, 0, 0, 0, 10, 0, 0, 0, 26, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 33, 0, 0, 0, 18, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 26, 0, 0, 0, 18, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 22, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 4, 0, 0, 0, 3, 0, 0, 0, 17, 0, 0, 0, 3, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 29, 0, 0, 0, 255, 0, 0, 0, 29, 0, 0, 0, 255, 0, 0, 0, 22, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 22, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 22, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 22, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 18, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 32, 0, 0, 0, 22, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 22, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 22, 0, 0, 0, 26, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 22, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 22, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 26, 0, 0, 0, 18, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 22, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 32, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 32, 0, 0, 0, 5, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 32, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 32, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 32, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 32, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 32, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 5, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 32, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 32, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 32, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 32, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 32, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 32, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 26, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 26, 0, 0, 0, 35, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 26, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 26, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 26, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, + 31, 0, 0, 0, 26, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 26, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 18, 0, 0, 0, 26, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 26, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, + 255, 0, 0, 0, 18, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, + 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 36, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 14, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 8, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 4, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 35, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 255, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 36, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 10, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 31, 0, 0, 0, + 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 4, 0, 0, 0, 31, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 31, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 34, 0, 0, 0, + 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 11, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, + 34, 0, 0, 0, 34, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 3, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 19, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, + 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 16, 0, 0, 0, + 16, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 30, 0, 0, 0, 31, 0, 0, 0, 30, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 29, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 29, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 29, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/LineBreakTrie.Generated.cs b/SixLabors.Fonts/Unicode/Resources/LineBreakTrie.Generated.cs new file mode 100644 index 0000000..9de1296 --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/LineBreakTrie.Generated.cs @@ -0,0 +1,786 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class LineBreakTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 8, 14, 0, 0, 0, 0, 0, 208, 44, 1, 0, 208, 3, 0, 0, 216, 3, 0, 0, 224, 3, 0, 0, 232, 3, 0, 0, 16, 4, 0, 0, 24, 4, 0, 0, 32, 4, 0, 0, 40, 4, 0, 0, 46, 4, 0, 0, 54, 4, 0, 0, 46, 4, 0, 0, 54, 4, 0, 0, 46, 4, 0, 0, 54, 4, 0, 0, 46, 4, 0, 0, 54, 4, 0, 0, 46, 4, 0, 0, 54, 4, 0, 0, 46, 4, 0, 0, 54, 4, 0, 0, 46, 4, 0, 0, 54, 4, 0, 0, + 61, 4, 0, 0, 69, 4, 0, 0, 77, 4, 0, 0, 85, 4, 0, 0, 86, 4, 0, 0, 94, 4, 0, 0, 102, 4, 0, 0, 110, 4, 0, 0, 46, 4, 0, 0, 54, 4, 0, 0, 46, 4, 0, 0, 54, 4, 0, 0, 46, 4, 0, 0, 54, 4, 0, 0, 118, 4, 0, 0, 126, 4, 0, 0, 46, 4, 0, 0, 54, 4, 0, 0, 122, 4, 0, 0, 130, 4, 0, 0, 135, 4, 0, 0, 143, 4, 0, 0, 149, 4, 0, 0, 157, 4, 0, 0, 165, 4, 0, 0, + 173, 4, 0, 0, 181, 4, 0, 0, 189, 4, 0, 0, 195, 4, 0, 0, 203, 4, 0, 0, 46, 4, 0, 0, 54, 4, 0, 0, 208, 4, 0, 0, 216, 4, 0, 0, 223, 4, 0, 0, 231, 4, 0, 0, 237, 4, 0, 0, 245, 4, 0, 0, 244, 4, 0, 0, 252, 4, 0, 0, 4, 5, 0, 0, 12, 5, 0, 0, 20, 5, 0, 0, 27, 5, 0, 0, 35, 5, 0, 0, 43, 5, 0, 0, 47, 5, 0, 0, 46, 4, 0, 0, 55, 5, 0, 0, 63, 5, 0, 0, + 70, 5, 0, 0, 72, 5, 0, 0, 80, 5, 0, 0, 88, 5, 0, 0, 96, 5, 0, 0, 102, 5, 0, 0, 110, 5, 0, 0, 118, 5, 0, 0, 126, 5, 0, 0, 132, 5, 0, 0, 140, 5, 0, 0, 148, 5, 0, 0, 156, 5, 0, 0, 162, 5, 0, 0, 170, 5, 0, 0, 178, 5, 0, 0, 186, 5, 0, 0, 162, 5, 0, 0, 194, 5, 0, 0, 202, 5, 0, 0, 210, 5, 0, 0, 218, 5, 0, 0, 226, 5, 0, 0, 233, 5, 0, 0, 241, 5, 0, 0, + 247, 5, 0, 0, 255, 5, 0, 0, 7, 6, 0, 0, 15, 6, 0, 0, 21, 6, 0, 0, 29, 6, 0, 0, 37, 6, 0, 0, 45, 6, 0, 0, 50, 6, 0, 0, 58, 6, 0, 0, 66, 6, 0, 0, 74, 6, 0, 0, 81, 6, 0, 0, 89, 6, 0, 0, 97, 6, 0, 0, 105, 6, 0, 0, 107, 6, 0, 0, 115, 6, 0, 0, 0, 4, 0, 0, 123, 6, 0, 0, 130, 6, 0, 0, 138, 6, 0, 0, 0, 4, 0, 0, 146, 6, 0, 0, 154, 6, 0, 0, + 132, 4, 0, 0, 162, 6, 0, 0, 170, 6, 0, 0, 177, 6, 0, 0, 185, 6, 0, 0, 0, 4, 0, 0, 193, 6, 0, 0, 193, 6, 0, 0, 201, 6, 0, 0, 193, 6, 0, 0, 205, 6, 0, 0, 46, 4, 0, 0, 213, 6, 0, 0, 46, 4, 0, 0, 221, 6, 0, 0, 221, 6, 0, 0, 221, 6, 0, 0, 229, 6, 0, 0, 229, 6, 0, 0, 235, 6, 0, 0, 237, 6, 0, 0, 237, 6, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 245, 6, 0, 0, + 46, 4, 0, 0, 253, 6, 0, 0, 1, 7, 0, 0, 9, 7, 0, 0, 46, 4, 0, 0, 15, 7, 0, 0, 46, 4, 0, 0, 21, 7, 0, 0, 29, 7, 0, 0, 37, 7, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 45, 7, 0, 0, 53, 7, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, + 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 61, 7, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 69, 7, 0, 0, 77, 7, 0, 0, 85, 7, 0, 0, 93, 7, 0, 0, 101, 7, 0, 0, 193, 6, 0, 0, 193, 6, 0, 0, 109, 7, 0, 0, 117, 7, 0, 0, 125, 7, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 133, 7, 0, 0, 141, 7, 0, 0, + 147, 7, 0, 0, 46, 4, 0, 0, 151, 7, 0, 0, 159, 7, 0, 0, 167, 7, 0, 0, 175, 7, 0, 0, 180, 7, 0, 0, 193, 6, 0, 0, 188, 7, 0, 0, 194, 7, 0, 0, 46, 4, 0, 0, 202, 7, 0, 0, 193, 6, 0, 0, 106, 6, 0, 0, 210, 7, 0, 0, 218, 7, 0, 0, 226, 7, 0, 0, 230, 7, 0, 0, 238, 7, 0, 0, 246, 7, 0, 0, 249, 7, 0, 0, 0, 8, 0, 0, 8, 8, 0, 0, 16, 8, 0, 0, 24, 8, 0, 0, + 32, 8, 0, 0, 39, 8, 0, 0, 46, 4, 0, 0, 46, 8, 0, 0, 54, 8, 0, 0, 61, 8, 0, 0, 43, 5, 0, 0, 69, 8, 0, 0, 77, 8, 0, 0, 83, 8, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 91, 8, 0, 0, 95, 8, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, + 46, 4, 0, 0, 45, 7, 0, 0, 46, 4, 0, 0, 103, 8, 0, 0, 111, 8, 0, 0, 46, 4, 0, 0, 119, 8, 0, 0, 123, 8, 0, 0, 131, 8, 0, 0, 139, 8, 0, 0, 147, 8, 0, 0, 155, 8, 0, 0, 163, 8, 0, 0, 171, 8, 0, 0, 179, 8, 0, 0, 187, 8, 0, 0, 191, 8, 0, 0, 199, 8, 0, 0, 207, 8, 0, 0, 211, 8, 0, 0, 219, 8, 0, 0, 226, 8, 0, 0, 46, 4, 0, 0, 233, 8, 0, 0, 46, 4, 0, 0, + 241, 8, 0, 0, 249, 8, 0, 0, 1, 9, 0, 0, 9, 9, 0, 0, 17, 9, 0, 0, 24, 9, 0, 0, 46, 4, 0, 0, 32, 9, 0, 0, 38, 9, 0, 0, 45, 9, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 49, 9, 0, 0, 46, 4, 0, 0, 55, 9, 0, 0, 63, 9, 0, 0, 71, 9, 0, 0, 71, 9, 0, 0, 71, 9, 0, 0, 71, 9, 0, 0, 72, 9, 0, 0, 71, 9, 0, 0, + 71, 9, 0, 0, 80, 9, 0, 0, 84, 9, 0, 0, 92, 9, 0, 0, 100, 9, 0, 0, 107, 9, 0, 0, 115, 9, 0, 0, 123, 9, 0, 0, 131, 9, 0, 0, 139, 9, 0, 0, 147, 9, 0, 0, 155, 9, 0, 0, 163, 9, 0, 0, 171, 9, 0, 0, 179, 9, 0, 0, 187, 9, 0, 0, 46, 4, 0, 0, 191, 9, 0, 0, 199, 9, 0, 0, 205, 9, 0, 0, 46, 4, 0, 0, 212, 9, 0, 0, 219, 9, 0, 0, 227, 9, 0, 0, 46, 4, 0, 0, + 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 235, 9, 0, 0, 46, 4, 0, 0, 242, 9, 0, 0, 249, 9, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 1, 10, 0, 0, + 8, 10, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 14, 10, 0, 0, 46, 4, 0, 0, 213, 6, 0, 0, 46, 4, 0, 0, 22, 10, 0, 0, 30, 10, 0, 0, 38, 10, 0, 0, 38, 10, 0, 0, 77, 4, 0, 0, 46, 10, 0, 0, 54, 10, 0, 0, 62, 10, 0, 0, 0, 4, 0, 0, + 70, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 80, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 88, 10, 0, 0, 94, 10, 0, 0, 102, 10, 0, 0, 110, 10, 0, 0, 118, 10, 0, 0, 126, 10, 0, 0, 134, 10, 0, 0, 142, 10, 0, 0, 126, 10, 0, 0, 150, 10, 0, 0, 158, 10, 0, 0, 162, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 167, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 174, 10, 0, 0, 182, 10, 0, 0, 77, 10, 0, 0, 190, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 46, 4, 0, 0, 46, 4, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 194, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 200, 10, 0, 0, 77, 10, 0, 0, 207, 10, 0, 0, 61, 8, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, + 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 212, 10, 0, 0, 220, 10, 0, 0, 46, 4, 0, 0, 228, 10, 0, 0, 246, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 236, 10, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 244, 10, 0, 0, 252, 10, 0, 0, 4, 11, 0, 0, 12, 11, 0, 0, 46, 4, 0, 0, + 20, 11, 0, 0, 120, 4, 0, 0, 72, 4, 0, 0, 28, 11, 0, 0, 36, 11, 0, 0, 4, 5, 0, 0, 44, 11, 0, 0, 51, 11, 0, 0, 59, 11, 0, 0, 67, 11, 0, 0, 71, 11, 0, 0, 79, 11, 0, 0, 87, 11, 0, 0, 32, 8, 0, 0, 95, 11, 0, 0, 103, 11, 0, 0, 193, 6, 0, 0, 193, 6, 0, 0, 193, 6, 0, 0, 111, 11, 0, 0, 119, 11, 0, 0, 127, 11, 0, 0, 135, 11, 0, 0, 46, 4, 0, 0, 140, 11, 0, 0, + 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 148, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, + 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, + 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, + 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, + 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, + 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, + 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, + 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, + 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, + 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, + 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, + 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, + 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, + 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 162, 11, 0, 0, 156, 11, 0, 0, 157, 11, 0, 0, 158, 11, 0, 0, + 159, 11, 0, 0, 160, 11, 0, 0, 161, 11, 0, 0, 169, 11, 0, 0, 176, 11, 0, 0, 179, 11, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, + 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 195, 11, 0, 0, 203, 11, 0, 0, 211, 11, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, + 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 215, 11, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 223, 11, 0, 0, 227, 11, 0, 0, 235, 11, 0, 0, 243, 11, 0, 0, 250, 11, 0, 0, 2, 12, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 8, 12, 0, 0, 16, 12, 0, 0, 24, 12, 0, 0, 32, 12, 0, 0, 40, 12, 0, 0, 45, 12, 0, 0, 182, 10, 0, 0, 53, 12, 0, 0, + 61, 12, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, + 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 187, 11, 0, 0, 192, 15, 0, 0, 192, 15, 0, 0, 64, 16, 0, 0, 128, 16, 0, 0, 184, 16, 0, 0, 184, 16, 0, 0, 184, 16, 0, 0, 184, 16, 0, 0, 184, 16, 0, 0, 184, 16, 0, 0, 184, 16, 0, 0, 244, 16, 0, 0, 52, 17, 0, 0, 88, 17, 0, 0, 152, 17, 0, 0, 184, 16, 0, 0, 184, 16, 0, 0, + 184, 16, 0, 0, 216, 17, 0, 0, 184, 16, 0, 0, 232, 17, 0, 0, 28, 18, 0, 0, 84, 18, 0, 0, 148, 18, 0, 0, 212, 18, 0, 0, 12, 19, 0, 0, 184, 16, 0, 0, 64, 19, 0, 0, 124, 19, 0, 0, 180, 19, 0, 0, 208, 19, 0, 0, 16, 20, 0, 0, 225, 9, 0, 0, 33, 10, 0, 0, 97, 10, 0, 0, 161, 10, 0, 0, 225, 10, 0, 0, 12, 11, 0, 0, 76, 11, 0, 0, 111, 11, 0, 0, 144, 11, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 200, 11, 0, 0, 8, 12, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 72, 12, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 120, 12, 0, 0, 184, 12, 0, 0, 216, 12, 0, 0, 0, 7, 0, 0, 254, 12, 0, 0, 62, 13, 0, 0, 126, 13, 0, 0, 190, 13, 0, 0, 254, 13, 0, 0, 62, 14, 0, 0, 126, 14, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, + 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, + 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 190, 14, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, + 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 147, 1, 0, 0, 190, 14, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 254, 14, 0, 0, 131, 4, 0, 0, 69, 12, 0, 0, 77, 12, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 85, 12, 0, 0, 93, 12, 0, 0, 96, 12, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 102, 12, 0, 0, 109, 12, 0, 0, 113, 12, 0, 0, 117, 12, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 244, 10, 0, 0, 46, 4, 0, 0, 247, 10, 0, 0, 125, 12, 0, 0, 46, 4, 0, 0, 131, 12, 0, 0, 43, 5, 0, 0, 135, 12, 0, 0, 143, 12, 0, 0, 46, 4, 0, 0, 151, 12, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 111, 8, 0, 0, 129, 7, 0, 0, 159, 12, 0, 0, 165, 12, 0, 0, 46, 4, 0, 0, 170, 12, 0, 0, 46, 4, 0, 0, + 177, 12, 0, 0, 181, 12, 0, 0, 186, 12, 0, 0, 46, 4, 0, 0, 194, 12, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 30, 10, 0, 0, 151, 7, 0, 0, 197, 12, 0, 0, 109, 4, 0, 0, 205, 12, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 213, 12, 0, 0, 216, 12, 0, 0, 224, 12, 0, 0, 46, 4, 0, 0, + 159, 7, 0, 0, 232, 12, 0, 0, 0, 4, 0, 0, 240, 12, 0, 0, 247, 12, 0, 0, 255, 12, 0, 0, 37, 7, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 7, 13, 0, 0, 9, 10, 0, 0, 46, 4, 0, 0, 15, 13, 0, 0, 22, 13, 0, 0, 30, 13, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 38, 13, 0, 0, 46, 4, 0, 0, 46, 13, 0, 0, 113, 8, 0, 0, 54, 13, 0, 0, 60, 13, 0, 0, + 68, 13, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 107, 12, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 76, 13, 0, 0, 46, 4, 0, 0, 84, 13, 0, 0, 46, 4, 0, 0, 91, 13, 0, 0, 4, 5, 0, 0, 99, 13, 0, 0, 106, 13, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 159, 7, 0, 0, 46, 4, 0, 0, 114, 13, 0, 0, + 122, 13, 0, 0, 129, 13, 0, 0, 46, 4, 0, 0, 170, 12, 0, 0, 137, 13, 0, 0, 113, 12, 0, 0, 145, 13, 0, 0, 113, 12, 0, 0, 196, 12, 0, 0, 30, 10, 0, 0, 153, 13, 0, 0, 248, 7, 0, 0, 161, 13, 0, 0, 168, 13, 0, 0, 16, 8, 0, 0, 176, 13, 0, 0, 184, 13, 0, 0, 190, 13, 0, 0, 16, 8, 0, 0, 198, 13, 0, 0, 206, 13, 0, 0, 210, 13, 0, 0, 16, 8, 0, 0, 193, 4, 0, 0, 218, 13, 0, 0, + 226, 13, 0, 0, 106, 4, 0, 0, 234, 13, 0, 0, 242, 13, 0, 0, 0, 4, 0, 0, 250, 13, 0, 0, 2, 14, 0, 0, 111, 4, 0, 0, 10, 14, 0, 0, 18, 14, 0, 0, 24, 14, 0, 0, 32, 14, 0, 0, 40, 14, 0, 0, 48, 14, 0, 0, 53, 14, 0, 0, 61, 14, 0, 0, 69, 14, 0, 0, 46, 4, 0, 0, 19, 8, 0, 0, 77, 14, 0, 0, 85, 14, 0, 0, 46, 4, 0, 0, 73, 4, 0, 0, 93, 14, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 101, 14, 0, 0, 109, 14, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 73, 4, 0, 0, 117, 14, 0, 0, 125, 14, 0, 0, 46, 4, 0, 0, 133, 14, 0, 0, 141, 14, 0, 0, 148, 14, 0, 0, 156, 14, 0, 0, 164, 14, 0, 0, 172, 14, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, + 180, 14, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 188, 14, 0, 0, 196, 14, 0, 0, 202, 14, 0, 0, 210, 14, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 218, 14, 0, 0, 222, 14, 0, 0, 230, 14, 0, 0, 238, 14, 0, 0, 242, 14, 0, 0, 250, 14, 0, 0, 46, 4, 0, 0, 1, 15, 0, 0, 9, 15, 0, 0, 46, 4, 0, 0, 133, 7, 0, 0, 17, 15, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 25, 15, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 33, 15, 0, 0, 41, 15, 0, 0, 46, 15, 0, 0, 54, 15, 0, 0, 61, 15, 0, 0, 66, 15, 0, 0, 72, 15, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 80, 15, 0, 0, 84, 15, 0, 0, 92, 15, 0, 0, 100, 15, 0, 0, 106, 15, 0, 0, 129, 7, 0, 0, 165, 12, 0, 0, 114, 15, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 122, 15, 0, 0, 130, 15, 0, 0, 135, 15, 0, 0, 143, 15, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 150, 15, 0, 0, 158, 15, 0, 0, 166, 15, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, + 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 37, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 174, 15, 0, 0, 46, 4, 0, 0, + 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 182, 15, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 113, 12, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 76, 13, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, + 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 190, 15, 0, 0, 46, 4, 0, 0, 198, 15, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 201, 15, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, + 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 208, 15, 0, 0, 216, 15, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, + 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, + 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 85, 12, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, + 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 224, 15, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 231, 15, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 33, 8, 0, 0, 239, 15, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, + 133, 7, 0, 0, 159, 7, 0, 0, 247, 15, 0, 0, 46, 4, 0, 0, 159, 7, 0, 0, 129, 7, 0, 0, 252, 15, 0, 0, 46, 4, 0, 0, 4, 16, 0, 0, 11, 16, 0, 0, 19, 16, 0, 0, 223, 11, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 46, 4, 0, 0, 27, 16, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 35, 16, 0, 0, 43, 16, 0, 0, 194, 12, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 50, 16, 0, 0, 77, 4, 0, 0, 56, 16, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 64, 16, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, + 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 151, 7, 0, 0, 70, 16, 0, 0, 182, 10, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 78, 16, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 83, 16, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 91, 16, 0, 0, 96, 16, 0, 0, 103, 16, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 78, 10, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, + 46, 4, 0, 0, 111, 16, 0, 0, 116, 16, 0, 0, 124, 16, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 132, 16, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, + 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 140, 16, 0, 0, 247, 10, 0, 0, 247, 10, 0, 0, 77, 4, 0, 0, 148, 16, 0, 0, 155, 16, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 182, 15, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, + 151, 7, 0, 0, 46, 4, 0, 0, 139, 4, 0, 0, 46, 4, 0, 0, 162, 16, 0, 0, 170, 16, 0, 0, 176, 16, 0, 0, 46, 4, 0, 0, 63, 9, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 184, 16, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 194, 12, 0, 0, 194, 12, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 30, 10, 0, 0, 133, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 46, 4, 0, 0, 46, 4, 0, 0, 119, 8, 0, 0, 46, 4, 0, 0, 192, 16, 0, 0, 200, 16, 0, 0, 208, 16, 0, 0, 46, 4, 0, 0, 215, 16, 0, 0, 210, 16, 0, 0, 223, 16, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 83, 12, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, + 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 228, 16, 0, 0, 232, 16, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 77, 4, 0, 0, 240, 16, 0, 0, + 77, 4, 0, 0, 247, 16, 0, 0, 254, 16, 0, 0, 6, 17, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 159, 7, 0, 0, 13, 17, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 21, 17, 0, 0, 29, 17, 0, 0, 46, 4, 0, 0, 54, 9, 0, 0, + 37, 17, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 45, 17, 0, 0, 53, 17, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 113, 12, 0, 0, 61, 17, 0, 0, 46, 4, 0, 0, 69, 17, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 113, 12, 0, 0, 77, 17, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 113, 12, 0, 0, 85, 17, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 159, 7, 0, 0, 93, 17, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 101, 17, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 109, 17, 0, 0, 0, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 117, 17, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 252, 10, 0, 0, 46, 4, 0, 0, 125, 17, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 141, 4, 0, 0, 111, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 208, 16, 0, 0, 133, 17, 0, 0, 141, 17, 0, 0, 149, 17, 0, 0, 157, 17, 0, 0, 165, 17, 0, 0, 0, 4, 0, 0, 29, 15, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 173, 17, 0, 0, 178, 17, 0, 0, 71, 9, 0, 0, 184, 17, 0, 0, 71, 9, 0, 0, 189, 17, 0, 0, 77, 10, 0, 0, 196, 17, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 204, 17, 0, 0, 212, 17, 0, 0, 220, 17, 0, 0, 224, 17, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 232, 17, 0, 0, 239, 17, 0, 0, 247, 17, 0, 0, 255, 17, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 7, 18, 0, 0, 14, 18, 0, 0, 20, 18, 0, 0, 23, 18, 0, 0, 30, 18, 0, 0, 77, 10, 0, 0, 36, 18, 0, 0, 43, 18, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 50, 18, 0, 0, + 54, 18, 0, 0, 77, 10, 0, 0, 62, 18, 0, 0, 70, 18, 0, 0, 77, 10, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 78, 18, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 165, 9, 0, 0, 77, 10, 0, 0, 140, 11, 0, 0, 46, 4, 0, 0, 86, 18, 0, 0, 46, 4, 0, 0, 170, 12, 0, 0, 94, 18, 0, 0, 102, 18, 0, 0, 0, 4, 0, 0, 110, 18, 0, 0, 118, 18, 0, 0, 77, 10, 0, 0, 126, 18, 0, 0, + 77, 10, 0, 0, 132, 18, 0, 0, 139, 18, 0, 0, 77, 10, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 147, 18, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 155, 18, 0, 0, 159, 18, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 167, 18, 0, 0, 46, 4, 0, 0, 46, 4, 0, 0, 172, 18, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 180, 18, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, + 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 77, 10, 0, 0, 180, 18, 0, 0, 188, 18, 0, 0, 77, 4, 0, 0, 77, 4, 0, 0, 77, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 77, 4, 0, 0, + 77, 4, 0, 0, 77, 4, 0, 0, 77, 4, 0, 0, 77, 4, 0, 0, 77, 4, 0, 0, 77, 4, 0, 0, 196, 18, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, + 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 207, 3, 1, 0, 207, 3, 1, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 17, 0, 0, 0, 37, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 36, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 41, 0, 0, 0, 6, 0, 0, 0, 3, 0, 0, 0, 12, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 12, 0, 0, 0, + 9, 0, 0, 0, 8, 0, 0, 0, 16, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 6, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, + 2, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 1, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 38, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 3, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 18, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 3, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, + 33, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 33, 0, 0, 0, 18, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 18, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 18, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 8, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 8, 0, 0, 0, 46, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 46, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 6, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 6, 0, 0, 0, 21, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 10, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 6, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 8, 0, 0, 0, 6, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 21, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 11, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 9, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, + 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 12, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 9, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 18, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 18, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, + 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, + 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 12, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 255, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 12, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 12, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 4, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 17, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, + 12, 0, 0, 0, 6, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 17, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 17, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 17, 0, 0, 0, 18, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, + 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, + 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, + 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, + 27, 0, 0, 0, 27, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 46, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 5, 0, 0, 0, 39, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 9, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 17, 0, 0, 0, 17, 0, 0, 0, 18, 0, 0, 0, 12, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 4, 0, 0, 0, 21, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, + 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 48, 0, 0, 0, + 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 14, 0, 0, 0, 17, 0, 0, 0, + 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, + 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, + 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 4, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 4, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 18, 0, 0, 0, 12, 0, 0, 0, + 255, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 4, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 31, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 46, 0, 0, 0, 4, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 19, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 17, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 33, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 10, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 22, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 10, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 9, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, + 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, + 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, + 12, 0, 0, 0, 33, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, + 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 15, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, + 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, + 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, + 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, + 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, + 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 33, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 14, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 33, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 33, 0, 0, 0, + 33, 0, 0, 0, 33, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 14, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 14, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, + 33, 0, 0, 0, 33, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 33, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 12, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, + 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, + 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 6, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, + 46, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 6, 0, 0, 0, 12, 0, 0, 0, + 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 46, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, + 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 46, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 17, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 5, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 21, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 35, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 14, 0, 0, 0, 5, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, + 35, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 5, 0, 0, 0, 35, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 14, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 5, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 6, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 18, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, + 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, + 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 48, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 45, 0, 0, 0, + 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, + 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 21, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, + 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 17, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 23, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 23, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, + 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, + 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, + 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 13, 0, 0, 0, 21, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 12, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 21, 0, 0, 0, 4, 0, 0, 0, 21, 0, 0, 0, 4, 0, 0, 0, 21, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 21, 0, 0, 0, 4, 0, 0, 0, 21, 0, 0, 0, + 4, 0, 0, 0, 21, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 21, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 1, 0, 0, 0, 14, 0, 0, 0, 1, 0, 0, 0, 255, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 22, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 1, 0, 0, 0, 14, 0, 0, 0, 1, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 6, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 1, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 1, 0, 0, 0, 14, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 14, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 32, 0, 0, 0, 33, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, + 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 46, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 46, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, + 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 48, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, + 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 21, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 43, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 4, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 11, 0, 0, 0, 17, 0, 0, 0, + 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, + 12, 0, 0, 0, 18, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 18, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 255, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, + 43, 0, 0, 0, 43, 0, 0, 0, 255, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 255, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 17, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 48, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 45, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 43, 0, 0, 0, + 43, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 255, 0, 0, 0, 45, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 45, 0, 0, 0, 255, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 43, 0, 0, 0, + 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 48, 0, 0, 0, + 44, 0, 0, 0, 21, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 18, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, + 18, 0, 0, 0, 18, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, + 17, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, + 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 43, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 255, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 255, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, + 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 48, 0, 0, 0, 44, 0, 0, 0, 21, 0, 0, 0, 44, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, + 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 18, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 18, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 18, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, + 17, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 18, 0, 0, 0, 6, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 17, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 17, 0, 0, 0, + 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 44, 0, 0, 0, 21, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, + 255, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 48, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, + 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 1, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 17, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, + 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, + 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, + 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, + 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/MyanmarShapingData.Generated.cs b/SixLabors.Fonts/Unicode/Resources/MyanmarShapingData.Generated.cs new file mode 100644 index 0000000..7fb7a1c --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/MyanmarShapingData.Generated.cs @@ -0,0 +1,219 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; +using System.Collections.Generic; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static partial class MyanmarShapingData + { + public static int[][] StateTable => new int[65][] + { + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 2,2,3,4,5,5,6,7,2,2,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22 }, + new int[] { 0,0,23,24,25,25,26,27,0,0,0,0,28,29,30,31,32,33,34,35,36,37,38,39,26 }, + new int[] { 0,0,0,0,40,40,6,0,0,0,0,0,0,0,0,13,41,0,0,0,0,19,0,0,6 }, + new int[] { 42,42,0,0,0,0,0,0,0,0,42,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,40,40,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,0,0,6 }, + new int[] { 0,0,3,0,40,40,6,7,0,0,0,0,0,0,0,13,0,0,0,0,0,19,0,0,6 }, + new int[] { 0,0,23,24,25,25,26,27,0,0,0,0,28,29,30,31,43,33,34,35,36,37,38,39,26 }, + new int[] { 2,2,0,0,0,0,0,0,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,3,0,40,40,6,7,0,0,0,0,10,11,0,13,0,0,0,0,0,19,0,0,6 }, + new int[] { 0,0,3,0,40,40,6,7,0,0,0,0,0,11,0,13,0,0,0,0,0,19,0,0,6 }, + new int[] { 0,0,3,0,40,40,6,7,0,0,0,0,10,11,12,13,0,0,0,0,0,19,44,0,6 }, + new int[] { 0,0,45,0,40,40,6,46,0,0,0,0,47,0,0,13,48,49,0,0,0,19,0,48,6 }, + new int[] { 0,0,3,0,40,40,6,7,0,0,0,0,10,11,12,13,14,15,16,17,18,19,0,21,6 }, + new int[] { 0,0,3,0,40,40,6,7,0,0,0,0,10,11,12,13,44,0,0,0,0,19,0,21,6 }, + new int[] { 0,0,3,0,40,40,6,7,0,0,0,0,10,11,12,13,0,15,0,17,0,19,0,21,6 }, + new int[] { 0,0,3,0,40,40,6,7,0,0,0,0,10,11,12,13,44,50,0,0,0,19,0,21,6 }, + new int[] { 0,0,3,0,40,40,6,7,0,0,0,0,10,11,12,13,51,15,16,17,0,19,0,21,6 }, + new int[] { 0,0,52,0,40,40,6,19,0,0,0,0,0,0,0,0,6,0,0,0,0,19,0,0,6 }, + new int[] { 0,0,3,4,40,40,6,7,0,0,0,0,10,11,12,13,14,15,16,17,18,19,0,21,6 }, + new int[] { 0,0,3,0,40,40,6,7,0,0,0,0,10,11,12,13,44,0,0,0,0,19,0,0,6 }, + new int[] { 0,0,0,0,40,40,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,0,0,6 }, + new int[] { 0,0,0,0,25,25,26,0,0,0,0,0,0,0,0,31,53,0,0,0,0,37,0,0,26 }, + new int[] { 54,54,0,0,0,0,0,0,0,0,54,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,25,25,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,37,0,0,26 }, + new int[] { 0,0,23,0,25,25,26,27,0,0,0,0,0,0,0,31,0,0,0,0,0,37,0,0,26 }, + new int[] { 0,0,23,0,25,25,26,27,0,0,0,0,28,29,0,31,0,0,0,0,0,37,0,0,26 }, + new int[] { 0,0,23,0,25,25,26,27,0,0,0,0,0,29,0,31,0,0,0,0,0,37,0,0,26 }, + new int[] { 0,0,23,0,25,25,26,27,0,0,0,0,28,29,30,31,0,0,0,0,0,37,55,0,26 }, + new int[] { 0,0,56,0,25,25,26,57,0,0,0,0,58,0,0,31,59,60,0,0,0,37,0,59,26 }, + new int[] { 0,0,23,0,25,25,26,27,0,0,0,0,28,29,30,31,32,33,34,35,36,37,0,39,26 }, + new int[] { 0,0,23,0,25,25,26,27,0,0,0,0,28,29,30,31,55,0,0,0,0,37,0,39,26 }, + new int[] { 0,0,23,0,25,25,26,27,0,0,0,0,28,29,30,31,0,33,0,35,0,37,0,39,26 }, + new int[] { 0,0,23,0,25,25,26,27,0,0,0,0,28,29,30,31,55,61,0,0,0,37,0,39,26 }, + new int[] { 0,0,23,0,25,25,26,27,0,0,0,0,28,29,30,31,62,33,34,35,0,37,0,39,26 }, + new int[] { 0,0,63,0,25,25,26,37,0,0,0,0,0,0,0,0,26,0,0,0,0,37,0,0,26 }, + new int[] { 0,0,23,24,25,25,26,27,0,0,0,0,28,29,30,31,32,33,34,35,36,37,0,39,26 }, + new int[] { 0,0,23,0,25,25,26,27,0,0,0,0,28,29,30,31,55,0,0,0,0,37,0,0,26 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,40,40,6,0,0,0,0,0,0,0,0,13,0,0,0,0,0,19,0,0,6 }, + new int[] { 0,0,3,4,40,40,6,7,0,0,0,0,10,11,12,13,14,15,16,17,18,19,20,21,6 }, + new int[] { 0,0,23,64,25,25,26,27,0,0,0,0,28,29,30,31,32,33,34,35,36,37,0,39,26 }, + new int[] { 0,0,3,0,40,40,6,7,0,0,0,0,10,11,12,13,0,0,0,0,0,19,0,0,6 }, + new int[] { 0,0,0,0,40,40,6,0,0,0,0,0,0,0,0,13,41,0,0,0,0,19,0,0,6 }, + new int[] { 0,0,45,0,40,40,6,46,0,0,0,0,0,0,0,13,0,0,0,0,0,19,0,0,6 }, + new int[] { 0,0,45,0,40,40,6,46,0,0,0,0,47,0,0,13,0,0,0,0,0,19,0,0,6 }, + new int[] { 0,0,45,0,40,40,6,46,0,0,0,0,47,0,0,13,48,0,0,0,0,19,0,0,6 }, + new int[] { 0,0,45,0,40,40,6,46,0,0,0,0,47,0,0,13,48,0,0,0,0,19,0,48,6 }, + new int[] { 0,0,3,0,40,40,6,7,0,0,0,0,10,11,12,13,44,0,0,0,0,19,0,21,6 }, + new int[] { 0,0,3,0,40,40,6,7,0,0,0,0,10,11,12,13,0,15,16,17,0,19,0,21,6 }, + new int[] { 0,0,0,0,40,40,6,0,0,0,0,0,0,0,0,0,6,0,0,0,0,19,0,0,6 }, + new int[] { 0,0,0,0,25,25,26,0,0,0,0,0,0,0,0,31,0,0,0,0,0,37,0,0,26 }, + new int[] { 0,0,23,24,25,25,26,27,0,0,0,0,28,29,30,31,32,33,34,35,36,37,38,39,26 }, + new int[] { 0,0,23,0,25,25,26,27,0,0,0,0,28,29,30,31,0,0,0,0,0,37,0,0,26 }, + new int[] { 0,0,0,0,25,25,26,0,0,0,0,0,0,0,0,31,53,0,0,0,0,37,0,0,26 }, + new int[] { 0,0,56,0,25,25,26,57,0,0,0,0,0,0,0,31,0,0,0,0,0,37,0,0,26 }, + new int[] { 0,0,56,0,25,25,26,57,0,0,0,0,58,0,0,31,0,0,0,0,0,37,0,0,26 }, + new int[] { 0,0,56,0,25,25,26,57,0,0,0,0,58,0,0,31,59,0,0,0,0,37,0,0,26 }, + new int[] { 0,0,56,0,25,25,26,57,0,0,0,0,58,0,0,31,59,0,0,0,0,37,0,59,26 }, + new int[] { 0,0,23,0,25,25,26,27,0,0,0,0,28,29,30,31,55,0,0,0,0,37,0,39,26 }, + new int[] { 0,0,23,0,25,25,26,27,0,0,0,0,28,29,30,31,0,33,34,35,0,37,0,39,26 }, + new int[] { 0,0,0,0,25,25,26,0,0,0,0,0,0,0,0,0,26,0,0,0,0,37,0,0,26 }, + new int[] { 2,2,3,4,40,40,6,7,2,2,2,0,10,11,12,13,14,15,16,17,18,19,20,21,6 } + }; + + public static bool[] AcceptingStates => new bool[] + { + false, + true, + true, + true, + true, + true, + true, + true, + true, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true + }; + + public static string[][] Tags => new string[65][] + { + Array.Empty(), + new string[] { "broken_cluster" }, + new string[] { "consonant_syllable" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "joiner_or_spacing_mark","broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "consonant_syllable" }, + Array.Empty(), + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "joiner_or_spacing_mark","broken_cluster" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "consonant_syllable" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "broken_cluster" } + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/ScriptTrie.Generated.cs b/SixLabors.Fonts/Unicode/Resources/ScriptTrie.Generated.cs new file mode 100644 index 0000000..3500db5 --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/ScriptTrie.Generated.cs @@ -0,0 +1,819 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class ScriptTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 8, 14, 0, 0, 0, 0, 0, 0, 58, 1, 0, 31, 4, 0, 0, 39, 4, 0, 0, 47, 4, 0, 0, 55, 4, 0, 0, 79, 4, 0, 0, 87, 4, 0, 0, 95, 4, 0, 0, 103, 4, 0, 0, 109, 4, 0, 0, 117, 4, 0, 0, 109, 4, 0, 0, 117, 4, 0, 0, 109, 4, 0, 0, 117, 4, 0, 0, 109, 4, 0, 0, 117, 4, 0, 0, 109, 4, 0, 0, 117, 4, 0, 0, 109, 4, 0, 0, 117, 4, 0, 0, 111, 4, 0, 0, 119, 4, 0, 0, + 126, 4, 0, 0, 134, 4, 0, 0, 142, 4, 0, 0, 150, 4, 0, 0, 146, 4, 0, 0, 154, 4, 0, 0, 162, 4, 0, 0, 170, 4, 0, 0, 171, 4, 0, 0, 179, 4, 0, 0, 187, 4, 0, 0, 195, 4, 0, 0, 187, 4, 0, 0, 195, 4, 0, 0, 202, 4, 0, 0, 210, 4, 0, 0, 187, 4, 0, 0, 195, 4, 0, 0, 206, 4, 0, 0, 214, 4, 0, 0, 219, 4, 0, 0, 227, 4, 0, 0, 233, 4, 0, 0, 241, 4, 0, 0, 247, 4, 0, 0, + 255, 4, 0, 0, 7, 5, 0, 0, 15, 5, 0, 0, 23, 5, 0, 0, 31, 5, 0, 0, 36, 5, 0, 0, 44, 5, 0, 0, 45, 5, 0, 0, 53, 5, 0, 0, 61, 5, 0, 0, 69, 5, 0, 0, 75, 5, 0, 0, 83, 5, 0, 0, 91, 5, 0, 0, 99, 5, 0, 0, 107, 5, 0, 0, 115, 5, 0, 0, 131, 14, 0, 0, 136, 14, 0, 0, 42, 15, 0, 0, 214, 8, 0, 0, 218, 8, 0, 0, 15, 5, 0, 0, 15, 5, 0, 0, 123, 5, 0, 0, + 53, 9, 0, 0, 53, 9, 0, 0, 57, 9, 0, 0, 131, 5, 0, 0, 71, 9, 0, 0, 77, 9, 0, 0, 84, 9, 0, 0, 92, 9, 0, 0, 100, 9, 0, 0, 106, 9, 0, 0, 114, 9, 0, 0, 122, 9, 0, 0, 130, 9, 0, 0, 136, 9, 0, 0, 143, 9, 0, 0, 151, 9, 0, 0, 159, 9, 0, 0, 165, 9, 0, 0, 172, 9, 0, 0, 180, 9, 0, 0, 188, 9, 0, 0, 196, 9, 0, 0, 204, 9, 0, 0, 211, 9, 0, 0, 231, 9, 0, 0, + 237, 9, 0, 0, 244, 9, 0, 0, 252, 9, 0, 0, 4, 10, 0, 0, 10, 10, 0, 0, 17, 10, 0, 0, 25, 10, 0, 0, 33, 10, 0, 0, 38, 10, 0, 0, 45, 10, 0, 0, 52, 10, 0, 0, 60, 10, 0, 0, 67, 10, 0, 0, 75, 10, 0, 0, 83, 10, 0, 0, 99, 10, 0, 0, 139, 5, 0, 0, 100, 10, 0, 0, 63, 4, 0, 0, 108, 10, 0, 0, 115, 10, 0, 0, 123, 10, 0, 0, 63, 4, 0, 0, 131, 10, 0, 0, 131, 10, 0, 0, + 137, 10, 0, 0, 142, 10, 0, 0, 133, 10, 0, 0, 147, 10, 0, 0, 147, 5, 0, 0, 63, 4, 0, 0, 155, 10, 0, 0, 155, 10, 0, 0, 155, 10, 0, 0, 155, 10, 0, 0, 155, 10, 0, 0, 179, 10, 0, 0, 186, 10, 0, 0, 155, 5, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 5, 11, 0, 0, 5, 11, 0, 0, 11, 11, 0, 0, + 5, 11, 0, 0, 19, 11, 0, 0, 9, 11, 0, 0, 27, 11, 0, 0, 5, 11, 0, 0, 23, 11, 0, 0, 5, 11, 0, 0, 33, 11, 0, 0, 41, 11, 0, 0, 49, 11, 0, 0, 89, 11, 0, 0, 89, 11, 0, 0, 92, 11, 0, 0, 100, 11, 0, 0, 100, 11, 0, 0, 100, 11, 0, 0, 100, 11, 0, 0, 100, 11, 0, 0, 100, 11, 0, 0, 100, 11, 0, 0, 100, 11, 0, 0, 100, 11, 0, 0, 100, 11, 0, 0, 100, 11, 0, 0, 100, 11, 0, 0, + 100, 11, 0, 0, 100, 11, 0, 0, 100, 11, 0, 0, 100, 11, 0, 0, 100, 11, 0, 0, 100, 11, 0, 0, 100, 11, 0, 0, 100, 11, 0, 0, 128, 11, 0, 0, 136, 11, 0, 0, 136, 11, 0, 0, 163, 5, 0, 0, 148, 12, 0, 0, 171, 5, 0, 0, 156, 12, 0, 0, 164, 12, 0, 0, 144, 11, 0, 0, 144, 11, 0, 0, 145, 11, 0, 0, 150, 11, 0, 0, 179, 5, 0, 0, 158, 11, 0, 0, 158, 11, 0, 0, 160, 11, 0, 0, 158, 11, 0, 0, + 108, 11, 0, 0, 100, 11, 0, 0, 112, 11, 0, 0, 172, 12, 0, 0, 180, 12, 0, 0, 188, 12, 0, 0, 193, 12, 0, 0, 45, 13, 0, 0, 50, 13, 0, 0, 56, 13, 0, 0, 144, 11, 0, 0, 26, 13, 0, 0, 60, 14, 0, 0, 61, 14, 0, 0, 69, 14, 0, 0, 77, 14, 0, 0, 100, 12, 0, 0, 104, 12, 0, 0, 112, 12, 0, 0, 139, 13, 0, 0, 139, 13, 0, 0, 144, 13, 0, 0, 139, 13, 0, 0, 196, 13, 0, 0, 196, 13, 0, 0, + 13, 15, 0, 0, 16, 15, 0, 0, 204, 13, 0, 0, 206, 13, 0, 0, 213, 13, 0, 0, 217, 13, 0, 0, 172, 8, 0, 0, 190, 10, 0, 0, 187, 5, 0, 0, 195, 5, 0, 0, 109, 4, 0, 0, 31, 8, 0, 0, 34, 8, 0, 0, 42, 8, 0, 0, 109, 4, 0, 0, 49, 8, 0, 0, 142, 4, 0, 0, 142, 4, 0, 0, 109, 4, 0, 0, 109, 4, 0, 0, 109, 4, 0, 0, 109, 4, 0, 0, 109, 4, 0, 0, 109, 4, 0, 0, 109, 4, 0, 0, + 109, 4, 0, 0, 116, 8, 0, 0, 171, 4, 0, 0, 120, 8, 0, 0, 128, 8, 0, 0, 171, 4, 0, 0, 136, 8, 0, 0, 140, 8, 0, 0, 148, 8, 0, 0, 203, 5, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 210, 5, 0, 0, 218, 5, 0, 0, 31, 4, 0, 0, 226, 5, 0, 0, 120, 12, 0, 0, 31, 4, 0, 0, 234, 5, 0, 0, 239, 5, 0, 0, 109, 4, 0, 0, 247, 5, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, + 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 253, 5, 0, 0, 5, 6, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, + 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 18, 13, 0, 0, 18, 13, 0, 0, + 18, 13, 0, 0, 18, 13, 0, 0, 18, 13, 0, 0, 18, 13, 0, 0, 18, 13, 0, 0, 18, 13, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, + 13, 6, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 64, 13, 0, 0, 64, 13, 0, 0, 64, 13, 0, 0, 109, 4, 0, 0, 34, 13, 0, 0, 34, 13, 0, 0, 34, 13, 0, 0, 37, 13, 0, 0, 179, 10, 0, 0, 198, 10, 0, 0, 79, 13, 0, 0, 85, 13, 0, 0, 57, 11, 0, 0, 65, 11, 0, 0, 65, 11, 0, 0, 187, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 19, 6, 0, 0, 63, 4, 0, 0, + 235, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 245, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 253, 11, 0, 0, 75, 4, 0, 0, 27, 6, 0, 0, 35, 6, 0, 0, 171, 11, 0, 0, 172, 11, 0, 0, 43, 6, 0, 0, 51, 6, 0, 0, 201, 11, 0, 0, 53, 6, 0, 0, 225, 11, 0, 0, 214, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 61, 6, 0, 0, + 227, 11, 0, 0, 31, 4, 0, 0, 68, 6, 0, 0, 219, 10, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 76, 6, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 84, 6, 0, 0, 52, 6, 0, 0, 201, 11, 0, 0, 201, 11, 0, 0, 88, 6, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 31, 4, 0, 0, 31, 4, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, + 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 47, 12, 0, 0, 52, 12, 0, 0, 47, 12, 0, 0, 59, 12, 0, 0, 144, 14, 0, 0, 225, 13, 0, 0, 225, 13, 0, 0, + 225, 13, 0, 0, 225, 13, 0, 0, 225, 13, 0, 0, 225, 13, 0, 0, 225, 13, 0, 0, 225, 13, 0, 0, 225, 13, 0, 0, 230, 13, 0, 0, 187, 4, 0, 0, 187, 4, 0, 0, 187, 4, 0, 0, 160, 14, 0, 0, 160, 14, 0, 0, 162, 14, 0, 0, 31, 4, 0, 0, 96, 6, 0, 0, 109, 4, 0, 0, 109, 4, 0, 0, 102, 6, 0, 0, 109, 4, 0, 0, 57, 8, 0, 0, 65, 8, 0, 0, 93, 13, 0, 0, 110, 6, 0, 0, 186, 13, 0, 0, + 188, 13, 0, 0, 238, 13, 0, 0, 238, 13, 0, 0, 245, 13, 0, 0, 53, 9, 0, 0, 253, 13, 0, 0, 118, 6, 0, 0, 5, 14, 0, 0, 227, 10, 0, 0, 178, 14, 0, 0, 178, 14, 0, 0, 126, 6, 0, 0, 156, 10, 0, 0, 41, 14, 0, 0, 44, 14, 0, 0, 52, 14, 0, 0, 155, 10, 0, 0, 85, 14, 0, 0, 85, 14, 0, 0, 93, 14, 0, 0, 186, 14, 0, 0, 73, 11, 0, 0, 73, 8, 0, 0, 102, 4, 0, 0, 134, 6, 0, 0, + 89, 11, 0, 0, 89, 11, 0, 0, 194, 14, 0, 0, 199, 14, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, + 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, + 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, + 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, + 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, + 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, + 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, + 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, + 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, + 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, + 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, + 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, + 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, + 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, + 206, 10, 0, 0, 206, 10, 0, 0, 206, 10, 0, 0, 235, 10, 0, 0, 242, 10, 0, 0, 245, 10, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 5, 12, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 9, 12, 0, 0, 63, 4, 0, 0, 80, 8, 0, 0, 198, 8, 0, 0, 206, 8, 0, 0, 15, 5, 0, 0, 15, 5, 0, 0, 15, 5, 0, 0, 15, 5, 0, 0, 15, 5, 0, 0, 15, 5, 0, 0, 15, 5, 0, 0, 15, 5, 0, 0, 15, 5, 0, 0, 15, 5, 0, 0, 15, 5, 0, 0, + 15, 5, 0, 0, 15, 5, 0, 0, 15, 5, 0, 0, 142, 6, 0, 0, 15, 5, 0, 0, 15, 5, 0, 0, 15, 5, 0, 0, 15, 5, 0, 0, 224, 8, 0, 0, 228, 8, 0, 0, 150, 6, 0, 0, 158, 6, 0, 0, 162, 6, 0, 0, 169, 6, 0, 0, 15, 5, 0, 0, 15, 5, 0, 0, 15, 5, 0, 0, 175, 6, 0, 0, 183, 6, 0, 0, 47, 4, 0, 0, 47, 4, 0, 0, 190, 6, 0, 0, 195, 6, 0, 0, 219, 10, 0, 0, 253, 10, 0, 0, + 203, 6, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 252, 16, 0, 0, 252, 16, 0, 0, 60, 17, 0, 0, 124, 17, 0, 0, 180, 17, 0, 0, 180, 17, 0, 0, 180, 17, 0, 0, 180, 17, 0, 0, 180, 17, 0, 0, 180, 17, 0, 0, 188, 17, 0, 0, 248, 17, 0, 0, 56, 18, 0, 0, 72, 18, 0, 0, 136, 18, 0, 0, 172, 18, 0, 0, 236, 18, 0, 0, + 236, 18, 0, 0, 40, 19, 0, 0, 236, 18, 0, 0, 56, 19, 0, 0, 108, 19, 0, 0, 164, 19, 0, 0, 220, 19, 0, 0, 28, 20, 0, 0, 92, 20, 0, 0, 144, 20, 0, 0, 180, 20, 0, 0, 244, 20, 0, 0, 44, 21, 0, 0, 108, 21, 0, 0, 172, 21, 0, 0, 225, 9, 0, 0, 69, 12, 0, 0, 197, 12, 0, 0, 133, 12, 0, 0, 221, 14, 0, 0, 249, 15, 0, 0, 29, 15, 0, 0, 64, 15, 0, 0, 97, 15, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 57, 16, 0, 0, 93, 13, 0, 0, 161, 15, 0, 0, 161, 15, 0, 0, 161, 15, 0, 0, 201, 15, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 29, 13, 0, 0, 5, 13, 0, 0, 31, 10, 0, 0, 192, 6, 0, 0, 69, 10, 0, 0, 133, 10, 0, 0, 197, 11, 0, 0, 5, 12, 0, 0, 197, 10, 0, 0, 5, 11, 0, 0, 69, 11, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 157, 13, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 221, 13, 0, 0, 160, 1, 0, 0, 232, 13, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 30, 14, 0, 0, 192, 6, 0, 0, 64, 14, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 128, 14, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 157, 14, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, + 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 192, 6, 0, 0, 133, 11, 0, 0, 201, 12, 0, 0, 208, 12, 0, 0, 216, 12, 0, 0, 63, 4, 0, 0, 224, 12, 0, 0, 224, 12, 0, 0, 224, 12, 0, 0, 226, 12, 0, 0, 211, 6, 0, 0, 214, 6, 0, 0, 171, 4, 0, 0, 171, 4, 0, 0, 222, 6, 0, 0, 156, 8, 0, 0, 75, 4, 0, 0, 230, 6, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 13, 14, 0, 0, 21, 14, 0, 0, 25, 14, 0, 0, 238, 6, 0, 0, 67, 12, 0, 0, 74, 12, 0, 0, 80, 12, 0, 0, 161, 16, 0, 0, 234, 12, 0, 0, 101, 13, 0, 0, 108, 13, 0, 0, 63, 4, 0, 0, 88, 12, 0, 0, 88, 12, 0, 0, 92, 12, 0, 0, 242, 12, 0, 0, 250, 12, 0, 0, 255, 12, 0, 0, 142, 17, 0, 0, 148, 17, 0, 0, 198, 15, 0, 0, 147, 15, 0, 0, 151, 15, 0, 0, + 158, 15, 0, 0, 207, 18, 0, 0, 212, 18, 0, 0, 65, 19, 0, 0, 68, 19, 0, 0, 23, 16, 0, 0, 23, 16, 0, 0, 23, 16, 0, 0, 23, 16, 0, 0, 23, 16, 0, 0, 23, 16, 0, 0, 23, 16, 0, 0, 23, 16, 0, 0, 23, 16, 0, 0, 26, 16, 0, 0, 34, 16, 0, 0, 42, 16, 0, 0, 88, 8, 0, 0, 92, 8, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 179, 15, 0, 0, 179, 15, 0, 0, 179, 15, 0, 0, 185, 15, 0, 0, 190, 15, 0, 0, 62, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 246, 6, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, + 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 254, 6, 0, 0, 5, 7, 0, 0, 5, 7, 0, 0, 142, 4, 0, 0, 136, 12, 0, 0, 13, 7, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 62, 4, 0, 0, 63, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, + 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 17, 7, 0, 0, 31, 4, 0, 0, 25, 7, 0, 0, 31, 4, 0, 0, 32, 7, 0, 0, 40, 7, 0, 0, 46, 7, 0, 0, 31, 4, 0, 0, 5, 6, 0, 0, 171, 4, 0, 0, 171, 4, 0, 0, 164, 8, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 50, 7, 0, 0, 50, 7, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 58, 7, 0, 0, 66, 7, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 74, 7, 0, 0, 31, 4, 0, 0, 80, 7, 0, 0, 88, 7, 0, 0, 96, 7, 0, 0, 31, 4, 0, 0, 103, 7, 0, 0, 98, 7, 0, 0, 111, 7, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 118, 7, 0, 0, + 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 15, 6, 0, 0, 31, 4, 0, 0, 73, 16, 0, 0, 73, 16, 0, 0, 73, 16, 0, 0, 73, 16, 0, 0, 73, 16, 0, 0, 73, 16, 0, 0, 80, 16, 0, 0, 63, 4, 0, 0, 79, 17, 0, 0, 79, 17, 0, 0, 84, 17, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 126, 7, 0, 0, 31, 4, 0, 0, 131, 7, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 183, 6, 0, 0, 19, 6, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 254, 8, 0, 0, 6, 9, 0, 0, 14, 9, 0, 0, 22, 9, 0, 0, 30, 9, 0, 0, 38, 9, 0, 0, 63, 4, 0, 0, 45, 9, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 31, 4, 0, 0, + 242, 6, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 50, 7, 0, 0, 139, 7, 0, 0, 143, 7, 0, 0, 17, 7, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 252, 5, 0, 0, 63, 4, 0, 0, 151, 7, 0, 0, 159, 7, 0, 0, 163, 7, 0, 0, 171, 7, 0, 0, 179, 7, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, + 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, + 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 247, 6, 0, 0, 187, 7, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 195, 7, 0, 0, 203, 7, 0, 0, 242, 6, 0, 0, 31, 4, 0, 0, 211, 7, 0, 0, 31, 4, 0, 0, 219, 7, 0, 0, 224, 7, 0, 0, 232, 7, 0, 0, 63, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, + 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 120, 7, 0, 0, 240, 7, 0, 0, 248, 7, 0, 0, 31, 4, 0, 0, 255, 7, 0, 0, 7, 8, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 162, 6, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 15, 8, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 23, 8, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 31, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 142, 4, 0, 0, 142, 4, 0, 0, 142, 4, 0, 0, 142, 4, 0, 0, 142, 4, 0, 0, 142, 4, 0, 0, 142, 4, 0, 0, 140, 12, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 58, 17, 0, 0, 58, 17, 0, 0, 58, 17, 0, 0, 58, 17, 0, 0, 58, 17, 0, 0, 58, 17, 0, 0, 58, 17, 0, 0, 58, 17, 0, 0, 58, 17, 0, 0, + 58, 17, 0, 0, 58, 17, 0, 0, 58, 17, 0, 0, 58, 17, 0, 0, 58, 17, 0, 0, 58, 17, 0, 0, 58, 17, 0, 0, 58, 17, 0, 0, 58, 17, 0, 0, 58, 17, 0, 0, 58, 17, 0, 0, 63, 17, 0, 0, 71, 17, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 100, 8, 0, 0, 108, 8, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 71, 13, 0, 0, 180, 8, 0, 0, 187, 4, 0, 0, 185, 8, 0, 0, 190, 8, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 68, 18, 0, 0, 73, 18, 0, 0, 78, 18, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 194, 18, 0, 0, 199, 18, 0, 0, 86, 18, 0, 0, 88, 18, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 240, 18, 0, 0, 244, 18, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 37, 19, 0, 0, 41, 19, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 112, 19, 0, 0, 120, 19, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 81, 11, 0, 0, 7, 13, 0, 0, 10, 13, 0, 0, 207, 14, 0, 0, 143, 16, 0, 0, 127, 16, 0, 0, 135, 16, 0, 0, + 63, 4, 0, 0, 22, 17, 0, 0, 178, 13, 0, 0, 33, 14, 0, 0, 104, 19, 0, 0, 63, 4, 0, 0, 87, 15, 0, 0, 67, 15, 0, 0, 74, 15, 0, 0, 79, 15, 0, 0, 116, 13, 0, 0, 123, 13, 0, 0, 131, 13, 0, 0, 215, 14, 0, 0, 119, 16, 0, 0, 63, 4, 0, 0, 58, 16, 0, 0, 65, 16, 0, 0, 101, 14, 0, 0, 104, 14, 0, 0, 223, 14, 0, 0, 231, 14, 0, 0, 169, 16, 0, 0, 177, 16, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 239, 14, 0, 0, 239, 14, 0, 0, 245, 14, 0, 0, 63, 4, 0, 0, 38, 17, 0, 0, 42, 17, 0, 0, 38, 17, 0, 0, 50, 17, 0, 0, 8, 18, 0, 0, 14, 18, 0, 0, 252, 18, 0, 0, 3, 19, 0, 0, 10, 19, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 232, 8, 0, 0, 147, 18, 0, 0, 153, 18, 0, 0, 240, 8, 0, 0, 247, 8, 0, 0, + 34, 18, 0, 0, 22, 18, 0, 0, 26, 18, 0, 0, 171, 18, 0, 0, 177, 18, 0, 0, 96, 18, 0, 0, 101, 18, 0, 0, 42, 18, 0, 0, 216, 17, 0, 0, 217, 17, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 228, 16, 0, 0, 228, 16, 0, 0, 232, 16, 0, 0, 109, 18, 0, 0, 115, 18, 0, 0, 122, 18, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 50, 18, 0, 0, 53, 18, 0, 0, 60, 18, 0, 0, 208, 17, 0, 0, + 208, 17, 0, 0, 196, 17, 0, 0, 200, 17, 0, 0, 200, 17, 0, 0, 120, 11, 0, 0, 151, 16, 0, 0, 153, 16, 0, 0, 63, 9, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 121, 15, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 49, 19, 0, 0, 57, 19, 0, 0, 92, 17, 0, 0, 95, 17, 0, 0, 102, 17, 0, 0, 107, 17, 0, 0, 111, 17, 0, 0, 117, 17, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 168, 17, 0, 0, 171, 17, 0, 0, + 179, 17, 0, 0, 225, 17, 0, 0, 230, 17, 0, 0, 238, 17, 0, 0, 128, 19, 0, 0, 136, 19, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 246, 17, 0, 0, 220, 18, 0, 0, 225, 18, 0, 0, 233, 18, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 152, 14, 0, 0, 219, 9, 0, 0, 223, 9, 0, 0, 24, 15, 0, 0, 24, 15, 0, 0, 29, 15, 0, 0, + 34, 15, 0, 0, 253, 14, 0, 0, 253, 14, 0, 0, 5, 15, 0, 0, 129, 15, 0, 0, 50, 15, 0, 0, 53, 15, 0, 0, 59, 15, 0, 0, 50, 16, 0, 0, 115, 15, 0, 0, 115, 15, 0, 0, 115, 15, 0, 0, 91, 10, 0, 0, 2, 16, 0, 0, 7, 16, 0, 0, 15, 16, 0, 0, 63, 4, 0, 0, 30, 17, 0, 0, 196, 16, 0, 0, 200, 16, 0, 0, 206, 16, 0, 0, 206, 15, 0, 0, 128, 12, 0, 0, 213, 15, 0, 0, 221, 15, 0, 0, + 76, 19, 0, 0, 80, 19, 0, 0, 88, 19, 0, 0, 96, 19, 0, 0, 125, 17, 0, 0, 125, 17, 0, 0, 126, 17, 0, 0, 134, 17, 0, 0, 214, 16, 0, 0, 214, 16, 0, 0, 220, 16, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 185, 16, 0, 0, 188, 16, 0, 0, 186, 16, 0, 0, 63, 4, 0, 0, 88, 16, 0, 0, 88, 16, 0, 0, 95, 16, 0, 0, 163, 11, 0, 0, 137, 15, 0, 0, + 139, 15, 0, 0, 164, 10, 0, 0, 171, 10, 0, 0, 240, 16, 0, 0, 248, 16, 0, 0, 255, 16, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 180, 11, 0, 0, 172, 11, 0, 0, 172, 11, 0, 0, 172, 11, 0, 0, 172, 11, 0, 0, 172, 11, 0, 0, 172, 11, 0, 0, 172, 11, 0, 0, 172, 11, 0, 0, 188, 11, 0, 0, 193, 11, 0, 0, 217, 11, 0, 0, 187, 17, 0, 0, 187, 17, 0, 0, + 187, 17, 0, 0, 187, 17, 0, 0, 187, 17, 0, 0, 187, 17, 0, 0, 187, 17, 0, 0, 187, 17, 0, 0, 187, 17, 0, 0, 187, 17, 0, 0, 187, 17, 0, 0, 188, 17, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 209, 11, 0, 0, 160, 14, 0, 0, + 160, 14, 0, 0, 160, 14, 0, 0, 160, 14, 0, 0, 160, 14, 0, 0, 160, 14, 0, 0, 160, 14, 0, 0, 160, 14, 0, 0, 160, 14, 0, 0, 160, 14, 0, 0, 160, 14, 0, 0, 160, 14, 0, 0, 160, 14, 0, 0, 160, 14, 0, 0, 160, 14, 0, 0, 160, 14, 0, 0, 160, 14, 0, 0, 170, 14, 0, 0, 103, 16, 0, 0, 111, 16, 0, 0, 185, 18, 0, 0, 186, 18, 0, 0, 166, 15, 0, 0, 171, 15, 0, 0, 229, 15, 0, 0, 229, 15, 0, 0, + 236, 15, 0, 0, 242, 15, 0, 0, 250, 15, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 28, 19, 0, 0, 30, 19, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 254, 17, 0, 0, + 254, 17, 0, 0, 0, 18, 0, 0, 144, 19, 0, 0, 151, 19, 0, 0, 63, 4, 0, 0, 95, 15, 0, 0, 95, 15, 0, 0, 101, 15, 0, 0, 95, 15, 0, 0, 107, 15, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 17, 12, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 63, 4, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 25, 12, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 5, 12, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 33, 12, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 25, 12, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 39, 12, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, + 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 242, 11, 0, 0, 9, 12, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, + 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 154, 13, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 162, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, 152, 13, 0, 0, + 170, 13, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, + 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, + 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 115, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, + 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, + 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 112, 14, 0, 0, 123, 14, 0, 0, 7, 17, 0, 0, 7, 17, 0, 0, 7, 17, 0, 0, + 7, 17, 0, 0, 7, 17, 0, 0, 7, 17, 0, 0, 7, 17, 0, 0, 7, 17, 0, 0, 7, 17, 0, 0, 7, 17, 0, 0, 7, 17, 0, 0, 7, 17, 0, 0, 7, 17, 0, 0, 7, 17, 0, 0, 7, 17, 0, 0, 7, 17, 0, 0, 7, 17, 0, 0, 7, 17, 0, 0, 14, 17, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, + 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, + 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 130, 18, 0, 0, 130, 18, 0, 0, 130, 18, 0, 0, 130, 18, 0, 0, 130, 18, 0, 0, 130, 18, 0, 0, 130, 18, 0, 0, + 130, 18, 0, 0, 130, 18, 0, 0, 130, 18, 0, 0, 130, 18, 0, 0, 130, 18, 0, 0, 130, 18, 0, 0, 130, 18, 0, 0, 133, 18, 0, 0, 139, 18, 0, 0, 157, 17, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 156, 17, 0, 0, 160, 17, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 158, 18, 0, 0, 162, 18, 0, 0, 162, 18, 0, 0, 166, 18, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 18, 19, 0, 0, 20, 19, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, + 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 63, 4, 0, 0, 30, 4, 1, 0, 30, 4, 1, 0, 30, 4, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 1, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 1, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 46, 0, 0, 0, 1, 0, 0, 0, 46, 0, 0, 0, 1, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, + 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, + 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, + 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, + 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, + 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, + 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, + 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, + 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, + 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, + 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, + 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 2, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, + 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 0, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, + 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, + 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, + 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, + 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 147, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, + 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, + 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, + 31, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, + 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, + 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 0, 0, 0, 0, + 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, + 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 1, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, + 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, + 51, 0, 0, 0, 51, 0, 0, 0, 51, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 91, 0, 0, 0, 1, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, + 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, + 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 46, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 50, 0, 0, 0, 1, 0, 0, 0, 50, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, + 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, + 54, 0, 0, 0, 1, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, + 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, + 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, + 63, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, + 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 63, 0, 0, 0, + 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, + 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 1, 0, 0, 0, 62, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, + 118, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, + 61, 0, 0, 0, 61, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 46, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 0, + 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 1, 0, 0, 0, 63, 0, 0, 0, + 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, + 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 54, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 30, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 30, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 46, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, + 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, + 46, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, + 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, + 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, + 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 0, + 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, + 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, + 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 0, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 0, 0, 0, 0, 53, 0, 0, 0, 0, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 0, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 0, 0, 0, 0, + 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 53, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, + 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, + 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, + 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, + 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, + 48, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, + 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, + 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, + 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, + 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, + 47, 0, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, + 47, 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, + 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, + 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 0, + 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, + 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, + 0, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 0, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 0, 0, 0, 0, 141, 0, 0, 0, + 0, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, + 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 0, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 141, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 141, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, + 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, + 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, + 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 141, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 141, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, + 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 0, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 0, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, + 144, 0, 0, 0, 0, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, + 0, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 0, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 0, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 0, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 144, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 0, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, + 68, 0, 0, 0, 0, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 0, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, + 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 0, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 0, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 0, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 0, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68, 0, 0, 0, + 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 0, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 0, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 0, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, + 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, + 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 0, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 0, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, + 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, + 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 89, 0, 0, 0, 0, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, + 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, + 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, + 129, 0, 0, 0, 129, 0, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, + 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, + 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, + 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, + 0, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, + 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 0, 0, 0, 0, + 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, + 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 0, 0, 0, 0, 149, 0, 0, 0, 149, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, + 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 0, 0, 0, 0, 138, 0, 0, 0, + 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, + 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 95, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, + 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, + 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 40, 0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, + 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 16, 0, 0, 0, + 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, + 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, + 0, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, + 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, + 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, + 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, + 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, + 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, + 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, + 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, + 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, + 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, + 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 91, 0, 0, 0, + 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, + 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, + 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, + 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, + 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 120, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, + 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, + 65, 0, 0, 0, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 91, 0, 0, 0, + 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, + 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 91, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, + 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 63, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, + 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 54, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, + 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, + 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, + 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, + 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, + 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, + 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, + 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, + 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, + 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 142, 0, 0, 0, 101, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 67, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, + 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, + 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, + 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 50, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, + 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, + 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 161, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, + 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, + 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 60, 0, 0, 0, 60, 0, 0, 0, 60, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, + 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, 34, 0, 0, 0, + 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, + 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, + 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, + 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 146, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 146, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, + 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 137, 0, 0, 0, 137, 0, 0, 0, 137, 0, 0, 0, + 137, 0, 0, 0, 137, 0, 0, 0, 137, 0, 0, 0, 137, 0, 0, 0, 137, 0, 0, 0, 137, 0, 0, 0, 137, 0, 0, 0, 137, 0, 0, 0, 137, 0, 0, 0, 137, 0, 0, 0, 0, 0, 0, 0, 137, 0, 0, 0, 137, 0, 0, 0, 137, 0, 0, 0, 0, 0, 0, 0, 137, 0, 0, 0, 137, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, + 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 0, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, + 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, + 139, 0, 0, 0, 139, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 139, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, + 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 0, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, + 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 0, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, + 0, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 0, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, + 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, + 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 76, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, + 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 153, 0, 0, 0, 0, 0, 0, 0, 153, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, + 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 125, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, + 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, + 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, + 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, + 19, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, + 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, + 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, + 140, 0, 0, 0, 140, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 140, 0, 0, 0, 140, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, + 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, + 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 41, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, + 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 145, 0, 0, 0, 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 145, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, + 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 135, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, + 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, + 158, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 158, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, + 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, + 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, + 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, + 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 0, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 159, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, + 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 115, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, + 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 134, 0, 0, 0, + 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, + 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 134, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, + 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, + 73, 0, 0, 0, 73, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 73, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, + 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 103, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, + 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 154, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, + 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, + 123, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 123, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, + 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 62, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, + 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 118, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, + 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, + 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 79, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, + 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, + 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, + 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, + 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 143, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, + 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, + 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 121, 0, 0, 0, + 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 121, 0, 0, 0, 0, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, + 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, + 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 61, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, + 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, + 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 93, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 122, 0, 0, 0, + 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, + 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, + 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 117, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, + 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 113, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, + 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, + 104, 0, 0, 0, 104, 0, 0, 0, 104, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, + 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 132, 0, 0, 0, + 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, + 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, + 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, + 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 82, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 82, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, + 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, + 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, + 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, + 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 87, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, + 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 88, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, + 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, + 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 116, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, + 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, + 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 126, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, + 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 132, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, + 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 138, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 0, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, + 155, 0, 0, 0, 155, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, + 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 35, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, + 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, + 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, + 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, + 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 0, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, + 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, + 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, + 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 0, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, + 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 66, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, + 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, + 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, + 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, + 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, + 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, + 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, + 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 86, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, + 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 92, 0, 0, 0, + 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, + 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 0, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 92, 0, 0, 0, 92, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, + 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, + 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 97, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, + 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, + 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, + 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 109, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, + 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 110, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, + 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, + 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, + 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 127, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, + 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, + 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, + 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 150, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 156, 0, 0, 0, + 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, + 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 156, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 156, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, + 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 55, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, + 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 0, 0, 0, 0, 94, 0, 0, 0, 0, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 0, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, + 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 94, 0, 0, 0, 0, 0, 0, 0, 94, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, + 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, + 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 59, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, + 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 0, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, + 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 124, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, + 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 0, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, 84, 0, 0, 0, + 84, 0, 0, 0, 84, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, + 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, + 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 106, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, + 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 142, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 0, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 0, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, + 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 0, 0, 0, 0, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, + 0, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, + 43, 0, 0, 0, 43, 0, 0, 0, 43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, + 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, + 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, + 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 133, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, + 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, 162, 0, 0, 0, + 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, + 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, + 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, + 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, + 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 81, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, + 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, + 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, + 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, + 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, + 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, + 131, 0, 0, 0, 131, 0, 0, 0, 131, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, + 38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, + 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, + 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, + 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, + 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 57, 0, 0, 0, 57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, + 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 157, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 157, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, + 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, + 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, + 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, + 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, + 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 67, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 67, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, + 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, + 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 0, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 160, 0, 0, 0, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, + 108, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, + 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 151, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 152, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 0, 0, 0, 0, + 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 0, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 0, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, + 155, 0, 0, 0, 0, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 0, 0, 0, 0, 155, 0, 0, 0, 155, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, + 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 0, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, + 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, + 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 166, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, + 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 168, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, + 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, + 164, 0, 0, 0, 164, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, + 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 164, 0, 0, 0, 164, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, + 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 165, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, + 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, + 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 167, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, + 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, 169, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 169, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, + 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 171, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, + 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, 173, 0, 0, 0, + 173, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, + 175, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, + 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 175, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, 170, 0, 0, 0, + 170, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, + 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 0, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, + 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 172, 0, 0, 0, 172, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, + 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 174, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, + 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, + 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 163, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/UnicodeCategoryTrie.Generated.cs b/SixLabors.Fonts/Unicode/Resources/UnicodeCategoryTrie.Generated.cs new file mode 100644 index 0000000..731fc73 --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/UnicodeCategoryTrie.Generated.cs @@ -0,0 +1,894 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class UnicodeCategoryTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 0, 17, 0, 0, 0, 0, 0, 240, 86, 1, 0, 63, 4, 0, 0, 71, 4, 0, 0, 79, 4, 0, 0, 87, 4, 0, 0, 127, 4, 0, 0, 135, 4, 0, 0, 143, 4, 0, 0, 151, 4, 0, 0, 159, 4, 0, 0, 167, 4, 0, 0, 173, 4, 0, 0, 181, 4, 0, 0, 189, 4, 0, 0, 197, 4, 0, 0, 205, 4, 0, 0, 213, 4, 0, 0, 219, 4, 0, 0, 227, 4, 0, 0, 235, 4, 0, 0, 243, 4, 0, 0, 246, 4, 0, 0, 254, 4, 0, 0, + 6, 5, 0, 0, 14, 5, 0, 0, 22, 5, 0, 0, 30, 5, 0, 0, 26, 5, 0, 0, 34, 5, 0, 0, 42, 5, 0, 0, 50, 5, 0, 0, 55, 5, 0, 0, 63, 5, 0, 0, 71, 5, 0, 0, 79, 5, 0, 0, 83, 5, 0, 0, 91, 5, 0, 0, 99, 5, 0, 0, 107, 5, 0, 0, 115, 5, 0, 0, 123, 5, 0, 0, 119, 5, 0, 0, 127, 5, 0, 0, 132, 5, 0, 0, 140, 5, 0, 0, 146, 5, 0, 0, 154, 5, 0, 0, 162, 5, 0, 0, + 170, 5, 0, 0, 178, 5, 0, 0, 186, 5, 0, 0, 194, 5, 0, 0, 202, 5, 0, 0, 207, 5, 0, 0, 215, 5, 0, 0, 218, 5, 0, 0, 226, 5, 0, 0, 234, 5, 0, 0, 242, 5, 0, 0, 248, 5, 0, 0, 0, 6, 0, 0, 255, 5, 0, 0, 7, 6, 0, 0, 15, 6, 0, 0, 23, 6, 0, 0, 49, 18, 0, 0, 31, 6, 0, 0, 39, 6, 0, 0, 47, 6, 0, 0, 53, 6, 0, 0, 186, 5, 0, 0, 57, 18, 0, 0, 243, 19, 0, 0, + 174, 18, 0, 0, 176, 18, 0, 0, 184, 18, 0, 0, 65, 18, 0, 0, 61, 6, 0, 0, 67, 6, 0, 0, 75, 6, 0, 0, 83, 6, 0, 0, 91, 6, 0, 0, 97, 6, 0, 0, 105, 6, 0, 0, 113, 6, 0, 0, 121, 6, 0, 0, 127, 6, 0, 0, 135, 6, 0, 0, 143, 6, 0, 0, 151, 6, 0, 0, 157, 6, 0, 0, 165, 6, 0, 0, 173, 6, 0, 0, 181, 6, 0, 0, 189, 6, 0, 0, 197, 6, 0, 0, 204, 6, 0, 0, 212, 6, 0, 0, + 218, 6, 0, 0, 226, 6, 0, 0, 234, 6, 0, 0, 242, 6, 0, 0, 248, 6, 0, 0, 0, 7, 0, 0, 8, 7, 0, 0, 16, 7, 0, 0, 190, 18, 0, 0, 24, 7, 0, 0, 32, 7, 0, 0, 40, 7, 0, 0, 47, 7, 0, 0, 55, 7, 0, 0, 63, 7, 0, 0, 251, 5, 0, 0, 71, 7, 0, 0, 79, 7, 0, 0, 111, 4, 0, 0, 87, 7, 0, 0, 94, 7, 0, 0, 102, 7, 0, 0, 111, 4, 0, 0, 198, 18, 0, 0, 251, 19, 0, 0, + 109, 7, 0, 0, 114, 7, 0, 0, 122, 7, 0, 0, 129, 7, 0, 0, 137, 7, 0, 0, 111, 4, 0, 0, 186, 5, 0, 0, 206, 18, 0, 0, 214, 18, 0, 0, 222, 18, 0, 0, 230, 18, 0, 0, 95, 4, 0, 0, 145, 7, 0, 0, 248, 17, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 153, 7, 0, 0, + 186, 5, 0, 0, 161, 7, 0, 0, 165, 7, 0, 0, 173, 7, 0, 0, 186, 5, 0, 0, 179, 7, 0, 0, 186, 5, 0, 0, 185, 7, 0, 0, 193, 7, 0, 0, 201, 7, 0, 0, 95, 4, 0, 0, 95, 4, 0, 0, 209, 7, 0, 0, 238, 18, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 243, 18, 0, 0, 217, 7, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 225, 7, 0, 0, 233, 7, 0, 0, 241, 7, 0, 0, 249, 7, 0, 0, 1, 8, 0, 0, 186, 5, 0, 0, 247, 18, 0, 0, 9, 8, 0, 0, 17, 8, 0, 0, 25, 8, 0, 0, 186, 5, 0, 0, 73, 18, 0, 0, 33, 8, 0, 0, 255, 18, 0, 0, + 41, 8, 0, 0, 186, 5, 0, 0, 45, 8, 0, 0, 53, 8, 0, 0, 61, 8, 0, 0, 69, 8, 0, 0, 74, 8, 0, 0, 186, 5, 0, 0, 82, 8, 0, 0, 88, 8, 0, 0, 102, 21, 0, 0, 96, 8, 0, 0, 186, 5, 0, 0, 104, 8, 0, 0, 112, 8, 0, 0, 120, 8, 0, 0, 128, 8, 0, 0, 136, 8, 0, 0, 144, 8, 0, 0, 7, 19, 0, 0, 10, 19, 0, 0, 152, 8, 0, 0, 3, 20, 0, 0, 18, 19, 0, 0, 26, 19, 0, 0, + 186, 5, 0, 0, 160, 8, 0, 0, 186, 5, 0, 0, 168, 8, 0, 0, 176, 8, 0, 0, 75, 18, 0, 0, 184, 8, 0, 0, 188, 8, 0, 0, 196, 8, 0, 0, 204, 8, 0, 0, 239, 4, 0, 0, 0, 18, 0, 0, 83, 18, 0, 0, 6, 18, 0, 0, 13, 18, 0, 0, 83, 18, 0, 0, 22, 5, 0, 0, 22, 5, 0, 0, 159, 4, 0, 0, 159, 4, 0, 0, 159, 4, 0, 0, 159, 4, 0, 0, 92, 17, 0, 0, 159, 4, 0, 0, 159, 4, 0, 0, + 159, 4, 0, 0, 212, 8, 0, 0, 100, 17, 0, 0, 216, 8, 0, 0, 224, 8, 0, 0, 21, 18, 0, 0, 232, 8, 0, 0, 240, 8, 0, 0, 248, 8, 0, 0, 148, 20, 0, 0, 156, 20, 0, 0, 164, 20, 0, 0, 0, 9, 0, 0, 8, 9, 0, 0, 86, 21, 0, 0, 16, 9, 0, 0, 24, 9, 0, 0, 108, 17, 0, 0, 116, 17, 0, 0, 124, 17, 0, 0, 72, 20, 0, 0, 32, 9, 0, 0, 1, 21, 0, 0, 6, 21, 0, 0, 12, 21, 0, 0, + 17, 21, 0, 0, 17, 21, 0, 0, 17, 21, 0, 0, 17, 21, 0, 0, 17, 21, 0, 0, 17, 21, 0, 0, 17, 21, 0, 0, 17, 21, 0, 0, 204, 20, 0, 0, 212, 20, 0, 0, 102, 21, 0, 0, 25, 21, 0, 0, 33, 21, 0, 0, 20, 21, 0, 0, 41, 21, 0, 0, 49, 21, 0, 0, 102, 21, 0, 0, 39, 9, 0, 0, 47, 9, 0, 0, 83, 20, 0, 0, 84, 20, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 91, 20, 0, 0, 102, 21, 0, 0, + 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 52, 21, 0, 0, 60, 21, 0, 0, 62, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 70, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 99, 20, 0, 0, 105, 20, 0, 0, 102, 21, 0, 0, 220, 20, 0, 0, 227, 20, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, + 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 17, 21, 0, 0, 17, 21, 0, 0, 17, 21, 0, 0, 17, 21, 0, 0, 235, 20, 0, 0, 17, 21, 0, 0, 242, 20, 0, 0, 249, 20, 0, 0, 17, 21, 0, 0, 17, 21, 0, 0, 17, 21, 0, 0, 17, 21, 0, 0, 17, 21, 0, 0, 17, 21, 0, 0, 17, 21, 0, 0, 17, 21, 0, 0, 102, 21, 0, 0, 13, 21, 0, 0, 78, 21, 0, 0, + 55, 9, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 95, 4, 0, 0, 79, 5, 0, 0, 239, 4, 0, 0, 132, 17, 0, 0, 159, 4, 0, 0, 159, 4, 0, 0, 159, 4, 0, 0, 63, 9, 0, 0, 239, 4, 0, 0, 71, 9, 0, 0, 186, 5, 0, 0, 77, 9, 0, 0, 85, 9, 0, 0, 93, 9, 0, 0, 93, 9, 0, 0, 22, 5, 0, 0, 196, 20, 0, 0, 91, 18, 0, 0, 101, 9, 0, 0, 111, 4, 0, 0, + 109, 9, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 116, 9, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 124, 9, 0, 0, 51, 9, 0, 0, 99, 18, 0, 0, 107, 18, 0, 0, 251, 5, 0, 0, 186, 5, 0, 0, 132, 9, 0, 0, 238, 18, 0, 0, 186, 5, 0, 0, 115, 18, 0, 0, 140, 9, 0, 0, 144, 9, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 149, 9, 0, 0, + 186, 5, 0, 0, 102, 21, 0, 0, 156, 9, 0, 0, 164, 9, 0, 0, 113, 20, 0, 0, 119, 20, 0, 0, 102, 21, 0, 0, 113, 20, 0, 0, 127, 20, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 102, 21, 0, 0, 102, 21, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 123, 18, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 172, 9, 0, 0, 102, 21, 0, 0, 179, 9, 0, 0, 75, 18, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 129, 18, 0, 0, 187, 9, 0, 0, 159, 4, 0, 0, 140, 17, 0, 0, 148, 17, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 195, 9, 0, 0, 137, 18, 0, 0, 156, 17, 0, 0, 159, 4, 0, 0, 161, 17, 0, 0, 169, 17, 0, 0, 175, 17, 0, 0, 203, 9, 0, 0, 211, 9, 0, 0, 34, 19, 0, 0, 219, 9, 0, 0, 186, 5, 0, 0, + 227, 9, 0, 0, 42, 19, 0, 0, 45, 19, 0, 0, 235, 9, 0, 0, 53, 19, 0, 0, 15, 6, 0, 0, 61, 19, 0, 0, 243, 9, 0, 0, 251, 9, 0, 0, 174, 18, 0, 0, 65, 19, 0, 0, 3, 10, 0, 0, 11, 10, 0, 0, 186, 5, 0, 0, 19, 10, 0, 0, 27, 10, 0, 0, 145, 18, 0, 0, 186, 5, 0, 0, 73, 19, 0, 0, 35, 10, 0, 0, 43, 10, 0, 0, 51, 10, 0, 0, 59, 10, 0, 0, 29, 18, 0, 0, 65, 10, 0, 0, + 239, 4, 0, 0, 239, 4, 0, 0, 186, 5, 0, 0, 73, 10, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 81, 10, 0, 0, 88, 10, 0, 0, 91, 10, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, + 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, + 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, + 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, + 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, + 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, + 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, + 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, + 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, + 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 99, 10, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 103, 10, 0, 0, 111, 4, 0, 0, 111, 10, 0, 0, 119, 10, 0, 0, 127, 10, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 81, 19, 0, 0, 89, 19, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 94, 19, 0, 0, 102, 19, 0, 0, 186, 5, 0, 0, 106, 19, 0, 0, 186, 5, 0, 0, 133, 10, 0, 0, 137, 10, 0, 0, 145, 10, 0, 0, 11, 20, 0, 0, 153, 10, 0, 0, 161, 10, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 167, 10, 0, 0, 175, 10, 0, 0, 79, 4, 0, 0, 37, 18, 0, 0, 153, 18, 0, 0, 158, 18, 0, 0, 53, 8, 0, 0, 183, 10, 0, 0, + 191, 10, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, + 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 188, 20, 0, 0, 124, 17, 0, 0, 124, 17, 0, 0, 252, 17, 0, 0, 60, 18, 0, 0, 124, 18, 0, 0, 180, 18, 0, 0, 244, 18, 0, 0, 52, 19, 0, 0, 108, 19, 0, 0, 172, 19, 0, 0, 216, 19, 0, 0, 24, 20, 0, 0, 88, 20, 0, 0, 104, 20, 0, 0, 168, 20, 0, 0, 220, 20, 0, 0, 28, 21, 0, 0, + 76, 21, 0, 0, 140, 21, 0, 0, 204, 21, 0, 0, 220, 21, 0, 0, 16, 22, 0, 0, 72, 22, 0, 0, 136, 22, 0, 0, 200, 22, 0, 0, 8, 23, 0, 0, 60, 23, 0, 0, 104, 23, 0, 0, 168, 23, 0, 0, 224, 23, 0, 0, 252, 23, 0, 0, 60, 24, 0, 0, 128, 10, 0, 0, 192, 10, 0, 0, 0, 11, 0, 0, 64, 11, 0, 0, 128, 11, 0, 0, 171, 11, 0, 0, 235, 11, 0, 0, 160, 1, 0, 0, 14, 12, 0, 0, 64, 10, 0, 0, + 64, 10, 0, 0, 64, 10, 0, 0, 70, 12, 0, 0, 134, 12, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 198, 12, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 246, 12, 0, 0, 54, 13, 0, 0, 86, 13, 0, 0, 64, 10, 0, 0, 124, 13, 0, 0, 188, 13, 0, 0, 252, 13, 0, 0, 60, 14, 0, 0, 124, 14, 0, 0, 188, 14, 0, 0, 252, 14, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 188, 16, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 60, 15, 0, 0, 160, 1, 0, 0, 71, 15, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, + 160, 1, 0, 0, 125, 15, 0, 0, 64, 10, 0, 0, 159, 15, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 223, 15, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 160, 1, 0, 0, 252, 15, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, + 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, + 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, + 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, + 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, + 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, + 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, + 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, + 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, + 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, + 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, + 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, + 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, + 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, + 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 60, 16, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, + 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 64, 10, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 124, 16, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, + 0, 7, 0, 0, 124, 16, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 199, 10, 0, 0, 206, 10, 0, 0, 214, 10, 0, 0, 111, 4, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 222, 10, 0, 0, 230, 10, 0, 0, + 233, 10, 0, 0, 72, 20, 0, 0, 75, 20, 0, 0, 239, 10, 0, 0, 246, 10, 0, 0, 51, 9, 0, 0, 254, 10, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 251, 9, 0, 0, 186, 5, 0, 0, 6, 11, 0, 0, 14, 11, 0, 0, 186, 5, 0, 0, 20, 11, 0, 0, 28, 11, 0, 0, 32, 11, 0, 0, 40, 11, 0, 0, 186, 5, 0, 0, 48, 11, 0, 0, 111, 4, 0, 0, 95, 4, 0, 0, 81, 5, 0, 0, + 45, 18, 0, 0, 186, 5, 0, 0, 56, 11, 0, 0, 64, 11, 0, 0, 68, 11, 0, 0, 74, 11, 0, 0, 186, 5, 0, 0, 82, 11, 0, 0, 186, 5, 0, 0, 89, 11, 0, 0, 93, 11, 0, 0, 101, 11, 0, 0, 186, 5, 0, 0, 109, 11, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 85, 9, 0, 0, 45, 8, 0, 0, + 112, 11, 0, 0, 120, 11, 0, 0, 124, 11, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 132, 11, 0, 0, 135, 11, 0, 0, 143, 11, 0, 0, 111, 19, 0, 0, 53, 8, 0, 0, 151, 11, 0, 0, 111, 4, 0, 0, 159, 11, 0, 0, 167, 11, 0, 0, 175, 11, 0, 0, 103, 10, 0, 0, 111, 4, 0, 0, 186, 5, 0, 0, 183, 11, 0, 0, 191, 11, 0, 0, 83, 20, 0, 0, 199, 11, 0, 0, 206, 11, 0, 0, 214, 11, 0, 0, 119, 19, 0, 0, + 127, 19, 0, 0, 111, 4, 0, 0, 135, 19, 0, 0, 222, 11, 0, 0, 186, 5, 0, 0, 230, 11, 0, 0, 238, 11, 0, 0, 246, 11, 0, 0, 254, 11, 0, 0, 6, 12, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 14, 12, 0, 0, 111, 4, 0, 0, 95, 4, 0, 0, 22, 12, 0, 0, 239, 4, 0, 0, 30, 12, 0, 0, 186, 5, 0, 0, 38, 12, 0, 0, 183, 17, 0, 0, 46, 12, 0, 0, 53, 12, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 61, 12, 0, 0, 186, 5, 0, 0, 69, 12, 0, 0, 77, 12, 0, 0, 84, 12, 0, 0, 127, 19, 0, 0, 92, 12, 0, 0, 99, 12, 0, 0, 155, 11, 0, 0, 107, 12, 0, 0, 155, 11, 0, 0, 115, 12, 0, 0, 85, 9, 0, 0, 143, 19, 0, 0, 240, 5, 0, 0, 123, 12, 0, 0, 130, 12, 0, 0, 18, 19, 0, 0, 147, 19, 0, 0, + 138, 12, 0, 0, 144, 12, 0, 0, 155, 19, 0, 0, 152, 12, 0, 0, 160, 12, 0, 0, 164, 12, 0, 0, 18, 19, 0, 0, 159, 19, 0, 0, 167, 19, 0, 0, 172, 12, 0, 0, 180, 12, 0, 0, 175, 19, 0, 0, 188, 12, 0, 0, 111, 4, 0, 0, 196, 12, 0, 0, 204, 12, 0, 0, 18, 6, 0, 0, 212, 12, 0, 0, 220, 12, 0, 0, 226, 12, 0, 0, 234, 12, 0, 0, 242, 12, 0, 0, 250, 12, 0, 0, 254, 12, 0, 0, 6, 13, 0, 0, + 14, 13, 0, 0, 186, 5, 0, 0, 183, 19, 0, 0, 22, 13, 0, 0, 30, 13, 0, 0, 186, 5, 0, 0, 191, 19, 0, 0, 38, 13, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 186, 5, 0, 0, 46, 13, 0, 0, 54, 13, 0, 0, 111, 4, 0, 0, 186, 5, 0, 0, 199, 19, 0, 0, 62, 13, 0, 0, 70, 13, 0, 0, 186, 5, 0, 0, 78, 13, 0, 0, 86, 13, 0, 0, 93, 13, 0, 0, + 101, 13, 0, 0, 109, 13, 0, 0, 117, 13, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 186, 5, 0, 0, 125, 13, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 95, 4, 0, 0, 239, 4, 0, 0, 133, 13, 0, 0, 141, 13, 0, 0, 147, 13, 0, 0, 155, 13, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 163, 13, 0, 0, 167, 13, 0, 0, 175, 13, 0, 0, 207, 19, 0, 0, + 211, 19, 0, 0, 183, 13, 0, 0, 186, 5, 0, 0, 219, 19, 0, 0, 191, 13, 0, 0, 186, 5, 0, 0, 33, 8, 0, 0, 199, 13, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 207, 13, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 186, 5, 0, 0, 215, 13, 0, 0, 223, 13, 0, 0, 228, 13, 0, 0, 236, 13, 0, 0, 243, 13, 0, 0, 248, 13, 0, 0, 254, 13, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 6, 14, 0, 0, 10, 14, 0, 0, + 18, 14, 0, 0, 26, 14, 0, 0, 32, 14, 0, 0, 29, 8, 0, 0, 40, 14, 0, 0, 48, 14, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 56, 14, 0, 0, 64, 14, 0, 0, 69, 14, 0, 0, 77, 14, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 84, 14, 0, 0, 132, 20, 0, 0, 92, 14, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 103, 10, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 72, 20, 0, 0, 72, 20, 0, 0, 72, 20, 0, 0, 100, 14, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 108, 14, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 155, 11, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 116, 14, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 227, 19, 0, 0, 124, 14, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 222, 10, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 117, 13, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 1, 6, 0, 0, 132, 14, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 33, 8, 0, 0, 53, 8, 0, 0, 140, 14, 0, 0, 186, 5, 0, 0, 53, 8, 0, 0, 29, 8, 0, 0, 145, 14, 0, 0, 186, 5, 0, 0, 235, 19, 0, 0, 153, 14, 0, 0, 161, 14, 0, 0, 110, 11, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 166, 18, 0, 0, 169, 14, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 95, 4, 0, 0, 239, 4, 0, 0, 177, 14, 0, 0, 185, 14, 0, 0, 192, 14, 0, 0, 111, 4, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 200, 14, 0, 0, 56, 20, 0, 0, 206, 14, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 214, 14, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 45, 8, 0, 0, 220, 14, 0, 0, 53, 8, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 228, 14, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 233, 14, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 241, 14, 0, 0, 246, 14, 0, 0, 253, 14, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 91, 10, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 3, 15, 0, 0, 8, 15, 0, 0, 16, 15, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 24, 15, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, + 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 32, 15, 0, 0, 39, 15, 0, 0, 47, 15, 0, 0, 22, 5, 0, 0, 55, 15, 0, 0, 62, 15, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 69, 15, 0, 0, 111, 4, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, + 102, 21, 0, 0, 124, 9, 0, 0, 102, 21, 0, 0, 77, 15, 0, 0, 102, 21, 0, 0, 19, 20, 0, 0, 27, 20, 0, 0, 33, 20, 0, 0, 102, 21, 0, 0, 47, 9, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 85, 15, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 93, 15, 0, 0, 93, 15, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 101, 15, 0, 0, 109, 15, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 187, 17, 0, 0, 194, 17, 0, 0, 117, 15, 0, 0, 197, 17, 0, 0, 125, 15, 0, 0, 133, 15, 0, 0, 141, 15, 0, 0, 191, 17, 0, 0, 149, 15, 0, 0, 157, 15, 0, 0, 165, 15, 0, 0, 196, 17, 0, 0, 204, 17, 0, 0, 187, 17, 0, 0, 194, 17, 0, 0, 190, 17, 0, 0, 197, 17, 0, 0, 205, 17, 0, 0, 188, 17, 0, 0, 195, 17, 0, 0, 191, 17, 0, 0, 172, 15, 0, 0, 213, 17, 0, 0, 221, 17, 0, 0, + 228, 17, 0, 0, 235, 17, 0, 0, 216, 17, 0, 0, 224, 17, 0, 0, 231, 17, 0, 0, 238, 17, 0, 0, 180, 15, 0, 0, 64, 20, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 22, 5, 0, 0, + 41, 20, 0, 0, 22, 5, 0, 0, 48, 20, 0, 0, 188, 15, 0, 0, 196, 15, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 204, 15, 0, 0, 212, 15, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 220, 15, 0, 0, 228, 15, 0, 0, 83, 18, 0, 0, + 233, 15, 0, 0, 238, 15, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 186, 5, 0, 0, 246, 15, 0, 0, 254, 15, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 155, 11, 0, 0, 6, 16, 0, 0, 186, 5, 0, 0, 14, 16, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 155, 11, 0, 0, 22, 16, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 155, 11, 0, 0, 30, 16, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 53, 8, 0, 0, 38, 16, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 46, 16, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 54, 16, 0, 0, 111, 4, 0, 0, 95, 4, 0, 0, 246, 17, 0, 0, 62, 16, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 70, 16, 0, 0, 83, 20, 0, 0, 75, 16, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 83, 16, 0, 0, 88, 16, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 96, 16, 0, 0, 104, 16, 0, 0, 112, 16, 0, 0, 120, 16, 0, 0, 128, 16, 0, 0, 136, 16, 0, 0, 111, 4, 0, 0, 143, 16, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 102, 21, 0, 0, 151, 16, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 116, 9, 0, 0, 156, 16, 0, 0, 160, 16, 0, 0, 124, 9, 0, 0, 140, 20, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 165, 16, 0, 0, 111, 4, 0, 0, 172, 16, 0, 0, 180, 16, 0, 0, 184, 16, 0, 0, 192, 16, 0, 0, 200, 16, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 102, 21, 0, 0, + 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 94, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, + 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 208, 16, 0, 0, 211, 16, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 219, 16, 0, 0, 227, 16, 0, 0, 151, 16, 0, 0, 102, 21, 0, 0, 235, 16, 0, 0, 102, 21, 0, 0, 243, 16, 0, 0, 248, 16, 0, 0, 0, 17, 0, 0, 111, 4, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, + 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 8, 17, 0, 0, 16, 17, 0, 0, 24, 17, 0, 0, 102, 21, 0, 0, 31, 17, 0, 0, 39, 17, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 47, 17, 0, 0, 102, 21, 0, 0, 102, 21, 0, 0, 52, 17, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 56, 11, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 99, 10, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 60, 17, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 56, 11, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 47, 6, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 103, 10, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 68, 17, 0, 0, 172, 20, 0, 0, 172, 20, 0, 0, 172, 20, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 22, 5, 0, 0, 22, 5, 0, 0, 22, 5, 0, 0, 22, 5, 0, 0, 22, 5, 0, 0, 22, 5, 0, 0, 22, 5, 0, 0, 76, 17, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, + 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 111, 4, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, + 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, + 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, + 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 180, 20, 0, 0, 84, 17, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 111, 4, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, 186, 5, 0, 0, + 186, 5, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 11, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 26, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 24, 0, 0, 0, 25, 0, 0, 0, 24, 0, 0, 0, 19, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 24, 0, 0, 0, 21, 0, 0, 0, 27, 0, 0, 0, 18, 0, 0, 0, 27, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 20, 0, 0, 0, + 25, 0, 0, 0, 21, 0, 0, 0, 25, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 11, 0, 0, 0, 24, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 28, 0, 0, 0, 24, 0, 0, 0, 27, 0, 0, 0, 28, 0, 0, 0, 4, 0, 0, 0, + 22, 0, 0, 0, 25, 0, 0, 0, 15, 0, 0, 0, 28, 0, 0, 0, 27, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 27, 0, 0, 0, 1, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 27, 0, 0, 0, 10, 0, 0, 0, 4, 0, 0, 0, 23, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 25, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 27, 0, 0, 0, + 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, + 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 3, 0, 0, 0, 27, 0, 0, 0, 3, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, + 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 28, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 3, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 24, 0, 0, 0, 19, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 26, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 19, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 26, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 15, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 15, 0, 0, 0, + 28, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 28, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 15, 0, 0, 0, + 4, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 28, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 3, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 3, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 27, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, 26, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 24, 0, 0, 0, 26, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 28, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, + 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, + 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 26, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 26, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 3, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 5, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 11, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 3, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 26, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 19, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 15, 0, 0, 0, 5, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 3, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 27, 0, 0, 0, 1, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 29, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 29, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 10, 0, 0, 0, 3, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, + 3, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 29, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 10, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 3, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 19, 0, 0, 0, 24, 0, 0, 0, + 20, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, + 20, 0, 0, 0, 21, 0, 0, 0, 19, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 27, 0, 0, 0, + 27, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 26, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 3, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 24, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 25, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, + 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 26, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 19, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 25, 0, 0, 0, 19, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 26, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 15, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 26, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 24, 0, 0, 0, 25, 0, 0, 0, 24, 0, 0, 0, 19, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 24, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 25, 0, 0, 0, 27, 0, 0, 0, 28, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 24, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 29, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 29, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 19, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 19, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 15, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, + 5, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, + 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 28, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, + 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 4, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 26, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 24, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 24, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 29, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 29, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 24, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 5, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 26, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 3, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 26, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 25, 0, 0, 0, 25, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 29, 0, 0, 0, 15, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, + 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, + 17, 0, 0, 0, 17, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 0, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 1, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 1, 0, 0, 0, 28, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 24, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 24, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 24, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 27, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 27, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 20, 0, 0, 0, 25, 0, 0, 0, 21, 0, 0, 0, 25, 0, 0, 0, 20, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 24, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 22, 0, 0, 0, + 23, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 3, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 19, 0, 0, 0, 19, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 11, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 28, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 19, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 21, 0, 0, 0, 28, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 19, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, + 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 21, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, + 21, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 28, 0, 0, 0, 24, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 19, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 28, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 28, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 15, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 15, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, 5, 0, 0, 0, 28, 0, 0, 0, 5, 0, 0, 0, 28, 0, 0, 0, 5, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 24, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 24, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, + 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 5, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, + 21, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 28, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 22, 0, 0, 0, + 23, 0, 0, 0, 20, 0, 0, 0, 22, 0, 0, 0, 22, 0, 0, 0, 23, 0, 0, 0, 20, 0, 0, 0, 22, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 12, 0, 0, 0, 13, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 11, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 22, 0, 0, 0, 23, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 25, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 25, 0, 0, 0, 24, 0, 0, 0, 18, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 11, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, + 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, + 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 22, 0, 0, 0, 23, 0, 0, 0, 22, 0, 0, 0, 23, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, + 24, 0, 0, 0, 22, 0, 0, 0, 23, 0, 0, 0, 24, 0, 0, 0, 22, 0, 0, 0, 23, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 19, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 19, 0, 0, 0, 24, 0, 0, 0, 22, 0, 0, 0, 23, 0, 0, 0, 24, 0, 0, 0, 24, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, + 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, + 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, + 25, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, + 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, + 20, 0, 0, 0, 21, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, + 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, + 28, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, + 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, + 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, + 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 29, 0, 0, 0, + 29, 0, 0, 0, + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/UniversalShapingData.Generated.cs b/SixLabors.Fonts/Unicode/Resources/UniversalShapingData.Generated.cs new file mode 100644 index 0000000..dbbd2fd --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/UniversalShapingData.Generated.cs @@ -0,0 +1,298 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; +using System.Collections.Generic; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class UniversalShapingData + { + public static string[] Categories => new string[] + { + "O", + "IND", + "S", + "GB", + "B", + "FMPst", + "CGJ", + "VMAbv", + "VMPst", + "VAbv", + "VPst", + "CMBlw", + "VPre", + "VBlw", + "H", + "VMBlw", + "FMAbv", + "CMAbv", + "MBlw", + "CS", + "R", + "HVM", + "FMBlw", + "SUB", + "MPst", + "MPre", + "FAbv", + "FPst", + "FBlw", + "MAbv", + "SMAbv", + "SMBlw", + "RK", + "VMPre", + "ZWNJ", + "ZWJ", + "WJ", + "VS", + "N", + "HN" + }; + + public static Dictionary Decompositions => new() + { + { 0x9CB, new int[] { 0x9C7,0x9BE } }, + { 0x9CC, new int[] { 0x9C7,0x9D7 } }, + { 0xB48, new int[] { 0xB47,0xB56 } }, + { 0xB4B, new int[] { 0xB47,0xB3E } }, + { 0xB4C, new int[] { 0xB47,0xB57 } }, + { 0xBCA, new int[] { 0xBC6,0xBBE } }, + { 0xBCB, new int[] { 0xBC7,0xBBE } }, + { 0xBCC, new int[] { 0xBC6,0xBD7 } }, + { 0xC48, new int[] { 0xC46,0xC56 } }, + { 0xCC0, new int[] { 0xCBF,0xCD5 } }, + { 0xCC7, new int[] { 0xCC6,0xCD5 } }, + { 0xCC8, new int[] { 0xCC6,0xCD6 } }, + { 0xCCA, new int[] { 0xCC6,0xCC2 } }, + { 0xCCB, new int[] { 0xCC6,0xCC2,0xCD5 } }, + { 0xD4A, new int[] { 0xD46,0xD3E } }, + { 0xD4B, new int[] { 0xD47,0xD3E } }, + { 0xD4C, new int[] { 0xD46,0xD57 } }, + { 0xDDA, new int[] { 0xDD9,0xDCA } }, + { 0xDDC, new int[] { 0xDD9,0xDCF } }, + { 0xDDD, new int[] { 0xDD9,0xDCF,0xDCA } }, + { 0xDDE, new int[] { 0xDD9,0xDDF } }, + { 0xF73, new int[] { 0xF71,0xF72 } }, + { 0xF75, new int[] { 0xF71,0xF74 } }, + { 0xF76, new int[] { 0xFB2,0xF80 } }, + { 0xF78, new int[] { 0xFB3,0xF80 } }, + { 0xF81, new int[] { 0xF71,0xF80 } }, + { 0x1B3B, new int[] { 0x1B3A,0x1B35 } }, + { 0x1B3D, new int[] { 0x1B3C,0x1B35 } }, + { 0x1B40, new int[] { 0x1B3E,0x1B35 } }, + { 0x1B41, new int[] { 0x1B3F,0x1B35 } }, + { 0x1B43, new int[] { 0x1B42,0x1B35 } }, + { 0x1112E, new int[] { 0x11131,0x11127 } }, + { 0x1112F, new int[] { 0x11132,0x11127 } }, + { 0x1134B, new int[] { 0x11347,0x1133E } }, + { 0x1134C, new int[] { 0x11347,0x11357 } }, + { 0x113C5, new int[] { 0x113C2,0x113C2 } }, + { 0x113C7, new int[] { 0x113C2,0x113B8 } }, + { 0x113C8, new int[] { 0x113C2,0x113C9 } }, + { 0x114BB, new int[] { 0x114B9,0x114BA } }, + { 0x114BC, new int[] { 0x114B9,0x114B0 } }, + { 0x114BE, new int[] { 0x114B9,0x114BD } }, + { 0x115BA, new int[] { 0x115B8,0x115AF } }, + { 0x115BB, new int[] { 0x115B9,0x115AF } }, + { 0x11938, new int[] { 0x11935,0x11930 } }, + { 0x16121, new int[] { 0x1611E,0x1611E } }, + { 0x16122, new int[] { 0x1611E,0x16129 } }, + { 0x16123, new int[] { 0x1611E,0x1611F } }, + { 0x16124, new int[] { 0x16129,0x1611F } }, + { 0x16125, new int[] { 0x1611E,0x16120 } }, + { 0x16126, new int[] { 0x1611E,0x1611E,0x1611F } }, + { 0x16127, new int[] { 0x1611E,0x16129,0x1611F } }, + { 0x16128, new int[] { 0x1611E,0x1611E,0x16120 } }, + { 0x16D68, new int[] { 0x16D67,0x16D67 } }, + { 0x16D69, new int[] { 0x16D63,0x16D67 } }, + { 0x16D6A, new int[] { 0x16D63,0x16D67,0x16D67 } } + }; + + public static int[][] StateTable => new int[57][] + { + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 2,2,3,4,4,0,0,5,6,7,8,9,10,11,12,13,0,14,15,0,16,0,0,17,10,18,19,20,21,22,0,0,0,23,0,0,2,0,24,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,27,0,0,0,0,0,26,0,0 }, + new int[] { 0,0,0,0,0,28,0,29,30,31,32,33,34,35,36,37,38,39,40,0,0,0,41,42,34,43,44,45,46,47,0,0,48,49,0,0,0,39,0,0 }, + new int[] { 0,0,0,0,0,0,0,5,6,0,0,0,0,0,0,13,0,0,0,0,0,0,0,0,0,0,19,20,21,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,20,21,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,5,6,7,8,0,0,11,0,13,0,0,0,0,0,0,0,0,0,0,19,20,21,0,0,0,0,23,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,5,6,0,8,0,0,0,0,13,0,0,0,0,0,0,0,0,0,0,19,20,21,0,0,0,0,23,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,5,6,7,8,9,10,11,12,13,0,0,15,0,0,0,0,17,10,18,19,20,21,22,0,0,0,23,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,5,6,7,8,0,10,11,0,13,0,0,0,0,0,0,0,0,0,0,19,20,21,0,0,0,0,23,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,5,6,0,8,0,0,11,0,13,0,0,0,0,0,0,0,0,0,0,19,20,21,0,0,0,0,23,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,13,0,0,0,0,0,0,0,0,0,0,19,20,21,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,5,6,7,8,9,10,11,12,13,0,14,15,0,0,0,0,17,10,18,19,20,21,22,0,0,0,23,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,5,6,7,8,0,10,11,0,13,0,0,0,0,0,0,0,0,10,0,19,20,21,0,0,0,0,23,0,0,0,0,0,0 }, + new int[] { 0,0,0,4,4,0,0,5,6,7,8,9,10,11,12,13,0,14,15,0,0,0,0,17,10,18,19,20,21,22,0,0,0,23,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,5,6,7,8,50,10,11,12,13,0,50,15,0,0,0,0,17,10,18,19,20,21,22,0,0,0,23,0,0,0,51,0,0 }, + new int[] { 0,0,0,0,0,0,0,5,6,7,8,0,10,11,0,13,0,0,15,0,0,0,0,0,10,0,19,20,21,22,0,0,0,23,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,20,21,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,21,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,5,6,7,8,0,10,11,0,13,0,0,15,0,0,0,0,0,10,0,19,20,21,0,0,0,0,23,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,5,6,0,0,0,0,0,0,13,0,0,0,0,0,0,0,0,0,0,19,20,21,0,0,0,0,23,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52,0,53 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,27,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,27,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,28,0,29,30,0,0,0,0,0,0,37,38,0,0,0,0,0,41,0,0,0,44,45,46,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,28,0,0,30,0,0,0,0,0,0,0,38,0,0,0,0,0,41,0,0,0,44,45,46,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,28,0,29,30,31,32,0,0,35,0,37,38,0,0,0,0,0,41,0,0,0,44,45,46,0,0,0,0,49,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,28,0,29,30,0,32,0,0,0,0,37,38,0,0,0,0,0,41,0,0,0,44,45,46,0,0,0,0,49,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,28,0,29,30,31,32,33,34,35,36,37,38,0,40,0,0,0,41,42,34,43,44,45,46,47,0,0,48,49,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,28,0,29,30,31,32,0,34,35,0,37,38,0,0,0,0,0,41,0,0,0,44,45,46,0,0,0,0,49,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,28,0,29,30,0,32,0,0,35,0,37,38,0,0,0,0,0,41,0,0,0,44,45,46,0,0,0,0,49,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,42,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,28,0,0,30,0,0,0,0,0,0,37,38,0,0,0,0,0,41,0,0,0,44,45,46,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,28,0,29,30,31,32,33,34,35,36,37,38,39,40,0,0,0,41,42,34,43,44,45,46,47,0,0,48,49,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,28,0,29,30,31,32,0,34,35,0,37,38,0,0,0,0,0,41,0,34,0,44,45,46,0,0,0,0,49,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,28,0,29,30,31,32,54,34,35,36,37,38,54,40,0,0,0,41,42,34,43,44,45,46,47,0,0,48,49,0,0,0,55,0,0 }, + new int[] { 0,0,0,0,0,28,0,29,30,31,32,0,34,35,0,37,38,0,40,0,0,0,41,0,34,0,44,45,46,47,0,0,0,49,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,28,0,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,41,0,0,0,44,45,46,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,28,0,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,41,0,0,0,0,45,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,28,0,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,41,0,0,0,0,45,46,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,28,0,29,30,31,32,0,34,35,0,37,38,0,40,0,0,0,41,0,34,0,44,45,46,0,0,0,0,49,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,28,0,29,30,0,0,0,0,0,0,37,38,0,0,0,0,0,41,0,0,0,44,45,46,0,0,0,0,49,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,5,6,7,8,50,10,11,12,13,0,0,15,0,0,0,0,17,10,18,19,20,21,22,0,0,0,23,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,5,6,7,8,50,10,11,12,13,0,50,15,0,0,0,0,17,10,18,19,20,21,22,0,0,0,23,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,56,0 }, + new int[] { 0,0,0,0,0,28,0,29,30,31,32,54,34,35,36,37,38,0,40,0,0,0,41,42,34,43,44,45,46,47,0,0,48,49,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,28,0,29,30,31,32,54,34,35,36,37,38,54,40,0,0,0,41,42,34,43,44,45,46,47,0,0,48,49,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52,0,53 } + }; + + public static bool[] AcceptingStates => new bool[] + { + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true + }; + + public static string[][] Tags => new string[57][] + { + Array.Empty(), + new string[] { "broken_cluster" }, + new string[] { "independent_cluster" }, + new string[] { "symbol_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + Array.Empty(), + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "numeral_cluster" }, + new string[] { "independent_cluster" }, + new string[] { "symbol_cluster" }, + new string[] { "symbol_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "virama_terminated_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "virama_terminated_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "numeral_cluster" }, + new string[] { "number_joiner_terminated_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "standard_cluster" }, + new string[] { "numeral_cluster" } + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/UniversalShapingTrie.Generated.cs b/SixLabors.Fonts/Unicode/Resources/UniversalShapingTrie.Generated.cs new file mode 100644 index 0000000..4e6bb5c --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/UniversalShapingTrie.Generated.cs @@ -0,0 +1,534 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class UniversalShapingTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 0, 2, 0, 0, 0, 0, 0, 96, 202, 0, 0, 54, 3, 0, 0, 62, 3, 0, 0, 70, 3, 0, 0, 78, 3, 0, 0, 94, 3, 0, 0, 102, 3, 0, 0, 110, 3, 0, 0, 118, 3, 0, 0, 78, 3, 0, 0, 86, 3, 0, 0, 78, 3, 0, 0, 86, 3, 0, 0, 78, 3, 0, 0, 86, 3, 0, 0, 78, 3, 0, 0, 86, 3, 0, 0, 78, 3, 0, 0, 86, 3, 0, 0, 78, 3, 0, 0, 86, 3, 0, 0, 78, 3, 0, 0, 86, 3, 0, 0, + 78, 3, 0, 0, 86, 3, 0, 0, 78, 3, 0, 0, 86, 3, 0, 0, 123, 3, 0, 0, 131, 3, 0, 0, 139, 3, 0, 0, 147, 3, 0, 0, 78, 3, 0, 0, 86, 3, 0, 0, 78, 3, 0, 0, 86, 3, 0, 0, 78, 3, 0, 0, 86, 3, 0, 0, 155, 3, 0, 0, 163, 3, 0, 0, 78, 3, 0, 0, 86, 3, 0, 0, 78, 3, 0, 0, 86, 3, 0, 0, 165, 3, 0, 0, 173, 3, 0, 0, 179, 3, 0, 0, 187, 3, 0, 0, 195, 3, 0, 0, + 203, 3, 0, 0, 209, 3, 0, 0, 217, 3, 0, 0, 217, 3, 0, 0, 225, 3, 0, 0, 78, 3, 0, 0, 86, 3, 0, 0, 229, 3, 0, 0, 237, 3, 0, 0, 245, 3, 0, 0, 253, 3, 0, 0, 78, 3, 0, 0, 86, 3, 0, 0, 78, 3, 0, 0, 86, 3, 0, 0, 249, 3, 0, 0, 1, 4, 0, 0, 54, 3, 0, 0, 9, 4, 0, 0, 131, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 17, 4, 0, 0, 19, 4, 0, 0, 27, 4, 0, 0, 35, 4, 0, 0, 43, 4, 0, 0, 49, 4, 0, 0, 57, 4, 0, 0, 65, 4, 0, 0, 73, 4, 0, 0, 79, 4, 0, 0, 87, 4, 0, 0, 95, 4, 0, 0, 103, 4, 0, 0, 109, 4, 0, 0, 117, 4, 0, 0, 125, 4, 0, 0, 133, 4, 0, 0, 139, 4, 0, 0, 147, 4, 0, 0, 155, 4, 0, 0, 163, 4, 0, 0, 171, 4, 0, 0, 179, 4, 0, 0, 186, 4, 0, 0, 194, 4, 0, 0, + 200, 4, 0, 0, 208, 4, 0, 0, 216, 4, 0, 0, 224, 4, 0, 0, 230, 4, 0, 0, 238, 4, 0, 0, 246, 4, 0, 0, 254, 4, 0, 0, 3, 5, 0, 0, 11, 5, 0, 0, 19, 5, 0, 0, 27, 5, 0, 0, 34, 5, 0, 0, 42, 5, 0, 0, 50, 5, 0, 0, 58, 5, 0, 0, 63, 5, 0, 0, 71, 5, 0, 0, 78, 3, 0, 0, 79, 5, 0, 0, 86, 5, 0, 0, 94, 5, 0, 0, 78, 3, 0, 0, 102, 5, 0, 0, 110, 5, 0, 0, + 118, 5, 0, 0, 123, 5, 0, 0, 131, 5, 0, 0, 138, 5, 0, 0, 146, 5, 0, 0, 78, 3, 0, 0, 154, 5, 0, 0, 160, 5, 0, 0, 168, 5, 0, 0, 176, 5, 0, 0, 184, 5, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 192, 5, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 200, 5, 0, 0, 204, 5, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 211, 5, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 217, 5, 0, 0, 225, 5, 0, 0, 233, 5, 0, 0, 241, 5, 0, 0, 249, 5, 0, 0, 154, 5, 0, 0, 1, 6, 0, 0, 9, 6, 0, 0, 17, 6, 0, 0, 25, 6, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 59, 5, 0, 0, 33, 6, 0, 0, 41, 6, 0, 0, 46, 6, 0, 0, 154, 5, 0, 0, 54, 6, 0, 0, 60, 6, 0, 0, 68, 6, 0, 0, 76, 6, 0, 0, 154, 5, 0, 0, 84, 6, 0, 0, 92, 6, 0, 0, 100, 6, 0, 0, 108, 6, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 116, 6, 0, 0, 119, 6, 0, 0, 127, 6, 0, 0, 135, 6, 0, 0, 143, 6, 0, 0, 151, 6, 0, 0, + 154, 5, 0, 0, 158, 6, 0, 0, 154, 5, 0, 0, 166, 6, 0, 0, 174, 6, 0, 0, 164, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 182, 6, 0, 0, 190, 6, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 197, 6, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 204, 6, 0, 0, 212, 6, 0, 0, 220, 6, 0, 0, 228, 6, 0, 0, 236, 6, 0, 0, 68, 6, 0, 0, 244, 6, 0, 0, 248, 6, 0, 0, 0, 7, 0, 0, 8, 7, 0, 0, 15, 7, 0, 0, 54, 3, 0, 0, 21, 7, 0, 0, 29, 7, 0, 0, 34, 7, 0, 0, 40, 7, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 48, 7, 0, 0, 56, 7, 0, 0, 68, 6, 0, 0, 59, 7, 0, 0, 67, 7, 0, 0, 74, 7, 0, 0, 79, 7, 0, 0, 67, 6, 0, 0, 68, 6, 0, 0, 87, 7, 0, 0, 71, 7, 0, 0, 54, 3, 0, 0, 90, 7, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 87, 7, 0, 0, 68, 6, 0, 0, + 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 97, 7, 0, 0, 105, 7, 0, 0, 109, 7, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 117, 7, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 123, 7, 0, 0, 74, 7, 0, 0, 68, 6, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, + 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 68, 6, 0, 0, 121, 7, 0, 0, 130, 7, 0, 0, + 134, 7, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 142, 7, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 73, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 150, 7, 0, 0, 158, 7, 0, 0, 166, 7, 0, 0, 78, 3, 0, 0, + 174, 7, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 40, 7, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 181, 7, 0, 0, 75, 7, 0, 0, 189, 7, 0, 0, 197, 7, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 192, 5, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 205, 7, 0, 0, + 54, 3, 0, 0, 68, 6, 0, 0, 212, 7, 0, 0, 220, 7, 0, 0, 228, 7, 0, 0, 234, 7, 0, 0, 68, 6, 0, 0, 228, 7, 0, 0, 242, 7, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, + 68, 6, 0, 0, 68, 6, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 75, 7, 0, 0, 68, 6, 0, 0, 250, 7, 0, 0, 164, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 255, 7, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 3, 8, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 167, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 11, 8, 0, 0, 19, 8, 0, 0, 154, 5, 0, 0, + 27, 8, 0, 0, 35, 8, 0, 0, 38, 8, 0, 0, 45, 8, 0, 0, 53, 8, 0, 0, 154, 5, 0, 0, 61, 8, 0, 0, 68, 8, 0, 0, 54, 3, 0, 0, 76, 8, 0, 0, 80, 8, 0, 0, 88, 8, 0, 0, 96, 8, 0, 0, 154, 5, 0, 0, 104, 8, 0, 0, 112, 8, 0, 0, 120, 8, 0, 0, 154, 5, 0, 0, 128, 8, 0, 0, 136, 8, 0, 0, 144, 8, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 154, 5, 0, 0, 152, 8, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 160, 8, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 121, 7, 0, 0, 54, 3, 0, 0, 240, 6, 0, 0, 54, 3, 0, 0, 166, 8, 0, 0, 90, 7, 0, 0, 174, 8, 0, 0, 73, 3, 0, 0, 181, 8, 0, 0, 189, 8, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 197, 8, 0, 0, 70, 3, 0, 0, 54, 3, 0, 0, 205, 8, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 213, 8, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 88, 13, 0, 0, 88, 13, 0, 0, 120, 13, 0, 0, 184, 13, 0, 0, 56, 13, 0, 0, 56, 13, 0, 0, 56, 13, 0, 0, 56, 13, 0, 0, 56, 13, 0, 0, 56, 13, 0, 0, 56, 13, 0, 0, 56, 13, 0, 0, 56, 13, 0, 0, 236, 13, 0, 0, 44, 14, 0, 0, 56, 13, 0, 0, 56, 13, 0, 0, + 56, 13, 0, 0, 108, 14, 0, 0, 56, 13, 0, 0, 56, 13, 0, 0, 148, 14, 0, 0, 204, 14, 0, 0, 12, 15, 0, 0, 68, 15, 0, 0, 100, 15, 0, 0, 56, 13, 0, 0, 148, 15, 0, 0, 212, 15, 0, 0, 56, 13, 0, 0, 56, 13, 0, 0, 228, 15, 0, 0, 96, 8, 0, 0, 160, 8, 0, 0, 224, 8, 0, 0, 32, 9, 0, 0, 96, 9, 0, 0, 139, 9, 0, 0, 203, 9, 0, 0, 203, 9, 0, 0, 216, 9, 0, 0, 161, 1, 0, 0, + 161, 1, 0, 0, 161, 1, 0, 0, 16, 10, 0, 0, 80, 10, 0, 0, 160, 1, 0, 0, 161, 1, 0, 0, 46, 2, 0, 0, 144, 10, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 161, 1, 0, 0, 46, 2, 0, 0, 208, 10, 0, 0, 240, 10, 0, 0, 161, 1, 0, 0, 22, 11, 0, 0, 86, 11, 0, 0, 150, 11, 0, 0, 214, 11, 0, 0, 21, 12, 0, 0, 85, 12, 0, 0, 149, 12, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 221, 8, 0, 0, 224, 8, 0, 0, 54, 3, 0, 0, 232, 8, 0, 0, 239, 8, 0, 0, 246, 8, 0, 0, 75, 7, 0, 0, 254, 8, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 200, 3, 0, 0, 54, 3, 0, 0, 73, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 6, 9, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 10, 9, 0, 0, 16, 9, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 200, 3, 0, 0, 200, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 24, 9, 0, 0, 31, 9, 0, 0, 39, 9, 0, 0, 200, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 46, 9, 0, 0, 50, 9, 0, 0, 54, 3, 0, 0, 56, 9, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 64, 9, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 72, 9, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 79, 9, 0, 0, 54, 3, 0, 0, 226, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 87, 9, 0, 0, 89, 9, 0, 0, 97, 9, 0, 0, 104, 9, 0, 0, 112, 9, 0, 0, 116, 9, 0, 0, 124, 9, 0, 0, 54, 3, 0, 0, 132, 9, 0, 0, 139, 9, 0, 0, 147, 9, 0, 0, 151, 9, 0, 0, 112, 9, 0, 0, 159, 9, 0, 0, 167, 9, 0, 0, 175, 9, 0, 0, 183, 9, 0, 0, 188, 9, 0, 0, 196, 9, 0, 0, 78, 3, 0, 0, 204, 9, 0, 0, 212, 9, 0, 0, 216, 9, 0, 0, 224, 9, 0, 0, 232, 9, 0, 0, + 238, 9, 0, 0, 246, 9, 0, 0, 254, 9, 0, 0, 6, 10, 0, 0, 10, 10, 0, 0, 18, 10, 0, 0, 26, 10, 0, 0, 154, 5, 0, 0, 34, 10, 0, 0, 42, 10, 0, 0, 50, 10, 0, 0, 58, 5, 0, 0, 58, 10, 0, 0, 66, 10, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 154, 5, 0, 0, 74, 10, 0, 0, 82, 10, 0, 0, 78, 3, 0, 0, 154, 5, 0, 0, 90, 10, 0, 0, + 98, 10, 0, 0, 106, 10, 0, 0, 154, 5, 0, 0, 114, 10, 0, 0, 122, 10, 0, 0, 129, 10, 0, 0, 137, 10, 0, 0, 145, 10, 0, 0, 153, 10, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 154, 5, 0, 0, 161, 10, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 169, 10, 0, 0, 175, 10, 0, 0, 183, 10, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 191, 10, 0, 0, 195, 10, 0, 0, 203, 10, 0, 0, 211, 10, 0, 0, 215, 10, 0, 0, 223, 10, 0, 0, 154, 5, 0, 0, 230, 10, 0, 0, 221, 8, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 246, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 238, 10, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 246, 10, 0, 0, 254, 10, 0, 0, 3, 11, 0, 0, 11, 11, 0, 0, 16, 11, 0, 0, + 21, 11, 0, 0, 27, 11, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 35, 11, 0, 0, 39, 11, 0, 0, 47, 11, 0, 0, 55, 11, 0, 0, 61, 11, 0, 0, 17, 6, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 69, 11, 0, 0, 77, 11, 0, 0, 82, 11, 0, 0, 90, 11, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 54, 3, 0, 0, 233, 8, 0, 0, 98, 11, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 106, 11, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 112, 11, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 120, 11, 0, 0, 128, 11, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 135, 11, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 241, 10, 0, 0, 54, 3, 0, 0, 139, 11, 0, 0, 147, 11, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 155, 11, 0, 0, 161, 11, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 168, 11, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 196, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 175, 11, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, + 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 183, 11, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 191, 11, 0, 0, 242, 7, 0, 0, 121, 7, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 75, 7, 0, 0, + 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 198, 11, 0, 0, 78, 3, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 181, 7, 0, 0, 68, 6, 0, 0, 206, 11, 0, 0, 68, 6, 0, 0, 213, 11, 0, 0, 221, 11, 0, 0, 227, 11, 0, 0, 68, 6, 0, 0, 71, 7, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 235, 11, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 7, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, + 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 54, 3, 0, 0, 238, 11, 0, 0, 54, 3, 0, 0, 245, 11, 0, 0, 252, 11, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 1, 12, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 222, 8, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 200, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 164, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 6, 12, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 152, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, 54, 3, 0, 0, + 78, 3, 0, 0, 54, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 68, 6, 0, 0, 14, 12, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 40, 7, 0, 0, 19, 12, 0, 0, 23, 12, 0, 0, 181, 7, 0, 0, 31, 12, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 36, 12, 0, 0, 78, 3, 0, 0, + 43, 12, 0, 0, 51, 12, 0, 0, 79, 7, 0, 0, 57, 12, 0, 0, 88, 7, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 67, 7, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, + 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 65, 12, 0, 0, 68, 12, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 76, 12, 0, 0, 84, 12, 0, 0, 14, 12, 0, 0, + 68, 6, 0, 0, 92, 12, 0, 0, 68, 6, 0, 0, 100, 12, 0, 0, 105, 12, 0, 0, 244, 6, 0, 0, 78, 3, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 113, 12, 0, 0, 121, 12, 0, 0, 129, 12, 0, 0, 68, 6, 0, 0, 136, 12, 0, 0, 144, 12, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, + 68, 6, 0, 0, 68, 6, 0, 0, 152, 12, 0, 0, 68, 6, 0, 0, 68, 6, 0, 0, 157, 12, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, + 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 78, 3, 0, 0, 53, 3, 1, 0, 53, 3, 1, 0, 53, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 11, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, + 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 4, 0, 0, 0, + 10, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 17, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, + 18, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, + 7, 0, 0, 0, 17, 0, 0, 0, 7, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, + 19, 0, 0, 0, 19, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, + 12, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 20, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 17, 0, 0, 0, 7, 0, 0, 0, 9, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 18, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 2, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0, 22, 0, 0, 0, 2, 0, 0, 0, 22, 0, 0, 0, 2, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 13, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 22, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 12, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 7, 0, 0, 0, 15, 0, 0, 0, 8, 0, 0, 0, 14, 0, 0, 0, 9, 0, 0, 0, 24, 0, 0, 0, 25, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 18, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 15, 0, 0, 0, 4, 0, 0, 0, 8, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, + 10, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 10, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 16, 0, 0, 0, 26, 0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, + 9, 0, 0, 0, 14, 0, 0, 0, 16, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 15, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, + 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 27, 0, 0, 0, 28, 0, 0, 0, 9, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, + 13, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 25, 0, 0, 0, 18, 0, 0, 0, 23, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 29, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 9, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 9, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 26, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 17, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 14, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 30, 0, 0, 0, 31, 0, 0, 0, + 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 30, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 7, 0, 0, 0, 26, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 14, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 17, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 26, 0, 0, 0, + 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 33, 0, 0, 0, 33, 0, 0, 0, 16, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 1, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 7, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 34, 0, 0, 0, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 14, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 24, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 14, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 26, 0, 0, 0, 27, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 26, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 17, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 9, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 14, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, + 9, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 24, 0, 0, 0, 25, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 26, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 26, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 4, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 4, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 12, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, + 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 37, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 15, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 14, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 38, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 9, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, + 12, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 14, 0, 0, 0, 11, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 12, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 14, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 11, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, + 12, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 14, 0, 0, 0, 4, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 22, 0, 0, 0, 11, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, + 1, 0, 0, 0, 12, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 7, 0, 0, 0, 14, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 7, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 11, 0, 0, 0, + 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, + 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, + 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 14, 0, 0, 0, 20, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 10, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 14, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 11, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 16, 0, 0, 0, 4, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 12, 0, 0, 0, 9, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 14, 0, 0, 0, 11, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 14, 0, 0, 0, 11, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 14, 0, 0, 0, 9, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 9, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 14, 0, 0, 0, 11, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 25, 0, 0, 0, 29, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 12, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, + 12, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 14, 0, 0, 0, 11, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 10, 0, 0, 0, 14, 0, 0, 0, 20, 0, 0, 0, + 24, 0, 0, 0, 20, 0, 0, 0, 18, 0, 0, 0, 11, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 14, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 22, 0, 0, 0, 13, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 19, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 18, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, + 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 17, 0, 0, 0, 14, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, + 10, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, + 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 14, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, + 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 23, 0, 0, 0, 13, 0, 0, 0, 12, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, + 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 11, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 14, 0, 0, 0, 20, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, + 10, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 12, 0, 0, 0, 10, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 20, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 12, 0, 0, 0, 12, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 14, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 25, 0, 0, 0, 25, 0, 0, 0, 24, 0, 0, 0, 7, 0, 0, 0, 18, 0, 0, 0, 13, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, + 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/VerticalOrientationTrie.Generated.cs b/SixLabors.Fonts/Unicode/Resources/VerticalOrientationTrie.Generated.cs new file mode 100644 index 0000000..a4fc1cc --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/VerticalOrientationTrie.Generated.cs @@ -0,0 +1,459 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class VerticalOrientationTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 8, 14, 0, 0, 0, 0, 0, 48, 173, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, 144, 3, 0, 0, 152, 3, 0, 0, 176, 3, 0, 0, 184, 3, 0, 0, 192, 3, 0, 0, 200, 3, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, + 206, 3, 0, 0, 214, 3, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, 217, 3, 0, 0, 225, 3, 0, 0, 233, 3, 0, 0, 241, 3, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, 242, 3, 0, 0, 250, 3, 0, 0, 255, 3, 0, 0, 7, 4, 0, 0, 13, 4, 0, 0, 21, 4, 0, 0, 27, 4, 0, 0, + 35, 4, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, 128, 3, 0, 0, 136, 3, 0, 0, 238, 3, 0, 0, 246, 3, 0, 0, 43, 4, 0, 0, 51, 4, 0, 0, 47, 4, 0, 0, 55, 4, 0, 0, 63, 4, 0, 0, 71, 4, 0, 0, 128, 3, 0, 0, 79, 4, 0, 0, 87, 4, 0, 0, 95, 4, 0, 0, 99, 4, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, + 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 106, 4, 0, 0, 112, 4, 0, 0, 119, 4, 0, 0, 127, 4, 0, 0, 135, 4, 0, 0, 141, 4, 0, 0, 149, 4, 0, 0, 157, 4, 0, 0, 165, 4, 0, 0, 171, 4, 0, 0, 178, 4, 0, 0, 186, 4, 0, 0, 194, 4, 0, 0, 171, 4, 0, 0, 201, 4, 0, 0, 209, 4, 0, 0, 217, 4, 0, 0, 225, 4, 0, 0, 233, 4, 0, 0, 240, 4, 0, 0, 248, 4, 0, 0, + 254, 4, 0, 0, 5, 5, 0, 0, 13, 5, 0, 0, 248, 4, 0, 0, 19, 5, 0, 0, 26, 5, 0, 0, 34, 5, 0, 0, 248, 4, 0, 0, 128, 3, 0, 0, 42, 5, 0, 0, 49, 5, 0, 0, 57, 5, 0, 0, 64, 5, 0, 0, 72, 5, 0, 0, 80, 5, 0, 0, 5, 4, 0, 0, 31, 4, 0, 0, 153, 3, 0, 0, 160, 3, 0, 0, 88, 5, 0, 0, 95, 5, 0, 0, 103, 5, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, + 252, 3, 0, 0, 110, 5, 0, 0, 248, 3, 0, 0, 115, 5, 0, 0, 119, 5, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 127, 5, 0, 0, 128, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 133, 5, 0, 0, + 128, 3, 0, 0, 141, 5, 0, 0, 131, 5, 0, 0, 149, 5, 0, 0, 128, 3, 0, 0, 145, 5, 0, 0, 128, 3, 0, 0, 9, 4, 0, 0, 155, 5, 0, 0, 53, 4, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 163, 5, 0, 0, 171, 5, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 155, 5, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 179, 5, 0, 0, 187, 5, 0, 0, 195, 5, 0, 0, 155, 3, 0, 0, 203, 5, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 209, 3, 0, 0, 211, 5, 0, 0, 53, 4, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 179, 5, 0, 0, 128, 3, 0, 0, + 219, 5, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 178, 3, 0, 0, 227, 5, 0, 0, 235, 5, 0, 0, 240, 5, 0, 0, 128, 3, 0, 0, 248, 5, 0, 0, 254, 5, 0, 0, 128, 3, 0, 0, 224, 3, 0, 0, 128, 3, 0, 0, 178, 3, 0, 0, 6, 6, 0, 0, 211, 5, 0, 0, 213, 3, 0, 0, 209, 3, 0, 0, 157, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 14, 6, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, + 128, 3, 0, 0, 24, 4, 0, 0, 128, 3, 0, 0, 18, 6, 0, 0, 25, 6, 0, 0, 128, 3, 0, 0, 95, 4, 0, 0, 9, 4, 0, 0, 27, 4, 0, 0, 29, 6, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, + 128, 3, 0, 0, 163, 5, 0, 0, 128, 3, 0, 0, 37, 6, 0, 0, 209, 3, 0, 0, 128, 3, 0, 0, 117, 5, 0, 0, 45, 6, 0, 0, 53, 6, 0, 0, 61, 6, 0, 0, 69, 6, 0, 0, 77, 6, 0, 0, 84, 6, 0, 0, 89, 6, 0, 0, 128, 3, 0, 0, 97, 6, 0, 0, 105, 6, 0, 0, 113, 6, 0, 0, 121, 6, 0, 0, 129, 6, 0, 0, 160, 3, 0, 0, 135, 6, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, + 139, 6, 0, 0, 147, 6, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 155, 6, 0, 0, 163, 6, 0, 0, 128, 3, 0, 0, 155, 5, 0, 0, 171, 6, 0, 0, 209, 3, 0, 0, 179, 6, 0, 0, 187, 6, 0, 0, 160, 3, 0, 0, 195, 6, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, + 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 197, 6, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 205, 6, 0, 0, 171, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, + 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 55, 4, 0, 0, 172, 3, 0, 0, 213, 6, 0, 0, + 147, 6, 0, 0, 192, 3, 0, 0, 154, 3, 0, 0, 221, 6, 0, 0, 226, 6, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 234, 6, 0, 0, 128, 3, 0, 0, 127, 5, 0, 0, 128, 3, 0, 0, 241, 6, 0, 0, 195, 5, 0, 0, 249, 6, 0, 0, 249, 6, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 1, 7, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 9, 7, 0, 0, 17, 7, 0, 0, 25, 7, 0, 0, 33, 7, 0, 0, 41, 7, 0, 0, 49, 7, 0, 0, 33, 7, 0, 0, 57, 7, 0, 0, 160, 3, 0, 0, 32, 7, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 65, 7, 0, 0, 160, 3, 0, 0, 72, 7, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 34, 7, 0, 0, 76, 7, 0, 0, 76, 7, 0, 0, 78, 7, 0, 0, 84, 7, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 172, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, + 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 157, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 154, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 155, 5, 0, 0, 92, 7, 0, 0, 128, 3, 0, 0, 97, 7, 0, 0, 128, 3, 0, 0, + 154, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 102, 7, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 110, 7, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 118, 7, 0, 0, 178, 3, 0, 0, 128, 3, 0, 0, 195, 5, 0, 0, 126, 7, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 134, 7, 0, 0, 195, 5, 0, 0, 142, 7, 0, 0, 253, 6, 0, 0, 128, 3, 0, 0, 248, 5, 0, 0, + 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 150, 7, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, + 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 158, 7, 0, 0, 166, 7, 0, 0, 174, 7, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, + 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 156, 3, 0, 0, 172, 3, 0, 0, 156, 3, 0, 0, 156, 3, 0, 0, 182, 7, 0, 0, 190, 7, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 6, 6, 0, 0, 198, 7, 0, 0, 206, 7, 0, 0, 214, 7, 0, 0, 222, 7, 0, 0, 128, 3, 0, 0, 178, 3, 0, 0, 230, 7, 0, 0, + 238, 7, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, + 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 14, 0, 0, 128, 14, 0, 0, 192, 14, 0, 0, 0, 15, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 56, 15, 0, 0, 0, 14, 0, 0, 100, 15, 0, 0, 164, 15, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, + 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 200, 15, 0, 0, 252, 15, 0, 0, 52, 16, 0, 0, 108, 16, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 0, 14, 0, 0, 184, 15, 0, 0, 172, 16, 0, 0, 188, 16, 0, 0, 252, 16, 0, 0, 225, 9, 0, 0, 33, 10, 0, 0, 97, 10, 0, 0, 161, 10, 0, 0, 225, 10, 0, 0, 12, 11, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, + 156, 1, 0, 0, 156, 1, 0, 0, 76, 11, 0, 0, 140, 11, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 201, 11, 0, 0, 233, 11, 0, 0, 156, 1, 0, 0, 15, 12, 0, 0, 69, 12, 0, 0, 133, 12, 0, 0, 197, 12, 0, 0, 5, 13, 0, 0, 61, 13, 0, 0, 125, 13, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, + 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, + 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, + 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, + 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, + 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, + 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, + 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, + 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, + 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, + 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, + 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, + 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, + 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, + 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, + 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, + 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 156, 1, 0, 0, 189, 13, 0, 0, 251, 3, 0, 0, 246, 7, 0, 0, 254, 7, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 29, 6, 0, 0, 6, 8, 0, 0, 19, 6, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 89, 6, 0, 0, 171, 5, 0, 0, 172, 3, 0, 0, 209, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 155, 5, 0, 0, 128, 3, 0, 0, 10, 8, 0, 0, 153, 3, 0, 0, 128, 3, 0, 0, 18, 8, 0, 0, 95, 4, 0, 0, 29, 6, 0, 0, 139, 6, 0, 0, 128, 3, 0, 0, 25, 8, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 209, 3, 0, 0, 106, 7, 0, 0, 33, 8, 0, 0, 153, 3, 0, 0, 128, 3, 0, 0, 27, 4, 0, 0, 128, 3, 0, 0, + 40, 8, 0, 0, 44, 8, 0, 0, 49, 8, 0, 0, 128, 3, 0, 0, 155, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 195, 5, 0, 0, 54, 4, 0, 0, 158, 3, 0, 0, 240, 3, 0, 0, 57, 8, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 65, 8, 0, 0, 68, 8, 0, 0, 141, 6, 0, 0, 128, 3, 0, 0, + 178, 3, 0, 0, 76, 8, 0, 0, 160, 3, 0, 0, 84, 8, 0, 0, 91, 8, 0, 0, 99, 8, 0, 0, 53, 4, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 107, 8, 0, 0, 114, 8, 0, 0, 128, 3, 0, 0, 121, 8, 0, 0, 128, 8, 0, 0, 136, 8, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 144, 8, 0, 0, 128, 3, 0, 0, 152, 8, 0, 0, 211, 3, 0, 0, 242, 4, 0, 0, 159, 8, 0, 0, + 167, 8, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 175, 8, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 183, 8, 0, 0, 128, 3, 0, 0, 191, 8, 0, 0, 128, 3, 0, 0, 198, 8, 0, 0, 128, 3, 0, 0, 26, 6, 0, 0, 203, 8, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 178, 3, 0, 0, 128, 3, 0, 0, 211, 8, 0, 0, + 219, 8, 0, 0, 197, 6, 0, 0, 128, 3, 0, 0, 27, 4, 0, 0, 53, 4, 0, 0, 172, 3, 0, 0, 227, 8, 0, 0, 172, 3, 0, 0, 157, 3, 0, 0, 195, 5, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 235, 8, 0, 0, 187, 5, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 243, 8, 0, 0, 223, 8, 0, 0, 128, 3, 0, 0, 117, 5, 0, 0, 27, 4, 0, 0, 195, 5, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, + 251, 8, 0, 0, 237, 3, 0, 0, 128, 3, 0, 0, 187, 6, 0, 0, 160, 3, 0, 0, 3, 9, 0, 0, 106, 7, 0, 0, 128, 3, 0, 0, 11, 9, 0, 0, 106, 4, 0, 0, 19, 9, 0, 0, 26, 9, 0, 0, 34, 9, 0, 0, 42, 9, 0, 0, 141, 6, 0, 0, 50, 9, 0, 0, 58, 9, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 247, 3, 0, 0, 187, 6, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 198, 8, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 66, 9, 0, 0, 74, 9, 0, 0, 128, 3, 0, 0, 53, 4, 0, 0, 106, 7, 0, 0, 159, 3, 0, 0, 9, 4, 0, 0, 248, 5, 0, 0, 82, 9, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, + 153, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 90, 9, 0, 0, 98, 9, 0, 0, 104, 9, 0, 0, 111, 9, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 48, 5, 0, 0, 225, 3, 0, 0, 119, 9, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 179, 5, 0, 0, 227, 8, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 158, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 127, 9, 0, 0, 15, 6, 0, 0, 192, 3, 0, 0, 209, 6, 0, 0, 159, 5, 0, 0, 114, 8, 0, 0, 135, 9, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 143, 9, 0, 0, 146, 9, 0, 0, 198, 8, 0, 0, 154, 9, 0, 0, 159, 9, 0, 0, 106, 7, 0, 0, 153, 3, 0, 0, 227, 8, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 179, 5, 0, 0, 167, 9, 0, 0, 172, 9, 0, 0, 29, 6, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 180, 9, 0, 0, 128, 3, 0, 0, 188, 9, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, + 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 53, 4, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 196, 9, 0, 0, 128, 3, 0, 0, + 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 159, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 172, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 183, 8, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 53, 4, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, + 128, 3, 0, 0, 179, 5, 0, 0, 178, 3, 0, 0, 204, 9, 0, 0, 128, 3, 0, 0, 178, 3, 0, 0, 106, 7, 0, 0, 209, 9, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 213, 9, 0, 0, 219, 9, 0, 0, 156, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 128, 3, 0, 0, 53, 4, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 29, 6, 0, 0, 7, 6, 0, 0, 155, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 227, 9, 0, 0, 128, 3, 0, 0, 233, 9, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 241, 9, 0, 0, 246, 9, 0, 0, 253, 9, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 5, 10, 0, 0, 10, 10, 0, 0, 159, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, + 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 155, 5, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 17, 10, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 186, 6, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 155, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 117, 5, 0, 0, 128, 3, 0, 0, 115, 5, 0, 0, 25, 10, 0, 0, 33, 10, 0, 0, 128, 3, 0, 0, 40, 10, 0, 0, 35, 10, 0, 0, 48, 10, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 215, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, + 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 47, 5, 0, 0, 128, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 178, 3, 0, 0, 56, 10, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 64, 10, 0, 0, 72, 10, 0, 0, 128, 3, 0, 0, + 77, 10, 0, 0, 192, 6, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 85, 10, 0, 0, 90, 10, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 172, 3, 0, 0, 98, 10, 0, 0, 128, 3, 0, 0, 99, 8, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 172, 3, 0, 0, 53, 4, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 172, 3, 0, 0, 31, 4, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 178, 3, 0, 0, 106, 10, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 114, 10, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 122, 10, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 130, 10, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 92, 7, 0, 0, 128, 3, 0, 0, 9, 8, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 5, 4, 0, 0, 209, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 33, 10, 0, 0, 138, 10, 0, 0, 146, 10, 0, 0, 154, 10, 0, 0, 162, 10, 0, 0, 170, 10, 0, 0, 160, 3, 0, 0, 123, 9, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 178, 10, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 248, 5, 0, 0, 128, 3, 0, 0, 198, 8, 0, 0, 128, 3, 0, 0, 27, 4, 0, 0, 186, 10, 0, 0, 194, 10, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 193, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 29, 6, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 202, 10, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 128, 3, 0, 0, 156, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, + 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 160, 3, 0, 0, 127, 3, 1, 0, 127, 3, 1, 0, 127, 3, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, + 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + } +} diff --git a/SixLabors.Fonts/Unicode/Resources/WordBreakTrie.Generated.cs b/SixLabors.Fonts/Unicode/Resources/WordBreakTrie.Generated.cs new file mode 100644 index 0000000..4f5fb2a --- /dev/null +++ b/SixLabors.Fonts/Unicode/Resources/WordBreakTrie.Generated.cs @@ -0,0 +1,603 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class WordBreakTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 8, 14, 0, 0, 0, 0, 0, 80, 229, 0, 0, 170, 3, 0, 0, 178, 3, 0, 0, 186, 3, 0, 0, 194, 3, 0, 0, 233, 3, 0, 0, 241, 3, 0, 0, 249, 3, 0, 0, 1, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, + 17, 4, 0, 0, 25, 4, 0, 0, 33, 4, 0, 0, 41, 4, 0, 0, 37, 4, 0, 0, 45, 4, 0, 0, 53, 4, 0, 0, 61, 4, 0, 0, 62, 4, 0, 0, 70, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 78, 4, 0, 0, 86, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 82, 4, 0, 0, 90, 4, 0, 0, 95, 4, 0, 0, 103, 4, 0, 0, 109, 4, 0, 0, 117, 4, 0, 0, 125, 4, 0, 0, + 133, 4, 0, 0, 141, 4, 0, 0, 149, 4, 0, 0, 155, 4, 0, 0, 163, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 168, 4, 0, 0, 176, 4, 0, 0, 184, 4, 0, 0, 192, 4, 0, 0, 198, 4, 0, 0, 206, 4, 0, 0, 205, 4, 0, 0, 213, 4, 0, 0, 221, 4, 0, 0, 229, 4, 0, 0, 13, 5, 0, 0, 20, 5, 0, 0, 28, 5, 0, 0, 53, 11, 0, 0, 36, 5, 0, 0, 7, 4, 0, 0, 44, 5, 0, 0, 52, 5, 0, 0, + 59, 5, 0, 0, 61, 5, 0, 0, 69, 5, 0, 0, 77, 5, 0, 0, 85, 5, 0, 0, 91, 5, 0, 0, 99, 5, 0, 0, 107, 5, 0, 0, 115, 5, 0, 0, 121, 5, 0, 0, 129, 5, 0, 0, 137, 5, 0, 0, 145, 5, 0, 0, 151, 5, 0, 0, 159, 5, 0, 0, 167, 5, 0, 0, 175, 5, 0, 0, 151, 5, 0, 0, 183, 5, 0, 0, 191, 5, 0, 0, 199, 5, 0, 0, 207, 5, 0, 0, 215, 5, 0, 0, 45, 14, 0, 0, 223, 5, 0, 0, + 229, 5, 0, 0, 237, 5, 0, 0, 245, 5, 0, 0, 253, 5, 0, 0, 3, 6, 0, 0, 11, 6, 0, 0, 19, 6, 0, 0, 27, 6, 0, 0, 32, 6, 0, 0, 40, 6, 0, 0, 48, 6, 0, 0, 56, 6, 0, 0, 57, 11, 0, 0, 63, 6, 0, 0, 71, 6, 0, 0, 218, 3, 0, 0, 76, 6, 0, 0, 83, 6, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 90, 6, 0, 0, 98, 6, 0, 0, 218, 3, 0, 0, 106, 6, 0, 0, 114, 6, 0, 0, + 92, 4, 0, 0, 122, 6, 0, 0, 129, 6, 0, 0, 136, 6, 0, 0, 144, 6, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 150, 6, 0, 0, 158, 6, 0, 0, 166, 6, 0, 0, 174, 6, 0, 0, 7, 4, 0, 0, 65, 11, 0, 0, 0, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 71, 11, 0, 0, + 7, 4, 0, 0, 79, 11, 0, 0, 69, 11, 0, 0, 87, 11, 0, 0, 7, 4, 0, 0, 83, 11, 0, 0, 7, 4, 0, 0, 182, 6, 0, 0, 218, 3, 0, 0, 93, 11, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 101, 11, 0, 0, 201, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 109, 11, 0, 0, 117, 11, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 125, 11, 0, 0, 190, 6, 0, 0, 198, 6, 0, 0, 206, 6, 0, 0, 214, 6, 0, 0, 218, 3, 0, 0, 219, 6, 0, 0, 224, 6, 0, 0, 249, 7, 0, 0, 232, 6, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 133, 11, 0, 0, 240, 6, 0, 0, + 246, 6, 0, 0, 7, 4, 0, 0, 141, 11, 0, 0, 255, 3, 0, 0, 254, 6, 0, 0, 45, 14, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 49, 14, 0, 0, 218, 3, 0, 0, 6, 7, 0, 0, 218, 3, 0, 0, 13, 7, 0, 0, 21, 7, 0, 0, 57, 14, 0, 0, 220, 6, 0, 0, 29, 7, 0, 0, 37, 7, 0, 0, 45, 7, 0, 0, 28, 4, 0, 0, 53, 7, 0, 0, 60, 7, 0, 0, 68, 7, 0, 0, 76, 7, 0, 0, + 7, 4, 0, 0, 83, 7, 0, 0, 7, 4, 0, 0, 91, 7, 0, 0, 149, 11, 0, 0, 156, 11, 0, 0, 53, 11, 0, 0, 164, 11, 0, 0, 97, 7, 0, 0, 103, 7, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 33, 4, 0, 0, 33, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 101, 11, 0, 0, 7, 4, 0, 0, 172, 11, 0, 0, 156, 11, 0, 0, 7, 4, 0, 0, 180, 11, 0, 0, 188, 11, 0, 0, 196, 11, 0, 0, 111, 7, 0, 0, 5, 5, 0, 0, 37, 14, 0, 0, 204, 10, 0, 0, 137, 10, 0, 0, 218, 3, 0, 0, 220, 6, 0, 0, 119, 7, 0, 0, 204, 11, 0, 0, 212, 11, 0, 0, 220, 11, 0, 0, 7, 4, 0, 0, 228, 11, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 231, 11, 0, 0, 7, 4, 0, 0, 237, 11, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 127, 7, 0, 0, 7, 4, 0, 0, 65, 11, 0, 0, 7, 4, 0, 0, 135, 7, 0, 0, 245, 11, 0, 0, 253, 11, 0, 0, 253, 11, 0, 0, 33, 4, 0, 0, 218, 3, 0, 0, 5, 12, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 13, 12, 0, 0, 143, 7, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 151, 7, 0, 0, 251, 10, 0, 0, 251, 10, 0, 0, 253, 10, 0, 0, 20, 12, 0, 0, 90, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 25, 12, 0, 0, + 7, 4, 0, 0, 218, 3, 0, 0, 247, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 247, 10, 0, 0, 252, 10, 0, 0, 251, 10, 0, 0, 251, 10, 0, 0, 4, 11, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 33, 12, 0, 0, 218, 3, 0, 0, 24, 5, 0, 0, 156, 11, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 41, 12, 0, 0, 49, 12, 0, 0, 7, 4, 0, 0, 159, 7, 0, 0, 207, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 167, 7, 0, 0, 26, 5, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 57, 12, 0, 0, 17, 12, 0, 0, 175, 7, 0, 0, 183, 7, 0, 0, 7, 4, 0, 0, + 65, 12, 0, 0, 80, 4, 0, 0, 28, 4, 0, 0, 191, 7, 0, 0, 199, 7, 0, 0, 221, 4, 0, 0, 207, 7, 0, 0, 214, 7, 0, 0, 57, 12, 0, 0, 59, 5, 0, 0, 153, 4, 0, 0, 222, 7, 0, 0, 229, 7, 0, 0, 7, 4, 0, 0, 237, 7, 0, 0, 245, 7, 0, 0, 252, 7, 0, 0, 218, 3, 0, 0, 4, 8, 0, 0, 12, 8, 0, 0, 20, 8, 0, 0, 73, 12, 0, 0, 81, 12, 0, 0, 7, 4, 0, 0, 87, 12, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 28, 8, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 94, 12, 0, 0, 101, 12, 0, 0, 16, 4, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 237, 4, 0, 0, 245, 4, 0, 0, 253, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 105, 12, 0, 0, 110, 12, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 156, 11, 0, 0, 24, 5, 0, 0, 7, 4, 0, 0, 115, 12, 0, 0, 7, 4, 0, 0, 121, 12, 0, 0, 125, 12, 0, 0, 36, 8, 0, 0, 44, 8, 0, 0, 22, 14, 0, 0, 132, 12, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 212, 10, 0, 0, 29, 14, 0, 0, 186, 3, 0, 0, 194, 3, 0, 0, 11, 11, 0, 0, 52, 8, 0, 0, 255, 3, 0, 0, 140, 12, 0, 0, + 220, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 40, 15, 0, 0, 40, 15, 0, 0, 164, 15, 0, 0, 228, 15, 0, 0, 28, 16, 0, 0, 28, 16, 0, 0, 28, 16, 0, 0, 28, 16, 0, 0, 28, 16, 0, 0, 28, 16, 0, 0, 28, 16, 0, 0, 68, 16, 0, 0, 132, 16, 0, 0, 148, 16, 0, 0, 212, 16, 0, 0, 248, 16, 0, 0, 28, 16, 0, 0, + 28, 16, 0, 0, 56, 17, 0, 0, 28, 16, 0, 0, 72, 17, 0, 0, 124, 17, 0, 0, 180, 17, 0, 0, 244, 17, 0, 0, 52, 18, 0, 0, 108, 18, 0, 0, 28, 16, 0, 0, 160, 18, 0, 0, 224, 18, 0, 0, 24, 19, 0, 0, 52, 19, 0, 0, 116, 19, 0, 0, 225, 9, 0, 0, 33, 10, 0, 0, 97, 10, 0, 0, 161, 10, 0, 0, 201, 13, 0, 0, 244, 13, 0, 0, 225, 10, 0, 0, 96, 5, 0, 0, 52, 14, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 33, 11, 0, 0, 97, 11, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 73, 13, 0, 0, 137, 13, 0, 0, 161, 11, 0, 0, 155, 1, 0, 0, 199, 11, 0, 0, 2, 12, 0, 0, 66, 12, 0, 0, 130, 12, 0, 0, 194, 12, 0, 0, 249, 12, 0, 0, 103, 14, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 57, 13, 0, 0, 91, 4, 0, 0, 148, 12, 0, 0, 156, 12, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 22, 12, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 164, 12, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 60, 8, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 57, 12, 0, 0, 7, 4, 0, 0, 165, 12, 0, 0, 68, 8, 0, 0, 7, 4, 0, 0, 18, 12, 0, 0, 53, 11, 0, 0, 76, 8, 0, 0, 156, 11, 0, 0, 7, 4, 0, 0, 173, 12, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 156, 11, 0, 0, 113, 9, 0, 0, 181, 12, 0, 0, 16, 4, 0, 0, 7, 4, 0, 0, 187, 12, 0, 0, 7, 4, 0, 0, + 194, 12, 0, 0, 198, 12, 0, 0, 203, 12, 0, 0, 7, 4, 0, 0, 65, 12, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 245, 11, 0, 0, 141, 11, 0, 0, 121, 12, 0, 0, 60, 4, 0, 0, 211, 12, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 219, 12, 0, 0, 222, 12, 0, 0, 141, 11, 0, 0, 245, 11, 0, 0, + 255, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 230, 12, 0, 0, 141, 11, 0, 0, 238, 12, 0, 0, 238, 12, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 17, 4, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 84, 8, 0, 0, 91, 8, 0, 0, 218, 3, 0, 0, 57, 12, 0, 0, 57, 12, 0, 0, 218, 3, 0, 0, 92, 4, 0, 0, 99, 8, 0, 0, 7, 4, 0, 0, 141, 11, 0, 0, 141, 11, 0, 0, 24, 12, 0, 0, 105, 12, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 228, 11, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 24, 12, 0, 0, 7, 4, 0, 0, 24, 12, 0, 0, 7, 4, 0, 0, 107, 8, 0, 0, 221, 4, 0, 0, 115, 8, 0, 0, 243, 12, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 121, 8, 0, 0, + 251, 12, 0, 0, 126, 8, 0, 0, 57, 12, 0, 0, 2, 13, 0, 0, 134, 8, 0, 0, 24, 5, 0, 0, 142, 8, 0, 0, 24, 5, 0, 0, 9, 13, 0, 0, 245, 11, 0, 0, 68, 7, 0, 0, 27, 4, 0, 0, 150, 8, 0, 0, 157, 8, 0, 0, 68, 7, 0, 0, 165, 8, 0, 0, 173, 8, 0, 0, 17, 13, 0, 0, 68, 7, 0, 0, 180, 8, 0, 0, 188, 8, 0, 0, 192, 8, 0, 0, 68, 7, 0, 0, 153, 4, 0, 0, 200, 8, 0, 0, + 218, 3, 0, 0, 57, 4, 0, 0, 208, 8, 0, 0, 216, 8, 0, 0, 218, 3, 0, 0, 25, 13, 0, 0, 129, 11, 0, 0, 150, 4, 0, 0, 224, 8, 0, 0, 232, 8, 0, 0, 238, 8, 0, 0, 246, 8, 0, 0, 254, 8, 0, 0, 30, 13, 0, 0, 6, 9, 0, 0, 14, 9, 0, 0, 22, 9, 0, 0, 7, 4, 0, 0, 71, 7, 0, 0, 30, 9, 0, 0, 38, 13, 0, 0, 7, 4, 0, 0, 29, 4, 0, 0, 38, 9, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 46, 9, 0, 0, 54, 9, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 29, 4, 0, 0, 62, 9, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 70, 9, 0, 0, 61, 14, 0, 0, 68, 14, 0, 0, 77, 9, 0, 0, 85, 9, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, + 93, 9, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 46, 13, 0, 0, 54, 13, 0, 0, 101, 9, 0, 0, 109, 9, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 60, 13, 0, 0, 117, 9, 0, 0, 125, 9, 0, 0, 133, 9, 0, 0, 137, 9, 0, 0, 145, 9, 0, 0, 7, 4, 0, 0, 152, 9, 0, 0, 24, 5, 0, 0, 7, 4, 0, 0, 133, 11, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 160, 9, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 68, 13, 0, 0, 224, 9, 0, 0, 168, 9, 0, 0, 68, 13, 0, 0, 75, 13, 0, 0, 176, 9, 0, 0, 182, 9, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 82, 13, 0, 0, 190, 9, 0, 0, 198, 9, 0, 0, 89, 13, 0, 0, 206, 9, 0, 0, 113, 9, 0, 0, 16, 4, 0, 0, 249, 7, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 214, 9, 0, 0, 222, 9, 0, 0, 227, 9, 0, 0, 235, 9, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 97, 13, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 228, 10, 0, 0, 243, 9, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 207, 4, 0, 0, 251, 9, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 133, 11, 0, 0, 255, 3, 0, 0, 113, 9, 0, 0, 7, 4, 0, 0, 255, 3, 0, 0, 113, 9, 0, 0, 3, 10, 0, 0, 7, 4, 0, 0, 11, 10, 0, 0, 121, 13, 0, 0, 129, 13, 0, 0, 93, 11, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 137, 13, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 218, 3, 0, 0, 145, 13, 0, 0, 65, 12, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 19, 10, 0, 0, + 33, 4, 0, 0, 25, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 33, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 152, 13, 0, 0, 41, 10, 0, 0, 235, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 42, 13, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 33, 4, 0, 0, + 49, 10, 0, 0, 150, 8, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 57, 10, 0, 0, 65, 10, 0, 0, 71, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 79, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 160, 13, 0, 0, 7, 4, 0, 0, 166, 13, 0, 0, 174, 13, 0, 0, 182, 13, 0, 0, 7, 4, 0, 0, 189, 13, 0, 0, 184, 13, 0, 0, 197, 13, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 162, 11, 0, 0, 201, 13, 0, 0, 0, 4, 0, 0, 160, 13, 0, 0, 160, 13, 0, 0, 251, 3, 0, 0, 251, 3, 0, 0, 224, 9, 0, 0, 224, 9, 0, 0, 207, 13, 0, 0, 76, 14, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 33, 4, 0, 0, 87, 10, 0, 0, 33, 4, 0, 0, 94, 10, 0, 0, 101, 10, 0, 0, 109, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 255, 3, 0, 0, 215, 13, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 117, 10, 0, 0, 125, 10, 0, 0, 7, 4, 0, 0, 106, 12, 0, 0, 133, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 141, 10, 0, 0, 223, 13, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 24, 5, 0, 0, 149, 10, 0, 0, 7, 4, 0, 0, 157, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 24, 5, 0, 0, 157, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 24, 5, 0, 0, 165, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 255, 3, 0, 0, 173, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 231, 13, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 181, 10, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 189, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 182, 13, 0, 0, 239, 13, 0, 0, 247, 13, 0, 0, 255, 13, 0, 0, 7, 14, 0, 0, 15, 14, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 24, 5, 0, 0, 87, 12, 0, 0, 87, 12, 0, 0, 237, 11, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 196, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 146, 6, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 243, 10, 0, 0, 33, 4, 0, 0, 33, 4, 0, 0, 33, 4, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 33, 4, 0, 0, 33, 4, 0, 0, 33, 4, 0, 0, 33, 4, 0, 0, + 33, 4, 0, 0, 33, 4, 0, 0, 33, 4, 0, 0, 93, 7, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 19, 11, 0, 0, 27, 11, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 35, 11, 0, 0, 38, 11, 0, 0, 45, 11, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 238, 12, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 25, 12, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 105, 13, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 24, 5, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 165, 12, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 22, 12, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 113, 13, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 42, 13, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 169, 3, 1, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 10, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 13, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 16, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 2, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 14, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 14, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 9, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 15, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 8, 0, 0, 0, 3, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 13, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 16, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 16, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 15, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 3, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, + 17, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, + 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 17, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 13, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 16, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 16, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + }; + } +} diff --git a/SixLabors.Fonts/Unicode/ScriptClass.cs b/SixLabors.Fonts/Unicode/ScriptClass.cs new file mode 100644 index 0000000..f006521 --- /dev/null +++ b/SixLabors.Fonts/Unicode/ScriptClass.cs @@ -0,0 +1,901 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode { + /// + /// Unicode Script property values. + /// + /// + /// + /// and are Unicode script values. + /// is an OpenType fallback tag rather than a Unicode Script + /// property value. + /// + public enum ScriptClass + { + /// + /// Unknown script. Shortcode: Zzzz. + /// + Unknown, + + /// + /// Common script. Shortcode: Zyyy. + /// + Common, + + /// + /// Inherited script. Shortcode: Zinh, Qaai. + /// + Inherited, + + /// + /// Shortcode: Adlm + /// + Adlam, + + /// + /// Shortcode: Aghb + /// + CaucasianAlbanian, + + /// + /// Shortcode: Ahom + /// + Ahom, + + /// + /// Shortcode: Arab + /// + Arabic, + + /// + /// Shortcode: Armi + /// + ImperialAramaic, + + /// + /// Shortcode: Armn + /// + Armenian, + + /// + /// Shortcode: Avst + /// + Avestan, + + /// + /// Shortcode: Bali + /// + Balinese, + + /// + /// Shortcode: Bamu + /// + Bamum, + + /// + /// Shortcode: Bass + /// + BassaVah, + + /// + /// Shortcode: Batk + /// + Batak, + + /// + /// Shortcode: Beng + /// + Bengali, + + /// + /// Shortcode: Bhks + /// + Bhaiksuki, + + /// + /// Shortcode: Bopo + /// + Bopomofo, + + /// + /// Shortcode: Brah + /// + Brahmi, + + /// + /// Shortcode: Brai + /// + Braille, + + /// + /// Shortcode: Bugi + /// + Buginese, + + /// + /// Shortcode: Buhd + /// + Buhid, + + /// + /// Shortcode: Cakm + /// + Chakma, + + /// + /// Shortcode: Cans + /// + CanadianAboriginal, + + /// + /// Shortcode: Cari + /// + Carian, + + /// + /// Shortcode: Cham + /// + Cham, + + /// + /// Shortcode: Cher + /// + Cherokee, + + /// + /// Shortcode: Chrs + /// + Chorasmian, + + /// + /// Shortcode: Copt, Qaac + /// + Coptic, + + /// + /// Shortcode: Cpmn + /// + CyproMinoan, + + /// + /// Shortcode: Cprt + /// + Cypriot, + + /// + /// Shortcode: Cyrl + /// + Cyrillic, + + /// + /// Shortcode: Deva + /// + Devanagari, + + /// + /// Shortcode: Diak + /// + DivesAkuru, + + /// + /// Shortcode: Dogr + /// + Dogra, + + /// + /// Shortcode: Dsrt + /// + Deseret, + + /// + /// Shortcode: Dupl + /// + Duployan, + + /// + /// Shortcode: Egyp + /// + EgyptianHieroglyphs, + + /// + /// Shortcode: Elba + /// + Elbasan, + + /// + /// Shortcode: Elym + /// + Elymaic, + + /// + /// Shortcode: Ethi + /// + Ethiopic, + + /// + /// Shortcode: Geor + /// + Georgian, + + /// + /// Shortcode: Glag + /// + Glagolitic, + + /// + /// Shortcode: Gong + /// + GunjalaGondi, + + /// + /// Shortcode: Gonm + /// + MasaramGondi, + + /// + /// Shortcode: Goth + /// + Gothic, + + /// + /// Shortcode: Gran + /// + Grantha, + + /// + /// Shortcode: Grek + /// + Greek, + + /// + /// Shortcode: Gujr + /// + Gujarati, + + /// + /// Shortcode: Guru + /// + Gurmukhi, + + /// + /// Shortcode: Hang + /// + Hangul, + + /// + /// Shortcode: Hani + /// + Han, + + /// + /// Shortcode: Hano + /// + Hanunoo, + + /// + /// Shortcode: Hatr + /// + Hatran, + + /// + /// Shortcode: Hebr + /// + Hebrew, + + /// + /// Shortcode: Hira + /// + Hiragana, + + /// + /// Shortcode: Hluw + /// + AnatolianHieroglyphs, + + /// + /// Shortcode: Hmng + /// + PahawhHmong, + + /// + /// Shortcode: Hmnp + /// + NyiakengPuachueHmong, + + /// + /// Shortcode: Hrkt + /// + KatakanaOrHiragana, + + /// + /// Shortcode: Hung + /// + OldHungarian, + + /// + /// Shortcode: Ital + /// + OldItalic, + + /// + /// Shortcode: Java + /// + Javanese, + + /// + /// Shortcode: Kali + /// + KayahLi, + + /// + /// Shortcode: Kana + /// + Katakana, + + /// + /// Shortcode: Khar + /// + Kharoshthi, + + /// + /// Shortcode: Khmr + /// + Khmer, + + /// + /// Shortcode: Khoj + /// + Khojki, + + /// + /// Shortcode: Kits + /// + KhitanSmallScript, + + /// + /// Shortcode: Knda + /// + Kannada, + + /// + /// Shortcode: Kthi + /// + Kaithi, + + /// + /// Shortcode: Lana + /// + TaiTham, + + /// + /// Shortcode: Laoo + /// + Lao, + + /// + /// Shortcode: Latn + /// + Latin, + + /// + /// Shortcode: Lepc + /// + Lepcha, + + /// + /// Shortcode: Limb + /// + Limbu, + + /// + /// Shortcode: Lina + /// + LinearA, + + /// + /// Shortcode: Linb + /// + LinearB, + + /// + /// Shortcode: Lisu + /// + Lisu, + + /// + /// Shortcode: Lyci + /// + Lycian, + + /// + /// Shortcode: Lydi + /// + Lydian, + + /// + /// Shortcode: Mahj + /// + Mahajani, + + /// + /// Shortcode: Maka + /// + Makasar, + + /// + /// Shortcode: Mand + /// + Mandaic, + + /// + /// Shortcode: Mani + /// + Manichaean, + + /// + /// Shortcode: Marc + /// + Marchen, + + /// + /// Shortcode: Medf + /// + Medefaidrin, + + /// + /// Shortcode: Mend + /// + MendeKikakui, + + /// + /// Shortcode: Merc + /// + MeroiticCursive, + + /// + /// Shortcode: Mero + /// + MeroiticHieroglyphs, + + /// + /// Shortcode: Mlym + /// + Malayalam, + + /// + /// Shortcode: Modi + /// + Modi, + + /// + /// Shortcode: Mong + /// + Mongolian, + + /// + /// Shortcode: Mroo + /// + Mro, + + /// + /// Shortcode: Mtei + /// + MeeteiMayek, + + /// + /// Shortcode: Mult + /// + Multani, + + /// + /// Shortcode: Mymr + /// + Myanmar, + + /// + /// Shortcode: Nand + /// + Nandinagari, + + /// + /// Shortcode: Narb + /// + OldNorthArabian, + + /// + /// Shortcode: Nbat + /// + Nabataean, + + /// + /// Shortcode: Newa + /// + Newa, + + /// + /// Shortcode: Nkoo + /// + Nko, + + /// + /// Shortcode: Nshu + /// + Nushu, + + /// + /// Shortcode: Ogam + /// + Ogham, + + /// + /// Shortcode: Olck + /// + OlChiki, + + /// + /// Shortcode: Orkh + /// + OldTurkic, + + /// + /// Shortcode: Orya + /// + Oriya, + + /// + /// Shortcode: Osge + /// + Osage, + + /// + /// Shortcode: Osma + /// + Osmanya, + + /// + /// Shortcode: Ougr + /// + OldUyghur, + + /// + /// Shortcode: Palm + /// + Palmyrene, + + /// + /// Shortcode: Pauc + /// + PauCinHau, + + /// + /// Shortcode: Perm + /// + OldPermic, + + /// + /// Shortcode: Phag + /// + PhagsPa, + + /// + /// Shortcode: Phli + /// + InscriptionalPahlavi, + + /// + /// Shortcode: Phlp + /// + PsalterPahlavi, + + /// + /// Shortcode: Phnx + /// + Phoenician, + + /// + /// Shortcode: Plrd + /// + Miao, + + /// + /// Shortcode: Prti + /// + InscriptionalParthian, + + /// + /// Shortcode: Rjng + /// + Rejang, + + /// + /// Shortcode: Rohg + /// + HanifiRohingya, + + /// + /// Shortcode: Runr + /// + Runic, + + /// + /// Shortcode: Samr + /// + Samaritan, + + /// + /// Shortcode: Sarb + /// + OldSouthArabian, + + /// + /// Shortcode: Saur + /// + Saurashtra, + + /// + /// Shortcode: Sgnw + /// + SignWriting, + + /// + /// Shortcode: Shaw + /// + Shavian, + + /// + /// Shortcode: Shrd + /// + Sharada, + + /// + /// Shortcode: Sidd + /// + Siddham, + + /// + /// Shortcode: Sind + /// + Khudawadi, + + /// + /// Shortcode: Sinh + /// + Sinhala, + + /// + /// Shortcode: Sogd + /// + Sogdian, + + /// + /// Shortcode: Sogo + /// + OldSogdian, + + /// + /// Shortcode: Sora + /// + SoraSompeng, + + /// + /// Shortcode: Soyo + /// + Soyombo, + + /// + /// Shortcode: Sund + /// + Sundanese, + + /// + /// Shortcode: Sylo + /// + SylotiNagri, + + /// + /// Shortcode: Syrc + /// + Syriac, + + /// + /// Shortcode: Tagb + /// + Tagbanwa, + + /// + /// Shortcode: Takr + /// + Takri, + + /// + /// Shortcode: Tale + /// + TaiLe, + + /// + /// Shortcode: Talu + /// + NewTaiLue, + + /// + /// Shortcode: Taml + /// + Tamil, + + /// + /// Shortcode: Tang + /// + Tangut, + + /// + /// Shortcode: Tavt + /// + TaiViet, + + /// + /// Shortcode: Telu + /// + Telugu, + + /// + /// Shortcode: Tfng + /// + Tifinagh, + + /// + /// Shortcode: Tglg + /// + Tagalog, + + /// + /// Shortcode: Thaa + /// + Thaana, + + /// + /// Shortcode: Thai + /// + Thai, + + /// + /// Shortcode: Tibt + /// + Tibetan, + + /// + /// Shortcode: Tirh + /// + Tirhuta, + + /// + /// Shortcode: Tnsa + /// + Tangsa, + + /// + /// Shortcode: Toto + /// + Toto, + + /// + /// Shortcode: Ugar + /// + Ugaritic, + + /// + /// Shortcode: Vaii + /// + Vai, + + /// + /// Shortcode: Vith + /// + Vithkuqi, + + /// + /// Shortcode: Wara + /// + WarangCiti, + + /// + /// Shortcode: Wcho + /// + Wancho, + + /// + /// Shortcode: Xpeo + /// + OldPersian, + + /// + /// Shortcode: Xsux + /// + Cuneiform, + + /// + /// Shortcode: Yezi + /// + Yezidi, + + /// + /// Shortcode: Yiii + /// + Yi, + + /// + /// Shortcode: Zanb + /// + ZanabazarSquare, + + /// + /// Shortcode: Berf + /// + BeriaErfe, + + /// + /// Shortcode: Gara + /// + Garay, + + /// + /// Shortcode: Gukh + /// + GurungKhema, + + /// + /// Shortcode: Kawi + /// + Kawi, + + /// + /// Shortcode: Krai + /// + KiratRai, + + /// + /// Shortcode: Nagm + /// + NagMundari, + + /// + /// Shortcode: Onao + /// + OlOnal, + + /// + /// Shortcode: Sidt + /// + Sidetic, + + /// + /// Shortcode: Sunu + /// + Sunuwar, + + /// + /// Shortcode: Tayo + /// + TaiYo, + + /// + /// Shortcode: Todr + /// + Todhri, + + /// + /// Shortcode: Tols + /// + TolongSiki, + + /// + /// Shortcode: Tutg + /// + TuluTigalari, + + /// + /// OpenType default script tag. Shortcode: DFLT. + /// + Default = 999 + } +} diff --git a/SixLabors.Fonts/Unicode/SpanCodePointEnumerator.cs b/SixLabors.Fonts/Unicode/SpanCodePointEnumerator.cs new file mode 100644 index 0000000..be7bc4c --- /dev/null +++ b/SixLabors.Fonts/Unicode/SpanCodePointEnumerator.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Unicode { + /// + /// An enumerator for retrieving instances from a . + /// Methods are pattern-matched by compiler to allow using foreach pattern. + /// + public ref struct SpanCodePointEnumerator + { + private ReadOnlySpan source; + + /// + /// Initializes a new instance of the struct. + /// + /// The buffer to read from. + public SpanCodePointEnumerator(ReadOnlySpan source) + { + this.source = source; + this.Current = CodePoint.ReplacementChar; + } + + /// + /// Gets the element in the collection at the current position of the enumerator. + /// + public CodePoint Current { get; private set; } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An enumerator that iterates through the collection. + public readonly SpanCodePointEnumerator GetEnumerator() => this; + + /// + /// Advances the enumerator to the next element of the collection. + /// + /// + /// if the enumerator was successfully advanced to the next element; + /// if the enumerator has passed the end of the collection. + /// + public bool MoveNext() + { + this.Current = CodePoint.DecodeFromUtf16At(this.source, 0, out int consumed); + this.source = this.source.Slice(consumed); + return consumed > 0; + } + } +} diff --git a/SixLabors.Fonts/Unicode/SpanGraphemeEnumerator.cs b/SixLabors.Fonts/Unicode/SpanGraphemeEnumerator.cs new file mode 100644 index 0000000..da73a72 --- /dev/null +++ b/SixLabors.Fonts/Unicode/SpanGraphemeEnumerator.cs @@ -0,0 +1,663 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Unicode { + /// + /// An enumerator for retrieving Grapheme instances from a . + ///
+ /// Implements the Unicode Grapheme Cluster Algorithm. UAX:29 + /// + ///
+ /// Supports the UAX #29 extended grapheme cluster rule for Indic conjunct sequences + /// (GB9c) using the property. + ///
+ /// Methods are pattern-matched by compiler to allow using foreach pattern. + ///
+ public ref struct SpanGraphemeEnumerator + { + private ReadOnlySpan source; + private readonly TerminalWidthOptions terminalWidthOptions; + private int sourceOffset; + + /// + /// Initializes a new instance of the struct. + /// + /// The buffer to read from. + public SpanGraphemeEnumerator(ReadOnlySpan source) + : this(source, TerminalWidthOptions.Default) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The buffer to read from. + /// The terminal width options to apply while enumerating. + public SpanGraphemeEnumerator(ReadOnlySpan source, TerminalWidthOptions terminalWidthOptions) + { + this.source = source; + this.terminalWidthOptions = terminalWidthOptions; + this.sourceOffset = 0; + this.Current = default; + } + + /// + /// Gets the element in the collection at the current position of the enumerator. + /// + public GraphemeCluster Current { get; private set; } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An enumerator that iterates through the collection. + public readonly SpanGraphemeEnumerator GetEnumerator() => this; + + /// + /// Advances the enumerator to the next element of the collection. + /// + /// + /// if the enumerator was successfully advanced to the next element; + /// if the enumerator has passed the end of the collection. + /// + public bool MoveNext() + { + // GB9c is a stateful rule: whether the next consonant can join depends on + // the InCB classes already consumed into the current cluster. Keep that state + // outside Processor so Processor remains a simple UTF-16/code-point reader. + IndicConjunctState indicConjunctState = default; + TerminalWidthState terminalWidthState = new(this.terminalWidthOptions); + int utf16Offset = this.sourceOffset; + + // Accept the current scalar into the cluster and advance to the next scalar. + // IMPORTANT: Processor.Current* represents the next scalar not yet included in CharsConsumed. + void ConsumeCurrentAndAdvance(ref Processor p) + { + indicConjunctState.Consume(p.CurrentCodePoint); + terminalWidthState.Consume(p.CurrentCodePoint, p.CurrentType); + p.MoveNext(); + } + + // Drain trailers per GB9/GB9a, plus GB9c-style Indic conjunct tailoring. + // GB9 and GB9a always keep Extend, ZWJ, and SpacingMark with the preceding + // cluster. GB9c additionally keeps an Indic consonant with the same cluster + // when the cluster so far matches: + // InCB=Consonant [InCB=Extend InCB=Linker]* InCB=Linker [InCB=Extend InCB=Linker]* + // x InCB=Consonant + void DrainTrailersAndIndicConjuncts(ref Processor p) + { + while (true) + { + // rules GB9, GB9a + while (p.CurrentType is GraphemeClusterClass.Extend + or GraphemeClusterClass.ZeroWidthJoiner + or GraphemeClusterClass.SpacingMark) + { + ConsumeCurrentAndAdvance(ref p); + } + + // Rule GB9c only fires when the already-consumed cluster has seen + // a consonant and a following linker. Extend values preserve that + // state, so they are consumed above before this check runs. + if (indicConjunctState.CanLinkConsonant + && CodePoint.GetIndicConjunctBreakClass(p.CurrentCodePoint) == IndicConjunctBreakClass.Consonant) + { + ConsumeCurrentAndAdvance(ref p); + continue; + } + + break; + } + } + + if (this.source.IsEmpty) + { + return false; + } + + // Algorithm given at https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules. + Processor processor = new(this.source); + + processor.MoveNext(); + + // First, consume as many Prepend scalars as we can (rule GB9b). + while (processor.CurrentType == GraphemeClusterClass.Prepend) + { + ConsumeCurrentAndAdvance(ref processor); + } + + // Next, make sure we're not about to violate control character restrictions. + // Essentially, if we saw Prepend data, we can't have Control | CR | LF data afterward (rule GB5). + if (processor.CharsConsumed > 0) + { + if (processor.CurrentType is GraphemeClusterClass.Control + or GraphemeClusterClass.CarriageReturn + or GraphemeClusterClass.LineFeed) + { + goto Return; + } + } + + // Now begin the main state machine. + GraphemeClusterClass previousClusterBreakType = processor.CurrentType; + ConsumeCurrentAndAdvance(ref processor); + + switch (previousClusterBreakType) + { + case GraphemeClusterClass.CarriageReturn: + if (processor.CurrentType != GraphemeClusterClass.LineFeed) + { + goto Return; // rules GB3 & GB4 (only can follow ) + } + + ConsumeCurrentAndAdvance(ref processor); + goto case GraphemeClusterClass.LineFeed; + + case GraphemeClusterClass.Control: + case GraphemeClusterClass.LineFeed: + goto Return; // rule GB4 (no data after Control | LF) + + case GraphemeClusterClass.HangulLead: + if (processor.CurrentType == GraphemeClusterClass.HangulLead) + { + ConsumeCurrentAndAdvance(ref processor); // rule GB6 (L x L) + goto case GraphemeClusterClass.HangulLead; + } + else if (processor.CurrentType == GraphemeClusterClass.HangulVowel) + { + ConsumeCurrentAndAdvance(ref processor); // rule GB6 (L x V) + goto case GraphemeClusterClass.HangulVowel; + } + else if (processor.CurrentType == GraphemeClusterClass.HangulLeadVowel) + { + ConsumeCurrentAndAdvance(ref processor); // rule GB6 (L x LV) + goto case GraphemeClusterClass.HangulLeadVowel; + } + else if (processor.CurrentType == GraphemeClusterClass.HangulLeadVowelTail) + { + ConsumeCurrentAndAdvance(ref processor); // rule GB6 (L x LVT) + goto case GraphemeClusterClass.HangulLeadVowelTail; + } + else + { + break; + } + + case GraphemeClusterClass.HangulLeadVowel: + case GraphemeClusterClass.HangulVowel: + if (processor.CurrentType == GraphemeClusterClass.HangulVowel) + { + ConsumeCurrentAndAdvance(ref processor); // rule GB7 (LV | V x V) + goto case GraphemeClusterClass.HangulVowel; + } + else if (processor.CurrentType == GraphemeClusterClass.HangulTail) + { + ConsumeCurrentAndAdvance(ref processor); // rule GB7 (LV | V x T) + goto case GraphemeClusterClass.HangulTail; + } + else + { + break; + } + + case GraphemeClusterClass.HangulLeadVowelTail: + case GraphemeClusterClass.HangulTail: + if (processor.CurrentType == GraphemeClusterClass.HangulTail) + { + ConsumeCurrentAndAdvance(ref processor); // rule GB8 (LVT | T x T) + goto case GraphemeClusterClass.HangulTail; + } + else + { + break; + } + + case GraphemeClusterClass.ExtendedPictographic: + // Attempt processing extended pictographic (rules GB11, GB9). + // First, drain any Extend scalars that might exist + while (processor.CurrentType == GraphemeClusterClass.Extend) + { + ConsumeCurrentAndAdvance(ref processor); + } + + // Now see if there's a ZWJ + extended pictograph again. + if (processor.CurrentType != GraphemeClusterClass.ZeroWidthJoiner) + { + break; + } + + ConsumeCurrentAndAdvance(ref processor); + if (processor.CurrentType != GraphemeClusterClass.ExtendedPictographic) + { + break; + } + + ConsumeCurrentAndAdvance(ref processor); + goto case GraphemeClusterClass.ExtendedPictographic; + + case GraphemeClusterClass.RegionalIndicator: + // We've consumed a single RI scalar. Try to consume another (to make it a pair). + if (processor.CurrentType == GraphemeClusterClass.RegionalIndicator) + { + ConsumeCurrentAndAdvance(ref processor); + } + + // Standalone RI scalars (or a single pair of RI scalars) can only be followed by trailers. + break; // nothing but trailers after the final RI + + default: + break; + } + + DrainTrailersAndIndicConjuncts(ref processor); + + Return: + + terminalWidthState.Complete(); + ReadOnlySpan grapheme = this.source[..processor.CharsConsumed]; + this.Current = new GraphemeCluster( + grapheme, + utf16Offset, + terminalWidthState.CodePointCount, + terminalWidthState.TerminalCellWidth, + terminalWidthState.Flags, + terminalWidthState.FirstCodePoint); + + this.source = this.source[processor.CharsConsumed..]; + this.sourceOffset += processor.CharsConsumed; + + return true; // rules GB2, GB999 + } + + /// + /// Tracks terminal width metadata for the grapheme cluster currently being enumerated. + /// + /// + /// This state is updated as each scalar is accepted into the current UAX #29 cluster, so width, + /// flags, and scalar counts are produced without slicing and re-reading the completed cluster. + /// + private struct TerminalWidthState + { + private readonly TerminalWidthOptions options; + + /// + /// Stores the maximum advancing scalar width before cluster-level overrides are applied. + /// + private int terminalCellWidth; + + /// + /// Indicates that control policy must determine the final cluster width. + /// + private bool containsControl; + + /// + /// Indicates that the cluster contains emoji-related data, even when that data is zero-width. + /// + private bool containsEmoji; + + /// + /// Indicates that terminal practice should treat this emoji-shaped cluster as two cells. + /// + private bool containsEmojiWideOverride; + + /// + /// Stores the first scalar's emoji properties for sequence checks that complete later in the cluster. + /// + private EmojiProperties firstEmojiProperties; + + /// + /// Stores the previous scalar's emoji properties so variation selectors can validate their base. + /// + private EmojiProperties previousEmojiProperties; + + /// + /// Indicates that the cluster contains a valid U+FE0F emoji presentation selector. + /// + private bool containsEmojiPresentationSelector; + + /// + /// Indicates that the cluster contains a valid U+FE0E text presentation selector. + /// + private bool containsTextPresentationSelector; + + /// + /// Initializes a new instance of the struct. + /// + /// The terminal width options to apply to the current cluster. + public TerminalWidthState(TerminalWidthOptions options) + { + this.options = options; + this.terminalCellWidth = 0; + this.containsControl = false; + this.containsEmoji = false; + this.containsEmojiWideOverride = false; + this.firstEmojiProperties = EmojiProperties.None; + this.previousEmojiProperties = EmojiProperties.None; + this.containsEmojiPresentationSelector = false; + this.containsTextPresentationSelector = false; + this.CodePointCount = 0; + this.FirstCodePoint = CodePoint.ReplacementChar; + this.Flags = GraphemeClusterFlags.AllZeroWidth; + } + + /// + /// Gets the number of scalar values consumed into the current cluster. + /// + public int CodePointCount { get; private set; } + + /// + /// Gets the first scalar value consumed into the current cluster. + /// + public CodePoint FirstCodePoint { get; private set; } + + /// + /// Gets the flags derived from the scalars consumed into the current cluster. + /// + public GraphemeClusterFlags Flags { get; private set; } + + /// + /// Gets the policy-resolved terminal cell width of the current cluster. + /// + public readonly int TerminalCellWidth + { + get + { + if (this.containsControl) + { + return this.options.ControlCharacterWidth switch + { + TerminalControlCharacterWidth.Zero => 0, + TerminalControlCharacterWidth.Narrow => 1, + _ => -1, + }; + } + + if (this.containsEmojiWideOverride + && !this.containsTextPresentationSelector + && this.options.EmojiWidth == TerminalEmojiWidth.Wide) + { + return 2; + } + + return this.terminalCellWidth; + } + } + + /// + /// Adds a scalar value to the current cluster metadata. + /// + /// The scalar value accepted into the current cluster. + /// The grapheme break class for . + public void Consume(in CodePoint codePoint, GraphemeClusterClass graphemeClusterClass) + { + EmojiProperties emojiProperties = CodePoint.GetEmojiProperties(codePoint); + if (this.CodePointCount == 0) + { + this.FirstCodePoint = codePoint; + this.firstEmojiProperties = emojiProperties; + } + + this.CodePointCount++; + + if (codePoint.Value == 0) + { + return; + } + + if (CodePoint.IsControl(codePoint)) + { + this.containsControl = true; + this.Flags = (this.Flags & ~GraphemeClusterFlags.AllZeroWidth) | GraphemeClusterFlags.ContainsControl; + return; + } + + if (CodePoint.IsVariationSelector(codePoint)) + { + this.Flags |= GraphemeClusterFlags.ContainsVariationSelector; + + // U+FE0F VARIATION SELECTOR-16 requests emoji presentation. + // Only honor it for bases listed by Unicode as emoji-presentation sequence bases. + if (codePoint.Value == 0xFE0F + && (this.previousEmojiProperties & EmojiProperties.EmojiPresentationSequenceBase) != 0) + { + this.containsEmoji = true; + this.containsEmojiPresentationSelector = true; + this.containsEmojiWideOverride = true; + } + + // U+FE0E VARIATION SELECTOR-15 requests text presentation, which suppresses + // the terminal emoji-wide override even when the base is emoji-capable. + else if (codePoint.Value == 0xFE0E + && (this.previousEmojiProperties & EmojiProperties.TextPresentationSequenceBase) != 0) + { + this.containsEmoji = true; + this.containsTextPresentationSelector = true; + } + + this.previousEmojiProperties = emojiProperties; + + return; + } + + if (graphemeClusterClass == GraphemeClusterClass.ZeroWidthJoiner) + { + this.Flags |= GraphemeClusterFlags.ContainsZwjSequence; + if (this.containsEmoji) + { + this.containsEmojiWideOverride = true; + } + + this.previousEmojiProperties = emojiProperties; + return; + } + + if ((emojiProperties & EmojiProperties.EmojiModifier) != 0) + { + this.containsEmoji = true; + this.previousEmojiProperties = emojiProperties; + return; + } + + if ((emojiProperties & EmojiProperties.Emoji) != 0) + { + this.containsEmoji = true; + this.Flags |= GraphemeClusterFlags.ContainsEmoji; + } + + if ((emojiProperties & EmojiProperties.EmojiPresentation) != 0 && !this.containsTextPresentationSelector) + { + this.containsEmojiWideOverride = true; + } + + // U+20E3 COMBINING ENCLOSING KEYCAP completes keycap emoji sequences + // such as "#\uFE0F\u20E3" when the cluster started from a valid keycap base. + if (codePoint.Value == 0x20E3 + && this.containsEmojiPresentationSelector + && (this.firstEmojiProperties & EmojiProperties.EmojiKeycapSequenceBase) != 0) + { + this.containsEmoji = true; + this.containsEmojiWideOverride = true; + } + + if (IsZeroWidthGraphemeExtension(graphemeClusterClass)) + { + this.previousEmojiProperties = emojiProperties; + return; + } + + if (graphemeClusterClass == GraphemeClusterClass.ExtendedPictographic) + { + this.containsEmoji = true; + this.Flags |= GraphemeClusterFlags.ContainsEmoji; + } + else if (graphemeClusterClass == GraphemeClusterClass.RegionalIndicator) + { + this.containsEmoji = true; + this.containsEmojiWideOverride = true; + this.Flags |= GraphemeClusterFlags.ContainsEmoji; + } + + int scalarWidth = this.GetScalarWidth(codePoint); + if (scalarWidth == 0) + { + return; + } + + this.Flags &= ~GraphemeClusterFlags.AllZeroWidth; + if (scalarWidth == 2) + { + this.Flags |= GraphemeClusterFlags.ContainsWide; + } + + if (scalarWidth > this.terminalCellWidth) + { + this.terminalCellWidth = scalarWidth; + } + + this.previousEmojiProperties = emojiProperties; + } + + /// + /// Finalizes metadata that depends on the completed cluster. + /// + public void Complete() + { + if (this.CodePointCount == 1) + { + this.Flags |= GraphemeClusterFlags.IsSingleCodePoint; + } + + if (this.containsEmoji) + { + this.Flags |= GraphemeClusterFlags.ContainsEmoji; + } + } + + /// + /// Gets the terminal cell width contribution for a non-zero-width scalar. + /// + /// The scalar value to measure. + /// The scalar width after applying East Asian Width and ambiguous-width policy. + private int GetScalarWidth(in CodePoint codePoint) + { + EastAsianWidthClass width = CodePoint.GetEastAsianWidthClass(codePoint); + if (width == EastAsianWidthClass.Ambiguous) + { + this.Flags |= GraphemeClusterFlags.ContainsAmbiguous; + return this.options.AmbiguousWidth == TerminalAmbiguousWidth.Wide ? 2 : 1; + } + + return width is EastAsianWidthClass.Fullwidth or EastAsianWidthClass.Wide ? 2 : 1; + } + + /// + /// Returns a value indicating whether the grapheme break class contributes no terminal advance. + /// + /// The grapheme break class to inspect. + /// if the class is zero-width for terminal measurement. + private static bool IsZeroWidthGraphemeExtension(GraphemeClusterClass graphemeClusterClass) + => graphemeClusterClass is GraphemeClusterClass.Extend + or GraphemeClusterClass.SpacingMark + or GraphemeClusterClass.Prepend; + } + + private ref struct Processor + { + private readonly ReadOnlySpan source; + private int charsConsumed; + + public Processor(ReadOnlySpan source) + { + this.source = source; + this.CurrentType = GraphemeClusterClass.Any; + this.CurrentCodePoint = CodePoint.ReplacementChar; + this.charsConsumed = 0; + this.CharsConsumed = 0; + } + + public GraphemeClusterClass CurrentType { get; private set; } + + public CodePoint CurrentCodePoint { get; private set; } + + public int CharsConsumed { get; private set; } + + public void MoveNext() + { + this.CharsConsumed += this.charsConsumed; + CodePoint codePoint = CodePoint.DecodeFromUtf16At(this.source, this.CharsConsumed, out this.charsConsumed); + this.CurrentCodePoint = codePoint; + this.CurrentType = CodePoint.GetGraphemeClusterClass(codePoint); + } + } + + /// + /// Tracks the already-consumed part of the UAX #29 GB9c Indic conjunct rule. + /// + /// + /// GB9c prevents a grapheme break before an Indic consonant when the current cluster already + /// contains an Indic consonant followed by at least one linker, with optional extend/linker + /// code points in between. This state machine consumes the same code points as the main + /// grapheme enumerator and remembers only the minimum information needed for that decision. + /// + private struct IndicConjunctState + { + /// + /// Indicates that the current cluster contains an InCB=Consonant starter. + /// + private bool hasConsonant; + + /// + /// Indicates that a linker has been consumed after the current consonant starter. + /// + private bool hasLinker; + + /// + /// Gets a value indicating whether GB9c should suppress a break before the next consonant. + /// + /// + /// This becomes true only after a consonant and a following linker have both been consumed. + /// InCB=Extend values leave the state unchanged, so combining marks can appear + /// between the linker and the next consonant. + /// + public readonly bool CanLinkConsonant => this.hasConsonant && this.hasLinker; + + /// + /// Updates the GB9c state with a code point that has just been consumed into the cluster. + /// + /// The consumed code point. + public void Consume(in CodePoint codePoint) + { + switch (CodePoint.GetIndicConjunctBreakClass(codePoint)) + { + case IndicConjunctBreakClass.Consonant: + // A consonant starts or restarts the GB9c candidate. It cannot link a + // following consonant until a linker has also been consumed. + this.hasConsonant = true; + this.hasLinker = false; + break; + + case IndicConjunctBreakClass.Linker: + // Linkers only matter after a consonant starter. Leading linkers cannot + // create a GB9c sequence by themselves. + if (this.hasConsonant) + { + this.hasLinker = true; + } + + break; + + case IndicConjunctBreakClass.Extend: + // Extend values are transparent for GB9c and preserve the current candidate. + break; + + default: + // Any other class ends the candidate conjunct sequence. + this.hasConsonant = false; + this.hasLinker = false; + break; + } + } + } + } +} diff --git a/SixLabors.Fonts/Unicode/SpanWordEnumerator.cs b/SixLabors.Fonts/Unicode/SpanWordEnumerator.cs new file mode 100644 index 0000000..0ea2ad1 --- /dev/null +++ b/SixLabors.Fonts/Unicode/SpanWordEnumerator.cs @@ -0,0 +1,375 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Unicode { + /// + /// An enumerator for retrieving word-boundary segments from a . + ///
+ /// Implements the Unicode Word Boundary Algorithm. UAX #29 + /// + ///
+ /// Methods are pattern-matched by compiler to allow using foreach pattern. + ///
+ public ref struct SpanWordEnumerator + { + private ReadOnlySpan source; + private int sourceOffset; + private int codePointOffset; + + /// + /// Initializes a new instance of the struct. + /// + /// The buffer to read from. + public SpanWordEnumerator(ReadOnlySpan source) + { + this.source = source; + this.sourceOffset = 0; + this.codePointOffset = 0; + this.Current = default; + } + + /// + /// Gets the element in the collection at the current position of the enumerator. + /// + public WordSegment Current { get; private set; } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An enumerator that iterates through the collection. + public readonly SpanWordEnumerator GetEnumerator() => this; + + /// + /// Advances the enumerator to the next element of the collection. + /// + /// + /// if the enumerator was successfully advanced to the next element; + /// if the enumerator has passed the end of the collection. + /// + public bool MoveNext() + { + if (this.sourceOffset >= this.source.Length) + { + return false; + } + + int segmentStart = this.sourceOffset; + int segmentCodePointStart = this.codePointOffset; + WordBreakCodePoint current = this.ReadForward(this.sourceOffset); + int currentEnd = current.Utf16End; + int boundaryCodePoint = this.codePointOffset + 1; + + while (currentEnd < this.source.Length) + { + WordBreakCodePoint next = this.ReadForward(currentEnd); + if (this.IsBoundary(current, next)) + { + break; + } + + current = next; + currentEnd = current.Utf16End; + boundaryCodePoint++; + } + + this.Current = new WordSegment( + this.source[segmentStart..currentEnd], + segmentStart, + segmentCodePointStart, + boundaryCodePoint - segmentCodePointStart); + + this.sourceOffset = currentEnd; + this.codePointOffset = boundaryCodePoint; + return true; + } + + private readonly bool IsBoundary(in WordBreakCodePoint current, in WordBreakCodePoint next) + { + // WB3, WB3a, WB3b, and WB3c are evaluated before the ignore rule, so they + // use the adjacent code points exactly as they appear in the source. + if (current.Is(WordBreakClass.CarriageReturn) && next.Is(WordBreakClass.LineFeed)) + { + return false; + } + + if (IsNewline(current) || IsNewline(next)) + { + return true; + } + + if (current.Is(WordBreakClass.ZeroWidthJoiner) + && CodePoint.GetGraphemeClusterClass(next.CodePoint) == GraphemeClusterClass.ExtendedPictographic) + { + return false; + } + + if (current.Is(WordBreakClass.WSegSpace) && next.Is(WordBreakClass.WSegSpace)) + { + return false; + } + + if (IsIgnored(next)) + { + return false; + } + + WordBreakCodePoint left = this.GetEffectivePrevious(current); + WordBreakClass right = next.Class; + + if (IsAHLetter(left.Class) && IsAHLetter(right)) + { + return false; + } + + if (IsAHLetter(left.Class) + && IsMidLetterMidNumLetQ(right) + && this.TryGetNextSignificant(next.Utf16End, out WordBreakCodePoint after) + && IsAHLetter(after.Class)) + { + return false; + } + + if (IsAHLetter(right) + && IsMidLetterMidNumLetQ(left.Class) + && this.TryGetPreviousSignificant(left.Utf16Start, out WordBreakCodePoint before) + && IsAHLetter(before.Class)) + { + return false; + } + + if (left.Is(WordBreakClass.HebrewLetter) && right == WordBreakClass.SingleQuote) + { + return false; + } + + if (left.Is(WordBreakClass.HebrewLetter) + && right == WordBreakClass.DoubleQuote + && this.TryGetNextSignificant(next.Utf16End, out after) + && after.Is(WordBreakClass.HebrewLetter)) + { + return false; + } + + if (right == WordBreakClass.HebrewLetter + && left.Is(WordBreakClass.DoubleQuote) + && this.TryGetPreviousSignificant(left.Utf16Start, out before) + && before.Is(WordBreakClass.HebrewLetter)) + { + return false; + } + + if (left.Is(WordBreakClass.Numeric) && right == WordBreakClass.Numeric) + { + return false; + } + + if (IsAHLetter(left.Class) && right == WordBreakClass.Numeric) + { + return false; + } + + if (left.Is(WordBreakClass.Numeric) && IsAHLetter(right)) + { + return false; + } + + if (right == WordBreakClass.Numeric + && IsMidNumMidNumLetQ(left.Class) + && this.TryGetPreviousSignificant(left.Utf16Start, out before) + && before.Is(WordBreakClass.Numeric)) + { + return false; + } + + if (left.Is(WordBreakClass.Numeric) + && IsMidNumMidNumLetQ(right) + && this.TryGetNextSignificant(next.Utf16End, out after) + && after.Is(WordBreakClass.Numeric)) + { + return false; + } + + if (left.Is(WordBreakClass.Katakana) && right == WordBreakClass.Katakana) + { + return false; + } + + if (IsAHLetterNumericKatakanaExtendNumLet(left.Class) && right == WordBreakClass.ExtendNumLet) + { + return false; + } + + if (left.Is(WordBreakClass.ExtendNumLet) && IsAHLetterNumericKatakana(right)) + { + return false; + } + + if (left.Is(WordBreakClass.RegionalIndicator) + && right == WordBreakClass.RegionalIndicator + && (this.CountRegionalIndicatorsBefore(next.Utf16Start) & 1) == 1) + { + return false; + } + + return true; + } + + private readonly WordBreakCodePoint GetEffectivePrevious(in WordBreakCodePoint current) + { + if (!IsIgnored(current)) + { + return current; + } + + int scanEnd = current.Utf16Start; + while (this.TryReadBackward(scanEnd, out WordBreakCodePoint previous)) + { + if (!IsIgnored(previous)) + { + // WB4 deliberately stops ignoring after sot and hard line breaks. + return IsNewline(previous) ? current : previous; + } + + scanEnd = previous.Utf16Start; + } + + return current; + } + + private readonly int CountRegionalIndicatorsBefore(int utf16End) + { + int count = 0; + int scanEnd = utf16End; + while (this.TryGetPreviousSignificant(scanEnd, out WordBreakCodePoint previous)) + { + if (!previous.Is(WordBreakClass.RegionalIndicator)) + { + break; + } + + count++; + scanEnd = previous.Utf16Start; + } + + return count; + } + + private readonly bool TryGetPreviousSignificant(int utf16End, out WordBreakCodePoint codePoint) + { + int scanEnd = utf16End; + while (this.TryReadBackward(scanEnd, out codePoint)) + { + if (!IsIgnored(codePoint)) + { + return true; + } + + scanEnd = codePoint.Utf16Start; + } + + codePoint = default; + return false; + } + + private readonly bool TryGetNextSignificant(int utf16Start, out WordBreakCodePoint codePoint) + { + int scanStart = utf16Start; + while (this.TryReadForward(scanStart, out codePoint)) + { + if (!IsIgnored(codePoint)) + { + return true; + } + + scanStart = codePoint.Utf16End; + } + + codePoint = default; + return false; + } + + private readonly WordBreakCodePoint ReadForward(int utf16Start) + { + CodePoint codePoint = CodePoint.DecodeFromUtf16At(this.source, utf16Start, out int charsConsumed); + return new WordBreakCodePoint(codePoint, CodePoint.GetWordBreakClass(codePoint), utf16Start, utf16Start + charsConsumed); + } + + private readonly bool TryReadForward(int utf16Start, out WordBreakCodePoint codePoint) + { + if (utf16Start >= this.source.Length) + { + codePoint = default; + return false; + } + + codePoint = this.ReadForward(utf16Start); + return true; + } + + private readonly bool TryReadBackward(int utf16End, out WordBreakCodePoint codePoint) + { + if (utf16End <= 0) + { + codePoint = default; + return false; + } + + int utf16Start = utf16End - 1; + if (utf16Start > 0 + && char.IsLowSurrogate(this.source[utf16Start]) + && char.IsHighSurrogate(this.source[utf16Start - 1])) + { + utf16Start--; + } + + codePoint = this.ReadForward(utf16Start); + return true; + } + + private static bool IsAHLetter(WordBreakClass cls) + => cls is WordBreakClass.ALetter or WordBreakClass.HebrewLetter; + + private static bool IsAHLetterNumericKatakana(WordBreakClass cls) + => IsAHLetter(cls) || cls is WordBreakClass.Numeric or WordBreakClass.Katakana; + + private static bool IsAHLetterNumericKatakanaExtendNumLet(WordBreakClass cls) + => IsAHLetterNumericKatakana(cls) || cls == WordBreakClass.ExtendNumLet; + + private static bool IsIgnored(in WordBreakCodePoint codePoint) => IsIgnored(codePoint.Class); + + private static bool IsIgnored(WordBreakClass cls) + => cls is WordBreakClass.Extend or WordBreakClass.Format or WordBreakClass.ZeroWidthJoiner; + + private static bool IsMidLetterMidNumLetQ(WordBreakClass cls) + => cls is WordBreakClass.MidLetter or WordBreakClass.MidNumLet or WordBreakClass.SingleQuote; + + private static bool IsMidNumMidNumLetQ(WordBreakClass cls) + => cls is WordBreakClass.MidNum or WordBreakClass.MidNumLet or WordBreakClass.SingleQuote; + + private static bool IsNewline(in WordBreakCodePoint codePoint) + => codePoint.Class is WordBreakClass.CarriageReturn or WordBreakClass.LineFeed or WordBreakClass.Newline; + + private readonly struct WordBreakCodePoint + { + public WordBreakCodePoint(CodePoint codePoint, WordBreakClass cls, int utf16Start, int utf16End) + { + this.CodePoint = codePoint; + this.Class = cls; + this.Utf16Start = utf16Start; + this.Utf16End = utf16End; + } + + public CodePoint CodePoint { get; } + + public WordBreakClass Class { get; } + + public int Utf16Start { get; } + + public int Utf16End { get; } + + public bool Is(WordBreakClass cls) => this.Class == cls; + } + } +} diff --git a/SixLabors.Fonts/Unicode/StateAutomation/DeterministicFiniteAutomata.cs b/SixLabors.Fonts/Unicode/StateAutomation/DeterministicFiniteAutomata.cs new file mode 100644 index 0000000..b2e6fd4 --- /dev/null +++ b/SixLabors.Fonts/Unicode/StateAutomation/DeterministicFiniteAutomata.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#nullable enable + +using System.Collections.Generic; + +namespace UnicodeTrieGenerator.StateAutomation { + /// + /// This is an implementation of the direct regular expression to DFA algorithm described + /// in section 3.9.5 of "Compilers: Principles, Techniques, and Tools" by Aho, + /// Lam, Sethi, and Ullman. + /// There is a PDF of the book here: + /// + /// + internal static class DeterministicFiniteAutomata + { + internal static readonly EndMarker EndMarker = new(); + + public static IEnumerable Build(ILogicalNode root, int numSymbols) + { + root = new Concatenation(root, EndMarker); + root.CalcFollowPos(); + + State failState = new(new HashSet(), numSymbols); + State initialState = new(root.FirstPos, numSymbols); + + List dstates = [failState, initialState]; + + // While there is an unmarked state S in dstates + while (true) + { + State? s = null; + + for (int i = 1; i < dstates.Count; i++) + { + if (!dstates[i].Marked) + { + s = dstates[i]; + break; + } + } + + if (s is null) + { + break; + } + + // Mark S + s.Marked = true; + + // For each input symbol a + for (int a = 0; a < numSymbols; a++) + { + // let U be the union of followpos(p) for all + // p in S that correspond to a + HashSet u = []; + foreach (INode p in s.Positions) + { + if (p is Literal l && l.Value == a) + { + NodeUtilities.AddAll(u, p.FollowPos); + } + } + + if (u.Count == 0) + { + continue; + } + + // if U is not in dstates + int ux = -1; + for (int i = 0; i < dstates.Count; i++) + { + if (NodeUtilities.Equal(u, dstates[i].Positions)) + { + ux = i; + break; + } + } + + if (ux == -1) + { + // Add U as an unmarked state to dstates + dstates.Add(new State(u, numSymbols)); + ux = dstates.Count - 1; + } + + s.Transitions[a] = ux; + } + } + + return dstates; + } + } +} diff --git a/SixLabors.Fonts/Unicode/StateAutomation/INode.cs b/SixLabors.Fonts/Unicode/StateAutomation/INode.cs new file mode 100644 index 0000000..c8ca320 --- /dev/null +++ b/SixLabors.Fonts/Unicode/StateAutomation/INode.cs @@ -0,0 +1,447 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#nullable enable + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace UnicodeTrieGenerator.StateAutomation; + +/// +/// Defines an AST node. +/// +internal interface INode : IEnumerable +{ + /// + /// Gets the following position. + /// + HashSet FollowPos { get; } + + /// + /// Gets a value indicating whether this node is nullable. + /// + bool Nullable { get; } + + /// + /// Gets the number of child nodes in this node. + /// + int Count { get; } + + /// + /// Gets or sets the node at the given position. + /// + /// The index of the node. + /// The node at the given position. + INode this[int index] { get; set; } + + /// + /// Calculates the follow position for this instance. + /// + void CalcFollowPos(); + + /// + /// Returns a copy of the node. + /// + /// The . + INode Copy(); +} + +/// +/// Defines a logical AST node. +/// +internal interface ILogicalNode : INode +{ + /// + /// Gets the collection of nodes as the first position. + /// + HashSet FirstPos { get; } + + /// + /// Gets the collection of nodes at the last position. + /// + HashSet LastPos { get; } +} + +/// +/// The base AST node. +/// +internal abstract class Node : INode +{ + protected List Enumerator { get; } = new(); + + /// + public HashSet FollowPos { get; } = new(); + + /// + public virtual bool Nullable => false; + + public int Count => this.Enumerator.Count; + + public INode this[int index] + { + get => this.Enumerator[index]; + set => this.Enumerator[index] = value; + } + + /// + public virtual void CalcFollowPos() + { + foreach (INode node in this) + { + node.CalcFollowPos(); + } + } + + /// + public abstract INode Copy(); + + public IEnumerator GetEnumerator() => this.Enumerator.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); +} + +/// +/// Represents a variable reference. +/// +internal class Variable : Node, ILogicalNode +{ + public Variable(string name) => this.Name = name; + + public string Name { get; } + + /// + HashSet ILogicalNode.FirstPos { get; } = new HashSet(); + + /// + HashSet ILogicalNode.LastPos { get; } = new HashSet(); + + /// + public override INode Copy() => new Variable(this.Name); +} + +/// +/// Represents a comment. +/// +internal class Comment : Node +{ + public Comment(string value) => this.Value = value; + + public string Value { get; } + + /// + public override INode Copy() => new Comment(this.Value); +} + +/// +/// Represents an assignment statement. e.g. `variable = expression;` +/// +internal class Assignment : Node +{ + public Assignment(Variable variable, ILogicalNode expression) + { + this.Enumerator.Add(variable); + this.Enumerator.Add(expression); + } + + public Variable Variable => (Variable)this[0]; + + public ILogicalNode Expression => (ILogicalNode)this[1]; + + /// + public override INode Copy() => new Assignment(this.Variable, this.Expression); +} + +/// +/// Represents an alternation. e.g. `a | b` +/// +internal class Alternation : Node, ILogicalNode +{ + public Alternation(ILogicalNode a, ILogicalNode b) + { + this.Enumerator.Add(a); + this.Enumerator.Add(b); + } + + public ILogicalNode A => (ILogicalNode)this[0]; + + public ILogicalNode B => (ILogicalNode)this[1]; + + /// + public override bool Nullable => this.A.Nullable || this.B.Nullable; + + /// + public HashSet FirstPos => NodeUtilities.Union(this.A.FirstPos, this.B.FirstPos); + + /// + public HashSet LastPos => NodeUtilities.Union(this.A.LastPos, this.B.LastPos); + + /// + public override INode Copy() + => new Alternation((ILogicalNode)this.A.Copy(), (ILogicalNode)this.B.Copy()); +} + +/// +/// Represents a concatenation, or chain. e.g. `a b c` +/// +internal class Concatenation : Node, ILogicalNode +{ + public Concatenation(ILogicalNode a, ILogicalNode b) + { + this.Enumerator.Add(a); + this.Enumerator.Add(b); + } + + public ILogicalNode A => (ILogicalNode)this[0]; + + public ILogicalNode B => (ILogicalNode)this[1]; + + /// + public override bool Nullable => this.A.Nullable && this.B.Nullable; + + /// + public HashSet FirstPos + { + get + { + HashSet s = this.A.FirstPos; + if (this.A.Nullable) + { + s = NodeUtilities.Union(s, this.B.FirstPos); + } + + return s; + } + } + + /// + public HashSet LastPos + { + get + { + HashSet s = this.B.LastPos; + if (this.B.Nullable) + { + s = NodeUtilities.Union(s, this.A.LastPos); + } + + return s; + } + } + + /// + public override void CalcFollowPos() + { + base.CalcFollowPos(); + foreach (INode n in this.A.LastPos) + { + NodeUtilities.AddAll(n.FollowPos, this.B.FirstPos); + } + } + + /// + public override INode Copy() + => new Concatenation((ILogicalNode)this.A.Copy(), (ILogicalNode)this.B.Copy()); +} + +/// +/// Represents a repetition. e.g. `a+`, `b*`, or `c?` +/// +internal class Repeat : Node, ILogicalNode +{ + public Repeat(ILogicalNode expression, string op) + { + this.Enumerator.Add(expression); + this.Op = op; + } + + public ILogicalNode Expression => (ILogicalNode)this[0]; + + public string Op { get; } + + /// + public override bool Nullable => this.Op is "*" or "?"; + + /// + public HashSet FirstPos => this.Expression.FirstPos; + + /// + public HashSet LastPos => this.Expression.LastPos; + + /// + public override void CalcFollowPos() + { + base.CalcFollowPos(); + if (this.Op is "*" or "+") + { + foreach (INode n in this.LastPos) + { + NodeUtilities.AddAll(n.FollowPos, this.FirstPos); + } + } + } + + /// + public override INode Copy() + => new Repeat((ILogicalNode)this.Expression.Copy(), this.Op); +} + +/// +/// Base class for leaf nodes. +/// +internal abstract class Leaf : Node, ILogicalNode +{ + /// + public HashSet FirstPos => new() { this }; + + /// + public HashSet LastPos => new() { this }; +} + +/// +/// Represents a literal value, e.g. a number. +/// +internal class Literal : Leaf +{ + public Literal(int value) => this.Value = value; + + public int Value { get; } + + /// + public override INode Copy() => new Literal(this.Value); +} + +/// +/// Marks the end of an expression. +/// +internal class EndMarker : Leaf +{ + /// + public override INode Copy() => throw new NotImplementedException(); +} + +/// +/// Represents a tag e.g. `a:(a b)`. +/// +internal class Tag : Leaf +{ + public Tag(string value) => this.Name = value; + + public string Name { get; } + + public override bool Nullable => true; + + /// + public override INode Copy() => new Tag(this.Name); +} + +internal static class NodeUtilities +{ + /// + /// Builds a repetition of the given expression. + /// + /// The expression to repeat. + /// The minimum value to repeat. + /// The maximum number to repeat. + /// THe . + /// Thrown if is out of range. + public static ILogicalNode BuildRepetition(ILogicalNode expression, int min, double max = double.PositiveInfinity) + { + if (min < 0 || min > max) + { + throw new ArgumentOutOfRangeException(nameof(min), $"Invalid repetition range: {min} {max}"); + } + + ILogicalNode? result = null; + for (int i = 0; i < min; i++) + { + result = Concat(result, (ILogicalNode)expression.Copy()); + } + + if (max == double.PositiveInfinity) + { + result = Concat(result, new Repeat((ILogicalNode)expression.Copy(), "*")); + } + else + { + for (int i = min; i < max; i++) + { + result = Concat(result, new Repeat((ILogicalNode)expression.Copy(), "?")); + } + } + + return result!; + } + + /// + /// Concatenates two nodes. + /// + /// The first node. + /// The second node. + /// The combined . + public static ILogicalNode Concat(ILogicalNode? a, ILogicalNode b) + { + if (a is null) + { + return b; + } + + return new Concatenation(a, b); + } + + /// + /// Creates a union of two node sequences. + /// + /// The first node sequence. + /// The second node sequence. + /// The . + public static HashSet Union(HashSet a, HashSet b) + { + var s = new HashSet(a); + AddAll(s, b); + return s; + } + + /// + /// Adds all the elements from set to . + /// + /// The first node sequence. + /// The second node sequence. + public static void AddAll(HashSet a, HashSet b) + { + foreach (INode n in b) + { + _ = a.Add(n); + } + } + + /// + /// Determines whether two sets are equal. + /// + /// The first node sequence. + /// The second node sequence. + /// The + public static bool Equal(ICollection a, ICollection b) + { + if (a == b) + { + return true; + } + + if (a.Count != b.Count) + { + return false; + } + + foreach (INode x in a) + { + if (!b.Contains(x)) + { + return false; + } + } + + return true; + } +} diff --git a/SixLabors.Fonts/Unicode/StateAutomation/State.cs b/SixLabors.Fonts/Unicode/StateAutomation/State.cs new file mode 100644 index 0000000..b080a65 --- /dev/null +++ b/SixLabors.Fonts/Unicode/StateAutomation/State.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Linq; + +namespace UnicodeTrieGenerator.StateAutomation { + internal sealed class State + { + public State(ICollection positions, int length) + { + this.Positions = positions; + this.Transitions = new int[length]; + this.Accepting = positions.Any(x => x == DeterministicFiniteAutomata.EndMarker); + this.Marked = false; + this.Tags = new HashSet(); + foreach (INode pos in positions) + { + if (pos is Tag tag) + { + this.Tags.Add(tag.Name); + } + } + } + + public ICollection Positions { get; set; } + + public int[] Transitions { get; } + + public bool Marked { get; set; } + + public bool Accepting { get; } + + public ICollection Tags { get; set; } + } +} diff --git a/SixLabors.Fonts/Unicode/StateAutomation/StateMachine.cs b/SixLabors.Fonts/Unicode/StateAutomation/StateMachine.cs new file mode 100644 index 0000000..7ed45b0 --- /dev/null +++ b/SixLabors.Fonts/Unicode/StateAutomation/StateMachine.cs @@ -0,0 +1,155 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace UnicodeTrieGenerator.StateAutomation { + internal class StateMachine + { + private const int InitialState = 1; + private const int FailState = 0; + + /// + /// Initializes a new instance of the class. + /// + /// The state table. + /// The accepting states. + /// The tags. + public StateMachine(int[][] stateTable, bool[] accepting, string[][] tags) + { + this.StateTable = stateTable; + this.Accepting = accepting; + this.Tags = tags; + } + + /// + /// Gets the state table. + /// + public int[][] StateTable { get; } + + /// + /// Gets the accepting states. + /// + public bool[] Accepting { get; } + + /// + /// Gets the tags. + /// + public string[][] Tags { get; } + + /// + /// Returns an iterable object that yields pattern matches over the input sequence. + /// + /// The input sequence. + /// The . + public IEnumerable Match(ReadOnlySpan input) + { + int state = InitialState; + int? startRun = null; + int? lastAccepting = null; + + List matches = new(input.Length); + + for (int i = 0; i < input.Length; i++) + { + int c = input[i]; + + int lastState = state; + state = this.StateTable[state][c]; + + if (state == FailState) + { + // yield the last match if any. + if (startRun != null && lastAccepting != null && lastAccepting >= startRun) + { + matches.Add(new StateMatch() + { + StartIndex = startRun.Value, + EndIndex = lastAccepting.Value, + Tags = this.Tags[lastState] + }); + } + + // reset the state as if we started over from the initial state + state = this.StateTable[InitialState][c]; + startRun = null; + } + + // start a run if not in the failure state + if (state != FailState && startRun == null) + { + startRun = i; + } + + // if accepting, mark the potential match end + if (this.Accepting[state]) + { + lastAccepting = i; + } + + // reset the state to the initial state if we get into the failure state + if (state == FailState) + { + state = InitialState; + } + } + + // yield the last match if any. + if (startRun != null && lastAccepting != null && lastAccepting >= startRun) + { + matches.Add(new StateMatch() + { + StartIndex = startRun.Value, + EndIndex = lastAccepting.Value, + Tags = this.Tags[state] + }); + } + + return matches; + } + + /// + /// For each match over the input sequence, action functions matching + /// the tag definitions in the input pattern are called with the startIndex, + /// length, and the sequence to be sliced. + /// + /// The input sequence. + /// The collection of actions. + public void Apply(int[] input, Dictionary>> actions) + { + foreach (StateMatch match in this.Match(input)) + { + foreach (string tag in match.Tags) + { + if (actions.TryGetValue(tag, out Action>? action)) + { + action(match.StartIndex, match.EndIndex, new ArraySlice(input, match.StartIndex, match.EndIndex + 1 - match.StartIndex)); + } + } + } + } + } + + internal class StateMatch : IEquatable + { + public int StartIndex { get; set; } + + public int EndIndex { get; set; } + + public IList Tags { get; set; } = Array.Empty(); + + public override bool Equals(object? obj) => this.Equals(obj as StateMatch); + + public bool Equals(StateMatch? other) + => other is not null + && this.StartIndex == other.StartIndex + && this.EndIndex == other.EndIndex + && this.Tags.SequenceEqual(other.Tags); + + public override int GetHashCode() + => HashCode.Combine(this.StartIndex, this.EndIndex, this.Tags); + } +} diff --git a/SixLabors.Fonts/Unicode/StateAutomation/SymbolTable.cs b/SixLabors.Fonts/Unicode/StateAutomation/SymbolTable.cs new file mode 100644 index 0000000..355443e --- /dev/null +++ b/SixLabors.Fonts/Unicode/StateAutomation/SymbolTable.cs @@ -0,0 +1,81 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#nullable enable + +using System; +using System.Collections.Generic; + +namespace UnicodeTrieGenerator.StateAutomation; + +internal class SymbolTable +{ + public SymbolTable(IList statements, Dictionary externalSymbols) + { + this.Size = 0; + this.AddExternalSymbols(externalSymbols); + this.Process(statements); + } + + public Dictionary Variables { get; set; } = new(); + + public Dictionary Symbols { get; set; } = new(); + + public int Size { get; set; } + + public ILogicalNode Main() + { + if (!this.Variables.TryGetValue(nameof(this.Main), out ILogicalNode? main)) + { + throw new InvalidOperationException("No 'Main' variable declaration found"); + } + + return main; + } + + private void AddExternalSymbols(Dictionary externalSymbols) + { + foreach (string key in externalSymbols.Keys) + { + int symbol = externalSymbols[key]; + this.Variables[key] = new Literal(symbol); + this.Symbols[key] = symbol; + this.Size++; + } + } + + private void Process(IList statements) + { + foreach (INode statement in statements) + { + if (statement is Assignment assignment) + { + this.Variables[assignment.Variable.Name] = (ILogicalNode)this.ProcessExpression(assignment.Expression); + + if (assignment.Expression is Literal literal) + { + this.Symbols[assignment.Variable.Name] = literal.Value; + this.Size++; + } + } + } + } + + private INode ProcessExpression(INode expression) + { + // Process children + for (int i = 0; i < expression.Count; i++) + { + expression[i] = this.ProcessExpression(expression[i]); + } + + // Replace variable references with their values + if (expression is Variable variable) + { + ILogicalNode value = this.Variables[variable.Name]; + expression = this.ProcessExpression(value.Copy()); + } + + return expression; + } +} diff --git a/SixLabors.Fonts/Unicode/TerminalAmbiguousWidth.cs b/SixLabors.Fonts/Unicode/TerminalAmbiguousWidth.cs new file mode 100644 index 0000000..3cefdb6 --- /dev/null +++ b/SixLabors.Fonts/Unicode/TerminalAmbiguousWidth.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode { + /// + /// Defines how East Asian Width Ambiguous scalars resolve for terminal cell measurement. + /// + public enum TerminalAmbiguousWidth + { + /// + /// Resolve ambiguous scalars as one terminal cell. + /// + Narrow = 0, + + /// + /// Resolve ambiguous scalars as two terminal cells. + /// + Wide = 1 + } +} diff --git a/SixLabors.Fonts/Unicode/TerminalControlCharacterWidth.cs b/SixLabors.Fonts/Unicode/TerminalControlCharacterWidth.cs new file mode 100644 index 0000000..d909368 --- /dev/null +++ b/SixLabors.Fonts/Unicode/TerminalControlCharacterWidth.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode { + /// + /// Defines how C0 and C1 control scalars resolve for terminal cell measurement. + /// + public enum TerminalControlCharacterWidth + { + /// + /// Resolve controls as non-printable. + /// + NonPrintable = 0, + + /// + /// Resolve controls as zero terminal cells. + /// + Zero = 1, + + /// + /// Resolve controls as one terminal cell. + /// + Narrow = 2 + } +} diff --git a/SixLabors.Fonts/Unicode/TerminalEmojiWidth.cs b/SixLabors.Fonts/Unicode/TerminalEmojiWidth.cs new file mode 100644 index 0000000..dc2df03 --- /dev/null +++ b/SixLabors.Fonts/Unicode/TerminalEmojiWidth.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode { + /// + /// Defines how emoji clusters resolve for terminal cell measurement. + /// + public enum TerminalEmojiWidth + { + /// + /// Resolve emoji clusters as two terminal cells. + /// + Wide = 0, + + /// + /// Do not apply a whole-cluster emoji override; use East Asian Width-derived scalar widths. + /// + EastAsianWidth = 1 + } +} diff --git a/SixLabors.Fonts/Unicode/TerminalWidthOptions.cs b/SixLabors.Fonts/Unicode/TerminalWidthOptions.cs new file mode 100644 index 0000000..ad4a907 --- /dev/null +++ b/SixLabors.Fonts/Unicode/TerminalWidthOptions.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode { + /// + /// Defines policy used when resolving grapheme clusters to terminal cell widths. + /// + public struct TerminalWidthOptions + { + /// + /// Gets the default terminal width policy. + /// + public static TerminalWidthOptions Default => default; + + /// + /// Gets or sets the width used for East Asian Width Ambiguous scalars. + /// + public TerminalAmbiguousWidth AmbiguousWidth { get; set; } + + /// + /// Gets or sets the width policy used for emoji clusters. + /// + public TerminalEmojiWidth EmojiWidth { get; set; } + + /// + /// Gets or sets the width policy used for C0 and C1 control scalars. + /// + public TerminalControlCharacterWidth ControlCharacterWidth { get; set; } + } +} diff --git a/SixLabors.Fonts/Unicode/UnicodeData.cs b/SixLabors.Fonts/Unicode/UnicodeData.cs new file mode 100644 index 0000000..9e1188d --- /dev/null +++ b/SixLabors.Fonts/Unicode/UnicodeData.cs @@ -0,0 +1,109 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Globalization; +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Unicode.Resources; + +namespace SixLabors.Fonts.Unicode { + internal static class UnicodeData + { + private static readonly Lazy LazyBidiTrie = new(() => GetBidiTrie(), true); + private static readonly Lazy LazyBidiMirrorTrie = new(() => GetBidiMirrorTrie(), true); + private static readonly Lazy LazyEastAsianWidthTrie = new(() => GetEastAsianWidthTrie(), true); + private static readonly Lazy LazyEmojiTrie = new(() => GetEmojiTrie(), true); + private static readonly Lazy LazyGraphemeTrie = new(() => GetGraphemeTrie(), true); + private static readonly Lazy LazyLineBreakTrie = new(() => GetLineBreakTrie(), true); + private static readonly Lazy LazyWordBreakTrie = new(() => GetWordBreakTrie(), true); + private static readonly Lazy LazyScriptTrie = new(() => GetScriptTrie(), true); + private static readonly Lazy LazyCategoryTrie = new(() => GetCategoryTrie(), true); + private static readonly Lazy LazyArabicShapingTrie = new(() => GetArabicShapingTrie(), true); + private static readonly Lazy LazyIndicConjunctBreakTrie = new(() => GetIndicConjunctBreakTrie(), true); + private static readonly Lazy LazyIndicSyllabicCategoryTrie = new(() => GetIndicSyllabicCategoryTrie(), true); + private static readonly Lazy LazyIndicPositionalCategoryTrie = new(() => GetIndicPositionalCategoryTrie(), true); + private static readonly Lazy LazyVerticalOrientationTrie = new(() => GetVerticalOrientationTrie(), true); + private static readonly Lazy LazyUniversalShapingTrie = new(() => GetUniversalShapingTrie(), true); + private static readonly Lazy LazyIndicShapingTrie = new(() => GetIndicShapingTrie(), true); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetBidiData(uint codePoint) => LazyBidiTrie.Value.Get(codePoint); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetBidiMirror(uint codePoint) => LazyBidiMirrorTrie.Value.Get(codePoint); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static EastAsianWidthClass GetEastAsianWidthClass(uint codePoint) => (EastAsianWidthClass)LazyEastAsianWidthTrie.Value.Get(codePoint); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static EmojiProperties GetEmojiProperties(uint codePoint) => (EmojiProperties)LazyEmojiTrie.Value.Get(codePoint); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GraphemeClusterClass GetGraphemeClusterClass(uint codePoint) => (GraphemeClusterClass)LazyGraphemeTrie.Value.Get(codePoint); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static LineBreakClass GetLineBreakClass(uint codePoint) => (LineBreakClass)LazyLineBreakTrie.Value.Get(codePoint); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static WordBreakClass GetWordBreakClass(uint codePoint) => (WordBreakClass)LazyWordBreakTrie.Value.Get(codePoint); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScriptClass GetScriptClass(uint codePoint) => (ScriptClass)LazyScriptTrie.Value.Get(codePoint); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetJoiningClass(uint codePoint) => LazyArabicShapingTrie.Value.Get(codePoint); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UnicodeCategory GetUnicodeCategory(uint codePoint) => (UnicodeCategory)LazyCategoryTrie.Value.Get(codePoint); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IndicConjunctBreakClass GetIndicConjunctBreakClass(uint codePoint) => (IndicConjunctBreakClass)LazyIndicConjunctBreakTrie.Value.Get(codePoint); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IndicSyllabicCategory GetIndicSyllabicCategory(uint codePoint) => (IndicSyllabicCategory)LazyIndicSyllabicCategoryTrie.Value.Get(codePoint); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IndicPositionalCategory GetIndicPositionalCategory(uint codePoint) => (IndicPositionalCategory)LazyIndicPositionalCategoryTrie.Value.Get(codePoint); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static VerticalOrientationType GetVerticalOrientation(uint codePoint) => (VerticalOrientationType)LazyVerticalOrientationTrie.Value.Get(codePoint); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetUniversalShapingSymbolCount(uint codePoint) => (int)LazyUniversalShapingTrie.Value.Get(codePoint); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetIndicShapingProperties(uint codePoint) => (int)LazyIndicShapingTrie.Value.Get(codePoint); + + private static UnicodeTrie GetBidiTrie() => new(BidiTrie.Data); + + private static UnicodeTrie GetBidiMirrorTrie() => new(BidiMirrorTrie.Data); + + private static UnicodeTrie GetEastAsianWidthTrie() => new(EastAsianWidthTrie.Data); + + private static UnicodeTrie GetEmojiTrie() => new(EmojiTrie.Data); + + private static UnicodeTrie GetGraphemeTrie() => new(GraphemeTrie.Data); + + private static UnicodeTrie GetLineBreakTrie() => new(LineBreakTrie.Data); + + private static UnicodeTrie GetWordBreakTrie() => new(WordBreakTrie.Data); + + private static UnicodeTrie GetScriptTrie() => new(ScriptTrie.Data); + + private static UnicodeTrie GetCategoryTrie() => new(UnicodeCategoryTrie.Data); + + private static UnicodeTrie GetArabicShapingTrie() => new(ArabicShapingTrie.Data); + + private static UnicodeTrie GetIndicConjunctBreakTrie() => new(IndicConjunctBreakTrie.Data); + + private static UnicodeTrie GetIndicSyllabicCategoryTrie() => new(IndicSyllabicCategoryTrie.Data); + + private static UnicodeTrie GetIndicPositionalCategoryTrie() => new(IndicPositionalCategoryTrie.Data); + + private static UnicodeTrie GetVerticalOrientationTrie() => new(VerticalOrientationTrie.Data); + + private static UnicodeTrie GetUniversalShapingTrie() => new(UniversalShapingTrie.Data); + + private static UnicodeTrie GetIndicShapingTrie() => new(IndicShapingTrie.Data); + } +} diff --git a/SixLabors.Fonts/Unicode/UnicodeTrie.cs b/SixLabors.Fonts/Unicode/UnicodeTrie.cs new file mode 100644 index 0000000..4ab8c13 --- /dev/null +++ b/SixLabors.Fonts/Unicode/UnicodeTrie.cs @@ -0,0 +1,174 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using static SixLabors.Fonts.Unicode.UnicodeTrieBuilder; + +namespace SixLabors.Fonts.Unicode { + /// + /// A read-only Trie, holding 32 bit data values. + /// A UnicodeTrie is a highly optimized data structure for mapping from Unicode + /// code points(values ranging from 0 to 0x10ffff) to a 32 bit value. + /// + internal sealed class UnicodeTrie + { + private readonly uint[] data; + private readonly int highStart; + private readonly uint errorValue; + + public UnicodeTrie(ReadOnlySpan rawData) + { + UnicodeTrieHeader header = MemoryMarshal.Read(rawData); + + if (!BitConverter.IsLittleEndian) + { + header.HighStart = BinaryPrimitives.ReverseEndianness(header.HighStart); + header.ErrorValue = BinaryPrimitives.ReverseEndianness(header.ErrorValue); + header.DataLength = BinaryPrimitives.ReverseEndianness(header.DataLength); + } + + int length = header.DataLength; + uint[] data = new uint[length / sizeof(uint)]; + rawData[^length..].CopyTo(MemoryMarshal.AsBytes(data.AsSpan())); + + if (!BitConverter.IsLittleEndian) + { + for (int i = 0; i < data.Length; i++) + { + data[i] = BinaryPrimitives.ReverseEndianness(data[i]); + } + } + + this.highStart = header.HighStart; + this.errorValue = header.ErrorValue; + this.data = data; + } + + /// + /// Initializes a new instance of the class. + /// + /// The stream containing the compressed data. + public UnicodeTrie(Stream stream) + { + // Read the header info + using (BinaryReader br = new(stream, Encoding.UTF8, true)) + { + this.highStart = br.ReadInt32(); + this.errorValue = br.ReadUInt32(); + this.data = new uint[br.ReadInt32() / sizeof(uint)]; + } + + // Read the data in compressed format. + using (BinaryReader br = new(stream, Encoding.UTF8, true)) + { + for (int i = 0; i < this.data.Length; i++) + { + this.data[i] = br.ReadUInt32(); + } + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The uncompressed trie data. + /// The start of the last range which ends at U+10ffff. + /// The value for out-of-range code points and illegal UTF-8. + public UnicodeTrie(uint[] data, int highStart, uint errorValue) + { + this.data = data; + this.highStart = highStart; + this.errorValue = errorValue; + } + + /// + /// Get the value for a code point as stored in the trie. + /// + /// The code point. + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint Get(uint codePoint) + { + uint index; + ref uint dataBase = ref MemoryMarshal.GetReference(this.data.AsSpan()); + + if (codePoint is < 0x0d800 or (> 0x0dbff and <= 0x0ffff)) + { + // Ordinary BMP code point, excluding leading surrogates. + // BMP uses a single level lookup. BMP index starts at offset 0 in the Trie2 index. + // 16 bit data is stored in the index array itself. + index = this.data[codePoint >> UTRIE2_SHIFT_2]; + index = (index << UTRIE2_INDEX_SHIFT) + (codePoint & UTRIE2_DATA_MASK); + return Unsafe.Add(ref dataBase, (nint)index); + } + + if (codePoint <= 0xffff) + { + // Lead Surrogate Code Point. A Separate index section is stored for + // lead surrogate code units and code points. + // The main index has the code unit data. + // For this function, we need the code point data. + // Note: this expression could be refactored for slightly improved efficiency, but + // surrogate code points will be so rare in practice that it's not worth it. + index = this.data[UTRIE2_LSCP_INDEX_2_OFFSET + ((codePoint - 0xd800) >> UTRIE2_SHIFT_2)]; + index = (index << UTRIE2_INDEX_SHIFT) + (codePoint & UTRIE2_DATA_MASK); + return Unsafe.Add(ref dataBase, (nint)index); + } + + if (codePoint < this.highStart) + { + // Supplemental code point, use two-level lookup. + index = UTRIE2_INDEX_1_OFFSET - UTRIE2_OMITTED_BMP_INDEX_1_LENGTH + (codePoint >> UTRIE2_SHIFT_1); + index = this.data[index]; + index += (codePoint >> UTRIE2_SHIFT_2) & UTRIE2_INDEX_2_MASK; + index = this.data[index]; + index = (index << UTRIE2_INDEX_SHIFT) + (codePoint & UTRIE2_DATA_MASK); + return Unsafe.Add(ref dataBase, (nint)index); + } + + if (codePoint <= 0x10ffff) + { + return Unsafe.Add(ref dataBase, (nint)(this.data.Length - UTRIE2_DATA_GRANULARITY)); + } + + // Fall through. The code point is outside of the legal range of 0..0x10ffff. + return this.errorValue; + } + + /// + /// Saves the to the stream in a compressed format. + /// + /// The output stream. + public void Save(Stream stream) + { + // Write the header info + using (BinaryWriter bw = new(stream, Encoding.UTF8, true)) + { + bw.Write(this.highStart); + bw.Write(this.errorValue); + bw.Write(this.data.Length * sizeof(uint)); + } + + // Write the data. + using (BinaryWriter bw = new(stream, Encoding.UTF8, true)) + { + for (int i = 0; i < this.data.Length; i++) + { + bw.Write(this.data[i]); + } + } + } + + private struct UnicodeTrieHeader + { + public int HighStart; + public uint ErrorValue; + public int DataLength; + } + } +} diff --git a/SixLabors.Fonts/Unicode/UnicodeTrieBuilder.cs b/SixLabors.Fonts/Unicode/UnicodeTrieBuilder.cs new file mode 100644 index 0000000..6fd0afa --- /dev/null +++ b/SixLabors.Fonts/Unicode/UnicodeTrieBuilder.cs @@ -0,0 +1,1205 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Unicode { + /// + /// Builder class to manipulate and generate a trie. + /// This is useful for ICU data in primitive types. + /// Provides a compact way to store information that is indexed by Unicode + /// values, such as character properties, types, keyboard values, etc. + /// This is very useful when you have a block of Unicode data that contains significant + /// values while the rest of the Unicode data is unused in the application or + /// when you have a lot of redundance, such as where all 21,000 Han ideographs + /// have the same value. However, lookup is much faster than a hash table. + /// A trie of any primitive data type serves two purposes: + ///
    + ///
  • Fast access of the indexed values.
  • + ///
  • Smaller memory footprint.
  • + ///
+ ///
+ internal class UnicodeTrieBuilder + { + // These have been kept in the original format for now to aid porting + // and testing. +#pragma warning disable SA1310 // Field names should not contain underscore + + // Shift size for getting the index-1 table offset. + internal const int UTRIE2_SHIFT_1 = 6 + 5; + + // Shift size for getting the index-2 table offset. + internal const int UTRIE2_SHIFT_2 = 5; + + // Difference between the two shift sizes, + // for getting an index-1 offset from an index-2 offset. 6=11-5 + private const int UTRIE2_SHIFT_1_2 = UTRIE2_SHIFT_1 - UTRIE2_SHIFT_2; + + // Number of index-1 entries for the BMP. 32=0x20 + // This part of the index-1 table is omitted from the serialized form. + internal const int UTRIE2_OMITTED_BMP_INDEX_1_LENGTH = 0x10000 >> UTRIE2_SHIFT_1; + + // Number of code points per index-1 table entry. 2048=0x800 + private const int UTRIE2_CP_PER_INDEX_1_ENTRY = 1 << UTRIE2_SHIFT_1; + + // Start with allocation of 16k data entries. + private const int INITIAL_DATA_LENGTH = 1 << 14; + + // Grow about 8x each time. + private const int UNEWTRIE2_MEDIUM_DATA_LENGTH = 1 << 17; + + private const int INDEX_1_LENGTH = 0x110000 >> UTRIE2_SHIFT_1; + + // Number of entries in a data block. 32=0x20 + private const int UTRIE2_DATA_BLOCK_LENGTH = 1 << UTRIE2_SHIFT_2; + + // Mask for getting the lower bits for the in-data-block offset. + internal const int UTRIE2_DATA_MASK = UTRIE2_DATA_BLOCK_LENGTH - 1; + + // Shift size for shifting left the index array values. + // Increases possible data size with 16-bit index values at the cost + // of compactability. + // This requires data blocks to be aligned by UTRIE2_DATA_GRANULARITY. + internal const int UTRIE2_INDEX_SHIFT = 2; + + // The alignment size of a data block. Also the granularity for compaction. + internal const int UTRIE2_DATA_GRANULARITY = 1 << UTRIE2_INDEX_SHIFT; + + // The BMP part of the index-2 table is fixed and linear and starts at offset 0. + // Length=2048=0x800=0x10000>>UTRIE2_SHIFT_2. + private const int UTRIE2_INDEX_2_OFFSET = 0; + + // The part of the index-2 table for U+D800..U+DBFF stores values for + // lead surrogate code _units_ not code _points_. + // Values for lead surrogate code _points_ are indexed with this portion of the table. + // Length=32=0x20=0x400>>UTRIE2_SHIFT_2. (There are 1024=0x400 lead surrogates.) + internal const int UTRIE2_LSCP_INDEX_2_OFFSET = 0x10000 >> UTRIE2_SHIFT_2; + private const int UTRIE2_LSCP_INDEX_2_LENGTH = 0x400 >> UTRIE2_SHIFT_2; + + // Count the lengths of both BMP pieces. 2080=0x820 + private const int UTRIE2_INDEX_2_BMP_LENGTH = UTRIE2_LSCP_INDEX_2_OFFSET + UTRIE2_LSCP_INDEX_2_LENGTH; + + // The 2-byte UTF-8 version of the index-2 table follows at offset 2080=0x820. + // Length 32=0x20 for lead bytes C0..DF, regardless of UTRIE2_SHIFT_2. + private const int UTRIE2_UTF8_2B_INDEX_2_OFFSET = UTRIE2_INDEX_2_BMP_LENGTH; + private const int UTRIE2_UTF8_2B_INDEX_2_LENGTH = 0x800 >> 6; // U+0800 is the first code point after 2-byte UTF-8 + + // The index-1 table, only used for supplementary code points, at offset 2112=0x840. + // Variable length, for code points up to highStart, where the last single-value range starts. + // Maximum length 512=0x200=0x100000>>UTRIE2_SHIFT_1. + // (For 0x100000 supplementary code points U+10000..U+10ffff.) + // + // The part of the index-2 table for supplementary code points starts + // after this index-1 table. + // + // Both the index-1 table and the following part of the index-2 table + // are omitted completely if there is only BMP data. + internal const int UTRIE2_INDEX_1_OFFSET = UTRIE2_UTF8_2B_INDEX_2_OFFSET + UTRIE2_UTF8_2B_INDEX_2_LENGTH; + private const int UTRIE2_MAX_INDEX_1_LENGTH = 0x100000 >> UTRIE2_SHIFT_1; + + // Maximum length of the build-time index-2 array. + // Maximum number of Unicode code points (0x110000) shifted right by UTRIE2_SHIFT_2, + // plus the part of the index-2 table for lead surrogate code points, + // plus the build-time index gap, + // plus the null index-2 block. + private const int UNEWTRIE2_MAX_INDEX_2_LENGTH = (0x110000 >> UTRIE2_SHIFT_2) + + UTRIE2_LSCP_INDEX_2_LENGTH + + UNEWTRIE2_INDEX_GAP_LENGTH + + UTRIE2_INDEX_2_BLOCK_LENGTH; + + private const int UNEWTRIE2_INDEX_1_LENGTH = 0x110000 >> UTRIE2_SHIFT_1; + + // Number of entries in an index-2 block. 64=0x40 + private const int UTRIE2_INDEX_2_BLOCK_LENGTH = 1 << UTRIE2_SHIFT_1_2; + + // Mask for getting the lower bits for the in-index-2-block offset. + internal const int UTRIE2_INDEX_2_MASK = UTRIE2_INDEX_2_BLOCK_LENGTH - 1; + + // At build time, leave a gap in the index-2 table, + // at least as long as the maximum lengths of the 2-byte UTF-8 index-2 table + // and the supplementary index-1 table. + // Round up to UTRIE2_INDEX_2_BLOCK_LENGTH for proper compacting. + private const int UNEWTRIE2_INDEX_GAP_OFFSET = UTRIE2_INDEX_2_BMP_LENGTH; + private const int UNEWTRIE2_INDEX_GAP_LENGTH = + (UTRIE2_UTF8_2B_INDEX_2_LENGTH + UTRIE2_MAX_INDEX_1_LENGTH + UTRIE2_INDEX_2_MASK) & ~UTRIE2_INDEX_2_MASK; + + // Maximum length of the build-time data array. + // One entry per 0x110000 code points, plus the illegal-UTF-8 block and the null block, + // plus values for the 0x400 surrogate code units. + private const int UNEWTRIE2_MAX_DATA_LENGTH = 0x110000 + 0x40 + 0x40 + 0x400; + + // The illegal-UTF-8 data block follows the ASCII block, at offset 128=0x80. + // Used with linear access for single bytes 0..0xbf for simple error handling. + // Length 64=0x40, not UTRIE2_DATA_BLOCK_LENGTH. + private const int UTRIE2_BAD_UTF8_DATA_OFFSET = 0x80; + + // The start of non-linear-ASCII data blocks, at offset 192=0xc0. + private const int UTRIE2_DATA_START_OFFSET = 0xc0; + + // The null data block. + // Length 64=0x40 even if UTRIE2_DATA_BLOCK_LENGTH is smaller, + // to work with 6-bit trail bytes from 2-byte UTF-8. + private const int UNEWTRIE2_DATA_NULL_OFFSET = UTRIE2_DATA_START_OFFSET; + + // The null index-2 block, following the gap in the index-2 table. + private const int UNEWTRIE2_INDEX_2_NULL_OFFSET = UNEWTRIE2_INDEX_GAP_OFFSET + UNEWTRIE2_INDEX_GAP_LENGTH; + + // The start of allocated index-2 blocks. + private const int UNEWTRIE2_INDEX_2_START_OFFSET = UNEWTRIE2_INDEX_2_NULL_OFFSET + UTRIE2_INDEX_2_BLOCK_LENGTH; + + // The start of allocated data blocks. + private const int UNEWTRIE2_DATA_START_OFFSET = UNEWTRIE2_DATA_NULL_OFFSET + 0x40; + + // The start of data blocks for U+0800 and above. + // Below, compaction uses a block length of 64 for 2-byte UTF-8. + // From here on, compaction uses UTRIE2_DATA_BLOCK_LENGTH. + // Data values for 0x780 code points beyond ASCII. + private const int UNEWTRIE2_DATA_0800_OFFSET = UNEWTRIE2_DATA_START_OFFSET + 0x780; + + // Maximum length of the runtime index array. + // Limited by its own 16-bit index values, and by uint16_t UTrie2Header.indexLength. + // (The actual maximum length is lower, + // (0x110000>>UTRIE2_SHIFT_2)+UTRIE2_UTF8_2B_INDEX_2_LENGTH+UTRIE2_MAX_INDEX_1_LENGTH.) + private const int UTRIE2_MAX_INDEX_LENGTH = 0xffff; + + // Maximum length of the runtime data array. + // Limited by 16-bit index values that are left-shifted by UTRIE2_INDEX_SHIFT, + // and by uint16_t UTrie2Header.shiftedDataLength. + private const int UTRIE2_MAX_DATA_LENGTH = 0xffff << UTRIE2_INDEX_SHIFT; + +#pragma warning restore SA1310 // Field names should not contain underscore + + private readonly uint initialValue; + private readonly uint errorValue; + private int highStart; + private uint[] data; + private int dataCapacity; + private readonly int[] index1; + private readonly int[] index2; + private int firstFreeBlock; + private bool isCompacted; + private readonly int[] map; + private int dataNullOffset; + private int dataLength; + private int index2NullOffset; + private int index2Length; + + /// + /// Initializes a new instance of the class. + /// + /// The initial value that is set for all code points. + /// The value for out-of-range code points and illegal UTF-8. + public UnicodeTrieBuilder(uint initialValue = 0, uint errorValue = 0) + { + this.initialValue = initialValue; + this.errorValue = errorValue; + this.highStart = 0x110000; + + this.index1 = new int[INDEX_1_LENGTH]; + this.index2 = new int[UNEWTRIE2_MAX_INDEX_2_LENGTH]; + this.data = new uint[INITIAL_DATA_LENGTH]; + this.dataCapacity = INITIAL_DATA_LENGTH; + + this.firstFreeBlock = 0; + this.isCompacted = false; + + // Multi-purpose per-data-block table. + // + // Before compacting: + // + // Per-data-block reference counters/free-block list. + // 0: unused + // >0: reference counter (number of index-2 entries pointing here) + // <0: next free data block in free-block list + // + // While compacting: + // + // Map of adjusted indexes, used in compactData() and compactIndex2(). + // Maps from original indexes to new ones. + this.map = new int[UNEWTRIE2_MAX_DATA_LENGTH >> UTRIE2_SHIFT_2]; + + // preallocate and reset + // - ASCII + // - the bad-UTF-8-data block + // - the null data block + int i; + for (i = 0; i < 0x80; ++i) + { + this.data[i] = initialValue; + } + + for (; i < 0xc0; ++i) + { + this.data[i] = errorValue; + } + + for (i = UNEWTRIE2_DATA_NULL_OFFSET; i < UNEWTRIE2_DATA_START_OFFSET; ++i) + { + this.data[i] = initialValue; + } + + this.dataNullOffset = UNEWTRIE2_DATA_NULL_OFFSET; + this.dataLength = UNEWTRIE2_DATA_START_OFFSET; + + // set the index-2 indexes for the 2=0x80>>UTRIE2_SHIFT_2 ASCII data blocks + int j; + for (i = 0, j = 0; j < 0x80; ++i, j += UTRIE2_DATA_BLOCK_LENGTH) + { + this.index2[i] = j; + this.map[i] = 1; + } + + // reference counts for the bad-UTF-8-data block */ + for (; j < 0xc0; ++i, j += UTRIE2_DATA_BLOCK_LENGTH) + { + this.map[i] = 0; + } + + // Reference counts for the null data block: all blocks except for the ASCII blocks. + // Plus 1 so that we don't drop this block during compaction. + // Plus as many as needed for lead surrogate code points. + // i==newdataNullOffset + this.map[i++] = (0x110000 >> UTRIE2_SHIFT_2) + - (0x80 >> UTRIE2_SHIFT_2) + + 1 + + UTRIE2_LSCP_INDEX_2_LENGTH; + + j += UTRIE2_DATA_BLOCK_LENGTH; + for (; j < UNEWTRIE2_DATA_START_OFFSET; ++i, j += UTRIE2_DATA_BLOCK_LENGTH) + { + this.map[i] = 0; + } + + // set the remaining indexes in the BMP index-2 block + // to the null data block + for (i = 0x80 >> UTRIE2_SHIFT_2; i < UTRIE2_INDEX_2_BMP_LENGTH; ++i) + { + this.index2[i] = UNEWTRIE2_DATA_NULL_OFFSET; + } + + // Fill the index gap with impossible values so that compaction + // does not overlap other index-2 blocks with the gap. + for (i = 0; i < UNEWTRIE2_INDEX_GAP_LENGTH; ++i) + { + this.index2[UNEWTRIE2_INDEX_GAP_OFFSET + i] = -1; + } + + // set the indexes in the null index-2 block + for (i = 0; i < UTRIE2_INDEX_2_BLOCK_LENGTH; ++i) + { + this.index2[UNEWTRIE2_INDEX_2_NULL_OFFSET + i] = UNEWTRIE2_DATA_NULL_OFFSET; + } + + this.index2NullOffset = UNEWTRIE2_INDEX_2_NULL_OFFSET; + this.index2Length = UNEWTRIE2_INDEX_2_START_OFFSET; + + // set the index-1 indexes for the linear index-2 block + for (i = 0, j = 0; i < UTRIE2_OMITTED_BMP_INDEX_1_LENGTH; ++i, j += UTRIE2_INDEX_2_BLOCK_LENGTH) + { + this.index1[i] = j; + } + + // set the remaining index-1 indexes to the null index-2 block + for (; i < UNEWTRIE2_INDEX_1_LENGTH; ++i) + { + this.index1[i] = UNEWTRIE2_INDEX_2_NULL_OFFSET; + } + + // Preallocate and reset data for U+0080..U+07ff, + // for 2-byte UTF-8 which will be compacted in 64-blocks + // even if UTRIE2_DATA_BLOCK_LENGTH is smaller. + for (i = 0x80; i < 0x800; i += UTRIE2_DATA_BLOCK_LENGTH) + { + this.Set(i, initialValue); + } + } + + /// + /// Gets the value for a code point as stored in the trie. + /// + /// The code point. + /// The value. + public uint Get(int c) => this.Get(c, true); + + /// + /// Sets a value for a given code point. + /// + /// The code point. + /// The value. + /// Invalid codepoint. + /// Already compacted. + public void Set(int codePoint, uint value) + { + if (codePoint is < 0 or > 0x10ffff) + { + throw new ArgumentOutOfRangeException(nameof(codePoint)); + } + + if (this.isCompacted) + { + throw new InvalidOperationException("Already compacted"); + } + + int block = this.GetDataBlock(codePoint, true); + this.data[block + (codePoint & UTRIE2_DATA_MASK)] = value; + } + + /// + /// Set a value in a range of code points [start..end]. + /// All code points c with start <= c <= end will get the value if + /// is or if the old value is the + /// initial value. + /// + /// The first code point to get the value. + /// The last code point to get the value (inclusive). + /// The value. + /// Whether old non-initial values are to be overwritten. + /// Invalid codepoint. + /// Already compacted. + public void SetRange(int start, int end, uint value, bool overwrite) + { + if ((start > 0x10ffff) || (end > 0x10ffff) || start > end) + { + throw new ArgumentOutOfRangeException(nameof(start)); + } + + if (this.isCompacted) + { + throw new InvalidOperationException("Already compacted"); + } + + if (!overwrite && value == this.initialValue) + { + return; // Nothing to do. + } + + int block; + int rest; + int repeatBlock; + int limit = end + 1; + if ((start & UTRIE2_DATA_MASK) != 0) + { + int nextStart; + + // set partial block at [start..following block boundary[ + block = this.GetDataBlock(start, true); + nextStart = (start + UTRIE2_DATA_MASK) & ~UTRIE2_DATA_MASK; + if (nextStart <= limit) + { + this.FillBlock(block, start & UTRIE2_DATA_MASK, UTRIE2_DATA_BLOCK_LENGTH, value, this.initialValue, overwrite); + start = nextStart; + } + else + { + this.FillBlock(block, start & UTRIE2_DATA_MASK, limit & UTRIE2_DATA_MASK, value, this.initialValue, overwrite); + return; + } + } + + // number of positions in the last, partial block + rest = limit & UTRIE2_DATA_MASK; + + // round down limit to a block boundary + limit &= ~UTRIE2_DATA_MASK; + + // iterate over all-value blocks + if (value == this.initialValue) + { + repeatBlock = this.dataNullOffset; + } + else + { + repeatBlock = -1; + } + + while (start < limit) + { + int i2; + bool setRepeatBlock = false; + + if (value == this.initialValue && this.IsInNullBlock(start, true)) + { + start += UTRIE2_DATA_BLOCK_LENGTH; // nothing to do + continue; + } + + // get index value + i2 = this.GetIndex2Block(start, true); + i2 += (start >> UTRIE2_SHIFT_2) & UTRIE2_INDEX_2_MASK; + block = this.index2[i2]; + if (this.IsWritableBlock(block)) + { + // already allocated + if (overwrite && block >= UNEWTRIE2_DATA_0800_OFFSET) + { + // We overwrite all values, and it's not a + // protected (ASCII-linear or 2-byte UTF-8) block: + // replace with the repeatBlock. + setRepeatBlock = true; + } + else + { + // !overwrite, or protected block: just write the values into this block + this.FillBlock(block, 0, UTRIE2_DATA_BLOCK_LENGTH, value, this.initialValue, overwrite); + } + } + else if (this.data[block] != value && (overwrite || block == this.dataNullOffset)) + { + // Set the repeatBlock instead of the null block or previous repeat block: + // + // If !isWritableBlock() then all entries in the block have the same value + // because it's the null block or a range block (the repeatBlock from a previous + // call to utrie2_setRange32()). + // No other blocks are used multiple times before compacting. + // + // The null block is the only non-writable block with the initialValue because + // of the repeatBlock initialization above. (If value==initialValue, then + // the repeatBlock will be the null data block.) + // + // We set our repeatBlock if the desired value differs from the block's value, + // and if we overwrite any data or if the data is all initial values + // (which is the same as the block being the null block, see above). + setRepeatBlock = true; + } + + if (setRepeatBlock) + { + if (repeatBlock >= 0) + { + this.SetIndex2Entry(i2, repeatBlock); + } + else + { + // create and set and fill the repeatBlock + repeatBlock = this.GetDataBlock(start, true); + this.WriteBlock(repeatBlock, value); + } + } + + start += UTRIE2_DATA_BLOCK_LENGTH; + } + + if (rest > 0) + { + // set partial block at [last block boundary..limit[ + block = this.GetDataBlock(start, true); + this.FillBlock(block, 0, rest, value, this.initialValue, overwrite); + } + } + + /// + /// Compacts the data and populates an optimized readonly Trie. + /// + /// The . + /// Trie data is too large. + public UnicodeTrie Freeze() + { + int allIndexesLength, i; + if (!this.isCompacted) + { + this.CompactTrie(); + } + + if (this.highStart <= 0x10000) + { + allIndexesLength = UTRIE2_INDEX_1_OFFSET; + } + else + { + allIndexesLength = this.index2Length; + } + + int dataMove = allIndexesLength; + + // are indexLength and dataLength within limits? + if ((allIndexesLength > UTRIE2_MAX_INDEX_LENGTH) // for unshifted indexLength + || ((dataMove + this.dataNullOffset) > 0xffff) // for unshifted dataNullOffset + || ((dataMove + UNEWTRIE2_DATA_0800_OFFSET) > 0xffff) // for unshifted 2-byte UTF-8 index-2 values + || ((dataMove + this.dataLength) > UTRIE2_MAX_DATA_LENGTH)) + { + // for shiftedDataLength + throw new InvalidOperationException("Trie data is too large."); + } + + // calculate the sizes of, and allocate, the index and data arrays + int indexLength = allIndexesLength + this.dataLength; + uint[] data32 = new uint[indexLength]; + + // write the index-2 array values shifted right by UTRIE2_INDEX_SHIFT, after adding dataMove + int destIdx = 0; + for (i = 0; i < UTRIE2_INDEX_2_BMP_LENGTH; i++) + { + data32[destIdx++] = (uint)((this.index2[i] + dataMove) >> UTRIE2_INDEX_SHIFT); + } + + // write UTF-8 2-byte index-2 values, not right-shifted + for (i = 0; i < 0xc2 - 0xc0; i++) + { + // C0..C1 + data32[destIdx++] = (uint)(dataMove + UTRIE2_BAD_UTF8_DATA_OFFSET); + } + + for (; i < 0xe0 - 0xc0; i++) + { + // C2..DF + data32[destIdx++] = (uint)(dataMove + this.index2[i << (6 - UTRIE2_SHIFT_2)]); + } + + if (this.highStart > 0x10000) + { + int index1Length = (this.highStart - 0x10000) >> UTRIE2_SHIFT_1; + int index2Offset = UTRIE2_INDEX_2_BMP_LENGTH + UTRIE2_UTF8_2B_INDEX_2_LENGTH + index1Length; + + // write 16-bit index-1 values for supplementary code points + for (i = 0; i < index1Length; i++) + { + data32[destIdx++] = (uint)(UTRIE2_INDEX_2_OFFSET + this.index1[i + UTRIE2_OMITTED_BMP_INDEX_1_LENGTH]); + } + + // write the index-2 array values for supplementary code points, + // shifted right by INDEX_SHIFT, after adding dataMove + for (i = 0; i < this.index2Length - index2Offset; i++) + { + data32[destIdx++] = (uint)((dataMove + this.index2[index2Offset + i]) >> UTRIE2_INDEX_SHIFT); + } + } + + // write 16-bit data values + for (i = 0; i < this.dataLength; i++) + { + data32[destIdx++] = this.data[i]; + } + + return new UnicodeTrie(data32, this.highStart, this.errorValue); + } + + private uint Get(int c, bool fromLSCP) + { + if (c is < 0 or > 0x10ffff) + { + return this.errorValue; + } + + int i2; + int block; + + if (c >= this.highStart && (!U_IS_LEAD(c) || fromLSCP)) + { + return this.data[this.dataLength - UTRIE2_DATA_GRANULARITY]; + } + + if (U_IS_LEAD(c) && fromLSCP) + { + i2 = UTRIE2_LSCP_INDEX_2_OFFSET - (0xd800 >> UTRIE2_SHIFT_2) + (c >> UTRIE2_SHIFT_2); + } + else + { + i2 = this.index1[c >> UTRIE2_SHIFT_1] + ((c >> UTRIE2_SHIFT_2) & UTRIE2_INDEX_2_MASK); + } + + block = this.index2[i2]; + return this.data[block + (c & UTRIE2_DATA_MASK)]; + } + + private int GetDataBlock(int c, bool forLSCP) + { + int i2 = this.GetIndex2Block(c, forLSCP); + i2 += (c >> UTRIE2_SHIFT_2) & UTRIE2_INDEX_2_MASK; + + int oldBlock = this.index2[i2]; + if (this.IsWritableBlock(oldBlock)) + { + return oldBlock; + } + + // allocate a new data block + int newBlock = this.AllocDataBlock(oldBlock); + this.SetIndex2Entry(i2, newBlock); + return newBlock; + } + + private int GetIndex2Block(int c, bool forLSCP) + { + if (U_IS_LEAD(c) && forLSCP) + { + return UTRIE2_LSCP_INDEX_2_OFFSET; + } + + int i1 = c >> UTRIE2_SHIFT_1; + int i2 = this.index1[i1]; + if (i2 == this.index2NullOffset) + { + i2 = this.AllocIndex2Block(); + this.index1[i1] = i2; + } + + return i2; + } + + /// + /// Is this code point a lead surrogate (U+d800..U+dbff)? + /// + /// The code point. + /// The . + private static bool U_IS_LEAD(int c) => (c & 0xfffffc00) == 0xd800; + + private bool IsWritableBlock(int block) + => block != this.dataNullOffset && this.map[block >> UTRIE2_SHIFT_2] == 1; + + private bool IsInNullBlock(int c, bool forLSCP) + { + int i2, block; + + if (U_IS_LEAD(c) && forLSCP) + { + i2 = UTRIE2_LSCP_INDEX_2_OFFSET - (0xd800 >> UTRIE2_SHIFT_2) + (c >> UTRIE2_SHIFT_2); + } + else + { + i2 = this.index1[c >> UTRIE2_SHIFT_1] + ((c >> UTRIE2_SHIFT_2) & UTRIE2_INDEX_2_MASK); + } + + block = this.index2[i2]; + return block == this.dataNullOffset; + } + + private void SetIndex2Entry(int i2, int block) + { + int oldBlock; + + // increment first, in case block==oldBlock! + ++this.map[block >> UTRIE2_SHIFT_2]; + + oldBlock = this.index2[i2]; + + if (--this.map[oldBlock >> UTRIE2_SHIFT_2] == 0) + { + this.ReleaseDataBlock(oldBlock); + } + + this.index2[i2] = block; + } + + // call when the block's reference counter reaches 0 + private void ReleaseDataBlock(int block) + { + // put this block at the front of the free-block chain + this.map[block >> UTRIE2_SHIFT_2] = -this.firstFreeBlock; + this.firstFreeBlock = block; + } + + private int AllocDataBlock(int copyBlock) + { + int newBlock, newTop; + + if (this.firstFreeBlock != 0) + { + // get the first free block + newBlock = this.firstFreeBlock; + this.firstFreeBlock = -this.map[newBlock >> UTRIE2_SHIFT_2]; + } + else + { + // get a new block from the high end + newBlock = this.dataLength; + newTop = newBlock + UTRIE2_DATA_BLOCK_LENGTH; + if (newTop > this.dataCapacity) + { + // out of memory in the data array. + int capacity; + uint[] newData; + + if (this.dataCapacity < UNEWTRIE2_MEDIUM_DATA_LENGTH) + { + capacity = UNEWTRIE2_MEDIUM_DATA_LENGTH; + } + else if (this.dataCapacity < UNEWTRIE2_MAX_DATA_LENGTH) + { + capacity = UNEWTRIE2_MAX_DATA_LENGTH; + } + else + { + // Should never occur. + // Either UNEWTRIE2_MAX_DATA_LENGTH is incorrect, + // or the code writes more values than should be possible. + throw new InvalidOperationException(nameof(capacity)); + } + + newData = new uint[capacity]; + + Array.Copy(this.data, newData, this.dataLength); + this.data = newData; + this.dataCapacity = capacity; + } + + this.dataLength = newTop; + } + + Array.Copy(this.data, copyBlock, this.data, newBlock, UTRIE2_DATA_BLOCK_LENGTH); + this.map[newBlock >> UTRIE2_SHIFT_2] = 0; + return newBlock; + } + + private int AllocIndex2Block() + { + int newBlock, newTop; + + newBlock = this.index2Length; + newTop = newBlock + UTRIE2_INDEX_2_BLOCK_LENGTH; + if (newTop > this.index2.Length) + { + // Should never occur. + // Either UTRIE2_MAX_BUILD_TIME_INDEX_LENGTH is incorrect, + // or the code writes more values than should be possible. + throw new InvalidOperationException(nameof(newTop)); + } + + this.index2Length = newTop; + Array.Copy(this.index2, this.index2NullOffset, this.index2, newBlock, UTRIE2_INDEX_2_BLOCK_LENGTH); + + return newBlock; + } + + private int FindSameIndex2Block(int index2Length, int otherBlock) + { + // ensure that we do not even partially get past index2Length + index2Length -= UTRIE2_INDEX_2_BLOCK_LENGTH; + + for (int block = 0; block <= index2Length; ++block) + { + if (Equal(this.index2, block, otherBlock, UTRIE2_INDEX_2_BLOCK_LENGTH)) + { + return block; + } + } + + return -1; + } + + private int FindSameDataBlock(int dataLength, int otherBlock, int blockLength) + { + // ensure that we do not even partially get past dataLength + dataLength -= blockLength; + + for (int block = 0; block <= dataLength; block += UTRIE2_DATA_GRANULARITY) + { + if (Equal(this.data, block, otherBlock, blockLength)) + { + return block; + } + } + + return -1; + } + + // Find the start of the last range in the trie by enumerating backward. + // Indexes for supplementary code points higher than this will be omitted. + private int FindHighStart(uint highValue) + { + uint[] data32; + + uint value, initialValue; + int c, prev; + int i1, i2, j, i2Block, prevI2Block, index2NullOffset, block, prevBlock, nullBlock; + + data32 = this.data; + initialValue = this.initialValue; + + index2NullOffset = this.index2NullOffset; + nullBlock = this.dataNullOffset; + + /* set variables for previous range */ + if (highValue == initialValue) + { + prevI2Block = index2NullOffset; + prevBlock = nullBlock; + } + else + { + prevI2Block = -1; + prevBlock = -1; + } + + prev = 0x110000; + + // enumerate index-2 blocks + i1 = UNEWTRIE2_INDEX_1_LENGTH; + c = prev; + while (c > 0) + { + i2Block = this.index1[--i1]; + if (i2Block == prevI2Block) + { + // the index-2 block is the same as + // the previous one, and filled with highValue + c -= UTRIE2_CP_PER_INDEX_1_ENTRY; + continue; + } + + prevI2Block = i2Block; + if (i2Block == index2NullOffset) + { + // this is the null index-2 block + if (highValue != initialValue) + { + return c; + } + + c -= UTRIE2_CP_PER_INDEX_1_ENTRY; + } + else + { + // enumerate data blocks for one index-2 block + for (i2 = UTRIE2_INDEX_2_BLOCK_LENGTH; i2 > 0;) + { + block = this.index2[i2Block + --i2]; + if (block == prevBlock) + { + // the block is the same as the previous one, and filled with highValue + c -= UTRIE2_DATA_BLOCK_LENGTH; + continue; + } + + prevBlock = block; + if (block == nullBlock) + { + // this is the null data block + if (highValue != initialValue) + { + return c; + } + + c -= UTRIE2_DATA_BLOCK_LENGTH; + } + else + { + for (j = UTRIE2_DATA_BLOCK_LENGTH; j > 0;) + { + value = data32[block + --j]; + if (value != highValue) + { + return c; + } + + --c; + } + } + } + } + } + + // deliver last range + return 0; + } + + // initialValue is ignored if overwrite=TRUE + private void FillBlock(int block, int start, int limit, uint value, uint initialValue, bool overwrite) + { + int pLimit = block + limit; + block += start; + if (overwrite) + { + while (block < pLimit) + { + this.data[block++] = value; + } + } + else + { + while (block < pLimit) + { + if (this.data[block] == initialValue) + { + this.data[block] = value; + } + + ++block; + } + } + } + + private void WriteBlock(int block, uint value) + { + int limit = block + UTRIE2_DATA_BLOCK_LENGTH; + while (block < limit) + { + this.data[block++] = value; + } + } + + private void CompactTrie() + { + // find highStart and round it up + uint highValue = this.Get(0x10ffff); + int localHighStart = this.FindHighStart(highValue); + localHighStart = (localHighStart + (UTRIE2_CP_PER_INDEX_1_ENTRY - 1)) & ~(UTRIE2_CP_PER_INDEX_1_ENTRY - 1); + if (localHighStart == 0x110000) + { + highValue = this.errorValue; + } + + // Set highStart only after Get(trie, highStart). + // Otherwise Get(highStart) would try to read the highValue. + this.highStart = localHighStart; + + if (localHighStart < 0x110000) + { + // Blank out [highStart..10ffff] to release associated data blocks. + int suppHighStart = this.highStart <= 0x10000 ? 0x10000 : this.highStart; + this.SetRange(suppHighStart, 0x10ffff, this.initialValue, true); + } + + this.CompactData(); + if (this.highStart > 0x10000) + { + this.CompactIndex2(); + } + + // Store the highValue in the data array and round up the dataLength. + // Must be done after compactData() because that assumes that dataLength + // is a multiple of UTRIE2_DATA_BLOCK_LENGTH. + this.data[this.dataLength++] = highValue; + while ((this.dataLength & (UTRIE2_DATA_GRANULARITY - 1)) != 0) + { + this.data[this.dataLength++] = this.initialValue; + } + + this.isCompacted = true; + } + + // Compact a build-time trie. + // + // The compaction + // - removes blocks that are identical with earlier ones + // - overlaps adjacent blocks as much as possible (if overlap==TRUE) + // - moves blocks in steps of the data granularity + // - moves and overlaps blocks that overlap with multiple values in the overlap region + // + // It does not + // - try to move and overlap blocks that are not already adjacent + private void CompactData() + { + int start, newStart, movedStart; + int blockLength, overlap; + int i, mapIndex, blockCount; + + // do not compact linear-ASCII data + newStart = UTRIE2_DATA_START_OFFSET; + for (start = 0, i = 0; start < newStart; start += UTRIE2_DATA_BLOCK_LENGTH, ++i) + { + this.map[i] = start; + } + + // Start with a block length of 64 for 2-byte UTF-8, + // then switch to UTRIE2_DATA_BLOCK_LENGTH. + blockLength = 64; + blockCount = blockLength >> UTRIE2_SHIFT_2; + for (start = newStart; start < this.dataLength;) + { + // start: index of first entry of current block + // newStart: index where the current block is to be moved + // (right after current end of already-compacted data) + if (start == UNEWTRIE2_DATA_0800_OFFSET) + { + blockLength = UTRIE2_DATA_BLOCK_LENGTH; + blockCount = 1; + } + + // skip blocks that are not used + if (this.map[start >> UTRIE2_SHIFT_2] <= 0) + { + // advance start to the next block + start += blockLength; + + // leave newStart with the previous block! + continue; + } + + // search for an identical block + if ((movedStart = this.FindSameDataBlock(newStart, start, blockLength)) >= 0) + { + // found an identical block, set the other block's index value for the current block + for (i = blockCount, mapIndex = start >> UTRIE2_SHIFT_2; i > 0; --i) + { + this.map[mapIndex++] = movedStart; + movedStart += UTRIE2_DATA_BLOCK_LENGTH; + } + + // advance start to the next block + start += blockLength; + + // leave newStart with the previous block! + continue; + } + + // see if the beginning of this block can be overlapped with the end of the previous block + // look for maximum overlap (modulo granularity) with the previous, adjacent block + overlap = blockLength - UTRIE2_DATA_GRANULARITY; + while (overlap > 0 && !Equal(this.data, newStart - overlap, start, overlap)) + { + overlap -= UTRIE2_DATA_GRANULARITY; + } + + if (overlap > 0 || newStart < start) + { + // some overlap, or just move the whole block + movedStart = newStart - overlap; + for (i = blockCount, mapIndex = start >> UTRIE2_SHIFT_2; i > 0; --i) + { + this.map[mapIndex++] = movedStart; + movedStart += UTRIE2_DATA_BLOCK_LENGTH; + } + + // move the non-overlapping indexes to their new positions + start += overlap; + for (i = blockLength - overlap; i > 0; --i) + { + this.data[newStart++] = this.data[start++]; + } + } + else + { + // no overlap && newStart==start + for (i = blockCount, mapIndex = start >> UTRIE2_SHIFT_2; i > 0; --i) + { + this.map[mapIndex++] = start; + start += UTRIE2_DATA_BLOCK_LENGTH; + } + + newStart = start; + } + } + + // now adjust the index-2 table + for (i = 0; i < this.index2Length; ++i) + { + if (i == UNEWTRIE2_INDEX_GAP_OFFSET) + { + // Gap indexes are invalid (-1). Skip over the gap. + i += UNEWTRIE2_INDEX_GAP_LENGTH; + } + + this.index2[i] = this.map[this.index2[i] >> UTRIE2_SHIFT_2]; + } + + this.dataNullOffset = this.map[this.dataNullOffset >> UTRIE2_SHIFT_2]; + + // ensure dataLength alignment + while ((newStart & (UTRIE2_DATA_GRANULARITY - 1)) != 0) + { + this.data[newStart++] = this.initialValue; + } + + this.dataLength = newStart; + } + + private void CompactIndex2() + { + int i, start, newStart, movedStart, overlap; + + // do not compact linear-BMP index-2 blocks + newStart = UTRIE2_INDEX_2_BMP_LENGTH; + for (start = 0, i = 0; start < newStart; start += UTRIE2_INDEX_2_BLOCK_LENGTH, ++i) + { + this.map[i] = start; + } + + // Reduce the index table gap to what will be needed at runtime. + newStart += UTRIE2_UTF8_2B_INDEX_2_LENGTH + ((this.highStart - 0x10000) >> UTRIE2_SHIFT_1); + + for (start = UNEWTRIE2_INDEX_2_NULL_OFFSET; start < this.index2Length;) + { + // start: index of first entry of current block + // newStart: index where the current block is to be moved + // (right after current end of already-compacted data) + // + // search for an identical block + if ((movedStart = this.FindSameIndex2Block(newStart, start)) >= 0) + { + // found an identical block, set the other block's index value for the current block + this.map[start >> UTRIE2_SHIFT_1_2] = movedStart; + + // advance start to the next block + start += UTRIE2_INDEX_2_BLOCK_LENGTH; + + // leave newStart with the previous block! + continue; + } + + // see if the beginning of this block can be overlapped with the end of the previous block + // look for maximum overlap with the previous, adjacent block + for (overlap = UTRIE2_INDEX_2_BLOCK_LENGTH - 1; + overlap > 0 && !Equal(this.index2, newStart - overlap, start, overlap); + --overlap) + { + } + + if (overlap > 0 || newStart < start) + { + // some overlap, or just move the whole block + this.map[start >> UTRIE2_SHIFT_1_2] = newStart - overlap; + + // move the non-overlapping indexes to their new positions + start += overlap; + for (i = UTRIE2_INDEX_2_BLOCK_LENGTH - overlap; i > 0; --i) + { + this.index2[newStart++] = this.index2[start++]; + } + } + else + { + // no overlap && newStart==start + this.map[start >> UTRIE2_SHIFT_1_2] = start; + start += UTRIE2_INDEX_2_BLOCK_LENGTH; + newStart = start; + } + } + + // now adjust the index-1 table + for (i = 0; i < UNEWTRIE2_INDEX_1_LENGTH; ++i) + { + this.index1[i] = this.map[this.index1[i] >> UTRIE2_SHIFT_1_2]; + } + + this.index2NullOffset = this.map[this.index2NullOffset >> UTRIE2_SHIFT_1_2]; + + // Ensure data table alignment: + // Needs to be granularity-aligned for 16-bit trie + // (so that dataMove will be down-shiftable), + // and 2-aligned for uint32_t data. + while ((newStart & ((UTRIE2_DATA_GRANULARITY - 1) | 1)) != 0) + { + // Arbitrary value: 0x3fffc not possible for real data. + this.index2[newStart++] = 0xffff << UTRIE2_INDEX_SHIFT; + } + + this.index2Length = newStart; + } + + private static bool Equal(uint[] a, int s, int t, int length) + { + for (int i = 0; i < length; i++) + { + if (a[s + i] != a[t + i]) + { + return false; + } + } + + return true; + } + + private static bool Equal(int[] a, int s, int t, int length) + { + for (int i = 0; i < length; i++) + { + if (a[s + i] != a[t + i]) + { + return false; + } + } + + return true; + } + } +} diff --git a/SixLabors.Fonts/Unicode/UnicodeUtility.cs b/SixLabors.Fonts/Unicode/UnicodeUtility.cs new file mode 100644 index 0000000..31bc54a --- /dev/null +++ b/SixLabors.Fonts/Unicode/UnicodeUtility.cs @@ -0,0 +1,549 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts.Unicode { + internal static class UnicodeUtility + { + /// + /// Returns if is an ASCII + /// character ([ U+0000..U+007F ]). + /// + /// + /// Per http://www.unicode.org/glossary/#ASCII, ASCII is only U+0000..U+007F. + /// + /// The codepoint to test. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAsciiCodePoint(uint value) => value <= 0x7Fu; + + /// + /// Returns if is in the + /// Basic Multilingual Plane (BMP). + /// + /// The codepoint to test. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsBmpCodePoint(uint value) => value <= 0xFFFFu; + + /// + /// Gets the codepoint value representing the vertical mirror for this instance. + ///
+ /// + ///
+ /// + ///
+ /// The codepoint to test. + /// + /// The representing the mirror or 0u if not found. + /// + public static uint GetVerticalMirror(uint value) + { + switch (value >> 8) + { + case 0x20: + switch (value) + { + case 0x2013u: + return 0xfe32u; // EN DASH + case 0x2014u: + return 0xfe31u; // EM DASH + case 0x2025u: + return 0xfe30u; // TWO DOT LEADER + case 0x2026u: + return 0xfe19u; // HORIZONTAL ELLIPSIS + } + + break; + case 0x30: + switch (value) + { + case 0x3001u: + return 0xfe11u; // IDEOGRAPHIC COMMA + case 0x3002u: + return 0xfe12u; // IDEOGRAPHIC FULL STOP + case 0x3008u: + return 0xfe3fu; // LEFT ANGLE BRACKET + case 0x3009u: + return 0xfe40u; // RIGHT ANGLE BRACKET + case 0x300au: + return 0xfe3du; // LEFT DOUBLE ANGLE BRACKET + case 0x300bu: + return 0xfe3eu; // RIGHT DOUBLE ANGLE BRACKET + case 0x300cu: + return 0xfe41u; // LEFT CORNER BRACKET + case 0x300du: + return 0xfe42u; // RIGHT CORNER BRACKET + case 0x300eu: + return 0xfe43u; // LEFT WHITE CORNER BRACKET + case 0x300fu: + return 0xfe44u; // RIGHT WHITE CORNER BRACKET + case 0x3010u: + return 0xfe3bu; // LEFT BLACK LENTICULAR BRACKET + case 0x3011u: + return 0xfe3cu; // RIGHT BLACK LENTICULAR BRACKET + case 0x3014u: + return 0xfe39u; // LEFT TORTOISE SHELL BRACKET + case 0x3015u: + return 0xfe3au; // RIGHT TORTOISE SHELL BRACKET + case 0x3016u: + return 0xfe17u; // LEFT WHITE LENTICULAR BRACKET + case 0x3017u: + return 0xfe18u; // RIGHT WHITE LENTICULAR BRACKET + } + + break; + case 0xfe: + switch (value) + { + case 0xfe4fu: + return 0xfe34u; // WAVY LOW LINE + } + + break; + case 0xff: + switch (value) + { + case 0xff01u: + return 0xfe15u; // FULLWIDTH EXCLAMATION MARK + case 0xff08u: + return 0xfe35u; // FULLWIDTH LEFT PARENTHESIS + case 0xff09u: + return 0xfe36u; // FULLWIDTH RIGHT PARENTHESIS + case 0xff0cu: + return 0xfe10u; // FULLWIDTH COMMA + case 0xff1au: + return 0xfe13u; // FULLWIDTH COLON + case 0xff1bu: + return 0xfe14u; // FULLWIDTH SEMICOLON + case 0xff1fu: + return 0xfe16u; // FULLWIDTH QUESTION MARK + case 0xff3bu: + return 0xfe47u; // FULLWIDTH LEFT SQUARE BRACKET + case 0xff3du: + return 0xfe48u; // FULLWIDTH RIGHT SQUARE BRACKET + case 0xff3fu: + return 0xfe33u; // FULLWIDTH LOW LINE + case 0xff5bu: + return 0xfe37u; // FULLWIDTH LEFT CURLY BRACKET + case 0xff5du: + return 0xfe38u; // FULLWIDTH RIGHT CURLY BRACKET + } + + break; + } + + return 0u; + } + + /// + /// Returns if is a Default Ignorable Code Point. + /// + /// The codepoint value. + /// + /// + /// + /// + public static bool IsDefaultIgnorableCodePoint(uint value) + { + // SOFT HYPHEN + if (value == 0x00AD) + { + return true; + } + + // COMBINING GRAPHEME JOINER + if (value == 0x034F) + { + return true; + } + + // COMBINING GRAPHEME JOINER + if (value == 0x061C) + { + return true; + } + + // HANGUL CHOSEONG FILLER..HANGUL JUNGSEONG FILLER + if (IsInRangeInclusive(value, 0x115F, 0x1160)) + { + return true; + } + + // KHMER VOWEL INHERENT AQ..KHMER VOWEL INHERENT AA + if (IsInRangeInclusive(value, 0x17B4, 0x17B5)) + { + return true; + } + + // MONGOLIAN FREE VARIATION SELECTOR ONE..MONGOLIAN FREE VARIATION SELECTOR THREE + if (IsInRangeInclusive(value, 0x180B, 0x180D)) + { + return true; + } + + // MONGOLIAN VOWEL SEPARATOR + if (value == 0x180E) + { + return true; + } + + // MONGOLIAN FREE VARIATION SELECTOR FOUR + if (value == 0x180F) + { + return true; + } + + // ZERO WIDTH SPACE..RIGHT-TO-LEFT MARK + if (IsInRangeInclusive(value, 0x200B, 0x200F)) + { + return true; + } + + // LEFT-TO-RIGHT EMBEDDING..RIGHT-TO-LEFT OVERRIDE + if (IsInRangeInclusive(value, 0x202A, 0x202E)) + { + return true; + } + + // WORD JOINER..INVISIBLE PLUS + if (IsInRangeInclusive(value, 0x2060, 0x2064)) + { + return true; + } + + // + if (value == 0x2065) + { + return true; + } + + // LEFT-TO-RIGHT ISOLATE..NOMINAL DIGIT SHAPES + if (IsInRangeInclusive(value, 0x2066, 0x206F)) + { + return true; + } + + // HANGUL FILLER + if (value == 0x3164) + { + return true; + } + + // VARIATION SELECTOR-1..VARIATION SELECTOR-16 + if (IsInRangeInclusive(value, 0xFE00, 0xFE0F)) + { + return true; + } + + // ZERO WIDTH NO-BREAK SPACE + if (value == 0xFEFF) + { + return true; + } + + // HALFWIDTH HANGUL FILLER + if (value == 0xFFA0) + { + return true; + } + + // .. + if (IsInRangeInclusive(value, 0xFFF0, 0xFFF8)) + { + return true; + } + + // SHORTHAND FORMAT LETTER OVERLAP..SHORTHAND FORMAT UP STEP + if (IsInRangeInclusive(value, 0x1BCA0, 0x1BCA3)) + { + return true; + } + + // MUSICAL SYMBOL BEGIN BEAM..MUSICAL SYMBOL END PHRASE + if (IsInRangeInclusive(value, 0x1D173, 0x1D17A)) + { + return true; + } + + // + if (value == 0xE0000) + { + return true; + } + + // LANGUAGE TAG + if (value == 0xE0001) + { + return true; + } + + // .. + if (IsInRangeInclusive(value, 0xE0002, 0xE001F)) + { + return true; + } + + // TAG SPACE..CANCEL TAG + if (IsInRangeInclusive(value, 0xE0020, 0xE007F)) + { + return true; + } + + // .. + if (IsInRangeInclusive(value, 0xE0080, 0xE00FF)) + { + return true; + } + + // VARIATION SELECTOR-17..VARIATION SELECTOR-256 + if (IsInRangeInclusive(value, 0xE0100, 0xE01EF)) + { + return true; + } + + // .. + if (IsInRangeInclusive(value, 0xE01F0, 0xE0FFF)) + { + return true; + } + + return false; + } + + /// + /// Gets a value indicating whether the specified code point should be rendered as a white space only. + /// + /// The code point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ShouldRenderWhiteSpaceOnly(in CodePoint codePoint) + { + if (CodePoint.IsWhiteSpace(codePoint)) + { + return true; + } + + // Note: While U+115F, U+1160, U+3164 and U+FFA0 are Default_Ignorable, + // we do NOT want to hide them, as the way Uniscribe has implemented them + // is with regular spacing glyphs, and that's the way fonts are made to work. + // As such, we make exceptions for those four. + // Also ignoring U+1BCA0..1BCA3. https://github.com/harfbuzz/harfbuzz/issues/503 + uint value = (uint)codePoint.Value; + if (value is 0x115F or 0x1160 or 0x3164 or 0xFFA0) + { + return true; + } + + if (IsInRangeInclusive(value, 0x1BCA0, 0x1BCA3)) + { + return true; + } + + return false; + } + + /// + /// Gets a value indicating whether the specified code point should not be rendered. + /// + /// The code point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ShouldNotBeRendered(in CodePoint codePoint) + => CodePoint.IsNewLine(codePoint) || (IsDefaultIgnorableCodePoint((uint)codePoint.Value) && !ShouldRenderWhiteSpaceOnly(codePoint)); + + /// + /// Returns the Unicode plane (0 through 16, inclusive) which contains this code point. + /// + /// The code point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetPlane(uint codePoint) + { + DebugAssertIsValidCodePoint(codePoint); + + return (int)(codePoint >> 16); + } + + /// + /// Given a Unicode scalar value, gets the number of UTF-16 code units required to represent this value. + /// + /// The code point. + public static int GetUtf16SequenceLength(uint codePoint) + { + DebugAssertIsValidCodePoint(codePoint); + + codePoint -= 0x10000; // if value < 0x10000, high byte = 0xFF; else high byte = 0x00 + codePoint += 2 << 24; // if value < 0x10000, high byte = 0x01; else high byte = 0x02 + codePoint >>= 24; // shift high byte down + return (int)codePoint; // and return it + } + + /// + /// Given a Unicode scalar value, gets the number of UTF-8 code units required to represent this value. + /// + /// The code point. + public static int GetUtf8SequenceLength(uint codePoint) + { + DebugAssertIsValidCodePoint(codePoint); + + // The logic below can handle all valid scalar values branchlessly. + // It gives generally good performance across all inputs, and on x86 + // it's only six instructions: lea, sar, xor, add, shr, lea. + + // 'a' will be -1 if input is < 0x800; else 'a' will be 0 + // => 'a' will be -1 if input is 1 or 2 UTF-8 code units; else 'a' will be 0 + int a = ((int)codePoint - 0x0800) >> 31; + + // The number of UTF-8 code units for a given scalar is as follows: + // - U+0000..U+007F => 1 code unit + // - U+0080..U+07FF => 2 code units + // - U+0800..U+FFFF => 3 code units + // - U+10000+ => 4 code units + // + // If we XOR the incoming scalar with 0xF800, the chart mutates: + // - U+0000..U+F7FF => 3 code units + // - U+F800..U+F87F => 1 code unit + // - U+F880..U+FFFF => 2 code units + // - U+10000+ => 4 code units + // + // Since the 1- and 3-code unit cases are now clustered, they can + // both be checked together very cheaply. + codePoint ^= 0xF800u; + codePoint -= 0xF880u; // if scalar is 1 or 3 code units, high byte = 0xFF; else high byte = 0x00 + codePoint += 4 << 24; // if scalar is 1 or 3 code units, high byte = 0x03; else high byte = 0x04 + codePoint >>= 24; // shift high byte down + + // Final return value: + // - U+0000..U+007F => 3 + (-1) * 2 = 1 + // - U+0080..U+07FF => 4 + (-1) * 2 = 2 + // - U+0800..U+FFFF => 3 + ( 0) * 2 = 3 + // - U+10000+ => 4 + ( 0) * 2 = 4 + return (int)codePoint + (a * 2); + } + + /// + /// Returns if is a valid Unicode code + /// point, i.e., is in [ U+0000..U+10FFFF ], inclusive. + /// + /// The code point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsValidCodePoint(uint codePoint) => codePoint <= 0x10FFFFu; + + /// + /// Returns if is a UTF-16 high surrogate code point, + /// i.e., is in [ U+D800..U+DBFF ], inclusive. + /// + /// The value to test. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHighSurrogateCodePoint(uint value) + => IsInRangeInclusive(value, 0xD800u, 0xDBFFu); + + /// + /// Returns if is a UTF-16 low surrogate code point, + /// i.e., is in [ U+DC00..U+DFFF ], inclusive. + /// + /// The value to test. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLowSurrogateCodePoint(uint value) + => IsInRangeInclusive(value, 0xDC00u, 0xDFFFu); + + /// + /// Returns if is a UTF-16 surrogate code point, + /// i.e., is in [ U+D800..U+DFFF ], inclusive. + /// + /// The value to test. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsSurrogateCodePoint(uint value) + => IsInRangeInclusive(value, 0xD800u, 0xDFFFu); + + /// + /// Returns if is between + /// and , inclusive. + /// + /// The value to test. + /// The lower bound. + /// The upper bound. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBound) + => (value - lowerBound) <= (upperBound - lowerBound); + + /// + /// Returns a Unicode scalar value from two code points representing a UTF-16 surrogate pair. + /// + /// The high surrogate code point. + /// The low surrogate code point. + public static uint GetScalarFromUtf16SurrogatePair(uint highSurrogateCodePoint, uint lowSurrogateCodePoint) + { + DebugAssertIsHighSurrogateCodePoint(highSurrogateCodePoint); + DebugAssertIsLowSurrogateCodePoint(lowSurrogateCodePoint); + + // This calculation comes from the Unicode specification, Table 3-5. + // Need to remove the D800 marker from the high surrogate and the DC00 marker from the low surrogate, + // then fix up the "wwww = uuuuu - 1" section of the bit distribution. The code is written as below + // to become just two instructions: shl, lea. + return (highSurrogateCodePoint << 10) + lowSurrogateCodePoint - ((0xD800U << 10) + 0xDC00U - (1 << 16)); + } + + /// + /// Decomposes an astral Unicode code point into UTF-16 high and low surrogate code units. + /// + /// The Unicode code point. + /// The high surrogate code point. + /// The low surrogate code point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void GetUtf16SurrogatesFromSupplementaryPlaneCodePoint(uint value, out char highSurrogateCodePoint, out char lowSurrogateCodePoint) + { + DebugAssertIsValidSupplementaryPlaneCodePoint(value); + + // This calculation comes from the Unicode specification, Table 3-5. + highSurrogateCodePoint = (char)((value + ((0xD800u - 0x40u) << 10)) >> 10); + lowSurrogateCodePoint = (char)((value & 0x3FFu) + 0xDC00u); + } + + [Conditional("DEBUG")] + internal static void DebugAssertIsHighSurrogateCodePoint(uint codePoint) + { + if (!IsHighSurrogateCodePoint(codePoint)) + { + Debug.Fail($"The value {ToHexString(codePoint)} is not a valid UTF-16 high surrogate code point."); + } + } + + [Conditional("DEBUG")] + internal static void DebugAssertIsLowSurrogateCodePoint(uint codePoint) + { + if (!IsLowSurrogateCodePoint(codePoint)) + { + Debug.Fail($"The value {ToHexString(codePoint)} is not a valid UTF-16 low surrogate code point."); + } + } + + [Conditional("DEBUG")] + internal static void DebugAssertIsValidCodePoint(uint codePoint) + { + if (!IsValidCodePoint(codePoint)) + { + Debug.Fail($"The value {ToHexString(codePoint)} is not a valid Unicode code point value."); + } + } + + [Conditional("DEBUG")] + internal static void DebugAssertIsValidSupplementaryPlaneCodePoint(uint codePoint) + { + if (!IsValidCodePoint(codePoint) || IsBmpCodePoint(codePoint)) + { + Debug.Fail($"The value {ToHexString(codePoint)} is not a valid supplementary plane Unicode code point value."); + } + } + + /// + /// Formats a code point as the hex string "U+XXXX". + /// + /// + /// The input value doesn't have to be a real code point in the Unicode codespace. It can be any integer. + /// + /// The code point. + internal static string ToHexString(uint codePoint) => FormattableString.Invariant($"U+{codePoint:X4}"); + } +} diff --git a/SixLabors.Fonts/Unicode/VerticalOrientationType.cs b/SixLabors.Fonts/Unicode/VerticalOrientationType.cs new file mode 100644 index 0000000..dc68af3 --- /dev/null +++ b/SixLabors.Fonts/Unicode/VerticalOrientationType.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode { + /// + /// Unicode Vertical_Orientation property values. + /// + /// + /// + /// These values are used when laying out text vertically. They describe the default + /// orientation of the character's code chart glyph and whether a vertical alternate + /// glyph should be used when the font supplies one. + /// + public enum VerticalOrientationType + { + /// + /// Upright (U): displayed upright with the same orientation used in the code charts. + /// + Upright, + + /// + /// Rotated (R): displayed sideways, rotated 90 degrees clockwise from the code charts. + /// + Rotate, + + /// + /// Transformed upright (Tu): normally uses a vertical alternate glyph, falling back to upright. + /// + TransformUpright, + + /// + /// Transformed rotated (Tr): normally uses a vertical alternate glyph, falling back to rotated. + /// + TransformRotate + } +} diff --git a/SixLabors.Fonts/Unicode/WordBreakClass.cs b/SixLabors.Fonts/Unicode/WordBreakClass.cs new file mode 100644 index 0000000..01752e9 --- /dev/null +++ b/SixLabors.Fonts/Unicode/WordBreakClass.cs @@ -0,0 +1,106 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode { + /// + /// Unicode Word_Break property values. + /// + /// + public enum WordBreakClass : uint + { + /// + /// U+000D CARRIAGE RETURN (CR). + /// + CarriageReturn = 0, + + /// + /// U+000A LINE FEED (LF). + /// + LineFeed = 1, + + /// + /// Newline characters other than CR and LF. + /// + Newline = 2, + + /// + /// Extending code points that are ignored by most word boundary rules. + /// + Extend = 3, + + /// + /// U+200D ZERO WIDTH JOINER. + /// + ZeroWidthJoiner = 4, + + /// + /// Regional indicator symbols used to build flag emoji pairs. + /// + RegionalIndicator = 5, + + /// + /// Format characters that are ignored by most word boundary rules. + /// + Format = 6, + + /// + /// Katakana characters. + /// + Katakana = 7, + + /// + /// Hebrew letters. + /// + HebrewLetter = 8, + + /// + /// Alphabetic letters. + /// + ALetter = 9, + + /// + /// Single quote. + /// + SingleQuote = 10, + + /// + /// Double quote. + /// + DoubleQuote = 11, + + /// + /// Mid-letter and mid-number punctuation. + /// + MidNumLet = 12, + + /// + /// Mid-letter punctuation. + /// + MidLetter = 13, + + /// + /// Mid-number punctuation. + /// + MidNum = 14, + + /// + /// Numeric characters. + /// + Numeric = 15, + + /// + /// Connector characters that extend letters, numbers, and Katakana. + /// + ExtendNumLet = 16, + + /// + /// Horizontal whitespace segmented as word-segmentation space. + /// + WSegSpace = 17, + + /// + /// Other. + /// + Other = 0xFF + } +} diff --git a/SixLabors.Fonts/Unicode/WordSegment.cs b/SixLabors.Fonts/Unicode/WordSegment.cs new file mode 100644 index 0000000..30b9c60 --- /dev/null +++ b/SixLabors.Fonts/Unicode/WordSegment.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.Fonts.Unicode { + /// + /// Represents a segment between two Unicode word boundaries. + /// + public readonly ref struct WordSegment + { + /// + /// Initializes a new instance of the struct. + /// + /// The UTF-16 span containing the word-boundary segment. + /// The UTF-16 offset of the segment in the original source. + /// The code point offset of the segment in the original source. + /// The number of Unicode scalar values in the segment. + public WordSegment( + ReadOnlySpan span, + int utf16Offset, + int codePointOffset, + int codePointCount) + { + this.Span = span; + this.Utf16Offset = utf16Offset; + this.Utf16Length = span.Length; + this.CodePointOffset = codePointOffset; + this.CodePointCount = codePointCount; + } + + /// + /// Gets the UTF-16 span containing the word-boundary segment. + /// + public ReadOnlySpan Span { get; } + + /// + /// Gets the UTF-16 offset of the segment in the original source. + /// + public int Utf16Offset { get; } + + /// + /// Gets the UTF-16 length of the segment. + /// + public int Utf16Length { get; } + + /// + /// Gets the code point offset of the segment in the original source. + /// + public int CodePointOffset { get; } + + /// + /// Gets the number of Unicode scalar values in the segment. + /// + public int CodePointCount { get; } + } +} diff --git a/SixLabors.Fonts/Utilities/EncodingIDExtensions.cs b/SixLabors.Fonts/Utilities/EncodingIDExtensions.cs new file mode 100644 index 0000000..ffe6cec --- /dev/null +++ b/SixLabors.Fonts/Utilities/EncodingIDExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Text; +using SixLabors.Fonts.WellKnownIds; + +namespace SixLabors.Fonts.Utilities { + /// + /// Converts encoding ID to TextEncoding + /// + internal static class EncodingIDExtensions + { + /// + /// Converts encoding ID to TextEncoding + /// + /// The identifier. + /// the encoding for this encoding ID + public static Encoding AsEncoding(this EncodingIDs id) + { + switch (id) + { + case EncodingIDs.Unicode11: + case EncodingIDs.Unicode2: + return Encoding.BigEndianUnicode; + default: + return Encoding.UTF8; + } + } + } +} diff --git a/SixLabors.Fonts/Utilities/StringLoader.cs b/SixLabors.Fonts/Utilities/StringLoader.cs new file mode 100644 index 0000000..153effe --- /dev/null +++ b/SixLabors.Fonts/Utilities/StringLoader.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Text; + +namespace SixLabors.Fonts.Utilities { + [DebuggerDisplay("Offset: {Offset}, Length: {Length}, Value: {Value}")] + internal class StringLoader + { + public StringLoader(ushort length, ushort offset, Encoding encoding) + { + this.Length = length; + this.Offset = offset; + this.Encoding = encoding; + this.Value = string.Empty; + } + + public ushort Length { get; } + + public ushort Offset { get; } + + public string Value { get; private set; } + + public Encoding Encoding { get; } + + public static StringLoader Create(BigEndianBinaryReader reader) + => Create(reader, Encoding.BigEndianUnicode); + + public static StringLoader Create(BigEndianBinaryReader reader, Encoding encoding) + => new StringLoader(reader.ReadUInt16(), reader.ReadUInt16(), encoding); + + public void LoadValue(BigEndianBinaryReader reader) + => this.Value = reader.ReadString(this.Length, this.Encoding).Replace("\0", string.Empty); + } +} diff --git a/SixLabors.Fonts/VerticalAlignment.cs b/SixLabors.Fonts/VerticalAlignment.cs new file mode 100644 index 0000000..91c38f4 --- /dev/null +++ b/SixLabors.Fonts/VerticalAlignment.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Vertical alignment modes. + /// + public enum VerticalAlignment + { + /// + /// Aligns downward from the top. + /// + Top = 0, + + /// + /// Aligns text up and down from the middle. + /// + Center = 1, + + /// + /// Aligns text upwards from the bottom. + /// + Bottom = 2, + + /// + /// Aligns text to the baseline. + /// + Baseline = 3 + } +} diff --git a/SixLabors.Fonts/VerticalMetrics.cs b/SixLabors.Fonts/VerticalMetrics.cs new file mode 100644 index 0000000..b7f0ed1 --- /dev/null +++ b/SixLabors.Fonts/VerticalMetrics.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Represent the metrics of a font face specific to vertical text. + /// + public class VerticalMetrics : IMetricsHeader + { + /// + public short Ascender { get; internal set; } + + /// + public short Descender { get; internal set; } + + /// + public short LineGap { get; internal set; } + + /// + public short LineHeight { get; internal set; } + + /// + public short AdvanceWidthMax { get; internal set; } + + /// + public short AdvanceHeightMax { get; internal set; } + + /// + /// Gets or sets a value indicating whether the metrics have been synthesized. + /// + internal bool Synthesized { get; set; } + } +} diff --git a/SixLabors.Fonts/WellKnownIds/EncodingIDs.cs b/SixLabors.Fonts/WellKnownIds/EncodingIDs.cs new file mode 100644 index 0000000..c45587f --- /dev/null +++ b/SixLabors.Fonts/WellKnownIds/EncodingIDs.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.WellKnownIds { + /// + /// Encoding IDS + /// + internal enum EncodingIDs : ushort + { + /// + /// Unicode 1.0 semantics + /// + Unicode1 = 0, + + /// + /// Unicode 1.1 semantics + /// + Unicode11 = 1, + + /// + /// ISO/IEC 10646 semantics + /// + ISO10646 = 2, + + /// + /// Unicode 2.0 and onwards semantics, Unicode BMP only (cmap subtable formats 0, 4, 6). + /// + Unicode2 = 3, + + /// + /// Unicode 2.0 and onwards semantics, Unicode full repertoire (cmap subtable formats 0, 4, 6, 10, 12). + /// + Unicode2Plus = 4, + + /// + /// Unicode Variation Sequences (cmap subtable format 14). + /// + UnicodeVariationSequences = 5, + + /// + /// Unicode full repertoire (cmap subtable formats 0, 4, 6, 10, 12, 13) + /// + UnicodeFull = 6, + } +} diff --git a/SixLabors.Fonts/WellKnownIds/KnownNameIds.cs b/SixLabors.Fonts/WellKnownIds/KnownNameIds.cs new file mode 100644 index 0000000..b2e91dd --- /dev/null +++ b/SixLabors.Fonts/WellKnownIds/KnownNameIds.cs @@ -0,0 +1,121 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.WellKnownIds { + /// + /// Provides enumeration of common name ids + /// + /// + public enum KnownNameIds : ushort + { + /// + /// The copyright notice + /// + CopyrightNotice = 0, + + /// + /// The font family name; Up to four fonts can share the Font Family name, forming a font style linking + /// group (regular, italic, bold, bold italic — as defined by OS/2.fsSelection bit settings). + /// + FontFamilyName = 1, + + /// + /// The font subfamily name; The Font Subfamily name distinguishes the font in a group with the same Font Family name (name ID 1). + /// This is assumed to address style (italic, oblique) and weight (light, bold, black, etc.). A font with no particular differences + /// in weight or style (e.g. medium weight, not italic and fsSelection bit 6 set) should have the string "Regular" stored in this position. + /// + FontSubfamilyName = 2, + + /// + /// The unique font identifier + /// + UniqueFontID = 3, + + /// + /// The full font name; a combination of strings 1 and 2, or a similar human-readable variant. If string 2 is "Regular", it is sometimes omitted from name ID 4. + /// + FullFontName = 4, + + /// + /// Version string. Should begin with the syntax 'Version <number>.<number>' (upper case, lower case, or mixed, with a space between "Version" and the number). + /// The string must contain a version number of the following form: one or more digits (0-9) of value less than 65,535, followed by a period, followed by one or more + /// digits of value less than 65,535. Any character other than a digit will terminate the minor number. A character such as ";" is helpful to separate different pieces of version information. + /// The first such match in the string can be used by installation software to compare font versions. + /// Note that some installers may require the string to start with "Version ", followed by a version number as above. + /// + Version = 5, + + /// + /// Postscript name for the font; Name ID 6 specifies a string which is used to invoke a PostScript language font that corresponds to this OpenType font. + /// When translated to ASCII, the name string must be no longer than 63 characters and restricted to the printable ASCII subset, codes 33 to 126, + /// except for the 10 characters '[', ']', '(', ')', '{', '}', '<', '>', '/', '%'. + /// In a CFF OpenType font, there is no requirement that this name be the same as the font name in the CFF’s Name INDEX. + /// Thus, the same CFF may be shared among multiple font components in a Font Collection. See the 'name' table section of + /// Recommendations for OpenType fonts "" for additional information. + /// + PostscriptName = 6, + + /// + /// Trademark; this is used to save any trademark notice/information for this font. Such information should + /// be based on legal advice. This is distinctly separate from the copyright. + /// + Trademark = 7, + + /// + /// The manufacturer + /// + Manufacturer = 8, + + /// + /// Designer; name of the designer of the typeface. + /// + Designer = 9, + + /// + /// Description; description of the typeface. Can contain revision information, usage recommendations, history, features, etc. + /// + Description = 10, + + /// + /// URL Vendor; URL of font vendor (with protocol, e.g., http://, ftp://). If a unique serial number is embedded in + /// the URL, it can be used to register the font. + /// + VendorUrl = 11, + + /// + /// URL Designer; URL of typeface designer (with protocol, e.g., http://, ftp://). + /// + DesignerUrl = 12, + + /// + /// License Description; description of how the font may be legally used, or different example scenarios for licensed use. + /// This field should be written in plain language, not legalese. + /// + LicenseDescription = 13, + + /// + /// License Info URL; URL where additional licensing information can be found. + /// + LicenseInfoUrl = 14, + + /// + /// Typographic Family name: The typographic family grouping doesn't impose any constraints on the number of faces within it, + /// in contrast with the 4-style family grouping (ID 1), which is present both for historical reasons and to express style linking groups. + /// If name ID 16 is absent, then name ID 1 is considered to be the typographic family name. + /// (In earlier versions of the specification, name ID 16 was known as "Preferred Family".) + /// + TypographicFamilyName = 16, + + /// + /// Typographic Subfamily name: This allows font designers to specify a subfamily name within the typographic family grouping. + /// This string must be unique within a particular typographic family. If it is absent, then name ID 2 is considered to be the + /// typographic subfamily name. (In earlier versions of the specification, name ID 17 was known as "Preferred Subfamily".) + /// + TypographicSubfamilyName = 17, + + /// + /// Sample text; This can be the font name, or any other text that the designer thinks is the best sample to display the font in. + /// + SampleText = 19, + } +} diff --git a/SixLabors.Fonts/WellKnownIds/PlatformIDs.cs b/SixLabors.Fonts/WellKnownIds/PlatformIDs.cs new file mode 100644 index 0000000..7d31399 --- /dev/null +++ b/SixLabors.Fonts/WellKnownIds/PlatformIDs.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.WellKnownIds { + /// + /// platforms ids + /// + internal enum PlatformIDs : ushort + { + /// + /// Unicode platform + /// + Unicode = 0, + + /// + /// Script manager code + /// + Macintosh = 1, + + /// + /// [deprecated] ISO encoding + /// + ISO = 2, + + /// + /// Window encoding + /// + Windows = 3, + + /// + /// Custom platform + /// + Custom = 4 // Custom None + } +} diff --git a/SixLabors.Fonts/WordBreaking.cs b/SixLabors.Fonts/WordBreaking.cs new file mode 100644 index 0000000..13b7a26 --- /dev/null +++ b/SixLabors.Fonts/WordBreaking.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Defines modes to determine when line breaks should appear when words overflow + /// their content box. + /// + public enum WordBreaking + { + /// + /// Use the default line break rule. + /// + Standard, + + /// + /// To prevent overflow, word breaks should be inserted between any two + /// characters (excluding Chinese/Japanese/Korean text). + /// + BreakAll, + + /// + /// Word breaks should not be used for Chinese/Japanese/Korean (CJK) text. + /// Non-CJK text behavior is the same as for + /// + KeepAll, + + /// + /// Uses a combination of and rules in that order. + /// + BreakWord + } +} diff --git a/SixLabors.Fonts/WordMetrics.cs b/SixLabors.Fonts/WordMetrics.cs new file mode 100644 index 0000000..2f48d74 --- /dev/null +++ b/SixLabors.Fonts/WordMetrics.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Represents the positioned metrics for one Unicode word-boundary segment. + /// + public readonly struct WordMetrics + { + /// + /// Initializes a new instance of the struct. + /// + /// The positioned logical advance rectangle for the word-boundary segment in pixel units. + /// The rendered glyph bounds for the word-boundary segment in pixel units. + /// The union of the positioned logical advance bounds and rendered glyph bounds in pixel units. + /// The inclusive grapheme insertion index where the word-boundary segment starts. + /// The exclusive grapheme insertion index where the word-boundary segment ends. + /// The inclusive UTF-16 index where the word-boundary segment starts. + /// The exclusive UTF-16 index where the word-boundary segment ends. + internal WordMetrics( + FontRectangle advance, + FontRectangle bounds, + FontRectangle renderableBounds, + int graphemeStart, + int graphemeEnd, + int stringStart, + int stringEnd) + { + this.Advance = advance; + this.Bounds = bounds; + this.RenderableBounds = renderableBounds; + this.GraphemeStart = graphemeStart; + this.GraphemeEnd = graphemeEnd; + this.StringStart = stringStart; + this.StringEnd = stringEnd; + } + + /// + /// Gets the positioned logical advance rectangle for the word-boundary segment in pixel units. + /// + public FontRectangle Advance { get; } + + /// + /// Gets the rendered glyph bounds for the word-boundary segment in pixel units. + /// + public FontRectangle Bounds { get; } + + /// + /// Gets the union of the positioned logical advance bounds and rendered glyph bounds in pixel units. + /// + public FontRectangle RenderableBounds { get; } + + /// + /// Gets the inclusive grapheme insertion index where the word-boundary segment starts. + /// + public int GraphemeStart { get; } + + /// + /// Gets the exclusive grapheme insertion index where the word-boundary segment ends. + /// + public int GraphemeEnd { get; } + + /// + /// Gets the inclusive UTF-16 index where the word-boundary segment starts. + /// + public int StringStart { get; } + + /// + /// Gets the exclusive UTF-16 index where the word-boundary segment ends. + /// + public int StringEnd { get; } + } +} diff --git a/SixLabors.Fonts/WordSegmentRun.cs b/SixLabors.Fonts/WordSegmentRun.cs new file mode 100644 index 0000000..50bd6f0 --- /dev/null +++ b/SixLabors.Fonts/WordSegmentRun.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts { + /// + /// Describes one source-order Unicode word-boundary segment run. + /// + internal readonly struct WordSegmentRun + { + /// + /// Initializes a new instance of the struct. + /// + /// The inclusive grapheme insertion index where the word-boundary segment starts. + /// The exclusive grapheme insertion index where the word-boundary segment ends. + /// The inclusive UTF-16 index where the word-boundary segment starts. + /// The exclusive UTF-16 index where the word-boundary segment ends. + public WordSegmentRun( + int graphemeStart, + int graphemeEnd, + int stringStart, + int stringEnd) + { + this.GraphemeStart = graphemeStart; + this.GraphemeEnd = graphemeEnd; + this.StringStart = stringStart; + this.StringEnd = stringEnd; + } + + /// + /// Gets the inclusive grapheme insertion index where the word-boundary segment starts. + /// + public int GraphemeStart { get; } + + /// + /// Gets the exclusive grapheme insertion index where the word-boundary segment ends. + /// + public int GraphemeEnd { get; } + + /// + /// Gets the inclusive UTF-16 index where the word-boundary segment starts. + /// + public int StringStart { get; } + + /// + /// Gets the exclusive UTF-16 index where the word-boundary segment ends. + /// + public int StringEnd { get; } + } +} diff --git a/SixLabors.ImageSharp.props b/SixLabors.ImageSharp.props new file mode 100644 index 0000000..353dce2 --- /dev/null +++ b/SixLabors.ImageSharp.props @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ThrowHelper.cs b/ThrowHelper.cs new file mode 100644 index 0000000..3fb8379 --- /dev/null +++ b/ThrowHelper.cs @@ -0,0 +1,129 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace SixLabors { + /// + /// Helper methods to throw exceptions. + /// +#pragma warning disable RCS1043 // Remove 'partial' modifier from type with a single part. + internal static partial class ThrowHelper +#pragma warning restore RCS1043 // Remove 'partial' modifier from type with a single part. + { + /// + /// Throws an when fails. + /// + /// The value. + /// The argument name. + [DoesNotReturn] + public static void ThrowArgumentExceptionForNotNullOrWhitespace(string? value, string name) + { + if (value is null) + { + ThrowArgumentNullException(name, $"Parameter \"{name}\" must be not null."); + } + else + { + ThrowArgumentException(name, $"Parameter \"{name}\" must not be empty or whitespace."); + } + } + + /// + /// Throws an when fails. + /// + /// The type of value. + /// The value. + /// The maximum allowable value. + /// The argument name. + [DoesNotReturn] + public static void ThrowArgumentOutOfRangeExceptionForMustBeLessThan(T value, T max, string name) + => ThrowArgumentOutOfRangeException(name, $"Parameter \"{name}\" ({typeof(T)}) must be less than {max}, was {value}"); + + /// + /// Throws an when fails. + /// + /// The type of value. + /// The value. + /// The maximum allowable value. + /// The argument name. + [DoesNotReturn] + public static void ThrowArgumentOutOfRangeExceptionForMustBeLessThanOrEqualTo(T value, T maximum, string name) + => ThrowArgumentOutOfRangeException(name, $"Parameter \"{name}\" ({typeof(T)}) must be less than or equal to {maximum}, was {value}"); + + /// + /// Throws an when fails. + /// + /// The type of value. + /// The value. + /// The minimum allowable value. + /// The argument name. + [DoesNotReturn] + public static void ThrowArgumentOutOfRangeExceptionForMustBeGreaterThan(T value, T minimum, string name) + => ThrowArgumentOutOfRangeException(name, $"Parameter \"{name}\" ({typeof(T)}) must be greater than {minimum}, was {value}"); + + /// + /// Throws an when fails. + /// + /// The type of value. + /// The value. + /// The minimum allowable value. + /// The argument name. + [DoesNotReturn] + public static void ThrowArgumentOutOfRangeExceptionForMustBeGreaterThanOrEqualTo(T value, T minimum, string name) + => ThrowArgumentOutOfRangeException(name, $"Parameter \"{name}\" ({typeof(T)}) must be greater than or equal to {minimum}, was {value}"); + + /// + /// Throws an when fails. + /// + /// The type of value. + /// The value. + /// The minimum allowable value. + /// The maximum allowable value. + /// The argument name. + [DoesNotReturn] + public static void ThrowArgumentOutOfRangeExceptionForMustBeBetweenOrEqualTo(T value, T minimum, T maximum, string name) + => ThrowArgumentOutOfRangeException(name, $"Parameter \"{name}\" ({typeof(T)}) must be between or equal to {minimum} and {maximum}, was {value}"); + + /// + /// Throws an when fails. + /// + /// The minimum allowable length. + /// The paramere name. + [DoesNotReturn] + public static void ThrowArgumentOutOfRangeExceptionForMustBeSizedAtLeast(int minLength, string parameterName) + => ThrowArgumentException($"Spans must be at least of length {minLength}!", parameterName); + + /// + /// Throws a new . + /// + /// The message to include in the exception. + /// The argument name. + /// Thrown with and . + [DoesNotReturn] + public static void ThrowArgumentException(string message, string name) + => throw new ArgumentException(message, name); + + /// + /// Throws a new . + /// + /// The argument name. + /// The message to include in the exception. + /// Thrown with and . + [DoesNotReturn] + public static void ThrowArgumentNullException(string name, string message) + => throw new ArgumentNullException(name, message); + + /// + /// Throws a new . + /// + /// The argument name. + /// The message to include in the exception. + /// Thrown with and . + [DoesNotReturn] + public static void ThrowArgumentOutOfRangeException(string name, string message) + => throw new ArgumentOutOfRangeException(name, message); + } +} diff --git a/sixlabors.imagesharp.128.png b/sixlabors.imagesharp.128.png new file mode 100644 index 0000000..91cea93 Binary files /dev/null and b/sixlabors.imagesharp.128.png differ